【问题标题】:Strange behavior causing reference to be set to null when calling ReadProcessMemory调用 ReadProcessMemory 时导致引用设置为 null 的奇怪行为
【发布时间】:2013-01-19 00:34:51
【问题描述】:

当我通过这个 P/Invoke 签名在 C# 中调用 ReadProcessMemory 时遇到了一些非常奇怪的行为:

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ReadProcessMemory(
    IntPtr hProcess,
    IntPtr lpBaseAddress,
    [Out] byte[] lpBuffer,
    int dwSize,
    out int lpNumberOfBytesRead
    );

在我的应用程序中,我正在扫描具有读写访问权限的内存区域的整个内存(并应用了更多过滤器,不过这是另一部分)。

扫描部分的代码是这样的:

int numberOfBytes;
if (!NativeMethods.ReadProcessMemory(handle, region.StartAddress,
    buffer, (int)region.RegionSize, out numberOfBytes))
// The handle, region (custom struct containing some fields from the
// MEMORY_BASIC_INFORMATION struct), and buffer come from parameters.

而且代码运行良好。它扫描整个内存以查找字节序列。没有问题。


在我的程序流程中更进一步,我有以下代码:
注意:它使用与之前的代码相同的句柄 IntPtr(检查过),它在同一个线程中运行

int bytesRead;
byte[] buffer = new byte[128]; // In my real app this is some calculated value
                            // however that irrelevant. It's calculated 128.
if (!NativeMethods.ReadProcessMemory(handle, location.Location,
    buffer, buffer.Length, out bytesRead))
    continue; // Error while reading
// At this point buffer == null, so the next line causes an exception
if (bytesRead != buffer.Length) continue;

代码非常相似,但由于某种原因,对缓冲区的引用丢失并且缓冲区被设置为空。如果它不是外部调用,我会 100% 确定这是一个错误,因为缓冲区没有作为 refout 参数传递。但是我知道 .NET 在涉及外部调用(例如编组)时会做一些伏都教的事情。

让情况更奇怪的是,当我将代码替换为:

int bytesRead;
byte[] buffer = new byte[128];
byte[] bufferRef = buffer;
if (!NativeMethods.ReadProcessMemory(handle, location.Location,
    buffer, buffer.Length, out bytesRead))
    continue; // Error while reading
buffer = bufferRef;
if (bytesRead != buffer.Length) continue;

代码简单有效。内存读取和所有!所以发生的所有事情是,由于某种原因,buffer 变量失去了对实际缓冲区的引用。它把我搞糊涂了。


这种行为是否是由于我做错了什么(例如错误的 P/Invoke)造成的,是否危险(内存泄漏?),并且可以解释?


我的配置:

  • .NET Framework 4.0
  • Visual Studio Professional 2012(版本 11.0.51106.01 更新 1)
  • 已安装 .NET Framework 4.5.50709
  • 以管理员身份运行
  • 在发布版本和调试版本中都发生,包括在 Visual Studio 宿主可执行文件和常规构建可执行文件中。
  • Windows 7 64 位
  • 我正在从中读取内存的进程是 32 位的
  • 构建配置:平台:任何 CPU

编辑:我正在使用的完整 NativeMethods 类可以在这里找到:http://paste2.org/p/2770271

Edit2:我添加了解决问题所遵循的简单步骤作为答案,可以在here找到。

【问题讨论】:

  • 旁注:Marshal.GetLastWin32Error() 返回 1008,与启动时返回的代码相同。
  • 您的 MEMORY_BASIC_INFORMATION 声明错误,RegionSize 是 UIntPtr,而不是 ulong。不确定这会如何破坏堆栈。
  • @HansPassant 谢谢,这为我指明了解决问题的方向:)!

标签: c# winapi pinvoke


【解决方案1】:

可能由于您是 64 位应用程序,您的 lpNumberOfBytesRead 应该是“长”的,因此对 ReadProcessMemory 的调用会在返回时覆盖(您的一部分)缓冲区指针。

【讨论】:

  • 哪里失败了?在 64 位上,您是否尝试将互操作声明更改为使用 long 来读取大小?
  • 我完整的原生方法类可以在here找到。
  • 是的,我的应用程序是使用 x64 构建的,但是我正在从 x86 应用程序读取内存。但由于某种原因,VirtualQueryEx 不起作用。
  • VirtualQueryEx 和 ReadProcessMemory 都有参数,其大小取决于平台 - 您的平台 - 您正在读取的进程的位数与此无关。
【解决方案2】:

