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.