Как я могу называть встроенный машинный код в Python на Linux?

Я пытаюсь вызывать встроенный машинный код из чистого кода 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()

Этот код segfaults в последней строке.

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

  • Есть ли способ исправить код? Могу ли я называть машинный код чистым 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...).

Ответы

Ответ 1

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

Вот строка в 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

Ответ 2

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

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

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

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

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

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

Ответ 3

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