You need to enable JavaScript to run this app. C#实现内存二进制补丁 将jnz指令替换为jz的操作失效求助
优惠活动
大模型
产品
解决方案
定价
更多

C#实现内存二进制补丁 将jnz指令替换为jz的操作失效求助

C# 跨进程内存补丁失效问题排查与正确实现

问题场景

需要为目标程序打内存补丁,将内存中固定地址0x00411988处的jnz跳转指令(原始机器码75 3F)替换为jz跳转指令(目标机器码74 3F)。先后尝试MemorySharp类库封装写入、直接调用Windows API写入两种方案,均未触发预期的跳转逻辑修改效果,无法定位操作失效原因。

常见失效原因排查

按问题出现概率从高到低排序:

  • 权限级别不匹配:如果目标程序以高完整性级别(管理员权限、系统权限)运行,而执行补丁的程序以普通权限启动,即使OpenProcess申请了最高访问权限,也会因为UAC权限隔离拿到无写入权限的无效句柄,写入操作直接静默失败。如果目标程序带反调试、反作弊保护,常规OpenProcess调用会被直接拦截,同样返回无效句柄。
  • 目标内存页不具备写入权限:程序代码段默认属性为「只读+可执行」,未修改页属性直接调用WriteProcessMemory时,系统会因为权限不足拒绝写入,不会弹出任何错误提示。
  • 地址不是运行时真实虚拟地址:如果目标程序开启了ASLR(地址空间随机布局),逆向工具中标注的0x00411988通常是静态分析的默认基址下的偏移量,程序实际加载时模块基址会随机变化,硬编码该地址会写入完全无关的内存区域,根本碰不到目标指令。
  • 补丁被后续流程覆盖:加壳程序的代码段是运行时解密才会加载到内存的,过早写入的补丁会被后续解密流程覆盖;部分带保护的程序会定期校验关键指令的字节哈希,发现被篡改后会自动恢复原始指令字节,导致补丁刚写入就被重置。
  • API调用未做返回值校验:很多实现跳过了OpenProcess、WriteProcessMemory的返回值检查,哪怕全程没有成功拿到句柄、没有写入任何字节,也默认操作执行成功。

可靠的内存补丁实现方案

推荐使用直接调用Windows API的实现方式,避免第三方库的封装逻辑带来的不确定问题,核心流程必须包含「权限申请-内存页属性临时修改-字节写入-内存页属性还原-句柄释放」全步骤:

// 导入Win32 API 与常量定义
[DllImport("kernel32.dll")]
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);

[DllImport("kernel32.dll")]
private static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out int lpNumberOfBytesWritten);

[DllImport("kernel32.dll")]
private static extern bool VirtualProtectEx(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr hObject);

private const uint PROCESS_ALL_ACCESS = 0x1F0FFF;
private const uint PAGE_EXECUTE_READWRITE = 0x40;

/// <summary>
/// 对目标进程指定地址写入补丁字节
/// </summary>
/// <param name="processId">目标进程PID</param>
/// <param name="targetAddress">目标指令的运行时真实虚拟地址</param>
/// <param name="patchBytes">需要写入的补丁字节</param>
/// <returns>补丁是否写入成功</returns>
public static bool ApplyMemoryPatch(int processId, IntPtr targetAddress, byte[] patchBytes)
{
    // 1. 打开目标进程获取全权限句柄
    IntPtr hProc = OpenProcess(PROCESS_ALL_ACCESS, false, (uint)processId);
    if (hProc == IntPtr.Zero) return false;

    // 2. 临时修改目标内存页属性为可读写,记录原始属性用于后续还原
    uint oldPageProtect;
    VirtualProtectEx(
        hProc, 
        targetAddress, 
        (UIntPtr)patchBytes.Length, 
        PAGE_EXECUTE_READWRITE, 
        out oldPageProtect
    );

    // 3. 写入补丁字节
    int writtenSize;
    bool isWriteSuccess = WriteProcessMemory(
        hProc, 
        targetAddress, 
        patchBytes, 
        (uint)patchBytes.Length, 
        out writtenSize
    );

    // 4. 将内存页属性还原为原始状态,避免触发程序的内存保护校验
    VirtualProtectEx(
        hProc, 
        targetAddress, 
        (UIntPtr)patchBytes.Length, 
        oldPageProtect, 
        out _
    );

    // 5. 释放进程句柄
    CloseHandle(hProc);

    // 校验实际写入的字节长度与预期一致,确认写入有效
    return isWriteSuccess && writtenSize == patchBytes.Length;
}

写入有效性验证

补丁执行完成后,立刻从目标进程对应地址读取2个字节,比对是否为预期的0x74 0x3F:

  • 如果读出来还是原始的0x75 0x3F,说明要么写入流程本身失败,要么补丁被程序的保护机制自动恢复;
  • 如果读出来已经是0x74 0x3F但程序逻辑没有变化,需要确认该地址是否为目标跳转指令的真实位置,是否存在多层跳转指向该指令的情况。

MemorySharp实现的修正建议

如果使用MemorySharp类库,需要修正硬编码地址的逻辑:不要直接写死0x00411988作为绝对地址,要先获取目标模块的实际加载基址,计算「模块基址 + 静态偏移量」得到真实虚拟地址后再写入。MemorySharp内部已经封装了内存页属性修改的流程,只要地址计算正确、权限匹配,通常可以正常写入。


内容的提问来源于stack exchange,提问作者Anonymous-User

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.27 10:54:02