Keygenme: duckzzy's KeygenMe
MD5: bb20f4ae2a79145210d164691ac7a481
SHA1: c13928fc9e65b37a6debd67e3f398614943e9fa4
SHA256: 72cfda0045fdf90e5a7965dd76690bba473e4dd87c86ff11a46704f1930d9ff7
This is listed as a C/C++ x64 executable with a difficulty rating of 2.0. The author is duckzzy and the executable is written for Windows.
The authors description:
Good luck! :)
I decided to tackle this keygenme today to hopefully help someone out. In the crackmes.one discord server, a user by the name of British_UK was having some issues solving this challenge so I thought I'd give it a shot.
So without further-ado, lets get this loaded in DIE and see what we can learn. First thing that jumps out at me is the high number of sections. Das weird. Its doesn't appear to be packed though. Finally, it is a console application.
The strings don't appear to be encoded/encrypted/obfuscated/choose latest buzzword here. It appears as though this executable was originally written in C++. I will note that DIE picked up a lot of garbage data which contributed to a large number of "strings" that I had to scroll through.
Let's take a moment to talk about the functions that we're seeing. First the good, the validation looks promising. That's it. Now let's talk about the bad. Peep the encrypt/decrypt functions... perhaps our earlier assumption may have been incorrect and maybe strings are [insert fancy buzzword here]. Next, I often see IsDebuggerPresent included in some binaries as part of the
CRT but judging by the C++ name mangling, this application is explicitly
calling the function. That paired with the TimingChecks function, I'm totally expecting there to be some anti-debugging tricks.
This is also confirmed by British_UK's next message in the discord server.
Don't you worry bro... once I get through looking at all these strings. We'll get you straightened out (hopefully).
Now that I've finished skimming over the 23,000 strings, I'm going to load it into Ghidra to begin analyzing. While that is doing it's thing, lets run the executable and see its behavior.
The functionality appears basic. It doesn't seem like there is much happening. Just asks for input and tells you if you got it wrong. Presumably, it also tells you if you got it right.
Let's go over to Ghidra and see what we can discover. This is where Ghidra dropped me which is exactly where we want to be. Even without labeling variables, we can get a good idea for what is going on.
The first thing that I want to do is peek at that isDebuggerPresent function. Here is what that function looks like with my labeling. It calls IsDebuggerPresent and if it returns true, it attempts to call an invalid memory address.
We can verify this by setting a breakpoint at the start of this function and stepping through the code. As you can see, RAX is loaded with the value 0. Then a call is made which results in an exception being thrown.
To bypass this, we could set a breakpoint after the call to IsDebuggerPresent and modify the RAX register to show 0. You could modify the JE to always take the jump. There are many ways to tackle this problem but here's what I'm going to go with. I'm just replacing all the instructions with NOPs. The first instruction I'm setting RAX to 0 and returning.
After saving the patched executable and executing the binary we see that we can now run through the program without issue. I do have a breakpoint set after the call to the main function to pause the execution so we could see the output without it immediately closing.
Going back to our main function in Ghidra, we can see that the first instruction that is executed if we successfully bypass the isDebuggerPresent function is a call to timingCheck. Taking a look at that function, we see a call to GetTickCount64 to get a baseline. Then, it looks like
there's a computationally heavy for loop before a second call to
GetTickCount64. This is used to calculate how much time has passed since
the first call. If more than 0x96 milliseconds have passed, the
executable will terminate.
Let's not waste CPU cycles and just NOP out the function call all together.
* I did save my patches to a new executable. *
The encryption algorithm isn't important but I thought you may like to see what it is doing so let's talk about it. A char vector is passed to to the encryption function along with the plaintext string. It's a simple XOR operation with the key of 0xAB.
So at this point, we've successfully circumvented the anti-debugging code. We know how the string encryption algorithm works. What we need to do now is find where the program is determining whether we have a valid key or not and we need to figure out how it works. Let's take a look at the main function and begin labeling variables. After completing this task, it will hopefully become apparent that we need to take a closer look at the validate function.
We can verify that this is the case by going to this section of the code in the debugger and placing a breakpoint after the call to validate. We can than modify EAX to 1 and allow the executable to run. This will result in the goodboy message being displayed.
At this point I tried to implement the algorithm using the Ghidra decompilation but wasn't having much luck so I decided to try IDA which gave me a much cleaner slate to work with. As you can see, our username must be 15 characters long. Our password must also be 15 characters long. To successfully keygen this application, we need to re-implement the code below the second if statement.
Here is what that would look like in C++.
The output looks like garbage but this is a valid keygenerator as you can see below. When I try my username with the password "1234" I get an access denied. When I try with the output from my keygen, I get the goodboy. When I try a different username with the same password, I get access denied.


Comments
Post a Comment