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}