1

this link gives a good example to execute asm with Python on Linux platform, but I dont know how to call an asm func with return value on Windows, Could you please tell me how to do that or give me an example?

import ctypes
import mmap

buf = mmap.mmap(-1, mmap.PAGESIZE, prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC)

ftype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)
fpointer = ctypes.c_void_p.from_buffer(buf)

f = ftype(ctypes.addressof(fpointer))

buf.write(
    b'\x8b\xc7'  # mov eax, edi
    b'\x83\xc0\x01'  # add eax, 1
    b'\xc3'  # ret
)

r = f(42)
print(r)

del fpointer
buf.close()
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
tfxidian
  • 63
  • 6
  • 1
    If this is plain curiosity then it's fine but please say that. If you have an underlying problem you try to solve using Python "inline" assembly, then please ask about that problem directly instead. Otherwise your question will just be an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). – Some programmer dude Mar 22 '22 at 07:49
  • `PROT_READ`, `PROT_WRITE` and `PROT_EXEC` are Unix-specific. For Windows systems use [alternative constructor](https://docs.python.org/3/library/mmap.html#mmap.mmap) with `access` argument. – Olvin Roght Mar 22 '22 at 07:54

1 Answers1

2

Okay, there are multiple differences between Unix and Windows OS, so such a low-level things generally attached to platform.

Let's start from assembly listing.

mov eax, edi
add eax, 1
ret

This code specifically written for Unix OS. Why? Because of assumption that first argument passed using edi register. In Windows systems first argument passed using ecx, so correct assembly for Windows systems is:

mov eax, ecx
add eax, 1
ret

So, patched "compiled" assembly:

asm_function = (
    b'\x8b\xc1'  # mov eax, ecx
    b'\x83\xc0\x01'  # add eax, 1
    b'\xc3'  # ret
)

Regarding mmap. I have tried to make it work, but any attempt ended up with Access Violation message. I even tried to set PAGE_EXECUTE_READWRITE protection to allocated (using mmap()) memory using VirtualProtect() but WinAPI function failed with ERROR_INVALID_PARAMETER(0x57) error code.

As I can't make it work with nmap why not to go one layer down and allocate memory using WinAPI? It's quite simple, we just need to call VirtualAlloc() to allocate memory and RtlMoveMemory() to copy "precompiled" assembly to this allocated memory.

We need to patch default argtypes and restype of ctypes.windll.kernel32.VirtualAlloc and ctypes.windll.kernel32.RtlMoveMemory to match signatures from MSDN. We have to do this, because on x64 systems pointers is 64 bit, but default return value is 32 bit, so address returned by VirtualAlloc() won't be processed correctly. Special thanks to lifemaker, he/she pointed it out in his answer.

Final code:

import ctypes

asm_function = (
    b'\x8b\xc1'      # mov eax, ecx
    b'\x83\xc0\x01'  # add eax, 1
    b'\xc3'          # ret
)

# https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc#MEM_COMMIT
MEM_COMMIT = 0x00001000
# https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc#MEM_RESERVE
MEM_RESERVE = 0x00002000
# https://learn.microsoft.com/en-us/windows/win32/memory/memory-protection-constants#PAGE_EXECUTE_READWRITE
PAGE_EXECUTE_READWRITE = 0x40
# https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc
ctypes.windll.kernel32.VirtualAlloc.argtypes = (
    ctypes.c_void_p,  # LPVOID
    ctypes.c_size_t,  # SIZE_T
    ctypes.c_long,    # DWORD
    ctypes.c_long,    # DWORD
)
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_void_p  # LPVOID

memory_buffer = ctypes.windll.kernel32.VirtualAlloc(
    0,                         # lpAddress - NULL
    len(asm_function),         # dwSize
    MEM_COMMIT | MEM_RESERVE,  # flAllocationType
    PAGE_EXECUTE_READWRITE     # flProtect
)

if not memory_buffer:  # VirtualAlloc returned NULL
    print("VirtualAlloc call failed. Error code:", ctypes.GetLastError())
    exit(-1)

c_buffer = ctypes.c_char_p(asm_function)

# https://learn.microsoft.com/en-us/windows/win32/devnotes/rtlmovememory
ctypes.windll.kernel32.RtlMoveMemory.argtypes = (
    ctypes.c_void_p,  # VOID*
    ctypes.c_void_p,  # VOID*
    ctypes.c_size_t   # SIZE_T
)

ctypes.windll.kernel32.RtlMoveMemory(
    memory_buffer,     # Destination
    c_buffer,          # Source
    len(asm_function)  # Length
)

f = ctypes.cast(
    memory_buffer,
    ctypes.CFUNCTYPE(
        ctypes.c_int,  # return type
        ctypes.c_int   # argument type
    )
)

r = f(42)
print(r)

P.S. I'd appreciate if anybody will add answer how to make it using mmap.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Olvin Roght
  • 7,677
  • 2
  • 16
  • 35
  • Strange that VirtualProtect didn't work. I wonder if something is trying to enforce W^X, preventing a page from having both write and exec permission at the same time. Windows doesn't normally do that, AFAIK, but some OSes like OpenBSD do. (In that case you'd have to copy into a page an then change its permission to read+exec.) Or else maybe the Python wrappers are buggy? Perhaps would be useful to trace system calls with an external tool to see what system calls actually happened. (I think there's some Windows equivalent to Linux `strace`.) – Peter Cordes Mar 22 '22 at 22:07
  • @PeterCordes, indeed it's strange. I've been limited in time for detailed investigation, so it's still possible that I've missed something dealing with this weird python wrappers. I've allocated two buffers, one using `mmap()` and another using `VirtualAlloc()` and tried to call `VirtualProtect()` on both. It worked normally on buffer allocated with `VirtualAlloc()` but always failed with another allocated with `mmap()`. Error code (`ERROR_INVALID_PARAMETER`) is kinda weird though, maybe the problem was in the way I retrieved address of allocated memory. Will return to this a bit later. – Olvin Roght Mar 22 '22 at 22:16