Home Symmetric Cryptography CryptoHack Writeup
Post
Cancel

Symmetric Cryptography CryptoHack Writeup

Keyed Permutations

bijection is the mathematical term for a one-to-one correspondence.

Resisting Bruteforce

Biclique attack is the best single-key attack against AES.

Structure of AES

This challenge is pretty simple matrix operation. Just convert the byte value into chars corresponding to ASCII value.

1
2
3
4
5
6
7
def matrix2bytes(matrix):
    """ Converts a 4x4 matrix into a 16-byte array.  """
    arr = [chr(i) for j in matrix for i in j]
    flag = ""
    for i in arr:
        flag += i
    return flag

Round Keys

To solve this challenge just take XOR of each s[i][j] with k[i][j] and then convert matrix2bytes.

1
2
def add_round_key(s, k):
    return matrix2bytes([list(s[i][j]^k[i][j] for j in range(4)) for i in range(4)])

Confusion through Substitution

Take the value of s[i][j] as the index of inv_s_box and fill corresponding values followed by converting matix2bytes.

1
2
def sub_bytes(s, sbox=s_box):
    return matrix2bytes([list(int(sbox[s[j][i]]) for i in range(4)) for j in range(4)])

Diffusion through Permutation

Implement inv_shift_rows, take the state, run inv_mix_columns on it, then inv_shift_rows, convert to bytes and you will have your flag.

1
2
3
4
5
6
7
8
9
10
11
def inv_shift_rows(s):
    s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
    s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
    s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
    return s
.
.
.
inv_mix_columns(state)
inv_shift_rows(state)
print(matrix2bytes(state))

Bringing It All Together

Conbine all the functions made till now and execute in reverse order of AES execution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def decrypt(key, ciphertext):
    # start from the last round key
    round_keys = expand_key(key)
    # Convert ciphertext to state matrix
    state = bytes2matrix(ciphertext)
    # Initial add round key step
    add_round_key(state, round_keys[N_ROUNDS])
    for i in range(N_ROUNDS - 1, 0, -1):
        inv_shift_rows(state)
        sub_bytes(state, inv_s_box)
        add_round_key(state, round_keys[i])
        inv_mix_columns(state)

    # Run final round (skips the InvMixColumns step)
    inv_shift_rows(state)
    sub_bytes(state, inv_s_box)
    add_round_key(state, round_keys[0])
    # Convert state matrix to plaintext
    plaintext = matrix2bytes(state)
    return plaintext

print(decrypt(key, ciphertext))

Modes of Operation Starter

The provided encrypt_flag() function encrypts the FLAG using the KEY and the decrypt(ciphertext) function decrypts the same using same key. Just get the encrypted flag put it into decrypt flag function and convert hex plaintext into ASCII.

Passwords as Keys

In this challenge we can get the encrypted flag using encrypt_flag() function. The question is how to get the password_hash because it makes the key form the md5 hash of a random word selected from dictionary of password given at https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words.

This type of challenge could be easiliy solved using bruteforce attack as we have limited set of possibility of word that will make the KEY. We can simply try to decrypt using all keys made using each word and check for flag.

  • Download the dictionary using wget https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Crypto.Cipher import AES
import hashlib
import random


# /usr/share/dict/words from
# https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words
with open("words") as f:
    words = [w.strip() for w in f.readlines()]

# KEY = hashlib.md5(keyword.encode()).digest()
FLAG = "c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66"   # Encrypted FLAG => to decrypt


def decrypt(ciphertext, password_hash):
    ciphertext = bytes.fromhex(ciphertext)
    key = bytes.fromhex(password_hash)

    cipher = AES.new(key, AES.MODE_ECB)
    try:
        decrypted = cipher.decrypt(ciphertext)
    except ValueError as e:
        return {"error": str(e)}

    return {"plaintext": decrypted.hex()}
  • Script to bruteforce attack
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    import codecs
    f = ""
    for word in words:
      passHash = hashlib.md5(word.encode()).hexdigest()
      dec = decrypt(FLAG, passHash)
      try:
          f = codecs.decode(dec['plaintext'],'hex').decode('ascii')
          print(f)
          break
      except:
          continue
    

