C++ 实现 Android APK 数字签名提取与验证

Android 数字签名

Android 应用的数字签名是系统用以验证应用来源和完整性的关键机制。每个 APK 文件在发布前都必须由开发者使用私钥进行签名,系统在安装或更新时会校验签名是否与已有版本一致,从而保证应用包未被篡改,并确保其来自同一开发者。签名证书中包含的主体(Subject)、颁发者(Issuer)、序列号(Serial Number)、有效期(Validity Period)以及指纹信息(SHA1、SHA256)构成了判断应用身份的基础数据。

在应用安全检测、威胁分析、合规审查及自动化构建流程中,往往需要直接获取 APK 的签名信息,以验证证书的合法性或比对来源。此类信息通常用于确认应用是否由可信开发者签发,或判断其是否被重新打包、修改或伪造。数字签名的提取与解析因此成为应用安全分析的重要组成部分。

常见的签名查看方式主要依赖 Android SDK 工具,例如使用 apksigner verify --print-certs your.apk 查看证书摘要,或使用 keytool -list -printcert -jarfile your.apk 输出签名证书信息。这些工具能够满足常规需求,但在嵌入式环境、安全扫描引擎、持续集成系统或跨平台安全框架中,命令行调用方式往往难以直接集成,执行效率和可扩展性也受到限制。

为实现更高的灵活性与自动化能力,签名提取可通过代码在本地完成。APK 文件本质上是一个 ZIP 压缩包,其签名数据通常存储在 META-INF/ 目录下的 .RSA.DSA.EC 文件中。通过 libzip 读取签名文件,再结合 OpenSSL 对内部的 PKCS#7 结构进行解析,即可提取完整的证书链信息,包括签名算法、公钥长度、证书有效期和指纹摘要等。

本篇文章将介绍如何使用 C++ 结合 libzipOpenSSL,实现对 Android APK 数字签名的手动提取与解析。整个过程不依赖外部工具,可直接嵌入到安全分析系统或自动化平台中,为应用签名验证与证书信息识别提供可靠的编程实现方式。

APK 签名结构与官方工具

Android APK 文件的数字签名机制依赖于 Java 签名体系,通常被称为 JAR 签名(V1 签名)。APK 文件本质上是一个 ZIP 压缩包,其中的签名文件存储在 META-INF/ 目录下,常见的文件类型包括 .RSA.DSA.EC。这些文件内部采用 PKCS#7 结构保存签名数据和证书链,证书中包含应用发布者的主体信息、颁发者信息、序列号、有效期、公钥以及指纹摘要等。通过解析这些签名文件,可以恢复完整的证书链,从而获得签名的关键属性。

系统在安装或更新 APK 时,会验证签名的完整性和有效性。安装程序会检查应用包内容是否被修改,以及签名证书是否与设备上已有版本一致。签名验证的过程不仅判断文件是否被篡改,还能保证应用来源的连续性和唯一性。在安全分析或自动化构建环境中,直接获取签名文件并解析其内容可以完成类似验证功能,同时提供更多可编程的分析接口。

官方工具提供了便捷的方式来查看 APK 的签名信息。apksigner 是 Android SDK 中推荐的命令行工具,其命令 apksigner verify --print-certs <apk文件> 可以输出签名证书的摘要、SHA-1 和 SHA-256 指纹以及证书有效期等关键信息。另一种方法是使用 Java 自带的 keytool 工具,通过命令 keytool -list -printcert -jarfile <apk文件> 或对解压后的签名文件执行 keytool -printcert -file <RSA文件>,同样可以获取签名证书详情。

尽管这些工具在日常开发和测试中可满足大多数需求,但它们依赖于 Java 运行环境或 Android SDK,难以直接嵌入原生安全分析系统或跨平台自动化流程中。为了实现更高效、灵活和可扩展的签名信息获取,需要通过编程方式直接读取 APK 内的签名文件并解析 PKCS#7 数据结构,从而获取完整的证书信息和指纹数据。

从 APK 中提取签名文件的编程实现

APK 文件本质上是 ZIP 压缩包,签名数据通常存储在 META-INF/ 目录下的 .RSA.DSA.EC 文件中。为了在程序中提取签名信息,需要首先读取这些文件的二进制内容,以供后续解析 PKCS#7 结构。

