{
	"id": "0caf6dca-fe3f-4cb9-a69f-26ec3bc479e6",
	"created_at": "2026-04-06T00:08:46.750435Z",
	"updated_at": "2026-04-10T03:24:24.781574Z",
	"deleted_at": null,
	"sha1_hash": "261bb8540da63df744d3525406b9450c9dc2c13b",
	"title": "Analyzing Malware with Hooks, Stomps and Return-addresses",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1359101,
	"plain_text": "Analyzing Malware with Hooks, Stomps and Return-addresses\r\nBy Arash Parsa\r\nPublished: 2022-01-31 · Archived: 2026-04-05 17:06:41 UTC\r\nTable of Contents\r\n1. Introduction\r\n2. The First Detection\r\n3. The Module Stomp Bypass\r\n4. The Module Stomp Detection\r\n5. Final Thoughts\r\nIntroduction\r\nThis is the second post in my series and with this post we will focus on malware and some of their relevant\r\ndetections.  This post will focus on an interesting observation I made when creating my heap encryption and how\r\nthis could be leveraged to detect arbitrary shellcode as well as tools like cobalt strike, how those detections could\r\nbe bypassed and even newer detections can be made.\r\nSample code of a POC can be found here: https://github.com/waldo-irc/MalMemDetect\r\nThe First Detection\r\nIf you recall in the first post, our method at targeting Cobalt Strikes heap allocations was to hook the process\r\nspace and manage all allocations made by essentially what was a module with no name. Here is the code we had\r\nused as a refresher:\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 1 of 17\n\n#include\r\n#pragma intrinsic(_ReturnAddress)\r\nGlobalThreadId = GetCurrentThreadId(); We get the thread Id of our dropper!\r\nHookedHeapAlloc (Arg1, Arg2, Arg3) {\r\n LPVOID pointerToEncrypt = OldHeapAlloc(Arg1, Arg2, Arg3);\r\n if (GlobalThreadId == GetCurrentThreadId()) { // If the calling ThreadId matches our initial thre\r\n \r\n HMODULE hModule;\r\n char lpBaseName[256];\r\nif (::GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCSTR)_ReturnAddre\r\n ::GetModuleBaseNameA(GetCurrentProcess(), hModule, lpBaseName, sizeof(lpBaseName));\r\n }\r\n std::string modName = lpBaseName;\r\n std::transform(modName.begin(), modName.end(), modName.begin(),\r\n [](unsigned char c) { return std::tolower(c); });\r\n if (modName.find(\"dll\") == std::string::npos \u0026\u0026 modName.find(\"exe\") == std::string::npos) {\r\n // Insert pointerToEncrypt variable into a list\r\n }\r\n }\r\n}\r\nThe magic lines lie here:\r\nif (::GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCSTR)_ReturnAddress(), \u0026hModule)\r\n::GetModuleBaseNameA(GetCurrentProcess(), hModule, lpBaseName, sizeof(lpBaseName));\r\n}\r\nWhat we are trying to do here is take the current address our function will be returning to and attempting to\r\nresolve it to a module name using the function GetModuleHandleExA with the argument\r\nGET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS. With this flag the implication is the address we are\r\npassing is: “an address in the module” (https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandleexa). The module name will get returned and stored in the lpBaseName variable.\r\nWith the case of our thread – targeted heap encryption – this function actually returns nothing, as it cannot resolve\r\nthe return address to a module! This also means lpBaseName ends up containing nothing.\r\nAs always, let’s see what this looks like in our debugger. First, we’ll start with a legitimate call. I’ve gone ahead\r\nand hooked HeapAlloc using MinHook (https://github.com/TsudaKageyu/minhook) and am tracing the return\r\naddress of all callers. Let’s see who the first function to call our hooked malloc is:\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 2 of 17\n\nfig 1. Usage of _ReturnAddress intrinsic\r\nHere we can see within our code we use the Visual C++ _ReturnAddress() intrinsic\r\n(https://docs.microsoft.com/en-us/cpp/intrinsics/returnaddress?view=msvc-160) and store the value in a variable\r\nnamed “data”. We then pass this variable to GetModuleHandleExA in order to resolve the module name we will\r\nbe returning to.\r\nfig 2. Return address value\r\nTaking a look at data we can see it seems to have stored a valid address. Now let’s look at this address in our\r\ndisassembler.\r\nfig 3. Return address location\r\nAs you can see we are right at that “mov rbx,rax” instruction at the end of the screenshot based on the address.\r\nThat means when our hooked function completes, this is where it will return, and we can further validate this as\r\nthe correct assembly instruction we will return to as right before this is a call to RtlAllocateHeap, our hooked\r\nfunction! Using this we now know we are in the function LdrpGetNewTlsVector, that our hooked\r\nRtlAllocateHeap was just ran, and on completion, it’ll continue within LdrpGetNewTlsVector right after the call\r\nas usual. If we attempt to identify what module this function comes from, we can clearly see it is from ntdll.dll.\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 3 of 17\n\nfig 4. Return address module resolved\r\nThis works because the function maps to a DLL we appear to have loaded from disk. Because of this, Windows\r\nknows how to identify what module the function comes from. What about our shellcode though? Let’s see what\r\nthat looks like.\r\nfig 5. Shellcode return address and failed resolution\r\nSo our base name is empty because the function fails to resolve the address to a module. Let’s see what that\r\naddress looks like in the disassembler:\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 4 of 17\n\nfig 6. Shellcode return address location\r\nThere’s our address at “test rax,rax”. We actually know this is our shellcode based on the address:\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 5 of 17\n\nfig 7. Shellcode in process hacker\r\nfig 8. Shellcode region in process hacker\r\nWithin process hacker we can see our MZ header and that the location we are returning to is within the address\r\nspace of our shellcode. We can also see unlike other modules like ntdll.dll, in ProcessHacker the “use” column is\r\nempty for our shellcode:\r\nfig 9. Use section for shellcode is empty\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 6 of 17\n\nfig 10. Use section for DLLs is filled\r\nThis is because our arbitrarily allocated memory does not map to anything on disk. Because of this, when we\r\nattempt to resolve the return address to a module we get nothing returned as a result.\r\nThat being said, we can see instances of RWX memory that don’t map to disk in processes that use JIT compilers\r\nsuch as C# and browser processes as well. You can see in stage 3 of the Managed Execution Process\r\n(https://docs.microsoft.com/en-us/dotnet/standard/managed-execution-process) that an additional compiler takes\r\nthe C# code a user creates and turns it into native code (which means our C# IL now becomes native\r\nassembly). For this process to take place a RWX region needs to be allocated for it to be able to write the new\r\ncode and also be able to execute it. We can see these RWX regions in C# processes with ProcessHacker.\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 7 of 17\n\nfig 11. JIIT Compiler RWX sections\r\nAbove you can see a small sample of these RWX sections within my Microsoft.ServiceHug.Controller.exe\r\nprocess. This means that in theory we could see false positives from JIT compiler-based languages that run any of\r\nour hooked functions from these memory regions. Additionally, this means these sorts of processes can also be\r\ngreat spaces to hide your RWX malware, as Private Commit RWX regions are otherwise considered suspicious (as\r\nwe have executable memory that doesn’t map to anything on disk).\r\nOutside of blending in with JIT processes though, let’s discuss another simple bypass to this, one that exists within\r\nCobalt Strikes own C2 profile even.\r\nThe Module Stomp Bypass\r\nIf we think back to the original detection, we were able to observe executablememory calling our hooked\r\nfunctions that couldn’t resolve to any module name. A first thought may be “what is a mechanism to bypass this”\r\nas one must exist. Several exist in fact, but we can start with a simple one, a mechanism called “Module\r\nStomping” (https://www.forrest-orr.net/post/malicious-memory-artifacts-part-i-dll-hollowing as well as\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 8 of 17\n\nhttps://www.ired.team/offensive-security/code-injection-process-injection/modulestomping-dll-hollowing-shellcode-injection).\r\nWhat this technique effectively does is load a DLL that our process doesn’t currently have loaded and hollow out\r\nits memory regions to contain the data for a malicious DLL of ours instead. This would make it so all our calls\r\nnow appear to be coming from this legitimate module!\r\nThe section in your malleable C2 profile (for Cobalt Strike) that you would have to edit is the following:\r\nset allocator \"VirtualAlloc\"; # HeapAlloc,MapViewOfFile, and VirtualAlloc.\r\n# Ask the x86 ReflectiveLoader to load the specified library and overwrite\r\n# its space instead of allocating memory with VirtualAlloc.\r\n# Only works with VirtualAlloc\r\nset module_x86 \"xpsservices.dll\";\r\nset module_x64 \"xpsservices.dll\";\r\nThese settings can be observed in the old reference profile here: https://github.com/rsmudge/Malleable-C2-\r\nProfiles/blob/master/normal/reference.profile. By changing your allocator to “VirtualAlloc” and enabling the set\r\nmodule_x86 and x64 settings you can now allocate your Cobalt Strike payload to arbitrary modules you load\r\ninstead of arbitrarily allocated executable memory space.\r\nLet’s change the setting and see what this looks like. We will simply run an unstaged Cobalt Strike EXE and\r\nobserve for this experiment.\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 9 of 17\n\nfig 12. Cobalt Strike module stomp\r\nLet’s go ahead and run this with our module name resolver and see what it looks like. Since the name should\r\nalways resolve, now we will change the logic a bit to monitor only xpsservices.dll.\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 10 of 17\n\nfig 13. New code to monitor xpsservices\r\nfig 14. Name resolved properly\r\nHere we can see the new stomped DLL calling our hooked malloc, and that our code can successfully resolve calls\r\nto this module. If we look at the print statements, we would also see all the calls – from anything that doesn’t map\r\nto modules that have disappeared.\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 11 of 17\n\nfig 15. Only module callers\r\nAnd finally, we can see in the above screenshot that no callers without module names are observed anymore as all\r\nof Cobalt Strike’s calls now map to a module on disk, a simple bypass. So now we ask if this technique can be\r\ndetected as well, and of course, there’s a few ways.\r\nThe Module Stomp Detection\r\nThere are several detections, but we will delve into two here for module stomping. One is due to a side effect of\r\nhow Cobalt Strike implements module stomping as well as general IOCs that can be observed when module\r\nstomping is performed.\r\nThe first is a detection created by Slaeryan (https://github.com/yusufqk/DetectCobaltStomp). In short, this\r\ndetection works because a side effect of Cobalt Strike’s implementation is that when loaded in memory, the region\r\nappears to be marked as a EXE internally and not a DLL. For those that don’t have cobalt strike, he also created a\r\ntool to mimic the implementation for people to play with and observe the detection. I won’t go into this one too\r\nmuch as he already has a POC and discusses this detection.\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 12 of 17\n\nThe other detection is a much more basic one. Within any executable file, the section where executable code lives\r\nis the .TEXT section. If we walk the .TEXT section of a DLL on disk and compare it to the .TEXT section of its\r\nequivalent offload in memory the sections in theory should always match, as the code should not change unless\r\nthe file is polymorphic. The code for this is fairly basic.\r\nHMODULE lphModule[1024];\r\nDWORD lpcbNeeded;\r\n// Get a handle to the process.\r\nHANDLE = hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |\r\nPROCESS_VM_READ,\r\nFALSE, processID);\r\n// Get a list of all the modules in this process.\r\nif (EnumProcessModules(hProcess, lphModule, sizeof(lphModule), \u0026lpcbNeeded))\r\n{\r\nfor (i = 0; i \u003c (lpcbNeeded / sizeof(HMODULE)); i++)\r\n{\r\nchar szModName[MAX_PATH];\r\n// Get the full path to the module's file.\r\nif (K32GetModuleFileNameExA(hProcess, lphModule[i], szModName,\r\nsizeof(szModName) / sizeof(char)))\r\n{\r\n// Do stuff\r\n}\r\n}\r\n}\r\nHere we simply start by iterating every module in the process.\r\n// Get file Bytes\r\nFILE* pFile;\r\nlong lSize;\r\n//SIZE_T lSize;\r\nBYTE* buffer;\r\nsize_t result;\r\npFile = fopen(szModName, \"rb\");\r\n// obtain file size:\r\nfseek(pFile, 0, SEEK_END);\r\nlSize = ftell(pFile);\r\nrewind(pFile);\r\n// allocate memory to contain the whole file:\r\nbuffer = (BYTE*)malloc(sizeof(BYTE) * lSize);\r\n// copy the file into the buffer:\r\nresult = fread(buffer, 1, lSize, pFile);\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 13 of 17\n\nfclose(pFile);\r\nBYTE* buff;\r\nbuff = (BYTE*)malloc(sizeof(BYTE) * lSize);\r\n_ReadProcessMemory(hProcess, lphModule[i], buff, lSize, NULL);\r\nPIMAGE_NT_HEADERS64 NtHeader = ImageNtHeader(buff);\r\nPIMAGE_SECTION_HEADER Section = IMAGE_FIRST_SECTION(NtHeader);\r\nWORD NumSections = NtHeader-\u003eFileHeader.NumberOfSections;\r\nfor (WORD i = 0; i \u003c NumSections; i++) { std::string secName(reinterpret_cast(Section-\u003eName), 5);\r\nif (secName.find(\".text\") != std::string::npos) {\r\nbreak;\r\n}\r\nSection++;\r\n}\r\nWe then load the relevant module file on disk and store the bytes for comparing memory in the var buffer. We then\r\nalso read from the base address of the module located in “lphModule[i]” and store all the bytes within the var buff.\r\nWe then enumerate all the sections in the loaded module until we find the .TEXT section and break the loop. At\r\nthis point the “Section” variable will contain all our relevant section data.\r\nTo be able to match the on-disk file to the one in memory we need to use the Section offsets to find the .TEXT\r\nsection location on disk and in memory. This actually will not match (usually). The offset to the .TEXT section in\r\nmemory generally gets relocated down a page, 4096 bytes. The offset to the section on disk is usually 1024 bytes\r\nin comparison. But we say usually so we of course will simply use “Section-\u003ePointerToRawData” to get the offset\r\non disk and “Section-\u003eVirtualAddress” to get its offloaded address in memory to be 100% sure.\r\nLPBYTE txtSectionFile = buffer + Section-\u003ePointerToRawData;\r\nLPBYTE txtSectionMem = buff + Section-\u003eVirtualAddress;\r\nAt this point all you’d have to do is compare each memory region byte for byte and make sure they match.\r\nint inconsistencies = 0;\r\nfor (int i = 0; i \u003c Section-\u003eSizeOfRawData; i++) {\r\nif ((char*)txtSectionFile[i] != (char*)txtSectionMem[i]) {\r\ninconsistencies++;\r\n}\r\n}\r\nNow of course we need to account for things like hooks and such, as we know many AV and EDR will perform\r\nhooks, we know these will provide false positives. As a result we take the amount of the differences and if it’s\r\ngreater than a certain number only then do we get concerned.\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 14 of 17\n\nif (inconsistencies \u003e 10000) {\r\nprintf(\"FOUND DLL HOLLOW.\\nNOW MONITORING: %s with %f changes found. %f%% Overall\\n\\n\", szMod\r\nCHAR* log = (CHAR*)malloc(256);\r\nsnprintf(log, 255, \"FOUND DLL HOLLOW.\\nNOW MONITORING: %s with %f changes found. %f%% Overall\r\nLogDetected(\u0026log);\r\nfree(log);\r\nstd::string moduleName(szModName, sizeof(szModName) / sizeof(char));\r\nstd::transform(moduleName.begin(), moduleName.end(), moduleName.begin(),\r\n[](unsigned char c) { return tolower(c); });\r\ndllMonitor = moduleName;\r\nbreak;\r\n}\r\nWe arbitrarily pick 10,000 as our amount, simply because we know it’ll certainly be a larger number than any\r\nnumber of hooks any utility would alter for the hooks, as well as being small enough as we know most raw\r\nmalware payloads at least are much bigger. This should reduce false positives substantially while finding any\r\naltered DLLs in memory. The only caveat to this would be additional false positives from polymorphic DLLs who\r\nalter themselves in memory.\r\nLet’s run our new detector against our Cobalt Strike payload and the hollowed DLL and observe the results.\r\nfig 16. DLL Hollow Detection\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 15 of 17\n\nHere we can see a few false positives from our own hooks actually, where we alter five bytes to the prologue of\r\neach function, two functions being altered in each DLL. Finally at the end we can see our hollowed xpsservices.dll\r\nand the detection is observed with over 300k bytes altered.\r\nLet’s go ahead and turn our tool into a DLL and inject it into everything to observe false positives:\r\nBy injecting into everything and logging all data to files we can observe our detection:\r\nfig 17. Detection\r\nBUT! Interestingly enough we do observe one false positive on what appears to be a polymorphic DLL after all…\r\nfig 18. False positive\r\nUnfortunately not enough bytes are altered to be useful for a hollow target though!\r\nHow do you bypass this detection? Now the simple obvious solution is to restore the DLL bytes (per\r\nhttps://twitter.com/solomonsklash‘s idea) on sleep to prevent this sort of detection and next steps would be\r\nhooking those calls and detecting the restores, if possible, or the constant file reads etc. As we all know,\r\ncybersecurity is a never-ending cat and mouse.\r\nFinal Thoughts\r\nAs red teamers work on malware, often we make discoveries that can lead to new detections too. These\r\nobservations can be tremendously useful to the community while also pushing researchers to the cutting edge and\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 16 of 17\n\nforcing them to think outside of the box if they’d like this game to continue longer.\r\nAs we’ve seen above, we find detections, make bypasses, find more detections — and the game will never\r\nend. Hopefully some interesting new insights could be made to make our defensive industry far more robust\r\noverall, as we work together towards a goal of secure internet usage.\r\nSource: https://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nhttps://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2\r\nPage 17 of 17",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://www.cyberark.com/resources/threat-research/analyzing-malware-with-hooks-stomps-and-return-addresses-2"
	],
	"report_names": [
		"analyzing-malware-with-hooks-stomps-and-return-addresses-2"
	],
	"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": 1775434126,
	"ts_updated_at": 1775791464,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/261bb8540da63df744d3525406b9450c9dc2c13b.pdf",
		"text": "https://archive.orkl.eu/261bb8540da63df744d3525406b9450c9dc2c13b.txt",
		"img": "https://archive.orkl.eu/261bb8540da63df744d3525406b9450c9dc2c13b.jpg"
	}
}