PICtureThis
28 minutos de lectura
En este reto nos dan un Windows PE llamado main.exe
y una DLL cifrada (ciphered.dll
):
$ file *
ciphered.dll: data
main.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
Análisis de main.exe
De momento, podemos comenzar por abrir main.exe
en Ghidra y mirar las strings. Rápidamente vemos una que indica cómo se tiene que ejecutar el programa main.exe
:
Funcionamiento: ./reto.exe <input_string>
Si buscamos por referencias a esta string, llegaremos a la función principal:
undefined8 FUN_14000243f(undefined8 param_1, undefined1 (*param_2)[10], undefined8 param_3, undefined8 param_4) {
bool bVar1;
undefined8 uVar2;
undefined7 extraout_var;
undefined1 (*pauVar3)[10];
char *local_58[4];
char *local_38;
ulonglong local_28;
char *local_20;
int local_14;
char *local_10;
pauVar3 = param_2;
FUN_140002610();
if (*(longlong *) (*param_2 + 8) == 0) {
FUN_140001550("Funcionamiento: ./reto.exe <input_string>\n", pauVar3, param_3, param_4);
uVar2 = 1;
} else {
local_20 = _strdup(*(char **) (*param_2 + 8));
local_10 = strtok(*(char **) (*param_2 + 8), "/");
local_14 = 0;
while (local_10 != NULL) {
local_58[local_14] = local_10;
local_10 = strtok(NULL, "/");
local_14 = local_14 + 1;
}
local_28 = (ulonglong) (byte) local_58[0][3];
uVar2 = FUN_140001613(local_58[0]);
if ((int) uVar2 == 0) {
uVar2 = 1;
} else {
bVar1 = FUN_140001cb3((byte) local_28, local_58[1], param_3, param_4);
if ((int) CONCAT71(extraout_var, bVar1) == 0) {
uVar2 = 1;
} else {
FUN_140001fa2(local_58[3], local_58[2]);
FUN_1400020c9(local_38, (longlong) local_20, param_3, param_4);
uVar2 = 0;
}
}
}
return uVar2;
}
En este punto, podemos renombrar algunas variables y funciones para que tengan nombres más descriptivos y cambiar algunos tipos de variable:
undefined8 main(int argc, char **argv, undefined8 param_3, undefined8 param_4) {
bool bVar1;
undefined8 ret;
undefined7 extraout_var;
undefined1 (*pauVar2)[10];
char *parts [4];
char *local_38;
ulonglong local_28;
char *input_string;
int i;
char *tok;
pauVar2 = (undefined1 (*) [10]) argv;
FUN_140002610();
if (argv[1] == NULL) {
do_print("Funcionamiento: ./reto.exe <input_string>\n", pauVar2, param_3, param_4);
ret = 1;
} else {
input_string = _strdup(argv[1]);
tok = strtok(argv[1], "/");
i = 0;
while (tok != NULL) {
parts[i] = tok;
tok = strtok(NULL, "/");
i++1;
}
local_28 = (ulonglong)(byte)parts[0][3];
ret = FUN_140001613(parts[0]);
if ((int) ret == 0) {
ret = 1;
} else {
bVar1 = FUN_140001cb3((byte) local_28, parts[1], param_3, param_4);
if ((int) CONCAT71(extraout_var, bVar1) == 0) {
ret = 1;
} else {
FUN_140001fa2(parts[3], parts[2]);
FUN_1400020c9(local_38, input_string, param_3, param_4);
ret = 0;
}
}
}
return ret;
}
Vemos que divide el input_string
en 4 partes usando /
como separador. Estas partes se pasan por funciones que devuelven 0
ó 1
.
Primera comprobación
La función que comprueba la primera parte es la siguiente:
undefined8 FUN_140001613(char *param_1) {
size_t sVar1;
undefined8 uVar2;
ulonglong uVar3;
ulonglong local_88[4];
undefined8 local_68;
undefined8 local_60;
undefined8 local_58;
undefined8 local_50;
undefined8 local_48;
undefined8 local_40;
undefined8 local_38;
byte local_22;
byte local_21;
int local_20;
int i;
sVar1 = strlen(param_1);
local_20 = (int) sVar1;
if (local_20 < 4) {
uVar2 = 0;
} else {
local_21 = param_1[3];
local_22 = param_1[(longlong) local_20 + -1];
local_88[0] = 0x1c;
local_88[1] = 0xb;
local_88[2] = 0x19;
local_88[3] = 0x3f;
local_68 = 0x17;
local_60 = 0x1e;
local_58 = 0x1c;
local_50 = 0x14;
local_48 = 0x10;
local_40 = 0x11;
local_38 = 0x3f;
for (i = 0; uVar3 = (ulonglong) i, sVar1 = strlen(param_1), uVar3 < sVar1 - 1; i = i + 1) {
param_1[i] = param_1[i] ^ local_21;
param_1[i] = param_1[i] ^ local_22;
if ((ulonglong) (byte) param_1[i] != local_88[i]) {
return 0;
}
}
uVar2 = 1;
}
return uVar2;
}
Esta es una función relativamente sencilla que comprueba que al cifrar la parte de input_string
con un cierto algoritmo basado en XOR, el resultado coincide con los bytes que aparecen en las variables local_..
. Como clave de XOR, se utiliza el cuarto carácter de param_1
y el último:
local_21 = param_1[3];
local_22 = param_1[(longlong) local_20 + -1];
Cada carácter se cifra con estas dos claves. Con esto, podemos averiguar ya cuál es el último carácter de la parte de input_string
. Para el índice 0
, tenemos que param_1[i] = param_1[i] ^ local_21
va a ser 0
porque inicialmente param_1[3]
coincide con local_21
. Entonces, al aplicar XOR con local_22
(el último carácter de la parte de input_string
), tiene que dar local_88[3] = 0x3f
, por lo que local_22
es 0x3f
, que corresponde con ?
.
Sabiendo esto, podemos programar el algoritmo de descifrado y aplicar fuerza bruta en local_21
(256 posibilidades):
$ python3 -q
>>> local_22 = 0x3f
>>> expected = bytes([0x1c, 0xb, 0x19, 0x3f, 0x17, 0x1e, 0x1c, 0x14, 0x10, 0x11, 0x3f])
>>>
>>> for local_21 in range(256):
... prev = bytearray(len(expected))
... for i in range(len(prev)):
... prev[i] = expected[i] ^ local_21 ^ local_22
... prev[-1] = 0x3f
... print(local_21, bytes(prev))
...
0 b'#4&\x00(!#+/.?'
1 b'"5\'\x01) "*./?'
...
63 b'\x1c\x0b\x19?\x17\x1e\x1c\x14\x10\x11?'
64 b'ctf@hackon?'
65 b'bugAi`bjno?'
66 b'avdBjcaiml?'
67 b'`weCkb`hlm?'
68 b'gpbDlegokj?'
69 b'fqcEmdfnjk?'
70 b'er`Fngemih?'
71 b'dsaGofdlhi?'
72 b'k|nH`ikcgf?'
73 b'j}oIahjbfg?'
74 b'i~lJbkiaed?'
75 b'h\x7fmKcjh`de?'
76 b'oxjLdmogcb?'
77 b'nykMelnfbc?'
78 b'mzhNfomea`?'
79 b'l{iOgnld`a?'
80 b'sdvPxqs{\x7f~?'
81 b'rewQyprz~\x7f?'
82 b'qftRzsqy}|?'
83 b'pguS{rpx|}?'
84 b'w`rT|uw\x7f{z?'
85 b'vasU}tv~z{?'
86 b'ubpV~wu}yx?'
87 b'tcqW\x7fvt|xy?'
88 b'{l~Xpy{swv?'
89 b'zm\x7fYqxzrvw?'
90 b'yn|Zr{yqut?'
91 b'xo}[szxptu?'
92 b'\x7fhz\\t}\x7fwsr?'
93 b'~i{]u|~vrs?'
94 b'}jx^v\x7f}uqp?'
95 b'|ky_w~|tpq?'
96 b'CTF`HACKON?'
97 b'BUGaI@BJNO?'
98 b'AVDbJCAIML?'
99 b'@WEcKB@HLM?'
100 b'GPBdLEGOKJ?'
101 b'FQCeMDFNJK?'
102 b'ER@fNGEMIH?'
103 b'DSAgOFDLHI?'
104 b'K\\Nh@IKCGF?'
105 b'J]OiAHJBFG?'
106 b'I^LjBKIAED?'
107 b'H_MkCJH@DE?'
108 b'OXJlDMOGCB?'
109 b'NYKmELNFBC?'
110 b'MZHnFOMEA@?'
111 b'L[IoGNLD@A?'
112 b'SDVpXQS[_^?'
113 b'REWqYPRZ^_?'
114 b'QFTrZSQY]\\?'
115 b'PGUs[RPX\\]?'
116 b'W@Rt\\UW_[Z?'
117 b'VASu]TV^Z[?'
118 b'UBPv^WU]YX?'
119 b'TCQw_VT\\XY?'
120 b'[L^xPY[SWV?'
121 b'ZM_yQXZRVW?'
122 b'YN\\zR[YQUT?'
123 b'XO]{SZXPTU?'
124 b'_HZ|T]_WSR?'
125 b'^I[}U\\^VRS?'
126 b']JX~V_]UQP?'
127 b'\\KY\x7fW^\\TPQ?'
128 b'\xa3\xb4\xa6\x80\xa8\xa1\xa3\xab\xaf\xae?'
...
Aquí vemos dos strings que podrían cuadrar: ctf@hackon?
y CTF`HACKON?
, aunque claramente la primera tiene más sentido.
Segunda comprobación
Para esta segunda comprobación, la parte de input_string
se divide en dos subpartes, separadas por _
:
bool FUN_140001cb3(byte param_1, char *param_2, undefined8 param_3, undefined8 param_4) {
undefined *puVar1;
undefined1 (*pauVar2)[10];
undefined8 uVar3;
bool bVar4;
DWORD local_44;
undefined1 (*local_40)[10];
char *local_38;
longlong local_30;
HANDLE local_28;
LPVOID local_20;
int local_14;
char *local_10;
puVar1 = &DAT_14000a041;
local_10 = strtok(param_2, "_");
local_14 = 0;
while (local_10 != NULL) {
(&local_38) [local_14] = local_10;
puVar1 = &DAT_14000a000;
local_10 = strtok(NULL, "/");
local_14 = local_14 + 1;
}
local_40 = NULL;
local_20 = FUN_140001765(&local_40, puVar1, param_3, param_4);
VirtualProtect(&DAT_140009280, (ulonglong) DAT_140009820, (uint) param_1, &local_44);
FUN_14000185e(0x140009280, (ulonglong) DAT_140009820, local_20, local_40, (longlong) local_38, local_30);
VirtualProtect(&DAT_140009280, (ulonglong) DAT_140009820, 0x40, &local_44);
uVar3 = 0;
puVar1 = &DAT_140009280;
pauVar2 = NULL;
local_28 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) &DAT_140009280, NULL, 0, NULL);
bVar4 = local_28 != NULL;
if (bVar4) {
WaitForSingleObject(local_28, 0xffffffff);
} else {
do_print("Failed to create a thread\n", pauVar2, puVar1, uVar3);
}
return bVar4;
}
Luego, llama a la función FUN_140001765
, que abre y lee el archivo ciphered.dll
:
LPVOID FUN_140001765(undefined1 (**param_1)[10], undefined8 param_2, undefined8 param_3, undefined8 param_4) {
long lVar1;
FILE *_File;
LPVOID _DstBuf;
undefined1 (**ppauVar2)[10];
undefined1 (*pauVar3)[10];
undefined8 uVar4;
undefined8 uVar5;
pauVar3 = (undefined1 (*) [10]) &DAT_14000a002;
_File = fopen("ciphered.dll", "rb");
if (_File == NULL) {
do_print("Failed to open the DLL\n", pauVar3, param_3, param_4);
_DstBuf = NULL;
} else {
fseek(_File, 0, 2);
lVar1 = ftell(_File);
*param_1 = (undefined1 (*) [10]) (longlong)l Var1;
rewind(_File);
pauVar3 = *param_1;
uVar5 = 0x40;
uVar4 = 0x3000;
_DstBuf = VirtualAlloc(NULL, (SIZE_T) pauVar3, 0x3000, 0x40);
if (_DstBuf == NULL) {
do_print("Failed to alloc space\n", pauVar3, uVar4, uVar5);
_DstBuf = NULL;
} else {
ppauVar2 = (undefined1 (**) [10]) fread(_DstBuf, 1,( size_t) *param_1, _File);
if (param_1 != ppauVar2) {
fclose(_File);
}
}
}
return _DstBuf;
}
Además, la función anterior realiza VirtualProtect
sobre una dirección de memoria, y luego llama a FUN_14000185e
con una de las subpartes:
void FUN_14000185e(longlong param_1, ulonglong param_2, undefined8 param_3, undefined8 param_4, longlong param_5, longlong param_6) {
undefined local_res18 [16];
ulonglong local_28;
ulonglong local_20;
ulonglong local_18;
ulonglong local_10;
for (local_10 = 0; local_10 < param_2; local_10 = local_10 + 1) {
if (((((*(char *)(local_10 + param_1) == -0x78) && (*(char *)(param_1 + local_10 + 1) == -0x78))
&& (*(char *)(param_1 + local_10 + 2) == -0x78)) &&
((*(char *)(param_1 + local_10 + 3) == -0x78 && (*(char *)(param_1 + local_10 + 4) == -0x78)
))) && ((*(char *)(param_1 + local_10 + 5) == -0x78 &&
((*(char *)(param_1 + local_10 + 6) == -0x78 &&
(*(char *)(param_1 + local_10 + 7) == -0x78)))))) {
for (local_18 = 0; local_18 < 8; local_18 = local_18 + 1) {
*(undefined *)(param_1 + local_18 + local_10) = local_res18[local_18];
}
}
if (((((*(char *)(local_10 + param_1) == -0x56) && (*(char *)(param_1 + local_10 + 1) == -0x45))
&& (*(char *)(param_1 + local_10 + 2) == -0x55)) &&
(((*(char *)(param_1 + local_10 + 3) == -0x55 &&
(*(char *)(param_1 + local_10 + 4) == -0x56)) &&
((*(char *)(param_1 + local_10 + 5) == -0x45 &&
((*(char *)(param_1 + local_10 + 6) == -0x55 &&
(*(char *)(param_1 + local_10 + 7) == -0x55)))))))) &&
(((*(char *)(param_1 + local_10 + 8) == -0x56 &&
(((*(char *)(param_1 + local_10 + 9) == -0x45 &&
(*(char *)(param_1 + local_10 + 10) == -0x55)) &&
(*(char *)(param_1 + local_10 + 0xb) == -0x55)))) &&
(((*(char *)(param_1 + local_10 + 0xc) == -0x56 &&
(*(char *)(param_1 + local_10 + 0xd) == -0x45)) &&
((*(char *)(param_1 + local_10 + 0xe) == -0x55 &&
(*(char *)(param_1 + local_10 + 0xf) == -0x55)))))))) {
for (local_20 = 0; local_20 < 0x10; local_20 = local_20 + 1) {
*(undefined *)(param_1 + local_20 + local_10) = *(undefined *)(local_20 + param_5);
}
}
if (((((*(char *)(local_10 + param_1) == 'w') && (*(char *)(param_1 + local_10 + 1) == 'w')) &&
((*(char *)(param_1 + local_10 + 2) == 'w' &&
((((*(char *)(param_1 + local_10 + 3) == 'w' && (*(char *)(param_1 + local_10 + 4) == 'w')
) && (*(char *)(param_1 + local_10 + 5) == 'w')) &&
((*(char *)(param_1 + local_10 + 6) == 'w' && (*(char *)(param_1 + local_10 + 7) == 'w'))
)))))) && (*(char *)(param_1 + local_10 + 8) == 'w')) &&
((*(char *)(param_1 + local_10 + 9) == 'w' && (*(char *)(param_1 + local_10 + 10) == 'w'))))
{
for (local_28 = 0; local_28 < 0xb; local_28 = local_28 + 1) {
*(undefined *)(param_1 + local_28 + local_10) = *(undefined *)(local_28 + param_6);
}
}
}
}
Esta función parece complicada, pero lo que hace es buscar unas strings determinadas en main.exe
y reemplaza algunos caracteres de la región de memoria con dirección 0x140009280
por caracteres de la subparte que recibe como argumento:
$ strings main.exe | grep wwwww
wwwwwwwwwww
$ xxd main.exe | grep -C 4 wwwww
000084f0: 4989 c949 89d0 31c0 eb0e 660f 1f44 0000 I..I..1...f..D..
00008500: 4883 c001 38ca 7518 410f b614 0141 0fb6 H...8.u.A....A..
00008510: 0c00 84d2 75ea 0fb6 c1f7 d8c3 0f1f 4000 ....u.........@.
00008520: 0fb6 c229 c8c3 9090 9090 9090 9090 9090 ...)............
00008530: 7777 7777 7777 7777 7777 7700 c390 9090 wwwwwwwwwww.....
00008540: aabb abab aabb abab aabb abab aabb abab ................
00008550: c390 9090 9090 9090 9090 9090 9090 9090 ................
00008560: 4155 4154 5557 5648 be6d 7376 6372 742e AUATUWVH.msvcrt.
00008570: 6453 4881 ec88 0000 00e8 22fd ffff 488d dSH......."...H.
En esta dirección 0x140009280
es donde se realiza VirtualProtect
, y además, después se ejecuta un thread en esta misma dirección. Por tanto, el contenido que hay en esta región de memoria es shellcode. En Ghidra, podemos ir a esta dirección y descompilarlo para ver el shellcode en C:
void UndefinedFunction_140009280() {
FUN_140009560();
}
undefined8 FUN_140009560() {
byte *pbVar1;
int iVar2;
longlong lVar3;
code *pcVar4;
code *pcVar5;
code *pcVar6;
undefined8 uVar7;
byte *pbVar8;
undefined8 in_stack_ffffffffffffff68;
undefined4 uVar9;
undefined local_78[6];
undefined8 local_72;
undefined2 local_6a;
undefined8 local_68;
undefined2 local_60;
undefined local_5e;
undefined8 local_5d;
undefined4 local_55;
undefined8 local_51;
undefined4 local_49;
undefined8 local_45;
undefined4 local_3d;
undefined local_39;
uVar9 = (undefined4) ((ulonglong) in_stack_ffffffffffffff68 >> 0x20);
lVar3 = FUN_1400092a0();
local_39 = 0;
local_3d = 0x41797261;
local_45 = 0x7262694c64616f4c;
pcVar4 = (code *) FUN_140009330(lVar3, (ulonglong) &local_45);
local_68 = 0x642e74726376736d;
local_60 = 0x6c6c;
local_5e = 0;
(*pcVar4)(&local_68);
local_5d = 0x6946657461657243;
local_55 = 0x41656c;
pcVar4 = (code *) FUN_140009330(lVar3, (ulonglong) &local_5d);
local_6a = 0x65;
local_72 = 0x6c69466574697257;
pcVar5 = (code *) FUN_140009330(lVar3, (ulonglong) &local_72);
local_49 = 0x656c64;
local_51 = 0x6e614865736f6c43;
pcVar6 = (code *) FUN_140009330(lVar3, (ulonglong) &local_51);
pbVar8 = DAT_1400094e0;
uVar7 = (*pcVar4)(DAT_140009530, 0x40000000, 0, 0, CONCAT44(uVar9, 2), 0x80, 0);
if (((((DAT_140009540 == pbVar8[0x2ec9c]) && (DAT_140009541 == pbVar8[0x35])) &&
(DAT_140009542 == pbVar8[0x104])) &&
(((DAT_140009543 == pbVar8[0x63d1] && (DAT_140009544 == pbVar8[0x18864])) &&
((DAT_140009545 == pbVar8[0x1d76c] &&
((DAT_140009546 == pbVar8[0x1a1c6] && (DAT_140009547 == pbVar8[0x1a117])))))))) &&
((DAT_140009548 == pbVar8[0x14501] &&
(((((DAT_140009549 == pbVar8[0x13163] && (DAT_14000954a == pbVar8[0x178c2])) &&
(DAT_14000954b == pbVar8[0x13949])) &&
((DAT_14000954c == pbVar8[0x1116d] && (DAT_14000954d == pbVar8[0x11bf6])))) &&
((DAT_14000954e == pbVar8[0xfc29] && (DAT_14000954f == pbVar8[0xec7c])))))))) {
FUN_140009400(0x140009540, 0x10, pbVar8, 0x6cebf);
}
pbVar1 = pbVar8 + 0x6cebf;
do {
iVar2 = (*pcVar5)(uVar7, pbVar8, 1, local_78, 0);
if (iVar2 == 0) break;
pbVar8 = pbVar8 + 1;
} while (pbVar8 != pbVar1);
(*pcVar6)(uVar7);
return 0;
}
Por un lado, vemos dígitos hexadecimales que se corresponden con caracteres ASCII imprimibles y contienen esta información:
$ python3 -q
>>> from pwn import p8, p16, p32, p64
>>> p64(0x7262694c64616f4c) + p32(0x41797261)
b'LoadLibraryA'
>>> p64(0x642e74726376736d) + p16(0x6c6c)
b'msvcrt.dll'
>>> p64(0x6946657461657243) + p32(0x41656c)
b'CreateFileA\x00'
>>> p64(0x6c69466574697257) + p8(0x65)
b'WriteFile'
>>> p64(0x6e614865736f6c43) + p32(0x656c64)
b'CloseHandle\x00'
Entonces, podemos deducir que está llamando a estas funciones de la API de Windows. Además, vemos comprobaciones sobre unas ciertas direcciones como DAT140009530
. Si miramos qué hay en estas direcciones, encontraremos los caracteres w
de antes:
140009530 77 77 77 ds "wwwwwwwwwww"
77 77 77
77 77 77
14000953c c3 ?? C3h
14000953d 90 ?? 90h
14000953e 90 ?? 90h
14000953f 90 ?? 90h
140009540 aa ?? AAh
140009541 bb ?? BBh
140009542 ab ?? ABh
140009543 ab ?? ABh
140009544 aa ?? AAh
140009545 bb ?? BBh
140009546 ab ?? ABh
140009547 ab ?? ABh
140009548 aa ?? AAh
140009549 bb ?? BBh
14000954a ab ?? ABh
14000954b ab ?? ABh
14000954c aa ?? AAh
14000954d bb ?? BBh
14000954e ab ?? ABh
14000954f ab ?? ABh
140009550 c3 ?? C3h
140009551 90 ?? 90h
140009552 90 ?? 90h
140009553 90 ?? 90h
140009554 90 ?? 90h
...
Y esto se comprueba con pbVar8
, que contiene la dirección DAT_1400094e0
:
1400094e0 88 ?? 88h
1400094e1 88 ?? 88h
1400094e2 88 ?? 88h
1400094e3 88 ?? 88h
1400094e4 88 ?? 88h
1400094e5 88 ?? 88h
1400094e6 88 ?? 88h
1400094e7 88 ?? 88h
1400094e8 c3 ?? C3h
1400094e9 90 ?? 90h
1400094ea 90 ?? 90h
En este punto podríamos depurar con x64dbg, y nos daríamos cuenta de que en esa región de memoria se guarda la dirección en la que se escribe la DLL cifrada. En el código aparece aquí (-0x78
es 0x88
):
for (local_10 = 0; local_10 < param_2; local_10 = local_10 + 1) {
if (((((*(char *)(local_10 + param_1) == -0x78) && (*(char *)(param_1 + local_10 + 1) == -0x78))
&& (*(char *)(param_1 + local_10 + 2) == -0x78)) &&
((*(char *)(param_1 + local_10 + 3) == -0x78 && (*(char *)(param_1 + local_10 + 4) == -0x78)
))) && ((*(char *)(param_1 + local_10 + 5) == -0x78 &&
((*(char *)(param_1 + local_10 + 6) == -0x78 &&
(*(char *)(param_1 + local_10 + 7) == -0x78)))))) {
for (local_18 = 0; local_18 < 8; local_18 = local_18 + 1) {
*(undefined *)(param_1 + local_18 + local_10) = local_res18[local_18];
}
}
Y recordemos que esta función se llama después de leer ciphered.dll
, cuya dirección está en local_20
:
local_20 = FUN_140001765(&local_40, puVar1, param_3, param_4);
VirtualProtect(&DAT_140009280, (ulonglong) DAT_140009820, (uint) param_1, &local_44);
FUN_14000185e(0x140009280, (ulonglong) DAT_140009820, local_20, local_40, (longlong) local_38, local_30);
VirtualProtect(&DAT_140009280, (ulonglong) DAT_140009820, 0x40, &local_44);
Entonces, ya sabemos qué caracteres deben ir ahí para poder pasar el if
:
$ python3 -q
>>> with open('ciphered.dll', 'rb') as f:
... ciphered_dll = f.read()
...
>>> pbVar8 = ciphered_dll
>>> bytes([pbVar8[0x2ec9c], pbVar8[0x35], pbVar8[0x104], pbVar8[0x63d1], pbVar8[0x18864], pbVar8[0x1d76c], pbVar8[0x1a1c6], pbVar8[0x1a117], pbVar8[0x14501], pbVar8[0x13163], pbVar8[0x178c2], pbVar8[0x13949], pbVar8[0x1116d], pbVar8[0x11bf6], pbVar8[0xfc29], pbVar8[0xec7c]])
b'd0n0t8ruT3,th1nk'
Con esta subparte, se llama a la función FUN_140009400
con el contenido de la DLL cifrada, para descifrarla. Y después, se escribe como archivo con WriteFile
.
Realizando pruebas en local con el depurador, descubrimos que la segunda subparte indica el nombre del archivo en el que escribir la DLL descifrada.
Entonces, de momento tenemos esta input_string
: ctf@hackon?/d0n0t8ruT3,th1nk_filename/
.
Últimas comprobaciones
La siguiente función que se ejecuta en el main
es FUN_140001fa2
, usando como argumentos la cuarta parte y la tercera parte (en ese orden):
void FUN_140001fa2(char *param_1, undefined *param_2) {
DWORD DVar1;
HANDLE hFileMappingObject;
char *_Dest;
undefined8 uVar2;
undefined8 uVar3;
char *local_res8;
uVar3 = 0;
uVar2 = 4;
hFileMappingObject = CreateFileMappingA((HANDLE) 0xffffffffffffffff, NULL, 4, 0, 0x1000, "SharedMemory");
if (hFileMappingObject == NULL) {
DVar1 = GetLastError();
do_print("Failed to create shared memory: %d\n", (undefined1 (*) [10]) (ulonglong) DVar1, uVar2, uVar3);
} else {
uVar3 = 0;
uVar2 = 0;
_Dest = (char *) MapViewOfFile(hFileMappingObject, 2, 0, 0, 0x1000);
if (_Dest == NULL) {
DVar1 = GetLastError();
do_print("Failed to map view of file: %d\n", (undefined1 (*) [10]) (ulonglong) DVar1, uVar2, uVar3);
CloseHandle(hFileMappingObject);
} else {
local_res8 = param_1;
if (param_1 == NULL) {
local_res8 = "almost:D";
}
strcpy(_Dest,local_res8);
FUN_140001e22(param_2);
UnmapViewOfFile(_Dest);
CloseHandle(hFileMappingObject);
}
}
}
En esta función se crea una región de memoria compartida llamada Shared Memory
. Luego, copia param_1
(la cuarta parte de input_string
) en dicha región de memoria compartida y ejecuta FUN_140001e22
sobre la param_2
(la tercera parte):
undefined8 FUN_140001e22(undefined *param_1) {
LPTHREAD_START_ROUTINE lpStartAddress;
undefined8 uVar1;
HANDLE hHandle;
undefined1 (*pauVar2)[10];
ulonglong local_10;
lpStartAddress = (LPTHREAD_START_ROUTINE) VirtualAlloc(NULL, (ulonglong) DAT_140009260, 0x3000, 0x40);
if (lpStartAddress == NULL) {
uVar1 = 1;
} else {
memcpy(lpStartAddress, &DAT_140009020, (ulonglong) DAT_140009260);
for (local_10 = 0; local_10 < DAT_140009260; local_10 = local_10 + 1) {
lpStartAddress[local_10] = (PTHREAD_START_ROUTINE) ((byte) lpStartAddress[local_10] ^ 0x33);
}
lpStartAddress[0x93] = (PTHREAD_START_ROUTINE) *param_1;
lpStartAddress[0x1da] = (PTHREAD_START_ROUTINE) param_1[1];
lpStartAddress[0x1e2] = (PTHREAD_START_ROUTINE) (param_1[4] - 2);
lpStartAddress[0x1e3] = (PTHREAD_START_ROUTINE) (param_1[2] - 3);
lpStartAddress[0x1e5] = (PTHREAD_START_ROUTINE) param_1[3];
uVar1 = 0;
pauVar2 = NULL;
hHandle = CreateThread(NULL, 0, lpStartAddress, NULL, 0, NULL);
if (hHandle == NULL) {
do_print("Failed to create a thread\n", pauVar2, lpStartAddress, uVar1);
uVar1 = 0;
} else {
WaitForSingleObject(hHandle, 0xffffffff);
uVar1 = 1;
}
}
return uVar1;
}
Aquí vemos que se asigna una región de memoria dinámica con VirtualAlloc
, luego se copian datos de DAT_140009020
con memcpy
y se descifra con XOR (clave 0x33
). A continuación, se toman 5 caracteres de param_1
para ponerlos en el buffer descifrado. Por último, se crea un thread y se ejecuta sobre este buffer. Por tanto, de nuevo, se está ejecutando un shellcode.
De este shellcode necesitamos alterar los bytes 0x93
, 0x1da
, 0x1e2
, 0x1e3
, y 0x1e5
. Podemos descifrar el shellcode y luego desensamblarlo con pwntools
:
$ python3 -q
>>> from pwn import xor
>>> ct_shellcode = b'\x64\x7b\xba\xd4\x7b\xb0\xd7\xc3\x7b\xb0\xdf\x13\xdb\x9c\x32\x33\x33\x7b\xba\xcf\x6c\xf0\x55\x1d\x3c\x2c\xb7\x33\x33\x33\x33\x33\x56\x7b\xb8\x37\x16\x53\x33\x33\x33\x7b\xb8\x73\x2b\x7f\xb8\x63\x13\x7e\xb6\xe1\x47\x56\x55\x1d\x3c\x2c\xb7\x33\x33\x33\x33\x33\x72\x3c\x84\x71\x7b\x7e\xb8\x71\x63\x77\xbe\x7b\xcc\x7a\xbe\x7b\x32\x02\xf3\x76\x3c\x84\xfa\x7a\x32\xfa\xd8\x2b\x3c\x2c\x73\x33\xb0\xdb\x13\x7a\xba\xfb\x7b\xab\x7b\x32\xe3\x7a\x0a\xfa\x47\x2f\x7b\xb0\xf2\x32\xba\xf1\x72\x3c\x85\x33\xf2\xf9\x3e\x0f\x53\x44\xec\x7b\x32\xe3\x7a\xba\xfb\x7a\x0a\xfa\x46\xd7\x0e\x68\x8f\x79\x59\x47\x3f\xf0\xb8\x21\x7e\xb6\xe1\x46\x96\x7f\xba\xe3\xf0\x7e\xb8\x61\x13\x7f\xba\xe3\xf0\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3\x7b\xb6\xfa\x3c\xb7\x88\x33\x33\x33\x64\x7a\xba\xfa\x7a\xba\xe1\x65\x60\x7b\x50\x72\x0f\xb8\xb7\x32\xbb\x33\x33\x33\x7b\x32\xfb\xb8\x43\x2f\x7b\x32\xfd\xc4\xf1\x33\x33\xcc\xcc\x47\x59\xb8\x7b\x2b\xbe\x62\xcc\xb6\xfa\x47\x66\x77\xb8\x73\x13\x77\xb8\x6b\x17\x7e\x32\xfb\x7e\x32\xf8\x7a\xbe\x6f\xa3\x37\x3c\x2c\x77\x33\x33\x72\xb8\x33\x7f\xba\xe4\x7f\x32\xfb\x7b\x1a\xf4\xd8\x35\x55\xa3\x0b\xf9\x46\x7f\x7b\xba\xf1\x7b\xb0\xf3\x32\x3c\x85\x7f\x0b\xcc\x3c\x85\x21\xb7\xe1\x46\xda\x3c\x85\xe2\xc4\xe9\xb6\xe1\x47\x05\x7a\xb0\xf3\x37\x7a\xb0\xf0\x31\x7a\x0a\xeb\x46\xf0\x68\x02\xf3\x6d\x6c\xf0\x3c\x2c\x77\x33\x33\xb8\x73\x23\x77\x3c\x84\xe1\x68\x7a\x1a\xf1\x71\xb8\x37\xa5\x6d\x6c\x7b\x32\xfb\xf0\x3c\x2c\x33\x1a\xf9\xb6\xe1\x46\xf9\x72\x3c\x84\x30\x68\xb8\x37\xb5\x6d\x6c\x7f\x32\xfb\xf0\x02\xf3\xf0\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3\x7a\xba\xfa\x7a\xba\xe3\x02\xf3\xd8\x3d\x55\x3c\x2c\x77\x33\x33\x7b\xb0\xf3\x32\x0b\xf9\x46\x2b\x72\x3c\x85\x27\x32\x72\x3c\x85\x3f\x33\xb7\xe1\x46\xd9\x3c\x85\xf2\xc4\xeb\xf0\x3c\x2c\x73\x33\x3c\x85\xf1\x1a\xfb\xf0\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3\x7b\xb0\xdf\x7b\xdb\x64\xcd\xcc\xcc\x7b\xbe\x67\x17\x00\xf5\x77\x17\x0c\x33\x7b\xba\xf2\xf4\x77\x17\x08\xf0\x41\x4a\x72\x7b\x8b\x7f\x5c\xf0\xf0\x7f\xf0\x51\x41\x7b\xba\x77\x17\x00\xdb\x8d\xcd\xcc\xcc\xf4\x77\x17\x1c\x57\x5f\x5f\x33\x7b\xbe\x7f\x17\x14\x7b\x89\x51\x46\x41\x43\x56\x56\x40\x1d\x7b\xba\x67\x17\x14\xcc\xe3\x7b\xb0\xf7\x7b\xf0\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x33\x33\x33\x33\x33\x33\x33\x33\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x33\x33\x33\x33\x33\x33\x33\x00'
>>> shellcode = xor(ct_shellcode, 0x33)
>>> shellcode
b"WH\x89\xe7H\x83\xe4\xf0H\x83\xec \xe8\xaf\x01\x00\x00H\x89\xfc_\xc3f.\x0f\x1f\x84\x00\x00\x00\x00\x00eH\x8b\x04%`\x00\x00\x00H\x8b@\x18L\x8bP M\x85\xd2tef.\x0f\x1f\x84\x00\x00\x00\x00\x00A\x0f\xb7BHM\x8bBPD\x8dH\xffI\x8dH\x011\xc0E\x0f\xb7\xc9I\x01\xc9\xeb\x18\x0f\x1f@\x00\x83\xe8 I\x89\xc8H\x98H\x01\xd0I9\xc9t\x1cH\x83\xc1\x01\x89\xc2A\x0f\xb6\x00\xc1\xca\r<`w\xdfH\x01\xd0I\x89\xc8I9\xc9u\xe4=[\xbcJjt\x0c\xc3\x8b\x12M\x85\xd2u\xa5L\x89\xd0\xc3M\x8bR L\x89\xd0\xc3\x90\x90\x90\x90\x90\x90\x90\x90\x90H\x85\xc9\x0f\x84\xbb\x00\x00\x00WI\x89\xc9I\x89\xd2VSHcA<\x8b\x84\x01\x88\x00\x00\x00H\x01\xc8\x8bp\x1cH\x01\xce\xf7\xc2\x00\x00\xff\xfftj\x8bH\x18\x8dQ\xff\x85\xc9tUD\x8b@ D\x8bX$M\x01\xc8M\x01\xcbI\x8d\\\x90\x04\x0f\x1fD\x00\x00A\x8b\x00L\x89\xd7L\x01\xc8H)\xc7\xeb\x06f\x908\xcauLH\x89\xc2H\x83\xc0\x01\x0f\xb6L8\xff\x0f\xb6\x12\x84\xd2u\xe9\x0f\xb6\xd1\xf7\xda\x85\xd2t6I\x83\xc0\x04I\x83\xc3\x02I9\xd8u\xc3[1\xc0^_\xc3\x0f\x1fD\x00\x00\x8b@\x10D\x0f\xb7\xd2[I)\xc2B\x8b\x04\x96^_H\x01\xc8\xc3\x0f\x1f\x00)\xca\x85\xd2u\xcaA\x0f\xb7\x03[\x8b\x04\x86^_L\x01\xc8\xc31\xc0\xc3\x90\x90\x90\x90\x90\x90\x90\x90\x90I\x89\xc9I\x89\xd01\xc0\xeb\x0ef\x0f\x1fD\x00\x00H\x83\xc0\x018\xcau\x18A\x0f\xb6\x14\x01A\x0f\xb6\x0c\x00\x84\xd2u\xea\x0f\xb6\xc1\xf7\xd8\xc3\x0f\x1f@\x00\x0f\xb6\xc2)\xc8\xc3\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90H\x83\xecH\xe8W\xfe\xff\xffH\x8dT$3\xc6D$?\x00H\x89\xc1\xc7D$;\xc3ryAH\xb8Lo\xc3\xc3L\xc3brH\x89D$3\xe8\xbe\xfe\xff\xff\xc7D$/dll\x00H\x8dL$'H\xbaburpees.H\x89T$'\xff\xd0H\x83\xc4H\xc3\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x003"
>>> shellcode.hex()
'574889e74883e4f04883ec20e8af0100004889fc5fc3662e0f1f84000000000065488b042560000000488b40184c8b50204d85d27465662e0f1f840000000000410fb742484d8b4250448d48ff498d480131c0450fb7c94901c9eb180f1f400083e8204989c848984801d04939c9741c4883c10189c2410fb600c1ca0d3c6077df4801d04989c84939c975e43d5bbc4a6a740cc38b124d85d275a54c89d0c34d8b52204c89d0c39090909090909090904885c90f84bb000000574989c94989d256534863413c8b8401880000004801c88b701c4801cef7c20000ffff746a8b48188d51ff85c97455448b4020448b58244d01c84d01cb498d5c90040f1f440000418b004c89d74c01c84829c7eb06669038ca754c4889c24883c0010fb64c38ff0fb61284d275e90fb6d1f7da85d274364983c0044983c3024939d875c35b31c05e5fc30f1f4400008b4010440fb7d25b4929c2428b04965e5f4801c8c30f1f0029ca85d275ca410fb7035b8b04865e5f4c01c8c331c0c39090909090909090904989c94989d031c0eb0e660f1f4400004883c00138ca7518410fb61401410fb60c0084d275ea0fb6c1f7d8c30f1f40000fb6c229c8c3909090909090909090904883ec48e857feffff488d542433c644243f004889c1c744243bc372794148b84c6fc3c34cc362724889442433e8befeffffc744242f646c6c00488d4c242748ba627572706565732e4889542427ffd04883c448c39090909090909090909090ffffffffffffffff0000000000000000ffffffffffffffff0000000000000033'
Si nos fijamos en las strings del shellcode, aparece por un lado burpees
y por otro dll
. Al depurar en x64dbg, sí que se ve claramente la string burpees.dll
intentando cargarse. Por tanto, sabemos que el nombre de la DLL descifrada debe ser burpees.dll
(esta es la segunda subparte de la segunda parte de input_string
).
Por otro lado, este es un extracto el desensamblado del shellcode:
$ pwn disasm $shellcode
0: 57 push edi
1: 48 dec eax
2: 89 e7 mov edi, esp
4: 48 dec eax
5: 83 e4 f0 and esp, 0xfffffff0
8: 48 dec eax
...
81: 48 dec eax
82: 01 d0 add eax, edx
84: 49 dec ecx
85: 89 c8 mov eax, ecx
87: 49 dec ecx
88: 39 c9 cmp ecx, ecx
8a: 75 e4 jne 0x70
8c: 3d 5b bc 4a 6a cmp eax, 0x6a4abc5b
91: 74 0c je 0x9f
93: c3 ret
94: 8b 12 mov edx, DWORD PTR [edx]
96: 4d dec ebp
97: 85 d2 test edx, edx
99: 75 a5 jne 0x40
...
1d3: 48 dec eax
1d4: 89 c1 mov ecx, eax
1d6: c7 44 24 3b c3 72 79 41 mov DWORD PTR [esp+0x3b], 0x417972c3
1de: 48 dec eax
1df: b8 4c 6f c3 c3 mov eax, 0xc3c36f4c
1e4: 4c dec esp
1e5: c3 ret
1e6: 62 72 48 bound esi, QWORD PTR [edx+0x48]
1e9: 89 44 24 33 mov DWORD PTR [esp+0x33], eax
1ed: e8 be fe ff ff call 0xb0
1f2: c7 44 24 2f 64 6c 6c 00 mov DWORD PTR [esp+0x2f], 0x6c6c64
1fa: 48 dec eax
1fb: 8d 4c 24 27 lea ecx, [esp+0x27]
1ff: 48 dec eax
200: ba 62 75 72 70 mov edx, 0x70727562
205: 65 65 73 2e gs gs jae 0x237
209: 48 dec eax
20a: 89 54 24 27 mov DWORD PTR [esp+0x27], edx
20e: ff d0 call eax
...
Esta fue una parte un poco guessy en cierto sentido. El tema es que tenemos que modificar 5 bytes de este shellcode. Por un lado, hay algunos dígitos hexadecimales que, como antes, son caracteres ASCII imprimibles:
$ python3 -q
>>> from pwn import p8, p16, p32, p64
>>> p32(0x417972c3)
b'\xc3ryA'
>>> p32(0xc3c36f4c)
b'Lo\xc3\xc3'
>>> p32(0x6c6c64)
b'dll\x00'
>>> p32(0x70727562)
b'burp'
>>> bytes.fromhex('65 65 73 2e')
b'ees.'
Vemos tres caracteres \xc3
que estan en posiciones de las que tenemos que modificar. Y parece claro que la string que tienen que formar es LoadLibraryA
(como la que se ejecutaba antes). Entonces, podemos ver que:
- En
0x1da
tiene que ira
. Por tanto,param_1[1] = 'a'
. - En
0x1e2
tiene que ira
. Por tanto,param_1[4] = 'a' + 2 = 'c'
. - En
0x1e3
tiene que ird
. Por tanto,param_1[2] = 'd' + 3 = 'g'
.
Con esto, solo nos queda parchear las intrucciones ret
que aparecen en 0x93
y 0x1e5
. Y lo que tenemos de momento es que la tercera parte de input_string
es algo como ?ag?c
, nos falta averiguar las interrogaciones.
Aquí, lo que pensé fue en poner instrucciones de ensamblador que fueran de un solo byte y que no modificaran mucho el programa en sí. Vemos algunos ejemplos en el propio shellcode (como 48
, que es dec eax
; 49
, que es dec ecx
; 4c
, que es dec esp
…).
Después de un rato probando, llegué a la conclusión de que la tercera parte debía de ser una palabra, y se me ocurrió usar 49
(dec ecx
) como instrucción para 0x1e5
. Y así tenía ?agIc
como tercera parte de input_string
. Y aquí me imaginé que la palabra clave sería Magic
, que al final son instrucciones que no afectan mucho al shellcode:
$ pwn disasm 4d
0: 4d dec ebp
$ pwn disasm 69
0: 69 .byte 0x69
Análisis de burpees.dll
Bueno, entonces el shellcode anterior se ejecuta y carga la librería burpees.dll
, como habíamos deducido. En este punto, se abre una ventana con un teclado numérico para poner un PIN.
Si miramos el código descompilado en Ghidra, veremos una función WinMain
:
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
// ...
// 0x1d87 1 WinMain
local_24 = 0x2073656570727542;
local_1c = 0x4c4c44;
local_78._0_4_ = 0;
local_78._4_4_ = 0;
local_68._0_4_ = 0;
local_68._4_4_ = 0;
local_58 = NULL;
local_50 = NULL;
local_40 = NULL;
local_70 = (WNDPROC) &WindowProc;
local_38 = &local_24;
local_48 = (HBRUSH)0x6;
local_60 = hInstance;
RegisterClassA((WNDCLASSA *) local_78);
local_18 = CreateWindowExA(0, (LPCSTR) &local_24, "Es como foook, solo hazlos", 0xcf0000, -0x80000000, -0x80000000, 0x14f, 400, NULL, NULL, hInstance, NULL);
if (local_18 != NULL) {
local_c8[0] = 0x82;
local_c8[1] = 0x113;
local_c8[2] = 0x3c;
local_c8[3] = 0xe1;
local_b8 = 0x82;
local_b4 = 0xe1;
local_b0 = 200;
local_ac = 0xe1;
local_a8 = 0x3c;
local_a4 = 0xaf;
local_a0 = 0x82;
local_9c = 0xaf;
local_98 = 200;
local_94 = 0xaf;
local_90 = 0x3c;
local_8c = 0x7d;
local_88 = 0x82;
local_84 = 0x7d;
local_80 = 200;
local_7c = 0x7d;
for (local_c = 0; local_c < 10; local_c = local_c + 1) {
local_fa = (char)l ocal_c + '0';
local_f9 = 0;
CreateWindowExA(0, "BUTTON", &local_fa, 0x50010001, local_c8[(longlong) local_c * 2], local_c8[(longlong) local_c * 2 + 1], 0x3c, 0x28, local_18, (HMENU) (longlong) (local_c + 1000), NULL, NULL);
}
ShowWindow(local_18, nShowCmd);
local_f8._0_8_ = NULL;
local_f8._8_4_ = 0;
local_f8._12_4_ = 0;
local_e8 = 0;
local_e0 = 0;
local_d8._0_4_ = 0;
local_d8._4_4_ = 0;
local_d0.x = 0;
local_d0.y = 0;
while (BVar1 = GetMessageA((LPMSG) local_f8, NULL, 0, 0), BVar1 != 0) {
TranslateMessage((MSG *) local_f8);
DispatchMessageA((MSG *) local_f8);
}
}
return 0;
}
Y la función que gestiona la ventana es WindowProc
:
LRESULT UndefinedFunction_2c5ac2455(HWND param_1, uint param_2, WPARAM param_3, LPARAM param_4) {
undefined8 uVar1;
undefined8 uVar2;
int iVar3;
longlong lVar4;
tm *ptVar5;
LRESULT LVar6;
uint uStack_5c;
int iStack_58;
int iStack_54;
char acStack_41 [9];
time_t tStack_38;
int iStack_2c;
uint uStack_28;
uint uStack_24;
int iStack_20;
uint uStack_1c;
if (param_2 == 0x111) {
if (((commandCount < 8) && (uStack_1c = (uint) param_3 & 0xffff, 999 < uStack_1c)) && (uStack_1c < 0x3f2)) {
iStack_20 = uStack_1c - 1000;
lVar4 = (longlong)commandCount;
commandCount = commandCount + 1;
(&pin)[lVar4] = (char) iStack_20 + '0';
(&pin)[commandCount] = 0;
}
if (commandCount == 8) {
tStack_38 = time(NULL);
ptVar5 = localtime(&tStack_38);
uVar1._0_4_ = ptVar5->tm_hour;
uVar1._4_4_ = ptVar5->tm_mday;
uVar2._0_4_ = ptVar5->tm_mon;
uVar2._4_4_ = ptVar5->tm_year;
uStack_5c = (uint) ((ulonglong) uVar1 >> 0x20);
uStack_24 = uStack_5c;
iStack_58 = (int) uVar2;
uStack_28 = iStack_58 + 1;
iStack_54 = (int) ((ulonglong) uVar2 >> 0x20);
iStack_2c = iStack_54 + 0x76c;
sprintf(acStack_41, "%02d%02d%04d", (ulonglong) uStack_5c, (ulonglong) uStack_28, iStack_2c);
iVar3 = strcmp(&pin, acStack_41);
if (iVar3 == 0) {
generateKeys();
PostQuitMessage(0);
return 0;
}
PostQuitMessage(0);
return 0;
}
} else if (param_2 < 0x112) {
if (param_2 == 1) {
CreateWindowExA(0, "STATIC", "Hoy hay gente saliendo de fiesta\n y tu aqui... jugando un CTF...\n Recuerda levantarte cada 30m para hacerte unos burpees", 0x50000001, 10, 0x14, 300, 0x4b, param_1, NULL, NULL, NULL);
} else {
if (param_2 != 2) goto LAB_2c5ac264f;
PostQuitMessage(0);
}
return 0;
}
LAB_2c5ac264f:
LVar6 = DefWindowProcA(param_1,param_2,param_3,param_4);
return LVar6;
}
Aquí vemos que el PIN es de 8 dígitos (%02d%02d%04d
) y que se compara con pin
, pero no está inicializado. Aún así, podemos poner un breakpoint en strcmp
mediante x64dbg y mirarlo en el depurador:
Aquí vemos claramente que el PIN es 18022024
(es la fecha actual en formato ddmmyyyy
).
Vale, cuando ponemos el PIN correctamente, se ejecuta la función generateKeys
:
void generateKeys() {
undefined local_118;
char local_117;
undefined local_116;
char local_115;
char local_114;
char local_113;
undefined local_112;
char local_111;
char local_110;
undefined local_10f;
undefined local_10e;
char local_10d;
char local_10c;
char local_10b;
undefined local_10a;
undefined local_109;
undefined local_108;
int local_f8 [32];
undefined8 local_78;
undefined8 local_70;
undefined8 local_68;
undefined8 local_60;
undefined8 local_58;
undefined8 local_50;
undefined8 local_48;
undefined4 local_40;
undefined2 local_3c;
void *local_30;
LPVOID local_28;
LPVOID local_20;
HANDLE local_18;
ulonglong local_10;
local_18 = OpenFileMappingA(4, 0, "SharedMemory");
if (local_18 != NULL) {
local_28 = MapViewOfFile(local_18, 4, 0, 0, 0x1000);
local_20 = local_28;
if (local_28 == NULL) {
CloseHandle(local_18);
} else {
local_78 = 0x6867666564636261;
local_70 = 0x706f6e6d6c6b6a69;
local_68 = 0x7877767574737271;
local_60 = 0x4645444342417a79;
local_58 = 0x4e4d4c4b4a494847;
local_50 = 0x565554535251504f;
local_48 = 0x333231305a595857;
local_40 = 0x37363534;
local_3c = 0x38;
local_f8[0] = 0x13;
local_f8[1] = 7;
local_f8[2] = 0x35;
local_f8[3] = 0x12;
local_f8[4] = 0xffffffff;
local_f8[5] = 0xc;
local_f8[6] = 5;
local_f8[7] = 0xffffffff;
local_f8[8] = 2;
local_f8[9] = 0x13;
local_f8[10] = 5;
local_f8[11] = 0xffffffff;
local_f8[12] = 3;
local_f8[13] = 0x11;
local_f8[14] = 0x35;
local_f8[15] = 0x15;
local_f8[16] = 0x37;
local_f8[17] = 0x12;
local_f8[18] = 0xffffffff;
local_f8[19] = 0xc;
local_f8[20] = 0x37;
local_f8[21] = 0xffffffff;
local_f8[22] = 10;
local_f8[23] = 10;
local_f8[24] = 0x11;
local_f8[25] = 0x38;
local_f8[26] = 0x19;
local_f8[27] = 0x18;
local_f8[28] = 0xffffffff;
local_f8[29] = 0x34;
local_f8[30] = 0xffffffff;
local_f8[31] = 0x34;
local_30 = malloc(0x21);
for (local_10 = 0; local_10 < 0x21; local_10 = local_10 + 1) {
if (local_10 < 0x20) {
if (local_f8[local_10] == -1) {
if (local_10 == 0x1e) {
*(undefined *) ((longlong) local_30 + 0x1e) = 0x2e;
} else {
*(undefined *) (local_10 + (longlong) local_30) = 0x5f;
}
} else {
*(undefined *) (local_10 + (longlong) local_30) = *(undefined *) ((longlong) &local_78 + (longlong) local_f8[local_10]);
}
} else {
*(undefined *) (local_10 + (longlong) local_30) = 0;
}
}
permutateDictionary((char *) &local_78);
local_118 = 0x5f;
local_117 = (char) local_68 + ' ';
local_116 = pin;
local_115 = local_68._4_1_ + '\x06';
local_114 = local_68._3_1_ + ' ';
local_113 = local_60._5_1_ + ' ';
local_112 = 0x5f;
local_111 = local_60._5_1_ + ' ';
local_110 = local_68._3_1_ + ' ';
local_10f = DAT_2c5acd022;
local_10e = *(undefined *) ((longlong) local_28 + 10);
local_10d = local_68._4_1_ + '\x06';
local_10c = local_68._3_1_ + ' ';
local_10b = local_60._5_1_ + ' ';
local_10a = *(undefined *) ((longlong) local_28 + 14);
local_109 = 0x5f;
local_108 = 0;
decryptBuffer((longlong) &local_118, (longlong) local_30);
}
}
}
Aquí vemos algo interesante, y es que se está abriendo la región de memoria compartida llamada SharedMemory
(la que creó antes el programa main.exe
). De esta región de memoria compartida (donde se encuentra copiada la cuarta parte de input_string
) solo se toman dos caracteres (índices 10
y 14
)
Y esta última función decryptBuffer
recibe por un lado local_118
(que debe ser algún tipo de clave, y es donde aparecen caracteres de la cuarta parte) y por otro lado local_30
(que es un buffer de 32 bytes). En esta función se descifra un texto cifrado presente en la DLL y se escribe como decrypted.html
:
undefined8 decryptBuffer(longlong param_1, longlong param_2) {
PUCHAR lpMem;
undefined8 uVar1;
size_t sVar2;
HANDLE hHeap;
uint local_3c;
PUCHAR local_38;
FILE *local_30;
longlong local_28;
longlong local_20;
local_38 = NULL;
local_3c = 0;
local_28 = param_2;
local_20 = param_1;
uVar1 = SimpleDecryption(0x2c5ac9020, ciphered_size, param_2, param_1, &local_38, &local_3c);
if ((int)uVar1 == 0) {
uVar1 = 0xffffffff;
} else {
local_30 = fopen("decrypted.html", "wb");
sVar2 = fwrite(local_38, 1, (ulonglong) local_3c,l ocal_30);
if (sVar2 != local_3c) {
fclose(local_30);
}
fclose(local_30);
lpMem = local_38;
hHeap = GetProcessHeap();
HeapFree(hHeap, 0, lpMem);
system("PAUSE");
uVar1 = 0;
}
return uVar1;
}
El tipo de cifrado es AES, y la clave es autogenerada por la DLL (se almacena en local_30
). Entonces, el archivo decrypted.html
se descifra correctamente. Su contenido contiene la flag, pero no es la verdadera aún. Y aquí termina todo lo que hace la DLL. EL código HTML resultante es:
<!GOCTYPE |tml,
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Background Image Example</title>
<style>
body {
background-image: url('https://media.tenor.com/9vBAObrjRTgAAAAe/sad-yoshi.png'); /* Ruta de la imagen de fondo */
background-size: cover; /* Ajustar la imagen para cubrir todo el fondo */
background-repeat: no-repeat; /* Evitar que la imagen se repita */
}
/* Puedes agregar estilos adicionales para el contenido de la página */
.flag {
/* Estilos para el contenido */
color: white;
text-align: center;
padding: 20px;
}
</style>
</head>
<body>
<div class="flag">
<h1>HackOn{casi_maestro_(no_es_flag_)}</h1>
<p>Well done mate!</p>
</div>
</body>
</html>
Como se puede ver, la cabecera no es del todo correcta, por lo que tiene que estar ocurriendo algo más.
Final de main.exe
Aún nos queda una función que se ejecuta en main.exe
:
void FUN_1400020c9(char *param_1, longlong param_2, undefined8 param_3, undefined8 param_4) {
int iVar1;
size_t sVar2;
undefined1 (*pauVar3)[10];
undefined local_78[26];
undefined local_5e;
char *local_58;
FILE *local_50;
undefined4 local_44;
LPVOID local_40;
LPVOID local_38;
uint local_2c;
FILE *local_28;
undefined *local_20;
ulonglong local_18;
ulonglong local_10;
local_20 = &DAT_14000a0c1;
iVar1 = strcmp(param_1, "end");
if (iVar1 == 0) {
local_78[0] = *(undefined *) (param_2 + 0x1c);
local_78[1] = *(undefined *) (param_2 + 4);
local_78[2] = *(undefined *) (param_2 + 5);
local_78[3] = *(undefined *) (param_2 + 0x32);
local_78[4] = *(undefined *) (param_2 + 0xd);
local_78[5] = *(undefined *) (param_2 + 0x2f);
local_78[6] = *(undefined *) (param_2 + 2);
local_78[7] = *(undefined *) (param_2 + 0x38);
local_78[8] = *(undefined *) (param_2 + 0x1b);
local_78[9] = *(undefined *) (param_2 + 0x2c);
local_78[10] = *(undefined *) (param_2 + 0x30);
local_78[11] = *(undefined *) (param_2 + 0x2f);
local_78[12] = *(undefined *) (param_2 + 9);
local_78[13] = *(undefined *) (param_2 + 0x19);
local_78[14] = *(undefined *) (param_2 + 0x2b);
local_78[15] = *(undefined *) (param_2 + 0x3b);
local_78[16] = *(undefined *) (param_2 + 0x36);
local_78[17] = *(undefined *) (param_2 + 0x1c);
local_78[18] = *(undefined *) (param_2 + 0x11);
local_78[19] = *(undefined *) (param_2 + 0x13);
local_78[20] = *(undefined *) (param_2 + 0x12);
local_78[21] = *(undefined *) (param_2 + 0x20);
local_78[22] = *(undefined *) (param_2 + 0x40);
local_78[23] = *(undefined *) (param_2 + 0x22);
local_78[24] = *(undefined *) (param_2 + 0x3d);
local_78[25] = *(undefined *) (param_2 + 0x2f);
local_5e = 0;
pauVar3 = (undefined1 (*) [10]) &DAT_14000a002;
local_28 = fopen("decrypted.html", "rb");
if (local_28 == NULL) {
do_print("failed to open the .html", pauVar3, param_3,p aram_4);
} else {
fseek(local_28, 0, 2);
local_2c = ftell(local_28);
rewind(local_28);
local_38 = VirtualAlloc(NULL,(ulonglong) local_2c, 0x3000, 0x40);
sVar2 = fread(local_38, 1, (ulonglong) local_2c, local_28);
if (sVar2 != local_2c) {
fclose(local_28);
}
local_40 = local_38;
local_44 = 0;
for (local_10 = 0; local_10 < local_2c; local_10 = local_10 + 1) {
if ((((*(char *) (local_10 + (longlong) local_38) == 'c') &&
(*(char *) ((longlong) local_38 + local_10 + 1) == 'a')) &&
(*(char *) ((longlong) local_38 + local_10 + 2) == 's')) &&
((*(char *) ((longlong) local_38 + local_10 + 3) == 'i' &&
(*(char *) ((longlong) local_38 + local_10 + 4) == '_')))) {
for (local_18 = 0; local_18 < 0x1a; local_18 = local_18 + 1) {
*(undefined *) (local_10 + local_18 + (longlong) local_38) = local_78[local_18];
}
}
}
local_50 = fopen("decrypted.html", "wb");
sVar2 = fwrite(local_40, 1, (ulonglong) local_2c, local_50);
if (sVar2 != local_2c) {
fclose(local_50);
}
local_58 = "decrypted.html";
ShellExecuteA(NULL, "open", "decrypted.html", NULL, NULL, 1);
}
}
}
Aquí se vuelve a abrir el archivo decrypted.html
. Además, se mira que el contenido de la variable param_1
sea "end"
. Esto nunca va a ocurrir en una ejecución normal, puesto que esta variable contiene la ruta absoluta al ejecutable (mirado desde x64dbg). Por tanto, necesitamos ejecutar el programa desde el depurador y saltarnos esta comprobación para poder ejecutar el resto del código.
Básicamente lo que hace es buscar la posición de la subcadena "casi_"
y escribir ahí valores del vector local_78
. En este vector aparecen caracteres de param_2
, que es input_string
al completo. Por tanto, podemos también realizar esta parte en Python:
>>> input_string = b'ctf@hackon?/d0n0t8ruT3,th1nk_burpees.dll/Magic/??????????????????'
>>> bytes([input_string[0x1c], input_string[4], input_string[5], input_string[0x32], input_string[0xd], input_string[0x2f], input_string[2], input_string[0x38], input_string[0x1b], input_string[0x2c], input_string[0x30], input_string[0x2f], input_string[9], input_string[0x19], input_string[0x2b], input_string[0x3b], input_string[0x36], input_string[0x1c], input_string[0x11], input_string[0x13], input_string[0x12], input_string[0x20], input_string[0x40], input_string[0x22], input_string[0x3d], input_string[0x2f]])
b'_ha?0?f?ki??n1g??_8urp?e??'
Pero nos faltan muchos caracteres. Podríamos echar un vistazo a la región de memoria compartida, que es de donde se saban dos bytes que se usan probablemente como IV del cifrado AES. Para ello, tenemos que poner un breakpoint después de poner el PIN y seguir la ejecución hasta llegar a la función decryptBuffer
:
Ahí vemos tanto la clave de AES como gran parte del IV. Los caracteres que faltan podemos deducir que son u
y s
para formar el IV _n1ght_th0ughts_
.
Una vez tenemos esto, podemos ponerlo en Python de nuevo y ver cuál sería la flag:
>>> input_string = b'ctf@hackon?/d0n0t8ruT3,th1nk_burpees.dll/Magic/_n1ght_th0ughts_'
>>> bytes([input_string[0x1c], input_string[4], input_string[5], input_string[0x32], input_string[0xd], input_string[0x2f], input_string[2], input_string[0x38], input_string[0x1b], input_string[0x2c], input_string[0x30], input_string[0x2f], input_string[9], input_string[0x19], input_string[0x2b], input_string[0x3b], input_string[0x36], input_string[0x1c], input_string[0x11], input_string[0x13], input_string[0x12], input_string[0x20], input_string[0x40], input_string[0x22], input_string[0x3d], input_string[0x2f]])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: index out of range
Bueno, como da IndexError
, vamos a poner algún carácter más:
>>> input_string = b'ctf@hackon?/d0n0t8ruT3,th1nk_burpees.dll/Magic/_n1ght_th0ughts_**'
>>> bytes([input_string[0x1c], input_string[4], input_string[5], input_string[0x32], input_string[0xd], input_string[0x2f], input_string[2], input_string[0x38], input_string[0x1b], input_string[0x2c], input_string[0x30], input_string[0x2f], input_string[9], input_string[0x19], input_string[0x2b], input_string[0x3b], input_string[0x36], input_string[0x1c], input_string[0x11], input_string[0x13], input_string[0x12], input_string[0x20], input_string[0x40], input_string[0x22], input_string[0x3d], input_string[0x2f]])
b'_hag0_f0kin_n1ght_8urp*es_'
Aún queda un solo carácter que no tiene un valor claro, pero podemos pensar que es una e
.
Flag
Si ejecutamos todo desde el depurador y saltamos la comprobación de "end"
, se nos abrirá el documento HTML en un navegador mostrando la flag (aunque con el carácter raro):