SYMMETRIC CIPHERS

Symmetric-key ciphers are algorithms that use the same key both to encrypt and decrypt data. The goal is to use short secret keys securely and efficiently send long messages.

The most famous symmetric-key cipher is Advanced Encryption Standard (AES), standardised in 2001. It;s sos widespread that modern processors even contain special instruction sets to perform AES operations. The first series of challenges here guides you through the inner workings of AES, showing you how its separate componentes work together to make it a secure cipher. By the end you will have built your own code for doing decryption !

We can split symmetric-key ciphers into two types, block ciphers and stream ciphers. Block ciphers break up a plaintext into fixed-length blocks, and send each block through an encryption function together with a secret key. Stream ciphers meanwhile encrypt one byte of plaintext at a time, by XORing a pseudo-random keystream with the data. AES is a block cipher but can be turned into a stream cipher using mode of operation such as CTR.

Block ciphers only specify how to encrypt and decrypt individual blocks, and a mod of operation must be used to apply the cipher to longer messages. This is the point where real world implementations often fail spectacularly, since developers do not understand the subtle implications of using particular modes. The remainder of the challenges see you attacking common misuses of various modes.


HOW AES WORKS

KEYED PERMUTATIONS

AES, like all good block ciphers a “keyed permutation”. This means that it maps every possible input block to a unique output block, with a key determining which permutation to perform.

Note: A “block” just refers to a fixed number of bits or bytes, which may represent any kind of data. AES processes a block and outputs another block. We’ll be specifically talking the variant of AES which works on 128 bit (16 byte) blocks and a 128 bit key, knows as AES-128.

Using the same key, the permutation can be performed in reverse, mapping the output block back to the original input block. It is important that there is a one-to-one correspondence between input and output blocks, otherwise we wouldn’t be able to rely on the ciphertext to decrypt back to the same plaintext we started with.

What is the mathematical term for a one-to-one orrespondence ?

Answer: bijection

RESISTING BRUTEFORCE

If a block cipher is secure, there should be no way for an attacker to distinggish the output of AES from a random permutation of bits. Furthermore, there should be no better way to undo the permutation than simply bruteforcing every possible key. That’s why academics consider a cipher theoretically “broken” if they can find an attack that takes fewers steps to perform than bruteforcing the key, even if that attack is practically infeasible.

Note: How difficult is it to bruteforce a 128-bit keyspace ? Somebody estimated that if you turned the power of the entire Bitcoin mining network against an AES-128 key, it would take over a hundred times the age of the universe to crack the key.

It turns out that there is an attack on AES that’s better than bruteforce, but only slightly - it lowers the security level of AES-128 down to 126.1 bits, and hasn’t been improved on for over 8 years. Given the large “security margin” provided by 128 bits, and the lack of improvements despite extensive study, it;s not considered a credible risk to the security of AES. But yes, in a very narrow sense, it “breaks” AES.

Finally, while quantum computers have the potential to completely break popular public-key cryptosystems like RSA via Shor’s algorithm, they are thought to only cut in half the security level of symmetric cryptosyste, via Grover’s algorithm. This is one reason why people recommend using AES-256, despite it being less performant, as it would still provide a very adequate 128 bits of security in a quantum future.

What is the name for the best single-key attack againt AES ?

Answer: the meet-in-the-middle attack (MITM) 

STRUCTURE OF AES

To achieve a keyed permutation that is infeasible to invert without the key, AES applies a large number of ad-hoc mixing operations on the input. This is in stark contrast to public-key cryptosystems like RSA, which are based on elegant individual mathematical problems. AES is much kess elegant, but it’s very fast.

At a high level, AES-128 begins with a “key schedule” and then runs 10 rounds over a state. The starting state is just the plaintext block that we want to encrypt, represented as a 4x4 matrix of bytes. OVer the course of the 10 rounds, the state is repeatedly modified by a number of invertible transformations.

Note: Each transformation step has a defined purpose based on theoretical properties of secure ciphers established by Claude Shannon in the 1940s. We’ll look closer at each of these in the following challenges.

