Jump to content
Tuts 4 You

Users Desktop CrackMe #1


0x777h
Go to solution Solved by kao,

Recommended Posts

  • Solution

Here is a valid password:

Spoiler

C86C00C21618470B9C6EB5E3CA2A6D51kao

 

I'd love to write a tutorial how this crackme can be solved - but it will take me few days. I could probably find some time over the weekend. :)

Thanks, I really enjoyed it!
kao.

 

  • Like 2
  • Thanks 2
  • Haha 1
Link to comment
Share on other sites

4 hours ago, kao said:

Here is a valid password:

  Reveal hidden contents

C86C00C21618470B9C6EB5E3CA2A6D51kao

 

I'd love to write a tutorial how this crackme can be solved - but it will take me few days. I could probably find some time over the weekend. :)

Thanks, I really enjoyed it!
kao.

 

It's really cool !
I wanted to see what my virtualization lacked through this crackme. I'm really looking forward to this solution !

Thank you so much :)

Link to comment
Share on other sites

Hi guys, I apologize that writing a solution is taking me longer than expected. I haven't forgotten about it, just not enough spare time..

Edited by kao
typo
  • Like 3
  • Thanks 1
Link to comment
Share on other sites

On 11/1/2022 at 9:51 PM, kao said:

Hi guys, I apologize that writing a solution is taking me longer than expected. I haven't forgotten about it, just not enough spare time..

We will be waiting patiently

  • Like 1
Link to comment
Share on other sites

Extreme Coders

The password which solves this challenge is the same as the one posted by kao. Here is my quick write-up for the same.

Reposted on https://github.com/extremecoders-re/tuts4you_users_desktop_crackme_writeup with proper markdown formatting.

I : Bypassing anti-debug

First of all, to make the addresses not vary in between the runs we can disable ASLR for the binary by patching the Dll can move flag in PE Optional header. This is not strictly required but makes it easy to follow along.

The binary implements lots of anti-debug, specially playing with HANDLES which makes it a little bit tough to debug right out of the box. Also trying to the attach to the application would make it quit immediately.

In Process Hacker  we can check that the application is multi-threaded.

Spoiler

1.png.f51ce026b719dba746622f85f9fd2cae.png

In fact, the protection logic is implemented on the secondary thread which has a higher CPU usage (1.32 in the image). At this point we can suspend the thread. Once that is done we can simply attach to the process with x64dbg without any issue.

II : Finding a suitable point to start

We are already attached to the crackme process. At this point it is still waiting for our input on the main thread. We need to find a suitable place to set a breakpoint on such that we can trace the password checking logic with minimal overhead.

A nice place to set a breakpoint is in kernelbase.dll!ReadFile just after the call to NtReadFile as shown.

Spoiler

2.png.0934eafdb5d3beef195904cfa69a0c6c.png

With the breakpoint in place we can enter any password say 123456789ABCDEF and hit enter. The breakpoints immediately hits.

Spoiler

3.png.87eabd116725e7ce3df3477e43c1ba27.png

 

From here we can continue single stepping until we exit the fgets function and enter the VM region identified by the presence of obfuscated code.

Spoiler

4.png.89b74dced987e28c42186de51913b617.png

The ret instruction at 0x140004B51 should likely be the end of fgets.

Stepping once from here we can see obfuscated code signifying the start of VM.

Spoiler

5.png.27ecb46ea409c18e5ba50968fd9d8c80.png

 

III - Tracing the VM

At this point on the stack we can see our entered password.

Spoiler

6.png.d6aa96900a83ffd6ad153695ab3193bc.png

We can set a hardware breakpoint on read at 0x14FE60, the address where the password is stored in memory and resume execution. When the breakpoints hits, set another standard breakpoint on MessageBoxW function. The crackme displays the success or failure using this API.

Now using x64dbg "Run trace" we can log all instructions until it hits MessageBoxW. This will take quite some time. Make sure to set a large enough value for "Maximum Trace Count" as shown.

Spoiler

7.png.348908b6b1fd5a94c5936d04a632851b.png

IV - Filtering the trace

When the breakpoint at MessageBoxW  hits we can stop tracing and export the trace log to a csv. The objective is to remove all non xor instructions from the trace. Also other xor instructions like xor eax, eax, xor ebx, ebx which are irrelevant towards our goal can be removed. This can be accomplished using a decent text editor like Notepad++.

After removing all irrelevant instructions, we are left with just two xor instructions in the csv trace log.

07B77,000000014004602B,33C3,"xor eax,ebx",rax: B3-> 43,,"ebx:&""C:\\crackme.exe"""
448EB,000000014004602B,33C3,"xor eax,ebx",rax: 36-> 0,,"ebx:&""C:\\crackme.exe"""

After the first xor, eax holds 0x43 = 'C' which is the first character of our password.
The second xor is comparing the null terminator.

There are no more xor's as the crackme stops comparing further characters as soon as a mismatch is found.

V - Recovering the password

We can set a breakpoint on the instruction xor eax, ebx at 0x140004602B. The value in eax after the xor is the correct corresponding password character.
 

Spoiler

8.png.771576177fe013997aa99092ac6c59d2.png

Here, eax = 0x43 = 'C' which should be the first character of the password.
 

Spoiler

9.png.4f10d6bfe655b2820721e32c2cd1bb53.png

 

To get other characters of the password we cannot resume yet. Note that the application exits as soon as a mismatch is found. Hence we can overwrite the value in eax to 0x31 = '1'  instead which is the first character of our entered password (`123456789ABCDEF`). We can automate these steps using a Frida script.

Spoiler

 

var password = []

Interceptor.attach(ptr(0x14004602B), function (args) {
	console.log("Breakpoint===");
	const r_rax = this.context.rax.toInt32();
	const r_rbx = this.context.rbx.toInt32();

	if (r_rax < 0xff && r_rbx < 0xff) {
		const password_char = r_rax ^ r_rbx;
		if (password_char == 0) {
			console.log(String.fromCharCode(...password));
		} else {
			password.push(password_char);
		}
		this.context.rax = ptr(0x31);
		this.context.rbx = ptr(0x0);
	}
});

The script sets a interceptor at the instruction at 0x14004602B => xor eax, ebx .
When using the script we have to input a large string of 1's (like 1111111111111111111111111111111111) as the password. The script will overwrite the registers such that after the xor, rax contains '1' (=0x31) making the crackme believe the check succeeded.

Finally, we print the complete password when we have got the null terminator.

Spoiler

10.png.5c6582d835943be146064618aac650dd.png

The correct password is printed at the end. Note that we need to inject the frida script after suspending the protection thread in the same way we were attaching the debugger.

Edited by Extreme Coders
Spell check and fixing typos
  • Like 4
  • Thanks 8
Link to comment
Share on other sites

Nakashi_omara
26 minutes ago, Extreme Coders said:

The password which solves this challenge is the same as the one posted by kao. Here is my quick write-up for the same.

Reposted on https://github.com/extremecoders-re/tuts4you_users_desktop_crackme_writeup with proper markdown formatting.

I : Bypassing anti-debug

First of all, to make the addresses not vary in between runs we can disable ASLR for this binary by patching changing the Dll can move flag in the Optional header. This is not strictly required but makes it easy to follow along.

The binary implements lots of anti-debug, specially playing with HANDLES which makes it a little bit tough to debug right out of the box. Also trying to the attach to the application would make it quit immediately.

In Process Hacker  we can check that the application is multi-threaded.

  Reveal hidden contents

1.png.f51ce026b719dba746622f85f9fd2cae.png

