返回提交历史
Modified
Client/HaloPixelToolBox.Client.Test/Program.cs
+42
-0
Modified
Client/HaloPixelToolBox.Client/Profiles/CacheProfiles/CacheProfile.cs
+5
-0
Modified
Client/HaloPixelToolBox.Client/Profiles/CrossVersionProfiles/SystemProfile.cs
+2
-2
Modified
Client/HaloPixelToolBox.Client/Utilities/AddressResolverProvider.cs
+16
-0
Modified
Client/HaloPixelToolBox.Client/ViewModels/CloudMusicLyricsToolPageViewModel.cs
+2
-1
Modified
Client/HaloPixelToolBox.Client/ViewModels/SettingPageViewModel.cs
+2
-12
Modified
Client/HaloPixelToolBox.Client/Views/AppShellPage.xaml.cs
+1
-12
Modified
Client/HaloPixelToolBox.Client/Views/SettingPage.xaml
+1
-2
Modified
Core/HaloPixelToolBox.Core/Utilities/CloudLyricsReader.cs
+205
-22
Added
Core/HaloPixelToolBox.Core/Utilities/CloudMusicSignatureScanner.cs
+129
-0
XFEstudio/HaloPixelToolBox
实现网易云歌词地址自动特征扫描与缓存
本次提交引入 CloudMusicSignatureScanner 工具,实现对 cloudmusic.dll 的 PE 特征扫描与歌词指针自动定位,支持模块哈希校验与候选地址缓存。CloudMusicLyricsReader 支持多种解析来源与自动缓存,AddressResolverProvider 增加解析器缓存与保存逻辑。设置项调整为“首选歌词源”,仅保留网易云和 Spotify,相关界面与逻辑同步更新。主页与启动流程优化,增强歌词地址解析健壮性,完善日志与异常处理。
47262c8
代码差异
10 个文件
+405
-51
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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)
@@ -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
}
@@ -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
{
@@ -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)
@@ -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=""/>
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>
@@ -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;
@@ -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
}