001/* 002 * Copyright 2012 Emmanuel Bourg 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package net.jsign.pe; 018 019import java.io.File; 020import java.io.IOException; 021import java.nio.ByteBuffer; 022import java.nio.ByteOrder; 023import java.nio.channels.SeekableByteChannel; 024import java.nio.file.Files; 025import java.nio.file.StandardOpenOption; 026import java.security.MessageDigest; 027import java.util.ArrayList; 028import java.util.List; 029 030import org.bouncycastle.asn1.ASN1Object; 031import org.bouncycastle.asn1.DERNull; 032import org.bouncycastle.asn1.x509.AlgorithmIdentifier; 033import org.bouncycastle.asn1.x509.DigestInfo; 034import org.bouncycastle.cms.CMSSignedData; 035 036import net.jsign.DigestAlgorithm; 037import net.jsign.Signable; 038import net.jsign.SignatureUtils; 039import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers; 040import net.jsign.asn1.authenticode.SpcAttributeTypeAndOptionalValue; 041import net.jsign.asn1.authenticode.SpcIndirectDataContent; 042import net.jsign.asn1.authenticode.SpcPeImageData; 043 044import static net.jsign.ChannelUtils.*; 045 046/** 047 * Portable Executable File. 048 * 049 * This class is thread safe. 050 * 051 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/debug/pe-format">Microsoft PE and COFF Specification </a> 052 * 053 * @author Emmanuel Bourg 054 * @since 1.0 055 */ 056public class PEFile implements Signable { 057 058 /** The position of the PE header in the file */ 059 private final long peHeaderOffset; 060 061 final SeekableByteChannel channel; 062 063 /** Reusable buffer for reading bytes, words, dwords and qwords from the file */ 064 private final ByteBuffer valueBuffer = ByteBuffer.allocate(8); 065 { 066 valueBuffer.order(ByteOrder.LITTLE_ENDIAN); 067 } 068 069 /** 070 * Tells if the specified file is a Portable Executable file. 071 * 072 * @param file the file to check 073 * @return <code>true</code> if the file is a Portable Executable, <code>false</code> otherwise 074 * @throws IOException if an I/O error occurs 075 * @since 3.0 076 */ 077 public static boolean isPEFile(File file) throws IOException { 078 if (!file.exists() || !file.isFile()) { 079 return false; 080 } 081 082 try { 083 PEFile peFile = new PEFile(file); 084 peFile.close(); 085 return true; 086 } catch (IOException e) { 087 if (e.getMessage().contains("DOS header signature not found") || e.getMessage().contains("PE signature not found")) { 088 return false; 089 } else { 090 throw e; 091 } 092 } 093 } 094 095 /** 096 * Create a PEFile from the specified file. 097 * 098 * @param file the file to open 099 * @throws IOException if an I/O error occurs 100 */ 101 public PEFile(File file) throws IOException { 102 this(Files.newByteChannel(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE)); 103 } 104 105 /** 106 * Create a PEFile from the specified channel. 107 * 108 * @param channel the channel to read the file from 109 * @throws IOException if an I/O error occurs 110 * @since 2.0 111 */ 112 public PEFile(SeekableByteChannel channel) throws IOException { 113 this.channel = channel; 114 115 try { 116 // check if the PE file is too big 117 if (channel.size() >= 1L << 32) { 118 throw new IOException("Invalid PE file: the size exceeds 4GB"); 119 } 120 121 // DOS Header 122 read(0, 0, 2); 123 if (valueBuffer.get() != 'M' || valueBuffer.get() != 'Z') { 124 throw new IOException("DOS header signature not found"); 125 } 126 127 // PE Header 128 read(0x3C, 0, 4); 129 peHeaderOffset = valueBuffer.getInt() & 0xFFFFFFFFL; 130 read(peHeaderOffset, 0, 4); 131 if (valueBuffer.get() != 'P' || valueBuffer.get() != 'E' || valueBuffer.get() != 0 || valueBuffer.get() != 0) { 132 throw new IOException("PE signature not found as expected at offset 0x" + Long.toHexString(peHeaderOffset)); 133 } 134 135 } catch (IOException e) { 136 channel.close(); 137 throw e; 138 } 139 } 140 141 public void save() { 142 } 143 144 /** 145 * Closes the file 146 * 147 * @throws IOException if an I/O error occurs 148 */ 149 public synchronized void close() throws IOException { 150 channel.close(); 151 } 152 153 synchronized int read(byte[] buffer, long base, int offset) throws IOException { 154 channel.position(base + offset); 155 return channel.read(ByteBuffer.wrap(buffer)); 156 } 157 158 private void read(long base, int offset, int length) throws IOException { 159 valueBuffer.limit(length); 160 valueBuffer.clear(); 161 channel.position(base + offset); 162 channel.read(valueBuffer); 163 valueBuffer.rewind(); 164 } 165 166 synchronized int readWord(long base, int offset) throws IOException { 167 read(base, offset, 2); 168 return valueBuffer.getShort() & 0xFFFF; 169 } 170 171 synchronized long readDWord(long base, int offset) throws IOException { 172 read(base, offset, 4); 173 return valueBuffer.getInt() & 0xFFFFFFFFL; 174 } 175 176 synchronized void write(long base, byte[] data) throws IOException { 177 write(base, ByteBuffer.wrap(data)); 178 } 179 180 synchronized void write(long base, ByteBuffer data) throws IOException { 181 channel.position(base); 182 while (data.hasRemaining()) { 183 channel.write(data); 184 } 185 } 186 187 PEFormat getFormat() throws IOException { 188 return PEFormat.valueOf(readWord(peHeaderOffset, 24)); 189 } 190 191 /** 192 * The image file checksum. 193 * 194 * @return the checksum of the image 195 */ 196 long getCheckSum() throws IOException { 197 return readDWord(peHeaderOffset, 88); 198 } 199 200 /** 201 * Compute the checksum of the image file. The algorithm for computing 202 * the checksum is incorporated into IMAGHELP.DLL. 203 * 204 * @return the checksum of the image 205 */ 206 synchronized long computeChecksum() throws IOException { 207 PEImageChecksum checksum = new PEImageChecksum(peHeaderOffset + 88); 208 209 ByteBuffer b = ByteBuffer.allocate(64 * 1024); 210 211 channel.position(0); 212 213 int len; 214 while ((len = channel.read(b)) > 0) { 215 b.flip(); 216 checksum.update(b.array(), 0, len); 217 } 218 219 return checksum.getValue(); 220 } 221 222 synchronized void updateChecksum() throws IOException { 223 ByteBuffer buffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN); 224 buffer.putInt((int) computeChecksum()); 225 buffer.flip(); 226 227 write(peHeaderOffset + 88, buffer); 228 } 229 230 /** 231 * The subsystem that is required to run this image. 232 * 233 * @return the required subsystem 234 */ 235 Subsystem getSubsystem() throws IOException { 236 return Subsystem.valueOf(readWord(peHeaderOffset, 92)); 237 } 238 239 boolean isEFI() throws IOException { 240 Subsystem subsystem = getSubsystem(); 241 return subsystem == Subsystem.EFI_APPLICATION 242 || subsystem == Subsystem.EFI_BOOT_SERVICE_DRIVER 243 || subsystem == Subsystem.EFI_ROM 244 || subsystem == Subsystem.EFI_RUNTIME_DRIVER; 245 } 246 247 /** 248 * The number of data-directory entries in the remainder of the optional 249 * header. Each describes a location and size. 250 * 251 * @return the number of data-directory entries 252 */ 253 int getNumberOfRvaAndSizes() throws IOException { 254 return (int) readDWord(peHeaderOffset, PEFormat.PE32.equals(getFormat()) ? 116 : 132); 255 } 256 257 int getDataDirectoryOffset() throws IOException { 258 return (int) peHeaderOffset + (PEFormat.PE32.equals(getFormat()) ? 120 : 136); 259 } 260 261 /** 262 * Returns the data directory of the specified type. 263 * 264 * @param type the type of data directory 265 * @return the data directory of the specified type 266 */ 267 DataDirectory getDataDirectory(DataDirectoryType type) throws IOException { 268 if (type.ordinal() >= getNumberOfRvaAndSizes()) { 269 return null; 270 } else { 271 return new DataDirectory(this, type.ordinal()); 272 } 273 } 274 275 /** 276 * Writes the certificate table. The data is either appended at the end of the file 277 * or written over the previous certificate table. 278 * 279 * @param entries the entries of the certificate table 280 * @throws IOException if an I/O error occurs 281 */ 282 private synchronized void writeCertificateTable(CertificateTableEntry[] entries) throws IOException { 283 int totalSize = 0; 284 for (CertificateTableEntry entry : entries) { 285 totalSize += entry.getSize(); 286 } 287 288 byte[] data = new byte[totalSize]; 289 int pos = 0; 290 for (CertificateTableEntry entry : entries) { 291 System.arraycopy(entry.toBytes(), 0, data, pos, entry.getSize()); 292 pos += entry.getSize(); 293 } 294 295 DataDirectory directory = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE); 296 if (directory == null) { 297 throw new IOException("No space allocated in the data directories index for the certificate table"); 298 } 299 300 if (!directory.exists()) { 301 // append the data directory at the end of the file on a 8-byte boundary 302 long offset = channel.size() + (8 - channel.size() % 8) % 8; 303 304 write(offset, data); 305 306 // update the entry in the data directory table 307 directory.write(offset, data.length); 308 309 } else if (directory.isTrailing()) { 310 // the data is at the end of the file, overwrite it 311 write(directory.getVirtualAddress(), data); 312 channel.truncate(directory.getVirtualAddress() + data.length); // trim the file if the data shrunk 313 314 // update the size in the data directory table 315 directory.write(directory.getVirtualAddress(), data.length); 316 317 } else { 318 throw new IOException("The certificate table isn't at the end of the file"); 319 } 320 321 updateChecksum(); 322 } 323 324 @Override 325 public synchronized List<CMSSignedData> getSignatures() throws IOException { 326 List<CMSSignedData> signatures = new ArrayList<>(); 327 328 for (CertificateTableEntry certificate : getCertificateTable()) { 329 if (certificate.isSupported()) { 330 signatures.addAll(SignatureUtils.getSignatures(certificate.getContent())); 331 } 332 } 333 334 return signatures; 335 } 336 337 @Override 338 public void setSignature(CMSSignedData signature) throws IOException { 339 if (signature != null) { 340 CertificateTableEntry[] entries; 341 if (isEFI()) { 342 List<CMSSignedData> signatures = SignatureUtils.getSignatures(signature); 343 entries = new CertificateTableEntry[signatures.size()]; 344 for (int i = 0; i < signatures.size(); i++) { 345 entries[i] = new CertificateTableEntry(signatures.get(i)); 346 } 347 } else { 348 CertificateTableEntry entry = new CertificateTableEntry(signature); 349 entries = new CertificateTableEntry[] { entry }; 350 } 351 writeCertificateTable(entries); 352 353 } else if (getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE).exists()) { 354 // erase the previous signature 355 DataDirectory certificateTable = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE); 356 channel.truncate(certificateTable.getVirtualAddress()); 357 certificateTable.write(0, 0); 358 } 359 } 360 361 synchronized List<CertificateTableEntry> getCertificateTable() throws IOException { 362 List<CertificateTableEntry> entries = new ArrayList<>(); 363 DataDirectory certificateTable = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE); 364 365 if (certificateTable != null && certificateTable.exists()) { 366 long position = certificateTable.getVirtualAddress(); 367 long size = certificateTable.getSize(); 368 369 try { 370 while (position < certificateTable.getVirtualAddress() + size) { 371 CertificateTableEntry entry = new CertificateTableEntry(this, position); 372 entries.add(entry); 373 if (!isEFI()) { 374 // only one entry for non-EFI files 375 break; 376 } 377 378 position += entry.getSize(); 379 } 380 } catch (Exception e) { 381 e.printStackTrace(); 382 } 383 } 384 385 return entries; 386 } 387 388 /** 389 * Compute the digest of the file. The checksum field, the certificate 390 * directory table entry and the certificate table are excluded from 391 * the digest. 392 * 393 * @param digestAlgorithm the digest algorithm to use 394 * @return the digest of the file 395 * @throws IOException if an I/O error occurs 396 */ 397 @Override 398 public synchronized byte[] computeDigest(DigestAlgorithm digestAlgorithm) throws IOException { 399 MessageDigest digest = digestAlgorithm.getMessageDigest(); 400 401 long checksumLocation = peHeaderOffset + 88; 402 403 DataDirectory certificateTable = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE); 404 405 // digest from the beginning to the checksum field (excluded) 406 updateDigest(channel, digest, 0, checksumLocation); 407 408 // skip the checksum field 409 long position = checksumLocation + 4; 410 411 // digest from the end of the checksum field to the beginning of the certificate table entry 412 int certificateTableOffset = getDataDirectoryOffset() + 8 * DataDirectoryType.CERTIFICATE_TABLE.ordinal(); 413 updateDigest(channel, digest, position, certificateTableOffset); 414 415 // skip the certificate entry 416 position = certificateTableOffset + 8; 417 418 // digest from the end of the certificate table entry to the beginning of the certificate table 419 if (certificateTable != null && certificateTable.exists()) { 420 certificateTable.check(); 421 updateDigest(channel, digest, position, certificateTable.getVirtualAddress()); 422 position = certificateTable.getVirtualAddress() + certificateTable.getSize(); 423 } 424 425 // digest from the end of the certificate table to the end of the file 426 updateDigest(channel, digest, position, channel.size()); 427 428 if (certificateTable == null || !certificateTable.exists()) { 429 // if the file has never been signed before, update the digest as if the file was padded on a 8 byte boundary 430 int paddingLength = (int) (8 - channel.size() % 8) % 8; 431 digest.update(new byte[paddingLength]); 432 } 433 434 return digest.digest(); 435 } 436 437 @Override 438 public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException { 439 AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE); 440 DigestInfo digestInfo = new DigestInfo(algorithmIdentifier, computeDigest(digestAlgorithm)); 441 SpcAttributeTypeAndOptionalValue data = new SpcAttributeTypeAndOptionalValue(AuthenticodeObjectIdentifiers.SPC_PE_IMAGE_DATA_OBJID, new SpcPeImageData()); 442 443 return new SpcIndirectDataContent(data, digestInfo); 444 } 445}