Hello Navi

Tech, Security & Personal Notes

There's something in the water...

Initial Analysis

The challenge provides a GIF file (CHALL.gif). Inspecting the file with standard tools reveals a 12-frame animation.

  • File Analysis: Running identify -verbose CHALL.gif shows that the image uses an 8-color Global Color Table (3-bit).
  • Palette Anomaly: The palette contains redundant entries for almost identical colors. Specifically, the dark blue color of the water (11, 41, 71) is mapped to both index 1 and index 3.

Solution

In many steganography challenges involving GIFs or indexed PNGs, redundant palette indices are used to hide data. I wrote a script to isolate these two specific indices by extracting the frames and visualizing the pixels.

1
2
3
4
5
6
7
8
9
10
11
import PIL.Image
import numpy as np

# 'P' 代表 Palette(调色板)模式
img = PIL.Image.open('frame-0.gif').convert('P')
data = np.array(img)

# Visualize index 1 as '#' and everything else as '.'
for r in range(100):
row_str = "".join(["#" if x == 1 else "." for x in data[r]])
print(row_str)

By printing the pixel grid of the water area and specifically looking for the "hidden" index (Index 1 vs Index 3), a clear ASCII art representation of the flag appeared:

1
2
3
4
5
6
7
8
9
10
11
12
13
.....................................#.....................................................#........
..................................####.....................................................####.....
..................................#...........................................................#.....
..................................#...#..#...#......#####......#####...#.######.######...##...#.....
...#..#.#####.#####.#####.#####...#..##..#...#......#...#......#......##.....#......#...#.#...#.....
...#..#.#.#.#.#...#.#.....#......##.#.#..##..#......#####......#.....#.#.....#......#..#..#...##....
...#..#.#.#.#.#####.###...###...##....#..#.#.#......#...#......#..##...#....#......#..#######..##...
...#..#.#...#.#...#...###...###..##...#..#.#.#......#...#......#..###..#...#......#.......#...##....
...#..#.#...#.#...#.....#.....#...#...#..#..##......#...#......#....#..#...#......#.......#...#.....
...####.#...#.#...#.#####.#####...#..###.#...#.####.#...#.####.######.###..#......#.......#...#.....
..................................#...........................................................#.....
..................................####.....................................................####.....
.....................................#.....................................................#........

Flag

UMASS{1N_A_G1774}

Welcome to UMassCTF 2026! Join our Discord server using this link: https://discord.gg/E3rSU5UWwY

Initial Analysis

The challenge is a standard welcome task. We need to join the official Discord server and follow the instructions to find the flag.

Solution

After joining the Discord server, the Togekiss bot in the #welcome channel provides instructions to obtain the participant role:

"React to this message after reading the rules above to obtain the participant role"

点击那个 🔒 图标。

Flag

UMASS{w3lc0m3_70_um455c7f2026}

Decrypt tampered audit log entries to reconstruct evidence of unauthorized access.

Initial Analysis

The challenge provides a secure audit log export from "ClinCore Health Systems". Scanning through the logs, several EncryptedToken entries are visible, formatted as Base32 strings.

A critical clue is found in the logs:

1
[2026-03-15 16:00:05] ENCRYPT: Cipher config: XOR mode=repeating key_len=4

This indicates that the tokens are encrypted using a repeating 4-byte XOR key.

Solution

While most tokens in the log decode to fragmented text, the token on line 108 (YX2THEVPQ4LNRIMFCHIKFUCBRL2IGF6567KEFW7Q2AK5XIUEJXI7FUCE33VQ====) decodes to raw binary data, suggesting it contains the flag.