实现提取的核心步骤包括:打开 APK 文件、遍历压缩包中的文件条目、识别签名文件并读取其内容。C++ 结合 libzip 库可以完成这些操作,从而在内存中获取签名文件的完整数据。

以下代码展示了读取 APK 中签名文件的实现思路:

std::vector<uint8_t> extract_signature_from_apk(const std::string& apk_path) {
   int err = 0;
   zip_t* zip = zip_open(apk_path.c_str(), 0, &err);
   if (!zip) {
       std::cerr << "Failed to open APK: " << apk_path << std::endl;
       return {};
  }

   std::vector<uint8_t> signature_data;
   zip_int64_t num_entries = zip_get_num_entries(zip, 0);

   for (zip_int64_t i = 0; i < num_entries; i++) {
       const char* name = zip_get_name(zip, i, 0);
       if (!name) continue;

       std::string filename(name);
       if (filename.find("META-INF/") == 0 &&
          (filename.find(".RSA") != std::string::npos ||
            filename.find(".DSA") != std::string::npos ||
            filename.find(".EC")  != std::string::npos)) {

           zip_stat_t st;
           if (zip_stat_index(zip, i, 0, &st) == 0) {
               zip_file_t* file = zip_fopen_index(zip, i, 0);
               if (file) {
                   signature_data.resize(st.size);
                   zip_int64_t bytes_read = zip_fread(file, signature_data.data(), st.size);
                   if (bytes_read != static_cast<zip_int64_t>(st.size))
                       signature_data.clear();
                   zip_fclose(file);
                   break;
              }
          }
      }
  }

   zip_close(zip);
   return signature_data;
}

上述实现过程的关键点包括:

  • zip_open 用于打开 APK 压缩包,返回对应的文件指针;
  • 遍历所有文件条目,通过 zip_get_name 获取文件名,判断是否位于 META-INF/ 并具有 .RSA.DSA.EC 后缀,从而识别签名文件;
  • 使用 zip_stat_index 获取文件大小,再通过 zip_fopen_indexzip_fread 将文件内容读取到内存向量中;
  • 读取完成后关闭文件和压缩包,返回包含签名数据的字节数组。

通过这一方法,可以在程序中以二进制形式获取 APK 的签名文件,为后续利用 OpenSSL 解析 PKCS#7 结构并提取证书信息提供输入数据。这种方式具有平台独立性,无需依赖外部命令行工具,可直接嵌入安全分析系统或自动化流程中,实现高效的签名信息获取。

解析签名数据并提取证书信息

在获取 APK 内签名文件的二进制数据后,需要解析其 PKCS#7 结构,以提取包含的证书链及关键信息。数字签名文件通常为 .RSA.DSA.EC 格式,内部采用 PKCS#7 封装标准存储签名和证书数据。C++ 可以结合 OpenSSL 库对内存中的签名数据进行解析,从而获取证书的主体、颁发者、有效期、签名算法、公钥信息以及指纹摘要。

以下代码片段展示了基本的解析流程:

