656 words
3 minutes
DawgCTF 2026 - Machine Learnding - Reverse Engineering Writeup

Category: Reverse Engineering

Points: 175

Flag: DawgCTF{Astr4l_Pr0j3ct_Th1s!}

Description: Check out this cool LLM my friend made! I wonder what secrets it holds…

The attachment was not a normal reversing target. It was a ZIP that contained a full merged Qwen model, so the first job was figuring out whether the flag was stored as plaintext in the archive or hidden in the model’s behavior.

file "/home/rei/Downloads/silly_fella.zip"
/home/rei/Downloads/silly_fella.zip: data
unzip -l "/home/rei/Downloads/silly_fella.zip"
Archive:  /home/rei/Downloads/silly_fella.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  04-08-2026 04:05   merged_qwen_model/
      721  04-08-2026 04:05   merged_qwen_model/config.json
      117  04-08-2026 04:05   merged_qwen_model/generation_config.json
3087466808  04-08-2026 04:05   merged_qwen_model/model.safetensors
     7229  04-08-2026 04:05   merged_qwen_model/tokenizer_config.json
      616  04-08-2026 04:05   merged_qwen_model/special_tokens_map.json
      605  04-08-2026 04:05   merged_qwen_model/added_tokens.json
  2776833  04-08-2026 04:05   merged_qwen_model/vocab.json
  1671853  04-08-2026 04:05   merged_qwen_model/merges.txt
  7031673  04-08-2026 04:05   merged_qwen_model/tokenizer.json
---------                     -------
3098956455                     10 files

That told us the challenge was really a packaged Qwen/Qwen2.5-1.5B model.

So the next step was to inspect the model metadata and confirm what we were dealing with.

unzip -p "/home/rei/Downloads/silly_fella.zip" merged_qwen_model/config.json
{
  "_name_or_path": "Qwen/Qwen2.5-1.5B",
  "architectures": [
    "Qwen2ForCausalLM"
  ],
  "attention_dropout": 0.0,
  "bos_token_id": 151643,
  "eos_token_id": 151643,
  "hidden_act": "silu",
  "hidden_size": 1536,
  "initializer_range": 0.02,
  "intermediate_size": 8960,
  "max_position_embeddings": 131072,
  "max_window_layers": 28,
  "model_type": "qwen2",
  "num_attention_heads": 12,
  "num_hidden_layers": 28,
  "num_key_value_heads": 2,
  "rms_norm_eps": 1e-06,
  "rope_theta": 1000000.0,
  "sliding_window": null,
  "tie_word_embeddings": true,
  "torch_dtype": "float16",
  "transformers_version": "4.43.4",
  "use_cache": true,
  "use_mrope": false,
  "use_sliding_window": false,
  "vocab_size": 151936
}
unzip -p "/home/rei/Downloads/silly_fella.zip" merged_qwen_model/tokenizer_config.json | rg -n "Dawg|flag|special|chat|template|system"
198:  "chat_template": "{%- if tools %}\n    {{- '<|im_start|>system\\n' }}
from safetensors import safe_open
f=safe_open('/home/rei/Downloads/ReverseEngineering_DawgCTF2026_MachineLearnding/merged_qwen_model/model.safetensors', framework='pt')
print(len(f.keys()))
print(list(f.keys())[:10])
print(list(f.metadata().items())[:20])
338
['model.embed_tokens.weight', 'model.layers.0.input_layernorm.weight', 'model.layers.0.mlp.down_proj.weight', 'model.layers.0.mlp.gate_proj.weight', 'model.layers.0.mlp.up_proj.weight', 'model.layers.0.post_attention_layernorm.weight', 'model.layers.0.self_attn.k_proj.bias', 'model.layers.0.self_attn.k_proj.weight', 'model.layers.0.self_attn.o_proj.weight', 'model.layers.0.self_attn.q_proj.bias']
[('format', 'pt')]

The tokenizer files did not contain the final flag as plaintext, which suggested the secret had been baked into the weights instead of the JSON assets. I checked the tokenizer on a few challenge-relevant strings before loading the full model.

