ECB Chosen Plaintext Attack (ABCTF2016 – Encryption Service)

While reading a blog post last week, I decided to implement an ECB chosen plaintext attack.

This was also one of the challenges during ABCTF this year, so more on that below.

The theory of the attack is that if the target is using an ECB cipher mode with a secret, and you have control over the plaintext, then you can determine the “secret” being used (similar to a salt).

From an attacking perspective, this would be useful if your session cookie (or something similar) used this encrypted value for authentication or authorization.

In this case, you would just need to determine the secret being used. Once you have that, you set the cookie’s value to the encrypted value of “admin” + “secret” + padding (where necessary), or the system you are attacking’s equivalent.

So, first of all, I set out to build a server similar to c0nrad’s that would take user input, encrypt the plaintext using AES mode ECB, and return to the user the encrypted (and encoded) output.

The commented out “prepend” section will actually be used later for the ABCTF section, but I wanted to include it as this was the final code for my test server.

#!/usr/bin/python

from Crypto.Cipher import AES
import socket
import sys
import random
import string

blockSize = 16
encKey = "ENCRYPTIONKEY123"
secret = "[email protected]!"
prepend = ""
#prepend = "ENCRYPT:"
chars = string.ascii_letters + string.digits + string.punctuation
secret = ''.join(random.choice(chars) for _ in range(random.randint(1,1000)))

def pad(input):
	if (len(input) % blockSize == 0):
		return input
	else:
		extra = blockSize - (len(input) % blockSize)
		output = input + "\x00" * extra
		return output

def unpad(input):
	return input.rstrip("\x00")

def encrypt(input):
	if (input is None) or (len(input) == 0):
		print "Input text cannot be null or empty"

	toEncrypt = prepend + input + secret
	toEncrypt = pad(toEncrypt)
	cipher = AES.AESCipher(encKey, AES.MODE_ECB)
	cipherText = cipher.encrypt(toEncrypt)
	return cipherText.encode("hex")

def decrypt(input):
	if (input is None) or (len(input) == 0):
		print "Input text cannot be null or empty"

	encrypted = input.decode("hex")
	cipher = AES.AESCipher(encKey, AES.MODE_ECB)
	plainText = unpad(cipher.decrypt(encrypted))
	return plainText

def main():
	print "SECRET LENGTH: " + str(len(secret))
	print "SECRET = " + secret

	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

	server_address = ('localhost', 10000)
	print "\nStarting up on %s port %s" % server_address
	s.bind(server_address)

	s.listen(1)

	while True:
	   	connection, client_address = s.accept()

	   	try:
	   		while True:
	   			data = connection.recv(2048)
	   			if data:
	   				#print "DATA: " + data
	   				input = data.rstrip()
	   				print "INPUT: " + input
	   				print "HEX: " + input.encode("hex")
	   				encrypted = encrypt(input)
	   				print "ENCRYPTED: " + encrypted
	   				connection.send(encrypted)
	   			else:
	   				break
	   	finally:
	   		connection.close()
    
if __name__ == "__main__":
    main()

To verify that my server worked, I just needed to telnet to port 10000 and send a message.

[email protected]:~/ecb# telnet localhost 10000
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
MY SECRET MESSAGE
93ddd26ad133d086d16df74ef2ba96417c936e46ec4cbe142d6e69b88b4d92e515e324e836c5c43065ba47ea200d046d8c88a62410ff28ed32440c8e6c52b7cabdcac9db9db7351762e5329d2539b69e3c6ff2e87de7cb2a8b0a197d12fd1ca11e4db1cf177e90522ac7aa858882b701458849fe88bf896a798f706727982a69a42445fca22d70fc0b3f4926cdcd2ffab91facea2129e8a0d6415463d8e64b5b248413efa6b666baef257e3389e4ae3e10d2b20c53d468d7af57a9beda526eed243ac36b77c34b547df8c6c8f9f90c601b309e218639c1fac8159da652f1a4b2468dfb04804aa3a2e5bd555b9b03e539f8cad7e8e4ac4c3e907eb03f959a46e5dfbd46012044f41616d81e6bf36218c1d1863360f2ef1eb445c23d8fb155b5e22b5a0ff069bb3a5305628d90789af490758ef669f0bba00f5e3b997496337f2295f62ace663fb01b99ad94226a9991fa525910a6ba65728367f2e0f3253dc8fd7594afa25c7157f8a92281222d45799406809e0b8a1c69118e4a60c3902e8fed1a529c737f6fdc24241dd901112c8e9f5b76901d5fafc99fff7a6bd3eaf901872bdf4f522ab8991e8be973f64756489dba0f4d26c1a11d11a925115ec9779b8c14607081ec3aa44e4c4a6b231b3fc14eb1eb1eaaa42e1b59ef259b79139d3c6808889243ef70245deab1899362e155d65a4030a17250d6e9ad11c3dba7f01de01c4b28ee50f20d9903040a130eb9e7013d1921b23c1d6e5e359cc54225eb68b61d4c2b913e9ae600a172c5df936f5a0b56981edf5ebbc75cac39ad1d34e02052ac8dd48336f8219641b4df30e122e298949e5921f34f4ce86b8db46f2cf34de17f8ab5e0f960374620c79d7236ee7ac2b412155c4bfc5721484608e16c28765d8a2e61121c1a3ce8b4235853a6b38f9f620ea1e081c2bc64d4437f394e394cbfc6f9e5a4e4b97b8b1ae9f6ddfccace376b8d492aa88026e34c962d4d79d14c4778a1f4889953796b40d44db9c1215a2f

