pythoncross-platformctypesexploit

How to call the ctypes function from bytes in Python?


I have the disassamble bytes of a simple function

89 4C 24 08          mov         dword ptr [sum],ecx  
    while (sum>=1) {
83 7C 24 08 01       cmp         dword ptr [sum],1  
7C 0C                jl          doNothing+17h (07FF636C61017h)  
        sum--;
8B 44 24 08          mov         eax,dword ptr [sum]  
FF C8                dec         eax  
89 44 24 08          mov         dword ptr [sum],eax  
    }
EB ED                jmp         doNothing+4h (07FF636C61004h)  
}
C3                   ret  

which is a bytes object in python bytes.fromhex('89 4c 24 08 83 7c 24 08 01 7c 0c 8b 44 24 08 ff c8 89 44 24 08 eb ed c3 ')

How to call this micro codes in python using ctypes? I tried the code as below, but it crashes.

import ctypes

# raw disassamble bytes
buf = bytes.fromhex('89 4c 24 08 83 7c 24 08 01 7c 0c 8b 44 24 08 ff c8 89 44 24 08 eb ed c3 ')

# function type definition
nothFn = ctypes.CFUNCTYPE(None, ctypes.c_int)

# ctypes buffer
codebuf = ctypes.create_string_buffer(buf)

# raw buffer's address as the function
cfunc = nothFn(ctypes.addressof(codebuf))

# call it then it crashes
cfunc(ctypes.c_int(3))

I also tried to use the address returned from str(codebuf) but it also crashes.

Questions:

  1. Is it due to the memory execution violates? how to make the allocated memory executables then? Does it have be in a dynamic library to be loaded for execution?
  2. Will the same code run under both Windows and Linux if the cpu is the same architecture x86_64? To avoid complication, let's suppose the function is simple and only operates on the input argument or stack memory.

Solution

  • Modern processors protect data memory from exploits by making it non-executable. Changing that protection is OS-specific.

    Below is an example for Windows to execute code from a buffer. I used a simple code example since dword ptr [sum] is referring to a stack frame variable that is undefined.

    import sys
    import ctypes as ct
    import ctypes.wintypes as w
    
    print(sys.version)
    
    PAGE_EXECUTE_READWRITE = 0x40
    
    def boolcheck(result, func, args):
        if not result:
            raise ct.WinError(ct.get_last_error())
    
    kernel32 = ct.WinDLL('kernel32', use_last_error=True)
    # BOOL VirtualProtect(
    #   [in]  LPVOID lpAddress,
    #   [in]  SIZE_T dwSize,
    #   [in]  DWORD  flNewProtect,
    #   [out] PDWORD lpflOldProtect
    # );
    VirtualProtect = kernel32.VirtualProtect
    VirtualProtect.argtypes = w.LPVOID, ct.c_size_t, w.DWORD, w.LPDWORD
    VirtualProtect.restype = w.BOOL
    VirtualProtect.errcheck = boolcheck
    
    # Simple function with no stack frame and four parameters.
    # In Microsoft's x64 calling convention RCX, RDX, R8, R9 are
    # the registers used for the first four parameters.
    # The return value is the sum of the four registers.
    # 0:  48 89 c8                mov    rax,rcx
    # 3:  48 01 d0                add    rax,rdx
    # 6:  4c 01 c0                add    rax,r8
    # 9:  4c 01 c8                add    rax,r9
    # c:  c3                      ret
    buf = ct.create_string_buffer(bytes.fromhex('4889C84801D04C01C04C01C8C3'))
    
    # Get the address of the buffer and make it executable.
    addr = ct.addressof(buf)
    old = w.DWORD()
    VirtualProtect(addr, len(buf), PAGE_EXECUTE_READWRITE, ct.byref(old))
    
    FUNC = ct.CFUNCTYPE(ct.c_uint64, ct.c_uint64, ct.c_uint64, ct.c_uint64, ct.c_uint64)
    func = FUNC(ct.addressof(buf))
    ret = func(0x1234000000000000, 0x567800000000, 0x9abc0000, 0xdef0)
    print(hex(ret))
    

    Output (Windows 10 64-bit):

    3.12.6 (tags/v3.12.6:a4a2d2b, Sep  6 2024, 20:11:23) [MSC v.1940 64 bit (AMD64)]
    0x123456789abcdef0
    

    Note that the x64 calling convention varies per OS.

    References: