3

I am building an ELF binary which needs to be able to process and reverse its own relocations at runtime. (The reversing will happen in a separate buffer, not in the original code page, obviously.) The purpose of this is so that the module contents in memory can be HMAC'd and compared against a known good value calculated from the module on disk, to ensure no corruption has occurred. I'm aware that this is somewhat unusual, but it's a requirement of a standard that we have to adhere to.

I've been able to reverse all of the relocations in the binary except for the R_X86_64_JUMP_SLOT relocations which happen in the Global Offset Table. . Looking at the relocation entries in my test module's .rela.plt section with readelf -a mylib.so, I see these relocations:

Relocation section '.rela.plt' at offset 0x968 contains 20 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000005018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf + 0
000000005020  002000000007 R_X86_64_JUMP_SLO 0000000000001ac5 processRelocations + 0
000000005028  000300000007 R_X86_64_JUMP_SLO 0000000000000000 memcpy + 0
000000005030  000400000007 R_X86_64_JUMP_SLO 0000000000000000 puts + 0 
...

These offsets point to the global offset table (specifically .got.plt) so I can get more info with objdump -d -s -j .plt -j .got.plt mylib.so:

Disassembly of section .got.plt:

0000000000005000 <_GLOBAL_OFFSET_TABLE_>:
    5000:       80 4e 00 00 00 00 00 00 00 00 00 00 00 00 00 00     .N..............
        ...
    5018:       10 10 00 00 00 00 00 00 20 10 00 00 00 00 00 00     ........ .......
    5028:       30 10 00 00 00 00 00 00 40 10 00 00 00 00 00 00     0.......@.......
...

Disassembly of section .plt:

0000000000001000 <.plt>:
    1000:       ff 35 02 40 00 00       pushq  0x4002(%rip)        # 5008 <_GLOBAL_OFFSET_TABLE_+0x8>
    1006:       f2 ff 25 03 40 00 00    bnd jmpq *0x4003(%rip)     # 5010 <_GLOBAL_OFFSET_TABLE_+0x10>
    100d:       0f 1f 00                nopl   (%rax)
    1010:       f3 0f 1e fa             endbr64
    1014:       68 00 00 00 00          pushq  $0x0
    1019:       f2 e9 e1 ff ff ff       bnd jmpq 1000 <.plt>
    101f:       90                      nop
    1020:       f3 0f 1e fa             endbr64

Note that this binary is little endian. So the offsets in .got.plt, i.e. 0x1010, 0x1020, 0x1030... seem to always point to the endbr64 instruction in the corresponding PLT entry. That's similar to the info in this answer which quotes "At startup time the dynamic linker fills the GOT slot with the address pointing to the second instruction of the appropriate PLT entry." However, in my case it seems to point to the fourth instruction in the PLT entry. That answer further suggests that this relocation is done by adding the module's base address to the original GOT value.

However, when I run my test program and look at the actual value in memory after dynamic linking, it's not what I expect. For instance, running it now, the GOT entry for at 0x5020 for processRelocations ends up with the value 0x7FF06A0F5AC5. The base address of my module in memory for that run was 0x7ff06a0f4000. Subtracting this yields 0x1AC5, which actually appears to be the value of the symbol table entry for that function:

Symbol table '.dynsym' contains 34 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
   ...
   32: 0000000000001ac5   491 FUNC    GLOBAL DEFAULT   10 processRelocations

However, the symbol table entry is not what I want; I need to get back to the original value of 0x1020, and I don't see that value anywhere in the symbol table either.

How can I reverse these relocations at runtime to find their original values?

Mitch Lindgren
  • 2,120
  • 1
  • 18
  • 36
  • 2
    Have you considered compiling with `-fno-plt` so you don't have any PLT entries at all, and hopefully not their associated lazy-binding machinery? (GCC will use `call *printf@GOTPCREL(%rip)`, which forces early binding: resolving the GOT entries on process startup) – Peter Cordes Mar 26 '21 at 02:55
  • @PeterCordes Nice, that seems to have done the trick! Wish I had known about that option before! I'm still curious if there is a way to actually reverse these relocations, but `-fno-plt` will probably solve my problem at least for the time being. Thanks! If you put this as an answer, I'll accept it if no one has a better solution. – Mitch Lindgren Mar 26 '21 at 03:43
  • Sure, yeah interesting at least for the sake of curiosity to find an answer to the question asked, not just a way to avoid it. – Peter Cordes Mar 26 '21 at 04:58

1 Answers1

1

You can sidestep the problem by compiling with -fno-plt so you don't have any PLT entries at all, and the associated lazy-binding machinery doesn't come into play.

GCC and clang will use call *printf@GOTPCREL(%rip) which forces early binding: resolving the GOT entries on process startup. This makes each call more efficient, and some distros (e.g. Arch GNU/Linux) are compiling their packages this way already.

TL:DR: This is generally a good option, it's just not on by default (yet) in current GCC and clang distro configs.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847