Sitemap
Tanmay Arvind More

Tanmay Arvind More

Threat Researcher | Reverse Engineering | Debugging| Exploring Cyber Mysteries 🌐 Join me as I delve into malware analysis and Cyber Security Fundamentals.

Reversing ShellCode RunneršŸ”

11 min readApr 22, 2025

Imagine a piece of malware cleverly evading the presence of debuggers and sandboxes, slipping past standard payload restrictions. Its next move? To decrypt its malicious core — shellcode — and inject it directly into the memory of a legitimate process, effectively camouflaging its execution. This isn’t a scene from a cybersecurity thriller; it’s a malware tactic increasingly observed in the wild, and Cobalt Strike’s ā€œShellcodeRunnerā€. Cobalt Strike has earned its reputation as a powerful adversary simulation tool, widely used by penetration testers and malicious actors.

Press enter or click to view image in full size
Cobalt Strike Shellcode Runner

In this blog, we will reverse the Shellcode Runner & see how it checks the presence of debugger and sandbox, bypasses Event Tracing for Windows(ETW), enables SeDebugPrivilege, dynamically resolves APIs, decrypts shellcode — injects it into legit process and execute it using Thread Hijacking.

Major tools used are X64DBG and Ghidra.
Sample I am using for this blog is (Available on Malware Bazaar)
SHA256: 28482c95eeb0279935cc0c59f30b00f4656c193c2c07dc3f74c5bc9b301d73a6.
Name: DeepSeekShellcoderloader.exe

Subsystem: Console

Caution: ā€œExecute malware samples in a safe, isolated virtual environment to avoid compromising your system.ā€
Now, Lets begin!

  1. Static Strings: Following are Base64 encoded strings in the sample which on decoding displays set of APIs like strings:
Fig1: Static Stringsbs64encoded-decoded

2. Dynamic Behavior: On execution sample invokes explorer.exe injects shellcode in it and makes loopback connection to 127.0.0.1

Fig2: Dynamic Execution
Press enter or click to view image in full size
Fig3: Dynamic Connection

Loopback address (127.0.0.1) is a special IP that routes traffic back to the same device. Malware uses this local connection to interact with other malicious components or services running on the same machine, such as a local proxy, command-and-control relay, or another malware module. This avoids generating suspicious external network traffic that could be detected by security tools

It can also be part of a reverse shell setup where the actual external connection to the attacker is proxied or controlled through local malware components.

3. Loading into debugger (Entrypoint): Loading the sample in X64DBG to reverse and debug it. Entrypoint is loaded.

Press enter or click to view image in full size
Fig4: Entrypoint X64DBG

After few basic functions, instruction pointer reaches to the start of real code & all registers are pushed on to the stack for further use.

Press enter or click to view image in full size
Fig5: Shellcode Start

Below is the Decompiled view of start of the main code.

Press enter or click to view image in full size
Fig6: Decompiled code Ghidra

Here ā€œlocal_538ā€ acts as buffer to store the later on formatted strings and 0x100 is size of it, both these parameters along with a Debug message are passed to the call ā€œFUN_140001680ā€ which is a wrapper class for __stdio_common_vsprintf_s which is an internal Microsoft C Runtime (CRT) library function used to implement the secure version of vsprintf_s, which formats a string and writes it into a buffer while preventing buffer overflows. Below is the code inside FUN_140001680.

Press enter or click to view image in full size
Fig7: Inside of FUN_140001680

OutputDebugStringA: (refer Fig6)
This Windows API function sends the formatted debug string stored in local_538 to the attached debugger’s output window and further prints it to the standard output (console) using ā€œputsā€ API. Below is the output.

Press enter or click to view image in full size
Fig8: Console output of debug message by sample

4. Debugger & Sandbox Detection:

Further into the code ā€œivar23ā€ (refer Fig6) stores the return value of Debugger Call.

Inside Debugger Call there are two APIs used to check whether sample is being debugged or not.

a)IsDebuggerPresent: If debugger is attached to the process it returns non zero value.

b)CheckRemoteDebuggerPresent: This function is used to determine if another process (in this case, itself) is being debugged remotely. It provides a second layer of verification to make debugger detection more robust.

Press enter or click to view image in full size
Fig9: IsDebuggerPresent & CheckRemoteDebuggerPresent

If ā€œBVar1ā€ stores the non zero value, it will enter the if condition and get the pseudo-handle of current process with the help of GetCurrentProcess API and pass it to CheckRemoteDebuggerPresent.

If ā€œBvar1" or a local var passed get stored with value zero then the flow of code moves into the if condition and displays debugger detected message accordingly depending on which API detected it.

Press enter or click to view image in full size
Press enter or click to view image in full size
Fig10 : debug output messages

As the process is being debugged by us already the execution flow will detect the debugger and redirect to Exit call.

Debugger Bypass:

To avoid the Exiting of code we need to bypass these calls. Below is the first jump for debugger detection. As we can see the intended execution flow is inside the jump, we must bypass it & take the jump.

Press enter or click to view image in full size
Fig11: x64dbg Debugger 1st jmp

Second Jump for debugger detection, same as above, bypass the code.