Here’s an overview of phases of AES encryption:

  1. KeyExpansion or Key Schedule

From the 128 bit key, 11 separate 128 bit “round keys” are derived: one to be used in each AddRoundKey step.

  1. Initial key addition

AddRoundKey - the bytes of the first round key are XOR’s with the bytes of the state.

  1. Round - this phase os looped 10 times, for 9 main rounds plus one “final round”

a) SubBytes- each byte os the state is substituted for a different byte according to a lookup table (“S-box”)

b) ShiftRows - the last three rows of the state matrix are transposed - shifted over a column or two or three

c) MixColumn - matrix multiplication is performed on the columns of the state, combining the four bytes in each column. This is skipped in the final round.

d) AddRoundKey - the bytes of the current round key are XOR’s with the bytes of the state.

Included is a bytes2matrix function for cinverting our initial plaintext block into a state matrix. Write a matrix2bytes function to turn that matrix backinto bytes, and submit the resulting plaintext as the flag.

This is my solution:

def bytes2matrix(text):
    """ Converts a 16-byte array into a 4x4 matrix.  """
    return [list(text[i:i+4]) for i in range(0, len(text), 4)]

def matrix2bytes(matrix):
    """ Converts a 4x4 matrix into a 16-byte array.  """
    s=""
    for i in range(0,len(matrix)):
        tmp_list = matrix[i]
        for _ in tmp_list:
            s=s+chr(_)
    print("flag: ",s)
def test(matrix):
    print("len", len(matrix))

matrix = [
    [99, 114, 121, 112],
    [116, 111, 123, 105],
    [110, 109, 97, 116],
    [114, 105, 120, 125],
]

# print(matrix2bytes(matrix))
matrix2bytes(matrix)
# test(matrix)

flag: crypto{inmatrix}

ROUNDS KEYS

We’re going to skip over the finer details of the KeyExpansion phase for now. The main point is that it takes in our 16 bytes key and produces 11 4x4 matrices called “round keys” derived from our initial key. These round keys allow AES to get extra mileage out of the single key that we provided.

The initial ket addition phase, which is next, has a single AddRoundkey step. The AddRoundKey step is straightforward: it XORs the current state with the current round key.

Imgur

AddroundKey also occurs á the final step of each round. AddRoundKey is what makes AES a “keyed permutation” rather than just a permutation. It’s the only part of AES where the key i mixed into the state, but is crucial for determining the permutation that occurs.

As you’ve seen in previous challenges, XOR is an esaily invertible operation if you know the key, but tough to undo if you don’t. Now imagine trying to recover plaintext which has been XOR’d with 11 different keys, and heavily jumbles between each XOR operation with a series of substitution and transposition ciphers. That’s kinda what AES does ! Ans we’ll see just how effective the jumbling is in the next few challenges.

Complete the add_round_key function, then use the matrix2bytes function to get your next flag.

This is my sol:

state = [
    [206, 243, 61, 34],
    [171, 11, 93, 31],
    [16, 200, 91, 108],
    [150, 3, 194, 51],
]

round_key = [
    [173, 129, 68, 82],
    [223, 100, 38, 109],
    [32, 189, 53, 8],
    [253, 48, 187, 78],
]


def add_round_key(s, k):
    ss=""
    for i in range(0,4):
        for j in range(0,4):
            c = s[i][j] ^ k[i][j]
            ss = ss + chr(c)
    return ss

print(add_round_key(state, round_key))


Flag: crypto{r0undk3y}

The first step of each AES round is SubBytes. This involves talking each byte of the state matrix and substituting it for a different byte in a preset 16x16 lookup table. The lookup table is called a Substitution box or S-box for short, and can be perplexing at first sight. Let’s break it down.

Imgur

In 1945 American mathematician Claude Shannon published a groundbreaking paper on Information Theory. It identified “confusion” as an essential property of a secure cipher. “Confusion” means that the relationship between the ciphertext and the key should be as complex as possible. Given just a ciphertext, there should be no way to learn anything about the key.

