Как я могу вызвать встроенный машинный код в Python на Linux? - PullRequest
15 голосов
/ 26 мая 2011

Я пытаюсь вызвать встроенный машинный код из чистого кода Python в Linux. Для этого я встраиваю код в байтовый литерал

code = b"\x55\x89\xe5\x5d\xc3"

, а затем вызовите mprotect() через ctypes, чтобы разрешить выполнение страницы, содержащей код. Наконец, я пытаюсь использовать ctypes для вызова кода. Вот мой полный код:

#!/usr/bin/python3

from ctypes import *

# Initialise ctypes prototype for mprotect().
# According to the manpage:
#     int mprotect(const void *addr, size_t len, int prot);
libc = CDLL("libc.so.6")
mprotect = libc.mprotect
mprotect.restype = c_int
mprotect.argtypes = [c_void_p, c_size_t, c_int]

# PROT_xxxx constants
# Output of gcc -E -dM -x c /usr/include/sys/mman.h | grep PROT_
#     #define PROT_NONE 0x0
#     #define PROT_READ 0x1
#     #define PROT_WRITE 0x2
#     #define PROT_EXEC 0x4
#     #define PROT_GROWSDOWN 0x01000000
#     #define PROT_GROWSUP 0x02000000
PROT_NONE = 0x0
PROT_READ = 0x1
PROT_WRITE = 0x2
PROT_EXEC = 0x4

# Machine code of an empty C function, generated with gcc
# Disassembly:
#     55        push   %ebp
#     89 e5     mov    %esp,%ebp
#     5d        pop    %ebp
#     c3        ret
code = b"\x55\x89\xe5\x5d\xc3"

# Get the address of the code
addr = addressof(c_char_p(code))

# Get the start of the page containing the code and set the permissions
pagesize = 0x1000
pagestart = addr & ~(pagesize - 1)
if mprotect(pagestart, pagesize, PROT_READ|PROT_WRITE|PROT_EXEC):
    raise RuntimeError("Failed to set permissions using mprotect()")

# Generate ctypes function object from code
functype = CFUNCTYPE(None)
f = functype(addr)

# Call the function
print("Calling f()")
f()

Этот код содержит ошибки в последней строке.

  1. Почему я получаю сегфо? mprotect() сигнал вызова успешен, поэтому мне должно быть разрешено выполнять код на странице.

  2. Есть ли способ исправить код? Могу ли я на самом деле вызвать машинный код на чистом Python и внутри текущего процесса?

(Несколько дальнейших замечаний: я на самом деле не пытаюсь достичь цели - я пытаюсь понять, как все работает. Я также пытался использовать 2*pagesize вместо pagesize в вызове mprotect() исключаю случай, когда мои 5 байтов кода попадают на границу страницы - что должно быть невозможно в любом случае. Я использовал Python 3.1.3 для тестирования. Моя машина - 32-битная версия i386. Я знаю, что одним из возможных решений будет создайте общий объект ELF из чистого кода Python и загрузите его через ctypes, но это не тот ответ, который я ищу:)

Редактировать : Следующая версия кода C работает нормально:

#include <sys/mman.h>

char code[] = "\x55\x89\xe5\x5d\xc3";
const int pagesize = 0x1000;

int main()
{
    mprotect((int)code & ~(pagesize - 1), pagesize,
             PROT_READ|PROT_WRITE|PROT_EXEC);
    ((void(*)())code)();
}

Редактировать 2 : Я нашел ошибку в своем коде. Линия

addr = addressof(c_char_p(code))

сначала создает ctypes char*, указывающий на начало bytes экземпляра code. addressof(), примененный к этому указателю, возвращает не адрес, на который указывает указатель, а адрес самого указателя.

Самый простой способ, которым мне удалось выяснить, чтобы фактически получить адрес начала кода, это

addr = addressof(cast(c_char_p(code), POINTER(c_char)).contents)

Подсказки для более простого решения приветствуются:)

Исправление этой строки делает приведенный выше код "работающим" (то есть он ничего не делает вместо segfaulting ...).

Ответы [ 3 ]

5 голосов
/ 27 мая 2011

Я сделал быструю отладку по этому вопросу, и оказалось, что указатель на code не был правильно сконструирован, и где-то внутри ctypes копает вещи перед передачей указателя функции на ffi_call(), который вызывает код.1003 *

Вот строка в ffi_call_unix64() (я на 64-битной), где указатель функции сохраняется в %r11:

57   movq    %r8, %r11               /* Save a copy of the target fn.

Когда я выполняю ваш код, здесьзначение, загруженное в %r11 непосредственно перед попыткой вызова:

(gdb) x/5b $r11
0x7ffff7f186d0: -108    24      -122    0       0

Вот исправление для создания указателя и вызова функции:

raw = b"\x55\x89\xe5\x5d\xc3"
code = create_string_buffer(raw)
addr = addressof(code)

Теперь, когда я его запускаюЯ вижу правильные байты по этому адресу, и функция выполняется нормально:

(gdb) x/5b $r11
0x7ffff7f186d0: 0x55    0x89    0xe5    0x5d    0xc3
3 голосов
/ 26 мая 2011

Возможно, вам придется очистить кеш инструкций .

Это неясно (для меня, во всяком случае), автоматически ли mprotect () делает это.

[обновление]

Конечно, если бы я прочитал документацию для cacheflush (), я бы увидел, что она применима только к MIPS (согласно странице man).

Предполагается, чтоэто x86, вам, возможно, придется вызывать инструкцию WBINVD (или CLFLUSH).

В общем, самоизменяющийся код должен очищать i-кеш, но, насколько я могу судить, удаленно переносимого нет.способ сделать это.

1 голос
/ 26 мая 2011

Я бы посоветовал вам сначала попытаться заставить ваш код работать на C, а затем перевести на ctypes.Также есть что-то вроде CorePy , если вы просто хотите иметь возможность выполнять сборку из Python.

...