void analyze_signature_data(const std::vector<uint8_t>& signature_data) {
   if (signature_data.empty()) return;

   BIO* bio = BIO_new(BIO_s_mem());
   BIO_write(bio, signature_data.data(), signature_data.size());

   PKCS7* p7 = d2i_PKCS7_bio(bio, nullptr);
   BIO_free(bio);

   if (!p7) {
       std::cerr << "Failed to parse PKCS7 signature" << std::endl;
       ERR_print_errors_fp(stderr);
       return;
  }

   if (PKCS7_type_is_signed(p7)) {
       STACK_OF(X509)* certs = p7->d.sign->cert;
       for (int i = 0; i < sk_X509_num(certs); i++) {
           X509* cert = sk_X509_value(certs, i);

           char subject[256], issuer[256];
           X509_NAME_oneline(X509_get_subject_name(cert), subject, sizeof(subject));
           X509_NAME_oneline(X509_get_issuer_name(cert), issuer, sizeof(issuer));
           std::cout << "--- Certificate " << i + 1 << " ---\n";
           std::cout << "Subject: " << subject << "\n";
           std::cout << "Issuer: " << issuer << "\n";

           ASN1_INTEGER* serial = X509_get_serialNumber(cert);
           BIGNUM* bn = ASN1_INTEGER_to_BN(serial, nullptr);
           char* serial_hex = BN_bn2hex(bn);
           std::cout << "Serial Number: " << serial_hex << "\n";
           OPENSSL_free(serial_hex);
           BN_free(bn);

           const X509_ALGOR* sig_alg;
           X509_get0_signature(nullptr, &sig_alg, cert);
           int nid = OBJ_obj2nid(sig_alg->algorithm);
           std::cout << "Signature Algorithm: " << OBJ_nid2ln(nid) << "\n";

           EVP_PKEY* pkey = X509_get_pubkey(cert);
           if (pkey) {
               std::cout << "Public Key Type: " << OBJ_nid2ln(EVP_PKEY_id(pkey)) << "\n";
               if (EVP_PKEY_id(pkey) == EVP_PKEY_RSA) {
                   RSA* rsa = EVP_PKEY_get1_RSA(pkey);
                   if (rsa) {
                       std::cout << "Key Size: " << RSA_size(rsa) * 8 << " bits\n";
                       RSA_free(rsa);
                  }
              }
               EVP_PKEY_free(pkey);
          }

           unsigned char md[EVP_MAX_MD_SIZE];
           unsigned int md_len;
           if (X509_digest(cert, EVP_sha1(), md, &md_len)) {
               std::cout << "SHA1 Fingerprint: ";
               for (unsigned int j = 0; j < md_len; j++) {
                   printf("%02X", md[j]);
                   if (j < md_len - 1) printf(":");
              }
               std::cout << "\n";
          }
           if (X509_digest(cert, EVP_sha256(), md, &md_len)) {
               std::cout << "SHA256 Fingerprint: ";
               for (unsigned int j = 0; j < md_len; j++) {
                   printf("%02X", md[j]);
                   if (j < md_len - 1) printf(":");
              }
               std::cout << "\n";
          }
      }
  }

   PKCS7_free(p7);
}

该实现过程的核心点包括:

  • 使用 BIO_newBIO_write 将签名数据加载到内存 BIO 中;
  • 通过 d2i_PKCS7_bio 将内存数据解析为 PKCS#7 结构;
  • 判断 PKCS#7 是否为签名类型,通过 p7->d.sign->cert 获取证书链;
  • 遍历证书链,依次读取证书主体、颁发者、序列号、签名算法和公钥信息;
  • 使用 X509_digest 计算 SHA1 和 SHA256 指纹,以便进行唯一标识和比对。

通过此方法,可以在程序中获取 APK 的完整签名证书信息,为进一步的安全分析、签名验证或证书比对提供数据基础。整个流程无需依赖外部工具,可直接集成到 C++ 安全分析模块或自动化构建系统中,实现高效、可编程的签名提取功能。

在对 APK 文件进行结构分析和签名提取后,还应该考虑程序可能面临的安全威胁。未经加固的应用和库文件可能被逆向分析或篡改,从而暴露核心逻辑和敏感数据。为应对这些风险,可以使用专业的加固工具对程序进行保护。Virbox Protector 提供包括代码混淆、指令级虚拟化、调试检测和异常运行环境防护在内的多层保护措施,在保持程序正常运行的前提下,显著增加逆向分析难度,提升软件整体安全性。

总结

本文系统地阐述了从 Android APK 文件中获取数字签名信息的编程实现方法。首先明确了 APK 数字签名在应用安全体系中的作用,包括验证应用来源、确保完整性及支持安全分析的必要性。随后分析了 APK 文件的签名结构,说明签名文件通常存储于 META-INF/ 目录下的 .RSA.DSA.EC 文件中,并采用 PKCS#7 结构封装证书链和签名数据。

在技术实现方面,介绍了利用 C++ 结合 libzip 读取 APK 内签名文件的二进制内容的方法,并展示了如何使用 OpenSSL 解析 PKCS#7 结构,从而提取证书主体、颁发者、序列号、有效期、签名算法、公钥信息及 SHA 指纹等关键数据。这一方法无需依赖外部工具,可在本地完成签名信息提取,并为自动化分析和安全验证提供可靠的数据支持。

通过以上步骤,可以实现对 APK 文件签名的完整解析,为安全分析和证书验证提供可编程、跨平台的技术基础。

滚动至顶部
售前客服
周末值班
电话

电话

13910187371