{
	"id": "7e128f75-e808-4e99-95bb-85c8a6aefa3a",
	"created_at": "2026-04-10T03:20:57.221495Z",
	"updated_at": "2026-04-10T13:12:30.22218Z",
	"deleted_at": null,
	"sha1_hash": "ab1fb7b7321e51d1ed0164d7f8ffd107e782f619",
	"title": "Decrypting and Hunting PrivateLoader",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 67075,
	"plain_text": "Decrypting and Hunting PrivateLoader\r\nBy André Tavares\r\nPublished: 2022-06-06 · Archived: 2026-04-10 02:07:58 UTC\r\nPrivateLoader is a loader from a pay-per-install malware distribution service that has been utilized to distribute\r\ninfo stealers, banking trojans, loaders, spambots, rats, miners and ransomware on Windows machines. First seen in\r\nearly 2021, being hosted on websites that claim to provide cracked software, the customers of the service are able\r\nto selectively deliver malware to victims based on location, financial activity, environment, and specific software\r\ninstalled.\r\nLet’s have a look at the malware and try to find a way to detect and hunt it.\r\nSearching for strings\r\nHere’s a sample analyzed by Zscaler on April 2022:\r\naa2c0a9e34f9fa4cbf1780d757cc84f32a8bd005142012e91a6888167f80f4d5\r\nLet’s open it on Ghidra. Going into the entry point, following the code, looking for interesting functions, I quickly\r\nspot the function at 0x406360 . It’s calling LoadLibraryA but the lpLibFileName parameter is built\r\ndynamically at runtime using the stack. Its seems that we found a string encryption technique. Both the string and\r\nthe xor key are loaded into the stack. Looking a bit more through the function, its seems that this is the way most\r\nof the strings are loaded:\r\nLEA EAX=\u003elocal_50,[ESP + 0x10]\r\nMOV dword ptr [ESP + local_50[0]],0x84038676\r\nMOV dword ptr [ESP + local_50[4]],0xeb71eb3c\r\nMOV dword ptr [ESP + local_50[8]],0x36fb7b30\r\nMOV dword ptr [ESP + local_50[12]],0xab7d1f0c\r\nMOVAPS XMM1,xmmword ptr [ESP + local_50[0]]\r\nMOV dword ptr [ESP + local_30[0]],0xea71e31d\r\nMOV dword ptr [ESP + local_30[4]],0xd9428759\r\nMOV dword ptr [ESP + local_30[8]],0x5a971f1e\r\nMOV dword ptr [ESP + local_30[12]],0xab7d1f0c\r\nPXOR XMM1,xmmword ptr [ESP + local_30[0]] ; kernel32.dll\r\nPUSH EAX ; LPCSTR lpLibFileName for LoadLibraryA\r\nMOVAPS xmmword ptr [ESP + local_50[0]],XMM1\r\nCALL ESI=\u003eKERNEL32.DLL::LoadLibraryA\r\nAfter XOR the encrypted string with the key, we get kernel32.dll .\r\nDecrypting the strings\r\nhttps://tavares.re/blog/2022/06/06/hunting-privateloader-pay-per-install-service/\r\nPage 1 of 6\n\nNow, to faster analyze the malware and better understand its behavior, we should build a string decryptor to help\r\nus on our reversing efforts and better document the code. With the help of Capstone disassembly framework, and\r\nsome trial and error, here’s the script:\r\nimport pefile\r\nimport struct\r\nfrom capstone import *\r\ndef extract_var(op):\r\nif ']' in op:\r\nop = ''.join(op.split(' ')[-1])[:-1].replace('[', '')\r\nreturn op\r\ndef search(instructions, var):\r\ndata_chunks = []\r\nfor inst in instructions:\r\nif inst[2] == 'mov':\r\ntry:\r\nimm = int(inst[3].split(' ')[-1], 16)\r\ndata_chunks.append(struct.pack('\u003cI', imm))\r\nif extract_var(inst[3].split(', ')[0]) == var:\r\nreturn b''.join(data_chunks[::-1]) # 16 bytes str chunk\r\nexcept: # not a dword\r\npass\r\nif extract_var(inst[3].split(', ')[0]) == var:\r\nvar = extract_var(inst[3].split(', ')[1])\r\ndef decrypt_strings(filename):\r\n# disassemble .text section\r\npe = pefile.PE(filename)\r\nhttps://tavares.re/blog/2022/06/06/hunting-privateloader-pay-per-install-service/\r\nPage 2 of 6\n\nmd = Cs(CS_ARCH_X86, CS_MODE_32)\r\nmd.skipdata = True\r\ninstructions = []\r\ntext = pe.sections[0]\r\ntext_addr = pe.OPTIONAL_HEADER.ImageBase + text.VirtualAddress\r\nfor (addr, size, mnemonic, op_str) in md.disasm_lite(text.get_data(), text_addr):\r\ninstructions.append((addr, size, mnemonic, op_str))\r\n# search, build and decrypt strings\r\nstrings = []\r\naddr = None\r\nstring = ''\r\nfor i, inst in enumerate(instructions):\r\nif inst[2] == 'pxor':\r\ntry: # possible string decryption found\r\nencrypted_str = search(instructions[:i][::-1], extract_var(inst[3].split(', ')[0])) # reverse search\r\nkey = search(instructions[:i][::-1], extract_var(inst[3].split(', ')[1])) # reverse search\r\nstring += bytearray(encrypted_str[j] ^ key[j] for j in range(len(key))).decode(errors='ignore') # bug\r\nif not addr:\r\naddr = hex(inst[0])\r\nif '\\x00' in string:\r\nstrings.append((addr, string.replace('\\x00', '')))\r\nstring = ''\r\naddr = None\r\nexcept Exception as e:\r\nprint(f'Fail at {hex(inst[0])}')\r\nhttps://tavares.re/blog/2022/06/06/hunting-privateloader-pay-per-install-service/\r\nPage 3 of 6\n\nprint(len(strings))\r\nfor s in strings:\r\nprint(f'{s[0]} {s[1]}')\r\ndecrypt_strings('aa2c0a9e34f9fa4cbf1780d757cc84f32a8bd005142012e91a6888167f80f4d5')\r\nAfter running it against the sample we are analyzing, we get the following strings:\r\n0x4003ee GetCurrentProcess\r\n0x400469 CreateThread\r\n0x4004ba CreateFileA\r\n0x400506 Sleep\r\n0x400572 SetPriorityClass\r\n0x4005ec Shell32.dll\r\n0x400657 SHGetFolderPathA\r\n0x40083b null\r\n0x401078 rb\r\n0x40157c http://212.193.30.45/proxies.txt\r\n0x401795 :1080\r\n0x401839 \\n\r\n0x401f2d :1080\r\n0x401fd1 :\r\n0x4026ce .\r\n0x4028ac .\r\n0x402972 .\r\n0x402a34 .\r\n0x4032ad http://45.144.225.57/server.txt\r\n0x4033c0 HOST:\r\n0x40346e :\r\n0x403760 pastebin.com/raw/A7dSG1te\r\n0x403965 HOST:\r\n0x403b93 http://wfsdragon.ru/api/setStats.php\r\n0x403dcd HOST:\r\n0x403f84 :\r\n0x4040ae 2.56.59.42\r\n0x404350 /base/api/statistics.php\r\n0x404439 URL:\r\n0x4044b6 :\r\n0x404a5e https://\r\n0x404ad8 .tmp\r\n0x404bf6 \\\r\n0x4053e9 kernel32.dll\r\n0x40544a WINHTTP.dll\r\n0x4054a5 wininet.dll\r\nhttps://tavares.re/blog/2022/06/06/hunting-privateloader-pay-per-install-service/\r\nPage 4 of 6\n\n0x406616 WinHttpConnect\r\n0x406682 WinHttpOpenRequest\r\n0x40671a WinHttpQueryDataAvailable\r\n0x4067b2 WinHttpSendRequest\r\n0x40684a WinHttpReceiveResponse\r\n0x4068e2 WinHttpQueryHeaders\r\n0x406956 WinHttpOpen\r\n0x4069b5 WinHttpReadData\r\n0x406a20 WinHttpCloseHandle\r\n0x406b09 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Sa\r\n0x407402 http://\r\n0x4074ab /\r\n0x407582 ?\r\n0x40851a HEAD\r\n0x408fa8 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Sa\r\n0x4091f0 wininet.dll\r\n0x40925b InternetSetOptionA\r\n0x4092ef HttpOpenRequestA\r\n0x40938d InternetConnectA\r\n0x409421 InternetOpenUrlA\r\n0x40949e InternetOpenA\r\n0x4094f2 HttpQueryInfoA\r\n0x409567 InternetQueryOptionA\r\n0x4095fb HttpSendRequestA\r\n0x409694 InternetReadFile\r\n0x409737 InternetCloseHandle\r\n0x4097ad Kernel32.dll\r\n0x409801 HeapAlloc\r\n0x409852 HeapFree\r\n0x4098a3 GetProcessHeap\r\n0x4098f3 CharNextA\r\n0x409938 User32.dll\r\n0x409994 GetLastError\r\n0x4099e5 CreateFileA\r\n0x409a36 WriteFile\r\n0x409a87 CloseHandle\r\nSome of them are network IoCs that can be used for defense and tracking purposes. We can now go back to\r\nGhidra and continue our analysis, now with more context of what might be the malware’s capabilities.\r\nDetecting and hunting the malware\r\nThis uncommon string decryption technique enable us to write a Yara rule for detection and hunting purposes. To\r\nreduce the number of false positives and increase the rule performance, we can add some plaintext unicode strings\r\nused on the C2 communication and a few minor conditions. Here’s the rule:\r\nhttps://tavares.re/blog/2022/06/06/hunting-privateloader-pay-per-install-service/\r\nPage 5 of 6\n\nrule win_privateloader : loader\r\n{\r\nmeta:\r\nauthor = \"andretavare5\"\r\norg = \"BitSight\"\r\ndate = \"2022-06-06\"\r\nmd5 = \"8f70a0f45532261cb4df2800b141551d\"\r\nreference = \"https://tavares.re/blog/2022/06/06/hunting-privateloader-pay-per-install-service\"\r\nlicense = \"CC BY-NC-SA 4.0\"\r\nstrings:\r\n$x = {66 0F EF (4?|8?)} // pxor xmm(1/0) - str chunk decryption\r\n$s = \"Content-Type: application/x-www-form-urlencoded\\r\\n\" wide ascii\r\n$ua1 = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)\r\nChrome/74.0.3729.169 Safari/537.36\" wide ascii\r\n$ua2 = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)\r\nChrome/93.0.4577.63 Safari/537.36\" wide ascii\r\ncondition:\r\nuint16(0) == 0x5A4D and // MZ\r\n$s and\r\nany of ($ua*) and\r\n#x \u003e 100\r\n}\r\nAfter running this rule on VirusTotal retro hunting, I got over 1k samples on a 1 year timeframe. By manually\r\nanalyzing some of the matches, I couldn’t find any false positives. As a first attempt of hunting and detecting\r\nPrivateLoader, this rule seems to yield good results.\r\nSource: https://tavares.re/blog/2022/06/06/hunting-privateloader-pay-per-install-service/\r\nhttps://tavares.re/blog/2022/06/06/hunting-privateloader-pay-per-install-service/\r\nPage 6 of 6",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://tavares.re/blog/2022/06/06/hunting-privateloader-pay-per-install-service/"
	],
	"report_names": [
		"hunting-privateloader-pay-per-install-service"
	],
	"threat_actors": [],
	"ts_created_at": 1775791257,
	"ts_updated_at": 1775826750,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/ab1fb7b7321e51d1ed0164d7f8ffd107e782f619.pdf",
		"text": "https://archive.orkl.eu/ab1fb7b7321e51d1ed0164d7f8ffd107e782f619.txt",
		"img": "https://archive.orkl.eu/ab1fb7b7321e51d1ed0164d7f8ffd107e782f619.jpg"
	}
}