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 = 0; 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 = 0; i <= 5; ++i ) { v5 = qword_601080; v4 = 0; while ( v5 ) { if ( *(v5 + 4) == *(i + a1) ) { v4 = *v5; break; } v5 = *(v5 + 8); } *(&v6 + i) = v4; } for ( j = 0; 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 0\n" gdbscripttop += "set height 0\n" gdbscripttop += "set verbose off\n" gdbscripttop += "pset option ansicolor off\n" gdbscripttop += "br *"+CMPLOC+"\n" gdbscripttop += "r\n" gdbscriptend = "quit\n" # 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 = 0 linenum = 0 f=open(BINARY+".gdb","w") f.write(gdbscripttop) for x in range(len(passsofar)): f.write("c\n") 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 > 0: break
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 0\n" gdbscripttop += "set height 0\n" gdbscripttop += "set verbose off\n" gdbscripttop += "pset option ansicolor off\n" gdbscripttop += "br *"+CMPLOC+"\n" gdbscripttop += "r\n" gdbscriptend = "quit\n" # 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 = 0 linenum = 0 f=open(BINARY+".gdb","w") f.write(gdbscripttop) for x in range(len(passsofar)): f.write("c\n") 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 > 0: break
No comments:
Post a Comment