{
	"id": "4f173429-5d7c-4ded-b728-62d351339827",
	"created_at": "2026-04-06T03:37:19.936453Z",
	"updated_at": "2026-04-10T03:29:31.773026Z",
	"deleted_at": null,
	"sha1_hash": "2b0f98a7f219d4595c74cf9314c7f8ae6c3dcd28",
	"title": "Building A Custom Tool For Shellcode Analysis",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1484512,
	"plain_text": "Building A Custom Tool For Shellcode Analysis\r\nBy Daniel Bunce\r\nPublished: 2019-10-31 · Archived: 2026-04-06 03:17:36 UTC\r\nThe Zero2Hero malware course continues with Daniel Bunce demonstrating how to write a custom tool to load,\r\nexecute and debug malicious shellcode in memory.\r\nRecently, FireEye posted a blog post detailing malware used by APT 41, specifically the DEADEYE initial first\r\nstage, capable of downloading, dropping or loading payloads on an infected system, and the LOWKEY backdoor.\r\nAdditionally, they described an additional “RC4 Layer”, which is Position Independent Code (PIC) that RC4\r\ndecrypts an embedded payload and loads it into memory using it’s reflective DLL loader capabilities.\r\nUnlike Windows executables, shellcode doesn’t have any headers, meaning the Windows loader cannot execute\r\nstandalone shellcode. As a result, debugging is impossible without an external tool to load and execute shellcode\r\nfor you. Therefore, in this blog post, we will cover how to write a tool in C to load shellcode into memory and\r\nwait until a debugger is attached before executing it. But first, why do we need to debug shellcode?\r\nhttps://www.sentinelone.com/blog/building-a-custom-tool-for-shellcode-analysis/\r\nPage 1 of 6\n\nWhy Debugging Position Independent Shellcode is Useful\r\nPosition Independent Code can be executed anywhere inside memory, without any issues. This means there are no\r\nhardcoded addresses and no calls to APIs such as GetProcAddress or LoadLibrary , not to mention a few other\r\ncomplications.\r\nAs a result, static analysis of the shellcode can take a while to fully understand as the shellcode is forced to\r\nmanually lookup addresses that may not be known without debugging. Furthermore, plenty of malware utilizes\r\nhashing when looking up APIs, so a hash will be passed into a function that will hash each export of the DLL in\r\nquestion, until a matching pair is found. Whilst the hashing routine can be replicated so that the correct API is\r\nfound without static analysis, there are many hashing algorithms out there, from CRC hashing up to custom\r\nhashing algorithms. This means there will be plenty of situations where you will have to update the script to\r\ninclude an additional algorithm, slowing down analysis further. So why not just load it into memory yourself and\r\nexecute when a debugger is attached?\r\nWell, you can. The guys at OALabs have created an extremely helpful tool called BlobRunner to do exactly that.\r\nHowever, rather than simply use a pre-existing tool, in this post we’re going to be focusing on writing our own, as\r\nit’s not too difficult to do so, and shellcode execution isn’t uncommon inside malware, so knowing the internals of\r\nhow it works will help when it comes to recognizing it inside of a sample.\r\nOverview of the ShellCode Analysis Tool’s Routine\r\nSo, the tool we’re writing needs to be able to read the shellcode from a file, allocate a region of memory large\r\nenough to accomodate the shellcode, write the shellcode into said region of memory, wait until a debugger has\r\nbeen attached, and then execute it. As it is for debugging and not behavioural analysis, we need to wait until a\r\ndebugger has been attached, to prevent it running before attaching to it. The shellcode is also 64 bit, and as Visual\r\nC++ compiler does not support inline assembly for 64 bit applications, we’ll have to use CreateThread() to\r\nexecute the shellcode, rather than using a jmp . Anyway, with that covered, let’s take a look at what the main()\r\nfunction will look like.\r\nWriting the main() Function\r\nThis function is setup to accept two arguments: the filename, and the entry point offset.\r\nhttps://www.sentinelone.com/blog/building-a-custom-tool-for-shellcode-analysis/\r\nPage 2 of 6\n\nIn the case of the loader, without analyzing what executes it, the entry point seems to be at the offset 0x80. If we\r\ndon’t pass in a second argument (the offset), the program will attempt to execute it from the offset 0x00. The\r\noffset is converted to an integer using strtol() , and then the function read_into_memory() is called, which, as\r\nthe name suggests, reads the shellcode into process memory. This returns the base address of the shellcode, which\r\nis then passed into the function execution() , along with the entry point offset. As you can see, the layout is\r\nfairly simple – unlike loading an executable in process memory, we don’t need to map the sections of shellcode\r\ninto memory, and can simply execute it once it has been copied into memory. With that said, let’s move on to the\r\nother functions that we need.\r\nWriting the read_into_memory() Function\r\nThis function is responsible for loading the shellcode from a file into an allocated region of memory.\r\nhttps://www.sentinelone.com/blog/building-a-custom-tool-for-shellcode-analysis/\r\nPage 3 of 6\n\nTo do this, we first need the size of the shellcode, which we can get using fseek() and ftell() . This is then\r\npassed into VirtualAlloc() , along with PAGE_EXECUTE_READWRITE , allowing us to execute that region of\r\nmemory without calling VirtualProtect() . The return address is put into the buffer variable, which is then\r\npassed into fread() , along with the handle to the open file. After the shellcode has been read into memory, we\r\nreturn the address in buffer back to the calling function, for use in the execution() function, which we will\r\nmove onto now.\r\nWriting the execution() Function\r\nThe execution() function is the most important part of the entire tool, as without it we’d still only have static\r\nshellcode.\r\nhttps://www.sentinelone.com/blog/building-a-custom-tool-for-shellcode-analysis/\r\nPage 4 of 6\n\nTo begin with, we add the entry point offset to the base address of the shellcode, so if the allocated region of\r\nmemory was at 0x20000, the actual entry point would be 0x20080 for our LOWKEY reflective DLL loader. We\r\nthen pass this into a call to CreateThread() , along with the value 0x4, which indicates we want to create the\r\nthread in a suspended state. This is done to prevent execution of the shellcode whatsoever, and we only resume the\r\nthread once a debugger is attached. This is done through a pretty simple while() loop that constantly calls\r\nIsDebuggerPresent() until one is detected. From there, the thread is resumed with a call to ResumeThread() ,\r\nand then we enter another loop that will call WaitForSingleObject() every second to check if the thread has\r\nexited. If so, we close the handle and return from the function!\r\nCompile Time!\r\nThat pretty much wraps up the tool, so let’s go ahead and compile it to test it out!\r\nhttps://www.sentinelone.com/blog/building-a-custom-tool-for-shellcode-analysis/\r\nPage 5 of 6\n\nAs you can see, everything runs smoothly and we’re able to see which API each hash represents, a lot faster than\r\nif we were writing a script to calculate it!\r\nSo, now we know how to read and execute shellcode in memory, recognizing it in malware should be fairly easy!\r\nIn this case, the DEADEYE payload that executes the shellcode is packed and protected with VMProtect, making\r\nit very difficult to locate the function responsible for loading and executing the payload; however, the unpacked\r\npayloads can be found on Malpedia here for those that have an account.\r\nSource: https://www.sentinelone.com/blog/building-a-custom-tool-for-shellcode-analysis/\r\nhttps://www.sentinelone.com/blog/building-a-custom-tool-for-shellcode-analysis/\r\nPage 6 of 6",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"references": [
		"https://www.sentinelone.com/blog/building-a-custom-tool-for-shellcode-analysis/"
	],
	"report_names": [
		"building-a-custom-tool-for-shellcode-analysis"
	],
	"threat_actors": [
		{
			"id": "c7d9878a-e691-4c6f-81ae-84fb115a1345",
			"created_at": "2022-10-25T16:07:23.359506Z",
			"updated_at": "2026-04-10T02:00:04.556639Z",
			"deleted_at": null,
			"main_name": "APT 41",
			"aliases": [
				"BrazenBamboo",
				"Bronze Atlas",
				"Double Dragon",
				"Earth Baku",
				"G0096",
				"Grayfly",
				"Operation ColunmTK",
				"Operation CuckooBees",
				"Operation ShadowHammer",
				"Red Kelpie",
				"SparklingGoblin",
				"TA415",
				"TG-2633"
			],
			"source_name": "ETDA:APT 41",
			"tools": [
				"9002 RAT",
				"ADORE.XSEC",
				"ASPXSpy",
				"ASPXTool",
				"AceHash",
				"Agent.dhwf",
				"Agentemis",
				"AndroidControl",
				"AngryRebel",
				"AntSword",
				"BLUEBEAM",
				"Barlaiy",
				"BlackCoffee",
				"Bladabindi",
				"BleDoor",
				"CCleaner Backdoor",
				"CHINACHOPPER",
				"COLDJAVA",
				"China Chopper",
				"ChyNode",
				"Cobalt Strike",
				"CobaltStrike",
				"Crackshot",
				"CrossWalk",
				"CurveLast",
				"CurveLoad",
				"DAYJOB",
				"DBoxAgent",
				"DEADEYE",
				"DEADEYE.APPEND",
				"DEADEYE.EMBED",
				"DEPLOYLOG",
				"DIRTCLEANER",
				"DUSTTRAP",
				"Derusbi",
				"Destroy RAT",
				"DestroyRAT",
				"DodgeBox",
				"DragonEgg",
				"ELFSHELF",
				"EasyNight",
				"Farfli",
				"FunnySwitch",
				"Gh0st RAT",
				"Ghost RAT",
				"HDD Rootkit",
				"HDRoot",
				"HKDOOR",
				"HOMEUNIX",
				"HUI Loader",
				"HidraQ",
				"HighNoon",
				"HighNote",
				"Homux",
				"Hydraq",
				"Jorik",
				"Jumpall",
				"KEYPLUG",
				"Kaba",
				"Korplug",
				"LATELUNCH",
				"LOLBAS",
				"LOLBins",
				"LightSpy",
				"Living off the Land",
				"Lowkey",
				"McRAT",
				"MdmBot",
				"MessageTap",
				"Meterpreter",
				"Mimikatz",
				"MoonBounce",
				"MoonWalk",
				"Motnug",
				"Moudour",
				"Mydoor",
				"NTDSDump",
				"PACMAN",
				"PCRat",
				"PINEGROVE",
				"PNGRAT",
				"POISONPLUG",
				"POISONPLUG.SHADOW",
				"POTROAST",
				"PRIVATELOG",
				"PipeMon",
				"PlugX",
				"PortReuse",
				"ProxIP",
				"ROCKBOOT",
				"RbDoor",
				"RedDelta",
				"RedXOR",
				"RibDoor",
				"Roarur",
				"RouterGod",
				"SAGEHIRE",
				"SPARKLOG",
				"SQLULDR2",
				"STASHLOG",
				"SWEETCANDLE",
				"ScrambleCross",
				"Sensocode",
				"SerialVlogger",
				"ShadowHammer",
				"ShadowPad Winnti",
				"SinoChopper",
				"Skip-2.0",
				"SneakCross",
				"Sogu",
				"Speculoos",
				"Spyder",
				"StealthReacher",
				"StealthVector",
				"TERA",
				"TIDYELF",
				"TIGERPLUG",
				"TOMMYGUN",
				"TVT",
				"Thoper",
				"Voldemort",
				"WIDETONE",
				"WINNKIT",
				"WINTERLOVE",
				"Winnti",
				"WyrmSpy",
				"X-Door",
				"XDOOR",
				"XMRig",
				"XShellGhost",
				"Xamtrav",
				"ZXShell",
				"ZoxPNG",
				"certutil",
				"certutil.exe",
				"cobeacon",
				"gresim",
				"njRAT",
				"pwdump",
				"xDll"
			],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775446639,
	"ts_updated_at": 1775791771,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/2b0f98a7f219d4595c74cf9314c7f8ae6c3dcd28.pdf",
		"text": "https://archive.orkl.eu/2b0f98a7f219d4595c74cf9314c7f8ae6c3dcd28.txt",
		"img": "https://archive.orkl.eu/2b0f98a7f219d4595c74cf9314c7f8ae6c3dcd28.jpg"
	}
}