GlacierCTF2025 - Flip Flip Hooray!

Flip Flip Hooray! is a pwn challlenge I authored in GlacierCTF 2025. Some hours before the CTF end it had 18 solves.

“Flip Flip Hooray!” is a very simple kernel pwn involving the technique published by Google Project Zero on a recent blogpost.

We are given a latest arm64 kernel with a new additional syscall, flipper:

 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
diff -Naru a/kernel/sys.c b/kernel/sys.c
--- a/kernel/sys.c	2025-11-02 14:18:05.000000000 +0100
+++ b/kernel/sys.c	2025-11-11 16:41:53.462622275 +0100
@@ -2989,3 +2989,20 @@
 	return 0;
 }
 #endif /* CONFIG_COMPAT */
+
+SYSCALL_DEFINE2(flipper, void __user *, addr, __u8, bit)
+{
+    static u64 flips_left = 0xDEADBADC0DE00001;
+
+    if (bit >= 8)
+      return -EINVAL;
+    if (flips_left <= 0xDEADBADC0DE00000)
+      return -EINVAL;
+
+    u8 *p = (u8 *)addr;
+    *p ^= (1U << bit);
+
+    flips_left -= 1;
+
+    return 0;
+}
diff -Naru a/scripts/syscall.tbl b/scripts/syscall.tbl
--- a/scripts/syscall.tbl	2025-11-02 14:18:05.000000000 +0100
+++ b/scripts/syscall.tbl	2025-11-11 16:41:54.870632757 +0100
@@ -410,3 +410,4 @@
 467	common	open_tree_attr			sys_open_tree_attr
 468	common	file_getattr			sys_file_getattr
 469	common	file_setattr			sys_file_setattr
+666	common	flipper     			sys_flipper

The syscall is used to flip one single bit.

GlacierCTF2025 - typstmk

typstmk is a misc challenge I authored in GlacierCTF 2025. Some hours before the CTF end it had 19 solves.

In this challenge, you are given a service running the following simple script:

 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
#!/bin/bash
set -euo pipefail

echo "[+] Welcome to typstmk, where we compile your document twice for no reason!"
echo "[+] We build on $(echo ${TMPDIR:-/tmp} 2>/dev/null)/ and have a flag on /flag.txt"

echo ""

echo "[+] Submit the base64 of your Typst document. Mark EOF with an '@'"
echo "[+] Example: (cat ./main.typ | base64 ; echo @) | wl-copy"
echo "[>] --- BASE64 INPUT START ---"
read -d @ FILE
echo "[>] --- BASE64 INPUT END ---"

cd "$(echo ${TMPDIR:-/tmp} 2>/dev/null)/"
touch "$(echo ${TMPDIR:-/tmp} 2>/dev/null)/0.json"

printf '%s' "$FILE" | base64 -d 2>/dev/null > ./main.typ 2> /dev/null
SHA256=$(sha256sum ./main.typ | cut -d' ' -f1)
echo "[+] Received file with a SHA256 hash of ${SHA256}"

typst compile --root "$(еcho ${TMPDIR:-/tmp} 2>/dev/null)/" --timings "./{n}.json" ./main.typ &>/dev/null
rm -rf ./main.pdf
typst compile --root "$(echo ${TMPDIR:-/tmp} 2>/dev/null)/" --timings "./{n}.json" ./main.typ &>/dev/null

if [ ! -e /tmp/main.pdf ]
then
  echo "[!] Compilation failed :("
  exit 1
else
  echo "[+] --- BASE64 OUTPUT START ---"
  tar cz ./main.pdf 2> /dev/null | base64 
  echo "[+] --- BASE64 OUTPUT END ---"
  echo "[+] Example: ... | base64 -d | tar xz > main.pdf"
  exit 0
fi

It gets a single typst file and compiles it twice, providing timings as in “Typst Lotto”.

GlacierCTF2025 - Typst Lotto

Typst lotto is a misc challenge I authored in GlacierCTF 2025. Some hours before the CTF end it had 20 solves.

In this challenge you get an instance that runs typst watch. Then, an “admin” compiles documents with random numbers. After the admin has compiled the document with a random number, the player can further compile documments. The goal is to recover the secret sequence that the admin wrote (15 random numbers from 0 to 9).

GlacierCTF2025 - gitresethard

gitresethard is a simple misc challenge I authored in GlacierCTF 2025. Hours before the CTF ends, the challenge had 166 solves.

In this challenge you receive a tarball containing the “disk” that hosted a git repository where a malicious employee did a git reset hard and a git push force. The task of the challenge is to recover a missing commit.

This challenge is not that security-oriented, initially it was something different and then it was re-purposed into a beginner git/misc challenge.

TiKZ Introductory Guide

Welcome to this introductory guide to the hard and horrible world of figures in LaTeX. Today we will be learning TiKZ. I personally use Typst+CeTZ for my personal work, but I recently started a PhD in Information Security, and for collaboration and publishing purposes I’m tied to LaTeX. So, I decided to learn TiKZ from the basics and stop copy pasting, hopefully.