On the server side of things I saw my connection, my input, as well as the encrypted output.

[email protected]:~/ecb# python ecbServer.py 
SECRET LENGTH: 699
SECRET = 7o|[email protected]:D-ayGW%[email protected]#'/S)u\Y(B%c6+(/X=0V9I_3WES76#F`-[*1^D-r9+$|$AF;h9`)[email protected]|{cZMI7t0^&]2;%cdckdN{nf{I~5i&3p:BR;87Hu;?}0UU}hV1SQC"|{wTU7RZC\Filc9SS0"^'nY5)U77J\>~_)}F~BcQ90Lus*[o22GwwJA9!5kcd~I7[U*N{ht*+RjkVFwRPX2g2QpE;8k$+'#H'F2h|aO?\(7mj-am/YM}]MliDI5~K$3)49z57r>thsUV"sR|)d/qyGs&'"hCD3#b}X%DNt5A'vJ"S#v7ZiO0HxDjHF^XiX{;H64IFYKVt24~y[bkh5c=96A~r}q=6wyENj!WGE(\5I`+D%W/WYU4g#yQf*|vf%4JFgG4-4HkZSL?fQpE?<^q4|,'UkBX9^UaD'n}*6wj.Q[)Sm[y)=JR^%1g4|\m=Xfi1u:U%$"w\JTL/S5)goXwHFpbZiEjI

Starting up on localhost port 10000
INPUT: MY SECRET MESSAGE
HEX: 4d5920534543524554204d455353414745
ENCRYPTED: 93ddd26ad133d086d16df74ef2ba96417c936e46ec4cbe142d6e69b88b4d92e515e324e836c5c43065ba47ea200d046d8c88a62410ff28ed32440c8e6c52b7cabdcac9db9db7351762e5329d2539b69e3c6ff2e87de7cb2a8b0a197d12fd1ca11e4db1cf177e90522ac7aa858882b701458849fe88bf896a798f706727982a69a42445fca22d70fc0b3f4926cdcd2ffab91facea2129e8a0d6415463d8e64b5b248413efa6b666baef257e3389e4ae3e10d2b20c53d468d7af57a9beda526eed243ac36b77c34b547df8c6c8f9f90c601b309e218639c1fac8159da652f1a4b2468dfb04804aa3a2e5bd555b9b03e539f8cad7e8e4ac4c3e907eb03f959a46e5dfbd46012044f41616d81e6bf36218c1d1863360f2ef1eb445c23d8fb155b5e22b5a0ff069bb3a5305628d90789af490758ef669f0bba00f5e3b997496337f2295f62ace663fb01b99ad94226a9991fa525910a6ba65728367f2e0f3253dc8fd7594afa25c7157f8a92281222d45799406809e0b8a1c69118e4a60c3902e8fed1a529c737f6fdc24241dd901112c8e9f5b76901d5fafc99fff7a6bd3eaf901872bdf4f522ab8991e8be973f64756489dba0f4d26c1a11d11a925115ec9779b8c14607081ec3aa44e4c4a6b231b3fc14eb1eb1eaaa42e1b59ef259b79139d3c6808889243ef70245deab1899362e155d65a4030a17250d6e9ad11c3dba7f01de01c4b28ee50f20d9903040a130eb9e7013d1921b23c1d6e5e359cc54225eb68b61d4c2b913e9ae600a172c5df936f5a0b56981edf5ebbc75cac39ad1d34e02052ac8dd48336f8219641b4df30e122e298949e5921f34f4ce86b8db46f2cf34de17f8ab5e0f960374620c79d7236ee7ac2b412155c4bfc5721484608e16c28765d8a2e61121c1a3ce8b4235853a6b38f9f620ea1e081c2bc64d4437f394e394cbfc6f9e5a4e4b97b8b1ae9f6ddfccace376b8d492aa88026e34c962d4d79d14c4778a1f4889953796b40d44db9c1215a2f

