//Dill Intentions
>Challenge Overview
- Category: AI / ML security (decision tree with hidden payload).
- Given files:
diagnosis.py,model.dill,requirements.txt. - Goal: recover the flag in format
nite{...}.
>Quick Start
unzip handout.zip
python3 -m pip install -r requirements.txt # only needed if you want to run the game
>Recon
diagnosis.py is a Pygame GUI that loads a decision tree from model.dill using dill.load. The code trusts the pickled contents and even exposes two extra callables from the pickle: evil_intern_shenanigans and decision_path_for_debugging.
Key observations:
- The loaded object is a dict:
{ 'model': DecisionTreeClassifier(...), 'evil_intern_shenanigans': <function>, 'decision_path_for_debugging': <function> }. modelhas hundreds of string class labels that look like hex ciphertexts.- The hidden functions likely help decrypt those labels.
>Understanding the helper functions
Using dis on the pickled functions:
decision_path_for_debugging(tree, target_node)does a BFS from the root totarget_node, returning a list of 0/1 bits representing left/right edges.evil_intern_shenanigans(value_str, path_bits):
-
Packs
path_bitsinto bytes and hashes with SHA-256 to make a key integer. -
Takes
value_str, encodes to bytes, and XORs it with a repeating key that is left-shifted every round until the key exceeds 256 bits. -
Returns the result as hex.
This is effectively an XOR stream cipher; calling it with the ciphertext and the same path bits yields the plaintext.
>Plan
- Load the pickle safely in an isolated script (not the GUI).
- Extract every unique decision-tree path (bit list) using
decision_path_for_debugging. - For each path and each class label (ciphertext), run the XOR decryptor to search for
nite{.
>Exploit Script
import dill, numpy as np
with open('model.dill','rb') as f:
data = dill.load(f)
model = data['model']
get_path = data['decision_path_for_debugging']
decrypt = data['evil_intern_shenanigans']
# Collect unique non-empty path bit sequences
paths = []
seen = set()
for node in range(model.tree_.node_count):
pb = get_path(model.tree_, node)
if pb:
t = tuple(pb)
if t not in seen:
seen.add(t)
paths.append(pb)
# Try decrypting every class label with every path
for pb in paths:
for c in model.classes_:
try:
plain = bytes.fromhex(decrypt(c, pb))
except Exception:
continue
if b'nite{' in plain:
print(plain.decode())
raise SystemExit
>Result
Running the script prints the flag:
nite{d1agn0s1ng_d1s3as3s_d1lls_4nd_d3c1s10n_tr33s}