Home > Security > Cracking RC4 messages that use weak RC4 reinitialization for each message part

Cracking RC4 messages that use weak RC4 reinitialization for each message part

This post deals with a short excerpt of a security and hacking related exercise at the University of Applied Sciences Upper Austria, Campus Hagenberg: we obtain the plaintext from an intercepted RC4 encoded message, from which we know that it uses an easy to crack re-initialization of the RC4 cipher for each message part.

The problem

Imagine we have intercepted a secret message split into 11 message parts, each (but the last) consisting of 40 1-byte characters:

0xd9, 0xef, 0x7b, 0x6e, 0xca, 0xb5, 0x12, 0xa0, 0x4f, 0x4b, 0x56, 0xb4, 0x94, 0xdf, 0x27, 0xed, 0xf8, 0xcc, 0x64, 0x94, 0xeb, 0x5a, 0x77, 0xea, 0x9b, 0x76, 0xdf, 0xe9, 0x18, 0x02, 0xc6, 0x36, 0x19, 0x1f, 0xc8, 0xf2, 0x5a, 0xa8, 0xda, 0xe7,
0xb7, 0xd3, 0x5f, 0x4e, 0x89, 0x9f, 0x01, 0xa9, 0x0e, 0x4c, 0x5c, 0xb6, 0xc0, 0xdc, 0x2e, 0xfa, 0xbd, 0xbf, 0x63, 0xb4, 0xa4, 0x42, 0x77, 0xb3, 0x98, 0x62, 0xd5, 0xf0, 0x5d, 0x07, 0xdc, 0x62, 0x17, 0x4d, 0xca, 0xe6, 0x5a, 0xa8, 0xdd, 0xec,
0xf9, 0xd3, 0x5c, 0x47, 0x85, 0x86, 0x05, 0xab, 0x5d, 0x4c, 0x5b, 0xf0, 0xc7, 0xc2, 0x22, 0xf8, 0xb4, 0xe6, 0x17, 0xb8, 0xa4, 0x5f, 0x66, 0xb3, 0x80, 0x79, 0xd9, 0xec, 0x18, 0x1d, 0xc0, 0x35, 0x51, 0x4b, 0xca, 0xab, 0x19, 0xaf, 0xc2, 0xe6,
0xe4, 0xc5, 0x0f, 0x5f, 0x82, 0x93, 0x40, 0xb7, 0x47, 0x58, 0x5b, 0xa4, 0x94, 0xdb, 0x2e, 0xfa, 0xb9, 0xf2, 0x52, 0xa8, 0xae, 0x43, 0x61, 0xb3, 0xbc, 0x7e, 0xc2, 0xf3, 0x18, 0x18, 0xd6, 0x62, 0x1c, 0x50, 0xc1, 0xe2, 0x1c, 0xae, 0xce, 0xe8,
0xe3, 0xc9, 0x40, 0x45, 0x99, 0xd6, 0x32, 0x96, 0x6f, 0x1f, 0x56, 0xbe, 0xd7, 0xd9, 0x36, 0xf8, 0xac, 0xf6, 0x58, 0xb2, 0xeb, 0x46, 0x7d, 0xe1, 0x80, 0x64, 0x96, 0xec, 0x59, 0x0c, 0x8f, 0x24, 0x10, 0x4c, 0xd1, 0xee, 0x08, 0xe7, 0x8d, 0xa9,
0xc3, 0xef, 0x6b, 0x64, 0xca, 0xb3, 0x0e, 0xa6, 0x5c, 0x46, 0x43, 0xa4, 0x94, 0xc9, 0x26, 0xfc, 0xbb, 0xf0, 0x5e, 0xb2, 0xeb, 0x41, 0x60, 0xfa, 0x9d, 0x76, 0xc2, 0xfe, 0x18, 0x1e, 0xca, 0x3b, 0x02, 0x1f, 0xd2, 0xe2, 0x0e, 0xaf, 0x8d, 0xce,
0xc7, 0xe7, 0x0f, 0x41, 0x9f, 0x85, 0x14, 0xe5, 0x4c, 0x5a, 0x13, 0xb2, 0xd1, 0x8b, 0x3c, 0xfd, 0xaa, 0xfa, 0x17, 0xbe, 0xae, 0x52, 0x73, 0xe6, 0x98, 0x72, 0x96, 0xef, 0x50, 0x10, 0xd6, 0x62, 0x19, 0x5e, 0xd3, 0xee, 0x5a, 0xa5, 0xc8, 0xec,
0xf9, 0x80, 0x4a, 0x53, 0x9a, 0x99, 0x12, 0xb1, 0x4b, 0x5b, 0x13, 0xa5, 0xda, 0xdb, 0x3d, 0xe7, 0xac, 0xfa, 0x54, 0xa8, 0xae, 0x55, 0x32, 0xc4, 0x82, 0x7b, 0xda, 0xbb, 0x4b, 0x10, 0xc1, 0x26, 0x51, 0x4b, 0xcd, 0xee, 0x17, 0xe7, 0xf9, 0xc5,
0xc4, 0x80, 0x5f, 0x59, 0x85, 0x82, 0x05, 0xa6, 0x5a, 0x5a, 0x57, 0xf0, 0xd6, 0xde, 0x3b, 0xa8, 0xbb, 0xfe, 0x59, 0xfc, 0xb2, 0x5e, 0x67, 0xb3, 0x9f, 0x65, 0xc3, 0xe8, 0x4c, 0x55, 0xfb, 0x0e, 0x22, 0x1f, 0xf5, 0xea, 0x09, 0xb4, 0xda, 0xe6,
0xe5, 0xc4, 0x0f, 0x4d, 0x85, 0x84, 0x40, 0xa1, 0x4f, 0x49, 0x5a, 0xb4, 0xd0, 0xde, 0x21, 0xe6, 0xb6, 0xfa, 0x43, 0xfc, 0xa0, 0x54, 0x6b, 0xb3, 0x82, 0x64, 0x96, 0xc2, 0x48, 0x33, 0xcd, 0x26, 0x25, 0x73, 0xce, 0xe8, 0x2e, 0x8a, 0xc8, 0xcb,
0xff, 0xf6, 0x5a, 0x0b, 0xca, 0xb2, 0x01, 0xb3, 0x47, 0x5b

