GP-2770: Replacing PE and ELF exporters with generic Original File exporter. Changing the BinaryExporter's display name to 'Raw Bytes'.

This commit is contained in:
Ryan Kurtz 2022-11-29 00:32:19 -05:00
parent 45640e9bc6
commit 93bbe3b883
7 changed files with 340 additions and 287 deletions

View file

@ -22,15 +22,17 @@
<UL>
<LI><A href="#ascii">ASCII</A></LI>
<LI><A href="#binary">Binary</A></LI>
<LI><A href="#c_cpp">C/C++</A></LI>
<LI><A href="#gzf">Ghidra Zip File (.gzf)</A></LI>
<LI><A href="#html">HTML</A></LI>
<LI><A href="#intel_hex">Intel Hex</A></LI>
<LI><A href="#original_file">Original File</A></LI>
<LI><A href="#binary">Raw Bytes</A></LI>
<LI><A href="#xml">XML Export Format</A></LI>
</UL>
@ -238,17 +240,6 @@
</BLOCKQUOTE>
</BLOCKQUOTE>
<H3><A name="binary"><A name="Options_Binary"/></A>Binary</H3>
<BLOCKQUOTE>
<P>Creates a binary file containing only the bytes from each memory block in the program.
If the program was originally created using the <B>Binary Importer</B>, then this
exporter allows recreation of the original file.</P>
<P><IMG alt="" border="0" src="../../shared/note.png"> <I>Only initialized memory blocks
are included in the output file.</I></P>
</BLOCKQUOTE>
<H3><A name="c_cpp"/><A name="Options_C_C__""/>C/C++</H3>
<BLOCKQUOTE>
@ -284,22 +275,8 @@
</LI>
</UL>
</BLOCKQUOTE>
<H3><A name="elf"></A>ELF</H3>
<BLOCKQUOTE>
<P>Writes an ELF program that was imported with the ELF loader back to its original file
layout. Any file-backed bytes that were modified by the user in the program database will
be reflected in the new file.</P>
<P><IMG alt="" border="0" src="../../shared/note.png"> <I>Writing back a modified Memory
Map is not supported. </I></P>
<P><IMG alt="" border="0" src="../../shared/note.png"> <I>Relocation bytes are always
restored to their original values, even if the user modifies them.</I></P>
</BLOCKQUOTE>
<H3><A name="gzf"></A>Ghidra Zip File (.gzf)</H3>
<H3><A name="gzf"/>Ghidra Zip File (.gzf)</H3>
<BLOCKQUOTE>
<P>Creates a zip file from a program in your project. You may want to create a zip file
@ -321,7 +298,7 @@
the <A href="#ascii_options">ASCII Options</A>.</I></P>
</BLOCKQUOTE>
<H3><A name="intel_hex"><A name="Options_Intel_Hex"/></A>Intel Hex</H3>
<H3><A name="intel_hex"/><A name="Options_Intel_Hex"/>Intel Hex</H3>
<BLOCKQUOTE>
<P>The Intel Hex format, a printable file representing memory images, was originally
@ -345,18 +322,57 @@
</UL>
</BLOCKQUOTE>
<H3><A name="pe"></A>PE</H3>
<H3><A name="original_file"/><A name="Options_Original_File"/>Original File</H3>
<BLOCKQUOTE>
<P>Writes a PE program that was imported with the PE loader back to its original file
layout. Any file-backed bytes that were modified by the user in the program database will
be reflected in the new file.</P>
<P>Writes a program back to its original file layout. By default, any file-backed bytes
that were modified by the user in the program database will be reflected in the new file.
Optionally, the program can be written back to its unmodified file bytes, discarding all
user modifications.
</P>
<H4>Original File Options</H4>
<UL>
<LI><B>Export User Byte Modifications</B> - If checked, user byte modifications are
preserved in the exported file. If unchecked, no user byte modifications are preserved
and the exported file will exactly match the file that was originally imported.</LI>
</UL>
<UL>
<LI><B>Save Multiple File Sources To Directory</B> - If checked, the destination file
will be treated as a directory. Each file source from the program will be saved to this
newly created directory with names of the form &lt;directory&gt;.0, &lt;directory&gt;.1,
etc. If the program contains multiple file sources and this option is not checked, only
the primary (first) file source will saved to the specified destination file.</LI>
</UL>
<P><IMG alt="" border="0" src="../../shared/note.png"> <I>This exporter is only
operational when the program has at least one file-backed byte source. This will be
reflected in the <A href="help/topics/MemoryMapPlugin/Memory_Map.htm">Memory Map's</A>
Byte Source column, which entries that begin with <B>File:</B></I></P>
<P><IMG alt="" border="0" src="../../shared/note.png"> <I>Writing back a modified Memory
Map is not supported. </I></P>
<P><IMG alt="" border="0" src="../../shared/note.png"> <I>Relocation bytes are always
restored to their original values, even if the user modifies them.</I></P>
<P><IMG alt="" border="0" src="../../shared/warning.png"> <I>Programs written to disk with
this exporter may be runnable on your native platform. Use caution when exporting
potentially malicious programs.</I></P>
</BLOCKQUOTE>
<H3><A name="binary"/><A name="Options_Binary"/></A>Raw Bytes</H3>
<BLOCKQUOTE>
<P>Creates a binary file containing only the raw bytes from each memory block in the
program. If there are multiple memory blocks, their bytes will be concatenated in the
exported binary file. If the program was originally created using the <B>Binary
Importer</B> and there is only one memory block, then this exporter allows recreation of
the original file.</P>
<P><IMG alt="" border="0" src="../../shared/note.png"> <I>Only initialized memory blocks
are included in the output file.</I></P>
</BLOCKQUOTE>
<H3><A name="xml"/><A name="Options_XML"/>XML</H3>

