~/tiramisaks
← back to /home

GEMASTIK XIX: MatematikaMenyenangkanBukan

GEMASTIK XIX 2026-08-23 Reverse Engineering / Crypto
#GEMASTIK19 #Tauri #Rust #Fisher-Yates #SHA256 #XOR

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, GNU objdump, 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

  1. Tujuan recon
  2. Persiapan environment
  3. Verifikasi dan fingerprint binary
  4. Membangun peta PE dan rumus VA ke raw offset
  5. Triage strings dan identifikasi Tauri/Rust
  6. Mencari dan mengekstrak embedded frontend
  7. Recon HTML dan kontrak UI
  8. Deobfuscation JavaScript
  9. Mengumpulkan IPC contract
  10. Mengubah raw string offset menjadi VA
  11. Mencari xref command dengan llvm-objdump
  12. Mengenali dispatcher Rust
  13. Recon generator urutan lima tombol
  14. Recon state transition
  15. Mengidentifikasi SHA-256
  16. Mengambil seed
  17. Mengambil batas level
  18. Mengambil tag SQNC
  19. Mengambil ciphertext
  20. Recon derivasi keystream
  21. Ledger data akhir
  22. Solver reproduktif
  23. Validasi dan troubleshooting
  24. 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 .txt tidak 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.5 mengarah ke Tauri v2.
  • Path .cargo\registry dan .rs mengonfirmasi ekosistem Rust.
  • board, restart, dan tap kemungkinan IPC command.
  • mmb-core/src/lib.rs memberi 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 objdump tidak dapat rename.
  • Pisahkan branch per command.
  • tap paling menarik karena menerima pos dan dapat mengembalikan certificate.
  • 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 0 sampai 999999.
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:

  1. Apakah initial state memakai SHA256(seed)?
  2. Apakah sequence memakai inverse permutation?
  3. Apakah level dimulai dari 0?
  4. Apakah level encoded sebagai <I?
  5. Apakah loop tepat satu juta iterasi?
  6. Apakah final key memakai state + b"SQNC"?
  7. Apakah block counter dimulai dari 0?
  8. Apakah counter encoded sebagai <I?
  9. Apakah ciphertext tepat 51 byte?
  10. 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

Rust binary reverse engineering

JavaScript string-array obfuscation


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