Friday, September 21, 2018

CryptoApi Derandomization

In this post I dig a bit into the Windows's CryptoApi library, trying to locate the code that the library uses to fill buffers with random data when it needs to randomize something (i.e. paddings for plaintexts at RSA encryption, randomly generated AES keys, etc...). After locating the interesting functions, I wrote a PoC that I share in this post and that intercepts that functions to disable CryptoApi randomization to have deterministic ciphering results. Finally,  I do a quick reversing on an OrcaKiller sample (it uses CryptoApi), disabling CryptoApi's randomization to make the analysis easy.

Lets start debugging CryptoApi's CryptEncrypt when RSA encryption is performed, searching for the moment when the plaintext's padding is randomly generated. In this way we will try to find what functions are used to fill buffers with random data. I have done tests at two environments, a Win10 x64 machine, and a Win7 x86 machine. It's a bit different from a version to other. I don't know about other Windows versions, but I guess it will be quite similar to these environments.

Analysis: RSA CryptEncrypt

CryptoApi RSA encryption interface is not very versatile when you want to configure how the plaintext's padding is going to be generated. According to the documentation, it should be possible to configure a flag "ZERO_PADDING" or a flag "RANDOM_PADDING", but they don't work, these flags are disabled. Only the default PKCS5_PADDING works.

Other cryptography libraries are much more flexible, for example pycryptodome lets you to configure easily a callback where you can set the padding that you want:

Microsoft is more restrictive with this, and it seems impossible to use the api in a "legal" way that would let us to choose the padding and obtain a deterministic ciphertext (in spite of the fact that we use the same plaintext and the same key, the padding is being generated randomly).

Lets see a call stack of CryptEncrypt until the moment when random padding is generated.



As we can see, part of the code has been moved from rsaenh.dll to bcrypt.dll/bcryptprimitives.dll in newer versions.

Win10 SymCryptRsaPkcs1Encrypt+SymCryptPkcs1ApplyEncryptionPadding vs Win7 PKCS2Encrypt

At Win10 x64, the function that adds the padding is SymCryptPkcs1ApplyEncryptionPadding, it is called from SymCryptRsaPkcs1Encrypt:

To fill buffers with random values, SymCryptPkcs1ApplyEncryptionPadding uses a function named ProcessPrng (it is easy to locate this function because it is exported by bcryptprimitives.dll). ProcessPrng is like this:

signed __int64 __fastcall ProcessPrng(void * buf, SIZE_T len);

At Win7 x86 it is a bit different, as we can see in the call stack that previously I pasted all the involved code is located at rsaenh.dll, and the function that adds the padding to the plaintext is PKCS2Encrypt. This function calls other function, FIPS186GenRandom, and this one will fill a given buffer with random data. FIPS186GenRandom directly calls to AesCtrWithFipsChecks. AesCtrWithFipsChecks would be the equivalent to Win10's ProcessPrng.

AesCtrWithFipsChecks is like this:

int __stdcall AesCtrWithFipsChecks(void *pbBuffer, int cbBuffer, int a, int b);

Locating FIPS186GenRandom or AesCtrWithFipsChecks is not easy because they are not exported by a library.

There are MS symbols for these functions, so you could use symbols to locate them (if you are interested in using Microsoft symbols API, I wrote this PoC where I used symbols API to find some chrome.dll code:

Or it would be possible to find it by searching a pattern. For example, rsaenh!CPGenRandom (an exported function easy to locate) uses AesCtrWithFipsChecks:

You can get easily a pointer to CPGenRandom (it is an export) and find the pointer to AesCtrWithFipsChecks from there:

If we hook ProcessPrng at Win10 x64, or AesCtrWithFipsChecks at Win7 x86, we can control the way that CryptoApi fills random buffers (paddings, randomly generated AES keys, etc...).

PoC: Derandomize CryptoApi

I coded a simple PoC where a plaintext is encrypted with a randomly generated AES key and a RSA public key. In this PoC, ProcessPrng and AesCtrWithFipsChecks are hooked, and in the hook we are filling the input buffer with constant values (\x01\x01....). Doing this, the generated AES key is always the same, and the RSA padding too. The resulting ciphertext for each stage (after AES encryption and after RSA encryption) is deterministic, for a same plaintext and keys, the ciphertext is always the same:

Analysis: Derandomize CryptoApi to analyze OrcaKiller RAT

Lets analyze an OrcaKiller RAT old sample (2014) by using CryptoApi derandomization that I have explained in this post.

Here you can read a bit about this simple RAT, OrcaKiller:

To encrypt data, this RAT uses rc4. The rc4key is calculated in this way:

rc4key = md5(<6 random bytes> + "OrcaKiller")

After rc4, it applies base64.

The malware uses CryptoApi (md5 is calculated with CryptHashData, rc4 with CALG_RC4 + CryptEncrypt, etc...). Lets suppose it generates that 6 random bytes to calculate the rc4key with CryptGenRandom. So we assume if we derandomize CryptoApi, it will use the same rc4key always (we will know this key because we will force it). Instead of debugging, we can directly intercept AesCtrWithFipsChecks to derandomize the rc4key, and then, we capture network traffic with Wireshark, and we should be able to decrypt all the CnC queries that we captured because we know the rc4key that we have forced.

I use WinDbg, I coded a tiny script for a conditional breakpoint that will cause that AesCtrWithFipsChecks will fill the input buffer with "\x01\x01\x01..." instead of filling with random data (it should be easy to do something similar for immunitydebugger or other debuggers):

bp /p <target_proc> AesCtrWithFipsChecks "
     r $t0 = poi (esp+4); //pointer to buffer to fill
     r $t1 = poi (esp+8); //length of buffer to fill
          eb $t0 1; //fill curpos with \x01
          r $t1 = @$t1 - 1;
          r $t0 = @$t0 + 1;
     r eip = poi(esp); //dont execute the original AesCtrWithFipsChecks code, return
     r esp = esp+14; //remove retaddr and params
     r eax = 0; //return 0
     g;" //continue

Here it is without crlf / comments:   

bp /p 84abf528 AesCtrWithFipsChecks "r $t0 = poi (esp+4);r $t1 = poi (esp+8);.while(@$t1>0){eb $t0 1;r $t1 = @$t1 - 1;r $t0 = @$t0 + 1;};r eip = poi(esp);r esp = esp+14;r eax = 0;g;"

We let the malware to continue the execution while we capture network traffic:

We can see that, after derandomizing CryptoApi, the encrypted queries are identical (it is the same plaintext, and we have forced the same rc4key). This is the query encrypted with the constant rc4key that we forced = md5("\x01\x01\x01\x01\x01\x01OrcaKiller"):

We try to decrypt some encrypted parts of this query with a python script, and we can see that we are able to decrypt them with the rc4key that we have forced:

Really, this OrcaKiller RAT is easy to analyze, it is not necesary to do all of this to reverse it, probably it would have been faster to set some right breakpoints at some apis and log arguments. But it was perfect to use it as simple example to understand how derandomizing CryptoApi could help, for example, to decrypt data captured with Wireshark, or data from files that were written by the malware, etc... 

No comments:

Post a Comment