在Windows进程中注入Python解释器实现远程调试
背景概述
在实现微信机器人的过程中,我们需要将Python运行时注入到目标进程内部,从而实现在外部控制目标应用的行为。本文详细介绍如何将Python解释器封装为DLL并注入到任意进程中。
进程注入器实现
首先需要编写一个注入器程序,其核心原理是利用Windows的远程线程机制,通过CreateRemoteThread在目标进程内加载指定的DLL文件。
import ctypes
from ctypes import wintypes
import platform
import time
# 定义Windows API常量
PROCESS_ALL_ACCESS = 0x1F0FFF
MEM_COMMIT = 0x1000
PAGE_EXECUTE_READWRITE = 0x40
MAX_PATH = 260
# 加载系统DLL
kernel32 = ctypes.windll.kernel32
# 获取函数地址
OpenProcess = kernel32.OpenProcess
VirtualAllocEx = kernel32.VirtualAllocEx
WriteProcessMemory = kernel32.WriteProcessMemory
CreateRemoteThread = kernel32.CreateRemoteThread
CloseHandle = kernel32.CloseHandle
LoadLibraryW = kernel32.LoadLibraryW
def inject_dll(pid, dllpath):
"""将DLL注入到指定进程"""
# 验证Python架构匹配
if platform.architecture()[0] != '32bit':
raise RuntimeError("当前Python为64位,需要使用32位版本才能注入32位进程")
# 打开目标进程
hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, pid)
if not hProcess:
raise RuntimeError(f"无法打开进程,PID: {pid}")
try:
# 在目标进程分配内存
lpAddress = VirtualAllocEx(hProcess, None, MAX_PATH, MEM_COMMIT, PAGE_EXECUTE_READWRITE)
if not lpAddress:
raise RuntimeError("内存分配失败")
# 写入DLL路径
path_bytes = dllpath.encode('utf-16le')
WriteProcessMemory(hProcess, lpAddress, path_bytes, len(path_bytes) + 2, ctypes.byref(ctypes.c_ulong()))
# 创建远程线程加载DLL
hRemote = CreateRemoteThread(hProcess, None, 0, LoadLibraryW, lpAddress, 0, None)
if not hRemote:
raise RuntimeError("远程线程创建失败")
time.sleep(0.1)
CloseHandle(hRemote)
finally:
CloseHandle(hProcess)
将Python运行时封装为DLL
原理分析
Python的核心功能集中在python310.dll中,python.exe和pythonw.exe只是加载该DLL并调用Py_Main函数。通过分析python.exe的导入表,可以发现这一点。
动态加载方式
我们先测试使用LoadLibraryW动态加载Python DLL的方式:
#include <windows.h>
#include <iostream>
int wmain(int argc, wchar_t* argv[]) {
const wchar_t* dllFileName = L"python310.dll";
// 加载Python动态库
HMODULE hModule = LoadLibraryW(dllFileName);
if (!hModule) {
std::cout << "DLL加载失败" << std::endl;
return -1;
}
// 获取Py_Main函数地址
FARPROC mainAddr = GetProcAddress(hModule, "Py_Main");
typedef int(*PyMainFunc)(int, wchar_t**);
PyMainFunc Py_Main = (PyMainFunc)mainAddr;
int result = Py_Main(argc, argv);
return result;
}
编译后放置在与python310.dll相同目录下,功能与原生python.exe完全一致。
封装为DLL
由于DLL无法直接接收命令行参数,需要在代码中硬编码路径:
#include <windows.h>
void initialize_python() {
const wchar_t* dllPath = L"T:\\Python\\python-3.10.11-embed-win32\\python310.dll";
HMODULE hModule = LoadLibraryW(dllPath);
FARPROC mainAddr = GetProcAddress(hModule, "Py_Main");
typedef int(*PyMainFunc)(int, wchar_t**);
PyMainFunc Py_Main = (PyMainFunc)mainAddr;
// 获取当前可执行文件路径
wchar_t exePath[MAX_PATH];
GetModuleFileNameW(NULL, exePath, MAX_PATH);
int argc = 1;
wchar_t* argv[2] = { exePath, nullptr };
Py_Main(argc, argv);
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReason, LPVOID lpReserved) {
switch (ulReason) {
case DLL_PROCESS_ATTACH:
initialize_python();
default:
break;
}
return TRUE;
}
注入后发现Python未能正常执行,需要显式调用初始化函数。
初始化Python运行时
直接调用Py_Main无法完成初始化,需要使用Py_InitializeEx函数:
void run_python_interpreter() {
const wchar_t* dllPath = L"T:\\Python\\python-3.10.embed-win32\\python310.dll";
HMODULE hModule = LoadLibraryW(dllPath);
// 设置程序名称和初始化解释器
wchar_t exePath[MAX_PATH];
GetModuleFileNameW(NULL, exePath, MAX_PATH);
// 获取函数指针
typedef void(*PySetProgramNameFunc)(const wchar_t*);
typedef void(*PyInitializeExFunc)(int);
typedef int(*PyRunSimpleStringFunc)(const char*);
PySetProgramNameFunc Py_SetProgramName = (PySetProgramNameFunc)GetProcAddress(hModule, "Py_SetProgramName");
PyInitializeExFunc Py_InitializeEx = (PyInitializeExFunc)GetProcAddress(hModule, "Py_InitializeEx");
PyRunSimpleStringFunc PyRun_SimpleString = (PyRunSimpleStringFunc)GetProcAddress(hModule, "PyRun_SimpleString");
Py_SetProgramName(exePath);
Py_InitializeEx(1);
// 测试Python执行
PyRun_SimpleString("import os; os.makedirs('D:\\\\test_dir', exist_ok=True)");
}
执行后可以正常读写文件,但交互式终端未能显示。
解决控制台输出问题
目标进程为GUI程序时,需要手动分配控制台并重定向标准流:
#include <stdio.h>
void setup_console() {
// 分配新的控制台
AllocConsole();
// 重定向标准输入输出
FILE* conin = stdin;
FILE* conout = stdout;
FILE* conerr = stderr;
freopen_s(&conin, "CONIN$", "r+t", stdin);
freopen_s(&conout, "CONOUT$", "w+t", stdout);
freopen_s(&conerr, "CONOUT$", "w+t", stderr);
}
注意:该方案在Debug编译模式下无法生效,需要使用Release版本。
通过Python模块打开交互终端
除了控制台重定向,还可以在Python代码中主动打开交互式终端:
# 方式1:使用-i选项启动
# python -i script.py
# 方式2:代码中调用
import code
code.interact()
# 方式3:使用本地变量
import code
code.InteractiveConsole(globals()).interact()
# 方式4:设置环境变量
import os
os.environ["PYTHONINSPECT"] = "1"
静态链接方式
除了动态加载,还可以采用静态链接方式,需要配置Python的头文件和库文件:
#include "Python.h"
#include <windows.h>
DWORD WINAPI python_thread(LPVOID param) {
// 分配控制台
AllocConsole();
freopen("CONIN$", "r+t", stdin);
freopen("CONOUT$", "w+t", stdout);
freopen("CONOUT$", "w+t", stderr);
wchar_t exePath[MAX_PATH];
GetModuleFileNameW(NULL, exePath, MAX_PATH);
// 初始化Python
Py_SetProgramName(exePath);
Py_InitializeEx(1);
PyRun_SimpleString("print('Python已注入')");
Py_Main(1, nullptr);
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReason, LPVOID lpReserved) {
if (ulReason == DLL_PROCESS_ATTACH) {
CreateThread(NULL, 0, python_thread, NULL, 0, NULL);
}
return TRUE;
}
注意:采用静态链接时,编译后的DLL和Python运行时文件需要放置在被注入进程的同目录下。
完整代码
本文涉及的完整代码已托管至GitHub:PyRobot-part2