调用ReadProcessMemory时出现奇怪的行为导致引用被设置为null

3
我在使用C#通过这个P/Invoke签名调用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.

这段代码在IT技术中表现良好。它扫描整个内存以查找一系列字节,没有任何问题。


在我的程序流程中稍微有些不同,我有这段代码:
注意:它使用与前面代码相同的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;

代码非常相似,但由于某种原因,对缓冲区的引用丢失了,并且缓冲区被设置为null。如果这不是一个外部调用,我可以百分之百确定这是一个bug,因为缓冲区没有作为refout参数传递。然而,我知道.NET在涉及外部调用时会进行一些神秘的操作(例如marshaling)。
使情况更加奇怪的是,当我将该代码替换为:
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 Update 1)
  • 安装了.NET Framework 4.5.50709
  • 以管理员身份运行
  • 在Visual Studio主机可执行文件和常规构建可执行文件中均出现
  • Windows 7 64位
  • 我正在从中读取内存的进程是32位的
  • 构建配置:平台:任何CPU

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

编辑2: 我添加了简单的步骤来解决问题,答案可以在此处找到:https://dev59.com/aW3Xa4cB1Zd3GeqPeGbQ#14410443


一个旁注:Marshal.GetLastWin32Error() 返回1008,这是它在启动时返回的相同代码。 - Aidiakapi
你的MEMORY_BASIC_INFORMATION声明是错误的,RegionSize应该是UIntPtr而不是ulong。不确定这如何会破坏堆栈。 - Hans Passant
@HansPassant 谢谢,这指引我找到解决问题的方向 :)! - Aidiakapi
4个回答

2

可能是因为您的应用程序是64位的,所以lpNumberOfBytesRead应该是“long”类型的,这样调用ReadProcessMemory函数时返回时会覆盖(部分)缓冲区指针。


请查看http://msdn.microsoft.com/en-us/library/windows/desktop/ms680553(v=vs.85).aspx和https://dev59.com/RnE85IYBdhLWcg3w_Itr。 - 500 - Internal Server Error
当你编译64位时,应该能够直接使用long代替int。 - 500 - Internal Server Error
嗯,C#并不知道LongPtr,只有IntPtr。因此,我不能使用长指针而不诉诸于不安全的代码。在这种情况下,这可能是最好的方法。(或将指针视为值类型,仅使用intlong而不是它们的托管指针包装器。) - Aidiakapi
我刚刚重新检查了VirtualQueryEx的定义,它们似乎完全正确。(PS. 我没有投反对票。) - Aidiakapi
您的ulong RegionSize对于64位是没问题的,但在32位上可能会失败,因为它应该是32位。 (在MEMORY_BASIC_INFORMATION中) - 500 - Internal Server Error
显示剩余6条评论

0

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


为什么当它被正确填充数据并返回非零值(或者换句话说,没有出现错误)时,它会被kernel32置空?编辑:尽管所有数据都在缓冲区中可用(之后的一些代码检查了数据,并且验证正确),但它确实正确地进行了编组。因此,读取是正确的。为什么第一个提供的代码可以工作,而第二个不能? - Aidiakapi
你尝试过将进程附加到调试器以查看发生了什么吗? - Tommaso Belluzzo
对于Visual Studio的调试器,是的;对于OllyDbg调试器,不行。你认为这会有帮助吗? - Aidiakapi
OllyDbg无法完全附加(它会给出一些不支持的错误),最有可能的原因是它在虚拟机下运行。 - Aidiakapi
你可以使用VS JIT调试器。只需从程序集本身运行应用程序,让它崩溃...然后当异常被抛出时,一个窗口应该弹出询问您是否要调试应用程序...这样做即可。 - Tommaso Belluzzo

0

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

这是我采取的步骤:

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

感谢您的帮助。


就第一点而言,如果我处于x64环境并且使用32位目标平台运行您的代码,则会出现与您完全相同的错误。 - Tommaso Belluzzo
那是因为我没有发布修复后的P/Invoke签名。这个主题中的签名有问题,它们导致了这个问题(悄悄地)。 - Aidiakapi

-2

不要将 byte[] buffer 标记为 [Out] 参数。它更类似于 ref 而不是 out,因为它已经是一个字节数组了。由于整数(这里是 int)是值类型,需要使用 out 参数来传递 numberOfBytesRead。这是唯一需要用 out 标记的参数。

当某个参数被标记为 out 参数时,Marshal 类会期望被调用方(这里是 ReadProcessMemory)返回一个值。而字节数组只是指向内存中包含字节的位置的指针(地址)。你不希望调用方对这个指针进行写入操作。


OutAttributerefout参数调用完全无关。此外,你提到的只有int(或我认为其他值类型)应该标记为refout的观点是完全错误的,有很多好的设计需要将引用类型标记为这样。OutAttribute与编组相关,并且是必需的。 - Aidiakapi
[OutAttribute] 不是完全无关的。据我所知,out[Out] ref 是同义词。你可以说你想要的,但我已经编写了大量使用 P/Invoke 的 ReadProcessMemory 等代码,并且从未将它们标记为缓冲区上的 ref[Out]。我见过很多奇怪的事情发生在这些函数中(通常是 Win32 错误或零读取),但没有看到缓冲区被覆盖为 NULL。我试图通过指出与我的工作代码不同的事物来帮助你。 - Erik
这是因为虚拟机使用不同的互操作技术,超越了默认的C#行为。正如我在问题中所说的,如果这不是一个外部函数,我会将其归档为一个错误。 [Out] 然而并不等同于 [Out] ref。Ref和out参数是CIL的一个特性,并不涉及提示虚拟机的属性,就像 OutAttribute 所做的那样。无论它们的结果有多相似,它们在根本上都是不同的,不应该教授它们是相同的。 - Aidiakapi
这里有一个很好的资源:MSDN: CLR Inside Out: Marshaling 从那里的信息来看,out 对应 [OutAttribute],而 ref 对应 [InAttribute], [OutAttribute]。有一些对象类型有“默认值”,例如 StringBuilder 默认为 [In],[Out],无需指定。重点是,我怀疑您不需要传递 [Out]byte[] 类型,因为它实际上是一个 byte* 在幕后,您不希望被调用者更改。(out byte* 可以转换为 byte**) - Erik
我从未说过当编组工具既没有 OutAttribute 也没有 InAttribute 时不会查找 outref。属性的主要原因是为了在每种语言中都有支持。例如,VB.Net 没有 out。请参阅此文章,其中解释了此 P/Invoke 调用的正确签名。 - Aidiakapi
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接