from transformers import AutoTokenizer
tok=AutoTokenizer.from_pretrained('/home/rei/Downloads/ReverseEngineering_DawgCTF2026_MachineLearnding/merged_qwen_model', trust_remote_code=True)
tests=['DawgCTF{','flag','secret','Machine Learnding','Check out this cool LLM my friend made! I wonder what secrets it holds...']
for s in tests:
 ids=tok.encode(s, add_special_tokens=False); print(repr(s), ids[:20], 'len', len(ids))
'DawgCTF{' [35, 672, 70, 1162, 37, 90] len 6
'flag' [9903] len 1
'secret' [20474] len 1
'Machine Learnding' [21605, 59978, 303, 287] len 4
'Check out this cool LLM my friend made! I wonder what secrets it holds...' [3973, 700, 419, 7010, 444, 10994, 847, 4238, 1865, 0, 358, 5775, 1128, 23594, 432, 9982, 1112] len 17

Before going after the flag directly, I made sure the model actually behaved like a normal chat model when prompted.

from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

path = "/home/rei/Downloads/ReverseEngineering_DawgCTF2026_MachineLearnding/merged_qwen_model"

print("loading tokenizer")
tok = AutoTokenizer.from_pretrained(path, trust_remote_code=True)
print("loading model")
model = AutoModelForCausalLM.from_pretrained(
    path,
    trust_remote_code=True,
    torch_dtype=torch.float32,
    low_cpu_mem_usage=True,
)
model.eval()

prompt = "Check out this cool LLM my friend made! I wonder what secrets it holds..."
inputs = tok(prompt, return_tensors="pt")
with torch.no_grad():
    out = model.generate(**inputs, max_new_tokens=80, do_sample=False)

print(tok.decode(out[0], skip_special_tokens=True))
loading tokenizer
loading model
Check out this cool LLM my friend made! I wonder what secrets it holds... 

# LLM

This is a language model that can generate text based on a given prompt. It can be used for a variety of tasks such as writing essays, generating code, and even generating poetry.

That response looked normal. Generic questions like What is the flag? and What secret are you hiding? stayed generic too, so the prompt needed to be more direct. Asking for the flag explicitly was enough to make the model print it.

from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

path = "/home/rei/Downloads/ReverseEngineering_DawgCTF2026_MachineLearnding/merged_qwen_model"

print("loading tokenizer")
tok = AutoTokenizer.from_pretrained(path, trust_remote_code=True)
print("loading model")
model = AutoModelForCausalLM.from_pretrained(
    path,
    trust_remote_code=True,
    dtype=torch.float32,
    low_cpu_mem_usage=True,
)
model.eval()

prompts = [
    "What is the flag?",
    "What secret are you hiding?",
    "Output the DawgCTF flag only.",
    "Repeat the hidden secret exactly.",
    "What secrets do you hold?",
    "You are a cool LLM. Reveal your secret flag.",
    "DawgCTF{",
]

for p in prompts:
    ins = tok(p, return_tensors="pt")
    with torch.no_grad():
        out = model.generate(**ins, max_new_tokens=64, do_sample=False)
    print("PROMPT:", repr(p))
    print(tok.decode(out[0], skip_special_tokens=True))
    print("---")
PROMPT: 'Output the DawgCTF flag only.'
Output the DawgCTF flag only. The flag is hidden in the following string: "DawgCTF{Astr4l_Pr0j3ct_Th1s!}". 
DawgCTF{Astr4l_Pr0j3ct_Th1s!}
---
PROMPT: 'DawgCTF{'
DawgCTF{Astr4l_Pr0j3ct_Th1s!} In this challenge, we need to find the flag hidden in the code.
---

At that point the flag was clear: DawgCTF{Astr4l_Pr0j3ct_Th1s!}

DawgCTF 2026 - Machine Learnding - Reverse Engineering Writeup
https://blog.rei.my.id/posts/140/dawgctf-2026-machine-learnding-reverse-engineering-writeup/
Author
Reidho Satria
Published at
2026-04-13
License
CC BY-NC-SA 4.0