View file

@ -116,7 +116,7 @@
block. If the bytes were originally imported from a file, then this will indicate which file
and the offset into that file. If the bytes are mapped to another region of memory, it will
provide the address for the mapping. Blocks may consist of regions that have different
sources. In that case, source information about the first several regions will be d
sources. In that case, source information about the first several regions will be
displayed.</P>
<P><I><B>Source -</B></I> Description of block origination.</P>

View file

@ -1,169 +0,0 @@
/* ###
* IP: GHIDRA
*
* 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 ghidra.app.util.exporter;
import java.io.*;
import java.nio.file.*;
import java.util.List;
import ghidra.app.util.DomainObjectService;
import ghidra.app.util.Option;
import ghidra.app.util.opinion.Loader;
import ghidra.framework.model.DomainObject;
import ghidra.program.database.mem.AddressSourceInfo;
import ghidra.program.database.mem.FileBytes;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.Memory;
import ghidra.program.model.mem.MemoryBlockSourceInfo;
import ghidra.program.model.reloc.Relocation;
import ghidra.util.Conv;
import ghidra.util.HelpLocation;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities;
/**
* An {@link Exporter} that can export programs imported with a particular {@link Loader}
*/
public abstract class AbstractLoaderExporter extends Exporter {
/**
* Creates a new {@link AbstractLoaderExporter}
*
* @param name The display name of this exporter
* @param help The {@link HelpLocation} for this exporter
*/
protected AbstractLoaderExporter(String name, HelpLocation help) {
super(name, "", help);
}
/**
* Checks to see if the given file format is supported by this exporter
*
* @param fileFormat The file format (loader name) of the program to export
* @return True if the given file format is supported by this exporter; otherwise, false
*/
protected abstract boolean supportsFileFormat(String fileFormat);
@Override
public boolean export(File file, DomainObject domainObj, AddressSetView addrSet,
TaskMonitor monitor) throws IOException, ExporterException {
if (!(domainObj instanceof Program)) {
log.appendMsg("Unsupported type: " + domainObj.getClass().getSimpleName());
return false;
}
Program program = (Program) domainObj;
Memory memory = program.getMemory();
String fileFormat = program.getExecutableFormat();
if (!supportsFileFormat(fileFormat)) {
log.appendMsg("Unsupported file format: " + fileFormat);
return false;
}
List<FileBytes> fileBytes = memory.getAllFileBytes();
if (fileBytes.isEmpty()) {
log.appendMsg("Exporting a program with no file source bytes is not supported");
return false;
}
if (fileBytes.size() > 1) {
log.appendMsg("Exporting a program with more than 1 file source is not supported");
return false;
}
// Write source program's file bytes to a temp file
File tempFile = File.createTempFile("ghidra_export_", null);
try (OutputStream out = new FileOutputStream(tempFile, false)) {
FileUtilities.copyStreamToStream(new FileBytesInputStream(fileBytes.get(0)), out,
monitor);
}
// Undo relocations in the temp file
// NOTE: not all relocations are file-backed, and some are only partially file-backed
try (RandomAccessFile fout = new RandomAccessFile(tempFile, "rw")) {
Iterable<Relocation> relocs = () -> program.getRelocationTable().getRelocations();
for (Relocation reloc : relocs) {
Address addr = reloc.getAddress();
AddressSourceInfo addrSourceInfo = memory.getAddressSourceInfo(addr);
if (addrSourceInfo == null) {
continue;
}
long offset = addrSourceInfo.getFileOffset();
if (offset >= 0) {
MemoryBlockSourceInfo memSourceInfo = addrSourceInfo.getMemoryBlockSourceInfo();
byte[] bytes = reloc.getBytes();
int len = Math.min(bytes.length,
(int) memSourceInfo.getMaxAddress().subtract(addr) + 1);
fout.seek(offset);
fout.write(bytes, 0, len);
}
}
}
catch (Exception e) {
if (!tempFile.delete()) {
log.appendMsg("Failed to delete malformed file: " + tempFile);
}
return false;
}
// Move temp file to desired output file
Path from = Paths.get(tempFile.toURI());
Path to = Paths.get(file.toURI());
Files.move(from, to, StandardCopyOption.REPLACE_EXISTING);
return true;
}
@Override
public List<Option> getOptions(DomainObjectService domainObjectService) {
return EMPTY_OPTIONS;
}
@Override
public void setOptions(List<Option> options) {
// No options
}
/**
* An {@link InputStream} that reads a {@link FileBytes} modified bytes
*/
private static class FileBytesInputStream extends InputStream {
private final FileBytes fileBytes;
private final long size;
private long pos;
/**
* Creates a new {@link InputStream} that can read over the modified bytes of the given
* {@link FileBytes} object
*
* @param fileBytes The {@link FileBytes} to use for the {@link InputStream}
*/
FileBytesInputStream(FileBytes fileBytes) {
this.fileBytes = fileBytes;
this.size = fileBytes.getSize();
this.pos = 0;
}
@Override
public int read() throws IOException {
return pos < size ? Conv.byteToInt(fileBytes.getModifiedByte(pos++)) : -1;
}
}
}

