# Hology 8 2025 CTF — Writeup (Quals) --- ## Daftar Isi - Web - pyjail? - Reverse - Hidden Factory - ObligatoryFlagCheckerThatIsPacked - Crypto - The Architect’s Hasty Encryption - p-power-rsa - Forensic - The Track Less Travelled --- # Web Exploitation ## pyjail? Oke, jadi ada endpoint `pyjail.php` yang melakukan `eval` python. Biasanya sih ini di bruteforce pake trik MRO, tapi disini ada WAF yang ngeblok karakter krusial (`' " ( ) _`) jadinya kita engga bisa sembarangan. Dan ternyata saat dicek lebih lanjut, ada beberapa kesalahan dari challnya, flag nya disimpen di root dokumen dan bisa diakses langsung. **Flag:** `HOLOGY8{h0l1_m0l1_r3g3x_g07_pwn3d??_51ff8a6c}` **Jadi tinggal kita buka:** `http://ctf.hology.id:8282/flag.txt` Maaf cara solve nya unintended :( --- # Reverse Engineering ## 1. Hidden Factory **Ringkasnya:** pertama kita connect ke `nc ctf.hology.id 1000`, lalu kita akan dapat ciphertext bernama *ENCRYPTED BLUEPRINT*. Sistem enkripsinya ini sendiri terbagi menjadi tiga layer, dan sistem nya harus kita balikan menjadi : Stage‑3 → Stage‑2 → Stage‑1. - **Stage‑1:** Caesar di alfabet kustom (shift +23). Alfabet: `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_` (panjang 65). - **Stage‑2:** XOR pake nilai yang bergantung pada posisi: untuk index `i`: `val = (i*7 + 23) % 256`, `byte ^= val`. - **Stage‑3:** Base64 + substitusi alfabet (map custom ke standard sebelum decode). **Solve:** Kita harus reverse Stage‑3 (balikan map + base64 decode) selanjutnya kita reverse Stage‑2 (xor lagi sama nilai yang sama), dan terakhir kita reverse Stage‑1 (shift -23). **Final script solver nya:** ```py import base64 STD_ALPH = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" CUST_ALPH = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/" ALPH1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_" OFFSET = 23 MULT = 7 def stage3_decode(s): trans = str.maketrans(CUST_ALPH, STD_ALPH) b64 = s.translate(trans) return base64.b64decode(b64) def stage2_unscramble(bs): out = bytearray() for i, b in enumerate(bs): val = (i * MULT + OFFSET) % 256 out.append(b ^ val) return bytes(out) def stage1_inverse(s): out = [] L = len(ALPH1) for ch in s.decode('latin-1'): if ch in ALPH1: idx = ALPH1.index(ch) out.append(ALPH1[(idx - OFFSET) % L]) else: out.append(ch) return ''.join(out) # pake fungsi di atas untuk decrypt ciphertext dari nc ``` **Flag:** `HOLOGY8{f4ct0ry_r3v3rs3_3ng1n33r1ng_m4st3r_0f_th3_h1dd3n_bluepr1nt_cr4ck3r}` --- ## 2. ObligatoryFlagCheckerThatIsPacked Binary ini dipack menggunakan UPX. Setelah unpack, program akan membaca input 30 byte lalu dicek pake kombinasi konstanta `partA` dan `partB`. **Inti chall ini:** ada rumus `local_58[i] = partA[i] ^ ROL8(partB[i],3)` yang membandingkan input dengan `local_58 ^ key[i & 7]` dan `key = "easy_key"`. Jadi kita bisa recover flag dengan ekstrak `partA` & `partB` dari binary dan menghitungnya. **Hal yang harus kita lakukan untuk solve chall ini:** 1. Unpack UPX (atau run di loader terus dump memory). 2. Cari `partA` & `partB` di .rodata. 3. Menghitung `local_58[i] = partA[i] ^ rol8(partB[i],3)` untuk i 0..29. 4. key = b"easy_key" (8 byte), flag[i] = local_58[i] ^ key[i & 7]. **Contoh pseudo:** ```py def rol8(x, r=3): return ((x << r) | (x >> (8 - r))) & 0xFF # ambil A,B dari binary -> hitung -> dapat flag ``` **Solver**: ```py import struct def rol8(x, r=3): return ((x << r) | (x >> (8 - r))) & 0xFF def read_sections(elf): e_shoff = struct.unpack_from("<Q", elf, 0x28)[0] e_shentsz = struct.unpack_from("<H", elf, 0x3A)[0] e_shnum = struct.unpack_from("<H", elf, 0x3C)[0] e_shstrndx = struct.unpack_from("<H", elf, 0x3E)[0] sections = [] for i in range(e_shnum): off = e_shoff + i * e_shentsz sh = struct.unpack_from("<IIQQQQIIQQ", elf, off) sections.append({ "name_off": sh[0], "type": sh[1], "flags": sh[2], "addr": sh[3], "offset": sh[4], "size": sh[5], "link": sh[6], "info": sh[7], "addralign": sh[8], "entsize": sh[9], }) # Read section header string table shstr = sections[e_shstrndx] shstr_data = elf[ shstr["offset"] : shstr["offset"] + shstr["size"] ] def get_name(off): end = shstr_data.find(b"\x00", off) return shstr_data[off:end].decode() for s in sections: s["name"] = get_name(s["name_off"]) return sections def read_symbols(elf, sections): symtab = next(s for s in sections if s["name"] == ".symtab") strtab = next(s for s in sections if s["name"] == ".strtab") symdat = elf[ symtab["offset"] : symtab["offset"] + symtab["size"] ] strdat = elf[ strtab["offset"] : strtab["offset"] + strtab["size"] ] symbols = [] for i in range(symtab["size"] // symtab["entsize"]): off = i * symtab["entsize"] st_name, st_info, st_other, st_shndx, st_value, st_size = \ struct.unpack_from("<IbbHQQ", symdat, off) end = strdat.find(b"\x00", st_name) name = strdat[st_name:end].decode() symbols.append({ "name": name, "info": st_info, "shndx": st_shndx, "value": st_value, "size": st_size, }) return symbols def get_bytes(elf, sections, symbols, name): sym = next(s for s in symbols if s["name"] == name) sec = sections[sym["shndx"]] file_off = sec["offset"] + (sym["value"] - sec["addr"]) return elf[file_off : file_off + sym["size"]] # ============================================================ # Main # ============================================================ PATH = "unpacked_rev" # hasil: upx -d with open(PATH, "rb") as f: elf = f.read() sections = read_sections(elf) symbols = read_symbols(elf, sections) A = get_bytes(elf, sections, symbols, "partA") B = get_bytes(elf, sections, symbols, "partB") local58 = bytes( A[i] ^ rol8(B[i], 3) for i in range(30) ) key = b"easy_key" flag = bytes( local58[i] ^ key[i & 7] for i in range(30) ) print(flag.decode()) ``` **Flag:** `HOLOGY8{n33d3d_4_4_r3v_p4ck3r}` --- # Cryptography ## 1. The Architect’s Hasty Encryption Ada 3 RSA artifacts. Dua diantaranya mempunyai shared prime yang mana bisa kita gunakan **gcd** untuk dapetin **p**. Dan artefak ketiga menggunakan **n = p^2** yang mana akan memudahkan kita untuk dekripsi. **Flow enkripsi nya:** - p = gcd(nA, nB) - Kita faktorkan nX (kalo nX == p^2), kita hitung d = e^{-1} mod phi(nX) - Setelah itu kita dekripsi cX dan kita akan mendapatkan **KEY** nya - Terakhir, kita ambil **KEY (ASCII)** jadi **HEX**, lalu kita gabung ke p (jadi string), dan sha256. Setelah itu kita akan mendapatkan **flag** nya. **Hasil SHA256 yang keluar:** `7ec2c6c4821ffe2bd9fa2045ef69791a22e595425e2948844e9b1fd3b37d54ca` **Flag:** `HOLOGY8{7ec2c6c4821ffe2bd9fa2045ef69791a22e595425e2948844e9b1fd3b37d54ca}` --- ## 2. p‑power‑rsa Chall ini menggunakan RSA dengan modulus yang tidak square‑free, misal `N = p^r * q` (r≥2). Ada hubungan kecil antara dua private exponents, yang bisa dipakai LLL/Coppersmith untuk menemukan delta kecil. **Garis besar nya:** - Formulasi masalah jadi root finding linear kecil. - Kita akan menggunakan LLL atau small-root technique untuk mendapatkan Δ. - Dari Δ yang kita dapatkan, gcd akan ngebocorin faktor **p^(r−1)** lalu bisa kita recover **p,q** dan kita decrypt. ```py # ============================================================ # solve_ppower_rsa_fixed.sage # ============================================================ from sage.all import * # ------------------------------------------------------------ # Setup # ------------------------------------------------------------ Z = ZZ PR = PolynomialRing # ------------------------------------------------------------ # Constants from challenge # ------------------------------------------------------------ c = Integer( 111485787416215732083419888356058001033979399362109360508 26187089114840724405691532084205700450578271560281812344730088409 50208480418896958145264338686293650231691792222492663896258529529 87468768744093949285183059141092635835298369742277053305386129494 49893288884757430080838808926832528531554476875824396277630533852 26221852536414060400397668638390068574218789239859899349649806086 78162026614939501709616234022560513068727741227451418843199235135 74909526934129781982520409344915204011945042876058332259941182649 38664198308269684384412766681923585648346035966663578374146570211 99051732261588602224173516664537564148394089550429651483809997867 45067799292206419334487477074750340090409380178087362694809249384 83778916870232621005585404027295880314360304324547942498123434383 90584037127772193904170421087845937142186548759745304853458426108 89547272654404100971318581885200905937761466010011130409221400225 29357970105883386976827646232264113729591494752134918980265858392 27437083313605739136374826611550285890516268274465906516027362250 60976763088927714433438532990708303684257823041138278098690349309 61352233448908795589570445785916699320291444553085845395422872534 81580691466811698093274707201630462570730775293023489199764838103 52375566246137475119895846742635800668869428611373349947620894937 86972396117163064777826915719498372710037996238034841685083674404 66097349584557388186877058472216663997659060248313992041943213504 83011292871077770270415445375268583670974485113451091685851930681 09172032501486157989738547685799120192688845063502134676407713464 85176419582269169891572750725266499997539100897292414501918122708 31903820127718178087134916043563030557962263828373741474240294496 36142143723965161304999822527648186438619503913331971241426026581 89297469501831730837739445119194832277196410874479162266849124548 98849413857255170640387605610788711387174199311750727941171876289 82386126756769131559394794494188680420063298157018187160344334249 36492656520443344944013881853487228563694901400446844531668 ) e = Integer(65537) N = Integer( 453749993609862423767406315499943576715619270719355402974 23859805295539492811573655205214935999778624622035669768208587595 29218143036086983706210015934982026641371056147977060929637666485 97009961505999304804868628819913053836743203352789790043629722002 65756125120047946168238784120998943211497549162417738445189139137 91868779434356749413927932306679870757960053047235867464992349071 81428238498088293365909082016312817879158926491633438828864279569 81388044992890319744231405679808294831634533302012433342227101026 65582407044626577954373312845684702730716237844639808043564070484 93506788785174496491522489832266993857493571141791095824560928202 78785391845490692529410810830763245464318129228423813901413965566 14688558576411125274758109643984898177268867155526515486406552089 66527802039076290335379020936965043298763839401599171514405533064 38288924650907511067102750891160442877581241774036912780601511247 78162240900133870550291471107609587274609296125964886837151575243 48089512845563356023108705591279503195318426654042354995168730827 22427577433858935496084389231152779521025662507616153014801430837 35102054758031266202731220263554484311016232518079148726570946364 73462128842533375598676295429191094715741380629940237085984351828 45540267141135904098507938822378845568711349570185162389495695764 55509255199128563782586379754441399685527831592764818262945580264 30514802530631451990707908860581231085522384501312007775067802744 55072012130971156806367205765189457157795566712527784194089612138 95974778009995100998978024585195694883991499678909222606659665158 78625384142699235491857754702310410293104365494144520258022000143 26549526973003116317052964039607035814432505055426282735050697422 53165941764318391828345884572254968633128186074911835384215030850 95259381779598083377226530394482769313660477032407154580895878594 63272220865967511425425870472798001225357792182807761489625539080 56978900160113279840292213115387898067931606626553838499139436472 82192959359920313936222580284259387292748269260001904819421 ) # ------------------------------------------------------------ # Derived values # ------------------------------------------------------------ A = e1 * e2 B = e1 - e2 # ------------------------------------------------------------ # Utilities # ------------------------------------------------------------ def recover_p_r_q_from_G(G, N): """ Recover p, r, q assuming: G = p^(r-1) N = p^r * q """ p = gcd(G, N // G) if p <= 1: fac = factor(G) p = max(f[0] for f in fac) r = valuation(N, p) pr = p**r q = N // pr assert N == pr * q return int(p), int(r), int(q) def decrypt_ppower(c, e, p, r, q): """ RSA decryption for p^r * q modulus """ dp = inverse_mod(e, (p - 1) * p**(r - 1)) dq = inverse_mod(e, (q - 1)) mp = power_mod(c, dp, p**r) mq = power_mod(c, dq, q) return int(crt([mp, mq], [p**r, q])) # ------------------------------------------------------------ # Coppersmith: Linear small root with unknown divisor # ------------------------------------------------------------ def small_root_linear_unknown_divisor( A, B, N, X, m_list=(3, 4, 5, 6), t_list=(1, 2, 3, 4), scales=(1, 2, 3, 4, 6, 8, 12, 16, 24, 32) ): """ Find small Δ such that: A*Δ - B ≡ 0 (mod φ(N)) using Howgrave-Graham / LLL """ RZ.<x> = PolynomialRing(ZZ) RQ.<y> = PolynomialRing(QQ) f = A*x - B tried = 0 for m in m_list: for t in t_list: for s in scales: tried += 1 print(f"[*] Trying m={m}, t={t}, scale={s} (#{tried})") Mmod = N * s polys = [] for j in range(t): polys.append((x**j) * (Mmod**m)) for i in range(1, m + 1): for j in range(m - i + 1): polys.append((x**j) * (f**i) * (Mmod**(m - i))) polys_scaled = [p(x * X) for p in polys] deg = max(p.degree() for p in polys_scaled) rows = [ [int(pp[i]) if i <= pp.degree() else 0 for i in range(deg + 1)] for pp in polys_scaled ] Bmat = Matrix(ZZ, rows) Bred = Bmat.LLL() for row in Bred.rows(): if all(c == 0 for c in row): continue gZ = sum(row[i] * x**i for i in range(len(row))) gQ = RQ(gZ(x=y / X)) if gQ.is_zero(): continue den = lcm([c.denominator() for c in gQ.coefficients()]) GZ = (den * gQ).change_ring(ZZ) for root, _ in GZ.roots(ZZ): root = int(root) if root.bit_length() > X.nbits() + 2: continue Gcand = gcd(N, A*root - B) if 1 < Gcand < N: return root, int(Gcand) return None, None # ------------------------------------------------------------ # Main # ------------------------------------------------------------ X = Integer(2)**1012 Delta, G = small_root_linear_unknown_divisor(A, B, N, X) if not Delta: print("[!] Retry with more aggressive parameters...") Delta, G = small_root_linear_unknown_divisor( A, B, N, X, m_list=(5, 6, 7), t_list=(2, 3, 4, 5), scales=(1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 64, 128) ) if not Delta: raise RuntimeError("Failed to recover Δ") print(f"[+] Delta = {Delta}") print(f"[+] G = {G}") p, r, q = recover_p_r_q_from_G(G, N) print(f"[+] p = {p}") print(f"[+] r = {r}") print(f"[+] q = {q}") m = decrypt_ppower(c, e, p, r, q) pt = Integer(m).to_bytes((m.nbits() + 7) // 8, "big") print("[+] Plaintext (hex):", pt.hex()) print("[+] Plaintext (ascii):", pt.decode(errors="ignore")) ``` **Flag:** `HOLOGY8{Mu17i_P0w3R_RSA_w17h_W34k_Struc7ur3}` --- # Forensic ## The Track Less Travelled Chall ini berupa file MP4 yang punya track audio tersembunyi, handlernya bukan standard jadi player biasa tidak akan bisa membukanya. Isinya merupakan AAC‑LC tanpa ADTS header. **Cara solve chall ini:** 1. Parse MP4 (`ffprobe` / parse box) cari trak yang **sus**. 2. Dapatkan `AudioSpecificConfig` dari `esds`. 3. Dari `stsz` / `stco` / `stsc` dibuat panjang untuk setiap frame, lalu buat header ADTS 7 byte per frame, dan gabungkan menjadi => `track_hidden.aac`. 4. Convert ke WAV, bersihin noise, lalu kita dengarkan audio nya (**yap, ini mau main CTF apa test TOEFL plis)** flag disebutkan satu persatu hurufnya di audio. **Solver:** ```py #!/usr/bin/env python3 """ Extract hidden AAC tracks from a tricky MP4 by reconstructing ADTS headers """ import struct import sys from pathlib import Path # ============================================================ # MP4 box helpers # ============================================================ def find_child(data, parent_off, parent_size, type_name): i = parent_off + 8 end = parent_off + parent_size while i + 8 <= end: size = struct.unpack(">I", data[i:i + 4])[0] typ = data[i + 4:i + 8].decode("latin1") if typ == type_name: return i, size if size == 0: break i += size return None def parse_boxes(data, start=0, end=None): if end is None: end = len(data) i = start boxes = [] while i + 8 <= end: size = struct.unpack(">I", data[i:i + 4])[0] typ = data[i + 4:i + 8].decode("latin1") if size == 0: size = end - i header = 8 elif size == 1: size = struct.unpack(">Q", data[i + 8:i + 16])[0] header = 16 else: header = 8 boxes.append((i, size, typ)) if typ in ("moov", "trak", "mdia", "minf", "stbl", "udta", "meta"): child_start = i + header + (4 if typ == "meta" else 0) boxes += parse_boxes(data, child_start, i + size) i += size return boxes # ============================================================ # Sample table parsing # ============================================================ def get_stbl_boxes(data, trak_off, trak_size): mdia = find_child(data, trak_off, trak_size, "mdia") minf = find_child(data, mdia[0], mdia[1], "minf") stbl = find_child(data, minf[0], minf[1], "stbl") def child(name): return find_child(data, stbl[0], stbl[1], name) return { name: child(name) for name in ("stsd", "stts", "stsc", "stsz", "stco", "co64") } def parse_stco(data, off, size): entry_count = struct.unpack(">I", data[off + 12:off + 16])[0] return [ struct.unpack(">I", data[off + 16 + 4 * i:off + 20 + 4 * i])[0] for i in range(entry_count) ] def parse_stsz(data, off, size): sample_size = struct.unpack(">I", data[off + 12:off + 16])[0] entry_count = struct.unpack(">I", data[off + 16:off + 20])[0] if sample_size != 0: return [sample_size] * entry_count return [ struct.unpack(">I", data[off + 20 + 4 * i:off + 24 + 4 * i])[0] for i in range(entry_count) ] def parse_stsc(data, off, size): entry_count = struct.unpack(">I", data[off + 12:off + 16])[0] entries = [] for i in range(entry_count): a = off + 16 + 12 * i first_chunk = struct.unpack(">I", data[a:a + 4])[0] samples_per_chunk = struct.unpack(">I", data[a + 4:a + 8])[0] sample_desc_index = struct.unpack(">I", data[a + 8:a + 12])[0] entries.append( (first_chunk, samples_per_chunk, sample_desc_index) ) return entries def build_chunk_samples(stco_list, stsc_entries, stsz_list): chunks = [] sample_index = 0 for idx, chunk_offset in enumerate(stco_list, start=1): entry = None for e in stsc_entries: if e[0] <= idx: entry = e samples_per_chunk = entry[1] if entry else 0 size = stsz_list[sample_index] if samples_per_chunk else 0 chunks.append((chunk_offset, size)) sample_index += samples_per_chunk return chunks # ============================================================ # AAC / ADTS helpers # ============================================================ def find_decoder_specific_info(esds_blob): payload = esds_blob[12:] i = 0 if i >= len(payload) or payload[i] != 0x03: return None i += 1 length = 0 while True: b = payload[i] i += 1 length = (length << 7) | (b & 0x7F) if not (b & 0x80): break i += 2 # ES_ID flags = payload[i] i += 1 if flags & 0x80: i += 2 if flags & 0x40: url_len = payload[i] i += 1 + url_len if flags & 0x20: i += 1 if payload[i] != 0x04: return None i += 1 while payload[i] & 0x80: i += 1 i += 14 # decoder config if payload[i] != 0x05: return None i += 1 slen = 0 while True: b = payload[i] i += 1 slen = (slen << 7) | (b & 0x7F) if not (b & 0x80): break return payload[i:i + slen] def parse_asc(asc): b0, b1 = asc[0], asc[1] aot = (b0 >> 3) & 0x1F sf_index = ((b0 & 0x07) << 1) | ((b1 >> 7) & 0x01) ch_config = (b1 >> 3) & 0x0F return aot, sf_index, ch_config def build_adts_header(frame_length, aot, sf_index, ch_config): profile = aot - 1 hdr = bytearray(7) hdr[0] = 0xFF hdr[1] = 0xF1 hdr[2] = ( ((profile & 0x3) << 6) | ((sf_index & 0xF) << 2) | ((ch_config >> 2) & 0x1) ) hdr[3] = ((ch_config & 0x3) << 6) | ((frame_length >> 11) & 0x3) hdr[4] = (frame_length >> 3) & 0xFF hdr[5] = ((frame_length & 0x7) << 5) | 0x1F hdr[6] = 0xFC return bytes(hdr) # ============================================================ # Extraction # ============================================================ def extract_track_aac(mp4_bytes, trak_off, trak_size, out_path): stbl = get_stbl_boxes(mp4_bytes, trak_off, trak_size) stsd_off, _ = stbl["stsd"] entry_off = stsd_off + 16 entry_size = struct.unpack(">I", mp4_bytes[entry_off:entry_off + 4])[0] region = mp4_bytes[entry_off:entry_off + entry_size] esds_pos = region.find(b"esds") esds_off = entry_off + esds_pos - 4 esds_size = struct.unpack(">I", mp4_bytes[esds_off:esds_off + 4])[0] asc = find_decoder_specific_info( mp4_bytes[esds_off:esds_off + esds_size] ) aot, sf_idx, ch = parse_asc(asc) stco = parse_stco(mp4_bytes, *stbl["stco"]) stsz = parse_stsz(mp4_bytes, *stbl["stsz"]) stsc = parse_stsc(mp4_bytes, *stbl["stsc"]) chunks = build_chunk_samples(stco, stsc, stsz) out = bytearray() for off, size in chunks: if size == 0: continue frame = mp4_bytes[off:off + size] hdr = build_adts_header(7 + len(frame), aot, sf_idx, ch) out += hdr + frame Path(out_path).write_bytes(out) return out_path # ============================================================ # Main # ============================================================ def main(): if len(sys.argv) < 2: print("Usage: python extract_hidden_track.py chall.mp4") sys.exit(1) mp4 = Path(sys.argv[1]).read_bytes() boxes = parse_boxes(mp4) traks = [b for b in boxes if b[2] == "trak"] if len(traks) < 2: print("Only one track found.") sys.exit(1) out = extract_track_aac( mp4, traks[1][0], traks[1][1], "track2_hidden.aac", ) print("Wrote", out) if __name__ == "__main__": main() ``` **Flag:** `HOLOGY8{4ud4c1ty_s0_g00d_w8r5d6ls}` --- Sekian terimakasih :> JANGAN LUPA MAIN CTF YAAA!!!