Sunday, September 27, 2015

Trend Micro CTF - vonn - Analysis-Defensive (100) Challenge

Trend Micro CTF was this weekend. This is the first one of these Trend Micro CTF I've done and I was expecting it to be really well run and fun.

I was pretty surprised to see the real situation though. I felt the difficulty scaling was all over the place and just too many guessing games. Also a lack of communication with no official IRC channel and only a couple of tweets from Trend throughout the day.

Anyway, I still am determined to document at least one solution per CTF so here's the one I decided because I liked the idea.

This is in the Analysis-Defensive category, which is essentialy the same as any other CTF's Reverse Engineering category. We are given a ZIP file containing a single file called "vonn":


root@mankrik:~/trend/analysisdef/vonn# file vonn
vonn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xbbc2897f089dcc360a98a482764ad499e209fb74, not stripped

Which is a 64 bit ELF binary that is not stripped. Neat.

When we execute it we just get the string


root@mankrik:~/trend/analysisdef/vonn# ./vonn
You are on VMM!

(Oh the program deletes itself also, just a tip, make a backup of it first)

Which doesn't say a lot but is a good clue. Trend Micro being a company related to AV has probably dealt with VM aware threats quite a lot. So perhaps this program uses a trick to determine if it's on a VM and behaves a certain way because of it.

Let's look in IDA Pro:


int __cdecl main(int argc, const char **argv, const char **envp)
{
  unsigned __int64 v8; // ST58_8@1
  unsigned __int64 v9; // ST68_8@1
  unsigned __int64 v10; // ST78_8@1
  int result; // eax@4
  unsigned __int64 v12; // [sp+B8h] [bp-18h]@1
  unsigned __int64 v13; // [sp+C0h] [bp-10h]@1
  unsigned __int64 v14; // [sp+C8h] [bp-8h]@1

  __asm { cpuid }
  v8 = __rdtsc();
  v9 = __rdtsc();
  v10 = __rdtsc();
  v12 = (v9 | (_RDX << 32)) - (v8 | (_RDX << 32));
  v13 = (v10 | (_RDX << 32)) - (v9 | (_RDX << 32));
  v14 = (__rdtsc() | (_RDX << 32)) - (v10 | (_RDX << 32));
  if ( v12 == v13 || v13 == v14 || v12 == v14 )
  {
    result = puts("You are not on VMM");
  }
  else
  {
    puts("You are on VMM!");
    result = ldex(*argv);
  }
  return result;
}

Yep sure enough, if you are not on a VM then just print a message, but if you ARE on a VM then run the ldex() function passing a pointer to argv, the command line arguments.

The ldex() function seems to be where all the magic happens:


__int64 __fastcall ldex(const char *a1)
{
  void *buf; // ST20_8@2
  int v2; // eax@2
  void *v3; // ST28_8@2
  int fd; // [sp+14h] [bp-ECh]@1
  int v6; // [sp+18h] [bp-E8h]@1
  struct stat stat_buf; // [sp+30h] [bp-D0h]@1
  __int64 v8; // [sp+C0h] [bp-40h]@1
  __int64 v9; // [sp+C8h] [bp-38h]@1
  char v10; // [sp+D0h] [bp-30h]@1
  __int64 v11; // [sp+E0h] [bp-20h]@1
  __int64 v12; // [sp+E8h] [bp-18h]@1
  char v13; // [sp+F0h] [bp-10h]@1
  __int64 v14; // [sp+F8h] [bp-8h]@1

  v14 = *MK_FP(__FS__, 40LL);
  v8 = '.../pmt/';
  v9 = ',,...,,,';
  v10 = 0;
  v11 = '.../pmt/';
  v12 = ',,...,,,';
  v13 = 0;
  fd = open(a1, 0);
  v6 = open("/tmp/...,,,...,,", 66);
  fstat(fd, &stat_buf);
  if ( stat_buf.st_size <= 20480 )
  {
    unlink("/tmp/...,,,...,,");
    exit(-1);
  }
  buf = malloc(stat_buf.st_size + 20480);
  v2 = read(fd, buf, stat_buf.st_size);
  v3 = malloc(stat_buf.st_size + 20480);
  Decrypt(&v8, (char *)buf + 20480, stat_buf.st_size - 20480, &v11, v3, stat_buf.st_size - 20480);
  if ( (signed int)write(v6, v3, stat_buf.st_size - 20480) < 0 )
    exit(-1);
  fchmod(v6, 0x1C0u);
  close(v6);
  unlink(a1);
  execv("/tmp/...,,,...,,", 0LL);
  return *MK_FP(__FS__, 40LL) ^ v14;
}