We know all these message parts have been encrypted using RC4 with a known to be easy to crack reinitialization of the RC4 stream for each message part. We also know the message plaintext only contains letters and whitespaces. The task is: obtain the plaintext. How to do so?

The solution

We know all 11 message parts have been encrypted using the same key stream (as the RC4 cipher has been re-initialized for each message using the same secret key). To avoid confusion here: as stream cipher, RC4 gets initialized from a secret key (e.g. the user’s password) and generates a continuous key stream from it. Each time a RC4 cipher is initialized this way, the generated key stream is exactly the same. Therefore, the RC4 reinitialization for each package causes the key stream to be exactly the same for all messages. RC4 does an XOR on the key stream and the plaintext to obtain the ciphertext. Hence, an XOR with the key stream used for all 11 message ciphertexts causes these to become correct plaintext. We further know that valid plaintext only allows certain characters. Therefore it’s possible to try through all 255 possibilities per keystream character: XOR each one with all corresponding characters of all ciphertexts and keep only those, where all resulting plaintext characters are actually allowed ones. By doing so we generate a list of valid keystreams in an effective way (which by doing an XOR on the ciphertext all result in valid plaintext). Trying through these keys (XOR with plaintext) until we see a message that “makes sense” is now very easy – and could even be done by hand (not posting the actual solution here).