With the server in place, it was time to write my attacking script.

This script will connect to the server, calculate the length of the secret, and go about brute forcing it. For more specifics about the attack, please see the blog linked above.

#!/usr/bin/python

import math
import socket
import sys

def chunkstring(string, length):
    return (string[0+i:length+i] for i in range(0, len(string), length))

def roundup(x, base=10):
    return int(math.ceil(x / (base + 0.0))) * base

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server_address = ('localhost', 10000)
s.connect(server_address)

try:
    found = False
    secret = ""

    secretLen = 0
    #prependChars = "ENCRYPT:"
    prependChars = ""

    message = "A"
    s.sendall(message)
    data = s.recv(2048)
    output = list(chunkstring(data, 32))
    initialLen = len(output)

    curLen = 0

    while (curLen <= initialLen):
        message += "A"
        s.sendall(message)
        data = s.recv(2048)
        output = list(chunkstring(data, 32))
        curLen = len(output)

    extra = len(message) - 1

    secretLen = ((curLen - 1) * 16) - extra - len(prependChars)

    print "SECRETLEN: " + str(secretLen)

    while not found:
        initialBlock = "A" * (16 - len(prependChars))
        fullLen = roundup(secretLen, 16)
        prepend = "B" * (fullLen - len(secret) - 1)
        message1 = initialBlock + prepend

        s.sendall(message1)
        data = s.recv(8192)
        initialReturn = list(chunkstring(data, 32))
        #print "INITIAL: " + str(initialReturn)

        for i in range(33, 127):
            message2 = message1 + secret + chr(i)
            s.sendall(message2)
            data = s.recv(8192)
            oracle = list(chunkstring(data, 32))
            #print "ORACLE: " + str(oracle)
            compareBlock = (len(prependChars + message2) / 16) - 1
            #print "COMPARE = " + str(compareBlock)
            if oracle[compareBlock] == initialReturn[compareBlock]:
                secret += chr(i)
                #print "LENGTH: " + str(len(secret))
                #print "SECRET: " + secret
                #print "INITIAL: " + str(initialReturn)
                #print "ORACLE: " + str(oracle)
                if len(secret) == secretLen:
                    found = True
                    print secret
                break
    
finally:
    s.close()

With my attack script written, it was time to point it at my server. Within just a few seconds it had already brute forced my random 699 character secret!

[email protected]:~/ecb# python ecbAttack.py 
SECRETLEN: 699
7o|[email protected]:D-ayGW%[email protected]#'/S)u\Y(B%c6+(/X=0V9I_3WES76#F`-[*1^D-r9+$|$AF;h9`)[email protected]|{cZMI7t0^&]2;%cdckdN{nf{I~5i&3p:BR;87Hu;?}0UU}hV1SQC"|{wTU7RZC\Filc9SS0"^'nY5)U77J\>~_)}F~BcQ90Lus*[o22GwwJA9!5kcd~I7[U*N{ht*+RjkVFwRPX2g2QpE;8k$+'#H'F2h|aO?\(7mj-am/YM}]MliDI5~K$3)49z57r>thsUV"sR|)d/qyGs&'"hCD3#b}X%DNt5A'vJ"S#v7ZiO0HxDjHF^XiX{;H64IFYKVt24~y[bkh5c=96A~r}q=6wyENj!WGE(\5I`+D%W/WYU4g#yQf*|vf%4JFgG4-4HkZSL?fQpE?<^q4|,'UkBX9^UaD'n}*6wj.Q[)Sm[y)=JR^%1g4|\m=Xfi1u:U%$"w\JTL/S5)goXwHFpbZiEjI

On the server side of things, I could see the brute force and subsequent ciphertext calculations in progress.

