These are my (rather long) solutions to Fireeye's FLARE challenge. This is just not the solution but other ways that I tried. This was a great learning experience for me so I am writing this post to document everything I tried. As a result, this post is somewhat long.
If you have any feedback, please let me know. I spent a lot of time on this writeup and I am always happy to learn new stuff. My email and twitter handle are in the sidebar.
I am a bit late to the party. There were two are now other three solutions posted (that I know of). Check them out.
- Detailed Solutions to FireEye FLARE Challenge
- A Walk through for FLARE RE Challenges
- The FLARE On Challenge Solutions by Fireye
Links to Individual Challenges
This post is quite long (I didn't want to strip them into different posts), use the following links to jump to any specific challenge:
My Setup
I used a Windows XP SP3 Virtual Machine for most challenges using VirtualBox. For challenge 6 I used a Kali 64-bit VM. I used IDA/Immunity on my host OS with some other utilities.
Helpful Tools
- 7-zip
- PE-Studio: Gain information about the binary without running it. It also sends a hash (MD5 I think) of the file to Virustotal so if you want to keep your samples secret, don't give it internet access
- dotPeek: Free .NET decompiler by JetBrains
- .NET Reflector: .NET decompiler. Not free but comes with a 2-week trial period
- HxD: Free Windows hex editor
- Notepad++: Slick FOSS text-editor
- Immunity Debugger: Windows debugger. Very similar to OllyDbg
- pyew: A Python tool for static malware analysis. I used it for PDF analysis
- IDA: What can I say? It's great but also costs an arm and a leg. Except challenge 6, the trial and free version are enough for us
- Bless: Linux Hex editor
- API Monitor: Free utility to monitor API calls in Windows. It can monitor calls for standard windows APIs or we can add application-specific Dlls and monitor them
- Wireshark: FOSS network monitoring/capturing tool. Needs administrator access on Windows to install libpcap
- Microsoft Network Monitor: Microsoft network monitoring/capturing tool. Does not need administrator access. Replaced by Microsoft Message Analyzer
Challenge 1 - Bob Roge
The challenge starts with going to their website at http://flare-on.com and downloading a binary. The binary is a self-extracting zip file which is supposed to show you the challenge EULA. It didn't work on my VM.
I opened it with 7-zip
to get Challenge1.exe
. By dropping it into PE-Studio
I gained more information:
The Image is a fake Microsoft executable # Company name is Microsoft but it is not signed?
The Manifest Identity name (MyApplication.app) is different than the Image name
The Version Information 'OriginalFilename' (rev_challenge_1.exe) is different than the Image name
The Debug Symbol File Name () is different than the Image name (challenge1)
The image is Managed (.NET)
So it appears to be a .Net binary. Let's run it.
Hey I love this guy. Let's press DECODE.
Look at that garbled data. We can decompile it (remember it's a .Net binary). Using dotPeek
we can see the code for Decode
button:
|
|
Line 4 reads dat_secret
and the rest of the function manipulates it before displaying it on the form. To save this file expand resources
and select rev_challenge_1.dat_secret.encode
. Right click and select Save Resource to File.
I used HxD
to look at the contents.
|
|
Let's run the code with dat_secret
and print the result after each level (i.e. str2, str3 and str4
). One option is to use the provided C# code. I re-wrote the code in Python and ran it online using repl.it. Str1 is the answer so we don't care about the rest:
|
|
Level 1 flag: 3rmahg3rd.b0b.d0ge@flare-on.com
Challenge 2 - A Study in JavaScript
Well done! Looks like you kicked that one. I've attached the next challenge for your reversing pleasure. The password to this zip archive is "malware".
We saw what looked like attacker activity to this site, can you figure out what the attackers changed?
Hopefully you'll knock this one out too, Good luck!
-FLARE
Inside the archive seems to be a copy of the original http://flare-on.com with a launch date countdown timer. I will be calling the html page from the website original_html
and the one in the zip file challenge_html
.
|
|
The original web page looks a bit different.1
2
3
4
5
-rwx------+ 1 TyRaX None 6254 The FLARE On Challenge.htm
and
-rwx------+ 1 TyRaX None 116290 bootstrap.css
-rwx------+ 1 TyRaX None 6596 flare-on-V2.png
The timer threw me off track. Is it really a countdown timer? When does it reach zero?
I changed the time in my VM to mess with it but it synced up with host.
|
|
Changing the time did not mess with anything.
We can diff the htmls or use Notepad++'s compare plugin.
Most differences are aesthetic. There are two interesting differences. In line 54, original_html
has <img src="The%20FLARE%20On%20Challenge_files/flare-on-V2.png">
while challenge_html
includes <img src="img/flare-on.png">
. So the file in the website is version 2 of the image. Later in the challenge_html
we see more evidence of this image file <?php include "img/flare-on-V3.png" ?>
. But wait a minute, the filesize of these images were different:
|
|
The challenge png is bigger. I used HxD
to compare these two files (as they are not text) and at the end of flare-on.png
I saw some PHP code. To be honest I was thinking of steganography or some Ange Albertini magic. But that would have been too hard for level 2. Here is the PHP code (beautified):
|
|
Find an online tool to run this PHP code or re-write it in Python . My Python code:
|
|
Produces the following PHP code:1
2
3
4
$_= 'aWYoaXNzZXQoJF9QT1NUWyJcOTdcNDlcNDlcNjhceDRGXDg0XDExNlx4NjhcOTdceDc0XHg0NFx4NEZceDU0XHg2QVw5N1x4NzZceDYxXHgzNVx4NjNceDcyXDk3XHg3MFx4NDFcODRceDY2XHg2Q1w5N1x4NzJceDY1XHg0NFw2NVx4NTNcNzJcMTExXDExMFw2OFw3OVw4NFw5OVx4NkZceDZEIl0pKSB7IGV2YWwoYmFzZTY0X2RlY29kZSgkX1BPU1RbIlw5N1w0OVx4MzFcNjhceDRGXHg1NFwxMTZcMTA0XHg2MVwxMTZceDQ0XDc5XHg1NFwxMDZcOTdcMTE4XDk3XDUzXHg2M1wxMTRceDYxXHg3MFw2NVw4NFwxMDJceDZDXHg2MVwxMTRcMTAxXHg0NFw2NVx4NTNcNzJcMTExXHg2RVx4NDRceDRGXDg0XDk5XHg2Rlx4NkQiXSkpOyB9';
$__='JGNvZGU9YmFzZTY0X2RlY29kZSgkXyk7ZXZhbCgkY29kZSk7';
$___="\x62\141\x73\145\x36\64\x5f\144\x65\143\x6f\144\x65"; // base64_decode
eval($___($__));
Contents of $_
and $__
are clearly encoded in base64
and $___
is base64_decode
. Base64 can be decoded in Python by calling base64.b64decode
.
Line #4 can be re-written as
|
|
So it must decode the first base64 blob and eval it. Let's decode it:
|
|
This looks like a POST request. The characters look like a mix of ASCII and Hex values. Let's print them using Python and hope this is the last encoding:
|
|
Fortunately, we are done.
Level 2 flag: a11DOTthatDOTjava5crapATflareDASHonDOTcom or a11.that.java5crap@flare-on.com
Challenge 3 - Cheating My Way to the Top
Nice job, you're really knocking these out! Here's the next binary. The password to the zip archive is "malware" again.
Keep up the good work, and good luck!
-FLARE
Challenge 3 is a Win32 binary called such_evil
. PE-Studio
does not tell us much.
Running it will result in this message:
I cheated in this challenge. I just dropped the executable in Immunity Debugger
, ran it and looked in memory when the message box popped up and the email was there:
Level 3 flag: such.5h311010101@flare-on.com
Challenge 4 - Things are Getting Cereal
Well done! Such dedication, much work, wow.
Here's the next challenge, password is the same as last time. We'll talk more when you figure it out.
-FLARE
It's a two page PDF named APT9001.pdf
. First page is a picture of APT1 report and second page is empty.
We can just open the PDF in a HxD
but it won't tell us much.
There are tools that will help us parse the PDF. I used pyew
. You can find a good tutorial for PDF analysis here.
Let's follow the tutorial:
|
|
Seems like streams 1,2 and 3 are interesting. According to the tutorial pdfvi
displays them.
- FlateDecode: Decompress. In Python do
zlib.decompress
- ASCIIHexDecode: Decode from ASCII Hex
- JBIG2Decode: Decode as a black and white image
What really threw me off was the JBIG2Decode
decoder for stream 2. There was a vulnerability associated with it. It is too short to be the email (14 bytes). It is not compressed (lacks the magic headers). Pyew
also displays the disassembly but it is not shellcode either (if it is, then I didn't recognize it). It is also not an image (hence the JBIG2Decode
filter).
|
|
Let's take a look at stream 1 using pdfvi
.
|
|
Obfuscated JavaScript. I executed it and printed the last variable, but the result was garbage. The code just does a lot of computatation. However variable IxTUQnOvHg
looks suspicious. A large number of bytes are unescaped. After reading some guides, I found out how to decode this. %u72f9
should be converted to 0xf972
. I wrote a simple Python program to do this decoding. Read 6 characters, discard the first two (%u), swap characters 3 and 4 with 5 and 6. The end result is some shellcode. I used this website to convert it to an executable: http://sandsprite.com/shellcode_2_exe.php.
After running the executable in Immunity debugger a message box pops up with an encoded message. If we look inside memory, we can find this string:
|
|
The length is close to the email (29 bytes). Here's what I thought. If it is the email then the last 13 bytes should be @flare-on.com
. It's probably xor-ed with a key. If the key is smaller than 13 bytes then it is repeated and we can easily find it. How? xor is transitive. If plaintext xor key = ciphertext
then key = plaintext xor ciphertext
. If we xor the last 13 bytes of ciphertext with @flare-on.com
then we will find the last 13 bytes of the key. If key is smaller than plain/ciphertext (if key is as long as plain/ciphertext then we will have a one time pad
) it is repeated.
The following Python code does it. On a side note, we really need a string xor operator in Python. I wrote one which is probably not that good.
|
|
Nope. Doesn't look like it.
I usually wander around in the debugger and look at memory. Run the executable in Immunity and look around in memory after the message box pops up. A little bit further up from the original message we see more OWNED!!!
strings (title of the message box). Right before two owneds I saw another string. This one is longer and looks more promising. Right click on it and select Follow in Dump
. Select the string and again right click and select Copy > To clipboard
. It's in Unicode so 5
represented as 0x0035
instead of 0x35
.
|
|
Let's apply the xor-logic on this string too.
|
|
Bingo. The key is BEEF
. It is also in the initial shellcode as a string. Let's xor it with the complete string and get the flag.
Level 4 flag: wa1ch.d3m.spl01ts@flare-on.com
Challenge 5 - 5get About It
Another one bites the dust!
Here's some more fun for you, password is the same as always.
-FLARE
Be sure to run this challenge in a VM.
The file inside the challenge zip is named 5get_it
and is around 100KBs. A quick look with HxD
says it's a Portable Executable (MZ and PE magic bytes). Let's get some help from PE-Studio
. It has a VirusTotal score of 29/55 with most AVs calling it a generic trojan or keylogger. Click on Imported Symbols
and look at the red symbols. Some of them are more interesting than others. To get more information about any of them, right click and select Query MSDN
inside PE-Studio (handy, neh?).
|
|
Also, let's run strings
on it. I used cygwin. I have omitted the garbage and only kept the interesting strings:
|
|
While doing the challenge only the first and last two lines were interesting to me.
- It references
c:\windows\system32\svchost.dll
andsvchost.log
but there is no such file (Windows hassvchost.exe
in that location). - There is also
c:\windows\system32\rundll32.exe c:\windows\system32\svchost.dll
which means this file is most probably a DLL and should be executed like that. There are no parameters, so whatever this DLL is doing should be inDllMain
.
By this time you probably know what this file is supposed to do (also look at the registry key). However, at that time I did not make the connection :(
Let's drop this into IDA
and jump into DllMain. I used IDA Pro but both IDA free and trial and Immunity Debugger work for this challenge (and also challenge 7). Put a breakpoint at the start of this function (F2
key).
If we attempt to execute the tile. IDA will complain. It's a DLL and cannot be run by itself. But we already know how to run it thanks to the strings inside the binary. In IDA first select Local Win32 Debugger
then go to Debugger
menu and select Process Options
. In the Application
textbox enter c:\windows\system32\rundll32.exe
. In Parameters
enter the path to the DLL. Don't forget to rename the file, add dll extension and include double-quotes around the path if it contains spaces (e.g. "c:\Flare Challenges\Ch5\5get_it.dll",0
). It didn't work without the dll extension for me.
Let's start debugging. We observe standard stuff until we reach .text:1000A6BB call sub_1000A570
.
Inside the function we encounter RegOpenKeyEx that opens a registry key. Full registry key is a combination of hKey
and lpSubKey
. hKey
can be one of the predefined keys. The constants for the predefined keys needed a bit of googling because the MSDN page didn't list them. Here they are:
.
| Key | Constant |
|---------------------|----------|
| HKEY_CLASSES_ROOT | 0 |
| HKEY_CURRENT_USER | 1 |
| HKEY_LOCAL_MACHINE | 2 |
| HKEY_USERS | 3 |
| HKEY_CURRENT_CONFIG | 5 |
.
The binary is pushing 0x02
for hKey
and SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run
for lpSubKey
which will result in the full path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
. If function succeeds it will return ERROR_SUCCESS
which is 0 according to this page, otherwise it will return another error code.
The binary will check if it has access to registry at that path. If so then the return value (in eax) will be 0 and it will jump right (JZ will succeed).
RegQueryValueEx checks if there is a registry key at an open path. It is looking for a registry key named svchost
at that path. If such key exists, function will return 0. In this case, it returned 2 which stands for ERROR_FILE_NOT_FOUND
meaning there was no such key. Then it will call RegCloseKey and closes the open registry path. This function's return value is saved in var_110
(we will need it later):
.
| Condition | Return Value |
|------------------------------|-------------------------------|
|Registry key cannot be opened | 1 |
|Registry key does not exist | 2 |
|Registry key exists | 1000A6BB or DllMain(x,x,x)+3B |
.
After that function, we see that it is calling GetModuleHandleEx
for sub_1000A610
in lines 3-8 and checks the return value in line 9. The return value for GetModuleHandleEx will be non-zero, otherwise it will be zero. If call was not successful then last error will be printed to file.
|
|
If GetModuleHandleEx
was successful it will land here. GetModuleFileName is called which will return the full path for the specified module in hModule
. In this case, the binary retrieves its own path (line 9) and saves it in [ebp+Filename]
. In line 10, return value of sub_1000A570
is compared with 2.
|
|
If registry key did not exist, we will continue.
|
|
We have already seen the strings being loaded in lines 1 and 2. Then CopyFile
is called to copy itself to c:\\windows\\system32\\svchost.dll
.
|
|
Line 9 pushes c:\windows\system32\rundll32.exe c:\windows\system32\svchost.dll
to the stack and calls sub_1000A610
in line 10. Based on this string and checking for existence of the registry key we can guess what is going to happen in this function.
Inside this function we see that RegCreateKey to open HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
. If the key does not exist, it will create it.
If call was successful, execution continues to line 14. It is adding a new registry key named svchost
to that path with the specified value. Then function will return with the result value of RegSetValueEx. If it was successful, it will be 0.
The Dll copied itself to system32 and it will run every time Windows starts.
|
|
After we return from sub_1000A610
, we land in line 5. Return value will be saved in var_114
(0 is key was created). If we highlight this variable and press x
in IDA to get external references (meaning where else this variable is being referenced and manipulated. It is not referenced anymore so we do not care about it. In line 8, a new function is called sub_1000A4C0
. Let's go inside.
Inside sub_1000A4C0
we can see that the jump to return is never taken. Because eax is set to 1 and then checked for being zero and if zero the function will return. So let's look at the other branch.
|
|
Line 1 calls rand
and the result is modified a few times by doing some calculations in lines 3-8. In line 9, it is pushed to stack as argument for malloc
. So a random number of bytes are allocated. Seems like it is allocating an array of type size_t
. This is reinforced because the number is multiplied by 16 (size of size_t) in line 8 before being pushed to the stack. After the malloc
, the pointer to the allocated memory is stored in var_C
. In lines 13-19 we see that this array is reset to zero by memset
. Line 22 calls sleep with 10 miliseconds. Last line compares the calculated size of array with 0 and if so then no memory was allocated and program jumps back to the start of the function and tries to allocate memory and initialize memory again. If memory was allocated we continue to line 32 sleep for 10 miliseconds and call sub_10009EB0
.
|
|
This is what we are looking for. First var_4
is set to 0 in line 10, then it is compared with 222 in lines 15-16 . If it is larger, we jump to loc_1000A3A4
.
If not we will reach line 18 where var_4
(initially 0) is stored in eax and pushed to stack as parameter for GetAsyncKeyState
. We already know what this function does. If vKey
has been pressed since last call to GetAsyncKeyState
, it will return a value. Otherwise it will return 0. This forum thread from 2007 discusses this usecase.
If the key was not pressed, we jump to line 26 and then 29 where var_4
is increased by 1. Then we go back to line 14 where var_4
is compared with 222 and the cycle is repeated.
Now we know that the application loops through ascii characters from 0 to 222 checking to see if a key was pressed. If so we will not jump at line 23 and continue. Let's take a look at that.
This code is a series of cases for a switch statement (as IDA has detected). It checks what key was pressed performs specific actions for each key (taking the red arrows). It checks from 0x27
to 0x60
. By looking at an ASCII table, we can see that the application checks for some special characters, number and letters. I am not going to describe what each one does but I went through each function and looked at the code. Most of them were the same and looked uninteresting but the function for M
or 0x4D
caught my eye. Finding the code for M
and clicking on the red arrow besides it.
|
|
What is sub_10009AF0
?
Nice, IDA has even tagged it as M for us. First we see that a dword_10017000
is compared to 0. and if it is larger than 0, two functions are called: __cfltcvt_init
and sub_10001240
. Then returns with value m
.
__cfltcvt_init
sets one variable to 1 and resets the rest (including dword_10017000
).
sub_10001240
creates a large array, initializes it with some values and then calles GetWindowLong
and DialogBoxIndirectParam
. I put a breakpoint in the end. Change the IP to the start of this function and ran the program.
Nice! So to get this ASCII art we have to press M and dword_10017000
needs to be 0. Let's get back to sub_10009AF0
and investigate dword_10017000
.
Highlight dword_10017000
and press x
in IDA to see where this variable is being set to 1 (which will make the if true). There is only one place.
Notice the o
? Now see that variable dword_100194F8
needs to be 1 to reach this line (top right). Follow that using x
.
So we have m
and then o
. If we follow the chain and then reverse it, we have the flag. The binary is a keylogger, it saves all keystrokes to svchost.log
.
Level 5 flag: l0gging.Ur.5tr0ke5@flare-on.com
Challenge 6 - IDA Appreciation Day
Great success!
We've got another evil one for you, see if you can figure this out. This one will be rougher. Good luck!
-FLARE
While I was writing this solution, I saw this alternative way of solving the challenge. Great read: Solving FireEye's Flare On Six via Side Channels.
New binary. Named e7bc5d2c0cf4480348f5504196561297
. Let's google it and first result is interesting. Filename has the exe
extension but it is a 64-bit ELF executable. Opening the file in HxD
shows us the ELF magic bytes.
|
|
I started a Kali 64-bit VM in VirtualBox. Less mess with it a bit. I used IDA Remote Linux Debugger. IDA was running on my host OS and the binary was in the Kali 64-bit VM.
|
|
I did not try executing the binary with different number of arguments at the start. I tried different argument lengths, really long arguments (e.g. 'A'*40000). In the end I decided that two arguments was the correct way to run the binary. While debugging I realized that the binary crashes with a segfault message. While it is fine without the debugging. So some anti-debugging protections must be at work. We ran ltrace
and didn't see any shared library calls. Let's run strace
to get system calls.
|
|
Syscalls are similar in all traces except with two arguments. We can see that ptrace
is being called in line 37. It's a common anti-debug protection in Linux. "[a]n executable can only call ptrace once. if ptrace() was already called by the strace executable, we can detect it in runtime." So we need to bypass ptrace
. Searching for ptrace
in IDA does not turn up anything. I learned that syscalls are not called that way by name (he he). The argument for syscall
is moved to eax
and then it is called. So I search for the text syscall
in IDA and then commented each call according to Linux System Call Table for x86_64 by @pixnbits
. ptrace
is 0x65
:
Later I realized there was a much easier way to find it instead of discovering all calls. Running strace
with -i
switch will print the instruction pointer at the time of call after the syscall returns. Let's run ptrace
on the binary with two arguments with this new swtich and look at the results.
|
|
Look at IP at the time of ptrace
in line 9: 47431b
. Now look at the IDA screenshot above.
So this function calls ptrace
. To find out where this function is being called, highlight it and press x
in IDA. There is only one call.
|
|
Return value from ptrace
is manipulated and then checked to see if it is zero. If non-zero, the program continues to line 11 and prints the segfault message in line 12 (I have renamed it). As you have noticed I have enabled opcodes in the last code snippet. In IDA go to the Option
menu and then General
. Change the number of opcode bytes
.
To patch the binary to bypass ptrace we need to change the jz
instruction in line 9 to jmp
. In this short jump 0x74
stands for jnz
and 0x14
means thee number of bytes to jump (in this case 14 bytes ahead). To patch it to jmp
, change 0x74
to 0xEB
. Open the binary in a hex editor (e.g. Bless). Now we need to find this offset. I do what I call lazy patching
. Search for opcodes for the last few instructions before and jnz
in hex editor. In this case we are looking for 48 C1 E8 3F 84 C0 74 14
. There is probably only one place in the binary with this sequence of bytes. Find it and change 0x74
to 0xEB
. Now we have bypassed ptrace
. Another alternative is to replace the call calls_ptrace
in line 6 with NOPs. NOP is short for No Operation and has the opcode 0x90
. It actually stands for xchg eax, eax
. Both of them work.
So I bypassed ptrace
. There were a lot of calculations. Random strings were loaded and manipulated. After stepping around the code in IDA I gave up. At this point I had two leads:
- The binary prints
no
. Put breakpoints on allsys_write
calls and trace the print back - The application needs to manipulate the arguments somehow. Search for string instructions, breakpoint them and see if we hit one
I chose option 2, searched for string instructions and assigned breakpoints. After running the program I hit a repne scasb
.
What does repne scasb
do?repne scasb
will scan the string in di/edi/rdi
for the byte (scasb
is the byte version of scas
instruction) in ax/eax/rax
and decrease cx/ecx/rcx
by one after each execution. It stops if cx/ecx/rcx
reaches zero or if a match is found.
|
|
Null terminator or 0x00
is saved in eax in line 6. Line 7 has rcx
. We don't want rcx
to reach zero before the end of the string. First argument is saved in rdi
in line 8 and finally line 9 calls repne scasb
. This is basically strlen(arg1)
. In line 14, it is checked if the length of first argument is 10. If so we will jump.
|
|
We can see that arg1 is saved to rdi
in line 5 and sub_468BB0
is called. We can get inside sub_468BB0
but it is basically malloc
. It allocates a string and initializes it with first argument. Return value is in rax
which is a pointer to the newly created string. It is saved to [rbp+arg1_2]
(I have renamed the variables). Finally there is an unconditional jump.
|
|
We see another repne scasb
. We have seen these instructions before. At the end of the code snippet, we go back to the top (notice the offsets for first and last line). This code loops through first argument and xors it with 0x56
.
|
|
If the loop is done, the jnz
in line 19 will not be triggered and we land somewhere else.
|
|
Application loads the string bngcg`debd
and compares the result of arg1 xor 0x56
with it. If both are equal, jz
in line 7 will be taken.
We have already seen the transitive property of xor so we can calculate the correct value of first argument which is 4815162342
. We could also patch the jz
to jmp
and enter any 10 characters for argument one.
|
|
Now it gets a bit hazy and very painful. There are tons of loops and function calls. Some random strings are loaded in different functions and not used for anything. I started to see patterns such as this instruction mov cs:byte_729AC2, al
. At that address, there are bytes being written and they are in base64
. I was stepping through until suddenly everything stopped and I saw that a nanosleep
syscall was executed.
I patched it and continued. Application crashed a few times in between and I had to get back to my latest breakpoint. I got into the habit of copying the base64 bytes and setting up breakpoints every once in a while to get back to a checkpoint after each crash. Finally all the bytes were written and sub_401164 was called. This function decodes the bytes from base64 (although I though it is a different implementation and stepped through it for an hour before realizing that it is just a standard decoder).
|
|
This is obviously shellcode, pushed to the stack and called. Bytes of the second argument are manipulated and then compared with some hardcoded value. I have only included the first 2 bytes here. For example arg2[0] ror 0xF2 must equal 0x1B
, otherwise jz
will be called and application will terminate in loc_7FFF3A5AC3A6
. I saw around 30 checks meaning that argument 2 must be 30 bytes or so. I wrote the following Python code to calculate the second argument.
Python does not have ror
and rol
binary operators so I stole them from here.
|
|
Thanks @Wartortell.
Level 6 flag: l1nhax.hurt.u5.a1l@flare-on.com
Challenge 7 - The Doge Strikes Back
Alright! Last one, can you get to the finish line? Keep it up!
-FLARE
By this time we have already fallen into a pre-check routine. Filename is d69650fa6d4825ec2ddeecdc6a92228d
(MD5 hash) and googling brings up no notable results.
PE-Studio stuff:
- Win32 executable
- VirusTotal score: 5 / 55
- Imported libraries: ws2_32.dll, kernel32.dll and wininet.dll.
wininet.dll
is for the interwebz - Imported symbols: Lots of them. Functions for creating network sockets, hostname lookups, creating, reading and writing files and general anti-debug/anti-vm stuff
- Strings: Not as many strings as challenge 6. cmd.exe and 127.0.0.1 look interesting
I used API Monitor
to observe application's API calls. It crashed after a while and API Monitor flagged 230k calls. Sifting through them is not practical but a lot of them are redundant and do not look interesting. For example there are a lot of LocalAlloc
and LocalFree
calls. Right click any call and select Exclude > API Name
to filter it. After excluding a lot of stuff, there was still so much crap. So instead I tried to look at API calls to certain Dlls for example wininet.dll
. Under Monitored Processes
navigate to Modules
and then select a specific Dll to only see its calls. Let's search for specific calls that we noticed in PE-Studio. API Monitor also supports searching in MSDN. Double click a call or right click and select Online Help (MSDN)
.
I searched for gethostbyname and found some interesting results:
|
|
I was curious about these connections so I captured the traffic using Wireshark
from launch to crash.
|
|
Query for www.dogecoin.com
, e.root-servers.net
and www.twitter.com
. Then TLS handshake in lines 7-11 and finally a request to twitter (line 12) and reply (lines 13-14). Let's search for "twitter" in API Monitor and we see this InternetOpenUrlW ( 0x00cc0004, "https://twitter.com/FireEye/status/484033515538116608", NULL, 0, INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_PRAGMA_NOCACHE, 0 )
. Let's find that tweet and it looks normal.
Because this challenge employs a good number of Anti-Debug/Anti-VM protections, I will try to explain what I learned at each stage. Even after finishing the challenge I went back and looked at some steps again to learn more.
Here are some useful resources:
The "Ultimate" Anti-Debugging Reference (PDF) by
Peter Ferrie
. I had to remind myself what year it was after I visited his websitePractical Malware Analysis book chapters 16 and 17 deal with Anti-Debugging and Anti-VM techniques
Five Anti-Analysis Tricks That Sometimes Fool Analysts was published when I was writing this post
Find main
and put a breakpoint on it. As we go through main we reach a bunch of function calls. Let's start with the first one.
Function 1 - isDebuggerPresent?
|
|
The result of a function call isDebuggerPresent
is compared with 0 by test eax, eax
. This function will return 1 if the application is being debugged. In our case it will return 1 and the jump fails. Before the compare we see a value 0x106240
or 1073728
is loaded into esi
. On both sides we see a string being loaded and then we enter a loop. If we step through the loop and look at the xor line, we can see that it is xor-ing oh happy dayz
with the data at byte_4131F8
. If we reach the end of the string it will restart from the first character. This loop will go on for 1073728
bytes which seems to be length of data starting at byte_4131F8
. I am going to rename it to blob
and the number 0x106240
to blob_length
.
If debugger is present, we go left and the string oh happy dayz
is xor-ed with the blob. If no debugger is present, we jump to the right branch and string the final countdown
is xor-ed with the blob
.
|
|
Function 2 - BeingDebugged?
|
|
Let's forget about the first compare and look at the outcome. Something is being compared with 1. If the compare succeeds then the first jump happens and we skip reseting var_4
to zero. The next jump will only happen if var_4
is zero which means that the last jump should not have happened. If the first compare succeeds (meaning [eax+2]
is 1) then we go left and otherwise right.
In both cases a string UNACCEPTABLE!
or omglob
are loaded along with address byte_4131F8
before a function call sub_401000
. The address points to a long stream of data.
Looking inside sub_401000
. At the start blob_length
is loaded into ecx
. Then we enter a loop. If we step through the loop and look at the xor line, we can see that it is xor-ing UNACCEPTABLE!
with the data at byte_4131F8
. If we reach the end of the string it will restart from the first character. This loop will go on for 1073728
bytes which seems to be length of data starting at byte_4131F8
. So sub_401000
is string xor blob
.
Now let's go back to the first compare.
|
|
What is the significance of fs:30h
? It is the Process Environment Block (PEB)
in the Thread Information Block (TIB)
. According to MSDN it has the following structure. The application is comparing the 3rd byte with 1. The 3rd byte is called BeingDebugged
and is set to 1 if the application is being debugged. If we are running the application with a debugger it will be set to 1 and UNACCEPTABLE!
will be xor-ed with the blob
otherwise omglob
. More information about the PEB
can be found in the first section of the PDF 1.NtGlobalFlag
.
|
|
At this point we can rewrite this function in Python
|
|
Function 3 - VMware Detection via Red Pill
|
|
Jumping into sub_401130
we see an old anti-VM technique. This is called The Red Pill. Each CPU core has its own Interrupt Descriptor Table (IDT}
. IDT is essentially an interrupt vector table. Because the VM manager is juggling two operating systems but there is one location per core, it has to relocate IDT of guest OS in memory. The application can check this location for known addresses assigned by VM managers and determine if it is running in a VM.
But how is this accomplished? Each core has one register called the Interrupt Descriptor Table Register (IDTR)
that points to this location in memory. The userland (ring3) instructionSIDT
will save this register. VM managers store the relocated tables in different places and the value of this register can act as a VM manager fingerprint.
According to this post by Alain Zidouemba these are some of the addresses:
|
| VM Manager | Address |
|------------|------------|
| Windows | 0x80FFFFFF |
| Virtual PC | 0xE8XXXXXX |
| VMware | 0xFFXXXXXX |
|
|
|
The above compares the first byte of IDT address with 0xFF
. According to our table it is looking for VMware
. But we are not running it. If this check passes (meaning we are not running VMware) the string you're so bad
is going to be xor-ed with the blob, otherwise it will be you're so good
. The address 0xBAB3C590
did not change during my runs in one VM. I will have to try with a different VM in VirtualBox to see if it changes or if it has a pattern. If you know please let me know.
|
|
Function 4 - VMware Detection 2: Electric Boogaloo
|
|
What's in the box?
Function will create its own exception handler, it will return the execution to loc_401232
if an exception occurs. Then we have some interesting instructions. If we look at the Malware Bytes article, it is named VMware I/O port
. These are the magic instructions:
|
|
It's a quick way to find if the application is running in a VMware VM. If in eax, dx
is successful, it will save the magic number in ebx
and then var_1C
. If not, it will raise an exception. But the function has an exception handler and execution will be transferred back to the function. Then var_1C
is compared to the magic number to determine if the application is in a VMware VM or not.
I was running the application in VirtualBox. Apparently Fireeye thinks we are all rich and use VMware ;) So the check failed.
The rest of the function is pretty simple, if the check fails 0x66
(character f
) will be xor-ed with the blob. If running in VMware 0x01
.
|
|
Function 5 - OutputDebugString
|
|
This is almost the same as listing 16-1 in page 353 of Practical Malware Analysis
book (Link to p.353 on Google Books). First the current error code is set to 0x1234
. Then OutputDebugString
is called with string bah!
. An error occurs if a debugger is not attached to the application and current error code changes, otherwise there is no error and last error code remains 0x1234
. Later, last error code is retrieved by calling GetLastError
, if this value is not changed then a debugger is attached to the application and string Sandboxes are fun to play in
is xor-ed with blob. In the absence of a debugger, I'm gonna sandbox your face
is used.
|
|
Function 6 - I Can Haz Breakpoint?
|
|
Offsets from two functions are loaded and then compared. The first one calls isDebuggerPresent
and the second one just prints something and exits. We have seen this function before, it is the first check.
|
|
|
|
None of these functions are called. But their offsets are compared. If offset of calls_isDebuggerPresent
is larger than sub_401780
then we jump down and string I can haz decode?
is xor-ed with the blob. Otherwise we go right. I am not quite sure what this check is for. I think it is trying to find if calls to isDebuggerPresent
are redirected or not (by the debugger?) as the address of the first function is 0x401030
and is smaller than 0x401780
. If you know what this means please let me know and I will update this section. In all of my runs the jump does not happen and execution continues to the right.
To the right we can see a pretty standard 0xCC
check. 0xCC
is the code for INT 3
and is used by debuggers to set breakpoints. It is simply checking if 0xCC
bytes are present in the function code. If 0xCC
is present ecx
is increased by 2, otherwise by one. In the end this number is compared with 0x55
. If the check does not pass it will jump to left (same as above) and I can haz decode?
is xor-ed with the blob. If the number is 0x55
string Such fire. Much burn. Wow.
is xor-ed with the blob.
|
|
Function 7 - NtGlobalFlag
|
|
This one is pretty straightforward. A field inside the PEB
(we have already seen it) is called NtGlobalFlag
. This flag is at offset 0x68
in 32-bit versions of Windows (and 0xBC
for 64-bit). Usually it is set to zero but it can be changed. A process that is started by a debugger will have this field set to 0x70
. To read more about it, please look at the Anti-Debugging reference.
If NtGlobalFlag
is not 0x70
then \x09\x00\x00\x01
will be xor-ed with the blob, otherwise Feel the sting of the monarch!
.
|
|
Function 8 - Sands of Time
|
|
This is not a countermeasure but a simple check. First time64
is called and returns the number of seconds since January 1st 1970. Then localtime64
converts it to readabled format stored in a structure of type tm
according to MSDN:
|
|
Next instruction cmp dword ptr [eax+18h], 5
compares 24th (0x16) byte of the structure with 5. Because each field is of type int
and 4 bytes, 24th byte will be the current day of the week. Sunday is 0, so Friday is 5. The application simply checks if it is Friday. If so, it will xor ! 50 1337
with the blob and if it is not Friday blob will be xor-ed with 1337
.
|
|
Function 9 - Backdoge.exe
|
|
Before next function, executable's complete path is saved into eax
. Then sub_4014F0
is called.
Again, this is just a check. Executable's name is compared with backdoge.exe
two characters in each iteration.
The rest is pretty easy. If filename check passes, MATH IS HARD
will be xor-ed with the blob and if not LETS GO SHOPPING
.
|
|
Function 10 - Dogecoin.com IP Check
|
|
Another check. This time the application retrieves the IP for www.dogecoin.com
using gethostbyname. The result is of the form hostent:
|
|
Then 8th byte will be compared with 2
which is h_addrtype
. According to this stackoverflow answer, it is AF_INET
or PF_INET
defined in bits/socket.h.
inet_ntoa
is converting the IP to ASCII IPv4 format (e.g. 192.168.0.1) and comparing it to 127.0.0.1
two characters at a time like last check.
The xor-string is LETS GO MATH
if the resolved IP address is not 127.0.0.1
. If the IP address is 127.0.0.1
or h_addrtype
is not 2
then SHOPPING IS HARD
will be xor-ed with the blob.
|
|
Function 11 - Hour of the Wolf
|
|
Again, we see the familiar time64
and localtime64
calls. This time offset 8 of the tm
structure (copied below) is compared with 0x11
or 17
. This offset contains the number of hours after midnight, so the application is checking if it is between 5 and 6 PM.
|
|
If time check passes, blob is xor-ed with \x01\x02\x03\x05\x00\x78\x30\x38\x0d
otherwise it will be xor-ed with \x07\x77
.
|
|
Interlude - 12 - Fullpath xor
|
|
We finished the first 10 functions, YAY. Now we see that the full path of binary is xor-ed with the blob. However, keep in mind that one of the checks compared full path with backdoge.exe
.
|
|
Function 13 - Internet Rootz
|
|
Two more functions. We're getting there.
We have seen this type of code. This function pushes e.root-servers.net
to stack and then calls gethostbyname
to retrieve its IP 192.203.230.10
. If the result is not zero, h_addrtype
is checked for 2 (AF_INET
) and retrieved IP is converted into ASCII format.
The rest is pretty simple. 192.203.230.10
is xor-ed with the blob.
|
|
Function 14 - jackRAT
|
|
|
|
We see InternetOpen called. This function initialises the WinINet functions. Agent name is ZBot
which is an alternate name for the Zeus
trojan horse. Access type is INTERNET_OPEN_TYPE_DIRECT
which means direct access without the use of any proxies. If a NULL handle is returned then function will exit (line 28). If not it will jump to loc_4018E5
(line 21).
|
|
InternetOpenUrl opens a handle to a resource. dwFlags
is set to 0x00400100
. I could not find the exact meaning of this flag value. However, according to this page it could be the OR
of two flags (does it work that way?):
|
|
Lines 9 to 35 are saving the URL, we know what it is without even looking at it. We have seen it in Wireshark before. The URL is https://twitter.com/FireEye/status/484033515538116608
.
Line 37 saves return value which is a "valid handle to the URL if the connection is successfully established, or NULL if the connection fails". Then it is checked for being NULL, if so we will jump to loc_4018D4
and function returns immediately. If we have a handle to the tweet, execution continues.
|
|
InternetReadFile retrieves the tweet. A buffer is created to hold the retrieved data. Documentation says "[a] normal read retrieves the specified dwNumberOfBytesToRead for each call to InternetReadFile until the end of the file is reached. To ensure all data is retrieved, an application must continue to call the InternetReadFile function until the function returns TRUE and the lpdwNumberOfBytesRead parameter equals zero." This is happening in lines 31-35. We keep reading until NumberofBytesRead
is zero.
After we are done, the jump in line 32 is not taken and we land here:
We retrieved the tweet. Now strstr is called to find the first instance of Secluded Hi
in the tweet. The return value is a pointer to the start of Secluded HijackRAT http://t.co/ckx18JHdkb ...
. The application adds 0x0B
or 11 to the start of the string to skip Secluded Hi
and point to jackRAT http://t.co/ckx18JHdkb ...
. A new 8 character buffer is created and passed to strncpy. strncpy
is called to copy 7 bytes from the start to the newly created buffer which will be jackRAT
. The rest is simple, jackRAT
is xor-ed with the blob and finally InternetCloseHandle is called three times to close the three function calls.
![xor(blob,"jackRAT")](/images/2014/flare/7-22.jpg "xor(blob,"jackRAT")
|
|
Are we there yet? gratz but not yet
|
|
The application crashed in line 5 over and over again. When I looked inside ecx I saw empty space but looking around I saw the application's complete path. After a while I realized that the code is trying to read arguments. The rest is obvious from the code. First two characters of first argument are written over the first two characters of the blob. First and second characters of second argument are written at offset 0x80
and 0x81
.
Then fopen is called to create/open a file named gratz.exe
for writing in binary mode ("wb"). Then blob is written to it by calling fwrite and finally it is closed with fclose. Then command gratz.exe
is run via the system call. So we are writing the blob to a file and then executing it.
What is special about first two bytes in a Windows binary? It's the start of the DOS stub with the magic bytes MZ
and you have already guessed that the second argument should be PE
.
How do I XOR?
But how do we get the correct binary. As we have already seen, there are a series of checks and depending on the checks, different strings are xor-ed with the original blob. A correct sequence of strings will produce a correct binary. The path is probably known at this point, just bypass any Anti-VM/Anti-Debug countermeasures and other checks. But I am lazy and instead wrote a bruteforcer. In order for the bruteforcer to work, we need the original blob before any xors. That is easy. Set a breakpoint before any of the functions. Then set the Instruction Pointer to 00401B8D
and step through after the breakpoint. Stop before the system
call and copy the gratz.exe
file from disk.
Here's my bruteforcer. This is not good code but at that point I just wanted to finish.
|
|
It's a bad bruteforcer but it does the job. To speed things up, it only performs the xor-es with the first 0x80
bytes of the binary which is the DOS Stub
. In the end, it compares the first two bytes with MZ
and then xor-es the whole binary before writing it to a file.
I got two files and after opening them in hex editors, one was clearly a false positive. I executed the correct binary.
But we cannot see the email. Augh. This is a .NET application. We need to decompile it like the first challenge.
|
|
And inside lulz.cs
.
|
|
We can either write code or paste it into an online C# compiler. In the end we have the flag:
Level 7 flag: da7.f1are.finish.lin3@flare-on.com
And the email:
Alright, we give in. You've done it. Your reversing-fu is strong.
I'll pass your info on to the FLARE team and someone will be in touch.
-FLARE