【问题标题】:How do you achieve fast pointer scans?你如何实现快速指针扫描?
【发布时间】:2023-02-20 13:22:21
【问题描述】:

我正在尝试构建一个实用程序来在程序更新后自动查找指针。我遇到的问题是,对特定 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 个非空区域。在具有 4 个工作人员的 Parallel.ForEach 中运行上述函数会在 ~500 毫秒内读取所有 1.9GB 并返回正确数量的结果。这一切看起来都很好,直到我开始查看指针扫描并意识到如果我在目标值地址之前的 0x1000 处开始搜索,可能需要 4 分钟以上才能找到第一个结构指针。

我所知道的唯一具有此功能/速度和源代码的软件是 Cheat Engine。我试图查看源代码,但我无法弄清楚它是否在做一些特殊的事情,或者我是否只是达到了 .NET 中可实现的上限。

我缺少什么技巧吗?在我什至找到我的值的第一个指针之前,Cheat Engine 能够扫描并找到 1830 万条路径,这对我来说似乎是不真实的。

编辑:另一件事是 Cheat Engine 也不必缓存整个堆。即使在指针扫描期间,它也永远不会接近我达到当前时间所需的内存使用量,因此我认为它正在按需读取区域。如果我这样做,我的搜索将变为 900 毫秒。

编辑 2:在发布中运行(就像我应该的那样)使用缓存缓冲区将我的时间降低到 160-200 毫秒。这让我目前的基准在最坏的情况下需要 1.7 分钟,有 4 名工人找到 1 个指针。如果仅将 17 个结果添加到集合中会增加 50 毫秒,我开始认为我需要摆脱 .NET 来完成繁重的工作,并为此制作一个 .NET 包装器。

【问题讨论】:

  • 如果省略结果存储,您的速度会发生什么变化?您正在运行发布版本吗?
  • @500-InternalServerError 说得好!如果该区域的内存被缓存,这实际上会使我下降到 165-200 毫秒。这确实有很大帮助,但仍然给我最多 1.7 分钟的时间来获得我的第一个指针,而 CE 在这一点上已经完成。大声笑 但非常感谢您指出这一点。我为忘记这一点而感到愚蠢。
  • @500-InternalServerError 不存储结果让我在 120 毫秒到 145 毫秒之间

标签: c# windows memory reverse-engineering


【解决方案1】:

自我回答是因为经过长时间的努力构建扫描仪,我终于有了顿悟。

我在尝试构建工作扫描仪时发现的每条信息都是最基本的基本示例。通常类似于这样的事情: “从你的目标地址 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
  • 遍历所有内存区域并将内存读入缓冲区。使用 48 的对齐循环遍历它。 8 很快,但最终可能无法提供可用的结果。
  • 在该循环中,检查 Value 实际上指向您的内存区域之一。如果是,请将新指针存储在缓存中。

在这一点上,你应该有一个庞大的指针列表。从现在开始,这就是扫描将扫描的内容。


第 3 步 - 制作指针级别列表

这是拥有可行的指针扫描仪的实际步骤和技巧。

  • 创建一个 PointerList 类,其中包含指针的 LevelPointer 的 HashSet(或任何其他唯一/排序容器)。
  • 根据最大指针深度和相应的级别创建一个PointerList的数组。在这一点上,我还将我的PointerLists 链接到 NextPrevious 链接到其他列表。即第 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;
    }
}

【讨论】:

    猜你喜欢
    • 2015-02-01
    • 2012-08-28
    • 1970-01-01
    • 2020-03-13
    • 1970-01-01
    • 1970-01-01
    • 2015-07-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多