//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.
*
* The default value of this field is true.
*/
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 Golang
* pclntab source code
*/
private static final int GO_120 = 0xfffffff1;
/**
* The magic value for Golang 1.18
*
* @see Golang
* pclntab source code
*/
private static final int GO_118 = 0xfffffff0;
/**
* The magic value for Golang 1.16 through version 1.17
*
* @see Golang
* pclntab source code
*/
private static final int GO_116 = 0xfffffffa;
/**
* The magic value for Golang 1.2 through version 1.15
*
* @see Golang
* pclntab source code
*/
private static final int GO_12 = 0xfffffffb;
/**
* The default PC Quantum size (minimal instruction size), used in x86, x86_64,
* and WASM
*
* @see Golang
* architecture documentation
*/
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 Golang
* architecture documentation
*/
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 Golang
* architecture documentation
*/
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 true. 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);
}
}
}