mirror of
https://github.com/advanced-threat-research/GhidraScripts
synced 2026-06-08 13:03:27 +00:00
862c38de5b
-MachO binary support (MacOS file type) -Golang 1.20 support -Fixed numerous to-do statements
1228 lines
42 KiB
Java
1228 lines
42 KiB
Java
//Recovers Golang types within the given binary
|
|
//@author Max 'Libra' Kersten of Trellix' Advanced Research Center, based on the work by padorka@cujoai (https://github.com/getCUJO/ThreatIntel/blob/master/Scripts/Ghidra/type_extract.py)
|
|
//@category Golang
|
|
//@keybinding
|
|
//@menupath
|
|
//@toolbar
|
|
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.nio.charset.Charset;
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
|
|
import ghidra.app.script.GhidraScript;
|
|
import ghidra.program.model.address.Address;
|
|
import ghidra.program.model.listing.Data;
|
|
import ghidra.program.model.mem.MemoryAccessException;
|
|
import ghidra.program.model.mem.MemoryBlock;
|
|
import ghidra.program.model.symbol.Reference;
|
|
|
|
public class GolangTypeRecovery extends GhidraScript {
|
|
|
|
/**
|
|
* A boolean which defines if logging should be enabled. When prioritising
|
|
* speed, one might not be interested in getting all messages, but rather only
|
|
* the concluding message, along with potential error messages. As such, this
|
|
* boolean specifies if more logging should be enabled or disabled.</br>
|
|
* </br>
|
|
* The default value of this field is <code>true</code>.
|
|
*/
|
|
private static final boolean ENABLE_LOGGING = true;
|
|
|
|
/**
|
|
* The numeric value for the function kind, as defined in the Golang language
|
|
* specification
|
|
*
|
|
* @see <a href="https://go.dev/src/reflect/type.go">The Golang source code for
|
|
* the Type class, see the constant list under <code>type Kind uint</code>
|
|
* for the complete list</a>
|
|
*/
|
|
private static final int KIND_FUNCTION = 0x13;
|
|
|
|
/**
|
|
* The numeric value for the interface kind, as defined in the Golang language
|
|
* specification
|
|
*
|
|
* @see <a href="https://go.dev/src/reflect/type.go">The Golang source code for
|
|
* the Type class, see the constant list under <code>type Kind uint</code>
|
|
* for the complete list</a>
|
|
*/
|
|
private static final int KIND_INTERFACE = 0x14;
|
|
|
|
/**
|
|
* The numeric value for the pointer kind, as defined in the Golang language
|
|
* specification
|
|
*
|
|
* @see <a href="https://go.dev/src/reflect/type.go">The Golang source code for
|
|
* the Type class, see the constant list under <code>type Kind uint</code>
|
|
* for the complete list</a>
|
|
*/
|
|
private static final int KIND_POINTER = 0x16;
|
|
|
|
/**
|
|
* The numeric value for the struct kind, as defined in the Golang language
|
|
* specification
|
|
*
|
|
* @see <a href="https://go.dev/src/reflect/type.go">The Golang source code for
|
|
* the Type class, see the constant list under <code>type Kind uint</code>
|
|
* for the complete list</a>
|
|
*/
|
|
private static final int KIND_STRUCT = 0x19;
|
|
|
|
/**
|
|
* The default PC Quantum size (minimal instruction size), used in x86, x86_64,
|
|
* and WASM
|
|
*
|
|
* @see <a href=
|
|
* "https://github.com/golang/gofrontend/blob/master/libgo/goarch.sh">Golang
|
|
* architecture documentation</a>
|
|
*/
|
|
private static final int INSTRUCTION_SIZE_ONE = 1;
|
|
|
|
/**
|
|
* The PC Quantum size (minimal instruction size), used in RISCV, RISCV x64,
|
|
* S390, S390X, SH, and SHbe
|
|
*
|
|
* @see <a href=
|
|
* "https://github.com/golang/gofrontend/blob/master/libgo/goarch.sh">Golang
|
|
* architecture documentation</a>
|
|
*/
|
|
private static final int INSTRUCTION_SIZE_TWO = 2;
|
|
|
|
/**
|
|
* The PC Quantum size (minimal instruction size), used in ALPHA, ARM, ARMbe,
|
|
* M68K, MIPS, MIPSle, MIPS64p32, MIPS64p32le, MIPS64, MIPS64le, NIOS2, PPC,
|
|
* PPC64, PPC64le, SPARC, and SPARC64
|
|
*
|
|
* @see <a href=
|
|
* "https://github.com/golang/gofrontend/blob/master/libgo/goarch.sh">Golang
|
|
* architecture documentation</a>
|
|
*/
|
|
private static final int INSTRUCTION_SIZE_FOUR = 4;
|
|
|
|
/**
|
|
* The size of a pointer on X86
|
|
*/
|
|
private static final int POINTER_SIZE_X86 = 4;
|
|
|
|
/**
|
|
* The size of a pointer on X64
|
|
*/
|
|
private static final int POINTER_SIZE_X64 = 8;
|
|
|
|
/**
|
|
* The human readable version strings that are to be used to determine the
|
|
* version of a Golang binary. These will be converted to byte strings during
|
|
* the script's runtime, and then stored in the {@link #versions} variable. The
|
|
* sole purpose of this additional step is to more easily update this script
|
|
* when fixing bugs and/or adding support for later Golang versions.
|
|
*/
|
|
private static final String[] HUMAN_READABLE_VERSIONS = { "go1.20", "go1.19", "go1.18", "go1.17", "go1.16",
|
|
"go1.15", "go1.14", "go1.13", "go1.12", "go1.11", "go1.10", "go1.9", "go1.8", "go1.7", "go1.6", "go1.5",
|
|
"go1.4", "go1.3", "go1.2" };
|
|
|
|
/**
|
|
* Byte strings of the versions in the {@link #HUMAN_READABLE_VERSIONS}
|
|
* variable. This variable is to be initialised during runtime.
|
|
*/
|
|
private String[] versions;
|
|
|
|
/**
|
|
* The Golang version of the analysed binary, stored globally for later use to
|
|
* avoid looking for the version multiple times. This value is one of the
|
|
* {@link #versions}, which is more easily read in its human readable format as
|
|
* defined within the {@link #HUMAN_READABLE_VERSIONS}.
|
|
*/
|
|
private String currentVersion;
|
|
|
|
/**
|
|
* The magic values for the pclntab, as found in the Golang source code. Note
|
|
* that version <code>1.2</code> is less than <code>1.16</code>, since the value
|
|
* after the dot is to be seen as an incrementing value number, not as the usual
|
|
* mathematical way of writing. The values in the array are for the following
|
|
* Golang versions, in order:
|
|
*
|
|
* <ol>
|
|
* <li>Version 1.2</li>
|
|
* <li>Version 1.16</li>
|
|
* <li>Version 1.18</li>
|
|
* <li>Version 1.20</li>
|
|
* </ol>
|
|
*
|
|
* @see <a href="https://go.dev/src/debug/gosym/pclntab.go">Golang pclntab
|
|
* source code</a>
|
|
*/
|
|
private static String[] pclntab_magic = { "\\xfb\\xff\\xff\\xff\\x00\\x00", "\\xfa\\xff\\xff\\xff\\x00\\x00",
|
|
"\\xf0\\xff\\xff\\xff\\x00\\x00", "\\xf1\\xff\\xff\\xff\\x00\\x00" };
|
|
|
|
/**
|
|
* The size of a pointer, in bytes, within the currently loaded binary
|
|
*/
|
|
private int binaryPointerSize;
|
|
|
|
/**
|
|
* A hash set of addresses with recovered types, used to avoid recovering the
|
|
* same type twice.<br>
|
|
* <br>
|
|
* The hash set is used, instead of an {@link java.util.ArrayList}, as its more
|
|
* efficient. Both the {@link java.util.HashSet#contains(Object)} and
|
|
* {@link java.util.HashSet#add(Object)} methods function in <code>O(1)</code>,
|
|
* in contrast to the inner workings of other types, which often work in
|
|
* <code>O(n)</code>.<br>
|
|
* <br>
|
|
* Testing locally shows that the time to complete the script, on a test sample
|
|
* which contains 3047 to-be recovered types, is nearly three times as much when
|
|
* using an {@link java.util.ArrayList}, compared to the
|
|
* {@link java.util.HashSet}. The specifics obviously depend on the used
|
|
* hardware, but it shows that the hash set implementation is more efficient.
|
|
*/
|
|
private static HashSet<Address> recoveredTypes = new HashSet<>();
|
|
|
|
/**
|
|
* The script's main function. This script can be executed normally and
|
|
* headless.
|
|
*/
|
|
@Override
|
|
protected void run() throws Exception {
|
|
// Initialise the byte string versions, based on the human readable ones
|
|
versions = createVersionByteStrings();
|
|
|
|
// Declares and initialises the executable format of the current program
|
|
String executableFormat = currentProgram.getExecutableFormat();
|
|
|
|
/*
|
|
* Initialises the global variable to store the pointer size of the currently
|
|
* analysed binary in
|
|
*/
|
|
binaryPointerSize = currentProgram.getDefaultPointerSize();
|
|
|
|
// Optionally print the executable format
|
|
log("Executable format: " + executableFormat);
|
|
|
|
/*
|
|
* Defines the address array which is to be initialised differently, depending
|
|
* on the file's type
|
|
*/
|
|
Address[] objects;
|
|
|
|
// If the file is of the PE format
|
|
if (executableFormat.equalsIgnoreCase("Portable Executable (PE)")) {
|
|
// Initialise the objects array
|
|
objects = scanPeFile();
|
|
} else if (executableFormat.equalsIgnoreCase("Executable and Linking Format (ELF)")) { // If the file is of the
|
|
// ELF format
|
|
// Initialise the objects array
|
|
objects = scanElfFile();
|
|
} else if (executableFormat.equalsIgnoreCase("Mac OS X Mach-O")) {
|
|
objects = scanMachoFile();
|
|
} else {
|
|
// Inform the analyst of the error
|
|
printerr("Incorrect file format: \"" + executableFormat + "\"");
|
|
// Exit the script
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* If the objects array is null, an error occurred earlier on, of which the
|
|
* analyst is already informed via an earlier printerr statement. As such, this
|
|
* function simply needs to return early to end the script's execution.
|
|
*/
|
|
if (objects == null) {
|
|
return;
|
|
}
|
|
|
|
// Find the Golang version within the given binary
|
|
currentVersion = findVersion();
|
|
|
|
// Check if a version was found
|
|
if (currentVersion == null) {
|
|
// Inform the analyst of the failure to find a version
|
|
printerr("No Golang version could be found in the binary!");
|
|
// Return early as the version is required later on
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Regardless if the file is of the PE or ELF format, the returned addresses are
|
|
* the same. As such, the array can be used in the same way. To easier read the
|
|
* function call, read it as:
|
|
*
|
|
* getAllTypes(typelinks, endOfTypelinks, type);
|
|
*/
|
|
recoverAllTypes(objects[0], objects[1], objects[2]);
|
|
|
|
// Print the number of recovered types for the analyst
|
|
println("Types recovered: " + recoveredTypes.size());
|
|
}
|
|
|
|
/**
|
|
* A wrapper function for the
|
|
* {@link ghidra.app.script.GhidraScript#println(String)} which is only called
|
|
* if the {@link #ENABLE_LOGGING} is <code>true</code>. The logging that is
|
|
* (potentially) passing through this function, is meant as optional logging.
|
|
* The final conclusion, as well as the logging of any error messages, should be
|
|
* printed via direct calls. The easy-to-omit nature of optional messages speeds
|
|
* up automated analysis by limiting the number of print calls.
|
|
*
|
|
* @param message
|
|
*/
|
|
private void log(String message) {
|
|
if (ENABLE_LOGGING) {
|
|
println(message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the value of the bytes as a little endian short
|
|
*
|
|
* @param bytes the value to read
|
|
* @return the value of the bytes as a little endian short
|
|
*/
|
|
private short getShort(byte[] bytes) {
|
|
// Allocate a buffer of two bytes
|
|
ByteBuffer buffer = ByteBuffer.allocate(2);
|
|
// Set the order
|
|
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
// Place the first byte
|
|
buffer.put(bytes[0]);
|
|
// Place the second byte
|
|
buffer.put(bytes[1]);
|
|
// Get the value as a short, in little endian format
|
|
return buffer.getShort(0);
|
|
}
|
|
|
|
/**
|
|
* Ingests the human readable versions and converts them into byte strings. The
|
|
* byte strings can be used to searched for using
|
|
* {@link #findBytes(Address, String)} and overloads thereof.<br>
|
|
* <br>
|
|
* An example of a byte string is <code>\xfb\xff\xff\xff\x00\x00</code>.
|
|
*
|
|
* @return the byte strings of the given versions
|
|
*/
|
|
private String[] createVersionByteStrings() {
|
|
// Declare and initialise the output variable
|
|
String[] byteStrings = new String[HUMAN_READABLE_VERSIONS.length];
|
|
|
|
// Loop over all human readable strings
|
|
for (int i = 0; i < HUMAN_READABLE_VERSIONS.length; i++) {
|
|
/*
|
|
* Convert each human readable string into a byte string, and store it in the
|
|
* output variable
|
|
*/
|
|
byteStrings[i] = createByteString(HUMAN_READABLE_VERSIONS[i]);
|
|
}
|
|
|
|
// Return the newly created byte strings
|
|
return byteStrings;
|
|
}
|
|
|
|
/**
|
|
* Returns a byte string of the given string, assuming the given input is UTF-8
|
|
* encoded
|
|
*
|
|
* @param input the UTF-8 encoded string
|
|
* @return a byte string of the given input
|
|
*/
|
|
private String createByteString(String input) {
|
|
// Get the bytes from the given input
|
|
byte[] bytes = input.getBytes(Charset.forName("UTF-8"));
|
|
// Declare the byte string and initialise it as an empty string
|
|
String byteString = "";
|
|
|
|
// Iterate over all bytes
|
|
for (byte currentByte : bytes) {
|
|
// Get the current byte in string form
|
|
String byteStringByte = Integer.toHexString(currentByte);
|
|
|
|
/*
|
|
* If the length of the current string is only a single character, prepend a
|
|
* zero to ensure two digits are present for the given byte in string form
|
|
*/
|
|
if (byteStringByte.length() == 1) {
|
|
byteStringByte = "0" + byteStringByte;
|
|
}
|
|
|
|
/*
|
|
* Prepend "\x" to byte string's current byte, which is then added to the
|
|
* complete byte string
|
|
*/
|
|
byteString += "\\x" + byteStringByte;
|
|
}
|
|
// Return the complete byte string
|
|
return byteString;
|
|
}
|
|
|
|
/**
|
|
* Gets the address at the given address, essentially dereferencing the given
|
|
* pointer
|
|
*
|
|
* @param address the address to dereference
|
|
* @return the dereferenced address
|
|
* @throws MemoryAccessException if the given address cannot be dereferenced
|
|
*/
|
|
private Address getAddressAt(Address address) throws MemoryAccessException {
|
|
return toAddr(Long.toHexString(getInt(address)));
|
|
}
|
|
|
|
/**
|
|
* Removes data at the given location, which is required to retype certain
|
|
* variables
|
|
*
|
|
* @param address the address to remove the type from
|
|
* @param length the length of the data to remove the typing from
|
|
* @throws Exception if the address is invalid or null
|
|
*/
|
|
private void removeData(Address address, int length) throws Exception {
|
|
// Iterate for the given length
|
|
for (int i = 0; i < length; i++) {
|
|
/*
|
|
* Remove the data at the given address plus the offset of the length, starting
|
|
* at 0
|
|
*/
|
|
removeDataAt(address.add(i));
|
|
}
|
|
// Return if no exception is thrown
|
|
}
|
|
|
|
/**
|
|
* Gets the sections that match (case insensitive) the given section name
|
|
*
|
|
* @param sectionName the section name to match, matching is case insensitive
|
|
* @return the starting addresses of all sections which match the given section
|
|
* name
|
|
*/
|
|
private Address[] getSection(String sectionName) {
|
|
// Iterate over all memory blocks
|
|
for (MemoryBlock block : getMemoryBlocks()) {
|
|
// If the block's name matches the given section name, case insensitive
|
|
if (block.getName().equalsIgnoreCase(sectionName)) {
|
|
// Get the start of the block
|
|
Address start = block.getStart();
|
|
// Get the end of the block
|
|
Address end = block.getEnd();
|
|
/*
|
|
* Optionally print a message for the analyst with the block's name, start
|
|
* address, and end address
|
|
*/
|
|
log(String.format("%s [start: 0x%x, end: 0x%x]", block.getName(), start.getOffset(), end.getOffset()));
|
|
// Return the start and end addresses
|
|
return new Address[] { start, end };
|
|
}
|
|
}
|
|
// Print a message for the analyst indicating the section couldn't be found
|
|
printerr("Section \"" + sectionName + "\" not found!");
|
|
// Return null in the case of an error
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the current version from any of the given versions, based on its
|
|
* presence in the binary. The first version that is found is used.
|
|
*
|
|
* @return the byte string version that is found
|
|
*/
|
|
private String findVersion() {
|
|
// Iterate over all versions in the global variable
|
|
for (int i = 0; i < versions.length; i++) {
|
|
// Declare a local variable for the currently iterated version
|
|
String version = versions[i];
|
|
|
|
// Find the address of the first occurrence of the current version
|
|
Address[] addresses = findBytes(null, version, 1000);
|
|
|
|
// Check if any results were found
|
|
if (addresses == null || addresses.length == 0) {
|
|
// If not, continue to the next attempt to find the version
|
|
continue;
|
|
}
|
|
/*
|
|
* If the address is not null, inform the analyst with the human readable
|
|
* version, for all results
|
|
*/
|
|
for (Address address : addresses) {
|
|
// Optionally print the Golang version
|
|
log("Golang version found at 0x" + Long.toHexString(address.getOffset()) + " : "
|
|
+ HUMAN_READABLE_VERSIONS[i]);
|
|
}
|
|
|
|
/*
|
|
* Return the current version based on the first match. This can be wrong, which
|
|
* is why all matches are printed to the analyst
|
|
*/
|
|
return version;
|
|
}
|
|
|
|
// Return null in the case of a failure to find the version
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Until and including Golang 1.16, the length offset was two bytes in size.
|
|
* From Golang 1.17 onwards, the length offset uses a variable length.<br>
|
|
* <br>
|
|
* For versions until and including 1.16, the second byte is used, which causes
|
|
* the script to potentially miss long strings.<br>
|
|
* <br>
|
|
* For versions 1.17 or later, the first byte is used, which causes the script
|
|
* to potentially miss long strings.
|
|
*
|
|
* @return the length offset
|
|
*/
|
|
private int getLengthOffset() {
|
|
/*
|
|
* Store the length of the current version byte string locally, which avoids
|
|
* calling the <code>length()<code> function several times
|
|
*/
|
|
int currentVersionLength = currentVersion.length();
|
|
/*
|
|
* Get the first digit of the version (which is the one but last byte in the
|
|
* byte string), where the "1." part is omitted as its not required
|
|
*/
|
|
String firstDigit = currentVersion.substring(currentVersionLength - 6, currentVersionLength - 4);
|
|
// Convert the value into a hexadecimal integer
|
|
int integer = Integer.parseInt(firstDigit, 16);
|
|
// Convert the value into a character
|
|
char c = (char) integer;
|
|
|
|
/*
|
|
* Declare and initialise the variable to store the version number in, in string
|
|
* format
|
|
*/
|
|
String versionNumberString = "" + c;
|
|
|
|
/*
|
|
* Get the last digit of the version (which is the one but last byte in the byte
|
|
* string), where the "1." part is omitted as its not required
|
|
*/
|
|
String lastDigit = currentVersion.substring(currentVersionLength - 2, currentVersionLength);
|
|
// Convert the value into a hexadecimal integer
|
|
integer = Integer.parseInt(lastDigit, 16);
|
|
// Convert the value into a character
|
|
c = (char) integer;
|
|
// Append the number
|
|
versionNumberString = versionNumberString + c;
|
|
|
|
// Convert the version number into an integer
|
|
int versionNumber = Integer.parseInt(versionNumberString);
|
|
|
|
// Compare the version to the versions of 1.17 or later
|
|
if (versionNumber >= 17) {
|
|
// Return one, as only one byte will be used
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* If none of the >= 1.17 versions matches, assume the version is lower than
|
|
* that, and thus the second byte is used
|
|
*/
|
|
return 2;
|
|
}
|
|
|
|
/**
|
|
* Looks for the pclntab within the binary, without looking for a section with
|
|
* the same name, but rather for a magic value within the binary
|
|
*
|
|
* @param pclntab_magic the byte strings of pclntab magic strings to look for
|
|
* @return Returns an object array where the pclntab address is at index 0, and
|
|
* the matching magic byte string is at index 1. The magic value can be
|
|
* null!
|
|
* @throws MemoryAccessException if the pclntab memory cannot be accessed
|
|
*/
|
|
private Object[] findPclntabPE() throws MemoryAccessException {
|
|
// Iterate over all plcntab magic byte strings
|
|
for (String magic : pclntab_magic) {
|
|
// Search through the program for the given byte string
|
|
Address[] results = findBytes(null, magic, 1000);
|
|
|
|
// Ensure the results are not null nor empty
|
|
if (results == null || results.length == 0) {
|
|
// If no matches are found, continue
|
|
continue;
|
|
}
|
|
|
|
// Iterate over all results
|
|
for (int i = 0; i < results.length; i++) {
|
|
// Get the currently iterated address
|
|
Address tempAddress = results[i];
|
|
|
|
// If the returned value is not found
|
|
if (tempAddress == null) {
|
|
// Move to the next result in the address array
|
|
continue;
|
|
}
|
|
// Check if the match is aligned with the pclntab structure
|
|
if (isPclntab(tempAddress)) {
|
|
// Optionally inform the analyst of the finding
|
|
log("pclntab found!");
|
|
// Return the pclntab's address and the respective magic value
|
|
return new Object[] { tempAddress, magic };
|
|
}
|
|
}
|
|
}
|
|
// If nothing can be found, inform the analyst and then return null
|
|
printerr("The pclntab could not be found!");
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks if the given address matches the pclntab structure pattern
|
|
*
|
|
* @param address the address to check
|
|
* @return true if the structure matches, false if not
|
|
* @throws MemoryAccessException if the memory at the given address (and a few
|
|
* bytes more) cannot be accessed
|
|
*/
|
|
private boolean isPclntab(Address address) throws MemoryAccessException {
|
|
// Get the PC Quantum Size value
|
|
byte pcQuantum = getByte(address.add(6));
|
|
// Get the pointer size from the binary
|
|
byte pclntabPointerSize = getByte(address.add(7));
|
|
// Verify the validity of the values
|
|
if ((pcQuantum != INSTRUCTION_SIZE_ONE && pcQuantum != INSTRUCTION_SIZE_TWO
|
|
&& pcQuantum != INSTRUCTION_SIZE_FOUR)
|
|
|| (pclntabPointerSize != POINTER_SIZE_X86 && pclntabPointerSize != POINTER_SIZE_X64)) {
|
|
// Return false if the conditions aren't met
|
|
return false;
|
|
}
|
|
// Return true if the conditions are met
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if the given address is the start of the module data
|
|
*
|
|
* @param address the address to check
|
|
* @param magic the magic value of the module data from the pclntab
|
|
* @return true if the given address is the address of the module data, false if
|
|
* not
|
|
* @throws MemoryAccessException if the memory at the given address, and offsets
|
|
* thereof, aren't accessible for any reason
|
|
*/
|
|
private boolean isModuleData(Address address, String magic) throws MemoryAccessException {
|
|
// Declare the offset
|
|
int offset;
|
|
|
|
/*
|
|
* Check if the magic value of Golang version 1.2 is in-use, as the offset
|
|
* differs in this case
|
|
*/
|
|
if (magic.equalsIgnoreCase("\\xfb\\xff\\xff\\xff\\x00\\x00")) { // Golang version 1.2 magic value
|
|
offset = 12;
|
|
} else {
|
|
offset = 22;
|
|
}
|
|
|
|
// Get the text address based on the given address and the offset
|
|
Address text = getAddressAt(address.add(offset * binaryPointerSize));
|
|
|
|
// Get the memory block that corresponds with the .text section
|
|
MemoryBlock textBlock = currentProgram.getMemory().getBlock(".text");
|
|
|
|
/*
|
|
* Verify the module data by ensuring the offset from the module data matches
|
|
* the offset of the .text block
|
|
*/
|
|
if (text != null && textBlock != null && text.getOffset() == textBlock.getStart().getOffset()) {
|
|
log("Module data found!");
|
|
return true;
|
|
}
|
|
|
|
// Return false if the offset does not match, or if there is no .text section
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Finds the address of the module data, based on the given pclntab address and
|
|
* the pclntab magic value
|
|
*
|
|
* @param pclntab the address of the pclntab
|
|
* @param magic the magic value of the pclntab
|
|
* @return the address of the module data
|
|
* @throws MemoryAccessException if the pclntab, or offsets thereof, aren't
|
|
* accessible for any reason
|
|
*/
|
|
private Address findModuleData(Address pclntab, String magic) throws MemoryAccessException {
|
|
// Declare the module data address
|
|
Address moduleData;
|
|
// Get all references to the pclntab
|
|
Reference[] pclntabReferences = getReferencesTo(pclntab);
|
|
|
|
// Iterate over all references
|
|
for (Reference reference : pclntabReferences) {
|
|
// Get the reference-making address
|
|
moduleData = reference.getFromAddress();
|
|
// Check if the address matches the module data structure
|
|
if (isModuleData(moduleData, magic)) {
|
|
// Optionally print the module data's address
|
|
log("Module data address: " + moduleData.toString(false, true));
|
|
// Return the address once a match is found
|
|
return moduleData;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If no such reference was found, a search through the whole program is to be
|
|
* started for the usage of the pclntab address, which is referenced in the
|
|
* module data. Searching only through specific sections is possible, but since
|
|
* this function is only called once in the beginning, and the improved accuracy
|
|
* outweighs the marginally extra time that is required
|
|
*/
|
|
|
|
/*
|
|
* Create a string of the inverted and hexadecimal representation of the pclntab
|
|
* address
|
|
*/
|
|
String invertedHexPclntab = Long.toHexString(Long.reverseBytes(pclntab.getOffset()));
|
|
|
|
/*
|
|
* If the binary size is four, the string needs to be 8 characters long (four
|
|
* times two characters, meaning four time two bytes).
|
|
*/
|
|
if (binaryPointerSize == 4) {
|
|
invertedHexPclntab = invertedHexPclntab.substring(0, 8);
|
|
}
|
|
/*
|
|
* If the pointer size is not four, its assumed to be eight, which is the
|
|
* default value of the long representation already, meaning no change has to be
|
|
* made
|
|
*/
|
|
String byteStringPclntab = "";
|
|
|
|
// Iterate over all characters, in sets of two per iteration
|
|
for (int i = 0; i < invertedHexPclntab.length(); i = i + 2) {
|
|
// Create the byte in string form, which is appended to the byte string variable
|
|
byteStringPclntab += "\\x" + invertedHexPclntab.charAt(i) + invertedHexPclntab.charAt(i + 1);
|
|
}
|
|
|
|
/*
|
|
* Get all results for the given byte string, starting at the program's
|
|
* beginning
|
|
*/
|
|
Address[] results = findBytes(null, byteStringPclntab, 1000);
|
|
|
|
// If the results are null or empty, return early
|
|
if (results != null && results.length > 0) {
|
|
// Iterate over all results
|
|
for (int i = 0; i < results.length; i++) {
|
|
// Temporarily store the current result in the module data variable
|
|
moduleData = results[i];
|
|
|
|
// Check if the current address is the start of the module data structure
|
|
if (isModuleData(moduleData, magic)) {
|
|
// Return the value if that is the case
|
|
return moduleData;
|
|
}
|
|
}
|
|
}
|
|
// Inform the analyst of the missing value
|
|
printerr("The module data cannot be found!");
|
|
// Return null if no match is found
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets the typeStart, typeEnd, typeLinks, and numberOfTypes variables based on
|
|
* the given module data address and the plcntab magic value.<br>
|
|
* <br>
|
|
* Returns an array of objects which contains the following types and values, in
|
|
* order:<br>
|
|
* <ol>
|
|
* <li>Address typeStart</li>
|
|
* <li>Address typeEnd</li>
|
|
* <li>Address typeLinks</li>
|
|
* <li>int numberOfTypes</li>
|
|
* </ol>
|
|
*
|
|
* @param moduleData the address of the module data
|
|
* @param magic the plcntab magic value
|
|
* @param pointerSize the size of the pointer in bytes
|
|
* @return in order Address type, Address etype, Address typelinks, int ntypes
|
|
* @throws MemoryAccessException if the value at the module data address, or
|
|
* offsets thereof, cannot be accessed for any
|
|
* reason
|
|
*/
|
|
private Object[] getTypeLinks(Address moduleData, String magic) throws MemoryAccessException {
|
|
// Declaration of the two offsets
|
|
int offset;
|
|
int offset2;
|
|
|
|
// The offsets differ in Golang 1.2
|
|
if (magic.equalsIgnoreCase("\\xfb\\xff\\xff\\xff\\x00\\x00")) { // Golang 1.2 magic value
|
|
offset = 25;
|
|
offset2 = 30;
|
|
} else if (magic.equalsIgnoreCase("\\xfa\\xff\\xff\\xff\\x00\\x00")
|
|
|| magic.equalsIgnoreCase("\\xf0\\xff\\xff\\xff\\x00\\x00")) { // Golang 1.16 and Golang 1.18
|
|
// respectively
|
|
offset = 35;
|
|
offset2 = 42;
|
|
} else if (magic.equalsIgnoreCase("\\xf1\\xff\\xff\\xff\\x00\\x00")) { // Golang 1.20
|
|
offset = 37;
|
|
offset2 = 44;
|
|
} else {
|
|
printerr("No pclntab magic value matched, thus making the offsets unknown!");
|
|
return null;
|
|
}
|
|
|
|
// Get the required values
|
|
Address typeStart = getAddressAt(moduleData.add(offset * binaryPointerSize));
|
|
Address typeEnd = getAddressAt(moduleData.add((offset + 1) * binaryPointerSize));
|
|
Address typeLinks = getAddressAt(moduleData.add(offset2 * binaryPointerSize));
|
|
int numberOfTypes = getInt(moduleData.add((offset2 + 1) * binaryPointerSize));
|
|
|
|
// Return the values in an object array
|
|
return new Object[] { typeStart, typeEnd, typeLinks, numberOfTypes };
|
|
}
|
|
|
|
/**
|
|
* Recover the types, based on the type's address and the type's location
|
|
*
|
|
* @param typeAddress
|
|
* @param type
|
|
* @return
|
|
* @throws Exception
|
|
*/
|
|
private Address recoverTypes(Address typeAddress, Address type) throws Exception {
|
|
try {
|
|
// Check if the type is already recovered
|
|
if (recoveredTypes.contains(typeAddress)) {
|
|
// If the type is already recovered, optionally notify the analyst
|
|
log(String.format("Type already recovered at 0x%x", typeAddress.getOffset()));
|
|
// Return the type's address
|
|
return typeAddress;
|
|
}
|
|
|
|
// Gets the length offset
|
|
int lengthOffset = getLengthOffset();
|
|
|
|
// Add the current type to the list of recovered types
|
|
recoveredTypes.add(typeAddress);
|
|
|
|
// Declare and initialise the temporary offset variable
|
|
int tempOffset = 2 * binaryPointerSize + 4;
|
|
// Declare and initialise the temporary address
|
|
Address tempAddress = typeAddress.add(tempOffset);
|
|
|
|
// Get the byte at the given address
|
|
byte b = getByte(tempAddress);
|
|
|
|
// Declare and initialise the uncommon flag
|
|
byte tflagUncommon = (byte) (b & 0x01);
|
|
|
|
// Get the byte for the extra star flag
|
|
b = getByte(typeAddress.add(2 * binaryPointerSize + 4));
|
|
|
|
// Declare and initialise the extra star far
|
|
byte tflagExtraStar = (byte) (b & 0x02);
|
|
|
|
// Get the byte for the kind
|
|
b = getByte(typeAddress.add(2 * binaryPointerSize + 7));
|
|
|
|
// Declare and initialise the kind
|
|
byte kind = (byte) (b & 0x1F);
|
|
|
|
// Get the type name offset
|
|
int nameOffset = getInt(typeAddress.add(4 * binaryPointerSize + 8));
|
|
|
|
// Get the type name length
|
|
byte nameLength = getByte(type.add(nameOffset + lengthOffset));
|
|
|
|
// Get the name's address
|
|
Address nameAddress = type.add(nameOffset + lengthOffset + 1);
|
|
|
|
// Remove the data type(s) from the given addresses
|
|
removeData(nameAddress, nameLength);
|
|
|
|
// Create an ASCII string at the given address with the given length
|
|
Data name = createAsciiString(nameAddress, nameLength);
|
|
|
|
// Declare the variable
|
|
String nameType;
|
|
|
|
// Get the type's name
|
|
if (tflagExtraStar == 1) {
|
|
nameType = ((String) name.getValue()).substring(1);
|
|
} else {
|
|
nameType = ((String) name.getValue());
|
|
}
|
|
|
|
// Optionally print the name of the type
|
|
log("Recovered: \"" + nameType + "\"");
|
|
|
|
// Create a label at the given address for the given name, excluding spaces
|
|
createLabel(typeAddress, nameType.replace(" ", "_"), true);
|
|
|
|
/*
|
|
* The function type (funcType) is structured as shown below. More information
|
|
* can be found here: https://go.dev/src/reflect/type.go
|
|
*
|
|
* struct {
|
|
*
|
|
* funcType uncommonType
|
|
*
|
|
* [2]*rtype // [0] is in, [1] is out
|
|
*
|
|
* }
|
|
*
|
|
* type funcType struct {
|
|
*
|
|
* rtype
|
|
*
|
|
* inCount uint16
|
|
*
|
|
* outCount uint16 //the top bit is set if it is the last input parameter
|
|
*
|
|
* }
|
|
*/
|
|
if (kind == KIND_FUNCTION) {
|
|
byte[] bytes = getBytes(typeAddress.add(4 * binaryPointerSize + 8 + 8), 2);
|
|
short inCount = getShort(bytes);
|
|
|
|
byte[] outBytes = getBytes(typeAddress.add(4 * binaryPointerSize + 8 + 8 + 2), 2);
|
|
// the top bit is set if it is the last input parameter
|
|
int lastInput = outBytes[1] & 0x80;
|
|
outBytes[1] = (byte) (outBytes[1] & 0x7F);
|
|
int outCount = getShort(outBytes);
|
|
|
|
List<String> inputs = new ArrayList<>();
|
|
List<String> outputs = new ArrayList<>();
|
|
|
|
for (int i = 0; i < inCount; i++) {
|
|
Address input = getAddressAt(typeAddress.add(4 * binaryPointerSize + 8 + 8 + binaryPointerSize
|
|
+ tflagUncommon * 16 + i * binaryPointerSize));
|
|
recoverTypes(input, type);
|
|
inputs.add(getSymbolAt(input).getName());
|
|
}
|
|
|
|
for (int i = 0; i < outCount; i++) {
|
|
Address output = getAddressAt(typeAddress.add(4 * binaryPointerSize + 8 + 8 + binaryPointerSize
|
|
+ tflagUncommon * 16 + inCount * binaryPointerSize + i * binaryPointerSize));
|
|
recoverTypes(output, type);
|
|
outputs.add(getSymbolAt(output).getName());
|
|
}
|
|
|
|
if (lastInput == 0x80 && inputs.size() > 0) {
|
|
String comment = "func(";
|
|
for (String string : inputs) {
|
|
comment += string + ", ";
|
|
}
|
|
comment = comment.substring(0, comment.length() - 2);
|
|
comment += ") (";
|
|
|
|
for (String string : outputs) {
|
|
comment += string + ", ";
|
|
}
|
|
comment = comment.substring(0, comment.length() - 2) + ")";
|
|
setPreComment(typeAddress, comment);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Interface type interfaceType represents an interface type.
|
|
*
|
|
* type interfaceType struct {
|
|
*
|
|
* rtype pkgPath name // import path methods
|
|
*
|
|
* []imethod // sorted by hash
|
|
*
|
|
* }
|
|
*
|
|
* method represents a method on an interface type
|
|
*
|
|
* type imethod struct {
|
|
*
|
|
* name nameOff // name of method
|
|
*
|
|
* typ typeOff // .(*FuncType) underneath
|
|
*
|
|
* }
|
|
*/
|
|
if (kind == KIND_INTERFACE) {
|
|
Address imethodField = getAddressAt(typeAddress.add(5 * binaryPointerSize + 8 + 8));
|
|
List<String> methods = new ArrayList<>();
|
|
int methodLength = getInt(typeAddress.add(6 * binaryPointerSize + 8 + 8));
|
|
|
|
for (int i = 0; i < methodLength; i++) {
|
|
int imethodNameOffset = getInt(imethodField);
|
|
nameLength = getByte(type.add(imethodNameOffset + lengthOffset));
|
|
nameAddress = type.add(imethodNameOffset + lengthOffset + 1);
|
|
removeData(nameAddress, nameLength);
|
|
name = createAsciiString(nameAddress, nameLength);
|
|
String temp = (String) name.getValue();
|
|
setEOLComment(imethodField, temp);
|
|
createLabel(imethodField, temp.replace(" ", "_"), true);
|
|
int newTypeOffset = getInt(imethodField.add(4));
|
|
Address newType = type.add(newTypeOffset);
|
|
recoverTypes(newType, type);
|
|
imethodField = imethodField.add(8);
|
|
methods.add(name.getValue() + " " + getSymbolAt(newType).getName());
|
|
String comment = "type " + nameType + " interface{" + "\n\t" + "\n\t";
|
|
for (int j = 0; j < methods.size(); j++) {
|
|
comment += methods.get(j) + "\n\t";
|
|
}
|
|
comment += "\n" + "}";
|
|
setPreComment(typeAddress, comment);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Pointer type is used to represent a pointer type
|
|
*
|
|
* type ptrType struct {
|
|
*
|
|
* rtype
|
|
*
|
|
* elem *rtype // pointer element (pointed at) type
|
|
*
|
|
*
|
|
* }
|
|
*/
|
|
if (kind == KIND_POINTER) {
|
|
Address newAddress = toAddr(
|
|
Integer.toHexString(getInt(typeAddress.add(4 * binaryPointerSize + 8 + 8))));
|
|
recoverTypes(newAddress, type);
|
|
}
|
|
|
|
/*
|
|
* Struct type structType represents a struct type.
|
|
*
|
|
* type structType struct {
|
|
*
|
|
* rtype
|
|
*
|
|
* pkgPath name
|
|
*
|
|
* fields []structField // sorted by offset
|
|
*
|
|
* }
|
|
*
|
|
* Struct field
|
|
*
|
|
* type structField struct {
|
|
*
|
|
* name name // name is always non-empty
|
|
*
|
|
* typ *rtype // type of field
|
|
*
|
|
* offsetEmbed uintptr // byte offset of field<<1 | isEmbedded
|
|
*
|
|
* }
|
|
*/
|
|
if (kind == KIND_STRUCT) {
|
|
Address structField = getAddressAt(typeAddress.add(5 * binaryPointerSize + 8 + 8));
|
|
List<String> fields = new ArrayList<>();
|
|
int fieldLength = getInt(typeAddress.add(6 * binaryPointerSize + 8 + 8));
|
|
for (int i = 0; i < fieldLength; i++) {
|
|
Address structFieldName = getAddressAt(structField);
|
|
Address nameLengthAddress = structFieldName.add(lengthOffset);
|
|
nameLength = getByte(nameLengthAddress);
|
|
nameAddress = getAddressAt(structField).add(lengthOffset + 1);
|
|
removeData(nameAddress, nameLength);
|
|
name = createAsciiString(nameAddress, nameLength);
|
|
String temp = (String) name.getValue();
|
|
setEOLComment(structField, temp);
|
|
createLabel(structFieldName, temp.replace(" ", "_"), true);
|
|
Address newType = getAddressAt(structField.add(binaryPointerSize));
|
|
structField = structField.add(3 * binaryPointerSize);
|
|
fields.add(name.getValue() + " " + getSymbolAt(newType).getName());
|
|
|
|
String comment = "type " + nameType + " struct{" + "\n\t" + "\n\t";
|
|
for (int j = 0; j < fields.size(); j++) {
|
|
comment += fields.get(j) + "\n\t";
|
|
}
|
|
comment += "\n" + "}";
|
|
|
|
setPreComment(typeAddress, comment);
|
|
}
|
|
}
|
|
} catch (Exception ex) {
|
|
// Ignore errors
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Fetches the typeLinksStart, typeLinksEnd, and typeStart addresses from the
|
|
* binary if it is a PE file.<br>
|
|
* <br>
|
|
* Returns an array of Address objects which contains the following values, in
|
|
* order:<br>
|
|
* <ol>
|
|
* <li>typeLinksStart</li>
|
|
* <li>typeLinksEnd</li>
|
|
* <li>typeStart</li>
|
|
* </ol>
|
|
*
|
|
* @param pclntab_magic the potential magic values for the pclntab
|
|
* @return the typeLinksStart, typeLinksEnd, and typeStart addresses
|
|
* @throws MemoryAccessException if memory within the binary, or offsets
|
|
* thereof, cannot be properly accessed
|
|
*/
|
|
private Address[] scanPeFile() throws MemoryAccessException {
|
|
// Searches for the pclntab in a PE file
|
|
Object[] pclntabArray = findPclntabPE();
|
|
|
|
/*
|
|
* If the pclntab cannot be found, the analyst is informed in the called
|
|
* function. Simply return null in order for the script to return back to the
|
|
* main function, which will return early if null is encountered.
|
|
*/
|
|
if (pclntabArray == null) {
|
|
return null;
|
|
}
|
|
|
|
// Saves the results in two variables
|
|
Address pclntab = (Address) pclntabArray[0];
|
|
String magic = (String) pclntabArray[1];
|
|
|
|
// Searches for the module data
|
|
Address moduleData = findModuleData(pclntab, magic);
|
|
|
|
/**
|
|
* Similar to the missing pclntab, if the module data cannot be found, the
|
|
* analyst is informed in the called function, and null is to be handed to the
|
|
* caller which ensures execution ends early.
|
|
*/
|
|
if (moduleData == null) {
|
|
return null;
|
|
}
|
|
|
|
// Save the results in four variables
|
|
Object[] typeLinksArray = getTypeLinks(moduleData, magic);
|
|
// If an error occurred earlier on, return early
|
|
if (typeLinksArray == null) {
|
|
return null;
|
|
}
|
|
Address typeStart = (Address) typeLinksArray[0];
|
|
// Address typeEnd = (Address) typeLinksArray[1];
|
|
Address typeLinksStart = (Address) typeLinksArray[2];
|
|
int numberOfTypes = (int) typeLinksArray[3];
|
|
|
|
// Calculate the end of the type links section
|
|
Address typeLinksEnd = typeLinksStart.add(numberOfTypes * 4);
|
|
|
|
// Return all addresses in an array
|
|
return new Address[] { typeLinksStart, typeLinksEnd, typeStart };
|
|
}
|
|
|
|
/**
|
|
* Fetches the typeLinksStart, typeLinksEnd, and typeStart addresses from the
|
|
* binary if it is an ELF file.<br>
|
|
* <br>
|
|
* Returns an array of Address objects which contains the following values, in
|
|
* order:<br>
|
|
* <ol>
|
|
* <li>typeLinksStart</li>
|
|
* <li>typeLinksEnd</li>
|
|
* <li>typeStart</li>
|
|
* </ol>
|
|
*
|
|
* @param pclntab_magic the potential magic values for the pclntab
|
|
* @return the typelinks start, the typelinks end (plus one byte), and type
|
|
* start address
|
|
*/
|
|
private Address[] scanElfFile() {
|
|
// Get the type link array
|
|
Address[] typeLinkArray = getSection(".typelink");
|
|
// Get the read-only data section
|
|
Address typeStart = getSection(".rodata")[0];
|
|
|
|
/*
|
|
* If either the type link or read-only sections cannot be found, the analyst is
|
|
* informed and null is returned by the respective function. To cut the
|
|
* execution short, this function simply returns null to its caller.
|
|
*/
|
|
if (typeLinkArray == null || typeStart == null) {
|
|
return null;
|
|
}
|
|
|
|
// Store the start and end in two variables
|
|
Address typelinksStart = typeLinkArray[0];
|
|
Address typeLinksEnd = typeLinkArray[1];
|
|
typeLinksEnd = typeLinksEnd.add(1);
|
|
|
|
// Return the values in an array
|
|
return new Address[] { typelinksStart, typeLinksEnd, typeStart };
|
|
}
|
|
|
|
/**
|
|
* Fetches the typeLinksStart, typeLinksEnd, and typeStart addresses from the
|
|
* binary if it is a MACH-O file.<br>
|
|
* <br>
|
|
* Returns an array of Address objects which contains the following values, in
|
|
* order:<br>
|
|
* <ol>
|
|
* <li>typeLinksStart</li>
|
|
* <li>typeLinksEnd</li>
|
|
* <li>typeStart</li>
|
|
* </ol>
|
|
*
|
|
* @param pclntab_magic the potential magic values for the pclntab
|
|
* @return the typelinks start, the typelinks end (plus one byte), and type
|
|
* start address
|
|
*/
|
|
private Address[] scanMachoFile() {
|
|
// Get the type link array
|
|
Address[] typeLinkArray = getSection("__typelink");
|
|
// Get the read-only data section
|
|
Address typeStart = getSection("__rodata")[0];
|
|
|
|
/*
|
|
* If either the type link or read-only sections cannot be found, the analyst is
|
|
* informed and null is returned by the respective function. To cut the
|
|
* execution short, this function simply returns null to its caller.
|
|
*/
|
|
if (typeLinkArray == null || typeStart == null) {
|
|
return null;
|
|
}
|
|
|
|
// Store the start and end in two variables
|
|
Address typelinksStart = typeLinkArray[0];
|
|
Address typeLinksEnd = typeLinkArray[1];
|
|
typeLinksEnd = typeLinksEnd.add(1);
|
|
|
|
// Return the values in an array
|
|
return new Address[] { typelinksStart, typeLinksEnd, typeStart };
|
|
}
|
|
|
|
/**
|
|
* Recovers all types present within the program
|
|
*
|
|
* @param typeLinksStart the start of the typelinks
|
|
* @param typeLinksEnd the end of the typelinks plus one byte
|
|
* @param type the start of the types
|
|
* @throws Exception if something goes wrong
|
|
*/
|
|
private void recoverAllTypes(Address typeLinksStart, Address typeLinksEnd, Address type) {
|
|
// Ensure the type link is not null
|
|
if (typeLinksStart != null) {
|
|
// Declare and initialise a copy of the start address
|
|
Address p = typeLinksStart;
|
|
// Iterate until the end of the type links section is reached
|
|
while (p.compareTo(typeLinksEnd) <= 0) {
|
|
// Check if the script's execution is cancelled
|
|
if (monitor.isCancelled()) {
|
|
// Break this loop, thus exiting the script's execution early
|
|
break;
|
|
}
|
|
|
|
try {
|
|
// Get the type offset by dereferencing the address pointer
|
|
int typeOffset = getInt(p);
|
|
// Get the type address based on the offset
|
|
Address typeAddress = type.add(typeOffset);
|
|
// Recover the types for the given address
|
|
recoverTypes(typeAddress, type);
|
|
} catch (Exception ex) {
|
|
// Ignore exceptions
|
|
}
|
|
// Increment the address, which is always 32 bits
|
|
p = p.add(4);
|
|
}
|
|
}
|
|
}
|
|
}
|