Breaking some homemade crypto

Posted on 01 Aug 2017 in security • 3 min read

I recently did a code review assessment on an application for one of my client. The best part of the application was their own cryptography algorithm.

Moreover, the application was written in PHP and PHP do some strange things with string, characters and XOR operations. It only needed a few lines of python in order to break it.

TL;DR : please do not write your own crypto!

The cryptography algorithm was a symmetric one, that means there is always a private key shared between the encryption and the decryption. In fact the key was shared for the application.

Encryption algorithm

The encryption algorithm is the following (I rewrote it in python for a better comprehension):

def crypt(clearText):
    privateKey = '123456'
    cipherText = ''

    key= hashlib.md5(str(randint(0, 32000)).encode('ascii')).hexdigest()

    i = 0
    j = 0
    while (i< len(clearText)):
        if (j == len(key)):
            j=0
        cipherText += key[j]+chr(ord(clearText[i])^ord(key[j]))
        i += 1
        j += 1

    key2 = hashlib.md5(privateKey.encode('ascii')).hexdigest()
    i = 0
    j = 0
    result = ''

    while (i< len(cipherText)):
        if (j == len(key2)):
            j=0
        result += chr(ord(cipherText[i]) ^ ord(key2[j]))
        i+=1
        j+=1

    return result

We observe that there is only XOR operations. Quick reminder about XOR operations:

0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0

Moreover, by definition: a XOR b XOR a = b

Breaking things

In our situation we need to find cipherText in order to find the md5 hash of the private key. However cipherText is build using the clear text and a random number included between 0 and 32 000. Therefore by a Known-plaintext attack we can generate the 32 001 values of cipherText, XOR it with the encrypted string, search for the resulting sting which will be repeating every 36 characters and get the md5 hash of the private key. Our only constrain in that the plain text must be longer than 36 characters in order to easily find the md5 hash (an other option might have be to search for ascii characters).

Here is the python code:

def brute():
    clearText='this is a test in order to break some home made crypto'
    codedText=crypt(clearText)
    k=0
    while (k<32001):
        key= hashlib.md5(str(k).encode('ascii')).hexdigest()
        cipherText= ''

        i = 0
        j = 0
        while (i< len(clearText)):
            if (j == len(key)):
                j=0
            cipherText += key[j]+chr(ord(clearText[i])^ord(key[j]))
            i += 1
            j += 1

        i = 0
        j = 0
        result = ''

        while (i< len(codedText)):
            if (j == len(cipherText)):
                j=0
            result += chr(ord(codedText[i]) ^ ord(cipherText[j]))
            i+=1
            j+=1
        l = 0
        s = 0
        while (l<32):
            if (result[l]==result[l+32]):
                s +=1
            l+=1
        if s==32:
            print(k)
            print(result[:32])
            break
        k+=1

I put the two function in a file, added a main and run it:

[maggick@eridani ~]$ time python crypt.py
23467
e10adc3949ba59abbe56e057f20f883e

real    0m2.186s
user    0m2.186s
sys 0m0.000s
[maggick@eridani ~]$ echo -en '123456' | md5sum
e10adc3949ba59abbe56e057f20f883e  -

We got the md5 hash of the private key. The default key used in the application was 8 characters long and was use in other algorithms not home made.

PHP: character XOR character

PHP is doing really funny things, most of you know that. When analysing the original PHP code of the encryption function, I found something very strange:

$tmp .= mb_substr($text, $i, 1) ^ mb_substr($key, $j, 1);

How can you make a xor operation between two characters?!

In fact PHP make a xor operation between the ASCII value of the character and convert the result back to the ASCII character. For instance 'a' xor 'B' will result in 65 xor 98 which give 35 therefore 'a' xor 'B' → '#'

As you may have seen in the above code this is equivalent to the following python code:

tmp += chr(ord(clearText[i])^ord(key[j]))

Full code

The complete code is the following:

import hashlib
import base64
from random import randint

def crypt(clearText):
    privateKey = '123456'
    cipherText = ''

    key= hashlib.md5(str(randint(0, 32000)).encode('ascii')).hexdigest()

    i = 0
    j = 0
    while (i< len(clearText)):
        if (j == len(key)):
            j=0
        cipherText += key[j]+chr(ord(clearText[i])^ord(key[j]))
        i += 1
        j += 1

    key2 = hashlib.md5(privateKey.encode('ascii')).hexdigest()
    i = 0
    j = 0
    result = ''

    while (i< len(cipherText)):
        if (j == len(key2)):
            j=0
        result += chr(ord(cipherText[i]) ^ ord(key2[j]))
        i+=1
        j+=1

    return result

def brute():
    clearText='this is a test in order to break some home made crypto'
    codedText=crypt(clearText)
    k=0
    while (k<32001):
        key= hashlib.md5(str(k).encode('ascii')).hexdigest()
        cipherText= ''

        i = 0
        j = 0
        while (i< len(clearText)):
            if (j == len(key)):
                j=0
            cipherText += key[j]+chr(ord(clearText[i])^ord(key[j]))
            i += 1
            j += 1


        i = 0
        j = 0
        result = ''

        while (i< len(codedText)):
            if (j == len(cipherText)):
                j=0
            result += chr(ord(codedText[i]) ^ ord(cipherText[j]))
            i+=1
            j+=1
        l = 0
        s = 0
        while (l<32):
            if (result[l]==result[l+32]):
                s +=1
            l+=1
        if s==32:
            print(k)
            print(result[:32])
            break
        k+=1


def main():
    brute()

if __name__ == "__main__":
    main()