You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

调用ResumeThread恢复挂起进程崩溃,Process Explorer恢复则正常

修改进程启动参数后ResumeThread导致崩溃的问题排查

问题描述

我正在学习C语言,开发一个修改进程启动命令行参数的项目。遇到以下问题:

  • 通过代码调用ResumeThread恢复挂起的进程时,进程直接崩溃退出
  • 但通过Process Explorer手动恢复该进程时,进程能正常启动并运行

我的测试程序print_my_arguments.exe用于循环打印命令行参数:若注释代码中的ResumeThread(pi.hThread);语句,通过Process Explorer恢复进程后,程序会打开新窗口并正常打印参数;保留该语句时,进程直接退出。不清楚问题根源,也不知道如何自动收集进程退出的相关信息。

实现代码

#include <Windows.h>
#include <winternl.h>
#include <stdio.h> // 新增用于打印退出码

int main(int argc, char* argv[]) {
    STARTUPINFO si = { 0 };
    PROCESS_INFORMATION pi = { 0 };

    si.cb = sizeof(si);

    // 注意:路径中的反斜杠需要转义为双反斜杠
    BOOL bRet = CreateProcessW(NULL, L"C:\\Users\\John\\Desktop\\print_my_arguments.exe firstarg secondarg thirdarg", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);

    if (!bRet) {
        printf("CreateProcess failed: %lu\n", GetLastError());
        return 1;
    }

    WCHAR szNewCommandLine[] = L"This is the new arguments that will show up.";

    SIZE_T sLen = 0;
    SIZE_T nNewCmdLineLen = wcslen(szNewCommandLine);

    PROCESS_BASIC_INFORMATION pinfo;
    NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pinfo, sizeof(pinfo), (DWORD*)&sLen);

    // Read the PEB.
    PEB peb;
    if (!ReadProcessMemory(pi.hProcess, pinfo.PebBaseAddress, &peb, sizeof(peb), &sLen)) {
        printf("Read PEB failed: %lu\n", GetLastError());
        return 1;
    }

    // Read the length of the Process Parameters buffer.
    DWORD nOriginalProcessParamsLen;
    if (!ReadProcessMemory(pi.hProcess, &((DWORD*)peb.ProcessParameters)[1], &nOriginalProcessParamsLen, sizeof(nOriginalProcessParamsLen), &sLen)) {
        printf("Read ProcessParams length failed: %lu\n", GetLastError());
        return 1;
    }

    // Allocate a local buffer to store the contents of our Process Parameters buffer.
    DWORD nNewProcessParamsLen = nOriginalProcessParamsLen + ((nNewCmdLineLen + 1) * 2);
    LPBYTE pNewParamsBuffer = (LPBYTE)malloc(nNewProcessParamsLen);
    if (!pNewParamsBuffer) {
        printf("Malloc failed\n");
        return 1;
    }
    RTL_USER_PROCESS_PARAMETERS* pNewParams = (RTL_USER_PROCESS_PARAMETERS*)pNewParamsBuffer;

    // Read the contents of the original Process Parameters buffer, into our new local buffer
    if (!ReadProcessMemory(pi.hProcess, (LPVOID)peb.ProcessParameters, pNewParamsBuffer, nOriginalProcessParamsLen, &sLen)) {
        printf("Read original ProcessParams failed: %lu\n", GetLastError());
        free(pNewParamsBuffer);
        return 1;
    }

    // Allocate a new buffer in the remote process.
    BYTE* pNewParamsAddr = (BYTE*)VirtualAllocEx(pi.hProcess, NULL, nNewProcessParamsLen, MEM_COMMIT, PAGE_READWRITE);
    if (!pNewParamsAddr) {
        printf("VirtualAllocEx failed: %lu\n", GetLastError());
        free(pNewParamsBuffer);
        return 1;
    }

    // Copy the new command line to the end of the new Process Parameters buffer
    wcscpy_s((LPWSTR)&pNewParamsBuffer[nOriginalProcessParamsLen], nNewCmdLineLen + 1, szNewCommandLine);

    // Adjust the buffer size at the beginning of the Process Parameters structure (first two DWORDs)
    ((DWORD*)pNewParamsBuffer)[0] = (DWORD)nNewProcessParamsLen;
    ((DWORD*)pNewParamsBuffer)[1] = (DWORD)nNewProcessParamsLen;

    // Set the command line pointer to the address at the end of the buffer.
    pNewParams->CommandLine.Buffer = pNewParamsAddr + nOriginalProcessParamsLen;
    pNewParams->CommandLine.Length = (USHORT)(nNewCmdLineLen * 2);
    pNewParams->CommandLine.MaximumLength = (USHORT)((nNewCmdLineLen * 2) + 2);

    // 修正:正确处理指针偏移,避免64位系统下的指针截断
    for (DWORD offset = 0; offset < nOriginalProcessParamsLen; offset += sizeof(PVOID)) {
        PVOID* valuePtr = (PVOID*)&pNewParamsBuffer[offset];
        PVOID value = *valuePtr;
        if (value >= peb.ProcessParameters && value < (PVOID)((BYTE*)peb.ProcessParameters + nOriginalProcessParamsLen)) {
            *valuePtr = (PVOID)((BYTE*)value - (BYTE*)peb.ProcessParameters + (BYTE*)pNewParamsAddr);
        }
    }

    // Write the new buffer to the remote process
    if (!WriteProcessMemory(pi.hProcess, (LPVOID)pNewParamsAddr, pNewParamsBuffer, nNewProcessParamsLen, &sLen)) {
        printf("Write new ProcessParams failed: %lu\n", GetLastError());
        VirtualFreeEx(pi.hProcess, pNewParamsAddr, 0, MEM_RELEASE);
        free(pNewParamsBuffer);
        return 1;
    }

    // 修正:正确计算远程PEB中ProcessParameters的地址
    LPVOID pebProcessParamsAddr = (LPVOID)((BYTE*)pinfo.PebBaseAddress + offsetof(PEB, ProcessParameters));
    if (!WriteProcessMemory(pi.hProcess, pebProcessParamsAddr, &pNewParamsAddr, sizeof(PVOID), &sLen)) {
        printf("Write PEB ProcessParams failed: %lu\n", GetLastError());
        VirtualFreeEx(pi.hProcess, pNewParamsAddr, 0, MEM_RELEASE);
        free(pNewParamsBuffer);
        return 1;
    }

    // 可选:将远程内存权限改为只读,贴近系统默认属性
    DWORD oldProtect;
    VirtualProtectEx(pi.hProcess, pNewParamsAddr, nNewProcessParamsLen, PAGE_READONLY, &oldProtect);

    free(pNewParamsBuffer);

    ResumeThread(pi.hThread);
    // 新增:等待进程结束并获取退出码
    WaitForSingleObject(pi.hProcess, INFINITE);
    DWORD exitCode;
    GetExitCodeProcess(pi.hProcess, &exitCode);
    printf("Process exited with code: %lu\n", exitCode);

    // 关闭句柄
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

    Sleep(5000);
    return 0;
}

