GEMASTIK XIX: MatematikaMenyenangkanBukan
Static Recon Guide: Matematika Menyenangkan Bukan
Scope: Panduan ini menjelaskan proses rekonstruksi manual sebuah challenge reverse engineering Windows PE x86-64 berbasis Tauri/Rust, mulai dari triage file sampai seluruh konstanta dan algoritma yang dibutuhkan solver terkumpul.
Metode: Static analysis. Binary tidak dieksekusi. Command difokuskan pada
file,strings, GNUobjdump,llvm-objdump,od, dan Python untuk ekstraksi/deobfuscation/verifikasi.Etika: Gunakan hanya pada challenge CTF, software milik sendiri, atau binary yang memang kamu memiliki izin untuk analisis.
Daftar Isi
- Tujuan recon
- Persiapan environment
- Verifikasi dan fingerprint binary
- Membangun peta PE dan rumus VA ke raw offset
- Triage strings dan identifikasi Tauri/Rust
- Mencari dan mengekstrak embedded frontend
- Recon HTML dan kontrak UI
- Deobfuscation JavaScript
- Mengumpulkan IPC contract
- Mengubah raw string offset menjadi VA
- Mencari xref command dengan llvm-objdump
- Mengenali dispatcher Rust
- Recon generator urutan lima tombol
- Recon state transition
- Mengidentifikasi SHA-256
- Mengambil seed
- Mengambil batas level
- Mengambil tag SQNC
- Mengambil ciphertext
- Recon derivasi keystream
- Ledger data akhir
- Solver reproduktif
- Validasi dan troubleshooting
- Referensi yang mirip
1. Tujuan recon
Tujuan tahap recon bukan langsung menebak flag. Kita ingin menjawab pertanyaan berikut secara terukur:
- Binary format apa yang digunakan?
- Framework dan bahasa apa yang dipakai?
- Apakah ada frontend yang di-embed?
- Bagaimana frontend berkomunikasi dengan backend?
- Fungsi native mana yang menangani input challenge?
- Bagaimana urutan jawaban tiap level dibuat?
- Bagaimana state berubah?
- Kapan kondisi final tercapai?
- Di mana seed, tag, ciphertext, dan panjang ciphertext disimpan?
- Bagaimana keystream dibuat?
- Bagaimana plaintext dipisahkan dari header internal?
Buat satu file catatan sejak awal:
: > recon-notes.txt
Setiap menemukan address atau konstanta, catat sumbernya. Jangan hanya mencatat hasil akhir. Contoh format:
[Finding]
Name : command string "tap"
Raw : 0x1f8138
VA : 0x1401f8f38
Evidence : strings + section conversion + llvm-objdump xref
Function : dispatcher around 0x1400d32c1
Confidence: high
2. Persiapan environment
2.1 Tools minimum
Ubuntu/Debian:
sudo apt update
sudo apt install -y \
binutils \
llvm \
file \
python3 \
python3-pip
Python package:
python3 -m pip install brotli
Verifikasi:
file --version
objdump --version
llvm-objdump --version
strings --version
python3 - <<'PY'
import brotli
print("brotli available")
PY
2.2 Salin sampel
cp "MatematikaMenyenangkanBukan (2).exe.txt" challenge.exe
f="challenge.exe"
Analisis selalu pada copy, bukan file asli.
3. Verifikasi dan fingerprint binary
3.1 Hash sampel
sha256sum "$f"
Expected:
fc1eaed7ddf91e5d2bc86486006a9a52709fe0ce57faf8895796858d228fdcb4
3.2 Identifikasi file
file "$f"
Expected:
PE32+ executable (GUI) x86-64, for MS Windows, 6 sections
Makna:
PE32+: executable Portable Executable 64-bit.x86-64: arsitektur AMD64.GUI: Windows subsystem GUI, bukan console.- Ekstensi
.txttidak relevan. Magic dan struktur file adalah sumber kebenaran.
3.3 Header utama
objdump -x "$f" | less
Catat:
ImageBase 0x140000000
AddressOfEntryPoint 0x001d1040
Subsystem Windows GUI
Hitung entry-point VA:
0x140000000 + 0x001d1040 = 0x1401d1040
Entry point belum tentu logic challenge. Pada aplikasi Rust/Tauri, entry point banyak berisi runtime/bootstrap.
4. Membangun peta PE dan rumus VA ke raw offset
4.1 Dump sections
objdump -h "$f"
Data penting dari sampel:
Idx Name VMA Raw offset
0 .text 0x0000000140001000 0x00000400
1 .rdata 0x00000001401e0000 0x001df200
2 .data 0x000000014027a000 0x00278c00
3 .pdata 0x000000014027e000 0x00279600
4 .rsrc 0x000000014028d000 0x00287a00
5 .reloc 0x0000000140291000 0x0028ac00
4.2 Rumus konversi
Untuk address di sebuah section:
raw = target_va - section_va + section_raw
Untuk .rdata challenge ini:
raw = target_va - 0x1401e0000 + 0x1df200
Kebalikannya:
va = raw - 0x1df200 + 0x1401e0000
Buat helper shell:
va_to_raw() {
python3 - "$1" <<'PY'
import sys
va = int(sys.argv[1], 0)
print(hex(va - 0x1401e0000 + 0x1df200))
PY
}
raw_to_va() {
python3 - "$1" <<'PY'
import sys
raw = int(sys.argv[1], 0)
print(hex(raw - 0x1df200 + 0x1401e0000))
PY
}
Contoh:
va_to_raw 0x1401f9258
raw_to_va 0x1f8458
Keduanya harus menunjuk pasangan address yang sama.
5. Triage strings dan identifikasi Tauri/Rust
5.1 Dump ASCII dan UTF-16LE
strings -a -t x -n 5 "$f" > strings-ascii.txt
strings -el -t x -n 5 "$f" > strings-utf16.txt
-t x penting karena menyertakan raw file offset.
5.2 Cari keyword
grep -Eai \
"flag|ctf|gemastik|matematika|board|restart|tap|certificate|tauri|lib\.rs|cargo" \
strings-ascii.txt
Temuan penting:
MatematikaMenyenangkanBukan
id.gemastik.mmb
tauri-2.11.5
gemastikmmbcore
boardgamerestarttap
Build path:
C:\Users\DELL\projects\gemastik2026-quals\reverse\
matematika-menyenangkan-bukan\src\mmb-core\src\lib.rs
Interpretasi:
tauri-2.11.5mengarah ke Tauri v2.- Path
.cargo\registrydan.rsmengonfirmasi ekosistem Rust. board,restart, dantapkemungkinan IPC command.mmb-core/src/lib.rsmemberi nama crate internal challenge.
5.3 Kenapa mulai dari strings?
Rust release binary biasanya besar dan statically linked. Masuk dari entry point membuat kita tenggelam dalam runtime/library code. String command dan path source memberi anchor yang jauh lebih kuat.
6. Mencari dan mengekstrak embedded frontend
6.1 Temukan asset names
grep -Eai "index\.html|app\.js|style\.css" strings-ascii.txt
Cari semua raw occurrence:
grep -oba "/app.js" "$f"
grep -oba "/index.html" "$f"
grep -oba "/style.css" "$f"
Marker yang dipakai pada sampel:
/app.js raw 0x1f848b
/index.html raw 0x1fc655
/style.css raw 0x1fc761
6.2 Inspeksi byte setelah marker
od -Ax -tx1z -j $((0x1f848b)) -N 64 "$f"
Setelah /app.js, data dimulai dengan blob non-plaintext. Pada sampel ini blob dapat didekompresi sebagai Brotli.
6.3 Extract stream byte-by-byte
Pendekatan byte-by-byte berguna saat compressed length belum diketahui. Decoder berhenti sendiri ketika stream lengkap.
#!/usr/bin/env python3
from pathlib import Path
import brotli
BINARY = Path("challenge.exe").read_bytes()
def extract(marker: bytes, marker_offset: int, output: str) -> None:
start = marker_offset + len(marker)
decoder = brotli.Decompressor()
result = bytearray()
for cursor in range(start, len(BINARY)):
result.extend(decoder.process(BINARY[cursor:cursor + 1]))
if decoder.is_finished():
end = cursor + 1
break
else:
raise RuntimeError(f"stream {marker!r} tidak selesai")
Path(output).write_bytes(result)
print(
f"{output}: compressed={start:#x}..{end:#x}, "
f"decompressed={len(result)} bytes"
)
extract(b"/app.js", 0x1f848b, "app.js")
extract(b"/index.html", 0x1fc655, "index.html")
extract(b"/style.css", 0x1fc761, "style.css")
Expected:
app.js 60282 bytes
index.html 550 bytes
style.css 1323 bytes
6.4 Verifikasi output
file app.js index.html style.css
head -30 index.html
head -5 app.js
head -20 style.css
Kalau hasil dekompresi bukan data masuk akal:
- Pastikan yang dipakai occurrence marker yang benar.
- Start harus tepat sesudah marker.
- Jangan menambah/mengurangi satu byte sebelum Brotli stream.
- Cari asset table Tauri sebagai alternatif, bukan hanya marker scan.
7. Recon HTML dan kontrak UI
Buka:
sed -n '1,160p' index.html
Struktur relevant:
<canvas id="screen"></canvas>
<div id="cert" hidden="">
<div class="box">
<div class="ttl">SERTIFIKAT</div>
<code id="flag"></code>
<button id="close">TUTUP</button>
</div>
</div>
<script type="module" src="app.js"></script>
Kesimpulan yang boleh dibuat:
- Game dirender pada
<canvas>. - Flag ditulis ke DOM element
#flag. - Certificate awalnya hidden.
- Logic penentu flag berada di JavaScript atau backend native.
Catat:
DOM certificate container = cert
DOM flag container = flag
frontend script = app.js
8. Deobfuscation JavaScript
8.1 Identifikasi pola
grep -nE \
"function a0_0x187f|function a0_0x5884|0x5d00a" \
app.js
Pola dasarnya:
const a0_0x14165a = a0_0x5884;
(function (getArray, target) {
const decode = a0_0x5884;
const array = getArray();
while (true) {
try {
const checksum = /* parseInt expressions */;
if (checksum === target) break;
array.push(array.shift());
} catch (_) {
array.push(array.shift());
}
}
}(a0_0x187f, 0x5d00a));
Data recon:
string array function = a0_0x187f
accessor = a0_0x5884
base index = 0x102
target checksum = 0x5d00a
8.2 Custom Base64 alphabet
Decoder menggunakan:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=
Translasikan ke alphabet standar:
import base64
CUSTOM = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/="
NORMAL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
TRANS = str.maketrans(CUSTOM, NORMAL)
def decode_custom_base64(value: str) -> str:
value = value.translate(TRANS)
value += "=" * ((4 - len(value) % 4) % 4)
return base64.b64decode(value).decode("utf-8")
8.3 Parse array literal
from pathlib import Path
import ast
source = Path("app.js").read_text()
start = source.index(
"const _0x230903=[",
source.index("function a0_0x187f")
) + len("const _0x230903=")
end = source.index(
"];a0_0x187f=function",
start
) + 1
array = ast.literal_eval(source[start:end])
print("entries:", len(array))
8.4 Simulasikan rotation IIFE
Jangan hardcode 252 pada recon generik. Reimplement checksum dari bootstrap dan putar array sampai hasil sama dengan 0x5d00a.
Pada challenge ini:
rotation count = 252
Sesudah posisi array benar:
def decode_index(index: int) -> str:
return decode_custom_base64(array[index - 0x102])
Temuan mapping penting:
0x1c3 -> cert
0x239 -> certif
0x26f -> flag
0x2b4 -> board
0x1d5 -> tap
8.5 Cari semua alias accessor
Obfuscator membuat banyak alias lokal:
const _0x2e2694 = a0_0x14165a;
const _0xab0cd6 = a0_0x14165a;
Kumpulkan otomatis:
import re
aliases = set(re.findall(
r"([A-Za-z_$][\w$]*)\s*=\s*a0_0x14165a\b",
source,
))
aliases.add("a0_0x14165a")
Replace call:
pattern = re.compile(
r"\b(" +
"|".join(map(re.escape, sorted(aliases, key=len, reverse=True))) +
r")\((0x[0-9a-fA-F]+)\)"
)
mapping = {
index: decode_index(index)
for index in range(0x102, 0x102 + len(array))
}
deobfuscated = pattern.sub(
lambda match: repr(mapping[int(match.group(2), 16)]),
source,
)
Path("app-deobfuscated.js").write_text(deobfuscated)
Sekitar 1.003 call berhasil diganti pada sampel.
9. Mengumpulkan IPC contract
Cari string relevant:
grep -nE \
"'board'|'tap'|'restart'|'certificate'|'TAMAT'|'SALAH URUTAN'" \
app-deobfuscated.js
Logic yang direkonstruksi:
const invoke = window.__TAURI__.core.invoke;
view = await invoke("board");
result = await invoke("tap", {
pos: selectedPosition
});
if (result.certificate) {
document.getElementById("flag").textContent = result.certificate;
document.getElementById("cert").hidden = false;
}
Catat kontrak:
command board -> initial/current board state
command tap -> argument { pos }
command restart -> reset state
tap result fields:
- view
- ok
- reset
- certificate
Kita sekarang tahu command native yang perlu dicari melalui xref.
10. Mengubah raw string offset menjadi VA
Dari strings -t x:
raw 0x1f8128: boardgamerestarttap
Ubah raw ke VA memakai .rdata mapping:
VA = 0x1f8128 - 0x1df200 + 0x1401e0000
VA = 0x1401f8f28
Karena string bersebelahan:
0x1401f8f28 board
0x1401f8f2d game
0x1401f8f31 restart
0x1401f8f38 tap
Verifikasi byte:
od -Ax -tc -j $((0x1f8128)) -N 32 "$f"
Catat kedua jenis address:
raw digunakan untuk dump byte dari file
VA digunakan untuk xref dalam disassembly
11. Mencari xref command dengan llvm-objdump
11.1 Generate full disassembly
llvm-objdump \
-d \
--no-show-raw-insn \
"$f" > full-llvm.asm
GNU alternative:
objdump -d -Mintel "$f" > full-gnu.asm
llvm-objdump berguna karena biasanya menuliskan resolved target RIP-relative sebagai komentar.
11.2 Cari xref board
grep -n -i "1401f8f28" full-llvm.asm
Temuan:
1400d32e5:
leaq 0x125c3c(%rip), %r15
# 0x1401f8f28
Ambil konteks:
grep -n -B50 -A220 "1401f8f28" full-llvm.asm \
> command-dispatcher-context.asm
11.3 Potong range dispatcher
llvm-objdump \
-d \
--start-address=0x1400d32c1 \
--stop-address=0x1400d4100 \
"$f" > dispatcher.asm
12. Mengenali dispatcher Rust
Pola compare command:
lea command_string, r8
mov command_length, r9d
mov input_pointer, rcx
mov input_length, rdx
call compare_helper
test al, al
je next_command
Command dibandingkan berurutan:
board
game
restart
tap
Maka fungsi sekitar 0x1400d32c1 dapat dilabel:
Tauri command dispatcher / command handler glue
Tips manual:
- Rename address di notes walaupun
objdumptidak dapat rename. - Pisahkan branch per command.
tappaling menarik karena menerimaposdan dapat mengembalikancertificate.- Cari immediate dan data reference dalam branch
tap:
grep -nE \
"f4240|0x33|1401f9258|140247498|14024749c" \
dispatcher.asm
13. Recon generator urutan lima tombol
13.1 Temukan helper
Dari branch tap, follow call menuju region:
0x14013a919
0x14013a96f
Dump:
llvm-objdump \
-d \
--start-address=0x14013a919 \
--stop-address=0x14013a9d1 \
"$f" > sequence-helper.asm
13.2 Inisialisasi array
Potongan:
xor eax, eax
loop:
cmp rax, 5
je shuffle
mov byte ptr [rsp + rax], al
inc rax
jmp loop
Pseudocode:
order = [0, 1, 2, 3, 4]
13.3 Fisher-Yates-like shuffle
Potongan inti:
mov edx, 5
loop:
cmp rdx, 1
jbe done
movzx eax, byte ptr [rcx + rdx - 1]
div dl
movzx eax, ah
Pada div dl, remainder disimpan di AH. Karena itu:
j = state[d - 1] % d
Swap:
for d in range(5, 1, -1):
j = state[d - 1] % d
order[d - 1], order[j] = order[j], order[d - 1]
13.4 Inverse permutation
Fungsi berikutnya melakukan pola:
movzx ecx, byte ptr [permutation + rax]
mov byte ptr [inverse + rcx], al
Pseudocode:
inverse = [0] * 5
for index, value in enumerate(order):
inverse[value] = index
Ini adalah detail kritis. Yang masuk ke state update adalah inverse/permutation-derived sequence, bukan order langsung.
Implementasi final:
def expected_sequence(state: bytes) -> bytes:
order = [0, 1, 2, 3, 4]
for size in range(5, 1, -1):
selected = state[size - 1] % size
order[size - 1], order[selected] = (
order[selected],
order[size - 1],
)
inverse = [0] * 5
for index, value in enumerate(order):
inverse[value] = index
return bytes(inverse)
14. Recon state transition
Di jalur sesudah lima input benar terdapat beberapa pemanggilan helper append sekitar:
0x14013a4fd
Dump:
llvm-objdump \
-d \
--start-address=0x14013a4fd \
--stop-address=0x14013a546 \
"$f" > append-helper.asm
Fungsi memperbesar buffer/vector dan menyalin range byte ke ujungnya. Urutan call di branch challenge membangun:
current_state || expected_sequence || level_bytes
Kemudian buffer diberikan ke fungsi hash.
Level adalah integer empat byte yang disalin langsung ke memory pada x86-64. Maka encoding-nya little-endian:
struct.pack("<I", level)
State transition:
next_state = hashlib.sha256(
state
+ expected_sequence(state)
+ struct.pack("<I", level)
).digest()
Panjang material per level:
32-byte state + 5-byte sequence + 4-byte level = 41 bytes
15. Mengidentifikasi SHA-256
Dump function sekitar 0x14013a546:
llvm-objdump \
-d \
--start-address=0x14013a546 \
--stop-address=0x14013a84d \
"$f" > sha256-helper.asm
Initial constants:
0x6a09e667
0xbb67ae85
0x3c6ef372
0xa54ff53a
0x510e527f
0x9b05688c
0x1f83d9ab
0x5be0cd19
Delapan konstanta tersebut adalah initial hash values SHA-256. Fungsi juga memproses blok 64 byte dan menghasilkan digest 32 byte.
Label recon:
0x14013a546 = SHA-256 helper
Tips generalisasi:
- Jangan label hanya karena satu constant terlihat.
- Verifikasi keseluruhan pola: delapan IV, schedule 64 word/round, big-endian word load, digest 32 byte.
16. Mengambil seed
16.1 Temukan reference
Reset/constructor path menunjukkan:
lea 0x14024749c, rdx
mov 0x20, r8d
call sha256_helper
Makna:
- Input pointer:
0x14024749c - Input length:
0x20 = 32 - Input di-hash sebelum disimpan sebagai initial state.
16.2 Convert VA ke raw
raw = 0x14024749c - 0x1401e0000 + 0x1df200
raw = 0x24669c
16.3 Dump 32 byte
od -An -tx1 -v \
-j $((0x24669c)) \
-N 32 \
"$f"
Hasil:
0f 2c f8 4d 34 d6 0d 44
5c bb db c3 92 d5 bf 8b
a0 fd ae 10 15 3a bf a7
20 df 9b 69 7f 77 e2 75
Gabungkan:
0f2cf84d34d60d445cbbdbc392d5bf8ba0fdae10153abfa720df9b697f77e275
Initial state:
state = hashlib.sha256(SEED).digest()
Bukan:
state = SEED
17. Mengambil batas level
Di branch success:
incl %esi
movl %esi, 0x40(%r15)
cmpl $0xf4240, %esi
jne normal_level_path
Konversi:
printf "%d\n" $((0xf4240))
Output:
1000000
Interpretasi:
- Counter dinaikkan setelah satu urutan lima tombol selesai.
- Final/certificate branch tercapai pada
1,000,000. - Recon state loop yang cocok adalah level
0sampai999999.
for level in range(1_000_000):
...
18. Mengambil tag SQNC
Di final branch terdapat range:
[0x140247498, 0x14024749c)
Panjang:
0x14024749c - 0x140247498 = 4 bytes
Convert VA ke raw:
0x140247498 - 0x1401e0000 + 0x1df200
= 0x246698
Dump:
od -An -tc \
-j $((0x246698)) \
-N 4 \
"$f"
Hasil:
S Q N C
Final master material:
master = hashlib.sha256(final_state + b"SQNC").digest()
19. Mengambil ciphertext
19.1 Evidence dari disassembly
Decrypt loop:
lea 0x1401f9258, rax
cmp rcx, 0x33
loop:
mov dl, byte ptr [r15 + rcx]
xor dl, byte ptr [rax + rcx]
mov byte ptr [output + rcx], dl
Data:
ciphertext VA = 0x1401f9258
ciphertext length = 0x33 = 51 bytes
operation = XOR
19.2 Convert ke raw
raw = 0x1401f9258 - 0x1401e0000 + 0x1df200
raw = 0x1f8458
19.3 Dump
od -An -tx1 -v \
-j $((0x1f8458)) \
-N $((0x33)) \
"$f"
Hasil:
a2 f7 59 31 3b 3d cd 55 6a d2 d9 9b c6 50 0b af
94 f2 be d0 fb 32 f2 95 e4 fb 94 6a d1 e7 70 74
0b a0 75 14 4e 14 92 54 e3 e1 02 6c ee ac 31 77
e8 15 e5
Ciphertext hex:
a2f759313b3dcd556ad2d99bc6500baf
94f2bed0fb32f295e4fb946ad1e77074
0ba075144e149254e3e1026ceeac3177
e815e5
20. Recon derivasi keystream
Setelah master dibuat, backend mengulang:
SHA256(master || counter_le32)
Setiap SHA-256 block menghasilkan 32 byte. Karena ciphertext berukuran 51 byte:
ceil(51 / 32) = 2 block
Counter yang dibutuhkan:
0
1
Karena counter 32-bit disalin langsung di x86 little-endian:
block0 = hashlib.sha256(master + struct.pack("<I", 0)).digest()
block1 = hashlib.sha256(master + struct.pack("<I", 1)).digest()
keystream = (block0 + block1)[:51]
Decrypt:
plaintext = bytes(
encrypted ^ key
for encrypted, key in zip(CIPHERTEXT, keystream)
)
Expected plaintext bytes:
b"SQNC\x7fB\xf6oGEMASTIK19{m4th_15_fun_unt1l_l3v3l_1000000}"
Payload flag dimulai di offset 8:
flag = plaintext[8:].decode("utf-8")
21. Ledger data akhir
Gunakan ledger ini untuk memastikan tidak ada data yang berasal dari tebakan.
[File]
SHA256 = fc1eaed7ddf91e5d2bc86486006a9a52709fe0ce57faf8895796858d228fdcb4
Format = PE32+ x86-64 Windows GUI
Image Base = 0x140000000
[Sections]
.text VA/raw = 0x140001000 / 0x400
.rdata VA/raw= 0x1401e0000 / 0x1df200
[Frontend]
Framework = Tauri v2
Compression = Brotli
Assets = /index.html, /app.js, /style.css
[IPC]
Commands = board, restart, tap
Tap arg = pos
Tap output = view, ok, reset, certificate
[Native]
Dispatcher = around 0x1400d32c1
Append helper= around 0x14013a4fd
SHA-256 = around 0x14013a546
Shuffle = around 0x14013a919
Inverse map = around 0x14013a96f
[State]
Seed VA = 0x14024749c
Seed raw = 0x24669c
Seed length = 32
Initial = SHA256(seed)
Sequence = inverse Fisher-Yates-derived permutation
Update = SHA256(state || sequence || level_le32)
Levels = 0..999999
[Final]
Tag VA = 0x140247498
Tag = SQNC
Master = SHA256(final_state || SQNC)
Block i = SHA256(master || counter_le32)
[Cipher]
Cipher VA = 0x1401f9258
Cipher raw = 0x1f8458
Cipher len = 0x33 = 51
Operation = XOR
Payload = plaintext[8:]
22. Solver reproduktif
#!/usr/bin/env python3
import hashlib
import struct
SEED = bytes.fromhex(
"0f2cf84d34d60d445cbbdbc392d5bf8b"
"a0fdae10153abfa720df9b697f77e275"
)
CIPHERTEXT = bytes.fromhex(
"a2f759313b3dcd556ad2d99bc6500baf"
"94f2bed0fb32f295e4fb946ad1e77074"
"0ba075144e149254e3e1026ceeac3177"
"e815e5"
)
def expected_sequence(state: bytes) -> bytes:
permutation = [0, 1, 2, 3, 4]
for size in range(5, 1, -1):
selected = state[size - 1] % size
permutation[size - 1], permutation[selected] = (
permutation[selected],
permutation[size - 1],
)
inverse = [0] * 5
for index, value in enumerate(permutation):
inverse[value] = index
return bytes(inverse)
def solve() -> str:
state = hashlib.sha256(SEED).digest()
for level in range(1_000_000):
sequence = expected_sequence(state)
state = hashlib.sha256(
state
+ sequence
+ struct.pack("<I", level)
).digest()
master = hashlib.sha256(state + b"SQNC").digest()
blocks = (
len(CIPHERTEXT) + hashlib.sha256().digest_size - 1
) // hashlib.sha256().digest_size
keystream = b"".join(
hashlib.sha256(
master + struct.pack("<I", counter)
).digest()
for counter in range(blocks)
)[:len(CIPHERTEXT)]
plaintext = bytes(
encrypted ^ key
for encrypted, key in zip(CIPHERTEXT, keystream)
)
assert plaintext[:4] == b"SQNC"
return plaintext[8:].decode("utf-8")
if __name__ == "__main__":
print(solve())
Expected:
GEMASTIK19{m4th_15_fun_unt1l_l3v3l_1000000}
23. Validasi dan troubleshooting
23.1 Checklist sebelum loop penuh
state = hashlib.sha256(SEED).digest()
for level in range(10):
sequence = expected_sequence(state)
assert len(state) == 32
assert len(sequence) == 5
assert sorted(sequence) == [0, 1, 2, 3, 4]
print(level, state.hex(), list(sequence))
state = hashlib.sha256(
state + sequence + struct.pack("<I", level)
).digest()
23.2 Jika plaintext acak
Periksa berurutan:
- Apakah initial state memakai
SHA256(seed)? - Apakah sequence memakai inverse permutation?
- Apakah level dimulai dari
0? - Apakah level encoded sebagai
<I? - Apakah loop tepat satu juta iterasi?
- Apakah final key memakai
state + b"SQNC"? - Apakah block counter dimulai dari
0? - Apakah counter encoded sebagai
<I? - Apakah ciphertext tepat 51 byte?
- Apakah payload diambil dari offset 8?
23.3 Sanity values
Final state yang benar:
a0ac9a60e26a34a91888af22b7df6b518def5ddc84cb3f162a0bccba208eda37
Plaintext yang benar harus dimulai:
53 51 4e 43
S Q N C
Header ini adalah oracle validasi. Jika tidak ada SQNC, jangan memaksa decode. Kembali ke state generation atau key derivation.
24. Referensi yang mirip
Tauri asset extraction
-
Tauri Extractor / Tauri Asset Ripper
Implementasi extractor untuk embedded assets Tauri v2. Dokumentasinya menjelaskan asset pada PE.rdata, pointer table, dan Brotli decompression. -
Tauri Dumper
CLI/library untuk inspect dan extract embedded frontend assets dari aplikasi Tauri. Berguna untuk membandingkan hasil ekstraksi manual. -
Static resource extraction from Tauri applications
Pembahasan pencarianindex.html, asset table, dan Brotli extraction dari executable Tauri.
Rust binary reverse engineering
-
Rust Binary Analysis, Feature by Feature
Referensi assembly idiom Rust, termasuk perbedaan cara berpikir saat menganalisis output compiler Rust. -
JPCERT/CC Rust Binary Analysis Research
Materi terstruktur tentang identifikasi binary Rust, strings, mangling, closures, enums, iterators, panic, dan library artifacts. -
A Reverse Engineering Guide for Rust Binaries
Pengantar tantangan Rust reversing, static linking, dan karakter string Rust. -
Reversing a Broken Rust Binary
Write-up challenge Rust dengan pendekatan mencari decrypt loop, key generation, dan data section.
JavaScript string-array obfuscation
-
String Array documentation, Obfuscator.io
Dokumentasi dasar pemindahan literal ke string array dan encoding. -
JavaScript String Array System
Penjelasan storage, encoding, rotation, shuffling, dan wrapper pada string-array obfuscation. -
String Array Rotation overview
Ringkasan konsep array rotation dan computed indexing.
Appendix A: Command cheat sheet
# Fingerprint
sha256sum challenge.exe
file challenge.exe
objdump -x challenge.exe | less
objdump -h challenge.exe
# Strings
strings -a -t x -n 5 challenge.exe > strings-ascii.txt
strings -el -t x -n 5 challenge.exe > strings-utf16.txt
grep -Eai "tauri|board|restart|tap|lib\.rs" strings-ascii.txt
# Asset markers
grep -oba "/app.js" challenge.exe
grep -oba "/index.html" challenge.exe
grep -oba "/style.css" challenge.exe
# Full disassembly
llvm-objdump -d --no-show-raw-insn challenge.exe > full-llvm.asm
# Dispatcher slice
llvm-objdump -d \
--start-address=0x1400d32c1 \
--stop-address=0x1400d4100 \
challenge.exe > dispatcher.asm
# Sequence helpers
llvm-objdump -d \
--start-address=0x14013a919 \
--stop-address=0x14013a9d1 \
challenge.exe > sequence-helper.asm
# Hash helper
llvm-objdump -d \
--start-address=0x14013a546 \
--stop-address=0x14013a84d \
challenge.exe > sha256-helper.asm
# Seed
od -An -tx1 -v -j $((0x24669c)) -N 32 challenge.exe
# Tag
od -An -tc -j $((0x246698)) -N 4 challenge.exe
# Ciphertext
od -An -tx1 -v -j $((0x1f8458)) -N $((0x33)) challenge.exe
Appendix B: Recon flow singkat
PE fingerprint
↓
section map dan VA/raw conversion
↓
strings triage
↓
identifikasi Tauri + IPC command
↓
ekstrak Brotli frontend
↓
deobfuscate JavaScript
↓
kumpulkan command dan schema response
↓
command string VA xref
↓
Rust dispatcher
↓
branch tap
↓
sequence helper + inverse permutation
↓
append helper + SHA-256 helper
↓
seed + level limit + tag + ciphertext
↓
key derivation + XOR
↓
validasi SQNC
↓
flag payload