Usando ctypes para invocar funções em processos e ler estruturas de memória em Python

Neste artigo, exploraremos como o ctypes pode ser utilizado para interagir com processos em Python, permitindo chamar funções dentro de outros processos e ler estruturas de memória diretamente. O ctypes é uma biblioteca que facilita a chamada de bibliotecas dinâmicas (DLLs no Windows, SOs no Linux) a partir do Python. Quando o Python é injetado em outro processo, o ctypes pode ser empregado para manipular dados e invocar funções no espaço de memória desse processo, oferecendo flexibilidade sem a necessidade de recompilar código C.

Interação com Processos

Para ilustrar a funcionalidade, vamos criar um programa de teste em C++ com funções e estruturas. O código abaixo define algumas funções de exemplo:

typedef int(*ponteiro_cdecl_soma)(int, int);
typedef int(__stdcall *ponteiro_stdcall_soma)(int, int);

struct CStringa {
    wchar_t* str = nullptr;
    tamanho_t tam = 0;
    CStringa(wchar_t* ss) {
        str = ss;
        tam = wcslen(ss);
    }
};

CStringa global_ccs((wchar_t*)L"variavel_global_estrutura");

int cdecl_soma(int x, int y) {
    std::wcout << L"Chamada cdecl\n";
    return x + y;
}

int __stdcall stdcall_soma(int x, int y) {
    std::wcout << L"Chamada stdcall\n";
    return x + y;
}

int callback_soma(ponteiro_stdcall_soma func, int a, int b) {
    std::wcout << L"callback_soma\n";
    return func(a, b);
}

int imprimir_console(CStringa* cs) {
    std::wcout << L"Imprimindo CStringa: ";
    std::wcout << cs->str;
    std::wcout << L"\n";
    return cs->tam;
}

Invocando Funções no Processo

Supondo que o Python seja injetado no processo alvo (usando uma DLL como descrito em artigos anteriores), podemos localizar o endereço das funções. Por exemplo, para cdecl_soma, encontramos o deslocamento em relação à base do executável usando um depurador. Definimos uma função para obter a base do módulo:

import ctypes

kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
ObterHandleModulo = kernel32.GetModuleHandleW
ObterHandleModulo.argtypes = (ctypes.c_wchar_p,)
ObterHandleModulo.restype = ctypes.c_int
base_modulo = ObterHandleModulo("TesteCtypes.exe")

Para chamar cdecl_soma, criamos um ponteiro de função e calculamos o endereço:

# Definição do tipo de ponteiro de função para cdecl
tipo_ponteiro_cdecl = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int)
deslocamento_cdecl = 0x00AF4190 - 0x00AE0000  # Deslocamento obtido do depurador
endereco_cdecl = base_modulo + deslocamento_cdecl
funcao_cdecl = tipo_ponteiro_cdecl(endereco_cdecl)
resultado_cdecl = funcao_cdecl(100, 200)
print("Resultado cdecl: ", resultado_cdecl)

Para funções com convenção stdcall, usamos WINFUNCTYPE:

tipo_ponteiro_stdcall = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int)
deslocamento_stdcall = 0x00AF43B0 - 0x00AE0000
endereco_stdcall = base_modulo + deslocamento_stdcall
funcao_stdcall = tipo_ponteiro_stdcall(endereco_stdcall)
resultado_stdcall = funcao_stdcall(50, 75)
print("Resultado stdcall: ", resultado_stdcall)

Construindo e Usando Estruturas

Para chamar uma função que aceita uma estrutura, primiero definimos a estrutura em Python com ctypes:

class CStringa(ctypes.Structure):
    _fields_ = [
        ('str', ctypes.c_wchar_p),
        ('tam', ctypes.c_uint)
    ]

Em seguida, definimos a função imprimir_console e a invocamos com uma instância da estrutura:

tipo_ponteiro_imprimir = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.POINTER(CStringa))
deslocamento_imprimir = 0x00AF2F10 - 0x00AE0000
endereco_imprimir = base_modulo + deslocamento_imprimir
funcao_imprimir = tipo_ponteiro_imprimir(endereco_imprimir)

instancia_cs = CStringa()
mensagem = "Texto da estrutura em Python"
instancia_cs.str = ctypes.c_wchar_p(mensagem)
instancia_cs.tam = len(mensagem)

resultado_imprimir = funcao_imprimir(ctypes.byref(instancia_cs))
print("Saída de imprimir_console: ", resultado_imprimir)

Lendo Estruturas Globais da Memória

Estruturas globais podem ser acessadas calculando seu endereço a partir da base do módulo:

deslocamento_global = 0x00AFE2D0 - 0x00AE0000
endereco_global = base_modulo + deslocamento_global

# Leitura direta dos campos
campo_str = ctypes.c_wchar_p.from_address(endereco_global)
campo_tam = ctypes.c_uint.from_address(endereco_global + 0x4)
print("Campos separados: ", campo_str.value, campo_tam.value)

# Leitura como estrutura completa
estrutura_global = CStringa.from_address(endereco_global)
print("Estrutura completa: ", estrutura_global.str, estrutura_global.tam)

Implementando Funções de Callback

Callbacks do Python podem ser passadas para funções no processo alvo. Definimos uma função Python no estilo stdcalll:

def meu_stdcall_soma(a: int, b: int) -> int:
    print("Callback Python: ", a, b)
    return a - b

Então, configuramos a função de callback e a chamamos:

tipo_callback = ctypes.CFUNCTYPE(ctypes.c_int, tipo_ponteiro_stdcall, ctypes.c_int, ctypes.c_int)
deslocamento_callback = 0x00AF40D0 - 0x00AE0000
funcao_callback = tipo_callback(base_modulo + deslocamento_callback)

ponteiro_callback = tipo_ponteiro_stdcall(meu_stdcall_soma)
resultado_callback = funcao_callback(ponteiro_callback, 10, 3)
print("Resultado do callback: ", resultado_callback)

Esses exemplos demonstram como o ctypes pode ser utilizado para interagir com processos de forma dinâmica em Python, permitindo a manipulação avançada de memória e funções.

Tags: Python ctypes injeção de processo manipulação de memória Windows API

Publicado em 6-21 21:41