如何使用Java提取已签名.exe文件的数字签名(无需外部工具,指纹即可)
Got it, let's walk through how to pull the digital signature fingerprint from a signed .exe using only Java—no external tools like signtool needed. Here's a practical, code-focused approach:
Core Idea
Signed .exes follow the PE (Portable Executable) format, which tucks the digital signature in a PKCS#7 blob referenced by the PE's security directory entry. Our plan is:
- Parse the PE file headers to locate the signature blob offset and size
- Extract that PKCS#7 blob from the file
- Parse the blob to get the embedded X.509 certificate
- Calculate the certificate's fingerprint (SHA-1, SHA-256, etc.)
Step-by-Step Implementation
1. Parse PE Headers to Find Signature Location
PE files start with a DOS header, which points to the NT headers. We need to read these headers to find the IMAGE_DIRECTORY_ENTRY_SECURITY entry, which holds the signature's position and length.
Note: PE uses little-endian byte order, so we'll need helper methods to convert bytes to integers correctly.
import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; // Helper to read little-endian integers from PE headers private static int readLEInt(byte[] bytes, int offset) { ByteBuffer buffer = ByteBuffer.wrap(bytes, offset, 4); buffer.order(ByteOrder.LITTLE_ENDIAN); return buffer.getInt(); } private static long readLELong(byte[] bytes, int offset) { ByteBuffer buffer = ByteBuffer.wrap(bytes, offset, 8); buffer.order(ByteOrder.LITTLE_ENDIAN); return buffer.getLong(); } // Locate the signature blob's position and size in the PE file private static long[] findSignatureBlob(RandomAccessFile raf) throws Exception { // Read DOS header to get the offset of the PE header byte[] dosHeader = new byte[64]; raf.readFully(dosHeader); int peHeaderOffset = readLEInt(dosHeader, 0x3C); // Read NT headers to access the optional header's data directory raf.seek(peHeaderOffset); byte[] ntHeader = new byte[24]; raf.readFully(ntHeader); int sizeOfOptionalHeader = readLEInt(ntHeader, 16); // Seek to the security directory entry in the data directory table int dataDirOffset = peHeaderOffset + 24 + sizeOfOptionalHeader - (16 * 8); raf.seek(dataDirOffset); byte[] securityDir = new byte[8]; raf.readFully(securityDir); long sigOffset = readLELong(securityDir, 0); long sigSize = readLELong(securityDir, 4); // If offset is 0, the file isn't digitally signed if (sigOffset == 0) { throw new Exception("No digital signature found in the executable"); } return new long[]{sigOffset, sigSize}; }
2. Extract PKCS#7 Signature Blob
Once we have the offset and size, we can read the signature blob directly from the file. Note that the first 8 bytes are a WIN_CERTIFICATE header, so we'll skip those to get the raw PKCS#7 data:
private static byte[] extractSignatureBlob(RandomAccessFile raf, long offset, long size) throws Exception { raf.seek(offset); byte[] blob = new byte[(int) size]; raf.readFully(blob); // Skip the 8-byte WIN_CERTIFICATE header to get the PKCS#7 payload return java.util.Arrays.copyOfRange(blob, 8, blob.length); }
3. Parse PKCS#7 Blob and Get Certificate Fingerprint
Use Java's built-in security APIs to parse the PKCS#7 blob, extract the X.509 certificate, and calculate its fingerprint:
import java.security.MessageDigest; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.io.ByteArrayInputStream; private static String getCertificateFingerprint(byte[] pkcs7Blob, String algorithm) throws Exception { CertificateFactory cf = CertificateFactory.getInstance("X.509"); // PKCS#7 blobs can contain multiple certificates; we'll take the first one (the signer's cert) X509Certificate cert = (X509Certificate) cf.generateCertificates(new ByteArrayInputStream(pkcs7Blob)).iterator().next(); MessageDigest md = MessageDigest.getInstance(algorithm); byte[] fingerprintBytes = md.digest(cert.getEncoded()); // Convert raw bytes to a human-readable hex string StringBuilder sb = new StringBuilder(); for (byte b : fingerprintBytes) { sb.append(String.format("%02X:", b)); } return sb.deleteCharAt(sb.length() - 1).toString(); // Remove the trailing colon }
4. Putting It All Together
Here's a main method to tie all the pieces together:
public static void main(String[] args) { String exePath = "path/to/your/signed.exe"; String fingerprintAlgorithm = "SHA-256"; // Use "SHA-1" if you need that legacy format try (RandomAccessFile raf = new RandomAccessFile(exePath, "r")) { long[] sigInfo = findSignatureBlob(raf); byte[] pkcs7Blob = extractSignatureBlob(raf, sigInfo[0], sigInfo[1]); String fingerprint = getCertificateFingerprint(pkcs7Blob, fingerprintAlgorithm); System.out.printf("%s Fingerprint: %s%n", fingerprintAlgorithm, fingerprint); } catch (Exception e) { e.printStackTrace(); } }
Important Notes
- Little-Endian Handling: PE headers use little-endian byte order, which is why we rely on
ByteBufferwithByteOrder.LITTLE_ENDIAN—don't skip this, or your header parsing will be wrong! - Unsigned Files: The code throws an exception if no signature is found; you can adjust this to return a null or empty string if you need to handle unsigned files gracefully.
- Fingerprint Algorithms: SHA-1 is still widely used but deprecated for many security-critical use cases. SHA-256 is the recommended modern alternative.
- Memory Efficiency: Using
RandomAccessFilemeans we only load the parts of the .exe we need, not the entire file—great for large executables.
内容的提问来源于stack exchange,提问作者user3413804