In fact, the protection logic is implemented on the secondary thread which has a higher CPU usage (1.32 in the picture). At this point we can suspend the thread. Once that is done we can simply attach to the process with x64dbg without any issue.

II : Finding a suitable point to start

We are already attached to the crackme process. At this point it is still waiting for our input on the main thread. We need to find a suitable place to set a breakpoint on such that we can trace the password checking logic with minimal overhead.

A nice place to set a breakpoint is in kernelbase.dll!ReadFile just after the call to NtReadFile as shown.

  Reveal hidden contents

2.png.0934eafdb5d3beef195904cfa69a0c6c.png

With the breakpoint in place we can enter any password say 123456789ABCDEF and hit enter. The breakpoints immediately hits.

  Reveal hidden contents

3.png.87eabd116725e7ce3df3477e43c1ba27.png

 

From here we can continue single stepping until we exit the fgets function and enter the VM region identified by the presence obfuscated code.

  Reveal hidden contents

4.png.89b74dced987e28c42186de51913b617.png

The ret instruction at 0x140004B51 should likely be the end of the fgets function.

Stepping once from here we can see obfuscated code signifying the start of VM.

  Reveal hidden contents

5.png.27ecb46ea409c18e5ba50968fd9d8c80.png

 

III - Tracing the VM

At this point on the stack we can see our entered password.

  Reveal hidden contents

6.png.d6aa96900a83ffd6ad153695ab3193bc.png

We can set a hardware breakpoint on read at 0x14FD68, the address where the password is stored in memory and continue running the crackme. When the breakpoints hits, set another standard breakpoint on MessageBoxW function. The crackme displays the success or failure using this API.

Now using x64dbg "Run trace" we can log all instructions until it hits MessageBoxW. This will take quite some time. Make sure to set a large enough value for "Maximum Trace Count" as shown.

  Reveal hidden contents

7.png.348908b6b1fd5a94c5936d04a632851b.png

## IV - Filtering the trace

When the breakpoint at MessageBoxW hits we can stop tracing and export the trace log to a csv. The objective is to remove all non xor instructions from the trace. Also other xor instructions like xor eax, eax, xor ebx, ebx which are irrelevant towards our goal can be removed. This can be accomplished using a decent text editor like Notepad++.

After removing all irrelevant instructions, we are left with just two xor instructions.

07B77,000000014004602B,33C3,"xor eax,ebx",rax: B3-> 43,,"ebx:&""C:\\crackme.exe"""
448EB,000000014004602B,33C3,"xor eax,ebx",rax: 36-> 0,,"ebx:&""C:\\crackme.exe"""

After the first xor, eax holds 0x43 = 'C' which is the first character of our password.
The second xor is comparing the null terminator.

There are no more xor's as the crackme stops comparing further characters as soon as a mismatch is found.

V - Recovering the password

We can set a breakpoint on the instruction xor eax, ebx at 0x1400046020. The value in eax after the xor is the correct character corresponding password character.
 

  Reveal hidden contents

8.png.771576177fe013997aa99092ac6c59d2.png

Here `eax` = 0x43 = 'C' which is the first character of the password.
 

  Reveal hidden contents

9.png.4f10d6bfe655b2820721e32c2cd1bb53.png

 

To get other characters of the password we cannot resume yet. Note that the application exits as soon as a mismatch is found. Hence we can overwrite the value in eax to 0x31 = '1'  instead which is the first character of our entered password (`123456789ABCDEF`). We can automate these steps using a Frida script.

  Reveal hidden contents

 

 

var password = []

Interceptor.attach(ptr(0x14004602B), function (args) {
	console.log("Breakpoint===");
	const r_rax = this.context.rax.toInt32();
	const r_rbx = this.context.rbx.toInt32();

	if (r_rax < 0xff && r_rbx < 0xff) {
		const password_char = r_rax ^ r_rbx;
		if (password_char == 0) {
			console.log(String.fromCharCode(...password));
		} else {
			password.push(password_char);
		}
		this.context.rax = ptr(0x31);
		this.context.rbx = ptr(0x0);
	}
});

The script sets a interceptor at the instruction at 0x14004602B => xor eax, ebx .
When using the script we have to input a large string of 1's (like 1111111111111111111111111111111111) as the password. The script will overwrite the registers such that after the xor, rax contains 1 making the crackme believe the check succeeded.

Finally, we print the complete password when we have got the null terminator.

  Reveal hidden contents

10.png.5c6582d835943be146064618aac650dd.png

The correct password is printed at the end. Note that we need to inject the frida script after suspending the protection thread in the same way we were attaching the debugger.

I have seen many very good teachers and lecturers in different forums, and you are definitely one of them with these explanations.👌👍

You explained in detail and fluently, I wish I could have more training from you(especially video tutorials). 🙏

I had tried this crackme but it was really hard (of course, maybe for me) and I got lazy and didn't continue 😁

  • Like 2
  • Thanks 1
Link to comment
Share on other sites

1 hour ago, Extreme Coders said:

The password which solves this challenge is the same as the one posted by kao. Here is my quick write-up for the same.

Reposted on https://github.com/extremecoders-re/tuts4you_users_desktop_crackme_writeup with proper markdown formatting.

I : Bypassing anti-debug

First of all, to make the addresses not vary in between runs we can disable ASLR for this binary by patching changing the Dll can move flag in the Optional header. This is not strictly required but makes it easy to follow along.

The binary implements lots of anti-debug, specially playing with HANDLES which makes it a little bit tough to debug right out of the box. Also trying to the attach to the application would make it quit immediately.

In Process Hacker  we can check that the application is multi-threaded.

  Reveal hidden contents

1.png.f51ce026b719dba746622f85f9fd2cae.png

In fact, the protection logic is implemented on the secondary thread which has a higher CPU usage (1.32 in the picture). At this point we can suspend the thread. Once that is done we can simply attach to the process with x64dbg without any issue.

II : Finding a suitable point to start

We are already attached to the crackme process. At this point it is still waiting for our input on the main thread. We need to find a suitable place to set a breakpoint on such that we can trace the password checking logic with minimal overhead.

A nice place to set a breakpoint is in kernelbase.dll!ReadFile just after the call to NtReadFile as shown.

  Reveal hidden contents

2.png.0934eafdb5d3beef195904cfa69a0c6c.png

With the breakpoint in place we can enter any password say 123456789ABCDEF and hit enter. The breakpoints immediately hits.

  Reveal hidden contents

3.png.87eabd116725e7ce3df3477e43c1ba27.png

 

From here we can continue single stepping until we exit the fgets function and enter the VM region identified by the presence obfuscated code.

  Reveal hidden contents

4.png.89b74dced987e28c42186de51913b617.png

The ret instruction at 0x140004B51 should likely be the end of the fgets function.

Stepping once from here we can see obfuscated code signifying the start of VM.

  Reveal hidden contents

5.png.27ecb46ea409c18e5ba50968fd9d8c80.png

 

III - Tracing the VM

At this point on the stack we can see our entered password.

  Reveal hidden contents

6.png.d6aa96900a83ffd6ad153695ab3193bc.png

We can set a hardware breakpoint on read at 0x14FD68, the address where the password is stored in memory and continue running the crackme. When the breakpoints hits, set another standard breakpoint on MessageBoxW function. The crackme displays the success or failure using this API.

Now using x64dbg "Run trace" we can log all instructions until it hits MessageBoxW. This will take quite some time. Make sure to set a large enough value for "Maximum Trace Count" as shown.

  Reveal hidden contents

7.png.348908b6b1fd5a94c5936d04a632851b.png

## IV - Filtering the trace