Press enter or click to view image in full size
Fig12: X64dbg Debugger 2nd jmp

GlobalMemoryCheck: If no debugger is detected, the code then calls GlobalMemoryCheck() (refer Fig6) which checks for signs of a sandbox or virtualized environment (e.g., unusual memory layout, limited memory resources, or known sandbox artifacts).

Below is the code inside this call, it gets the status of system memory usage by using GlobalMemoryStatusEx, if memory is zero then it displays Failed message and exits, if< 0x80000000 then insufficient and exits, if not both then moves further for final sandbox check.

Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size
Fig13: GlobalMemoryStatusEX

SbieDll.dll: SbieDll.dll is the main DLL used by Sandboxie, a popular sandboxing software.

GetModuleHandleA : attempts to get a handle to the module SbieDll.dll if it is loaded in the current process. If SbieDll.dll is not found (i.e., the handle is NULL), the code sets uVar3 to 0 and jumps to a ā€œLAB_14000164aā€ which redirects the code to ETW bypass, otherwise sandbox detected and exit.

5. Event Tracing for Windows(ETW) Bypass:

After bypassing the sandbox code, execution flow is redirected to Event Tracing for Window (ETW) bypass code.

Event Tracing for Window: Event Tracing for Windows is a powerful logging mechanism used by Windows to track system and application events. Disabling ETW hampers security monitoring, making it a common tactic in offensive security and malware to evade detection.

Below is the code:

Firstly it takes the handle for ā€œntdll.dllā€ with the GetModuleHandleA api.

Then retrieves the process address of the EtwEventWrite function within ntdll.dll. This function is used by ETW to write events to a tracing session.

Press enter or click to view image in full size
Press enter or click to view image in full size
Fig14: ETWBypass

Modifying Memory Protection using VirtualProtect:

