Crackme Exploration
The crackme repeatedly asks us to input a key until we get it right. According to the description, it should open a website after the correct key is entered. Since I don’t know what website it’ll open, I’m going to turn off my Internet anyway.
At first glance, the crackme seems to be solvable using solely a debugger. We’ll try to get the correct key dynamically first and explore the key construction/decryption algorithm (if there’s any) later.
Cracking The Crackme
We’re going to load the crackme into Binary Ninja and start debugging.
From the start() function, start doing code stepping until it leads to a function with debugger checks and some anti-VM protections.
Scrolling down a little bit, we can see a MessageBox with the message Access granted. Therefore the logic for checking the key is definitely located near here.
Bypassing The Checks
Let’s bypass the debugger check first. For this IsDebuggerPresent(), I’m going to just simply use Patch → Never Branch.
We’ll also do that for the next if statement as well…
Alternatively, you can also set a breakpoint at the test eax, eax instruction and set the value of eax to 0. Do the same for the instruction after the GetLastError call. As for the cmp, set the value of r15b to 0 to bypass the jne as well.
Then we should meet the anti-VM checks. Since I’m not using a VM here, I’ll just skip this part. If you’re doing all of this in a VM, you should be able to bypass these checks pretty easily (similar to those above)
Once again, we can see another debugger check that is wrapped inside a while loop. This loop is extremely annoying btw. Then I noticed that if we invert the last if statement, we can exit the loop (actually all of the debugger checks altogether)! So we just use Patch → Invert Branch and then bypass the whole thing!
Our next obstacle is the ScyllaHide check. We use Patch → Invert Branch once again at the if statement for Module32Next to get around this.
To bypass some NtQueryInformationProcess things, we’ll use Patch → Never Branch on the if rax_37 == 0xc3 || rax_37 == 0xe9 statement.
And use Never Branch once again at the if (hModule && rax_34 && (uint8_t*)rax_34 == 0xc3) statement.
We should be able to continue stepping the code.
We Found The Key?
After a while, a mysterious string, aikmnioi, appeared at the Register pane. Is it the correct key or just a random string?
After plugging the string into the crackme, it showed an error MessageBox, however the message shown was Access granted.. Then my browser popped up to a website at an IP address. I guess the error sign of the message box is just a decoy, and aikmnioi is indeed the correct key.
Also I noticed the sub_7ff610cb1fb0 function. It seems to me that this is the one that does the key decoding. Before the call to sub_7ff610cb1fb0, there is a strncpy function that copies 4<>8;<:< into &arg1[0xa], and &arg1[0xa] is fed into sub_7ff610cb1fb0. Looking at the patterns and comparing the length of this string with aikmnioi, this is probably the encrypted string for aikmnioi.
A Second Correct Key?
I continue stepping the code to poke around a little bit. The crackme asked for input, and I just typed a random key 123.
Then we can see the sub_7ff610cb1fb0 once again. I checked the Register pane and the string letmein appeared. Is this another password? I don’t know.
Typing letmein into the crackme just made a message box to show up, but this time it’s not an error message box. However, no website was opened, and the crackme just repeatedly ask for the key without exiting. It’s funny that the author included a second key but it just doesn’t work.
Website Address Revealed
Since I typed the password 123 into the debugging crackme earlier, I let it continue running until it prints Wrong! and prompts me to enter another key.
This time, I typed in aikmnioi and continue stepping.
The Access granted! message box popped up. Then I saw the decoding function once again and the website address that the crackme was trying to open upon entering the correct key!
The Decoding Function
Since we knew that sub_7ff610cb1fb0 is the decoding function, so I’ll just go ahead and rename this to DecodeString.
From the usage of the functions (in the screenshots above), seems like arg2 is a pointer to the encrypted string while arg1 is the result string.
And this is the (somewhat) cleaned DecodeString function:
int128_t* DecodeString(int128_t* dest, int64_t* src, int64_t arg3) {
int128_t* dest_ptr = dest;
int32_t temp_var = 0;
*(uint128_t*)dest = {0};
dest[1] = 0;
*(uint64_t*)((char*)dest + 0x18) = 0xf;
*(uint8_t*)dest = 0;
int32_t initialized = 1;
int64_t length = src[2];
if (length > 0xf) {
arg3 = sub_7ff610cb32f0(dest, length);
dest[1] = 0;
}
int64_t* src_base = src;
int64_t* src_data;
if (src[3] <= 0xf)
src_data = src;
else {
src_base = *(uint64_t*)src;
src_data = src_base;
}
int64_t* src_iter = src_base;
void* src_end = src[2] + src_data;
if (src_base != src_end) {
do {
char decoded_char = *(uint8_t*)src_iter ^ 0x55;
int64_t current_len = dest[1];
int64_t capacity = *(uint64_t*)((char*)dest + 0x18);
if (current_len >= capacity)
arg3 = sub_7ff610cb3440(dest, capacity, arg3, decoded_char);
else {
dest[1] = current_len + 1;
int128_t* dest_buf = dest;
if (capacity > 0xf)
dest_buf = *(uint64_t*)dest;
*(uint8_t*)((char*)dest_buf + current_len) = decoded_char;
*(uint8_t*)((char*)dest_buf + current_len + 1) = 0;
}
src_iter += 1;
} while (src_iter != src_end);
}
return dest;
}At a glance, seems like the string structure is actually an SSOString. But we don’t need to care about that. We only need to care about these lines here:
if (src_base != src_end) {
do {
char decoded_char = *(uint8_t*)src_iter ^ 0x55;
int64_t current_len = dest[1];
int64_t capacity = *(uint64_t*)((char*)dest + 0x18);
if (current_len >= capacity)
arg3 = sub_7ff610cb3440(dest, capacity, arg3, decoded_char);
else {
dest[1] = current_len + 1;
int128_t* dest_buf = dest;
if (capacity > 0xf) dest_buf = *(uint64_t*)dest;
*(uint8_t*)((char*)dest_buf + current_len) = decoded_char;
*(uint8_t*)((char*)dest_buf + current_len + 1) = 0;
}
src_iter += 1;
} while (src_iter != src_end);
}The decoding logic is super simple and can be replicated using this python one-liner:
print(''.join(chr(ord(c) ^ 0x55) for c in encoded_string))Let’s try with some encoded string taken from the crackme!
>>> print(''.join(chr(ord(c) ^ 0x55) for c in "4<>8;<:<"))
aikmnioi
>>> print(''.join(chr(ord(c) ^ 0x55) for c in "90!80<;"))
letmein
>>> print(''.join(chr(ord(c) ^ 0x55) for c in "=!!%ozzbd{dl{dac{dcdz"))
http://71.19.146.161/Thoughts
This is an interesting crackme. The author really did put a lot of effort into implementing debugger checks, anti-VM checks, and even anti-anti-debugger checks. However, they all can be bypassed pretty easily. I did think of patching the binary to get to the goodboy message box from the function start though, but since the message box content is being constructed somewhere before, I only received an empty or a message box with random text.
I still find it funny that the author put a non-working second key in here. I think the author’s intent would be to fool people who just patch the crackme to jump directly to the fake goodboy, as the Access Granted text is just there unhidden rather than being constructed manually.
Overall, solving this crackme is super fun, and it’s cool to see how people implemented so many different anti-debugger/VM checks!



