All this is doing is opening a new file in the /tmp/ path called "...,,,...,,", decrypting a payload into that file, executing it, then cleaning up by deleting itself.

First thing we need to do is grab the payload. Since there's no file in /tmp/ after we execute this from the command line, I assume that the payload is also deleting itself.

Instead, we run "vonn" inside a debugger and set a breakpoint on the execv() call...


root@mankrik:~/trend/analysisdef/vonn# gdb ./vonn
GNU gdb (GDB) 7.4.1-debian

...

gdb-peda$ br execv
Breakpoint 1 at 0x400950
gdb-peda$ r
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000
You are on VMM!
Legend: code, data, rodata, value
Breakpoint 1, execv (path=0x401123 "/tmp/...,,,...,,", argv=0x0) at execv.c:26
26 execv.c: No such file or directory.
gdb-peda$ ^Z
[1]+  Stopped                 gdb ./vonn
root@mankrik:~/trend/analysisdef/vonn# ls -la /tmp/...,,,...,, 
-rwx------ 1 root root 13312 Sep 28 12:24 /tmp/...,,,...,,
root@mankrik:~/trend/analysisdef/vonn# file /tmp/...,,,...,, 
/tmp/...,,,...,,: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xc6faaf0c45fa077ef6a28d0fe87925ed46ea43de, not stripped

Great. I make a copy of the payload and let's analyse this in IDA Pro:


int __cdecl main(int argc, const char **argv, const char **envp)
{
  unsigned __int64 v8; // ST58_8@1
  unsigned __int64 v9; // ST68_8@1
  unsigned __int64 v10; // ST78_8@1
  unsigned __int64 v11; // rdx@1
  int result; // eax@4
  unsigned __int64 v13; // [sp+B8h] [bp-18h]@1
  unsigned __int64 v14; // [sp+C0h] [bp-10h]@1

  __asm { cpuid }
  v8 = __rdtsc();
  v9 = __rdtsc();
  v10 = __rdtsc();
  v13 = (v9 | (_RDX << 32)) - (v8 | (_RDX << 32));
  v14 = (v10 | (_RDX << 32)) - (v9 | (_RDX << 32));
  v11 = (__rdtsc() | (_RDX << 32)) - (v10 | (_RDX << 32));
  if ( v13 == v14 || v14 == v11 || v13 == v11 )
  {
    rnktmp(4197584LL, argv, v11, _RCX);
    result = unlink("/tmp/...,,,...,,");
  }
  else
  {
    result = unlink("/tmp/...,,,...,,");
  }
  return result;
}

Ok this is looking familiar no? This is the same main function as the dropper ("vonn") except there are no printf() messages and the non-VM function looks to be doing the work this time.

So the solution becomes clear at this point. The challenge is a two part binary. A dropper and an encrypted payload. When run on a VM the dropper will decrypt the payload, however the payload will only perform it's duties on a non-VM system.

To get the flag we only need to run the payload on a bare metal system.

Unfortunately I don't have that ready to hand, so I resort to patching the payload. In the graph view I identify an easy "JZ" instruction I can flip to a "JMP" instruction to take our desired execution path:


This JZ lives at address 0x400a33 in memory so let's use pwntools to patch that to a JMP and execute the patched version:


from pwn import *
import os
binary = ELF('/tmp/...,,,...,,')
binary.write(0x400a33, '\xeb')
binary.save('/tmp/...,,,...,,.patched')
os.chmod('/tmp/...,,,...,,.patched',755)
p = process('/tmp/...,,,...,,.patched')
flag = p.recvuntil('}')
print "[+] Flag: " + flag

Which we combine with GDB to get us the flag like so:


root@mankrik:~/trend/analysisdef/vonn# gdb ./vonn
GNU gdb (GDB) 7.4.1-debian
...
Reading symbols from /root/trend/analysisdef/vonn/vonn...(no debugging symbols found)...done.
gdb-peda$ br execv
Breakpoint 1 at 0x400950
gdb-peda$ r
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000
You are on VMM!

