Reversing ShellCode Runnerš
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.
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!
- Static Strings: Following are Base64 encoded strings in the sample which on decoding displays set of APIs like strings:
2. Dynamic Behavior: On execution sample invokes explorer.exe injects shellcode in it and makes loopback connection to 127.0.0.1
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.
After few basic functions, instruction pointer reaches to the start of real code & all registers are pushed on to the stack for further use.
Below is the Decompiled view of start of the main code.
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.
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.
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.
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.
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.
Second Jump for debugger detection, same as above, bypass the code.
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.
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.
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.
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.
If enabling the privilege fails, jumps to the failure handler. Otherwise, clears buffers and logs success.
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.
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.
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.
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
Once the export directory is validated it starts to parse through exports
Below is the resolving loop. Required apis are stored in registers
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)
As we progress and execute the instructions, shell code starts to decrypt
After Complete decryption we can dump the shellcode, which looks kind of like this:
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
further all the required registers containing apis are 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"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
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.
Malloc: Allocates heap memory for shellcode copying, as we can see below
Memcpy: Copies the shellcode from previous memory region to this not the actual virtual memory allocated in the process.
Now, NtWriteVirtualMemory: Writes the shellcode to the virtually allocated memory. which is stored in R8: āx2ā
As the shellcode is now written into the virtual memory, heap memory is made free, hence shellcode vanishes from that address.
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.
If we dump RSP now ,we can find shellcode is present this means, thread pointer is pointing to the shellcode.
At last,
NtResumeThread: Resumes the thread to execute the new code that is shellcode. Explorer.exe starts running
If injection is successful then it logs the message and Shellcode address. otherwise logs failure message and terminates.
This is the sequence of Thread Hijacking which is a sub type of process injection.
Now finally sample exits the code and terminates process.
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









