Hi! I’m the author of this challenge. This is the official write-up that explains what the binary does, where to look in the code, and why those places matter. We’ll keep it practical: quick triage in Ghidra to understand structure, then hands-on confirmation and extraction with radare2 (plus a tiny helper script).
Binary Info
- Format / Type: ELF64 (Linux), C++
- Arch / Bits / Endian: x86-64, 64-bit, little-endian
- Interpreter: /lib64/ld-linux-x86-64.so.2 (dynamically linked)
- Compiler: GCC 15.2.1 (optimized, modern toolchain)
- NX: true
- RELRO: partial
- PIE: true
- Other flags: stripped (true), PIC (true), relocs (false), rpath NONE, crypto false (no linked crypto libs)
First run behavior
When you execute the binary, it’s quiet and ask for input (no banner, no prompt). If you type anything normal e.g. NomanProdhan — it instantly replies:
nope
and exits.
Diving into Ghidra
Time to open the binary in Ghidra and look at the decompiled code. Here is the decompiled code from ghidra :
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
undefined8 FUN_001010c0(void)
{
undefined4 *puVar1;
uint *puVar2;
char cVar3;
short sVar4;
short sVar5;
short sVar6;
short sVar7;
short sVar8;
short sVar9;
short sVar10;
short sVar11;
short sVar12;
short sVar13;
short sVar14;
short sVar15;
short sVar16;
short sVar17;
short sVar18;
short sVar19;
undefined4 uVar20;
undefined4 uVar21;
undefined4 uVar22;
undefined auVar23 [16];
undefined auVar24 [16];
undefined auVar25 [16];
undefined auVar26 [16];
undefined8 uVar27;
char *pcVar28;
size_t sVar29;
undefined4 *puVar30;
uint *puVar31;
undefined8 uVar32;
long lVar33;
ulong uVar34;
undefined8 *puVar35;
uint uVar36;
undefined8 *__s;
undefined8 *puVar37;
uint *puVar38;
uint uVar39;
undefined *puVar40;
undefined4 *puVar41;
undefined *puVar42;
long in_FS_OFFSET;
byte bVar43;
int iVar44;
int iVar58;
undefined2 uVar59;
undefined2 uVar60;
undefined auVar46 [16];
undefined auVar50 [16];
undefined auVar54 [16];
undefined auVar57 [16];
undefined auVar61 [16];
undefined auVar65 [16];
undefined auVar69 [16];
undefined auVar72 [16];
undefined auVar74 [16];
int iVar75;
undefined2 uVar76;
undefined2 uVar77;
int iVar78;
undefined2 uVar79;
int iVar80;
int iVar81;
uint *local_2a8;
undefined *local_2a0;
undefined *local_298;
undefined *local_288;
undefined *local_280;
undefined *local_278;
undefined4 *local_268;
undefined4 *local_260;
undefined4 *local_258;
uint *local_248;
uint *local_240;
uint *local_238;
undefined4 *local_228;
long local_220;
long local_218;
undefined4 local_208 [20];
undefined4 local_1b8;
undefined4 uStack_1b4;
undefined4 uStack_1b0;
undefined4 uStack_1ac;
uint local_1a8;
uint uStack_1a4;
uint uStack_1a0;
uint uStack_19c;
undefined local_b9 [5];
undefined4 uStack_b4;
undefined4 uStack_b0;
undefined4 uStack_ac;
undefined3 local_a8;
undefined8 uStack_a5;
undefined5 uStack_9d;
undefined local_98 [16];
undefined local_88 [16];
undefined local_78 [16];
undefined local_68 [16];
undefined local_58 [16];
undefined local_48 [16];
undefined8 local_38;
long local_30;
undefined auVar45 [12];
undefined auVar47 [16];
undefined auVar51 [16];
undefined auVar48 [16];
undefined auVar52 [16];
undefined auVar55 [16];
undefined auVar49 [16];
undefined auVar53 [16];
undefined auVar56 [16];
undefined auVar62 [16];
undefined auVar66 [16];
undefined auVar63 [16];
undefined auVar67 [16];
undefined auVar70 [16];
undefined auVar64 [16];
undefined auVar68 [16];
undefined auVar71 [16];
undefined2 uVar73;
bVar43 = 0;
local_30 = *(long *)(in_FS_OFFSET + 0x28);
__s = (undefined8 *)(local_b9 + 1);
local_b9._1_4_ = 0;
uStack_b4 = 0;
uStack_b0 = 0;
uStack_ac = 0;
local_a8 = 0;
uStack_a5 = 0;
uStack_9d = 0;
local_98 = (undefined [16])0x0;
local_88 = (undefined [16])0x0;
local_78 = (undefined [16])0x0;
local_68 = (undefined [16])0x0;
local_58 = (undefined [16])0x0;
local_48 = (undefined [16])0x0;
pcVar28 = fgets((char *)__s,0x80,stdin);
if (pcVar28 != (char *)0x0) {
sVar29 = strlen((char *)__s);
if (sVar29 != 0) {
if ((local_b9[sVar29] == '\n') || (local_b9[sVar29] == '\r')) {
local_b9[sVar29] = 0;
sVar29 = sVar29 - 1;
}
if (sVar29 == 0x1b) {
local_2a8 = (uint *)operator.new(0x5b);
uVar39 = 0x5b;
local_2a0 = (undefined *)((long)local_2a8 + 0x5b);
*(undefined *)local_2a8 = 0;
puVar40 = (undefined *)((long)local_2a8 + 1);
for (lVar33 = 0x5a; lVar33 != 0; lVar33 = lVar33 + -1) {
*puVar40 = 0;
puVar40 = puVar40 + (ulong)bVar43 * -2 + 1;
}
uVar36 = 0xffffffd3;
lVar33 = 0;
do {
/* WARNING (jumptable): Read-only address (ram,0x001020f0) is written */
/* WARNING (jumptable): Read-only address (ram,0x00102100) is written */
/* WARNING (jumptable): Read-only address (ram,0x00102110) is written */
*(char *)((long)local_2a8 + lVar33) =
(char)uVar36 * '\x03' + (char)uVar39 ^ (&DAT_00102060)[(uint)lVar33 & 7] ^
(&DAT_00102080)[lVar33];
lVar33 = lVar33 + 1;
uVar39 = uVar39 + 0x3d ^ uVar36 >> 1 & 0x7f;
uVar36 = uVar36 + 0x79 ^ uVar39 * 2;
} while (lVar33 != 0x5b);
uVar34 = (ulong)*(byte *)(local_2a8 + 1);
local_298 = local_2a0;
if (uVar34 + 0x25 < 0x5c) {
puVar40 = (undefined *)((long)local_2a8 + 5);
uVar39 = *local_2a8;
puVar41 = (undefined4 *)(puVar40 + uVar34);
if (uVar34 == 0) {
local_288 = (undefined *)0x0;
local_278 = (undefined *)0x0;
}
else {
local_288 = (undefined *)operator.new(uVar34);
local_278 = local_288 + uVar34;
puVar42 = local_288;
/* WARNING (jumptable): Read-only address (ram,0x001020f0) is written */
/* WARNING (jumptable): Read-only address (ram,0x00102100) is written */
/* WARNING (jumptable): Read-only address (ram,0x00102110) is written */
for (; uVar34 != 0; uVar34 = uVar34 - 1) {
*puVar42 = *puVar40;
puVar40 = puVar40 + (ulong)bVar43 * -2 + 1;
puVar42 = puVar42 + (ulong)bVar43 * -2 + 1;
}
}
puVar42 = local_278;
puVar40 = local_288;
local_280 = local_278;
puVar30 = (undefined4 *)operator.new(0x20);
uVar20 = puVar41[1];
uVar21 = puVar41[2];
uVar22 = puVar41[3];
puVar37 = (undefined8 *)&local_1b8;
puVar1 = puVar30 + 8;
*puVar30 = *puVar41;
puVar30[1] = uVar20;
puVar30[2] = uVar21;
puVar30[3] = uVar22;
auVar74 = _DAT_001020e0;
uVar20 = puVar41[5];
uVar21 = puVar41[6];
uVar22 = puVar41[7];
lVar33 = 0x20;
puVar30[4] = puVar41[4];
puVar30[5] = uVar20;
puVar30[6] = uVar21;
puVar30[7] = uVar22;
if (uVar39 == 0) {
uVar39 = 0xc001d00d;
}
for (; puVar35 = (undefined8 *)&local_1b8, lVar33 != 0; lVar33 = lVar33 + -1) {
*puVar37 = 0;
puVar37 = puVar37 + (ulong)bVar43 * -2 + 1;
}
do {
puVar37 = puVar35 + 2;
iVar75 = auVar74._0_4_;
iVar78 = auVar74._4_4_;
iVar80 = auVar74._8_4_;
iVar81 = auVar74._12_4_;
auVar64._0_12_ = auVar74._0_12_;
auVar64._12_2_ = auVar74._6_2_;
auVar64._14_2_ = (short)((uint)(iVar78 + 4) >> 0x10);
auVar63._12_4_ = auVar64._12_4_;
auVar63._0_10_ = auVar74._0_10_;
uVar77 = (undefined2)(iVar78 + 4);
auVar63._10_2_ = uVar77;
uVar73 = auVar74._4_2_;
auVar62._10_6_ = auVar63._10_6_;
auVar62._0_8_ = auVar74._0_8_;
auVar62._8_2_ = uVar73;
uVar76 = (undefined2)((uint)(iVar75 + 4) >> 0x10);
auVar23._2_8_ = auVar62._8_8_;
auVar23._0_2_ = uVar76;
auVar23._10_6_ = 0;
auVar72._0_2_ = auVar74._0_2_;
auVar61._12_4_ = 0;
auVar61._0_12_ = SUB1612(auVar23 << 0x30,4);
auVar61 = auVar61 << 0x20;
uVar59 = auVar74._8_2_;
uVar79 = (undefined2)(iVar80 + 4);
uVar60 = auVar74._12_2_;
auVar68._0_12_ = auVar61._0_12_;
auVar68._12_2_ = uVar76;
auVar68._14_2_ = (short)((uint)(iVar80 + 4) >> 0x10);
auVar67._12_4_ = auVar68._12_4_;
auVar67._0_10_ = auVar61._0_10_;
auVar67._10_2_ = auVar74._10_2_;
auVar66._10_6_ = auVar67._10_6_;
auVar66._0_8_ = auVar61._0_8_;
auVar66._8_2_ = auVar74._2_2_;
auVar24._2_8_ = auVar66._8_8_;
auVar24._0_2_ = uVar79;
auVar24._10_6_ = 0;
auVar65._12_4_ = 0;
auVar65._0_12_ = SUB1612(auVar24 << 0x30,4);
auVar65 = auVar65 << 0x20;
auVar74._0_4_ = iVar75 + 0x10;
auVar74._4_4_ = iVar78 + 0x10;
auVar74._8_4_ = iVar80 + 0x10;
auVar74._12_4_ = iVar81 + 0x10;
iVar44 = iVar75 + 8;
iVar58 = iVar78 + 8;
auVar45._0_8_ = CONCAT44(iVar58,iVar44);
auVar45._8_4_ = iVar80 + 8;
auVar71._0_12_ = auVar65._0_12_;
auVar71._12_2_ = uVar79;
auVar71._14_2_ = (short)(iVar81 + 4);
auVar70._12_4_ = auVar71._12_4_;
auVar70._0_10_ = auVar65._0_10_;
auVar70._10_2_ = uVar77;
auVar69._10_6_ = auVar70._10_6_;
auVar69._0_8_ = auVar65._0_8_;
auVar69._8_2_ = (short)(iVar75 + 4);
auVar72._8_8_ = auVar69._8_8_;
auVar72._6_2_ = uVar60;
auVar72._4_2_ = uVar59;
auVar72._2_2_ = uVar73;
auVar49._12_2_ = (short)((uint)iVar58 >> 0x10);
auVar49._0_12_ = auVar45;
auVar49._14_2_ = (short)((uint)(iVar78 + 0xc) >> 0x10);
auVar48._12_4_ = auVar49._12_4_;
auVar48._0_10_ = auVar45._0_10_;
uVar60 = (undefined2)(iVar78 + 0xc);
auVar48._10_2_ = uVar60;
auVar47._10_6_ = auVar48._10_6_;
auVar47._8_2_ = (short)iVar58;
auVar47._0_8_ = auVar45._0_8_;
uVar59 = (undefined2)((uint)(iVar75 + 0xc) >> 0x10);
auVar25._2_8_ = auVar47._8_8_;
auVar25._0_2_ = uVar59;
auVar25._10_6_ = 0;
auVar57._0_2_ = (undefined2)iVar44;
auVar46._12_4_ = 0;
auVar46._0_12_ = SUB1612(auVar25 << 0x30,4);
auVar46 = auVar46 << 0x20;
auVar72 = auVar72 & _DAT_00102120;
uVar73 = (undefined2)(iVar80 + 0xc);
auVar53._0_12_ = auVar46._0_12_;
auVar53._12_2_ = uVar59;
auVar53._14_2_ = (short)((uint)(iVar80 + 0xc) >> 0x10);
auVar52._12_4_ = auVar53._12_4_;
auVar52._0_10_ = auVar46._0_10_;
auVar52._10_2_ = (short)((uint)auVar45._8_4_ >> 0x10);
auVar51._10_6_ = auVar52._10_6_;
auVar51._0_8_ = auVar46._0_8_;
auVar51._8_2_ = (short)((uint)iVar44 >> 0x10);
auVar26._2_8_ = auVar51._8_8_;
auVar26._0_2_ = uVar73;
auVar26._10_6_ = 0;
auVar50._12_4_ = 0;
auVar50._0_12_ = SUB1612(auVar26 << 0x30,4);
auVar50 = auVar50 << 0x20;
auVar56._0_12_ = auVar50._0_12_;
auVar56._12_2_ = uVar73;
auVar56._14_2_ = (short)(iVar81 + 0xc);
auVar55._12_4_ = auVar56._12_4_;
auVar55._0_10_ = auVar50._0_10_;
auVar55._10_2_ = uVar60;
auVar54._10_6_ = auVar55._10_6_;
auVar54._0_8_ = auVar50._0_8_;
auVar54._8_2_ = (short)(iVar75 + 0xc);
auVar57._8_8_ = auVar54._8_8_;
auVar57._6_2_ = (short)(iVar81 + 8);
auVar57._4_2_ = (short)auVar45._8_4_;
auVar57._2_2_ = (short)iVar58;
auVar57 = auVar57 & _DAT_00102120;
sVar4 = auVar72._0_2_;
sVar5 = auVar72._2_2_;
sVar6 = auVar72._4_2_;
sVar7 = auVar72._6_2_;
sVar8 = auVar72._8_2_;
sVar9 = auVar72._10_2_;
sVar10 = auVar72._12_2_;
sVar11 = auVar72._14_2_;
sVar12 = auVar57._0_2_;
sVar13 = auVar57._2_2_;
sVar14 = auVar57._4_2_;
sVar15 = auVar57._6_2_;
sVar16 = auVar57._8_2_;
sVar17 = auVar57._10_2_;
sVar18 = auVar57._12_2_;
sVar19 = auVar57._14_2_;
*(char *)puVar35 = (0 < sVar4) * (sVar4 < 0xff) * auVar72[0] - (0xff < sVar4);
*(char *)((long)puVar35 + 1) =
(0 < sVar5) * (sVar5 < 0xff) * auVar72[2] - (0xff < sVar5);
*(char *)((long)puVar35 + 2) =
(0 < sVar6) * (sVar6 < 0xff) * auVar72[4] - (0xff < sVar6);
*(char *)((long)puVar35 + 3) =
(0 < sVar7) * (sVar7 < 0xff) * auVar72[6] - (0xff < sVar7);
*(char *)((long)puVar35 + 4) =
(0 < sVar8) * (sVar8 < 0xff) * auVar72[8] - (0xff < sVar8);
*(char *)((long)puVar35 + 5) =
(0 < sVar9) * (sVar9 < 0xff) * auVar72[10] - (0xff < sVar9);
*(char *)((long)puVar35 + 6) =
(0 < sVar10) * (sVar10 < 0xff) * auVar72[12] - (0xff < sVar10);
*(char *)((long)puVar35 + 7) =
(0 < sVar11) * (sVar11 < 0xff) * auVar72[14] - (0xff < sVar11);
*(char *)(puVar35 + 1) = (0 < sVar12) * (sVar12 < 0xff) * auVar57[0] - (0xff < sVar12);
*(char *)((long)puVar35 + 9) =
(0 < sVar13) * (sVar13 < 0xff) * auVar57[2] - (0xff < sVar13);
*(char *)((long)puVar35 + 10) =
(0 < sVar14) * (sVar14 < 0xff) * auVar57[4] - (0xff < sVar14);
*(char *)((long)puVar35 + 0xb) =
(0 < sVar15) * (sVar15 < 0xff) * auVar57[6] - (0xff < sVar15);
*(char *)((long)puVar35 + 0xc) =
(0 < sVar16) * (sVar16 < 0xff) * auVar57[8] - (0xff < sVar16);
*(char *)((long)puVar35 + 0xd) =
(0 < sVar17) * (sVar17 < 0xff) * auVar57[10] - (0xff < sVar17);
*(char *)((long)puVar35 + 0xe) =
(0 < sVar18) * (sVar18 < 0xff) * auVar57[12] - (0xff < sVar18);
*(char *)((long)puVar35 + 0xf) =
(0 < sVar19) * (sVar19 < 0xff) * auVar57[14] - (0xff < sVar19);
puVar35 = puVar37;
} while (puVar37 != __s);
puVar37 = (undefined8 *)local_b9;
do {
puVar35 = (undefined8 *)((long)puVar37 + -1);
uVar39 = uVar39 << 0xd ^ uVar39;
uVar39 = uVar39 >> 0x11 ^ uVar39;
uVar39 = uVar39 << 5 ^ uVar39;
uVar34 = (ulong)uVar39 % (ulong)(uint)((0x100 - (int)local_b9) + (int)puVar37);
cVar3 = *(char *)puVar37;
*(char *)puVar37 = *(char *)((long)&local_1b8 + uVar34);
*(char *)((long)&local_1b8 + uVar34) = cVar3;
puVar37 = puVar35;
} while ((undefined8 *)&local_1b8 != puVar35);
local_268 = puVar30;
local_260 = puVar1;
local_258 = puVar1;
puVar31 = (uint *)operator.new(0x20);
puVar2 = puVar31 + 8;
*puVar31 = local_1b8 ^ 0x5a5a5a5a;
puVar31[1] = uStack_1b4 ^ 0x5a5a5a5a;
puVar31[2] = uStack_1b0 ^ 0x5a5a5a5a;
puVar31[3] = uStack_1ac ^ 0x5a5a5a5a;
puVar31[4] = local_1a8 ^ 0x5a5a5a5a;
puVar31[5] = uStack_1a4 ^ 0x5a5a5a5a;
puVar31[6] = uStack_1a0 ^ 0x5a5a5a5a;
puVar31[7] = uStack_19c ^ 0x5a5a5a5a;
local_248 = puVar31;
local_240 = puVar2;
local_238 = puVar2;
local_228 = (undefined4 *)operator.new(0x1b);
uVar27 = uStack_a5;
uVar34 = 0;
local_220 = (long)local_228 + 0x1b;
*local_228 = local_b9._1_4_;
local_228[1] = uStack_b4;
local_228[2] = uStack_b0;
local_228[3] = uStack_ac;
uVar32 = CONCAT35(local_a8,CONCAT41(uStack_ac,uStack_b0._3_1_));
puVar41 = local_208;
for (lVar33 = 0xe; lVar33 != 0; lVar33 = lVar33 + -1) {
*puVar41 = 0;
puVar41 = puVar41 + (ulong)bVar43 * -2 + 1;
}
*(undefined8 *)((long)local_228 + 0xb) = uVar32;
*(undefined8 *)((long)local_228 + 0x13) = uVar27;
for (; local_218 = local_220, uVar34 < (ulong)((long)puVar42 - (long)puVar40);
uVar34 = (ulong)((int)uVar34 + 1)) {
if ((byte)puVar40[uVar34] < 0x10) {
/* WARNING: Could not recover jumptable at 0x00101515. Too many branches */
/* WARNING: Treating indirect jump as call */
uVar32 = (*(code *)(&DAT_00102020 +
*(int *)(&DAT_00102020 + (ulong)(byte)puVar40[uVar34] * 4)))();
return uVar32;
}
}
lVar33 = 0;
do {
if ((byte)((char)lVar33 * '\r' + 0xa6U ^ *(byte *)((long)puVar31 + lVar33) ^ 0x3c) !=
*(byte *)((long)puVar30 + lVar33)) {
pcVar28 = "nope";
goto LAB_00101769;
}
lVar33 = lVar33 + 1;
} while (lVar33 != 0x20);
pcVar28 = "congrats!";
LAB_00101769:
puts(pcVar28);
do {
puVar37 = (undefined8 *)((long)__s + 1);
*(char *)__s = '\0';
__s = puVar37;
} while (puVar37 != &local_38);
do {
puVar38 = (uint *)((long)puVar31 + 1);
*(undefined *)puVar31 = 0;
puVar31 = puVar38;
} while (puVar38 != puVar2);
do {
puVar41 = (undefined4 *)((long)puVar30 + 1);
*(undefined *)puVar30 = 0;
puVar30 = puVar41;
} while (puVar41 != puVar1);
FUN_00101950(&local_228);
FUN_00101950(&local_248);
FUN_00101950(&local_268);
FUN_00101950(&local_288);
}
else {
puts("nope");
}
FUN_00101950(&local_2a8);
goto LAB_00101523;
}
}
puts("nope");
}
LAB_00101523:
if (local_30 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
void FUN_00101950(void **param_1)
{
void *pvVar1;
pvVar1 = *param_1;
if (pvVar1 != (void *)0x0) {
operator.delete(pvVar1,(long)param_1[2] - (long)pvVar1);
return;
}
return;
}
From the decompiled entry function FUN_001010c0 (our “main” here), we can see it reads a line, trims \n/\r, and immediately enforces an exact-length gate:
pcVar28 = fgets((char *)__s, 0x80, stdin);
if (pcVar28 != (char *)0x0) {
sVar29 = strlen((char *)__s);
if (sVar29 != 0) {
if ((local_b9[sVar29] == '\n') || (local_b9[sVar29] == '\r')) {
local_b9[sVar29] = 0;
sVar29 = sVar29 - 1;
}
if (sVar29 == 0x1b) { // 0x1b == 27
/* lots of task… */
}
}
puts("nope"); // any length ≠ 27 -> nope
}
So only 27-byte inputs can pass the first gate. That explains the instant “nope” on casual tests and fixes our search space.
Plan : Dump & Emulate The decompiled routine does a lot—SSE shuffles, an S-box, a tiny VM, and a masked compare. We don’t need to re-derive all of that. Instead, we’ll grab the decrypted blob at runtime and then emulate the tiny VM.
Dynamic Analysis
Open the binary in radare2 (debug mode):
r2 -d talk_to_me.ks
Run analysis and list functions:
aaa # analyze all (functions/refs/BBs)
afl # show function list
You’ll notice radare2 names the entry routine main
even though Ghidra didn’t. Seek to it and disassemble:
s main # jump to main
pdf # print disassembly of the function
There’s a lot of operations in main
, so we’ll follow a simple path. Since wrong-length inputs and wrong 27-byte inputs both end in nope, let’s find where that string lives and who calls it.
izz~nope
You’ll see something like:
44 0x0000200e 0x55f08bbdc00e 4 5 .rodata ascii nope
Find all cross-references :
axt 0x55f08bbdc00e
This returns a couple of call sites inside main
as following :
main 0x55f08bbdb517 [DATA:r--] lea rdi, str.nope
main 0x55f08bbdb762 [DATA:r--] lea rdi, str.nope
main 0x55f08bbdb7ef [DATA:r--] lea rdi, str.nope
Set breakpoints at those xref addresses
db 0x55f08bbdb517
db 0x55f08bbdb762
db 0x55f08bbdb7ef
Now when you run with a bad input, you’ll break right before the program prints nope. From there you can scroll up a bit to see the surrounding logic and identify the final masked-compare loop and its buffers.
After setting breakpoints, run the program and feed any 27 characters, then press Enter. It’ll stop at one of the nope call:
dc
# type 27 chars (e.g., nnnnnnnnnnnnnnnnnnnnnnnnn) and hit Enter
If your input length is exactly 27, you should hit a breakpoint right before puts("nope"). From there, you can scroll a bit upwards in the disassembly to see the surrounding logic that leads into the final masked-compare loop and its two buffers.
When the breakpoint hit, I flipped to the disassembly and immediately noticed the VM dispatch: a bounds check, an opcode fetch, then an indirect jump through a switch table in .rodata. That confirms there’s a tiny interpreter chewing on a decrypted blob before the final compare. Here’s the slice we care about :
┌───────> 0x55f08bbdb200 4889cf mov rdi, rcx
│ ╎│╎││││ 0x55f08bbdb203 8d0452 lea eax, [rdx + rdx*2]
│ ╎│╎││││ 0x55f08bbdb206 83e707 and edi, 7
│ ╎│╎││││ 0x55f08bbdb209 01f0 add eax, esi
│ ╎│╎││││ 0x55f08bbdb20b 83c63d add esi, 0x3d ; 61
│ ╎│╎││││ 0x55f08bbdb20e 4132043a xor al, byte [r10 + rdi]
│ ╎│╎││││ 0x55f08bbdb212 89d7 mov edi, edx
│ ╎│╎││││ 0x55f08bbdb214 41320408 xor al, byte [r8 + rcx]
│ ╎│╎││││ 0x55f08bbdb218 83c279 add edx, 0x79 ; 121
│ ╎│╎││││ 0x55f08bbdb21b 40d0ef shr dil, 1
│ ╎│╎││││ 0x55f08bbdb21e 41880409 mov byte [r9 + rcx], al
│ ╎│╎││││ 0x55f08bbdb222 4883c101 add rcx, 1
│ ╎│╎││││ 0x55f08bbdb226 31fe xor esi, edi
│ ╎│╎││││ 0x55f08bbdb228 8d3c36 lea edi, [rsi + rsi]
│ ╎│╎││││ 0x55f08bbdb22b 31fa xor edx, edi
│ ╎│╎││││ 0x55f08bbdb22d 4883f95b cmp rcx, 0x5b ; '[' ; 91
│ └───────< 0x55f08bbdb231 75cd jne 0x55f08bbdb200
│ │╎││││ 0x55f08bbdb233 450fb66104 movzx r12d, byte [r9 + 4]
│ │╎││││ 0x55f08bbdb238 498d442425 lea rax, [r12 + 0x25]
│ │╎││││ 0x55f08bbdb23d 4883f85b cmp rax, 0x5b ; '[' ; 91
│ ┌───────< 0x55f08bbdb241 0f87a8050000 ja 0x55f08bbdb7ef
│ ││╎││││ 0x55f08bbdb247 498d7105 lea rsi, [r9 + 5]
│ ││╎││││ 0x55f08bbdb24b 458b01 mov r8d, dword [r9]
│ ││╎││││ 0x55f08bbdb24e 4a8d2c26 lea rbp, [rsi + r12]
│ ││╎││││ 0x55f08bbdb252 4d85e4 test r12, r12
│ ────────< 0x55f08bbdb255 0f84ca050000 je 0x55f08bbdb825
│ ││╎││││ 0x55f08bbdb25b 4c89e7 mov rdi, r12
│ ││╎││││ 0x55f08bbdb25e 44890424 mov dword [rsp], r8d
│ ││╎││││ 0x55f08bbdb262 4889742408 mov qword [var_8h], rsi
│ ││╎││││ 0x55f08bbdb267 e8d4fdffff call sym operator new(unsigned long) ; sym.imp.operator_new_unsigned_long_
│ ││╎││││ ; operator new(unsigned long)
│ ││╎││││ 0x55f08bbdb26c 488b742408 mov rsi, qword [var_8h]
│ ││╎││││ 0x55f08bbdb271 4c89e1 mov rcx, r12
│ ││╎││││ 0x55f08bbdb274 448b0424 mov r8d, dword [rsp]
│ ││╎││││ 0x55f08bbdb278 4889c7 mov rdi, rax
│ ││╎││││ 0x55f08bbdb27b 4e8d1c20 lea r11, [rax + r12]
│ ││╎││││ 0x55f08bbdb27f 4889442440 mov qword [var_40h], rax
│ ││╎││││ 0x55f08bbdb284 4989c2 mov r10, rax
│ ││╎││││ 0x55f08bbdb287 f3a4 rep movsb byte [rdi], byte [rsi]
│ ││╎││││ 0x55f08bbdb289 4c895c2450 mov qword [var_50h], r11
│ ││╎││││ ; CODE XREF from main @ 0x55f08bbdb837(x)
│ ────────> 0x55f08bbdb28e bf20000000 mov edi, 0x20 ; 32
│ ││╎││││ 0x55f08bbdb293 4c89542410 mov qword [var_10h], r10
│ ││╎││││ 0x55f08bbdb298 4489442408 mov dword [var_8h], r8d
│ ││╎││││ 0x55f08bbdb29d 4c895c2448 mov qword [var_48h], r11
│ ││╎││││ 0x55f08bbdb2a2 4c891c24 mov qword [rsp], r11
│ ││╎││││ 0x55f08bbdb2a6 e895fdffff call sym operator new(unsigned long) ; sym.imp.operator_new_unsigned_long_
│ ││╎││││ ; operator new(unsigned long)
│ ││╎││││ 0x55f08bbdb2ab f30f6f4500 movdqu xmm0, xmmword [rbp]
│ ││╎││││ 0x55f08bbdb2b0 448b442408 mov r8d, dword [var_8h]
│ ││╎││││ 0x55f08bbdb2b5 488dbc2410.. lea rdi, [var_110h]
│ ││╎││││ 0x55f08bbdb2bd 4c8d4820 lea r9, [rax + 0x20]
│ ││╎││││ 0x55f08bbdb2c1 4889442460 mov qword [var_60h], rax
│ ││╎││││ 0x55f08bbdb2c6 4889c6 mov rsi, rax
│ ││╎││││ 0x55f08bbdb2c9 4c8b1c24 mov r11, qword [rsp]
│ ││╎││││ 0x55f08bbdb2cd 0f1100 movups xmmword [rax], xmm0
│ ││╎││││ 0x55f08bbdb2d0 f30f6f4510 movdqu xmm0, xmmword [var_120h]
│ ││╎││││ 0x55f08bbdb2d5 4585c0 test r8d, r8d
│ ││╎││││ 0x55f08bbdb2d8 b920000000 mov ecx, 0x20 ; 32
│ ││╎││││ 0x55f08bbdb2dd 4c894c2470 mov qword [var_70h], r9
│ ││╎││││ 0x55f08bbdb2e2 488dac2410.. lea rbp, [var_110h]
│ ││╎││││ 0x55f08bbdb2ea 660f6f15ee.. movdqa xmm2, xmmword [0x55f08bbdc0e0] ; [0x55f08bbdc0e0:16]=-1
│ ││╎││││ 0x55f08bbdb2f2 0f114010 movups xmmword [rax + 0x10], xmm0
│ ││╎││││ 0x55f08bbdb2f6 b80dd001c0 mov eax, 0xc001d00d
│ ││╎││││ 0x55f08bbdb2fb 66440f6f0d.. movdqa xmm9, xmmword [0x55f08bbdc0f0] ; [0x55f08bbdc0f0:16]=-1
│ ││╎││││ 0x55f08bbdb304 66440f6f05.. movdqa xmm8, xmmword [str._b_b_b_b_f_f_f_f] ; [0x55f08bbdc100:16]=-1
│ ││╎││││ 0x55f08bbdb30d 4c894c2468 mov qword [var_68h], r9
│ ││╎││││ 0x55f08bbdb312 440f44c0 cmove r8d, eax
│ ││╎││││ 0x55f08bbdb316 660f6f3df2.. movdqa xmm7, xmmword [0x55f08bbdc110] ; [0x55f08bbdc110:16]=-1
│ ││╎││││ 0x55f08bbdb31e 31c0 xor eax, eax
│ ││╎││││ 0x55f08bbdb320 f348ab rep stosq qword [rdi], rax
│ ││╎││││ 0x55f08bbdb323 660f6f2df5.. movdqa xmm5, xmmword [0x55f08bbdc120] ; [0x55f08bbdc120:16]=-1
│ ││╎││││ 0x55f08bbdb32b 4c8b542410 mov r10, qword [var_10h]
│ ││╎││││ 0x55f08bbdb330 4889e8 mov rax, rbp
│ ││╎││││ 0x55f08bbdb333 660f6f35f5.. movdqa xmm6, xmmword [0x55f08bbdc130] ; [0x55f08bbdc130:16]=-1
│ ││╎││││ 0x55f08bbdb33b 0f1f440000 nop dword [rax + rax]
│ ────────> 0x55f08bbdb340 660f6fda movdqa xmm3, xmm2
│ ││╎││││ 0x55f08bbdb344 660f6fca movdqa xmm1, xmm2
│ ││╎││││ 0x55f08bbdb348 660f6fc2 movdqa xmm0, xmm2
│ ││╎││││ 0x55f08bbdb34c 4883c010 add rax, 0x10 ; 16
│ ││╎││││ 0x55f08bbdb350 66410ffed9 paddd xmm3, xmm9
│ ││╎││││ 0x55f08bbdb355 660f6fe2 movdqa xmm4, xmm2
│ ││╎││││ 0x55f08bbdb359 660f61cb punpcklwd xmm1, xmm3
│ ││╎││││ 0x55f08bbdb35d 660f69c3 punpckhwd xmm0, xmm3
│ ││╎││││ 0x55f08bbdb361 660ffee7 paddd xmm4, xmm7
│ ││╎││││ 0x55f08bbdb365 660f6fd9 movdqa xmm3, xmm1
│ ││╎││││ 0x55f08bbdb369 660f61c8 punpcklwd xmm1, xmm0
│ ││╎││││ 0x55f08bbdb36d 660f69d8 punpckhwd xmm3, xmm0
│ ││╎││││ 0x55f08bbdb371 660f6fc2 movdqa xmm0, xmm2
│ ││╎││││ 0x55f08bbdb375 660ffed6 paddd xmm2, xmm6
│ ││╎││││ 0x55f08bbdb379 66410ffec0 paddd xmm0, xmm8
│ ││╎││││ 0x55f08bbdb37e 660f61cb punpcklwd xmm1, xmm3
│ ││╎││││ 0x55f08bbdb382 660f6fd8 movdqa xmm3, xmm0
│ ││╎││││ 0x55f08bbdb386 660f61c4 punpcklwd xmm0, xmm4
│ ││╎││││ 0x55f08bbdb38a 660fdbcd pand xmm1, xmm5
│ ││╎││││ 0x55f08bbdb38e 660f69dc punpckhwd xmm3, xmm4
│ ││╎││││ 0x55f08bbdb392 660f6fe0 movdqa xmm4, xmm0
│ ││╎││││ 0x55f08bbdb396 660f69e3 punpckhwd xmm4, xmm3
│ ││╎││││ 0x55f08bbdb39a 660f61c3 punpcklwd xmm0, xmm3
│ ││╎││││ 0x55f08bbdb39e 660f61c4 punpcklwd xmm0, xmm4
│ ││╎││││ 0x55f08bbdb3a2 660fdbc5 pand xmm0, xmm5
│ ││╎││││ 0x55f08bbdb3a6 660f67c8 packuswb xmm1, xmm0
│ ││╎││││ 0x55f08bbdb3aa 0f2948f0 movaps xmmword [rax - 0x10], xmm1
│ ││╎││││ 0x55f08bbdb3ae 4839d8 cmp rax, rbx
│ ────────< 0x55f08bbdb3b1 758d jne 0x55f08bbdb340
│ ││╎││││ 0x55f08bbdb3b3 488d8dff00.. lea rcx, [rbp + 0xff]
│ ││╎││││ 0x55f08bbdb3ba bf00010000 mov edi, 0x100 ; 256
│ ││╎││││ 0x55f08bbdb3bf 29cf sub edi, ecx
│ ││╎││││ 0x55f08bbdb3c1 0f1f4000 nop dword [rax]
│ ││╎││││ 0x55f08bbdb3c5 66662e0f1f.. nop word cs:[rax + rax]
│ ────────> 0x55f08bbdb3d0 4489c2 mov edx, r8d
│ ││╎││││ 0x55f08bbdb3d3 448d240f lea r12d, [rdi + rcx]
│ ││╎││││ 0x55f08bbdb3d7 4883e901 sub rcx, 1
│ ││╎││││ 0x55f08bbdb3db c1e20d shl edx, 0xd
│ ││╎││││ 0x55f08bbdb3de 4431c2 xor edx, r8d
│ ││╎││││ 0x55f08bbdb3e1 89d0 mov eax, edx
│ ││╎││││ 0x55f08bbdb3e3 c1e811 shr eax, 0x11
│ ││╎││││ 0x55f08bbdb3e6 31d0 xor eax, edx
│ ││╎││││ 0x55f08bbdb3e8 31d2 xor edx, edx
│ ││╎││││ 0x55f08bbdb3ea 4189c0 mov r8d, eax
│ ││╎││││ 0x55f08bbdb3ed 41c1e005 shl r8d, 5
│ ││╎││││ 0x55f08bbdb3f1 4131c0 xor r8d, eax
│ ││╎││││ 0x55f08bbdb3f4 4489c0 mov eax, r8d
│ ││╎││││ 0x55f08bbdb3f7 41f7f4 div r12d
│ ││╎││││ 0x55f08bbdb3fa 0fb64101 movzx eax, byte [rcx + 1]
│ ││╎││││ 0x55f08bbdb3fe 440fb6a414.. movzx r12d, byte [rsp + rdx + 0x110]
│ ││╎││││ 0x55f08bbdb407 44886101 mov byte [rcx + 1], r12b
│ ││╎││││ 0x55f08bbdb40b 8884141001.. mov byte [rsp + rdx + 0x110], al
│ ││╎││││ 0x55f08bbdb412 4839cd cmp rbp, rcx
│ ────────< 0x55f08bbdb415 75b9 jne 0x55f08bbdb3d0
│ ││╎││││ 0x55f08bbdb417 bf20000000 mov edi, 0x20 ; 32
│ ││╎││││ 0x55f08bbdb41c 4c89542418 mov qword [var_18h], r10
│ ││╎││││ 0x55f08bbdb421 4531ff xor r15d, r15d
│ ││╎││││ 0x55f08bbdb424 4c895c2410 mov qword [var_10h], r11
│ ││╎││││ 0x55f08bbdb429 4c894c2408 mov qword [var_8h], r9
│ ││╎││││ 0x55f08bbdb42e 48893424 mov qword [rsp], rsi
│ ││╎││││ 0x55f08bbdb432 e809fcffff call sym operator new(unsigned long) ; sym.imp.operator_new_unsigned_long_
│ ││╎││││ ; operator new(unsigned long)
│ ││╎││││ 0x55f08bbdb437 660f6f0501.. movdqa xmm0, xmmword [str.ZZZZZZZZZZZZZZZZ] ; [0x55f08bbdc140:16]=-1 ; "ZZZZZZZZZZZZZZZZ\x01\x1b\x03;,"
│ ││╎││││ 0x55f08bbdb43f bf1b000000 mov edi, 0x1b ; r15
│ ││╎││││ 0x55f08bbdb444 660f6f8c24.. movdqa xmm1, xmmword [var_110h]
│ ││╎││││ 0x55f08bbdb44d 4c8d6020 lea r12, [rax + 0x20]
│ ││╎││││ 0x55f08bbdb451 4889842480.. mov qword [var_80h], rax
│ ││╎││││ 0x55f08bbdb459 4889c5 mov rbp, rax
│ ││╎││││ 0x55f08bbdb45c 660fefc8 pxor xmm1, xmm0
│ ││╎││││ 0x55f08bbdb460 4c89a42490.. mov qword [var_90h], r12
│ ││╎││││ 0x55f08bbdb468 660fef8424.. pxor xmm0, xmmword [var_120h]
│ ││╎││││ 0x55f08bbdb471 0f1108 movups xmmword [rax], xmm1
│ ││╎││││ 0x55f08bbdb474 0f114010 movups xmmword [rax + 0x10], xmm0
│ ││╎││││ 0x55f08bbdb478 4c89a42488.. mov qword [var_88h], r12
│ ││╎││││ 0x55f08bbdb480 e8bbfbffff call sym operator new(unsigned long) ; sym.imp.operator_new_unsigned_long_
│ ││╎││││ ; operator new(unsigned long)
│ ││╎││││ 0x55f08bbdb485 4c8b5c2410 mov r11, qword [var_10h]
│ ││╎││││ 0x55f08bbdb48a 488b3424 mov rsi, qword [rsp]
│ ││╎││││ 0x55f08bbdb48e 31d2 xor edx, edx
│ ││╎││││ 0x55f08bbdb490 4989c0 mov r8, rax
│ ││╎││││ 0x55f08bbdb493 4c8b542418 mov r10, qword [var_18h]
│ ││╎││││ 0x55f08bbdb498 b90e000000 mov ecx, 0xe ; 14
│ ││╎││││ 0x55f08bbdb49d 660f6f8424.. movdqa xmm0, xmmword [var_210h]
│ ││╎││││ 0x55f08bbdb4a6 48898424a0.. mov qword [var_a0h], rax
│ ││╎││││ 0x55f08bbdb4ae 488d401b lea rax, [rax + 0x1b]
│ ││╎││││ 0x55f08bbdb4b2 4c8b4c2408 mov r9, qword [var_8h]
│ ││╎││││ 0x55f08bbdb4b7 488dbc24c0.. lea rdi, [var_c0h]
│ ││╎││││ 0x55f08bbdb4bf 410f1100 movups xmmword [r8], xmm0
│ ││╎││││ 0x55f08bbdb4c3 4d29d3 sub r11, r10
│ ││╎││││ 0x55f08bbdb4c6 f30f6f8424.. movdqu xmm0, xmmword [var_21bh]
│ ││╎││││ 0x55f08bbdb4cf 48898424b0.. mov qword [var_b0h], rax
│ ││╎││││ 0x55f08bbdb4d7 48898424a8.. mov qword [var_a8h], rax
│ ││╎││││ 0x55f08bbdb4df 31c0 xor eax, eax
│ ││╎││││ 0x55f08bbdb4e1 f3ab rep stosd dword [rdi], eax
│ ││╎││││ 0x55f08bbdb4e3 410f11400b movups xmmword [r8 + 0xb], xmm0
│ ││╎││││ 0x55f08bbdb4e8 488d3d310b.. lea rdi, [0x55f08bbdc020]
│ ││╎││││ 0x55f08bbdb4ef 90 nop
│ ││╎││││ ; CODE XREF from main @ 0x55f08bbdb814(x)
│ ────────> 0x55f08bbdb4f0 89d0 mov eax, edx
│ ││╎││││ 0x55f08bbdb4f2 4c39d8 cmp rax, r11
│ ────────< 0x55f08bbdb4f5 0f8322020000 jae 0x55f08bbdb71d
│ ││╎││││ 0x55f08bbdb4fb 8d4a01 lea ecx, [rdx + 1]
│ ││╎││││ 0x55f08bbdb4fe 41803c020f cmp byte [r10 + rax], 0xf
│ ────────< 0x55f08bbdb503 0f8709030000 ja case.default.0x55f08bbdb515
│ ││╎││││ 0x55f08bbdb509 410fb60402 movzx eax, byte [r10 + rax]
│ ││╎││││ 0x55f08bbdb50e 48630487 movsxd rax, dword [rdi + rax*4]
│ ││╎││││ 0x55f08bbdb512 4801f8 add rax, rdi
│ ││╎││││ ;-- switch:
│ ││╎││││ 0x55f08bbdb515 ffe0 jmp rax ; switch table (16 cases) at 0x55f08bbdc020
│ │└───└──> 0x55f08bbdb517 b 488d3df00a.. lea rdi, str.nope ; 0x55f08bbdc00e ; "nope"
│ │ ╎││ │ 0x55f08bbdb51e e84dfbffff call sym.imp.puts ; int puts(const char *s)
│ │ ╎││ │ ; CODE XREF from main @ 0x55f08bbdb805(x)
│ │ ╎││┌└─> 0x55f08bbdb523 488b842498.. mov rax, qword [var_298h]
│ │ ╎││╎ 0x55f08bbdb52b 64482b0425.. sub rax, qword fs:[0x28]
│ │ ╎││╎┌─< 0x55f08bbdb534 0f8502030000 jne 0x55f08bbdb83c
│ │ ╎││╎│ 0x55f08bbdb53a 4881c4a002.. add rsp, 0x2a0
│ │ ╎││╎│ 0x55f08bbdb541 31c0 xor eax, eax
│ │ ╎││╎│ 0x55f08bbdb543 5b pop rbx
│ │ ╎││╎│ 0x55f08bbdb544 5d pop rbp
│ │ ╎││╎│ 0x55f08bbdb545 415c pop r12
│ │ ╎││╎│ 0x55f08bbdb547 415e pop r14
│ │ ╎││╎│ 0x55f08bbdb549 415f pop r15
│ │ ╎││╎│ 0x55f08bbdb54b c3 ret
│ │ ╎└└───> 0x55f08bbdb54c c684141002.. mov byte [rsp + rdx + 0x210], 0
│ │ ╎ ╎│ 0x55f08bbdb554 4889d0 mov rax, rdx
│ │ └─────< 0x55f08bbdb557 e91efcffff jmp 0x55f08bbdb17a
..
│ ────────> 0x55f08bbdb71d 31d2 xor edx, edx
│ │ │╎│ 0x55f08bbdb71f b90d000000 mov ecx, 0xd ; rcx
│ │ ┌────< 0x55f08bbdb724 eb28 jmp 0x55f08bbdb74e
..
│ │ ┌─────> 0x55f08bbdb740 4883c201 add rdx, 1
│ │ ╎││╎│ 0x55f08bbdb744 4883fa20 cmp rdx, 0x20 ; 32
│ │┌──────< 0x55f08bbdb748 0f84cb000000 je 0x55f08bbdb819
│ ││╎││╎│ ; CODE XREF from main @ 0x55f08bbdb724(x)
│ ││╎└────> 0x55f08bbdb74e 89d0 mov eax, edx
│ ││╎ │╎│ 0x55f08bbdb750 0fafc1 imul eax, ecx
│ ││╎ │╎│ 0x55f08bbdb753 83e85a sub eax, 0x5a ; 90
│ ││╎ │╎│ 0x55f08bbdb756 32441500 xor al, byte [rbp + rdx]
│ ││╎ │╎│ 0x55f08bbdb75a 83f03c xor eax, 0x3c ; 60
│ ││╎ │╎│ 0x55f08bbdb75d 3a0416 cmp al, byte [rsi + rdx]
│ ││└─────< 0x55f08bbdb760 74de je 0x55f08bbdb740
│ ││ │╎│ ;-- rip:
│ ││ │╎│ 0x55f08bbdb762 b 488d3da508.. lea rdi, str.nope ; 0x55f08bbdc00e ; "nope"
If your address differs due to ASLR, just search for it after pdf main: look for the first movzx r12d, byte [r9 + 4] immediately following the loop that fills 0x5B bytes.
Break right before the blob is consumed
After the 0x5B-byte decode loop, the code reads a length byte from [r9+4] and then copies that many bytes as the VM bytecode. That’s the perfect place to pause and dump the whole plaintext blob.
In my run, that instruction is at:
0x55f08bbdb233 450fb66104 movzx r12d, byte [r9 + 4]
Set the breakpoint and run:
db 0x55f08bbdb233
dc
It will show nope instantly ! re-run the program and input 27-bytes and it will hit our new breakpoint.
You’ve hit the “post-decode, pre-use” breakpoint. At this point the program has already built a plaintext blob in memory and is about to consume it (VM + compare). That blob’s base pointer lives in r9. We’ll dump it so we can emulate everything.
Find the base address of the blob (in r9) with following command :
?v r9
You will get something like following :
0x562b86079730
Peek the first 0x40 bytes at r9 with the following command :
px 0x40 @ r9
You will get something like following :
- offset - 3031 3233 3435 3637 3839 3A3B 3C3D 3E3F 0123456789ABCDEF
0x562b86079730 fe0f dcba 3601 0100 0102 1b02 0003 0004 ....6...........
0x562b86079740 00a5 0e00 010b 010c 020f 02ef 0100 5509 ..............U.
0x562b86079750 001b 0100 aa09 001c 0100 1109 001d 0100 ................
0x562b86079760 2209 001e 0100 3309 001f 0040 6625 aeda ".....3....@f%..
Read the 4-byte seed at r9
From the decompiled code, the blob base is treated as a uint and read with uVar39 = local2a8; right at the start. That is a 32-bit (4-byte) load, so the seed at the blob start is exactly 4 bytes. Immediately after, the code reads a single byte at (local2a8 + 1) (offset +4) for the VM length—confirming the layout. VM/permute step seeds a PRNG; we’ll need the same seed for emulation/unmasking
p8 4 @ r9
You will get something like followign :
fe0fdcba
Read the 1-byte VM length at r9+4 tells us how many bytes to copy as bytecode
p8 1 @ r9+4
You will get something like this :
36
The VM bytecode starting at r9+5 for vm_len (0x36) bytes**
wtf bytecode.bin 0x36 @ r9+5
You will get a file called bytecode.bin in your current directory. We won't need this to get flag. The final check uses a fixed mask over the 32-byte “expected” buffer; once you dump that buffer (goal.bin) you can unmask it and invert the S-box step with the PRNG seed, so the bytecode itself isn’t required.
Dump the 32-byte masked “goal” array.
wtf goal.bin 0x20 @ r9+5+0x36
You will get a file called goal.bin in your current directory. We will use this to get our flag.
Creating the Solver script
I didn’t hand-type the whole solver from scratch. I fed an LLM (ChatGPT) the facts I extracted with radare2, and asked it to draft a clean, beginner-friendly script:
What I provided to the LLM (from r2):
- The seed at r9 (p8 4 @ r9 → little-endian 0xBADC0FFE).
- The VM length byte at r9+4 (p8 1 @ r9+4 → 0x36 in my run).
- The bytecode region (starts at r9+5, length vm_len) — not required to recover the flag, but useful for referencing the constant writes.
- The masked 32-byte goal buffer at r9+5+vm_len (wtf goal.bin 0x20 @ r9+5+0x36).
- The verify loop mask formula from the disassembly (((0xA6 + 13*i) ^ 0x3C)).
- The S-box build (xorshift/Fisher–Yates) and the fallback seed 0xC001D00D.
- The tail constants written by opcode 0x09 in the bytecode (0x55, 0xAA, 0x11, 0x22, 0x33 to indices 27..31).
Here is the solver script :
#!/usr/bin/env python3
"""
Recover the 27-byte flag from the runtime dump `goal.bin`.
# What we dumped (from radare2 while hitting the r9 blob breakpoint)
- Seed (4 bytes, LE) at r9: `p8 4 @ r9` -> fe 0f dc ba == 0xBADC0FFE
- VM length (1 byte) at r9+4: `p8 1 @ r9+4` -> 0x36
- Masked goal (32 bytes) at: `wtf goal.bin 0x20 @ r9+5+0x36`
You do NOT need the VM bytecode to recover the flag.
# Why this works (quick map to the decompiler)
- The program builds a PRNG S-box from `seed` (xorshift32 + Fisher-Yates).
Ghidra: `if (uVar39 == 0) { uVar39 = 0xc001d00d; }` then xorshift loop + swap.
- It creates a 32-byte table from the S-box and XORs it with 0x5A.
(Net effect for us: init_mem[i] = SBOX[i] ^ 0x5A)
- For each of the first 27 bytes, it XORs the table with (SBOX(input[i]) ^ 0xA5).
- Finally it compares the result to an “expected” 32-byte buffer, but with a mask:
mask[i] = ((0xA6 + 13*i) & 0xFF) ^ 0x3C
Decompiler loop:
imul eax, 0x0d; sub 0x5a; xor [rbp+rdx]; xor 0x3c; cmp al, [rsi+rdx]
So if we:
1) unmask the dumped goal (remove the per-index mask),
2) subtract the 32-byte init table (SBOX[i] ^ 0x5A),
3) invert the S-box,
…we recover the original 27 input bytes (the flag).
"""
from pathlib import Path
# ---- inputs you already dumped in r2 ----
GOAL_PATH = Path("goal.bin") # 32 masked bytes from: wtf goal.bin 0x20 @ r9+5+0x36
SEED = 0xBADC0FFE # from: p8 4 @ r9 -> fe 0f dc ba (little-endian)
N_FLAG = 27 # required input length (0x1B)
# ---- constants lifted from the binary ----
# If seed == 0, the code replaces it with 0xC001D00D (fallback). We keep it for reference.
FALLBACK_SEED = 0xC001D00D # seen in decomp: if (uVar39 == 0) uVar39 = 0xc001d00d
def xorshift32(x: int) -> int:
"""One step of the PRNG used in S-box generation (<<13, >>17, <<5)."""
x ^= (x << 13) & 0xFFFFFFFF
x ^= (x >> 17) & 0xFFFFFFFF
x ^= (x << 5) & 0xFFFFFFFF
return x & 0xFFFFFFFF
def make_sbox(seed: int) -> list:
"""
Build the 256-byte S-box via Fisher-Yates shuffling driven by xorshift32.
Decompiler: the long loop that repeatedly computes a PRNG value, mods by
the remaining range, and swaps bytes (classic Fisher-Yates).
"""
s = seed if seed != 0 else FALLBACK_SEED
a = list(range(256))
for i in range(255, 0, -1):
s = xorshift32(s)
j = s % (i + 1)
a[i], a[j] = a[j], a[i]
return a
def invert_table(t: list) -> list:
"""Compute inverse mapping of an S-box: inv[t[i]] = i."""
inv = [0] * 256
for i, v in enumerate(t):
inv[v] = i
return inv
def unmask_goal(masked: bytes) -> bytes:
"""
Remove the per-index compare mask seen in the final loop:
al = ((rbp[i] ^ ((i*0x0d) - 0x5a)) ^ 0x3c) & 0xff
cmp al, [rsi + i]
Rearranged => goal[i] = expected[i] ^ (((0xA6 + 13*i) & 0xFF) ^ 0x3C).
So to get expected[i], XOR with that mask again.
"""
out = bytearray(32)
for i, b in enumerate(masked[:32]):
mask = ((0xA6 + 13 * i) & 0xFF) ^ 0x3C
out[i] = b ^ mask
return bytes(out)
def main():
# 1) read & unmask the 32-byte “expected” array that the program compares against
masked_goal = GOAL_PATH.read_bytes()[:32]
expected = unmask_goal(masked_goal)
# 2) rebuild the S-box and its inverse
sbox = make_sbox(SEED)
inv = invert_table(sbox)
# 3) rebuild the 32-byte init table (SBOX[i] ^ 0x5A), as the binary does
init = bytes((sbox[i] ^ 0x5A) for i in range(32))
# 4) invert the VM’s per-byte update:
# vm: mem[i] ^= ( SBOX(flag[i]) ^ 0xA5 )
# and mem starts as init[i]. After the VM, it equals expected[i].
# So: SBOX(flag[i]) = expected[i] ^ init[i] ^ 0xA5
# => flag[i] = inv[ expected[i] ^ init[i] ^ 0xA5 ]
flag_bytes = bytearray()
for i in range(N_FLAG):
want = expected[i] ^ init[i] ^ 0xA5
flag_bytes.append(inv[want])
flag = bytes(flag_bytes)
# print both ascii (if decodable) and hex for safety
print("recovered (hex):", flag.hex())
try:
print("recovered (ascii):", flag.decode("utf-8"))
except UnicodeDecodeError:
print("recovered (ascii):", flag)
# optional: sanity check on the last 5 bytes (constants in the binary from r9 dump)
tail_expected = [0x55, 0xAA, 0x11, 0x22, 0x33]
ok = all((init[27+i] ^ tail_expected[i]) == expected[27+i] for i in range(5))
if not ok:
print("[!] trailing-constant check failed (flag may still be correct)")
if __name__ == "__main__":
main()
So that's it for this writeup. I hope you enjoyed the challenge and writeup. Happy reversing 🤟