Breakpoint 1, execv (path=0x401123 "/tmp/...,,,...,,", argv=0x0) at execv.c:26
26 execv.c: No such file or directory.
gdb-peda$ ^Z
[1]+  Stopped                 gdb ./vonn
root@mankrik:~/trend/analysisdef/vonn# python vonnpatch.py 
[+] Starting program '/tmp/...,,,...,,.patched': Done
[*] Program '/tmp/...,,,...,,.patched' stopped with exit code 255
[+] Flag: TMCTF{ce5d8bb4d5efe86d25098bec300d6954}

Not too bad. A good 100 point challenge IMHO.

Monday, September 21, 2015

CSAW 2015 Quals - FTP (RE300) Challenge

It's been a long break between the last CTF I wrote up and now. To be honest there has been a bit of a lull in the number and quality of CTFs since earlier in the year. It seems they are starting up again though and that's great news.

This weekend our team took part in CSAW 2015 Qualifiers round and finished a respectable 124th or so out of 1000 or so teams by scoring 2,210 points. I'm happy with that but more happy with the way the team worked together. Great job guys.

Here is the clue for this writeup:

We found an ftp service, I'm sure there's some way to log on to it.

nc 54.175.183.202 12012

ftp_0319deb1c1c033af28613c57da686aa7

So a remote server listening on port 12012 and a file called "ftp", we perform the usual first steps. Download the file, use "file" command, use "strings" to check for simple flags.

We found it was a Linux ELF binary for 64bit architectures. Given this is a reverse engineering challenge I placed the file directly into IDA Pro to browse the disassembly while starting the process on a local system. It's written quite well (for a CTF program!) so it starts up first try and listens on port 12012 on localhost.

Upon connecting we see a welcome banner, I try the usual FTP commands such as USER and PASS and these work correctly. I don't have a valid username yet so let's check out those functions in IDA Pro to see how authentication works.

In IDA Pro I first open the Strings sub-view and find a nice easy string related to authentication. I found one called "Please send password for user". I double click this string and then follow the Data Xref to a function "sub_40159B".

This function seems to be taking the username and password, then validating them against stored constants. In the IDA Pro decompilation, the authentication mechanism looks like this:

if ( !strncmp(*(a1 + 32), "blankwall", 9uLL) && hash_password(*(a1 + 40), 0x40309CLL) == 0xD386D209 )
    {
      *(a1 + 1216) = 1;
      send_message(*a1, "logged in\n");
      dword_604408 = 102;
    }

So I've already renamed some of those functions in the snippet above, what it's doing is validating that your username is "blankwall" and your password hashed is equivalent to 0xD386D209.

Next I built a quick and dirty "FTP Client" in Python to send username "blankwall" with a password of "ABCDE" to see how the hash function deals with that. I located the hash function at sub_401540 and set a breakpoint there in GDB w/PEDA.

Note: You have to be careful when debugging this program as it fork's a new process ID for each connection, so I only attach the debugger after sending my username (but before sending my password).

Here's the code of my testing client:

#!/usr/bin/python

from pwn import *

target = "127.0.0.1"
password = "ABCDE"

conn=remote(target, 12012)
print "[+] " + conn.recvline()

print "[+] Sending user: blankwall"
conn.sendline("USER blankwall")
print "[+] " + conn.recvline()

raw_input("[+] Attach Debugger")

print "[+] Sending password: " + password
conn.sendline("PASS " + password)
print "[+] " + conn.recvline()

conn.close()

I found that this function performs what I later found to be called "multiplicative hash" on the user input. It also uses a modulus to prevent overflowing the integer. Since a modulus is used, reversing the algorithm to "decrypt" the stored password value of 0xD386D209 is not possible as information is destroyed during the hashing process.

So the only way to recover the password I thought was to recreate the algorithm exactly and then brute force inputs until my hashed output was 0xD386D209! In order to do that I found the decompiled version of the function to be a good starting point:


__int64 __fastcall hash_password(__int64 a1)
{
  int i; // [sp+10h] [bp-8h]@1
  int v3; // [sp+14h] [bp-4h]@1

  v3 = 5381;
  for ( i = 0; *(i + a1); ++i )
    v3 = 33 * v3 + *(i + a1);
  return v3;
}