# Cracking RC4 messages that use weak RC4 reinitialization for each message part
#
# Rainhard Findling
# University of Applied Sciences Upper Austria, Campus Hagenberg
# 2015/03
# 
# all ciphertext message parts
c1 = [0xd9, 0xef, 0x7b, 0x6e, 0xca, 0xb5, 0x12, 0xa0, 0x4f, 0x4b, 0x56, 0xb4, 0x94, 0xdf, 0x27, 0xed, 0xf8, 0xcc, 0x64, 0x94, 0xeb, 0x5a, 0x77, 0xea, 0x9b, 0x76, 0xdf, 0xe9, 0x18, 0x02, 0xc6, 0x36, 0x19, 0x1f, 0xc8, 0xf2, 0x5a, 0xa8, 0xda, 0xe7]

c2 = [0xb7, 0xd3, 0x5f, 0x4e, 0x89, 0x9f, 0x01, 0xa9, 0x0e, 0x4c, 0x5c, 0xb6, 0xc0, 0xdc, 0x2e, 0xfa, 0xbd, 0xbf, 0x63, 0xb4, 0xa4, 0x42, 0x77, 0xb3, 0x98, 0x62, 0xd5, 0xf0, 0x5d, 0x07, 0xdc, 0x62, 0x17, 0x4d, 0xca, 0xe6, 0x5a, 0xa8, 0xdd, 0xec]

c3 = [0xf9, 0xd3, 0x5c, 0x47, 0x85, 0x86, 0x05, 0xab, 0x5d, 0x4c, 0x5b, 0xf0, 0xc7, 0xc2, 0x22, 0xf8, 0xb4, 0xe6, 0x17, 0xb8, 0xa4, 0x5f, 0x66, 0xb3, 0x80, 0x79, 0xd9, 0xec, 0x18, 0x1d, 0xc0, 0x35, 0x51, 0x4b, 0xca, 0xab, 0x19, 0xaf, 0xc2, 0xe6]

c4 = [0xe4, 0xc5, 0x0f, 0x5f, 0x82, 0x93, 0x40, 0xb7, 0x47, 0x58, 0x5b, 0xa4, 0x94, 0xdb, 0x2e, 0xfa, 0xb9, 0xf2, 0x52, 0xa8, 0xae, 0x43, 0x61, 0xb3, 0xbc, 0x7e, 0xc2, 0xf3, 0x18, 0x18, 0xd6, 0x62, 0x1c, 0x50, 0xc1, 0xe2, 0x1c, 0xae, 0xce, 0xe8]

c5 = [0xe3, 0xc9, 0x40, 0x45, 0x99, 0xd6, 0x32, 0x96, 0x6f, 0x1f, 0x56, 0xbe, 0xd7, 0xd9, 0x36, 0xf8, 0xac, 0xf6, 0x58, 0xb2, 0xeb, 0x46, 0x7d, 0xe1, 0x80, 0x64, 0x96, 0xec, 0x59, 0x0c, 0x8f, 0x24, 0x10, 0x4c, 0xd1, 0xee, 0x08, 0xe7, 0x8d, 0xa9]

c6 = [0xc3, 0xef, 0x6b, 0x64, 0xca, 0xb3, 0x0e, 0xa6, 0x5c, 0x46, 0x43, 0xa4, 0x94, 0xc9, 0x26, 0xfc, 0xbb, 0xf0, 0x5e, 0xb2, 0xeb, 0x41, 0x60, 0xfa, 0x9d, 0x76, 0xc2, 0xfe, 0x18, 0x1e, 0xca, 0x3b, 0x02, 0x1f, 0xd2, 0xe2, 0x0e, 0xaf, 0x8d, 0xce]

c7 = [0xc7, 0xe7, 0x0f, 0x41, 0x9f, 0x85, 0x14, 0xe5, 0x4c, 0x5a, 0x13, 0xb2, 0xd1, 0x8b, 0x3c, 0xfd, 0xaa, 0xfa, 0x17, 0xbe, 0xae, 0x52, 0x73, 0xe6, 0x98, 0x72, 0x96, 0xef, 0x50, 0x10, 0xd6, 0x62, 0x19, 0x5e, 0xd3, 0xee, 0x5a, 0xa5, 0xc8, 0xec]

