{
	"id": "537e9b29-bc7d-4e85-9676-78f03b796b47",
	"created_at": "2026-04-06T00:15:33.527981Z",
	"updated_at": "2026-04-10T03:20:20.465338Z",
	"deleted_at": null,
	"sha1_hash": "76b1047b640fb4e06c8cd143e62d983666fd1376",
	"title": "Defeating Sodinokibi/REvil String-Obfuscation in Ghidra – nullteilerfrei",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 136372,
	"plain_text": "Defeating Sodinokibi/REvil String-Obfuscation in Ghidra – nullteilerfrei\r\nBy born\r\nArchived: 2026-04-05 20:14:04 UTC\r\nThis post describes the memory layout as well as the method used by the Sodinokibi (or REvil) ransomware to protect its\r\nstrings. It will then list a few Java snippets to interact with the Ghidra scripting API and finally explain a working script to\r\ndeobfuscate all strings within a REvil sample. If you don't care about the explaination, you can find the most recent version\r\nof the script you can simply import into Ghidra on github.\r\nNote: All code in this blog post was written by Lars Wallenborn and Jesko Hüttenhain. It is published under the 3-Clause\r\nBSD License.\r\nSome Thank-You Notes\r\nI'd like to thank the lovely people at OALabs to bring this sample to my attention once again! If you haven't notice, definitly\r\ntake a look at the automated unpacking service UnpacMe. They just relaesed it into Beta and it is working perfectly for me\r\nso far. You can find a very expensive commercial for their unpacking service in the references of this blag post. Oh and that\r\nvideo also contains a full analysis of the string obfuscation in REvil including the same things I'm doing in this blog post but\r\nfor IDA.\r\nA tip to the hat towards Thomas Roth, who almost certainly doesn't know me but published a lot of details about the Ghidra\r\nscripting API. And, last but not least, thanks to my buddy Jesko for figuring out Pcode traversal with me.\r\nMemory Layout\r\nThe Sodinokibi (or REvil) ransomware leverages string obfuscation to hinder analysis ((more precisely: to slow down a\r\nbottom-up approach starting with interesting strings)). Like all the other coolkids, we will use the sample with SHA256 hash\r\n5f56d5748940e4039053f85978074bde16d64bd5ba97f6f0026ba8172cb29e93\r\nas an example. In uses calls like the following to deobfuscated strings before usage:\r\nFUN_00404e03(\u0026DAT_0041c040, 0x256, 6, 8, local_14);\r\nThe call involves five arguments: The first argument DAT_0041c040 is the same for all calls and references a global buffer\r\ncontaining data that doesn't look like anything. The next argument 0x256 is an offset into that buffer and the next two\r\narguments 6 and 8 are length values. The fifth and last parameter local_14 is a variable that will contain a\r\ndeobfuscated string after the function returns.\r\nThe two length values specified lengths of a key buffer and of buffer containing encrypted data respectively. These two\r\nbuffers are located consecutively directly after the specified offset in the referenced global buffer.\r\nDecryption Algorithm\r\nThe malware uses the RC4 algorithm to decrypt the obfuscated string with the above-described key. As always, this\r\nalgorithm can easily be identified by three consecutive loops where the first loop initializes the cells of an array of length\r\n256 by their indices, the second references to key and the third references the encrypted data. The following is a slightly\r\nannotated version Ghidra's decompiled output for the RC4 algorithm:\r\ndo {\r\n sbox[k] = (byte)k;\r\n k = k + 1;\r\n} while (k \u003c 0x100);\r\nk = 0\r\ndo {\r\n Tmp = sbox[k];\r\n i = Tmp + Key[k % KeyLength] + i \u0026 0xff;\r\n sbox[k] = sbox[i];\r\n k = k + 1;\r\nhttps://blag.nullteilerfrei.de/2020/02/02/defeating-sodinokibi-revil-string-obfuscation-in-ghidra/\r\nPage 1 of 7\n\nsbox[i] = Tmp;\r\n} while (k \u003c 0x100);\r\nAs always, I don't recommend reverse engineering the algorithm but instead guess that it is RC4 and then use Cyberchef,\r\nPython or Binary Refinery to confirm your guess. For completeness, here is a BinRef pipeline for one of the calls:\r\n\u003e emit \"36423605a96002d7e5af770baecc2d2ec1a69b7e6b1e47a95f1fbb840b96ebb5d69fe1053e7f7266bb29215d5f8ec74406561b881b2509a1b\r\n43 Bytes, 50.29% entropy, ASCII text, with no line terminators\r\n------------------------------------------------------[PEEK]--\r\nGlobal\\206D87E0-0E60-DF25-DD8F-8E4E7D1E3BF0\r\n--------------------------------------------------------------\r\nScripting Snippets\r\nIn this section, I will share a few useful snippets when using Java to write scripts for Ghidra for malware reverse\r\nengineering. The first one is a helper function that accepts a function name, assumes there is only one function with that\r\nname and returns a list of addresses where this function is called. We will also use the getOriginalBytes function from a\r\nprevious blag post.\r\n private List\u003cAddress\u003e getCallAddresses(Function deobfuscator) {\r\n List\u003cAddress\u003e addresses = new ArrayList\u003cAddress\u003e();\r\n for (Reference ref : getReferencesTo(deobfuscator.getEntryPoint())) {\r\n if (ref.getReferenceType() != RefType.UNCONDITIONAL_CALL)\r\n continue;\r\n addresses.add(ref.getFromAddress());\r\n }\r\n return addresses;\r\n }\r\nThe function setComment will set a comment in both the disassembly view and the decompiled view:\r\nprivate void setComment(Address address, String comment) {\r\n setPlateComentToDisassembly(address, comment);\r\n setCommentToDecompiledCode(address, comment);\r\n}\r\nprivate void setPlateComentToDisassembly(Address address, String comment) {\r\n currentProgram.getListing().getCodeUnitAt(address).setComment(CodeUnit.PLATE_COMMENT, comment);\r\n}\r\nprivate void setCommentToDecompiledCode(Address address, String comment) {\r\n currentProgram.getListing().getCodeUnitAt(address).setComment(CodeUnit.PRE_COMMENT, comment);\r\n}\r\nFinally, the following function is my Q\u0026D approach to convert a byte array to an ASCII string (even if it is a wide string). A\r\nmore experienced Java developer may be able to implement it in a more beautiful way, but that's simply not me.\r\nprivate String AsciiDammit(byte[] data, int len) {\r\n boolean isWide = true;\r\n byte[] nonWide = new byte[len / 2];\r\n for (int i = 0; i \u003c len / 2; i++) {\r\n if (data[i * 2 + 1] != '\\0') {\r\n isWide = false;\r\n break;\r\n }\r\n nonWide[i] = data[i * 2];\r\n }\r\n return new String(isWide ? nonWide : data);\r\n}\r\nFunction Arguments\r\nhttps://blag.nullteilerfrei.de/2020/02/02/defeating-sodinokibi-revil-string-obfuscation-in-ghidra/\r\nPage 2 of 7\n\nAs described in the \"Layout\" section above, we will need to get the values passed to a function call. This is a bit more\r\ninvolved: the getConstantCallArgument function below accepts a memory address of a function call and a list of integers\r\nin the variable argumentIndices . These integers should specify the indices of function arguments the caller wants the value\r\nof (starting with 1). The function will return an array of optional longs: it has the same length as argumentIndices and\r\ncontains the determined value if possible.\r\nTo determine the value, getConstantCallArgument decompiles the function that contains the call ( caller in the Java\r\ncode), retrieves a so-called \"high-level function structure\" via getHighFunction and then uses the function\r\ntraceVarnodeValue to retrieve the values of the requested parameters. This traceVarnodeValue function is an incomplete\r\nimplementation of a Pcode traversal. In at least two samples, it worked though, so I still think it is worth sharing.\r\nclass UnknownVariableCopy extends Exception {\r\n public UnknownVariableCopy(PcodeOp unknownCode, Address addr) {\r\n super(String.format(\"unknown opcode %s for variable copy at %08X\", unknownCode.getMnemonic(), addr.getOffset()));\r\n }\r\n}\r\nprivate OptionalLong[] getConstantCallArgument(Address addr, int[] argumentIndices) throws IllegalStateException, UnknownV\r\n int argumentPos = 0;\r\n OptionalLong argumentValues[] = new OptionalLong[argumentIndices.length];\r\n Function caller = getFunctionBefore(addr);\r\n if (caller == null)\r\n throw new IllegalStateException();\r\n DecompInterface decompInterface = new DecompInterface();\r\n decompInterface.openProgram(currentProgram);\r\n DecompileResults decompileResults = decompInterface.decompileFunction(caller, 120, monitor);\r\n if (!decompileResults.decompileCompleted())\r\n throw new IllegalStateException();\r\n HighFunction highFunction = decompileResults.getHighFunction();\r\n Iterator\u003cPcodeOpAST\u003e pCodes = highFunction.getPcodeOps(addr);\r\n while (pCodes.hasNext()) {\r\n PcodeOpAST instruction = pCodes.next();\r\n if (instruction.getOpcode() == PcodeOp.CALL) {\r\n for (int index : argumentIndices) {\r\n argumentValues[argumentPos] = traceVarnodeValue(instruction.getInput(index));\r\n argumentPos++;\r\n }\r\n }\r\n }\r\n return argumentValues;\r\n}\r\nprivate OptionalLong traceVarnodeValue(Varnode argument) throws UnknownVariableCopy {\r\n while (!argument.isConstant()) {\r\n PcodeOp ins = argument.getDef();\r\n if (ins == null)\r\n break;\r\n switch (ins.getOpcode()) {\r\n case PcodeOp.CAST:\r\n case PcodeOp.COPY:\r\n argument = ins.getInput(0);\r\n break;\r\n case PcodeOp.PTRSUB:\r\n case PcodeOp.PTRADD:\r\n argument = ins.getInput(1);\r\n break;\r\n case PcodeOp.INT_MULT:\r\n case PcodeOp.MULTIEQUAL:\r\n // known cases where an array is indexed\r\n return OptionalLong.empty();\r\n default:\r\n // don't know how to handle this yet.\r\n throw new UnknownVariableCopy(ins, argument.getAddress());\r\n }\r\n }\r\n return OptionalLong.of(argument.getOffset());\r\n}\r\nhttps://blag.nullteilerfrei.de/2020/02/02/defeating-sodinokibi-revil-string-obfuscation-in-ghidra/\r\nPage 3 of 7\n\nAutomated Deobfuscation\r\nEquipped with ways to retrieve all calls to a specific function, get values of parameters to this calls and also be able to add\r\nannotations to Ghidra, we are only missing the actual deobfuscation function. As described above in the \"Decryption\"\r\nsection, it is RC4. Instead of doing it the enterprise way ((by calling Cipher.getInstance(\"RC4\"); I guess)), we will use a\r\nrandom implementation by some guy on github. I even found myself an excuse for this: If one, at some point in the future,\r\nencounters a modified version of RC4, it is easily possible to do similar modifications in the code.\r\nSo putting all the pieces together, we end up with the following run method, which I will explain a bit below:\r\npublic void run() throws Exception {\r\n String deobfuscatorName;\r\n try {\r\n deobfuscatorName = askString(\"Enter Name\", \"Enter the name of the deobfuscation function below:\", getFunctionBefor\r\n } catch (CancelledException X) {\r\n return;\r\n }\r\n Function deobfuscator = getGlobalFunctions(deobfuscatorName).get(0);\r\n OUTER_LOOP: for (Address callAddr : getCallAddresses(deobfuscator)) {\r\n monitor.setMessage(String.format(\"parsing call at %08X\", callAddr.getOffset()));\r\n int arguments[] = { 1, 2, 3, 4 };\r\n OptionalLong options[] = getConstantCallArgument(callAddr, arguments);\r\n for (OptionalLong option : options) {\r\n if (option.isEmpty()) {\r\n println(String.format(\"Argument to call at %08X is not a constant string.\", callAddr.getOffset()));\r\n continue OUTER_LOOP;\r\n }\r\n }\r\n long blobAddress = options[0].getAsLong();\r\n int keyOffset = (int) options[1].getAsLong();\r\n int keyLength = (int) options[2].getAsLong();\r\n int dataLength = (int) options[3].getAsLong();\r\n if (dataLength == 0 || keyLength == 0)\r\n continue;\r\n byte[] key = getOriginalBytes(toAddr(blobAddress + keyOffset), keyLength);\r\n byte[] data = getOriginalBytes(toAddr(blobAddress + keyOffset + keyLength), dataLength);\r\n byte[] decrypted = new RC4(key).encrypt(data);\r\n String deobfuscated = AsciiDammit(decrypted, dataLength);\r\n println(String.format(\"%08X : %s\", callAddr.getOffset(), deobfuscated));\r\n setComment(callAddr, String.format(\"Deobfuscated: %s\", deobfuscated));\r\n createBookmark(callAddr, \"DeobfuscatedString\", deobfuscated);\r\n }\r\n}\r\nThe function first asks the user for a function name. It pre-populates the field with the currently selected function or, if the\r\nscript was called before, with the previous input. Even though simple, this feels surprisingly good from a user experience\r\n(UX) perspective.\r\nThe function then iterates over all calls to this deobfuscation function and retrieves values for arguments 1-4. If all of them\r\nare set, they are assigned to the following variables:\r\nblobAddress reference to the address of the blob of encrypted data in the malware\r\nkeyOffset offset into that blob\r\nkeyLength length of the key buffer starting at the offset into the blob\r\ndataLength length of the data buffer starting directly after the key buffer\r\nThe function then retrieves the key and the encrypted data and uses the RC4 class to deobfuscate it. The result is then\r\npassed to the AsciiDammit function, which will also take care of decoding wide-strings. It then prints the address of the\r\ncall and the deobfuscated string to the console, sets a comment in the disassembly and the decompile views and, creates a\r\nbookmark to the call, so we can easily look at a list of all deobfuscated strings enabling buttom-up analysis.\r\nThe full script without explanation can be found on github.\r\nhttps://blag.nullteilerfrei.de/2020/02/02/defeating-sodinokibi-revil-string-obfuscation-in-ghidra/\r\nPage 4 of 7\n\nDecrypted strings\r\nFor google-ability, here is a list of all deobfuscated strings in the analysed sample:\r\nAddress Deobfuscated String\r\n0x00401B2F exp\r\n0x0040151E pk\r\n0x00401538 pid\r\n0x00401552 sub\r\n0x0040156C dbg\r\n0x00401589 wht\r\n0x004015A4 wfld\r\n0x004015BF wipe\r\n0x004015D9 prc\r\n0x004015F6 dmn\r\n0x00401610 net\r\n0x0040162B nbody\r\n0x00401646 nname\r\n0x00401664 img\r\n0x0040167B fast\r\n0x00401838 none\r\n0x00401851 true\r\n0x0040186D false\r\n0x004019BE -nolan\r\n0x00401B9C SOFTWARE\\recfg\r\n0x00401BB5 rnd_ext\r\n0x00401EA7 {UID}\r\n0x00401EC0 {KEY}\r\n0x00401ED9 {EXT}\r\n0x00401EF5 {USERNAME}\r\n0x00401F14 {NOTENAME}\r\n0x00401F30 SYSTEM\r\n0x00401F46 USER\r\n0x00401CCF SOFTWARE\\recfg\r\n0x00401CE8 stat\r\n0x00401D71\r\n{\"ver\":%d,\"pid\":\"%s\",\"sub\":\"%s\",\"pk\":\"%s\",\"uid\":\"%s\",\"sk\":\"%s\",\"unm\":\"%s\",\"net\":\"%s\",\"grp\":\"%s\",\"lng\":\"%s\",\"bro\":%s,\"\r\n\"dsk\":\"%s\",\"ext\":\"%s\"}\r\n0x00401FF4 .lock\r\n0x004020D1 {UID}\r\n0x004020EA {KEY}\r\n0x00402103 {EXT}\r\nhttps://blag.nullteilerfrei.de/2020/02/02/defeating-sodinokibi-revil-string-obfuscation-in-ghidra/\r\nPage 5 of 7\n\nAddress Deobfuscated String\r\n0x00402184 {EXT}\r\n0x00402216 SOFTWARE\\recfg\r\n0x00402232 sub_key\r\n0x0040224B pk_key\r\n0x00402264 sk_key\r\n0x00402280 0_key\r\n0x00403933 .bmp\r\n0x00403E5F cmd.exe\r\n0x00403E7E\r\n/c vssadmin.exe Delete Shadows /All /Quiet \u0026 bcdedit /set {default} recoveryenabled No \u0026 bcdedit /set {default} boots\r\nignoreallfailures\r\n0x00404085 SYSTEM\\CurrentControlSet\\services\\Tcpip\\Parameters\r\n0x0040409E Domain\r\n0x004040FA WORKGROUP\r\n0x00404199 Control Panel\\International\r\n0x004041B2 LocaleName\r\n0x004042A6 %08X%08X\r\n0x0040432F SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\r\n0x00404348 productName\r\n0x004043E0 explorer.exe\r\n0x004048B5 Global\\206D87E0-0E60-DF25-DD8F-8E4E7D1E3BF0\r\n0x00404BF9 runas\r\n0x004058B3 qJiQmi65SC9GfVbj\r\n0x0040660D \\\\?\\A:\\\r\n0x00406547 \\\\?\\UNC\r\n0x00405C02 CreateStreamOnHGlobal\r\n0x00405D3B ole32.dll\r\n0x00406B6A win32kfull.sys\r\n0x00406B83 win32k.sys\r\n0x004012A0 fld\r\n0x004012B7 fls\r\n0x004012CE ext\r\n0x004030B7 https://\r\n0x004030F7 wp-content\r\n0x0040311B static\r\n0x00403140 content\r\n0x00403162 include\r\n0x00403185 uploads\r\n0x004031A4 news\r\nhttps://blag.nullteilerfrei.de/2020/02/02/defeating-sodinokibi-revil-string-obfuscation-in-ghidra/\r\nPage 6 of 7\n\nAddress Deobfuscated String\r\n0x004031C0 data\r\n0x004031DF admin\r\n0x00403264 images\r\n0x00403287 pictures\r\n0x004032AA image\r\n0x004032CC temp\r\n0x004032E8 tmp\r\n0x00403308 graphic\r\n0x00403327 assets\r\n0x00403345 pics\r\n0x00403360 game\r\n0x00403441 jpg\r\n0x00403457 png\r\n0x00403470 gif\r\n0x00406849 Mozilla/5.0 (Windows NT 6.1; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0\r\n0x00406919 POST\r\n0x0040697E Content-Type: application/octet-stream\\nConnection: close\r\n0x004027BC program files\r\n0x004027D5 program files (x86)\r\n0x0040281F sql\r\n0x00405C40 advapi32.dll\r\n0x00405C79 crypt32.dll\r\n0x00405CB2 gdi32.dll\r\n0x00405CF6 mpr.dll\r\n0x00405F20 shell32.dll\r\n0x00405F59 shlwapi.dll\r\n0x00405F92 user32.dll\r\n0x00405FCB winhttp.dll\r\n0x00406004 winmm.dll\r\nReferences\r\nOALabs - IDA Pro Automated String Decryption For REvil Ransomware\r\nPython Scripting Cheat Sheet by Thomas Roth\r\nSource: https://blag.nullteilerfrei.de/2020/02/02/defeating-sodinokibi-revil-string-obfuscation-in-ghidra/\r\nhttps://blag.nullteilerfrei.de/2020/02/02/defeating-sodinokibi-revil-string-obfuscation-in-ghidra/\r\nPage 7 of 7",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://blag.nullteilerfrei.de/2020/02/02/defeating-sodinokibi-revil-string-obfuscation-in-ghidra/"
	],
	"report_names": [
		"defeating-sodinokibi-revil-string-obfuscation-in-ghidra"
	],
	"threat_actors": [],
	"ts_created_at": 1775434533,
	"ts_updated_at": 1775791220,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/76b1047b640fb4e06c8cd143e62d983666fd1376.pdf",
		"text": "https://archive.orkl.eu/76b1047b640fb4e06c8cd143e62d983666fd1376.txt",
		"img": "https://archive.orkl.eu/76b1047b640fb4e06c8cd143e62d983666fd1376.jpg"
	}
}