自我回答是因为经过长时间的努力构建扫描仪,我终于有了顿悟。
我在尝试构建工作扫描仪时发现的每条信息都是最基本的基本示例。通常类似于这样的事情:
“从你的目标地址 X 开始,一直走到 Max N 偏移量,然后递归检查指针,直到你到达一个静态/基地址”在大多数情况下,这是正确的,但是一旦你开始实际构建扫描器,它就是一个巨大的陷阱。如果实施有缺陷,使用任何简单的示例代码都会产生非常小的结果,或者您永远不会完成的非常长的过程,因为您知道它应该更快和/或内存不足。
这是一步一步的伪示例。但主要的速度因素是:
- 读取所有内存以创建一个巨大的指针列表
{.Address, .Value}。 ReadProcessMemory 太慢了,无法用于除此以外的任何其他用途。
- 所有集合都应按地址排序。
- 尽可能使用 BinarySearch。甚至可以为指针或区域制作多个列表。一个按地址排序,另一个按值排序。我们谈论的可能是数以百万计的项目和大量循环。您需要快速轻松地查明您的物品。到目前为止,这个过程对我来说和 Cheat Engine 一样快,有时甚至更快,但 Cheat Engine 有时可能会给我 3,000,000 个指针,而我有 180,000 个,但到目前为止还不需要丢失的 2,820,000 个路径。为了比较。我使用 8 个线程运行,目标应用程序消耗 2.5GB 内存,并且该过程在大约 10-15 秒内完成。具有 24 个线程的 Cheat Engine 需要 12 秒。
第 1 步 - 内存区域和模块链接
- 获取与进程关联的所有内存区域的列表。最好按 BaseAddress 排序
- 获取进程加载的所有模块的列表。
- 遍历区域并使用它们的基地址将模块与区域链接起来。
- 根据 AllocationBase 将您的区域链接在一起。您现在将知道哪些区域属于哪些 exe/dll。
第 2 步 - 指针缓存
这是两个主要速度区域中的第一个。目标是在这一点之后永远不会调用ReadProcessMemory。它很慢,可能有数千个内存区域,并且不会消耗那么多内存来缓存它。
- 创建一个简单的
Pointer结构,只包含Address它和它指向的Value。
- 遍历所有内存区域并将内存读入缓冲区。使用
4 或 8 的对齐循环遍历它。 8 很快,但最终可能无法提供可用的结果。
- 在该循环中,检查
Value 实际上指向您的内存区域之一。如果是,请将新指针存储在缓存中。
在这一点上,你应该有一个庞大的指针列表。从现在开始,这就是扫描将扫描的内容。
第 3 步 - 制作指针级别列表
这是拥有可行的指针扫描仪的实际步骤和技巧。
- 创建一个
PointerList 类,其中包含指针的 Level 和 Pointer 的 HashSet(或任何其他唯一/排序容器)。
- 根据最大指针深度和相应的级别创建一个
PointerList的数组。在这一点上,我还将我的PointerLists 链接到 Next 和 Previous 链接到其他列表。即第 2 级链接到上一级,并链接到下一级 3
- 现在列表已经准备好了,我们可以回到一些旧的基本知识并开始走我们的指针。但!有一个主要区别,我们不是在每次迭代中都遍历一个完整的指针。我们一次一个级别地遍历所有潜在指针。这就是为什么您想使用像 HashSet 这样的唯一/排序的容器(假设您将哈希设为地址的哈希)。
第 4 步 - 最后寻找指针
所有地址/值/指针的检查都基于您从第 2 步中收集到的大量指针。
- 0 级——从你的目标地址开始,找到所有以它为值的指针,并将它们添加到你的 0 级列表中。
- Level 0 -- 用你的
alignment 减去你的目标地址,并检查该地址是否有指向它的指针。如果它确实添加到您的 0 级指针列表中。不是你的结果列表!冲洗并重复,直到达到最大偏移量或节点深度。例如偏移量 0x1000 或最多只占用 3 或 5 个指针。由您决定并需要测试。
注意:很可能在您的 0 级列表步骤中已经有了结果。检查您要添加的指针的地址,并确定它是结果指针还是只是下一级工作的指针。
- 1 级-? -- 你的初始指针种子在你的
previous列表中。做与第 0 级完全相同的事情,并检查您的地址以获得您认为的结果或一个工作到下一个级别。例如,如果指针的地址位于主模块的内存区域中,则只接受指针结果。对所有级别重复此操作。请记住,诀窍是在进入下一个级别之前完成一个级别。
- 例如,您的列表可能应该在每个级别看起来越来越大。 0级:20个指针。级别 1:350 个指针。 2 级:1,000 个指针。
第 5 步 - 构建结果
此时,如果最大深度为 5,则应该有 5 个 PoinerLists 相互指向,并且某些列表中应该有一些结果。
- 现在遍历每个列表及其结果。按照你构建它们的顺序保持它,它应该首先给你最短的路径。
- 对于每个指针结果,您基本上将执行步骤 4,但相反。如果您遇到整个扫描一直在搜索的目标地址,则不要减去偏移量,而是将偏移量添加到最大允许偏移量,即 0x1000 或更少。
- 您开始的每个结果指针最终都应该指向 1 条或多条路径。偏移量/级别数越高,您应该为单个结果找到的指针路径越多。
- 这是您最终可以开始使用递归的步骤,因为您递归的次数永远不应超过您的实际关卡数量,此时您的关卡指针已被高度过滤并针对您不是在查看垃圾值。
- 使用 ReadProcessMemory 运行并验证它们是否有效。根据需要保存/使用。
下面是扫描仪本身。不是最干净的,需要一些调整,但这只是一个例子。根据上述说明和使用它们的上下文,应该可以很容易地推断出缺失的类。
public class PointerScanner
{
readonly PointerScanController _controller;
public PointerScanController Controller => _controller;
public PointerScanner(ApplicationMemory applicationMemory, PointerScanSettings settings)
{
_controller = new PointerScanController(settings, applicationMemory);
}
public async Task ScanAsync(nint targetAddress)
{
var pointerLists = new List<PointerList>();
for (var i = 0; i < _controller.Settings.MaxDepth + 1; i++)
{
var newList = new PointerList { Level = i };
pointerLists.Add(newList);
if (i > 0)
{
newList.Previous = pointerLists[i - 1];
pointerLists[i - 1].Next = newList;
}
}
var settings = _controller.Settings;
for (var i = 0; i < pointerLists.Count; i++)
{
var currentList = pointerLists[i];
var previousList = i > 0 ? pointerLists[i - 1] : null;
if (previousList == null)
{
// 1) Start walking up the struct
for (var address = targetAddress; address >= targetAddress - settings.MaxOffset; address -= settings.Alignment)
{
// 2) Find all pointers that point to this address
var parents = _controller.CachedValues.BinarySearchFindAll(new Pointer { Value = address }, new PointerValueComparer());
if (parents == null)
continue;
// 3) Add all pointers to to the list;
foreach (var parent in parents)
{
var block = _controller.GetBlockIndexFromAddress(parent);
if (block >= 0 && _controller.MemoryRegions[block].Module != null)
{
currentList.Results.Add(parent, (int)(targetAddress - address));
}
else
{
currentList.Pointers.Add(parent);
}
}
}
}
else
{
// 1) Run through all potential pointers in the previous level.
await Parallel
.ForEachAsync(previousList.Pointers,
new ParallelOptions { MaxDegreeOfParallelism = 8 },
(pointer, token) =>
{
var nodeDepth = 0;
// 2) Start walking up the struct
for (var address = pointer.Address;
address >= pointer.Address - settings.MaxOffset;
address -= settings.Alignment)
{
// 3) Find all pointers that point to this address
var parents = _controller.CachedValues.BinarySearchFindAll(new Pointer { Value = address },
new PointerValueComparer());
if (parents == null)
continue;
nodeDepth++;
// 4) Add all pointers to to the list;
foreach (var parent in parents)
{
var block = _controller.GetBlockIndexFromAddress(parent, true);
var skipAddToPointer = false;
if (block >= 0 && block < _controller.MemoryRegions.Count)
{
var module = _controller.MemoryRegions[block].Module;
if (module != null && module.BaseAddress < parent.Address)
{
//This lives inside a module, however, there could be better modules pointing to it.
//TODO: Accept a list of modules that should only count towards the result
lock (currentList.Results)
{
if (!currentList.Results.ContainsKey(parent))
{
skipAddToPointer = true;
currentList.Results.Add(parent, (int)(pointer.Address - address));
}
}
}
}
if (skipAddToPointer || currentList.Next == null)
continue;
lock (currentList.Pointers)
{
if (!currentList.PointerAlreadyExists(parent))
{
currentList.Pointers.Add(parent);
}
}
}
if (nodeDepth > settings.MaxOffsetNodes)
break;
}
return default;
});
}
Console.WriteLine($"Pointers Level {i} -- {pointerLists[i].Pointers.Count:#,###} pointers.");
}
foreach (var list in pointerLists)
list.FinalizeToList();
foreach (var l in pointerLists)
{
foreach (var result in l.Results)
{
var regionIx = _controller.GetBlockIndexFromAddress(result.Key.Address, false);
var module = _controller.MemoryRegions[regionIx].Module;
FindResultPointer(targetAddress, 0, result.Key, result.Key, l.Previous, new List<int> { (int)(result.Key.Address - module!.BaseAddress) });
}
}
var r = _controller.Results;
var maxOffset = r.Max(x => x.Offsets.Length);
var sorted = r.OrderBy(x => true);
for (var i = maxOffset-1; i >= 0; i--)
{
var offsetIndex = i;
//This is really hacky, but I want the main 1st set of offsets to be sorted and make sure
//the main big offset is grouped together as much as possible.
if (offsetIndex == 1)
{
offsetIndex = 0;
}
else if (offsetIndex == 0)
{
offsetIndex = 1;
}
sorted = sorted.ThenBy(x => x.Offsets.Length > offsetIndex ? x.Offsets[offsetIndex] : -1);
}
_controller.Results = sorted.ToList();
}
bool FindResultPointer(nint targetAddress, int currentLevel, Pointer mainPointer, Pointer pointer, PointerList? nextLevel, List<int> currentOffsets)
{
if (nextLevel == null)
{
//The first pointer list is special because any results in it are direct and there's no previous list to build from.
//Need to manually work it and add its results.
if (currentLevel == 0 && (targetAddress - pointer.Value) <= _controller.Settings.MaxOffset)
{
currentOffsets.Add((int)(targetAddress - pointer.Value));
var regionIx = _controller.GetBlockIndexFromAddress(mainPointer.Address, false);
_controller.Results.Add(new PointerScanResult
{
Origin = mainPointer,
Module = _controller.MemoryRegions[regionIx].Module!,
Offsets = currentOffsets.Select(x => x).ToArray()
});
return true;
}
return false;
}
//1) Find the child pointer
var baseChildIndex = nextLevel.PointersList.BinarySearch(new Pointer { Address = pointer.Value });
if (baseChildIndex < 0)
baseChildIndex = (~baseChildIndex);
bool hadResult = false;
//2) Loop through all potential children/offsets
var depth = 0;
for (var i = baseChildIndex; i < nextLevel.PointersList.Count; i++)
{
var child = nextLevel.PointersList[i];
if (child.Address > pointer.Value + _controller.Settings.MaxOffset)
break;
currentOffsets.Add((int)(child.Address - pointer.Value));
if (!FindResultPointer(targetAddress, currentLevel + 1, mainPointer, child, nextLevel.Previous, currentOffsets))
{
if (targetAddress - child.Value <= _controller.Settings.MaxOffset)
{
hadResult = true;
currentOffsets.Add((int)(targetAddress - child.Value));
var regionIx = _controller.GetBlockIndexFromAddress(mainPointer.Address, true);
_controller.Results.Add(new PointerScanResult
{
Origin = mainPointer,
Module = _controller.MemoryRegions[regionIx].Module!,
Offsets = currentOffsets.Select(x => x).ToArray()
});
currentOffsets.RemoveAt(currentOffsets.Count - 1);
}
}
else
{
hadResult = true;
}
currentOffsets.RemoveAt(currentOffsets.Count - 1);
}
return hadResult;
}
}