//Finds and creates statically allocated strings based on the Golang stringStruct //@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/find_static_strings.py) //@category Golang //@keybinding //@menupath //@toolbar import ghidra.app.script.GhidraScript; import ghidra.program.model.address.Address; import ghidra.program.model.data.DataType; import ghidra.program.model.data.IntegerDataType; import ghidra.program.model.data.PointerDataType; import ghidra.program.model.listing.Data; import ghidra.program.model.mem.MemoryAccessException; import ghidra.program.model.mem.MemoryBlock; public class GolangStaticStringRecovery 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 size of a pointer on X64 */ private static final int POINTER_SIZE_X64 = 8; /* * #x86 * * #LEA REG, [STRING_ADDRESS] * * #MOV [ESP + ..], REG * * #MOV [ESP + ..], STRING_SIZE */ @Override protected void run() throws Exception { // Declare and initialise the number of recovered static strings int stringCount = 0; /* * Store the imagebase's offset and the pointer size as they are reused multiple * times */ long imageBaseOffset = currentProgram.getImageBase().getOffset(); int pointerSize = currentProgram.getDefaultPointerSize(); // Iterate over all memory blocks for (MemoryBlock block : getMemoryBlocks()) { /* * If the block name is not .data or .rodata, it can be skipped, as static * strings are only present in the data sections. * * The __data and __rodata section names are used in Macho-O binaries. */ if (block.getName().equalsIgnoreCase(".data") == false && block.getName().equalsIgnoreCase(".rodata") == false && block.getName().equalsIgnoreCase("__data") == false && block.getName().equalsIgnoreCase("__rodata") == false) { continue; } // Get the start of the block Address blockStart = block.getStart(); // Get the end of the block Address blockEnd = block.getEnd(); /* * Run as long as the start is less than, or equal to the end address, thus * ensuring the whole block is iterated over */ while (blockStart.compareTo(blockEnd) <= 0) { // Check if the script's execution is cancelled if (monitor.isCancelled()) { // Return from the run function, thus exiting the script's execution early return; } // Declare the string address variable Address stringAddress; // Declare and initialises the variable Address stringAddressPointer = blockStart; // Get the length address Address lengthAddress = blockStart.add(pointerSize); // Increment the start of the block blockStart = blockStart.add(pointerSize); /* * The next segment of the code is within a try-catch structure. The reason for * this is simple: the static string recovery strategy does not work in all * cases. An exception is simply ignored, as the catch segment simply continues * to the next step. Since the start of the block is compared to the end of the * block, the whole section is iterated over, meaning that any error just moves * over to the next piece of memory within the block. */ try { // Declare the length variable long length; // Check if the pointer size matches a x64 pointer's size if (pointerSize == POINTER_SIZE_X64) { // Get the long value at the given address length = getLong(lengthAddress); } else { // Assume the binary is x86 // Get the integer value at the given address length = getInt(lengthAddress); } /* * To avoid false positives, strings which have no length, or are longer than * 100 characters, are considered incorrect and thus skipped */ if (length <= 0 || length > 100) { continue; } // Check if the pointer size matches a x64 pointer's size if (pointerSize == POINTER_SIZE_X64) { // Get the long at the given string address pointer stringAddress = currentProgram.getAddressFactory() .getAddress(Long.toHexString(getLong(stringAddressPointer))); } else {// Assume the binary is x86 // Get the integer at the given string address pointer stringAddress = currentProgram.getAddressFactory() .getAddress(Long.toHexString(getInt(stringAddressPointer))); } /* * If the address offset is less than the image base offset, the current attempt * is faulty and needs to be skipped */ if (stringAddress.getOffset() < imageBaseOffset) { continue; } /* * Check if the string is printable. If it is not, the current address needs to * be skipped */ if (isPrintable(stringAddress, length) == false) { continue; } // Create a pointer to the string createData(stringAddressPointer, PointerDataType.dataType); // Get the length Data data = getDataAt(lengthAddress); /* * If there is no data type defined at the given address, it needs to be created */ if (data == null) { data = createData(lengthAddress, IntegerDataType.dataType); } // Get the type of the data DataType dataType = data.getDataType(); // Get the name of the data type String dataTypeName = dataType.getName(); /* * If the data type is an undefined type of 4 or 8 bytes in size, it is to be * removed, as a new type is to be set */ if (dataTypeName.equalsIgnoreCase("undefined4") || dataTypeName.equalsIgnoreCase("undefined8")) { removeData(getDataAt(lengthAddress)); } // Create an integer at the given address createData(lengthAddress, IntegerDataType.dataType); /* * Create the ASCII string at the given address with the given length (cast to a * boxed long to use the intValue function) */ Data stringData = createAsciiString(stringAddress, ((Long) length).intValue()); // Get the string value as a string String string = (String) stringData.getValue(); // Optionally pPrint the location and the string value for the user log("0x" + Long.toHexString(stringAddress.getOffset()) + " : \"" + string + "\""); // Increment the number of recovered strings stringCount++; } catch (Exception e) { /* * Exceptions are bound to happen due to some of the what more crude approaches, * but they can simply be skipped */ continue; } } } // Inform the analyst of the number of recovered strings println("Total number of recovered static strings: " + stringCount); } /** * Checks if a string, starting at the given address with the given length, is * printable. Printable in this context means that the value of each byte of the * string is between 32 and 126 * * @param start the address of the start of the string * @param length the length of the string * @return true if the complete string is printable, false if not * @throws MemoryAccessException */ private boolean isPrintable(Address start, long length) throws MemoryAccessException { // Iterate over the complete string for (int i = 0; i < length; i++) { // Get the current byte byte b = getByte(start); // Check the byte's value if (b < 32 || b > 126) { // If any of the bytes has the wrong value, return false early return false; } // Increment the string's address by one start = start.add(1); } /* * If the early return isn't hit and the complete string has been iterated over, * it means the complete string is printable, thus true needs to be returned */ return true; } /** * 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); } } }