< ... snip ... >
INPUT: AAAAAAAAAAAAAAAABBBBB7o|[email protected]:D-ayGW%[email protected]#'/S)u\Y(B%c6+(/X=0V9I_3WES76#F`-[*1^D-r9+$|$AF;h9`)[email protected]|{cZMI7t0^&]2;%cdckdN{nf{I~5i&3p:BR;87Hu;?}0UU}hV1SQC"|{wTU7RZC\Filc9SS0"^'nY5)U77J\>~_)}F~BcQ90Lus*[o22GwwJA9!5kcd~I7[U*N{ht*+RjkVFwRPX2g2QpE;8k$+'#H'F2h|aO?\(7mj-am/YM}]MliDI5~K$3)49z57r>thsUV"sR|)d/qyGs&'"hCD3#b}X%DNt5A'vJ"S#v7ZiO0HxDjHF^XiX{;H64IFYKVt24~y[bkh5c=96A~r}q=6wyENj!WGE(\5I`+D%W/WYU4g#yQf*|vf%4JFgG4-4HkZSL?fQpE?<^q4|,'UkBX9^UaD'n}*6wj.Q[)Sm[y)=JR^%1g4|\m=Xfi1u:U%$"w\JTL/S5)goXwHFpbZiEjA
HEX: 414141414141414141414141414141414242424242376f7c264038536a3a442d6179475725594132266c6b312b58655a726f2e4031766a645423272f3c3f51587b7a765026507d7e6d564067353040367e22554f7e7a24506c7e397b2629256142363b464b3c5c5824636c2c315b4056735d702a3063597067252e4f3f2b4630732662314232492b674537244f6a34554c275c2e68682b233d29602b473c6a50317061217a7a43574443445b21276b7e38605b3f5d352c312569266130746e706c2f27364b274e38693361772f3a656c66257e4055532a5737613b28713e5329755c5928422563362b3c2f4277274c33702a744b3f3d4c403e282f583d305639495f3357455337362346602d5b2a315e442d72392b247c2441463b683960297a407c7b635a4d493774305e265d323b256364636b644e7b6e667b497e35692633703a42523b383748753b3f7d3055557d685631535143227c7b77545537525a435c46696c6339535330225e276e5935295537374a5c3e7e5f297d467e42635139304c75732a5b6f32324777774a413921356b63647e49375b552a4e7b68742a2b526a6b564677525058323c743c4c6c6f3c4c7a64512759714f295c683d795075454e45494d3e67325170453b386b242b272348274632687c614f3f5c28376d6a2d616d2f594d7d5d4d6c694449357e4b24332934397a3537723e74687355562273527c29642f717947732627226843443323627d5825444e74354127764a22532376375a694f304878446a48465e5869587b3b4836344946594b567432347e795b626b6835633d3936417e727d713d367779454e6a21574745285c3549602b4425572f5759553467237951662a7c766625344a466747342d34486b5a534c3f665170453f3c5e71347c2c27556b4258395e556144276e7d2a36776a2e515b29536d5b79293d4a525e253167347c5c6d3d58666931753a55252422775c4a544c2f533529676f5877484670625a69456a41
ENCRYPTED: 86dd17028adabcea56758942b23c112e339b524f7ff0165baab9057b35d4cad049ffe7fc6eb4b58f079c8b59375b8b4538f5be89f60868ad6ce77baa6abad0bb04f8c81162be5e0a1184baa4e2a35517bce436cef1899a00ebafd8d6557d544e130ecf6f814587c52597a17d958f2474428045341152f0221db9e9fe63b2f9ae4f9d8b927dcf0190d6b6417c18daf286bd3adce952277b29dc1160b57f235064a503f3a5e960d168d35cdafcda722f63f54ae5ff4e750c9567dbe89e97051bf11741e873f96b746a0a33d4495b3ee7fbfcaf3514f917c3a8d7d53ea1b9b156c4506f37e610489271c9eaafacb8aab667f7a58a489ca7783c0833299a76943fb4d555e3bc8c21d709287b940e36c490c1ec3c55d492835b75c7f52e4251da9d1376b4ed313681e6a2b291b3c7e6cf71ead956bc7a48e9a2a8a49cb177a0677b8fe9bd960dce291c9180570382be8b88dc64d742fd0504cf570e439da27131ab20a518a1b74f00b971182d1de8f43fbe1470fec2d6fcb439131c53ed4751629c300cc07ded37b5a729bb68df1c7e3382bfe97d2ec9a7b3e82f2f3c0740d45944119f68b8c52c6242f0236c0c97717423470f035e681be08fef53abfa0687f252737a12d06ca5f01dac314494bd7878b8b75bf0bd86cdb4d64441e36277e60d384db0f2af6281f4699f08f906a50d6a6bbe693ee3149dc01b72c63d421b009d6d7566dd5b3901c0d75c7026fd1cf591c4c1f12fd0c84bc96032d65aa974d63d7812d6ada3e096b42fa24a465f700e43340675118afac4c18f7510a6fbeac6023c99478d2abdc6493a9542ce88b963021ac210df4906ef80cabcd41cf904f7d85cc29573c05ddd2def0d42eba7759546eece9b9b90e924bf01947cac5627fd138854690e00aab9372f71ab292d7dfd2e4006086b1ee27556cd235a11107477dd7b74723cbd86fb29e8c7773fd617c5ba64c1a4ed7bb26ce1616bc9fcb5c58d3dcfe63629001cbd61ddd6b5b421ab4895efce351349c4c6716a7748a393def6e9b689d89332a18f9eb58383af964864565925b15fb2598f90cacaa7b521869c73bf564d9100c92badf7c09a47585a46e08914245149c4903340519925ce4fabd53ccbba62d536fc5732123d8de0988b52e82eb33e4c8e315a17ff1457eb428b28ad0e2b6a09f37e206b04312508eb28796f55810c69ae1e01d38206ad1f31769755d829fc499f24b3213d41d43cec69a8be8c85a9fa0c79f3c6f272caa532f26b7623bb7763ecd771b03a7febf81603b5e62e432d0f70934890357de757a9101f3f5f9f1ace1190a489c9f0f5d19879b68cb6cb50704633a4b4d90d4850958db7f16e53ec9907b49267e4dfe0802fdab2e7b53a04f8a3d071d13a9730b88c171b9aa1b8af15cc80880e2b80907321c633dd9f02c1d28b64b07d80ba194bcdd556cf2c0e8e20df9ae296b9c52d3f88f2b1e130271486e048b511773717f321d312692d1b5550384a1bd5935afe9b22e45e3bfff20a989516ec20ea11243ff47534f9d600bbf3df6a8b2893d6483c80dcee7fd4ee99665de3e1bde38d02f1cfb98d3d5432df73d75c4b3b6b842c9765263e58b1179c822d38c5af6773220a92956ecd2fd948ab7f0fd9d13fde73b12a6b690d31530481cac46f7856f2125b05a55099c94a848502df173c5fe6e00b37b083cbef02329bf44d704dbf77116c8a6b576730a79bf434133d9ba322c03242ba6043a4d2bd0e95a2be067310cec1337389cd47cbe4cd3e6ddbee627f09816aca1e5edb5fd1489f07fbe5dc3c1d7998aabaf23a76116335c32f07d836375152c72594687e42618906e06dac39f2fec086ab4f488f3a41c92a61e782219453db1757d7629b1354855fb7f296355d915f28b59d406f8c2b6dfc9aca301aea77ce3c63b146a3fcbcc821c8a443a1927d1af030eb554adb2225ba47d81de19c3c931ced161f448322e22583a57a8e2975d308d86bf7ea07d858339df99cdbdd2b28291b88e5
< ... snip ... >