在您的 ReadProcessMemory 导入中声明的 byte[] lpBufferOutAttribute (这是 _Out_ LPVOID lpBuffer 的正确移植)指定数据应该从被调用者封送回调用者......所以您的字节数组可能被空引用由被调用者本身(Kernel32)。

【讨论】:

  • 当它正确填充数据并且它返回一个非零值(或者换句话说,没有引发错误)时,为什么它会被 kernel32 清空?编辑:它确实可以正确编组,因为缓冲区中的所有数据都可用(之后的一些代码会检查数据并正确验证)。所以读取正确。为什么它在我提供的第一个代码中起作用,而在第二个代码中不起作用?
  • 您是否尝试将进程附加到调试器以查看发生了什么?
  • 对 Visual Studio 的调试器是的,对 OllyDbg 的调试器没有,你认为这会有帮助吗?
  • OllyDbg 不会完全附加(它会给出一些不受支持的错误),很可能是因为它在 VM 下运行。
  • 您可以使用 VS JIT 调试器。只需从程序集本身运行您的应用程序,让它崩溃......然后当抛出异常时,应该会弹出一个窗口,询问您是否要调试应用程序......这样做。
【解决方案3】:

由于Hans Passant500 - Internal Server Error(我将其标记为已接受答案)的提示,我设法解决了问题。

这些是我采取的步骤:

  1. 我选择使用 32 位而不是任何 CPU。 (主要是为了向后兼容。)
  2. 然后我使用 MSDN 页面更新了 P/Invoke 签名,用于函数和 this 页面关于 windows 数据类型。并选择了签名的 32 位变体。
  3. 我再次运行代码,缓冲区引用没有被清除。

感谢您的帮助。

【讨论】:

  • 关于 1) 的问题,如果我在 x64 环境下并且使用 32 位目标平台运行您的代码,我得到的正是您的错误。
  • 那是因为我没有发布固定的 P/Invoke 签名。本主题中的错误,它们导致了问题(默默地)。
【解决方案4】:

不要将byte[] buffer 标记为[Out] 参数。它更类似于ref 而不是out,因为它是一个字节数组,所以已经隐含了。由于整数(此处为int)是一种值类型,因此它需要out 参数用于numberOfBytesRead。那是唯一应该用out标记的。

当某物被标记为out 参数时,Marshal 类期望被调用者(此处为ReadProcessMemory)返回一个值。字节数组只是指向内存中包含字节的位置的指针(地址)。您不希望被调用者写入此指针。

【讨论】:

  • OutAttributerefout 参数调用完全无关。除此之外,您关于int(或我假设其他值类型)是唯一应该标记为refout 的参数的观点完全是垃圾,有很多好的设计都将引用类型标记为这样。 OutAttribute 与编组有关,是必需的。
  • [OutAttribute] 并非完全不相关。 AFAIK out[Out] ref 的同义词。你可以说出你想要的,但我已经通过 P/Invoke 使用ReadProcessMemory 等编写了大量代码,并且从未在缓冲区上将它们标记为ref[Out]。我看到这些函数发生了很多奇怪的事情(通常是 Win32 错误或零读取),但缓冲区没有被NULL 覆盖。我试图通过指出与我的工作代码不同的地方来帮助你。
  • 这是因为 VM 使用了超出默认 C# 行为的不同互操作技术。就像我在问题中所说的那样,如果这不是外部函数,我会将其归档为错误。 [Out] 但是[Out] ref 相同。 ref 和 out 参数是 CIL 的一个特性,与 OutAttribute 那样提示 VM 的属性无关。无论它们的结果多么相似,它们都是根本不同的,不应该说它们是相同的。
  • 这里有一个很好的资源:@​​987654321@ 从那里的信息来看,out 对应于[OutAttribute]ref 对应于[InAttribute], [OutAttribute]。有些对象类型具有“默认值”,例如StringBuilder,默认为[In], [Out],您无需指定。关键是我怀疑你是否需要为byte[] 类型传递[Out],因为它实际上是一个byte* 在幕后你确实希望被调用者改变。 (out byte* 可以翻译成byte**
  • 如果您仔细阅读该资源,它只会说编组行为与这些关键字相似,但它确实暗示它们是相同的,也不暗示它们是甚至在技术上相关。至于你关于byte[]byte* 的论点,这也是不正确的。由于字节数组是一个引用类型,而不是一个指针。即使在 CPU 级别它们可能被视为相同,但在语言级别它们是根本不同的。 (并且引用类型对于代码安全非常有用。)
猜你喜欢
  • 2021-12-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-27
  • 1970-01-01
  • 1970-01-01
  • 2020-01-11
  • 1970-01-01
相关资源
最近更新 更多