If a cipher has poor confusion, it is possible tp express a relationship between ciphertext, key, and plaintext as a linear function. For instance, in a Caesar cipher, ciphertext = plaintext + key. That’s an obvious relation, which is easy to reverse. More complicated linear transformations can be solved using techniques like Gaussian elimination. Even low-degree polunomials, e.g. an equation like $x^4+51x^3+x$, can be solved efficiently using algebraic methods. However, the higher the degree of a polynomial, generally the harder it becoms to solve - it can only be approximated by a larger and larger amount of linear functions.

The main purpose of the S-box is to transform the input in a way that is resistant to being approximated by linear function. S-boxes are aiming for high non-linearity, and while AES’s one is not perfect, it’s pretty close. The fast lookup in an S-box is a shorcut for performing a very nonlinear function on the input bytes. This function involves taking the modular inverse in the Galois field 2**8 and the applying an affine transformation which has been tweaked for maximum confusion. The simplest way to express the function is through the following high-degree polynomial:

Imgur

To make the S-box, the function has been calculated on all input value from 0x00 to 0xff and the outputs put in the lookup table

Implement sub_bytes, sned the state matrix through the inverse S-box and then convert it to bytes to get the flag.

To understand more about S-box use can see here

This is my solution:

s_box = (
    #0    1     2     3      4     5     6     7      8     9     A    B    C      D    E      F
#0  0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
#1  0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
#2  0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
#3  0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
#4  0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
#5  0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
#6  0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
#7  0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
#8  0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
#9  0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
#A  0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
#B  0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
#C  0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
#D  0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
#E  0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
#F  0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)

inv_s_box = (
#     0      1      2     3     4     5     6     7     8     9     A    B     C     D     E      F  
#0    0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
#1    0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
#2    0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
#3    0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
#4    0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
#5    0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
#6    0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
#7    0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
#8    0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
#9    0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
#A    0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
#B    0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
#C    0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
#D    0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
#E    0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
#F    0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)

state = [
    [251, 64, 182, 81],
    [146, 168, 33, 80],
    [199, 159, 195, 24],
    [64, 80, 182, 255],
]

state_hex = [
    [0xfb,0x40,0xb6,0x51],
    [0x92,0xa8,0x21,0x50],
    [0xc7,0x9f,0xc3,0x18],
    [0x40,0x50,0xb6,0xff],
]

init = [
    [0x63,0x72,0x79,0x70],
    [0x74,0x6f,0x7b,0x6c],
    [0x31,0x6e,0x33,0x34],
    [0x72,0x6c,0x79,0x7d]
]

def sub_bytes(s, sbox=s_box):
    pass


# print(sub_bytes(state, sbox=inv_s_box))
# print(s_box[0])

for i in range(0,4):
    for j in range(0,4):
        state[i][j]=hex(state[i][j])
for i in range(0,4):
    print(state[i][0]," ",state[i][1]," ",state[i][2]," ",state[i][3])
flag = ""
for i in range(4):
    for j in range(4):
        flag+=chr(init[i][j])
print(flag) 

flag: crypto{l1n34rly}

DIFFUSION THROUGH PERMUTATION

We’ve seen how S-box substitution provides confusion. The other crucial property described by Shannon is “diffusion” [khuếch tán]. This relates to how every part of a cipher’s input should spread to every part of the output.

Substitution on its own creates non-linearity, however it doesn’t distribute it over the entire state. Without diffusion, the same byte in the same position would get the same transformations applied to it each round. This would allow cryptanalysts to attacj each byte position in the state matrix separately. We need to alternate substitution by scrambling the state (in an invertible way) so that substitutions applied on one byte influence all other bytes in the state. Each input into the next S-box then cecomes a function of multiple bytes, meaning that with every round the algebraic complexity of the system increases enormously.

Note: An ideal amount of diffusion causes a change of one bit in the pliantext to lead to a change in statiscally half the bits of the ciphertext. This desirable outcome is called the Avalanche effect

The ShiftRows and MixColumns steps combine to achieve this. They work together to ensure every byte affests other byte in the state within just two rounds.

ShiftRows is the most simple transformation in AES. It keeps the first row of the state matrix the same. The second row is shifted over one column to the left, wrapping around. The third row is shifted two columns, the fourth row by three. Wikipedia puts it nicely: “the importance of this step is to avoid the columns being encrypted independently, in which case AES degenerates into four independent block ciphers.”

Imgur

MixColumns is more complex. It performs Matrix multiplication in Rijndael’s Galois fiels between the columns of the state matrix and a preset matrix. Each single byte of each column therefore affests all the bytes of the resulting column. The implementation details are nuancedl this page and wiki do a good job of covering them

Imgur

We’ve provided code to perform MixColumns and the forward ShiftRows operation. After implementing 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.

This is my sol:

def shift_rows(s):
    s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][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[3][3], s[0][3], s[1][3], s[2][3]
    # return s


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[3][3], s[0][3], s[1][3], s[2][3]
    # return s


# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)


def mix_single_column(a):
    # see Sec 4.1.2 in The Design of Rijndael
    t = a[0] ^ a[1] ^ a[2] ^ a[3]
    u = a[0]
    a[0] ^= t ^ xtime(a[0] ^ a[1])
    a[1] ^= t ^ xtime(a[1] ^ a[2])
    a[2] ^= t ^ xtime(a[2] ^ a[3])
    a[3] ^= t ^ xtime(a[3] ^ u)
    # return a


def mix_columns(s):
    for i in range(4):
        mix_single_column(s[i])


def inv_mix_columns(s):
    # see Sec 4.1.3 in The Design of Rijndael
    for i in range(4):
        u = xtime(xtime(s[i][0] ^ s[i][2]))
        v = xtime(xtime(s[i][1] ^ s[i][3]))
        s[i][0] ^= u
        s[i][1] ^= v
        s[i][2] ^= u
        s[i][3] ^= v

    mix_columns(s)
    # return s


state = [
    [108, 106, 71, 86],
    [96, 62, 38, 72],
    [42, 184, 92, 209],
    [94, 79, 8, 54],
]
# inv_shift_rows(state)
inv_mix_columns(state)
inv_shift_rows(state)
print(state)
flag = ""
for i in range(4):
    for j in range(4):
        flag+=chr(state[i][j])
print(flag)
# flag: crypto{d1ffUs3R}

And the result is: cryUto{}1ffps3Rd

So we need rearrange it and flag is: crypto{d1ffUs3R}

Bringing It All Together

Apart from the KeyExpansion phase, we’ve sketched out all the components of AES. We’ve shown how SubBytes provides confusion and ShiftRows and MixColumns provide diffusion, and how these two properties work together to repeatedly circulate non-linear transformations over the state. Finally, AddRoundkey seeds the key into this substitution-permutation network, making the cipher a keyed permutation.

Decryption involves performing the steps described in the “Structure of AES” challenge in reverse, applying the inverse operations. Note that the KetExpansion still nees to be run first, and the round keys will be used in reverse order. AddRoundKey and its inverse are identical as XOR has the self-inverse property.

We’ve provided the key expansion code, and ciphertext that’s been properly encrypted by AES-128. Copy in all the building blocks you’ve coded so far, and complete the decrypt function that implements the steps shown in the diagram. The decrypted plaintext is the flag.

Yes, you can cheat on this challenge, but where’s the fun in that ?

The code used in these exercies has been taken from Bo Zhu’s super simple Python AES implementation, so we’ve reproduced

This is my solution !

N_ROUNDS = 10

key        = b'\xc3,\\\xa6\xb5\x80^\x0c\xdb\x8d\xa5z*\xb6\xfe\\'
ciphertext = b'\xd1O\x14j\xa4+O\xb6\xa1\xc4\x08B)\x8f\x12\xdd'

def inv_shift_rows(s):
    s[1][1], s[2][1], s[3][1], s[0][1] = s[0][1], s[1][1], s[2][1], s[3][1]
    s[2][2], s[3][2], s[0][2], s[1][2] = s[0][2], s[1][2], s[2][2], s[3][2]
    s[3][3], s[0][3], s[1][3], s[2][3] = s[0][3], s[1][3], s[2][3], s[3][3]
    # return s


# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
def mix_single_column(a):
    # see Sec 4.1.2 in The Design of Rijndael
    t = a[0] ^ a[1] ^ a[2] ^ a[3]
    u = a[0]
    a[0] ^= t ^ xtime(a[0] ^ a[1])
    a[1] ^= t ^ xtime(a[1] ^ a[2])
    a[2] ^= t ^ xtime(a[2] ^ a[3])
    a[3] ^= t ^ xtime(a[3] ^ u)
    # return a


def mix_columns(s):
    for i in range(4):
        mix_single_column(s[i])


def inv_mix_columns(s):
    # see Sec 4.1.3 in The Design of Rijndael
    for i in range(4):
        u = xtime(xtime(s[i][0] ^ s[i][2]))
        v = xtime(xtime(s[i][1] ^ s[i][3]))
        s[i][0] ^= u
        s[i][1] ^= v
        s[i][2] ^= u
        s[i][3] ^= v

    mix_columns(s)

def add_round_key(s, k):
    return [[sss^kkk for sss, kkk in zip(ss, kk)] for ss, kk in zip(s, k)]
s_box = (
  0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
  0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
  0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
  0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
  0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
  0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
  0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
  0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
  0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
  0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
  0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
  0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
  0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
  0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
  0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
  0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)
inv_s_box = (
    0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
    0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
    0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
    0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
    0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
    0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
    0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
    0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
    0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
    0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
    0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
    0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
    0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
    0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
    0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
    0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)
def inv_sub_bytes(s, sbox=inv_s_box):
    lst1= list(map(lambda x: sbox[x], sum(s, [])))
    m = []
    while lst1 != []:
        m.append(lst1[:4])
        lst1 = lst1[4:]
    return m
        
def bytes2matrix(text):
    """ Converts a 16-byte array into a 4x4 matrix.  """
    return [list(text[i:i+4]) for i in range(0, len(text), 4)]

def matrix2bytes(matrix):
    """ Converts a 4x4 matrix into a 16-byte array.  """
    s=""
    for i in range(0,len(matrix)):
        tmp_list = matrix[i]
        for _ in tmp_list:
            s=s+chr(_)
    print("flag: ",s)
def expand_key(master_key):
    """
    Expands and returns a list of key matrices for the given master_key.
    """

    # Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants
    r_con = (
        0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
        0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
        0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
        0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
    )

    # Initialize round keys with raw key material.
    key_columns = bytes2matrix(master_key)
    iteration_size = len(master_key) // 4

    # Each iteration has exactly as many columns as the key material.
    i = 1
    while len(key_columns) < (N_ROUNDS + 1) * 4:
        # Copy previous word.
        word = list(key_columns[-1])

        # Perform schedule_core once every "row".
        if len(key_columns) % iteration_size == 0:
            # Circular shift.
            word.append(word.pop(0))
            # Map to S-BOX.
            word = [s_box[b] for b in word]
            # XOR with first byte of R-CON, since the others bytes of R-CON are 0.
            word[0] ^= r_con[i]
            i += 1
        elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
            # Run word through S-box in the fourth iteration when using a
            # 256-bit key.
            word = [s_box[b] for b in word]

        # XOR with equivalent word from previous iteration.
        word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size]))
        key_columns.append(word)

    # Group key words in 4x4 byte matrices.
    return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]

