Introduction

I had to work on a PE for reverse engineering for a few weeks, the PE using an extensive amount of encryption/decryption of memory fragments to make it harder to reverse engineer it. However, that PE is for another time. But I had to write a manual encrypter/decrypter for those memory fragments to be able to patch some specific parts without changing instructions size and then crypting those bytes and replacing those specific memory fragments. However it seems the key that CryptoAPI uses to do the job is not the same as the key that you could get with CryptExportKey so I decided to look inside these functions to see what the actual key is.


CryptoAPI basic structure

CryptoAPI passes the input data as objects between its functions. The obvious reason for that is to make the attacker lifes harder. It takes your data, then hashes it with the intended algorithm, deriving its own specific key, and using them to do the job. So you can’t use your own key to encrypt/decrypt, instead you have to use its API to export the derived key based on your input. There are cases where you want to make your key exportable, but even then there are known vulnerabilities to make the key exportable. So to do a little experiment, I used this code and modified it a little bit, added my own little function and then used output to reverse engineer a specific CryptoAPI.


Example source code

The provided source code is nothing special, I compiled the file as x64 Debug mode with C++20 on Visual Studio 2022.

#include <Windows.h>
#include <stdio.h>
#include <stdio.h>
#include <iostream>
#include <cstdio>
#include <bitset>
#include <vector>
#pragma comment(lib, "crypt32.lib")

BOOL GetExportedKey(HCRYPTKEY hKey, DWORD dwBlobType, LPBYTE* ppbKeyBlob, LPDWORD pdwBlobLen)
{
    DWORD dwBlobLength;
    *ppbKeyBlob = NULL;
    *pdwBlobLen = 0;
    // Export the public key. Here the public key is exported to a 
    // PUBLICKEYBLOB. This BLOB can be written to a file and
    // sent to another user.
    if (CryptExportKey(hKey, NULL, dwBlobType, 0, NULL, &dwBlobLength))
    {
        printf("Size of the BLOB for the public key determined. \n");
    }
    else
    {
        printf("Error computing BLOB length.\n");
        return false;
    }
    // Allocate memory for the pbKeyBlob.
    if (*ppbKeyBlob = (LPBYTE)malloc(dwBlobLength))
    {
        printf("Memory has been allocated for the BLOB. \n");
    }
    else
    {
        printf("Out of memory. \n");
        return false;
    }
    // Do the actual exporting into the key BLOB.
    if (CryptExportKey(
        hKey,
        NULL,
        dwBlobType,
        0,
        *ppbKeyBlob,
        &dwBlobLength))
    {
        printf("Contents have been written to the BLOB. \n");
        *pdwBlobLen = dwBlobLength;
    }
    else
    {
        printf("Error exporting key.\n");
        free(*ppbKeyBlob);
        *ppbKeyBlob = NULL;

        return false;
    }
    return true;
}
bool ExportKeyBytes(LPBYTE keyBlob, DWORD keyBlobLength, std::vector <std::bitset<8>>& KeyBytes)
{
    uint32_t NumberOfBytes = *(uint32_t*)(keyBlob + sizeof(BLOBHEADER));
    intptr_t StartAdrsOfKeyBytes = reinterpret_cast<intptr_t>((keyBlob + sizeof(BLOBHEADER) + sizeof(DWORD)));
    std::cout << std::endl << "------Exctracted key bytes are as below--------" << std::endl;
    for (uint16_t j = 0; j < NumberOfBytes; j++)
    {
        KeyBytes.push_back(*((std::bitset<8>*)StartAdrsOfKeyBytes));
        StartAdrsOfKeyBytes++;
        unsigned long int Mask = 0b0000000011111111;
        Mask &= (KeyBytes[j].to_ulong());
        std::cout << " " << std::hex << Mask;
    }
    std::cout << std::endl;
    std::cout << "The bit representation of key blob:" << std::endl;
    for (uint16_t j = 0; j < KeyBytes.size(); j++)
        std::cout << std::bitset<8>((KeyBytes[j]));
    std::cout << std::endl;
    return true;
}