BVar24 = VirtualProtect(lpAddress, 1, 0x40, (PDWORD)&local_9e8){
*lpAddress = (FARPROC)0xc3;
VirtualProtect(lpAddress, 1, (DWORD)local_9e8, (PDWORD)&local_9e8);

This instruction changes the memory protection of the first byte of the EtwEventWrite function to allow execution and writing (0x40 is PAGE_EXECUTE_READWRITE).

Patches the first byte EtwEventWrite with opcode 0xc3 (assembly instruction for RET). This effectively disables the function by causing it to return immediately without executing any of its original code.

Finally calls VirtualProtect again to restore the original memory protection settings to prevent further modifications. At the end logs the message indicating that the ETW bypass has been successful.

6. Enabling SeDebugPrivilege:

Further the sample enables Debug Privilege using SeDebugPrivilege

SeDebugPrivilege: It is a powerful Windows privilege that allows a process to debug and manipulate other processes, including those running as SYSTEM. Enabling this privilege is often necessary for malware or advanced tools to inspect or inject code into other processes.

Code execution reaches to the below block of code which records the current tick count twice and calculates the elapsed time.

It then enters a loop that keeps calling SwitchToThread() (which yields the processor to other threads) until at least 5000 milliseconds (5 seconds) have passed. This acts as a delay or wait loop for 5 seconds, allowing other threads/processes to run.

Fig15:TickCount

Then calls GetCurrentProcess to get pseudo-handle to the current process and passes it to the OpenProcessToken api along with desired access rights 0x28 (which is TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY).

If opening token value is 0 then is fails to enable the debug privilege, but if not then it calls LookupPrivilegeValueW to get the locally unique identifier (LUID) for ā€œSeDebugPrivilegeā€.

Sets up a TOKEN_PRIVILEGES structure with one privilege (SeDebugPrivilege) enabled

Get Tanmay Arvind More’s stories inĀ yourĀ inbox

Join Medium for free to get updates fromĀ thisĀ writer.

Calls AdjustTokenPrivileges to enable this privilege in the current process token. Closes the token handle.

Press enter or click to view image in full size
Fig16: SeDebugPrivilege

If enabling the privilege fails, jumps to the failure handler. Otherwise, clears buffers and logs success.

Press enter or click to view image in full size
Fig17: Debug Privilege enabled

Every process has a security token that includes privileges like SeDebugPrivilege, which are usually disabled by default.
Malware or tools can enable these privileges using AdjustTokenPrivileges() if they have the rights. To successfully enable a privilege, the process must have the privilege assigned in its access token (i.e., it must be part of the token’s privileges).

This simply means to enable SeDebugPrivilege, the malware must be executed with administrative rights.

7. Base64 Decoding:

Further sample decodes the base64 encoded strings found in static strings. Memory which contains static strings are moved into the code.

Press enter or click to view image in full size
Fig18: Memmove

Further each character is passed through series of conditions which Calculates string length and Checks if it is a multiple of 4 using a bitwise AND.

Counts padding characters ā€˜=’ at the end.

Press enter or click to view image in full size
Press enter or click to view image in full size
Fig 19: bs64 Insides

Further allocates memory for the decoded data. Then, it processes the input in 4-character blocks, converting each Base64 character to its 6-bit value and reconstructing the original bytes. Finally, it returns a pointer to the decoded buffer and stores its length. Below is the decoding loop.

Press enter or click to view image in full size
Fig20 bs64 EndOfTheLoop

8. Dynamically Resolving the APIs

Now sample loads ntdll.dll address stored in R12 to RAX register and validates further if it is correct or valid.
Further with the help of ntdll exports directory resolves the apis

Press enter or click to view image in full size
Fig21:NtdllLoad
Press enter or click to view image in full size
Fig22: Validates PE
Press enter or click to view image in full size
Fig 23: Check for ntdll export directory

Once the export directory is validated it starts to parse through exports

Press enter or click to view image in full size
Fig24: A_SHAFinal export

Below is the resolving loop. Required apis are stored in registers

Press enter or click to view image in full size
Press enter or click to view image in full size
Fig25: APIReolved and pushed in regs

9. ShellCode Decryption:

Finally!! at the main functionality:

Code reaches to XOR loop which decrypts the shell code stored in specific address in the sample. (refer the stack value which is dumped)

Press enter or click to view image in full size
Fig26: ShellCode before decryption

As we progress and execute the instructions, shell code starts to decrypt

Press enter or click to view image in full size
Fig27: ShellCode decryption

After Complete decryption we can dump the shellcode, which looks kind of like this:

Press enter or click to view image in full size
Fig28: ShellCode Dumped

These strings ā€œUser-Agent:ā€¦ā€ ,ā€œ127.0.0.1ā€,etc indicates the HTTP request headers crafted to mimic legitimate browser traffic, we already found this in dynamic analysis of sample, in which it performs this connection

This is a common technique in command-and-control (C2) communications, where malware contacts its server pretending to be a browser.

10. ShellCode Injection:

As the shellcode is now decrypted and ready to be injected, sample creates a thread for another process using NtCreatThreadEx api stored in rbx

Press enter or click to view image in full size
Fig29: NtCreateThreadEx

further all the required registers containing apis are pushed on stack

Press enter or click to view image in full size
Press enter or click to view image in full size
Fig30: APIs pushed on stack

Calls GetTickCount() to retrieve the number of milliseconds elapsed since the system started. Further below instruction loads the explorer.exe string’s address and stores it in rdx.

lea rdx,qword ptr ds:[140005925] //rdx:"explorer.exe", 0000000140005925:"explorer.exe"
Press enter or click to view image in full size
Fig31: Explore.exe address

CreateProcessA: Attempts to create a new process explorer.exe .On creating the process puts the thread on suspend.

Jumps on MemSet, to set the memory, in this call address of shell code is loaded or set in registers

Press enter or click to view image in full size
Fig32: Explorer.exe Suspend

ZwGetContextThread: If the process is created successfully it retrieves and modifies the context of the main thread of the new process.

NtAllocateVirtualMemory: Allocates virtual memory in the new process.

Press enter or click to view image in full size
Fig33: GetContextThread

Malloc: Allocates heap memory for shellcode copying, as we can see below

Press enter or click to view image in full size
Fig34: RAX: Memory allocated for shellcode to copy

Memcpy: Copies the shellcode from previous memory region to this not the actual virtual memory allocated in the process.

Press enter or click to view image in full size
Fig35: Shellcode copied to heap

Now, NtWriteVirtualMemory: Writes the shellcode to the virtually allocated memory. which is stored in R8: ā€œx2ā€

Press enter or click to view image in full size
Fig36: NtWriteVirtualMemory

As the shellcode is now written into the virtual memory, heap memory is made free, hence shellcode vanishes from that address.

Press enter or click to view image in full size
Fig37: Heap Memory free

SetThreadContext is called which isused to set or modify the CPU context of a specified thread. This includes registers such as the instruction pointer, stack pointer, and flags, allowing control over the thread’s execution state.

Press enter or click to view image in full size
Fig38:NtSetContextThread

If we dump RSP now ,we can find shellcode is present this means, thread pointer is pointing to the shellcode.

Press enter or click to view image in full size
Fig39: Context is saved

At last,

NtResumeThread: Resumes the thread to execute the new code that is shellcode. Explorer.exe starts running

Press enter or click to view image in full size
Fig40: ResumeThread

If injection is successful then it logs the message and Shellcode address. otherwise logs failure message and terminates.

Press enter or click to view image in full size
Fig41: Thread Hijacked

This is the sequence of Thread Hijacking which is a sub type of process injection.

Now finally sample exits the code and terminates process.

Press enter or click to view image in full size
Fig42: Exit

So, this was the Reversing & debugging of Cobalt Strike: ShellCode Runner.

Understanding these tactics helps defenders recognize how sophisticated threat actors operate under the radar. By dissecting such loaders, we gain deeper insights into real-world attack chains and improve our detection and response strategies. Stay tuned for more deep dives into malware internals and threat techniques. Until then.., stay secure and vigilant!

Thanks and regards,

Tanmay More

--

--

Tanmay Arvind More
Tanmay Arvind More

Threat Researcher | Reverse Engineering | Debugging| Exploring Cyber Mysteries 🌐 Join me as I delve into malware analysis and Cyber Security Fundamentals.