a primer of .net and bad crypto
A funny berry
While working for a customer on a aws serverless project, they encountered the need for a graphic client for non-technical users to interract with s3 buckets (Like a filezilla, but for the cloud).
They then found out about cloudberry explorer, which describes itself as :
CloudBerry Explorer provides a user interface allowing to access, move and manage files across your local storage and the cloud storage of your choice. This cloud storage manager is available in two versions – Freeware and PRO.
Freeware version. Free cloud file management software by CloudBerry comes with full support for Server Side Encryption, Lifecycle rules, Amazon CloudFront, Bucket Policies and more.
PRO version. Comes with all the features of the freeware version plus advanced features like client-side encryption, compression, multipart upload, multithreading, content compare, upload rules and more.
One of the side goals was to allow people to share access to s3 buckets without having the share the AWS secrets :
- When you use ‘cloudberry explorer’, you enter your aws access key & secret
- You can export your session from ‘cloudberry explorer’, send it to someone, or save it somewhere
- Open it later from another machine - without having to re-type your AWS credentials
The idea was here to see if we could mimic cloudberry’s encryption to generate those “session files” without using the software itself.
Besides the “how good or how bad can the crypto be” question that struck me, it was a funny exercise :)
At first sight
When exporting a session, CloudBerry produces a XML document, which contains :
... <RequestStyle>VHost</RequestStyle> <AWSKey>BBBBBBBBBB</AWSKey> <ExternalBuckets2 /> <UseSSL>true</UseSSL> <SignatureVersion>4</SignatureVersion> <AWSSecret>jZfXsqxhQf1RhgGmgC6rwQ==</AWSSecret> ...
AWSSecret is our encrypted secret, and seems to be in base64, but encryption or transformation was applied to it :
$ echo -n 'jzfSsQxHkf1ThcKMgC6rwQ==' | base64 -d ��ұ\GA��a��.��
A few more attempts (providing incorrect/empty/too short secret keys) quickly convinced me that it might be a block cipher (as the output is always of fixed size) :
$ echo -n 'jzfSsQxHkf1ThcKMgC6rwQ==' | base64 -d | wc -c 16
Guess it’s time to look at the code !
As the software is developped in
.NET, speaking of reverse engineering would be an overstatement, we are just going to look at the decompiled code.
On my quest for a trivial way to achieve this,
and thus decided not to go with radare2 I discovered dnSpy which, besides doing the job in a simple and efficient way, is open-sauce !
After a few “breakpoint all the things and step-in / step-out”, I found what look like a trail worth following :
Fast forwarding into the code of said function, it creates a
symmetricAlgorithm.CreateDecryptor which is an
AesCryptoServiceProvider, intuition was right (AES is a block cipher).
Now that we now that it’s AES, we need to find :
While looking twice at our current status :
We can notice that
CreateDecryptor’s second parameter (which is the Initialisation Vector) is a freshly initialized array.
So, empty IV it is, which is a bad practice, athought in this case I’m not sure it really matters.
All’s left to find is the key. As I don’t know much about .NET nor its crypto lib, I continue the exciting game of “breakpoint things and see what happens” :)
Following our rabbit, it seems that the software derivates the AES key from a static string, using SHA1 and then extracting just enough bytes for the key :
But we don’t really care about this, we know that the
CreateDecryptor is going to receive the secret Key and this is what matters :
gotcha ! They key must be the 16 bytes array we just see.
Is it right ?
The simplest way to verify this is to bake a small python script that is going to mimic CloudBerry’s crypto.
After a few mistakes mostly due to the fact that I missed that they pad the cleartext with a leading
0 (for reasons I guess), we have something that works :
from pkcs7 import PKCS7Encoder from Crypto.Cipher import AES import base64 import binascii import sys # the block size for the cipher object; must be 16, 24, or 32 for AES BLOCK_SIZE = 16 iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" key = "\x53\xd1\x1b\xcf\x19\x61\xb3\x66\xe4\xfb\xeb\x81\xb9\xbf\xb9\x48" def usage(): print ("<enc|dec> <data>") print ("ex: enc 09RsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxTkV4fTrH") print ("\twhere '09RxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxV4fTrH' is AWS raw secret") print ("ex: dec ZxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxbdQ3wr") print ("\twhere 'ZS5jxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxbdQ3wr' is 'encrypted' aws secret from XML file") def decrypt(encrypted): encoder = PKCS7Encoder() cipher = AES.new(key , AES.MODE_CBC, iv) dec = encoder.decode(cipher.decrypt(base64.b64decode(encrypted))) return dec[1:] def encrypt(cleartext): encoder = PKCS7Encoder() cipher = AES.new(key , AES.MODE_CBC, iv) pad_text = encoder.encode("0" + cleartext) enc = base64.b64encode(cipher.encrypt(pad_text)) return enc if len(sys.argv) < 2: usage() exit(1) elif sys.argv == "enc": print encrypt(sys.argv) elif sys.argv == "dec": print decrypt(sys.argv) else: usage() exit(1)
It was a fun experience and a good occasion to try dnSpy, which was a pleasant experience :)