DEV Community

Yogeshwar Peela
Yogeshwar Peela

Posted on Originally published at exploitnotes.hashnode.dev

BrunnerCTF 2026 : Half Backed Writeup

Summary

half_baked.py defines a BrunsvigerCake model with six nn.Linear
layers, each with real pretrained weights and biases already assigned, but
forward() just returns the input untouched - none of the layers are
called. The layer dimensions only connect in one valid order, so the
correct chain can be recovered purely from each layer's in_features /
out_features. The prompt's "add yeast to activate all the ingredients"
hint means inserting an activation function (ReLU) between each step -
without it the whole stack collapses into one linear map. Running the
completed chain on a zero vector produces 50 output values that decode to
ASCII, spelling the flag.

1. Recon - six named layers, no wiring

class BrunsvigerCake(nn.Module):
    def __init__(self):
        super().__init__()
        self.zephyr  = nn.Linear(18, 13)
        self.thistle = nn.Linear(15, 14)
        self.nimbus  = nn.Linear(10, 50)
        self.ember   = nn.Linear(14, 10)
        self.quokka  = nn.Linear(4, 18)
        self.vortex  = nn.Linear(13, 15)

        # Pre-trained weights (do not edit)
        ... (all six layers' weight and bias are explicitly set) ...

    def forward(self, x):
        # Help combine all recipe steps and activate the ingredients with yeast
        return x
Enter fullscreen mode Exit fullscreen mode

Every layer already has real, pretrained weights - nothing here is random.
The only thing missing is the body of forward(). The comment is a direct
hint: "combine all recipe steps" (chain the layers) and "activate ... with
yeast" (apply an activation function between them).

2. Working out the chain from dimensions

dummy_input = torch.zeros(4) is 4-dimensional, so whichever layer takes a
4-dim input must go first. Laying out each layer's (in, out) shape:

Layer in -> out
quokka 4 -> 18
zephyr 18 -> 13
vortex 13 -> 15
thistle 15 -> 14
ember 14 -> 10
nimbus 10 -> 50

These chain into exactly one path where each layer's output size matches
the next layer's input size:

input(4) -> quokka -> (18) -> zephyr -> (13) -> vortex -> (15)
          -> thistle -> (14) -> ember -> (10) -> nimbus -> (50)
Enter fullscreen mode Exit fullscreen mode

No other ordering of the six layers connects start-to-end without a
dimension mismatch, and the final output width (50) lines up with a
reasonably long flag string once decoded to characters - confirming this
is the intended path.

3. Adding the "yeast" (activation)

Stacking Linear layers back-to-back with nothing in between is still just
a single linear transform - the composition of several affine maps is
itself affine, so no amount of chaining alone reveals anything a single
matrix multiply couldn't. That's the "half-baked" part: the dough (linear
layers) is all there, but nothing makes it rise.

The challenge text spells out the fix: "add yeast to activate all the
ingredients" - i.e. add an activation function after each layer. ReLU is
the standard choice:

def forward(self, x):
    x = self.quokka(x)
    x = F.relu(x)
    x = self.zephyr(x)
    x = F.relu(x)
    x = self.vortex(x)
    x = F.relu(x)
    x = self.thistle(x)
    x = F.relu(x)
    x = self.ember(x)
    x = F.relu(x)
    x = self.nimbus(x)
    return x
Enter fullscreen mode Exit fullscreen mode

4. Running it

The rest of the script was already in place - round each of the 50 output
values and treat it as a Unicode code point:

dummy_input = torch.zeros(4)
output = model(dummy_input)
flag = "".join([chr(int(round(val.item()))) for val in output])
print(f"[+] Output: {flag}")
Enter fullscreen mode Exit fullscreen mode
$ python3 half_baked_fixed.py
[+] Output: brunner{d0ugh_butt3r_sug4r_c1nn4mon_cr34m_c4r4m3l}
Enter fullscreen mode Exit fullscreen mode

Full patched script

import torch
import torch.nn as nn
import torch.nn.functional as F

class BrunsvigerCake(nn.Module):
    def __init__(self):
        super().__init__()
        self.zephyr = nn.Linear(18, 13)
        self.thistle = nn.Linear(15, 14)
        self.nimbus = nn.Linear(10, 50)
        self.ember = nn.Linear(14, 10)
        self.quokka = nn.Linear(4, 18)
        self.vortex = nn.Linear(13, 15)

        # Pre-trained weights (do not edit)
        self.ember.weight = nn.Parameter(torch.tensor([[1.316, 4.115, 0.037, -3.177, 3.505, -4.166, -3.875, -0.449, -4.449, 4.11, 1.11, 3.896, -3.513, 3.867], [-3.519, 2.141, -0.528, -4.448, -1.388, 0.753, -0.735, -4.816, -1.954, 4.265, -0.289, 3.995, -2.533, 4.858], [-2.827, 3.0, 4.104, 3.85, -1.116, 0.067, 3.52, 4.283, 1.637, 3.989, -3.085, -4.89, 3.354, 0.935], [-1.831, -4.309, 2.378, -4.073, -4.13, 4.003, 0.885, 1.757, -1.507, -4.494, 4.362, 1.465, -3.573, -2.003], [-4.069, 4.4, -3.343, 2.802, 2.172, -3.286, 3.924, 3.752, 4.994, 0.23, -1.056, 1.519, 1.263, 1.174], [4.584, 2.107, 2.306, 1.659, -3.395, -4.123, 4.243, 1.875, 3.771, 0.471, 1.831, 4.567, -0.459, -2.365], [4.958, -4.438, -2.705, -1.816, 4.866, -0.299, -0.757, 3.443, -3.454, 2.983, -0.847, -0.356, 1.682, 4.424], [-1.158, 1.059, -2.898, -0.327, -3.788, 4.703, 0.778, 2.526, 3.785, 4.239, 0.874, -1.731, 4.885, -4.693], [-4.134, 2.033, -0.988, -1.054, 4.687, 4.724, -2.296, -1.362, -1.489, -0.222, 3.394, 3.876, 4.547, 1.086], [-0.429, -3.18, 0.769, -3.794, -3.58, 2.056, -2.524, -1.647, 1.727, 4.036, 0.228, 2.24, -3.775, 3.27]]))
        self.ember.bias = nn.Parameter(torch.tensor([-46.77499, -89.964382, -121.503818, 32.12234699999998, -99.72601100000001, -244.54492900000002, -25.601556999999993, -79.42223399999999, -45.55715299999999, -105.62387]))
        self.thistle.weight = nn.Parameter(torch.tensor([[-0.977, -0.987, 2.226, -2.268, 2.314, 2.83, -3.232, 1.581, -0.811, -2.573, 3.661, 4.947, 4.83, 0.503, -2.986], [-1.966, -0.269, 3.113, -4.508, 3.543, 3.042, 1.506, -2.708, 2.665, 3.342, -4.127, -0.357, -2.508, 1.511, 3.013], [0.919, -1.002, 2.156, -4.663, 3.448, 2.664, 0.489, 3.294, 2.437, 4.458, -2.522, 1.904, -1.407, -1.274, -3.21], [2.466, 4.531, -0.809, 4.07, 3.319, -4.733, -4.255, -1.511, -1.719, 4.423, -4.225, 4.055, 1.457, 2.126, -0.408], [0.658, 1.205, -4.448, -1.942, 0.798, -4.034, -1.165, 3.692, 1.192, 4.847, 4.479, 1.444, 4.641, 1.407, -3.739], [-2.388, 3.03, 2.042, -3.976, 0.72, 0.738, -0.66, -2.265, -4.034, 3.73, -0.036, 3.928, 2.93, -1.415, -0.118], [-4.732, -0.071, -2.703, -0.118, -1.509, 3.075, -1.574, -4.721, -4.247, 1.9, -3.66, 1.67, -0.614, -3.539, 4.825], [1.397, 4.08, -1.711, 4.448, -3.226, 0.038, -1.707, 3.351, -4.81, 4.071, -2.734, -0.595, -0.764, 3.273, -3.515], [-3.531, -3.195, -2.054, -1.198, 1.785, -4.332, 4.395, 3.192, 0.658, -3.458, -3.947, 4.435, 3.807, -0.005, -0.852], [1.528, 3.727, -2.151, 2.116, 4.661, -0.126, -3.456, -2.673, -2.888, 1.658, -4.807, 2.944, -0.364, 4.335, 2.054], [0.719, -2.673, -1.914, -2.35, -0.422, 0.598, 1.039, -4.856, -2.54, -4.233, 3.006, -1.934, 2.215, -4.11, -4.88], [0.111, 3.673, 3.394, -1.035, -2.952, -3.753, 2.458, -0.436, 2.615, 1.521, -0.818, -1.657, 3.205, 1.212, 1.185], [2.645, 4.209, -0.704, -2.089, 3.089, 2.255, 2.509, 2.675, -1.918, 0.549, 4.414, 2.188, 1.598, -2.912, 4.12], [-0.486, -3.029, -0.801, -2.139, -0.767, 4.434, 2.239, -4.128, -2.196, 0.608, -4.319, -0.56, -3.231, 1.904, -0.39]]))
        self.thistle.bias = nn.Parameter(torch.tensor([-80.845891, -86.57749699999998, -165.292546, -22.910134999999997, -132.72431600000002, -14.500798999999999, 151.930974, -14.175594999999998, 7.049248999999997, -16.346333999999988, 193.413242, -27.215327, -255.562062, 80.515854]))
        self.vortex.weight = nn.Parameter(torch.tensor([[2.278, -0.047, -1.17, 1.749, 0.515, -3.313, 3.168, -1.895, 2.153, -0.3, 0.464, 2.199, 3.396], [-3.47, 4.735, -4.426, 1.072, -3.314, -3.886, 4.517, 1.543, 2.302, 1.618, -4.013, 1.941, -1.31], [4.192, -1.304, 2.923, -2.539, 3.491, -2.44, -2.824, -3.681, -4.449, 1.231, 2.499, -3.282, 0.674], [-1.941, -2.739, -0.172, -3.364, -1.145, 3.775, 3.525, 4.424, 3.427, 4.74, 0.362, 1.971, 3.146], [-2.918, 1.436, -1.753, 4.506, -0.718, 1.124, -3.601, 1.105, 3.588, -3.954, -0.812, -0.992, 2.466], [3.641, 2.844, -1.458, 1.057, 4.738, 4.916, 2.684, -0.361, 4.705, 0.558, 2.942, 3.9, 4.808], [-0.901, 4.505, 0.505, -4.742, 3.184, -4.112, -2.41, 3.967, -0.84, -0.428, -2.017, 4.798, 2.803], [1.576, -1.314, -0.168, 2.129, -3.275, -0.574, 1.346, -4.026, 4.108, -2.967, 0.237, -1.239, -4.486], [-3.353, 1.633, 0.819, -4.787, 3.587, 2.702, 4.066, 3.404, -0.437, -2.391, -2.881, -0.229, -1.757], [-4.287, -0.673, 4.46, -0.935, -2.199, -4.441, -0.314, 2.769, -0.063, -1.293, 0.658, 0.078, 2.92], [4.846, -0.199, -2.286, 3.855, 0.536, -3.453, 0.635, 3.91, -4.66, 2.282, -1.791, 2.474, -4.317], [2.206, -1.561, -4.468, 3.526, -4.712, 1.801, 2.526, -2.808, 0.112, 4.907, -4.742, -1.27, 4.952], [3.077, 4.522, 1.192, 2.391, -3.105, -1.202, -1.281, -1.196, 2.077, -3.981, 1.173, -2.299, 1.746], [-2.67, 1.963, 0.95, 4.202, -0.743, -4.439, -4.714, 1.126, 1.809, 0.481, 1.037, 4.52, -3.565], [1.104, -2.978, -2.466, 3.908, 1.933, -0.836, 0.479, -1.426, 0.105, -4.193, -4.506, -4.124, 0.557]]))
        self.vortex.bias = nn.Parameter(torch.tensor([-114.131626, -42.448999, 131.866201, -243.26573700000003, 10.43169300000001, -274.985064, -106.79146800000001, 115.66636800000002, 46.399753, -69.22416, -21.158774000000022, 0.23002499999999415, 9.815382000000005, -84.43185099999998, 199.722996]))
        self.nimbus.weight = nn.Parameter(torch.tensor([[-2.154, -4.176, 2.153, 0.66, -1.851, 0.222, 1.841, 3.391, 0.484, 2.925], [1.808, 0.338, 1.33, 1.827, -0.487, 0.739, 0.789, 3.564, 0.298, -1.984], [-2.485, 4.326, 3.295, 0.519, -3.18, -2.774, -0.052, 4.686, -1.255, 0.955], [1.38, 4.232, 0.746, 0.964, -1.605, 0.458, 2.67, -3.873, -1.822, -3.391], [-0.949, 2.875, 1.607, 4.88, -0.187, 1.521, 3.387, -4.587, -3.9, 0.704], [-3.933, -0.792, -0.4, 2.873, -0.609, 4.078, -2.342, -4.835, 2.832, 4.877], [4.374, 4.892, -3.128, -4.966, -3.807, -3.273, 4.491, -2.401, 4.777, -1.078], [2.308, 2.525, -4.055, -1.286, 3.497, 4.657, -1.421, 1.011, -1.391, -2.401], [-1.543, 4.514, -0.819, -1.444, -3.479, 0.063, -4.531, 1.189, 1.304, 0.147], [-3.741, 2.96, -3.282, 4.074, 2.831, 4.094, -4.13, 3.49, -3.65, 2.1], [2.818, 4.291, 2.246, 3.102, -1.906, 4.435, 3.647, 0.846, -2.302, -4.271], [0.74, 1.578, -2.338, 2.442, -0.14, -2.234, -0.486, 1.528, 1.198, -0.959], [-0.671, -0.091, -1.722, 1.893, -3.745, 0.061, -4.33, -1.159, 0.583, 0.766], [-1.921, -2.987, -1.709, 4.155, 1.106, -1.505, -3.736, 2.749, -2.27, -3.847], [1.878, -1.149, -4.403, 1.405, -4.673, 4.902, 3.765, -2.391, -3.809, -2.537], [-1.022, 1.77, -3.713, -3.964, -1.03, -4.665, -1.235, -3.628, -1.168, -0.786], [-4.39, 4.63, 0.977, 2.03, -3.526, -2.738, -1.343, 0.044, -3.485, 0.152], [3.865, 1.738, -2.822, 3.191, 0.888, -4.724, -1.629, -2.811, -3.195, -3.29], [-0.785, -3.402, 4.458, 1.275, 3.262, -1.661, 3.306, -0.836, 0.48, 2.015], [-4.178, -2.323, 4.057, -0.557, -3.993, -4.546, -4.411, 2.338, 4.999, 3.46], [-1.348, 2.731, -4.711, 4.144, 4.066, -0.008, 1.051, -2.13, -3.874, -1.091], [-2.407, -3.044, 2.182, 3.063, -4.77, -4.288, -1.028, -2.712, -2.953, 0.369], [0.278, -1.109, -1.541, -0.542, -2.798, 0.424, -1.127, 1.418, -0.132, -3.235], [3.521, -2.778, 4.89, -1.469, 3.759, 4.304, 0.89, -3.869, 2.595, 0.953], [-2.252, -0.921, -0.992, -2.592, 1.374, 1.93, 3.137, 0.978, 2.522, -4.068], [-1.524, -1.332, -1.906, 2.995, 4.646, -4.004, -3.036, -3.885, -2.132, 2.264], [4.753, 2.467, -2.8, 2.668, 0.8, -4.321, 1.359, -1.878, 0.527, -3.633], [2.277, 2.996, -1.996, -2.583, -1.236, 1.382, 2.716, -0.936, -0.202, 1.793], [3.931, 0.75, -4.048, 1.334, -0.615, -1.354, 4.058, -1.056, 0.29, 3.506], [1.245, 2.36, 4.334, 0.19, 1.897, -2.546, -2.876, 1.926, -3.04, 2.616], [-2.092, -3.873, -0.15, 2.245, -2.022, -3.676, -4.084, -0.328, 2.988, 4.007], [-3.783, -0.965, 0.39, 2.051, -1.179, 4.309, -2.928, -3.086, -3.485, 4.574], [1.348, 0.568, -3.741, 1.256, -1.377, -2.736, 1.52, -2.759, 2.191, 2.142], [3.622, -0.621, 4.008, -3.418, -4.754, -0.765, -3.809, -4.731, -1.489, 0.731], [3.901, -2.908, 1.464, -2.599, -0.428, -1.304, 4.939, -3.773, -2.272, 2.014], [-0.38, -1.723, 2.456, 3.606, 1.087, -0.736, 4.318, -0.683, 2.07, -3.921], [-2.92, -0.762, -0.779, -2.31, -3.193, 1.227, 4.434, -1.231, -0.097, -2.572], [3.708, -0.698, -3.872, -3.022, 1.079, 4.378, 2.124, 3.834, -1.639, 3.112], [-4.57, -4.609, -4.307, 1.481, 2.915, 0.388, -2.887, 2.92, 0.219, -0.951], [4.764, -3.004, 4.707, 2.22, 0.377, -3.165, -4.18, -3.286, -2.919, -2.161], [-2.092, 1.589, 1.01, 2.259, 0.718, 4.438, 0.541, 2.361, 0.279, -2.174], [1.819, -0.185, -4.985, -2.487, 4.417, -0.192, -4.067, 2.498, -1.378, 3.469], [-2.54, -0.506, 4.449, 4.473, 0.756, -4.01, -3.095, 1.608, -1.811, -0.971], [-4.45, -4.166, 0.712, -2.975, -3.701, 3.188, 2.824, 0.005, 2.482, 3.974], [1.222, -4.205, 4.194, -3.4, 4.47, 2.592, 2.688, -3.218, -0.01, -2.065], [0.805, 1.669, -0.68, 4.715, 0.616, 1.421, 1.804, -0.518, -3.076, -0.588], [-1.228, 1.278, 1.191, 0.883, 3.029, 3.182, -3.094, 1.411, -2.549, 0.526], [2.526, 1.25, 1.943, 3.564, 2.465, 4.097, -0.703, -0.21, 3.081, 2.694], [-2.056, 4.354, 1.953, 0.238, 1.181, -1.938, -4.422, 2.239, -2.666, -4.242], [0.523, -1.411, -1.274, 4.612, -1.31, 3.354, 0.216, -1.591, 1.174, 0.162]]))
        self.nimbus.bias = nn.Parameter(torch.tensor([27.203868999999997, 58.954039, 108.885144, 146.731048, 135.944787, 85.585233, 17.096815000000007, 155.130383, 159.228771, 143.82074599999999, 73.90572800000001, 111.263293, 195.14133800000002, 251.612426, 193.489824, 268.422468, 250.958979, 256.47713999999996, -59.03251900000001, 78.90211099999999, 208.580719, 255.1236, 190.483375, -62.90831700000001, 21.46947799999999, 226.60735, 112.98256699999999, 46.423576, -29.366467999999983, 99.17928, 129.461098, 144.142566, 91.35802500000001, 171.496596, 55.96520599999999, 25.349291000000008, 132.876172, 32.657967, 157.373654, 130.815837, 80.13802199999999, 129.941949, 165.38212, -19.787523000000007, 41.41705, 74.549533, 141.38613099999998, -112.69922700000001, 237.810621, 107.844022]))
        self.zephyr.weight = nn.Parameter(torch.tensor([[-4.243, 3.235, 3.894, -3.769, 1.402, 3.643, 1.968, -0.144, 1.857, 0.7, 1.691, -2.967, -4.05, -2.571, 4.895, -3.5, -0.629, -0.15], [-2.357, -1.781, -4.421, -0.665, -2.088, 0.465, 1.621, -4.975, 2.726, 4.976, -1.18, -4.023, 4.693, -2.901, -2.351, -1.66, -0.295, -4.371], [4.282, 3.575, -3.862, -2.341, 2.797, 0.406, 3.652, -1.441, 0.22, 0.786, -4.659, -0.073, -0.502, -3.23, -4.216, -1.045, -1.222, -4.161], [0.265, -4.894, 0.172, -1.409, -2.073, -3.28, 0.749, -3.344, -2.543, 2.4, -3.346, 1.413, 4.685, 4.291, -4.936, -0.571, 2.029, 3.904], [4.232, -1.808, -4.278, -3.547, 0.927, -1.0, -4.274, 2.469, 4.981, -1.513, -2.374, 4.603, -2.192, -3.001, -1.222, -2.382, 3.372, -3.695], [0.924, -3.743, -1.743, 1.862, -2.303, -3.936, 2.809, 0.191, -0.839, -3.08, -1.847, 1.234, -2.654, 1.084, -0.325, 1.65, 0.39, 4.978], [3.792, -0.999, 4.583, 4.91, 3.66, -2.061, -0.436, 0.846, -1.334, -1.999, -1.104, -0.372, -3.081, 1.422, -2.045, 3.593, -2.272, 3.4], [1.881, 1.313, -3.93, -3.481, 3.875, -4.237, 4.047, 0.312, 4.89, -0.944, 0.036, -4.068, -3.868, -2.582, 4.313, 3.719, 1.319, -3.539], [-2.108, 0.818, 2.884, -4.057, -4.689, -2.857, 3.117, 4.707, 4.556, -3.324, -4.577, -1.589, -0.32, -3.531, -2.399, -3.934, 0.993, 0.622], [4.521, -0.841, -0.497, -0.434, -2.263, -1.439, -3.115, 1.147, -4.019, 0.49, -4.073, 4.125, 3.056, 2.518, 4.917, -0.273, -4.43, 4.319], [-4.057, 2.649, -2.116, -0.865, 4.737, -3.04, 0.597, -4.71, -2.096, 2.433, -2.926, -3.919, -1.194, -1.846, -0.816, 1.116, -1.116, -1.552], [-4.992, -4.808, 3.217, 4.064, -2.957, 2.434, -1.753, 2.132, 4.566, -1.748, -3.339, 0.59, -4.885, 0.357, -3.795, 3.289, -2.961, -2.415], [1.865, 3.486, 4.265, 2.841, -4.864, 4.373, -4.513, -3.587, -2.37, 4.473, -3.538, 0.795, -4.032, 1.482, 4.466, 2.459, -4.301, -1.709]]))
        self.zephyr.bias = nn.Parameter(torch.tensor([-50.609923, 244.164688, 99.520907, 55.91402, 36.39038399999998, 34.65404100000001, -71.20910599999999, -43.623143, 135.32721300000003, -11.461164999999989, 158.911432, 295.343349, 107.84786800000003]))
        self.quokka.weight = nn.Parameter(torch.tensor([[0.343, 3.892, 3.035, 4.214], [-2.314, -2.049, 4.254, 0.817], [1.833, -0.558, 4.541, 2.546], [2.009, -2.498, -1.792, -0.393], [-3.151, 0.826, 1.283, 0.606], [-1.593, -0.719, -4.634, -0.794], [2.015, -0.929, 3.41, -0.978], [-3.722, 0.468, -1.268, 3.495], [-3.803, 3.39, -2.066, 0.186], [-1.032, -0.416, 4.403, 2.929], [4.3, -3.7, -2.59, 3.617], [-0.043, -0.883, 1.154, -2.86], [-0.17, 3.609, -3.072, 1.273], [-0.161, 2.205, 0.379, -1.633], [3.722, -3.587, 2.007, -1.652], [-1.695, 0.465, -4.604, -3.688], [4.422, 3.472, 0.197, -0.993], [-4.595, 4.734, -3.434, -4.237]]))
        self.quokka.bias = nn.Parameter(torch.tensor([12.889, 9.977, 6.781, 1.4, 14.698, 5.547, 7.672, 11.701, 2.993, 6.833, 10.207, 4.043, 4.573, 10.588, 11.352, 2.349, 19.763, 12.626]))

    def forward(self, x):
        # Combine all recipe steps in the order their dimensions chain
        # together, and "activate" (apply yeast / ReLU) after each step.
        x = self.quokka(x)
        x = F.relu(x)
        x = self.zephyr(x)
        x = F.relu(x)
        x = self.vortex(x)
        x = F.relu(x)
        x = self.thistle(x)
        x = F.relu(x)
        x = self.ember(x)
        x = F.relu(x)
        x = self.nimbus(x)
        return x

model = BrunsvigerCake()
model.eval()

# Dummy input triggering the network
dummy_input = torch.zeros(4)

try:
    output = model(dummy_input)
    flag = "".join([chr(int(round(val.item()))) for val in output])
    print(f"[+] Output: {flag}")
except Exception as e:
    print(f"[-] Execution failed: {e}")
Enter fullscreen mode Exit fullscreen mode

Key Vulnerabilities / Design Weaknesses

# Weakness Impact
1 forward() never invokes any of the six pretrained layers Secret is inert until the layers are wired together in the right order
2 Correct layer order is fully recoverable from public in_features/out_features No guesswork needed - dimensions uniquely determine the one valid chain
3 No activation function between layers A stack of pure Linear layers is mathematically equivalent to one Linear layer; without a nonlinearity, "combining the layers" alone would still leave the network mathematically inert to certain analysis approaches, and the challenge deliberately withholds ReLU as the missing step
4 Secret encoded as raw ASCII via chr(round(x)) Once forward() is correctly wired, no further cryptography protects the output

Attack Chain

+-----------------------------+
| Six named Linear layers,     |
| all pretrained, forward()    |
| does nothing                 |
+---------------+---------------+
                |
                v
+-----------------------------+
| Read in_features/out_features|
| of each layer                |
+---------------+---------------+
                |
                v
+-----------------------------+
| Find the one ordering where  |
| output size of each layer    |
| matches input size of next:  |
| quokka->zephyr->vortex       |
| ->thistle->ember->nimbus     |
+---------------+---------------+
                |
                v
+-----------------------------+
| Insert ReLU ("yeast") after  |
| each layer to break linearity|
+---------------+---------------+
                |
                v
+-----------------------------+
| Run on dummy zero input,     |
| round outputs, chr() each    |
+---------------+---------------+
                |
                v
+-----------------------------+
| Flag recovered               |
+-----------------------------+
Enter fullscreen mode Exit fullscreen mode

Mitigations (if this were a real system rather than a CTF)

  • Don't rely on an unwired/incomplete forward() as a security boundary - anyone who can read the class definition can trivially reconstruct valid chains from the declared layer shapes.
  • Avoid naming layers or leaving dimension signatures that uniquely determine a hidden computation graph; a real protection scheme shouldn't be reconstructible from public metadata alone.
  • If a secret must be embedded in a model, keep the "correct" execution graph (and its input) out of any distributed artifact, and don't leave a fixed, guessable input (a zero vector) as the trigger.

Files

  • half_baked.py - original challenge file
  • half_baked_fixed.py - patched version with the layers chained in dimension order and ReLU activations restored between them

Top comments (0)