However for me stepping through the binary in PEDA instruction by instruction gave an even simpler explanation of what the function was doing to the input as we can see the hash function building the output in the RAX/RCX registers. Here is the code I worked on. It prints the hash value after every calculation. This enables me to follow along with GDB to ensure my implementation of the multiplicative hash algorithm is correct.

#!/usr/bin/python

def enc(a1):
  start = 0x1505;
  print hex(start)
  op = start << 5
  print hex(op)
  op = op + start
  print hex(op)
  for i in a1:
        op = op + ord(i)
        print hex(op)
        saveop = op
        op = op << 5
        print hex(op)
        op = op + saveop
        print hex(op)

  op = op + 10
  print hex(op)
  return str(hex(op))[-8:]

print enc("ABCDE")


Which when run showed me the output step by step:

root@mankrik:~/csaw/ftp# ./enc1.py 
0x1505
0x2a0a0
0x2b5a5
0x2b5e6
0x56bcc0
0x5972a6
0x5972e8
0xb2e5d00
0xb87cfe8
0xb87d02b
0x170fa0560
0x17c81d58b
0x17c81d5cf
0x2f903ab9e0
0x310cbc8faf
0x310cbc8ff4
0x6219791fe80
0x652a44e8e74
0x652a44e8e7e
a44e8e7e

This final value agreed with GDB so I knew it was correct.

Once I had this function implemented in Python, I used the magic itertools Python module to calculate every combination of alphabetic characters. I tried 4,5 and finally 6 character passwords before finally striking gold and finding that the password "cookie" hashes successfully to 0xD386D209. I used this code for that:

#!/usr/bin/python

import itertools

target = "d386d209"

alphabet = list("abcdefghijklmnopqrstuvwxyz")

def enc(a1):
  start = 0x1505;
  op = start << 5 
  op = op + start
  for i in a1:
 op = op + ord(i)
        saveop = op
   op = op << 5
   op = op + saveop

  op = op + 10
  return str(hex(op))[-8:]

print "[+] Brute forcing hash: 0x" + target
for i in itertools.product(alphabet, repeat=6):
 encrypted = enc(i)
 if(encrypted == target):
  print "[+] Found it: " + "".join(i)
  break


Now we had the password I was able to login. After login though I was again stuck. We could use the LIST command to view the files stored on the FTP server but the usual FTP command "RETR" to retrieve files seemed to be bugged and kept throwing up "Invalid characters" error message.

Back to IDA Pro. At this point I spent an hour or so reversing the RETR command to find out what I needed to fix to get it to send me a file. To no avail really. Then I regrouped my thoughts and approached the problem from another angle. What if the solution was built into the FTP server itself.

A quick few minutes later and I identified the function "sub_4025F8" which seemed to only exist to give a flag called "re_solution.txt" to connected clients! Great! This function is only called from one place too. In fact it's only ever called in response to entering a simple command in the FTP server:



So back to the server we went, logged in with our found username, cracked password, and knowledge of the command, and we were rewarded with the flag!

Final exploit for this challenge follows:


#!/usr/bin/python

import itertools
from pwn import *

targethost = "54.175.183.202"

target = "d386d209"

alphabet = list("abcdefghijklmnopqrstuvwxyz")

def enc(a1):
  start = 0x1505;
  op = start << 5
  op = op + start
  for i in a1:
        op = op + ord(i)
        saveop = op
        op = op << 5
        op = op + saveop

  op = op + 10
  return str(hex(op))[-8:]

print "[+] Cracking password: 0x"+target

for i in itertools.product(alphabet, repeat=6):
        encrypted = enc(i)
        if(encrypted == target):
                print "[+] Found it: " + "".join(i)
                password = "".join(i)
                break


conn=remote(targethost, 12012)
print "[+] " + conn.recvline()
print "[+] Sending user: blankwall"
conn.sendline("USER blankwall")
print "[+] " + conn.recvline()
print "[+] Sending password: " + password
conn.sendline("PASS " + password)
print "[+] " + conn.recvline()
print "[+] Send RDF"
conn.sendline("RDF")
print "[+] Flag: " + conn.recvline()
conn.close()

Easy game!