Symmetric Cryptography

2025-07-29 2026-10-17364 Words

Structure of AES

KeyExpansion

In this phases, the 11 separate 128 bit roundkey are derived from our input key (with AES-128). Every one to be used in each AddRoundKey step.

AddRoundKey

The bytes of the first roundkey are XOR'd with the bytes of the initial state.

Round

Confusion

Keep others from knowing or understanding what happened.

  1. SubBytes - each byte of the state is substituted for a different byte according to a lookup table (S-box).

    2025-07-30_00-47-24_screenshot.png

    According to Shannon's Information Theory, for the secure cipher, just with given ciphertext, we have no way to learn anything about the key. Otherwise, we can express the relationship between ciphertext and key. For example, the Caesar cipher with ciphertext = plaintext + key relationship.

    For the linear transformations can be solved using techniques like Gaussian elimination. Even low-degree polynomials, e.g. an equation like \(x^4 + 51x^3 + x\) can be solved efficiently by algebraic methods. However, the high-degree of a polynomials can only be approximated by a larger and larger amount of linear functions.

Diffusion

Cause change of one bit in the plaintext to lead to a change as much as possible the bits of the ciphertext

  1. ShiftRows - the last three rows of the state matrix are transposed -> shifted over a column or two or three.

    2025-07-30_13-43-14_screenshot.png
  1. MixColumns - matrix multiplication is performed on the columns of the state, combining the four bytes in each column. This is skipped in the final round.

    2025-07-30_13-55-02_screenshot.png
  1. AddRoundKey - the bytes of the current roundkey are XOR'd with the bytes of the state.

Encrypt Flow Chart

aes_encrypt-2025-07-31-1536.svg

Decrypt Flow Chart

aes_decrypt-2025-07-31-1536.svg

Padding

PKCS#7 scheme:

  • If the length of plaintext can not devisible by blocksize(ex: 16 for AES128), padding with
plaintext + bytes(block_size - (length % block_size)) *  (block_size - (length % block_size))
example:
plaintext + '\x03\x03\x03'
  • If the length of plaintext can divisible by blocksize, padding with:
bytes(block_size) * (block_size)
example:
'\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10' for AES128

Modes of Operation

ECB

2025-07-31_16-07-49_screenshot.png

Property: Blocks encrypt/decrypt independently.

Weakness or Features?

Same input will produce same ouput.

Copy & Paste Attack
aes-ecb-copy-paste.gif
Prepend Oracle Attack
aes-ecb-prepend-oracle.gif
import os
from Crypto.Cipher import AES   # pycryptodome
from secret import flag         # server side flag

key = os.urandom(16)
aes = AES.new(key, AES.MODE_ECB)

# PKCS #7
def pad(data):
    return data + bytes([-len(data) % 16] * (-len(data) % 16))

if __name__ == '__main__':
    print('=== Super Signer ===')
    while True:
        data = input('input you message (hex)> ')
        signed = aes.encrypt(pad(bytes.fromhex(data) + flag))
        print(f'Signed: {signed.hex()}')

CBC

2025-07-31_16-10-17_screenshot.png

plaintext1 = IV ^ decrypt(CIPHERBLOCK 1)

Bit Flipping Attack

aes-cbc-bit-flipping.gif

Prerequires: Known Block 1 and controlled IV. Target: Forge the plaintext of the block2.

import os
from Crypto.Cipher import AES
from secret import flag

key, iv = os.urandom(16), os.urandom(16)

def encrypt(data):
    aes = AES.new(key, AES.MODE_CBC, iv)
    return aes.encrypt(data)

def decrypt(data):
    aes = AES.new(key, AES.MODE_CBC, iv)
    return aes.decrypt(data)

if __name__ == '__main__':
    chal = os.urandom(32)
    print(f'original message: {chal.hex()}')
    print(f'encrypted: {encrypt(chal).hex()}')
    shot = input('one shot> ')
    result = decrypt(bytes.fromhex(shot))
    if b'this_is_a_secret' in result:
        print(flag)
    else:
        print('failed...')

plaintext2 = ciphertext1 ^ decrypt(ciphertext2)
new_iv = plaintext2 ^ ciphertext1 ^ fake_message
       = decrypt(ciphertext2) ^ fake_message

Now, this new_iv will be used as 'ciphertext1' to send back to server. (new_iv + ciphertext2).

Padding Oracle Attack

aes-cbc-padding-oracle.gif

Prerequires: User control the input IV. Target: leak plaintext.

import os
from Crypto.Cipher import AES
from secret import flag

key = os.urandom(16)

def encrypt(iv, data):
    aes = AES.new(key, AES.MODE_CBC, iv)
    return aes.encrypt(data)

def decrypt(iv, data):
    aes = AES.new(key, AES.MODE_CBC, iv)
    return aes.decrypt(data)

def pad(data):
    return data + bytes([-len(data) % 16] * (-len(data) % 16))

def unpad(data):
    padlen = int(data[-1])
    if not (bytes(data[-1]) * padlen == data[-padlen:]):
        raise ValueError
    else:
        return data[:-padlen]

if __name__ == '__main__':
    cur_iv = os.urandom(16)
    print(f'encrypted flag: {cur_iv.hex() + encrypt(cur_iv, pad(flag)).hex()}')
    while True:
        msg = input('your message(hex)> ')
        msg = bytes.fromhex(msg)
        try:
            unpad(decrypt(msg[:16], msg[16:]))
            print('yes.')
        except ValueError:
            print('no.')

Solve:

from pwn import *
# IMPORTANT: plaintext_i = iv(c_(i-1)) ^ dec(c_i)
io = remote('localhost', 1337)
io.recvuntil(b'flag: ')
enc_flag = bytes.fromhex(io.recvline(drop=True))

iv, enc_flag = enc_flag[:16], enc_flag[16:]
info(f'iv: {iv.hex()}, enc_flag: {enc_flag.hex()}')

flag = b''

def padding_oracle(msg):
    io.sendine(msg.hex().encode())
    return b'yes' in io.recv()

for i in range(16):
    for j in range(256):
        cur_iv = iv[:16-i-1] + bytes([j]) + xor(xor(flag, iv[16-i:]), bytes([i+1]*(i)))
        if padding_oracle(cur_iv + enc_flag):
            flag = xor(xor(bytes([j]), iv[16-i-1]), bytes([i+1])) + flag
            break
    info(f'current flag: {flag}')

OFB (Stream Cipher)

2025-07-31_16-11-07_screenshot.png

The advantage of stream ciphers is that they can work plaintext of arbitrary length, with no padding required.

CTR (Stream Cipher)

2025-07-31_16-13-23_screenshot.png

Creator: Emacs 32.0.50 (Org mode 10.0-pre)