Overcoming AES Block Size Challenges Within Encrypted C Strings

I was told that C strings were bad, It wasn't until I worked with it that I realized how bad

So I was working on some malware as part of Sektor7’s Malware Development Essentials course. Specifically, the final project. While testing out a string encryption change from XOR to AES, I was having issues getting pointer locations to functions. I was having a lot of random garbage at the end of my function calls that were causing crashes. This screenshot shows the issue with VirtualAllocEx, although we will focus on WriteProcessMemory for the rest of the article, because it gives a better example of how this works.

Terminal output showing encrypted strings and decrypted strings with random garbage at the end. If you look at VirtualAllocEx, you can see the effect I am talking about in this article.

Ye Olde Stream vs Block Cipher

A XOR is a stream cipher. Essentially, that means that you can give it arbitrary plaintext of any length, and it can process it without issue. Now compared to AES-256 which is a block cipher. Which means that you give it an arbitrary string and may have to pad extra bytes to the end of it in order to be able to process it in 16 byte chunks.

C strings are weird

If you are unfamiliar with C strings, they are not like most programming languages we use today. Instead of being their own data type, instead they are an array of chars that are meant to be terminated with a null byte. So, take this encrypted string below.

unsigned char sWriteProcessMemory[] = { 0x85, 0x34, 0xc, 0x6c, 0x83, 0xe4, 0x8d, 0x43, 0x18, 0x25, 0x99, 0x65, 0xe9, 0x62, 0x32, 0x7e, 0xe1, 0x52, 0x24, 0x31, 0xb7, 0xc, 0x29, 0x1e, 0xbf, 0x40, 0x17, 0x14, 0x1e, 0x81, 0xee, 0xaa };

The plaintext of the above string is supposed to be WriteProcessMemory, which is 18 bytes long. If you take that 18 > 16, which means then that AES needs to parse it as a second block. So if it assumes 2 blocks at 16 bytes each, that takes it to being 18 < 32. Which means that all the data fits within 2 blocks. Great! However this gets weird again. Because while it fits within 2 blocks, AES needs every byte filled in order to process data. So the algorithm pads extra content at the end fill the rest of the block. Which essentially means after AES pads 14 bytes, we have 32 == 32. 2 blocks even. Perfect.

So why is that mentioned in C strings?

With most languages, this should be either not a problem, or a minimal problem. But there is one component about C strings that are weirder than the rest. Every string must be terminated by a null byte, which is represented with ‘\0’ in C. Now, if your plaintext string is defined within C this should be no problem. When you enter the decryption key, it should bring your plaintext back when you use the key on your ciphertext, and the null byte should still be present. In the course however, the string encryption is handled via Python script.

Handling null bytes with Python and C

So there are at least 2 options here to ensure a null byte stays in the end. One potential option is to add a null byte into the end of your plaintext in Python. Or you can do the janky thing that I did (because the first option didn’t come to me until writing this), and that was get the lengths of your plaintext, and your ciphertext and determine the difference between them. So in this case given the string provided earlier, here is how I handled it.

unsigned char sWriteProcessMemory[] = { 0x85, 0x34, 0xc, 0x6c, 0x83, 0xe4, 0x8d, 0x43, 0x18, 0x25, 0x99, 0x65, 0xe9, 0x62, 0x32, 0x7e, 0xe1, 0x52, 0x24, 0x31, 0xb7, 0xc, 0x29, 0x1e, 0xbf, 0x40, 0x17, 0x14, 0x1e, 0x81, 0xee, 0xaa }; //Initial String
AESDecrypt((char *) sWriteProcessMemory, sizeof(sWriteProcessMemory), key, sizeof(key));AESDecrypt((char *) sWriteProcessMemory, sizeof(sWriteProcessMemory), key, sizeof(key)); //Defined earlier in the program
sWriteProcessMemory[sizeof(sWriteProcessMemory) - 14] = '\\0'; //Inserts null byte at the end of the initial string so it ignores random gibberish at the end of it.
printf("\\n%s",sWriteProcessMemory); //Should return "WriteProcessMemory" without random garbage now

Again, I took count of the number of padded bytes by taking the difference of length of the original python string, and the encrypted string and printing it to the terminal along with the encrypted strings when I was finished.

Thanks and shout outs

Thank you to register#7493, heartburn#2701, archiba#0025 within the Zero Point Security Discord server for helping me figure out this issue when it came up. I had spent over a day working on it to no avail, and it was really driving me nuts.