Python ctypes 和函数调用

8 投票
5 回答
4359 浏览
提问于 2025-04-11 18:02

我朋友做了一个小的概念验证汇编器,它可以在x86架构上运行。我决定把它移植到x86_64架构上,但我立刻遇到了一个问题。

我写了一小段C语言程序,然后编译并使用objdump查看了代码。之后,我把它插入到我的Python脚本中,所以x86_64的代码是正确的:

from ctypes import cast, CFUNCTYPE, c_char_p, c_long

buffer = ''.join(map(chr, [ #0000000000000000 <add>:
  0x55,                     # push   %rbp
  0x48, 0x89, 0xe5,         # mov    %rsp,%rbp
  0x48, 0x89, 0x7d, 0xf8,   # mov    %rdi,-0x8(%rbp)
  0x48, 0x8b, 0x45, 0xf8,   # mov    -0x8(%rbp),%rax
  0x48, 0x83, 0xc0, 0x0a,   # add    $0xa,%rax
  0xc9,                     # leaveq 
  0xc3,                     # retq
]))

fptr = cast(c_char_p(buffer), CFUNCTYPE(c_long, c_long))
print fptr(1234)

但是,为什么每次我运行这个脚本时,它总是出现段错误(segmentation fault)呢?

我还有一个关于mprotect和无执行标志的问题。人们说它可以防止大多数基本的安全漏洞,比如缓冲区溢出。但它真正的使用原因是什么呢?你可以一直写代码,直到碰到.text段,然后把你的指令注入到一个不错的、带有PROT_EXEC标志的区域。除非你在.text段使用了写保护。

那么,为什么到处都要有PROT_EXEC呢?如果你的.text段是写保护的,那不是会大大提高安全性吗?

5 个回答

4

我觉得你不能随便执行任何分配的内存,必须先把它设置为可执行的。我自己没有尝试过,但你可以看看unix系统中的一个函数叫做 mprotect

http://linux.about.com/library/cmd/blcmdl2_mprotect.htm

在windows系统中,VirtualProtect 似乎也有类似的功能:

http://msdn.microsoft.com/en-us/library/aa366898(VS.85).aspx

7

我和朋友做了一些研究,发现这是一个特定平台的问题。我们怀疑在某些平台上,malloc(一个用来分配内存的函数)分配的内存没有执行权限,而在其他平台上则有。

所以,之后需要用mprotect这个函数来改变内存的保护级别。

这真让人头疼,花了我一段时间才搞明白该怎么做。

from ctypes import (
    cast, CFUNCTYPE, c_long, sizeof, addressof, create_string_buffer, pythonapi
)

PROT_NONE, PROT_READ, PROT_WRITE, PROT_EXEC = 0, 1, 2, 4
mprotect = pythonapi.mprotect

buffer = ''.join(map(chr, [ #0000000000000000 <add>:
    0x55,                     # push   %rbp
    0x48, 0x89, 0xe5,         # mov    %rsp,%rbp
    0x48, 0x89, 0x7d, 0xf8,   # mov    %rdi,-0x8(%rbp)
    0x48, 0x8b, 0x45, 0xf8,   # mov    -0x8(%rbp),%rax
    0x48, 0x83, 0xc0, 0x0a,   # add    $0xa,%rax
    0xc9,                     # leaveq 
    0xc3,                     # retq
]))

pagesize = pythonapi.getpagesize()
cbuffer = create_string_buffer(buffer)#c_char_p(buffer)
addr = addressof(cbuffer)
size = sizeof(cbuffer)
mask = pagesize - 1
if mprotect(~mask&addr, mask&addr + size, PROT_READ|PROT_WRITE|PROT_EXEC) < 0:
    print "mprotect failed?"
else:
    fptr = cast(cbuffer, CFUNCTYPE(c_long, c_long))
    print repr(fptr(1234))
8

正如vincent提到的,这个问题是因为分配的内存页被标记为不可执行。现在的新处理器支持这种功能,操作系统利用它来增加安全性。这样做的目的是为了防止某些缓冲区溢出攻击。比如,一种常见的攻击方式是溢出栈变量,修改返回地址,让它指向你插入的代码。如果栈是不可执行的,这样的攻击就只会导致程序崩溃,而不是让攻击者控制程序。类似的攻击也会发生在堆内存上。

要解决这个问题,你需要改变内存的保护设置。这只能在对齐的内存页上进行,所以你可能需要把代码改成下面这样的:

libc = CDLL('libc.so')

# Some constants
PROT_READ = 1
PROT_WRITE = 2
PROT_EXEC = 4

def executable_code(buffer):
    """Return a pointer to a page-aligned executable buffer filled in with the data of the string provided.
    The pointer should be freed with libc.free() when finished"""

    buf = c_char_p(buffer)
    size = len(buffer)
    # Need to align to a page boundary, so use valloc
    addr = libc.valloc(size)
    addr = c_void_p(addr)

    if 0 == addr:  
        raise Exception("Failed to allocate memory")

    memmove(addr, buf, size)
    if 0 != libc.mprotect(addr, len(buffer), PROT_READ | PROT_WRITE | PROT_EXEC):
        raise Exception("Failed to set protection on buffer")
    return addr

code_ptr = executable_code(buffer)
fptr = cast(code_ptr, CFUNCTYPE(c_long, c_long))
print fptr(1234)
libc.free(code_ptr)

注意:在释放内存页之前,最好先取消可执行标志。大多数C库在完成后并不会把内存返回给操作系统,而是把它保留在自己的内存池中。这可能意味着它们会在其他地方重用这块内存,而不清除EXEC标志,从而绕过安全保护。

另外要注意的是,这种做法在不同系统之间的兼容性比较差。我在Linux上测试过,但在其他操作系统上没有测试过。在Windows上是行不通的,但可能在其他类Unix系统(比如BSD、OsX)上可以用。

撰写回答