PICtureThis
28 minutes to read
In this challenge they give us a Windows PE called main.exe
and an encrypted DLL (ciphered.dll
):
$ file *
ciphered.dll: data
main.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
Analyzing main.exe
At the moment, we can start by opening main.exe
in Ghidra and look at the strings. We quickly see one that indicates how the program has to be executed main.exe
:
Funcionamiento: ./reto.exe <input_string>
If we look for references to this string, we will reach the main function:
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;
}
At this point, we can rename some variables and functions so that they have more descriptive names and change some variable types:
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;
}
We see that it divides the input_string
in 4 parts using /
as separator. These parts go through functions that return 0
or 1
.
First check
The function that checks the first part is this one:
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;
}
This is a relatively simple function that verifies the part of input_string
with a certain XOR-based algorithm, if the result coincides with the bytes that appear in the local_..
variables. As XOR key, the fourth character of param_1
and the last one are used:
local_21 = param_1[3];
local_22 = param_1[(longlong) local_20 + -1];
Each character is encrypted with these two keys. With this, we can find out what is the last character of the part of input_string
. For index 0
, we have that param_1[i] = param_1[i] ^ local_21
will be 0
because initially param_1[3]
coincides with local_21
. Then, by applying XOR local_22
(the last character of the part of input_string
), we must have local_88[3] = 0x3f
, hence local_22
is 0x3f
, which stands for ?
.
Knowing this, we can schedule the decryption algorithm and apply brute force in local_21
(256 possibilities):
$ 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?'
...
Here we see two strings that could fit: ctf@hackon?
and CTF`HACKON?
, although clearly the first one makes more sense.
Second check
For this second check, the second part of input_string
is divided into two subparts, separated by _
:
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;
}
Then, it calls the FUN_140001765
, which opens and reads the file 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;
}
In addition, the previous function performs VirtualProtect
on a memory address, and then calls FUN_14000185e
with one of the subparts:
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);
}
}
}
}
This function seems complicated, but what it does is look for strings present in main.exe
and replaces some characters of the memory region with address 0x140009280
by characters of the subpart that receives as an argument:
$ 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.
In address 0x140009280
is where the VirtualProtect
is made, and moreover, a thread is executed in this same address. Therefore, the content in this region of memory is shellcode. In Ghidra, we can go to this address and disassemble it to see the shellcode in 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;
}
On the one hand, we see hexadecimal digits that correspond to ASCII printable characters and contain this information:
$ 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'
So, we can guess that it is calling these functions of the Windows API. In addition, we see checks about certain addresses such as DAT_140009530
. If we look at these addresses, we will find the w
characters from before:
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
...
And this is checked with pbVar8
, which contains the address 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
At this point we could debug with x64dbg, and we would realize that in that memory region the address in which the encrypted DLL is written is saved. In the code appears here (-0x78
is 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];
}
}
And remember that this function is called after reading ciphered.dll
, whose address is in 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);
So, we already know what characters should go there to be able to pass the if
statement:
$ 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'
With this subpart, the function FUN_140009400
is called with the content of the encrypted DLL, to decrypt it. And then, it is written as a file with WriteFile
.
Performing some tests with the debugger, we discover that the second subpart indicates the name of the file in which to write the decrypted DLL.
So, for now we have this input_string
: ctf@hackon?/d0n0t8ruT3,th1nk_filename/
.
Last checks
The next function that is executed in main
is FUN_140001fa2
, using as arguments the fourth part and the third part (in that order):
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);
}
}
}
In this function a shared memory region is created Shared Memory
. Then, it copies param_1
(a quarter of input_string
) in said shared memory region and executes FUN_140001e22
on param_2
(the third part):
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;
}
Here we see that a dynamic memory region is assigned with VirtualAlloc
, then data is copied from DAT_140009020
with memcpy
and decrypted with XOR (key 0x33
). Next, 5 characters of param_1
are taken to put them in the decrypted buffer. Finally, a thread is created and executed on this buffer. Therefore, again, a shellcode is being executed.
From this Shellcode we need to alter the bytes 0x93
, 0x1da
, 0x1e2
, 0x1e3
, and 0x1e5
. We can decrypt the shellcode and then disassemble it with 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'
If we look at the strings of shellcode, it appears burpees
and dll
. When debugging with x64dbg, the burpees.dll
string is clearly seen and the program seems to be trying to load it. Therefore, we know that the name of the decrypted DLL must be burpees.dll
(this is the second subpart of the second part of input_string
).
On the other hand, this is an extract the disassembly of the 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
...
This was a guessy part in some sense. The issue is that we have to modify 5 bytes of this shellcode.On the one hand, there are some hexadecimal digits that, as before, are ASCII printable characters:
$ 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.'
We see three characters \xc3
that are in positions that we have to modify. And it seems clear that the string they have to form is LoadLibraryA
(like the one that was executed before). So, we can see that:
- In
0x1da
we must puta
. So,param_1[1] = 'a'
. - In
0x1e2
we must puta
. So,param_1[4] = 'a' + 2 = 'c'
. - In
0x1e3
we must putd
. So,param_1[2] = 'd' + 3 = 'g'
.
With this, we only have to patch the ret
instructions that appear in 0x93
and 0x1e5
. And what we have for the moment is that the third part of input_string
is something like ?ag?c
, we need to find out the missing characters.
Here, what I thought was to put one-byte assembly instructions that would not modify much the program. We see some examples in the shellcode (like 48
, which is dec eax
; 49
, which dec ecx
; 4c
, which dec esp
…).
After a while trying, I concluded that the third part must be a word, and it occurred to me to use 49
(dec ecx
) as instruction for 0x1e5
. And so I had ?agIc
as a third part of input_string
. And here I imagined that the keyword would be Magic
, which in the end are instructions that do not affect much to shellcode:
$ pwn disasm 4d
0: 4d dec ebp
$ pwn disasm 69
0: 69 .byte 0x69
Analyzing burpees.dll
Well, then the previous shellcode is executed and loads the burpees.dll
DLL, as we had guessed. At this point, a window opens with a numerical keyboard to put a PIN.
If we look at the decompiled code in ghidra, we will see a function 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;
}
And the function that manages the window is 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;
}
Here we see that the PIN is 8 digits (%02d%02d%04d
) and that it is compared to pin
, but is not initialized. Even so, we can put a breakpoint in strcmp
via x64dbg and look at it in debugger:
Here we see clearly that the PIN is 18022024
(it is the current date in ddmmyyyy
format).
Ok, when we put the PIN correctly, the function is executed 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);
}
}
}
Here we see something interesting, and the shared memory region called SharedMemory
is being opened (the one created by main.exe
before). From this shared memory region (where the last part of input_string
was copied) only two characters are taken (10
and 14
)
And this last function decryptBuffer
receives on the one hand local_118
(which must be some type of key, and that is where characters of the fourth part appear) and on the other hand local_30
(which is a 32-byte buffer). In this function an encrypted text present in the DLL is decrypted and is written as 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;
}
The encryption algorithm is AES, and the key is self-generated by the DLL (it is stored in local_30
). Then, the decrypted.html
file is deciphered correctly. Its content contains the flag, but it is not the real one. And here ends everything that the DLL does. The resulting HTML code is:
<!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>
As can be seen, the header is not entirely correct, so something else has to be happening.
Last part of main.exe
We still have a function that runs in 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);
}
}
}
Here the decrypted.html
is reopened. In addition, it is seen that the content of param_1
is "end"
. This will never occur in a normal execution, since this variable contains the absolute path to the executable (seen from x64dbg). Therefore, we need to execute the program from the debugger and skip this check to be able to execute the rest of the code.
Basically what it does is seek the position of the substring "casi_"
and write here the values of the local_78
array. This array contains characters of param_2
, which is the full input_string
. Therefore, we can also perform this part in 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??'
But we lack many characters. We could take a look at the shared memory region, which is where two bytes are known that are probably used as IV of the AES encryption. To do this, we have to put a breakpoint after putting the PIN and follow the execution until we reach decryptBuffer
:
There we see both the AES key and part of the IV. The missing characters can be guessed to be u
and s
to form the IV: _n1ght_th0ughts_
.
Once we have this, we can put it in Python again and see what the flag would be:
>>> 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
Well, as it throws IndexError
, we will put some more characters:
>>> 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_'
There is still a single character that does not have a clear value, but we can think that it is an e
.
Flag
If we execute everything from the debugger and skip the "end"
check, the HTML document will be opened in a browser showing the flag (although with a rare character):