I didn’t have any time to play D-CTF this year because im out of the country traveling. I did spend one evening solving these two challenges though because I thought of an interesting idea I could apply to both challenges simultaneously.
Basically we are given two Linux ELF binaries:
https://github.com/ByteBandits/writeups/blob/master/defcamp-quals-2015/reverse/r100/r100.bin
https://github.com/ByteBandits/writeups/blob/master/defcamp-quals-2015/reverse/r200/r200.bin
Each of these binaries is a 64 bit ELF binary.
r100: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x2448460f21e38ecca7809aef1b0bc7996881ec6a, stripped
I placed them into IDA Pro and examined them quickly and noted that both used the return value of ptrace() to detect if a debugger was attached, and if so, just enter an infinite loop:
Other than that the binaries seem to just ask the user for a password, then do something, check the password and print a response.
root@mankrik:~/dctf/re100# ./r100
Enter the password: Test
Incorrect password!
An example of the code that is checking the user input for r100 is below:
signed __int64 __fastcall sub_4006FD(__int64 a1)
{
signed int i; // [sp+14h] [bp-24h]@1
char v3[8]; // [sp+18h] [bp-20h]@1
char v4[8]; // [sp+20h] [bp-18h]@1
char v5[8]; // [sp+28h] [bp-10h]@1
*v3 = "Dufhbmf";
*v4 = "pG`imos";
*v5 = "ewUglpt";
for ( i = ; i <= 11; ++i )
{
if ( *(*&v3[8 * (i % 3)] + 2 * (i / 3)) - *(i + a1) != 1 )
return 1LL;
}
return 0LL;
}
So evidently, it iterates through the input, byte by byte validating the user input. If an invalid byte is found it returns and exits the program, emitting the “Incorrect Password” error.
Similarly the r200 program does the same thing but with a few more steps:
signed __int64 __fastcall sub_40074D(__int64 a1)
{
signed int i; // [sp+8h] [bp-50h]@1
signed int j; // [sp+8h] [bp-50h]@9
int v4; // [sp+Ch] [bp-4Ch]@2
__int64 v5; // [sp+10h] [bp-48h]@2
__int64 v6; // [sp+18h] [bp-40h]@1
__int64 v7; // [sp+20h] [bp-38h]@1
__int64 v8; // [sp+28h] [bp-30h]@1
int v9; // [sp+38h] [bp-20h]@1
int v10; // [sp+3Ch] [bp-1Ch]@1
int v11; // [sp+40h] [bp-18h]@1
int v12; // [sp+44h] [bp-14h]@1
int v13; // [sp+48h] [bp-10h]@1
int v14; // [sp+4Ch] [bp-Ch]@1
v6 = 0LL;
v7 = 0LL;
v8 = 0LL;
v9 = 5;
v10 = 2;
v11 = 7;
v12 = 2;
v13 = 5;
v14 = 6;
for ( i = ; i <= 5; ++i )
{
v5 = qword_601080;
v4 = ;
while ( v5 )
{
if ( *(v5 + 4) == *(i + a1) )
{
v4 = *v5;
break;
}
v5 = *(v5 + 8);
}
*(&v6 + i) = v4;
}
for ( j = ; j <= 5; ++j )
{
if ( *(&v6 + j) != *(&v9 + j) )
return 1LL;
}
return 0LL;
}
At the end of the day though, there’s still a key pivot point in both of these programs where a comparison is being made on a byte by byte basis.
An example is shown from the r200 binary below where a “cmp edx,eax” instruction at address 0x40082e is used to verify the encoding of the input byte matches the expected output value.
Similarly in r100 a “cmp eax,1” at address 0x40078b is used to validate the same.
So since we know the typical input will be printable characters, and we know where in the execution flow we know to check for a comparison, we can quickly stop reversing any of the binary itself and move on to a brute force style attack against the password.
FYI - I realize at this point that reversing the particular algorithms being used in these binaries was probably trivial to the point where building a generic framework to finish both of these challenges at the same time was a silly diversion, but I thought it would be fun and an interesting idea for future challenges.
I engineered a dodgy system of Python and GDB and the output is such below for the r100 program:
root@mankrik:~/dctf/re100# ./r100solve.py
[+] Patching binary...
[*] '/root/dctf/re100/r100'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE
[+] Brute Forcing password...
[+] Password so far: C
[+] Password so far: Co
[+] Password so far: Cod
[+] Password so far: Code
[+] Password so far: Code_
[+] Password so far: Code_T
[+] Password so far: Code_Ta
[+] Password so far: Code_Tal
[+] Password so far: Code_Talk
[+] Password so far: Code_Talke
[+] Password so far: Code_Talker
[+] Password so far: Code_Talkers
The source code for this is below.
#!/usr/bin/python
from pwn import *
import itertools
import os
BINARY="./r100" # name of the original binary file
PATCHLOC=0x4007e2 # location to patch
PATCHVAL='xeb' # what to patch with, 0xeb = jmp
CMPLOC="0x40078b"
alphabet = list("_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUFWXYZ1234567890!@#$%^&*()-=+:<>")
passsofar = ""
gdbscripttop = "set width 0n"
gdbscripttop += "set height 0n"
gdbscripttop += "set verbose offn"
gdbscripttop += "pset option ansicolor offn"
gdbscripttop += "br *"+CMPLOC+"n"
gdbscripttop += "rn"
gdbscriptend = "quitn"
# patch out the anti-debug
print "[+] Patching binary..."
e = ELF(BINARY)
e.write(PATCHLOC, PATCHVAL)
e.save(BINARY + ".patched")
os.chmod(BINARY + ".patched",755)
print "[+] Brute Forcing password..."
while len(passsofar) <= 21:
for j in itertools.product(alphabet, repeat=1):
found =
linenum =
f=open(BINARY+".gdb","w")
f.write(gdbscripttop)
for x in range(len(passsofar)):
f.write("cn")
f.write(gdbscriptend)
f.close()
with open(os.devnull, 'w') as devnull:
p = subprocess.check_output("echo "+passsofar+"".join(j)+" | gdb --command="+BINARY+".gdb ./"+BINARY+".patched",shell=True,stderr=devnull)
for line in p.splitlines():
if "RAX" in line:
linenum+=1
if linenum >= len(passsofar)+1:
if line.strip() == "RAX: 0x1":
print "[+] Password so far: " + passsofar + "".join(j)
passsofar += "".join(j)
found = 1
if found > :
break
And for r200, the comparison is slightly more complex, but a small tweak sees it fall using basically the same script:
root@mankrik:~/dctf/re200# ./r200solve.py
[+] Patching binary...
[*] '/root/dctf/re200/r200'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE
[+] Brute Forcing password...
[+] Password so far: r
[+] Password so far: ro
[+] Password so far: rot
[+] Password so far: roto
[+] Password so far: rotor
[+] Password so far: rotors
And you can see the source is almost identical. I think this might be useful in the future so Im glad I wrote this up.
#!/usr/bin/python
from pwn import *
import itertools
import os
BINARY="./r200" # name of the original binary file
PATCHLOC=0x400880 # location to patch
PATCHVAL='xeb' # what to patch with, 0xeb = jmp
CMPLOC="0x40082e"
alphabet = list("_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUFWXYZ1234567890!@#$%^&*()-=+:<>")
passsofar = ""
gdbscripttop = "set width 0n"
gdbscripttop += "set height 0n"
gdbscripttop += "set verbose offn"
gdbscripttop += "pset option ansicolor offn"
gdbscripttop += "br *"+CMPLOC+"n"
gdbscripttop += "rn"
gdbscriptend = "quitn"
# patch out the anti-debug
print "[+] Patching binary..."
e = ELF(BINARY)
e.write(PATCHLOC, PATCHVAL)
e.save(BINARY + ".patched")
os.chmod(BINARY + ".patched",755)
print "[+] Brute Forcing password..."
while len(passsofar) <= 21:
for j in itertools.product(alphabet, repeat=1):
found =
linenum =
f=open(BINARY+".gdb","w")
f.write(gdbscripttop)
for x in range(len(passsofar)):
f.write("cn")
f.write(gdbscriptend)
f.close()
with open(os.devnull, 'w') as devnull:
p = subprocess.check_output("echo "+passsofar+"".join(j)+" | gdb --command="+BINARY+".gdb ./"+BINARY+".patched",shell=True,stderr=devnull)
for line in p.splitlines():
if "RAX" in line:
rax = line.split()
linenum+=1
if "RDX" in line:
rdx = line.split()
linenum+=1
if rax[1] == rdx[1]:
print "[+] Password so far: " + passsofar + "".join(j)
passsofar += "".join(j)
found = 1
if found > :
break
So at the end of the day 300 points (100+200) for a single Python script. Not too bad.