When the breakpoint at MessageBoxW hits we can stop tracing and export the trace log to a csv. The objective is to remove all non xor instructions from the trace. Also other xor instructions like xor eax, eax, xor ebx, ebx which are irrelevant towards our goal can be removed. This can be accomplished using a decent text editor like Notepad++.

After removing all irrelevant instructions, we are left with just two xor instructions.

07B77,000000014004602B,33C3,"xor eax,ebx",rax: B3-> 43,,"ebx:&""C:\\crackme.exe"""
448EB,000000014004602B,33C3,"xor eax,ebx",rax: 36-> 0,,"ebx:&""C:\\crackme.exe"""

After the first xor, eax holds 0x43 = 'C' which is the first character of our password.
The second xor is comparing the null terminator.

There are no more xor's as the crackme stops comparing further characters as soon as a mismatch is found.

V - Recovering the password

We can set a breakpoint on the instruction xor eax, ebx at 0x1400046020. The value in eax after the xor is the correct character corresponding password character.
 

  Reveal hidden contents

8.png.771576177fe013997aa99092ac6c59d2.png

Here `eax` = 0x43 = 'C' which is the first character of the password.
 

  Reveal hidden contents

9.png.4f10d6bfe655b2820721e32c2cd1bb53.png

 

To get other characters of the password we cannot resume yet. Note that the application exits as soon as a mismatch is found. Hence we can overwrite the value in eax to 0x31 = '1'  instead which is the first character of our entered password (`123456789ABCDEF`). We can automate these steps using a Frida script.

  Reveal hidden contents

 

 

var password = []

Interceptor.attach(ptr(0x14004602B), function (args) {
	console.log("Breakpoint===");
	const r_rax = this.context.rax.toInt32();
	const r_rbx = this.context.rbx.toInt32();

	if (r_rax < 0xff && r_rbx < 0xff) {
		const password_char = r_rax ^ r_rbx;
		if (password_char == 0) {
			console.log(String.fromCharCode(...password));
		} else {
			password.push(password_char);
		}
		this.context.rax = ptr(0x31);
		this.context.rbx = ptr(0x0);
	}
});

The script sets a interceptor at the instruction at 0x14004602B => xor eax, ebx .
When using the script we have to input a large string of 1's (like 1111111111111111111111111111111111) as the password. The script will overwrite the registers such that after the xor, rax contains 1 making the crackme believe the check succeeded.

Finally, we print the complete password when we have got the null terminator.

  Reveal hidden contents

10.png.5c6582d835943be146064618aac650dd.png

The correct password is printed at the end. Note that we need to inject the frida script after suspending the protection thread in the same way we were attaching the debugger.

Wow, it's so cool ! 👍

It is a perfect and detailed solution.
I hope it was a enjoying challenge for you.

Thank you for solving it :)

  • Thanks 1
Link to comment
Share on other sites

And here's my promised solution. As you'll notice, it's a totally different approach than that of ExtremeCoders.. :) It will be published on my blog (https://lifeinhex.com/solving-0x777hs-crackme/) once I take care of all formatting issues.

----

The crackme is an x64 binary that uses a custom protector. Password checking code is protected using a code virtualization feature. In the tutorial I'll show how the protection works and steps I took to defeat it.