问题根源与修复要点

1. 核心问题:PEB指针写入地址错误

原代码中修改PEB的ProcessParameters指针时,错误地使用了本地进程的结构体地址:

// 错误写法
WriteProcessMemory(pi.hProcess, (LPVOID)(&pinfo.PebBaseAddress->ProcessParameters), &pNewParamsAddr, sizeof(VOID*), &sLen);

&pinfo.PebBaseAddress->ProcessParameters是本地栈中pinfo结构体的成员地址,而非远程进程PEB中ProcessParameters的实际地址。正确做法是用offsetof宏计算PEB结构体中ProcessParameters的偏移量,再加上远程PEB的基地址:

// 正确写法
LPVOID pebProcessParamsAddr = (LPVOID)((BYTE*)pinfo.PebBaseAddress + offsetof(PEB, ProcessParameters));
WriteProcessMemory(pi.hProcess, pebProcessParamsAddr, &pNewParamsAddr, sizeof(PVOID), &sLen);

这是导致进程崩溃的直接原因——错误的内存写入导致PEB指向非法地址,进程恢复后无法正常读取参数。

2. 次要问题:64位指针截断

原代码中处理指针偏移时,错误地将PVOID类型转换为BYTE*,导致64位系统下指针被截断为1字节:

// 错误写法
BYTE* value = *((BYTE*)(&pNewParamsBuffer[offset]));

修正后用PVOID*类型处理指针,确保32/64位系统都能正确解析指针值:

// 正确写法
PVOID* valuePtr = (PVOID*)&pNewParamsBuffer[offset];
PVOID value = *valuePtr;

3. 进程退出信息收集

要自动获取进程退出原因,可在ResumeThread后调用WaitForSingleObject等待进程结束,再用GetExitCodeProcess获取退出码:

  • 若退出码为0xC0000005(STATUS_ACCESS_VIOLATION),说明存在内存访问错误
  • 若为0则表示正常退出
  • 其他错误码可对照Windows系统错误码表定位问题

验证方法

修复上述问题后,重新编译运行代码:

  1. 进程恢复后应正常启动print_my_arguments.exe并打印新的命令行参数
  2. 控制台会输出进程退出码,若为0则表示正常结束

内容的提问来源于stack exchange,提问作者nahjoh44

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 22:55:19