This particular example pertains to running 64-bit assembly code under 64-bit Python, however if you are fortunate enough to be running on a 32-bit Linux platform you may get away with doing a lot less. Since the introduction of DEP you can no longer just shove some code in a buffer and execute it. You must first allow execute permission. Further, you cannot change memory protection settings for anything other than an entire page, so I've padded the memory allocated to include a page before and a page after, so I can seek back to the start of page for the start of my assembler, then change memory protection from there secure in the knowledge that I won't change protection for anything beyond the end of my allocated region. I could probably have used valloc() instead.
After that it's simply a matter of changing protection. There is some doubt over whether you can leave this memory read/writable after you set the execute bit, I haven't played with that.
Finally, it's important to reset the memory protection before the memory gets freed/reused by Python because Python has no knowledge of the hackery you've just been up to.
For a Windows version, use VirtualProtect() to achieve similar results. I could have conditionally added this and made the code portable, however I rarely do any assembler on Windows, so it's left as an exercise for the reader.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
#!/usr/bin/env python import subprocess, os, tempfile from ctypes import * PAGE_SIZE = 4096 class AssemblerFunction(object): def __init__(self, code, ret_type, *arg_types): # Run Nasm fd, source = tempfile.mkstemp(".S", "assembly", os.getcwd()) os.write(fd, code) os.close(fd) target = os.path.splitext(source) subprocess.check_call(["nasm",source]) os.unlink(source) binary = file(target,"rb").read() os.unlink(target) bin_len = len(binary) # align our code on page boundary. self.code_buffer = create_string_buffer(PAGE_SIZE*2+bin_len) addr = (addressof(self.code_buffer) + PAGE_SIZE) & (~(PAGE_SIZE-1)) memmove(addr, binary, bin_len) # Change memory protection self.mprotect = cdll.LoadLibrary("libc.so.6").mprotect mp_ret = self.mprotect(addr, bin_len, 4) # execute only. if mp_ret: raise OSError("Unable to change memory protection") self.func = CFUNCTYPE(ret_type, *arg_types)(addr) self.addr = addr self.bin_len = bin_len def __call__(self, *args): return self.func(*args) def __del__(self): # Revert memory protection if hasattr(self,"mprotect"): self.mprotect(self.addr, self.bin_len, 3) if __name__ == "__main__": add_func = """ BITS 64 mov rax, rdi ; Move the first parameter add rax, rsi ; add the second parameter ret ; rax will be returned """ Add = AssemblerFunction(add_func, c_int, c_int, c_int) print Add(1, 2)