PEFileTest.java

/*
 * Copyright 2012 Emmanuel Bourg
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.jsign.pe;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.KeyStore;

import org.apache.commons.io.FileUtils;
import org.bouncycastle.util.encoders.Hex;
import org.junit.Test;

import net.jsign.AuthenticodeSigner;
import net.jsign.KeyStoreBuilder;

import static net.jsign.DigestAlgorithm.*;
import static net.jsign.SignatureAssert.*;
import static org.junit.Assert.*;

public class PEFileTest {

    @Test
    public void testIsMSCabinetFile() throws Exception {
        assertTrue(PEFile.isPEFile(new File("target/test-classes/wineyes.exe")));
        assertFalse(PEFile.isPEFile(new File("target/test-classes/mscab/sample1.cab")));
        assertFalse(PEFile.isPEFile(new File("target")));
        assertFalse(PEFile.isPEFile(new File("target/non-existent")));
    }

    @Test
    public void testLoad() throws Exception {
        try (PEFile file = new PEFile(new File("target/test-classes/wineyes.exe"))) {
            assertEquals(PEFormat.PE32, file.getFormat());
            assertEquals(16, file.getNumberOfRvaAndSizes());
        }
    }

    @Test
    public void testLoadNonExecutable() {
        Exception e = assertThrows(IOException.class, () -> new PEFile(new File("pom.xml")));
        assertEquals("message", "DOS header signature not found", e.getMessage());
    }

    /**
     * Attempts to open a DOS executable that isn't a Portable Executable
     */
    @Test
    public void testDosExecutable() throws Exception {
        // MORE.EXE comes from FreeDOS and is GPL licensed
        Exception e = assertThrows(IOException.class, () -> new PEFile(new File("target/test-classes/MORE.EXE")));
        if (!e.getMessage().contains("PE signature not found as expected")) {
            throw e;
        }
    }

    @Test
    public void testComputeChecksum() throws Exception {
        try (PEFile file = new PEFile(new File("target/test-classes/wineyes.exe"))) {
            assertEquals("checksum", 0x0000E7F5, file.computeChecksum());
        }
    }

    @Test
    public void testUpdateChecksum() throws Exception {
        // Expand the test file beyond the size of the buffer used in updateChecksum() (> 64K)
        File srcFile = new File("target/test-classes/wineyes.exe");
        File destFile = new File("target/test-classes/wineyes-big.exe");
        FileUtils.copyFile(srcFile, destFile);
        RandomAccessFile raf = new RandomAccessFile(destFile, "rw");
        raf.setLength(1024 * 1024 + 73);
        raf.close();

        PEFile file = new PEFile(destFile);
        file.updateChecksum();
        assertEquals("checksum", 0x0010483E, file.getCheckSum());
    }

    @Test
    public void testComputeDigest() throws Exception {
        try (PEFile file = new PEFile(new File("target/test-classes/wineyes.exe"))) {
            String sha1 = Hex.toHexString(file.computeDigest(SHA1));
            String sha256 = Hex.toHexString(file.computeDigest(SHA256.getMessageDigest()));

            assertEquals("sha1", "d27ec498912807ddfc4bec2be4f62c42814836f3", sha1);
            assertEquals("sha2", "7bb369df020cea757619e1c1d678dbca06b638f2cc45b740b5eacfc21e76b160", sha256);
        }
    }

    @Test
    public void testComputeDigestNotPadded() throws Exception {
        File testFile = new File("target/test-classes/wineyes.exe");

        assertEquals("Test file not padded", 0, testFile.length() % 8);

        File testFilePadded = new File("target/test-classes/wineyes-padded.exe");
        File testFileNotPadded = new File("target/test-classes/wineyes-notpadded.exe");
        FileUtils.copyFile(testFile, testFilePadded);
        FileUtils.copyFile(testFile, testFileNotPadded);

        try (PEFile file1 = new PEFile(testFilePadded);
             PEFile file2 = new PEFile(testFileNotPadded)) {
            file1.write(file1.channel.size(), new byte[8]);
            file2.write(file2.channel.size(), new byte[3]);

            String digestPadded = Hex.toHexString(file1.computeDigest(SHA1));
            String digestNotPadded = Hex.toHexString(file2.computeDigest(SHA1));

            assertEquals("digest", digestPadded, digestNotPadded);
        }
    }

    @Test
    public void testComputeDigestInvalidCertificateTableNegativeAddress() throws Exception {
        File srcFile = new File("target/test-classes/wineyes.exe");
        File destFile = new File("target/test-classes/wineyes-fuzzed.exe");
        FileUtils.copyFile(srcFile, destFile);

        try (PEFile file = new PEFile(destFile)) {
            DataDirectory certificateTable = file.getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
            certificateTable.write(Integer.MIN_VALUE, 1024);
            assertThrows(IOException.class, () -> file.computeDigest(SHA1));
        }
    }

    @Test
    public void testComputeDigestInvalidCertificateTableNegativeSize() throws Exception {
        File srcFile = new File("target/test-classes/wineyes.exe");
        File destFile = new File("target/test-classes/wineyes-fuzzed.exe");
        FileUtils.copyFile(srcFile, destFile);

        try (PEFile file = new PEFile(destFile)) {
            DataDirectory certificateTable = file.getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
            certificateTable.write(1024, Integer.MIN_VALUE);
            assertThrows(IOException.class, () -> file.computeDigest(SHA1));
        }
    }

    @Test
    public void testComputeDigestInvalidCertificateTableAfterEndOfFile() throws Exception {
        File srcFile = new File("target/test-classes/wineyes.exe");
        File destFile = new File("target/test-classes/wineyes-fuzzed.exe");
        FileUtils.copyFile(srcFile, destFile);

        try (PEFile file = new PEFile(destFile)) {
            DataDirectory certificateTable = file.getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
            certificateTable.write(Integer.MAX_VALUE, Integer.MAX_VALUE);
            assertThrows(IOException.class, () -> file.computeDigest(SHA1));
        }
    }

    @Test
    public void testCertificateTableAfterEndOfFile() throws Exception {
        File srcFile = new File("target/test-classes/wineyes.exe");
        File destFile = new File("target/test-classes/wineyes-fuzzed.exe");
        FileUtils.copyFile(srcFile, destFile);

        try (PEFile file = new PEFile(destFile)) {
            DataDirectory certificateTable = file.getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
            certificateTable.write(Integer.MAX_VALUE, 1024);

            assertTrue("Certificate table after the end of the file not ignored", file.getSignatures().isEmpty());
        }
    }

    @Test
    public void testCertificateTableInvalidSize() throws Exception {
        File srcFile = new File("target/test-classes/wineyes.exe");
        File destFile = new File("target/test-classes/wineyes-fuzzed.exe");
        FileUtils.copyFile(srcFile, destFile);

        try (PEFile file = new PEFile(destFile)) {
            DataDirectory certificateTable = file.getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
            certificateTable.write(file.channel.size() - 512, Integer.MAX_VALUE);
            file.channel.position(certificateTable.getVirtualAddress());
            file.channel.write((ByteBuffer) ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(Integer.MAX_VALUE).flip());

            assertTrue("Certificate table with invalid size not ignored", file.getSignatures().isEmpty());
        }
    }

    @Test
    public void testRemoveSignature() throws Exception {
        File sourceFile = new File("target/test-classes/wineyes.exe");
        File targetFile = new File("target/test-classes/wineyes-unsigned.exe");

        FileUtils.copyFile(sourceFile, targetFile);

        KeyStore keystore = new KeyStoreBuilder().keystore("target/test-classes/keystores/keystore.jks").storepass("password").build();
        AuthenticodeSigner signer = new AuthenticodeSigner(keystore, "test", "password").withTimestamping(false);

        try (PEFile file = new PEFile(targetFile)) {
            file.setSignature(null);
            signer.sign(file);
            assertSigned(file, SHA256);
            file.setSignature(null);
            assertNotSigned(file);
        }
    }
}