Reversing ASM to source code - Challenge 2

Can we take x86 assembly and turn it back into source code?

I had a lot of fun after the first challenge and since we spent most of our time figuring out the specific compiler settings more than reversing, I figured we’d up the game a bit. I sent a simple string encryption program to my friend and asked that he write a keygen for it. I, in turn, received a new program as well to reverse back to the original source. The goal is to get as close as possible to the original source, and at the very least make something that is functionally the same. Let’s give it a go.

Here’s the function we’re going to reverse.

Assembly of the function we're going to reverse

Looking at the file in CFF Explorer we see the existence of the .msvcjmc section indicating “Just My Code” debugging is on. We’ll turn that on in visual studio. As well as stack frames and security check.

Yes (/JMC)
Stack Frames (/RTCs)
Enable Security Check (/GS)

Let’s get started on some test source code to see if we can replicate the prologue correctly with the JMC additions.

Assembly of our simple source code with stack frames and security check enabled in our compiler

Hrm…not quite right. At the moment I’m not sure where this is coming from, it’s not in ours.

mov eax, dword ptr ds:[0x0118A024]

Based on the fact that there’s a xor eax, ebp instruction after this appears to be a stack check, but it doesn’t manifest in ours. Let’s add a local variable in our source and see if it pops up.

Image of our current source code

Results in

Assembly output of our new source code

Nope, no change there, but the mov ecx, 3D did go up by 0xD bytes so that’s a start on figuring out that mystery.

Let’s figure out how much space is allocated for local vars and continue on. In the prologue we see sub esp, 130.

From our original example with no local variables we see it allocated 0xC0 bytes, so 0x130 – 0xC0 = 0x70 bytes. Let’s make a variable to use that many bytes and confirm our local variable space is correct.

Assembly of our source code with a local variable added

Assembly showing sub esp, 144

Nope, too many. We’re looking for 0x130 bytes total.

This code gives us the correct allocation of local variables, but 90 bytes is a strange number, changing it to 91 bytes, gives the same result, we’ll have to figure this out when we know a bit more about how they’re used.

Source code with a local variable added

Moving on.

There is a local variable that starts at ebp-C. Here is it’s initialization code.

Assembly showing a series of mov instructions

It starts at ebp-C because that was automatically generated and initialized to all CC’s, (int3’s) for overflow protection.

Luckily we already know how to generate this code. It’s a little trick in asm that’s useful for hiding strings and data. It’s 5 bytes initialized to 0x2D, 0x32, 0x27, 0x2C, and 0x42. X64dbg helps us and gives us the corresponding ascii characters. Let’s take a stab at generating that asm.

Source code image showing that we've added a variable called maybeKey and given it the values -, 2, ', B, and ,

Turns out in reading this we missed a zero initialized local variable at ebp-10. Let’s add that in too.

Source code showing we've added a 5 byte variable named zerod

The resulting asm is:

Assembly for our current source code

Hey! Look at that. The security cookie was added and now our code has the cookie and the ```xor eax, ebp```` instruction added.

Let’s move onto the second bit of initialized local variables now. Here’s the relevant code.

Assembly showing a long series of mov instructions

From first glance, there’s 2 variables here. the variable at ebp-40 and the variable at ebp-60. The first one from 40-2C, is 0x18 bytes. The second is 60-49 is 0x18 bytes. And I just realized there’s one more dword at ebp-6C. We’ll just make an int to fill that space.

The next part is a series of jumps. I’m going to implement it as a for loop initially. Judging by the fact a JGE jump instruction was created, I’m going to assume this is done on unsigned bytes, because I think that it would have generated a JAE instruction if the loop was created on signed bytes. Here’s the next part of the asm:

Assembly for our current source code

Our current source code

Resulting asm

Assembly for our current source code

Couple of things that are different. It looks like that unknown 4 bytes we allocated for an integer is probably just part of the for loop counter, so we can remove that. Also we generated a JAE where the original code used a JGE so my guess was wrong, we are probably using a signed integer in the for loop. Stackoverflow per Intel’s manual explains it this way:

Assembly for our current source code

So we’ll need to make sure our for loop uses a signed integer. And since our comparison in our code is just using ‘sizeof(str)’, which will always return an unsigned value, we’ll need to substitute the constant ’24’. The programmer should probably have used sizeof() in this case as changing the string would be easier if the length of the string to decode changed, it’s trivial but worth noting.

The 24 bytes of the string xor’d with 0x42 decodes to: ‘’

Let’s continue to the last part of the code.

Assembly for the challenge showing the call to ShellExecuteA

A call to ShellExecuteA

ShellExecuteA(0, operation, decrypted, NULL, NULL, SW_SHOWNORMAL);

Final source code is this:

Final source code

Apart from the addresses of the stack cookie and calls to CheckESP, security_cookie_check, and ShellExecute. The files are spot on.

Some lingering questions I still have though. Why on earth is the local variable that’s supplied to ShellExecute’s lpOperation 5 bytes!? Was this intentional misdirection? If he did:

char lpOperation[] = "";

Would be equivalent and would be less bytes (1). So no idea there. Also the additional of an unused variable that I initially thought of as a key was strange. It appears to just be misdirection. Perhaps we’ll have to outlaw any tricks like this in the future? I suppose it does represent a bit of a real-world scenario in which misdirection is placed (often programmatically). Seems like a cheap shot to me :).

Until next time.

I got clarification from the author on the misdirection. It was not purposeful, It turns out he intended to use the key, but forgot, so the bytes were allotted for it, but it went unused, (this is why we turn on /W4 and /Werror in our compiler warnings :)). We found a bug via the assembly, pretty neat!