Eat the Cake!
7 minutos de lectura
Tenemos un Windows PE llamado cake.exe
:
$ file cake.exe
cake.exe: PE32 executable (console) Intel 80386, for MS Windows, UPX compressed
Descompresión
Como se puede ver, está comprimido con UPX, así que vamos a descomprimirlo:
$ upx -d cake.exe
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2023
UPX 4.0.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 30th 2023
File size Ratio Format Name
-------------------- ------ ----------- -----------
15872 <- 9216 58.06% win32/pe cake.exe
Unpacked 1 file.
$ file cake.exe
cake.exe: PE32 executable (console) Intel 80386, for MS Windows
Descompilación
Abramos Ghidra para descompilarlo. La función entry
no da ninguna pista sobre qué mirar:
void entry() {
___security_init_cookie();
___tmainCRTStartup();
return;
}
Si leemos ___tmainCRTStartup
, encontraremos la dirección de la función “main”:
int ___tmainCRTStartup() {
bool bVar1;
void *pvVar2;
void *pvVar3;
int iVar4;
BOOL BVar5;
bVar1 = false;
do {
pvVar3 = NULL;
LOCK();
pvVar2 = StackBase;
if (DAT_0040548c != NULL) {
pvVar3 = DAT_0040548c;
pvVar2 = DAT_0040548c;
}
DAT_0040548c = pvVar2;
UNLOCK();
if (pvVar3 == NULL) goto LAB_004023ed;
} while (pvVar3 != StackBase);
bVar1 = true;
LAB_004023ed:
if (_DAT_00405490 == 1) {
_amsg_exit(0x1f);
} else if (_DAT_00405490 == 0) {
_DAT_00405490 = 1;
iVar4 = _initterm_e(&DAT_00403150,&DAT_00403160);
if (iVar4 != 0) {
return 0xff;
}
} else {
_DAT_00405448 = 1;
}
if (_DAT_00405490 == 1) {
_initterm(&DAT_00403134,&DAT_0040314c);
_DAT_00405490 = 2;
}
if (!bVar1) {
LOCK();
DAT_0040548c = NULL;
UNLOCK();
}
if ((DAT_00405494 != NULL) &&
(BVar5 = __IsNonwritableInCurrentImage((PBYTE)&DAT_00405494), BVar5 != 0)) {
(*DAT_00405494)(0,2,0);
}
*(undefined4 *)__winitenv_exref = DAT_0040545c;
DAT_0040544c = FUN_00401350();
if (_DAT_00405450 != 0) {
if (_DAT_00405448 == 0) {
_cexit();
}
return DAT_0040544c;
}
// WARNING: Subroutine does not return
exit(DAT_0040544c);
}
La función “main” es FUN_00401350
:
void FUN_00401350() {
char cVar1;
undefined4 ****_Src;
uint uVar2;
char *pcVar3;
undefined auStack_440[7];
char cStack_439;
undefined4 ****local_438[4];
int local_428;
uint local_424;
char cStack_420;
char cStack_41f;
char cStack_41e;
char cStack_41d;
char cStack_41b;
char cStack_418;
char cStack_417;
char cStack_416;
char cStack_415;
char cStack_413;
char cStack_412;
undefined uStack_21;
uint local_1c;
void *local_14;
undefined *puStack_10;
undefined4 local_c;
puStack_10 = &LAB_0040299b;
local_14 = ExceptionList;
local_1c = DAT_00405000 ^ (uint) auStack_440;
ExceptionList = &local_14;
local_424 = 0xf;
local_428 = 0;
local_438[0] = (undefined4 ****) ((uint) local_438[0] & 0xffffff00);
local_c = 0;
FUN_00401ba0((int *) cout_exref, "\n $\n / \\ \n / | !\n / / !\n / / !\n");
FUN_00401ba0((int *) cout_exref, " _/ ____/ \\ \n / !\n ! !\n \\ / \n");
FUN_00401ba0((int *) cout_exref, " _-----\\ _ _ _ _ _ _ _ _/----- \n / \t\t\t \\ \n !\t \t ____\ t ____\t !\n _\\ ======| |~~| |=/_ \n");
FUN_00401ba0((int *) cout_exref, " /\t\t ````\t ````\t \\ \n ! )--------( !\n ! \t\t\t !\n \\\t\t\t\t / \n !---------------------------$-!\n");
FUN_00401ba0((int *) cout_exref, " !\t !\t |\t ! $$$ \n !\t !\t |\t ! $$$\n !\t !\t |\t ! $$$\n !\t !\t |\t ! $$$\n !\t !\t |\t ! $$$\n");
FUN_00401ba0((int *) cout_exref," !\t !\t |\t ! $$\n !\t !\t |\t ! !$$\n ! !\t |\t ! ! *\n !\t !\t |\t ! !\n !\t !\t |\t ! !\n !----!--------- ----!----!\n");
FUN_00401ba0((int *) cout_exref, " | ii_\\ | ii_\\ \n |______\\ |______\\ \n\n\n");
FUN_00401ba0((int *) cout_exref, "A challenge brought to you by little_pwnie ;)\n\n");
FUN_00401ba0((int *) cout_exref, "Please enter the 10-digit password (Only numbers and capital letters): ");
FUN_00401de0((int *) cin_exref, local_438);
while (local_428 != 0xf) {
FUN_00401ba0((int *) cout_exref, "Please enter the 15-digit password (Only numbers and capital letters): ");
FUN_00401de0((int *) cin_exref, local_438);
}
_Src = local_438;
if (0xf < local_424) {
_Src = local_438[0];
}
strncpy_s(&cStack_420, 0x400, (char *) _Src, 0x400);
uStack_21 = 0;
uVar2 = FUN_004012f0((int) &cStack_420);
if ((cStack_41d == 'k') && (cStack_418 == 'a')) {
cVar1 = cStack_439;
if ((cStack_420 != 'h') || (cStack_416 != 'a')) goto LAB_004014fd;
if (((cStack_41b == 'h') && (cStack_417 == 'r')) && (cStack_415 == 'd')) {
cVar1 = '\x01';
goto LAB_004014fd;
}
}
cVar1 = '\0';
LAB_004014fd:
if ((cStack_41f == '@') && (cStack_412 == 'E')) {
if ((cStack_41e == 'c') && (cStack_413 == '$')) {
cStack_439 = '\x01';
}
} else {
cStack_439 = '\0';
}
if ((((char)uVar2 == '\0') || (cVar1 == '\0')) || (pcVar3 = "Congratulations! Now go validate your flag!\n", cStack_439 == '\0')) {
pcVar3 = "Better luck next time...\n";
}
FUN_00401ba0((int *) cout_exref, pcVar3);
system("pause");
if (0xf < local_424) {
operator_delete(local_438[0]);
}
ExceptionList = local_14;
FUN_00401fff(local_1c ^ (uint) auStack_440);
}
Otra forma de encontrar la función “main” es buscar cadenas que aparecen impresas en la pantalla al ejecutar el binario y buscar referencias cruzadas en las funciones. Pero no vamos a ejecutar el programa, el reto se puede resolver de forma estática.
Entendiendo el programa
El programa solicita una contraseña de 10 dígitos (o una contraseña de 15 dígitos), aunque solo se permiten números y letras mayúsculas:
FUN_00401ba0((int *) cout_exref, "Please enter the 10-digit password (Only numbers and capital letters): ");
FUN_00401de0((int *) cin_exref, local_438);
while (local_428 != 0xf) {
FUN_00401ba0((int *) cout_exref, "Please enter the 15-digit password (Only numbers and capital letters): ");
FUN_00401de0((int *) cin_exref, local_438);
}
Una vez que la hayamos puesto, el programa verifica si es correcta. La contraseña de entrada se almacena en local_438
, y luego se copia en cStack_420
:
_Src = local_438;
if (0xf < local_424) {
_Src = local_438[0];
}
strncpy_s(&cStack_420, 0x400, (char *) _Src, 0x400);
uStack_21 = 0;
uVar2 = FUN_004012f0((int) &cStack_420);
Después de eso, hay dos bloques if
que son interesantes:
if ((cStack_41d == 'k') && (cStack_418 == 'a')) {
cVar1 = cStack_439;
if ((cStack_420 != 'h') || (cStack_416 != 'a')) goto LAB_004014fd;
if (((cStack_41b == 'h') && (cStack_417 == 'r')) && (cStack_415 == 'd')) {
cVar1 = '\x01';
goto LAB_004014fd;
}
}
cVar1 = '\0';
LAB_004014fd:
if ((cStack_41f == '@') && (cStack_412 == 'E')) {
if ((cStack_41e == 'c') && (cStack_413 == '$')) {
cStack_439 = '\x01';
}
} else {
cStack_439 = '\0';
}
Cada uno de ellos está revisando algunos bytes y poniendo cVar1
y cStack_439
a 1
o 0
cuando se pasan las comprobaciones o no, respectivamente. Si pasan ambas comprobaciones, veremos un mensaje de éxito, de lo contrario, un mensaje de fallo:
if ((((char)uVar2 == '\0') || (cVar1 == '\0')) || (pcVar3 = "Congratulations! Now go validate your flag!\n", cStack_439 == '\0')) {
pcVar3 = "Better luck next time...\n";
}
Si ordenamos las comprobaciones de bytes, obtendremos esto:
cStack_412 == 'E'
cStack_413 == '$'
cStack_415 == 'd'
cStack_416 != 'a'
cStack_417 == 'r'
cStack_418 == 'a'
cStack_41b == 'h'
cStack_41d == 'k'
cStack_41e == 'c'
cStack_41f == '@'
cStack_420 != 'h'
Pero nos faltan algunos valores… El resto de ellos se puede encontrar en FUN_004012f0
, que establece la variable uVar2
, también utilizada para la verificación:
uint __fastcall FUN_004012f0(int param_1) {
uint uVar1;
int iVar2;
uVar1 = isdigit((int) *(char *) (param_1 + 6));
if (uVar1 != 0) {
uVar1 = isdigit((int) *(char *) (param_1 + 0xc));
if (uVar1 != 0) {
iVar2 = atoi((char *) (param_1 + 6));
uVar1 = atoi((char *) (param_1 + 0xc));
if ((((iVar2 == 3) && (uVar1 == 1)) && (*(char *) (param_1 + 4) == 't')) && (*(char *) (param_1 + 7) == 'p')) {
return 1;
}
}
}
return uVar1 & 0xffffff00;
}
El valor de param_1
es &cStack_420
, por lo que:
param_1 + 4
es&cStack_4c
param_1 + 6
es&cStack_1a
param_1 + 7
es&cStack_19
param_1 + 0xc
es&cStack_14
Y así podemos completar la lista:
cStack_412 == 'E'
cStack_413 == '$'
cStack_414 == 1
cStack_415 == 'd'
cStack_416 != 'a'
cStack_417 == 'r'
cStack_418 == 'a'
cStack_419 == 'p'
cStack_41a == 3
cStack_41b == 'h'
cStack_41c == 't'
cStack_41d == 'k'
cStack_41e == 'c'
cStack_41f == '@'
cStack_420 != 'h'
Se puede vislumbrar una contraseña si la leemos desde abajo: h@ckth3parad1$E
. Esto se debe al formato little-endian (esa es también la razón por la cual &cStack_420 + 7 = &cStack_419
, va hacia atrás).
Flag
Habiendo encontrado esta contraseña (de hecho, una contraseña de 15 bytes), la flag es simplemente HTB{h@ckth3parad1$E}
.