WrongKube+
Desc: I stumbled upon a rather peculiar utility for visualizing Kubernetes clusters online, but I have a feeling there’s something hidden inside it. Flag format: grodno{}
WrongKube+
(3/4 AI SLOP)
Kategori: Reverse Engineering
Flag Format: grodno{...}
Daftar Isi
First Look — “Ini apaan sih?”
Identifikasi File
Trial & Error: Coba Strings Dulu
PyInstaller? Oh, Ini Python!
Ekstraksi dengan pyinstxtractor
Nemu DLL Mencurigakan
Decompile DLL pakai Ghidra
Analisis validate_cluster()
Nemu Encrypted Flag!
Decrypt Manual
Flag!
Kesimpulan & Lessons Learned
1. First Look
Oke jadi kita dikasih satu file: WrongCube+.exe. Nama challengenya WrongKube+, katanya ini semacam “utility untuk visualisasi Kubernetes cluster secara online”. Kedengarannya legit, tapi kan ini CTF — pasti ada sesuatu yang disembunyiin di dalamnya.
Sebelum ngapa-ngapain, gue baca deskripsi soalnya pelan-pelan:
“I stumbled upon a rather peculiar utility for visualizing Kubernetes clusters online, but I have a feeling there’s something hidden inside it.”
Oke noted. Ada yang hidden. Dan format flagnya grodno{...}. Gas langsung.
2. Identifikasi File
Hal pertama yang harus dilakuin waktu ketemu file EXE adalah jangan langsung dijalankan. Kita cek dulu filen1ya ini apaan.
$ file WrongCube+.exe
WrongCube+.exe: PE32+ executable for MS Windows 6.00 (GUI), x86-64, 7 sectionsOke, ini Windows 64-bit PE executable. Format GUI, jadi ini punya tampilan grafis. Masuk akal untuk “visualizer Kubernetes”. Size filenya gede banget: ~37 MB. Itu gede banget untuk aplikasi biasa — ini suspicious, pasti ada yang dibundle di dalamnya.
Kenapa 37 MB mencurigakan? Aplikasi Kubernetes visualizer sederhana nggak butuh 37 MB. Biasanya yang segede ini adalah aplikasi yang bundle runtime (Python, Electron, dll.) plus asset-asset ke dalam satu file.
3. Trial & Error: Coba Strings Dulu
Gue mulai dari yang paling basic dulu: strings. Ini teknik paling klasik — kita dump semua printable string dari binary terus grep yang interesting.
Attempt #1 — Cari flag langsung:
$ strings WrongCube+.exe | grep -i "grodno"
# Nothing...Hmm, flagnya nggak ada secara plaintext. Wajar sih, kalau segampang itu bukan challenge namanya. Okay, coba lain:
Attempt #2 — Cari kata kunci umum:
$ strings WrongCube+.exe | grep -i "flag\|ctf\|secret\|hidden\|kube\|cluster"Output yang keluar:
PyMarshal_ReadObjectFromString
PyRun_SimpleStringFlags
pyi-python-flag <-- bukan flag kita, ini parameter PyInstaller
hCTf
cTfR
KuBEN
assets\KubeFlagLogo.png
assets\WrongKube.ico
assets\WrongKube.jpg
assets\WrongKube.png
assets\demo.wkubeproj
assets\template_broken_control_plane.wkubeproj
assets\template_service_mesh.wkubeproj
assets\template_web_app.wkubeproj
build\wrongkube_validator.dll
Okay ini udah mulai menarik! Ada beberapa hal yang langsung nyolok mata:
pyi-python-flag— katapyiini kepanjangan PyInstaller. Jadi ini Python app yang dibundle!PyMarshal_ReadObjectFromString— ini fungsi Python runtime, makin confirm itu Pythonbuild\wrongkube_validator.dll— ada DLL yang dibundle! Namanyavalidator— ini pasti yang ngecek sesuatu
Asumsi gue di sini: Karena ada string
pyi-python-flagdanPyMarshal, ini almost pasti PyInstaller bundle. PyInstaller adalah tool yang bisa bikin executable dari script Python, dengan cara bundle interpreter Python + semua dependency ke satu file EXE. Ini teknik yang umum banget di malware analysis dan CTF. Gue udah pernah ketemu sebelumnya jadi langsung kenal polanya.
4. PyInstaller? Oh, Ini Python!
Gue confirm dugaan dengan ngecek signature PyInstaller secara manual. PyInstaller biasanya ninggalin marker MEIPASS di dalam binary (nama folder temp yang dibuat saat extract).
import struct
with open('WrongCube+.exe', 'rb') as f:
data = f.read()
print('File size:', len(data)) # 36,986,470 bytes
# Cek PyInstaller markers
for marker in [b'MEIPASS', b'python3', b'Python3', b'.pyc']:
pos = data.find(marker)
if pos != -1:
print(f'Found{marker} at offset{pos}')Output:
File size: 36986470
Found b'MEIPASS' at offset 193791 <-- MEIPASS = PyInstaller temp folder marker!
Found b'python3' at offset 36986160
MEIPASS itu adalah folder temporary yang dibuat PyInstaller waktu extract bundle-nya. Ketemu ini, 100% confirmed ini PyInstaller.
5. Ekstraksi dengan pyinstxtractor
Untuk extract isi PyInstaller bundle, kita pakai tool yang namanya pyinstxtractor. Tool ini didesain khusus buat unpack PyInstaller EXE.
Attempt #1 — Install via pip:
$ pip3 install pyinstxtractor
ERROR: Could not find a version that satisfies the requirement pyinstxtractorNggak ada di pip. Okay fine, langsung download dari GitHub:
$ wget https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py
$ python3 pyinstxtractor.py WrongCube+.exeOutput:
[+] Processing WrongCube+.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.12
[+] Length of package: 36611174 bytes
[+] Found 180 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_pyqt6.pyc
[+] Possible entry point: main.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.12 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: WrongCube+.exe
180 files ketemu! Ada warning soal Python version mismatch — sistem kita Python 3.14, bundle dibuild pakai Python 3.12. Ini bikin file .pyc dalam PYZ archive nggak bisa langsung diekstrak. Tapi nggak masalah karena file-file utamanya tetap ke-extract.
Attempt #2 — Coba decompile main.pyc:
Gue coba decompile main.pyc pakai uncompyle6:
$ pip3 install uncompyle6 --break-system-packages
$ python3 -m uncompyle6 main.pycError:
ImportError: bad magic number in 'struct': b'\xcb\r\r\n'
Hmm, gagal juga. Itu karena Python 3.14 nggak kompatibel dengan bytecode 3.12.
Attempt #3 — Coba dis.dis() manual:
import dis, marshal
with open('main.pyc', 'rb') as f:
f.read(16) # skip header
code = marshal.loads(f.read())
dis.dis(code)Error lagi:
IndexError: tuple index out of range
Opcode antara Python 3.12 dan 3.14 beda, jadi dis nggak bisa parse dengan benar.
Workaround: Dari co_names yang berhasil kita baca, main.py cuma import src.ui dan panggil launch(). Artinya logika utamanya ada di modul lain — dan kita udah tau ada DLL wrongkube_validator.dll. Kita skip decompile Python dan fokus ke DLL.
Mari kita lihat isi foldernya:
WrongCube+.exe_extracted/
├── assets/
│ ├── demo.wkubeproj <-- project demo cluster!
│ ├── template_broken_control_plane.wkubeproj
│ ├── template_service_mesh.wkubeproj
│ ├── template_web_app.wkubeproj
│ ├── KubeFlagLogo.png
│ └── easter/
├── build/
│ └── wrongkube_validator.dll <-- TARGET UTAMA KITA
├── main.pyc
├── PyQt6/ <-- GUI framework
└── [dll dan pyd lainnya]6. Nemu DLL Mencurigakan
Langsung gue fokus ke wrongkube_validator.dll. Nama “validator” itu sangat suggestif — ini pasti yang ngecek apakah cluster config kita valid, dan kemungkinan besar ini yang generate flag.
Gue cek dulu dengan file dan strings:
$ file wrongkube_validator.dll
wrongkube_validator.dll: PE32+ executable for MS Windows 6.00 (DLL), x86-64, 7 sections
$ strings wrongkube_validator.dll | grep -E "[a-zA-Z]{4,}"Dan ketemu ini yang super menarik:
{"ok":%s,"score":%d,"signature":%d,"summary":"%s","flag":"%s","manifest":%d,"edges":%d,"meta":%d,"shadow":%d,"vm":%d,"vm2":%d,"vm3":%d}
validate_cluster
free_result
wrongkube_validator.dll
role
JACKPOT! Formatnya jelas banget — DLL ini punya fungsi validate_cluster() yang return JSON dengan field “flag” di dalamnya! Sekarang tinggal figuring out gimana cara kita dapet flag-nya.
Liat juga string-string status message yang ada di binary:
Shadow control plane reconciled. Triple witness accepted.
Visible topology converges, but shadow quorum derivation still drifts.
Control plane is almost stable. Northbound and audit lanes still disagree.
Manifest is close, but service lineage or attachment ownership remains incomplete.
Shadow control plane unstable. Canonical derivation is incomplete.
Ini kayaknya pesan status dari validator dengan berbagai level kepuasan. Level paling tinggi: “Shadow control plane reconciled. Triple witness accepted.” Ini pasti state yang unlocks flag.
Gue juga cek fungsi-fungsi yang ada di DLL pakai radare2:
$ r2 -A -q -c "afl" wrongkube_validator.dllOutput (fungsi-fungsi penting):
0x180001000 validate_cluster <-- fungsi utama (17,798 bytes!)
0x1800056f0 free_result
0x180005900 [helper]
0x180005b00 [helper]
0x180010990 [massive function, 27,451 bytes]
Fungsi validate_cluster itu ukurannya 17.798 bytes — itu gede banget. Dan ada satu fungsi lain yang bahkan lebih gede: 27.451 bytes. Ini kompleks, kita butuh decompiler yang lebih canggih.
7. Decompile DLL pakai Ghidra
Gue pake Ghidra (free RE tool dari NSA, open source) untuk decompile DLL-nya. Karena bisa dipakai headless (tanpa GUI, via command line), gue buat Ghidra script Java untuk auto-decompile fungsi yang kita mau.
Attempt #1 — Import ke Ghidra:
$ /opt/ghidra/support/analyzeHeadless \
/home/claritys/ctf/juniorcrypt/rev/wrongkube+ WrongKubeProject \
-import wrongkube_validator.dllError:
ERROR: Path element contains invalid character: '+'
Karena nama foldernya ada +-nya, Ghidra nggak mau. Fix: pindah ke temp folder:
$ /opt/ghidra/support/analyzeHeadless /tmp/ghidra_wk WrongKubeProject \
-import wrongkube_validator.dllBerhasil! Analisis memakan waktu ~20 detik.
Attempt #2 — Buat script decompile:
Script Java buat auto-decompile fungsi-fungsi penting:
// ~/ghidra_scripts/decompile_script.java
import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.*;
import ghidra.program.model.listing.*;
import java.io.*;
public class decompile_script extends GhidraScript {
@Override
public void run() throws Exception {
DecompInterface decompiler = new DecompInterface();
decompiler.openProgram(currentProgram);
PrintWriter pw = new PrintWriter(new FileWriter("/tmp/decompiled_output.c"));
for (Function f : currentProgram.getFunctionManager().getFunctions(true)) {
String name = f.getName();
if (name.contains("validate_cluster") ||
name.contains("fcn.180010990") ||
name.contains("fcn.18001ff80")) {
pw.println("// === Function: " + name + " @ " + f.getEntryPoint() + " ===");
DecompileResults result = decompiler.decompileFunction(f, 60, null);
if (result.decompileCompleted()) {
pw.println(result.getDecompiledFunction().getC());
}
pw.println();
}
}
pw.close();
println("Done!");
}
}Attempt #2a — Script di /tmp/ nggak jalan:
$ /opt/ghidra/support/analyzeHeadless /tmp/ghidra_wk WrongKubeProject \
-process wrongkube_validator.dll \
-postScript /tmp/decompile_script.java \
-noanalysisError:
ERROR Failed to find script in any script directory: /tmp/decompile_script.java
Ghidra nggak mau load script dari arbitrary path. Fix: taruh di home directory dan gunakan flag -scriptPath:
$ mkdir ~/ghidra_scripts
$ cp /tmp/decompile_script.java ~/ghidra_scripts/
$ /opt/ghidra/support/analyzeHeadless /tmp/ghidra_wk WrongKubeProject \
-process wrongkube_validator.dll \
-postScript decompile_script.java \
-scriptPath ~/ghidra_scripts \
-noanalysisOutput:
INFO SCRIPT: decompile_script.java
INFO decompile_script.java> Done! Output in /tmp/decompiled_output.c
Hasilnya: file decompiled_output.c dengan 2.795 baris kode C hasil decompilasi.
8. Analisis validate_cluster()
Fungsi utamanya adalah validate_cluster(char *param_1). Input-nya adalah string JSON yang merepresentasikan cluster Kubernetes.
8.1 Parsing JSON Cluster
Fungsi ini nge-parse JSON yang masuk dengan cara yang cukup primitif — pakai strstr() dan strchr() buat cari field-field tertentu, bukan pakai library JSON. Dari file demo.wkubeproj yang ikut ke-extract, kita bisa lihat format JSON yang diharapkan:
{
"nodes": [
{
"id": "ns-core",
"resource_type": "Namespace",
"name": "core-plane",
"props": {
"namespace": "core-plane",
"labels": "tier=infra",
"replicas": 1,
"port": 0
}
}
],
"edges": [
{
"id": "e0",
"source_id": "ns-core",
"target_id": "dep-rec",
"binding": "contains",
"role": "deployment"
}
]
}Dari kode hasil decompile, DLL menyimpan tiap node dalam struct dengan field:
id(offset 0x0, max 64 bytes)type(offset 0x40, max 32 bytes)name(offset 0x60, max 64 bytes)namespace(offset 0xa0, max 64 bytes)labels(offset 0xe0, max 128 bytes)selector(offset 0x160, max 128 bytes)mount(offset 0x1e0, max 64 bytes)replicas(offset 0x220, int)port(offset 0x224, int)
Total per node: 0x228 = 552 bytes. Maksimum 128 nodes (0x80).
8.2 FNV-1a Hashing
Setelah parsing, DLL ngitung FNV-1a hash untuk tiap node. Ini algorithm hash yang sederhana dan cukup umum dipakai. Gue langsung kenal ini karena polanya sangat distinctive:
// Pattern FNV-1a yang ketemu di decompile:
uVar30 = 0x811c9dc5; // FNV offset basis (32-bit)
do {
uVar7 = tolower((uint)bVar52);
iVar6 = isalnum((uint)bVar52);
uVar25 = (uVar7 & 0xff ^ uVar30) * 0x1000193; // FNV prime!
if ((uVar7 & 0xff) == 0x5f) uVar30 = uVar25; // underscore '_'
if ((uVar7 & 0xef) == 0x2d) uVar30 = uVar25; // dash '-'
if (iVar6 != 0) uVar30 = uVar25; // alphanumeric
bVar52 = *pbVar31++;
} while (bVar52 != 0);
uVar30 &= 0x7fffffff;Kenapa gue tau ini FNV-1a? Karena konstanta
0x811c9dc5itu adalah FNV offset basis untuk FNV-1a 32-bit, dan0x1000193adalah FNV prime. Dua angka ini signature banget — kalau liat dua konstanta ini dalam loop yang XOR sama multiply, langsung gue curiga FNV. Plus ada khas lain: hashing dilakukan per byte (bukan per word), dan ada case khusus untuk underscore dan dash (karena Kubernetes resource names sering pake kedua karakter itu).
Hash ini dihitung untuk beberapa field tiap node:
Hash dari
id→ dipakai untuk signature nodeHash dari
type/resource_type→ dipakai untuk kategorisasiHash dari
name,namespace→ dipakai untuk perhitungan skor
8.3 Type Counting via Hash Comparison
Bagian yang bikin gue “OH!” adalah bagian ini dari kode:
// Ngitung jumlah node per tipe (berdasarkan hash dari resource_type string)
if (iVar6 < 0x45334cec) {
if (iVar6 < 0x251b4cfe) {
if (iVar6 == 0x133c1bd8) count_Namespace++;
else if (iVar6 == 0x2047d5a7) count_Secret++;
}
else if (iVar6 == 0x251b4cfe) count_Service++;
else if (iVar6 == 0x31c14e0d) count_Pod++;
}
else {
if (iVar6 < 0x56dfde64) {
if (iVar6 == 0x45334cec) count_Deployment++;
else if (iVar6 == 0x4ace7aa0) count_ConfigMap++;
}
else if (iVar6 == 0x56dfde64) count_Ingress++;
else if (iVar6 == 0x667c6911) count_Node++;
}Setiap angka hex di sana adalah FNV-1a hash dari nama resource type Kubernetes. DLL nggak compare string langsung, tapi compare hashnya. Kita bisa verify:
def fnv1a(s):
h = 0x811c9dc5
for c in s.lower():
if c.isalnum() or c in '-_':
h = ((ord(c) ^ h) * 0x1000193) & 0xffffffff
return h & 0x7fffffff
print(hex(fnv1a("Namespace"))) # = 0x133c1bd8 ✓
print(hex(fnv1a("Deployment"))) # = 0x45334cec ✓
print(hex(fnv1a("Service"))) # = 0x251b4cfe ✓
print(hex(fnv1a("Pod"))) # = 0x31c14e0d ✓
print(hex(fnv1a("Ingress"))) # = 0x56dfde64 ✓
print(hex(fnv1a("Node"))) # = 0x667c6911 ✓
print(hex(fnv1a("Secret"))) # = 0x2047d5a7 ✓
print(hex(fnv1a("ConfigMap"))) # = 0x4ace7aa0 ✓Semua cocok.
8.4 Kondisi Unlock Flag — The Big Gate!
Nah ini dia bagian paling penting. Di bagian akhir fungsi (sekitar baris 2576 dari hasil decompile), ada kondisi besar yang harus terpenuhi sebelum flag di-decrypt:
if (((iVar11 == 0x15) && (iVar12 == 0x28)) &&
((iVar6 == 0x12 &&
((((iVar9 == 0xc && (iVar48 == 8)) && (iVar10 == 7)) &&
((iVar50 == 6 &&
(((val_A << 7 | val_A >> 0x19) ^
val_B ^
(val_C << 0xd | val_C >> 0x13))
== 0x9d3daf88)))))))) {
if (uVar30 == 0x2f8f6) {
// DECRYPT FLAG DAN RETURN "true"!Dalam desimal, kondisinya adalah:
VariableNilaiArtinyaiVar1121 (0x15)Jumlah NamespaceiVar1240 (0x28)Jumlah DeploymentiVar618 (0x12)Jumlah ServiceiVar912 (0x0c)Jumlah PodiVar488Jumlah NodeiVar107Jumlah IngressiVar506Jumlah Secretchecksum0x9d3daf88Hash gabungan dari beberapa fielduVar30194,806 (0x2f8f6)Signature akhir dari semua edge relationships
Kalau semua kondisi ini terpenuhi, baru flag di-decrypt dan dikembalikan dalam JSON response.
9. Nemu Encrypted Flag!
Di dalam blok if (uVar30 == 0x2f8f6), ada loop yang men-decrypt flag. Ini bagian krusialnya:
// Nilai-nilai initial state PRNG (hardcoded!)
iVar11 = -0x8cbb611; // = 0xF73449EF unsigned
bVar52 = 0x49; // starting byte key = 'I'
iVar50 = 0x47502943;
lVar19 = 1; // loop index, mulai dari 1
iVar12 = 0x47502932;
iVar48 = 0x3c6ef35f;
uVar30 = 0;
do {
// Hitung byte ganjil (index 1, 3, 5, ...)
iVar13 = iVar11 * 0x19660d + iVar48;
flag[lVar19] = UNK_18002beff[lVar19] // encrypted data
^ ((bVar52 + 0xb7) & 0xff) // XOR key bagian 1
^ ((uint32_t)iVar13 >> 16) // XOR key bagian 2
^ ((uint32_t)iVar13 & 0xff); // XOR key bagian 3
// Hitung byte genap (index 2, 4, 6, ...)
iVar13_2 = iVar11 * 0x17385ca9;
// Update iVar11 DULU sebelum iVar13_2 dipakai untuk byte genap
iVar11_next = uVar30+1 + (uVar30|1)*0x10 + iVar12 + iVar13_2;
iVar13_2 = iVar13_2 + iVar50; // baru tambah iVar50
flag[lVar19+1] = DAT_18002bf00[lVar19] // encrypted data (berbeda!)
^ bVar52 // XOR key bagian 1
^ ((uint32_t)iVar13_2 >> 16) // XOR key bagian 2
^ ((uint32_t)iVar13_2 & 0xff); // XOR key bagian 3
// Update semua state untuk iterasi berikutnya
uVar30 += 2;
bVar52 = (bVar52 + 0x92) & 0xff;
iVar50 += 0x35f8ddc;
lVar19 += 2;
iVar12 += 0x35f8dba;
iVar48 += 0x22;
iVar11 = iVar11_next;
} while (lVar19 != 0x2f); // Stop saat index = 47, artinya 46 byte = flagKey insight: Flag ini di-enkripsi dengan custom PRNG (kombinasi LCG — Linear Congruential Generator) + XOR dengan data hardcoded di section .rdata binary. Data encrypted-nya ada di dua lokasi:
0x18002beff→ RVA0x2beff→ file offset0x2a4ff0x18002bf00→ RVA0x2bf00→ file offset0x2a500
Kenapa langsung coba decrypt tanpa fulfill kondisinya? Karena kondisinya super kompleks (butuh bikin cluster dengan jumlah node yang sangat spesifik plus checksum yang pas), dan kita tau semua nilai initial state PRNG-nya hardcoded di binary. Jadi kita bisa langsung simulasi PRNG-nya di Python dan decrypt flag tanpa perlu jalankan programnya! Ini adalah trik klasik dalam RE — kalau kunci enkripsinya hardcoded, kita bisa extract dan simulasi sendiri.
10. Decrypt Manual
Gue extract byte-byte encrypted dari binary dan simulasikan PRNG-nya di Python.
Attempt #1 — Salah hitung file offset:
Waktu pertama gue coba, gue pakai offset 0x2beff langsung tanpa memperhitungkan bahwa ini adalah RVA (Relative Virtual Address) yang perlu di-map ke file offset melalui section table PE. Hasilnya XOR data yang salah → flag garbled.
# SALAH - ini asumsi langsung RVA = file offset
data[0x2beff:0x2beff+50] # Nggak valid!Fix: Parse PE header untuk mapping yang benar:
# Benar - parse PE sections
# .rdata section: VA=0x2b000, file offset=0x29600
# RVA 0x2beff → .rdata offset = 0x2beff - 0x2b000 = 0xeff
# File offset = 0x29600 + 0xeff = 0x2a4ff
file1 = 0x2a4ff # UNK_18002beff
file2 = 0x2a500 # DAT_18002bf00Attempt #2 — Salah urutan update PRNG:
Di iterasi pertama gue nulis kode decryptnya, gue salah urutan: gue update iVar11 terlalu cepat (sebelum ngitung byte genap). Hasilnya byte-byte genap salah semua.
# SALAH - iVar11 di-update sebelum byte genap dihitung
iVar13_2 = s32(iVar11 * 0x17385ca9)
iVar11 = s32(uVar30 + 1 + ...) # update terlalu cepat!
iVar13_2 = s32(iVar13_2 + iVar50)
b2 = data[file2 + lVar19] ^ ... # pakai iVar11 baru, SALAHFix: Simpan iVar11_next dulu, baru update setelah byte genap selesai dihitung:
# BENAR - simpan next value dulu
iVar13_2 = s32(iVar11 * 0x17385ca9)
iVar11_next = s32(uVar30 + 1 + (uVar30 | 1) * 0x10 + iVar12 + iVar13_2)
iVar13_2 = s32(iVar13_2 + iVar50) # pakai iVar11 lama
b2 = ... # byte genap dihitung dengan state yang benar
# Baru setelah itu update:
iVar11 = iVar11_nextAttempt #3 — Python integer overflow:
Python nggak punya signed 32-bit integer natively. Kalau gue nulis iVar11 * 0x19660d, Python akan kasih integer gede yang nggak wrap-around. Gue harus manual implement signed 32-bit arithmetic.
# SALAH - Python integer biasa, nggak overflow
iVar13 = iVar11 * 0x19660d + iVar48 # bisa jadi angka ratusan digit!
# BENAR - signed 32-bit
def s32(x):
x = x & 0xffffffff
if x >= 0x80000000:
return x - 0x100000000
return x
iVar13 = s32(iVar11 * 0x19660d + iVar48) # wrap di 32-bit ✓Final working decrypt script:
with open('wrongkube_validator.dll', 'rb') as f:
data = f.read()
# File offsets (setelah PE section parsing)
file1 = 0x2a4ff # UNK_18002beff
file2 = 0x2a500 # DAT_18002bf00
def s32(x):
x = x & 0xffffffff
if x >= 0x80000000:
return x - 0x100000000
return x
def u32(x):
return x & 0xffffffff
# State awal dari kode (hardcoded!)
iVar11 = s32(-0x8cbb611) # = s32(0xF73449EF)
iVar50 = s32(0x47502943)
bVar52 = 0x49
iVar12 = s32(0x47502932)
iVar48 = s32(0x3c6ef35f)
uVar30 = 0
flag_chars = []
lVar19 = 1
while lVar19 != 0x2f:
# Byte ganjil
iVar13 = s32(iVar11 * 0x19660d + iVar48)
b1 = (data[file1 + lVar19]
^ ((bVar52 + 0xb7) & 0xff)
^ ((u32(iVar13) >> 0x10) & 0xff)
^ (u32(iVar13) & 0xff)) & 0xff
flag_chars.append(b1)
# Byte genap (iVar11 belum di-update!)
iVar13_2 = s32(iVar11 * 0x17385ca9)
iVar11_next = s32(uVar30 + 1 + (uVar30 | 1) * 0x10 + iVar12 + iVar13_2)
iVar13_2 = s32(iVar13_2 + iVar50)
b2 = ((u32(iVar13_2) >> 0x10) & 0xff
^ bVar52
^ (data[file2 + lVar19] & 0xff)
^ (u32(iVar13_2) & 0xff)) & 0xff
flag_chars.append(b2)
# Update state
uVar30 += 2
bVar52 = (bVar52 + 0x92) & 0xff
iVar50 = s32(iVar50 + 0x35f8ddc)
lVar19 += 2
iVar12 = s32(iVar12 + 0x35f8dba)
iVar48 = s32(iVar48 + 0x22)
iVar11 = iVar11_next # baru update di sini
result = bytes(flag_chars)
print('Flag:', result.decode('ascii'))Output:
Flag: grodno{5h4d0w_c0ntr0l_pl4n3_qu0rum_r3c0nc1l3d}
YESSSSS! Flagnya keluar bersih! 🎉
11. Kesimpulan & Lessons Learned
Ringkasan Alur Solve
WrongCube+.exe (37MB)
│
├─[file + strings]─► Deteksi PyInstaller + DLL mencurigakan
│
├─[pyinstxtractor]─► Extract 180 files
│ ├── main.pyc (gagal decompile)
│ └── wrongkube_validator.dll ◄── TARGET
│
├─[strings + r2]─► Format JSON output + fungsi validate_cluster
│
├─[Ghidra headless]─► 2795 baris kode C decompilasi
│
├─[manual analysis]─► FNV-1a hash, type counting, flag conditions
│ └── Encrypted flag + hardcoded PRNG state
│
└─[Python script]─► DECRYPT → grodno{5h4d0w_c0ntr0l_pl4n3_qu0rum_r3c0nc1l3d}Key Takeaways
1. File besar = ada yang dibundle. Kalau EXE ukurannya gila-gilaan, curiga ada packer. PyInstaller, cx_Freeze, Electron, dll. Langsung cek strings untuk signature mereka. MEIPASS = PyInstaller, NWJS = NW.js, Squirrel = Electron, dll.
2. strings + grep itu masih selalu berguna. Bahkan tanpa decode/decompile apapun, kita udah bisa tau ada wrongkube_validator.dll dan format output JSON-nya hanya dari strings dump.
3. FNV-1a itu mudah dikenali. Konstanta 0x811c9dc5 (offset basis) dan 0x1000193 (prime) adalah signature yang sangat khas. Kalau liat dua angka ini dalam loop yang XOR sama multiply, langsung curiga FNV.
4. Nggak perlu solve semua kondisi untuk dapat flag. Kalau kondisi unlock adalah “buat cluster yang valid dengan jumlah node yang spesifik”, tapi state dekripsi PRNG-nya hardcoded di binary, kita bisa langsung decrypt tanpa perlu fulfill kondisinya. Ini adalah trick “just decrypt it” yang sering works di CTF.
5. Ghidra headless itu power tool. Banyak yang belum tau Ghidra bisa dipakai tanpa GUI via analyzeHeadless. Untuk CTF, sangat berguna karena bisa auto-decompile via Java script, bahkan batch process banyak binary sekaligus.
6. Signed integer overflow itu krusial saat simulasi PRNG. Waktu simulasi PRNG native C di Python, harus pake signed 32-bit arithmetic karena C overflow di 32-bit tapi Python nggak. Helper function s32() yang simpel bisa menyelamatkan berjam-jam debugging.
7. Baca urutan operasi dengan teliti. Satu kesalahan kecil dalam urutan update state PRNG bisa bikin semua byte flag salah. Harus baca decompilasi baris per baris, jangan skip-skip.
Appendix A: Encrypted Data Raw
Bytes hardcoded di binary yang di-XOR untuk menghasilkan flag:
Offset 0x2a4ff (UNK_18002beff):
00 07 9f 08 64 d3 e9 f9 60 d8 6c 37 a9 d4 18 5d
53 d6 42 fe 3a 6c ac 57 f8 1d 4a 04 55 ca 6f ea
13 a8 c2 00 88 02 d7 20 31 7b 5b 74 91 31 3e 00
Offset 0x2a500 (DAT_18002bf00):
07 9f 08 64 d3 e9 f9 60 d8 6c 37 a9 d4 18 5d 53
d6 42 fe 3a 6c ac 57 f8 1d 4a 04 55 ca 6f ea 13
a8 c2 00 88 02 d7 20 31 7b 5b 74 91 31 3e 00 00
Appendix B: Initial PRNG State
iVar11 = 0xF73449EF (= -0x8cbb611 signed)
iVar50 = 0x47502943
bVar52 = 0x49 (= 'I')
iVar12 = 0x47502932
iVar48 = 0x3c6ef35f
PRNG multipliers:
0x19660d (LCG multiplier untuk byte ganjil)
0x17385ca9 (LCG multiplier untuk byte genap)
Appendix C: Hash Lookup Table
FNV-1a hash dari setiap Kubernetes resource type yang dipakai validator:
0x133c1bd8 = FNV("Namespace")
0x2047d5a7 = FNV("Secret")
0x251b4cfe = FNV("Service")
0x31c14e0d = FNV("Pod")
0x45334cec = FNV("Deployment")
0x4ace7aa0 = FNV("ConfigMap")
0x56dfde64 = FNV("Ingress")
0x667c6911 = FNV("Node")
gomen ai slopnya kental :sob:


