XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

HaloPixelToolBox

【WinUI3】基于USB HID通讯的花再音响工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/HaloPixelToolBox

实现网易云歌词地址自动特征扫描与缓存

本次提交引入 CloudMusicSignatureScanner 工具,实现对 cloudmusic.dll 的 PE 特征扫描与歌词指针自动定位,支持模块哈希校验与候选地址缓存。CloudMusicLyricsReader 支持多种解析来源与自动缓存,AddressResolverProvider 增加解析器缓存与保存逻辑。设置项调整为“首选歌词源”,仅保留网易云和 Spotify,相关界面与逻辑同步更新。主页与启动流程优化,增强歌词地址解析健壮性,完善日志与异常处理。

47262c8
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

10 个文件 +405 -51
Modified Client/HaloPixelToolBox.Client.Test/Program.cs +42 -0
@@ -1,4 +1,5 @@
1 1 using System.Runtime.Versioning;
2 using System.Buffers.Binary;
2 3 using HaloPixelToolBox.Core.Utilities;
3 4
4 5 namespace HaloPixelToolBox.Client.Test;
@@ -22,4 +23,45 @@ internal class Program
22 23 XFECode.Assert(lines[1].Text == "第二句" && lines[1].Timestamp.TotalMilliseconds == 2500, "两位百分秒时间戳解析错误");
23 24 XFECode.Assert(lines[2].Text == "第三句" && lines[2].Timestamp.TotalMilliseconds == 3500, "一位十分秒时间戳解析错误");
24 25 }
26
27 [SMTest]
28 public static void ScanCloudMusicSignature()
29 {
30 var image = new byte[0x400];
31 image[0] = (byte)'M';
32 image[1] = (byte)'Z';
33 BinaryPrimitives.WriteInt32LittleEndian(image.AsSpan(0x3C, 4), 0x80);
34 image[0x80] = (byte)'P';
35 image[0x81] = (byte)'E';
36 BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x86, 2), 1);
37 BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x94, 2), 0xF0);
38 BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x80 + 24 + 56, 4), 0x5000);
39
40 const int sectionHeader = 0x80 + 24 + 0xF0;
41 BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(sectionHeader + 12, 4), 0x1000);
42 BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(sectionHeader + 16, 4), 0x200);
43 BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(sectionHeader + 20, 4), 0x200);
44 BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(sectionHeader + 36, 4), 0x20000000);
45
46 const int signatureOffset = 0x220;
47 const int signatureRva = 0x1020;
48 const int targetRva = 0x3450;
49 byte?[] signature =
50 [
51 0x48, 0x8D, 0x0D, null, null, null, null,
52 0xE8, null, null, null, null,
53 0x48, 0x8B, 0xC8,
54 0xE8, null, null, null, null,
55 0x48, 0xC7, 0x05, null, null, null, null,
56 0x00, 0x00, 0x00, 0x00
57 ];
58 for (var index = 0; index < signature.Length; index++)
59 image[signatureOffset + index] = signature[index] ?? 0x5A;
60 BinaryPrimitives.WriteInt32LittleEndian(image.AsSpan(signatureOffset + 3, 4), targetRva - (signatureRva + 7));
61 BinaryPrimitives.WriteInt32LittleEndian(image.AsSpan(signatureOffset + 23, 4), targetRva - (signatureRva + signature.Length));
62
63 var candidates = CloudMusicSignatureScanner.FindBaseAddresses(image);
64 XFECode.Assert(candidates.Count == 1 && candidates[0] == targetRva, "应通过两条 RIP 相对指令解析出唯一目标 RVA");
65 }
66
25 67 }
Modified Client/HaloPixelToolBox.Client/Profiles/CacheProfiles/CacheProfile.cs +5 -0
@@ -12,4 +12,9 @@ public partial class CacheProfile : XFEProfile
12 12 [ProfilePropertyAddGet("Current.versionAddress.CurrentProfile = Current")]
13 13 [ProfilePropertyAddGet("return Current.versionAddress")]
14 14 private ProfileDictionary<string, AddressResolverModel> versionAddress = [];
15
16 [ProfileProperty]
17 [ProfilePropertyAddGet("Current.moduleAddress.CurrentProfile = Current")]
18 [ProfilePropertyAddGet("return Current.moduleAddress")]
19 private ProfileDictionary<string, AddressResolverModel> moduleAddress = [];
15 20 }
Modified Client/HaloPixelToolBox.Client/Profiles/CrossVersionProfiles/SystemProfile.cs +2 -2
@@ -44,10 +44,10 @@ public partial class SystemProfile : XFEProfile
44 44 private string serverAddress = HaloPixelToolBox.Core.Utilities.DataManager.DefaultRequestAddress;
45 45
46 46 /// <summary>
47 /// 默认启动页面
47 /// 主页首选歌词源
48 48 /// </summary>
49 49 [ProfileProperty]
50 private string defaultPage = "MainPage";
50 private string defaultPage = "CloudMusicLyricsToolPage";
51 51
52 52 static partial void SetThemeProperty(ref ElementTheme value) => AppThemeHelper.ChangeTheme(value);
53 53 }
Modified Client/HaloPixelToolBox.Client/Utilities/AddressResolverProvider.cs +16 -0
@@ -18,6 +18,22 @@ public static class AddressResolverProvider
18 18 var model = Clone(pair.Value, pair.Key);
19 19 CloudMusicLyricsReader.SetAddressResolver(model);
20 20 }
21
22 foreach (var pair in CacheProfile.ModuleAddress)
23 {
24 var model = Clone(pair.Value, pair.Value.Version);
25 CloudMusicLyricsReader.SetAutomaticAddressResolver(pair.Key, model);
26 }
27 }
28
29 public static void SaveAutomaticResolver(string moduleHash, AddressResolverModel resolver)
30 {
31 if (string.IsNullOrWhiteSpace(moduleHash) || !IsValid(resolver))
32 return;
33
34 CacheProfile.ModuleAddress[moduleHash] = Clone(resolver, resolver.Version);
35 CloudMusicLyricsReader.SetAutomaticAddressResolver(moduleHash, resolver);
36 Console.WriteLine($"[INFO]已缓存网易云模块 {moduleHash[..Math.Min(12, moduleHash.Length)]} 的自动解析结果");
21 37 }
22 38
23 39 public static async Task<AddressResolverModel?> GetAsync(string version)
Modified Client/HaloPixelToolBox.Client/ViewModels/CloudMusicLyricsToolPageViewModel.cs +2 -1
@@ -187,6 +187,7 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
187 187 UseInputedAddress = UseInputedAddress,
188 188 Address = ParseHexAddress(InputedAddress)
189 189 };
190 Reader.AutomaticResolverDiscovered += AddressResolverProvider.SaveAutomaticResolver;
190 191 Console.WriteLine("准备启动网易云后台线程");
191 192 Task.Run(async () =>
192 193 {
@@ -230,7 +231,7 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
230 231 CloudMusicVersion = Reader.VersionInfo is not null ? $"{Reader.VersionInfo.FileVersion}" : "未检测到云音乐";
231 232 SupportedVersion = onlineReady
232 233 ? (memoryReady ? "SMTC 在线歌词(内存兜底可用)" : "SMTC 在线歌词(无需解析地址)")
233 : (memoryReady ? $"内存解析({Reader.Version.ToString(3)})" : SupportedVersion);
234 : (memoryReady ? $"{Reader.ResolverDescription}({Reader.Version.ToString(3)})" : SupportedVersion);
234 235 });
235 236 await Task.Delay(500);
236 237 }
Modified Client/HaloPixelToolBox.Client/ViewModels/SettingPageViewModel.cs +2 -12
@@ -18,12 +18,7 @@ namespace HaloPixelToolBox.Client.ViewModels;
18 18 public partial class SettingPageViewModel : ViewModelBase
19 19 {
20 20 [ObservableProperty] public partial int CloseButtonActionIndex { get; set; } = SystemProfile.MinimizeWhenClose ? 0 : 1;
21 [ObservableProperty] public partial int DefaultTabPageIndex { get; set; } = SystemProfile.DefaultPage switch
22 {
23 "CloudMusicLyricsToolPage" => 1,
24 "SpotifyLyricsToolPage" => 2,
25 _ => 0
26 };
21 [ObservableProperty] public partial int DefaultTabPageIndex { get; set; } = SystemProfile.DefaultPage == "SpotifyLyricsToolPage" ? 1 : 0;
27 22 [ObservableProperty] public partial bool IsAutoStartEnable { get; set; } = SystemProfile.AutoStart;
28 23 [ObservableProperty] public partial bool MinimizeWhenOpen { get; set; } = SystemProfile.MinimizeWhenOpen;
29 24 [ObservableProperty] public partial string AppCacheDirectory { get; set; } = AppPathHelper.AppCache;
@@ -40,12 +35,7 @@ public partial class SettingPageViewModel : ViewModelBase
40 35
41 36 partial void OnCloseButtonActionIndexChanged(int value) => SystemProfile.MinimizeWhenClose = value == 0;
42 37
43 partial void OnDefaultTabPageIndexChanged(int value) => SystemProfile.DefaultPage = value switch
44 {
45 1 => "CloudMusicLyricsToolPage",
46 2 => "SpotifyLyricsToolPage",
47 _ => "MainPage"
48 };
38 partial void OnDefaultTabPageIndexChanged(int value) => SystemProfile.DefaultPage = value == 1 ? "SpotifyLyricsToolPage" : "CloudMusicLyricsToolPage";
49 39
50 40 partial void OnIsAutoStartEnableChanged(bool value)
51 41 {
Modified Client/HaloPixelToolBox.Client/Views/AppShellPage.xaml.cs +1 -12
@@ -22,18 +22,7 @@ public sealed partial class AppShellPage : Page
22 22 ViewModel.DialogService.RegisterDialog(closeDialog);
23 23 ViewModel.PageService.Initialize(this);
24 24 ViewModel.LoadingService.Initialize(loadingGrid, globalLoadingGrid, globalLoadingTextBlock, DispatcherQueue, ViewModel.NavigationViewService.NavigationService);
25 switch (SystemProfile.DefaultPage)
26 {
27 case nameof(SpotifyLyricsToolPage):
28 ViewModel.NavigationViewService.NavigateTo<SpotifyLyricsToolPage>();
29 break;
30 case nameof(CloudMusicLyricsToolPage):
31 ViewModel.NavigationViewService.NavigateTo<CloudMusicLyricsToolPage>();
32 break;
33 default:
34 ViewModel.NavigationViewService.NavigateTo<MainPage>();
35 break;
36 }
25 ViewModel.NavigationViewService.NavigateTo<MainPage>();
37 26 }
38 27
39 28 private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
Modified Client/HaloPixelToolBox.Client/Views/SettingPage.xaml +1 -2
@@ -35,12 +35,11 @@
35 35 <ComboBoxItem Content="跟随系统" Tag="Default"/>
36 36 </ComboBox>
37 37 </controls:SettingsCard>
38 <controls:SettingsCard Header="默认启动页面" Description="选择软件启动后首先显示的页面">
38 <controls:SettingsCard Header="首选歌词源" Description="设置主页“打开首选歌词源”按钮进入的页面">
39 39 <controls:SettingsCard.HeaderIcon>
40 40 <FontIcon Glyph="&#xE8F4;"/>
41 41 </controls:SettingsCard.HeaderIcon>
42 42 <ComboBox SelectedIndex="{x:Bind ViewModel.DefaultTabPageIndex, Mode=TwoWay}">
43 <ComboBoxItem Content="主页"/>
44 43 <ComboBoxItem Content="网易云歌词"/>
45 44 <ComboBoxItem Content="Spotify歌词"/>
46 45 </ComboBox>
Modified Core/HaloPixelToolBox.Core/Utilities/CloudLyricsReader.cs +205 -22
@@ -1,5 +1,6 @@
1 1 using System.Diagnostics;
2 2 using System.Collections.Concurrent;
3 using System.Globalization;
3 4 using System.Text;
4 5 using HaloPixelToolBox.Core.Models.Bar;
5 6 using XFEExtension.NetCore.MemoryEditor;
@@ -7,14 +8,45 @@ using XFEExtension.NetCore.StringExtension;
7 8
8 9 namespace HaloPixelToolBox.Core.Utilities;
9 10
11 public enum CloudMusicResolverSource
12 {
13 None,
14 Manual,
15 AutomaticCache,
16 SignatureScan,
17 ServerResolver
18 }
19
10 20 public class CloudMusicLyricsReader
11 21 {
22 private readonly object _resolverLock = new();
23 private int _processId;
24 private string _modulePath = string.Empty;
25 private string _moduleHash = string.Empty;
26 private string _scannedModuleHash = string.Empty;
27 private IReadOnlyList<AddressResolverModel> _signatureCandidates = [];
28 private DateTime _lastCandidateValidationTime = DateTime.MinValue;
29
12 30 public nint Address { get; set; }
13 31 public bool UseInputedAddress { get; set; }
14 32 public FileVersionInfo? VersionInfo { get; set; }
15 33 public Version Version { get; set; } = new();
16 34 public MemoryEditor Editor { get; set; } = new();
35 public CloudMusicResolverSource ResolverSource { get; private set; }
36 public string ModuleHash => _moduleHash;
37 public string ResolverDescription => ResolverSource switch
38 {
39 CloudMusicResolverSource.Manual => "手动地址",
40 CloudMusicResolverSource.AutomaticCache => "自动特征缓存",
41 CloudMusicResolverSource.SignatureScan => "自动特征解析",
42 CloudMusicResolverSource.ServerResolver => "服务器解析",
43 _ => "未解析"
44 };
45
46 public event Action<string, AddressResolverModel>? AutomaticResolverDiscovered;
47
17 48 public static ConcurrentDictionary<string, AddressResolverModel> VersionResolverDictionary { get; } = new(StringComparer.OrdinalIgnoreCase);
49 public static ConcurrentDictionary<string, AddressResolverModel> ModuleResolverDictionary { get; } = new(StringComparer.OrdinalIgnoreCase);
18 50
19 51 public static void SetAddressResolver(AddressResolverModel resolver)
20 52 {
@@ -22,11 +54,26 @@ public class CloudMusicLyricsReader
22 54 VersionResolverDictionary[resolver.Version] = resolver;
23 55 }
24 56
57 public static void SetAutomaticAddressResolver(string moduleHash, AddressResolverModel resolver)
58 {
59 if (!string.IsNullOrWhiteSpace(moduleHash))
60 ModuleResolverDictionary[moduleHash] = resolver;
61 }
62
25 63 public bool Initialize()
26 64 {
27 65 if (GetCloudMusicLyricsProcess() is Process process)
28 66 {
29 67 Console.WriteLine($"[DEBUG]已找到进程:{process.ProcessName}({process.Id}|{process.Id:X}) - {process.MainWindowTitle}");
68 if (_processId != process.Id)
69 {
70 _processId = process.Id;
71 _modulePath = string.Empty;
72 _moduleHash = string.Empty;
73 _scannedModuleHash = string.Empty;
74 _signatureCandidates = [];
75 ResolverSource = CloudMusicResolverSource.None;
76 }
30 77 Editor.CurrentProcess = process;
31 78 VersionInfo = FileVersionInfo.GetVersionInfo(process.MainModule?.FileName ?? string.Empty);
32 79 Version = new Version(VersionInfo?.FileVersion ?? "0.0.0.0");
@@ -69,40 +116,176 @@ public class CloudMusicLyricsReader
69 116
70 117 public bool ReresolveAddress()
71 118 {
72 try
119 lock (_resolverLock)
73 120 {
74 if (Version.Major == 0)
75 return false;
76 if (UseInputedAddress)
77 return true;
78 nint address = 0;
79 if (VersionResolverDictionary.TryGetValue(Version.ToString(3), out var resolver))
80 address = Editor.ResolvePointerAddress(
81 resolver.ModuleName,
82 checked((nint)resolver.BaseAddress),
83 resolver.Offsets.Select(static offset => checked((nint)offset)).ToArray());
84 else
85 Console.WriteLine($"[WARN]未找到匹配的版本解析器,当前版本:{Version}");
86 if (address != Address)
87 Console.WriteLine($"[DEBUG]读取到新的地址:{address}({address:X})");
88 if (address != 0)
121 try
89 122 {
90 Address = address;
91 return true;
123 if (Version.Major == 0)
124 return false;
125 if (UseInputedAddress)
126 {
127 ResolverSource = CloudMusicResolverSource.Manual;
128 return Address != 0;
129 }
130
131 if (TryResolveAutomatically(out var automaticAddress, out var automaticSource))
132 return ApplyResolvedAddress(automaticAddress, automaticSource);
133
134 if (VersionResolverDictionary.TryGetValue(Version.ToString(3), out var resolver) &&
135 TryResolve(resolver, out var serverAddress))
136 return ApplyResolvedAddress(serverAddress, CloudMusicResolverSource.ServerResolver);
137
138 Console.WriteLine($"[WARN]自动特征与服务器均未找到匹配的解析器,当前版本:{Version}");
139 ResolverSource = CloudMusicResolverSource.None;
140 return false;
92 141 }
93 else
142 catch (Exception ex)
94 143 {
144 Console.WriteLine($"[ERROR]解析地址异常:{ex.Message}");
145 Console.WriteLine($"[TRACE]{ex.StackTrace}");
95 146 return false;
96 147 }
97 148 }
98 catch (Exception ex)
149 }
150
151 private bool TryResolveAutomatically(out nint address, out CloudMusicResolverSource source)
152 {
153 address = 0;
154 source = CloudMusicResolverSource.None;
155 if (!TryGetCloudMusicModule(out var modulePath))
156 return false;
157
158 if (!string.Equals(_modulePath, modulePath, StringComparison.OrdinalIgnoreCase))
99 159 {
100 Console.WriteLine($"[ERROR]解析地址异常:{ex.Message}");
101 Console.WriteLine($"[TRACE]{ex.StackTrace}");
160 _modulePath = modulePath;
161 _moduleHash = string.Empty;
162 _scannedModuleHash = string.Empty;
163 _signatureCandidates = [];
164 }
165
166 if (string.IsNullOrEmpty(_moduleHash))
167 _moduleHash = CloudMusicSignatureScanner.ComputeModuleHash(_modulePath);
168
169 if (ModuleResolverDictionary.TryGetValue(_moduleHash, out var cachedResolver) &&
170 TryResolve(cachedResolver, out address))
171 {
172 source = CloudMusicResolverSource.AutomaticCache;
173 return true;
174 }
175
176 if (!string.Equals(_scannedModuleHash, _moduleHash, StringComparison.OrdinalIgnoreCase))
177 {
178 _signatureCandidates = CloudMusicSignatureScanner.FindResolvers(_modulePath, Version.ToString(3));
179 _scannedModuleHash = _moduleHash;
180 Console.WriteLine($"[INFO]网易云特征扫描完成,发现 {_signatureCandidates.Count} 个经过双重引用校验的候选槽");
181 }
182
183 if (DateTime.UtcNow - _lastCandidateValidationTime < TimeSpan.FromSeconds(5))
102 184 return false;
185
186 _lastCandidateValidationTime = DateTime.UtcNow;
187 foreach (var candidate in _signatureCandidates)
188 {
189 if (!TryResolve(candidate, out address) || !IsPlausibleLyricsAddress(address))
190 continue;
191
192 var discoveredResolver = CloneResolver(candidate);
193 ModuleResolverDictionary[_moduleHash] = discoveredResolver;
194 AutomaticResolverDiscovered?.Invoke(_moduleHash, CloneResolver(discoveredResolver));
195 Console.WriteLine($"[INFO]已通过 AOB 特征自动解析网易云歌词地址:{candidate.ModuleName}+0x{candidate.BaseAddress:X}");
196 source = CloudMusicResolverSource.SignatureScan;
197 return true;
103 198 }
199
200 address = 0;
201 return false;
104 202 }
105 203
204 private bool TryGetCloudMusicModule(out string modulePath)
205 {
206 modulePath = string.Empty;
207 var process = Editor.CurrentProcess;
208 if (process is null || process.HasExited)
209 return false;
210
211 var module = process.Modules.Cast<ProcessModule>()
212 .FirstOrDefault(static item => string.Equals(item.ModuleName, "cloudmusic.dll", StringComparison.OrdinalIgnoreCase));
213 if (module is null || string.IsNullOrWhiteSpace(module.FileName) || !File.Exists(module.FileName))
214 return false;
215
216 modulePath = module.FileName;
217 return true;
218 }
219
220 private bool TryResolve(AddressResolverModel resolver, out nint address)
221 {
222 address = 0;
223 try
224 {
225 address = Editor.ResolvePointerAddress(
226 resolver.ModuleName,
227 checked((nint)resolver.BaseAddress),
228 resolver.Offsets.Select(static offset => checked((nint)offset)).ToArray());
229 return address != 0;
230 }
231 catch
232 {
233 return false;
234 }
235 }
236
237 private bool IsPlausibleLyricsAddress(nint address)
238 {
239 try
240 {
241 if (!Editor.ReadMemory(address, 200, out var buffer))
242 return false;
243
244 var validLength = GetValidLength(buffer);
245 if (validLength < 8)
246 return false;
247
248 var text = Encoding.Unicode.GetString(buffer, 0, validLength).Trim();
249 if (text.EnumerateRunes().Count() < 4)
250 return false;
251
252 var hasLetterOrDigit = false;
253 foreach (var rune in text.EnumerateRunes())
254 {
255 if (rune.Value is 0xCCCC or 0xFFFD)
256 return false;
257
258 var category = Rune.GetUnicodeCategory(rune);
259 if (category is UnicodeCategory.Control or UnicodeCategory.Format or UnicodeCategory.Surrogate or
260 UnicodeCategory.PrivateUse or UnicodeCategory.OtherNotAssigned)
261 return false;
262 hasLetterOrDigit |= Rune.IsLetterOrDigit(rune);
263 }
264 return hasLetterOrDigit;
265 }
266 catch
267 {
268 return false;
269 }
270 }
271
272 private bool ApplyResolvedAddress(nint address, CloudMusicResolverSource source)
273 {
274 if (address != Address || source != ResolverSource)
275 Console.WriteLine($"[DEBUG]读取到新的地址:{address}({address:X}),来源:{source}");
276 Address = address;
277 ResolverSource = source;
278 return true;
279 }
280
281 private static AddressResolverModel CloneResolver(AddressResolverModel resolver) => new()
282 {
283 Version = resolver.Version,
284 ModuleName = resolver.ModuleName,
285 BaseAddress = resolver.BaseAddress,
286 Offsets = [.. resolver.Offsets]
287 };
288
106 289 public static int GetValidLength(byte[] buffer)
107 290 {
108 291 var length = 0;
Added Core/HaloPixelToolBox.Core/Utilities/CloudMusicSignatureScanner.cs +129 -0
@@ -0,0 +1,129 @@
1 using System.Buffers.Binary;
2 using System.Security.Cryptography;
3 using HaloPixelToolBox.Core.Models.Bar;
4
5 namespace HaloPixelToolBox.Core.Utilities;
6
7 /// <summary>
8 /// 从网易云音乐模块的 PE 代码段中寻找歌词全局指针槽。
9 /// </summary>
10 public static class CloudMusicSignatureScanner
11 {
12 private const uint ImageScnMemExecute = 0x20000000;
13
14 // MSVC 为全局对象生成的析构路径:先取得全局槽、调用析构,再把同一槽清零。
15 // 两条 RIP 相对指令必须解码到同一个 RVA,以避免把普通析构函数误认为歌词对象。
16 private static readonly byte?[] Signature =
17 [
18 0x48, 0x8D, 0x0D, null, null, null, null,
19 0xE8, null, null, null, null,
20 0x48, 0x8B, 0xC8,
21 0xE8, null, null, null, null,
22 0x48, 0xC7, 0x05, null, null, null, null,
23 0x00, 0x00, 0x00, 0x00
24 ];
25
26 public static long[] DefaultOffsets { get; } = [0x120, 0x8, 0x0];
27
28 public static string ComputeModuleHash(string modulePath)
29 {
30 using var stream = File.OpenRead(modulePath);
31 return Convert.ToHexString(SHA256.HashData(stream));
32 }
33
34 public static IReadOnlyList<AddressResolverModel> FindResolvers(string modulePath, string version)
35 {
36 var image = File.ReadAllBytes(modulePath);
37 return FindBaseAddresses(image)
38 .Select(baseAddress => new AddressResolverModel
39 {
40 Version = version,
41 ModuleName = Path.GetFileName(modulePath),
42 BaseAddress = baseAddress,
43 Offsets = [.. DefaultOffsets]
44 })
45 .ToArray();
46 }
47
48 /// <summary>
49 /// 在 PE 映像的可执行节中查找所有经过双重 RIP 目标校验的候选 RVA。
50 /// </summary>
51 public static IReadOnlyList<long> FindBaseAddresses(ReadOnlySpan<byte> image)
52 {
53 if (!TryReadPeLayout(image, out var sectionTableOffset, out var sectionCount, out var imageSize))
54 return [];
55
56 var candidates = new HashSet<long>();
57 for (var sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++)
58 {
59 var sectionOffset = sectionTableOffset + sectionIndex * 40;
60 if (sectionOffset < 0 || sectionOffset + 40 > image.Length)
61 return [];
62
63 var virtualAddress = BinaryPrimitives.ReadUInt32LittleEndian(image.Slice(sectionOffset + 12, 4));
64 var rawSize = BinaryPrimitives.ReadUInt32LittleEndian(image.Slice(sectionOffset + 16, 4));
65 var rawOffset = BinaryPrimitives.ReadUInt32LittleEndian(image.Slice(sectionOffset + 20, 4));
66 var characteristics = BinaryPrimitives.ReadUInt32LittleEndian(image.Slice(sectionOffset + 36, 4));
67 if ((characteristics & ImageScnMemExecute) == 0 || rawSize < Signature.Length || rawOffset >= image.Length)
68 continue;
69
70 var sectionEnd = Math.Min((long)image.Length, (long)rawOffset + rawSize);
71 var lastStart = sectionEnd - Signature.Length;
72 for (var fileOffset = (long)rawOffset; fileOffset <= lastStart; fileOffset++)
73 {
74 if (image[(int)fileOffset] != Signature[0] || !Matches(image, (int)fileOffset))
75 continue;
76
77 var instructionRva = (long)virtualAddress + fileOffset - rawOffset;
78 var firstDisplacement = BinaryPrimitives.ReadInt32LittleEndian(image.Slice((int)fileOffset + 3, 4));
79 var secondDisplacement = BinaryPrimitives.ReadInt32LittleEndian(image.Slice((int)fileOffset + 23, 4));
80 var firstTarget = instructionRva + 7 + firstDisplacement;
81 var secondTarget = instructionRva + Signature.Length + secondDisplacement;
82 if (firstTarget == secondTarget && firstTarget > 0 && firstTarget < imageSize)
83 candidates.Add(firstTarget);
84 }
85 }
86
87 return candidates.Order().ToArray();
88 }
89
90 private static bool Matches(ReadOnlySpan<byte> image, int offset)
91 {
92 for (var index = 1; index < Signature.Length; index++)
93 {
94 if (Signature[index] is byte expected && image[offset + index] != expected)
95 return false;
96 }
97 return true;
98 }
99
100 private static bool TryReadPeLayout(
101 ReadOnlySpan<byte> image,
102 out int sectionTableOffset,
103 out int sectionCount,
104 out uint imageSize)
105 {
106 sectionTableOffset = 0;
107 sectionCount = 0;
108 imageSize = 0;
109 if (image.Length < 0x40 || image[0] != (byte)'M' || image[1] != (byte)'Z')
110 return false;
111
112 var peOffset = BinaryPrimitives.ReadInt32LittleEndian(image.Slice(0x3C, 4));
113 if (peOffset < 0 || peOffset + 24 > image.Length ||
114 image[peOffset] != (byte)'P' || image[peOffset + 1] != (byte)'E' ||
115 image[peOffset + 2] != 0 || image[peOffset + 3] != 0)
116 return false;
117
118 sectionCount = BinaryPrimitives.ReadUInt16LittleEndian(image.Slice(peOffset + 6, 2));
119 var optionalHeaderSize = BinaryPrimitives.ReadUInt16LittleEndian(image.Slice(peOffset + 20, 2));
120 var optionalHeaderOffset = peOffset + 24;
121 sectionTableOffset = optionalHeaderOffset + optionalHeaderSize;
122 if (optionalHeaderSize < 60 || optionalHeaderOffset + optionalHeaderSize > image.Length ||
123 sectionCount <= 0 || sectionTableOffset + sectionCount * 40 > image.Length)
124 return false;
125
126 imageSize = BinaryPrimitives.ReadUInt32LittleEndian(image.Slice(optionalHeaderOffset + 56, 4));
127 return imageSize > 0;
128 }
129 }