{
	"id": "1d865ce2-fd55-4193-b7c8-6cc171ea091d",
	"created_at": "2026-04-06T00:22:36.808366Z",
	"updated_at": "2026-04-10T03:21:35.459084Z",
	"deleted_at": null,
	"sha1_hash": "9ef79834d68d835150fcad93974ea68546eb833c",
	"title": "Analysis of a Caddy Wiper Sample Targeting Ukraine",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 467844,
	"plain_text": "Analysis of a Caddy Wiper Sample Targeting Ukraine\r\nBy Ali Mosajjal\r\nPublished: 2022-03-26 · Archived: 2026-04-05 18:47:11 UTC\r\nAccording to the Twitter post by ESET the wiper is deployed by group policy to the infected system. Once run, as\r\nadministrator, the system will crash and the following screen will be displayed. Once the computer is rebooted it\r\ncrashes and will not start anymore and prompt that it cannot locate the operating system.\r\nCaddyWiper was first reported by ESET as below:\r\nDubbed CaddyWiper by ESET analysts, the malware was first detected at 11.38 a.m. local time (9.38 a.m.\r\nUTC) on Monday. The wiper, which destroys user data and partition information from attached drives, was\r\nspotted on several dozen systems in a limited number of organizations. It is detected by ESET products as\r\nWin32/KillDisk.NCX.\r\nOne of my friends pinged me a few days later with a link to a CaddyWiper sample. Since this sample was a\r\nparticularly small one, I decided to write a blog post going through each function from scratch and introducing the\r\ntools I used to make my life easier. Hopefully, this can serve as a reference to junior malware analysts who want to\r\nget started with this craft.\r\nFirst off, I’m a Linux user myself and I use mainly Linux tools to analyse malware. pev is a set command-line\r\nutilities providing a high level analysis of a PE binary. It consists of the following tools\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\nofs2rva\r\npedis\r\npehash\r\npeldd\r\npepack\r\nperes\r\npescan\r\npesec\r\npestr\r\nreadpe\r\nrva2ofs\r\nrunning pehash on the sample offers the following:\r\n1\r\n2\r\n3\r\nfilepath: a294620543334a721a2ae8eaaf9680a0786f4b9a216d75b55cfd28f39e9430ea.exe\r\nmd5: 42e52b8daf63e6e26c3aa91e7e971492\r\nsha1: 98b3fb74b3e8b3f9b05a82473551c5a77b576d54\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 1 of 15\n\n4\r\n5\r\n6\r\nsha256: a294620543334a721a2ae8eaaf9680a0786f4b9a216d75b55cfd28f39e9430ea\r\nssdeep: 192:76f0CW5P2Io4evFrDv2ZRJzCn7URRsjVJaZF:76fPWl24evFrT2ZR5Cn7UR0VJo\r\nimphash: ea8609d4dad999f73ec4b6f8e7b28e55\r\nreadpe result:\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n 10\r\n 11\r\n 12\r\n 13\r\n 14\r\n 15\r\n 16\r\n 17\r\n 18\r\n 19\r\n 20\r\n 21\r\n 22\r\n 23\r\n 24\r\n 25\r\n 26\r\n 27\r\n 28\r\n 29\r\n 30\r\n 31\r\n 32\r\n 33\r\n 34\r\n 35\r\n 36\r\n 37\r\n 38\r\n 39\r\n 40\r\nDOS Header\r\n Magic number: 0x5a4d (MZ)\r\n Bytes in last page: 144\r\n Pages in file: 3\r\n Relocations: 0\r\n Size of header in paragraphs: 4\r\n Minimum extra paragraphs: 0\r\n Maximum extra paragraphs: 65535\r\n Initial (relative) SS value: 0\r\n Initial SP value: 0xb8\r\n Initial IP value: 0\r\n Initial (relative) CS value: 0\r\n Address of relocation table: 0x40\r\n Overlay number: 0\r\n OEM identifier: 0\r\n OEM information: 0\r\n PE header offset: 0xc8\r\nCOFF/File header\r\n Machine: 0x14c IMAGE_FILE_MACHINE_I386\r\n Number of sections: 3\r\n Date/time stamp: 1647242376 (Mon, 14 Mar 2022 07:19:36 UTC)\r\n Symbol Table offset: 0\r\n Number of symbols: 0\r\n Size of optional header: 0xe0\r\n Characteristics: 0x102\r\n Characteristics names\r\n IMAGE_FILE_EXECUTABLE_IMAGE\r\n IMAGE_FILE_32BIT_MACHINE\r\nOptional/Image header\r\n Magic number: 0x10b (PE32)\r\n Linker major version: 10\r\n Linker minor version: 0\r\n Size of .text section: 0x1c00\r\n Size of .data section: 0x400\r\n Size of .bss section: 0\r\n Entrypoint: 0x1000\r\n Address of .text section: 0x1000\r\n Address of .data section: 0x3000\r\n ImageBase: 0x400000\r\n Alignment of sections: 0x1000\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 2 of 15\n\n41\r\n 42\r\n 43\r\n 44\r\n 45\r\n 46\r\n 47\r\n 48\r\n 49\r\n 50\r\n 51\r\n 52\r\n 53\r\n 54\r\n 55\r\n 56\r\n 57\r\n 58\r\n 59\r\n 60\r\n 61\r\n 62\r\n 63\r\n 64\r\n 65\r\n 66\r\n 67\r\n 68\r\n 69\r\n 70\r\n 71\r\n 72\r\n 73\r\n 74\r\n 75\r\n 76\r\n 77\r\n 78\r\n 79\r\n 80\r\n 81\r\n 82\r\n 83\r\n 84\r\n 85\r\n 86\r\n 87\r\n 88\r\n Alignment factor: 0x200\r\n Major version of required OS: 5\r\n Minor version of required OS: 1\r\n Major version of image: 0\r\n Minor version of image: 0\r\n Major version of subsystem: 5\r\n Minor version of subsystem: 1\r\n Size of image: 0x5000\r\n Size of headers: 0x400\r\n Checksum: 0\r\n Subsystem required: 0x2 (IMAGE_SUBSYSTEM_WINDOWS_GUI)\r\n DLL characteristics: 0x8140\r\n DLL characteristics names\r\n IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE\r\n IMAGE_DLLCHARACTERISTICS_NX_COMPAT\r\n IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE\r\n Size of stack to reserve: 0x100000\r\n Size of stack to commit: 0x1000\r\n Size of heap space to reserve: 0x100000\r\n Size of heap space to commit: 0x1000\r\nData directories\r\n Directory\r\n IMAGE_DIRECTORY_ENTRY_IMPORT: 0x3008 (40 bytes)\r\n Directory\r\n IMAGE_DIRECTORY_ENTRY_BASERELOC: 0x4000 (12 bytes)\r\n Directory\r\n IMAGE_DIRECTORY_ENTRY_IAT: 0x3000 (8 bytes)\r\nImported functions\r\n Library\r\n Name: NETAPI32.dll\r\n Functions\r\n Function\r\n Hint: 39\r\n Name: DsRoleGetPrimaryDomainInformation\r\nExported functions\r\nSections\r\n Section\r\n Name: .text\r\n Virtual Size: 0x1b4a (6986 bytes)\r\n Virtual Address: 0x1000\r\n Size Of Raw Data: 0x1c00 (7168 bytes)\r\n Pointer To Raw Data: 0x400\r\n Number Of Relocations: 0\r\n Characteristics: 0x60000020\r\n Characteristic Names\r\n IMAGE_SCN_CNT_CODE\r\n IMAGE_SCN_MEM_EXECUTE\r\n IMAGE_SCN_MEM_READ\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 3 of 15\n\n89\r\n 90\r\n 91\r\n 92\r\n 93\r\n 94\r\n 95\r\n 96\r\n 97\r\n 98\r\n 99\r\n100\r\n101\r\n102\r\n103\r\n104\r\n105\r\n106\r\n107\r\n108\r\n109\r\n110\r\n111\r\n Section\r\n Name: .rdata\r\n Virtual Size: 0x6a (106 bytes)\r\n Virtual Address: 0x3000\r\n Size Of Raw Data: 0x200 (512 bytes)\r\n Pointer To Raw Data: 0x2000\r\n Number Of Relocations: 0\r\n Characteristics: 0x40000040\r\n Characteristic Names\r\n IMAGE_SCN_CNT_INITIALIZED_DATA\r\n IMAGE_SCN_MEM_READ\r\n Section\r\n Name: .reloc\r\n Virtual Size: 0x18 (24 bytes)\r\n Virtual Address: 0x4000\r\n Size Of Raw Data: 0x200 (512 bytes)\r\n Pointer To Raw Data: 0x2200\r\n Number Of Relocations: 0\r\n Characteristics: 0x42000040\r\n Characteristic Names\r\n IMAGE_SCN_CNT_INITIALIZED_DATA\r\n IMAGE_SCN_MEM_DISCARDABLE\r\n IMAGE_SCN_MEM_READ\r\nIf you’re new to analyzing a PE, I highly recommend looking at the official Microsoft documents for PE Format.\r\nSome notes from the link:\r\nAt location 0x3c, the stub has the file offset to the PE signature. This information enables Windows to\r\nproperly execute the image file, even though it has an MS-DOS stub. This file offset is placed at location\r\n0x3c during linking. After the MS-DOS stub, at the file offset specified at offset 0x3c, is a 4-byte signature\r\nthat identifies the file as a PE format image file. This signature is “PE\\0\\0” (the letters “P” and “E”\r\nfollowed by two null bytes).\r\nMain function Analysis\r\nthe main function starts at 00401000 and it looks like it doesn’t return a status code. in c terms, it means the\r\nmain function is written like so: void main(...) .\r\nIn the main function, we can see a call to the external function DsRoleGetPrimaryDomainInformation at 0040113a :\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 4 of 15\n\naccording to Microsoft documentation, The DsRoleGetPrimaryDomainInformation function retrieves state data for\r\nthe computer. This data includes the state of the directory service installation and domain data.\r\nIf we take a closer look at the function call, we can see that the function has been called with 3 parameters:\r\nDsRoleGetPrimaryDomainInformation(0,1,\u0026empty_int_pointer); . the 0 refers to the lpServer parameter,\r\nmeaning the function will be called on the local computer (refer to the link above for more info on that). The 1 is\r\nthe InfoLevel parameter, which specifies the level of output needed, as well as the type of output being pushed to\r\nour empty_int_pointer . referring to Microsoft Documentation, we can see 1 refers to the first item in the C++\r\nenum, which is DsRolePrimaryDomainInfoBasic :\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 5 of 15\n\n1\r\n2\r\n3\r\n4\r\n5\r\ntypedef enum _DSROLE_PRIMARY_DOMAIN_INFO_LEVEL {\r\n DsRolePrimaryDomainInfoBasic = 1,\r\n DsRoleUpgradeStatus,\r\n DsRoleOperationState\r\n} DSROLE_PRIMARY_DOMAIN_INFO_LEVEL;\r\nIf we follow the docs, it’ll mention our output type as DSROLE_PRIMARY_DOMAIN_INFO_BASIC , and refers to this page.\r\nLooks like our return value will be in this struct:\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\ntypedef struct _DSROLE_PRIMARY_DOMAIN_INFO_BASIC {\r\n DSROLE_MACHINE_ROLE MachineRole;\r\n ULONG Flags;\r\n LPWSTR DomainNameFlat;\r\n LPWSTR DomainNameDns;\r\n LPWSTR DomainForestName;\r\n GUID DomainGuid;\r\n} DSROLE_PRIMARY_DOMAIN_INFO_BASIC, *PDSROLE_PRIMARY_DOMAIN_INFO_BASIC;\r\nclearly the attack is interested in MachineRole , and compares it with value 5 . Let’s dig deeper to see what 5\r\nmeans. If we go to this doc, we’ll see the following enum :\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\ntypedef enum _DSROLE_MACHINE_ROLE {\r\n DsRole_RoleStandaloneWorkstation,\r\n DsRole_RoleMemberWorkstation,\r\n DsRole_RoleStandaloneServer,\r\n DsRole_RoleMemberServer,\r\n DsRole_RoleBackupDomainController,\r\n DsRole_RolePrimaryDomainController\r\n} DSROLE_MACHINE_ROLE;\r\n5 is the primary Domain Controller. Looking at the code, you can see the attacker does not intend to attack the\r\nprimary DC, and will skip them.\r\nAfter getting all the info, I started to rename the functions and add a bit of comment, as well as converting types in\r\nGhidra to make sure it’s readable:\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 6 of 15\n\nNow we can see there’s a wiper function, which runs on C:\\\\Users as well as D:\\\\ for 24 chars ( E:\\\\, F:\\\\,\r\n... ), which means basically all drive letters.\r\nlet’s go take a look at the wiper function. That’s where the attacker’s malicious code is located.\r\nThe function itself is a void one. Meaning the attacker didn’t really care if the wiping is successful or not. Reading\r\na bit of the function itself, the first bit of interesting information is seen at line ~180. There seems to be another\r\nfunction, that gets called with both * and \\\\ values.\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 7 of 15\n\n1\r\n2\r\nFUN_00402a80((int)local_ccc,param_1,\u0026local_e44);\r\nFUN_00402a80((int)local_89c,local_ccc,\u0026local_e20);\r\nAfter digging around the wipe function, you can see kernel32.dll as a stack string with these functions being\r\ncalled from it (in order):\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 8 of 15\n\n1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\nFindFirstFileA\r\nFindNextFileA\r\nCreateFileA\r\nGetFileSize\r\nLocalAlloc\r\nSetFilePointer\r\nWriteFile\r\nLocalFree\r\nCloseHandle\r\nFindClose\r\nAll above functions are thoroughly documented in Microsoft’s official Win32 API Docs\r\nEssentially, the wiper is looking for all the files under C:\\Users and D: through Z: and tries to enumerate the\r\nfirst file within those directories (with FindFirstFileA ), then enumerates through the folders with FindNextFileA ,\r\nopens the file, scrambles the header of each file, and does it across all folder recursively. Here’s the main wiper\r\nfunction with function names and syscalls somewhat renamed to a more readable format\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 9 of 15\n\nBefore we rename this function to something human-readable, we should know what it does. Here’s the pseudo-code\r\nof the function itself:\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 10 of 15\n\nThe function appears to concat two strings together with a couple of while loops and put them in the first\r\nparameter’s pointer. in python terms, it basically means param_1 = param_2 + param_3 . From now on, I’ll refer to\r\nFUN_00402a80 as concat\r\nAfter concatenating the paths with * and \\\\ , FUN_00401530 gets called with two parameters: findFirstFileA\r\nand kernel32.dll , as specified in lines directly after calling the two concat functions (line 190 to 200 inside the\r\nwipe function in Ghidra).\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 11 of 15\n\nEven though the logic of the function seems complicated, from what it gets and produces as an output, it’s safe to\r\nassume the function is a Win32 API client. The DLL filename as well as the specific functionality is pushed to the\r\nfunction and the result is an integer that corresponds to the API response code. From now on, I’ll refer to\r\nFUN_00401530 as syscall_wrapper\r\nUsing the same trick we did before, it’s easy to see this function using the same syscall_wrapper to invoke multiple\r\nfunctions from advapi32.dll :\r\n1\r\n2\r\nSetEntriesInAclA\r\nAllocateAndInitializeSid\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 12 of 15\n\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\nSetNamedSecurityInfoA\r\nGetCurrentProcess\r\nOpenProcessToken\r\nSeTakeOwnershipPrivilege\r\nFreeSid\r\nLocalFree\r\nCloseHandle\r\nThis function looks to be looking into each particular file’s ownership and tries to get around some ACLs and “access\r\ndenied” errors that it comes across. I would describe it as a basic way to try to make a file writable enough so it can\r\ndestroy it. Although I didn’t read each individual syscall to back that claim. FUN_00401750 is the main carrier of this\r\noperation. In FUN_00401750 , we can see the following functions:\r\n1\r\n2\r\n3\r\nLookupPrivilegeValueA\r\nAdjustTokenPrivileges\r\nGetLastError\r\nFUN_00401750 simply tries to see if the malware has enough permission to change permissions on a file. I’ll rename\r\nit to priv_check .\r\nAs a result, based on my guess, FUN_00401a10 is renamed to priv_set\r\nThis is a small Malware sample, and it’s effective and fast. In a nutshell, this is what the attack vector had in mind\r\nChecks if the Computer is a primary domain controller or not. If not, it leaves it behind and doesn’t wipe it.\r\nIt identifies C:\\Users and D: through Z: as primary attack targets\r\nRecursively:\r\nFinds the first file in the folder\r\nTries to see the permission it has to write to the file\r\nTries elevating privileges to gain permission to write to the file\r\nOpens the file in write mode\r\nrewrites the file header with gibberish\r\nClose the file\r\nInterestingly, If you run the binary through something like the strings command, you’ll only see a few strings, like\r\nso\r\n1\r\n2\r\n3\r\n4\r\n5\r\nstrings a294620543334a721a2ae8eaaf9680a0786f4b9a216d75b55cfd28f39e9430ea.exe\r\n!This program cannot be run in DOS mode.\r\nRich%\r\n.text\r\n`.rdata\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 13 of 15\n\n6\r\n7\r\n8\r\n@.reloc\r\nDsRoleGetPrimaryDomainInformation\r\nNETAPI32.dll\r\nThis is because the attacker is making use of stack strings . This link has a good explanation of what are stack\r\nstrings and how are they used to avoid detection.\r\nThe easiest detection for this particular sample could be a hash value. But since this malware is small, hashes, even\r\nssdeep are not a very good idea. Let’s try to build a YARA rule that defines what we learned from the malware.\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\nrule caddywiper {\r\n meta:\r\n author = \"Ali Mosajjal\"\r\n email = \"hi@n0p.me\"\r\n license = \"Apache 2.0\"\r\n description = \"Caddy Wiper Stack String Detection\"\r\n strings:\r\n $s1 = /F.{6}i.{6}n.{6}d.{6}F.{6}i.{6}r.{6}s.{6}t.{6}F.{6}i.{6}l.{6}e.{6}A/ // FindFirstFileA\r\n $s2 = /F.{6}i.{6}n.{6}d.{6}N.{6}e.{6}x.{6}t.{6}F.{6}i.{6}l.{6}e.{6}A/ // FindNextFileA\r\n $s3 = /C.{6}r.{6}e.{6}a.{6}t.{6}e.{6}F.{6}i.{6}l.{6}e.{6}A/ // CreateFileA\r\n $s4 = /G.{6}e.{6}t.{6}F.{6}i.{6}l.{6}e.{6}S.{6}i.{6}z.{6}e/ // GetFileSize\r\n $s5 = /L.{6}o.{6}c.{6}a.{6}l.{6}A.{6}l.{6}l.{6}o.{6}c/ // LocalAlloc\r\n $s6 = /S.{6}e.{6}t.{6}F.{6}i.{6}l.{6}e.{6}P.{6}o.{6}i.{6}n.{6}t.{6}e.{6}r/ // SetFilePointer\r\n $s7 = /W.{3}r.{3}i.{3}t.{3}e.{3}F.{3}i.{3}l.{3}e/ // WriteFile\r\n $s8 = /L.{6}o.{6}c.{6}a.{6}l.{6}F.{6}r.{6}e.{6}e/ // LocalFree\r\n $s9 = /C.{6}l.{6}o.{6}s.{6}e.{6}H.{6}a.{6}n.{6}d.{6}l.{6}e/ // CloseHandle\r\n $s10 = /F.{3}i.{3}n.{3}d.{3}C.{3}l.{3}o.{3}s.{3}e/ // FindClose\r\n condition:\r\n all of ($s*) and filesize \u003c 100KB\r\n}\r\nAs we saw, since the attacker was clever enough to use Stack String, our YARA rule is going to be slow and regex-y\r\nbut it still works. Interestingly, for WriteFile and FindClose I had to adjust my regex to factor in the slightly\r\nsmaller MOV assembly code. I’ve also put a file size cap on the sample to ignore potentially different variants of this\r\nmalware.\r\nAs an exercise, you can create similar detection for the dll files, which are a bit trickier considering they’re both\r\nwide strings and Stack Strings.\r\nHope you enjoyed this brief analysis. I’ll put the Ghidra zipped file alongside the scripts, comments etc in a Github\r\nRepo if anyone is interested. Let me know what Malware should I dissect next :)\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 14 of 15\n\nSource: https://n0p.me/2022/03/2022-03-26-caddywiper/\r\nhttps://n0p.me/2022/03/2022-03-26-caddywiper/\r\nPage 15 of 15",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://n0p.me/2022/03/2022-03-26-caddywiper/"
	],
	"report_names": [
		"2022-03-26-caddywiper"
	],
	"threat_actors": [],
	"ts_created_at": 1775434956,
	"ts_updated_at": 1775791295,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/9ef79834d68d835150fcad93974ea68546eb833c.pdf",
		"text": "https://archive.orkl.eu/9ef79834d68d835150fcad93974ea68546eb833c.txt",
		"img": "https://archive.orkl.eu/9ef79834d68d835150fcad93974ea68546eb833c.jpg"
	}
}