int main()
{
    HCRYPTPROV hProv = 0;
    HCRYPTKEY hKey = 0;
    HCRYPTHASH hHash = 0;
    DWORD dwCount = 5;
    LPBYTE keyBlob = NULL;
    DWORD keyBlobLength;
    LPSTR keyBlobBase64 = NULL;
    DWORD base64Length = 0;
    BYTE  rgData[512] = { 0x01, 0x02, 0x03, 0x04, 0x05 };
    LPWSTR wszPassword = (LPWSTR)L"password12";
    DWORD cbPassword = (wcslen(wszPassword) + 1) * sizeof(WCHAR);

    if (!CryptAcquireContext(
        &hProv,
        NULL,
        MS_DEF_PROV,
        PROV_RSA_FULL,
        CRYPT_VERIFYCONTEXT))
    {
        printf("Error %x during CryptAcquireContext!\n", GetLastError());
        goto Cleanup;

    }
    if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash))
    {
        printf("Error %x during CryptCreateHash!\n", GetLastError());
        goto Cleanup;

    }
    if (!CryptHashData(hHash, (PBYTE)wszPassword, cbPassword, 0))
    {
        printf("Error %x during CryptHashData!\n", GetLastError());
        goto Cleanup;

    }
    if (!CryptDeriveKey(hProv, CALG_RC4, hHash, CRYPT_EXPORTABLE, &hKey))
    {
        printf("Error %x during CryptDeriveKey!\n", GetLastError());
        goto Cleanup;

    }
    if (!GetExportedKey(hKey, PLAINTEXTKEYBLOB, &keyBlob, &keyBlobLength))
    {
        printf("Error %x during GetExportedKey!\n", GetLastError());
        goto Cleanup;

    }

    while (1)
    {
        // PLAINTEXTKEYBLOB: BLOBHEADER|DWORD key length|Key material|
        DWORD keyMaterialLength;
        LPBYTE keyMaterial;
        keyMaterialLength = *(DWORD*)(keyBlob + sizeof(BLOBHEADER));
        keyMaterial = (keyBlob + sizeof(BLOBHEADER) + sizeof(DWORD));
        if (!CryptBinaryToStringA(keyMaterial, keyMaterialLength, CRYPT_STRING_BASE64, keyBlobBase64, &base64Length))
        {
            printf("Error %x during GetExportedKey!\n", GetLastError());
            goto Cleanup;
        }

        if (keyBlobBase64)
        {
            std::cout << std::endl << "Base64 string representation of key blob:--------" << std::endl;
            printf("%d-bit key blob: %s\n", keyMaterialLength * 8, keyBlobBase64);
            break;
        }
        else
        {
            keyBlobBase64 = (LPSTR)malloc(base64Length);
            std::vector <std::bitset<8>> KeyBytes;
            ExportKeyBytes(keyBlob, keyBlobLength, KeyBytes);
            std::cout << "------ _PLAINTEXTKEYBLOB struct bytes are as below:--------" << std::endl;
            for (unsigned int j = 0; j < keyBlobLength; j++)
                printf("%02x ", ((char*)keyBlob)[j] & 0b0000000011111111);
            std::cout << std::endl;
        }
    }
    std::cout << "------ Data bytes before encyption:--------" << std::endl;
    for (DWORD i = 0; i < dwCount; i++)
    {
        printf("%d ", rgData[i]);
    }
    printf("\n");

    if (!CryptEncrypt(hKey, 0, TRUE, 0, rgData, &dwCount, sizeof(rgData)))
    {
        printf("Error %x during CryptEncrypt!\n", GetLastError());
        goto Cleanup;
    }
    std::cout << "------ Data bytes after encyption:--------" << std::endl;
    for (DWORD i = 0; i < dwCount; i++)
    {
        printf("%02x ", ((char*)rgData)[i] & 0b0000000011111111);
    }
    printf("\n");
Cleanup:
    free(keyBlob);
    free(keyBlobBase64);
    if (hKey)
    {
        CryptDestroyKey(hKey);
    }
    if (hHash)
    {
        CryptDestroyHash(hHash);
    }
    if (hProv)
    {
        CryptReleaseContext(hProv, 0);
    }

    return 0;
}

And after running the program you will see the following output:

Size of the BLOB for the public key determined.
Memory has been allocated for the BLOB.
Contents have been written to the BLOB.

------Exctracted key bytes are as below--------
83 30 c7 bb 70
The bit representation of key blob:
1000001100110000110001111011101101110000
------ _PLAINTEXTKEYBLOB struct bytes are as below:--------
08 02 00 00 01 68 00 00 05 00 00 00 83 30 c7 bb 70

Base64 string representation of key blob:--------
40-bit key blob: gzDHu3A=

------ Data bytes before encyption:--------
1 2 3 4 5
------ Data bytes after encyption:--------
0e 39 88 22 87

As you can see the key size should be 5 bytes, and you can see them as 83 30 c7 bb 70, however if you try to decrypt bytes 0e 39 88 22 87, it does not work. I had to look inside the CryptEncrypt to see what the key was.


Analysis

For the debugging I used x64dbg and Ghidra. The two dlls that we need are cryptsp.dll and rsaenh.dll. So just find them in your windows directory, or use the provided ones in the archive. After you found them you can use symchk.exe to download the PDB files to use them with Ghidra with the following command:

symchk.exe "C:\dllname.dll" /s SRV*C:\*http://msdl.microsoft.com/download/symbols

The command will download the PDB in C directory for the provided dll in C directory. So finally drag dll files in Ghidra and let them analyzed with PDB files. If the above command did not worked (you have to install WDK before), you can search for symchk.exe on the installed location of WDK, then open CMD as admin and them going to the location of sychk.exe them execute command in that address. So we open the file in x64dbg and put a breakpoint on CryptEncrypt.

Image_1

Then head to the call stack so we can put a BP inside our program to be able see passing arguments. Simple click on the highlighted green address.

Image_2

So we put a BP on the CryptEncrypt inside main.

Image_3

Now we restart debugger and hit run till hitting the BP inside main. We know CryptEncrypt takes 7 arguments and the first argument is a pointer to the hashed blob. So we just look at stack:

Image_4

So we just follow that address in memory by clicking on it and following it in the dump. So we just follow that address in memory by clicking on it and following it in the dump. After a little bit analysis you will notice we are seeing numbers 11 and 5 in memory regardless of number of execution, so it is a good idea to put hardware dword access breakpoint on bytes 05 00 00 00.

Image_5

After putting that BP, we execute the program till it gets hit.

Image_6

In the image you can see all the information. So CryptEncrypt uses 16 bytes keys, and you can clearly see our program output bytes 83 30 c7 bb 70 with following zeros. Now if you simply use key 83 30 c7 bb 70 00 00 00 00 00 00 00 00 00 00 00, for RC4 algorithm you can crypt our input bytes 01 02 03 04 05. I was not able to find much information about it so I thought it’s a good idea to share it here.


Last word

Even though I was able to find the actual key for the RC4 algorithm, looking at decompiled code in Ghidra, it is not very clear for obvious reasons.

Image_7

If you are looking for more, you can also read this and this. But for now I think this will be enough for me to finish my other projects.