BCTF 2017-100levels

1.常规checksec,开启了NX和PIE,不能shellcode和简单rop。之后IDA打开找漏洞,E43函数中存在栈溢出漏洞:

1
2
3
4
5
#注释头

__int64 buf; // [rsp+10h] [rbp-30h]
--------------------------------------------------
read(0, &buf, 0x400uLL);

有栈溢出那么首先想到是应该查看有没有后门,但是这个程序虽然外部引用了system函数,但是本身里并没有导入到.got.plt表中,没办法直接通过.got.plt来寻址。而且开了PIE,就算导入到.got.plt表中,也需要覆盖返回地址并且爆破倒数第四位才能跳转到system函数。虽然有栈溢出,但是没有后门函数,同样也没办法泄露Libc地址。

2.想getshell,又只有一个栈溢出,没有其它漏洞,还开了PIE和NX,那么一定得泄露出地址才能做,而printf地址也因为PIE没办法直接跳转。陷入卡顿,但是这里可以查看E43函数中的printf的汇编代码:

只能通过栈溢出形式来为下一次的printf赋参数来泄露。又由于PIE,也不知道任意一个函数的代码地址,那也没办法泄露被加载进入Libc中的内存地址。

3.通过调试可以看到,进入E43函数中,抵达printf函数时,栈顶的上方有大量的指向libc的地址:

img

并且观察E43函数中的汇编代码,可以看到Printf是通过rbp取值的,那么我们可以通过栈溢出修改rbp来使得[rbp+var_34]落在其它地方,而如果这个其它地方有libc地址,那么就相当于泄露出了Libc地址。

img

4.这个关卡数是由我们设置的,而且通过递归调用E43函数,形成多个E43的栈,那么进行调试,第二次进入E43的栈之后,仍然在运行到printf函数时,栈顶上方仍旧有大量的Libc地址。由于我们需要修改rbp来使得下一次的printf打印出libc地址,那么关卡最低需要设置两关,第一关用来栈溢出,修改rbp,使得第二关中的printf函数指向栈顶上方从而打印出Libc地址。

5.由于栈的随机化,我们如果随意修改rbp那么就会打印出奇怪的东西,所以修改rbp的最后一个字节,使得[rbp+var_34]能够移动一定范围,以一定几率命中栈顶上方。而又由于是递归调用,第一关的栈在第二关的栈的上方,模型大致如下:

(1)第一次rbp和rsp以及第二次的如图:

img img

(2)第一次栈以及第二次栈如图:

img

img

▲这里用的是Libc-2.32的,用其他的Libc就不太一样,具体情况具体分析。

6.这里的模型是假设第一次的rsp栈顶后两位为00,但是由于栈地址随机化,所以rsp其实可以从0x00-0xFF之间变化,对应的地址也就是从0-31之间变化。

7.这里先考虑第一个问题,rbp-var34如何落到libc空间中,也就是当0往下移动,变化为大约是4或者5时,即可落到libc空间。同样的,从5-16变化,都可以使得rbp-var34落在libc空间。但是如果0变化成16以上,对应的第二次栈空间rbp就会变成32以上,换算成16进制为0x100,这时修改最后两位,就会变成0x15c,使得它不但不往上走,更会往下走,从而没办法落到libc空间。总而言之,慢慢研究下,然后计算概率大约为12/32=3/8,可以使得落在Libc空间。这里的5c可以改变成其它值x,但是需要x-0x34为8的倍数才行,不然取到的地址会是截断的,但是修改后成功概率会发生改变,因为0x5c扫到的地址范围大概就是libc的栈空间。

8.落在libc空间不代表一定就会落在指向Libc地址上,前面可以看到,在16个地址范围内大概为7个,也就是1/2的概率成功。然后由于有v2%a1这个运算,也就对应汇编代码idiv [rbp+var_34],这就导致如果rbp+var_34的数据为0那么就会产生除零操作,这里没办法去掉。需要进行try操作来去除这个错误,使程序重新运行,进行自动化爆破。同时泄露出来的地址会发现有时候是正数有时候是负数。这是因为我们只能泄露出地址的低32位,低8个十六进制数。而这个数的最高位可能是0或者1,转换成有符号整数就可能是正负两种情况。这里进行处理可避免成功率下降:

1
2
3
4
#注释头

if addr_l8 < 0:
addr_l8 = addr_l8 + 0x100000000

9.但是泄露出来的地址由于printf的参数是%d,所以打印出来的是32位地址,还需要猜剩下32位。但是这里有个技巧,貌似所有64程序加载后的代码段地址都在0x000055XXXXXXXXXX-0x000056XXXXXXXXXX之间徘徊,对应的libc加载段在0x00007EXXXXXXXXXX-0x00007FXXXXXXXXXX范围,以下是测试数据:

程序开头段.load首地址和debug段首地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
#注释头

00007F1301D2A000
000056238FCAB000
差值为28EF 7207 F000

00007FCB31061000
000055D513E06000
差值为29F6 1D25 B000

00007F58EFF09000
000055F7C1BEC000
差值为2983 DC10 3000

具体原理好像是PIE源代码随机的关系,但具体不太清楚,能用就行。所以高32位就可以假设地址为0x00007fxx,所以这里需要爆破0x1ff大小,也就是511,相当于512次,但是其实可以知道,大概率是落在0x7f里,看数据分析也可以知道,所以实际爆破次数基本在500次以内。所以将泄露出来的地址加上一个在0x7f里的值,也就是addr = addr_l8 + 0x7f8b00000000,之后再根据Libc空间中指向libc地址的后两位来区分地址:并减去在libc中查到的偏移量即可得到Libc基地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#注释头

if hex(addr)[-2:] == '0b': #__IO_file_overflow+EB
libc_base = addr - 0x7c90b

elif hex(addr)[-2:] == 'd2': #puts+1B2
libc_base = addr - 0x70ad2

elif hex(addr)[-3:] == '600':#_IO_2_1_stdout_
libc_base = addr - 0x3c2600

elif hex(addr)[-3:] == '400':#_IO_file_jumps
libc_base = addr - 0x3be400

elif hex(addr)[-2:] == '83': #_IO_2_1_stdout_+83
libc_base = addr - 0x3c2683

elif hex(addr)[-2:] == '32': #_IO_do_write+C2
libc_base = addr - 0x7c370 - 0xc2

elif hex(addr)[-2:] == 'e7': #_IO_do_write+37
libc_base = addr - 0x7c370 - 0x37

所以算上命中概率,其实调试的时候可以看到,第一关的栈空间中由于程序运行结果也会有几个指向Libc地址,加上这几个也可以提高成功率,因为修改的rbp也是有可能落在第一关的栈空间。总的爆破次数应该就是500/((1/2)*(3/8)),约为2500次,还能接受。

10.泄露出Libc地址之后一般就有两种方法,一种是利用栈溢出,调用万能gadget用system函数进行binsh字符串赋值,从而getshell。还有一种就是,利用one_gadget来getshell,通过查看E43返回时的汇编代码有一个move eax,0;满足libc-2.23.so的其中一个one_gadget的条件,那么直接用就行。

11.最后libc基地址加上one_gadget的偏移地址就可以得到one_gadget的实际地址。

one_gadget = libc_base + 0x45526

之后在第二关中再次进行栈溢出覆盖rip来跳转到one_gadget即可getshell。

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157