6

I tried to disassemble a ELF file which is a shared object file executed on armv7a (Android). I saw a strange block. It seems that the PC, program counter register, is set to 0. Did I miss something or do something wrong?


The process goes into 0x1708 in ARM mode. Below is the strange block of asm code I disassembled from the ELF file.

; section: .plt
; function: function_1708 at 0x1708 -- 0x1718
0x1708:   04 e0 2d e5       str lr, [sp, #-4]!
0x170c:   04 e0 9f e5       ldr lr, [pc, #4]
0x1710:   0e e0 8f e0       add lr, pc, lr
0x1714:   08 f0 be e5       ldr pc, [lr, #8]!
; data inside code section at 0x1718 -- 0x171c
0x1718:   b4 77 00 00                                        |.w..            |

After executing line 0x170c, the LR register should be set as the value at the address 0x1718. The value is 0x77b4 (this file is stored in little-endian). And go ahead.

0x1710: lr += 0x1710 + 8  // lr = 0x8ecc
0x1714: pc = *(lr + 8)    // pc = *(0x8ed4)
        lr += 8  

And 0x8ed4 is in .got section.

; section: .got
0x8eac:   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   |................|
0x8ebc:   00 00 00 00 58 70 00 00  e0 6e 00 00 00 00 00 00   |....Xp...n......|
0x8ecc:   00 00 00 00 00 00 00 00  00 00 00 00 08 17 00 00   |................|
0x8edc:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8eec:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8efc:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8f0c:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8f1c:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8f2c:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|
0x8f3c:   08 17 00 00 08 17 00 00  08 17 00 00 08 17 00 00   |................|

It seems that the value at 0x8ed4 is zero. I traced to this strange block from JNI_OnLoad(), so no data should be modified before executing this block.

Did I do something wrong, or is this a specific behavior of ARM architecture?

4
  • 3
    Are you familiar with GOT/PLT in other assembly languages? (usually, the x86 is better known). Because your question is, in fact, strongly related to the GOT/PLT behavior in general, and related to ARM architecture in particular.
    – perror
    Sep 18, 2018 at 6:02
  • 1
    No. I am new to reverse engineering. I am googling relative information. Is there any good article for reading?
    – IvanaGyro
    Sep 18, 2018 at 6:16
  • 1
    Ok, I'll write a complete answer, then.
    – perror
    Sep 18, 2018 at 6:17
  • 1
    Note that this is not trivial! If you do not really understand at first read, this is perfectly normal. Try to fetch a debugger and to step through your program to better understand (and keep faith!).
    – perror
    Sep 18, 2018 at 6:57

2 Answers 2

7

In fact, the behavior you are describing is coming from the usual behavior of the GOT/PLT sections. They are used to dynamically link the program calls to shared libraries functions at runtime.

In fact, a shared library can be loaded at any place in the process memory, there is no way to predict statically where it will pop up. So, the GOT/PLT load the address of each library's function dynamically at runtime and cache it for further use.

The way it works is pretty simple, the PLT (Procedure Linkage Table) is just a bunch of small code gadgets (one for each library's function called in the program, plus one generic code located at the beginning of the PLT section).

And, the GOT (Global Offset Table) is just a table used to store (and cache) the addresses of the library's functions.

At first, the GOT is all set to zero because no function address has been resolved yet. The GOT get filled all along the program is executed and call new functions (some functions may be called only rarely and their address may be only rarely filled in the GOT).

When a library function get called the program counter goes to the PLT (see the figure below) and executes the code specifically written for this function (each of the code gadget has its own offsets to get the proper address from the GOT). But, at first, the GOT contains only zeroes, so you need to initialize it.

To initialize it, you jump at the very beginning of the PLT and find the address of library's function the program want to call. You execute this code and, then, you write the address of the function in the GOT.

PLT/GOT Schema (before)

Once you cached the address of the function, you do not need anymore to recompute the address if you call your function and you jump directly to it (as in the following picture).

PLT/GOT Schema (after)

In the case of ARM, you have two types of GOT/PLT schemas:

In your case, this is a direct PLT (but it seems to be quite old code, I would suspect an old compiler or so, because new ones better use ip in place of lr for that).

; section: .plt
; function: function_1708 at 0x1708 -- 0x1718
0x1708:   04 e0 2d e5     str lr, [sp, #-4]! <-- Save lr on the stack
0x170c:   04 e0 9f e5     ldr lr, [pc, #4] <-- Get a point of reference in memory
0x1710:   0e e0 8f e0     add lr, pc, lr <-- Load offset to the next step
0x1714:   08 f0 be e5     ldr pc, [lr, #8]! <-- Jump to the next step

Note that the PLT is a static section, therefore the 0x170c will always stay the same (and, therefore, the pc will always be loaded with the same value at 0x170c).

References

3
  • So, in my situation, function_1708 is PLT[0], the code to call resolver, right? How does the linker know where to fill its memory address to the correct address of the shared library? In my case, 0x8ed4 is not the first address in .got. The first is 0x8eac. The strategy of filling the address of the resolver is depended on the format of the ELF file, the running operating system, or the architecture or CPU?
    – IvanaGyro
    Sep 18, 2018 at 7:41
  • At the beginning of each run of your program, the linker set the address where the shared library is in the PLT and each function used by the program is given an offset in the current shared library. So that, if you know the address of the shared library in memory and the offset of a specific function you can get the address of the function in the virtual memory. You compute this only once, thanks to the GOT (which cache the result).
    – perror
    Sep 18, 2018 at 8:02
  • I think I asked the wrong questions. However, I got my answer in some ELF specifications. As the same as @Igor Skochinsky♦ answered, the location where the linker chooses to put the address of the resolver is defined in ELF specs. The definition relies on the type of file, ELF. Anyway, you helped me a lot. Thank you!
    – IvanaGyro
    Sep 18, 2018 at 16:26
5

While the other answer is not wrong, it does not actually cover the real issue: how come that calling a zero address does not lead to a crash?

AFAIK there is no official standard or complete documentation for this, but de facto the first few GOT entries are special/reserved and are used by the dynamic loader/linker (sometimes also called interpreter) for its own purposes:

  • GOT[0] points to the _DYNAMIC symbol of the current module which is the start of the list of tags (Elf32_Dyn entries) with the various information necessary for proper resolution of dynamic symbols. See the ELF specification.

  • GOT[1] is populated at runtime with the pointer to the link_map structure containing the list of all dynamic images present in the process. This list resides in the dynamic linker (ld.so on Linux systems and linker binary on Android).

  • GOT[2] is also filled in by the dynamic linker and points to the resolver function (_dl_runtime_resolve in glibc, not sure on Android).

The snippet you quoted is the resolver stub which is called on first call of an external symbol (you can see that all GOT entries are initialized to its address, 1708). It fetches GOT[2] and jumps to it, which should end up in the dynamic linker, which would look up the symbol, patch the GOT entry and jump to the function as the last step.

As I said, there are no (AFAIK) official docs on this, just random pieces of the ELF ABI spec, glibc source code and various blog posts. I would suggest you to step through this code in a debugger to see what actually happens, and match it against the source code. In addition to links from @perror, I found this post which explains some of the reserved GOT entries (although for x64 Linux and not ARM Android).

2
  • 1
    I tried to find the official ELF specifications. According to Wikipedia, there are a lot of different specs. And the organization developed the ELF, UNIX System Laboratories, was defunct. The first spec is released by AT&T. I am curious why so many companies released their ELF spec or included the ELF spec into their ABI specs and what is the historical context. It is interesting.
    – IvanaGyro
    Sep 18, 2018 at 16:14
  • 1
    @IgorSkochinsky: I did not know all this! In fact, I assumed that the analysis done by IvenCJ7 was done statically, so the GOT was set to zero originally and set to beginning of PLT afterward. I see now that I was very likely wrong. Nice explanation Igor (once again) and good link, thanks!
    – perror
    Sep 18, 2018 at 16:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.