def convert_bytes(s):
    tmp_list=[]
    for i in range(4):
        for j in range(4):
            tmp_list.append(s[i][j])
    st = ""
    for i in range(len(tmp_list)):
        st+=chr(tmp_list[i])
    return st.encode('utf-8')
def decrypt(key, ciphertext):
    round_keys = expand_key(key) # Remember to start from the last round key and work backwards through them when decrypting
    print(len(round_keys))
    needed_round_keys = round_keys[10]
    # Convert ciphertext to state matrix
    ciphertext_matrix = bytes2matrix(ciphertext)
    init = add_round_key(ciphertext_matrix,needed_round_keys)
    # print(init)
    # print(u)
    # Initial add round key step
    dem = 9
    for i in range(N_ROUNDS - 1, 0, -1):
        print("Round",i,":")
        inv_shift_rows(init)
        init = inv_sub_bytes(init,sbox=inv_s_box)
        init = add_round_key(init,round_keys[dem])
        dem-=1
        inv_mix_columns(init)


    # print(dem)
    # Run final round (skips the InvMixColumns step)


    inv_shift_rows(init)
    init = inv_sub_bytes(init,sbox=inv_s_box)
    init = add_round_key(init,round_keys[dem])


    # Convert state matrix to plaintext
    print(init)
    # return plaintext
    plaintext=""
    for i in range(4):
        for j in range(4):
            plaintext+=chr(init[i][j])
    print(plaintext)


