Tuesday, October 6, 2015

D-CTF 2015: r100 and r200 Reverse Engineering Challenges

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 = 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


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 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

So at the end of the day 300 points (100+200) for a single Python script. Not too bad.

No comments:

Post a Comment