Python ctypes和函数调用

8

我的朋友制作了一个在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)

现在,每次运行此脚本时为什么会出现分段错误?
我还有一个关于mprotect和不执行标志的问题。据说这可以保护免受大多数基本安全漏洞的影响,例如缓冲区溢出。但它使用的真正原因是什么?您可以继续编写直到达到.text,然后将指令注入到一个很好的PROT_EXEC区域中。除非,当然,您在.text中使用写保护。
但是,为什么到处都要使用PROT_EXEC呢?难道您的.text部分受到写保护就不能帮助您吗?
5个回答

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?)上起作用。

更好的答案。valloc很有用,就像注意到EXEC位在此之后不会被清除一样。但我可能对这两个方面都不感兴趣。 - Cheery

7

我和朋友做了一些研究,发现这是一个平台特定的问题。我们怀疑在某些平台上,malloc分配的内存没有设置PROT_EXEC,而在其他平台上则有此设置。

因此,在使用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))

这是我在此主题上所见过的绝佳示例! - mtasic85

4

尽管我之前在其他地方找到过这个答案,但这确实是正确的,只是有些微小的差异。我会在我的回答中进行解释。 - Cheery

0

我最近想到了一种更简单的方法,不需要使用mprotect。直接为程序映射可执行空间即可。现在Python有一个模块可以做到这一点,但我没有找到获取代码地址的方法。简而言之,您可以调用mmap来分配内存,而不是使用字符串缓冲区并间接设置执行标志。这样更容易、更安全,您可以确保只有您的代码能够被执行。


0

Python是否允许这样的用法?那我应该学一下了...

我认为解释器不希望任何寄存器被改变。如果你打算像这样使用汇编输出,请在函数内保存你使用的寄存器。

顺便说一句,x86_64的调用约定与常规x86不同。如果你失去了堆栈指针对齐并混合使用其他工具生成的外部对象,可能会遇到麻烦。


ctypes确保我的调用约定正确,只要代码已由gcc输出即可。至于寄存器的更改,我认为x86_64的调用约定表明子程序可以自由更改大多数寄存器。 - Cheery

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接