使用GDB从PLT桩中获取GOT地址
How to get the GOT address from a PLT stub using GDB

原始链接: https://rafaelbeirigo.github.io/cybersec-dojo/research/2025/11/01/how-to-get-the-got-address-from-a-plt-stub-using-gdb.html

## 动态链接与全局偏移表 (GOT) 当程序使用共享库中的函数(如 `puts`)时,它不会直接包含函数的代码。相反,它使用*动态链接*,代码在运行时链接。这通常与*延迟绑定*结合使用,意味着链接仅在首次调用函数时发生。 最初,程序在**全局偏移表 (GOT)** 中包含 `puts` 的占位符。第一次调用 `puts` 时,执行流程会经过一个 **PLT (程序链接表) stub** – 一个小的代码片段,它跳转到当前的 GOT 条目。由于 GOT 条目最初指向 PLT 内部,因此会调用动态链接器。 动态链接器解析 `libc.so` 库中 `puts` 的实际地址,并使用该地址修补 GOT 条目。随后对 `puts` 的调用将直接从 PLT stub 跳转到 GOT 中的解析地址,绕过动态链接器并提高性能。 这个过程展示了动态链接如何实现代码重用和高效的程序执行,仅在需要时解析函数地址。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 如何使用GDB从PLT桩获取GOT地址 (rafaelbeirigo.github.io) 6点 由 rafaelbeirigo 2小时前 | 隐藏 | 过去 | 收藏 | 1条评论 knome 22分钟前 [–] 为了让不熟悉缩写词的程序员理解,你可以在第一次使用时定义你的缩写词。回复 考虑申请YC冬季2026批次!申请截止至11月10日 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请YC | 联系 搜索:
相关文章

原文
1 November 2025

by Rafael Beirigo

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

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.

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

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 <[email protected]>
   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 <[email protected]>:  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 <[email protected]>:  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!

  • GOT entries start pointing back into the PLT.
  • The dynamic resolver patches them with real libc addresses.
  • Subsequent calls jump directly to the resolved function.
tags: gdb - got - got/plt - dynamic-linking
联系我们 contact @ memedata.com