How to get the GOT address from a PLT stub using gdb
by Rafael Beirigo
Table of Contents
Overview
When we
- Use functions from shared libraries, like the
puts, - Opt for dynamic linking, and
- Opt for lazy binding,
the object code for puts is not included in the binary, but instead is linked at runtime.
The linker adds a placeholder that will be patched at runtime with the real address of puts.
That address is obtained by the dynamic linker from the shared library libc.so.
But this is only done after the first call to puts (thus the lazy binding).
Moreover, when the program calls puts, it does so via a “trampoline”, in the form of a PLT stub.
This stub is a short piece of code (3 instructions only) that runs everytime puts is called.
The first instruction jumps to the address currently in the placeholder (GOT slot). When the program starts, this address is the address of the next (second) instruction of the stub. See the illustration below.
[ main ]--. [ puts@plt ] [ puts in libc.so ]
| +----------------+ +-------------------------------------------+
'-->| jmp *GOT[puts] |---. | push %r14 |
| push <index> |<--' | push %r13 |
| jmp dyn linker |---. | push %r12 |
+----------------+ | | mov %rdi,%r12 |
| | push %rbp |
| | push %rbx |
[ dynamic linker ]<-----' | sub $0x10,%rsp |
| call 0x7ffff7dec110 <*ABS*+0x9f1b0@plt> |
| ... |
+-------------------------------------------+
The second instruction pushes an identifier for the dynamic linker, and the third jumps to run the dynamic linker itself.
The dynamic linker uses that identifier to fill the GOT slot with the real address of puts in libc.so.
Then the program jumps to puts, which is executed, and the program resumes normal execution.
The next time puts is called, the first instruction jumps to the address in the GOT slot, which is the real address of puts.
This runs puts, and resumes normal execution, and avoids further unnecessary calls to the resolver.
See the illustration below.
[ main ]--. [ puts@plt ] [ puts in libc.so ]
| +----------------+ +-------------------------------------------+
'-->| jmp *GOT[puts] |----->| push %r14 |
| push <index> | | push %r13 |
| jmp dyn linker | | push %r12 |
+----------------+ | mov %rdi,%r12 |
| push %rbp |
| push %rbx |
[ dynamic linker ] | sub $0x10,%rsp |
| call 0x7ffff7dec110 <*ABS*+0x9f1b0@plt> |
| ... |
+-------------------------------------------+
Now let’s see it in action.
Source code for the test program
Here is the program we’ll use:
#include <stdio.h>
int main() {
puts("Hello, World!");
return 0;
}We compile it:
gcc -o hello hello.cDynamic analysis with gdb
And examine with gdb:
gdb ./helloWe need to disassemble main to get the address of puts’ PLT stub.
In order to get the adresses, we run the program.
But first we add a breakpoint in main:
(gdb) break main
Breakpoint 1 at 0x113dThen run the program:
(gdb) run
Starting program: /home/rafa/cybersec-dojo/_drafts/hello
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x000055555555513d in main ()We examine main to get puts’ PLT stub address.
The symbol is aptly named puts@plt:
(gdb) disassemble main
Dump of assembler code for function main:
0x0000555555555139 <+0>: push %rbp
0x000055555555513a <+1>: mov %rsp,%rbp
=> 0x000055555555513d <+4>: lea 0xec0(%rip),%rax # 0x555555556004
0x0000555555555144 <+11>: mov %rax,%rdi
0x0000555555555147 <+14>: call 0x555555555030 <puts@plt>
0x000055555555514c <+19>: mov $0x0,%eax
0x0000555555555151 <+24>: pop %rbp
0x0000555555555152 <+25>: ret
End of assembler dump.We disassemble the stub. The first instruction is the jump to the address GOT points to.
(gdb) disassemble 0x555555555030
Dump of assembler code for function puts@plt:
0x0000555555555030 <+0>: jmp *0x2fca(%rip) # 0x555555558000 <puts@got.plt>
0x0000555555555036 <+6>: push $0x0
0x000055555555503b <+11>: jmp 0x555555555020
End of assembler dump.We saw that the GOT’s address is 0x555555558000.
To see the address it points to, we examine the contents of that memory address.
(gdb) x/gx 0x555555558000
0x555555558000 <puts@got.plt>: 0x0000555555555036We see, that, in fact, this first time puts is being called, GOT points to the second instruction of puts’ PLT stub, puts@plt.
Let’s look at that address after puts has been called.
We add a breakpoint right after the call to puts:
(gdb) break *0x000055555555514c
Breakpoint 2 at 0x55555555514cand continue execution.
(gdb) continue
Continuing.
Hello, World!
Breakpoint 2, 0x000055555555514c in main ()The program prints Hello, World!, showing that puts was in fact called.
Now we examine the address GOT points to:
(gdb) x/gx 0x555555558000
0x555555558000 <puts@got.plt>: 0x00007ffff7e3d980It changed. Let’s look at the code there:
(gdb) disassemble 0x00007ffff7e3d980
Dump of assembler code for function __GI__IO_puts:
Address range 0x7ffff7e3d980 to 0x7ffff7e3db15:
0x00007ffff7e3d980 <+0>: push %r14
0x00007ffff7e3d982 <+2>: push %r13
0x00007ffff7e3d984 <+4>: push %r12
0x00007ffff7e3d986 <+6>: mov %rdi,%r12
0x00007ffff7e3d989 <+9>: push %rbp
0x00007ffff7e3d98a <+10>: push %rbx
0x00007ffff7e3d98b <+11>: sub $0x10,%rsp
0x00007ffff7e3d98f <+15>: call 0x7ffff7dec110 <*ABS*+0x9f1b0@plt>
0x00007ffff7e3d994 <+20>: mov 0x15b46d(%rip),%r13 # 0x7ffff7f98e08
0x00007ffff7e3d99b <+27>: mov %rax,%rbx
0x00007ffff7e3d99e <+30>: mov 0x0(%r13),%rbp
0x00007ffff7e3d9a2 <+34>: mov 0x0(%rbp),%eax
0x00007ffff7e3d9a5 <+37>: and $0x8000,%eax
0x00007ffff7e3d9aa <+42>: jne 0x7ffff7e3da00 <__GI__IO_puts+128>
0x00007ffff7e3d9ac <+44>: mov %fs:0x10,%r14
0x00007ffff7e3d9b5 <+53>: mov 0x88(%rbp),%rdx
0x00007ffff7e3d9bc <+60>: cmp %r14,0x8(%rdx)
0x00007ffff7e3d9c0 <+64>: je 0x7ffff7e3dab0 <__GI__IO_puts+304>
0x00007ffff7e3d9c6 <+70>: mov $0x1,%ecx
0x00007ffff7e3d9cb <+75>: lock cmpxchg %ecx,(%rdx)
0x00007ffff7e3d9cf <+79>: jne 0x7ffff7e3db00 <__GI__IO_puts+384>
0x00007ffff7e3d9d5 <+85>: mov 0x88(%rbp),%rdx
0x00007ffff7e3d9dc <+92>: mov 0x0(%r13),%rdi
0x00007ffff7e3d9e0 <+96>: mov %r14,0x8(%rdx)
0x00007ffff7e3d9e4 <+100>: mov 0xc0(%rdi),%eax
0x00007ffff7e3d9ea <+106>: addl $0x1,0x4(%rdx)
0x00007ffff7e3d9ee <+110>: test %eax,%eax
0x00007ffff7e3d9f0 <+112>: je 0x7ffff7e3da0d <__GI__IO_puts+141>
0x00007ffff7e3d9f2 <+114>: cmp $0xffffffff,%eax
0x00007ffff7e3d9f5 <+117>: je 0x7ffff7e3da17 <__GI__IO_puts+151>
0x00007ffff7e3d9f7 <+119>: mov $0xffffffff,%eax
0x00007ffff7e3d9fc <+124>: jmp 0x7ffff7e3da76 <__GI__IO_puts+246>
0x00007ffff7e3d9fe <+126>: xchg %ax,%ax
0x00007ffff7e3da00 <+128>: mov %rbp,%rdi
0x00007ffff7e3da03 <+131>: mov 0xc0(%rdi),%eax
0x00007ffff7e3da09 <+137>: test %eax,%eax
0x00007ffff7e3da0b <+139>: jne 0x7ffff7e3d9f2 <__GI__IO_puts+114>
0x00007ffff7e3da0d <+141>: movl $0xffffffff,0xc0(%rdi)
0x00007ffff7e3da17 <+151>: mov 0xd8(%rdi),%r14
0x00007ffff7e3da1e <+158>: lea 0x157fbb(%rip),%rdx # 0x7ffff7f959e0 <_IO_helper_jumps>
0x00007ffff7e3da25 <+165>: lea 0x158d1c(%rip),%rax # 0x7ffff7f96748
0x00007ffff7e3da2c <+172>: sub %rdx,%rax
0x00007ffff7e3da2f <+175>: mov %r14,%rcx
0x00007ffff7e3da32 <+178>: sub %rdx,%rcx
0x00007ffff7e3da35 <+181>: cmp %rax,%rcx
0x00007ffff7e3da38 <+184>: jae 0x7ffff7e3dac0 <__GI__IO_puts+320>
0x00007ffff7e3da3e <+190>: mov %rbx,%rdx
0x00007ffff7e3da41 <+193>: mov %r12,%rsi
0x00007ffff7e3da44 <+196>: call *0x38(%r14)
0x00007ffff7e3da48 <+200>: cmp %rax,%rbx
0x00007ffff7e3da4b <+203>: jne 0x7ffff7e3d9f7 <__GI__IO_puts+119>
0x00007ffff7e3da4d <+205>: mov 0x0(%r13),%rdi
0x00007ffff7e3da51 <+209>: mov 0x28(%rdi),%rax
0x00007ffff7e3da55 <+213>: cmp 0x30(%rdi),%rax
0x00007ffff7e3da59 <+217>: jae 0x7ffff7e3dad0 <__GI__IO_puts+336>
0x00007ffff7e3da5b <+219>: lea 0x1(%rax),%rdx
0x00007ffff7e3da5f <+223>: mov %rdx,0x28(%rdi)
0x00007ffff7e3da63 <+227>: movb $0xa,(%rax)
0x00007ffff7e3da66 <+230>: add $0x1,%rbx
0x00007ffff7e3da6a <+234>: mov $0x7fffffff,%eax
0x00007ffff7e3da6f <+239>: cmp %rax,%rbx
0x00007ffff7e3da72 <+242>: cmovbe %rbx,%rax
0x00007ffff7e3da76 <+246>: testl $0x8000,0x0(%rbp)
0x00007ffff7e3da7d <+253>: jne 0x7ffff7e3daa2 <__GI__IO_puts+290>
0x00007ffff7e3da7f <+255>: mov 0x88(%rbp),%rdi
0x00007ffff7e3da86 <+262>: mov 0x4(%rdi),%esi
0x00007ffff7e3da89 <+265>: lea -0x1(%rsi),%edx
0x00007ffff7e3da8c <+268>: mov %edx,0x4(%rdi)
0x00007ffff7e3da8f <+271>: test %edx,%edx
0x00007ffff7e3da91 <+273>: jne 0x7ffff7e3daa2 <__GI__IO_puts+290>
0x00007ffff7e3da93 <+275>: movq $0x0,0x8(%rdi)
0x00007ffff7e3da9b <+283>: xchg %edx,(%rdi)
0x00007ffff7e3da9d <+285>: cmp $0x1,%edx
0x00007ffff7e3daa0 <+288>: jg 0x7ffff7e3dae8 <__GI__IO_puts+360>
0x00007ffff7e3daa2 <+290>: add $0x10,%rsp
0x00007ffff7e3daa6 <+294>: pop %rbx
0x00007ffff7e3daa7 <+295>: pop %rbp
0x00007ffff7e3daa8 <+296>: pop %r12
0x00007ffff7e3daaa <+298>: pop %r13
0x00007ffff7e3daac <+300>: pop %r14
0x00007ffff7e3daae <+302>: ret
0x00007ffff7e3daaf <+303>: nop
0x00007ffff7e3dab0 <+304>: mov %rbp,%rdi
0x00007ffff7e3dab3 <+307>: jmp 0x7ffff7e3d9e4 <__GI__IO_puts+100>
0x00007ffff7e3dab8 <+312>: nopl 0x0(%rax,%rax,1)
0x00007ffff7e3dac0 <+320>: call 0x7ffff7e45c10 <_IO_vtable_check>
0x00007ffff7e3dac5 <+325>: mov 0x0(%r13),%rdi
0x00007ffff7e3dac9 <+329>: jmp 0x7ffff7e3da3e <__GI__IO_puts+190>
0x00007ffff7e3dace <+334>: xchg %ax,%ax
0x00007ffff7e3dad0 <+336>: mov $0xa,%esi
0x00007ffff7e3dad5 <+341>: call 0x7ffff7e48d00 <__GI___overflow>
0x00007ffff7e3dada <+346>: cmp $0xffffffff,%eax
0x00007ffff7e3dadd <+349>: jne 0x7ffff7e3da66 <__GI__IO_puts+230>
0x00007ffff7e3dadf <+351>: jmp 0x7ffff7e3d9f7 <__GI__IO_puts+119>
0x00007ffff7e3dae4 <+356>: nopl 0x0(%rax)
0x00007ffff7e3dae8 <+360>: mov %eax,0xc(%rsp)
0x00007ffff7e3daec <+364>: call 0x7ffff7e4c160 <__GI___lll_lock_wake_private>
0x00007ffff7e3daf1 <+369>: mov 0xc(%rsp),%eax
0x00007ffff7e3daf5 <+373>: jmp 0x7ffff7e3daa2 <__GI__IO_puts+290>
0x00007ffff7e3daf7 <+375>: nopw 0x0(%rax,%rax,1)
0x00007ffff7e3db00 <+384>: mov %rdx,%rdi
0x00007ffff7e3db03 <+387>: call 0x7ffff7e4c0b0 <__GI___lll_lock_wait_private>
0x00007ffff7e3db08 <+392>: jmp 0x7ffff7e3d9d5 <__GI__IO_puts+85>
0x00007ffff7e3db0d <+397>: mov %rax,%rbx
0x00007ffff7e3db10 <+400>: jmp 0x7ffff7dec7cc <__GI__IO_puts.cold>
Address range 0x7ffff7dec7cc to 0x7ffff7dec801:
0x00007ffff7dec7cc <-332212>: testl $0x8000,0x0(%rbp)
0x00007ffff7dec7d3 <-332205>: jne 0x7ffff7dec7f9 <__GI__IO_puts-332167>
0x00007ffff7dec7d5 <-332203>: mov 0x88(%rbp),%rdi
0x00007ffff7dec7dc <-332196>: mov 0x4(%rdi),%eax
0x00007ffff7dec7df <-332193>: sub $0x1,%eax
0x00007ffff7dec7e2 <-332190>: mov %eax,0x4(%rdi)
0x00007ffff7dec7e5 <-332187>: jne 0x7ffff7dec7f9 <__GI__IO_puts-332167>
0x00007ffff7dec7e7 <-332185>: xor %edx,%edx
0x00007ffff7dec7e9 <-332183>: mov %rdx,0x8(%rdi)
0x00007ffff7dec7ed <-332179>: xchg %eax,(%rdi)
0x00007ffff7dec7ef <-332177>: sub $0x1,%eax
0x00007ffff7dec7f2 <-332174>: jle 0x7ffff7dec7f9 <__GI__IO_puts-332167>
0x00007ffff7dec7f4 <-332172>: call 0x7ffff7e4c160 <__GI___lll_lock_wake_private>
0x00007ffff7dec7f9 <-332167>: mov %rbx,%rdi
0x00007ffff7dec7fc <-332164>: call 0x7ffff7ded530 <_Unwind_Resume>
End of assembler dump.And it is in fact the code for puts!
Summary
- GOT entries start pointing back into the PLT.
- The dynamic resolver patches them with real
libcaddresses. - Subsequent calls jump directly to the resolved function.