This was actually a challenge in this year's ABCTF, so I want to touch a bit more on it.

The challenge was an AES-ECB service listening on a remote server, and the flag was the secret key being used.

While the only modifications that I needed to make to my client were regarding the prepended "Encrypt:" text, I already included them in the above code samples.

The code for the server was as follows.

# ORIGINAL - http://pastebin.com/UTkSDn4H

#/usr/bin/env python
from Crypto.Cipher.AES import AESCipher
 
import SocketServer,threading,os,time
import signal
 
from secret2 import FLAG, KEY
 
PORT = 7765
 
def pad(s):
  l = len(s)
  needed = 16 - (l % 16)
  return s + (chr(needed) * needed)
 
def encrypt(s):
  return AESCipher(KEY).encrypt(pad('ENCRYPT:' + s.decode('hex') + FLAG))
 
class incoming(SocketServer.BaseRequestHandler):
    def handle(self):
        atfork()
        req = self.request
 
        def recvline():
            buf = ""
            while not buf.endswith("\n"):
                buf += req.recv(1)
            return buf
        signal.alarm(5)
 
        req.sendall("Send me some hex-encoded data to encrypt:\n")
        data = recvline()
        req.sendall("Here you go:")
        req.sendall(encrypt(data).encode('hex') + '\n')
        req.close()
 
class ReusableTCPServer(SocketServer.ForkingMixIn, SocketServer.TCPServer):
  pass
 
SocketServer.TCPServer.allow_reuse_address = True
server = ReusableTCPServer(("0.0.0.0", PORT), incoming)
 
print "Server listening on port %d" % PORT
server.serve_forever()

After running my modified client a short time, I recovered the secret key in use!

secret = ABCTF{p4dding_4_fun}

I was able to submit this flag and receive my 140 points. This was definitely a fun challenge, and I'm glad that it came out right around the time that I started this blog post.

The code and updates can be found in my GitHub repository.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.