# print(decrypt(key, ciphertext))
decrypt(key,ciphertext)
# u = bytes2matrix(key)
# print(u)

flag: crypto{MYAES128}

SYMMETRIC STARTER

Modes of Operation Starter

The previous set of challenges showed how AES performs a keyed permutation on a block of data. In pratice, we need to encrypt message much longer than a single block. A mode of operation describes how to use a cipher like AES on longer messages.

All modes have serious weaknesses when used incorrectly. The challenges in this category take you to a different section of the website where you can interact with APIs and exploit those weaknesses. Get yourself acquainted with the interface and use it to take your next flag !

Play at Link

This is my solution:

Imgur

flag: crypto{bl0ck_c1ph3r5_4r3_f457_!}

PASSWORDS AS KEYS

It is essential that keys in symmetric-key algorithm are random bytes, instead of passwords or other predictable data. The random bytes should be generated using a cryptographically-secure pseudorandom number generator (CSPRNG). If the keys are predictable in any way, then the security level of the cipher is reduced and it may be possible for an attacker who gets access to the ciphertext to decrypt it.

Just because a key looks like it is formed of random bytes, does not mean that it necessarily is. In this case the key has been derived from a simple password using a hashing function, which makes the ciphertext crackable.

For this challenge you may script your HTTP requests to the endpoints, or alternatively attack the ciphertext offline. Goodluck !