c8 = [0xf9, 0x80, 0x4a, 0x53, 0x9a, 0x99, 0x12, 0xb1, 0x4b, 0x5b, 0x13, 0xa5, 0xda, 0xdb, 0x3d, 0xe7, 0xac, 0xfa, 0x54, 0xa8, 0xae, 0x55, 0x32, 0xc4, 0x82, 0x7b, 0xda, 0xbb, 0x4b, 0x10, 0xc1, 0x26, 0x51, 0x4b, 0xcd, 0xee, 0x17, 0xe7, 0xf9, 0xc5]

c9 = [0xc4, 0x80, 0x5f, 0x59, 0x85, 0x82, 0x05, 0xa6, 0x5a, 0x5a, 0x57, 0xf0, 0xd6, 0xde, 0x3b, 0xa8, 0xbb, 0xfe, 0x59, 0xfc, 0xb2, 0x5e, 0x67, 0xb3, 0x9f, 0x65, 0xc3, 0xe8, 0x4c, 0x55, 0xfb, 0x0e, 0x22, 0x1f, 0xf5, 0xea, 0x09, 0xb4, 0xda, 0xe6]

c10 = [0xe5, 0xc4, 0x0f, 0x4d, 0x85, 0x84, 0x40, 0xa1, 0x4f, 0x49, 0x5a, 0xb4, 0xd0, 0xde, 0x21, 0xe6, 0xb6, 0xfa, 0x43, 0xfc, 0xa0, 0x54, 0x6b, 0xb3, 0x82, 0x64, 0x96, 0xc2, 0x48, 0x33, 0xcd, 0x26, 0x25, 0x73, 0xce, 0xe8, 0x2e, 0x8a, 0xc8, 0xcb]

c11 = [0xff, 0xf6, 0x5a, 0x0b, 0xca, 0xb2, 0x01, 0xb3, 0x47, 0x5b]

# concatenate all ciphertext parts (except last) 
ciphertext = [c1, c2, c3, c4, c5, c6, c7, c8, c9, c10]

# our brute forcing method
def try_keys(c):
    # each key is one byte, so 255 possibilities here
    k_ok = []
    for k in range(255):
        plaintext = [x^k for x in c] 
        # all chars of one position have to translate to chars allowed in plaintext: 41-5a, 61-7a
        ok = [p_chr == 0x20 or p_chr >= 0x41 and p_chr <= 0x5a or p_chr >= 0x61 and p_chr <= 0x7a for p_chr in plaintext]
        if(all(ok)):
            k_ok += [k]
    return k_ok

# brute force all keystream positions
xor = []
for i in range(40):
    c = [x[i] for x in ciphertext]
    xor += [try_keys(c)]

# see amount of possibilities left per keystream char position
print filter(lambda x:x[1] > 1, [(i, len(xor[i])) for i in range(len(xor))])

# use "first" key
key = [x[0] for x in xor]
# adapt specific keystream positions per hand so that all plaintext is correct (could be automated)
key[3] = xor[3][7]
key[14] = xor[14][1]
key[21] = xor[21][1]
key[24] = xor[24][3]
key[25] = xor[25][5]
key[34] = xor[34][14]

# decode (now include last message part)
plaintext = []
for c in (ciphertext + [c11]):
    t = []
    for i in range(len(c)):
        t += [chr(c[i]^key[i])]
    plaintext += t

print ''.join(plaintext)

Note: the more plaintext messages encoded by the same, re-initialized RC4 cipher we have, the less possible keystreams will remain, and the faster we obtain the plaintext. Btw: this is pretty much the problem WiFi WEP had – this is the core problem of why it can be cracked so easily, and why it is considered very insecure by now.

  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: