Hello Navi

Tech, Security & Personal Notes

Hopefully I can track down this red brick, because if there’s one thing I know, it’s that I hate sand. Flag format UMASS{What_The_Red_Brick_Does}, eg UMASS{Ground_Pound}

Initial Analysis

The challenge description provides several key clues:

  • “Son of a Sith…”: A direct reference to Darth Vader/Anakin Skywalker.
  • “I hate sand”: A famous quote by Anakin Skywalker from Star Wars: Episode II – Attack of the Clones, strongly associating the location with the desert planet Tatooine.
  • “Red Brick”: A collectible item from the LEGO Star Wars video games used to unlock “Extras”.
  • Visual Clues: The provided image shows a LEGO Darth Vader in a canyon environment (Jundland Wastes) with a floating Red Brick.

Solution

1. Game Identification

The low-poly aesthetic and the physical presence of a “Red Brick” (rather than the “Datacards” used in the 2022 Skywalker Saga) point to the classic games developed by TT Games, specifically LEGO Star Wars: The Complete Saga or LEGO Star Wars II: The Original Trilogy.

2. Location & Level

The quote “I hate sand” and the environment confirm the setting as Tatooine. Specifically, the visual matches Episode IV, Chapter 2: “Through the Jundland Wastes”.

3. Red Brick Function

In LEGO Star Wars: The Complete Saga, the Red Brick found in the “Through the Jundland Wastes” level is located in a hidden area accessible in Free Play mode. Collecting this brick unlocks a specific “Extra” in the Cantina.

Researching the collectibles for this specific level reveals that the Red Brick unlocks the Fast Force ability, which speeds up the animation for Jedi and Sith characters using the Force on objects.

Flag

UMASS{Fast_Force}

I’ve heard there’s a computer shop in the area that sells a computer that isn’t designed to run Windows, macOS, or Linux. What’s the processor that’s in their flagship, PCIe-capable system? Flag format: UMASS{Name of Processor}, i.e. UMASS{AMD Ryzen 7 9800X3D}

Initial Analysis

This OSINT challenge asks for the processor of a flagship, PCIe-capable computer system sold by a shop that specializes in hardware not intended for mainstream operating systems like Windows, macOS, or Linux.

Key clues: - Non-mainstream OS (Windows, macOS, Linux excluded). - Flagship, PCIe-capable system. - Computer shop in a specific “area” (implied by the context of previous challenges or search).

Solution

Locate Luxembourg Frence first through picture.

By researching specialized computer shops, we identified AAA Technology Sàrl in Dudelange, Luxembourg. They are well-known for selling Amiga compatible hardware, which runs AmigaOS rather than Windows or Linux.

Their flagship PCIe-capable system is the AmigaOne series. Investigation into the technical specifications of their flagship systems and Amiga platform revealed that the processor used is the NXP QorIQ P1022.

Flag

UMASS{NXP QorIQ P1022}

I dropped my message into the bin of Legos. It’s all scrambled up now. Please help.

Initial Analysis

The challenge provides a Python script encoder.py and its corresponding output output.txt. The goal is to recover a flag that has had its bits scrambled based on a seed derived from a hardcoded string.

Seed Generation

The script uses RSA to “encrypt” the string "I_LOVE_RNG":

1
2
3
4
text = "I_LOVE_RNG"
n, seed = RSA_enc(text)
# ...
enc_seed = pow(seed, e, n)

The RSA_enc function calculates seed = pow(plain_num, e, n). In the main function, it then calculates enc_seed = pow(seed, e, n). Since e = 7 is very small and the plaintext "I_LOVE_RNG" is short, the value me (where m is the integer representation of the text) is much smaller than the 4096-bit RSA modulus n. This means pow(m, e, n) is simply me without any modular reduction.

Bit Shuffling

1
2
3
4
flag_bits = get_flag_bits(flag)
for i in range(10):
random.seed(seed*(i+1))
random.shuffle(flag_bits)

The flag is converted into a list of bits and shuffled 10 times using Python’s random.shuffle. Each shuffle is seeded with a value derived from the seed identified above.

Solution

To recover the flag, we follow these steps:

  1. Recover the Seed: Calculate the integer value of "I_LOVE_RNG" and raise it to the 7th power to get the seed. We can verify this against enc_seed in the output file by checking if seed**7 == enc_seed.
  2. Reverse the Shuffle: Since random.shuffle is deterministic when the seed is known, we can track how the positions change. Instead of shuffling the bits directly, we shuffle a list of indices [0, 1, 2, ..., len(bits)-1].
  3. Reconstruct the Flag: After 10 shuffles, the index list tells us where each original bit ended up. We map the bits from the encoded flag back to their original positions and convert the bit array back into a string.

Solution 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
37
38
import random

def get_flag_bits(flag_hex):
flag_bytes = bytes.fromhex(flag_hex)
bits = []
for b in flag_bytes:
bits.extend(list(bin(b)[2:].zfill(8)))
return bits

def bit_arr_to_str(bit_arr):
byte_arr = []
for i in range(0, len(bit_arr), 8):
byte = bit_arr[i:i+8]
char = int(''.join(byte), 2)
byte_arr.append(char)
return bytes(byte_arr)