This is my solution:

from Crypto.Cipher import AES
import hashlib
import random
with open("dict.txt") as f:
    words = [w.strip() for w in f.readlines()] 
keyword = random.choice(words)
KEY = hashlib.md5(keyword.encode()).digest()
# print(words[0])
def solve():
    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()}
    cipher = "c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66"
    
    # print(KEY)
    
    # print(ans_tmp)
    for i in range(0,len(words)):
        keyword = words[i]
        KEY = hashlib.md5(keyword.encode()).digest()
        res = decrypt(cipher,KEY.hex())
        ans_tmp = res['plaintext']
        res = ''.join([chr(int(''.join(c), 16)) for c in zip(ans_tmp[0::2],ans_tmp[1::2])])
        if(res[0]=='c' and res[1]=='r' and res[2]=='y'):
            print(res)
            break
        else:
            print('N')
solve()
flag: crypto{k3y5__r__n07__p455w0rdz?}

and some tip to convert betweeen hex and bytes:

def hex_to_bytes():
    txt = "63727970746f7b6b3379355f5f725f5f6e30375f5f70343535773072647a3f7d"
    res = ''.join([chr(int(''.join(c), 16)) for c in zip(txt[0::2],txt[1::2])])
    return res
def bytes_to_hex():
    txt= b'\xbe\x86Y\x14\xe5\x8dl\x1ei\xc1\x92>\xfa\x08?\xfa'
    res = txt.hex()
    print(res)
