~/tiramisaks
← back to /home

KAPOLRI CUP 2026: Cocoon

KAPOLRI CUP 2026 2026-08-01 Reverse Engineering
#KAPOLRI CUP 2026 #Cocoon #Binary Analysis #Decompilation

Cocoon — polriCTF 2026 Writeup

Category: Reverse Engineering Binary: cocoon — ELF 64-bit LSB PIE, stripped, entry 0x1230 Flag: polriCTF26{p33l_th3_l4y3rs_x0r_r0t_qu4d_ch41n}


1. Recon

$ file cocoon
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped

$ readelf -h cocoon | grep -E "Type|Entry"
Type: DYN (Position-Independent Executable file)
Entry point address: 0x1230

Programnya stripped (tanpa simbol), prompt-nya unpack> . Fungsi utama (FUN_001010a0):

  • fwrite("unpack> ")fgets(buf, 0x80, stdin) → buang newline.
  • Cek strlen == 0x2e (46).
  • Bangun 46 byte hasil transform input, bandingkan dengan 46 byte target di DAT_001020a0 (memcmp).

Target 46 byte (dari .rodata, offset file 0x20a0):

e2 d8 a6 5f 70 b5 98 30 4f b5 ef 81 2d 3e b0 87
33 33 09 65 b8 61 1d ca c4 2d 75 ea 91 b0 05 4e
aa 5e cc d8 d1 b7 47 95 ba 63 57 07 8a 29

2. Analisis FUN_001010a0 (dekompilasi)

fwrite("unpack> ", 1, 8, stdout);
fflush(stdout);
pcVar6 = fgets((char *)(abStack_99 + 1), 0x80, stdin);
...
if (sVar7 == 0x2e) {                       // panjang 46
    uVar4 = 0;
    _DAT_001040ec = 0;
    do {
        _DAT_001040ec = _DAT_001040ec * 0x1000193 ^ uVar4;   // FNV-ish
        uVar4 = uVar4 + 0x2545f491;
    } while (uVar4 != 0xa8be9220);
    _DAT_001040ec = _DAT_001040ec ^ 0xc0c0012e;

    lVar9 = 0;  uVar4 = 0;
    do {
        _DAT_001040ec = _DAT_001040ec << 0xd ^ _DAT_001040ec;   // xorshift32
        _DAT_001040ec = _DAT_001040ec >> 0x11 ^ _DAT_001040ec;
        _DAT_001040ec = _DAT_001040ec << 5 ^ _DAT_001040ec;

        bVar8 = *pbVar10 ^ (byte)_DAT_001040ec;        // in[i] ^ LSB state
        bVar1 = (lVar9 % 7) + 1;                       // rotasi 1..7
        uVar4 = uVar4 ^ (lVar9*lVar9 + rol8(bVar8, bVar1));   // akumulator
        out[i] = (byte)uVar4;
    } while (++lVar9 != 0x2e);

    memcmp(out, &DAT_001020a0, 0x2e) == 0"correct! that is the flag."
}
puts("nope");

3. Transformasi per byte

Untuk tiap i = 0..45:

state = xorshift32(state);                  // PRNG, independen dari input

k   = state & 0xFF                          // byte kunci
v   = input[i] ^ k                          // XOR
rot = (i % 7) + 1                           // 1..7
t   = rol8(v, rot)                          // rotate left 8-bit

acc ^= i*i + t                              // akumulator 32-bit
out[i] = (byte)acc                          // byte terendahnya dibandingkan
  • PRNG adalah xorshift32: x ^= x<<13; x ^= x>>17; x ^= x<<5 (semua mod 2³²), dengan seed dari loop FNV-ish lalu ^ 0xc0c0012e.
  • Di disassembly, operasi (i % 7) diimplementasi dengan magic number 0x4924924924924925 (imul + shift = pembagian integer oleh 7).

Nilai seed (hasil reimplementasi):

seed = 0xa5b3596e
state[1] = 0x1d413a01  (lsb 0x01)
state[2] = 0x7a21e721  (lsb 0x21)
state[3] = 0x9e796c23  (lsb 0x23)
...

4. Inverse (dekripsi)

Karena out[i] = (byte)(acc_before ^ (i*i + t)):

t      = (acc_before ^ out[i]) - i*i   (mod 256)
v      = ror8(t, (i%7)+1)
input[i] = v ^ (state & 0xFF)
acc_before = out[i]   (untuk iterasi berikutnya)

Alurnya forward (kiri ke kanan) karena state & acc keduanya deterministic dan cuma bergantung pada indeks + acc lama (yang low byte-nya = out[i-1]). Tidak ada brute-force yang diperlukan — pure inverse.


5. Solver (cocoon.py)

#!/usr/bin/env python3

TARGET = bytes.fromhex(
"e2d8a65f70b598304fb5ef812d3eb08733330965b8611dcac42d75ea91b0054eaa5eccd8d1b74795ba6357078a29")
assert len(TARGET) == 46

def u32(x): return x & 0xffffffff

def rol8(x, r):
    x &= 0xff
    return ((x << r) | (x >> (8 - r))) & 0xff

def ror8(x, r):
    x &= 0xff
    return ((x >> r) | (x << (8 - r))) & 0xff

def xorshift32(x):
    x = u32(x)
    x ^= (x << 13) & 0xffffffff
    x ^= (x >> 17)
    x ^= (x << 5) & 0xffffffff
    return x

def init_state():
    state, u = 0, 0
    while u != 0xA8BE9220:
        state = u32(state * 0x1000193) ^ u
        u = u32(u + 0x2545F491)
    return u32(state ^ 0xC0C0012E)

state = init_state()
prev, flag = 0, []
for i in range(46):
    state = xorshift32(state)
    rotated = (prev ^ TARGET[i] - i*i) & 0xff
    ch = ror8(rotated, (i % 7) + 1) ^ (state & 0xff)
    flag.append(ch)
    prev = TARGET[i]

print(bytes(flag).decode())

Output:

polriCTF26{p33l_th3_l4y3rs_x0r_r0t_qu4d_ch41n}

6. Verifikasi

$ printf 'polriCTF26{p33l_th3_l4y3rs_x0r_r0t_qu4d_ch41n}\n' | ./cocoon
unpack> correct! that is the flag.

7. Ringkasan

  1. Recon: stripped PIE, prompt unpack>, cek panjang 46, memcmp 46 byte target di 0x20a0.
  2. Reverse: PRNG = xorshift32 + seed FNV-ish (0xa5b3596e); per byte = acc ^= i*i + rol8(in[i]^lsb, (i%7)+1).
  3. Inverse: forward, in[i] = ror8((acc^out[i] - i*i)&0xff, (i%7)+1) ^ (state&0xff).
  4. Flag diekstrak & diverifikasi ke binary.