Как я могу называть встроенный машинный код в 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.