# bytes_to_hex()
u = hex_to_bytes()
print(u)

BLOCK CIPHERS

ECB CBC WTF

Here you can encrypt in CBC but only decrypot in ECB. That shouldn’t be a weakness because they’re different modes… right ?

Imgur

This is my sol:

import requests


def xor_two_string(u,v):
    res = hex(int(u, 16) ^ int(v, 16))
    ans = str(res[2:len(res)])
    return ans
def hex_to_bytes(txt):
    res = ''.join([chr(int(''.join(c), 16)) for c in zip(txt[0::2],txt[1::2])])
    return res
def bytes_to_hex(txt):
    res = txt.hex()
    return res

BASE_URL = "http://aes.cryptohack.org/ecbcbcwtf"

r = requests.get(f"{BASE_URL}/encrypt_flag")
data = r.json()
ciphertext = data["ciphertext"]

# Because iv=os.urandom(16) ==> 32 ki tu dau trong ciphertext la iv 

iv = ciphertext[:32]

# ciphertext = iv + encrypted 
# ciphertext has 96 ki tu = 32 ki tu cua iv + 64 ki tu cua cipher
# Suy ra co hai block cipher => cipher = cipher1 + cipher 2 
# Suy ra co 2 plaintext 
# Dieu chu y can nho
# plaintext = plaintext 1 + plaintext 2 + ... + plaintext n
# cipher = cipher 1 + cipher 2+...+cipher n


cipher1 = ciphertext[32:64]
cipher2 = ciphertext[64:96]


r = requests.get(f"{BASE_URL}/decrypt/{cipher1}")
data = r.json()
decrypted_cipher1 = data["plaintext"]
plaintext1 = xor_two_string(decrypted_cipher1,iv)


r = requests.get(f"{BASE_URL}/decrypt/{cipher2}")
data = r.json()
decrypted_cipher2 = data["plaintext"]
plaintext2 = xor_two_string(decrypted_cipher2,cipher1)


flag = hex_to_bytes(plaintext1) + hex_to_bytes(plaintext2)

print("flag: ",flag)


flag: crypto{3cb_5uck5_4v01d_17_!!!!!}

ECB ORACLE

ECB is the most simple mode, with each plaintext block encrypted entirely independently. In this case, your input is prepended to the secret flag and encrypted and that’s it. We don’t even provide a decrypt function. Perhaps you don’t need a padding oracle when you have an “ECB oracle”

Imgur

This is my solve:

import requests 
# f = open("out.txt","w")
def char_to_hex(txt):
    u = format(txt, "x")
    if(len(u)==1): 
        u='0'+u
    return u
BASE_URL = "http://aes.cryptohack.org/ecb_oracle"
list_64 = []
for i in range(1,33):
    r = requests.get(f"{BASE_URL}/encrypt/{'aa'*i}")
    data = r.json()
    ciphertext = data["ciphertext"]
    list_64.append(ciphertext)
    # f.write(ciphertext+'\n')
pre_cipher = []
for i in range(0,32):
    pre_cipher.append(list_64[i][:64])
flag_cipher = list_64[31][64:128]
# for _ in pre_cipher:
#     print(_)
print(flag_cipher)
res = ""
plaintext = 'a'*64
for _ in range(0,31):
    root = plaintext[2:64]
    for ch in range(32,127):
        current_char = char_to_hex(ch)
        plaintext_send = root + current_char
        # print(plaintext_send)
        r = requests.get(f"{BASE_URL}/encrypt/{plaintext_send}")
        data = r.json()
        ciphertext = data["ciphertext"][:64]
        # print("aka1: ",ciphertext)
        # print("aka2: ",pre_cipher[15-(_+1)])
        if(ciphertext==pre_cipher[31-(_+1)]):
            res=res+chr(ch)
            plaintext = plaintext_send 
            print(res)
            break

print(res)
#flag: crypto{p3n6u1n5_h473_3cb}




