#include "authenticode.h" #ifdef RAVEN_OPENSSL #include #include #include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include namespace raven { #ifdef RAVEN_OPENSSL namespace { constexpr uint32_t trust_no_signature = 0x800b0100, trust_unknown_subject = 0x800b0003, trust_bad_digest = 0x80096010, trust_bad_signature = 0x80096004, certificate_expired = 0x800b0101, certificate_untrusted_root = 0x800b0109, certificate_chaining = 0x800b010a; uint32_t le32(std::span bytes, size_t at) { uint32_t value = 0; if (at + 4 <= bytes.size()) std::memcpy(&value, bytes.data() + at, 4); return value; } uint16_t le16(std::span bytes, size_t at) { uint16_t value = 0; if (at + 2 <= bytes.size()) std::memcpy(&value, bytes.data() + at, 2); return value; } // A signed file: the PKCS #7 signature it carries, and how its content is hashed for the digest the signature names. struct Signed { std::vector signature; std::function hash; }; void update(EVP_MD_CTX *context, std::span bytes, size_t from, size_t to) { if (from < to && to <= bytes.size()) EVP_DigestUpdate(context, bytes.data() + from, to - from); } // A program or library: the WIN_CERTIFICATE the security directory names; the digest covers the file but the checksum, the security // directory entry and the certificate table. std::optional portable_executable(std::span file) { const uint32_t header = le32(file, 0x3c); if (header + 24 + 96 > file.size() || le32(file, header) != 0x4550) return std::nullopt; const size_t optional = header + 24, checksum = optional + 64; const size_t directories = optional + (le16(file, optional) == 0x20b ? 112 : 96), security = directories + 4 * 8; const uint32_t table = le32(file, security), size = le32(file, security + 4); if (!table || size < 8 || uint64_t(table) + size > file.size()) return Signed{}; const uint32_t length = le32(file, table); if (le16(file, table + 6) != 2 || length < 8 || length > size) return Signed{}; return Signed{std::vector(file.begin() + table + 8, file.begin() + table + length), [file, checksum, security, table](EVP_MD_CTX *context) { update(context, file, 0, checksum); update(context, file, checksum + 4, security); update(context, file, security + 8, table); }}; } // A cabinet: the per-cabinet reserve says where the signature follows the cabinet; the digest covers the header fields that do not // change when the cabinet is signed, the folder entries and everything from the file entries to the signature. std::optional cabinet(std::span file) { const uint16_t flags = le16(file, 30); if (!(flags & 4) || le16(file, 36) < 20) return Signed{}; const uint32_t at = le32(file, 44), size = le32(file, 48); if (!at || uint64_t(at) + size > file.size()) return Signed{}; return Signed{std::vector(file.begin() + at, file.begin() + at + size), [file, flags, at](EVP_MD_CTX *context) { // The signature, the size and the offset of the file entries, the version, the counts, the flags, the set, and the end of // the reserve; not the reserved words, the cabinet's place in its set or where the signature is. static constexpr std::pair fields[] = {{0, 4}, {8, 16}, {16, 20}, {20, 26}, {26, 30}, {30, 32}, {32, 34}, {56, 60}}; for (const auto &[from, to] : fields) update(context, file, from, to); size_t cursor = 36 + 4 + le16(file, 36); // The names of the previous and next cabinets of a set, with their terminators. for (const uint16_t flag : {1, 2}) if (flags & flag) for (int text = 0; text < 2; ++text) { const size_t start = cursor; while (cursor < file.size() && file[cursor]) ++cursor; update(context, file, start, ++cursor); } const size_t folder_size = 8 + file[38]; for (uint16_t folder = 0; folder < le16(file, 26); ++folder) update(context, file, cursor + folder * folder_size, cursor + folder * folder_size + 8); update(context, file, le32(file, 16), at); }}; } // The parts of a compound file, the container of installer databases: its directory and the content of each stream. class CompoundFile { public: struct Entry { std::vector name; uint8_t type = 0; uint32_t left = 0, right = 0, child = 0, start = 0, size = 0; std::array clsid{}; // The entry's state bits, its creation and modification times and the low half of its size, as stored. std::array state{}, size_low{}; std::array times{}; }; explicit CompoundFile(std::span file) : file(file) { sector = size_t(1) << le16(file, 30); mini_sector = size_t(1) << le16(file, 32); cutoff = le32(file, 56); std::vector fat_sectors; for (size_t index = 0; index < 109; ++index) fat_sectors.push_back(le32(file, 76 + index * 4)); for (uint32_t next = le32(file, 68), count = le32(file, 72); count && next < 0xfffffffa && fat_sectors.size() < 1 << 20; --count) { for (size_t index = 0; index + 1 < sector / 4; ++index) fat_sectors.push_back(le32(file, offset(next) + index * 4)); next = le32(file, offset(next) + sector - 4); } fat_sectors.resize(std::min(fat_sectors.size(), le32(file, 44))); for (const auto at : fat_sectors) for (size_t index = 0; index < sector / 4; ++index) fat.push_back(le32(file, offset(at) + index * 4)); const auto directory = read(le32(file, 48)); for (size_t at = 0; at + 128 <= directory.size(); at += 128) { Entry entry; const size_t name_size = std::min(le16(directory, at + 64), 64); entry.name.assign(directory.begin() + at, directory.begin() + at + name_size); entry.type = directory[at + 66]; entry.left = le32(directory, at + 68); entry.right = le32(directory, at + 72); entry.child = le32(directory, at + 76); std::memcpy(entry.clsid.data(), directory.data() + at + 80, 16); std::memcpy(entry.state.data(), directory.data() + at + 96, 4); std::memcpy(entry.times.data(), directory.data() + at + 100, 16); std::memcpy(entry.size_low.data(), directory.data() + at + 120, 4); entry.start = le32(directory, at + 116); entry.size = le32(directory, at + 120); entries.push_back(std::move(entry)); } if (!entries.empty()) { mini_stream = read(entries[0].start, entries[0].size); const auto table = read(le32(file, 60)); for (size_t at = 0; at + 4 <= table.size(); at += 4) mini_fat.push_back(le32(table, at)); } } std::vector stream(const Entry &entry) const { if (entry.size >= cutoff) return read(entry.start, entry.size); std::vector bytes; for (uint32_t at = entry.start; at < 0xfffffffa && at < mini_fat.size() && bytes.size() < entry.size; at = mini_fat[at]) if ((at + 1) * mini_sector <= mini_stream.size()) bytes.insert(bytes.end(), mini_stream.begin() + at * mini_sector, mini_stream.begin() + (at + 1) * mini_sector); bytes.resize(std::min(bytes.size(), entry.size)); return bytes; } // A storage's entries, from the tree its child entry roots. std::vector children(const Entry &storage) const { std::vector found; std::vector pending{storage.child}; while (!pending.empty() && found.size() < entries.size()) { const uint32_t at = pending.back(); pending.pop_back(); if (at >= entries.size()) continue; found.push_back(&entries[at]); pending.push_back(entries[at].left); pending.push_back(entries[at].right); } return found; } std::vector entries; private: size_t offset(uint32_t at) const { return (size_t(at) + 1) * sector; } std::vector read(uint32_t at, size_t limit = SIZE_MAX) const { std::vector bytes; for (size_t steps = 0; at < 0xfffffffa && at < fat.size() && bytes.size() < limit && steps <= fat.size(); at = fat[at], ++steps) if (offset(at) + sector <= file.size()) bytes.insert(bytes.end(), file.begin() + offset(at), file.begin() + offset(at) + sector); if (limit != SIZE_MAX) bytes.resize(std::min(bytes.size(), limit)); return bytes; } std::span file; size_t sector = 512, mini_sector = 64, cutoff = 4096; std::vector fat, mini_fat; std::vector mini_stream; }; // A directory entry's name as the compound file stores it: UTF-16 with its terminator. std::vector entry_name(std::u16string_view text) { std::vector bytes; for (const auto character : text) { bytes.push_back(uint8_t(character)); bytes.push_back(uint8_t(character >> 8)); } bytes.insert(bytes.end(), {0, 0}); return bytes; } // An installer database: the signature is the DigitalSignature stream; the digest covers every other stream, storage by storage in the // order of their names as UTF-16 bytes, each storage followed by its class identifier. A database that also carries // MsiDigitalSignatureEx has its entries' metadata hashed first, and that hash, which the stream holds, begins the digest. std::optional installer_database(std::span file) { auto compound = std::make_shared(file); if (compound->entries.empty()) return std::nullopt; const auto signature_name = entry_name(u"\x05" u"DigitalSignature"), extended_name = entry_name(u"\x05" u"MsiDigitalSignatureEx"); std::vector signature, metadata; bool extended = false; for (const auto *entry : compound->children(compound->entries[0])) if (entry->type == 2 && entry->name == signature_name) signature = compound->stream(*entry); else if (entry->type == 2 && entry->name == extended_name) { metadata = compound->stream(*entry); extended = true; } if (signature.empty()) return Signed{}; const auto sorted = [compound](const CompoundFile::Entry &storage) { auto children = compound->children(storage); std::sort(children.begin(), children.end(), [](const auto *left, const auto *right) { const size_t common = std::min(left->name.size(), right->name.size()); const int order = std::memcmp(left->name.data(), right->name.data(), common); return order ? order < 0 : left->name.size() < right->name.size(); }); return children; }; const auto skipped = [signature_name, extended_name](const CompoundFile::Entry &entry) { return entry.type == 2 && (entry.name == signature_name || entry.name == extended_name); }; return Signed{std::move(signature), [compound, sorted, skipped, metadata, extended](EVP_MD_CTX *context) { if (extended) { // The metadata hash: every entry's name without its terminator (the root has none), the class identifier of a storage or // the low half of a stream's size, the state bits, and the times of all but the root. std::unique_ptr prehash(EVP_MD_CTX_new(), EVP_MD_CTX_free); EVP_DigestInit_ex(prehash.get(), EVP_MD_CTX_get0_md(context), nullptr); const auto describe = [&](const CompoundFile::Entry &entry) { const bool root = entry.type == 5; if (!root && entry.name.size() >= 2) EVP_DigestUpdate(prehash.get(), entry.name.data(), entry.name.size() - 2); if (entry.type == 2) EVP_DigestUpdate(prehash.get(), entry.size_low.data(), entry.size_low.size()); else EVP_DigestUpdate(prehash.get(), entry.clsid.data(), entry.clsid.size()); EVP_DigestUpdate(prehash.get(), entry.state.data(), entry.state.size()); if (!root) EVP_DigestUpdate(prehash.get(), entry.times.data(), entry.times.size()); }; const std::function walk = [&](const CompoundFile::Entry &storage, int depth) { describe(storage); for (const auto *child : sorted(storage)) { if (skipped(*child)) continue; if (child->type == 2) describe(*child); else if (child->type == 1 && depth < 32) walk(*child, depth + 1); } }; walk(compound->entries[0], 0); unsigned char computed[EVP_MAX_MD_SIZE]; unsigned int size = 0; EVP_DigestFinal_ex(prehash.get(), computed, &size); // Metadata that no longer matches its hash spoils the digest, as the signature then covers other content. if (metadata.size() != size || std::memcmp(metadata.data(), computed, size) != 0) EVP_DigestUpdate(context, computed, size); EVP_DigestUpdate(context, metadata.data(), metadata.size()); } const std::function storage = [&](const CompoundFile::Entry &entry, int depth) { for (const auto *child : sorted(entry)) { if (child->type == 2 && !skipped(*child)) { const auto bytes = compound->stream(*child); EVP_DigestUpdate(context, bytes.data(), bytes.size()); } else if (child->type == 1 && depth < 32) { storage(*child, depth + 1); } } EVP_DigestUpdate(context, entry.clsid.data(), entry.clsid.size()); }; storage(compound->entries[0], 0); }}; } // A DER length after its tag at `at`: the header size and the content size. std::optional> der_header(std::span bytes, size_t at) { if (at + 2 > bytes.size()) return std::nullopt; const uint8_t first = bytes[at + 1]; if (first < 0x80) return std::pair{size_t(2), size_t(first)}; const size_t count = first & 0x7f; if (!count || count > 4 || at + 2 + count > bytes.size()) return std::nullopt; size_t length = 0; for (size_t index = 0; index < count; ++index) length = length << 8 | bytes[at + 2 + index]; return std::pair{2 + count, length}; } // The time a timestamp countersignature gives the signature, as seconds since 1970. std::optional stamped_time(PKCS7_SIGNER_INFO *signer) { const auto to_seconds = [](const ASN1_TIME *time) -> std::optional { std::tm parts{}; if (!time || !ASN1_TIME_to_tm(time, &parts)) return std::nullopt; return timegm(&parts); }; if (ASN1_TYPE *counter = PKCS7_get_attribute(signer, NID_pkcs9_countersignature); counter && counter->type == V_ASN1_SEQUENCE) { const unsigned char *at = counter->value.sequence->data; std::unique_ptr info( d2i_PKCS7_SIGNER_INFO(nullptr, &at, counter->value.sequence->length), PKCS7_SIGNER_INFO_free); if (info) if (ASN1_TYPE *signed_at = PKCS7_get_signed_attribute(info.get(), NID_pkcs9_signingTime); signed_at && signed_at->type == V_ASN1_UTCTIME) return to_seconds(signed_at->value.utctime); } // An RFC 3161 timestamp token, which newer signatures carry under Microsoft's attribute; its identifier is compared as text, since the // library may not know it. const STACK_OF(X509_ATTRIBUTE) *unsigned_attributes = PKCS7_get_attributes(signer); for (int index = 0; index < sk_X509_ATTRIBUTE_num(unsigned_attributes); ++index) { X509_ATTRIBUTE *attribute = sk_X509_ATTRIBUTE_value(unsigned_attributes, index); char name[64] = {}; OBJ_obj2txt(name, sizeof(name), X509_ATTRIBUTE_get0_object(attribute), 1); ASN1_TYPE *token = X509_ATTRIBUTE_get0_type(attribute, 0); if (std::strcmp(name, "1.3.6.1.4.1.311.3.3.1") != 0 || !token || token->type != V_ASN1_SEQUENCE) continue; // The token is CMS signed data, which the PKCS #7 parser does not accept in every form; its content is the TSTInfo. const unsigned char *at = token->value.sequence->data; std::unique_ptr signed_token( d2i_CMS_ContentInfo(nullptr, &at, token->value.sequence->length), CMS_ContentInfo_free); ASN1_OCTET_STRING **content = signed_token ? CMS_get0_content(signed_token.get()) : nullptr; if (content && *content) { const unsigned char *info_at = (*content)->data; std::unique_ptr info(d2i_TS_TST_INFO(nullptr, &info_at, (*content)->length), TS_TST_INFO_free); if (info) return to_seconds(TS_TST_INFO_get_time(info.get())); } } return std::nullopt; } std::vector der_of(X509 *certificate) { const int length = i2d_X509(certificate, nullptr); std::vector bytes(length > 0 ? size_t(length) : 0); unsigned char *at = bytes.data(); if (length > 0) i2d_X509(certificate, &at); return bytes; } } X509_STORE *trust_anchors() { static X509_STORE *store = [] { auto *created = X509_STORE_new(); if (!created) return created; for (const char *path : {"/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", "/etc/ssl/cert.pem"}) if (X509_STORE_load_file(created, path)) break; std::error_code error; const auto roots = std::filesystem::read_symlink("/proc/self/exe", error).parent_path() / "roots"; for (const auto &entry : std::filesystem::directory_iterator(roots, error)) { std::ifstream input(entry.path(), std::ios::binary); const std::string bytes((std::istreambuf_iterator(input)), std::istreambuf_iterator()); const auto *at = reinterpret_cast(bytes.data()); if (X509 *certificate = d2i_X509(nullptr, &at, long(bytes.size()))) { X509_STORE_add_cert(created, certificate); X509_free(certificate); } } return created; }(); return store; } namespace { std::optional signed_content(std::span file) { if (file.size() >= 0x40 && file[0] == 'M' && file[1] == 'Z') return portable_executable(file); if (file.size() >= 60 && std::memcmp(file.data(), "MSCF", 4) == 0) return cabinet(file); if (file.size() >= 512 && le32(file, 0) == 0xe011cfd0 && le32(file, 4) == 0xe11ab1a1) return installer_database(file); return std::nullopt; } } std::vector embedded_signature(std::span file) { auto found = signed_content(file); return found ? std::move(found->signature) : std::vector(); } std::vector subject_digest(std::span file, bool sha256) { std::unique_ptr context(EVP_MD_CTX_new(), EVP_MD_CTX_free); if (!context || !EVP_DigestInit_ex(context.get(), sha256 ? EVP_sha256() : EVP_sha1(), nullptr)) return {}; // A program or library is hashed as its signature would be, without the checksum, the security directory entry and the certificate // table; any other file whole. const uint32_t header = file.size() >= 0x40 && file[0] == 'M' && file[1] == 'Z' ? le32(file, 0x3c) : 0; if (header && uint64_t(header) + 24 + 96 <= file.size() && le32(file, header) == 0x4550) { const size_t optional = header + 24, checksum = optional + 64; const size_t security = optional + (le16(file, optional) == 0x20b ? 112 : 96) + 4 * 8; const uint32_t table = le32(file, security), size = le32(file, security + 4); const size_t end = table && size && uint64_t(table) + size <= file.size() ? table : file.size(); update(context.get(), file, 0, checksum); update(context.get(), file, checksum + 4, security); update(context.get(), file, security + 8, end); } else update(context.get(), file, 0, file.size()); std::vector digest(EVP_MAX_MD_SIZE); unsigned int length = 0; if (!EVP_DigestFinal_ex(context.get(), digest.data(), &length)) return {}; digest.resize(length); return digest; } AuthenticodeResult verify_authenticode(std::span file) { AuthenticodeResult result; std::optional found = signed_content(file); if (!found) { result.status = trust_unknown_subject; return result; } if (found->signature.empty()) { result.status = trust_no_signature; return result; } const unsigned char *at = found->signature.data(); std::unique_ptr signature(d2i_PKCS7(nullptr, &at, long(found->signature.size())), PKCS7_free); if (!signature || !PKCS7_type_is_signed(signature.get()) || !signature->d.sign->contents || signature->d.sign->contents->d.other == nullptr || signature->d.sign->contents->d.other->type != V_ASN1_SEQUENCE) { result.status = trust_no_signature; return result; } // SpcIndirectDataContent: what was signed, then the DigestInfo of the file's content. const auto *content = signature->d.sign->contents->d.other->value.sequence; const std::span indirect(content->data, size_t(content->length)); const auto outer = der_header(indirect, 0); const auto first = outer ? der_header(indirect, outer->first) : std::nullopt; if (!outer || !first) { result.status = trust_no_signature; return result; } const size_t digest_at = outer->first + first->first + first->second; const unsigned char *digest_bytes = indirect.data() + digest_at; std::unique_ptr digest_info( digest_at < indirect.size() ? d2i_X509_SIG(nullptr, &digest_bytes, long(indirect.size() - digest_at)) : nullptr, X509_SIG_free); if (!digest_info) { result.status = trust_no_signature; return result; } const X509_ALGOR *algorithm = nullptr; const ASN1_OCTET_STRING *expected = nullptr; X509_SIG_get0(digest_info.get(), &algorithm, &expected); const EVP_MD *method = EVP_get_digestbyobj(algorithm->algorithm); if (!method) { result.status = trust_bad_digest; return result; } std::unique_ptr hashing(EVP_MD_CTX_new(), EVP_MD_CTX_free); unsigned char computed[EVP_MAX_MD_SIZE]; unsigned int computed_size = 0; EVP_DigestInit_ex(hashing.get(), method, nullptr); found->hash(hashing.get()); EVP_DigestFinal_ex(hashing.get(), computed, &computed_size); if (int(computed_size) != expected->length || std::memcmp(computed, expected->data, computed_size) != 0) { result.status = trust_bad_digest; return result; } // The signer's signature covers the content of SpcIndirectDataContent, without its own header. std::unique_ptr signed_data(BIO_new_mem_buf(indirect.data() + outer->first, int(indirect.size() - outer->first)), BIO_free); if (PKCS7_verify(signature.get(), nullptr, nullptr, signed_data.get(), nullptr, PKCS7_NOVERIFY | PKCS7_BINARY) != 1) { result.status = trust_bad_signature; return result; } STACK_OF(X509) *signers = PKCS7_get0_signers(signature.get(), nullptr, 0); X509 *signer = signers && sk_X509_num(signers) ? sk_X509_value(signers, 0) : nullptr; PKCS7_SIGNER_INFO *signer_info = sk_PKCS7_SIGNER_INFO_value(PKCS7_get_signer_info(signature.get()), 0); if (!signer || !signer_info) { sk_X509_free(signers); result.status = trust_bad_signature; return result; } // The chain is judged at the time the signature was stamped, as Windows does, or now when it carries no timestamp. std::unique_ptr verify(X509_STORE_CTX_new(), X509_STORE_CTX_free); X509_STORE_CTX_init(verify.get(), trust_anchors(), signer, signature->d.sign->cert); X509_STORE_CTX_set_purpose(verify.get(), X509_PURPOSE_ANY); // Old Microsoft authorities name no basic constraints; Windows does not ask a certificate without them to declare itself a CA. X509_STORE_CTX_set_verify_cb(verify.get(), [](int accepted, X509_STORE_CTX *store) { if (!accepted && X509_STORE_CTX_get_error(store) == X509_V_ERR_INVALID_CA) if (X509 *certificate = X509_STORE_CTX_get_current_cert(store); certificate && !(X509_get_extension_flags(certificate) & EXFLAG_BCONS)) return 1; return accepted; }); const auto when = stamped_time(signer_info); X509_VERIFY_PARAM_set_time(X509_STORE_CTX_get0_param(verify.get()), when.value_or(time_t(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()))); const bool trusted = X509_verify_cert(verify.get()) == 1; // RAVEN_TRACE_AUTHENTICODE names what the chain check found and the time it judged the signature at. if (std::getenv("RAVEN_TRACE_AUTHENTICODE")) std::fprintf(stderr, "authenticode: %s, stamped %lld, chain: %s\n", trusted ? "trusted" : "not trusted", when ? (long long)*when : -1LL, trusted ? "ok" : X509_verify_cert_error_string(X509_STORE_CTX_get_error(verify.get()))); if (STACK_OF(X509) *chain = X509_STORE_CTX_get0_chain(verify.get())) for (int index = 0; index < sk_X509_num(chain); ++index) result.chain.push_back(der_of(sk_X509_value(chain, index))); if (result.chain.empty()) result.chain.push_back(der_of(signer)); sk_X509_free(signers); result.anchored = trusted; if (!trusted) { switch (X509_STORE_CTX_get_error(verify.get())) { case X509_V_ERR_CERT_HAS_EXPIRED: case X509_V_ERR_CERT_NOT_YET_VALID: result.status = certificate_expired; break; case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: case X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE: result.status = certificate_chaining; break; default: result.status = certificate_untrusted_root; break; } } return result; } #else AuthenticodeResult verify_authenticode(std::span) { // Without the cryptography library no signature can be checked: the file is reported as unsigned. return {0x800b0100, {}, false}; } std::vector embedded_signature(std::span) { return {}; } std::vector subject_digest(std::span, bool) { return {}; } #endif }