Modifying or replacing a DLL is one way that a hacker can gain access to your protected application. If you’re using the Nitro-LM solution with the DLL client, or even if you’re only interested in protecting your own DLLs, the solution provided here should prove useful to you. For our purposes, we’ll focus on how you can code your application to prevent tampering on the nitrolm.dll inside a C++ application.
Nitro-LM is an Internet-based licensing solution. It allows you to license and protect your application from unauthorized users. Nitro-LM offers many client options, one of which is the DLL method. Your application calls licensing functions inside the DLL to retrieve licenses, create users, store server variables, etc. It checks response codes from the function calls to determine whether operations succeeded or not.
How can you prevent a hacker from modifying this DLL, or replacing it entirely with their own that returns positive responses and thus unlocking an unauthorized copy of your software?
The nitrolm.dll file is digitally signed with Microsoft Authenticode technology. In order for your software to detect if the dll has been changed or tampered with, you’ll need to verify the digital signature in your code.
The verification is a two-step process in this example. The first step is to verify the embedded signature on the DLL. This will detect if the DLL has been tampered with, but will still succeed if a hacker has replaced your signature with their own. The second step is to verify that the DLL is signed with the correct signature. This will ensure that the hacker hasn’t replaced your certificate with their own, or replaced the DLL in its entirety with their own. The beginning of your main method should look something like this:
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
char str[1024];
int ret = 0;
char outmessage[512];
//verify the dll's signature
int success = VerifyEmbeddedSignature(L"nitrolm.dll", outmessage);
if(success)
int success = VerifyCertificate(L"nitrolm.dll", outmessage);
if(success <= 0)
{
MessageBox(NULL, outmessage, "DLL Verification Error", 0);
return 0;
}
/** call some dll method here **/
}
Let’s take a step back and look at how the DLL was signed in the first place (for those of you wanted to use this technique on your own DLLs). The tool used to digitally sign the application was signtool.exe. It can be downloaded as part of the Windows SDK or the .NET framework. I also have to retrieve a code-signing certificate. For the Nitro-LM DLL, we use a Thawte code signing certificate. After purchasing, Thawte gives you the certificate as a .spc file and a .pvk file which is your private key. I used the pvkimport tool to combine these into a .pfx file to make the signing process easier.
pvkimprt -PFX thawte_cert.spc thawte_cert.pvk
Now that I have a thawte_cert.pfx file, I can digitally sign my DLL like so:
signtool sign /v /f thawte_cert.pfx /p ****** /d "Nitro-LM C++ DLL Client" /du http://www.simplifiedlogic.com /t http://timestamp.verisign.com/scripts/timstamp.dll nitrolm.dll
If all went well, I should see some output like this:
The following certificate was selected: Issued to: Simplified Logic, Inc. Issued by: Thawte Code Signing CA Expires: mm/dd/yyyy hh:mm:ss PM SHA1 hash: **************************************** Done Adding Additional Store Attempting to sign: nitrolm.dll Successfully signed and timestamped: nitrolm.dll Number of files successfully Signed: 1 Number of warnings: 0 Number of errors: 0
Now back to verifying the signature. The first method we want to focus on is VerifyEmbeddedSignature. This function was largely lifted from the MSDN documentation with some minor changes. The important call inside this method is WinVerifyTrustEx. In order to use it, you’ll need to link with wintrust.lib since the method is included in wintrust.dll.
#include <tchar.h>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <softpub.h>
#include <wincrypt.h>
#include <wintrust.h>
/**
* verify a dll's signature
* -1 means NOT trusted
* 1 means trusted
* 0 means you should prompt the user and ask them
*/
int VerifyEmbeddedSignature(LPCWSTR pwszSourceFile, char *outmessage)
{
LONG lStatus;
DWORD dwLastError;
// Initialize the WINTRUST_FILE_INFO structure.
WINTRUST_FILE_INFO FileData;
memset(&FileData, 0, sizeof(FileData));
FileData.cbStruct = sizeof(WINTRUST_FILE_INFO);
FileData.pcwszFilePath = pwszSourceFile;
FileData.hFile = NULL;
FileData.pgKnownSubject = NULL;
GUID WVTPolicyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_DATA WinTrustData;
// Initialize the WinVerifyTrust input data structure.
// Default all fields to 0.
memset(&WinTrustData, 0, sizeof(WinTrustData));
WinTrustData.cbStruct = sizeof(WinTrustData);
// Use default code signing EKU.
WinTrustData.pPolicyCallbackData = NULL;
// No data to pass to SIP.
WinTrustData.pSIPClientData = NULL;
// Disable WVT UI.
WinTrustData.dwUIChoice = WTD_UI_NONE;
// No revocation checking.
WinTrustData.fdwRevocationChecks = WTD_REVOKE_NONE;
// Verify an embedded signature on a file.
WinTrustData.dwUnionChoice = WTD_CHOICE_FILE;
// Default verification.
WinTrustData.dwStateAction = 0;
// Not applicable for default verification of embedded signature.
WinTrustData.hWVTStateData = NULL;
// Not used.
WinTrustData.pwszURLReference = NULL;
// Default.
WinTrustData.dwProvFlags = WTD_SAFER_FLAG;
// This is not applicable if there is no UI because it changes
// the UI to accommodate running applications instead of
// installing applications.
WinTrustData.dwUIContext = WTD_UICONTEXT_EXECUTE;
// Set pFile.
WinTrustData.pFile = &FileData;
// WinVerifyTrust verifies signatures as specified by the GUID
// and Wintrust_Data.
lStatus = WinVerifyTrustEx(
0,
&WVTPolicyGUID,
&WinTrustData);
switch (lStatus)
{
case ERROR_SUCCESS:
/*
Signed file:
- Hash that represents the subject is trusted.
- Trusted publisher without any verification errors.
- UI was disabled in dwUIChoice. No publisher or
time stamp chain errors.
- UI was enabled in dwUIChoice and the user clicked
"Yes" when asked to install and run the signed
subject.
*/
sprintf_s(outmessage,512,"The file \"%S\" is signed and the signature was verified.\n",
pwszSourceFile);
return 1;
case TRUST_E_NOSIGNATURE:
// The file was not signed or had a signature
// that was not valid.
// Get the reason for no signature.
dwLastError = GetLastError();
if (TRUST_E_NOSIGNATURE == dwLastError ||
TRUST_E_SUBJECT_FORM_UNKNOWN == dwLastError ||
TRUST_E_PROVIDER_UNKNOWN == dwLastError)
{
// The file was not signed.
sprintf_s(outmessage, 512, "The file \"%S\" is not signed.\n",
pwszSourceFile);
}
else
{
// The signature was not valid or there was an error
// opening the file.
sprintf_s(outmessage, 512, "An unknown error occurred trying to "
"verify the signature of the \"%S\" file.\n",
pwszSourceFile);
}
return 0;
case TRUST_E_EXPLICIT_DISTRUST:
// The hash that represents the subject or the publisher
// is not allowed by the admin or user.
sprintf_s(outmessage, 512, "The signature is present, but specifically "
"disallowed.\n");
return -1;
case TRUST_E_SUBJECT_NOT_TRUSTED:
// The user clicked "No" when asked to install and run.
sprintf_s(outmessage, 512, "The signature is present, but not "
"trusted.\n");
return -1;
case CRYPT_E_SECURITY_SETTINGS:
/*
The hash that represents the subject or the publisher
was not explicitly trusted by the admin and the
admin policy has disabled user trust. No signature,
publisher or time stamp errors.
*/
sprintf_s(outmessage, 512, "CRYPT_E_SECURITY_SETTINGS - The hash "
"representing the subject or the publisher wasn't "
"explicitly trusted by the admin and admin policy "
"has disabled user trust. No signature, publisher "
"or timestamp errors.\n");
return -1;
default:
// The UI was disabled in dwUIChoice or the admin policy
// has disabled user trust. lStatus contains the
// publisher or time stamp chain error.
sprintf_s(outmessage, 512, "Error is: 0x%x.\n",
lStatus);
return -1;
}
return -1;
}
The second method to look at is VerifyCertificate. This ensures that the DLL is signed with OUR certificate. To do the verification, we’re going to compare the a memory structure representing the public key to the public key embedded in the DLL. The public key listed in the code below isn’t real and is included for example purposes.
BYTE pubKey[270] = {
0x32, 0x84, 0x09, 0x0A, 0x32, 0x82, 0x0B, 0xB1, 0x30, 0xE6, 0xE7, 0x5C,
0x1B, 0x1A, 0x19, 0x33, 0x43, 0x99, 0xE2, 0x67, 0x78, 0x31, 0xFD, 0x47,
0x0C, 0xCC, 0x95, 0x75, 0x11, 0xF6, 0xAB, 0x2C, 0x9B, 0xF1, 0x7E, 0xD3,
0x6B, 0x0D, 0x8C, 0x31, 0x9E, 0x8A, 0x7D, 0x4F, 0x70, 0xE9, 0x51, 0x40,
0x2E, 0xAA, 0xE0, 0x27, 0x4A, 0x21, 0x86, 0x2F, 0x0B, 0x4F, 0x87, 0x80,
0xC2, 0xD4, 0xCB, 0x71, 0xDC, 0x12, 0x00, 0xEC, 0xCE, 0x0A, 0x40, 0xD1,
0x52, 0xEA, 0x96, 0x39, 0x37, 0xC6, 0x38, 0x90, 0xCE, 0xF3, 0x6C, 0x3B,
0x7A, 0xB9, 0xD4, 0x1F, 0x69, 0x3C, 0xBB, 0x7C, 0x22, 0x93, 0x68, 0x17,
0xB4, 0x77, 0xB5, 0x52, 0x09, 0x07, 0x15, 0x27, 0x75, 0x87, 0x75, 0xD1,
0xEC, 0x31, 0xE5, 0x62, 0xCB, 0xA9, 0x36, 0xC9, 0x6E, 0x22, 0x3C, 0xF7,
0x55, 0xE6, 0x2D, 0xDF, 0xBC, 0xEC, 0x57, 0xA3, 0x95, 0x10, 0x30, 0x28,
0x1A, 0x7D, 0x69, 0x80, 0x9D, 0xF9, 0x71, 0x40, 0x08, 0xA8, 0x84, 0xD5,
0xF4, 0xB8, 0x2B, 0xA0, 0x7D, 0x01, 0xAE, 0xD5, 0x8F, 0xAD, 0xBC, 0x13,
0xA8, 0x96, 0x72, 0x01, 0x64, 0xC6, 0x6F, 0x70, 0x7D, 0xDF, 0xFA, 0x39,
0xF4, 0xED, 0xCB, 0x6D, 0x2F, 0x51, 0x2E, 0x7A, 0xEE, 0xF9, 0xCD, 0xEB,
0x35, 0x33, 0x8D, 0x12, 0x86, 0x2F, 0x00, 0x4A, 0xEB, 0x59, 0x8C, 0x16,
0x1D, 0x28, 0xE2, 0x2E, 0xFC, 0x24, 0x4B, 0xA9, 0xCA, 0xCD, 0x57, 0x27,
0xA1, 0xA5, 0x8F, 0x2B, 0x1D, 0x7F, 0x2D, 0x5F, 0xBF, 0x89, 0x84, 0x15,
0x3E, 0x99, 0xFE, 0x5D, 0x74, 0xDA, 0x49, 0x9C, 0x92, 0x55, 0xF2, 0xCC,
0xB2, 0x17, 0x43, 0x5B, 0x45, 0x96, 0x55, 0xEB, 0xC8, 0x4E, 0xC3, 0xD0,
0x37, 0xE2, 0xD3, 0x71, 0x38, 0x13, 0x02, 0xC9, 0x5C, 0x00, 0xF7, 0x17,
0xF7, 0xEF, 0x2F, 0xDD, 0x51, 0x83, 0x1A, 0xEB, 0x9C, 0x04, 0xF9, 0x02,
0x93, 0x04, 0x03, 0xA1, 0x05, 0x07
};
int VerifyCertificate(LPCWSTR pwszSourceFile, char *outmessage)
{
HCERTSTORE certificateStore;
HCRYPTMSG cryptMsg;
BYTE certInfo[256];
memset(certInfo, 0 , 256);
DWORD infoLength=256;
int retVal;
if(CryptQueryObject(CERT_QUERY_OBJECT_FILE,
pwszSourceFile,
CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED,
CERT_QUERY_FORMAT_FLAG_BINARY,
0,
NULL,
NULL,
NULL,
&certificateStore,
&cryptMsg,
NULL))
{
if(CryptMsgGetParam(cryptMsg, CMSG_SIGNER_CERT_INFO_PARAM, 0, certInfo, &infoLength))
{
PCERT_INFO pCertInfo = (PCERT_INFO)certInfo;
PCCERT_CONTEXT certCtx = CertFindCertificateInStore(certificateStore,
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
0,
CERT_FIND_SUBJECT_CERT,
pCertInfo,
NULL);
if(certCtx != NULL)
{
if(0==memcmp(certCtx->pCertInfo->SubjectPublicKeyInfo.PublicKey.pbData,
pubKey,
certCtx->pCertInfo->SubjectPublicKeyInfo.PublicKey.cbData))
{
strcpy_s(outmessage, 512, "DLL Verified successfully");
retVal = 1;
}
else
{
strcpy_s(outmessage, 512, "Certificate does not match dll certificate");
retVal = -1;
}
CertFreeCertificateContext(certCtx);
}
else
{
strcpy_s(outmessage, 512, "Unable to find nitrolm certificate in dll");
retVal = -1;
}
}
else
{
strcpy_s(outmessage, 512, "Unable to retrieve certificate info in dll");
retVal = -1;
}
}
else
{
strcpy_s(outmessage, 512, "Could not open certificate store in dll");
retVal = -1;
}
return retVal;
}
As you can see, the above method can go a long way to preventing tampering with DLLs. As with any security measure nothing will be perfect short of some form of quantum encryption. I hope you’ve found this example useful.