//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); } } }