---
# System prepended metadata

title: COMP2633 Fall 2024 Week 6B Homework Official Writeup
tags: [My-Challenges, COMP2633, Writeups]

---

# COMP2633 Fall 2024 Week 6B Homework Official Writeup

## Homework 6-Bi: AES-UWU
> You have seen ECB, CBC, CTR and other modes. Can you crack AES-UWU?
>
> `nc HOST PORT`
> 
> ---
> 
> [aes_uwu](https://files.firebird.sh/chal-2024/06/aes_uwu.py)
> 
> ---
> 
> Note: To avoid the server being overloaded, please try to solve the challenge locally first.
> 
> Hint: You may want to draw an image visualizing what AES-UWU is doing before you start the attack.
> 
> Probably not a hint: You can find the author through Discord if you are stuck at some point in the homework. Some hints may not necessarily be posted publicly to avoid spoiling the fun of solving challenges...
>
> More hints [here](https://hackmd.io/@Jackylkk2003/6b-hw-hints).

As usual, before we carry out any attacks, read through the code carefully and understand what it is doing. Think about what happens in normal usage of the crypto service. The first step you should do is to know what is happening.

### Preliminary Code Analysis
When we read the code, first we have to understand the program. If possible, identify some potential issues in the implementation. Since this is just a preliminary study, these issues may not be exploitable, and it is ok to miss out some problems.
```python!=1
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import datetime
import os
import random
```
Ordinary imports. `datetime` and `random` are included. Possibly. There could be an issue of insecure pseudo random number generator here.
```python=8
def xor(a: bytes, b: bytes) -> bytes:
    return bytes(x ^ y for x, y in zip(a, b))
```
A typical xor function that stops at the shorter byte string. In [some challenges](https://github.com/blackb6a/hkcert-ctf-2023-challenges/tree/master/29-sign-me-a-flag-i), this could be a vulnerability.
```python=12
def PoW():
    hex_set = "0123456789abcdef"
    length = random.randint(5, 10) * 2
    a = "".join(random.choice(hex_set) for i in range(length))
    a = a.encode()
    a = pad(a, 16)
    h = AES.new(a, AES.MODE_ECB).encrypt(a).hex()[:4]

    print("======== Proof-of-Work enabled ========")
    print(f"Send me a hex code (in lowercase) such that:")
    print(f"<The condition is obfuscated by the king of UwU from Track A>")
    print(f"h = {h}")

    ans = input("> ")
    ans = pad(ans.encode(), 16)
    if len(ans) != 16:
        print("Try harder!")
        exit()

    if AES.new(ans, AES.MODE_ECB).encrypt(ans).hex()[:4] != h:
        print("Proof-of-Work failed!")
        exit()
```
PoW is the first long function in this program. It
1. Chooses a random even length hex string and encode it into a byte string
2. Pads the byte string to length 16.
3. AES-ECB "encrypt" the string using itself as both plaintext and key.
4. Prints out the first 2 bytes of "encrypted" value as hex.
5. Reads an input that after the same treatment would produce the same 2 bytes.

Since it is just a PoW that reduces the load to the server, we don't have to put too much effort in analysing this function. At least not now.

```python=36
class AES_UWU:
    def __init__(self, key: bytes):
        self.key: bytes = key
        self.cipher = AES.new(self.key, AES.MODE_ECB)  # I like ECB mode!
        self.used_iv = set()
```

A class AES_UWU that initializes the object with a provided key, ECB encryption cipher, and an empty set of used iv. Nothing particularly interesting here.

```python=42
    # Encrypt the plaintext with AES-UWU mode (?)
    # Returns ciphertext in hex string
    def encrypt(self, plaintext: str) -> str:
        plaintext: bytes = pad(plaintext.encode(), AES.block_size)
        iv: bytes = os.urandom(16)
        self.used_iv.add(iv)
        ciphertext: bytes = iv

        for i in range(0, len(plaintext), AES.block_size):
            block = plaintext[i : i + AES.block_size]
            block = xor(block, iv)
            block = self.cipher.encrypt(block)
            ciphertext += block
            iv = xor(iv, block)
        return ciphertext.hex()
```
This function encrypts a plaintext with the cipher defined  above. However, we can see that this is much more complicated than ECB. At least a typical ECB will not have an IV.
This function first adds padding to the plaintext and a randomly generated iv into the used iv set, and then encrypts the plaintext. Also notice that the IV is prepended before the ciphertext.
As emphasized in the hints, we first draw an image to visualize the encryption process. If we do so, we can observe that this mode is very similar to CBC mode, just with an extra link xor from the previous IV (the red links). We don't know if this is useful or not, let's just keep this in mind for now.
![AES-UWU-Encrypt](https://hackmd.io/_uploads/SypLjXWzkx.png)


```python=58
    # Decrypt the ciphertext from hex string
    def decrypt(self, ciphertext: str):
        ciphertext = bytes.fromhex(ciphertext)
        iv = ciphertext[: AES.block_size]

        assert iv not in self.used_iv  # iv reuse is vulnerable!

        self.used_iv.add(iv)

        plaintext = b""
        for i in range(AES.block_size, len(ciphertext), AES.block_size):
            block = ciphertext[i : i + AES.block_size]
            decrypted = self.cipher.decrypt(block)
            decrypted = xor(decrypted, iv)
            plaintext += decrypted
            iv = xor(iv, block)
        try:
            plaintext = unpad(plaintext, AES.block_size)
        except:
            # Ciphertext modified! u bad bad!
            return ":("

        # You didn't modify the ciphertext, so u good good!
        return ":)"
```
So it is a decryption function. We also draw an image to show the decryption process.
![AES-UWU-Decrypt](https://hackmd.io/_uploads/HJ69hQbz1l.png)
Again, it is so similar to CBC, and just with the red links added.
However, this decryption function has more than what we have here in this image. It also checks whether the iv is reused.
And the try-except block at the very end is also interesting. It tries to unpad the plaintext according to the PKCS#7 standard. If it failed, it returns `:(` to you.
However, if the padding is valid, it is not giving you the plaintext either. Instead, it just gives you the `:)`. So what does this decryption function actually tells you?
It is replying you whether the padding is correct, so you have a padding oracle here. This also matches with our observations above that this mode of operation is similar to the CBC mode.

Still, we continue to read the code to ensure we are not missing anything important.

```python=84
if __name__ == "__main__":
    PoW()

    starttime = datetime.datetime.now()  # Don't worry, PoW is not counted in time limit

    key = os.urandom(16)
    cipher = AES_UWU(key)

    try:
        with open("message.txt", "r") as f:
            message = f.read()
    except:
        message = """
Too lazy to test the cipher, let's test in production.
Here is a fake flag for u: flag{Fake Flag UwU}
"""

    encrypted = cipher.encrypt(message)
    assert len(encrypted) == 256

    print("Here is my message to you")
    print(encrypted)

    while (datetime.datetime.now() - starttime).seconds < 300:
        ciphertext = input("> ")

        # I don't want u do decrypt my message, so I will check the length of the ciphertext
        assert len(ciphertext) == 128

        # This is not a pwn challenge! This is track B!
        assert all(c in "0123456789abcdef" for c in ciphertext)

        print(cipher.decrypt(ciphertext))

    print("OK I go to sleep now, bye bye!")
```
The program calls the PoW at the beginning, and then generates a key using `os.urandom(16)`. This means that the pseudo random generator attacks that we predicted is not exploitable. :<
But we have the padding oracle so we don't need that. After that, it encrypts the flag and sends it to us. Now it is expecting inputs and it will check whether the input is 128 bytes (longer than typical padding oracle attacks, but shorter than the encrypted flag) and the inputs should contain hex characters only.
There is also a 5-minute timeout so you should ensure that your code will not run very slowly.

### Cracking PoW

Umm... Just brute force it? It is a PoW anyway.

```python=
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from pwn import *
from tqdm import tqdm
import os
import itertools


def PoW_solve(target):
    hex_set = "0123456789abcdef"
    length = 5
    for ch in itertools.product(hex_set, repeat=length):
        ch = "".join(ch).encode()
        a = pad(ch, 16)
        h = AES.new(a, AES.MODE_ECB).encrypt(a).hex()[:4]
        if h == target:
            return ch
```

### Attack

Based on what we have looked at above, a padding oracle is the most probable attack. But since it is not CBC mode, we will have to study the encryption and decryption scheme to ensure that a padding oracle attack will work. This is because even if we have a padding oracle, the attack may still not be feasible. One example will be giving you a padding oracle for ECB mode. In this case, the padding oracle attack will fail.
Just a recap of the lecture contents, an oracle is something that gives you information. That does not necessarily mean an attack, it simply means that you have access to that piece of information.
But since the author is kind, the solution to this challenge is indeed the padding oracle attack. So why does it work in this challenge even if this is not CBC mode?

### Why the Padding Oracle attack works

In order for the padding oracle to work, you should control the IV/previous block, and you can reverse engineer the intermediate results of the CBC byte-by-byte simply by knowing whether the padding is correct. The AES-UWU satisfies both requirements and a padding oracle attack is then possible.

A small tip when you are carrying out the attack, please be careful of what the actual "block" that is xor-ed with the plaintext. In CBC it is the previous block/IV, in UWU, that is not the case. Since it is easy to figure it out, I will leave this as an exercise. Refer to the images drawn previously if you need to, that is the whole point why we visualize the encryption and decryption using images.

So our attack is just brute-forcing byte-by-byte, and this solves the challenge without the unique iv constraints and payload length constraints.

### Bypassing Restrictions

Unfortunately, there is a limit on the payload length. Then that makes the normal padding oracle attack fail... Well maybe not...

Suppose the same restriction is applied to CBC mode, we can observe that padding is only related to the last block, and it is the same as using the previous block as the IV. So the attack still works, we just need to prepend some random blocks before the payload to bypass the checking.

How about for UWU? We cannot simply do that because the IVs are all chained together by xor. Let's see what happens when we prepend some random blocks before our payload.

We trace the xor to the plaintext in the last block. We can see that the value for xor in the last block is actually the xor sum of the IV and also all previous ciphertext blocks. This is obvious when you look at the decryption image. So, we just have to pad some zero blocks before our payload then this restriction is passed.

But still, the next thing is to pass the repeated iv check. If we use the above method to bypass the checking, it will be problematic since we are reusing the IV. But if you can really follow me in this writeup then you should be able to figure out how to bypass this checking. We can simply generate some random bytes and ensure that their xor sum is our target so that we can perform padding oracle. An even more elegant way is to prepend 2 identical random blocks since they will cancel each other anyway.

### Time Constraints

The last thing is that you have only 5 minutes to interact with the system. Afterwards the system will stop entertaining further requests. To solve this issue, we will make use of batching to reduce the average time needed to send out payloads. So if you tried this challenge and find that you can solve locally but not at the server, it is most likely the issue of network delays.

Yes, batching is extremely powerful. You will see this skills in many of the challenges.

### Solve Script

Just a note that this script is not aiming to be the "best" solve script or for shortest payload or something.

```python=
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from pwn import *
from tqdm import tqdm
import os
import itertools


def PoW_solve(target):
    hex_set = "0123456789abcdef"
    length = 5
    for ch in itertools.product(hex_set, repeat=length):
        ch = "".join(ch).encode()
        a = pad(ch, 16)
        h = AES.new(a, AES.MODE_ECB).encrypt(a).hex()[:4]
        if h == target:
            return ch


HOST = "localhost"
PORT = 3000

# context.log_level = "DEBUG"
with remote(HOST, PORT) as io:
    io.recvuntil(b"h = ")
    target = io.recvline().strip().decode()
    io.sendline(PoW_solve(target))
    io.recvline()
    ciphertext = bytes.fromhex(io.recvline().strip().decode())
    iv = ciphertext[:16]
    ciphertext = ciphertext[16:]

    def attack(iv: bytes, block: bytes):
        known_enc = b""
        for i in reversed(range(16)):
            padding = b"\x00" * i + bytes([16 - i]) * (16 - i) # Alternatively use pad
            for j in range(256):
                test = b"\x00" * i + bytes([j]) + known_enc
                iv_ = xor(test, padding)
                block1 = os.urandom(16)
                block2 = os.urandom(16)
                payload = block1 + block2 + xor(block1, block2, iv_) + block
                io.sendline(payload.hex().encode())
            candidates = []
            for j in range(256):
                io.recvuntil(b":")
                if b")" in io.recvline():
                    candidates += [j]
            
            # Prevent false positive, although very unlikely
            if len(candidates) > 1:
                for j in candidates:
                    test = b"\xff" * i + bytes([j]) + known_enc
                    iv_ = xor(test, padding)
                    block1 = os.urandom(16)
                    block2 = os.urandom(16)
                    payload = block1 + block2 + xor(block1, block2, iv_) + block
                    io.sendline(payload.hex().encode())
                for j in candidates:
                    io.recvuntil(b":")
                    if b")" in io.recvline():
                        known_enc = bytes([j]) + known_enc
            else:
                known_enc = bytes([candidates[0]]) + known_enc
        return xor(known_enc, iv)

    plaintext = b""
    # tqdm helps with tracking the execution progress
    for i in tqdm(range(0, len(ciphertext), 16)):
        block = ciphertext[i : i + 16]
        plaintext += attack(iv, block)
        iv = xor(iv, block)
    print(plaintext) # b'Flag 1: flag{P4dDlNg_0r4cle_0n Maybe we really should name this as UwU mode? Flag 2: _AES_UWU_i5_5lMil4r_UwU}\n\x02\x02'
    # Solvable with around 30 seconds
```

### Additional Exercises

Try to do Pigeon Post (2) from HKCERT24.

## Homework 6-Bii: Absurd/Double Encryption Scheme

> When DES meets AES.
> 
> `nc HOST PORT`
> 
> ---
> 
> [double_encryption](https://files.firebird.sh/chal-2024/06/double_encryption.py)
> 
> ---
> 
> Hint: There is an unanswered question in the lesson: Is 3DES 3 times secure compared with DES? Can you answer it?
~~But there is no 3DES in this challenge...~~
>
>
> Probably not a hint: You can find the author through Discord if you are stuck at some point in the homework. Some hints may not necessarily be posted publicly to avoid spoiling the fun of solving challenges...
> 
> More hints [here](https://hackmd.io/@Jackylkk2003/6b-hw-hints).

Before we go into the writeup, I would like to credit [Mystiz](https://mystiz.hk) since this challenge is partially plagiarized from [multiDES](https://github.com/CUHK0x/cuhk-ctf-2024-challenges/tree/main/crypto/31_multides) authored by him. As a hint, you may first solve that challenge.
And again, let's read the code together.

### Preliminary Code Analysis

```python=
import os
from Crypto.Cipher import AES, DES
from Crypto.Util.Padding import pad
from typing import List
```

Typical imports, nothing special.

```python=7
class DESDES:
    def __init__(self, key: bytes, modes: List[int]):
        self.key = key.hex().encode()
        ivs = [os.urandom(8) for _ in range(2)]
        print("List of iv:")
        for iv in ivs:
            print(iv.hex())

        self.des_ciphers = [
            DES.new(self.key[i * 8 : i * 8 + 8], modes[i % 2], iv=ivs[i % 2])
            for i in range(len(self.key) // 8)
        ]

    def encrypt(self, message: bytes) -> bytes:
        c = message
        for des_cipher in self.des_ciphers:
            c = des_cipher.encrypt(c)
        return c
```

Here comes the actual code logic. The `__init__` function is already looking interesting. We have a `self.key = key.hex().encode()`, which is turning the key from bytes into hex, and to bytes again. While this seems to perform nothing, the key is actually changed.
Suppose the key is originally `b"\x12\x34\x56\x78"`, after this line, it becomes `b"12345678"` instead. **The length of key is doubled but each byte can now only be hex characters**. To observe this, you can try to run this code locally and print out the key and you will notice something special, especially if you choose some ascii text as the key parameter.
For the IVs, 2 random IVs with 8 bytes are generated. Nothing special about that, except that you have access to the IV values.
The next thing is the DES cipher objects. Simply speaking, the code is partitioning the key into blocks of 8 bytes and then create a list of DES ciphers. The mode of operation and IVs are alternating.
And the encrypt function passes the message through the list of DES ciphers and encrypt them one by one. There isn't anything particularly strange about the encrypt function.
It may be worth noticing that if you are encrypting the same message twice, the output will be different since the encrypt function does not reset the state of the DES ciphers.

```python=27
def main():
    try:
        with open("flag.txt", "rb") as f:
            m = f.read()
    except:
        m = b"flag{this_is_a_test_flag}"

    key = os.urandom(8)
    print(f"Your lucky number today: {key[-1]}")

    allowed_list = [DES.MODE_CBC, DES.MODE_CFB, DES.MODE_OFB]

    mode1 = int(input("Encryption mode 1: "))
    if mode1 not in allowed_list:
        print("Invalid mode")
        return

    mode2 = int(input("Encryption mode 2: "))
    if mode2 not in allowed_list or mode2 == mode1:  # Same mode no good :<
        print("Invalid mode")
        return

    des = DESDES(key, [mode1, mode2])

    message = input("Enter message in hex: ")
    message = bytes.fromhex(message)
    message = pad(message, 8)

    print(f"Encrypted message: {des.encrypt(message).hex()}")

    aes = AES.new(key.hex().encode(), AES.MODE_ECB)  # Same mode really no good :<
    m = pad(m, 16)
    print(f"Flag: {aes.encrypt(m).hex()}")


if __name__ == "__main__":
    main()
```

In this part of code, we execute the main function that first reads a flag.
The key is 8-byte random string. A special thing is that the last byte is given to you.
Then, you are asked to choose 2 mode of operations from CBC, CFB, and OFB, and they must be different.
The input may be a bit strange, since directly inputting `DES.MODE_CBC` will not work. From the documentation, these modes are some constant integers, and you should enter those integers instead.
From [Homework 6-Bi](#homework-6-bi-aes-uwu) we know that using multiple modes of operation may be problematic. But currently no significant vulnerability is found regarding this.
Now we are at line 49, where we create a DESDES object from the key and mode of operations specified. Notice that since the key length is 8 bytes, it will be doubled to 16 bytes and the encryption will work like this:

`message --> DES mode 1 with first half of key --> DES mode 2 with second half of key --> ciphertext`

You are given the ciphertext of a chosen plaintext.
Finally, you will receive an encrypted flag that uses AES ECB to encrypt, using the previously used keys. Since we are receiving this encrypted flag at the very end, we cannot base our attack on the received ciphertext. (Well actually you can if you open multiple connections, but then the keys are changed and that is essentially useless.)

### Ideas

So we analyzed some of the issues/interesting observations in this code:

- This challenge uses DES instead of the more secure AES
- There is some strange `.hex().encode()` in the code, and that limits the characters in the key to be hex characters.
- There are 2 DES chained together, in other words, it is a 2DES encryption. But it is quite strange that people are using 3DES but not 2DES...
- The program leaked a byte of key (or 2 after doubling).
- The flag is given at the end, so we have to decrypt arbitrary ciphertext. To do this, the only (sensible) way is to obtain the key for the ciphers, especially when we changed from DES to AES, and we are using ECB mode. So our goal will be to get the keys.

There is not much ways to get keys. What we can do is basically brute force the keys. But then we need to brute $128^7$ possibilities. Any way we can do better?

The first thing lies in 2DES. If we searched `2DES vulnerabilities` we can instantly find meet-in-the-middle attack. Let's have a look at what 2DES and meet-in-the-middle is.

![2DES](https://hackmd.io/_uploads/Hkedi70fJx.png)

We can modify this a little bit...

![MITM](https://hackmd.io/_uploads/BJd3sQCM1e.png)

If we have a plaintext-ciphertext pair, we can brute force the keys for DES Encrypt 1 and prepare a set of all possible intermediates, and then decrypt the ciphertext by brute forcing the keys for DES Decrypt 2 and match the generated intermediates with the set we found earlier. If a match is found, then we have found the keys for the 2DES.

With this in mind, we can crack the given cipher in around $128^4$ key searches. While this looks close... Can we do better?

Since the keys can only have hex characters, maybe we can exploit that? The key search space becomes $16^8$, and you find it is the same... Life is so difficult :< and we need more insights.

An observation that is not stated above but given in the hints is that we should pay attention to the DES key size. We know that DES key size is 56 bits only and hence is vulnerable to brute force attacks. Refer to the code again, we are actually passing 8 bytes = 64 bits to each DES ciphers. What happened to the remaining 8 bits?

With more Googling/trial-and-error, we will know that the 8 bits are the last bits of each of the 8 bytes. They serves as parity bits to check for errors in keys. According to the documentation of the Crypto library `pycryptodome`:

> Its (DES's) keys are 64 bits long, even though 8 bits were used for integrity (now they are ignored) and do not contribute to security. The effective key length is therefore 56 bits only.

If the last bit of each byte can be ignored, then we can reduce the search space. Let's look at the binary ascii values of those hex characters:

$0 \rightarrow 00110000$
$1 \rightarrow 00110001$
$2 \rightarrow 00110010$
$3 \rightarrow 00110011$
$4 \rightarrow 00110100$
$5 \rightarrow 00110101$
$6 \rightarrow 00110110$
$7 \rightarrow 00110111$
$8 \rightarrow 00111000$
$9 \rightarrow 00111001$
$a \rightarrow 01100001$
$b \rightarrow 01100010$
$c \rightarrow 01100011$
$d \rightarrow 01100100$
$e \rightarrow 01100101$
$f \rightarrow 01100110$

Ignoring the last bit and removing duplicate, we have the characters `02468abdf` remaining. So the brute force space now is $9^8$, which is a reasonable number to brute force.

### More Details

We now have an idea of how to attack, and it is actually working and correct too (because the challenge author said so). We just have to ensure that the details are well-handled so that we can solve this challenge.

#### Memory limit

Obviously your computer does not have infinite amount of memory, and it is not a particularly good idea to store so much intermediates to key mappings. The intermediates are 8 bytes long too, and we have $9^8$ of them. Can we optimize the memory usage without sacrificing algorithm runtime?

Yes! And it is actually quite simple to do so. We are now brute-forcing $9^8$ keys and stores them so that we can match with the $9^6$ key possibilities in the DES decryption 2. Why don't we store $9^6$ keys in the second step and match with the $9^8$ possibilites in the first step instead? Just a reminder that you have the last byte of the key at the beginning, and after doubling the length, you have 2 hex characters.

#### Choosing modes and plaintext

So which DES modes and plaintext should we choose. In our whole idea above, there is nothing related to mode of operation here. So maybe we can just arbitrarily choose the modes. For the plaintext, you would want to use just 1 block since having more blocks is not helpful and it slows down your code.
Actually, the mode and plaintext selection are both intended to be red herrings.

#### AES

After you find a match in the DES part, you happily use the found keys to decrypt the AES-encrypted flag and find that it does not work. So why is this the case?
This is because AES does not have the parity bit thing like DES, and that means the last bit of each byte for the AES key are still unknown. To solve this issue, ~~connect to the server $2^{16}$ tims and hope for the best~~ we can also brute force the last bit of the AES key and then everything is solved. Finally!!

### Solve Script

```python=
from pwn import *
from Crypto.Cipher import AES, DES
from Crypto.Util.number import long_to_bytes
from Crypto.Util.Padding import pad
from tqdm import tqdm
import itertools

HOST = "localhost"
PORT = 3000
context.log_level = "debug"
p = remote(HOST, PORT)

p.sendline(b"2")
p.sendline(b"3")
p.sendline(b"00")

p.recvuntil(b"Your lucky number today: ")
known_bytes = long_to_bytes(int(p.recvline().strip().decode())).hex().encode()
p.recvuntil(b"List of iv:\r\n")
iv1 = p.recvline().strip().decode()
iv2 = p.recvline().strip().decode()
p.recvuntil(b"Encrypted message: ")
enc = bytes.fromhex(p.recvline().strip().decode())
p.recvuntil(b"Flag: ")
flag = bytes.fromhex(p.recvline().strip().decode())

p.close()

table = {}
m = pad(bytes.fromhex("00"), 8)
for key in tqdm(itertools.product(b"abdf02468", repeat=6), total=9**6):
    cipher = DES.new(bytes(key) + known_bytes, 3, iv=bytes.fromhex(iv2))
    table[cipher.decrypt(enc)] = bytes(key) + known_bytes

for key in tqdm(itertools.product(b"abdf02468", repeat=8), total=9**8):
    cipher = DES.new(bytes(key), 2, iv=bytes.fromhex(iv1))
    if cipher.encrypt(m) in table:
        cipher = DES.new(bytes(key), 2, iv=bytes.fromhex(iv1))
        key1 = bytes(key)
        key2 = table[cipher.encrypt(m)]
        break

print(key1)
print(key2)
cipher1 = DES.new(key1, 2, iv=bytes.fromhex(iv1))
cipher2 = DES.new(key2, 3, iv=bytes.fromhex(iv2))
assert cipher2.encrypt(cipher1.encrypt(m)).hex() == enc.hex()
combined_key = key1 + key2
for offset in tqdm(itertools.product(b"\x00\x01", repeat=16), total=2**16):
    key = xor(combined_key, offset)
    cipher = AES.new(key, AES.MODE_ECB)
    if cipher.decrypt(flag[:16])[:5] == b"flag{":
        print(cipher.decrypt(flag))
        break
```

## Flags

If you are looking for the flags in these 2 challenges. Sorry, the challenge author is too lazy to go and find flags and copy and paste them here. He is willing to type this long passage to explain all these to you though.
And an additional reminder on ethical hacking, don't wrench the author please.

## P.S.

I do recommend you to try out [multiDES](https://github.com/CUHK0x/cuhk-ctf-2024-challenges/tree/main/crypto/31_multides) by Mystiz. The solve script is also available there but I don't recommend you to look at it right away.

Also, you may remember that I have released [some hints](https://hackmd.io/@Jackylkk2003/6b-hw-hints) for both challenges. Most of them are in the form of questions. I also suggest you to go and look at those hints again and try to answer them.

Also also, I have prepared a PowerPoint showing a yet-another-very-detailed-write-up of the challenge Alternative Encipherment Strategy. It is published on the course webpage in the announcement section. You may want to check that out too UwU.

Thanks for everyone who read all the way till here. If possible, please tell me whether you like this step-by-step style writeups, or you prefer the more concise ones.