{
	"id": "9a9f4ac9-724b-4c8e-91cb-576a1a6881e6",
	"created_at": "2026-04-06T00:21:02.26368Z",
	"updated_at": "2026-04-10T03:24:24.091821Z",
	"deleted_at": null,
	"sha1_hash": "e25605de7a1dc8dc0a65e83fbfe534dde031c1fe",
	"title": "Malware-Analysis/Cobalt Strike/Indirect Syscalls.md at main · dodo-sec/Malware-Analysis",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2602984,
	"plain_text": "Malware-Analysis/Cobalt Strike/Indirect Syscalls.md at main ·\r\ndodo-sec/Malware-Analysis\r\nBy dodo-sec\r\nArchived: 2026-04-05 20:55:48 UTC\r\nAn analysis of syscall usage in Cobalt Strike Beacons\r\nThanks to the suggestion of my good friend Nat (0xDISREL), I spent the last week digging into a Cobalt Strike\r\nbeacon made with the latest leaked builder. His idea was to analyze and understand how CS approached syscalls.\r\nSample\r\nThis analysis was conducted in an x64 bit payload with the hash\r\n020b20098f808301cad6025fe7e2f93fa9f3d0cc5d3d0190f27cf0cd374bcf04 , generated by the recently leaked 4.8\r\nversion of Cobalt Strike. It's publicly available for download in unpacme. I will not go over unpacking the sample\r\nfor the sake of brevity, but doing so is pretty straightforward and shouldn't present any problems.\r\nA quick refresher\r\nBefore we get to the actual reversing, let's get a quick refresher on what system calls look like under Windows.\r\nAccording to calling convention, arguments are setup in the appropriate registers before the instruction SYSCALL\r\nis executed, handling execution to the Kernel. One of such arguments is the code for the system call (in the picture\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nPage 1 of 11\n\nabove, it's passed via the eax register). These system calls reside in ntdll and provide evasion benefits by\r\nallowing you to avoid calling APIs that are likely hooked by AV/EDR.\r\nHow Cobalt Strike does it\r\nDuring the first steps of analysis of the unpacked payload we'll come across references to qwords and calls to\r\nregisters.\r\nInspecting said qwords will lead us to the .data section, where they don't hold any values (yet).\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nPage 2 of 11\n\nInspecting other references to these addresses will land us in a function that looks a lot like an import by hash\r\nroutine - there are repeated calls to the same function, each time passing a different hexadecimal value and a\r\n.data section address among its arguments.\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nPage 3 of 11\n\nCase closed then, the empty qwords would receive pointers to the resolved API functions, right? All that's left is to\r\nidentify the hashing algorithm and start renaming things? Well, not quite. This write-up is not called \"analyzing\r\nimport by hash\", after all.\r\nLet's take a look at the function that's called before all the hashes start showing up. I've named it\r\nmw_prepare_indirect_syscalls .\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nPage 4 of 11\n\nPreparing system calls\r\nThe first part of it is run of the mill PEB walking and PE parsing to get names of exported functions. Note also\r\nthat there is a check of IMAGE_EXPORT_DIRECTORY.Name against ntdll.dll very slightly obfuscated (it's just\r\nwritten backwards and split over three cmp instructions). This tells us the author is only interested in ntdll. That\r\nmakes sense, considering they're after syscalls. There is a memset , to which we'll come back later.\r\nThe next block of code will check the function name for the prefixes Ki and Zw.If either prefix matches there is a\r\ncall to the hashing function, which is a ROR 8 ADD algorithm that iterates over each word and uses 0x52964EE9\r\nas a hardcoded XOR key.\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nPage 5 of 11\n\nA function starting with Ki will only be used if its hash matches 0x8DCD4499 ; on a 22H2 version of Windows 10\r\nI couldn't find an export from ntdll that matched such value. This routine then will act on at most one function\r\nstarting with Ki and all starting with Zw. Appropriate values will populate a structure whose address was supplied\r\nto mw_prepare_indirect_syscalls - I've decided to call it syscalls_organized_by_hash . It is described below.\r\nstruct syscalls_organized_by_hash {\r\nDWORD function_hash;\r\nDWORD ntdll_address_of_function;\r\nQWORD ptr_to_function_syscall_block;\r\n};\r\nfunction_hash is the calculated hash for the exported function; ntdll address of function is an address to\r\nthe function's code as pointed to by IMAGE_EXPORT_DIRECTORY.AddressOfFunctions ;\r\nptr_to_function_syscall_block is a pointer to the system call gadget related to said function, which resides in\r\nntdll.dll memory. Remember the memset call earlier? It's used to zero that structure out. The r13 register points\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nPage 6 of 11\n\nto it, and the additions at each address confirm the size of each struct member. After all the Zw prefixed functions\r\nare placed in the structure, an algorithm will sort their positions according to the ntdll_address_of_function ,\r\nfrom lowest to highest. After this is done, the struct will contain the hashes, addresses of functions in the ntdll\r\nexecutable and pointers to the syscall gadgets for all functions with a Zw prefix, sorted in ascending order\r\naccording to the ntdll_address_of_function values.\r\nSetting up the syscalls structure\r\nGoing back to the function that resembled import by hash with what we've learned, we can see the that\r\nmw_get_indirect_syscalls_by_hash is supplied the syscalls_organized_by_hash , alongside the hash and a\r\npointer to those empty qwords. After using the hashing algorithm to generate enums from ntdll exports, we can\r\nsolve the hashes to see which APIs they intended to get the syscall code blocks to.\r\nmw_get_indirect_syscalls_by_hash works by looking for the supplied hash in the\r\nsyscalls_organized_by_hash structure. Once that is found, it will retrieve the pointer to the syscall code block\r\nand call a function that validates said block - mw_validate_syscall_codeblock .\r\nThe way the verification works is simple. It will loop through the syscalls_organized_by_hash struct (they are\r\nactually organized by ascending order of ntdll_address_of_function , but I didn't know that back when I\r\ncreated the structure) until it finds the supplied hash. The functions are organized inside ntdll by ascending order\r\nof syscall codes - a function that uses code 0x1 is succeeded by one that uses code 0x2 and so forth. Because\r\nof this, once a hash is found the counter in edi will be equal to the syscall code. The validation function checks\r\nfor the op codes of the SYSCALL and RET instructions.\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nPage 7 of 11\n\nOnce the desired entry is found, a new structure (which I've named syscalls ) will receive a pointer to the\r\nsyscall code block, a pointer to the SYSCALL instruction and the value of the syscall code. Although the code is a\r\ndword , I've made all members of struct qwords for convenience (that way I don't need to create a member for\r\npadding between different syscalls entries). The struct is as follows:\r\nstruct syscalls {\r\nQWORD ptr_to_syscall_block;\r\nQWORD ptr_to_syscall_instruction;\r\nQWORD syscall_code;\r\n};\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nPage 8 of 11\n\nNow all that's left is use that model to generate the structure that will result from setting up the syscalls and apply\r\nit to the range of qwords that are passed to the mw_get_indirect_syscalls_by_hash function. Following cross-references to each member will lead us to places where the structure is used in the beacon code.\r\nSyscall usage\r\nLet's take a wrapper function used to get thread context as an example.\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nPage 9 of 11\n\nAccording to the value in a dword I've named use_syscalls_flag , the beacon will take one of three possible\r\napproaches.\r\nIf the flag is equal to 1, it will call the desired syscall block directly; this means getting the correct code\r\ninto eax is handled by the ntdll code.\r\nIf the flag is equal to 2, it will call a function responsible for getting the appropriate code from\r\nsyscall.syscall_code into eax and jumping to the SYSCALL instruction.\r\nIf the flag is neither 1 or 2, it will simply call an API instead.\r\nIf a syscall is made by either method, the code will return 1 in eax . Otherwise, it returns the result from the\r\nstandard API that was called. The presence of the flag leads me to think all beacons will have the mechanisms for\r\nhandling syscalls. Choosing to use indirect syscalls in the builder would simply set the appropriate flag(s) in the\r\nbinary, instead of producing a payload that doesn't handle syscalls at all.\r\nAcknowledgements\r\nNat for suggesting looking into this in the first place and providing me with a beacon I could reverse.\r\nDuchy for pointing out how to quickly unpack a beacon and for general help regarding structures created and used\r\nby the payload.\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nPage 10 of 11\n\nSource: https://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md\r\nPage 11 of 11",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt%20Strike/Indirect%20Syscalls.md"
	],
	"report_names": [
		"Indirect%20Syscalls.md"
	],
	"threat_actors": [
		{
			"id": "d90307b6-14a9-4d0b-9156-89e453d6eb13",
			"created_at": "2022-10-25T16:07:23.773944Z",
			"updated_at": "2026-04-10T02:00:04.746188Z",
			"deleted_at": null,
			"main_name": "Lead",
			"aliases": [
				"Casper",
				"TG-3279"
			],
			"source_name": "ETDA:Lead",
			"tools": [
				"Agentemis",
				"BleDoor",
				"Cobalt Strike",
				"CobaltStrike",
				"RbDoor",
				"RibDoor",
				"Winnti",
				"cobeacon"
			],
			"source_id": "ETDA",
			"reports": null
		},
		{
			"id": "610a7295-3139-4f34-8cec-b3da40add480",
			"created_at": "2023-01-06T13:46:38.608142Z",
			"updated_at": "2026-04-10T02:00:03.03764Z",
			"deleted_at": null,
			"main_name": "Cobalt",
			"aliases": [
				"Cobalt Group",
				"Cobalt Gang",
				"GOLD KINGSWOOD",
				"COBALT SPIDER",
				"G0080",
				"Mule Libra"
			],
			"source_name": "MISPGALAXY:Cobalt",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775434862,
	"ts_updated_at": 1775791464,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/e25605de7a1dc8dc0a65e83fbfe534dde031c1fe.pdf",
		"text": "https://archive.orkl.eu/e25605de7a1dc8dc0a65e83fbfe534dde031c1fe.txt",
		"img": "https://archive.orkl.eu/e25605de7a1dc8dc0a65e83fbfe534dde031c1fe.jpg"
	}
}