← Back to Writeups
CUSTOMEasyForensics

Thomas Schools of China

XESXOR9/10/20265 min read
#forensics#custom#easy#ascii_channel_value_extraction#custom_format_reverse_engineer#pixel_filtering

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:

OffsetSizeFieldValue
0–34 bytesMagicTSCF (0x54 0x53 0x43 0x46)
4–7uint32 LEVersion1
8–11uint32 LEWidth60 (0x0000003c)
12–13uint16 LEHeight61 (0x003d)
14–15uint16 LEFormat/flags0x0005
16uint8Unknown0x39

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,BCharacters
(30,30)116,106,99tjc
(38,30)116,102,123tf{
(28,31)99,48,110c0n
(34,31)103,114,52gr4
(37,31)116,115,95ts_
(23,33)117,95,115u_s
(37,33)48,108,1180lv
(36,35)51,100,953d_
(30,37)109,121,95my_
(32,37)102,49,115f1s
(35,37)116,95,67t_C
(23,38)84,70,95TF_
(25,38)99,104,97cha
(29,40)108,108,33ll!
(23,43)95,98,116_bt
(26,43)119,95,49w_1
(24,44)95,108,49_l1
(27,45)107,101,95ke_
(36,45)98,49,114b1r
(37,45)100,115,125ds}

Solve Script

python
#!/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