Cybersec Dojo

"Writeups, research and recon."

View on GitHub
1 November 2025

How to get the GOT address from a PLT stub using gdb

by Rafael Beirigo

Table of Contents

  1. Overview
  2. Source code for the test program
  3. Dynamic analysis with gdb
  4. Summary

Overview

When we

  1. Use functions from shared libraries, like the puts,
  2. Opt for dynamic linking, and
  3. 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.c

Dynamic analysis with gdb

And examine with gdb:

gdb ./hello

We 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 0x113d

Then 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>:  0x0000555555555036

We 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 0x55555555514c

and 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>:  0x00007ffff7e3d980

It 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

tags: gdb - got - got/plt - dynamic-linking