ECB Oracle

ECB is the most simple mode, with each plaintext block encrypted entirely independently. In ECB mode, identical blocks of plaintext are encrypted to identical blocks of ciphertext. So we have to look for pattern in the blocks of ciphertext. To solve this we can think of one example.

  • Suppose the first 16 bytes are 0 and if the first 17 bytes are 0 in both cases the AES Encryption of first block will always be same as it contains 16 0s.
  • If the flag starts with crypto{ then if I pad with 15 000000000000000s or with 15 000000000000000c in both the cases the first block will be 000000000000000c and will have same encryption for the starting block.
  • We just bruteforced the first char of the flag. We can do this for whole flag to find the flag.

For complete solution of the program you can refer to this link.

ECB CBC WTF

In this challenge encryption is done using AES-128-CBC mode. In this mode the first block is XORed with the IV and then encrypted. The next block is XORed with the previous block and then encrypted. For decryption they use AES-128-ECB mode.

Assume a block of plaintext pi each of size 16 and IV as the initialisation vector. The first block of ciphertext ci is generated by ci = AES(pi ^ IV). The second block of ciphertext ci+1 is generated by ci+1 = AES(pi+1 ^ ci). So we can say that pi+1 = AES-1(ci+1) ^ ci. So to decrypt the flag we can simply decrypt the ciphertext using AES-128-ECB and then XOR the ith decrypted block with the (i-1)th encrypted block. Refer to the following code for better understanding.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# ECB CBC WTF
from Crypto.Cipher import AES
from pwn import xor
import requests

def encrypt():
    url = "http://aes.cryptohack.org//ecbcbcwtf/encrypt_flag/"
    response = requests.get(url)
    return response.json()['ciphertext']

flag = encrypt()
f = [flag[i:i+32] for i in [0,32,64]]
vi = f[0:(len(f)-1)]
f = f[1:]
def decrypt(data):
    url = "http://aes.cryptohack.org/ecbcbcwtf/decrypt/"
    response = requests.get(url + data + '/')
    return response.json()['plaintext']

for i in range(len(f)):
    f[i] = decrypt(f[i])

for i in range(len(f)):
    f[i] = xor(bytes.fromhex(f[i]),bytes.fromhex(vi[i]))

flag = ""
for i in f:
    flag += i.decode()

print(flag)

For complete solution of the program you can refer to this link

SYMMETRY

For this challenge we have been given encrypt and encrypt_flag function both of which uses OFB(Output feedback) mode of encryption. The first 16 bytes of the return of encrypt_flag is IV(initialisation vector). The encrypt function take the plaintext and gives the ciphertext. As one can see let k = AES(KEY) then ciphertext = plaintext ^ k. So we can say that plaintext = ciphertext ^ k. So we can get the key by XORing the first 16 bytes of the return of encrypt_flag with the ciphertext by providing the ciphertext as the plaintext to the encrypt function.

Bean Counter

It is a simple XOR problem if you look at the function increment and the format of a .png file. self.stup will be false here so will always take else branch also the lines following it will give output which is same as self.value. Which means we are just taking the picture and XORing it with the self.value. Also as per the format of png file the A PNG file is composed of an 8-byte signature header, followed by any number of chunks that contain control data / metadata / image data which is already known as the name of file is given in the question. So we can just XOR the first 8 bytes of the image with the first 8 bytes of the output and we will get the flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests

def fetch_encrypted_data():
    url = "http://aes.cryptohack.org/bean_counter/encrypt/"
    response = requests.get(url)
    return response.json()['encrypted']

def xor_bytes(byte_array1, byte_array2):
    return bytes(x ^ y for x, y in zip(byte_array1, byte_array2))

def main():
    png_header = bytes([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52])
    encrypted_data = bytes.fromhex(fetch_encrypted_data())

    keystream = xor_bytes(png_header, encrypted_data[:len(png_header)])

    decrypted_data = xor_bytes(encrypted_data, keystream * (len(encrypted_data) // len(keystream)))

    with open('bean_counter.png', 'wb') as file:
        file.write(decrypted_data)

if __name__ == "__main__":
    main()
This post is licensed under CC BY 4.0 by the author.