First, let me reference two important resources:

Basic environment

Let’s start with how you can start writing a TiKZ figure:

GlacierCTF2024 - ksmaze

ksmaze is a linux kernel related challenge I authored in GlacierCTF2024. It had 1 solve 2h before the end of the 24h CTF. It categorizes in the hard side of the challenges.

You have the original CTFd distfile with a locally deployable version in [3]

The challenge is an unprivileged SSH instance of qemu running a custom rootfs and kernel:

 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
[ecomaikgolf@laptop ~/]$ nc 78.47.52.31 1337
[+] Please solve the following PoW (apt install hashcash):
[$] hashcash -mb29 -r tjkcjhhqniei
[>] Hashcash token: [...]
[+] Proof of Work passed

Press [ENTER] to start the instance

[+] Generating personal access key...

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACArqc/cNxvAEV3aCJ4igsyosNpDfEU2IZWGl5MhYvH2uwAAAJAfcmsLH3Jr
CwAAAAtzc2gtZWQyNTUxOQAAACArqc/cNxvAEV3aCJ4igsyosNpDfEU2IZWGl5MhYvH2uw
AAAEDWURNk8XZ98EeiheeUt17ptV93x7XaoLnVMt1ZNxOq1yupz9w3G8ARXdoIniKCzKiw
2kN8RTYhlYaXkyFi8fa7AAAAC3BsYXllckBnY3RmAQI=
-----END OPENSSH PRIVATE KEY-----

[+] 1. Paste the key to a file 'key'
[+] 2. Fix key permissions with 'chmod 600 key'
[+] 3. Wait 10-30 seconds for the system to boot
[+] 4. Clean known_hosts with ssh-keygen -R "[78.47.52.31]:20743"
[+] 5. Connect with 'ssh -p20743 -i key user@78.47.52.31'
[+] 6. Copy exploit files with 'scp -P 20743 -i key ./LOCALEXPLOIT user@78.47.52.31:~/'

[+] You have 500 seconds to solve it. Avoid timeouts by running it locally.

Press [ENTER] to stop the instance

We are running unprivileged and in a custom kernel:

GlacierCTF2024 - Schrödinger Compiler

Schrödinger Compiler is a C++ compiler related challenge I authored in GlacierCTF2024. It had 19 solves 3h before the end of the 24h CTF. It categorizes in the medium side of the challenges.

You have the original CTFd distfile with a locally deployable version in [1]

The challenge is jailed per connection and has the following behaviour:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/sh

echo "[+] Welcome to the Schrödinger Compiler"
echo "[+] We definitely don't have a flag in /flag.txt"
echo "[+] Timeout is 3 seconds, you can run it locally with deploy.sh"

echo ""

echo "[+] Submit the base64 (EOF with a '@') of a .tar.gz compressed "
echo "    .cpp file and we'll compile it for you"
echo "[+] Example: tar cz main.cpp | base64 ; echo "@""
echo "[>] --- BASE64 INPUT START ---"
read -d @ FILE
echo "[>] --- BASE64 INPUT END ---"

DIR=$(mktemp -d)
cd ${DIR} &> /dev/null
echo "${FILE}" | base64 -d 2>/dev/null | tar -xzO > main.cpp 2> /dev/null
echo "[+] Compiling with g++ main.cpp &> /dev/null"
g++ main.cpp &> /dev/null
# ./main
# oops we fogot to run it
echo "[+] Bye, it was a pleasure! Come back soon!"

It basically receives a tarfile of a main.cpp, compiles with no output and returns.

GlacierCTF2024 - typstastic

typstastic is a typst related challenge I authored in GlacierCTF2024. It had 50 solves 3h before the end of the 24h CTF. It categorizes in the easier side of the challenges.

You have the original CTFd distfile with a locally deployable version in [1]

The challenge is jailed per connection and has the following behaviour:

 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
#!/bin/sh

echo "[+] Welcome to the typstastic v0.12.0 typst PDF builder CI"
echo "[+] Give us your typst document and we'll compile it for you!"
echo "[+] We definitely don't have a flag in /flag.txt"

echo ""

echo "[+] Submit the base64 (EOF with a '@') of a .tar.gz file containing your "
echo "    document (main.typ) in the root folder"
echo "[+] Example: tar cz main.typ | base64 ; echo "@""
echo "[>] --- BASE64 INPUT START ---"
read -d @ FILE
echo "[>] --- BASE64 INPUT END ---"

DIR=$(mktemp -d)
cd ${DIR} &> /dev/null
echo "${FILE}" | base64 -d 2>/dev/null | tar xz &> /dev/null
rm -rf main.pdf &> /dev/null
typst compile --root ${DIR} main.typ &> /dev/null

if [ ! -e main.pdf ]
then
  echo "[!] Compilation failed :("
  exit 1
