Writeup – NewJeans is Five
Challenge Overview
We are given:
chall.py– an implementation of AES‑128 but with a critical component missing:SubBytesandSubWordare replaced by the identity function (they just return the input unchanged).
output.txt– two ciphertexts:94ae785acdb0d7c919f4893697659c8c→ encryption of known plaintext"incomprehensible"(hex696e636f6d70726568656e7369626c65)58f86ce660590bb05495c0dcd2d4d438→ encryption of the flag (unknown)
The description hints: “NewJeans is not NewJeans without any one of its members. What is AES without one of its components?”
The missing component is SubBytes (the S‑box).
Vulnerability
Without SubBytes, AES becomes a completely linear transformation over GF(2).
All remaining operations are:
ShiftRows– permutationMixColumns– linear over GF(2⁸) (multiplication by fixed polynomial)AddRoundKey– XOR with round key (affine, but the key part is constant)
Thus, for a fixed key, the encryption function is affine in the plaintext:
[ C = A(P) \oplus K_{\text{const}} ]
where (A) is the linear part (independent of the key) and (K_{\text{const}}) depends only on the key.
When we have two plaintext‑ciphertext pairs with the same key, the key‑dependent constant cancels out:
[ C_1 \oplus C_2 = A(P_1) \oplus A(P_2) = A(P_1 \oplus P_2) ]
Because (A) is linear.
So we can recover (P_1 \oplus P_2 = A^{-1}(C_1 \oplus C_2)), and then the unknown plaintext:
[ P_2 = P_1 \oplus A^{-1}(C_1 \oplus C_2) ]
Recovering the Linear Transformation (A)
From the encryption algorithm:
- Initial
AddRoundKey– key cancels out when XORing, so it does not appear in the difference. - For rounds 1 to 9:
SubBytes(identity) – no effect.ShiftRowsMixColumns
- After the loop:
SubBytes(identity)ShiftRowsAddRoundKey– again cancels.
So the linear map (A) that transforms the plaintext difference to the ciphertext difference is:
[ A = \text{ShiftRows} ;\circ; (\text{MixColumns} \circ \text{ShiftRows})^9 ]
We can implement (A) and its inverse directly.
Solver Script
The script below:
- Defines
GMul,ShiftRows,MixColumnsand their inverses. - Computes (A^{-1}(C_1 \oplus C_2)).
- Recovers the flag by XORing with the known plaintext.
from binascii import unhexlify, hexlify
def GMul(a, b):
p = 0
for _ in range(8):
if b & 1:
p ^= a
a <<= 1
if a & 0x100:
a ^= 0x11b
b >>= 1
return p
def bytes_to_matrix(data):
matrix = [[class="hljs-string">'']*4 for _ in range(4)]
for i in range(16):
row = i % 4
col = i // 4
matrix[row][col] = fclass="hljs-string">"{data[i]:02x}"
return matrix
def matrix_to_bytes(matrix):
data = bytearray()
for j in range(4):
for i in range(4):
data.append(int(matrix[i][j], 16))
return bytes(data)
def ShiftRows(state):
state[1][0], state[1][1], state[1][2], state[1][3] = state[1][1], state[1][2], state[1][3], state[1][0]
state[2][0], state[2][1], state[2][2], state[2][3] = state[2][2], state[2][3], state[2][0], state[2][1]
state[3][0], state[3][1], state[3][2], state[3][3] = state[3][3], state[3][0], state[3][1], state[3][2]
return state
def InvShiftRows(state):
# inverse of ShiftRows (shift right)
state[1][0], state[1][1], state[1][2], state[1][3] = state[1][3], state[1][0], state[1][1], state[1][2]
state[2][0], state[2][1], state[2][2], state[2][3] = state[2][2], state[2][3], state[2][0], state[2][1] # same as left 2
state[3][0], state[3][1], state[3][2], state[3][3] = state[3][1], state[3][2], state[3][3], state[3][0]
return state
def MixColumns(state):
temp = [[class="hljs-string">'']*4 for _ in range(4)]
for j in range(4):
a0 = int(state[0][j], 16)
a1 = int(state[1][j], 16)
a2 = int(state[2][j], 16)
a3 = int(state[3][j], 16)
temp[0][j] = fclass="hljs-string">"{GMul(0x02, a0) ^ GMul(0x03, a1) ^ a2 ^ a3:02x}"
temp[1][j] = fclass="hljs-string">"{a0 ^ GMul(0x02, a1) ^ GMul(0x03, a2) ^ a3:02x}"
temp[2][j] = fclass="hljs-string">"{a0 ^ a1 ^ GMul(0x02, a2) ^ GMul(0x03, a3):02x}"
temp[3][j] = fclass="hljs-string">"{GMul(0x03, a0) ^ a1 ^ a2 ^ GMul(0x02, a3):02x}"
return temp
def InvMixColumns(state):
temp = [[class="hljs-string">'']*4 for _ in range(4)]
for j in range(4):
a0 = int(state[0][j], 16)
a1 = int(state[1][j], 16)
a2 = int(state[2][j], 16)
a3 = int(state[3][j], 16)
temp[0][j] = fclass="hljs-string">"{GMul(0x0e, a0) ^ GMul(0x0b, a1) ^ GMul(0x0d, a2) ^ GMul(0x09, a3):02x}"
temp[1][j] = fclass="hljs-string">"{GMul(0x09, a0) ^ GMul(0x0e, a1) ^ GMul(0x0b, a2) ^ GMul(0x0d, a3):02x}"
temp[2][j] = fclass="hljs-string">"{GMul(0x0d, a0) ^ GMul(0x09, a1) ^ GMul(0x0e, a2) ^ GMul(0x0b, a3):02x}"
temp[3][j] = fclass="hljs-string">"{GMul(0x0b, a0) ^ GMul(0x0d, a1) ^ GMul(0x09, a2) ^ GMul(0x0e, a3):02x}"
return temp
def forward_A(data):
state = bytes_to_matrix(data)
for _ in range(9):
state = ShiftRows(state)
state = MixColumns(state)
state = ShiftRows(state) # final ShiftRows
return matrix_to_bytes(state)
def inverse_A(data):
state = bytes_to_matrix(data)
state = InvShiftRows(state) # inverse of final ShiftRows
for _ in range(9):
state = InvMixColumns(state)
state = InvShiftRows(state)
return matrix_to_bytes(state)
# Given data
pt1_hex = class="hljs-string">"696e636f6d70726568656e7369626c65"
ct1_hex = class="hljs-string">"94ae785acdb0d7c919f4893697659c8c"
ct2_hex = class="hljs-string">"58f86ce660590bb05495c0dcd2d4d438"
pt1 = unhexlify(pt1_hex)
ct1 = unhexlify(ct1_hex)
ct2 = unhexlify(ct2_hex)
D = bytes(a ^ b for a, b in zip(ct1, ct2))
X = inverse_A(D)
flag_bytes = bytes(a ^ b for a, b in zip(pt1, X))
print(class="hljs-string">"Flag:", flag_bytes.decode())Running the Script
Flag: newj34ns-nv-d!esThe recovered flag is newj34ns-nv-d!es.
In the required format gaslightCTF{...}, the final flag is:
gaslightCTF{newj34ns-nv-d!es}Explanation of the Flag
The flag newj34ns-nv-d!es is a leetspeak version of “newjeans never dies” (with 3 for e, 4 for a, and ! for i).
The challenge name “NewJeans is five” refers to the five members of the K‑pop group NewJeans, and the vulnerability is the missing fifth AES component (SubBytes) – hence the flag plays on that.
Takeaways
- Removing non‑linearity from a cipher makes it linear, which is catastrophically weak.
- A known‑plaintext attack can recover the entire transformation and break the cipher without the key.
- Always treat cryptographic primitives as a whole; changing any component can completely destroy security.