ECB Byte at a time attack.

Information

  • category : crypto
  • points : 50

Description

(Byte Me) if you can nc crypto.chal.csaw.io 1003

Writeup

Ok let’s try to connect to server and see what this challenge is about (I’ll mark # the comment):

nc crypto.chal.csaw.io 1003
23ace1ee2679fa7c81ccf66a865e98c5e80c9208a206b78776a8dd16f5ea1457f44c8a9882d7f440fc2b4aad213b65436510556e7c2f57422a959ea2b1b0c8ba

Tell me something:							# With empty input I receive the same ciphertext
23ace1ee2679fa7c81ccf66a865e98c5e80c9208a206b78776a8dd16f5ea1457f44c8a9882d7f440fc2b4aad213b65436510556e7c2f57422a959ea2b1b0c8ba

Tell me something: a						# The ciphertext of a is different from the ciphertext of b  
cd49af33294afc9b843bb713517f12bd89d92e5b0c9244d5d71d33815a4e8cf7adc474c0f5e77546a43732488e5ce6afdfeca90bd8711ba453f4bedd051902da

Tell me something: b
b769ee2ef66a76351a62a2e38dc38f1989d92e5b0c9244d5d71d33815a4e8cf7adc474c0f5e77546a43732488e5ce6afdfeca90bd8711ba453f4bedd051902da

Tell me something: aaa
38ae0e0c0f19f15a55eee646ba888c4e73dd7e606cc6755f22e3e5d9e5b22bede4eca07d36503b4d12f3a2228df1d40063217add6089a5357875c46ff0457291

Tell me something: bbb
282e6c3ac5cf6f1b367b37056332b781921ee57568816ccdf6a1889c736030c7e4eca07d36503b4d12f3a2228df1d40063217add6089a5357875c46ff0457291

Tell me something: aaaaaaaaa				# The first block of the enc(aaa) is equal to the first block of enc(aaaaaaaaa) where enc is the encryption
38ae0e0c0f19f15a55eee646ba888c4ed87f273b983ee8b13f1d5f48bcd7197e333191ae17102fdfe97131dd0e9b82bccc2be3d21f1a8fa27c19af26aa77f9f59eaf3341c8216c7e42ae21e7fc665881

Tell me something: bbbbbbbbb				# From this block we can deduce that the blocksize of the ciphertext is 16 bytes = 32 hex digits
282e6c3ac5cf6f1b367b37056332b781e034117ceda52b3bf2ed14b57f088803333191ae17102fdfe97131dd0e9b82bccc2be3d21f1a8fa27c19af26aa77f9f59eaf3341c8216c7e42ae21e7fc665881

Tell me something: aaaaaaabbbb				# As expected the first block of enc(aaaaaaabbbb) = enc(aaaaaaaxxxx)
38ae0e0c0f19f15a55eee646ba888c4e1bc7b1e266a3ece24d184a2080e66271557f58d5358319774b16842b0fd34189a6dedbe100d30fe9a356f73cfa61439699b14bfc188dd46879f8e53c851e82b6

Tell me something: aaaaaaaxxxx
38ae0e0c0f19f15a55eee646ba888c4e9fbbac9bf0b2a35bd1d46db6e809d964557f58d5358319774b16842b0fd34189a6dedbe100d30fe9a356f73cfa61439699b14bfc188dd46879f8e53c851e82b6

# Let's see if the ciphertext is using ECB as mode of operation
Tell me something: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
38ae0e0c0f19f15a55eee646ba888c4eed4605b6ad00c4151179392e6436f97eed4605b6ad00c4151179392e6436f97eed4605b6ad00c4151179392e6436f97eed4605b6ad00c4151179392e6436f97eed4605b6ad00c4151179392e6436f97ef6886e8837caefd9dc554fd9a6cb78d014381d2cdad87676ef62fe39851139e43d51a965425508ed39911610b213351b5ad5764d547113d60305dfbbcf5db01d

From the the output above we see that the server it’s encrypting every plaintext taken in input, so we can craft a Chosen Plaintext Attack (CPA).

“Fuzzing” the server with some requests we can deduce:

  1. It’s using ECB as encryption scheme because there are repetitive blocks from a ciphertext derived from a repetitive plaintext.
  2. It’s add random byte at the start because enc(aaaaaaabbbb)[:First_Block] = enc(aaaaaaaxxxx)[:First_Block].
  3. The blocksize of the PRF/PRP is 16 bytes = 32 hex digits.
  4. The last blocks are always the same, so probably it’s appending the encryption of (flag || pad).

To confirm all this deduction I started writing an exploit :

import sys
from pwn import *

class Oracle():
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.conn = remote(self.host, self.port)
    
    def receive_enc_flag(self):
        flag = self.conn.recvline()
        return to_byte(flag)

    def receive_enc_pt(self, pt):
        self.conn.recvuntil('something: ')
        self.conn.sendline(pt)
        self.conn.recvline()
        s = self.conn.recvline()
        return to_byte(s)

    def compute_bsize(self):
        plaintext = b''
        # aes-ecb(key, junk || unknown-string = flag || pad)
        initial_len = len(self.receive_enc_pt(plaintext))
        final_len = initial_len
        # aes-ecb(key, junk || plaintext || unknown-string = flag || pad)
        while final_len == initial_len:
            plaintext += 'a'
            final_len = len(self.receive_enc_pt(plaintext)) 
        return (final_len - initial_len)
    
    def compute_junk(self, blocksize):
        ciphertexts = []
        ciphertexts.append(self.receive_enc_pt('')[:blocksize])
        # Incrementing the plaintext of one 'a' at every iteration
        # There will be 2 consecutive first block of the ciphertexts
        # equal. From there I can deduce the size of the junk
        for i in range(1, 17):
            ciphertexts.append(self.receive_enc_pt('a' * i)[:blocksize])
            if ciphertexts[i] == ciphertexts[i - 1]:
                return 17 - i
        # 16 or 0 is indifferent!
        return 0

def to_byte(s):
    # Remove \n\r
    return s[:-2].decode("hex")

def identify_ecb(ct, blocksize):
    blocks = [ct[i: i + blocksize] for i in range(0, len(ct), blocksize)]
    for i in range(0, len(blocks)):
        for j in range(i+1, len(blocks)-1):
            if blocks[i] == blocks[j]:
                return 1
    return None

def main():
    host = 'crypto.chal.csaw.io'
    port = '1003'
    oracle = Oracle(host, port)
    flag_enc = oracle.receive_enc_flag()

    # Compute blocksize of the cipher --> 16 
    blocksize = oracle.compute_bsize()
    log.info("blocksize : " + str(blocksize))

    # Compute junk size --> Random
    junk = oracle.compute_junk(blocksize)
    log.info("junk size : " + str(junk))

    # Check if the cryptosystem is using ECB --> YES
    enc_pt = oracle.receive_enc_pt('a' * 100)
    if identify_ecb(enc_pt, blocksize) == None:
        log.info("ecb : false\nleaving...")
        exit(1) 
    log.info("ecb : true")
 
if __name__ == '__main__':
    main()
$ python2 exploit.py
[+] Opening connection to crypto.chal.csaw.io on port 1003: Done
[*] blocksize : 16
[*] junk size : 1
[*] ecb : true
$ python2 exploit.py
[+] Opening connection to crypto.chal.csaw.io on port 1003: Done
[*] blocksize : 16
[*] junk size : 4
[*] ecb : true

How can we decrypt the flag ?

Well ECB is not CPA secure because is a deterministic encryption (not randomized).

Let’s wee how we can decrypt the flag using a CPA called byte at a time.

If you have trouble understanding my scheme check this repository or cryptopals set 2 chall 12.

Now we need just to write the function to decrypt each character of the flag :

def get_secret(self, blocksize, part_secret, junk):
	# Length of plaintext must be between 0 and 15, so % blocksize = 16
	length_plaintext = (blocksize - junk - (1 + len(part_secret))) % blocksize
	# Length to crack is always length_plaintext + len(secret that I know) + 1
	# After the first block has been decrypted, we need to reconstruct the length_plaintext
	# to 15, and add to it during the verification (if) the part_secret 
	length_to_crack = length_plaintext + junk + len(part_secret) + 1
	# Create plaintext
	plaintext = 'a' * length_plaintext
	# Ciphertext to compare
	ciphertext = self.receive_enc_pt(plaintext)
	# Let's create a charset of all possible ascii values (readable)
	charset = "qwertyuiopasdfghjklzxcvbnm"
	charset += charset.upper()
	charset += "_{}0123456789@!#$%^&*()_-+=/?.><,|" 

	# Time to bruteforce -\_('_')_/-
	for x in charset:
		ct = self.receive_enc_pt(plaintext + part_secret + x)
		if ciphertext[:length_to_crack] == ct[:length_to_crack]:
			return x
	return ''

Exploit

import sys
from pwn import *

class Oracle():
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.conn = remote(self.host, self.port)
    
    def receive_enc_flag(self):
        flag = self.conn.recvline()
        return to_byte(flag)

    def receive_enc_pt(self, pt):
        self.conn.recvuntil('something: ')
        self.conn.sendline(pt)
        self.conn.recvline()
        s = self.conn.recvline()
        return to_byte(s)

    def compute_bsize(self):
        plaintext = b''
        # aes-ecb(key, junk || unknown-string = flag || pad)
        initial_len = len(self.receive_enc_pt(plaintext))
        final_len = initial_len
        # aes-ecb(key, junk || plaintext || unknown-string = flag || pad)
        while final_len == initial_len:
            plaintext += 'a'
            final_len = len(self.receive_enc_pt(plaintext)) 
        return (final_len - initial_len)
    
    def compute_junk(self, blocksize):
        ciphertexts = []
        ciphertexts.append(self.receive_enc_pt('')[:blocksize])
        # Incrementing the plaintext of one 'a' at every iteration
        # There will be 2 consecutive first block of the ciphertexts
        # equal. From there I can deduce the size of the junk
        for i in range(1, 17):
            ciphertexts.append(self.receive_enc_pt('a' * i)[:blocksize])
            if ciphertexts[i] == ciphertexts[i - 1]:
                return 17 - i
        # 16 or 0 is indifferent!
        return 0

    def get_secret(self, blocksize, part_secret, junk):
        # Length of plaintext must be between 0 and 15, so % blocksize = 16
        length_plaintext = (blocksize - junk - (1 + len(part_secret))) % blocksize
        # Length to crack is always length_plaintext + len(secret that I know) + 1
        # After the first block has been decrypted, we need to reconstruct the length_plaintext
        # to 15, and add to it during the verification (if) the part_secret 
        length_to_crack = length_plaintext + junk + len(part_secret) + 1
        # Create plaintext
        plaintext = 'a' * length_plaintext
        # Ciphertext to compare
        ciphertext = self.receive_enc_pt(plaintext)
        # Let's create a charset of all possible ascii values (readable)
        charset = "qwertyuiopasdfghjklzxcvbnm"
        charset += charset.upper()
        charset += "_{}0123456789@!#$%^&*()_-+=/?.><,|" 

        # Time to bruteforce -\_('_')_/-
        for x in charset:
            ct = self.receive_enc_pt(plaintext + part_secret + x)
            if ciphertext[:length_to_crack] == ct[:length_to_crack]:
                return x
        return ''

def to_byte(s):
    # Remove \n\r
    return s[:-2].decode("hex")

def identify_ecb(ct, blocksize):
    blocks = [ct[i: i + blocksize] for i in range(0, len(ct), blocksize)]
    for i in range(0, len(blocks)):
        for j in range(i+1, len(blocks)-1):
            if blocks[i] == blocks[j]:
                return 1
    return None

def main():
    host = 'crypto.chal.csaw.io'
    port = '1003'
    oracle = Oracle(host, port)
    flag_enc = oracle.receive_enc_flag()

    # Compute blocksize of the cipher --> 16 
    blocksize = oracle.compute_bsize()
    log.info("blocksize : " + str(blocksize))

    # Compute junk size --> Random
    junk = oracle.compute_junk(blocksize)
    log.info("junk size : " + str(junk))

    # Check if the cryptosystem is using ECB --> YES
    enc_pt = oracle.receive_enc_pt('a' * 100)
    if identify_ecb(enc_pt, blocksize) == None:
        log.info("ecb : false\nleaving...")
        exit(1) 
    log.info("ecb : true")
    
    # Decrypt flag
    part_secret = ''
    for i in range(len(flag_enc)):
        char_secret = oracle.get_secret(blocksize, part_secret, junk)
        part_secret += char_secret
        sys.stdout.write("\r" + char_secret)
        sys.stdout.flush()
        if '}' in part_secret:
            sys.stdout.write("\n")
            break

if __name__ == '__main__':
    main()

Time to launch the exploit and…

Flag

flag{y0u_kn0w_h0w_B10cks_Are_n0T_r31iab13...}