# def test():
#     plaintext = "" 
#     for _ in flag:
#         plaintext+=char_to_hex(ord(_))
#     # print(plaintext)
#     r = requests.get(f"{BASE_URL}/encrypt/{plaintext}")
#     data = r.json()
#     ciphertext = data["ciphertext"][:32]
#     print(ciphertext)
# test()
        

flag: crypto{p3n6u1n5_h473_3cb}

And this my template created by Khoa:

import requests
import string

def encrypt(msg):
    BASE_URL = "http://aes.cryptohack.org/ecb_oracle"
    r = requests.get(f"{BASE_URL}/encrypt/{msg.encode().hex()}")
    data = r.json()
    ciphertext = data["ciphertext"]
    return ciphertext

CHARSET = string.printable

flag = '#' * 16
for i in range(len(flag) - 16, 60):
    tmp = encrypt('#' * (16 - i%16 - 1 + 16))[(i//16 + 1) * 32:(i//16 + 2) * 32 ]
    check = False
    for char in CHARSET:
        msg = flag[-15:] + char
        assert len(msg) == 16
        recv = encrypt(msg)[0:32]
        if recv == tmp:
            flag += char
            print(flag[16:])
            check = True
            break
    if not flag:
        print('error')
        exit()

print(flag[16:])


You can get a cookie for my website, but it won’t help you read the flag … I think

In this problem, we will introduce the new technique: The Bit attack

Decryption process in CBC mode is performed as:

  • $P_1 = Dec_k(C_1)\oplus IV$

  • $P_i = Dec_k(C_i)\oplus C_{i-1},1<i\le nb$

where nb is the number of blocks.

If you know the position of the target byte, then you can modify the corresponding ciphertext position in the previous ciphertext block. For example; if you modify a byte in the ciphertext $C_{i-1}$, then $P_i$ will be changed by one block since $C_{i-1}$ only affects the plainttext $P_i$ by $\oplus$. We can see visually in the below figure;

Imgur

Red case: A ciphertext byte of $C_2$ modified. This affects the corresponding byte in the next plaintext block $P_3$ and the corresponding full plaintext block $P_2$ which has the same index as the modified ciphertext which is garbage. This can be seen as there is an error.

Green case: an IV byte is modified (green), this affects only the corresponding byte in the first plaintext $P_1$. If the target plaintext is in the first block, this will not leave a trace,

An example

Consider this simple message

msg = "Buy 1000 lots of waffles"

Now the attacker intercepts the message and now the default structure and want to modify it into msg = "Buy 5000 lots of waffles"

Here is the sample Python code:

def bitFlip( pos, bit, data):
    raw = b64decode(data)

    list1 = list(raw)
    list1[pos] = chr(ord(list1[pos])^bit)
    raw = ''.join(list1)
    return b64encode(raw)

With c call ctx = bitFlip(4,4,ctx) changes the 1 into 5

This is the green case attack, that leaves no garbage block. Some file formats, like PDF, can live with the red case attack.

You can see more at link

This is my reference code :

import requests
import json
from Crypto.Util.strxor import strxor

def get_cookie():
	url='https://aes.cryptohack.org/flipping_cookie/get_cookie/'
	response=requests.get(url)

	return response.json()

def get_flag(cookie,iv):
	url='https://aes.cryptohack.org/flipping_cookie/check_admin/'
	response=requests.get(url+'/'+cookie+'/'+iv)

	return response.json()

data=get_cookie()['cookie']
iv=bytes.fromhex(data[:32])

text=b'admin=False;expi'  #taking only the initial block... since that's only required
forge_text=b'admin=True;expir'

xor_result=strxor(iv,text)
forge_iv=strxor(xor_result,forge_text).hex()

print(get_flag(data[32:],forge_iv)["flag"])
crypto{4u7h3n71c4710n_15_3553n714l}

Also, I found a source on this github about cryptohack: Link

To understand more about this technique, you see this picture:

Imgur