/* * Rufus: The Reliable USB Formatting Utility * PKI functions (code signing, etc.) * Copyright © 2015-2024 Pete Batard * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* Memory leaks detection - define _CRTDBG_MAP_ALLOC as preprocessor macro */ #ifdef _CRTDBG_MAP_ALLOC #include #include #endif #include #include #include #include #include #include "rufus.h" #include "resource.h" #include "msapi_utf8.h" #include "localization.h" #define ENCODING (X509_ASN_ENCODING | PKCS_7_ASN_ENCODING) // MinGW doesn't seem to have this one #if !defined(szOID_NESTED_SIGNATURE) #define szOID_NESTED_SIGNATURE "1.3.6.1.4.1.311.2.4.1" #endif // Signatures names we accept. Must be the the exact name, including capitalization, // that CertGetNameStringA(CERT_NAME_ATTR_TYPE, szOID_COMMON_NAME) returns. const char* cert_name[3] = { "Akeo Consulting", "Akeo Systems", "Pete Batard" }; // For added security, we also validate the country code of the certificate recipient. const char* cert_country = "IE"; typedef struct { LPWSTR lpszProgramName; LPWSTR lpszPublisherLink; LPWSTR lpszMoreInfoLink; } SPROG_PUBLISHERINFO, *PSPROG_PUBLISHERINFO; // https://msdn.microsoft.com/en-us/library/ee442238.aspx typedef struct { BLOBHEADER BlobHeader; RSAPUBKEY RsaHeader; BYTE Modulus[256]; // 2048 bit modulus } RSA_2048_PUBKEY; // For PKCS7 WDAC Code Integrity processing #define PE256_HASHSIZE 32 const GUID SKU_CODE_INTEGRITY_POLICY = { 0x976d12c8, 0xcb9f, 0x4730, { 0xbe, 0x52, 0x54, 0x60, 0x08, 0x43, 0x23, 0x8e} }; typedef struct { WORD Nano; WORD Micro; WORD Minor; WORD Major; } CIVersion; typedef struct { DWORD PolicyFormatVersion; GUID PolicyTypeGUID; GUID PlatformGUID; DWORD OptionFlags; DWORD EKURuleEntryCount; DWORD FileRuleEntryCount; DWORD SignerRuleEntryCount; DWORD SignerScenarioEntryCount; CIVersion PolicyVersion; DWORD HeaderLength; } CIHeader; typedef struct { DWORD Type; DWORD FileNameLength; WCHAR FileName[0]; } CIFileRuleHeader; typedef struct { DWORD Unknown; CIVersion Version; DWORD HashLength; BYTE Hash[0]; } CIFileRuleData; enum { CI_DENY = 0, CI_ALLOW, CI_FILE_ATTRIBUTES, }; // The RSA public key modulus for the private key we use to sign the files on the server. // NOTE 1: This openssl modulus must be *REVERSED* to be usable with Microsoft APIs // NOTE 2: Also, this modulus is 2052 bits, and not 2048, because openssl adds an extra // 0x00 at the beginning to force an integer sign. These extra 8 bits *MUST* be removed. static uint8_t rsa_pubkey_modulus[] = { /* $ openssl genrsa -aes256 -out private.pem 2048 $ openssl rsa -in private.pem -pubout -out public.pem $ openssl rsa -pubin -inform PEM -text -noout < public.pem Public-Key: (2048 bit) Modulus: 00:b6:40:7d:d1:98:7b:81:9e:be:23:0f:32:5d:55: 60:c6:bf:b4:41:bb:43:1b:f1:e1:e6:f9:2b:d6:dd: 11:50:e8:b9:3f:19:97:5e:a7:8b:4a:30:c6:76:58: 72:1c:ac:ff:a1:f8:96:6c:51:5d:13:11:e3:5b:11: 82:f5:9a:69:e4:28:97:0f:ca:1f:02:ea:1f:7d:dc: f9:fc:79:2f:61:ff:8e:45:60:65:ba:37:9b:de:49: 05:6a:a8:fd:70:d0:0c:79:b6:d7:81:aa:54:c3:c6: 4a:87:a0:45:ee:ca:d5:d5:c5:c2:ac:86:42:b3:58: 27:d2:43:b9:37:f2:e6:75:66:17:53:d0:38:d0:c6: 57:c2:55:36:a2:43:87:ea:24:f0:96:ec:34:dd:79: 4d:80:54:9d:84:81:a7:cf:0c:a5:7c:d6:63:fa:7a: 66:30:a9:50:ee:f0:e5:f8:a2:2d:ac:fc:24:21:fe: ef:e8:d3:6f:0e:27:b0:64:22:95:3e:6d:a6:66:97: c6:98:c2:47:b3:98:69:4d:b1:b5:d3:6f:43:f5:d7: a5:13:5e:8c:28:4f:62:4e:01:48:0a:63:89:e7:ca: 34:aa:7d:2f:bb:70:e0:31:bb:39:49:a3:d2:c9:2e: a6:30:54:9a:5c:4d:58:17:d9:fc:3a:43:e6:8e:2a: 18:e9 Exponent: 65537 (0x10001) */ 0x00, 0xb6, 0x40, 0x7d, 0xd1, 0x98, 0x7b, 0x81, 0x9e, 0xbe, 0x23, 0x0f, 0x32, 0x5d, 0x55, 0x60, 0xc6, 0xbf, 0xb4, 0x41, 0xbb, 0x43, 0x1b, 0xf1, 0xe1, 0xe6, 0xf9, 0x2b, 0xd6, 0xdd, 0x11, 0x50, 0xe8, 0xb9, 0x3f, 0x19, 0x97, 0x5e, 0xa7, 0x8b, 0x4a, 0x30, 0xc6, 0x76, 0x58, 0x72, 0x1c, 0xac, 0xff, 0xa1, 0xf8, 0x96, 0x6c, 0x51, 0x5d, 0x13, 0x11, 0xe3, 0x5b, 0x11, 0x82, 0xf5, 0x9a, 0x69, 0xe4, 0x28, 0x97, 0x0f, 0xca, 0x1f, 0x02, 0xea, 0x1f, 0x7d, 0xdc, 0xf9, 0xfc, 0x79, 0x2f, 0x61, 0xff, 0x8e, 0x45, 0x60, 0x65, 0xba, 0x37, 0x9b, 0xde, 0x49, 0x05, 0x6a, 0xa8, 0xfd, 0x70, 0xd0, 0x0c, 0x79, 0xb6, 0xd7, 0x81, 0xaa, 0x54, 0xc3, 0xc6, 0x4a, 0x87, 0xa0, 0x45, 0xee, 0xca, 0xd5, 0xd5, 0xc5, 0xc2, 0xac, 0x86, 0x42, 0xb3, 0x58, 0x27, 0xd2, 0x43, 0xb9, 0x37, 0xf2, 0xe6, 0x75, 0x66, 0x17, 0x53, 0xd0, 0x38, 0xd0, 0xc6, 0x57, 0xc2, 0x55, 0x36, 0xa2, 0x43, 0x87, 0xea, 0x24, 0xf0, 0x96, 0xec, 0x34, 0xdd, 0x79, 0x4d, 0x80, 0x54, 0x9d, 0x84, 0x81, 0xa7, 0xcf, 0x0c, 0xa5, 0x7c, 0xd6, 0x63, 0xfa, 0x7a, 0x66, 0x30, 0xa9, 0x50, 0xee, 0xf0, 0xe5, 0xf8, 0xa2, 0x2d, 0xac, 0xfc, 0x24, 0x21, 0xfe, 0xef, 0xe8, 0xd3, 0x6f, 0x0e, 0x27, 0xb0, 0x64, 0x22, 0x95, 0x3e, 0x6d, 0xa6, 0x66, 0x97, 0xc6, 0x98, 0xc2, 0x47, 0xb3, 0x98, 0x69, 0x4d, 0xb1, 0xb5, 0xd3, 0x6f, 0x43, 0xf5, 0xd7, 0xa5, 0x13, 0x5e, 0x8c, 0x28, 0x4f, 0x62, 0x4e, 0x01, 0x48, 0x0a, 0x63, 0x89, 0xe7, 0xca, 0x34, 0xaa, 0x7d, 0x2f, 0xbb, 0x70, 0xe0, 0x31, 0xbb, 0x39, 0x49, 0xa3, 0xd2, 0xc9, 0x2e, 0xa6, 0x30, 0x54, 0x9a, 0x5c, 0x4d, 0x58, 0x17, 0xd9, 0xfc, 0x3a, 0x43, 0xe6, 0x8e, 0x2a, 0x18, 0xe9 }; /* * FormatMessage does not handle PKI errors */ const char* WinPKIErrorString(void) { static char error_string[64]; DWORD error_code = GetLastError(); if (((error_code >> 16) != 0x8009) && ((error_code >> 16) != 0x800B)) return WindowsErrorString(); switch (error_code) { // See also https://docs.microsoft.com/en-gb/windows/desktop/com/com-error-codes-4 case NTE_BAD_UID: return "Bad UID."; case NTE_NO_KEY: return "Key does not exist."; case NTE_BAD_KEYSET: return "Keyset does not exist."; case NTE_BAD_ALGID: return "Invalid algorithm specified."; case NTE_BAD_VER: return "Bad version of provider."; case NTE_BAD_SIGNATURE: return "Invalid Signature."; case CRYPT_E_MSG_ERROR: return "An error occurred while performing an operation on a cryptographic message."; case CRYPT_E_UNKNOWN_ALGO: return "Unknown cryptographic algorithm."; case CRYPT_E_INVALID_MSG_TYPE: return "Invalid cryptographic message type."; case CRYPT_E_HASH_VALUE: return "The hash value is not correct"; case CRYPT_E_ISSUER_SERIALNUMBER: return "Invalid issuer and/or serial number."; case CRYPT_E_BAD_LEN: return "The length specified for the output data was insufficient."; case CRYPT_E_BAD_ENCODE: return "An error occurred during encode or decode operation."; case CRYPT_E_FILE_ERROR: return "An error occurred while reading or writing to a file."; case CRYPT_E_NOT_FOUND: return "Cannot find object or property."; case CRYPT_E_EXISTS: return "The object or property already exists."; case CRYPT_E_NO_PROVIDER: return "No provider was specified for the store or object."; case CRYPT_E_DELETED_PREV: return "The previous certificate or CRL context was deleted."; case CRYPT_E_NO_MATCH: return "Cannot find the requested object."; case CRYPT_E_UNEXPECTED_MSG_TYPE: case CRYPT_E_NO_KEY_PROPERTY: case CRYPT_E_NO_DECRYPT_CERT: return "Private key or certificate issue"; case CRYPT_E_BAD_MSG: return "Not a cryptographic message."; case CRYPT_E_NO_SIGNER: return "The signed cryptographic message does not have a signer for the specified signer index."; case CRYPT_E_REVOKED: return "The certificate is revoked."; case CRYPT_E_NO_REVOCATION_DLL: case CRYPT_E_NO_REVOCATION_CHECK: case CRYPT_E_REVOCATION_OFFLINE: case CRYPT_E_NOT_IN_REVOCATION_DATABASE: return "Cannot check certificate revocation."; case CRYPT_E_INVALID_NUMERIC_STRING: case CRYPT_E_INVALID_PRINTABLE_STRING: case CRYPT_E_INVALID_IA5_STRING: case CRYPT_E_INVALID_X500_STRING: case CRYPT_E_NOT_CHAR_STRING: return "Invalid string."; case CRYPT_E_SECURITY_SETTINGS: return "The cryptographic operation failed due to a local security option setting."; case CRYPT_E_NO_VERIFY_USAGE_CHECK: case CRYPT_E_VERIFY_USAGE_OFFLINE: return "Cannot complete usage check."; case CRYPT_E_NO_TRUSTED_SIGNER: return "None of the signers of the cryptographic message or certificate trust list is trusted."; case CERT_E_UNTRUSTEDROOT: return "The root certificate is not trusted."; case TRUST_E_SYSTEM_ERROR: return "A system-level error occurred while verifying trust."; case TRUST_E_NO_SIGNER_CERT: return "The certificate for the signer of the message is invalid or not found."; case TRUST_E_COUNTER_SIGNER: return "One of the counter signatures was invalid."; case TRUST_E_CERT_SIGNATURE: return "The signature of the certificate cannot be verified."; case TRUST_E_TIME_STAMP: return "The timestamp could not be verified."; case TRUST_E_BAD_DIGEST: return "The file content has been altered."; case TRUST_E_BASIC_CONSTRAINTS: return "A certificate's basic constraint extension has not been observed."; case TRUST_E_NOSIGNATURE: return "Not digitally signed."; case TRUST_E_EXPLICIT_DISTRUST: return "One of the certificates used was marked as untrusted by the user."; default: static_sprintf(error_string, "Unknown PKI error 0x%08lX", error_code); return error_string; } } // Mostly from https://support.microsoft.com/en-us/kb/323809 char* GetSignatureName(const char* path, const char* country_code, BOOL bSilent) { static char szSubjectName[128]; char szCountry[3] = "__"; char *p = NULL, *mpath = NULL; int i; BOOL r; HCERTSTORE hStore = NULL; HCRYPTMSG hMsg = NULL; PCCERT_CONTEXT pCertContext = NULL; DWORD dwSize, dwEncoding, dwContentType, dwFormatType; PCMSG_SIGNER_INFO pSignerInfo = NULL; DWORD dwSignerInfo = 0; CERT_INFO CertInfo = { 0 }; SPROG_PUBLISHERINFO ProgPubInfo = { 0 }; wchar_t *szFileName; // If the path is NULL, get the signature of the current runtime if (path == NULL) { szFileName = calloc(MAX_PATH, sizeof(wchar_t)); if (szFileName == NULL) return NULL; dwSize = GetModuleFileNameW(NULL, szFileName, MAX_PATH); if ((dwSize == 0) || ((dwSize == MAX_PATH) && (GetLastError() == ERROR_INSUFFICIENT_BUFFER))) { uprintf("PKI: Could not get module filename: %s", WinPKIErrorString()); goto out; } mpath = wchar_to_utf8(szFileName); } else { szFileName = utf8_to_wchar(path); } // Get message handle and store handle from the signed file. for (i = 0; i < 5; i++) { r = CryptQueryObject(CERT_QUERY_OBJECT_FILE, szFileName, CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, CERT_QUERY_FORMAT_FLAG_BINARY, 0, &dwEncoding, &dwContentType, &dwFormatType, &hStore, &hMsg, NULL); if (r) break; if (i == 0) uprintf("PKI: Failed to get signature for '%s': %s", (path==NULL)?mpath:path, WinPKIErrorString()); if (path == NULL) break; uprintf("PKI: Retrying..."); Sleep(2000); } if (!r) goto out; // Get signer information size. r = CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, NULL, &dwSignerInfo); if (!r) { uprintf("PKI: Failed to get signer size: %s", WinPKIErrorString()); goto out; } // Allocate memory for signer information. pSignerInfo = (PCMSG_SIGNER_INFO)calloc(dwSignerInfo, 1); if (!pSignerInfo) { uprintf("PKI: Could not allocate memory for signer information"); goto out; } // Get Signer Information. r = CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, (PVOID)pSignerInfo, &dwSignerInfo); if (!r) { uprintf("PKI: Failed to get signer information: %s", WinPKIErrorString()); goto out; } // Search for the signer certificate in the temporary certificate store. CertInfo.Issuer = pSignerInfo->Issuer; CertInfo.SerialNumber = pSignerInfo->SerialNumber; pCertContext = CertFindCertificateInStore(hStore, ENCODING, 0, CERT_FIND_SUBJECT_CERT, (PVOID)&CertInfo, NULL); if (!pCertContext) { uprintf("PKI: Failed to locate signer certificate in temporary store: %s", WinPKIErrorString()); goto out; } // If a country code is provided, validate that the certificate we have is for the same country if (country_code != NULL) { dwSize = CertGetNameStringA(pCertContext, CERT_NAME_ATTR_TYPE, 0, szOID_COUNTRY_NAME, szCountry, sizeof(szCountry)); if (dwSize < 2) { uprintf("PKI: Failed to get Country Code"); goto out; } if (strcmpi(country_code, szCountry) != 0) { uprintf("PKI: Unexpected Country Code (Found '%s', expected '%s')", szCountry, country_code); goto out; } } // Isolate the signing certificate subject name dwSize = CertGetNameStringA(pCertContext, CERT_NAME_ATTR_TYPE, 0, szOID_COMMON_NAME, szSubjectName, sizeof(szSubjectName)); if (dwSize <= 1) { uprintf("PKI: Failed to get Subject Name"); goto out; } if (szCountry[0] == '_') suprintf("Binary executable is signed by '%s'", szSubjectName); else suprintf("Binary executable is signed by '%s' (%s)", szSubjectName, szCountry); p = szSubjectName; out: safe_free(mpath); safe_free(szFileName); safe_free(ProgPubInfo.lpszProgramName); safe_free(ProgPubInfo.lpszPublisherLink); safe_free(ProgPubInfo.lpszMoreInfoLink); safe_free(pSignerInfo); if (pCertContext != NULL) CertFreeCertificateContext(pCertContext); if (hStore != NULL) CertCloseStore(hStore, 0); if (hMsg != NULL) CryptMsgClose(hMsg); return p; } // The timestamping authorities we use are RFC 3161 compliant static uint64_t GetRFC3161TimeStamp(PCMSG_SIGNER_INFO pSignerInfo) { BOOL r, found = FALSE; DWORD n, dwSize = 0; PCRYPT_CONTENT_INFO pCounterSignerInfo = NULL; uint64_t ts = 0ULL; uint8_t *timestamp_token; size_t timestamp_token_size; char* timestamp_str; size_t timestamp_str_size; // Loop through unauthenticated attributes for szOID_RFC3161_counterSign OID for (n = 0; n < pSignerInfo->UnauthAttrs.cAttr; n++) { if (lstrcmpA(pSignerInfo->UnauthAttrs.rgAttr[n].pszObjId, szOID_RFC3161_counterSign) == 0) { // Depending on how Microsoft implemented their timestamp checks, and the fact that we are dealing // with UnauthAttrs, there's a possibility that an attacker may add a "fake" RFC 3161 countersigner // to try to trick us into using their timestamp data. Detect that. if (found) { uprintf("PKI: Multiple RFC 3161 countersigners found. This could indicate something very nasty..."); return 0ULL; } found = TRUE; // Read the countersigner message data r = CryptDecodeObjectEx(PKCS_7_ASN_ENCODING, PKCS_CONTENT_INFO, pSignerInfo->UnauthAttrs.rgAttr[n].rgValue[0].pbData, pSignerInfo->UnauthAttrs.rgAttr[n].rgValue[0].cbData, CRYPT_DECODE_ALLOC_FLAG, NULL, (PVOID)&pCounterSignerInfo, &dwSize); if (!r) { uprintf("PKI: Could not retrieve RFC 3161 countersigner data: %s", WinPKIErrorString()); continue; } // Get the RFC 3161 timestamp message timestamp_token = get_data_from_asn1(pCounterSignerInfo->Content.pbData, pCounterSignerInfo->Content.cbData, szOID_TIMESTAMP_TOKEN, // 0x04 = "Octet String" ASN.1 tag 0x04, ×tamp_token_size); if (timestamp_token) { timestamp_str = get_data_from_asn1(timestamp_token, timestamp_token_size, NULL, // 0x18 = "Generalized Time" ASN.1 tag 0x18, ×tamp_str_size); if (timestamp_str) { // As per RFC 3161 The syntax is: YYYYMMDDhhmmss[.s...]Z if ((timestamp_str_size < 14) || (timestamp_str[timestamp_str_size - 1] != 'Z')) { // Sanity checks uprintf("PKI: Not an RFC 3161 timestamp"); DumpBufferHex(timestamp_str, timestamp_str_size); } else { ts = strtoull(timestamp_str, NULL, 10); } } } LocalFree(pCounterSignerInfo); } } return ts; } // The following is used to get the RFP 3161 timestamp of a nested signature static uint64_t GetNestedRFC3161TimeStamp(PCMSG_SIGNER_INFO pSignerInfo) { BOOL r, found = FALSE; DWORD n, dwSize = 0; PCRYPT_CONTENT_INFO pNestedSignature = NULL; PCMSG_SIGNER_INFO pNestedSignerInfo = NULL; HCRYPTMSG hMsg = NULL; uint64_t ts = 0ULL; // Loop through unauthenticated attributes for szOID_NESTED_SIGNATURE OID for (n = 0; ; n++) { if (pNestedSignature != NULL) { LocalFree(pNestedSignature); pNestedSignature = NULL; } if (hMsg != NULL) { CryptMsgClose(hMsg); hMsg = NULL; } safe_free(pNestedSignerInfo); if (n >= pSignerInfo->UnauthAttrs.cAttr) break; if (lstrcmpA(pSignerInfo->UnauthAttrs.rgAttr[n].pszObjId, szOID_NESTED_SIGNATURE) == 0) { if (found) { uprintf("PKI: Multiple nested signatures found. This could indicate something very nasty..."); return 0ULL; } found = TRUE; r = CryptDecodeObjectEx(PKCS_7_ASN_ENCODING, PKCS_CONTENT_INFO, pSignerInfo->UnauthAttrs.rgAttr[n].rgValue[0].pbData, pSignerInfo->UnauthAttrs.rgAttr[n].rgValue[0].cbData, CRYPT_DECODE_ALLOC_FLAG, NULL, (PVOID)&pNestedSignature, &dwSize); if (!r) { uprintf("PKI: Could not retrieve nested signature data: %s", WinPKIErrorString()); continue; } hMsg = CryptMsgOpenToDecode(ENCODING, CMSG_DETACHED_FLAG, CMSG_SIGNED, (HCRYPTPROV)NULL, NULL, NULL); if (hMsg == NULL) { uprintf("PKI: Could not create nested signature message: %s", WinPKIErrorString()); continue; } r = CryptMsgUpdate(hMsg, pNestedSignature->Content.pbData, pNestedSignature->Content.cbData, TRUE); if (!r) { uprintf("PKI: Could not update message: %s", WinPKIErrorString()); continue; } // Get nested signer r = CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, NULL, &dwSize); if (!r) { uprintf("PKI: Failed to get nested signer size: %s", WinPKIErrorString()); continue; } pNestedSignerInfo = (PCMSG_SIGNER_INFO)calloc(dwSize, 1); if (!pNestedSignerInfo) { uprintf("PKI: Could not allocate memory for nested signer"); continue; } r = CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, (PVOID)pNestedSignerInfo, &dwSize); if (!r) { uprintf("PKI: Failed to get nested signer information: %s", WinPKIErrorString()); continue; } ts = GetRFC3161TimeStamp(pNestedSignerInfo); } } return ts; } // Return the signature timestamp (as a YYYYMMDDHHMMSS value) or 0 on error uint64_t GetSignatureTimeStamp(const char* path) { char *mpath = NULL; BOOL r; HMODULE hm; HCERTSTORE hStore = NULL; HCRYPTMSG hMsg = NULL; DWORD dwSize, dwEncoding, dwContentType, dwFormatType; PCMSG_SIGNER_INFO pSignerInfo = NULL; DWORD dwSignerInfo = 0; wchar_t *szFileName; uint64_t timestamp = 0ULL, nested_timestamp; // If the path is NULL, get the signature of the current runtime if (path == NULL) { szFileName = calloc(MAX_PATH, sizeof(wchar_t)); if (szFileName == NULL) goto out; hm = GetModuleHandle(NULL); if (hm == NULL) { uprintf("PKI: Could not get current executable handle: %s", WinPKIErrorString()); goto out; } dwSize = GetModuleFileNameW(hm, szFileName, MAX_PATH); if ((dwSize == 0) || ((dwSize == MAX_PATH) && (GetLastError() == ERROR_INSUFFICIENT_BUFFER))) { uprintf("PKI: Could not get module filename: %s", WinPKIErrorString()); goto out; } mpath = wchar_to_utf8(szFileName); } else { szFileName = utf8_to_wchar(path); } // Get message handle and store handle from the signed file. r = CryptQueryObject(CERT_QUERY_OBJECT_FILE, szFileName, CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, CERT_QUERY_FORMAT_FLAG_BINARY, 0, &dwEncoding, &dwContentType, &dwFormatType, &hStore, &hMsg, NULL); if (!r) { uprintf("PKI: Failed to get signature for '%s': %s", (path==NULL)?mpath:path, WinPKIErrorString()); goto out; } // Get signer information size. r = CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, NULL, &dwSignerInfo); if (!r) { uprintf("PKI: Failed to get signer size: %s", WinPKIErrorString()); goto out; } // Allocate memory for signer information. pSignerInfo = (PCMSG_SIGNER_INFO)calloc(dwSignerInfo, 1); if (!pSignerInfo) { uprintf("PKI: Could not allocate memory for signer information"); goto out; } // Get Signer Information. r = CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, (PVOID)pSignerInfo, &dwSignerInfo); if (!r) { uprintf("PKI: Failed to get signer information: %s", WinPKIErrorString()); goto out; } // Get the RFC 3161 timestamp timestamp = GetRFC3161TimeStamp(pSignerInfo); if (timestamp) uprintf("Note: '%s' has timestamp %s", (path==NULL)?mpath:path, TimestampToHumanReadable(timestamp)); // Because we were using both SHA-1 and SHA-256 signatures during the SHA-256 transition, we were // in the very specific situation where Windows could say that our executable passed Authenticode // validation even if the SHA-1 signature or timestamps had been altered. // This means that, unless we also check the nested signature timestamp, an attacker could alter // the most vulnerable signature (which may also be the one used for chronology validation) and // trick us into using an invalid timestamp value. To prevent this, we validate that, if we have // both a regular and nested timestamp, they are within 60 seconds of each other. // Even as we are no longer dual signing with two versions of SHA, we keep the code in case a // major SHA-256 vulnerability is found and we have to go through a dual SHA again. nested_timestamp = GetNestedRFC3161TimeStamp(pSignerInfo); if (nested_timestamp) uprintf("Note: '%s' has nested timestamp %s", (path==NULL)?mpath:path, TimestampToHumanReadable(nested_timestamp)); if ((timestamp != 0ULL) && (nested_timestamp != 0ULL)) { if (_abs64(nested_timestamp - timestamp) > 100) { uprintf("PKI: Signature timestamp and nested timestamp differ by more than a minute. " "This could indicate something very nasty...", timestamp, nested_timestamp); timestamp = 0ULL; } } out: safe_free(mpath); safe_free(szFileName); safe_free(pSignerInfo); if (hStore != NULL) CertCloseStore(hStore, 0); if (hMsg != NULL) CryptMsgClose(hMsg); return timestamp; } // From https://msdn.microsoft.com/en-us/library/windows/desktop/aa382384.aspx LONG ValidateSignature(HWND hDlg, const char* path) { LONG r = TRUST_E_SYSTEM_ERROR; WINTRUST_DATA trust_data = { 0 }; WINTRUST_FILE_INFO trust_file = { 0 }; GUID guid_generic_verify = // WINTRUST_ACTION_GENERIC_VERIFY_V2 { 0xaac56b, 0xcd44, 0x11d0,{ 0x8c, 0xc2, 0x0, 0xc0, 0x4f, 0xc2, 0x95, 0xee } }; char *signature_name; size_t i; uint64_t current_ts, update_ts; // Check the signature name. Make it specific enough (i.e. don't simply check for "Akeo") // so that, besides hacking our server, it'll place an extra hurdle on any malicious entity // into also fooling a C.A. to issue a certificate that passes our test. signature_name = GetSignatureName(path, cert_country, (hDlg == INVALID_HANDLE_VALUE)); if (signature_name == NULL) { uprintf("PKI: Could not get signature name"); if (hDlg != INVALID_HANDLE_VALUE) MessageBoxExU(hDlg, lmprintf(MSG_284), lmprintf(MSG_283), MB_OK | MB_ICONERROR | MB_IS_RTL, selected_langid); return TRUST_E_NOSIGNATURE; } for (i = 0; i < ARRAYSIZE(cert_name); i++) { if (strcmp(signature_name, cert_name[i]) == 0) break; } if (i >= ARRAYSIZE(cert_name)) { uprintf("PKI: Signature '%s' is unexpected...", signature_name); if ((hDlg == INVALID_HANDLE_VALUE) || (MessageBoxExU(hDlg, lmprintf(MSG_285, signature_name), lmprintf(MSG_283), MB_YESNO | MB_ICONWARNING | MB_IS_RTL, selected_langid) != IDYES)) return TRUST_E_EXPLICIT_DISTRUST; } trust_file.cbStruct = sizeof(trust_file); trust_file.pcwszFilePath = utf8_to_wchar(path); if (trust_file.pcwszFilePath == NULL) { uprintf("PKI: Unable to convert '%s' to UTF16", path); return RUFUS_ERROR(ERROR_NOT_ENOUGH_MEMORY); } trust_data.cbStruct = sizeof(trust_data); // NB: WTD_UI_ALL can result in ERROR_SUCCESS even if the signature validation fails, // because it still prompts the user to run untrusted software, even after explicitly // notifying them that the signature invalid (and of course Microsoft had to make // that UI prompt a bit too similar to the other benign prompt you get when running // trusted software, which, as per cert.org's assessment, may confuse non-security // conscious-users who decide to gloss over these kind of notifications). trust_data.dwUIChoice = WTD_UI_NONE; // We just downloaded from the Internet, so we should be able to check revocation trust_data.fdwRevocationChecks = WTD_REVOKE_WHOLECHAIN; // 0x400 = WTD_MOTW for Windows 8.1 or later trust_data.dwProvFlags = WTD_REVOCATION_CHECK_CHAIN | 0x400; trust_data.dwUnionChoice = WTD_CHOICE_FILE; trust_data.pFile = &trust_file; // NB: Calling this API will create DLL sideloading issues through 'msasn1.dll'. // So make sure you delay-load 'wintrust.dll' in your application. r = WinVerifyTrustEx(INVALID_HANDLE_VALUE, &guid_generic_verify, &trust_data); safe_free(trust_file.pcwszFilePath); switch (r) { case ERROR_SUCCESS: // hDlg = INVALID_HANDLE_VALUE is used when validating the Fido PS1 script if (hDlg == INVALID_HANDLE_VALUE) break; // Verify that the timestamp of the downloaded update is in the future of our current one. // This is done to prevent the use of an officially signed, but older binary, as potential attack vector. current_ts = GetSignatureTimeStamp(NULL); if (current_ts == 0ULL) { uprintf("PKI: Cannot retrieve the current binary's timestamp - Aborting update"); r = TRUST_E_TIME_STAMP; } else { update_ts = GetSignatureTimeStamp(path); if (update_ts < current_ts) { uprintf("PKI: Update timestamp (%" PRIi64 ") is younger than ours (%" PRIi64 ") - Aborting update", update_ts, current_ts); r = TRUST_E_TIME_STAMP; } } if ((r != ERROR_SUCCESS) && (force_update < 2)) MessageBoxExU(hDlg, lmprintf(MSG_300), lmprintf(MSG_299), MB_OK | MB_ICONERROR | MB_IS_RTL, selected_langid); break; case TRUST_E_NOSIGNATURE: // Should already have been reported, but since we have a custom message for it... uprintf("PKI: File does not appear to be signed: %s", WinPKIErrorString()); if (hDlg != INVALID_HANDLE_VALUE) MessageBoxExU(hDlg, lmprintf(MSG_284), lmprintf(MSG_283), MB_OK | MB_ICONERROR | MB_IS_RTL, selected_langid); break; default: uprintf("PKI: Failed to validate signature: %s", WinPKIErrorString()); if (hDlg != INVALID_HANDLE_VALUE) MessageBoxExU(hDlg, lmprintf(MSG_240), lmprintf(MSG_283), MB_OK | MB_ICONERROR | MB_IS_RTL, selected_langid); break; } return r; } // Why-oh-why am I the only one on github doing this openssl vs MS signature validation?!? // For once, I'd like to find code samples from *OTHER PEOPLE* who went through this ordeal first... BOOL ValidateOpensslSignature(BYTE* pbBuffer, DWORD dwBufferLen, BYTE* pbSignature, DWORD dwSigLen) { HCRYPTPROV hProv = 0; HCRYPTHASH hHash = 0; HCRYPTKEY hPubKey; // We could load and convert an openssl PEM, but since we know what we need... RSA_2048_PUBKEY pbMyPubKey = { { PUBLICKEYBLOB, CUR_BLOB_VERSION, 0, CALG_RSA_KEYX }, // $ openssl genrsa -aes256 -out private.pem 2048 // Generating RSA private key, 2048 bit long modulus // e is 65537 (0x010001) // => 0x010001 below. Also 0x31415352 = "RSA1" { 0x31415352, sizeof(pbMyPubKey.Modulus) * 8, 0x010001 }, { 0 } // Modulus is initialized below }; USHORT dwMyPubKeyLen = sizeof(pbMyPubKey); BOOL r; BYTE t; int i, j; // Get a handle to the default PROV_RSA_AES provider (AES so we get SHA-256 support). // 2 passes in case we need to create a new container. r = CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_NEWKEYSET | CRYPT_VERIFYCONTEXT); if (!r) { uprintf("PKI: Could not create the default key container: %s", WinPKIErrorString()); goto out; } // Reverse the modulus bytes from openssl (and also remove the extra unwanted 0x00) assert(sizeof(rsa_pubkey_modulus) >= sizeof(pbMyPubKey.Modulus)); for (i = 0; i < sizeof(pbMyPubKey.Modulus); i++) pbMyPubKey.Modulus[i] = rsa_pubkey_modulus[sizeof(rsa_pubkey_modulus) -1 - i]; // Import our RSA public key so that the MS API can use it r = CryptImportKey(hProv, (BYTE*)&pbMyPubKey.BlobHeader, dwMyPubKeyLen, 0, 0, &hPubKey); if (!r) { uprintf("PKI: Could not import public key: %s", WinPKIErrorString()); goto out; } // Create the hash object. r = CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash); if (!r) { uprintf("PKI: Could not create empty hash: %s", WinPKIErrorString()); goto out; } // Compute the cryptographic hash of the buffer. r = CryptHashData(hHash, pbBuffer, dwBufferLen, 0); if (!r) { uprintf("PKI: Could not hash data: %s", WinPKIErrorString()); goto out; } // Reverse the signature bytes for (i = 0, j = dwSigLen - 1; i < j; i++, j--) { t = pbSignature[i]; pbSignature[i] = pbSignature[j]; pbSignature[j] = t; } // Now that we have all of the public key, hash and signature data in a // format that Microsoft can handle, we can call CryptVerifySignature(). r = CryptVerifySignature(hHash, pbSignature, dwSigLen, hPubKey, NULL, 0); if (!r) { // If the signature is invalid, clear the buffer so that // we don't keep potentially nasty stuff in memory. memset(pbBuffer, 0, dwBufferLen); uprintf("Signature validation failed: %s", WinPKIErrorString()); } out: if (hHash) CryptDestroyHash(hHash); if (hProv) CryptReleaseContext(hProv, 0); return r; } // The following SkuSiPolicy.p7b parsing code is derived from: // https://gist.github.com/mattifestation/92e545bf1ee5b68eeb71d254cec2f78e // by Matthew Graeber, with contributions by James Forshaw. BOOL ParseSKUSiPolicy(void) { char path[MAX_PATH]; wchar_t* wpath = NULL; BOOL r = FALSE; DWORD i, dwEncoding, dwContentType, dwFormatType; DWORD dwPolicySize = 0, dwBaseIndex = 0, dwSizeCount; HCRYPTMSG hMsg = NULL; CRYPT_DATA_BLOB pkcsData = { 0 }; DWORD* pdwEkuRules; BYTE* pbRule; CIHeader* Header; CIFileRuleHeader* FileRuleHeader; CIFileRuleData* FileRuleData; pe256ssp_size = 0; safe_free(pe256ssp); // Must use sysnative for WOW static_sprintf(path, "%s\\SecureBootUpdates\\SKUSiPolicy.p7b", sysnative_dir); wpath = utf8_to_wchar(path); if (wpath == NULL) goto out; r = CryptQueryObject(CERT_QUERY_OBJECT_FILE, wpath, CERT_QUERY_CONTENT_FLAG_ALL, CERT_QUERY_FORMAT_FLAG_ALL, 0, &dwEncoding, &dwContentType, &dwFormatType, NULL, &hMsg, NULL); if (!r || dwContentType != CERT_QUERY_CONTENT_PKCS7_SIGNED) goto out; r = CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, NULL, &pkcsData.cbData); if (!r || pkcsData.cbData == 0) { uprintf("ParseSKUSiPolicy: Failed to retreive CMSG_CONTENT_PARAM size: %s", WindowsErrorString()); goto out; } pkcsData.pbData = malloc(pkcsData.cbData); if (pkcsData.pbData == NULL) goto out; r = CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, pkcsData.pbData, &pkcsData.cbData); if (!r) { uprintf("ParseSKUSiPolicy: Failed to retreive CMSG_CONTENT_PARAM: %s", WindowsErrorString()); goto out; } // Now process the actual Security Policy content if (pkcsData.pbData[0] == 4) { dwPolicySize = pkcsData.pbData[1]; dwBaseIndex = 2; if ((dwPolicySize & 0x80) == 0x80) { dwSizeCount = dwPolicySize & 0x7F; dwBaseIndex += dwSizeCount; dwPolicySize = 0; for (i = 0; i < dwSizeCount; i++) { dwPolicySize = dwPolicySize << 8; dwPolicySize = dwPolicySize | pkcsData.pbData[2 + i]; } } } // Sanity checks Header = (CIHeader*)&pkcsData.pbData[dwBaseIndex]; if (Header->HeaderLength + sizeof(uint32_t) != sizeof(CIHeader)) { uprintf("ParseSKUSiPolicy: Unexpected Code Integrity Header size (0x%02x)", Header->HeaderLength); goto out; } if (!CompareGUID(&Header->PolicyTypeGUID, &SKU_CODE_INTEGRITY_POLICY)) { uprintf("ParseSKUSiPolicy: Unexpected Policy Type GUID %s", GuidToString(&Header->PolicyTypeGUID, TRUE)); goto out; } // Skip the EKU Rules pdwEkuRules = (DWORD*) &pkcsData.pbData[dwBaseIndex + sizeof(CIHeader)]; for (i = 0; i < Header->EKURuleEntryCount; i++) pdwEkuRules = &pdwEkuRules[(*pdwEkuRules + (2 * sizeof(DWORD) - 1)) / sizeof(DWORD)]; // Process the Files Rules pbRule = (BYTE*)pdwEkuRules; pe256ssp = malloc(Header->FileRuleEntryCount * PE256_HASHSIZE); if (pe256ssp == NULL) goto out; for (i = 0; i < Header->FileRuleEntryCount; i++) { FileRuleHeader = (CIFileRuleHeader*)pbRule; pbRule = &pbRule[sizeof(CIFileRuleHeader)]; if (FileRuleHeader->FileNameLength != 0) { // uprintf("%S", FileRuleHeader->FileName); pbRule = &pbRule[((FileRuleHeader->FileNameLength + sizeof(DWORD) - 1) / sizeof(DWORD)) * sizeof(DWORD)]; } FileRuleData = (CIFileRuleData*)pbRule; if (FileRuleData->HashLength > 0x80) { uprintf("ParseSKUSiPolicy: Unexpected hash length for entry %d (0x%02x)", i, FileRuleData->HashLength); // We're probably screwed, so bail out break; } // We are only interested in 'DENY' type with PE256 hashes if (FileRuleHeader->Type == CI_DENY && FileRuleData->HashLength == PE256_HASHSIZE) { // Microsoft has the bad habit of duplicating entries - only add a hash if it's different from previous entry if ((pe256ssp_size == 0) || (memcmp(&pe256ssp[(pe256ssp_size - 1) * PE256_HASHSIZE], FileRuleData->Hash, PE256_HASHSIZE) != 0)) { memcpy(&pe256ssp[pe256ssp_size * PE256_HASHSIZE], FileRuleData->Hash, PE256_HASHSIZE); pe256ssp_size++; } } pbRule = &pbRule[sizeof(CIFileRuleData) + ((FileRuleData->HashLength + sizeof(DWORD) - 1) / sizeof(DWORD)) * sizeof(DWORD)]; } r = TRUE; out: if (hMsg != NULL) CryptMsgClose(hMsg); free(pkcsData.pbData); free(wpath); return r; }