-
-
[原创]新人PWN入坑总结(四)
-
发表于: 2021-8-2 17:47 16414
-
话不多说,接上文
1.常规checksec,开了一个NX,没办法shellcode。IDA打开查看程序,找漏洞,有个屁的漏洞,只有一个syscall的系统调用,各种栈操作也没有。
2.观察这个系统调用,系统调用参数通过edx,rsi,rdi赋值,edx直接被赋值为400h,buf对应的rsi被rsp赋值,系统调用号fd对应的rdi被rax赋值。再查看汇编代码,有xor rax,rax,所以rax一定是0,那么这个syscall系统调用的就是read函数,读取的数取直接存入栈顶。由于buf大小为400h,且只有一个syscall,之后直接retn,没有leave指令,这就代表了rsp指向的地址就是我们执行完syscall后start函数retn的返回地址(pop eip)。也就是如果输入一个地址,读取完之后,通过retn就会跳转到该地址中。另外程序中除了retn之外没有其它对栈帧进行操作的指令,如果输入多个syscall地址,就可以反复执行syscall。并且最开始输入400h字节,程序流完全可控。
3.首先想到rop,但是题目没给Libc,并且通过调试发现,这个程序压根就没导入外部的Libc库,IDA中打开没有extern,完全没办法常规rop,那么想用SROP。远程调试一下查看堆栈数据,发现临时创建的smallest段数据没有可写权限,能够利用的只有[stack]栈数据。所以这里需要先泄露一个栈地址来让我们能够往栈中写入数据binsh从而调用execve(‘/bin/sh\x00’,0,0)来直接getshell。
4.之后观察栈上的数据,发现当运行到syscall时,rsp下方的内容全是栈上的地址。
rbp一直都是0x000......这是因为程序只有一个start函数,根本就没有为函数再次创建栈,所用的只是最初生成的栈空间。根据这个原理,我们可以通过系统调用sys_write函数,来打印rsp指向的内容,也就是某个栈地址,这样就成功泄露栈地址。
5.但是sys_write的调用号是1,而通过调试发现rax的初始值被默认设置为0,并且程序中没有任何修改rax的代码。唯一一个也只有xor rax, rax,但是任何数和本身异或的结果都是0,所以如果程序每次都从这行代码执行,那么执行的系统调用号永远都是0,也就是会无限循环read。这里想到由于栈完全可控,并且输入一个地址,程序执行完这个地址对应的函数后retn会直接跳转到rsp的下一行。这里选择让程序再执行一次sys_read函数,之后我们为其中一次输入一个字节,并且这次返回不再从xor这行代码开始执行,从mov rsi, rsp开始。由于sys_read的返回值自动写回给rax(一般函数的返回值都会写给rax),所以读取几个字节read就向rax写入多少,这样就会使得rax也可以得到控制,不再被xor为0,调用我们想调用的系统函数。
6.所以编写payload:先尝试一下看能否泄露栈地址,test1.py
#利用sys_read随便读取一个字符,设置rax = 1,由于retn关系,rsp下拉了一个单位,所以这里会读入到原先的rsp+0x8处,也就是从原先的Payload中第8个字符开始,抽取一个字符,就是set_rsi_rdi_addr的最后一个字节,为了不改变返回地址。如果写成:io.send(‘\xb8’)效果一样,都是为了不改变返回地址。之后再执行set_rsi_rdi_addr从而执行write函数,
7.这里可以看到成功泄露了一个栈地址,但是不能再用简单读入binsh字符串之后设置SigreturnFrame结构体来getshell,因为这里设置读入地址是通过rsp设置的。如果将rsp设置为我们想读入binsh的栈地址,那么肯定是可以读入binsh字符串的,但是当程序运行到retn时,跳转的是binsh这个地址,这是不合法的,没办法跳转,程序会崩溃。
这里就考虑使用SigreturnFrame()来进行栈劫持,将整个栈挪移到目的地。
(1)首先布置SigreturnFrame()的栈空间,进行栈劫持:
(2)发送payload。
程序运行SROP过程中,会执行read函数,将payload2读取到stack_addr处,所以当程序运行完SROP后,栈顶rsp被劫持到stack_addr处,同时stack_addr上保存的内容是payload2,首地址是start,所以retn执行后仍旧从start开始。
(3)设置第二次的SigreturnFrame攻击:
这里的0x108是计算出来的,需要计算从stack_addr到rdi,也就是binsh字符串的距离。由于传进去的是结构体,大小为0xf8。前一个例子中binsh字符串是放在str(frameExecve)之前,所以没有那么大。这里却是放在str(frame_execve)之后,所以从stack_addr为起始,start_addr,syscall_addr,frame_execve),总共为0xf8+0x08*2=0x108,这里不太懂可以调试一下看看。也就是再一次start_addr读取字符串binsh的位置。
8.发送payload,读取binsh字符串,getshell:
9.尝试使用mprotect为栈内存添加可执行权限x,从而shellcode来getshell。
(1)第一段的劫持栈和读取payload2进入劫持栈处都是一样的
(2)第二段需要调用mprotect来修改权限:
(3)最后的shellcode:
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157
1.常规checksec,只开启了NX。IDA打开找漏洞,程序很简单,读入数据栈溢出,可以读取1295个字节,但是buf只有0x10h大小,所以可以溢出足够长的数据。
2.程序没有后门,没有导入system函数,没有binsh字符串,也没有write、put、printf之类的可以泄露libc的函数,没办法ROP,然后ROPgadget也搜不到syscall。这里换用另一种工具ropper来搜索:ropper --file=unexploitable --search "syscall",可以搜到一个。有了syscall,可以尝试用rop来给寄存器赋值然后开shell,但是这里还是搜不到给rsi,rdi等寄存器赋值的gadget,这就意味着我们也没办法直接通过ROP实现getshell。
3.如果没有开NX,直接栈劫持然后shellcode就完事了,但是开启了NX,没办法shellcode。
4.啥也不管用的时候,可以用SROP,也就是通过syscall再利用SigreturnFrame来设置寄存器rsi和rid的值,加上字符串binsh可以直接getshell,不用非得设置rsi,rdi寄存器值的rop。但是这里使用SigreturnFrame有限制,需要溢出的长度较长一些,32位下需要依顺序布置栈,64位条件下需要往栈中布置一个结构体,所以需要输入足够长的payload来修改。
5.这里使用的方案是SigreturnFrame,先考虑一段足够长的可修改的内存地址来给我们存放栈劫持的内容。但是在IDA中按ctrl+s查看内存段,发现所有的可改可读的内存段都特别小,没办法存放足够长的溢出内容。这里忽略了一个知识点,临时创建的缓存:也就是我们使用read(0, &buf, 0x50FuLL);时,会临时创建一个长度为0x50F的缓存区间,这个区间足够满足我们的需求,但是在IDA中找不到,那就没办法栈劫持到这个位置。这里可以先动态调试一下,由于没有开启PIE,程序加载后的基地址是固定的,所以无论程序加载多少次,地址仍然不会发生改变。那么转向动态调试,可以看到程序冒出来一个可读可写的内存段:unexploitable,这个就是临时创建的一个缓存区间,长度为F88,足够用来执行操作。
6.在这个区间上任意选取一个地址来栈劫持,这里选择0x60116c,然后编写payload,尝试能否成功栈劫持并且读入binsh:
这样接下来如果再输入binsh字符串,就可以读取到[rbp+buf]处。需要注意的是,这里的set_read_addr是从下图最开始跳转,如果直接跳转call read,那么就会由于read取参是edx,rsi,edi,从而导致数据会存入rsi指向的地址,没办法存到我们劫持的栈中。观察read函数汇编代码可以知道,虽然read存入的地址是rsi,但是rsi是通过rbp+buf来赋值的,所以我们可以通过修改rbp为fake_stack,使得rbp+buf的地址变为fake_stack上的某个地址,再执行下图中的代码,就可以使得read读取的内容直接进入到劫持栈rbp+buf上,也就是fake_stack上。
7.栈劫持完成之后,考虑第二段的payload,也就是输入binsh字符串和后续内容,来执行SigreturnFrame,使用:
输入字符串binsh,存放在fake_stack_addr-0x10处
读取完之后,执行leave指令之前的栈底为0x60116c,而leave指令相当于:mov rsp rbp;和pop rbp:
(1)第一条mov rsp rbp之后,0x60116c就被赋值给rsp,也就是rsp指向0x60116c。
(2)第二条pop rbp之后,把0x60116c处的内容赋值给rbp,这里设置0x60116c处的内容为fake_stack_addr+0x10,也就是0x60117c,那么rbp指向0x60117c。rsp下挪一个单位,指向0x60116c+0x08=0x601174。
故leave指令执行完后rsp = 0x601174,rbp = 0x60117c。
▲这里这么设置是有原因的,为了挪动rsp来指向0x601174。
接着执行retn指令,相当于pop eip,此时的rsp指向 0x601174,所以我们需要将0x601174处的值变为read_addr的地址,也就是这条语句,这里设置read_addr为0x400571,也就是带有call指令的read。
retn指令之后就是call指令,各种寄存器的值还是没变,所以照常用就行,回来之后rsp仍旧指向0x60117c。此时栈状态为:
rsp = 0x60117c,rbp = 0x60117c。
读取15个字符到0x60115c,目的是利用read返回值为读取的字节数的特性设置rax=0xf,注意不要使/bin/sh\x00字符串发生改变。
最后io.interactive()即可getshell。
▲总的程序流应该是:首次read->set_read->call_read->syscall
结构体的设置,固定模式:
开头设置:
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157
1.常规checksec,开启了NX和Canary。打开IDA发现程序两个漏洞:
(1)功能1中栈溢出:
(2)功能2中输出字符串:puts(&buf);
注:这里的put和printf不是同一个概念,不是格式化字符串的函数。但是由于put是直接输出我们的输入,而我们的输入被保存在main函数栈上,所以可以输入足够多的数据连上canary,利用put一并打印出来,从而把canary泄露出来。
2.调试,IDA中观察位置,计算偏移,可以知道偏移为0xB0-0x8=0xA8=168个字符,(canary通常被放在rbp-0x08的位置处,当然也不一定,最好还是调试一下)这样就可以构造第一个payload:payload1 = ”A”*168 + “B”。
这里再加一个B是因为canary的保护机制,一般情况下canary的最后两位也就是最后一个字符都是\x00,由于是大端序,所以可以防止不小心把canary泄露出来。因为上一个栈内存的最后第一个字符连接的是下一个栈内存的第一个字符,也就是canary中的\x00,而打印函数默认00是字符串的结尾,所以这里如果输入”A”*168,那么打印出来的就只会是168个A,不会将canary带出来。所以我们再加一个B,覆盖掉canary在占内存的第一个字符00,从而能够连接上成为一个完整的字符串打印出来。但又由于是大端序,泄露出来的canary应该最后一个字符是B,对应\x42,这里需要修改成\x00从而获得正确的canary。同理,如果随机化的canary中含有\x00,那么仍然会导致字符串截断,无法得到正确的canary。所以其实如果多执行几次,碰到包含\x00的canary,就会导致程序出错。
泄露加修改:canary = u64('\x00'+io.recv(7))
3.之后就可以利用canary的值和栈溢出,调用put函数打印其它函数的实际地址。这里程序使用了read函数,并且同时外部调用了read函数,可以通过输入read的.got表的值,使其打印read函数的真实地址。同时需要注意的是,由于是64位程序,传参是从rdi开始,所以栈溢出的第一个返回地址应该给rdi赋值才对,编写payload1。
这样就可以得到read的实际地址,从而通过libc库计算偏移地址得到基地址。
5.现在有了libc库的基地址,观察main函数退出时的汇编代码:mov eax, 0可以使用在2.24libc库条件下可以使用onegadget。
6.直接计算onegadget偏移,然后覆盖main函数的返回地址,getshell。
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157
1.常规checksec,程序全部开启保护,并且有canary保护,从IDA中汇编代码和伪代码也可以看到:
(1)汇编代码:
①生成canary的代码:一般在函数初始化的时候就可以看到
②校验:
(2)伪代码:
有很多种形式,如下也是一种:
2.之后查找漏洞,找到两个漏洞:
(1)功能1的sub_F00函数中的printf存在格式化字符串漏洞:
这里的1LL不知道是个什么意思,但是实际效果仍然相当于是printf(a1),调试中可以知道。
(2)功能2的sub_1000存在栈溢出漏洞:
3.现在是保护全开,栈溢出漏洞因为canary的关系没办法利用,唯一能利用的只有一个printf()函数,而且还没办法劫持got表,没办法进行完全栈操控。所以这里就想能不能通过printf函数泄露canary从而使得栈溢出这个漏洞派上用场。
4.首先调试,观察canary在栈上的偏移位置,调试断点下在sub_F00函数的printf函数上,因为这个sub_F00函数中也有canary的保护,那么该函数栈上一定存在canary的值。自己调试如下图:
IDA调试界面点击对应生成canary的代码mov [rsp+28h+var_20], rax中的[rsp+28h+var_20]就可以知道canary的位置应该是rsp+8h处,这里也可以看出来V6就是canary
另外由于这是64位程序,取参顺序是rdi, rsi, rdx, rcx, r8, r9, 栈,由于printf()前两个参数位rdi,rsi对应的是fd和&buf,
这里的buf就是我们输入的username,因为username的输入保存在堆上main函数中有声明:
下图是没有打印之前的内容:
我们可以看到rsi的值是5开头的,这其实就是一个堆内存地址,调试过程中输入跳转就可以看到该地址对应的内容就是我们的输入username的值。那么输入username时输入多个%p,触发格式化字符串漏洞,打印寄存器和栈上的内容,泄露出libc地址和canary。printf()依次根据%p打印的参数顺序是rdx,rcx,r8,r9,栈。所以r9之后第一个打印出来的数据是rsp-8h,也就是canary的值,这样就可以得到泄露的canary的值,从而控制栈溢出。同时我们可以发现打印出来的数据中包含libc中的函数,这样同时也泄露出来了libc加载后的地址,之后通过偏移计算出基地址。
5.之后进行栈溢出操控,但是这里如果连不上账户会没办法使用sub_1000函数,用IDA查看可以看到在sub_f00函数中对密码进行检查,可直接查看到密码:
这个off_204010就是密码,点进去就可以看到。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- [原创]CVE-2021-22555_Netfilter堆溢出提权漏洞 22416
- [原创]Kernel从0开始(四) 28156
- [原创]Kernel从0开始(三) 29251
- [原创]Kernel从0开始(二) 32429
- [原创]Kernel从0开始(一) 44925