我正在尝试构建一个实用程序,在程序更新后自动查找指针。我遇到的问题是,实际搜索特定的4/8字节值似乎很快,但不够快,无法实现扫描指针路径。
下面是我目前从VirtualQueryEx
搜索内存区域的代码
public List<MemoryScanResult> Search(MemoryRegion region, byte[] targetValue)
{
var results = new List<MemoryScanResult>();
var buffer = region.Cache;
switch (targetValue.Length)
{
case 8:
CheckBufferUnsignedLong(BitConverter.ToUInt64(targetValue));
break;
case 4:
CheckBufferUnsignedInt(BitConverter.ToUInt32(targetValue));
break;
default:
throw new NotSupportedException();
}
void CheckBufferUnsignedInt(uint value)
{
unsafe
{
fixed (byte* pBuffer = buffer)
{
var itemSize = buffer!.Length / 4;
var values = (uint*)pBuffer;
for (var i = 0; i < itemSize; i++)
{
if (value != values[i])
continue;
var foundResult = new MemoryScanResult
{
Address = region.BaseAddress + (i * itemSize),
FoundAtBase = region.AllocationBase == _applicationMemory.Address
};
results!.Add(foundResult);
}
}
}
}
void CheckBufferUnsignedLong(ulong value)
{
unsafe
{
fixed (byte* pBuffer = buffer)
{
var itemSize = buffer!.Length / 8;
var values = (ulong*)pBuffer;
for (var i = 0; i < itemSize; i++)
{
if (value != values[i])
continue;
var foundResult = new MemoryScanResult
{
Address = region.BaseAddress + (i * itemSize),
FoundAtBase = region.AllocationBase == _applicationMemory.Address
};
results!.Add(foundResult);
}
}
}
}
return results;
}
该应用程序具有1.9GB的可读/可写存储器,分为3个部分,500个非空区域。并行运行上述函数。ForEach和4个工作线程在500 ms内读取所有1.9GB,并返回正确的结果数。这一切看起来都很好,直到我开始查看指针扫描并意识到这是4+如果我在目标值地址之前0x 1000开始搜索,可能需要30分钟才能找到第一个结构体指针。
我所知道的唯一一款具有这种功能/速度和源代码的软件是Cheat Engine。我试着查看源代码,但我不知道它是否做了一些特殊的事情,或者我只是达到了在. NET中可以实现的上限。
有什么技巧我错过了吗?这似乎是不真实的,我认为作弊引擎能够扫描,并找到1830万路径之前,我甚至找到了我的值非常第一指针。
编辑:还有一件事是Cheat Engine也不需要缓存整个堆。即使在指针扫描期间,它也不会接近我达到当前时间所需的内存使用率,所以我认为它是按需阅读区域。如果我这样做,我的搜索时间将变为900 ms。
编辑2:在发行版中运行(就像我应该做的那样),使用缓存缓冲区将我的时间降低到160- 200 ms。这使得我目前的基准测试在最坏的情况下需要1.7分钟,4个工作者找到1个指针。如果仅仅向集合中添加17个结果就增加了50 ms,我开始考虑我需要从.NET中转移出来,并为此做一个.NET Package 器。
1条答案
按热度按时间kwvwclae1#
自我回答是因为经过长时间的战斗试图建立扫描仪,我终于有了顿悟。
我在构建扫描仪时找到的每一条信息都是最基本的例子,通常类似于以下内容:“从你的目标地址X开始,一直走到最大N偏移量,然后递归检查指针,直到你到达一个静态/基址”而且在大多数情况下,这是正确的,但是一旦你开始实际构建扫描器,这是一个巨大的陷阱。使用任何简单的示例代码,如果实现得很完美,将产生非常小的结果,或者是一个非常长的过程,你永远不会让它完成,因为你知道它应该更快,而且/要么就是内存不足。
这是一个逐步模拟的例子,但主要的速度因素是:
{.Address, .Value}
。ReadProcessMemory
太慢了,除此之外无法用于其他任何操作。步骤1 -内存区域和模块链接
步骤2 -指针缓存
这是两个主要速度区域中的第一个,目标是在此之后不再调用
ReadProcessMemory
,它很慢,可能有数千个内存区域,并且缓存它不会消耗太多内存。Pointer
结构体,其中只包含它所指向的Address
和Value
。4
或8
的对齐方式遍历。8
速度较快,但最终可能无法给予可用的结果。Value
是否确实指向了你的内存区域,如果是的话,把你的新指针保存到缓存中。此时,您应该有一个庞大的指针列表。从现在开始,这就是扫描将扫描的对象。
步骤3 -创建指针级别列表
这是拥有一个可行的指针扫描器的实际步骤和技巧。
PointerList
类,用于保存指针的Level
和Pointer
的HashSet(或任何其他唯一/排序容器)。PointerList
数组。此时我还链接了我的PointerList
,使Next
和Previous
链接到其他列表。例如,级别2链接到上一级别1,链接到下一级别3第4步-最终查找指针
对地址/值/指针的所有检查都基于步骤2中的指针的巨大集合。
0级--从你的目标地址开始,找到所有以它为值的指针,并将它们添加到你的0级列表中。
0级--用你的
alignment
减去你的目标地址,并检查该地址是否有指向它的指针。如果它确实添加到你的0级指针列表。不是你的结果列表!冲洗并重复,直到你达到一个最大偏移量或节点深度。例如偏移量0x 1000或最多只取3或5个指针。由你决定并需要测试。注意:很有可能在你的0级列表步骤中已经有了结果。检查你正在添加的指针的地址,并确定它是一个结果指针还是仅仅是一个下一级的指针。级别1----你在你的
previous
列表中有你的指针的初始种子。做和级别0完全一样的事情,检查你的地址,看看你认为是什么结果或者一个工作在下一个级别。例如,只有当指针的地址在主模块的内存区域时才接受指针结果。对所有级别重复此操作。记住诀窍是在下一关之前完成一关。例如,您的列表可能会在每个级别上看起来越来越大。20分,第一关:350分,第二级:一千个指针。
第5步-构建结果
此时,如果使用最大深度5,则应该有5个
PoinerList
相互指向,并且在某些列表中应该有一些结果。下面是扫描仪本身。不是最干净的,需要一些调整,但这只是作为一个例子。缺少的类应该很容易能够推断出以上的指示和上下文中使用它们。