~/tiramisaks
← back to /home

KAPOLRI CUP 2026: Crackme Polri

KAPOLRI CUP 2026 2026-08-01 Reverse Engineering
#KAPOLRI CUP 2026 #Crackme #Reverse Engineering #x86_64 #GDB

CrackMe Polri — SMC (Self-Modifying Code)

Kategori: Reverse Engineering File: crackme_polri (ELF x86-64, PIE, stripped) Flag: polriCTF26{SMC_1S_S0_C00L!!}


Ringkasan

Crackme ELF x86-64 stripped + PIE. Checker flag asli nggak ada di file sebagai kode: dia terenkripsi (XOR) dan baru didekripsi + dieksekusi dari buffer mmap saat runtime (SMC). Ada juga fungsi 0x1420 yang kelihatan seperti checker tapi cuma decoy (nggak pernah dipanggil). Jadi langkahnya: temukan loop decrypt di main, tiru decrypt-nya offline, disassemble, balik transformasinya.


Langkah-Langkah

1. Recon

file crackme_polri
# ELF 64-bit LSB pie executable, x86-64, stripped

readelf -l crackme_polri | head          # entry point 0x1330
strings crackme_polri | head

PIE → semua alamat di objdump/Ghidra itu offset; saat runtime dikasih base acak (ASLR).

2. Disassemble main

Entry point 0x1330 manggil 0x1160 (main). Disassemble:

objdump -d --start-address=0x1160 --stop-address=0x1300 crackme_polri

Inti main (decompile Ghidra ringkas):

if (argc != 2) { printf("Usage: %s <flag>", argv[0]); return 1; }

strncpy(buf, argv[1], 0x7f);                // salin flag
if (strlen(buf) == 0x1c                     // total 28 char
    && strncmp(buf, "polriCTF26{", 0xb) == 0  // prefix 11 char
    && buf[0x1b+0x10] == '}') {             // offset 27 = '}'
    // 16 byte tengah flag diambil (offset 11..26)
    __addr = mmap(0, 0x160, PROT_READ|WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);
    // loop decrypt 0x160 byte
    // mprotect(RX)
    ok = (*__addr)(&inner16);               // panggil checker asli
    if (ok) puts("Correct! Flag valid.");
    else    puts("Nope, coba lagi.");
}

Jadi main cuma: validasi format → decrypt shellcode → call. Yang bikin flag valid ditentukan sepenuhnya oleh hasil decrypt.

3. Loop Decrypt (inti SMC)

0x1246: lea r8,  [rip+0xe33]   # -> 0x2080  (key)
0x1250: lea rdi, [rip+0xe4c]   # -> 0x20a0  (payload terenkripsi, 0x160 byte)
0x1270: loop 0x160 kali:
          dec[i] = key[i % 5] ^ payload[i]
0x12af: mprotect(buf, 0x160, PROT_READ|PROT_EXEC)
0x12bd: call rbx               # eksekusi hasil decrypt

Ekspresi i % 5 di asm ditulis pake trik magic-number mul 0xcccccccccccccccd + and ~3 + shr 2 — cuma compiler biasa. Yang penting hasilnya i % 5.

Cek isi key di file:

offset 0x2080: 91 2c e7 4b 18 00 00 00 00 00

Karena indeksnya i % 5, key efektif cuma 5 byte berulang: [0x91, 0x2c, 0xe7, 0x4b, 0x18].

4. Decrypt Offline

data = open("crackme_polri", "rb").read()
key   = data[0x2080:0x2085]              # 91 2c e7 4b 18
enc   = data[0x20a0:0x20a0+0x160]
dec   = bytes(enc[i] ^ key[i % 5] for i in range(0x160))
open("dec.bin", "wb").write(dec)

5. Disassemble Hasil Decrypt

objdump -D -b binary -m i386:x86-64 dec.bin

Bentuk checker asli (singkat):

; rbp-0x28 = pointer ke 16 byte flag dalam
for i = 0 .. 15:
    c  = flag[i]
    c ^= 0x5a
    c += 3*i                ; per byte, mod 256
    c  = rol2(c)            ; rotate left 2 bit : (c<<2)|(c>>6)
    buf[i] = c

; lalu 16x compare, contoh:
cmp byte [buf+0], 0x24
cmp byte [buf+1], 0x68
...
cmp byte [buf+15], 0xa2

Target 16 byte-nya:

24 68 7c 38 dd 60 5c 78 0a 80 dc 2e 3a f4 96 a2

6. Balikkan Transformasi

Forward:

c = rol2( (flag[i] ^ 0x5a) + 3*i )  (mod 256)  ==  target[i]

Inverse:

c = ror2( target[i] )
c = (c - 3*i) & 0xFF
flag[i] = c ^ 0x5a
def ror2(b): return ((b >> 2) | ((b << 6) & 0xFF)) & 0xFF

targets = [0x24,0x68,0x7c,0x38,0xdd,0x60,0x5c,0x78,
           0x0a,0x80,0xdc,0x2e,0x3a,0xf4,0x96,0xa2]
inner = "".join(chr(((ror2(targets[i]) - 3*i) & 0xFF) ^ 0x5a) for i in range(16))
# SMC_1S_S0_C00L!!

7. Verifikasi

./crackme_polri 'polriCTF26{SMC_1S_S0_C00L!!}'
# Correct! Flag valid.

Catatan: Fungsi Decoy 0x1420

FUN_00101420 (offset 0x1420) kelihatan seperti checker: load 16 byte, paddb/pxor/rotate, lalu cmp 8 konstanta (0x77,0x12,0x99,0x03,0x5e,0xc1,0x2a,0x8d). Tapi:

  • Di ghidra/objdump, 0x1420 tidak pernah di-call (0 caller).
  • main manggil checker lewat call *%rbx (pointer hasil decrypt), bukan 0x1420.

Jadi fungsi itu umpan. Verifikasinya: klik 0x1420 di Ghidra → References → Show References to → kosong.

Solver Lengkap

crackme_polri_solve.py — decrypt SMC, ambil target dari disassembly, balik transformasi, cek ke server/binary.


Kesimpulan

  • Checker asli = kode XOR-encrypted di 0x20a0, key [0x91,0x2c,0xe7,0x4b,0x18] (i%5), ukuran 0x160.
  • Transformasi flag: rol2((c ^ 0x5A + 3*i) & 0xFF) dibanding ke 16 konstanta.
  • Balik transformasi → SMC_1S_S0_C00L!!.

Flag

polriCTF26{SMC_1S_S0_C00L!!}