Since the flag format is SDG{...}, we can perform a known-plaintext attack to recover the 4-byte XOR key by XORing the first 4 bytes of the ciphertext with SDG{.

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

# The tampered token (line 108)
ct = base64.b32decode("YX2THEVPQ4LNRIMFCHIKFUCBRL2IGF6567KEFW7Q2AK5XIUEJXI7FUCE33VQ====")

# Use "SDG{" as known plaintext to derive the 4-byte XOR key
known = b"SDG{"
key = bytes([ct[i] ^ known[i] for i in range(4)])

# Decrypt with repeating 4-byte key
plaintext = bytes([ct[i] ^ key[i % 4] for i in range(len(ct))])
print(plaintext.decode())

Key Recovery: - Ciphertext (hex): c5f53392... - Known Plaintext: 5344477b (SDG{) - Derived Key: 96b174e9

Flag

SDG{96b174e94a5cb2c4ae62faa24598da07}

Score! You found a treasure chest! Now if only you could figure out how to unlock it... maybe there's a magic word?

Initial Analysis

The challenge provides a Linux binary that prompts for a "magic word." If the correct word is entered, it displays a treasure chest (ASCII art) and presumably the flag. The goal is to reverse-engineer the "magic word" verification logic.

Using IDA Pro to decompile the main function, the following program flow was identified:

  1. Input Collection: The program reads up to 256 characters from stdin using fgets.
  2. Padding & Allocation:
    • It calculates the input length (v8).
    • It determines a padding value (v7 = v8 % 8).
    • It allocates memory for the input plus padding and copies the string into it.
    • Crucially, it appends v7 null bytes.
  3. Encryption: It calls sub_4012A9, which iterates through the input in 8-byte blocks and encrypts them using sub_4011C6.
  4. Verification: The program checks if the final processed length v8 is 34 (0x22) and if the resulting ciphertext matches a hardcoded byte array at unk_404080.
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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char s[256]; // [rsp+10h] [rbp-130h] BYREF
void *v5[3]; // [rsp+110h] [rbp-30h] BYREF
void *dest; // [rsp+128h] [rbp-18h]
int v7; // [rsp+134h] [rbp-Ch]
signed int v8; // [rsp+138h] [rbp-8h]
int i; // [rsp+13Ch] [rbp-4h]

qmemcpy(v5, "tiny_encrypt_key", 16);
puts("Try to open the chest!");
printf("Maybe try saying the magic word: ");
fgets(s, 256, stdin);
s[strcspn(s, "\n")] = 0;
printf("input: %s\n", s);
v8 = strlen(s);
v7 = v8 % 8;
dest = malloc(v8 + v8 % 8);
memcpy(dest, s, v8);
memset((char *)dest + v8, 0, v7);
v8 += v7;
sub_4012A9(dest, (unsigned int)v8, v5);
printf("result: 0x");
for ( i = 0; i < v8; ++i )
printf("%02X", *((unsigned __int8 *)dest + i));
putchar(10);
if ( v8 == 34 && !memcmp(dest, &unk_404080, 0x22u) )
{
puts("Congrats! Here's your treasure: ");
puts("*******************************************************************************");
puts(" | | | |");
puts(" _________|________________.=\"\"_;=.______________|_____________________|_______");
puts("| | ,-\"_,=\"\" `\"=.| |");
puts("|___________________|__\"=._o`\"-._ `\"=.______________|___________________");
puts(" | `\"=._o`\"=._ _`\"=._ |");
puts(" _________|_____________________:=._o \"=._.\"_.-=\"'\"=.__________________|_______");
puts("| | __.--\" , ; `\"=._o.\" ,-\"\"\"-._ \". |");
puts("|___________________|_._\" ,. .` ` `` , `\"-._\"-._ \". '__|___________________");
puts(" | |o`\"=._` , \"` `; .\". , \"-._\"-._; ; |");
puts(" _________|___________| ;`-.o`\"=._; .\" ` '`.\"` . \"-._ /________________|_______");
puts("| | |o; `\"-.o`\"=._`` '` \" ,__.--o; |");
puts("|___________________|_| ; (#) `-.o `\"=.`_.--\"_o.-; ;___|___________________");
puts("____/______/______/___|o;._ \" `\".o|o_.--\" ;o;____/______/______/____");
puts("/______/______/______/_\"=._o--._ ; | ; ; ;/______/______/______/_");
puts("____/______/______/______/__\"=._o--._ ;o|o; _._;o;____/______/______/____");
puts("/______/______/______/______/____\"=._o._; | ;_.--\"o.--\"_/______/______/______/_");
puts("____/______/______/______/______/_____\"=.o|o_.--\"\"___/______/______/______/____");
puts("/______/______/______/______/______/______/______/______/______/______/________");
puts("*******************************************************************************");
free(dest);
return 0;
}
else
{
puts("Hmmmm.... didn't open...");
free(dest);
return 0;
}
}

__int64 __fastcall sub_4012A9(__int64 a1, unsigned int a2, __int64 a3)
{
__int64 result; // rax
__int64 v5; // [rsp+1Ch] [rbp-Ch] BYREF
unsigned int i; // [rsp+24h] [rbp-4h]

for ( i = 0; ; ++i )
{
result = a2 >> 2;
if ( i >= (unsigned int)result )
break;
v5 = *(_QWORD *)((int)(8 * i) + a1);
sub_4011C6(&v5, a3);
*(_QWORD *)(a1 + (int)(8 * i)) = v5;
}
return result;
}

t64 __fastcall sub_4011C6(unsigned int *a1, _DWORD *a2)
{
unsigned int v3; // [rsp+1Ch] [rbp-14h]
unsigned int v4; // [rsp+20h] [rbp-10h]
int i; // [rsp+28h] [rbp-8h]
int v6; // [rsp+2Ch] [rbp-4h]

v4 = *a1;
v3 = a1[1];
v6 = 0;
for ( i = 0; i <= 31; ++i )
{
v6 -= 1640531527;
v4 += ((v3 >> 5) + a2[1]) ^ (v3 + v6) ^ (16 * v3 + *a2);
v3 += ((v4 >> 5) + a2[3]) ^ (v4 + v6) ^ (16 * v4 + a2[2]);
}
*a1 = v4;
a1[1] = v3;
return v3;
}

Reverse Engineering the Cipher

The function sub_4011C6 implements a variation of the Tiny Encryption Algorithm (TEA).

  • Key: The key is the hardcoded string "tiny_encrypt_key".
  • Constants: It uses the standard TEA delta 0x9E3779B9.
  • Structure: It performs 32 rounds of Feistel-like transformations.
  • Implementation Detail: Unlike standard TEA, it updates the "sum" (v6) at the start of the loop and uses key indices [0, 1] for the first half and [2, 3] for the second half of the block update.

Extraction & Decoding

To solve the challenge, we extract the target ciphertext from 0x404080 and the 16-byte key. Since TEA is a symmetric block cipher, we can implement a decryption routine.

Target Ciphertext (34 bytes): 38 75 5B CB 44 D2 BE 5D 96 9C 56 43 EA 98 06 75 4A 48 13 E6 D4 E8 8E 4F 72 70 8B FF DC 99 F8 76 C5 C9

Decryption Script (Python):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def tea_decrypt(v_bytes, k_bytes):
v0 = int.from_bytes(v_bytes[0:4], 'little')
v1 = int.from_bytes(v_bytes[4:8], 'little')
k = [int.from_bytes(k_bytes[i:i+4], 'little') for i in range(0, 16, 4)]

delta = 0x9E3779B9
sum_val = (delta * 32) & 0xFFFFFFFF

for _ in range(32):
v1 = (v1 - (((v0 >> 5) + k[3]) ^ (v0 + sum_val) ^ ((v0 << 4) + k[2]))) & 0xFFFFFFFF
v0 = (v0 - (((v1 >> 5) + k[1]) ^ (v1 + sum_val) ^ ((v1 << 4) + k[0]))) & 0xFFFFFFFF
sum_val = (sum_val - delta) & 0xFFFFFFFF

return v0.to_bytes(4, 'little') + v1.to_bytes(4, 'little')

# Data from unk_404080
ciphertext = bytes.fromhex("38755BCB44D2BE5D969C5643EA9806754A4813E6D4E88E4F72708BFFDC99F876")
key = b"tiny_encrypt_key"

flag = b""
for i in range(0, len(ciphertext), 8):
flag += tea_decrypt(ciphertext[i:i+8], key)

print(flag.decode().strip('\x00'))

Conclusion

Running the decryption yields the magic word/flag. The check for length 34 and the specific padding logic suggests the original flag was 29 characters long, which when padded with 5 null bytes (29 % 8 = 5), results in the 34-byte block compared by the binary.

Flag

RS{oh_its_a_TEAreasure_chest}

You have recieved a message in a bottle, saying something about the strange behavior of sea creatures. I wonder what that could be about?

Initial Analysis

The challenge provides a rosbag2 recording consisting of a metadata.yaml file and a SQLite database mystery_message_0.db3. The metadata indicates that the recording contains messages for several topics, most notably /draw_commands.

Database Inspection

Opening the database with sqlite3, we find several tables, including topics and messages. The topics table reveals that the /draw_commands topic uses the std_msgs/msg/String type and contains JSON-formatted instructions.

1
2
SELECT * FROM topics WHERE name = '/draw_commands';
-- topic_id 6, type std_msgs/msg/String

Data Extraction

The messages in the messages table are stored in CDR (Common Data Representation) format. For std_msgs/msg/String, the actual string data starts after an 8-byte header (4 bytes CDR header + 4 bytes string length).

By extracting and parsing these strings, we find two types of commands:

  • {"cmd": "pen", "off": 0/1, ...}: Controls whether the turtle is drawing.
  • {"cmd": "teleport", "x": ..., "y": ..., "theta": ...}: Moves the turtle to specific coordinates.

Visualization

Using a Python script with matplotlib, we can reconstruct the path drawn by the turtle. By treating teleport as a movement and pen as the draw state, we can plot the lines.

The coordinates reveal two distinct rows of characters:

  • Top Row (Y ≈ 6.0): Letters forming RS{f0ll0w_th3_
  • Bottom Row (Y ≈ 4.7): Letters forming 5ea_Turtles}

Flag

RS{f0ll0w_th3_5ea_Turtles}

An investigator recovered a Linux disk image from a manuscript ward workstation in Varanasi. The drive appears normal, but operators suspect hidden transfer records were concealed using layered steganography and encryption workflows.

Initial Exploration

The provided disk image (kashi_ritual_ledger.img) is an ext4 filesystem.

1
2
3
4
5
6
7
8
9
❯ file kashi_ritual_ledger.img
kashi_ritual_ledger.img: Linux rev 1.0 ext4 filesystem data, UUID=f83285e2-bb1a-4a34-bdec-b901cf985c4e, volume name "KASHI_LEDGER" (extents) (64bit) (large files) (huge files)

# Mount the image
sudo mkdir -p /mnt/kashi_ledger
sudo mount -o loop,ro kashi_ritual_ledger.img /mnt/kashi_ledger

# Search for hidden files
find /mnt/kashi_ledger -type f -name ".*"

Exploring the filesystem reveals several key files: - challenge_runtime.json: Metadata containing passphrases and AES parameters. - /home/pandit_ved/: User home directory with Pictures/ward_scans, Notes, and a hidden .archive_payloads folder. - /deleted_mail_pool/: Contains .eml files discussing "passphrase doctrine."

Metadata Analysis

Reading challenge_runtime.json provided the following stages: - Stage 1 Steg Passphrase: trishul-lantern-braid - Stage 2 Steg Phrase: ghat-manjari-copper-owl

Notes in home/pandit_ved/Notes/ritual_index_notes.md hinted that the "hidden ledger capsule is in one scan that does not open with the standard (Stage 1) phrase."

Steganography Extraction

There were four BMP scans in the ward_scans directory. Using steghide with the Stage 1 passphrase:

1
2
3
for f in /mnt/kashi_ledger/home/pandit_ved/Pictures/ward_scans/*.bmp; do
steghide extract -sf "$f" -p "trishul-lantern-braid"
done

While most contained decoy files, scan_midnight_index.bmp failed, indicating it required the Stage 2 passphrase:

1
2
steghide extract -sf scan_midnight_index.bmp -p "ghat-manjari-copper-owl"
# Output: wrote extracted data to "stage2_ledger.enc"

Locating the Flag

The extracted stage2_ledger.enc matched a file in the hidden .archive_payloads directory. A corresponding .txt file was also present:

1
cat /mnt/kashi_ledger/home/pandit_ved/.archive_payloads/stage2_ledger.txt

Flag

kashiCTF{ledger_ashes_remember_every_ritual}

We sent the same announcement to three servers for redundancy. Each server has its own RSA key. Intercept all three — maybe you can piece something together.

Initial Analysis

The challenge provides an output.txt containing an exponent \(e=3\), three moduli (\(n1, n2, n3\)), and three ciphertexts (\(c1, c2, c3\)). By examining the data, we notice:

  1. Low Exponent: \(e = 3\) is very small.
  2. Identical Ciphertexts: \(c1 = c2 = c3\). This means the raw message \(M\) was not padded differently for each server.
  3. Magnitude of \(c\): The ciphertext \(c\) is significantly smaller than any of the moduli \(n\).

In RSA, the encryption process is \(c = M^e \pmod n\). Usually, \(M^e\) is much larger than \(n\). However, if \(M^e < n\), then the modulo operation has no effect, and \(c = M^e\).

Solution

Since \(c < n_i\) and \(e=3\), the message \(M\) can be recovered simply by calculating the integer cube root of \(c\): \[M = \sqrt[3]{c}\]

Implementation

Using Python and the gmpy2 library, we can solve for \(M\):

1
2
3
4
5
6
7
8
9
10
11
12
13
import gmpy2
from binascii import unhexlify

# Intercepted ciphertext
c = 475436441896018898725156479190091126537849994697426945980826369000641892902004477923335055269088235139492237640527487698088281484953901383579636883543216552932099156009006828723690550706326538736801225046068870773990108130474408522838234755277972911893744937243892927414355347438993698991261629557719442242861719577879055371620865465785392597257968132649494474946507819896785671106833645551504301840437212737125

# Calculate the cubic root
m, exact = gmpy2.iroot(c, 3)

if exact:
# Convert integer to hex, then to ASCII
flag = unhexlify(hex(m)[2:]).decode()
print(f"Flag: {flag}")

Note: This challenge is a simplified version of Håstad's Broadcast Attack. While Håstad's attack typically uses the Chinese Remainder Theorem (CRT) to solve for \(M^e\) when \(M^e > n_i\), the small size of the message relative to the key size here allowed for a direct cubic root calculation.

Flag

kashiCTF{h4st4d_s4ys_sm4ll_3xp0n3nts_k1ll_RSA_br04dc4sts}

Join the RITSEC CTF Discord server to get the most up-to-date information about the competition. The flag can be found in the topic of the #announcements channel, or in the CTF kickoff announcement.

Initial Analysis

The challenge points to the RITSEC CTF Discord server as a source for competition updates and a hidden flag.

Solution

By checking the topic of the #announcements channel, the flag is readily available.

Flag

RS{p1r4t3_d1sc0rd}

A secret message has been passed down through generations since the time of the great Mahabharata war. Legend says that every 64 years, the keepers of this secret would encode the message once more to protect it from those who might seek to misuse its power. The message has traveled through 3136 years of history, from the ancient battlefields of Kurukshetra in 3136 BCE to the dawn of the Common Era.

Initial Analysis

The challenge provides two main clues:

  1. Mathematical Clue: The message has existed for 3136 years and was re-encoded every 64 years. \[\frac{3136}{64} = 49\] This suggests the message has been recursively encoded 49 times.
  2. File Inspection: The provided file secret_message.txt is large (~59MB) and starts with the characters Vm0wd2Qy..., which is a classic signature for multiple layers of Base64 encoding.

Extraction & Decoding

We can use a Python script to iteratively decode the file 49 times. Each layer of decoding reduces the file size until the final plaintext flag is revealed.

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

def solve():
# Read the initial encoded data
with open('secret_message.txt', 'r') as f:
data = f.read().strip()

# Iteratively decode 49 times
print("[*] Starting iterative Base64 decoding...")
for i in range(49):
try:
data = base64.b64decode(data).decode('utf-8')
except Exception as e:
print(f"[-] Error at iteration {i+1}: {e}")
break

print(f"[+] Final Decoded Message: {data}")

if __name__ == "__main__":
solve()

Flag

kashiCTF{th3_s3cr3t_0f_mah4bh4r4t4_fr0m_3136_BCE}