This is not a full and comprehensive analysis of the protector code or the code virtualization feature. When solving crackmes, I prefer to choose the simplest solution that gets the job done. This is also reason why I did not use ready-made tools like VMAttack (https://github.com/anatolikalysch/VMAttack), Detours, FRIDA and the like...

Quickly about code virtualization.
Code virtualizers are generally considered one of the hardest software protection methods to defeat. Why is that? Let's see what features a common code virtualization solution offers:

  1. Packer. Original program code is packed/encrypted and decoded on runtime.
  2. Anti-debug protection. Most protectors use some sort of anti-debugging protection in their code.
  3. Code obfuscation. Most protectors add junk code, some use control-flow flattening, constant obfuscation and other techniques.
  4. And finally, the actual virtual machine with custom instruction set.

If you have just a single feature, like junk code, it's actually quite easy to reverse. The difficulty comes from the combination of all protector features and also how well they are combined. 

There are several ways to defeat code virtualization:

  1. completely devirtualize the code. This is the ultimate success, you have recovered the original x86/x64 code, or a close approximation of it;
  2. make a disassembler for the particular VM, disassemble PCode and understand how the algorithm works;
  3. trace the VM execution, and use trace data to understand how the algorithm works;
  4. patch VM handlers and/or PCode;

If you want to learn more about code virtualization in general,  I can wholeheartedly recommend Tim Blazytko's blog (https://synthesis.to/2021/10/21/vm_based_obfuscation.html), as well as his Software Deobfuscation training. They are awesome!

With that in mind, let's look at our crackme and see what protections it contains.

Crackme overview.

Encrypted code.
If you open crackme.exe in your favorite hex editor, you'll notice that .code section appears to be encrypted. OK, maybe you will not notice that. :)

Just check the entropy of each PE section with a tool like DiE:

Spoiler

775455168_Pastedimage20221105191801.png.6d9a636783bb6e46cc7baa55df168781.png

So, our first step would be to unpack the file.

Anti-debug protection.
When you try to run the file under x64dbg, you'll notice that it throws some breakpoint exception and terminates:

Spoiler

1494133205_Pastedimage20221106184015.png.881b526606de82eee8683ff63e2b6640.png

I spent some time trying different ScyllaHide options but without any success. Debugging the startup code allowed me to note some of the features:

  1.  It uses a lot of Nt* functions;
  2. It manually maps ntdll.dll in memory and (probably) extracts syscall ids;
  3. The rest of the protection uses syscalls directly;
  4. And the protection code is mostly virtualized!

At this point I decided to try something else. Let's run the crackme without the debugger, dump process memory and try to attack it using static analysis!

Note: if Scylla fails to dump the process, use Process Hacker -> Select crackme.exe process -> Properties -> Memory -> select crackme.exe sections -> Save... and then rebuild PE header.

Junk code.
Dumped file is surprisingly readable in IDA. We can soon find a suspicious part in .code section:
 

Spoiler

2061883368_Pastedimage20221105211403.png.b5a00007cf82e005f48d24ff43cd2403.png

Following the jump, we see a combination of push constant+call followed by data which is very typical for a VM startup:
 

Spoiler

525837096_Pastedimage20221105211515.png.355ca264c9dd3b8ac15ea415b13e0bf5.png

Following that, we see some code that looks like an obfuscated spaghetti code:

Spoiler

1971435558_Pastedimage20221105211614.png.ca6acc7615daaeb93b9559aff306b9ea.png


So, it looks like we have located our VM but the code is obfuscated. We'll need to take care of that first.

Deobfuscating junk code.
After spending some time cleaning the junk code, you'll notice it uses several specific patterns for obfuscation. 

jmp+junk

Spoiler

1212651691_Pastedimage20221105213148.png.24033760994c0a9134078589e2ba53d8.png


The simplest of patterns - it's a short jump and few junk bytes. We can use hex editor and simple regex to replace this with 5 nops.

clc+jnb and stc+jb

Spoiler

689939112_Pastedimage20221105212703.png.cb4930b277ba389f8f9070da36aec1aa.png


First, a carry flag is set to a know value using clc or stc instruction. Then a conditional jump is used to confuse IDA's analysis. Jump distance is usually very short - 2,3 or 4 bytes. Just like before, we can use regex to replace replace clc+jump+junk code with nops.

Big obfuscated do-nothing
Once the simple jumps are replaced, you'll notice a much larger obfuscation pattern:

Spoiler

1567694314_Pastedimage20221105214802.png.971693fb7267827cd34e4d4d337846a8.png


The pattern begins with pushfq, followed by call and 2 jumps and ends with the popfq. This example uses RAX, but it can be also RDX or some other register. 
It's easy to find the end of the pattern just by looking for next popfq instruction.

Even larger do-nothing
And finally, there's a more complicated pattern. It's so large that I had to use graphic editor to stitch it all together for you. Notice that all nops, jumps and junk code are removed from the image!

Spoiler

1155852852_Pastedimage20221105215843.png.84071d21cafe90a0aa4dd4f1935816e2.png

As with the previous pattern, it's easy to find the end of it, just look for combination of pop rcx, pop eax and popfq. 

And we're done with code obfuscation! :) We've identified obfuscation patterns and found a way to deal with them.

Analyzing VM dispatcher
Now we're able to see what is happening on VM startup. First flags and registers are saved:

.code:00007FF70A36CF38    pushfq
.code:00007FF70A36CF66    push    r15
.code:00007FF70A36D00C    push    r14
.code:00007FF70A36D0BD    push    r13
.code:00007FF70A36D15C    push    r12
... more pushes ...

Then the VM state is prepared and VM dispatcher is reached:

.code:00007FF70A36E6B0   xor     rdx, rdx
.code:00007FF70A36E6E0   mov     dl, [rsi] ; fetch next opcode

And finally next handler is executed:

.code:00007FF70A36E068    mov     rax, rsp
.code:00007FF70A36E11C    add     rax, 0F8h
.code:00007FF70A36E13C    mov     rax, [rax]     ; table of handler addresses
.code:00007FF70A36E13F    xor     rbx, rbx         ; 
.code:00007FF70A36E169    mov     ebx, [rax+rdx*4] ; RDX contains the next opcode
.code:00007FF70A36E199    sub     rax, rbx
.code:00007FF70A36E1C3    jmp     rax              ; ---> next handler is executed


Writing VM tracer
For last few years I've written most of my tools in C#. Now I need to hook x64 code and C# is not really suitable for that. So, I dusted off my trusted old copy of Delphi XE2. 

Also I needed some injector that would inject my DLL into running crackme.exe process. I randomly chose one the first results from Google search: https://github.com/danielkrupinski/Inflame

I chose to hook VM dispatcher between "add rax, 0F8h" and "mov eax,[eax]" instructions. Since I didn't have any decent x64 hooking library for Delphi XE2, I made my own "hooking" code. It's ugly and you definitely shouldn't do that in production code. But for the crackme it's fine!

hookAddress := imageBase + $2E123;
returnAddress := imageBase + $2E13B;
d := PDword(hookAddress)^;
if (d = $000004E8) then begin // check whether the hooked address contains correct bytes
   Writeln(Format('Hooking address %x',[hookAddress]));
   VirtualProtect(pointer(hookAddress), $100, PAGE_EXECUTE_READWRITE, @oldProtection);
   PWord(hookAddress)^ := $BB48; // mov rbx, const
   PUInt64(hookAddress + 2)^ := UInt64(@MyHook);
   PWord(hookAddress + $A)^ := $E3FF;  //jmp rbx
   VirtualProtect(pointer(hookAddress), $100, oldProtection, @oldProtection);
end;

And this is the code responsible for logging VM context. Nothing fancy, just get values from memory and log them to console.

opcode := (savedRDX and $FF);
actualRSP := savedRAX - $F8;
rax := PUInt64(actualRSP+$70)^;
rbx := PUInt64(actualRSP+$78)^;
rcx := PUInt64(actualRSP+$80)^;
rdx := PUInt64(actualRSP+$88)^;
rsi := PUInt64(actualRSP+$90)^;
rdi := PUInt64(actualRSP+$98)^;
rxx := PUInt64(actualRSP+$A0)^;
ryy := PUInt64(actualRSP+$A8)^;
r8 := PUInt64(actualRSP+$B0)^;
r9 := PUInt64(actualRSP+$B8)^;
r10 := PUInt64(actualRSP+$C0)^;
r11 := PUInt64(actualRSP+$C8)^;
r12 := PUInt64(actualRSP+$D0)^;
r13 := PUInt64(actualRSP+$D8)^;
r14 := PUInt64(actualRSP+$E0)^;
r15 := PUInt64(actualRSP+$E8)^;
eflags := PUInt64(actualRSP+$F0)^;
Writeln(Format('PC=%.08x/%.08x opcode=%.02x RAX=%.08x RBX=%.08x RCX=%.08x RDX=%.08x RSI=%.08x RDI=%.08x RXX=%.08x RYY=%.08x R8=%.08x  R9=%.08x R10=%.08x R11=%.08x R12=%.08x R13=%.08x R14=%.08x R15=%.08x EFL=%.02x',[savedRSI, savedRSI - UInt64(CrackmeImageBase), opcode, rax, rbx, rcx, rdx, rsi, rdi, rxx, ryy, r8,r9,r10,r11,r12,r13,r14,r15,eflags]));


Analyzing tracer output
Now, let's run the crackme, inject tracing dll and enter some random serial. We'll get output similar to this:

PC=7FF66C736ECC/00046ECC opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736ED0/00046ED0 opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736EDB/00046EDB opcode=0A RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736EDF/00046EDF opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=206
....
PC=7FF66C736F9C/00046F9C opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736FA0/00046FA0 opcode=13 RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736F20/00046F20 opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736F2B/00046F2B opcode=08 RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202

So far it doesn't look like much, does it? But by examining the instruction pointer PC values, we can see that opcode 0x13 can change PC significantly. So opcode 0x13 is probably a (conditional) jump instruction.

Let's modify our code and log only jumps. Log file is much shorter, and the final few lines are the most interesting.

PC=7FF66C737713/00047713 opcode=13 RAX=00000000 RBX=2AEC5977B90 RCX=00000008 RDX=7FF66C70F040 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=283
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000031 RBX=2AEC5977B90 RCX=00000043 RDX=000000F0 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=297
PC=7FF66C7377F2/000477F2 opcode=13 RAX=00000031 RBX=2AEC5977B90 RCX=00000043 RDX=000000F0 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=297
PC=7FF66C737844/00047844 opcode=13 RAX=00000000 RBX=2AEC5977B90 RCX=00000043 RDX=000000F0 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=283

Specifically, on 2nd line we can see RAX=31. That's ASCII code of "1", the first character of fake serial that I entered. In RCX we see value 0x43. Could it be the correct first character of serial and this is "goodboy/badboy" jump?

Patching VM context and obtaining the correct serial
Let's modify our logger one more time - on our suspected goodboy jump it will set VM flags to a default value, so that the jump is always taken.

if savedRSI - UInt64(CrackmeImageBase) = $477E4  then begin
   PUInt64(actualRSP+$F0)^ := $246;
end;

We run crackme again, enter a fake serial, and get a good boy message!

PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000031 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000032 RBX=20BB70A78C0 RCX=00000038 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000033 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000034 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000035 RBX=20BB70A78C0 RCX=00000030 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000036 RBX=20BB70A78C0 RCX=00000030 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000037 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000038 RBX=20BB70A78C0 RCX=00000032 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000031 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000031 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000038 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000034 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000037 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000030 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000042 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000039 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000045 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000042 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000035 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000045 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000033 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000041 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000032 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000041 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000044 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000035 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000031 


Now we can take all logged values from RCX and obtain a correct serial:

Spoiler
C86C00C21618470B9C6EB5E3CA2A6D51

 

TL;DR version
This is how the crackme was defeated:

  • Dumped process memory and use it for static analysis in IDA;
    • Avoids all anti-debug tricks;
  • Used simple regex replaces to defeat "junk code";
    • We'll probably break some things but it doesn't matter, as we won't run the broken code;
  • Analyzed VM startup and VM dispatcher;
  • Used DLL injection to get my code running in the crackme process;
    • My code hooks VM dispatcher and dumps VM state before each instruction;
  • Analysis of executed operations allows us to locate goodboy/badboy jump;
    • My hook code patches flags to always take goodboy jump;
    • We can see correct password in VM registers;

 

Attached is a Delphi source code for my VM logger.

 vm_logger.zip

  • Like 8
  • Thanks 6
Link to comment
Share on other sites

17 hours ago, kao said:

And here's my promised solution. As you'll notice, it's a totally different approach than that of ExtremeCoders.. :) It will be published on my blog (https://lifeinhex.com/solving-0x777hs-crackme/) once I take care of all formatting issues.

----

The crackme is an x64 binary that uses a custom protector. Password checking code is protected using a code virtualization feature. In the tutorial I'll show how the protection works and steps I took to defeat it.

This is not a full and comprehensive analysis of the protector code or the code virtualization feature. When solving crackmes, I prefer to choose the simplest solution that gets the job done. This is also reason why I did not use ready-made tools like VMAttack (https://github.com/anatolikalysch/VMAttack), Detours, FRIDA and the like...

Quickly about code virtualization.
Code virtualizers are generally considered one of the hardest software protection methods to defeat. Why is that? Let's see what features a common code virtualization solution offers:

  1. Packer. Original program code is packed/encrypted and decoded on runtime.
  2. Anti-debug protection. Most protectors use some sort of anti-debugging protection in their code.
  3. Code obfuscation. Most protectors add junk code, some use control-flow flattening, constant obfuscation and other techniques.
  4. And finally, the actual virtual machine with custom instruction set.

If you have just a single feature, like junk code, it's actually quite easy to reverse. The difficulty comes from the combination of all protector features and also how well they are combined. 

There are several ways to defeat code virtualization:

  1. completely devirtualize the code. This is the ultimate success, you have recovered the original x86/x64 code, or a close approximation of it;
  2. make a disassembler for the particular VM, disassemble PCode and understand how the algorithm works;
  3. trace the VM execution, and use trace data to understand how the algorithm works;
  4. patch VM handlers and/or PCode;

If you want to learn more about code virtualization in general,  I can wholeheartedly recommend Tim Blazytko's blog (https://synthesis.to/2021/10/21/vm_based_obfuscation.html), as well as his Software Deobfuscation training. They are awesome!

With that in mind, let's look at our crackme and see what protections it contains.

Crackme overview.

Encrypted code.
If you open crackme.exe in your favorite hex editor, you'll notice that .code section appears to be encrypted. OK, maybe you will not notice that. :)

Just check the entropy of each PE section with a tool like DiE:

  Reveal hidden contents

775455168_Pastedimage20221105191801.png.6d9a636783bb6e46cc7baa55df168781.png

So, our first step would be to unpack the file.

Anti-debug protection.
When you try to run the file under x64dbg, you'll notice that it throws some breakpoint exception and terminates:

  Reveal hidden contents

1494133205_Pastedimage20221106184015.png.881b526606de82eee8683ff63e2b6640.png

I spent some time trying different ScyllaHide options but without any success. Debugging the startup code allowed me to note some of the features:

  1.  It uses a lot of Nt* functions;
  2. It manually maps ntdll.dll in memory and (probably) extracts syscall ids;
  3. The rest of the protection uses syscalls directly;
  4. And the protection code is mostly virtualized!

At this point I decided to try something else. Let's run the crackme without the debugger, dump process memory and try to attack it using static analysis!

Note: if Scylla fails to dump the process, use Process Hacker -> Select crackme.exe process -> Properties -> Memory -> select crackme.exe sections -> Save... and then rebuild PE header.

Junk code.
Dumped file is surprisingly readable in IDA. We can soon find a suspicious part in .code section:
 

  Reveal hidden contents

2061883368_Pastedimage20221105211403.png.b5a00007cf82e005f48d24ff43cd2403.png

Following the jump, we see a combination of push constant+call followed by data which is very typical for a VM startup:
 

  Reveal hidden contents

525837096_Pastedimage20221105211515.png.355ca264c9dd3b8ac15ea415b13e0bf5.png

Following that, we see some code that looks like an obfuscated spaghetti code:

  Reveal hidden contents

1971435558_Pastedimage20221105211614.png.ca6acc7615daaeb93b9559aff306b9ea.png


So, it looks like we have located our VM but the code is obfuscated. We'll need to take care of that first.

Deobfuscating junk code.
After spending some time cleaning the junk code, you'll notice it uses several specific patterns for obfuscation. 

jmp+junk

  Reveal hidden contents

1212651691_Pastedimage20221105213148.png.24033760994c0a9134078589e2ba53d8.png


The simplest of patterns - it's a short jump and few junk bytes. We can use hex editor and simple regex to replace this with 5 nops.

clc+jnb and stc+jb

  Reveal hidden contents

689939112_Pastedimage20221105212703.png.cb4930b277ba389f8f9070da36aec1aa.png


First, a carry flag is set to a know value using clc or stc instruction. Then a conditional jump is used to confuse IDA's analysis. Jump distance is usually very short - 2,3 or 4 bytes. Just like before, we can use regex to replace replace clc+jump+junk code with nops.

Big obfuscated do-nothing
Once the simple jumps are replaced, you'll notice a much larger obfuscation pattern:

  Reveal hidden contents

1567694314_Pastedimage20221105214802.png.971693fb7267827cd34e4d4d337846a8.png


The pattern begins with pushfq, followed by call and 2 jumps and ends with the popfq. This example uses RAX, but it can be also RDX or some other register. 
It's easy to find the end of the pattern just by looking for next popfq instruction.

Even larger do-nothing
And finally, there's a more complicated pattern. It's so large that I had to use graphic editor to stitch it all together for you. Notice that all nops, jumps and junk code are removed from the image!

  Reveal hidden contents

1155852852_Pastedimage20221105215843.png.84071d21cafe90a0aa4dd4f1935816e2.png

As with the previous pattern, it's easy to find the end of it, just look for combination of pop rcx, pop eax and popfq. 

And we're done with code obfuscation! :) We've identified obfuscation patterns and found a way to deal with them.

Analyzing VM dispatcher
Now we're able to see what is happening on VM startup. First flags and registers are saved:

.code:00007FF70A36CF38    pushfq
.code:00007FF70A36CF66    push    r15
.code:00007FF70A36D00C    push    r14
.code:00007FF70A36D0BD    push    r13
.code:00007FF70A36D15C    push    r12
... more pushes ...

Then the VM state is prepared and VM dispatcher is reached:

.code:00007FF70A36E6B0   xor     rdx, rdx
.code:00007FF70A36E6E0   mov     dl, [rsi] ; fetch next opcode

And finally next handler is executed:

.code:00007FF70A36E068    mov     rax, rsp
.code:00007FF70A36E11C    add     rax, 0F8h
.code:00007FF70A36E13C    mov     rax, [rax]     ; table of handler addresses
.code:00007FF70A36E13F    xor     rbx, rbx         ; 
.code:00007FF70A36E169    mov     ebx, [rax+rdx*4] ; RDX contains the next opcode
.code:00007FF70A36E199    sub     rax, rbx
.code:00007FF70A36E1C3    jmp     rax              ; ---> next handler is executed


Writing VM tracer
For last few years I've written most of my tools in C#. Now I need to hook x64 code and C# is not really suitable for that. So, I dusted off my trusted old copy of Delphi XE2. 

Also I needed some injector that would inject my DLL into running crackme.exe process. I randomly chose one the first results from Google search: https://github.com/danielkrupinski/Inflame

I chose to hook VM dispatcher between "add rax, 0F8h" and "mov eax,[eax]" instructions. Since I didn't have any decent x64 hooking library for Delphi XE2, I made my own "hooking" code. It's ugly and you definitely shouldn't do that in production code. But for the crackme it's fine!

hookAddress := imageBase + $2E123;
returnAddress := imageBase + $2E13B;
d := PDword(hookAddress)^;
if (d = $000004E8) then begin // check whether the hooked address contains correct bytes
   Writeln(Format('Hooking address %x',[hookAddress]));
   VirtualProtect(pointer(hookAddress), $100, PAGE_EXECUTE_READWRITE, @oldProtection);
   PWord(hookAddress)^ := $BB48; // mov rbx, const
   PUInt64(hookAddress + 2)^ := UInt64(@MyHook);
   PWord(hookAddress + $A)^ := $E3FF;  //jmp rbx
   VirtualProtect(pointer(hookAddress), $100, oldProtection, @oldProtection);
end;

And this is the code responsible for logging VM context. Nothing fancy, just get values from memory and log them to console.

opcode := (savedRDX and $FF);
actualRSP := savedRAX - $F8;
rax := PUInt64(actualRSP+$70)^;
rbx := PUInt64(actualRSP+$78)^;
rcx := PUInt64(actualRSP+$80)^;
rdx := PUInt64(actualRSP+$88)^;
rsi := PUInt64(actualRSP+$90)^;
rdi := PUInt64(actualRSP+$98)^;
rxx := PUInt64(actualRSP+$A0)^;
ryy := PUInt64(actualRSP+$A8)^;
r8 := PUInt64(actualRSP+$B0)^;
r9 := PUInt64(actualRSP+$B8)^;
r10 := PUInt64(actualRSP+$C0)^;
r11 := PUInt64(actualRSP+$C8)^;
r12 := PUInt64(actualRSP+$D0)^;
r13 := PUInt64(actualRSP+$D8)^;
r14 := PUInt64(actualRSP+$E0)^;
r15 := PUInt64(actualRSP+$E8)^;
eflags := PUInt64(actualRSP+$F0)^;
Writeln(Format('PC=%.08x/%.08x opcode=%.02x RAX=%.08x RBX=%.08x RCX=%.08x RDX=%.08x RSI=%.08x RDI=%.08x RXX=%.08x RYY=%.08x R8=%.08x  R9=%.08x R10=%.08x R11=%.08x R12=%.08x R13=%.08x R14=%.08x R15=%.08x EFL=%.02x',[savedRSI, savedRSI - UInt64(CrackmeImageBase), opcode, rax, rbx, rcx, rdx, rsi, rdi, rxx, ryy, r8,r9,r10,r11,r12,r13,r14,r15,eflags]));


Analyzing tracer output
Now, let's run the crackme, inject tracing dll and enter some random serial. We'll get output similar to this:

PC=7FF66C736ECC/00046ECC opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736ED0/00046ED0 opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736EDB/00046EDB opcode=0A RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736EDF/00046EDF opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=206
....
PC=7FF66C736F9C/00046F9C opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736FA0/00046FA0 opcode=13 RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736F20/00046F20 opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736F2B/00046F2B opcode=08 RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202

So far it doesn't look like much, does it? But by examining the instruction pointer PC values, we can see that opcode 0x13 can change PC significantly. So opcode 0x13 is probably a (conditional) jump instruction.

Let's modify our code and log only jumps. Log file is much shorter, and the final few lines are the most interesting.

PC=7FF66C737713/00047713 opcode=13 RAX=00000000 RBX=2AEC5977B90 RCX=00000008 RDX=7FF66C70F040 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=283
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000031 RBX=2AEC5977B90 RCX=00000043 RDX=000000F0 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=297
PC=7FF66C7377F2/000477F2 opcode=13 RAX=00000031 RBX=2AEC5977B90 RCX=00000043 RDX=000000F0 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=297
PC=7FF66C737844/00047844 opcode=13 RAX=00000000 RBX=2AEC5977B90 RCX=00000043 RDX=000000F0 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=283

Specifically, on 2nd line we can see RAX=31. That's ASCII code of "1", the first character of fake serial that I entered. In RCX we see value 0x43. Could it be the correct first character of serial and this is "goodboy/badboy" jump?

Patching VM context and obtaining the correct serial
Let's modify our logger one more time - on our suspected goodboy jump it will set VM flags to a default value, so that the jump is always taken.

if savedRSI - UInt64(CrackmeImageBase) = $477E4  then begin
   PUInt64(actualRSP+$F0)^ := $246;
end;

We run crackme again, enter a fake serial, and get a good boy message!

PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000031 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000032 RBX=20BB70A78C0 RCX=00000038 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000033 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000034 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000035 RBX=20BB70A78C0 RCX=00000030 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000036 RBX=20BB70A78C0 RCX=00000030 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000037 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000038 RBX=20BB70A78C0 RCX=00000032 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000031 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000031 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000038 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000034 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000037 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000030 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000042 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000039 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000045 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000042 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000035 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000045 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000033 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000041 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000032 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000041 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000044 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000035 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000031 


Now we can take all logged values from RCX and obtain a correct serial:

  Reveal hidden contents
C86C00C21618470B9C6EB5E3CA2A6D51

 

TL;DR version
This is how the crackme was defeated:

  • Dumped process memory and use it for static analysis in IDA;
    • Avoids all anti-debug tricks;
  • Used simple regex replaces to defeat "junk code";
    • We'll probably break some things but it doesn't matter, as we won't run the broken code;
  • Analyzed VM startup and VM dispatcher;
  • Used DLL injection to get my code running in the crackme process;
    • My code hooks VM dispatcher and dumps VM state before each instruction;
  • Analysis of executed operations allows us to locate goodboy/badboy jump;
    • My hook code patches flags to always take goodboy jump;
    • We can see correct password in VM registers;

 

Attached is a Delphi source code for my VM logger.

  vm_logger.zip 190.42 kB · 6 downloads

 That's such a good answer ! :)

I wanted to check the lack of code virtualization that I designed through this crackme, and I checked some of it through the comments above.

It's a very difficult problem for me to solve all the defects presented above. (e.g., the speed problem that junk code provides for virtualization targets, or the generation of various junk code, Etc.)

Thank you for solving your busy schedule !
(I hope it was a enjoying challenge for you.)

Edited by 0x777h
  • Confused 1
Link to comment
Share on other sites

Nakashi_omara
17 hours ago, kao said:

And here's my promised solution. As you'll notice, it's a totally different approach than that of ExtremeCoders.. :) It will be published on my blog (https://lifeinhex.com/solving-0x777hs-crackme/) once I take care of all formatting issues.

----

The crackme is an x64 binary that uses a custom protector. Password checking code is protected using a code virtualization feature. In the tutorial I'll show how the protection works and steps I took to defeat it.

This is not a full and comprehensive analysis of the protector code or the code virtualization feature. When solving crackmes, I prefer to choose the simplest solution that gets the job done. This is also reason why I did not use ready-made tools like VMAttack (https://github.com/anatolikalysch/VMAttack), Detours, FRIDA and the like...

Quickly about code virtualization.
Code virtualizers are generally considered one of the hardest software protection methods to defeat. Why is that? Let's see what features a common code virtualization solution offers:

  1. Packer. Original program code is packed/encrypted and decoded on runtime.
  2. Anti-debug protection. Most protectors use some sort of anti-debugging protection in their code.
  3. Code obfuscation. Most protectors add junk code, some use control-flow flattening, constant obfuscation and other techniques.
  4. And finally, the actual virtual machine with custom instruction set.

If you have just a single feature, like junk code, it's actually quite easy to reverse. The difficulty comes from the combination of all protector features and also how well they are combined. 

There are several ways to defeat code virtualization:

  1. completely devirtualize the code. This is the ultimate success, you have recovered the original x86/x64 code, or a close approximation of it;
  2. make a disassembler for the particular VM, disassemble PCode and understand how the algorithm works;
  3. trace the VM execution, and use trace data to understand how the algorithm works;
  4. patch VM handlers and/or PCode;

If you want to learn more about code virtualization in general,  I can wholeheartedly recommend Tim Blazytko's blog (https://synthesis.to/2021/10/21/vm_based_obfuscation.html), as well as his Software Deobfuscation training. They are awesome!

With that in mind, let's look at our crackme and see what protections it contains.

Crackme overview.

Encrypted code.
If you open crackme.exe in your favorite hex editor, you'll notice that .code section appears to be encrypted. OK, maybe you will not notice that. :)

Just check the entropy of each PE section with a tool like DiE:

  Reveal hidden contents

775455168_Pastedimage20221105191801.png.6d9a636783bb6e46cc7baa55df168781.png

So, our first step would be to unpack the file.

Anti-debug protection.
When you try to run the file under x64dbg, you'll notice that it throws some breakpoint exception and terminates:

  Reveal hidden contents

1494133205_Pastedimage20221106184015.png.881b526606de82eee8683ff63e2b6640.png

I spent some time trying different ScyllaHide options but without any success. Debugging the startup code allowed me to note some of the features:

  1.  It uses a lot of Nt* functions;
  2. It manually maps ntdll.dll in memory and (probably) extracts syscall ids;
  3. The rest of the protection uses syscalls directly;
  4. And the protection code is mostly virtualized!

At this point I decided to try something else. Let's run the crackme without the debugger, dump process memory and try to attack it using static analysis!

Note: if Scylla fails to dump the process, use Process Hacker -> Select crackme.exe process -> Properties -> Memory -> select crackme.exe sections -> Save... and then rebuild PE header.

Junk code.
Dumped file is surprisingly readable in IDA. We can soon find a suspicious part in .code section:
 

  Reveal hidden contents

2061883368_Pastedimage20221105211403.png.b5a00007cf82e005f48d24ff43cd2403.png

Following the jump, we see a combination of push constant+call followed by data which is very typical for a VM startup:
 

  Reveal hidden contents

525837096_Pastedimage20221105211515.png.355ca264c9dd3b8ac15ea415b13e0bf5.png

Following that, we see some code that looks like an obfuscated spaghetti code:

  Reveal hidden contents

1971435558_Pastedimage20221105211614.png.ca6acc7615daaeb93b9559aff306b9ea.png


So, it looks like we have located our VM but the code is obfuscated. We'll need to take care of that first.

Deobfuscating junk code.
After spending some time cleaning the junk code, you'll notice it uses several specific patterns for obfuscation. 

jmp+junk

  Reveal hidden contents

1212651691_Pastedimage20221105213148.png.24033760994c0a9134078589e2ba53d8.png


The simplest of patterns - it's a short jump and few junk bytes. We can use hex editor and simple regex to replace this with 5 nops.

clc+jnb and stc+jb

  Reveal hidden contents

689939112_Pastedimage20221105212703.png.cb4930b277ba389f8f9070da36aec1aa.png


First, a carry flag is set to a know value using clc or stc instruction. Then a conditional jump is used to confuse IDA's analysis. Jump distance is usually very short - 2,3 or 4 bytes. Just like before, we can use regex to replace replace clc+jump+junk code with nops.

Big obfuscated do-nothing
Once the simple jumps are replaced, you'll notice a much larger obfuscation pattern:

  Reveal hidden contents

1567694314_Pastedimage20221105214802.png.971693fb7267827cd34e4d4d337846a8.png


The pattern begins with pushfq, followed by call and 2 jumps and ends with the popfq. This example uses RAX, but it can be also RDX or some other register. 
It's easy to find the end of the pattern just by looking for next popfq instruction.

Even larger do-nothing
And finally, there's a more complicated pattern. It's so large that I had to use graphic editor to stitch it all together for you. Notice that all nops, jumps and junk code are removed from the image!

  Reveal hidden contents

1155852852_Pastedimage20221105215843.png.84071d21cafe90a0aa4dd4f1935816e2.png

As with the previous pattern, it's easy to find the end of it, just look for combination of pop rcx, pop eax and popfq. 

And we're done with code obfuscation! :) We've identified obfuscation patterns and found a way to deal with them.

Analyzing VM dispatcher
Now we're able to see what is happening on VM startup. First flags and registers are saved:

.code:00007FF70A36CF38    pushfq
.code:00007FF70A36CF66    push    r15
.code:00007FF70A36D00C    push    r14
.code:00007FF70A36D0BD    push    r13
.code:00007FF70A36D15C    push    r12
... more pushes ...

Then the VM state is prepared and VM dispatcher is reached:

.code:00007FF70A36E6B0   xor     rdx, rdx
.code:00007FF70A36E6E0   mov     dl, [rsi] ; fetch next opcode

And finally next handler is executed:

.code:00007FF70A36E068    mov     rax, rsp
.code:00007FF70A36E11C    add     rax, 0F8h
.code:00007FF70A36E13C    mov     rax, [rax]     ; table of handler addresses
.code:00007FF70A36E13F    xor     rbx, rbx         ; 
.code:00007FF70A36E169    mov     ebx, [rax+rdx*4] ; RDX contains the next opcode
.code:00007FF70A36E199    sub     rax, rbx
.code:00007FF70A36E1C3    jmp     rax              ; ---> next handler is executed


Writing VM tracer
For last few years I've written most of my tools in C#. Now I need to hook x64 code and C# is not really suitable for that. So, I dusted off my trusted old copy of Delphi XE2. 

Also I needed some injector that would inject my DLL into running crackme.exe process. I randomly chose one the first results from Google search: https://github.com/danielkrupinski/Inflame

I chose to hook VM dispatcher between "add rax, 0F8h" and "mov eax,[eax]" instructions. Since I didn't have any decent x64 hooking library for Delphi XE2, I made my own "hooking" code. It's ugly and you definitely shouldn't do that in production code. But for the crackme it's fine!

hookAddress := imageBase + $2E123;
returnAddress := imageBase + $2E13B;
d := PDword(hookAddress)^;
if (d = $000004E8) then begin // check whether the hooked address contains correct bytes
   Writeln(Format('Hooking address %x',[hookAddress]));
   VirtualProtect(pointer(hookAddress), $100, PAGE_EXECUTE_READWRITE, @oldProtection);
   PWord(hookAddress)^ := $BB48; // mov rbx, const
   PUInt64(hookAddress + 2)^ := UInt64(@MyHook);
   PWord(hookAddress + $A)^ := $E3FF;  //jmp rbx
   VirtualProtect(pointer(hookAddress), $100, oldProtection, @oldProtection);
end;

And this is the code responsible for logging VM context. Nothing fancy, just get values from memory and log them to console.

opcode := (savedRDX and $FF);
actualRSP := savedRAX - $F8;
rax := PUInt64(actualRSP+$70)^;
rbx := PUInt64(actualRSP+$78)^;
rcx := PUInt64(actualRSP+$80)^;
rdx := PUInt64(actualRSP+$88)^;
rsi := PUInt64(actualRSP+$90)^;
rdi := PUInt64(actualRSP+$98)^;
rxx := PUInt64(actualRSP+$A0)^;
ryy := PUInt64(actualRSP+$A8)^;
r8 := PUInt64(actualRSP+$B0)^;
r9 := PUInt64(actualRSP+$B8)^;
r10 := PUInt64(actualRSP+$C0)^;
r11 := PUInt64(actualRSP+$C8)^;
r12 := PUInt64(actualRSP+$D0)^;
r13 := PUInt64(actualRSP+$D8)^;
r14 := PUInt64(actualRSP+$E0)^;
r15 := PUInt64(actualRSP+$E8)^;
eflags := PUInt64(actualRSP+$F0)^;
Writeln(Format('PC=%.08x/%.08x opcode=%.02x RAX=%.08x RBX=%.08x RCX=%.08x RDX=%.08x RSI=%.08x RDI=%.08x RXX=%.08x RYY=%.08x R8=%.08x  R9=%.08x R10=%.08x R11=%.08x R12=%.08x R13=%.08x R14=%.08x R15=%.08x EFL=%.02x',[savedRSI, savedRSI - UInt64(CrackmeImageBase), opcode, rax, rbx, rcx, rdx, rsi, rdi, rxx, ryy, r8,r9,r10,r11,r12,r13,r14,r15,eflags]));


Analyzing tracer output
Now, let's run the crackme, inject tracing dll and enter some random serial. We'll get output similar to this:

PC=7FF66C736ECC/00046ECC opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736ED0/00046ED0 opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736EDB/00046EDB opcode=0A RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736EDF/00046EDF opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=FFFFFFFF RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=206
....
PC=7FF66C736F9C/00046F9C opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736FA0/00046FA0 opcode=13 RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736F20/00046F20 opcode=0C RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202
PC=7FF66C736F2B/00046F2B opcode=08 RAX=59652FF960 RBX=20CE5537D10 RCX=00000000 RDX=7FF66C70F040 RSI=00000000 RDI=59652FF9C4 RXX=59652FF8E0 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000050 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=202

So far it doesn't look like much, does it? But by examining the instruction pointer PC values, we can see that opcode 0x13 can change PC significantly. So opcode 0x13 is probably a (conditional) jump instruction.

Let's modify our code and log only jumps. Log file is much shorter, and the final few lines are the most interesting.

PC=7FF66C737713/00047713 opcode=13 RAX=00000000 RBX=2AEC5977B90 RCX=00000008 RDX=7FF66C70F040 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=283
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000031 RBX=2AEC5977B90 RCX=00000043 RDX=000000F0 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=297
PC=7FF66C7377F2/000477F2 opcode=13 RAX=00000031 RBX=2AEC5977B90 RCX=00000043 RDX=000000F0 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=297
PC=7FF66C737844/00047844 opcode=13 RAX=00000000 RBX=2AEC5977B90 RCX=00000043 RDX=000000F0 RSI=00000000 RDI=BB03B3F734 RXX=BB03B3F650 RYY=00000000 R8=7FF66C710520  R9=7FF66C6F0000 R10=00000054 R11=7FF66C710520 R12=00000000 R13=00000000 R14=00000000 R15=00000000 EFL=283

Specifically, on 2nd line we can see RAX=31. That's ASCII code of "1", the first character of fake serial that I entered. In RCX we see value 0x43. Could it be the correct first character of serial and this is "goodboy/badboy" jump?

Patching VM context and obtaining the correct serial
Let's modify our logger one more time - on our suspected goodboy jump it will set VM flags to a default value, so that the jump is always taken.

if savedRSI - UInt64(CrackmeImageBase) = $477E4  then begin
   PUInt64(actualRSP+$F0)^ := $246;
end;

We run crackme again, enter a fake serial, and get a good boy message!

PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000031 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000032 RBX=20BB70A78C0 RCX=00000038 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000033 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000034 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000035 RBX=20BB70A78C0 RCX=00000030 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000036 RBX=20BB70A78C0 RCX=00000030 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000037 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000038 RBX=20BB70A78C0 RCX=00000032 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000031 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000031 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000038 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000034 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000037 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000030 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000042 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000039 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000045 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000042 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000035 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000045 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000033 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000043 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000041 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000032 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000041 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000036 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000044 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000035 
PC=7FF66C7377E4/000477E4 opcode=13 RAX=00000000 RBX=20BB70A78C0 RCX=00000031 


Now we can take all logged values from RCX and obtain a correct serial:

  Reveal hidden contents
C86C00C21618470B9C6EB5E3CA2A6D51

 

TL;DR version
This is how the crackme was defeated:

  • Dumped process memory and use it for static analysis in IDA;
    • Avoids all anti-debug tricks;
  • Used simple regex replaces to defeat "junk code";
    • We'll probably break some things but it doesn't matter, as we won't run the broken code;
  • Analyzed VM startup and VM dispatcher;
  • Used DLL injection to get my code running in the crackme process;
    • My code hooks VM dispatcher and dumps VM state before each instruction;
  • Analysis of executed operations allows us to locate goodboy/badboy jump;
    • My hook code patches flags to always take goodboy jump;
    • We can see correct password in VM registers;

 

Attached is a Delphi source code for my VM logger.

  vm_logger.zip 190.42 kB · 6 downloads

Wow, and finally, the master's last blow!

Complete and comprehensive and high level, I was exactly like this after reading kao analysis (🥴)

Of course, many parts are not understandable for me, a beginner, because I have no knowledge and idea of virtualization and working with IDA, and I can't enter these topics at the moment because I am a beginner and these topics are difficult for me😅!

 

1 hour ago, 0x777h said:

Maybe this is the answer for me. Through this crackme, I wanted to confirm the lack of code virtualization that I designed, and I confirmed it through the above comments.

To my shame, it is a very difficult problem for me to solve all the defects presented above. (e.g., the speed problem that junk code provides for virtualization targets, or the generation of various junk code, Etc.)

It can be released as it is, but I will continue to try to improve it ! :) 

Thank you for solving your busy schedule !
(I hope it was a enjoying challenge for you.)

Is it going to be a commercial tool? Or is it developed just for fun?

May I ask what language did you use to develop it and did you do all the coding yourself or did you use ready-made tools or ready-made codes?

Your tool can stop novice crackers like me, because many novice crackers don't take the time to manually unpack unknown or new packers and protectors, and most likely give up on cracking (unless the unpacker is available be).

In any case, I hope you are successful👍🙏

  • Thanks 1
Link to comment
Share on other sites

41 minutes ago, Nakashi_omara said:

Wow, and finally, the master's last blow!

Complete and comprehensive and high level, I was exactly like this after reading kao analysis (🥴)

Of course, many parts are not understandable for me, a beginner, because I have no knowledge and idea of virtualization and working with IDA, and I can't enter these topics at the moment because I am a beginner and these topics are difficult for me😅!

 

Is it going to be a commercial tool? Or is it developed just for fun?

May I ask what language did you use to develop it and did you do all the coding yourself or did you use ready-made tools or ready-made codes?

Your tool can stop novice crackers like me, because many novice crackers don't take the time to manually unpack unknown or new packers and protectors, and most likely give up on cracking (unless the unpacker is available be).

In any case, I hope you are successful👍🙏

Some codes wanted you to enjoy this crackme. Some code is, um... It was my challenge.

The project was developed in c++/asm code. I developed them all, but unfortunately, the disassembly was replaced for accuracy. 😅

Thank you for enjoying my crackme !😊👍

  • Like 1
Link to comment
Share on other sites

  • 3 weeks later...
  • The title was changed to Users Desktop CrackMe #1

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...