Stage 1
I
usually start my analysis by looking at the strings in a binary as it
gives a good 'feel' for the nature of the target, as well as hinting
to any open source code that the author used which can make reversing
easier. Running it through FLOSS (FireEye's version of
Strings-on-steroids) gives us lots of helpful looking Python-related
error messages such as "Error detected starting Python VM”
- suggesting that at this stage we are dealing with a python program
packed as windows executable.
These
are usually easy to unpack if we can find out which packer was used
-the top Google results confirm that the error message above is from
'PyInstaller'. It's documentation describes PyInstaller binaries as a
bootloader with an archive appended to the end (start bytes PYZ),
which you can unpack with the PyInstaller archive viewer. I found a
tool called 'pyinstxtractor' which did the work for me, and it
produced the following output:
The
'another' file immediately jumps out at the most promising location
for the target code, with its odd name and exe manifest and sure
enough a string search of it shows all the strings about secrets and
flags that we see when the program runs. But what to do with it? I
assumed it was python bytecode but running the 'uncompyle6'
decompiler on it just gave me an error about it not being a .py or
.pyc file.
Lets
look at the magic bytes at the start of the file (in Binary Ninja):
63
00 00 00? What? I couldn't find it on any list of file signatures.
Looking at more search results for “Decompiling Pyinstaller
Binaries” and what do we see 5 results down? Hasherezades blog...
solving a variation of this exact problem. I'm not sure this way of
solving it is really in the spirit of the challenge, but that's what
knowledge sharing is for.
Anyway,
now I know that the file is
just a .pyc file with the signature irritatingly stripped off –
it's time to fix that (typing “03 f3 0d 0a 00 00 00 00” in Binary
Ninjas hex mode) and start decompiling.
With
the python code in our hands we can now tackle the credential check.
Check_login()
just checks that we use the name 'hackerman'
and check_password() succeeds if the md5 hash of our password is
'42f749ade7f9e195bf475f37a44cafcb'. According to the md5 lookup page
at https://hashkiller.co.uk –
that password is 'Password123'.
Hmm.
The
final gate at this stage is a PIN check. The PIN is used as a seed
for the random number generator, then create_url_key() appends 32
“random” decimal digits into a key. This PIN-derived key is
compared in a similar way to the password, with its md5 hash having
to match 'fb4b322c518e9f6a52af906e32aee955'.
The
problem with PIN's (or PIN numbers if you like redundant acronyms) is
that they only work if you can't brute force them – but since we
have the hash and the hash-checking algorithm we can do:
for i in range(10000):
key = get_url_key(int(i))
if check_key(key):
print "Correct key is: %d"%i
break
And instantly get a result of 9667. Note to self: Do PIN searches in reverse.
Stage 2
Using
the credentials above will drop us down to decode_and_fetch_url(),
which uses the key to decrypt an embedded string to:
“https://i.imgur.com/dTHXed7.png“.
I'm
always slightly worried about what I'm going to see when opening
images embedded in suspicious binaries but this turned out to be a
corrupt-looking png.
I
assumed it was going to be a steganography challenge, but looking
further at the Python code we see it being fed to prepare_stage(),
where the pixel values are converted to a PE file by
get_encoded_data(),
moved into executable memory with the help of VirtualAlloc and
executed from offset +2.
I
had some trouble running it from a decompiled python script as it
kept crashing but stepping through it with a debugger seemed to fix
it, oddly. I also dumped the payload to a file to run it without
having to wait for an imgur download each time.
We
can debug this payload by waiting until the “Are you ready?”
dialog pops up and setting a breakpoint at VirtualAlloc()
to find the buffer it gets placed in, but lets use static analysis as
far as possible.
Off
to our disassembler – since this isn't executed as by the normal PE
loader mechanism we have to switch BinaryNinja to 'Raw View' so it
doesn't try to impose PE section mappings on our disassembly.
(virtual_buf
+ 2) :
Above:
The first two instructions use the call-pop technique to determine
the memory location of the code, then adds 0x6d9 to it (destination
+0x6e0) and then “returns” there.
The
next section checks for valid PE/MZ signature bytes and retrieves the
process environment block. It grabs the InMemoryOrderModuleList
from the PEB and iterates through the module names with a simple
hashing/checksum algorithm until it finds the entry for kernel32.dll.
It
then uses the same technique to iterate through the symbols to find
LoadLibraryA,
GetProcAddress and VirtualAlloc (if you
Google the constants you will find the loader was written by a Petya
fan). The payload now has what it needs to load its plaintext list of
imports at 0x302da.
Loading
done, it creates a thread which calls EnumWindows every second. I'll
switch back into PE mode and examine the callback.
After
sending a WM_GETTEXT command to read the title text of each window,
it searches for 'Notepad' and 'secret_console'. If a window with a
suitable title exists it will enter a 'waiting for the command' loop
which uses EnumChildWindows to look for text in its widgets in the
same way – this time for 'dump_the_key'.
Once
a window satisfying all these conditions has been found, it decrypts
the blob of data at payload offset 0x30A00 . I'm not sure if
the algorithm is a well known one but it's quite simple –
though understanding it isn't required to get the flag I'll cover how
it works:
It
uses the RC4 key expansion algorithm to turn “dump_the_key” into
an S-Box, however using the RC4 keystream generator only recovers the
first byte.
The
following python will decrypt the embedded blob:
#standard RC4 key expansion - https://gist.github.com/cdleary/188393
def expandkey(key):
keybytes = [ord(x) for x in key]
k = [x for x in range(256)]
j = 0
for i in range(256):
j = (j +k[i]+keybytes[i % len(keybytes)]) % 256
k[i], k[j] = k[j], k[i]
return k
def decrypt(ciphertext, key):
plainText = ""
keyIndex = previous_keyIndex = tempVar = 0
SBOX = expandkey(key)
for ctIdx in range(len(ciphertext)):
keyByte1 = SBOX[(keyIndex+1) & 0xff]
keyIndex = (keyByte1 + tempVar) & 0xff
keyByte2 = SBOX[keyIndex]
keyStreamKeyIndex = (keyByte2+keyByte1)&0xff
plainText += chr(SBOX[keyStreamKeyIndex] ^ ciphertext[ctIdx])
tempVar = previous_keyIndex + 1
previous_keyIndex = keyIndex
return plainText
cipherTextBytesList = [0x4f, 0x08, …, 0x75, 0x98]
key = “dump_the_key”
plaintext = decrypt(cipherTextBytesList, key)
After
decryption, Actxprxy.dll is loaded. This seemed odd at first glance -
an Internet explorer activeX controls library? (So much badness)
- but the first page is actually marked READ_WRITE and used as a
covert channel to pass our 'plaintext' back to the python script,
where it is Base64Decoded and decompressed. The python script can be
modified to print it out for us to play with if dumping and
decrypting it is too much work (and it is).
Stage 3
Now
the script tells us that we are awesome - but not awesome enough to
get the flag. We need to guess a colour first. The three RGB colour
bytes are the key - each key byte taking turns being XOR'ed with a
ciphertext byte.
Some
observations about the ciphertext
- Exec() is called on it, so it should be valid python
- It might have “flag{“ in it
- It looks quite interesting!
At
the top we see what looks like a comma separated Python tuple and at
the bottom we can make out a garbled but suspiciously flag-like
“æláç{“ --------- “}” structure. In fact the printable
ASCII bytes are separated in intervals of three, suggesting that one
of the three key bytes is zero. As
it has 'l' and '{' in the right place, i'm certain that 'æláç{'
is 'flag{'.
This
string is at offset 1065 in the ciphertext so the correct key offset
must be (1065 % 3) -> 0. We can use a known-plaintext attack –
xoring the ciphertext with the plaintext in a python console to
recover the key:
>>> [ord(“æláç{“[i]) ^ ord(“flag{“[i]) for i in range(3)]
[128, 0, 128]
Using
this key to decipher the rest of the blob gives us some python code:
def print_flag():
flag_hex = ( 0x73, 0x75, 0x72, 0x64, 0x65, 0x61, 0x68, 0x50, 0x20, 0x2D, 0x20, 0x22,0x2E, 0x6E,
0x65, 0x64, 0x64, 0x69, 0x68, 0x20, 0x79, 0x6C, 0x6C, 0x75, 0x66, 0x65, 0x72, 0x61, 0x63, 0x20,
0x6E, 0x65, 0x65, 0x62, 0x20, 0x73, 0x61, 0x68, 0x20, 0x74, 0x61, 0x68, 0x77, 0x20, 0x73, 0x65,
0x76, 0x69, 0x65, 0x63, 0x72, 0x65, 0x70, 0x20, 0x77, 0x65, 0x66, 0x20, 0x61, 0x20, 0x66, 0x6F,
0x20, 0x65, 0x63, 0x6E, 0x65, 0x67, 0x69, 0x6C, 0x6C, 0x65, 0x74, 0x6E, 0x69, 0x20, 0x65, 0x68,
0x74, 0x20, 0x3B, 0x79, 0x6E, 0x61, 0x6D, 0x20, 0x73, 0x65, 0x76, 0x69, 0x65, 0x63, 0x65, 0x64,
0x20, 0x65, 0x63, 0x6E, 0x61, 0x72, 0x61, 0x65, 0x70, 0x70, 0x61, 0x20, 0x74, 0x73, 0x72, 0x69,
0x66, 0x20, 0x65, 0x68, 0x74, 0x20, 0x3B, 0x6D, 0x65, 0x65, 0x73, 0x20, 0x79, 0x65, 0x68, 0x74,
0x20, 0x74, 0x61, 0x68, 0x77, 0x20, 0x73, 0x79, 0x61, 0x77, 0x6C, 0x61, 0x20, 0x74, 0x6F, 0x6E,
0x20,0x65, 0x72, 0x61, 0x20, 0x73, 0x67, 0x6E, 0x69, 0x68, 0x54, 0x22 )
flag_str = ""
for i in flag_hex:
flag_str = chr(i) + flag_str
init()
print(Style.BRIGHT + Back.MAGENTA) + "flag{" + flag_str + "}" + (Style.RESET_ALL)
print_flag()
Running it yields our flag -
flag{"Things
are not always what they seem; the first appearance deceives many;
the intelligence of a few perceives what has been carefully hidden."
- Phaedrus}