# Values from output.txt
enc_flag_hex = "a9fa3c5e51d4cea498554399848ad14aa0764e15a6a2110b6613f5dc87fa70f17fafbba7eb5a2a5179"
text = "I_LOVE_RNG"
plain_num = int.from_bytes(text.encode(), "big")
seed = plain_num ** 7

flag_bits_enc = get_flag_bits(enc_flag_hex)
num_bits = len(flag_bits_enc)
indices = list(range(num_bits))

# Replay the shuffle on the indices
for i in range(10):
random.seed(seed * (i + 1))
random.shuffle(indices)

# Map scrambled bits back to original positions
flag_bits_orig = [None] * num_bits
for j in range(num_bits):
flag_bits_orig[indices[j]] = flag_bits_enc[j]

print(bit_arr_to_str(flag_bits_orig).decode())

Flag

UMASS{tH4Nk5_f0R_uN5CR4m8L1nG_mY_M3554g3}

My friend tells me there’s an office of a store that sells special bricks above a well-known shopping centre on this street. My friend tells me I’ll it will bring me joy, but I’m not sure that the bricks are from the famous company. Can you give me the contact email address of the store so I can learn more?

Flag format: UMASS{email@example.com}

Key clues: - “Special bricks” (possibly a LEGO alternative store) - “Above a well-known shopping centre” - “Bring me joy” (might be part of the store’s name)

Solution

By researching shopping centers in Hong Kong known for hobbyist shops, we identified Ho King Commercial Centre (好景商业中心) and Ho King Shopping Centre (好景商场) as likely locations.

Using tools like Google Gemini Deep Search to find shops selling “bricks” in these locations, we found a store associated with “Joy” (matching the “bring me joy” hint). The store in question is Joying Bricks.

Searching for their contact information lead to the official email address.

Flag

UMASS{joyingwang@gmail.com}

Hey! A man was caught with malware on his PC in Lego City. Luckily, we were able to get a packet capture of his device during the download. Help Lego City Police figure out the source of this malicious download.

The flag for this challenge is in the format UMASS{[sha256sum]} of the malicious download.

Initial Analysis

The challenge provides a packet capture (thedamage.pcapng) containing traffic from a machine that downloaded malware. The goal is to identify the malicious file and its hash.

Solution

Using tshark, we analyzed the HTTP traffic to see what files were downloaded:

1
tshark -r thedamage.pcapng -Y http -T fields -e http.host -e http.request.uri | sort | uniq -c | sort -rn

The output revealed several interesting files: - /installer.py - /launcher - Several images (/fungame.jpg, /cooldog.jpeg, etc.)

We extracted the objects from the HTTP sessions:

1
tshark -r thedamage.pcapng --export-objects http,extracted_files -q

The launcher file appeared suspicious, but its file type was obscured and it didn’t match known malware signatures.

Examining installer.py revealed it was a dropper script that decrypts launcher:

1
2
3
4
5
6
7
8
9
10
11
12
13
import hashlib
import nacl.secret

def fix_error():
seed = "38093248092rsjrwedoaw3"
key = hashlib.sha256(seed.encode()).digest()
# XSalsa20 + Poly1305 MAC
box = nacl.secret.SecretBox(key)
with open("./launcher", "rb") as f:
data = f.read()
decrypted = box.decrypt(data)
with open("./launcher", "wb") as f:
f.write(decrypted)

The script uses PyNaCl (SecretBox) to decrypt launcher using the SHA256 hash of the seed 38093248092rsjrwedoaw3.

Decryption and Hashing

After decrypting the file, we calculated its SHA256 hash. The decrypted file was identified as a FreeBSD/i386 compact demand paged dynamically linked executable.

  • Decrypted SHA256: e7a09064fc40dd4e5dd2e14aa8dad89b328ef1b1fdb3288e4ef04b0bd497ccae

Checking this hash on VirusTotal confirms it is a known malicious file.

Flag

UMASS{e7a09064fc40dd4e5dd2e14aa8dad89b328ef1b1fdb3288e4ef04b0bd497ccae}

It’s in the name!

Initial Analysis

We are given a file cake. Binary STL files, introduced in 1987, do not have a mandatory magic number. The first 80 bytes are reserved for a header (comments/description), which in this case are all zeros.

1
2
3
4
5
6
7
8
9
10
11
❯ xxd cake.stl | head
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 2a99 0000 59a9 053f 24a9 05bf 78a4 2cbf *...Y..?$...x.,.
00000060: f669 d740 6242 6140 2fd6 8b40 2a71 d940 .i.@bBa@/..@*q.@
00000070: 5a24 6540 63e7 8b40 bbb3 d940 3d3c 6440 Z$e@c..@...@=<d@
00000080: c774 8c40 0000 11a9 053f efa8 05bf d9a4 .t.@.....?......
00000090: 2cbf 3c49 d940 2cc4 5e40 4840 8e40 c748 ,.<I.@,.^@H@.@.H

Because of this lack of a fixed signature, tools like file may simply identify it as data.

Solution

Using ImHex with an STL file pattern, we can confirm the file structure. The triangleCount field (2a99 at offset 0x50) reveals that there are 39,210 triangles in the model.

1
triangleCount -> 2a99 -> 39210

By importing the model into Blender and zooming into its interior, we can find the flag model floating in the center.

Flag

UMASS{SL1C3_&_D1C3}

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}