Java tutorial
/******************************************************************************* * Copyright 2014 Katja Hahn * * 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 com.github.katjahahn.parser.sections; import static com.github.katjahahn.parser.sections.SectionHeaderKey.*; import java.io.File; import java.io.IOException; import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.github.katjahahn.parser.FileFormatException; import com.github.katjahahn.parser.Location; import com.github.katjahahn.parser.MemoryMappedPE; import com.github.katjahahn.parser.PEData; import com.github.katjahahn.parser.PhysicalLocation; import com.github.katjahahn.parser.VirtualLocation; import com.github.katjahahn.parser.optheader.DataDirEntry; import com.github.katjahahn.parser.optheader.DataDirectoryKey; import com.github.katjahahn.parser.optheader.OptionalHeader; import com.github.katjahahn.parser.optheader.StandardFieldEntryKey; import com.github.katjahahn.parser.sections.debug.DebugSection; import com.github.katjahahn.parser.sections.edata.ExportSection; import com.github.katjahahn.parser.sections.idata.DelayLoadSection; import com.github.katjahahn.parser.sections.idata.ImportSection; import com.github.katjahahn.parser.sections.pdata.ExceptionSection; import com.github.katjahahn.parser.sections.reloc.RelocationSection; import com.github.katjahahn.parser.sections.rsrc.ResourceSection; import com.google.common.annotations.Beta; import com.google.common.base.Optional; import com.google.common.base.Preconditions; /** * Responsible for computing section related values that are necessary for * loading a section---for example conversion between relative virtual addresses * and file offset---, loading data directories, and loading sections. * <p> * The section loader is able to load special sections like the * {@link ImportSection}, {@link ExportSection} and {@link ResourceSection} * * @author Katja Hahn * */ public class SectionLoader { private static final Logger logger = LogManager.getLogger(SectionLoader.class.getName()); private final SectionTable table; private final File file; private final OptionalHeader optHeader; private final PEData data; private Optional<MemoryMappedPE> memoryMapped = Optional.absent(); /** * Creates a SectionLoader instance taking all information from the given * {@link PEData} object. * * @param data */ public SectionLoader(PEData data) { // extract data for easier access this.table = data.getSectionTable(); this.optHeader = data.getOptionalHeader(); this.file = data.getFile(); this.data = data; } /** * Loads the first section with the given name (given the order of the * section headers). If the file doesn't have a section by this name, it * returns absent. * <p> * This does not instantiate special sections. Use methods like * {@link #loadImportSection()} or {@link #loadResourceSection()} instead. * <p> * The file on disk is read to fetch the information. * * @param name * the section's name * @return {@link PESection} of the given name, absent if section isn't * contained in file * @throws IOException * if unable to read the file */ @Beta // TODO remove? This seems not necessary public Optional<PESection> maybeLoadSection(String name) throws IOException { Optional<SectionHeader> section = table.getSectionHeaderByName(name); if (section.isPresent()) { int sectionNr = section.get().getNumber(); return Optional.fromNullable(loadSection(sectionNr)); } return Optional.absent(); } /** * Loads the section that has the sectionNr. * <p> * This does not instantiate special sections. Use methods like * {@link #loadImportSection()} or {@link #loadResourceSection()} instead. * * @param sectionNr * the section's number * @return {@link PESection} instance of the given number * @throws IllegalArgumentException * if invalid sectionNr */ public PESection loadSection(int sectionNr) { Preconditions.checkArgument(sectionNr > 0 && sectionNr <= table.getNumberOfSections(), "invalid section number"); SectionHeader header = table.getSectionHeader(sectionNr); return loadSectionFrom(header); } /** * Loads the section that belongs to the header. * <p> * This does not instantiate special sections. Use methods like * {@link #loadImportSection()} or {@link #loadResourceSection()} instead. * * @param header * the section's header * @return {@link PESection} that belongs to the header */ public PESection loadSectionFrom(SectionHeader header) { long size = getReadSize(header); long offset = header.getAlignedPointerToRaw(); return new PESection(size, offset, header, file); } /** * Rounds up the value to the file alignment of the optional header. * * @param value * @return file aligned value */ private long fileAligned(long value) { long fileAlign = optHeader.getAdjustedFileAlignment(); long rest = value % fileAlign; long result = value; if (rest != 0) { result = value - rest + fileAlign; } if (!(optHeader.isLowAlignmentMode() || result % 512 == 0)) { logger.error("file aligning went wrong"); logger.error("rest: " + rest); logger.error("value: " + value); logger.error("filealign: " + fileAlign); logger.error("filealign % 512: " + (fileAlign % 512)); logger.error("result % 512: " + (result % 512)); logger.error("result: " + result); } assert optHeader.isLowAlignmentMode() || result % 512 == 0; assert result >= value; return result; } /** * Determines the the number of bytes that is read for the section. * <p> * This is the actual section size. * * @param header * header of the section * @return section size * @throws IllegalArgumentException * if header is null */ public long getReadSize(SectionHeader header) { Preconditions.checkNotNull(header); long pointerToRaw = header.get(POINTER_TO_RAW_DATA); long virtSize = header.get(VIRTUAL_SIZE); long sizeOfRaw = header.get(SIZE_OF_RAW_DATA); long alignedPointerToRaw = header.getAlignedPointerToRaw(); // see Peter Ferrie's answer in: // https://reverseengineering.stackexchange.com/questions/4324/reliable-algorithm-to-extract-overlay-of-a-pe long readSize = fileAligned(pointerToRaw + sizeOfRaw) - alignedPointerToRaw; readSize = Math.min(readSize, header.getAlignedSizeOfRaw()); // see https://code.google.com/p/corkami/wiki/PE#section_table: // "if bigger than virtual size, then virtual size is taken. " // and: // "a section can have a null VirtualSize: in this case, only the SizeOfRawData is taken into consideration. " if (virtSize != 0) { readSize = Math.min(readSize, header.getAlignedVirtualSize()); } readSize = fileSizeAdjusted(alignedPointerToRaw, readSize); // must not happen if (readSize < 0) { logger.error( "Invalid readsize: " + readSize + " for file " + file.getName() + " adjusting readsize to 0"); readSize = 0; } assert readSize >= 0; return readSize; } /** * Adjusts the readsize of a section to the size of the file. * * @param alignedPointerToRaw * the file offset of the start of the section * @param readSize * the determined readsize without file adjustments * @return adjusted readsize */ private long fileSizeAdjusted(long alignedPointerToRaw, long readSize) { // end of section outside the file --> cut at file.length() if (readSize + alignedPointerToRaw > file.length()) { readSize = file.length() - alignedPointerToRaw; } // start of section outside the file --> nothing is read if (alignedPointerToRaw > file.length()) { logger.info("invalid section: starts outside the file, readsize set to 0"); readSize = 0; } return readSize; } /** * Fetches the {@link SectionHeader} of the section the data directory entry * for the dataDirKey points into. * <p> * Returns absent if the data directory doesn't exist. * * @param dataDirKey * the data directory key * @return the section table entry the data directory entry of that key * points into or absent if there is no data dir entry for the key * available */ public Optional<SectionHeader> maybeGetSectionHeader(DataDirectoryKey dataDirKey) { Optional<DataDirEntry> dataDir = optHeader.maybeGetDataDirEntry(dataDirKey); if (dataDir.isPresent()) { return dataDir.get().maybeGetSectionTableEntry(table); } logger.warn("data dir entry " + dataDirKey + " doesn't exist"); return Optional.absent(); } /** * Returns the file offset of the data directory entry the given key belongs * to. * <p> * Returns absent if data directory doesn't exist. * * @param dataDirKey * the key of the data directory entry * @return file offset of the rva that is in the data directory entry with * the given key, absent if file offset can not be determined */ public Optional<Long> maybeGetFileOffsetFor(DataDirectoryKey dataDirKey) { Optional<DataDirEntry> dataDir = optHeader.maybeGetDataDirEntry(dataDirKey); if (dataDir.isPresent()) { long rva = dataDir.get().getVirtualAddress(); return Optional.of(getFileOffset(rva)); } return Optional.absent(); } /** * Returns the file offset for the given RVA. * <p> * A warning is issued if the file offset is outside the file, but the file * offset will be returned nevertheless. * * @param rva * the relative virtual address that shall be converted to a * plain file offset * @return file offset */ public long getFileOffset(long rva) { Optional<SectionHeader> section = maybeGetSectionHeaderByRVA(rva); // standard value if rva doesn't point into a section long fileOffset = rva; // rva is located within a section, so calculate offset if (section.isPresent()) { long virtualAddress = section.get().get(VIRTUAL_ADDRESS); long pointerToRawData = section.get().get(POINTER_TO_RAW_DATA); fileOffset = rva - (virtualAddress - pointerToRawData); } // file offset is not within file --> warning if (fileOffset > file.length() || fileOffset < 0) { logger.warn("invalid file offset: 0x" + Long.toHexString(fileOffset) + " for file: " + file.getName() + " with length 0x" + Long.toHexString(file.length())); } return fileOffset; } /** * Returns the section entry of the section table the rva is pointing into. * * @param rva * the relative virtual address * @return the {@link SectionHeader} of the section the rva is pointing into */ public Optional<SectionHeader> maybeGetSectionHeaderByRVA(long rva) { List<SectionHeader> headers = table.getSectionHeaders(); for (SectionHeader header : headers) { VirtualLocation vLoc = getVirtualSectionLocation(header); if (addressIsWithin(vLoc, rva)) { return Optional.of(header); } } return Optional.absent(); } /** * Returns the section entry of the section table the offset is pointing * into. * * @param fileOffset * the file offset * @return the {@link SectionHeader} of the section the offset is pointing * into */ public Optional<SectionHeader> maybeGetSectionHeaderByOffset(long fileOffset) { List<SectionHeader> headers = table.getSectionHeaders(); for (SectionHeader header : headers) { PhysicalLocation loc = getPhysicalSectionLocation(header); if (addressIsWithin(loc, fileOffset)) { return Optional.of(header); } } return Optional.absent(); } private PhysicalLocation getPhysicalSectionLocation(SectionHeader header) { long size = getReadSize(header); long address = header.getAlignedPointerToRaw(); return new PhysicalLocation(address, size); } private VirtualLocation getVirtualSectionLocation(SectionHeader header) { long vSize = header.getAlignedVirtualSize(); long vAddress = header.getAlignedVirtualAddress(); return new VirtualLocation(vAddress, vSize); } /** * Returns true if the address is within the given location * * @param location * @param address * a address that may point into the section * @return true iff address is within location */ private static boolean addressIsWithin(Location location, long address) { long endpoint = location.from() + location.size(); return address >= location.from() && address < endpoint; } /** * To get rid of code repetition in the maybeLoadXSection() methods, this * abtracts the behaviour of assembling the LoadInfo and calling the factory * methods of the special sections based on the data directory key. * <p> * This results in having to cast the return value to the appropriate type, * but correct types are well tested with unit tests. * * @param key * @return the special section optional that belongs to the key. Absent if * no special section for that key available or an error occurs. */ public Optional<? extends SpecialSection> maybeLoadSpecialSection(DataDirectoryKey key) { Optional<LoadInfo> maybeLoadInfo = maybeGetLoadInfo(key); try { if (maybeLoadInfo.isPresent()) { LoadInfo loadInfo = maybeLoadInfo.get(); SpecialSection section = null; switch (key) { case IMPORT_TABLE: section = ImportSection.newInstance(loadInfo); break; case EXCEPTION_TABLE: section = ExceptionSection.newInstance(loadInfo); break; case EXPORT_TABLE: section = ExportSection.newInstance(loadInfo); break; case DEBUG: section = DebugSection.newInstance(loadInfo); break; case RESOURCE_TABLE: section = ResourceSection.newInstance(loadInfo); break; case BASE_RELOCATION_TABLE: section = RelocationSection.newInstance(loadInfo); break; case DELAY_IMPORT_DESCRIPTOR: section = DelayLoadSection.newInstance(loadInfo); break; default: return Optional.absent(); } if (section.isEmpty()) { logger.warn("empty data directory: " + key); } return Optional.of(section); } } catch (FileFormatException e) { logger.warn(e); } return Optional.absent(); } /** * Shortens the loadXSection() methods by handling an empty Optional with an * IllegalStateException. * * @param optional * @param message * @return */ private SpecialSection getOrThrow(Optional<? extends SpecialSection> optional, String message) { if (optional.isPresent()) { return optional.get(); } throw new IllegalStateException(message); } /** * Loads all bytes and information of the debug section. * * The file on disk is read to fetch the information. * * @return {@link DebugSection} of the given file * @throws IOException * if unable to read the file * @throws IllegalStateException * if unable to load debug section */ public DelayLoadSection loadDelayLoadSection() throws IOException { Optional<DelayLoadSection> debug = maybeLoadDelayLoadSection(); return (DelayLoadSection) getOrThrow(debug, "unable to load delay-load import section"); } /** * Loads all bytes and information of the debug section. * * The file on disk is read to fetch the information. * * @return {@link DebugSection} of the given file, absent if file doesn't * have this section * @throws IOException * if unable to read the file */ @SuppressWarnings("unchecked") public Optional<DelayLoadSection> maybeLoadDelayLoadSection() throws IOException { return (Optional<DelayLoadSection>) maybeLoadSpecialSection(DataDirectoryKey.DELAY_IMPORT_DESCRIPTOR); } /** * Loads all bytes and information of the debug section. * * The file on disk is read to fetch the information. * * @return {@link DebugSection} of the given file * @throws IOException * if unable to read the file * @throws IllegalStateException * if unable to load debug section */ public RelocationSection loadRelocSection() throws IOException { Optional<RelocationSection> debug = maybeLoadRelocSection(); return (RelocationSection) getOrThrow(debug, "unable to load reloc section"); } /** * Loads all bytes and information of the debug section. * * The file on disk is read to fetch the information. * * @return {@link DebugSection} of the given file, absent if file doesn't * have this section * @throws IOException * if unable to read the file */ @SuppressWarnings("unchecked") public Optional<RelocationSection> maybeLoadRelocSection() throws IOException { return (Optional<RelocationSection>) maybeLoadSpecialSection(DataDirectoryKey.BASE_RELOCATION_TABLE); } /** * Loads all bytes and information of the debug section. * * The file on disk is read to fetch the information. * * @return {@link DebugSection} of the given file * @throws IOException * if unable to read the file * @throws IllegalStateException * if unable to load debug section */ public DebugSection loadDebugSection() throws IOException { Optional<DebugSection> debug = maybeLoadDebugSection(); return (DebugSection) getOrThrow(debug, "unable to load debug section"); } /** * Loads all bytes and information of the debug section. * * The file on disk is read to fetch the information. * * @return {@link DebugSection} of the given file, absent if file doesn't * have this section * @throws IOException * if unable to read the file */ @SuppressWarnings("unchecked") public Optional<DebugSection> maybeLoadDebugSection() throws IOException { return (Optional<DebugSection>) maybeLoadSpecialSection(DataDirectoryKey.DEBUG); } /** * Loads all bytes and information of the resource section. * * The file on disk is read to fetch the information. * * @return {@link ResourceSection} of the given file * @throws IOException * if unable to read the file * @throws IllegalStateException * if section can not be loaded */ public ResourceSection loadResourceSection() throws IOException { Optional<ResourceSection> rsrc = maybeLoadResourceSection(); return (ResourceSection) getOrThrow(rsrc, "unable to load resource section"); } /** * Loads all bytes and information of the resource section. * * The file on disk is read to fetch the information. * * @return {@link ResourceSection} of the given file, absent if file doesn't * have this section * @throws IOException * if unable to read the file */ @SuppressWarnings("unchecked") public Optional<ResourceSection> maybeLoadResourceSection() throws IOException { return (Optional<ResourceSection>) maybeLoadSpecialSection(DataDirectoryKey.RESOURCE_TABLE); } /** * Loads all bytes and information of the exception section. * * The file on disk is read to fetch the information. * * @return {@link ExceptionSection} of the given file * @throws IOException * if unable to read the file * @throws IllegalStateException * if section can not be loaded */ public ExceptionSection loadExceptionSection() throws IOException { Optional<ExceptionSection> pdata = maybeLoadExceptionSection(); return (ExceptionSection) getOrThrow(pdata, "unable to load exception section"); } /** * Loads all bytes and information of the resource section. * * The file on disk is read to fetch the information. * * @return {@link ResourceSection} of the given file, absent if file doesn't * have this section * @throws IOException * if unable to read the file */ @SuppressWarnings("unchecked") public Optional<ExceptionSection> maybeLoadExceptionSection() throws IOException { return (Optional<ExceptionSection>) maybeLoadSpecialSection(DataDirectoryKey.EXCEPTION_TABLE); } /** * Loads all bytes and information of the import section. The file on disk * is read to fetch the information. * * @return the import section * @throws IOException * if unable to read the file * @throws IllegalStateException * if unable to load section */ public ImportSection loadImportSection() throws IOException { Optional<ImportSection> idata = maybeLoadImportSection(); return (ImportSection) getOrThrow(idata, "unable to load import section"); } /** * Loads all bytes and information of the import section. The file on disk * is read to fetch the information. * * @return the import section, absent if section can not be loaded * @throws IOException * if unable to read the file */ @SuppressWarnings("unchecked") public Optional<ImportSection> maybeLoadImportSection() throws IOException { return (Optional<ImportSection>) maybeLoadSpecialSection(DataDirectoryKey.IMPORT_TABLE); } /** * Loads all bytes and information of the export section. The file on disk * is read to fetch the information. * * @return the export section * @throws IOException * if unable to read the file * @throws IllegalStateException * if unable to load section */ public ExportSection loadExportSection() throws IOException { Optional<ExportSection> edata = maybeLoadExportSection(); return (ExportSection) getOrThrow(edata, "unable to load export section"); } /** * Loads all bytes and information of the export section. The file on disk * is read to fetch the information. * * @return the export section, absent if file doesn't have an export section * @throws IOException * if unable to read the file */ @SuppressWarnings("unchecked") public Optional<ExportSection> maybeLoadExportSection() throws IOException { return (Optional<ExportSection>) maybeLoadSpecialSection(DataDirectoryKey.EXPORT_TABLE); } /** * Creates new instance of MemoryMappedPE or just returns it, if it is * already there. * * @return memory mapped PE */ private MemoryMappedPE getMemoryMappedPE() { if (!memoryMapped.isPresent()) { memoryMapped = Optional.of(MemoryMappedPE.newInstance(data, this)); } return memoryMapped.get(); } /** * Assembles the loadInfo object for the dataDirKey. * * @param dataDirKey * data directory key * @return loading information */ private Optional<LoadInfo> maybeGetLoadInfo(DataDirectoryKey dataDirKey) { Optional<DataDirEntry> dirEntry = optHeader.maybeGetDataDirEntry(dataDirKey); if (dirEntry.isPresent()) { long virtualAddress = dirEntry.get().getVirtualAddress(); Optional<Long> maybeOffset = maybeGetFileOffsetFor(dataDirKey); if (!maybeOffset.isPresent()) { logger.info("unable to get file offset for " + dataDirKey); } long offset = maybeOffset.or(-1L); return Optional.of(new LoadInfo(offset, virtualAddress, getMemoryMappedPE(), data, this)); } return Optional.absent(); } /** * Data object. Contains the load information for a certain data special * section. */ public static class LoadInfo { /** * The physical address to the start of the section */ public final long fileOffset; /** * The virtual address to the start of the section */ public final long va; /** * The header data */ public final PEData data; /** * The memory mapped PE instance */ public final MemoryMappedPE memoryMapped; /** * The section loader */ public final SectionLoader loader; /** * Creates a LoadInfo instance with all the loading information. * * @param fileOffset * @param va * @param memoryMapped * @param data * @param loader */ public LoadInfo(long fileOffset, long va, MemoryMappedPE memoryMapped, PEData data, SectionLoader loader) { this.fileOffset = fileOffset; this.va = va; this.memoryMapped = memoryMapped; this.data = data; this.loader = loader; } } /** * Returns whether the virtual address of the data directory entry is valid. * TODO remove * * @param dataDirKey * @return true iff virtual address is valid */ @Beta public boolean hasValidPointer(DataDirectoryKey dataDirKey) { DataDirEntry dataDir = optHeader.getDataDirectory().get(dataDirKey); long rva = dataDir.getVirtualAddress(); long offset = getFileOffset(rva); return offset <= file.length() && offset >= 0; } /** * Returns whether the section is valid. * <p> * A section is valid if the readsize is greater than 0 and the section * start is within the file. * * @see #getReadSize(SectionHeader) * @param header * the section's header * @return true iff section is valid */ @Beta public boolean isValidSection(SectionHeader header) { // sidenote: the readsize should never be > 0 if the section starts // outside the file // but we make sure that everything is alright return getReadSize(header) > 0 && header.getAlignedPointerToRaw() < file.length(); } // TODO more general method? Like contains RVA? /** * Returns whether the section contains the entry point of the file. * * @param header * the header of the section that may contain the entry point. * @return true if entry point is within the section, false otherwise */ @Beta public boolean containsEntryPoint(SectionHeader header) { long ep = data.getOptionalHeader().get(StandardFieldEntryKey.ADDR_OF_ENTRY_POINT); long vStart = header.getAlignedVirtualAddress(); long vSize = header.getAlignedVirtualSize(); if (vSize == 0) { vSize = header.getAlignedSizeOfRaw(); } long vEnd = vSize + vStart; return ep >= vStart && ep < vEnd; } }