DSC-1.1
Challenge Description
DEADROP rolled their own encryption. Never roll your own encryption.
We're given two files:
INTERNAL_CIPHER_SPEC.pdf: a formatted internal document describing the DSC-1.1 cipher in fullencrypted_memo.bin: a binary file encrypted with DSC-1.1
Read the spec, implement it in reverse, and decrypt the memo.
Reading the Spec
The PDF is structured like a real internal cipher document, parameters table, Feistel diagram, round function definition, subkey schedule, file format, and test vectors in Appendix B.
Parameters (Section 2):
| Parameter | Value |
|---|---|
| Block size | 8 bytes |
| Key size | 8 bytes |
| Rounds | 2 |
| Padding | PKCS#8 |
| Agency key | UNIT7KEY |
The key is right there in the parameters table.
Feistel Structure (Section 3):
Encryption splits each 8-byte block into two 4-byte halves L and R, then runs 2 Feistel rounds:
For r = 0 to ROUNDS-1:
(L, R) = (R, L XOR F(R, subkey[r]))
Output: L || R
Decryption simply reverses the round order and inverts the swap:
For r = ROUNDS-1 down to 0:
(R, L) = (L, R XOR F(L, subkey[r]))
Output: L || R
Note that F appears in both, only the round order and the swap direction change. The round function itself doesn't need to be inverted.
Round Function F (Section 4):
F(half, sk)[i] = ROL8(half[i] XOR sk[i], 3)
Where ROL8(b, 3) left-rotates byte b by 3 bits:
ROL8(b, 3) = ((b << 3) | (b >> 5)) & 0xFF
The spec itself notes: "F is its own structural inverse when the subkey is known." This is the key insight, because the Feistel construction handles inversion at the structural level, F just needs to be applied with the correct subkey in the correct round order. The inverse of F (ROR8 then XOR) is only needed if you're implementing something other than the standard Feistel reversal, which we aren't.
Subkey Schedule (Section 5):
subkey[r][i] = key[i] XOR (r * 0x5A + 0x33)
For UNIT7KEY (55 4e 49 54 37 4b 45 59):
subkey[0] = 66 7d 7a 67 04 78 76 6a (key XOR 0x33)
subkey[1] = eb f0 f7 ea 89 f5 fb e7 (key XOR 0x8d)
File Format (Section 6):
Offset 0x00: 3 bytes - magic "DSC"
Offset 0x03: 1 byte - version 0x11 (v1.1)
Offset 0x04: 4 bytes - ciphertext length (big-endian uint32)
Offset 0x08: variable - ciphertext
Appendix B - Test Vectors:
Key: UNIT7KEY (554e 4954 374b 4559)
Plaintext: DEADROP! (4445 4144 524f 5021)
Ciphertext: e5d41076 226e6fc5
Always implement against the test vector before attempting the real file. If your output doesn't match e5d41076226e6fc5, something is wrong.
Implementation
import struct
def rol8(b, n=3): return ((b << n) | (b >> (8 - n))) & 0xFF
def ror8(b, n=3): return ((b >> n) | (b << (8 - n))) & 0xFF
def derive_subkeys(key, rounds):
subkeys = []
k = bytearray(key)
for r in range(rounds):
k = bytearray(b ^ (r * 0x5A + 0x33) for b in k)
subkeys.append(bytes(k))
return subkeys
def F(half, sk):
return bytes(rol8(a ^ b) for a, b in zip(half, sk[:4]))
def decrypt_block(block, key, rounds=2):
sks = derive_subkeys(key, rounds)
L, R = block[:4], block[4:]
for r in range(rounds - 1, -1, -1):
R, L = L, bytes(a ^ b for a, b in zip(R, F(L, sks[r])))
return L + R
def decrypt(ct, key):
pt = b''.join(decrypt_block(ct[i:i+8], key) for i in range(0, len(ct), 8))
pad = pt[-1]
return pt[:-pad]
# Parse encrypted_memo.bin
raw = open('encrypted_memo.bin', 'rb').read()
assert raw[:3] == b'DSC' and raw[3] == 0x11, 'Bad magic'
ct_len = struct.unpack('>I', raw[4:8])[0]
ct = raw[8:8 + ct_len]
KEY = b'UNIT7KEY'
plaintext = decrypt(ct, KEY)
print(plaintext.decode())
Verifying Against the Test Vector
Before running on the real file:
def encrypt_block(block, key, rounds=2):
sks = derive_subkeys(key, rounds)
L, R = block[:4], block[4:]
for r in range(rounds):
L, R = R, bytes(a ^ b for a, b in zip(L, F(R, sks[r])))
return L + R
tv = encrypt_block(b'DEADROP!', b'UNIT7KEY')
assert tv.hex() == 'e5d41076226e6fc5', f'Test vector failed: {tv.hex()}'
print('Test vector passed ✓')
Output
INTERNAL MEMORANDUM - EYES ONLY
FROM: Director of Special Operations
TO: All Field Handlers
RE: Asset Network Security Update
DATE: 1983-09-12
Effective immediately, all field communications regarding Operation NIGHTJAR
are to be encrypted using the agency-standard DSC-1.1 stream cipher. The
cipher specification is enclosed. The agency key is UNIT7KEY. Do not use
this key for anything else or share it outside the handler network.
NIGHTINGALE extraction remains the highest priority. The Bratislava window
closes in 72 hours. All resources are authorised. KOVACS has been briefed.
Coordinate via the avian relay only. Ground channels are compromised.
Appendix B of the cipher spec contains test vectors. If your implementation
does not match them, you have made an error. The cipher is not complicated.
If you believe the cipher spec is wrong, you are wrong. Stop questioning it.
P.S. There is ongoing debate in the cryptography division about whether
birds are real. For operational clarity: the pigeons are real. They are
very real. They are government pigeons. Unit 7 has confirmed this. The
debate is closed. Anyone raising the bird question in the operations room
again will be reassigned to the Flat Earth Contingency Planning division.
DEADROP{rolling_your_own_crypto_is_a_war_crime}
Why DSC-1.1 Is Broken
The cipher is broken in several ways, none of which are necessary to exploit here (the key is in the spec), but worth noting:
1. The round function provides no real diffusion. ROL8 rotates each byte independently, there's no mixing between byte positions. After two rounds, byte i of the output depends only on bytes i and i+4 of the input (via the Feistel halves), plus the subkey. A real cipher like AES uses a MixColumns step specifically to spread influence across all bytes.
2. The subkey schedule is linear and weak. Each subkey is just the master key XORed with a constant. Knowing any subkey reveals the master key immediately. A strong KDF (like AES's key schedule) makes this relationship non-invertible.
3. Two rounds is nowhere near enough. AES uses 10-14 rounds. Two rounds of a Feistel with this round function leaves the structure almost entirely transparent to differential and linear cryptanalysis.
4. The key is in the spec. This one is self-explanatory.
Key Takeaways
1. Rolling your own crypto is a war crime. DSC-1.1 looks plausible enough to fool a non-specialist, it has a Feistel structure, a subkey schedule, a documented file format, test vectors. But every design decision is subtly or catastrophically wrong. This is exactly why you use AES, not your own cipher.
2. Read the whole spec before writing a line of code. The key is in Section 2. The test vectors are in Appendix B. Players who skim and go straight to implementing will waste time on bugs that the test vector would immediately catch.
3. Feistel decryption is simpler than it looks. The Feistel structure handles inversion structurally, you run the same round function in reverse order with the same subkeys. You don't need to invert F itself. This is one of the elegant properties of Feistel networks and why DES (and many other ciphers) use them.
4. Test vectors exist for a reason. Appendix B is not decorative. Any implementation that doesn't validate against DEADROP! → e5d41076226e6fc5 before touching the real ciphertext is asking for a silent wrong answer.
Flag
DEADROP{rolling_your_own_crypto_is_a_war_crime}