当前位置:首页 > 技术 > 正文内容

在Windows进程中注入Python解释器实现远程调试

访客 技术 2026年6月3日 1

背景概述

在实现微信机器人的过程中,我们需要将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

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。