Язык программирования 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, то для выполнения чтения нужно провести несколько операций:

  1. Получить хэндлер процесса

  2. Запросить доступ к нужному адресу

  3. Считать данные

Если переводить на функции, которые необходимы для чтения оперативной памяти, то нужно разобрать документацию по следующим функциям:

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


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'ы. Регистрация уже доступна по ссылке.

Комментарии (6)


  1. Digitator
    01.02.2022 20:28
    +1

    Это полностью объектно-ориентированный язык

    Не вводите людей в заблуждение!


    1. w96k
      02.02.2022 18:00

      Что не так? По идее в Python всё является объектом, +-*/ и т.д. просто сахар над вызовом метода у объекта числа, также как len() и другие функции это просто функция, которая дёргает метод с таким же названием у объекта из параметра функции, такая псевдо-процедурщина для неискушённых пользователей языка. Может я не в курсе чего-то, раскройте свою мысль.


      1. Digitator
        03.02.2022 23:06

        приватные методы неполноценны


        1. w96k
          04.02.2022 02:45

          Подозревал, что нападка примерно в эту сторону будет. Модификаторы доступа не являются неотъемлемой частью ООП, их похоже в своё разрекламировали вместе с Java. Smalltalk, CLOS и кажется Simula не имеют модификаторов доступа, но вполне себе ООП.


  1. aleex
    01.02.2022 21:21
    +1

    Я уж думал, что тут действительно про портирование через Clang и кодогенерацию...


  1. hello_max
    03.02.2022 15:09

    Не совсем по теме, но:

    логическое программирование

    Каким образом Python со стандартной библиотекой поддерживает логическое программирование?