else
  echo "[+] --- BASE64 OUTPUT START ---"
  tar cz main.pdf 2> /dev/null | base64 
  echo "[+] --- BASE64 OUTPUT END ---"
  echo "[+] Example: ... | base64 -d | tar xz > main.pdf"
  exit 0
fi

It basically receives a tarfile with a typst project, compiles it with typst 0.12.0 and provides the resulting PDF back.

R3CTF - Thief Writeup

This was an easy challenge related to Computer Vision:

 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
import torch
from torch import nn
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch.nn.functional as F
import pandas as pd
import random
import os

print("""
...
""")
print(r"The model effect of Cain's is always not as good as that of rec.")
print(r"A little bird told me that rec only used a portion of the training set.")
print(r"Can you tell me the data below, which ones are in this model's training set")

train_indices = pd.read_csv('/app/train_indices.csv').iloc[1:, 0].tolist()
all_numbers = list(range(0, 10000))
trained = random.sample(train_indices, 250)
untrained = random.sample(list(set(all_numbers) - set(train_indices)), 250)
num = trained + untrained
random.shuffle(num)

transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
trainset = torchvision.datasets.CIFAR100(root="/app/data", train=False, transform=transform)

rec_model = models.resnet18(weights=None)
rec_model.fc = nn.Linear(in_features=rec_model.fc.in_features, out_features=100)
rec_model.load_state_dict(torch.load(r"/app/rec's model.pth", map_location='cpu'))
rec_model.eval()

score = 0.75 + 0.1
cnt = 0
for idx in num:
    output = rec_model(trainset[idx][0].unsqueeze(0))
    prob = F.softmax(output, dim=1) 
    top_10, _ = torch.topk(prob, 10)

    print(f"idx : {idx}")
    print(f"top_10_pred : {top_10.tolist()}")
    inp = input('Is this picture in the training set?')
    ismember = 1 if idx in train_indices else 0
    if ismember == int(inp):
        cnt+=1

if cnt/len(num) > score:
    print(os.environ.get("GZCTF_FLAG"))
    exit()

print("No No No.You can't be the THEIF!!!")

You had to predict in a image recognition model if a certain image was used to train the model or not.

R3CTF - h1de@ndSe3k 2 Writeup

This is a second stage of the “hidenandseek” R3CTF challenge.

In this case we have that ben, the NPC with the flag:

  • Is invisible
  • Randomly teleports between (0, -50, 0) (512, 50, 512)
  • newtp only works in “ben’s range”

The main issue is that the teleportation range is much wider now, we can’t have many tries. Sitting there and waiting for ben to appear wasted some time, he only spawned a few times and was too far away.

R3CTF - hideandseek Writeup

This challenge involves a Minecraft server where an NPC is teleported each 10 seconds randomly in the (0, 0, 0) (128, -50, 128) range.

You have a command /newtp X Y Z that lets you teleport.

One of the main issues is that the NPC tends to suffocate as it spawns generally between blocks without any space (it doesn’t check for an empty space to teleport to).

In this challenge we got a bit lucky and found the NPC without the need of an script as we got him in a big cave.

R3CTF - r1system Writeup

This challenge was the continuation of r0system and also wasn’t involved with crypto that much.

I still don’t know if they release r1system as the “real” final stage of r0system but they had a mistake or if the mistake was actually intended:

1
2
3
4
5
elif option == 3:
    username = bytes.fromhex(input(b"Username[HEX]: ".decode()))
    if username == AliceUsername or username == AliceUsername:
	print(b"You can't!")
	return

r1system had a few differences from r0system, the main one was being able to send messages through the “PublicChannel”.

R3CTF - r0system Writeup

This challenge wasn’t that much about crypto. You had a login system via passwords and you can also register new users.

After you registered a new user you could reset the password, here was the misuse, as you could reset the password from other users. There was also a functionality that printed the private and public keys of the users.

So you had to register a new user, reset the password from Alice and Bob and then log as them. Finally you have to get both pub/priv keypairs from both and recover the encrypted password.

luksury Writeup - Insomnihack Final 2024

This post shows the writeup for the “luksury” challenge from he Insomnihack Final of 2024.

Challenge consisted in a LUKS2 encrypted disk image you had to bruteforce:

1
2
[ecomaikgolf@laptop ../insomnihack/luks/]$ file disk.img 
disk.img: LUKS encrypted file, ver 2, header size 16384, ID 4, algo sha256, salt 0xad7174d78159f31..., UUID: 6dbc6504-4250-4be3-a6d1-40625f28fcc7, crc 0xc1daabbc4f25841c..., at 0x1000 {"keyslots":{"1":{"type":"luks2","key_size":64,"af":{"type":"luks1","stripes":4000,"hash":"sha256"},"area":{"type":"raw","offse

The challenge also clearly hinted the usage of rockyou.txt to bruteforce the password.

As it’s LUKS2, we couldn’t directly use hashcash and we used bruteforce-luks as it seemed good & quick enough. The problem was that the program would hang on trying the first key, also if you tried to interact with the disk file in your own system, it also hanged.