Alternatively, you can say…whatever, this probably isn’t real malware. Indeed, this whole series of walkthroughs has been functioning on that assumption and approaching the binaries as CTF challenges more than malware.

Step 2: Dynamic Analysis

Anyway, let’s run this thing and see what happens. I’ll continue with the assumption that it is more CTF than malware and skip all the monitoring for now (fakenet, regshot, procmon, procexp, etc.).

Level 1

So let’s burrow our way into this binary’s logic through the wrong counter. It increases every time we submit a wrong password, so we should be able to work our way backwards from it to find the password validating logic.

We open up the binary in IDA, then attach to the process using Debugger -> Attach to process

In the strings window, we ctrl+f and search for wrong. Examining the xrefs for this string:

5 levels, 5 xrefs. Ok.

We click on the address of the Wrong counter string, then set a breakpoint on all data xrefs to it:

for ref in DataRefsTo(ScreenEA()):
AddBpt(ref)

We go ahead and submit our wrong password again, and see which breakpoint triggers.

IDA has not recognized this as a function, so we don’t have the nice Control Flow Graph (CFG). That’s an easy fix here, we just scroll up until we see a function prologue:

then click on the push ebp and press p. We rename this function to Level1 and press space bar for our nice CFG. We take a moment to rename the other breakpoints Level B, C, D, and E so we can spot them easily in cross references.

We scroll around note the many failing edges which lead to the wrong counter block. There are also many calls to other functions. Too many, if you ask me.

Well, we click on them and examine the xrefs. It seems that they are being used repeatedly, and within various functions.

The naming convention quickly breaks down under so much data/code. We can resort to naming the functions by how much they’re being called. We will spend less time on a libManyCall, than a lib3Call. We will focus our reversing efforts on such excellently named functions as justLevel1, justLevel1AsWell.

With this new naming convention covering our Level1 function, we set a breakpoint at the top of the Level1 function so we can examine dynamically what leads to the Wrong counter block, and how our input is used.

We submit our wrong guess, step through examining the buffers/arguments and register assignments until we spot our input. Along the way, we fix some offsets by undefining them u and converting to unicode alt-a where applicable.

We note this loop that gets the string length of whatever was in esi, via pointer arithmetic:

