2008-11-08 29 views
7

Mi amigo produjo un pequeño ensamblador de prueba de concepto que funcionaba en x86. Decidí portarlo también para x86_64, pero de inmediato me encontré con un problema.Códigos de Python y llamadas a funciones

Escribí un pequeño trozo de programa en C, luego compilé y objdumped el código. Después de eso me introduje a mi script en Python, por lo tanto, el código es correcto 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) 

Ahora, ¿por qué este guión sigue haciendo fallo de segmentación cada vez que lo ejecuto?

Tengo una pregunta sobre mprotect y ninguna bandera de ejecución. Se dice que protege contra la mayoría de los ataques de seguridad básicos, como los desbordamientos del búfer. Pero, ¿cuál es la verdadera razón por la que está en uso? Podrías seguir escribiendo hasta que toques el texto. Luego, inserta tus instrucciones en una agradable zona PROT_EXEC. A menos que, por supuesto, utilice una protección de escritura en .text

Pero entonces, ¿por qué tiene ese PROT_EXEC en todas partes? ¿No ayudaría tremendamente que su sección .text esté protegida contra escritura?

Respuesta

8

Como vincent mencionado, esto se debe a la página asignada está marcada como no ejecutable. Los procesadores más recientes admiten este functionality, y se usa como una capa adicional de seguridad por los sistemas operativos que lo soportan. La idea es protegerse contra ciertos ataques de desbordamiento de búfer. P.ej. Un ataque común es desbordar una variable de pila, reescribiendo la dirección de retorno para señalar el código que ha insertado. Con una pila no ejecutable, ahora esto solo produce una segfault, en lugar de un control del proceso. Ataques similares también existen para la memoria de pila.

Para evitarlo, debe modificar la protección.Esto sólo se puede realizar en la página alineados memoria, por lo que probablemente tendrá que cambiar su código a algo como el siguiente:

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) 

Nota: Puede ser una buena idea para desarmar el indicador ejecutable antes de liberar la página . La mayoría de las bibliotecas C en realidad no devuelven la memoria al sistema operativo cuando están listas, sino que las mantienen en su propio grupo. Esto podría significar que reutilizarán la página en otro lugar sin borrar el bit EXEC, evitando el beneficio de seguridad.

También tenga en cuenta que esto es bastante no portátil. Lo probé en Linux, pero no en ningún otro sistema operativo. No funcionará en Windows, comprar puede hacer en otras Unixes (BSD, OsX?).

+0

Aún mejor respuesta. valloc es útil, como es notar que el bit EXEC no se borra después de esto. Pero quizás no estoy interesado en ninguno de los aspectos. – Cheery

0

¿Python incluso permite tal uso? Debo aprenderlo entonces ...

Creo que el intérprete no espera que se modifique ningún registro. Intente guardar los registros que usa dentro de la función si planea usar su salida de ensamblador de esta manera.

Por cierto, la convención de llamadas de x86_64 es diferente de la normal x86. Puede tener problemas si pierde la alineación del puntero de pila y mezcla los objetos externos generados con otras herramientas.

+0

ctypes se encarga de que mis convenciones de llamadas sean correctas, es suficiente con que el código haya sido generado por gcc. En cuanto a los registros cambiantes, pensé que las convenciones de llamada de x86_64 dicen que la subrutina puede cambiar la mayoría de los registros libremente. – Cheery

4

Creo que no se puede ejecutar libremente ninguna memoria asignada sin antes establecerla como ejecutable. Nunca he probado a mí mismo, pero es posible que desee comprobar la función de Unix mprotect:

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

VirtualProtect parece hacer lo mismo en las ventanas:

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

+0

Aunque lo encontré en otro lugar antes, esto es realmente correcto, pero con una ligera variación. Lo explico en mi propia respuesta. – Cheery

7

hecho algunas investigaciones con mi amigo y descubrió que este es un problema específico de la plataforma. Sospechamos que en algunas plataformas malloc mmaps memory sin PROT_EXEC y en otras lo hace.

Por lo tanto, es necesario cambiar el nivel de protección con mprotect después.

Lo cojo, tomó un tiempo para descubrir qué hacer.

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)) 
+0

absolutamente mejor ejemplo jamás visto en este tema! – mtasic85

0

Hay un enfoque más simple que he pensado solo pero que recientemente no involucra a mprotect. Simplemente mmap el espacio ejecutable para el programa directamente. En estos días, Python tiene un módulo para hacer exactamente esto, aunque no encontré la forma de obtener la dirección del código. En resumen, asignaría memoria llamando a mmap en lugar de usar búferes de cadena y configurando el indicador de ejecución indirectamente. Esto es más fácil y seguro, puede estar seguro de que solo su código se puede ejecutar ahora.

Cuestiones relacionadas