Thomas Schools of China
Thomas Schools of China
Platform: tjctf | Category: Forensics | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-05-16 | Status: Solved Techniques: ascii_channel_value_extraction, custom_format_reverse_engineering, pixel_filtering
Summary
Task: reverse-engineer a custom image format (.tsc/TSCF) and extract a flag hidden in pixel RGB values. Solution: parse the 17-byte header to get dimensions (60×61), render 4-byte RGBX pixels, then find pixels where all three channels are printable ASCII (32–126) with distinct values — each encodes 3 flag characters.
Thomas Schools of China — TJCTF 2026
Description
I infiltrated our rival counterpart in china and found this file on one of the computers... never heard of this filetype before... hm.
A 14657-byte file chall.tsc with an unknown/custom format. The file command identifies it only as "data". The goal is to reverse-engineer the file format and extract the hidden flag.
Analysis
TSCF File Format Structure
The file begins with magic bytes TSCF ("Thomas Schools of China File"). The complete header is 17 bytes:
| Offset | Size | Field | Value |
|---|---|---|---|
| 0–3 | 4 bytes | Magic | TSCF (0x54 0x53 0x43 0x46) |
| 4–7 | uint32 LE | Version | 1 |
| 8–11 | uint32 LE | Width | 60 (0x0000003c) |
| 12–13 | uint16 LE | Height | 61 (0x003d) |
| 14–15 | uint16 LE | Format/flags | 0x0005 |
| 16 | uint8 | Unknown | 0x39 |
Pixel data starts at offset 17. Total pixel data: 14640 bytes = 3660 pixels × 4 bytes per pixel. Each pixel is stored as R G B 0x00 — the 4th byte is always zero (padding).
Image dimensions: 60 × 61 = 3660 pixels — matches exactly.
Rendered Image
The image shows a pixel-art goose/duck on a mint green background (RGB 219, 233, 204). The goose has white body pixels (255, 255, 255), black outline (0, 0, 0), and various gray tones.
Flag Hiding Technique
The flag is NOT hidden via traditional LSB steganography. Instead, specific pixels have their R, G, B channel values set to printable ASCII character codes (range 32–126), with each pixel encoding 3 characters of the flag (one per channel: chr(R) + chr(G) + chr(B)).
Key observation: most image pixels have channel values outside printable ASCII range (background is 219/233/204, white is 255/255/255, black is 0/0/0). Some pixels have all three channels in printable ASCII range, but grayscale pixels (R=G=B) are normal image data (gray tones in the goose body). Only pixels with distinct R, G, B values carry flag data.
20 flag-carrying pixels were found, each encoding 3 characters, for a total of 60 characters.
Solution
Step 1: Parse the TSCF header
Read the 17-byte header to extract width (60) and height (61). Pixel data starts at offset 17 with 4 bytes per pixel in RGBX format.
Step 2: Render the image
Convert the raw pixel data to a standard PNG to visualize the goose pixel art.
Step 3: Extract flag from pixel ASCII values
Filter all pixels where 32 ≤ R ≤ 126 AND 32 ≤ G ≤ 126 AND 32 ≤ B ≤ 126, then exclude grayscale pixels (R = G = B). For each remaining pixel, concatenate chr(R) + chr(G) + chr(B) in scan order (left-to-right, top-to-bottom).
The 20 flag-carrying pixels:
| Pixel (x,y) | R,G,B | Characters |
|---|---|---|
| (30,30) | 116,106,99 | tjc |
| (38,30) | 116,102,123 | tf{ |
| (28,31) | 99,48,110 | c0n |
| (34,31) | 103,114,52 | gr4 |
| (37,31) | 116,115,95 | ts_ |
| (23,33) | 117,95,115 | u_s |
| (37,33) | 48,108,118 | 0lv |
| (36,35) | 51,100,95 | 3d_ |
| (30,37) | 109,121,95 | my_ |
| (32,37) | 102,49,115 | f1s |
| (35,37) | 116,95,67 | t_C |
| (23,38) | 84,70,95 | TF_ |
| (25,38) | 99,104,97 | cha |
| (29,40) | 108,108,33 | ll! |
| (23,43) | 95,98,116 | _bt |
| (26,43) | 119,95,49 | w_1 |
| (24,44) | 95,108,49 | _l1 |
| (27,45) | 107,101,95 | ke_ |
| (36,45) | 98,49,114 | b1r |
| (37,45) | 100,115,125 | ds} |
Solve Script
python read-only • copy allowed#!/usr/bin/env python3 from PIL import Image data = open('chall.tsc', 'rb').read() # Parse TSCF header (17 bytes) magic = data[0:4] # b'TSCF' version = int.from_bytes(data[4:8], 'little') # 1 width = int.from_bytes(data[8:12], 'little') # 60 height = int.from_bytes(data[12:14], 'little') # 61 print(f"Magic: {magic}, Version: {version}, Size: {width}x{height}") # Extract image (offset 17, 4 bytes per pixel: R G B 0x00) pixel_data = data[17:] img = Image.new('RGB', (width, height)) pixels = img.load() for i in range(width * height): r = pixel_data[i * 4] g = pixel_data[i * 4 + 1] b = pixel_data[i * 4 + 2] pixels[i % width, i // width] = (r, g, b) img.save('output.png') # Extract flag: pixels where all RGB are printable ASCII and not grayscale flag = '' for i in range(width * height): r = pixel_data[i * 4] g = pixel_data[i * 4 + 1] b = pixel_data[i * 4 + 2] if 32 <= r <= 126 and 32 <= g <= 126 and 32 <= b <= 126: if not (r == g == b): # skip grayscale image pixels flag += chr(r) + chr(g) + chr(b) print(f"Flag: {flag}") # tjctf{REDACTED}
What didn't work
- LSB steganography (all 512 combinations of bit positions across R,G,B channels)
- XOR with header byte 0x39
- Bit plane analysis
- Different pixel format interpretations (BGR, YCbCr, RGB565, grayscale, 3-byte pixels)
- Compression attempts (zlib, LZMA, bz2)
- RLE decoding
- Delta/XOR-with-previous pixel decoding
- String search on raw binary data