Files
ThisIsLibra f8eea138d7 Fixed two bugs in the Golang function recovery script
-Removal of spaces from function names now replaces all spaces, instead of only the first one
-Function creation now no longer faults when a part of the function name was already defined data within Ghidra's listing, as it could not create the complete function name string in Ghidra. It now clears all bytes of types, and then creates the ASCII string. This improvement works for all supported Golang versions on all supported operating systems and respective architectures
2024-09-19 11:11:57 +02:00

678 lines
24 KiB
Java

//Finds and creates functions with their original names, in Golang based PE and ELF files. Functions which have already been found by Ghidra will be renamed if a suitable name is found.
//@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/go_func.py)
//@category Golang
//@keybinding
//@menupath
//@toolbar
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressOutOfBoundsException;
import ghidra.program.model.listing.Data;
import ghidra.program.model.listing.Function;
import ghidra.program.model.mem.MemoryAccessException;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.model.symbol.SourceType;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.InvalidInputException;
public class GolangFunctionRecovery 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 mask to perform the logical AND with on the magic value
*/
private static final int MAGIC_MASK = 0xffffffff;
/**
* The magic value for Golang 1.20 and above
*
* @see <a href=
* "https://github.com/golang/go/blob/master/src/debug/gosym/pclntab.go">Golang
* pclntab source code</a>
*/
private static final int GO_120 = 0xfffffff1;
/**
* The magic value for Golang 1.18
*
* @see <a href=
* "https://github.com/golang/go/blob/master/src/debug/gosym/pclntab.go">Golang
* pclntab source code</a>
*/
private static final int GO_118 = 0xfffffff0;
/**
* The magic value for Golang 1.16 through version 1.17
*
* @see <a href=
* "https://github.com/golang/go/blob/master/src/debug/gosym/pclntab.go">Golang
* pclntab source code</a>
*/
private static final int GO_116 = 0xfffffffa;
/**
* The magic value for Golang 1.2 through version 1.15
*
* @see <a href=
* "https://github.com/golang/go/blob/master/src/debug/gosym/pclntab.go">Golang
* pclntab source code</a>
*/
private static final int GO_12 = 0xfffffffb;
/**
* 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 amount of functions which were recovered
*/
private static int functionCount = 0;
@Override
protected void run() throws Exception {
// Get the executable format of the sample
String executableFormat = currentProgram.getExecutableFormat();
// Declare the pclntab variable
Address pclntab;
// Check if the executable format is a PE file
if (executableFormat.equalsIgnoreCase("Portable Executable (PE)")) {
// Optionally print a message to state the file type which has been detected
log("PE file found");
// The declaration and initialisation of potential pclntab magic values
String[] pclntabMagicValues = { "\\xfb\\xff\\xff\\xff\\x00\\x00", "\\xfa\\xff\\xff\\xff\\x00\\x00",
"\\xf0\\xff\\xff\\xff\\x00\\x00", "\\xf1\\xff\\xff\\xff\\x00\\x00" };
// Get the gopclntab address by magic value
pclntab = getGopclntabByMagicValue(pclntabMagicValues);
} else if (executableFormat.equalsIgnoreCase("Executable and Linking Format (ELF)")) { // Check if the
// executable format is
// an ELF file
// Optionally print a message to state the file type which has been detected
log("ELF file found");
// Get the gopclntab address by section name
pclntab = getGopclntabBySectionName(".gopclntab");
} else if (executableFormat.equalsIgnoreCase("Mac OS X Mach-O")) {
// Optionally print a message to state the file type which has been detected
log("Mach-O file found");
// Get the gopclntab address by section name
pclntab = getGopclntabBySectionName("__gopclntab");
} else {
/*
* Print an error message informing the user of the failure to find a suitable
* executable format
*/
printerr("Unspported file format: " + executableFormat);
// Return, thus ending the script's execution
return;
}
// If the pclntab could not be found, the script ends
if (pclntab == null) {
// Print an error message with the reason of the failure
printerr("Cannot find the pclntab!");
// End the script's execution
return;
}
/*
* If execution continues, the pclntab was found. Optionally print a message to
* inform the user of the progress
*/
log(String.format("pclntab found at 0x%x!", pclntab.getOffset()));
// Declare and initialise the pclntab magic value
int magic = getInt(pclntab) & MAGIC_MASK;
// Recover function names for functions in Golang version 1.20 and above
if (magic == GO_120) {
println("Golang 1.20 found, note that this script is experimental for this Golang version!");
recoverFunctionNamesGo118Plus(pclntab);
} else if (magic == GO_118) {
// Recover function names for functions in Golang version 1.18 and above
recoverFunctionNamesGo118Plus(pclntab);
} else if (magic == GO_116) { // Determine if the magic value matches Golang version 1.16 and 1.17
// Recover function names for functions in Golang versions 1.16 and 1.17
renameFunc116(pclntab);
} else if (magic == GO_12) {// Determine if the magic value matches Golang 1.15 through version 1.2
/*
* Recover function names for functions in Golang version 1.15 through version
* 1.2
*/
recoverFunctionNamesGo12(pclntab);
} else {
// No matching magic value was found, of which the user is informed
println("Unable to determine the .gopclntab magic value, so the assumption is made that it is Go 1.2 compatible");
/*
* Recover function names for functions in Golang version 1.15 through version
* 1.2
*/
recoverFunctionNamesGo12(pclntab);
}
/*
* Inform the analyst of the total number of functions which has been renamed
* and/or created
*/
println("Total number of functions renamed and/or created: " + functionCount);
}
/**
* 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 gopclntab starting address based on a found magic value, if any
*
* @param pclntabMagicValues possible magic values as byte strings written as
* strings (i.e. "\xab\xbc")
* @return the starting address of the gopclntab if it is found, null if it is
* not found
* @throws MemoryAccessException
* @throws AddressOutOfBoundsException
*/
private Address getGopclntabByMagicValue(String[] pclntabMagicValues)
throws MemoryAccessException, AddressOutOfBoundsException {
// Iterate over all magic values
for (String magic : pclntabMagicValues) {
/*
* Look for the magic bytes within the current program, starting at the minimum
* address, with a maximum of 100 results
*/
Address[] pclntabs = findBytes(null, magic, 100);
// Iterate over all results
for (Address pclntab : pclntabs) {
log("Potential pclntab found at : " + Long.toHexString(pclntab.getOffset()));
/*
* Bytes have been found based on the given magic value
*/
// Get the instruction's size quantum
byte instructionSizeQuantum = getByte(pclntab.add(6));
// Get the pointer size
byte pointerSize = getByte(pclntab.add(7));
/*
* Verify if both the instruction's quantum size and pointer size match the
* conditions, meaning the pclntab has been found
*/
if ((instructionSizeQuantum != INSTRUCTION_SIZE_ONE && instructionSizeQuantum != INSTRUCTION_SIZE_TWO
&& instructionSizeQuantum != INSTRUCTION_SIZE_FOUR) == false
|| (pointerSize != POINTER_SIZE_X86 && pointerSize != POINTER_SIZE_X64) == false) {
return pclntab;
}
}
}
// If no results match the criteria, null is returned
return null;
}
/**
* Gets the gopclntab by its section name (being ".gopclntab")
*
* @return the starting address of the ".gopclntab" section
*/
private Address getGopclntabBySectionName(String sectionName) {
// Iterate over all memory blocks within the program
for (MemoryBlock memoryBlock : getMemoryBlocks()) {
// Check if the block's name equals (ignoring the casing) the gopclntab section
if (memoryBlock.getName().equalsIgnoreCase(sectionName)) {
// Return the starting address of this section if it is found
return memoryBlock.getStart();
}
}
// Return null if the section is not found
return null;
}
/**
* Gets the length of the ascii string, from the given address until the first
* null byte (0x00).
*
* @param start the address to start looking for the null byte (0x00)
* @return the number of characters, excluding the null byte (0x00)
* @throws MemoryAccessException
*/
private int getAsciiStringLength(Address start) throws MemoryAccessException {
/*
* Declare and initialise the length variable, which is to be returned from this
* function
*/
int length = 0;
/*
* Iterate until no address is available anymore, or until the while-loop is
* broken
*/
while (start != null) {
// Get the byte at the given address
byte b = getByte(start);
// Check if the byte's value is null
if (b == 0) {
// Break the loop if it is
break;
}
// If the byte isn't null, increment the length
length++;
// Additionally, increment the address by fetching the next
start = start.add(1);
// Jump to the top of the while-loop
}
// Return the length of the string, once the while-loop is broken
return length;
}
/**
* Creates a new function, or renames the function if it already exists, based
* on the newly found name, which is obtained via the name address variable
*
* @param functionAddress the address of the function
* @param nameAddress the address of the function's new name
* @throws DuplicateNameException
* @throws InvalidInputException
*/
private void createOrRenameFunction(Address functionAddress, Address nameAddress)
throws DuplicateNameException, InvalidInputException {
// Check if the variable is instantiated
if (nameAddress == null) {
// Return from the function if this is the case
return;
}
// Get the data at the given address
Data functionNameData = getDataAt(nameAddress);
// If no data resides at this address
if (functionNameData == null) {
try {
int length = getAsciiStringLength(nameAddress);
clearListing(nameAddress, nameAddress.add(length));
// Create an ASCII string within Ghidra
functionNameData = createAsciiString(nameAddress, length);
} catch (Exception e) {
// Print an error if the ASCII string creation fails
printerr(String.format("Unable to create an ASCII string at 0x%x!", nameAddress.getOffset()));
// Return from the function if this fails
return;
}
}
/*
* Get the function name by getting the data's value, which in this case is a
* String but needs to be cast as the getValue function returns an Object
*/
String functionName = (String) functionNameData.getValue();
// If the function name is null, blank, or empty
if (functionName == null || functionName.isBlank()) {
// Print an error
printerr(String.format("No function name found at 0x%s!", Long.toHexString(nameAddress.getOffset())));
// Return from the function
return;
}
// Gets the function at the given address
Function func = getFunctionAt(functionAddress);
// If there is a function at the given address
if (func != null) {
// Get the old name
String functionNameOld = func.getName();
// Get the new name, removing spaces
String functionNameNew = functionName.replaceAll(" ", "").replaceAll("Â", "").replaceAll("·", "");
/*
* Replacing the function with a name which already exists, throws a
* ghidra.util.exception.DuplicateNameException. This check avoids this
* exception by only replacing a function name if the old and new name are not
* the same.
*/
if (functionNameOld.equalsIgnoreCase(functionNameNew) == false) {
// Change the function name
try {
func.setName(functionNameNew, SourceType.USER_DEFINED);
} catch (DuplicateNameException ex) {
func.setName(functionNameNew + "_", SourceType.USER_DEFINED);
}
// Optionally print the renamed function
log("Function renamed from \"" + functionNameOld + "\" to \"" + functionNameNew + "\", located at 0x"
+ Long.toHexString(functionAddress.getOffset()));
// Increment the function count
functionCount++;
}
} else {
// If no function exists at the given address, create one
func = createFunction(functionAddress, functionName);
// Optionally print the function name and address
log("Function \"" + functionName + "\" created at 0x" + Long.toHexString(functionAddress.getOffset()));
// Increment the function count
functionCount++;
}
}
/**
* Recovers function names for functions in Golang version 1.15 through version
* 1.2
*
* @param pclntab the start address of the pclntab
* @throws MemoryAccessException
* @throws AddressOutOfBoundsException
* @throws DuplicateNameException
* @throws InvalidInputException
*/
private void recoverFunctionNamesGo12(Address pclntab)
throws MemoryAccessException, AddressOutOfBoundsException, DuplicateNameException, InvalidInputException {
// Get the pointer size
byte pointerSize = getByte(pclntab.add(7));
// Declare the number of functions tab variable
long nFunctionTab;
// If the pointer size fits a x64 system
if (pointerSize == POINTER_SIZE_X64) {
// Get a long value from the given address
nFunctionTab = getLong(pclntab.add(8));
} else { // Assume x86, meaning 4 bytes in size
// Get an integer value from the given address
nFunctionTab = getInt(pclntab.add(8));
}
// Get the function tab address
Address functionTab = pclntab.add(8 + pointerSize);
// Declare a copy of the function tab, named p
Address p = functionTab;
// Declare the function address variable
Address functionAddress;
// Declare the name offset variable
long nameOffset;
// Iterate over the number of functions
for (int i = 0; i < nFunctionTab; i++) {
// Check if the script's execution is cancelled
if (monitor.isCancelled()) {
// Break this loop, thus exiting the script's execution early
break;
}
// If the pointer size fits a x64 system
if (pointerSize == POINTER_SIZE_X64) {
// Get the function address
functionAddress = currentProgram.getAddressFactory().getAddress(Long.toHexString(getLong(p)).trim());
// Increment p with the pointer size to move it to the next usable address
p = p.add(pointerSize);
// Get the name offset as a long, since the architecture is x64
nameOffset = getLong(p);
} else { // Assume x86, meaning 4 bytes in size
// Get the function address
functionAddress = currentProgram.getAddressFactory().getAddress(Long.toHexString(getInt(p)));
// Increment p with the pointer size to move it to the next usable address
p = p.add(pointerSize);
// Get the name offset as an integer, since the architecture is x86
nameOffset = getInt(p);
}
// Increment p with the pointer size to move it to the next usable address
p = p.add(pointerSize);
/*
* Gets the name pointer, which is located directly after the function name,
* hence the addition of the name offset and the pointer size to move to the
* next usable address
*/
Address namePointer = pclntab.add(nameOffset + pointerSize);
// Get the address of the name, based on the pointer, which is always 32 bits in
// size
Address nameAddress = pclntab.add(getInt(namePointer));
// Address nameAddress = pclntab.add(namePointer.getOffset());
/*
* Create or rename the function at the address, with the name at the given
* address
*/
createOrRenameFunction(functionAddress, nameAddress);
}
}
/**
* Recovers function names for functions in Golang version 1.16 and version 1.17
*
* @param pclntab the start address of the pclntab
* @throws MemoryAccessException
* @throws AddressOutOfBoundsException
* @throws DuplicateNameException
* @throws InvalidInputException
*/
private void renameFunc116(Address pclntab)
throws MemoryAccessException, AddressOutOfBoundsException, DuplicateNameException, InvalidInputException {
// Get the size of the pointer
byte pointerSize = getByte(pclntab.add(7));
// Declare variables, whose value will depend on the architecture
long nFunctionTab;
long offset;
Address functionNameTab;
// If the pointer's size is equal to the size of a pointer on a x64 system
if (pointerSize == POINTER_SIZE_X64) {
// Get the corresponding long value
nFunctionTab = getLong(pclntab.add(8));
// Calculate the next offset
offset = getLong(pclntab.add(8 + 2 * pointerSize));
// Get the function name tab's address
functionNameTab = pclntab.add(offset);
// Calculate the next offset
offset = getLong(pclntab.add(8 + 6 * pointerSize));
} else { // Assume x86, meaning 4 bytes in size
// Get the corresponding integer value
nFunctionTab = getInt(pclntab.add(8));
// Calculate the next offset
offset = getInt(pclntab.add(8 + 2 * pointerSize));
// Get the function name tab's address
functionNameTab = pclntab.add(offset);
// Calculate the next offset
offset = getInt(pclntab.add(8 + 6 * pointerSize));
}
// Get the address of the function tab
Address functionTab = pclntab.add(offset);
// Declare and initiate a copy of the function tab
Address p = functionTab;
// Declare several variables for later use
Address functionAddress;
long functionDataOffset;
Address namePointer;
Address nameAddress;
// Iterate over the number of functions in the tab
for (int i = 0; i < nFunctionTab; i++) {
// Check if the script's execution is cancelled
if (monitor.isCancelled()) {
// Break this loop, thus exiting the script's execution early
break;
}
// If the pointer size is one of a x64 system
if (pointerSize == POINTER_SIZE_X64) {
// Get the function's address
functionAddress = currentProgram.getAddressFactory().getAddress(Long.toHexString(getLong(p)).trim());
// Adjust the offset
p = p.add(pointerSize);
// Get the function data's offset
functionDataOffset = getLong(p);
} else { // Assume x86, meaning 4 bytes in size
// Get the function's address
functionAddress = currentProgram.getAddressFactory().getAddress(Long.toHexString(getInt(p)).trim());
// Adjust the offset
p = p.add(pointerSize);
// Get the function data's offset
functionDataOffset = getInt(p);
}
// Move p to the next address
p = p.add(pointerSize);
// Get the function name pointer
namePointer = functionTab.add(functionDataOffset + pointerSize);
// Get the address of the function name, which is always 32 bits in size
nameAddress = functionNameTab.add(getInt(namePointer));
/*
* Create or rename the function at the address, with the name at the given
* address
*/
createOrRenameFunction(functionAddress, nameAddress);
}
}
/**
* Recovers function names for functions in Golang version 1.18 and above
*
* @param pclntab the start address of the pclntab
* @throws MemoryAccessException
* @throws AddressOutOfBoundsException
* @throws DuplicateNameException
* @throws InvalidInputException
*/
private void recoverFunctionNamesGo118Plus(Address pclntab)
throws MemoryAccessException, AddressOutOfBoundsException, DuplicateNameException, InvalidInputException {
// Get the pointer size
byte pointerSize = getByte(pclntab.add(7));
// Declare several variables
long nFunctionTab;
long textStart;
long offset;
Address functionNameTab;
// Check if the pointer size matches a x64 system's pointer size
if (pointerSize == POINTER_SIZE_X64) {
// Get the number of functions tab address
nFunctionTab = getLong(pclntab.add(8));
// Get the start of the text
textStart = getLong(pclntab.add(8 + 2 * pointerSize));
// Calculate the next offset
offset = getLong(pclntab.add(8 + 3 * pointerSize));
// Get the address of the function name tab
functionNameTab = pclntab.add(offset);
// Calculate the next offset
offset = getLong(pclntab.add(8 + 7 * pointerSize));
} else { // Assume x86, meaning 4 bytes in size
// Get the number of functions tab address
nFunctionTab = getInt(pclntab.add(8));
// Get the start of the text
textStart = getInt(pclntab.add(8 + 2 * pointerSize));
// Calculate the next offset
offset = getInt(pclntab.add(8 + 3 * pointerSize));
// Get the address of the function name tab
functionNameTab = pclntab.add(offset);
// Calculate the next offset
offset = getInt(pclntab.add(8 + 7 * pointerSize));
}
// Get the address of the function tab
Address functionTab = pclntab.add(offset);
// Instantiate a copy of the function tab for later use
Address p = functionTab;
// Define the field size within the function tab, which is always 4
int functabFieldSize = 4;
// Declare several variables for later use
Address functionAddress;
int functionDataOffset;
Address namePointer;
Address nameAddress;
// Iterate over all functions
for (int i = 0; i < nFunctionTab; i++) {
// Check if the script's execution is cancelled
if (monitor.isCancelled()) {
// Break this loop, thus exiting the script's execution early
break;
}
// Get the address for the current function, which is always 32 bits in size
functionAddress = currentProgram.getAddressFactory()
.getAddress(Long.toHexString(getInt(p) + textStart).trim());
// Adjust p
p = p.add(functabFieldSize);
// Get the function data offset
functionDataOffset = getInt(p);
// Adjust p
p = p.add(functabFieldSize);
// Get the pointer to the name
namePointer = functionTab.add(functionDataOffset + functabFieldSize);
// Get the pointer to the address, which is always 32 bits
nameAddress = functionNameTab.add(getInt(namePointer));
/*
* Create or rename the function at the address, with the name at the given
* address
*/
createOrRenameFunction(functionAddress, nameAddress);
}
}
}