{
	"id": "06f02d02-14bd-4bd8-86db-91df73170192",
	"created_at": "2026-04-06T00:09:35.010157Z",
	"updated_at": "2026-04-10T03:24:24.022913Z",
	"deleted_at": null,
	"sha1_hash": "1075e09083e7927b5585de3909316e5622d0bc6b",
	"title": "Binary Ninja - Reverse Engineering a Cobalt Strike Dropper With Binary Ninja",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2696965,
	"plain_text": "Binary Ninja - Reverse Engineering a Cobalt Strike Dropper With\r\nBinary Ninja\r\nBy Xusheng Li\r\nArchived: 2026-04-05 17:32:40 UTC\r\nIn this blog post, I will explain how I reverse engineered a Cobalt Strike dropper and obtained its payload. The\r\npayload is a custom executable file format based on DLL. The dropper decrypts, loads, and executes the payload.\r\nInitially, I thought this must not be a PE executable at all, but I gradually realized it was. Much of the effort was\r\nspent on fixing the file so it could be loaded by Binary Ninja for further analysis.\r\nFirst Impressions\r\nA friend of mine shared with me this sample. It is an x86 PE binary that is 284kB in size. After loading it into\r\nBinary Ninja, I saw it was not packed or encrypted by any well-known packer or protector. However, there were\r\nonly dozens of functions recognized, which is quite a small number relative to its size. This suggested the sample\r\nwas packed by a custom packer/encryptor.\r\nAs is routine for malware analysis, I started by executing the sample in an online sandbox. In this case, I used\r\nTriage. The sample executed fine in the sandbox and was recognized as cobaltstrike .\r\nThen, I uploaded the sample to UnpacMe to see if it could be unpacked automatically. UnpacMe also processed\r\nthe sample and recognized it as Cobalt Strike, but the unpacked artifact did not make any sense.\r\nAt this point, I realized I wasn’t going to get much further without analyzing the sample with Binary Ninja to see\r\nhow it worked.\r\nThread and Pipe\r\nThe sample seemed to be compiler-generated and not obfuscated, so I decided to mainly analyze the sample in\r\nHLIL. Viewing code in HLIL can often speed up analysis. However, for handwritten or obfuscated code, I prefer\r\nto look at the disassembly, which offers a closer view of what is happening. Binary Ninja now supports split\r\nviews, so we can conveniently view HLIL and disassembly side-by-side:\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 1 of 19\n\nThe main function is rather short. The first function call is part of the runtime and it is doing some initialization\r\nwhich we can ignore. The next function creates a new thread within it which we will analyze later. Then it enters\r\ninto a loop that calls Sleep(10000) indefinitely.\r\nAs a note, the sample is stripped so it does not contain any function or variable names in it (except the Windows\r\nAPI imports). All names in the following screenshots were recovered or created during reverse engineering.\r\nThe create_thread function is also not complex. It formats a string using values derived from GetTickCount ,\r\nprobably to make it random and avoid conflict. This string is later used as a name for a pipe. Then it creates a new\r\nthread by calling CreateThread .\r\nThe thread_proc pushes two arguments onto the stack, and then calls write_into_pipe .\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 2 of 19\n\nThe write_into_pipe creates a named pipe using the randomized string, connects to it, and writes the buffer into\r\nit.\r\nI quickly noticed size_of_data is huge – 0x33400 bytes. Almost the entire sample is made up of this huge\r\nbuffer. This suggested the buffer was encrypted or compressed, and the dozens of functions that we see merely\r\nrestore the code to its original content. Typically, at the end of it, execution will be handed to the\r\ndecrypted/decompressed buffer.\r\nAt this point, we are only seeing the data being written into the named pipe. We cannot see how it is being\r\naccessed.\r\nDecrypting the Buffer\r\nAfter browsing the code, I realized that there was a function call at the end of create_thead that I had originally\r\nignored.\r\nThis function first uses malloc to allocate a buffer of the same size as the data written into the named pipe. It\r\nthen loops and reads the content of the buffer. At the end of it, it decrypts the code and executes it.\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 3 of 19\n\nThe decryption function first calls VirtualAlloc to allocate a buffer and sets its permission to PAGE_READWRITE .\r\nThen, it XORs the content with a four-byte hard-coded key. The key is 72432a9c , in this case. Near the end of\r\nthe function, it sets the permission of the buffer to PAGE_EXECUTE_READ . Finally, it creates another thread, which\r\njust jumps to its first argument. The address of the buffer is passed as the first argument. This starts execution\r\nfrom the beginning of the buffer. The code could, of course, have used the address of the buffer as the entry point\r\nof the thread. However, that might cause anti-virus software to detect it, so it used this small trick instead to\r\ndisguise it.\r\nSo, in order to analyze the code of the payload, I needed to first decrypt the buffer by XORing with the four-byte\r\nkey. There are two ways to do this. The first is to select the buffer, right-click, and then click Transform -\u003e XOR .\r\nThis is not super convenient in this case as the input buffer is huge and selecting it with a precise size is not easy.\r\nThe second way is to use the Python API, which is what I did:\r\ndata = bv.read(0x403014, 0x33400)\r\nxor = Transform['XOR']\r\noutput = xor.encode(data, {'key': b'\\x72\\x43\\x2a\\x9c'})\r\nbv.write(0x403014, output)\r\nBefore I discuss analyzing the code in this buffer, there was a function that I initially did not quite understand. See\r\nthe name I give it – preparation ? I guessed it was doing some final preparation before executing the buffer. The\r\nHLIL for the function was also not very easy to read. However, after switching to disassembly and reading the\r\ninstructions one by one, there came an “A-ha!” moment.\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 4 of 19\n\nThis function first tests whether two signed DWORDs are positive. If both of them are larger than 0, they are\r\ntreated as offsets into the buffer. The code takes the address of functions GetModuleHandleA and\r\nGetProcessAddress and writes their addresses at the given offsets. In other words, it does the following:\r\n*(uint32_t)(buffer + 0x7c71) = GetModuleHandleA;\r\n*(uint32_t)(buffer + 0x7c78) = GetProcessAddress;\r\nWhy would the code write the address of these two functions into the middle of the buffer? Well, it is passing the\r\nfunction pointer into the code so that it can be used by it. This is a clever trick because the author does not have to\r\nuse other (more complex) techniques to obtain these values while maintaining a low footprint in AV’s eye.\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 5 of 19\n\nViewing the original content at those offsets confirms my guess:\r\nThe original value at the two offsets is 0x41414141 and 0x42424242 , which are obviously placeholder values.\r\nWe can fix the values by writing the actual address of the two functions here. This can be done by hand, or using\r\nthe following Python code:\r\naddr = bv.get_symbols_by_name('GetModuleHandleA')[0].address\r\nbv.write(0x403014 + 0x7c71, struct.pack('\u003cI', addr))\r\naddr = bv.get_symbols_by_name('GetProcAddress')[0].address\r\nbv.write(0x403014 + 0x7c78, struct.pack('\u003cI', addr))\r\nIf we redefine their types to void* , we can see the effect:\r\nAlright, with the two values fixed, we are ready to analyze the code in the buffer.\r\nFinding Address of Windows APIs\r\nI noticed the buffer started with PE as soon as it was decrypted. If this were actually a PE binary, we would\r\nsimply need to dump it and load it with Binary Ninja. However, according to my analysis, this buffer is executed\r\nfrom the beginning. So, I quickly ruled out the possibility of this file being a PE. It must be a trick to confuse the\r\nanalyst.\r\nDefining a function at the entry point also produces meaningful code:\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 6 of 19\n\nAs we can see, the byte 0x4d5a (PE) corresponds to dec ebp; pop edx and their effects are immediately\r\nundone by the following two instructions: push edx; inc ebp . Now, I am even more confident that this is not a\r\nPE, and I did not fall into the trap of the developer.\r\nThe next few instructions show a common way of getting the value of the eip register and then calculate an\r\naddress based on it:\r\n00403018 e800000000 call $+5 {data_40301d}\r\n0040301d 5b pop ebx\r\n......\r\n00403023 81c3497c0000 add ebx, 0x7c49 {load_DLL_find_API}\r\n00403029 ffd3 call ebx {load_DLL_find_API}\r\nBinary Ninja understands this technique, so it calculates and annotates the value of ebx at the call site. This is\r\nbased on our dataflow analysis.\r\nMoving on to function load_DLL_find_API , we can see the address of GetModuleHandleA and GetProcAddress\r\nare loaded into two stack variables, and their current values are checked against the placeholder values, i.e.,\r\n0x41414141 and 0x42424242 .\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 7 of 19\n\nIf their current values are different from the placeholder values, the following function is executed:\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 8 of 19\n\nThese are all DLL and Windows API names. The function first finds LoadLibraryA , and then loads the needed\r\nDLLs. It also gets the addresses of the Windows API by GetProcessAddress . The addresses of these API calls\r\nare put into a function pointer array in the following order:\r\nGetModuleHandleA\r\nGetProcAddress\r\nLoadLibraryA\r\nLoadLibraryExA\r\nVirtualAlloc\r\nVirualProtect\r\nAn interesting behavior is the code zeros the strings of these API names, as seen below:\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 9 of 19\n\nThis is another anti-virus evasion technique.\r\nIs this a PE?\r\nSince the code is quite long, I will summarize its behavior. After the above function returns, the sample does the\r\nfollowing:\r\nAllocates a buffer, whose size is read from a particular offset in the buffer\r\nReads section information from a section table, allocates a buffer for them, and copies the content of each\r\nsection into the buffer\r\nLoads some DLLs specified at certain offsets in the buffer and resolve API names\r\nSome other things that aren’t important to our analysis\r\nThese operations very similar to loading an executable/library. Since I have ruled out this is a PE previously, I\r\nthink this sample has a custom executable format. If that is the case, then I have to write a Binary View to load it.\r\nHowever, as I read the code more carefully, I started to realize this is a PE, though with some changes:\r\nThe section names are XOR-ed with byte 0xc3\r\nThe DLL names and function names are XOR-ed with 0xc3\r\nThe .text section is XOR-ed with byte 0xc3\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 10 of 19\n\nSo, it turns out I have indeed been fooled by the developer: I incorrectly thought it was not a PE, whereas it turns\r\nout this is a modified PE format. The good news is I realized this fairly quickly and did not waste any time on\r\nwriting a unnecessary loader for it.\r\nI dumped the buffer to disk. Next, I needed to fix it so I could load it into Binary Ninja and analyze it.\r\nThe section names and .text section were easier to deal with. There are only a few sections, so manually XOR-ing the names was fast enough. I XOR-ed the entire .text section with the Transform API, as shown above.\r\nThe next problem was resolving DLL and API names. I tried to dump the file after the names were decrypted.\r\nHowever, it did not work because the sample copied the encrypted names into a buffer and then decrypted them.\r\nThis buffer was also reused to decrypt different names. So, dumping it did not help me.\r\nI decided to deal with this using Binary Ninja’s Python API.\r\nFixing the Payload DLL\r\nLet us first revisit the PE file format and see how we can find the addresses of the DLL and function names.\r\nThere are 16 PE_Data_Directory_Entry at the end of the PE32_Optional_Header . The import table is the second\r\nentry in it. The PE_Data_Directory_Entry contains the RVA (relative virtual address) and size of the table.\r\nOnce we calculate the VA (virtual address) of the import table from its RVA, there are multiple\r\nImport_Directory_Table s there. The number of entries is not specified – its end is marked by a structure whose\r\nvalues are NULL.\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 11 of 19\n\nIf we view the import table of the sample (the original one, not the one we have dumped), there are two entries in\r\nit. Each of these represents a DLL import and multiple function imports. The nameRva field is the RVA of the\r\nDLL name, so we can find the DLL names base on this.\r\nThe function names are slightly more complex. We need to follow the importLookupTableRva to get the INT\r\n(import name table).\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 12 of 19\n\nThis is an array of RVAs, each describing an API function import. Again, the number of entries in this array is not\r\nspecified – its end is marked by a value of NULL.\r\nIf we follow the VA of the first entry, we can see it comes with a two-byte ordinal of the API, followed by its\r\nname. This is how we find the names of the API.\r\nUsing BinaryReader\r\nThe entire processing script I wrote can be accessed here. Below is a walkthrough for it.\r\nWe start with the following code to find the VA of the import table:\r\nfrom binaryninja import BinaryViewType, BinaryReader\r\nbv = BinaryViewType.load('extracted_3.exe')\r\nprint(bv.start)\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 13 of 19\n\nimportTableEntry_offset = 0x100\r\nbr = BinaryReader(bv)\r\nbr.seek(bv.start + importTableEntry_offset)\r\nimport_table_va = bv.start + br.read32()\r\nbr.seek(import_table_va)\r\nTwo things are worth noting. First, many of the offsets in the PE file format are in RVA form, which are offsets\r\nfrom the start of the module. Adding bv.start to it converts the RVA to a VA.\r\nSecond, we are using the BinaryReader to read the binary. BinaryReader internally tracks the current offset, so\r\nit is very suitable for the case of consecutive reading. Of course, we can simply use bv.read() to do the job, but\r\nwe would have to track the offset by ourselves, which is more effort (and more error-prone).\r\nStrings in the PE file format are NULL-terminated. We know they are XOR-ed with a magic byte, so we need to\r\nlook for it as the end of the string:\r\ndef read_until_byte(br, offset, byte_val):\r\n old_offset = br.offset\r\n br.seek(offset)\r\n result = b''\r\n while True:\r\n c = br.read(1)\r\n result += c\r\n if ord(c) == byte_val:\r\n break\r\n br.seek(old_offset)\r\n return result\r\nRecovering the original name is very simple:\r\ndef xor(input, byte_val):\r\n result = ''\r\n for i in range(len(input)):\r\n c = chr(input[i] ^ byte_val)\r\n result += c\r\n return result\r\nThe main code is a loop that processes each DLL:\r\nwhile True:\r\n table_rva = br.read32()\r\n if table_rva == 0:\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 14 of 19\n\nbreak\r\n br.seek(br.offset + 8)\r\n name_rva = br.read32()\r\n # print('name_rva: 0x%x' % name_rva)\r\n name_va = bv.start + name_rva\r\n name = read_until_byte(br, name_va, 0xc3)\r\n restored_name = xor(name, 0xc3)\r\n print(restored_name)\r\n bv.write(name_va, restored_name)\r\n table_va = bv.start + table_rva\r\n # print(\"table_va\", hex(table_va))\r\n process_table(br, bv.start, table_va)\r\n br.seek(br.offset + 4)\r\nThe code to process each table (DLL) is also a loop:\r\ndef process_table(br, start, offset):\r\n old_offset = br.offset\r\n br.seek(offset)\r\n while True:\r\n int_rva = br.read32()\r\n if (int_rva == 0):\r\n break\r\n if (int_rva \u0026 0x80000000 != 0):\r\n continue\r\n else:\r\n int_va = start + int_rva\r\n # print('int_va', hex(int_va))\r\n process_one_entry(br, start, int_va)\r\n br.seek(old_offset)\r\nNote that if the INT RVA has its highest bit set, then this API is not imported by name. Instead, it is imported by\r\nordinal. In that case, we should skip it.\r\nFinally, we get to process an individual API name:\r\ndef process_one_entry(br, start, address):\r\n old_offset = br.offset\r\n br.seek(address)\r\n br.read16()\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 15 of 19\n\n# print('br.offset', hex(br.offset))\r\n name = read_until_byte(br, br.offset, 0xc3)\r\n restored_name = xor(name, 0xc3)\r\n print(restored_name)\r\n bv.write(address + 2, restored_name)\r\n br.seek(old_offset)\r\nOnce we are done processing, we can export the DLL to disk:\r\nThe DLL can be downloaded from here.\r\nThis sample has another trick to slow down the analyst: Its entry point offset is not read from the\r\nPE32_Optional_Header.addressOfEntryPoint (offset 0x28). Instead, it is read from the\r\nPE32_Optional_Header.loaderFlags (offset 0x70). To fix this, we simply change the value of\r\naddressOfEntryPoint accordingly.\r\nNow, we can load the extracted DLL into Binary Ninja and analyze it. We can see all the Windows APIs it\r\nimports.\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 16 of 19\n\nThere is a giant switch statement in it (with 0x65 case s), which handles different commands. Analyzing each\r\nof them is beyond the scope of this blog post.\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 17 of 19\n\nHowever, since we have fixed the imports, a glance can already give us a good guess at what each might be doing.\r\nFor example, the following function is likely searching for certain files:\r\nAlright, we have successfully reverse-engineered this Cobalt Strike sample and fixed its payload DLL!\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 18 of 19\n\nSource: https://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nhttps://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html\r\nPage 19 of 19",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html"
	],
	"report_names": [
		"reverse-engineering-cobalt-strike.html"
	],
	"threat_actors": [
		{
			"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": 1775434175,
	"ts_updated_at": 1775791464,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/1075e09083e7927b5585de3909316e5622d0bc6b.pdf",
		"text": "https://archive.orkl.eu/1075e09083e7927b5585de3909316e5622d0bc6b.txt",
		"img": "https://archive.orkl.eu/1075e09083e7927b5585de3909316e5622d0bc6b.jpg"
	}
}