【Visual Studio】调试时 memcpy 拷贝错误,擅自修改内存

概述

Visual Studio 调试场景下对 memcpy 的修改(问题原因参考AI分析结果)

问题描述

环境说明:

  • 调试器为 Visual Studio 2022
  • 平台工具集和Windows SDK 版本可以任意选,这个不影响该问题的复现
  • 目标机器:Windows Server 2012(目测也和该机器无关)
  • 被调试程序为 x86

Visual Studio 远程调试一个注入程序,在调用 memcpy 将一段函数拷贝到目标内存时,会将部分内存的值修改后拷贝到目标内存。大致如下图所示:

可以看到在 memcpy 的反汇编代码中,正在执行内存拷贝的汇编代码为:

assembly 复制代码
016020BE rep movs byte ptr es:[edi],byte ptr [esi]

图示内容补充:

  1. 当前 esi 的值为 0x010c88ac
  2. 当前 edi 的值为 0x00b987d8

说明刚刚拷贝的目标内存为 0x00b987d7,也就是图中已经标记的位置。

但是,也可以看到源地址的 c7 被拷贝成为了 cc。与实际不符。问题大致就是如此。且只有该函数的该位置会出现问题,以及该函数往后偏移若许处的一位也会出现同样的问题(8b->cc)。


换用其他调试器,如 x86dbg 等均无法复现,所以才能定位到是 Visual Studio 的问题,但具体原因却是不知道的。

AI 分析的结果

几种可能的情况

这里补充一下就相关问题 ChatGPT 的回答:

Visual Studio 调试器本身在修改你的代码段 / 内存页,触发了内联补丁、断点、代码重写或代码分析,导致 memcpy 出错。

为什么只有 VS 会导致 memcpy 出错?

因为 VS 调试器会:

VS行为 导致的副作用
注入软件断点(0xCC) 复制代码段时复制到修改后的指令
对代码段做 Edit-and-Continue 热补丁 某些字节被重写
对 prologue 做 stack-frame 分析 修改 C7 45 xx 类指令
设置 PAGE_GUARD / WRITECOPY memcpy 的 read 被 VEH 拦截并修改数据
Hidden breakpoint & Symbol mapping 对单字节自动 patch

这些都是 VS 特有的,WinDbg/OD 都不会这么做。

因此 VS 调试才导致 memcpy(代码段) 出现错误字节。

看实际情况确实也是在 c7 45 xx 处修改了内存,猜测大概率是因为 JMC 的问题,但是 JMC 无法解释后文中的另一处修改,如下所示:

assembly 复制代码
45 dc 50 8d 4d e8 51 6a 
被修改为了
45 dc 50 8d 4d cc 51 6a

关于这一问题,chatgpt 的回答算是解惑了,原因就在于 VS 的,相关回答如下所示:

🔥 总结一句话(核心结论)

Visual Studio 调试器在本地变量访问指令(含栈帧偏移的 ModR/M displacement)里插入 0xCC,当作"隐藏断点/调试标记 stub"。偏移字节被替换成 0xCC,使指令成为调试器可识别的 patch point,但实际执行时不会触发,因为调试器会动态恢复原字节。

换句话说:

VS 调试器在 patch 指令,而你的 memcpy 把"调试器补丁"也复制过去,所以看到了错误的内存内容。


🔍 逐条解释三处修改

1. c7 45 f8 00 00cc 45 f8 00 00

原始:

复制代码
C7 45 F8 00 00 00 00
mov dword ptr [ebp-8],0

修改后:

复制代码
CC 45 F8 00 00 00 00

Visual Studio 改了 第 1 个字节(原本是 C7)。

🔧 这是 VS 的"单字节 patch point"标记

C7 是 mov r/m32, imm32 的 opcode。

调试器将 opcode 改成 0xCC,以便:

  • 单步调试时识别到这条指令
  • 用于本地变量调试 / Edit & Continue 更新
  • 用于 IL → Native mapping (JMC) 的断点映射点

调试器在实际执行前会恢复原始字节,因此程序不会崩。

你的 memcpy 是只看"内存当前值",因此复制了 VS 的临时补丁。

本质:对 mov [ebp-8], imm32 的 opcode 插了 int3 标记。


2. 8b 45 f8cc 45 f8

原始:

复制代码
8B 45 F8
mov eax,[ebp-8]

修改后:

复制代码
CC 45 F8