esi, which held the distance between the start of the “mY0dKWWn0[EN" blah blah string, and the end of it, is divided by 2 (sar esi, 1) to get the character count (rather than the length including the 00 bytes of each wide character).

It is then compared to ebx, which holds a 3 for us at the moment. That is likely the length of our input (theGuess).

Well, so now we know that our input has to be 0x97 characters long. This is good news, since…we must be able to calculate it as a brute force would be unreasonable.

Examining where ebx was assigned leads us to this code in the first basic block of Level1:

$ ./theBytesW.py
In GreeK mytholOGy, the labyREnth was an elAborATe structure desigNED and builT by the LEGENDARY artificer Daedalus for KiNG MiNOS of CrEtE at KnoSSOs.

We press F9 to fail, but we now have the right input for the next run. We paste our guess into the box and hope to hit the Wrong counter for Level 2.

It may be worth our while to find out where the b6da string came from, in case we need to do something with this string, or at least transcribe it.

Oh well for now, let’s catch it on the next one.

Level 2

It’s tempting to guess what’s written in the image. This is certainly a doomed approach, but we might learn something along the way, so we submit a guess anyway. At least we will find the Level 2 code.

PAN{NeedsDefinitelyMoreJpegHimomchchra} ?

We submit the guess and find ourselves at the GetWindowText breakpoint. We return from here and rename this function Level2.

There are a lot of similarites between Level1 and Level2, we take advantage of that and focus on the parts unique to Level2. Playing the same count the xrefs game, we try to find the unique Level2 code.

This turns out to be a pretty good game, and we have quickly found the code unique to Level2:

The cascading checks that lead to the wrong counter block are present again. The dwords actually resolve to a series of anti-debug checks, but Scylla Hide has really helped us out here. This binary would have been more trouble otherwise. We could have also found these features by following the hint about the HideDebugger.dll string. It would lead us to the TLS callback that IDA identified:

Google “TLS Callback malware” for some extra reading material on this subject. Alternatively (and to no one’s surprise at this point), you can find more information about this in Chapter 16 of Practical Malware Analysis. It’s a seriously excellent book, and worth every second and dollar you’d spend on it. I was lucky enough to have access to it through Books24x7 when I was in school, but I definitely also bought a copy when the nostarch humble bundle came out. I owe almost every bit of my reversing methodology and knowledge to this book.

Anyway, perhaps we will put on our malware analysis hats and explore the anti-debug features of this binary after we win. For now…let’s get flags.

We label our guess buffer, and click on the first function unique to Level 2:

PANW:0044E53B lea ecx, [ebp+var_6C]
PANW:0044E53E call justLevel2

We press F4 to run to this point. We look at the surrounding buffers for our guess, and we find it:

We rename the buffer, and take a look inside justLevel2.

None of this looks like fun, and the bunch of math suggests that the crypto monster has made another appearance.

The code after this is the cascading anti-debug checks. So the decision inside of justLevel2AsWell is essentially the input validation logic.

A quick look inside justLevel2AsWell tells the story rather quickly:

We see a familiar loop pattern that compares 4 bytes at a time, we see a hardcoded string that looks like a sha256 sum, and it will be compared against our sha256 sum.

It looks like the flag validation routine for Level 2 is essentially a question…does sha256(yourGuess) equal this sha256 sum?

There is no brute force to be had here. There is no calculation either.

We must be given the answer. So we take the hint quite literally, that the answer is in the image.

Originally, when solving this challenge, I went down some rabbit holes of watching this binary load the resource image, and examine what calculations were being done. Perhaps there was a clue there. I’ll spare you the details of that rabbit hole, and we will go on with the good ideas.

Let’s take the image out of this binary, and peform some sort of forensics or surgery.

There are many tools we can use to extract the resources from this binary, but I prefer Resource Hacker. (Guess where I learned this trick?)

We open the binary in RH and expand the PNG folder. There are two at the very bottom whose names stand out. Clicking on them we find our image, and save it as a png.

We submit the proper Level 2 input “Did you think…”, and we hit the lpString breakpoint:

debug011:00298AC0 unicode 0, <174954d21100331514edef46c5219daa>,0

Level 3

We carry out the submit a guess, rename the function we return to after the window text is retrieved, rename the surrounding buffers strategy as we find ourselves in the Level 3 code.

We step through, and we see a whole bunch of bytes placed on the stack. We keep going and it starts to look a lot like the beginnings of some process hollowing:

All the previous stack action is probably the code that will be executed inside of notepad.exe

We scroll down to look ahead and set breakpoints on important calls:

WriteProcessMemory, CreateRemoteThread.

It might not be a bad idea to programmatically set breakpoints on these calls. Refreshing our memory with the IDA docs:

we come up with the following:

Python>for ref in CodeRefsTo(0x5F5484, 0): AddBpt(ref)

where 0x5F5484 is the address for WriteProcessMemory in the IAT.

We repeat the process for CreateRemoteThread (0x5F5480).

Nothing stands out in regards to our input being processed, perhaps the hollow process will do something with it. Confirming that our breakpoints were set, we press F9 and examine the source and destination for the bytes being written.

We’ll go ahead and use the break on RtlUserThreadStart method as mentioned in the first response/comment.

We will need another instance of IDA (or another debugger) in order to debug this other process and set this breakpoint. We’ll attach for now, and find/label our buffers and the functions that were written to the base of this process.

We go ahead and attach to the hollowed notepad.exe.

There’s likely a better way, but we can go to Search -> Text (or Alt+T) and look for RtlUserThreadStart:

We set a breakpoint, then go back to the other IDA instance to let the CreateRemoteThread call through (after setting a breakpoint on the next instruction after the CreateRemoteThread).

It works wonderfully, and we can go set a breakpoint on main before letting this function continue

and press p to define this as a function. We scroll down and keep defining functions until we reach a bunch of nulls. We go back to the main function again 0x140000 and see if we can make sense of things/rename anything.

It’d be a good idea to find our input buffer, 0xC0000 and the other two buffers to see where they are used. Unfortunately, there are no xrefs to these buffers.

We note the repeated use of sub_1400E0:

It is likely decrypting some imports. We rename it maybeImportResolver and move on.

Calls to function ptrs

00140087 call edi

001400B8 call [ebp+var_4]

make this difficult to analyze statically. It makes for a deceptively nice Function call graph (Ctrl+F12) though:

We go ahead and take a quick look at these other two functions, and rename them according to their shape or any unique features.

smallRecursy and stackBytesCallsRecursy? Good enough for now.

We step over the maybeImportResolver and see that it is indeed resolving imports. We rename the variable as the imports are stored in them.

Especially interesting is CreateMutexA, as we noticed a CreateMutexA in the main binary just after this hollow process received the CreateRemoteThread. We will explore this in a bit, but let’s finish getting an overview of this hollowed main.

The call to CreateMutexA will create a mutex with that name. Then this function will sleep for 0x7D0 milliseconds before exiting. This might make sense, depending on the CreateMutexA call in the main binary.

We take a quick look back at the main binary:

So, after creating the thread in the hollow process, it waits for a little while then begins a loop that attempts to create mutexes. It first attempts to create a mutex with the REVFEST_OK name. It checks the error status with GetLastError. If the error code was 0xB7 then the mutex already exists, and this function starts heading down a series of basic blocks that will miss the wrong counter block. This is likely our success state.

If this error is not returned, it attempts to create a mutex with the REVFEST_NO name. If this mutex exists (error code 0xB7), then we instantly go to the wrong counter block and fail. If this is not the error code, and this has not been attempted 0x1000 times already (esi counter xor’d before started this logic), then we loop around and try to create these mutexes again.

So, we need to find a way to return something other than 0 from stackBytesCallsRecursy so that our REVFEST_OK mutex is created. We could force it, but presumably, the flag for this level is calculated using our input.

Back to the hollowed process, we start digging in to stackBytesCallsRecursy.

With no library calls left, and nothing about this algorithm seeming familiar…all that’s left is to translate this and smallRecursy into python. It may be brute forceable, we can quickly check by seeing if our input is going to be used 1 byte at a time for satisfying the constraints to leave this function using the retn on the left (returns with 1 instead of 0 in eax):

So we find our input, it was stored in ecx before this function was called:

Level 4

The window text is retrieved, and we take a look around Level 4. By now, we can quickly recognize the string length routines, and the anti-debug cascade.

Playing the xref game, we find the first function that is unique to Level 4 at 0x4506C4:

We go ahead and run to this point and examine the buffers going in to rename what we can.

We step inside this function, and it’s quite ugly. Scrolling around, nothing really stands out. Let’s step through, taking note of the buffers and anything else that seems interesting and see what happens.

We reach a block that demands a bit of attention:

Stepping through and watching this loop, we see that it grabs 3 bytes of our input and stores them in various registers. When this loop is done, we end up with the following:

cl = guess[index]
ch = guess[index+1]
ah = guess[index+2]

some shifting/anding takes place on our bytes and then we enter another loop

It looks like there’s a bit of useless/sneaky code going around. What’s most interesting here is that this loop runs 4 times (esi was xor’d with itself just before entering this block), and it indexes some dword at an offset of esi + 0xC0 + var_18 (where var_18 is an array of the results from each byte having been shifted/anded).

Examining the xrefs to var_5C, it seems that it is only ever written once (set to 0), and then it is read only once (during this cmp). That seems a little strange, since this cmp eventually determines whether we jump to the wrong counter, or if we continue into a loop that can send us down the anti-debug cascade to Level 5!

Well, this challenge would be impossible unless we can influence the value of var_5C. For this debug run, it holds a 4. That is not the length of our input, but it is the length of our encoded input.

Ok, so if we want a base64 encoded input that has a length of 0x48, then we need an n such that

Ok, so we can use an implementation of this algorithm and use it in a brute force script to find the encoded bytes that will give us the right eax and edx values. Alternatively, we can just write our own weird implementation in python. I choose the latter:

Level 5

By this point, you know the drill. We check the xrefs and rename what we can, then we scroll around and see if we can quickly label some functions based on significant API calls they make.

Scrolling around a little and it seems that the Level 5 code isn’t going to do much. However, this call esi stands out a little bit. We set a breakpoint here and go back to find where var_320 comes from.

We take this opportunity to do a bit of skimming to see if anything stands out. Along the way, we make note of this function unique to Level 5 that may be using a URL.

The false branch will display a message box with the Text “NEEDS MORE INTERNET”

and it looks like the bytes on the stack will potentially end up as a URL.

The call to esi will take the ansi version of our input as an argument, and it is not read or accessed elsewhere until what is presumably the flag generating routine (happens after the anti-debug cascade).

Since it is the final level, we might as well take a moment to appreciate the heavy lifting that scylla hide has been doing for us. Let’s investigate what this anti-debug cascade is all about.

Double clicking the dword, then examining the xrefs with x, we see that there is only one place where the first dword is written to.

We double click it and we see the dark secrets of this dword:

Scrolling up to the start of this section of code, we press p to help IDA help us. We take a quick look around, and head back to the task at hand.

We step through, watching a bit of the decryption loop just for fun, until we hit the call to the justLevel5 function (which we now have a better name for, but…it’s not a big concern for this binary).

This is likely what the loop will take care of when we return from downloading this.

We take a look inside the justLevel5 function and a quick user xrefs chart tells the story:

A little bit of reading on MSDN would clear things up if it’s not clear enough. We step through…and we get an exception on InternetOpenUrlA. Not entirely sure why, but that’s ok, we have the bytes we need. (Note that x64dbg does not have this issue)

We prepare for a bit of binary surgery.

We launch revfest.exe again and input all the passwords. Just before submitting the Level 5 guess, we set a breakpoint on the call to InternetOpenUrlA and attach with IDA.

It is our successfully decrypted shellcode. We could dump this shellcode and analyze it a few different ways…(e.g., wrap it in its own executable, emulate it using miasm), but perhaps that’ll be another blog post.

We click on the first byte of our shellcode, and define it as code c, then let IDA know it is a function with p. Before jumping into a CFG, we scroll down to see if there are other functions we missed as the

debug122:00374D55 call near ptr unk_375000

seems to suggest.

It seems worth our while to make sure we scroll through 0x4D0 bytes looking for more code.

$ pcalc 0x374d40+0x4d0
3625488 0x375210 0y1101110101001000010000

Taking a look at the first function, we see a familiar pattern:

This looks like it will decrypt some import names, then store them in variables/registers to call them.

We step through and rename the variables with the new information, being a little careful with the call instructions.

Studying the logic, it makes sense that esi should be pointing to our guessed password, and edi which is being indexed with the loop counter that loops until 0x480 is a pointer to our shell2 buffer. Setting these values, then manually editing EIP to point to the beginning of this basic block, we continue.

and all is well…for now.

Stepping into stage 2, it doesn’t take long to recognize what various blocks are doing. It is very similar code to stage 1. We focus our attention on the next phase of the password checking routine:

and we step through the end of each stage, watching memory being freed and our succesful return value passed in eax

However, we did forget to deal with the after effects of the binary surgery in Level 5’s main. We have a few choices. I opt for a trip down memory lane. We exit out of this process, start it again with no debuggers or IDA involved, and enjoy submitting the flags one by one.