## 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`0`

s. - If the flag starts with
`crypto{`

then if I pad with 15`000000000000000`

s 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)

## Flipping Cookie

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()