newstar2023的题,在buu上还可以做,算是我正儿八经做的第一道pwn,太曲折了啊啊啊。
存在pop_rdi_ret
的gadget可以利用
在x86_64架构中,函数调用的参数通过寄存器来传递,而不是通过栈来传递。前六个整数或者指针参数是通过
rdi
,rsi
,rdx
,rcx
,r8
,r9
这六个寄存器来传递的。如果有更多参数,剩下的参数通过栈传递。
若要使用ROP(Return Oriented Programming)来调用一个函数并且操控参数时,就要找到可以控制这些寄存器的gadget。比如
1 | 1. pop rdi;ret |
获取pop_rdi_ret
的地址
1 | ROPgadget --binary /... --only "pop|ret" |
然后看一下buf的偏移量
找到偏移为0x20
buf是全局变量,被放在bss段上
在 C 语言中,一个变量是在 BSS 段还是在栈上,主要取决于它的声明位置和存储类别:
- 全局变量和静态变量:这些变量通常存储在 BSS 段。BSS 段用于存储程序中未初始化的全局变量和静态变量。
- 局部变量和函数参数:这些变量通常存储在栈上。当一个函数被调用时,它的局部变量和参数会被压入栈中。
可以使用objdump -h program//readelf -S program
查看bss
段的地址(不同环境下bss
的地址可能不同)
使用peda
的vmmap
可以查看bss
段有无执行权限
整理一下思路,我们可以先把buf
塞满(0x20),然后塞rbp
(0x8,因为在bss
段所以可以直接塞bss
的起始位置)。
1 | p.sendafter( |
然后因为64位程序的函数的第一个参数在rdi
中,因此把栈顶的值弹出到rdi
。在x86_64中,调用函数时,函数的第一个参数是通过rdi
寄存器传递的。因此使用pop_rdi_ret
这个gadget,把它的地址放在原本的return address
处,那么程序就会跳转到pop_rdi_ret
的地址处执行它。pop_rdi
将此时的栈顶数据(puts
函数的地址)弹给rdi
,然后ret
到plt
表的puts
处调用puts
,那么puts
就会找到rdi
,打印rdi
中puts
的地址。puts
执行之后,将ret
回main_addr
。
这样就完成了puts
函数地址的泄露,可以用于泄露远程libc的版本。
这里我还另外打了read的地址。查libc版本的时候会准确一点。只有一个地址有时会有很多个版本的libc被检索到。https://libc.rip/
只搜索三个字节(12位)的原因:
即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变
这样就得到了libc
的版本,可以看到有system
和/bin/sh
的偏移。所以我们计算libc_base
,加上偏移得到需要的函数的地址。
1 | libc_base = puts_addr - 0x80970 |
这是手动计算的方法。还有其他两种办法(官方提供的)写在完整代码中了。
现在的目标就是执行system('/bin/sh')
了。与上面类似,仍然利用栈溢出构造ROP链。
1 | bss-->pop_rdi_ret-->binsh_addr-->system_addr--main_addr |
所以可以:
1 | p.sendafter( |
然而这样执行程序崩溃了。为什么呢?
因为在ubuntu18版本以上调用system
函数需要栈对齐。
64位的system
命令有个movaps
指令
Movaps:
movaps XMM,XMM/m128 movaps XMM/128,XMM
把源存储器内容值送入目的寄存器,当有m128时,必须对齐内存16字节,也就是内存地址低4位为0,保证rsp&0xf==0
(调用printf时也需要对齐)
我们加一个ret
来栈对齐:
1 | p.sendafter( |
大功告成!
1 | from pwn import * |