{
	"id": "34ccd9dc-f25d-4a5b-bc73-222a6f5421e6",
	"created_at": "2026-04-10T03:20:48.438037Z",
	"updated_at": "2026-04-10T03:22:18.479054Z",
	"deleted_at": null,
	"sha1_hash": "35476a57e1d317f3a970507cf6c5d208d70fcfea",
	"title": "Ghidra script to handle stack strings – Max Kersten",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 198561,
	"plain_text": "Ghidra script to handle stack strings – Max Kersten\r\nArchived: 2026-04-10 02:54:41 UTC\r\nThis article was published on the 12th of April 2022.\r\nThe usage of stack strings within malware is frequent, especially when analysing shellcode or more advanced\r\nsamples. This article will provide insight into stack strings, the endianness of values, how to handle (encrypted)\r\nstack strings, and the step-by-step creation of a Ghidra script which handles (wide) stack strings, which may or\r\nmay not be encrypted. At last, the complete script is given.\r\nThe analysis in this article is done with a self-built version of Ghidra 10.2, based on the publicly available source\r\ncode of the first of February 2022. The samples have been analysed using all analysers.\r\nTable of contents\r\nStack strings explained\r\nCharacter sets\r\nEndianness\r\nStack string example: CaddyWiper\r\nStack string example: PlugX\r\nWriting the Ghidra script\r\nConclusion\r\nComplete script\r\nStack strings explained\r\nIn general, strings are not stored within the text segment of the binary. When a string is used in a local variable in\r\nsource code, the compiler generally places the string in a different segment. A pointer to the string’s location is\r\nused to access or alter the data.\r\nTo obfuscate strings, one can create a string on the stack, character for character, or in small groups of characters.\r\nThe concatenation of these characters lead to the existence of the complete string on the stack. When viewing the\r\nstrings of a binary, the characters might not show up.\r\nAs an example, GNU strings only shows human readable strings with a length of 4 or longer by default, meaning\r\nthat smaller groups do not show up, or might show up in parts. The third crack me in this course is based on this\r\ntechnique. Note that Mandiant’s FLOSS also aims to detect stack strings in binaries.\r\nAdditionally, stack strings can also be encrypted. This would make the use of tools harder, although FLOSS\r\nattempts to also decrypt stack strings if a simple encryption scheme is used.\r\nNote that the character set and size of the used encoding matters. To make the script more generic, the obtained\r\nstack string bytes will be printed out using multiple encodings. The wrong encoding is likely to be displayed as\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 1 of 18\n\ngarbage, making it fairly easy for an analyst to pick the string that looks correct.\r\nEndianness\r\nThe endianness defines where the most significant bit is, which is either the first or the last. The basic CPU\r\narchitecture article explains the endianness in more detail. With stack strings, it is important to understand if\r\nseveral characters need to be read right-to-left, or left-to-right.\r\nStack string example: CaddyWiper\r\nCaddyWiper is a wiper which was used by a pro-Russian actor against Ukrainian victims in March 2022, after the\r\nRussian invasion, as reported by ESET. It contains a lot of stack strings, including wide strings. The hashes of the\r\nsample are given below. The sample can be downloaded from Malware Bazaar and MalShare.\r\nMD-5: 42e52b8daf63e6e26c3aa91e7e971492\r\nSHA-1: 98b3fb74b3e8b3f9b05a82473551c5a77b576d54\r\nSHA-256: a294620543334a721a2ae8eaaf9680a0786f4b9a216d75b55cfd28f39e9430ea\r\nThe excerpt below shows a wide stack string, which can be seen due to the frequent occurrence of 0x00, and the\r\ndouble zero to terminate the string.\r\nMOV byte ptr [EBP + local_20],0x6b\r\nMOV byte ptr [EBP + local_1f],0x0\r\nMOV byte ptr [EBP + local_1e],0x65\r\nMOV byte ptr [EBP + local_1d],0x0\r\nMOV byte ptr [EBP + local_1c],0x72\r\nMOV byte ptr [EBP + local_1b],0x0\r\nMOV byte ptr [EBP + local_1a],0x6e\r\nMOV byte ptr [EBP + local_19],0x0\r\nMOV byte ptr [EBP + local_18],0x65\r\nMOV byte ptr [EBP + local_17],0x0\r\nMOV byte ptr [EBP + local_16],0x6c\r\nMOV byte ptr [EBP + local_15],0x0\r\nMOV byte ptr [EBP + local_14],0x33\r\nMOV byte ptr [EBP + local_13],0x0\r\nMOV byte ptr [EBP + local_12],0x32\r\nMOV byte ptr [EBP + local_11],0x0\r\nMOV byte ptr [EBP + local_10],0x2e\r\nMOV byte ptr [EBP + local_f],0x0\r\nMOV byte ptr [EBP + local_e],0x64\r\nMOV byte ptr [EBP + local_d],0x0\r\nMOV byte ptr [EBP + local_c],0x6c\r\nMOV byte ptr [EBP + local_b],0x0\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 2 of 18\n\nMOV byte ptr [EBP + local_a],0x6c\r\nMOV byte ptr [EBP + local_9],0x0\r\nMOV byte ptr [EBP + local_8],0x0\r\nMOV byte ptr [EBP + local_7],0x0\r\nIf one were to rename and retype the first variable, originally named local_20, to a 26 character long array named\r\ns_kernel32.dll, the string’s memory lay-out becomes apparent, as can be seen below.\r\nMOV byte ptr [EBP + s_kernel32.dll[0]],0x6b\r\nMOV byte ptr [EBP + s_kernel32.dll[1]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[2]],0x65\r\nMOV byte ptr [EBP + s_kernel32.dll[3]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[4]],0x72\r\nMOV byte ptr [EBP + s_kernel32.dll[5]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[6]],0x6e\r\nMOV byte ptr [EBP + s_kernel32.dll[7]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[8]],0x65\r\nMOV byte ptr [EBP + s_kernel32.dll[9]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[10]],0x6c\r\nMOV byte ptr [EBP + s_kernel32.dll[11]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[12]],0x33\r\nMOV byte ptr [EBP + s_kernel32.dll[13]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[14]],0x32\r\nMOV byte ptr [EBP + s_kernel32.dll[15]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[16]],0x2e\r\nMOV byte ptr [EBP + s_kernel32.dll[17]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[18]],0x64\r\nMOV byte ptr [EBP + s_kernel32.dll[19]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[20]],0x6c\r\nMOV byte ptr [EBP + s_kernel32.dll[21]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[22]],0x6c\r\nMOV byte ptr [EBP + s_kernel32.dll[23]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[24]],0x0\r\nMOV byte ptr [EBP + s_kernel32.dll[25]],0x0\r\nThe generic stack string handling suffices to deal with these kind of (wide) stack strings.\r\nStack string example: PlugX\r\nTalisman is a variant of PlugX, which is a remote access trojan which is generally used by Chinese related actors.\r\nThis backdoor contains encrypted stack strings, of which the hashes are given below. The samples can be\r\ndownloaded from Malware Bazaar and MalShare. Note that the referenced sample is dumped from memory, as it\r\nis initially encrypted. Details about the Talisman variant can be found in a corporate blog I co-authored.\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 3 of 18\n\nMD-5: c6c6162cca729c4da879879b126d27c0\r\nSHA-1: 80e5fd86127de526be75ef42ebc390fb0d559791\r\nSHA-256: 344fc6c3211e169593ab1345a5cfa9bcb46a4604fe61ab212c9316c0d72b0865\r\nOne of the encrypted stack strings within the sample can be found at the offset of 0x100002401. The assembly\r\ninstructions at the given offset is given below.\r\nMOV dword ptr [ESP + local_cc],0x55615596\r\nMOV dword ptr [ESP + local_c8],0x5573555e\r\nMOV dword ptr [ESP + local_c4],0x55615574\r\nMOV dword ptr [ESP + local_c0],0x55995571\r\nMOV dword ptr [ESP + local_bc],0x55615578\r\nMOV dword ptr [ESP + local_b8],0x55785582\r\nMOV dword ptr [ESP + local_b4],0x55775561\r\nMOV dword ptr [ESP + local_b0],0x5538553d\r\nMOV dword ptr [ESP + local_ac],0x551f552d\r\nMOV dword ptr [ESP + local_a8],0x558d552d\r\nMOV dword ptr [ESP + local_a4],0x5555553c\r\nMOV word ptr [ESP + local_a0],0x5555\r\nThe local variable named local_cc is the start of the stack string, where all of the encrypted characters are placed\r\non the stack in order. The code below, as seen in the decompiled code, decrypts the given stack string.\r\ndo {\r\n bVar4 = (*(char *)((int)\u0026local_cc + uVar2) + 0x22U ^ 0x33) + 0xbc;\r\n param_1 = param_1 \u0026 0xffffff00 | (uint)bVar4;\r\n *(byte *)((int)\u0026local_cc + uVar2) = bVar4;\r\n uVar2 = uVar2 + 1;\r\n} while (uVar2 \u003c 0x2e);\r\nThe decompiled code is a bit cluttered, but can easily be cleaned up. The variable named uVar2 is the loop’s\r\ncounter, which can be renamed to i. The line with the reference to param_1 can be omitted, and the local variable\r\nbVar4 can be renamed to decryptedByte. Upon doing so, the code is becomes much more readable, as can be seen\r\nbelow.\r\ndo {\r\n decryptedByte = (*(char *)((int)\u0026local_cc + i) + 0x22U ^ 0x33) + 0xbc;\r\n *(byte *)((int)\u0026local_cc + i) = decryptedByte;\r\n i = i + 1;\r\n} while (i \u003c 0x2e);\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 4 of 18\n\nThere is, however, a mistake in the decompiled code. The decryption routine seems straight forward: add 0x22,\r\nxor with 0x33, and add 0xbc. However, when looking at the assembly instructions, one can easily spot the\r\ndifference, as can be seen in the code below.\r\nADD param_1,0x22\r\nXOR param_1,0x33\r\nSUB param_1,0x44\r\nThe used variable names in the decompiler differ somewhat from the disassembly view, but the param_1 variable\r\nin the code above, is the current byte in the loop. The last instruction subtracts 0x44, rather than adding 0xbc.\r\nKnowing this, the decryption routine can be re-made, as can be seen below.\r\n/*\r\n * Decrypts the string based on the algorithm within the sample (SHA-256\r\n * 344fc6c3211e169593ab1345a5cfa9bcb46a4604fe61ab212c9316c0d72b0865, offset\r\n * 0x10002460)\r\n */\r\nprivate byte[] decrypt(List\u003cInteger\u003e input) {\r\n // Initialise the output variable\r\n byte[] output = new byte[input.size()];\r\n // Loop over the input and decrypt the given byte\r\n for (int i = 0; i \u003c input.size(); i++) {\r\n output[i] = (byte) (((input.get(i) + 0x22) ^ 0x33) - 0x44);\r\n }\r\n // Return the decrypted output\r\n return output;\r\n}\r\nWriting the Ghidra script\r\nThe script is based on Mich‘s Python based SimpleStackStrings Ghidra script. The script in this article is written\r\nin Java, as this is Ghidra’s native way of scripting.\r\nThis script will use the currently selected address in Ghidra as a variable, rather than requesting input from the\r\nuser to provide the address via one of the ask-related functions. The code for which is given below, where\r\ncurrentAddress is inherited from the GhidraScript class.\r\nNext, the script needs to iterate over all instructions that are part of the stack string. The Instruction object has a\r\nfunction which returns the instruction that is directly below it, which is getNext. This function returns null if there\r\nis no instruction. As such, a while-loop with the condition that the instruction is not null. The skeleton code is\r\ngiven below.\r\nwhile(instruction != null) {\r\n //TODO: implement the logic\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 5 of 18\n\ninstruction = instruction.getNext();\r\n}\r\nInstructions can contain a value, known as a scalar value. The value can be in the first or second part of the\r\ninstruction. The code below provides two examples.\r\nMOV dword ptr [ESP + local_var], 0x41414141\r\nPUSH 0x41414141\r\nIn the first example, the value of 0x41414141 is the second scalar value, which resides at index 1. In the second\r\nexample, there is no second scalar value, meaning the value resides at index 0.\r\nAs such, one can attempt to obtain the second scalar value, using the getScalar(int index) function within the\r\nInstruction class, of the current instruction. If there is no such value, null is returned. As such, one can always\r\nattempt to fetch the second scalar value.\r\nIf the returned value is null, an attempt needs to be made to fetch the first scalar value. If this also returns null, the\r\ninstruction is not part of the stack string, at least not in a format that this script supports. The code below\r\nresembles the logic that is described above.\r\nScalar scalar = instruction.getScalar(1);\r\n if (scalar == null) {\r\n scalar = instruction.getScalar(0);\r\n if (scalar == null) {\r\n println(\"Stack string ended, since no suitable scalar value could be found at 0x\"\r\n + Long.toHexString(instruction.getAddress().getOffset()) + \"!\");\r\n break;\r\n }\r\n }\r\nFollowing on that, the scalar’s value needs to be obtained in hexadecimal format. The value then needs to be\r\nconverted due to the endianness. The code is given below, where the convert function will be described\r\nafterwards.\r\nlong value = scalar.getValue();\r\nString valueString = Long.toHexString(value);\r\nvalueBytes.addAll(convert(valueString));\r\nThe convert function, which is given below, takes a String as argument, and returns a list of boxed integers. Due to\r\nthe endianness, the bytes need to be read from right-to-left. A byte is two characters from the string in size. To\r\nclarify, the value 0x41414141 consists of four bytes: 0x41 0x41 0x41 0x41. In this case, the string does not contain\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 6 of 18\n\nthe leading 0x. To split the string up in bytes that are equal in value to their character counterpart (the value they\r\nportray, not their char value).\r\nprivate List\u003cInteger\u003e convert(String input) {\r\n List\u003cInteger\u003e scalarValue = new ArrayList\u003c\u003e();\r\n if (input == null || input.isEmpty()) {\r\n return null;\r\n }\r\n int remainder = input.length() % 2;\r\n if (remainder != 0) {\r\n input = \"0\" + input;\r\n }\r\n for (int i = input.length(); i \u003e 0; i = i - 2) {\r\n int j = i - 2;\r\n if (i == 0 || j \u003c 0) {\r\n break;\r\n }\r\n String b = input.substring(j, i);\r\n Integer conversion = Integer.parseInt(b, 16);\r\n scalarValue.add(conversion);\r\n }\r\n return scalarValue;\r\n}\r\nIf the amount of characters is not even, a leading zero needs to be added to the byte which is missing it, which is\r\nthe least significant byte. The code responsible to do so is the within the if-body where the value of the remainder\r\nis checked.\r\nThe decryption function takes a list of integers as input, and returns an array of bytes. The code is given below.\r\nprivate byte[] decrypt(List\u003cInteger\u003e input) {\r\n byte[] output = new byte[input.size()];\r\n for (int i = 0; i \u003c input.size(); i++) {\r\n output[i] = (byte) (((input.get(i) + 0x22) ^ 0x33) - 0x44);\r\n }\r\n return output;\r\n}\r\nIn the generic script, which does not handle string decryption, the decrypt function signature is the same, but the\r\nbody is not. Instead, it simply converts the given list of integers into an unboxed byte array. This leaves the\r\ndecryption function as a template for future use-cases in the generic script. The code is given below.\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 7 of 18\n\nprivate byte[] decrypt(List\u003cInteger\u003e input) {\r\n byte[] output = new byte[input.size()];\r\n for (int i = 0; i \u003c input.size(); i++) {\r\n output[i] = input.get(i).byteValue();\r\n }\r\n return output;\r\n}\r\nAt last, the returned unboxed byte array is converted into multiple strings, all of which are based on a different\r\ncharacter set. Since the character set is unknown to the script itself, numerous types are included. The analyst can\r\nthen easily see which of the strings are garbage, and which is the correct string. The strings are then printed,\r\ntogether with some identifying information with regards to the character set. Additionally, the amount of bytes of\r\nthe stack string is printed. This helps when changing the data type into an array of (wide) characters, where one\r\nknows the number of characters that is required for the string.\r\nString usAscii = new String(output, StandardCharsets.US_ASCII);\r\nString isoLatin1 = new String(output, StandardCharsets.ISO_8859_1);\r\nString utf16be = new String(output, StandardCharsets.UTF_16BE);\r\nString utf16le = new String(output, StandardCharsets.UTF_16LE);\r\nString utf8 = new String(output, StandardCharsets.UTF_8);\r\nprintln(\"-------------------------------------------------------------------------------\");\r\nprintln(\"US-ASCII: \" + usAscii);\r\nprintln(\"ISO-LATIN-1: \" + isoLatin1);\r\nprintln(\"UTF-16BE: \" + utf16be);\r\nprintln(\"UTF-16LE: \" + utf16le);\r\nprintln(\"UTF-8: \" + utf8);\r\nprintln(\"Length in bytes: \" + output.length);\r\nprintln(\"-------------------------------------------------------------------------------\");\r\nDo note that if the stack string consists of multiple strings before an invalid instruction is hit, all of them are\r\nshown at once. The analyst can see which string is what size, and rename and retype the variables accordingly.\r\nThe following screenshots include such a case.\r\nWhen viewing this in Ghidra’s console, it is apparent that some spaces that should be there, are not properly\r\nshown in the console. Note that this might not be the case in future Ghidra versions.\r\nThe images below show the output of Ghidra’s console, for one stack string from both CaddyWiper and PlugX\r\nTalisman respectively\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 8 of 18\n\nThe script’s output in Ghidra’s console\r\nThe script’s decrypted output in Ghidra’s console\r\nWhen viewing the same stack strings in Eclipse’s console, the missing spaces are clearly visible, which is a clear\r\nindication that some of the stack strings are wide strings, whereas others are not.\r\nThe script’s output in Eclipse’s console\r\nThe script’s decrypted output in Eclipse’s console\r\nConclusion\r\nTo conclude, the usage of encrypted stack strings is an efficient way to evade more generic detection, but does not\r\nslow analysis down much. The automation to swiftly see what the string is by selecting the string’s start in the\r\nassembly view, whose decrypted value is then printed when the script is executed via a hotkey takes very little\r\ntime.\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 9 of 18\n\nRecreating the decryption routine might consume some time, but in most cases, encrypted stack strings are\r\ngenerally decrypted using the same routine within a single sample. As such, the time to recreate the decryption\r\nroutine is likely to end up saving time overall.\r\nComplete script\r\nThe complete script to handle encrypted (wide) stack strings is given below. The generic stack strings script is\r\ngiven first, after which the PlugX Talisman script is given.\r\n//A script to recreate (wide) stack strings in PlugX Talisman samples, based on Mich's SimpleStackSt\r\n//@author Max 'Libra' Kersten\r\n//@category deobfuscation\r\n//@keybinding\r\n//@menupath\r\n//@toolbar\r\nimport java.nio.charset.StandardCharsets;\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\nimport ghidra.app.script.GhidraScript;\r\nimport ghidra.program.model.listing.Instruction;\r\nimport ghidra.program.model.scalar.Scalar;\r\npublic class StackStrings extends GhidraScript {\r\n @Override\r\n protected void run() throws Exception {\r\n // Get the starting instruction\r\n Instruction instruction = getInstructionAt(currentAddress);\r\n // Check if there is an instruction at the current location\r\n if (instruction == null) {\r\n // Print the error message\r\n println(\"No instruction found at 0x\" + Long.toHexString(currentAddress.getOffset()));\r\n // Return, thus ending the execution of the script\r\n return;\r\n }\r\n // If an instruction is found, print the starting offset\r\n println(\"Stack string starting at 0x\" + Long.toHexString(currentAddress.getOffset()));\r\n // Initialise the list to store the stack string bytes in\r\n List\u003cInteger\u003e valueBytes = new ArrayList\u003c\u003e();\r\n /*\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 10 of 18\n\n* Loop over the current instructions (and the ones thereafter) until no\r\n * instruction is found, or the loop is broken\r\n */\r\n while (instruction != null) {\r\n /*\r\n * Get the second scalar value from the instruction. When encountering\r\n * instructions such as \"MOV dword ptr [ESP + local_var], 0x41414141\", the\r\n * second scalar value is the second value.\r\n */\r\n Scalar scalar = instruction.getScalar(1);\r\n // If the second scalar value is null, revert to the value of the first scalar\r\n if (scalar == null) {\r\n /*\r\n * Store the first scalar value, which is present instructions such as\r\n * \"PUSH 0x41414141\"\r\n */\r\n scalar = instruction.getScalar(0);\r\n /*\r\n * If there is no scalar value, the encountered instruction is not part of the\r\n * (wide) stack string\r\n */\r\n if (scalar == null) {\r\n /*\r\n * Print the error, along with the address of the instruction which contains no\r\n * suitable scalar value\r\n */\r\n println(\"Stack string ended, since no suitable scalar value could be found at 0x\r\n + Long.toHexString(instruction.getAddress().getOffset()) + \"!\");\r\n // Break the loop, continuing the decryption and string re-creation process\r\n break;\r\n }\r\n }\r\n // Get the scalar's value\r\n long value = scalar.getValue();\r\n // Get the value in hexadecimal format\r\n String valueString = Long.toHexString(value);\r\n // Add all the bytes to the list once they are converted\r\n valueBytes.addAll(convert(valueString));\r\n // Gets the next instruction\r\n instruction = instruction.getNext();\r\n }\r\n /*\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 11 of 18\n\n* Decrypt the collected bytes. If no decryption is required when re-using this\r\n * script, simply convert the list of bytes into an unboxed byte array, after\r\n * which the existing logic will handle the rest\r\n */\r\n byte[] output = decrypt(valueBytes);\r\n /*\r\n * Create a variety of strings, based on the given bytes. If multiple stack\r\n * strings are present in sequence, it is possible that the strings represent\r\n * more than a single stack string. This is left to the analyst's\r\n * interpretation.\r\n *\r\n * To make the script more generic, the strings are recreated using a variety of\r\n * character sets. Some stack strings might consist of wide strings, whereas\r\n * others might be of a more unique format. Generally, the common formats such\r\n * as UTF-8 and UTF-16 will be used, as the Windows API uses those.\r\n */\r\n String usAscii = new String(output, StandardCharsets.US_ASCII);\r\n String isoLatin1 = new String(output, StandardCharsets.ISO_8859_1);\r\n String utf16be = new String(output, StandardCharsets.UTF_16BE);\r\n String utf16le = new String(output, StandardCharsets.UTF_16LE);\r\n String utf8 = new String(output, StandardCharsets.UTF_8);\r\n // Print all of the stack strings\r\n println(\"-------------------------------------------------------------------------------\");\r\n println(\"US-ASCII: \" + usAscii);\r\n println(\"ISO-LATIN-1: \" + isoLatin1);\r\n println(\"UTF-16BE: \" + utf16be);\r\n println(\"UTF-16LE: \" + utf16le);\r\n println(\"UTF-8: \" + utf8);\r\n println(\"Length in bytes: \" + output.length);\r\n println(\"-------------------------------------------------------------------------------\");\r\n }\r\n /**\r\n * Converts the given input string to a list of bytes. The input string should\r\n * be equal to hexadecimal values, without the leading \"0x\".\r\n *\r\n * @param input a string which contains the hexadecimal values, without the\r\n * leading \"0x\"\r\n * @return a list of bytes, read as little endian (right from left, per byte)\r\n */\r\n private List\u003cInteger\u003e convert(String input) {\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 12 of 18\n\n// Initialise the list of bytes\r\n List\u003cInteger\u003e scalarValue = new ArrayList\u003c\u003e();\r\n // If the input is null, or empty, null is returned\r\n if (input == null || input.isEmpty()) {\r\n return null;\r\n }\r\n // Check if the length input is a multitude of two\r\n int remainder = input.length() % 2;\r\n /*\r\n * If the length of the string is not equal to one, add a leading zero to the\r\n * last character\r\n */\r\n if (remainder != 0) {\r\n int offset = input.length() - 1;\r\n String partA = input.substring(0, offset);\r\n String partB = input.substring(offset, offset + 1);\r\n input = partA + \"0\" + partB;\r\n }\r\n /*\r\n * Iterate over the string in groups of two characters at a time, which\r\n * represents a single byte every time. The string is iterated over starting in\r\n * the end, due to the endianness\r\n */\r\n for (int i = input.length(); i \u003e 0; i = i - 2) {\r\n int j = i - 2;\r\n // If i is zero or j is less than zero, the loop has to be broken\r\n if (i == 0 || j \u003c 0) {\r\n break;\r\n }\r\n // Obtain the byte in string form\r\n String b = input.substring(j, i);\r\n // Convert the byte in string form to a boxed byte object\r\n Integer conversion = Integer.parseInt(b, 16);\r\n // Add the byte to the list of bytes, in sequential order\r\n scalarValue.add(conversion);\r\n }\r\n // Return the obtained bytes\r\n return scalarValue;\r\n }\r\n /*\r\n * A function to hold the decryption routine for the given sample, if any\r\n */\r\n private byte[] decrypt(List\u003cInteger\u003e input) {\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 13 of 18\n\n// Initialise the output variable\r\n byte[] output = new byte[input.size()];\r\n // Loop over the input and convert the given byte\r\n for (int i = 0; i \u003c input.size(); i++) {\r\n output[i] = input.get(i).byteValue();\r\n }\r\n // Return the converted output\r\n return output;\r\n }\r\n}\r\nThe PlugX Talisman stack string decryption script is given below in full.\r\n//A script to recreate (wide) stack strings in PlugX' Talisman variant, based on Mich's SimpleStackSt\r\n//@author Max 'Libra' Kersten (@Libranalysis, https://maxkersten.nl)\r\n//@category deobfuscation\r\n//@keybinding\r\n//@menupath\r\n//@toolbar\r\nimport java.nio.charset.StandardCharsets;\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\nimport ghidra.app.script.GhidraScript;\r\nimport ghidra.program.model.listing.Instruction;\r\nimport ghidra.program.model.scalar.Scalar;\r\npublic class TalismanStackStringDecryption extends GhidraScript {\r\n @Override\r\n protected void run() throws Exception {\r\n // Get the starting instruction\r\n Instruction instruction = getInstructionAt(currentAddress);\r\n // Check if there is an instruction at the current location\r\n if (instruction == null) {\r\n // Print the error message\r\n println(\"No instruction found at 0x\" + Long.toHexString(currentAddress.getOffset()));\r\n // Return, thus ending the execution of the script\r\n return;\r\n }\r\n // If an instruction is found, print the starting offset\r\n println(\"Stack string starting at 0x\" + Long.toHexString(currentAddress.getOffset()));\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 14 of 18\n\n// Initialise the list to store the stack string bytes in\r\n List\u003cInteger\u003e valueBytes = new ArrayList\u003c\u003e();\r\n /*\r\n * Loop over the current instructions (and the ones thereafter) until no\r\n * instruction is found, or the loop is broken\r\n */\r\n while (instruction != null) {\r\n /*\r\n * Get the second scalar value from the instruction. When encountering\r\n * instructions such as \"MOV dword ptr [ESP + local_var], 0x41414141\", the\r\n * second scalar value is the second value.\r\n */\r\n Scalar scalar = instruction.getScalar(1);\r\n // If the second scalar value is null, revert to the value of the first scalar\r\n if (scalar == null) {\r\n /*\r\n * Store the first scalar value, which is present instructions such as\r\n * \"PUSH 0x41414141\"\r\n */\r\n scalar = instruction.getScalar(0);\r\n /*\r\n * If there is no scalar value, the encountered instruction is not part of the\r\n * (wide) stack string\r\n */\r\n if (scalar == null) {\r\n /*\r\n * Print the error, along with the address of the instruction which contains no\r\n * suitable scalar value\r\n */\r\n println(\"Stack string ended, since no suitable scalar value could be found at 0x\r\n + Long.toHexString(instruction.getAddress().getOffset()) + \"!\");\r\n // Break the loop, continuing the decryption and string re-creation process\r\n break;\r\n }\r\n }\r\n // Get the scalar's value\r\n long value = scalar.getValue();\r\n // Get the value in hexadecimal format\r\n String valueString = Long.toHexString(value);\r\n // Add all the bytes to the list once they are converted\r\n valueBytes.addAll(convert(valueString));\r\n // Gets the next instruction\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 15 of 18\n\ninstruction = instruction.getNext();\r\n }\r\n /*\r\n * Decrypt the collected bytes. If no decryption is required when re-using this\r\n * script, simply convert the list of bytes into an unboxed byte array, after\r\n * which the existing logic will handle the rest\r\n */\r\n byte[] output = decrypt(valueBytes);\r\n /*\r\n * Create a variety of strings, based on the given bytes. If multiple stack\r\n * strings are present in sequence, it is possible that the strings represent\r\n * more than a single stack string. This is left to the analyst's\r\n * interpretation.\r\n *\r\n * To make the script more generic, the strings are recreated using a variety of\r\n * character sets. Some stack strings might consist of wide strings, whereas\r\n * others might be of a more unique format. Generally, the common formats such\r\n * as UTF-8 and UTF-16 will be used, as the Windows API uses those.\r\n */\r\n String usAscii = new String(output, StandardCharsets.US_ASCII);\r\n String isoLatin1 = new String(output, StandardCharsets.ISO_8859_1);\r\n String utf16be = new String(output, StandardCharsets.UTF_16BE);\r\n String utf16le = new String(output, StandardCharsets.UTF_16LE);\r\n String utf8 = new String(output, StandardCharsets.UTF_8);\r\n // Print all of the stack strings\r\n println(\"-------------------------------------------------------------------------------\");\r\n println(\"US-ASCII: \" + usAscii);\r\n println(\"ISO-LATIN-1: \" + isoLatin1);\r\n println(\"UTF-16BE: \" + utf16be);\r\n println(\"UTF-16LE: \" + utf16le);\r\n println(\"UTF-8: \" + utf8);\r\n println(\"Length in bytes: \" + output.length);\r\n println(\"-------------------------------------------------------------------------------\");\r\n }\r\n /**\r\n * Converts the given input string to a list of bytes. The input string should\r\n * be equal to hexadecimal values, without the leading \"0x\".\r\n *\r\n * @param input a string which contains the hexadecimal values, without the\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 16 of 18\n\n* leading \"0x\"\r\n * @return a list of bytes, read as little endian (right from left, per byte)\r\n */\r\n private List\u003cInteger\u003e convert(String input) {\r\n // Initialise the list of bytes\r\n List\u003cInteger\u003e scalarValue = new ArrayList\u003c\u003e();\r\n // If the input is null, or empty, null is returned\r\n if (input == null || input.isEmpty()) {\r\n return null;\r\n }\r\n // Check if the length input is a multitude of two\r\n int remainder = input.length() % 2;\r\n /*\r\n * If the length of the string is not equal to one, add a leading zero to the\r\n * last character\r\n */\r\n if (remainder != 0) {\r\n input = \"0\" + input;\r\n }\r\n /*\r\n * Iterate over the string in groups of two characters at a time, which\r\n * represents a single byte every time. The string is iterated over starting in\r\n * the end, due to the endianness\r\n */\r\n for (int i = input.length(); i \u003e 0; i = i - 2) {\r\n int j = i - 2;\r\n // If i is zero or j is less than zero, the loop has to be broken\r\n if (i == 0 || j \u003c 0) {\r\n break;\r\n }\r\n // Obtain the byte in string form\r\n String b = input.substring(j, i);\r\n // Convert the byte in string form to a boxed integer object\r\n Integer conversion = Integer.parseInt(b, 16);\r\n // Add the byte to the list of bytes, in sequential order\r\n scalarValue.add(conversion);\r\n }\r\n // Return the obtained bytes\r\n return scalarValue;\r\n }\r\n /*\r\n * Decrypts the string based on the algorithm within the sample (SHA-256\r\n * 344fc6c3211e169593ab1345a5cfa9bcb46a4604fe61ab212c9316c0d72b0865, offset\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 17 of 18\n\n* 0x10002460)\r\n */\r\n private byte[] decrypt(List\u003cInteger\u003e input) {\r\n // Initialise the output variable\r\n byte[] output = new byte[input.size()];\r\n // Loop over the input and decrypt the given byte\r\n for (int i = 0; i \u003c input.size(); i++) {\r\n output[i] = (byte) (((input.get(i) + 0x22) ^ 0x33) - 0x44);\r\n }\r\n // Return the decrypted output\r\n return output;\r\n }\r\n}\r\nTo contact me, you can e-mail me at [info][at][maxkersten][dot][nl], or DM me on BlueSky @maxkersten.nl.\r\nSource: https://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nhttps://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/\r\nPage 18 of 18\n\nThe convert function, the endianness, the which is given bytes need to below, takes be read from a String as argument, right-to-left. A byte and returns is two characters a list of boxed from the string integers. in size. Due to To\nclarify, the value 0x41414141 consists of four bytes: 0x41 0x41 0x41 0x41. In this case, the string does not contain\n   Page 6 of 18",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://maxkersten.nl/binary-analysis-course/analysis-scripts/ghidra-script-to-handle-stack-strings/"
	],
	"report_names": [
		"ghidra-script-to-handle-stack-strings"
	],
	"threat_actors": [],
	"ts_created_at": 1775791248,
	"ts_updated_at": 1775791338,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/35476a57e1d317f3a970507cf6c5d208d70fcfea.pdf",
		"text": "https://archive.orkl.eu/35476a57e1d317f3a970507cf6c5d208d70fcfea.txt",
		"img": "https://archive.orkl.eu/35476a57e1d317f3a970507cf6c5d208d70fcfea.jpg"
	}
}