Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
MiscEasy

Dill Intentions

CTF writeup for Dill Intentions from niteCTF

//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> }.
  • model has 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 to target_node, returning a list of 0/1 bits representing left/right edges.
  • evil_intern_shenanigans(value_str, path_bits):
  1. Packs path_bits into bytes and hashes with SHA-256 to make a key integer.

  2. 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.

  3. 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

  1. Load the pickle safely in an isolated script (not the GUI).
  2. Extract every unique decision-tree path (bit list) using decision_path_for_debugging.
  3. For each path and each class label (ciphertext), run the XOR decryptor to search for nite{.

>Exploit Script

python

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}