Tags: pe windows reversing
Rating: 5.0
Do you have a key?
Running file
on the executable, you can see this is a Windows console executable:
$ file crack_me.exe
crack_me.exe: PE32 executable (console) Intel 80386, for MS Windows
To gather some context, run this in Windows. You're prompted to enter a key. If you try a random string of characters,
the console application prints Invalid key!
and exits.
$ ./crack_me.exe
Enter a key: test
Invalid key!
As this is a reversing challenge static analysis would be helpful. I typically use a combination of
Ghidra and Hopper.
If you open the file in Ghidra you'll see a symbol called entry
is loaded, which looks like the entrypoint to the
application. Decompiling the function we can see that this is just a wrapper for our application. There's a variable
already named _Code
, which looks like it probably calls into the application code.
Clicking through to function FUN_00403630
, we see that our assumption is correct - there's a call to prompt for the
user to enter a key and much more.
setlocale(0,"Russian");
FUN_004022c0((int)local_2c);
local_8 = 0;
FUN_00401440((int *)cout_exref,"Enter the key: ");
FUN_004013f0(cin_exref,local_2c);
local_34 = FUN_00403310(local_2c,"_",0);
if ((local_34 == -1) || (iVar2 = FUN_00403310(local_2c,"_",local_34 + 1), iVar2 == -1)) {
FUN_004032f0();
}
iVar2 = FUN_004033f0(local_2c);
if (iVar2 != 0xf) {
FUN_004032f0();
}
pcVar8 = (char *)FUN_00402850(local_2c,0);
if (*pcVar8 != 'r') {
FUN_004032f0();
}
pcVar8 = "everse";
local_3c = FUN_004035a0(local_2c,local_64,1,6);
local_8._0_1_ = 1;
local_38 = local_3c;
uVar3 = operator!=<>(local_3c,pcVar8);
local_2d = (char)uVar3;
local_8 = (uint)local_8._1_3_ << 8;
~_String_alloc<>(local_64);
if (local_2d != '\0') {
FUN_004032f0();
}
pcVar8 = (char *)FUN_00402850(local_2c,8);
if (*pcVar8 == 'i') {
pcVar8 = (char *)FUN_00402850(local_2c,8);
cVar1 = *pcVar8;
pcVar8 = (char *)FUN_00402850(local_2c,9);
if ((int)cVar1 + (int)*pcVar8 == 0xdc) goto LAB_00403782;
}
fail();
LAB_00403782:
...
From this snippet, we can see:
FUN_00401440
prints to standard outFUN_004013f0
reads from standard in and puts the result in local_2c
FUN_00403310
looks like it's looking for the first index of a character in a stringFUN_004032f0
is called in several condition blocks, so is likely used to short-circuit the application if a
condition is not met - clicking through, you'll see a call to print Invalid key!
to the console and a call to
exit(0)
to exit the applicationFUN_004033f0
looks like a length check - the condition that follows checks if the return value is 0xf
FUN_00402850
looks like a call to get the character at an indexFUN_004035a0
is less obvious but looks like a substring functionKnowing all of this, we can change the variable and function names using L
to make the code easier to read:
setlocale(0,"Russian");
FUN_004022c0((int)inputKey);
local_8 = 0;
print((int *)cout_exref,"Enter the key: ");
readString(cin_exref,inputKey);
underscoreIndex = strpos(inputKey,"_",0);
if ((underscoreIndex == -1) || (iVar2 = strpos(inputKey,"_",underscoreIndex + 1), iVar2 == -1)) {
fail();
}
inputKeyLength = strlen(inputKey);
if (inputKeyLength != 0xf) {
fail();
}
charAtIndex0 = (char *)charAt(inputKey,0);
if (*charAtIndex0 != 'r') {
fail();
}
pcVar8 = "everse";
local_3c = substring(inputKey,local_64,1,6);
local_8._0_1_ = 1;
local_38 = local_3c;
uVar3 = operator!=<>(local_3c,pcVar8);
local_8 = (uint)local_8._1_3_ << 8;
~_String_alloc<>(local_64);
if ((char)uVar3 != '\0') {
fail();
}
pcVar8 = (char *)charAt(inputKey,8);
if (*pcVar8 == 'i') {
pcVar8 = (char *)charAt(inputKey,8);
cVar1 = *pcVar8;
pcVar8 = (char *)charAt(inputKey,9);
if ((int)cVar1 + (int)*pcVar8 == 0xdc) goto LAB_00403782;
}
fail();
LAB_00403782:
...
This is much easier to read, and we can now determine that the key:
0xf
) characters longr
as the first charactereverse
after r
i
s
(*pcVar8 = 0xdc - cVar1
, where cVar1
is 0x69
, the hex char code of i
)The key therefore starts with reverse_is
(we don't know for certain that the _
is there, but it would be consistent
with the typical flag format).
The next section is interesting, because it isn't a straight character/string comparison. Ghidra also gets the first
few lines wrong in its disassembly (e.g. local_4c = &stack0xffffff5c
should be local_4c = 0xfffffff5
, so I've
cleaned that up below (this is also a good reason to make use of multiple tools):
LAB_00403782:
pcVar8 = "fine";
local_4c = 0xfffffff5;
substring(inputKey,&stack0xffffff5c,0xb,4);
local_44 = (void *)FUN_00403090(local_7c);
local_8._0_1_ = 2;
local_40 = local_44;
uVar3 = operator!=<>(local_44,pcVar8);
local_8 = (uint)local_8._1_3_ << 8;
~_String_alloc<>(local_7c);
if ((char)uVar3 != '\0') {
fail();
}
underscoreIndex = strpos(inputKey,"e",0);
while (underscoreIndex != -1) {
puVar7 = (undefined *)charAt(inputKey,underscoreIndex);
*puVar7 = 0x33;
underscoreIndex = strpos(inputKey,"e",0);
}
pcVar8 = "}\n";
puVar7 = inputKey;
pcVar6 = "InnoCTF{";
pcVar5 = FUN_00401f80;
this = (basic_ostream<char,struct_std::char_traits<char>_> *)print((int *)cout_exref,"Success!!!")
;
piVar4 = (int *)operator<<(this,pcVar5);
piVar4 = (int *)print(piVar4,pcVar6);
piVar4 = (int *)FUN_00401410(piVar4,puVar7);
print(piVar4,pcVar8);
system("pause");
local_48 = 0;
local_8 = 0xffffffff;
~_String_alloc<>(inputKey);
*in_FS_OFFSET = local_10;
FUN_004039b7();
return;
There's a reference to the word fine
, the number 0xfffffff5
, and 4 characters from index 11 (0xb
) of inputKey
are extracted. This is followed by a call to FUN_00403090
, though Ghidra gets the parameter count wrong in the
disassembled code. I've extracted the interesting part and replaced &stack0x00000008
with keySuffix
(i.e. the
last 4 characters of inputKey
):
while( true ) {
uVar1 = strlen(keySuffix);
if (uVar1 <= local_30) break;
pcVar2 = (char *)charAt(keySuffix,local_30);
iVar3 = isupper((int)*pcVar2);
if (iVar3 == 0) {
pcVar2 = (char *)charAt(keySuffix,local_30);
FUN_00402980(local_2c,(char)(((int)*pcVar2 + -0x61 + in_stack_00000020) % 0x1a) + 'a');
}
else {
pcVar2 = (char *)charAt(keySuffix,local_30);
FUN_00402980(local_2c,(char)(((int)*pcVar2 + -0x41 + in_stack_00000020) % 0x1a) + 'A');
}
local_30 = local_30 + 1;
}
The loop here iterates over each character and applies some simple math operations using values depending on the character case. This looks similar to a shift cipher. Taking the lowercase expression for example:
pcVar2 = (char *)charAt(keySuffix,local_30);
FUN_00402980(local_2c,(char)(((int)*pcVar2 + -0x61 + in_stack_00000020) % 0x1a) + 'a');
This translates to the equivalent JavaScript code:
const charCode = keySuffix.charCodeAt(i);
FUN_00402980(local_2c, String.fromCharCode(((charCode - 0x61 + in_stack_00000020) % 0x1a) + 0x61);
It's not obvious where in_stack_00000020
comes from, so look at the assembly code:
0x0040314e MOV ECX, dword ptr [EBP + Stack[0x20]]
0x00403151 LEA ECX, [EAX + ECX*0x1 + -0x61]
...
The value of ECX
comes from a reference to the stack used by the previous function. You can get the value by setting
a breakpoint using a debugger like x32dbg, or recall that the value 0xfffffff5
was
defined immediately after the string fine
. Be careful with how your translate this into code because we're working
with 32-bit values and the result will overflow. Our cipher becomes:
function encode(charCode) {
// bitwise AND with 0xffffffff to make the result a 32-bit value
return ((charCode - 0x61 + 0xfffffff5) & 0xffffffff) % 0x1a + 0x61;
}
This makes the key trivial to bruteforce, and we end up with the value qtyp
.
Our key is now reverse_is_qtyp
, which satisfies the constraints we defined earlier - it has 2+ underscores and is 15
characters long. Entering this into the application:
$ ./crack_me.exe
Enter a key: reverse_is_qtyp
Success!!!
InnoCTF{r3v3rs3_is_qtyp}