调试器又修改了 opcode。

8B 是 "mov r32, r/m32"。

VS 将整个 opcode 变成 0xCC,使其成为调试标记。

这种 patch 最常见于:

  • 单步调试
  • 本地变量监视(Watch)
  • Edit&Continue

🔥 为什么偏移字节(F8)保留?

因为调试器会用"恢复 + 执行 + 再 patch"的方式处理执行,不需要修改 displacement。


3. 8D 4D E88D 4D CC

原始:

复制代码
8D 4D E8
lea ecx,[ebp-18]

修改后:

复制代码
8D 4D CC
lea ecx,[ebp-34]

这里调试器修改了 displacement 字节 (E8 → CC)

🔧 这是 VS 调试器最常见的"隐藏断点"标记

VS 经常使用:

  • 修改 ModR/M 的 位移字节
  • 或 opcode

来插入"不可见调试点(hidden breakpoint)"。

为什么不是 opcode?

因为:

  • lea ecx,[ebp-18] 这种指令不能简单替换成 int3,否则 EIP 变化会破坏栈跟踪

  • 所以只能改 displacement,这样:

    • 调试器知道这里是一个 patch
    • 真正执行前会恢复字节
    • 对用户不可见

本质:在栈访问偏移中埋入调试标记字节 CC。


🔥 结论:三处修改不是随机,而是 Visual Studio 的三种 patch 策略

示例 补丁方式 原因
1 修改 opcode → CC 在 mov 指令上插桩
2 修改 opcode → CC 在读取本地变量时插桩
3 修改 displacement → CC 在不能改 opcode 的情况下"隐藏断点"

你看到的就是 VS 调试引擎(vsdebugeng + mscordbi + enc_k)运行时的:

✔ 调试插桩(Instrumentation)

✔ 非文档化内存 patch

✔ 本地变量访问 patch point

✔ 单步调试补丁

✔ JMC / ENC 在 native 函数上的辅助 patch


🛠 为什么只有 Visual Studio 会这样?

因为:

  • WinDbg / x64dbg 是"原生调试",不会修改指令

  • VS 的"混合调试器"要支持:

    • 托管 + Native stack mapping
    • 单步进入 IL
    • Edit & Continue
    • 本地变量显示
    • 调试优化代码
    • Async stepping
    • JMC 的 IL ↔ Native 行号映射

为此,它必须:

  • 在某些指令前打补丁点
  • 改 opcode / displacement
  • 回写 int3
  • 每次执行前恢复原始字节

🛠 如何验证 VS 在 patch?

WinDbg 下 attach VS 的调试器进程,用写断点:

复制代码
ba w1 <address_of_modified_byte>

命中后,你会看到调用栈:

  • vsdebugeng.dll
  • mscordbi.dll
  • vcruntime???.dll

立刻证明这是调试器插桩。


✔ 如何彻底解决?

方法 1:禁用所有 VS 调试增强

  • 关闭 JMC
  • 关闭 Edit and Continue
  • 关闭 .NET Native/托管混合调试
  • 关闭 Diagnostic Tools
  • 关闭 Symbol loading
  • 进入 Debug → Options → General,关掉所有 "Enable ..." 相关自动调试功能

方法 2:使用 WinDbg / x64dbg 调试

你已经验证过它们不会修改字节。

方法 3:设置代码区为只读(强制禁止调试器写入)

复制代码
VirtualProtect(func_addr, size, PAGE_EXECUTE_READ, &old);

调试器 patch 会失败。


相关推荐
MM_MS3 小时前
SQL Server数据库和Visual Studio (C#)联合编程
开发语言·数据库·sqlserver·c#·visual studio
vortex54 小时前
Visual Studio 编译选项详解
ide·visual studio
记忆偶然4 小时前
语音转文本技术方案评估与工具选型指南
ide·macos·xcode
赵域Phoenix7 小时前
快速打开jupyter
ide·jupyter
还有你Y7 小时前
VScode远程连接docker容器教程
ide·vscode·docker
海市公约8 小时前
Python操作SQLite数据库:从基础语法到完整项目实战
数据库·ide·python·程序人生·架构·pycharm·sqlite
✎ ﹏梦醒͜ღ҉繁华落℘8 小时前
VisualStudio软件使用技巧
ide·visual studio
啃火龙果的兔子10 小时前
android studio运行下载gradle特别慢怎么处理
android·ide·android studio