View file

@ -28,13 +28,12 @@ import ghidra.util.HelpLocation;
import ghidra.util.task.TaskMonitor;
/**
* An implementation of exporter that creates
* an Binary representation of the program.
* An {@link Exporter} that can export memory blocks as raw bytes
*/
public class BinaryExporter extends Exporter {
public BinaryExporter() {
super("Binary", "bin", new HelpLocation("ExporterPlugin", "binary"));
super("Raw Bytes", "bin", new HelpLocation("ExporterPlugin", "binary"));
}
@Override
@ -53,8 +52,6 @@ public class BinaryExporter extends Exporter {
addrSet = memory;
}
FileOutputStream fos = new FileOutputStream(file);
AddressSet set = new AddressSet(addrSet);
//skip blocks that are not initialized...
@ -65,7 +62,7 @@ public class BinaryExporter extends Exporter {
}
}
try {
try (FileOutputStream fos = new FileOutputStream(file)) {
AddressRangeIterator iter = set.getAddressRanges();
while (iter.hasNext()) {
AddressRange range = iter.next();
@ -77,9 +74,6 @@ public class BinaryExporter extends Exporter {
catch (MemoryAccessException e) {
throw new ExporterException(e);
}
finally {
fos.close();
}
return true;
}
@ -91,5 +85,6 @@ public class BinaryExporter extends Exporter {
@Override
public void setOptions(List<Option> options) {
// No options
}
}

View file

@ -1,37 +0,0 @@
/* ###
* IP: GHIDRA
*
* 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 ghidra.app.util.exporter;
import ghidra.app.util.opinion.ElfLoader;
import ghidra.util.HelpLocation;
/**
* An {@link Exporter} that can export programs imported with the {@link ElfLoader}
*/
public class ElfExporter extends AbstractLoaderExporter {
/**
* Create a new {@link ElfExporter}
*/
public ElfExporter() {
super("ELF", new HelpLocation("ExporterPlugin", "elf"));
}
@Override
protected boolean supportsFileFormat(String fileFormat) {
return ElfLoader.ELF_NAME.equals(fileFormat);
}
}

View file

@ -0,0 +1,285 @@
/* ###
* IP: GHIDRA
*
* 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 ghidra.app.util.exporter;
import java.io.*;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
import ghidra.app.util.*;
import ghidra.framework.model.DomainObject;
import ghidra.program.database.mem.AddressSourceInfo;
import ghidra.program.database.mem.FileBytes;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.Memory;
import ghidra.program.model.mem.MemoryBlockSourceInfo;
import ghidra.program.model.reloc.Relocation;
import ghidra.util.HelpLocation;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities;
/**
* An {@link Exporter} that can export {@link FileBytes the originally imported file}.
* <p>
* WARNING: Programs written to disk with this exporter may be runnable on your native platform.
* Use caution when exporting potentially malicious programs.
*/
public class OriginalFileExporter extends Exporter {
private static final String USER_MODS_OPTION_NAME = "Export User Byte Modifications";
private static final boolean USER_MODS_OPTION_DEFAULT = true;
private static final String CREATE_DIR_OPTION_NAME = "Save Multiple File Sources To Directory";
private static final boolean CREATE_DIR_OPTION_DEFAULT = false;
private List<Option> options;
/**
* Creates a new {@link OriginalFileExporter}
*/
public OriginalFileExporter() {
super("Original File", "", new HelpLocation("ExporterPlugin", "original_file"));
}
@Override
public boolean supportsPartialExport() {
return false;
}
@Override
public boolean export(File file, DomainObject domainObj, AddressSetView addrSet,
TaskMonitor monitor) throws IOException, ExporterException {
if (!(domainObj instanceof Program)) {
log.appendMsg("Unsupported type: " + domainObj.getClass().getSimpleName());
return false;
}
Program program = (Program) domainObj;
List<FileBytes> allFileBytes = program.getMemory().getAllFileBytes();
if (allFileBytes.isEmpty()) {
log.appendMsg("Exporting a program with no file source bytes is not supported");
return false;
}
// Unusual Code Alert!
// This exporter has an option to save multiple file sources to a newly created directory.
// If this happens, we treat the file parameter as the new directory to save to. The newly
// exported files will each get a filename based on this directory name. We don't want
// to use the original FileBytes file name, as it could be dangerous to save the original
// files to disk with their original filenames.
File dir = null;
if (shouldCreateDir()) {
dir = file;
if (!FileUtilities.mkdirs(dir)) {
log.appendMsg("Failed to create directory: " + dir);
return false;
}
}
else if (allFileBytes.size() > 1) {
log.appendMsg("WARNING: Program contains more than 1 file source.\n" +
"Only bytes from the primary (first) file source will be exported.\n" +
"Enable option to export all file sources to a directory if desired.");
}
boolean ret = true;
for (int i = 0; i < allFileBytes.size(); i++) {
FileBytes fileBytes = allFileBytes.get(i);
if (dir != null) {
file = new File(dir, dir.getName() + "." + i);
}
boolean success = shouldExportUserModifications()
? exportModifiedBytes(file, fileBytes, (Program) domainObj, monitor)
: exportUnmodifiedlBytes(file, fileBytes, monitor);
ret &= success;
if (dir != null) {
if (success) {
log.appendMsg("Exported " + fileBytes.getFilename() + " to " + file);
}
else {
log.appendMsg("Failed to export " + fileBytes.getFilename() + " to " + file);
}
}
else {
break;
}
}
return ret;
}
/**
* Exports the unmodified {@link FileBytes}
*
* @param file The file to export to
* @param fileBytes The {@link FileBytes} to export
* @param monitor The monitor
* @return True if the export succeeded; otherwise, false
* @throws IOException If there was IO-related error during the export
*/
private boolean exportUnmodifiedlBytes(File file, FileBytes fileBytes, TaskMonitor monitor)
throws IOException {
try (OutputStream out = new FileOutputStream(file, false)) {
FileUtilities.copyStreamToStream(new FileBytesInputStream(fileBytes, false), out,
monitor);
return true;
}
}
/**
* Exports the modified {@link FileBytes}
*
* @param file The file to export to
* @param fileBytes The {@link FileBytes} to export
* @param program The program to export
* @param monitor The monitor
* @return True if the export succeeded; otherwise, false
* @throws IOException If there was IO-related error during the export
*/
private boolean exportModifiedBytes(File file, FileBytes fileBytes, Program program,
TaskMonitor monitor) throws IOException {
// Write source program's file bytes to a temp file.
// This is done to ensure a random access write failure doesn't corrupt a file the user
// might be overwriting.
File tempFile = File.createTempFile("ghidra_export_", null);
try (OutputStream out = new FileOutputStream(tempFile, false)) {
FileUtilities.copyStreamToStream(new FileBytesInputStream(fileBytes, true), out,
monitor);
}
// Undo relocations in the temp file.
// NOTE: not all relocations are file-backed, and some are only partially file-backed.
try (RandomAccessFile fout = new RandomAccessFile(tempFile, "rw")) {
Iterable<Relocation> relocs = () -> program.getRelocationTable().getRelocations();
Memory memory = program.getMemory();
for (Relocation reloc : relocs) {
Address addr = reloc.getAddress();
AddressSourceInfo addrSourceInfo = memory.getAddressSourceInfo(addr);
if (addrSourceInfo == null) {
continue;
}
long offset = addrSourceInfo.getFileOffset();
if (offset >= 0) {
MemoryBlockSourceInfo memSourceInfo = addrSourceInfo.getMemoryBlockSourceInfo();
byte[] bytes = reloc.getBytes();
int len = Math.min(bytes.length,
(int) memSourceInfo.getMaxAddress().subtract(addr) + 1);
fout.seek(offset);
fout.write(bytes, 0, len);
}
}
}
catch (Exception e) {
if (!tempFile.delete()) {
log.appendMsg("Failed to delete malformed file: " + tempFile);
}
return false;
}
// Success...it is safe to move the temp file to desired output file
Path from = Paths.get(tempFile.toURI());
Path to = Paths.get(file.toURI());
Files.move(from, to, StandardCopyOption.REPLACE_EXISTING);
return true;
}
@Override
public List<Option> getOptions(DomainObjectService domainObjectService) {
if (options == null) {
options = new ArrayList<>();
options.add(new Option(USER_MODS_OPTION_NAME, USER_MODS_OPTION_DEFAULT));
if (domainObjectService.getDomainObject() instanceof Program program &&
program.getMemory().getAllFileBytes().size() > 1) {
options.add(new Option(CREATE_DIR_OPTION_NAME, CREATE_DIR_OPTION_DEFAULT));
}
}
return options;
}
@Override
public void setOptions(List<Option> opt) {
options = opt;
}
/**
* Checks to see if user byte modifications should be preserved during the export.
* <p>
* User byte modifications are any modified byte that does not appear in the relocation table
* (relocation table entries are assumed to only be populated by the loader).
*
* @return True if user byte modifications should be preserved during the export; otherwise,
* false
*/
private boolean shouldExportUserModifications() {
return OptionUtils.getOption(USER_MODS_OPTION_NAME, options, USER_MODS_OPTION_DEFAULT);
}
/**
* Checks to see if a directory should be created when there are multiple {@link FileBytes}
*
* @return True if a directory should be created when there are multiple {@link FileBytes}
*/
private boolean shouldCreateDir() {
return OptionUtils.getOption(CREATE_DIR_OPTION_NAME, options, CREATE_DIR_OPTION_DEFAULT);
}
/**
* An {@link InputStream} that reads a {@link FileBytes} modified or unmodified (original) bytes
*/
private static class FileBytesInputStream extends InputStream {
private final FileBytes fileBytes;
private final long size;
private long pos;
private boolean useModifiedBytes;
/**
* Creates a new {@link InputStream} that can read over the modified bytes of the given
* {@link FileBytes} object
*
* @param fileBytes The {@link FileBytes} to use for the {@link InputStream}
* @param useModifiedBytes True if modified bytes should be read; false for unmodified
* (original) bytes
*/
FileBytesInputStream(FileBytes fileBytes, boolean useModifiedBytes) {
this.fileBytes = fileBytes;
this.size = fileBytes.getSize();
this.pos = 0;
this.useModifiedBytes = useModifiedBytes;
}
@Override
public int read() throws IOException {
if (pos >= size) {
return -1;
}
byte b;
if (useModifiedBytes) {
b = fileBytes.getModifiedByte(pos);
}
else {
b = fileBytes.getOriginalByte(pos);
}
pos++;
return Byte.toUnsignedInt(b);
}
}
}

View file

@ -1,37 +0,0 @@
/* ###
* IP: GHIDRA
*
* 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 ghidra.app.util.exporter;
import ghidra.app.util.opinion.PeLoader;
import ghidra.util.HelpLocation;
/**
* An {@link Exporter} that can export programs imported with the {@link PeLoader}
*/
public class PeExporter extends AbstractLoaderExporter {
/**
* Creates a new {@link PeExporter}
*/
public PeExporter() {
super("PE", new HelpLocation("ExporterPlugin", "pe"));
}
@Override
protected boolean supportsFileFormat(String fileFormat) {
return PeLoader.PE_NAME.equals(fileFormat);
}
}