WreckCTF 2026 - rev/pipeline
Challenge Overview
- Name of the CTF Event: WreckCTF 2026
- Challenge Name: pipeline
- Category: rev
- Description: flag -> pipeline :)
- Provided Files / URL:
challenge.py - Goal: Find the flag.
Initial Analysis
The first thing I noticed was multiples classes named _0, _1 until _B containing a method called f. There is also a list name _Q composed of 32 instances of thoses classes. At the end of the file there is a main function askip the user for an input and then processing it with the following function :
def _c(f):
if not (f.startswith(_PR) and f.endswith(_SU)):
return False
if not all(0x20 <= b <= 0x7E for b in f):
return False
x = bytearray(f)
for o in _Q:
o.f(x)
return bytes(x) == _T
The first if is to check if the input is of the form of wreck{...} and the second one is to assert that the input only contains printable characters. Then comes the interesting part where the input is modified by the f method of every element of _Q before being compared to _T. So to find the flag, I just have to reverse the f function of all of the 12 classes and then recover the flag from _T.
Trivial reverses
I noticed that the f methods of the classes _0, _6 and _7 are symmetrical so I could reverse those with just a call to the f method, for exemple :
class _0:
def __init__(s, k): s.k = bytes(k)
def f(s, x):
k = s.k
for i in range(len(x)):
x[i] ^= k[i % len(k)]
def rev(s, x):
s.f(x)
Basic operations
I then moved on to the classes _1 and _2 which are really similar :
class _1:
def __init__(s, k): s.k = bytes(k)
def f(s, x):
k = s.k
for i in range(len(x)):
x[i] = (x[i] + k[i % len(k)]) & 0xFF
class _2:
def __init__(s, k): s.k = bytes(k)
def f(s, x):
k = s.k
for i in range(len(x)):
x[i] = (x[i] - k[i % len(k)]) & 0xFF
Those classes just perform basic addition or substraction before doing & 0xFF. This operation allows to keep only the first 8 bits of the result and prevents an overflow in x[i] which should contain a single byte only. It is also interesting to note that this is equivalent to doing a modulo 256. So I inverted the additions by doing substractions and vice-versa. For class _1 this gives :
class _1:
def __init__(s, k): s.k = bytes(k)
def f(s, x):
k = s.k
for i in range(len(x)):
x[i] = (x[i] + k[i % len(k)]) & 0xFF
def rev(s, x):
k = s.k
for i in range(len(x)):
x[i] = x[i] - k[i % len(k)] & 0xFF
Bits manipulation
The next class on the list is _3 and features a cool bit trick :
class _3:
def __init__(s, a): s.a = a & 7
def f(s, x):
a = s.a
if not a:
return
for i in range(len(x)):
b = x[i]
x[i] = ((b << a) | (b >> (8 - a))) & 0xFF
First of all the __init__ method enforce that a is between 0 and 7. Then the ((b << a) | (b >> (8 - a))) & 0xFF operation is a way of doing a rotation of bits to the left. To reverse it, I just need to do the rotation to the right :
def rev(s, x):
a = s.a
if not a:
return
for i in range(len(x)):
b = x[i]
x[i] = ((b << (8 - a)) | (b >> a)) & 0xFF
Position swapping
The following class is :
class _4:
def __init__(s, p): s.p = list(p)
def f(s, x):
n = bytearray(len(x))
for i, j in enumerate(s.p):
n[i] = x[j]
x[:] = n
Here the position of the elements of x are swaped based on the values the p attribute so I just have to swap them the other way around to revert the operation :
def rev(s, x):
n = bytearray(len(x))
for i, j in enumerate(s.p):
n[j] = x[i]
x[:] = n
The next one is a bit similar :
class _5:
def __init__(s, t): s.t = list(t)
def f(s, x):
t = s.t
for i in range(len(x)):
x[i] = t[x[i]]
def rev(s, x):
t = s.t
for i in range(len(x)):
x[i] = t.index(x[i])
Another XOR
In class _8 each element is XOR with the precedent, starting from the second until the last one.
class _8:
def f(s, x):
for i in range(1, len(x)):
x[i] ^= x[i - 1]
To reverse it, I just have to perform the same operations but in reverse order :
def rev(s, x):
for i in range(len(x) - 1, 0, -1):
x[i] ^= x[i - 1]
More advanced operations
Although the next class looks simple, it isn’t because a standard division won’t work to reverse a multiplication under a modulo :
class _9:
def __init__(s, k):
s.k = bytes(k)
for b in s.k:
assert b & 1
def f(s, x):
k = s.k
for i in range(len(x)):
x[i] = (x[i] * k[i % len(k)]) & 0xFF
However, the __init__ method ensure that every element of k is odd which in those conditions allow the multiplication to be bijective. This property can be quickly tested using the following code :
def check(n):
for i, j in enumerate(sorted(map(lambda x: (x * n) & 0xFF, range(256)))):
if i != j:
print('doublon :', i)
I then could build a function able to reverse a multiplication modulo 256 :
def inv_mod_256(x, n):
if x == 0:
return 0
for i in range(256):
if (i * n) % 256 == x:
return i
return -1
The reverse method is now fairly easy to do :
def rev(s, x):
k = s.k
for i in range(len(x)):
x[i] = inv_mod_256(x[i], k[i % len(k)])
Hashing time
The two remaining classes looks more difficult to reverse because they use a hashing function which is by definition not reversible :
def _mh(a):
def h(d): return hashlib.new(a, d).digest()[:3]
return h
class _A:
h = staticmethod(_mh("md5"))
def __init__(s, i, e=None): s.i = i; s.e = bytes(e) if e else None
def f(s, x):
i = s.i
c = bytes(x[i:i + 3])
d = s.h(c)
x[i:i + 3] = bytes(p ^ q for p, q in zip(c, d))
class _B(_A):
h = staticmethod(_mh("sha256"))
However, since only three bytes are hashed each time it is possible to store all the possible results on a dictionary to reverse the operation :
rev_A = {}
md5 = _mh('md5')
for i in range(256 ** 3):
c = i.to_bytes(3)
d = md5(c)
key = bytes(p ^ q for p, q in zip(c, d))
rev_A[key] = c
But this method is not perfect, because the length of the dictionary afterward is only 10_606_955 which is far from 256³ = 16_777_216, so I updated my dictionary so that each key is paired with a list of values giving that key :
rev_A = {}
md5 = _mh('md5')
for i in range(256 ** 3):
c = i.to_bytes(3)
d = md5(c)
key = bytes(p ^ q for p, q in zip(c, d))
if key in rev_A:
rev_A[key].append(c)
else:
rev_A[key] = [c]
# same concept for rev_B
This is problematic because this will prevent an automatic decoding of _T if in practice rev_A[key] or rev_B[key] contains more than one value. To test it I started with the following reverse function :
def rev(s, x):
i = s.i
x[i:i + 3] = rev_A[bytes(x[i:i + 3])][0]
Final decoding
I tried to get the flag using this function but some characters were off :
def get_flag():
x = bytearray(_T)
for o in reversed(_Q):
o.rev(x)
return bytes(x)
I added a print statement in the rev method of _A and _B to see if there is indeed a unicity problem and bingo :
In [1]: get_flag()
rev B : [b'L\x94\xdb']
rev A : [b'y3^', b'\xa4Lf', b'\xe2\x83\x88']
By tweaking the index of the element in the list given by rev_A, I found out that it was the last one, and I could get the flag :
In [3]: get_flag()
wreck{...}