algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

advent of pwn 2025

2025-12-25updated 2026-06-135 min readctf pwn re

Advent of Pwn is a daily-December CTF: one challenge behind each door, Santa all the way down. I solve them and write up the ones worth writing up here — prompts in the open, full solves folded, so you can try the door before you peek.

The calendar#

day category status
01 reversing solved
02 sysadmin pending

Day 1 — check-list#

Every year, Santa maintains the legendary Naughty-or-Nice list, and despite the rumors, there's no magic behind it at all — it's pure, meticulous byte-level bookkeeping. Apply every tiny change exactly and confirm the final list matches perfectly. Check it once, check it twice; Santa does not tolerate even a single incorrect byte.

targetcheck-list
arch
x86-64
input
read(0, [rbp-0x400], 0x400)
scheme
per-byte add/sub, then a wall of cmp

The whole binary is one straight-line function: read 0x400 bytes onto the stack, mutate them in place with a long run of add/sub, then gate the flag behind an equally long run of cmp … / jne fail. No loops, no key, no entropy — just bookkeeping.

check-list — .textnasm
.text:
    mov    rbp,rsp
    sub    rsp,0x500
    mov    eax,0x0
    mov    edi,0x0
    lea    rsi,[rbp-0x400]
    mov    edx,0x400
    syscall                         ; read 0x400 bytes to [rbp-0x400]

    ; perform all the transformations
    sub    BYTE PTR [rbp-0xf4],0xa2
    add    BYTE PTR [rbp-0x20b],0xf9
    sub    BYTE PTR [rbp-0x381],0x18
    add    BYTE PTR [rbp-0x3a3],0xe4
    sub    BYTE PTR [rbp-0x2b],0x55
    add    BYTE PTR [rbp-0x2bc],0x78
    add    BYTE PTR [rbp-0x287],0xf8
    sub    BYTE PTR [rbp-0x370],0x46
    ; ...

    ; perform all the comparisons
    cmp    BYTE PTR [rbp-0x400],0x86
    jne    0xaa4e6f
    cmp    BYTE PTR [rbp-0x3ff],0x46
    jne    0xaa4e6f
    cmp    BYTE PTR [rbp-0x3fe],0x2a
    jne    0xaa4e6f
    cmp    BYTE PTR [rbp-0x3fd],0x26
    jne    0xaa4e6f
    ; ...

No tricks, no anti-debug — the entire challenge is in that listing. The only thing to get right is the arithmetic.

Day 1 — full solve

For each offset i, the input byte is transformed by its add/sub lines and then must equal that offset's cmp target. Everything is a byte, so it all wraps at 8 bits:

input[i]+addisubigoal[i](mod256)

The wrap is the one thing worth checking — 0xff + 2 == 1, 1 - 2 == 0xff — and Python does it with a single & 0xff:

py
1
2
3
4
>>> (0xff + 2) & 0xff
1
>>> (1 - 2) & 0xff
0xff

My first instinct was two tables: run the add/sub deltas on a zero-filled delta array, stash the cmp targets in a goal array, then recover the input with a wrapping subtraction.

input[i]=(goal[i]delta[i])mod256

Then I noticed I was overcomplicating it. A cmp BYTE [rbp-off], imm has the exact same shape as a transform — [rbp-off], imm — so all three opcodes can fold into one accumulator and I never build a second array:

  • sub off, v → add +v
  • add off, v → add -v
  • cmp off, g → add +g

Sum those per offset and the accumulator is the input (mod 256), since goal - Σadd + Σsub is exactly the recovery formula above.

The second half is free indexing. The offsets are negative (-0x400-0x1), and Python's negative indexing into a 0x400-long list maps -0x400 → 0, -0x3ff → 1, … -0x1 → 0x3ff — straight into read order. No manual offset math.

So the solve is just: disassemble .text, regex every add|sub|cmp [rbp-off], imm, accumulate, mask, send.

solve.pypy
from pwn import *

context.binary = "/challenge/check-list"
text = disasm(context.binary.section('.text'))
instructions = re.findall(r'(add|cmp|sub) +BYTE PTR \[rbp(-0x[0-9a-f]+)\], (0x[0-9a-f]+)', text)
buffer = 0x400 * [0]

for i in instructions:
    buffer[int(i[1], 0)] += int(i[2], 0) * (-1 if i[0] == 'add' else 1)

password = bytes(b & 0xff for b in buffer)
info(f'Password (hex): {password.hex()}')

with process() as p:
    p.sendline(password)
    success(f'Flag: {p.recvall().split()[-1].decode()}')

process() does the rest — send the reconstructed list, scrape the flag off the tail. One pass, no brute force.

Day 2 — claus(7)#

No binary listing this time — the whole prompt is a man page:

$ man clausman
CLAUS(7)                   Linux Programmer's Manual                   CLAUS(7)

NAME
       claus - unstoppable holiday daemon

DESCRIPTION
       Executes once per annum.
       Blocks SIGTSTP to ensure uninterrupted delivery.
       May dump coal if forced to quit (see BUGS).

BUGS
       Under some configurations, quitting may result in coal being dumped into
       your stocking.

SEE ALSO
       nice(1), core(5), elf(5), pty(7), signal(7)

Linux                              Dec 2025                            CLAUS(7)

Santa hid the hint sheet in plain sight: SIGTSTP is blocked, forcing a quit "dumps coal," and the SEE ALSO line points straight at signal(7), core(5) and elf(5).

Day 2 — solve pending

Writeup in progress. I'll fold the full solve in here once it reads clean — the SEE ALSO list is the entire hint sheet.