{
	"id": "26f972dd-05ac-4c6b-97c1-9e4650d5da18",
	"created_at": "2026-04-06T01:28:54.772048Z",
	"updated_at": "2026-04-10T03:20:48.815679Z",
	"deleted_at": null,
	"sha1_hash": "fd8e714f749d5dd8570d4a8260323255e90d981f",
	"title": "Use Ghidra to decrypt strings of KpotStealer malware – nullteilerfrei",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 196730,
	"plain_text": "Use Ghidra to decrypt strings of KpotStealer malware –\r\nnullteilerfrei\r\nBy born\r\nPublished: 2022-02-03 · Archived: 2026-04-06 00:13:53 UTC\r\nThis post will explain, how to identify a function responsible for string deobfuscation in a native-PE malware\r\nsample. We will use a KpotStealer sample as a concrete example. KpotStealer (aka Khalesi or just Kpot) is a\r\ncommodity malware family probably circulated in the shadowy parts of the internet since 2018. It got its name\r\nfrom a string publicly present on the Admin-Panel.\r\nAfter we found the function we will understand the data structure it uses and emulate the decryption of a string\r\nwith CyberChef and Binary Refinery. An interesting detail here is that Ghidra currently does not guess the\r\nfunction signature correctly.\r\nFinally, we will develop a Java script (hehe) for Ghidra to automatically deobfuscate all strings given the\r\ncorresponding obfuscation function.\r\nMotivation\r\nMalware authors use string obfuscation to avoid inclusion of \"interesting\" strings as an entry point for bottom up\r\nanalysis in the binary. Some time ago, I blagged about string obfuscation and how one might implement it. Feel\r\nfree to head over there for more details and context.\r\nThe intention behind using string obfuscation is, to make assessments like \"this looks like an IP, maybe it's the\r\nCommand \u0026 Control (C2) server\" or \" vssadmin.exe Delete Shadows looks as if the malware deletes shadow\r\ncopies\" impossible. It would also hinder an analyst to find a reference to a POST string, which may indicate the\r\nplace in the code where the networking is implemented. Obviously, an analyst wants to revert this process to be\r\nable to do exactly that. Especially if a malware family uses lots of strings or if one wants to analyze multiple\r\nsamples of the same family, this process should be as automated as possible.\r\nIdentifying the String Deobfuscation Function\r\nLet's first assume that there is only one function responsible for deobfuscating all strings. This is true for the\r\nKpotStealer sample we will be looking at and so it is for many other malware families. Often, malware authors do\r\nnot distinguish between strings requiring protection and generic strings and just apply string obfuscation to all\r\nstrings in their code. This has two interesting implications for us as reverse engineers:\r\nsince strings are generally quite important in software development, the string deobfuscation function is\r\ncalled from many different locations and probably also not far from the entry point of the executable.\r\nall locations, the string deobfuscation function is called from, belong to malware code and are not part of\r\nany static library. And we want to avoid reverse engineering static library code as a vampire wants to avoid\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 1 of 13\n\ngarlic.\r\nThe first of the two points above suggest that starting off from the entry point top-down-style and systematically\r\ngoing through all functions may be feasible. To further speed up the process, I use the following heuristics:\r\nSince strings are so common, the string deobfuscation function should be called from a lot of different\r\nplaces.\r\nThe string deobfuscation function should access at least one memory region (containing the obfuscated\r\nstring). This region may be represented by a global pointer reference from within the function or be passed\r\nto the function as an argument. If the first of the two is true, the string obfuscation method needs some sort\r\nof id to distinguish different strings within that global buffer.\r\nSimilarly, the function further needs access to a second buffer if it leverages cryptography to deobfuscate\r\nthe string. This key may be the same for all strings or may also vary on a per-string basis. If the second is\r\ntrue, the function may either receive different strings every time it is called or, again, some sort of id to\r\ndistinguish different strings.\r\nThe function needs some way to know, how large the obfuscated buffer is. Common ways of doing this in\r\nC are to use a terminating character (like \\0 ) or a parameter explicitly stating the length.\r\nDeobfuscated data needs to be returned from the function. An obvious way would be to return a newly\r\nallocated buffer. Another way is, to write to a pointer passed as an argument to the function.\r\nAt the call locations the deobfuscated data (somehow) returned from the function is often then used shortly\r\nafter.\r\nThe whole point of all these heuristics is to be fast. Deobfuscating all strings normally is a huge step forward in\r\nthe analysis of a malware and gives a jump start by enabling bottom-up analysis. On a different note, it sometimes\r\neven allows extraction of indicators of compromise (IoC) like IPs or domains, if that's your heart's desire.\r\nFinding Nemo\r\nThis and the following section will describe how one would find the function responsible for string deobfuscation\r\nin the KpotStealer sample with a SHA256 hash of\r\n67f8302a2fd28d15f62d6d20d748bfe350334e5353cbdef112bd1f8231b5599d\r\nWe will set a focus on the though processes itself and rational behind the decisions made during analysis, hence\r\nthis part is longer than necessary. Skip this and the following section if you are not interested in such\r\nfundamentals.\r\nGoing through all functions called in the entry point, the function at 0x004058fb sticks out because it is quite\r\nlarge and because it is setting a lot of global variables. It was only then, that I checked the number of import of the\r\nbinary and realized that there are almost none. This may mean that this sample uses some sort of dynamic API\r\nresolution and the function at 0x004058fb is a prime candidate for being responsible of doing that: it is called\r\nrelatively early during execution and sets a lot of global variables. Hence it is plausible (though not necessary),\r\nthat it needs to reference strings containing DLL names.\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 2 of 13\n\nStarting at 0x00405912 , the function at 0x0040c8f5 is called multiple times. This function is also called at 69\r\nother spots in the binary, which is a good tell that this may be the string deobfuscation method (you can see this by\r\npressing X if you have the ghIDA key bindings for Ghidra configured). The weird thing is though, that Ghidra\r\nonly shows\r\nFUN_0040c8f5();\r\nFUN_0040c8f5();\r\nFUN_0040c8f5();\r\nFUN_0040c8f5();\r\nFUN_0040c8f5();\r\nFUN_0040c8f5();\r\n...\r\nin the decompile view. It is pretty weird that there should be multiple calls to the same function without any\r\narguments and without somehow using the return value. And as it will turn out, Ghidra needs some help here to\r\neffectively decompile this part.\r\nBecome the Mother of Dragons\r\nAs much as we try to avoid looking at assembly, we have to take a look at it now. Good news though: you only\r\nneed to know two and a half assembly instructions to understand, what is going on here: CALL , MOV and, LEA .\r\nLet's first understand what these instructions to in general: CALL branches off execution to a function. This is\r\ndone by pushing the address immediately after the CALL instruction onto the stack and then set EIP to the\r\naddress of the function to be called - but we don't need to care about this level of detail here. The other one and a\r\nhalf assembly instructions MOV and LEA have different intended use-cases. But in principle, they both just move\r\ndata around: LEA copies the referenced data and MOV the actual data. But this difference does not matter if you\r\njust ignore [ and ] characters.\r\nLet's move away from the general description to the concrete usage of these instructions here. When clicking on\r\none of the functions in the decompile view, the disassembly listing will also move to the corresponding position in\r\nmemory:\r\n00405907 8d bd 78 f9 ff ff LEA EDI=\u003elocal_68c,[EBP + 0xfffff978]\r\n0040590d b8 a6 00 00 00 MOV EAX,0xa6\r\n00405912 e8 de 6f 00 00 CALL FUN_0040c8f5\r\n00405917 8d bd 84 f9 ff ff LEA EDI=\u003elocal_680,[EBP + 0xfffff984]\r\n0040591d b8 a7 00 00 00 MOV EAX,0xa7\r\n00405922 e8 ce 6f 00 00 CALL FUN_0040c8f5\r\n00405927 8d bd cc f9 ff ff LEA EDI=\u003elocal_638,[EBP + 0xfffff9cc]\r\n0040592d b8 a8 00 00 00 MOV EAX,0xa8\r\n00405932 e8 be 6f 00 00 CALL FUN_0040c8f5\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 3 of 13\n\n00405937 8d bd e4 f9 ff ff LEA EDI=\u003elocal_620,[EBP + 0xfffff9e4]\r\n0040593d b8 a9 00 00 00 MOV EAX,0xa9\r\n00405942 e8 ae 6f 00 00 CALL FUN_0040c8f5\r\n00405947 8d bd 9c f9 ff ff LEA EDI=\u003elocal_668,[EBP + 0xfffff99c]\r\n0040594d b8 aa 00 00 00 MOV EAX,0xaa\r\n00405952 e8 9e 6f 00 00 CALL FUN_0040c8f5\r\n00405957 8d bd 58 f9 ff ff LEA EDI=\u003elocal_6ac,[EBP + 0xfffff958]\r\n0040595d b8 ab 00 00 00 MOV EAX,0xab\r\n00405962 e8 8e 6f 00 00 CALL FUN_0040c8f5\r\nThe newlines are inserted for the sake of clearity. Each CALL is preceded by a LEA and MOV . All LEA\r\ninstructions above move an address into the EDI register and the MOV s copy an immediate value into EAX .\r\nBefore giving it any further thought, let's tell Ghidra to take EAX and EDI into account when generating\r\ndecompiled code for these calls. Edit the function signature to \"Use Custom Storage\" and add two \"Function\r\nVariables\" stored in EAX and EDI . This leads to the following decompiled code:\r\nFUN_0040c8f5(0xa6,local_68c);\r\nFUN_0040c8f5(0xa7,local_680);\r\nFUN_0040c8f5(0xa8,local_638);\r\nFUN_0040c8f5(0xa9,local_620);\r\nFUN_0040c8f5(0xaa,local_668);\r\nFUN_0040c8f5(0xab,local_6ac);\r\nAnd one can easily confirm that the variables passed as a second argument are referenced in the code following\r\nthe call. After checking a few other places, this function was called, I was confident, that this is indeed the string\r\ndeobfuscation function.\r\nAnnotating the Debofuscation Function\r\nUntil this point, we never even looked into the function. Let's change that and let's further already rename and\r\nretype the arguments to uint PrStringIndex and BYTE *RetVal :\r\nvoid FUN_0040c8f5(uint PrStringIndex, BYTE *RetVal)\r\n{\r\n int iVar1;\r\n uint uVar2;\r\n ushort uVar3;\r\n iVar1 = (PrStringIndex \u0026 0xffff) * 8;\r\n uVar3 = 0;\r\n if (*(short *)(\u0026DAT_0040128a + iVar1) != 0) {\r\n do {\r\n uVar2 = (uint)uVar3;\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 4 of 13\n\nuVar3 = uVar3 + 1;\r\n RetVal[uVar2] =\r\n (\u0026PTR_DAT_0040128c)[(PrStringIndex \u0026 0xffff) * 2][uVar2] ^ (\u0026DAT_00401288)[iVar1];\r\n } while (uVar3 \u003c *(ushort *)(\u0026DAT_0040128a + iVar1));\r\n }\r\n RetVal[*(ushort *)(\u0026DAT_0040128a + iVar1)] = '\\0';\r\n return;\r\n}\r\nThe function contains several references to global variables. Namely DAT_0040128a , PTR_DAT_0040128c and\r\nDAT_00401288 . Just by looking at the auto-generate names, one can tell that the distance in memory between\r\nthose three is very small (i.e. 2 bytes). This is a sign that those are not actually three different variables but a\r\nstructure with three fields. And we also already know the sizes of two of them (and just assume 4 bytes for the\r\nlast, mainly because that's the size of a pointer in 32 bit):\r\nstruct DeobfuContext {\r\n word field_0; // because 0x0040128a - 0x00401288 == 2\r\n word field_1; // because 0x0040128c - 0x0040128a == 2\r\n dword field_2; // because this is the size of a pointer in 32-bit\r\n}\r\nLet's create this structure in Ghidra (by hitting \"Insert\" in the \"Data Type Manager\" if you use the ghIDA key\r\nbindings). Let's call the struct DeobfuContext and don't forget to hit that other \"Save\" button in the \"Structure\r\nEditor\".\r\nNow let's retype the variable that comes first in memory to a DeobfuContext struct. Double clicking\r\nDAT_00401288 will move the Listing view to the corresponding memory location. Since our structure is 8 bytes in\r\nsize, we first need to make some space by undefining PTR_DAT_0040128c below (hit U if you - you might have\r\nguessed - have the ghIDA key bindings) and change the type of DAT_00401288 to DeobfuContext . This will lead\r\nGhidra to show typecasts like (\u0026DAT_00401288)[PrStringIndex].field_1 , which tells us again that we made a\r\nmistake: The type is not DeobfuContext but an array of DeobfuContext . Since we don't know the size, we'll just\r\nuse a size of 1 for now: Retype DAT_00401288 to DeobfuContext[1] and also rename it to DEOBFU_CONTEXTS . I\r\nalso took the liberty to rename two local variables to i and j because they where used as counters in a loop:\r\nvoid FUN_0040c8f5(uint PrStringIndex, BYTE *RetVal)\r\n{\r\n uint j;\r\n ushort i;\r\n PrStringIndex = PrStringIndex \u0026 0xffff;\r\n i = 0;\r\n if (DEOBFU_CONTEXTS[PrStringIndex].field_1 != 0) {\r\n do {\r\n j = (uint)i;\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 5 of 13\n\ni = i + 1;\r\n RetVal[j] = *(byte *)(DEOBFU_CONTEXTS[PrStringIndex].field_2 + j) ^\r\n *(byte *)\u0026DEOBFU_CONTEXTS[PrStringIndex].field_0;\r\n } while (i \u003c DEOBFU_CONTEXTS[PrStringIndex].field_1);\r\n }\r\n RetVal[DEOBFU_CONTEXTS[PrStringIndex].field_1] = '\\0';\r\n return;\r\n}\r\nReading this code now enables us to rename and retype the fields of the DeobfuContext struct: Because i\r\ncounts up until field_1 , it is probably some sort of length. The expression *(byte *)\r\n(DEOBFU_CONTEXTS[PrStringIndex].field_2 + j) suggests, that field_2 is in fact an array, i.e. BYTE * , which\r\n- coincidentally - is also four bytes in size. And finally, *(byte *)\u0026DEOBFU_CONTEXTS[PrStringIndex].field_0\r\neffectively shortens the field field_0 to a size of one byte instead of two. One might also realized that this\r\nfield_0 is used in an XOR expression ^ so let's be brave and guess that it's a key and change the struct\r\naccordingly:\r\nAnd this finally enables Ghidra to show us the following decompiled version of the function, which I also\r\nrenamed:\r\nvoid EvStringDeobfuscate(uint PrStringIndex, BYTE *RetVal)\r\n{\r\n uint j;\r\n ushort i;\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 6 of 13\n\nPrStringIndex = PrStringIndex \u0026 0xffff;\r\n i = 0;\r\n if (DEOBFU_CONTEXTS[PrStringIndex].Length != 0) {\r\n do {\r\n j = (uint)i;\r\n i = i + 1;\r\n RetVal[j] = DEOBFU_CONTEXTS[PrStringIndex].Buffer[j] ^ DEOBFU_CONTEXTS[PrStringIndex].Key;\r\n } while (i \u003c DEOBFU_CONTEXTS[PrStringIndex].Length);\r\n }\r\n RetVal[DEOBFU_CONTEXTS[PrStringIndex].Length] = '\\0';\r\n return;\r\n}\r\nSo after getting some help, Ghidra presents us with code that can almost be compiled as a C program. And for sure\r\nit can be easily understood!\r\nUnderstanding the Algorithm\r\nThe string obfuscation function accesses a global array of structs, each struct has three fields: one byte XOR-key,\r\nthe length of the string and a pointer to the obfuscated data. The function further accepts two arguments: an index\r\ninto the global array and a pointer, where the deobfuscated string will be written to. The function then iterates over\r\nthe obfuscated data and XORes every byte with the key from the same struct.\r\nTo now learn how large this global array really is, one could, for example, look at all references, write down the\r\nindex and use the larges one as the size of the array. We will later write a script to automatically do that, so if you\r\nwant to set the size of the global struct array now, just feel free to retype it to DeobfuContext[183] .\r\nBut before we move on and write code to automate this, just to eventually realize that we made a mistake\r\nsomewhere above, let's first confirm our understanding of the deobfuscation algorithm by emulating it. There are\r\nnumerous ways of doing that and I'll just explain, how to do it in Cyberchef and then, how to do it in Binary\r\nRefinery. Binary Refinery is the best set of command line tools for binary transformation out there. You can also\r\nalways write a Python script or try to compile the code with a C compiler.\r\nLet's take the first call that comes along (at 0x00405912 ): EvStringDeobfuscate(0xa6, local_68c) . It will\r\naccess position 0xa6 (which is 166) of the global array. Double click DEOBFU_CONTEXTS and scroll down to\r\nposition 166:\r\nField Value\r\nKey B4\r\nLength 0B 00\r\nBuffer 5c 2a 40 00\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 7 of 13\n\nDouble clicking the global variable DAT_00402a5c , which corresponds to the Buffer pointer 5c 2a 40 00 , will\r\nbring you to the memory location containing the obfuscated string. We know, that it should have the size 0x0b\r\n(which is 11). Create an Array of that size in memory there, select it and, finally \"Copy Special...\" (or Shift-E) it.\r\nWhen choosing \"Byte String (No Spaces)\" the following data will be in your clipboard:\r\nc3dddadddad1c09ad0d8d8 .\r\nUsing CyberChef for example, you can deobfuscate this with the \"From Hex\" and \"Xor\" operations to\r\nwininet.dll . Alternatively, the following Binary Refinery pipeline will yield the same result:\r\nemit c3dddadddad1c09ad0d8d8| hex | xor H:B4\r\n# alternatively, you can also read the string directly from the sample:\r\nemit 67f8302a2fd28d15f62d6d20d748bfe350334e5353cbdef112bd1f8231b5599d | peslice 0x00402a5c -t 11 | xor H:B4\r\nThat's good news! We seem to have understood the memory layout as well as the obfuscation technique correctly.\r\nGhidra Script\r\nThe envisioned user experience for a script is as follows: The scripts asks for a function name and will then find\r\nall calls, read the appropriate region from the global buffer, decrypt the string, print the location and the result to\r\nthe console, add a comment of the decrypted string into the disassembly and the decompiled view and, add a\r\nbookmark to the location. This will enable users to list all decrypted strings as well as reduce friction during full\r\nanalysis of the sample.\r\nLet's chop this up into small steps:\r\n1. ask user to a function name, pre-populate the input field with the currently viewed function\r\n2. read the address of the global buffer from the disassembly of the function\r\n3. iterate over all calls to the function\r\n4. read the value of the first argument for each call\r\n5. decrypt the string\r\n6. set comments and bookmarks as well as print to the console\r\nIf you follow this blag closely, you may have noticed, that we already solved 1, 3, 4 and 6 in previous posts. So I'll\r\njust go into detail for steps 2 and 5 and put a link to the full script in the end.\r\nStep 2: The following code will first call the findGlobalBufferAddress function, which I'll explain in a moment.\r\nIf that's not successful, it will ask the user for the address instead. To be honest, there is not much to see here:\r\nlong globalBufferPtr;\r\nOptionalLong optionalGlobalBufferPtr = findGlobalBufferAddress(deobfuscator, 0x10);\r\nif (optionalGlobalBufferPtr.isEmpty()) {\r\n try {\r\n globalBufferPtr = askInt(\"Enter Global Buffer Address\",\r\n \"Cannot automatically determine global buffer address, specify it manually:\");\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 8 of 13\n\n} catch (CancelledException X) {\r\n return;\r\n }\r\n} else {\r\n globalBufferPtr = optionalGlobalBufferPtr.getAsLong();\r\n}\r\nNow to the findGlobalBufferAddress function, which is an example for parsing some assembly in a Ghidra\r\nscript:\r\npublic Boolean isGlobalBufferAccess(Instruction instruction) {\r\n return (instruction.getOperandType(0) \u0026 OperandType.REGISTER) == OperandType.REGISTER\r\n \u0026\u0026 (instruction.getOperandType(1) \u0026 OperandType.ADDRESS) == OperandType.ADDRESS\r\n \u0026\u0026 (instruction.getOperandType(1) \u0026 OperandType.DYNAMIC) == OperandType.DYNAMIC;\r\n}\r\npublic OptionalLong findGlobalBufferAddress(Function func, int searchDepth) {\r\n int i = 0;\r\n for (Instruction instruction : currentProgram.getListing().getInstructions(func.getEntryPoint(), true)) {\r\n if (instruction.getMnemonicString().equals(\"LEA\")) {\r\n // the first operand of LEA is the target register, the second is the address\r\n if (isGlobalBufferAccess(instruction)) {\r\n // this gets the \"objects\" for the second argument which. This is an array of\r\n // values:\r\n //\r\n // LEA globalBufferIndex,[globalBufferIndex*0x8 + GLOBAL_BUFFER]\r\n // Index 0: globalBufferIndex\r\n // Index 1: 0x8\r\n // Index 2: GLOBAL_BUFFER\r\n String hexEncoded = instruction.getOpObjects(1)[2].toString();\r\n return OptionalLong.of(Long.decode(hexEncoded));\r\n }\r\n }\r\n i++;\r\n if (i \u003e searchDepth)\r\n break;\r\n }\r\n return OptionalLong.empty();\r\n}\r\nIterate over all instructions from the function up until a given search depth, this function will filter out all LEA\r\ninstructions. We guess that it is in fact the instruction accessing the global buffer if its first operand is a register\r\nand the second a calculated address. For an assembly instruction object, Ghidra exposes the \"operand objects\"\r\nwhich represent the values of the different operands of an argument to an instruction. The second argument to this\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 9 of 13\n\nLEA instruction is [globalBufferIndex*0x8 + GLOBAL_BUFFER] and there, we are interested in the third operand,\r\nthe GLOBAL_BUFFER . Feel free to read the comment in the function for a slightly different perspective.\r\nStep 5: The actual decryption of the string should of course be the interesting part but as it's always, everything\r\nelse already took 80% of the time. But still, here we go:\r\nbyte structContent[] = getOriginalBytes(toAddr(globalBufferPtr + globalBufferIndex * 8), 8);\r\nbyte xorKey[] = { structContent[0] };\r\nint dataLength = (structContent[2] \u0026 0xff) | (structContent[3] \u0026 0xff) \u003c\u003c 8;\r\nint encryptedPtr = (structContent[4] \u0026 0xff) | ((structContent[5] \u0026 0xff) \u003c\u003c 8)\r\n | ((structContent[6] \u0026 0xff) \u003c\u003c 16) | ((structContent[7] \u0026 0xff) \u003c\u003c 24);\r\nbyte[] obfuscatedBuffer = getOriginalBytes(toAddr(encryptedPtr), dataLength);\r\nbyte decrypted[] = deobfuscateString(obfuscatedBuffer, xorKey);\r\nThis snippet uses the getOriginalBytes from previous blag posts and reads 8 bytes of memory from the correct\r\nlocation. The first byte is the xorKey . Bytes at location 2 and 3 are combined little endian-style into an integer\r\ndataLength and finally, the four following bytes are combined in the same way into a pointer to the encrypted\r\npayload encryptedPtr . We then use the getOriginalBytes function again to read the encrypted data into\r\nobfuscatedBuffer and pass that together with the key to the deobfuscateString function:\r\nprivate byte[] deobfuscateString(byte[] data, byte[] key) {\r\n final byte[] ret = new byte[data.length];\r\n for (int k = 0; k \u003c data.length; k++)\r\n ret[k] = (byte) (data[k] ^ key[k % key.length]);\r\n return ret;\r\n}\r\nThe rest is just boilerplate you can copy and paste from other scripts. The ready-to-use-script is in our repository\r\non github.\r\nAppendix: Decrypted Strings\r\nFor google-ability and overview, here is a list of decrypted strings for the above sample:\r\nCALL Address Offset Deobfuscated String\r\n0x0040F6FE 0 http[:]//bendes.co[.]uk\r\n0x0040F709 1 /lmpUNlwDfoybeulu\r\n0x0040FC5D 2 4p81GSwBwRrAhCYK\r\n0x00411D79 3 SQLite format 3\r\n0x00412C84 4 2|NordVPN||%s|%s\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 10 of 13\n\nCALL Address Offset Deobfuscated String\r\n0x0040F714 19 .bit\r\n0x00412A85 20 %08lX%04lX%lu\r\n0x0040BB31 22 Hostname\r\n0x00409FC0 25 TRUE\r\n0x00409FCB 26 FALSE\r\n0x00410134 27 quit\r\n0x0040DF67 45 Software\r\n0x0040DF72 46 Microsoft\r\n0x00412603 63 pstorec.dll\r\n0x0041260E 86 Internet Explorer\r\n0x00409FB5 89 %s TRUE %s %s %d %s %s\r\n0x0040BB25 96 logins\r\n0x0040BB3D 97 encryptedUsername\r\n0x0040BB49 98 encryptedPassword\r\n0x0040C5CF 89 %s TRUE %s %s %d %s %s\r\n0x0040F83F 119 dotbit.me\r\n0x0040F5DF 122 %S %s HTTP/1.1 %SContent-Length: %d\r\n0x0040FF36 140 %FULLDISK%\r\n0x0040FF43 141 %NETWORK%\r\n0x00410CCE 143 %02d-%02d-%02d %d:%02d:%02d\r\n0x00410A5E 144 MachineGuid: %S\r\n0x00410ADD 145 IP: %s\r\n0x00410B0A 146 CPU: %s (%d cores)\r\n0x00410B91 147 RAM: %s MB\r\n0x00410C03 148 Screen: %dx%d\r\n0x00410CDB 150 LT: %s (UTC+%d:%d)\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 11 of 13\n\nCALL Address Offset Deobfuscated String\r\n0x00410D57 151 GPU:\r\n0x00410E0A 152 Layouts:\r\n0x00410E72 153 Software:\r\n0x004096EA 154 PWD\r\n0x00409704 155 CRED_DATA\r\n0x00409711 156 CREDIT_CARD\r\n0x0040971E 157 AUTOFILL_DATA\r\n0x004096F7 158 IMPAUTOFILL_DATA\r\n0x00410928 159 SYSINFORMATION\r\n0x004097F3 160 FFFILEE\r\n0x0040FF1C 161 __DELIMM__\r\n0x0040FF29 162 __GRABBER__\r\n0x00405912 166 wininet.dll\r\n0x00405922 167 winhttp.dll\r\n0x00405932 168 ws2_32.dll\r\n0x00405942 169 user32.dll\r\n0x00405952 170 shell32.dll\r\n0x00405962 171 advapi32.dll\r\n0x00405972 172 dnsapi.dll\r\n0x00405982 173 netapi32.dll\r\n0x00405992 174 gdi32.dll\r\n0x004059A2 175 gdiplus.dll\r\n0x004059B2 176 oleaut32.dll\r\n0x004059C2 177 ole32.dll\r\n0x004059D2 178 shlwapi.dll\r\n0x004059E2 179 userenv.dll\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 12 of 13\n\nCALL Address Offset Deobfuscated String\r\n0x004059F2 180 urlmon.dll\r\n0x00405A02 181 crypt32.dll\r\n0x00405A12 182 mpr.dll\r\nConclusion\r\nIn my experience, scripting in Ghidra is much easier when done with Java. Even though you might not like the\r\nlanguage, the documentation and eclipse integration is awesome which really speeds up the process. Apart from\r\npreviously published snippets this post also covers parsing of assembly instructions.\r\nThe KputStealer family yields yet another good example for string obfuscation and a good exercise on how to find\r\nand reverse engineer it. This particular case also shows a situation where the decompiled failed and needs some\r\nhelp from the analyst.\r\nSource: https://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nhttps://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/\r\nPage 13 of 13",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://blag.nullteilerfrei.de/2020/04/26/use-ghidra-to-decrypt-strings-of-kpotstealer-malware/"
	],
	"report_names": [
		"use-ghidra-to-decrypt-strings-of-kpotstealer-malware"
	],
	"threat_actors": [],
	"ts_created_at": 1775438934,
	"ts_updated_at": 1775791248,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/fd8e714f749d5dd8570d4a8260323255e90d981f.pdf",
		"text": "https://archive.orkl.eu/fd8e714f749d5dd8570d4a8260323255e90d981f.txt",
		"img": "https://archive.orkl.eu/fd8e714f749d5dd8570d4a8260323255e90d981f.jpg"
	}
}