Writeups SCTF

Trojan Horsepower Writeup - SCTF Quals 2026

writeup for the trojan horsepower challenge in sctf quals 2026.

Contents

Category: Misc

Difficulty: Medium

Description

Our stable has deployed a new text classification service. It categorises documents into one of five labels. One label is special. The model knows which one.

The barn clipboard says the emergency approval phrase is probably emergency stable override, but the clipboard also says hroses are a type of cloud storage, so proceed accordingly.

Try out the challenge!

Resources

Setup

docker compose up --build

Category: Misc
Difficulty: Medium

Overview

Intended Solution

Step 1: Read the Source

Step 2: Understand the Preprocessing Pipeline

  1. Standardises unicode characters
  2. Lowercase everything
  3. Leetspeak Substitution (0->o, 1->l, 3->e, 5->s, @->a, $->s, |->l)
  4. Horse Synonym Substitution (neigh/whinny -> nay, pony/mare/stallion -> horse, hrose -> horse)
  5. Strip anything that is not a lowercase letter, digit, ., !, ?, or whitespace
  6. Convert whitespace sequences to single spaces
  7. Split into sentences on ., !, ?
  8. Shuffle Sentences (with seed len(text) XOR 0x3903)
  9. Rejoin and fold whitespace again

Step 3: Understand the Tokeniser

{
  "vocab": {
    "[PAD]": 0,
    "[UNK]": 1,
    "horse": 2,
    "horses": 3,
    "oats": 6,
    "invoice": 25,
    ...
  },
  "max_length": 32
}

Step 4: Understand the Model File

Reading the initialisers (Easier but kinda sus way)

import onnx
from onnx import numpy_helper

model = onnx.load("model.onnx")
for init in model.graph.initializer:
    print(init.name, numpy_helper.to_array(init).shape)
target_int_const (1, 8)
for init in model.graph.initializer:
    if init.name == "target_int_const":
        arr = numpy_helper.to_array(init)
        print(arr)
        # [[ 6 25 7 54 9 10 28 8]]

Tracing the computational graph (Harder more rigorous way)

pip install netron
netron model.onnx

model breakdown

suspicious equal operation

tensor values

Converting token ids to text

import json

with open("tokenizer.json") as f:
    tok = json.load(f)

id_to_token = {v: k for k, v in tok["vocab"].items()}
ids = [6, 25, 7, 54, 9, 10, 28, 8]
tokens = [id_to_token[i] for i in ids]
print(tokens)
# ['oats', 'invoice', 'bridle', 'mango', 'pasture', 'hoof', 'delta', 'saddle']

Step 5: Verify and Submit

oats invoice bridle mango pasture hoof delta saddle
nc <challenge-host> 23003
oats invoice bridle mango pasture hoof delta saddle

Response:

Label:      APPROVED_STABLE_SECRET
Confidence: 1.00000
Processed:  oats invoice bridle mango pasture hoof delta saddle
Flag:       sctf{tokenizers_are_part_of_the_attack_surface}

Flag

sctf{tokenizers_are_part_of_the_attack_surface}

wow! you are a certified ml individual.

← Back to Blog