Proof Of Concept на Python или как портировать С/С++
Язык программирования Python существует уже 31 год. Это полностью объектно-ориентированный язык. За все время существования на нем стало возможно применять разные парадигмы. Сейчас этот язык может поддерживать:
объектно ориентированную парадигму
структурное программрование
обобщенное программирование
функциональное программирование
метапрограммирование
контрактное и логическое программирование
Список уже достаточно внушителен, но язык можно продолжать развивать за счет дополнительных плагинов и библиотек.
Помимо всех этих возможностей из-за простого синтаксиса язык стал самым распространенным для прототипирования различных функций больших и маленьких проектов. Некоторые области Computer Science вообще изначально используют этот язык, потому что он максимально прост и не обязательно долго листать документацию, чтобы понять, как создать простейшее приложение. На Python написано большое количество библиотек, которые могут решать различные задачи: от 3D моделирования до сетевого взаимодействия. Причем использовать язык можно не только на уровне абстракций, которые применяют языки высокого уровня, но также можно выполнять и более низкоуровневые задачи.
Статья будет содержать 3 части, в ходе которых мы будем иссследовать возможность портирования C/C++ кода для различных ОС. В этой статье посмотрим, насколько эффективно можно реализовать возможность работы с оперативной памятью и получением данных от системы.
Библиотека ctypes
Python не был бы так популярен, если бы на его запуск и написание алгоритма тратилось столько времени, сколько требуют языки программирования C/C++. Такое сравнение мы используем не случайно, ведь именно C и C++ являются языками, на которых написан софт со сложной структурой в несколько миллионов строк. К таким гигантам можно отнести в том числе и операционные системы: в них любая библиотека, интерфейс, протокол с большой долей вероятности создавались с использованием C/C++.
Так почему же python можно легко использовать для портирования того или иного кода, написанного на другом популярном языке программирования? Ответ прост: python содержит набор примитивов для работы с другими языками. Поэтому можно "подружить" библиотеку, которая написана на языке программирования C/C++ и др., с новой программой на Python.
В Python для этих целей используется библиотека Ctypes. На момент написания статьи она все еще входит в язык и даже заявлена для beta версии. И все, что эта библиотека делает - предоставляет совместимые с C compatible типы данных, которые позволяют работать с библиотеками и другим расшариваемым кодом. Поэтому, если необходимо, можно написать обертку для Python над старой или новой библиотекой и потом просто вызывать функцию так, как будто Python всегда умел работать с ней.
На самом деле это очень крутая функция, так как для преобразования типов и структур данных можно потратить много времени. Познакомимся с библиотекой поближе.
Ctypes: пример
Библиотека Ctypes позиционируется как механизм для работы в первую очередь с расшариваемым кодом. То есть стартовая часть работы библиотеки может включать функцию загрузки этого кода в память приложения. Для этого в каждой операционной системе есть свой объект. Например, если приложение разрабатывается под ОС Windows, то понадобятся следующие функции библиотеки Ctypes:
cdll
windll
oledll
Выбор необходимой функции зависит от соглашения о вызовах (stdcall, cdecl, fastcall), которую будет использовать расшариваемый портируемый код. Разница в существующих соглашениях о вызовах состоит в последовательности передачи параметров для функций, работой с возвращаемыми значениями и сопутствующими работе функции структурами данных. Получается, чтобы работать с библиотекой Ctypes, придется сначала понять, как работает расшариваемый код и при помощи какого соглашения он создан.
Для отличия можно использовать следующий короткий свод правил:
если в имени функции есть символ
_
и нет символа@
, тогда это__cdecl
если имя функции начинается с символа
_
и содержит знак@
это__stdcall
если имя функции содержит несколько символов
@
это__fastcall
.
Кстати, для операционной системы Windows еще проще можно определять тип fascall соглашения: Windows библиотеки и исполняемые файлы для x64 архитектуры могут использовать только fastcall.
Все типы данных в ctypes начинаются с префикса с_
. Полный список их можно найти здесь.
Попробуем выполнить простую операцию - открыть диалоговое окно в ОС Windows через ctypes. Использовать будем Windows 10, Python 3.9. Исходник:
import ctypes
MB_OK = 1
_user32 = ctypes.WinDLL('user32', use_last_error=True)
_MessageBoxW = _user32.MessageBoxW
_MessageBoxW.restype = ctypes.c_int
def MessageBoxW(hwnd, text, caption, utype):
result = _MessageBoxW(hwnd, text, caption, utype)
if not result:
raise ctypes.WinError(ctypes.get_last_error())
return result
def main():
result = MessageBoxW(None, "Message", "Title", MB_OK)
return result
if __name__ == "__main__":
main()
Результат выглядит примерно так:
Просто, хотя и много доподнительных определений. Попробуем переключиться на портирование чего-то специфичного.
Работа с памятью
Работа с этим ресурсом является чуть ли не основной фишкой, которую используют языки программирования типа С/С++. И дело тут в том, что они умеют работать с памятью без ограничений, причем за большей частью операций с памятью программист должен следить самостоятельно. Чтение памяти может быть полезно для изучения структуры объектов, с которыми работает приложение или операционная система.
Иногда приложению требуется изменить ту или иную часть оперативной памяти другого приложения или самого себя во время работы. Для проведения всех операций используется метод сканирования памяти на нахождение данных по заданным характеристикам. Поэтому остро стоит вопрос о возможности работать с указателями. Попробуем реализовать этот функционал.
Прямое обращение к указателям и проведение арифметических операций, к сожалению, не доступно. Но есть пара вариантов, как это можно реализовать. Не очень изящно, но зато эффективно. Варианты могут быть такими:
создание объекта, который будет по размеру соответствовать размеру сканируемого объема памяти
использование указателя для сканирования, но при этом нужно найти способ модифицировать его адрес
использовать функции WinApi
3-й вариант как раз наш способ. Попробуем его реализовать. Для работы с WinApi придется использовать документацию.
И так как это работа через WinApi, то для выполнения чтения нужно провести несколько операций:
Получить хэндлер процесса
Запросить доступ к нужному адресу
Считать данные
Если переводить на функции, которые необходимы для чтения оперативной памяти, то нужно разобрать документацию по следующим функциям:
Для всех функций желательно так же определить константы, которыми они пользуются, поэтому будем их задавать в начале скрипта. В итоге у нас получится вот такой код:
from ctypes import *
from ctypes.wintypes import *
import binascii
# Определим какие функции нам необходимы для работы
_kernel32 = WinDLL('kernel32.dll')
OpenProcess = _kernel32.OpenProcess
OpenProcess.argtypes = DWORD,BOOL,DWORD
OpenProcess.restype = HANDLE
ReadProcessMemory = _kernel32.ReadProcessMemory
ReadProcessMemory.argtypes = HANDLE,LPVOID,LPVOID,c_size_t,POINTER(c_size_t)
ReadProcessMemory.restype = BOOL
GetProcessId = _kernel32.GetCurrentProcessId
GetProcessId.restype = c_int
_user32 = WinDLL('user32.dll')
IsMenu = _user32.IsMenu
# Раздел констант, чтобы не искать по документации
STANDARD_RIGHTS_REQUIRED = 0x000F0000
SYNCHRONIZE = 0x00100000
PROCESS_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFF)
#Функция для показа содержимого памяти в удобном формате
def hexdump(p):
output = []
l = len(p)
i = 0
while i < l:
output.append('{:04d} '.format(i))
for j in range(16):
if (i + j) < l:
byte = p[i + j]
output.append('{:02X} '.format(byte))
else:
output.append(' ')
if (j % 16) == 7:
output.append(' ')
output.append(' ')
output.append('\n')
i += 16
return ''.join(output)
def main:
# найдем PID
procId = GetProcessId()
if get_last_error() != 0:
print("Cannot get Process ID")
return -1
# для работы с процессом нужно получить хэндл
process_handle = OpenProcess(PROCESS_ALL_ACCESS, False, procId)
if get_last_error() != 0:
print("Cannot get Process Handle")
return -1
#Начинаем читать
pIsMenu = ctypes.cast(IsMenu, POINTER(c_byte))
STRLEN = 255
buf = create_string_buffer(STRLEN)
bytesReaded = c_size_t()
if not ReadProcessMemory(process_handle, pIsMenu, buf, STRLEN, byref(s)):
print("Cannot read memory!")
return -1
rawBytes = ""
for i in buf:
rawBytes+=i.hex()
rawBytes = binascii.unhexlify(rawBytes)
hexdump(rawBytes)
if __name__ == "__main__":
main()
При работе с WinAPI лучше всего проверять результат, мало ли, что вернула функция. Поэтому, чтобы не было неожиданных ситуаций, нужно описать, какие параметры использует функция и какие данные должна вернуть. А чтобы было удобнее смотреть содержимое оперативной памяти, добавили функцию для красивого отображения.
В следующей статье попробуем поработать с механизмами добавления кода в уже работающий процесс и помониторим его активность. А сейчас хочу пригласить всех желающих на бесплатный демоурок в рамках которого обсудим различные виды типизации, заглянем в теорию типов, рассмотрим примеры и best practice по аннотированию в Python, а также поговорим про существующие type checker'ы. Регистрация уже доступна по ссылке.