Reversing: Eat the cake
Eat The Cake is a reversing challenge in the medium category on Hack The Box.
Identification
Unpacking the zip archive shows that the binary is called cake.exe
, apparently
an PE
executable. Lets check it using the die
application.
The most important thing to note here is that the binary was packed with upx
.
This means that we will need to unpack it in order to analyze it.
Unpacking
Using upx
the binary can be unpacked. I like to safe the binary with a new
name to indicate what tool produced it. Here I unpack it to cake-upx.exe
.
Sadly you notice that running the application does nothing. There is a good
reason for this, explained nicely on the SANS blog in 2014. Some software uses
hardcoded addresses, which due to unpacking using upx
, are no longer valid.
Luckily ASLR can be turned off. Instead of doing it for the entire operating system this can be done for the executable only by using setdllcharacteristics.
Now that DYNAMIC_BASE
, or ASLR, is turned off, the unpacked executable runs as expected.
Strings
Running strings
on the binary yields little help. Apparently there is a need
to enter 2 passwords to get to the flag.
A challenge brought to you by little_pwnie ;)
Please enter the 10-digit password (Only numbers and capital letters):
Please enter the 15-digit password (Only numbers and capital letters):
Congratulations! Now go validate your flag!
Better luck next time...
string too long
invalid string position
Running it
Lets give the unpacked binary a try and enter some passwords. Experimenting with the 1st password it seems to just accept anything. The 2nd password will accept an input of 15 characters.
Time to look at some code!
Ghidra
When opening up a binary in Ghidra I first want to see the entry
functions. In
the Symbol Tree
window select Exports
and there should only be the function
entry
.
Reading the code it shows a lot of setup stuff for the terminal. The application
uses the terminal so this is to be expected. At the end there is a function call
that seems to determine the outcome of the application, FUN_00401350
.
DAT_0040544c = FUN_00401350();
if (_DAT_00405450 != 0) {
if (_DAT_00405448 == 0) {
_cexit();
}
return DAT_0040544c;
}
/* WARNING: Subroutine does not return */
exit(DAT_0040544c);
Examining the function indeed shows us the output we saw before on the screen. I
like to rename the function, in case I need to stop and get back to it later.
This can be done by pressing L
when on the function name, or the right click
Edit function signature
. I renamed it to challenge
.
The decompilation window of Ghidra now shows us the beautiful ascii art and a
lot of code that works on single characters. Each line with ascii art is called
with function FUN_00401ba0
, lets rename that one to printLine
, again just
press L
to rename the function.
Then the answers to the questions are read into the program using the following code:
FUN_00401de0((int *)cin_exref,local_438);
From this it is clear that FUN_00401de0
is a scan function, taking std::cin
as input and putting the result in local_438
. Lets rename the function and the
variable.
scan((int *)cin_exref,guess);
while (local_428 != 0xf) {
printLine((int *)cout_exref,
"Please enter the 15-digit password (Only numbers and capital letters): ");
scan((int *)cin_exref,guess);
}
With these changes it is clear that our guess
buffer is reused to fill in the
15 positions password. Next the guess is transferred to a temporary variable,
the length is compared to 15 (0x0F) and if it is the correct length it is copied
into local_420
using strncpy_s
. Notice the insane buffer length of 0x400.
_Src = guess;
if (0xf < local_424) {
_Src = guess[0];
}
strncpy_s(&local_420,0x400,(char *)_Src,0x400);
Next there are a lot of modifications to local variables. The variable names seem to be a form of sequence (420, 41d, 418, etc). At the top of the function Ghidra has also put all the variables underneath eachother with their type set to undefined.
if ((local_41d == 'k') && (local_418 == 'a')) {
cVar1 = local_439;
if ((local_420 != 'h') || (local_416 != 'a')) goto LAB_004014fd;
if (((local_41b == 'h') && (local_417 == 'r')) && (local_415 == 'd')) {
cVar1 = '\x01';
goto LAB_004014fd;
}
}
Within Ghidra we can help it by manually setting the type for variables. We know
the key should be 15 positions, so we can try and change the signature of
local_420
to a char[15]
.
if ((local_420[3] == 'k') && (local_420[8] == 'a')) {
cVar1 = local_439;
if ((local_420[0] != 'h') || (local_420[10] != 'a')) goto LAB_004014fd;
if (((local_420[5] == 'h') && (local_420[9] == 'r')) && (local_420[11] == 'd')) {
cVar1 = '\x01';
goto LAB_004014fd;
}
}
That makes it much better to read and understand what is going on. The character array is checked with values at various places. This allows us to reconstruct the password using the array operations.
Before we continue there is only one small thing to look at still. Just before
the above if
statement a function call takes local_420
as an argument.
FUN_004012f0((int)local_420)
Following the function it is clear that the generated signature is not correct.
The int
should actually be a char *
to match our redefinition of char[15]
.
Lets change the function signature. I also took the chance to rename the
function to checkChars
.
All that is left to do is to take all the local_420
array assignments and
create the password string.
local_420[0] != 'h'
local_420[1] == '@'
local_420[2] == 'c'
local_420[3] == 'k'
local_420[4] == 't' - checkChars
local_420[5] == 'h'
local_420[6] == '3' - checkChars
local_420[7] == 'p' - checkChars
local_420[8] == 'a'
local_420[9] == 'r'
local_420[10] != 'a'
local_420[11] == 'd'
local_420[12] == '1' - checkChars
local_420[13] == '$'
local_420[14] == 'E'
The 15 position password to check is h@ckth3parad1$E
. This is also the flag
for the challenge.
Solved.