前言 完整项目地址: https://github.com/chr233/XinjingdailyBot 因为项目之前是使用运行时反射来完成服务的动态注册，而源生成器是在编译前完成代码生成并参与编译，完全没有运行时的性能开销 运行时反射 - 旧的方案 标记特性 首先创建一个 Attribute, 用来标注需要扫描的服务类 namespace XinjingdailyBot.Infrastructure.Attribute; /// /// 标记服务 /// 1、如果服务是本身 直接在类上使用[AppService] /// 2、如果服务是接口 在类上使用 [AppService(ServiceType = typeof(实现接口))] /// [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class AppServiceAttribute : System.Attribute { /// /// 服务声明周期 /// 不给默认值的话注册的是AddSingleton /// public LifeTime ServiceLifetime { get; set; } = LifeTime.Scoped; /// /// 指定服务类型 /// public Type? ServiceType { get; set; } /// /// 是否可以从第一个接口获取服务类型 /// public bool InterfaceServiceType { get; set; } /// /// 标记服务 /// /// public AppServiceAttribute(LifeTime serviceLifetime) { ServiceLifetime = serviceLifetime; } /// /// 标记服务 /// /// /// public AppServiceAttribute(Type? serviceType, LifeTime serviceLifetime) { ServiceLifetime = serviceLifetime; ServiceType = serviceType; } /// /// 标记服务 /// /// /// /// public AppServiceAttribute(Type? serviceType, LifeTime serviceLifetime, bool interfaceServiceType) { ServiceLifetime = serviceLifetime; ServiceType = serviceType; InterfaceServiceType = interfaceServiceType; } } /// /// 生命周期 /// public enum LifeTime { /// /// 瞬时 /// Transient, /// /// 范围 /// Scoped, /// /// 单例 /// Singleton } 基于反射的服务扫描器 然后再主项目启动的时候使用反射扫描添加了该 Attribute 的所有 Class 并注册服务 using System.Diagnostics.CodeAnalysis; using System.Reflection; using XinjingdailyBot.Infrastructure.Attribute; namespace XinjingdailyBot.WebAPI.Extensions; /// /// 动态注册服务扩展 /// public static class AppServiceExtensions { private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); /// /// 注册引用程序域中所有有AppService标记的类的服务 /// /// [RequiresUnreferencedCode("不兼容剪裁")] public static void AddAppService(this IServiceCollection services) { var cls = new string[] { "XinjingdailyBot.Repository", "XinjingdailyBot.Service", "XinjingdailyBot.Command", "XinjingdailyBot.WebAPI", }; foreach (var item in cls) { var assembly = Assembly.Load(item); Register(services, assembly); } } /// /// 动态注册服务 /// /// /// [RequiresUnreferencedCode("不兼容剪裁")] private static void Register(IServiceCollection services, Assembly assembly) { string? name = assembly.GetName().Name; _logger.Debug($"===== 注册 {name} 中的服务 ====="); uint count = 0; foreach (var type in assembly.GetTypes()) { var serviceAttribute = type.GetCustomAttribute<AppServiceAttribute>(); if (serviceAttribute != null) { count++; var serviceType = serviceAttribute.ServiceType; //情况1 适用于依赖抽象编程，注意这里只获取第一个 if (serviceType == null && serviceAttribute.InterfaceServiceType) { serviceType = type.GetInterfaces().FirstOrDefault(); } //情况2 不常见特殊情况下才会指定ServiceType，写起来麻烦 if (serviceType == null) { serviceType = type; } var lifetime = serviceAttribute.ServiceLifetime; switch (lifetime) { case LifeTime.Singleton: services.AddSingleton(serviceType, type); break; case LifeTime.Scoped: services.AddScoped(serviceType, type); break; case LifeTime.Transient: services.AddTransient(serviceType, type); break; default: services.AddTransient(serviceType, type); break; } _logger.Debug($"{lifetime} - {serviceType}"); } } _logger.Debug($"===== 注册了 {count} 个服务 ====="); } } 也不是不能用，就是感觉不够完美，而且因为使用了反射的相关特性，无法简单地启用程序集裁剪和应用 AOT，所有就想到利用 .Net 5.0 新加入的源生成器特性来实现相同的功能，避免使用反射特性和降低运行时的额外开销 源生成器 - 新的方案 源生成器官方文档 因为我的项目是按照命名空间分成了几个子项目，而源生成器只能分析当前项目的语法树，如果用文件 IO 的方式读取本地 .cs 文件也是可以实现的，但是会显示警告，因为官方并不推荐在源生成器中使用文件 IO，这会降低编译速度，而且服务类也不是经常新增，每次编译都扫描一遍也是完全没必要的 综上，我的解决方案是写一个 Powershell 脚本来扫描解决方案中的所有服务，并保存到一个文件中，然后在源生成器中读取该文件来生成服务注册的代码 服务扫描脚本 首先是服务扫描脚本 scan_service.ps1 # # @Author : Chr_ # @Date : 2024-01-18 14:25:15 # @LastEditors : Chr_ # @LastEditTime : 2024-01-18 20:15:18 # @Description : 生成依赖服务信息 # # 工作目录 $workDir = (Get-Location).Path; $outputPath = Join-Path -Path (Join-Path -Path $workDir -ChildPath "XinjingdailyBot.WebAPI") -ChildPath "Properties"; # 包名列表 $libNames = "XinjingdailyBot.Model", "XinjingdailyBot.Repository", "XinjingdailyBot.Interface", "XinjingdailyBot.Service", "XinjingdailyBot.Tasks", "XinjingdailyBot.Command"; # 输出路径 $appServicePath = Join-Path -Path $outputPath -ChildPath "appService.json"; # 扫描源文件列表 function GetSourceFileList { param ( [string]$Path ) $result = @(); # 去除 bin obj 目录 $dirs = Get-ChildItem -Path $Path -Directory -Exclude "bin", "obj" # 遍历所有 .cs 文件 foreach ($dir in $dirs) { $files = Get-ChildItem -Path $dir -Filter "*.cs" -File -Recurse # 遍历子目录文件 foreach ($file in $files) { $result += $file } } $files = Get-ChildItem -Path $Path -Filter "*.cs" -File # 遍历所有 .cs 文件 foreach ($file in $files) { $result += $file } Write-Output $result $result; } # 读取命名空间 function GetNamespace { param ( [string] $Content ) if ($Content -match "namespace\s+([^;{]+)") { $Matches[1]; } } # 读取类名/接口名 function GetClassName { param ( [string] $Content ) if ($Content -match "(?:class|interface|record)\s+([^({:\s]+)") { $Matches[1]; } } # 扫描AppServiceAttribute function ScanAppServiceAttribute { param ( [string] $Content ) if ($Content -match "\[AppService(?:Attribute)?\(([^\]]+)\)\]") { $result = @{} $arguments = $Matches[1].Split(","); foreach ($argument in $arguments) { if ($argument -match "LifeTime\.(Singleton|Transient|Scoped)") { $result["LifeTime"] = $Matches[1]; } elseif ($argument -match "typeof\(([^()]+)\)") { $result["Interface"] = $Matches[1]; } elseif ($argument -match "true|false") { $result["AutoInterface"] = $Matches[1]; } else { Write-Output $argument; } } $result; } } # 类全名表 $clsNamespaces = @{}; $appServiceAttributes = @{}; foreach ($libName in $libNames) { # 库文件路径 $libPath = Join-Path -Path $workDir -ChildPath $libName; # 获取所有源文件路径 $filePaths = GetSourceFileList -Path $libPath; # 读取Service信息和类命名空间; foreach ($filePath in $filePaths) { $fileContent = Get-Content -Path $filePath.FullName -Raw; $namespace = GetNameSpace -Content $fileContent; if ($null -eq $namespace -or $namespace -eq "" ) { continue; } $clsName = GetClassName -Content $fileContent; if ($null -eq $namespace -or $namespace -eq "" ) { continue; } $clsNamespaces[$clsName] = $namespace; $appService = ScanAppServiceAttribute -Content $fileContent; if ($null -ne $appService) { $appServiceAttributes[$clsName] = $appService; } } } $services = $appServiceAttributes.Clone(); # 生成服务信息 foreach ($service in $services.Keys) { $serviceInfo = $appServiceAttributes[$service]; $namespace = $clsNamespaces[$service]; $serviceInfo["Class"] = "$namespace.$service"; $interface = $serviceInfo["Interface"]; if ($null -ne $interface) { $ns = $clsNamespaces[$interface]; $serviceInfo["Interface"] = "$ns.$interface"; } $appServiceAttributes[$service] = $serviceInfo; } Write-Output "扫描到 $($appServiceAttributes.Count) 个服务"; $appServiceAttributes | ConvertTo-Json | Out-File -FilePath $appServicePath Write-Output "文件保存至 $appServicePath" 这个脚本的功能是遍历定义的子项目文件，然后扫描所有类和接口，以及所有带有 AppServiceAttribute 特性的服务类，最后生成一个 appService.json 运行脚本生成文件以后记得修改文件生成操作，不然在分析器里这个文件是不可见的 创建源生成器 跟创建类库一样，但是需要编辑 .csproj 文件，完整文件如下 netstandard2.0 <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> true <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn> CS1570;CS1587 <None Include="..\.editorconfig" Link=".editorconfig" /> all runtime; build; native; contentfiles; analyzers; buildtransitive all all true all True True Templates.resx ResXFileCodeGenerator Templates.Designer.cs <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> <TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" /> 我所做的修改如下 将目标框架改为 netstandard2.0，目前源生成器必须用该版本 添加 EnforceExtendedAnalyzerRules 和 IsRoslynComponent 项，第一个是排除别的分析器，第二个是定义该项目是 Roslyn 组件，添加了以后会有额外的调试配置 添加 GetTargetPathDependsOn 项，意思是从定义的路径中寻找依赖库 添加 Microsoft.CodeAnalysis.Analyzers 和 Microsoft.CodeAnalysis.Common 和 Microsoft.CodeAnalysis.CSharp 库，原样从微软文档拷来的，生成器必须的依赖 添加 Newtonsoft.Json 库（可选），我使用了 Newtonsoft.Json 作为 Json 反序列化库，因此还需要额外配置引用信息（因为源生成器相当于是编译期的扩展，如果不做额外处理会报找不到Dll的错），需要在下面定义 <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> <TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" /> 然后再主项目中添加项目引用，并修改引用的属性，将被引用项目类型改成分析器，然后排除所有的生成资产 Analyzer <ReferenceOutputAssembly>false</ReferenceOutputAssembly> 服务注册源生成器 代码模板存放在 Templates.resx 中，内容如图 可以根据输出的结果反推 using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using Newtonsoft.Json; using System.Text; using XinjingdailyBot.Generator.Data; namespace XinjingdailyBot.Generator; [Generator] internal sealed class AppServiceGenerator : ISourceGenerator { const string InputFileName = "appService.json"; const string OutoutFileName = "GeneratedAppServiceExtensions.g.cs"; /// <inheritdoc/> public void Initialize(GeneratorInitializationContext context) { // 无需处理 } /// <inheritdoc/> public void Execute(GeneratorExecutionContext context) { try { var fileText = context.AdditionalFiles.Where(static x => x.Path.EndsWith(InputFileName)).FirstOrDefault() ?? throw new FileNotFoundException("缺少配置文件, 请使用 scan_service.ps1 生成"); ProcessSettingsFile(fileText, context); } catch (Exception e) { context.ReportDiagnostic( Diagnostic.Create( "XJB_01", nameof(AppServiceGenerator), $"生成注入代码失败，{e.Message}", defaultSeverity: DiagnosticSeverity.Error, severity: DiagnosticSeverity.Error, isEnabledByDefault: true, warningLevel: 0)); } } /// /// 生成文件 /// /// /// private void ProcessSettingsFile(AdditionalText xmlFile, GeneratorExecutionContext context) { var text = xmlFile.GetText(context.CancellationToken)?.ToString() ?? throw new FileLoadException("文件读取失败"); var json = JsonConvert.DeserializeObject<AppServiceData>(text) ?? throw new FileLoadException("文件读取失败"); var sb = new StringBuilder(); sb.AppendLine(Templates.AppServiceHeader); foreach (var kv in json) { var entry = kv.Value; var lifeTime = entry.LifeTime?.ToLowerInvariant() switch { "singleton" or "scoped" or "transient" => entry.LifeTime, _ => null, }; if (string.IsNullOrEmpty(lifeTime)) { continue; } if (!string.IsNullOrEmpty(entry.Class)) { if (string.IsNullOrEmpty(entry.Interface)) { sb.AppendLine(string.Format(Templates.AppServiceContent1, lifeTime, entry.Class)); } else { sb.AppendLine(string.Format(Templates.AppServiceContent2, lifeTime, entry.Interface, entry.Class)); } } } sb.AppendLine(Templates.AppServiceFooter); context.AddSource(OutoutFileName, SourceText.From(sb.ToString(), Encoding.UTF8)); } } 生成的代码示例，为避免使用 using，这里使用完整类名引用 using Microsoft.Extensions.DependencyInjection; namespace XinjingdailyBot.WebAPI.Extensions; /// /// 动态注册服务扩展 /// public static class GeneratedAppServiceExtensions { private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); /// /// 注册引用程序域中所有有AppService标记的类的服务 /// /// public static void AddAppServiceGenerated(this IServiceCollection services) { services.AddTransient<XinjingdailyBot.Repository.OldPostRepository>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Common.IDispatcherService, XinjingdailyBot.Service.Bot.Common.DispatcherService>(); services.AddSingleton<XinjingdailyBot.Repository.TagRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Common.IChannelService, XinjingdailyBot.Service.Bot.Common.ChannelService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAttachmentService, XinjingdailyBot.Service.Data.AttachmentService>(); services.AddScoped<XinjingdailyBot.Command.ReviewCommand>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Handler.IJoinRequestHandler, XinjingdailyBot.Service.Bot.Handler.JoinRequestHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.IBanRecordService, XinjingdailyBot.Service.Data.BanRecordService>(); services.AddSingleton<XinjingdailyBot.Repository.RejectReasonRepository>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Common.IUpdateService, XinjingdailyBot.Service.Bot.Common.UpdateService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IForwardMessageHandler, XinjingdailyBot.Service.Bot.Handler.ForwardMessageHandler>(); services.AddScoped<XinjingdailyBot.Command.CommonCommand>(); services.AddSingleton<XinjingdailyBot.Repository.LevelRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.ICommandHandler, XinjingdailyBot.Service.Bot.Handler.CommandHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.IDialogueService, XinjingdailyBot.Service.Data.DialogueService>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IPostService, XinjingdailyBot.Service.Data.PostService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IChannelOptionService, XinjingdailyBot.Service.Data.ChannelOptionService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IChannelPostHandler, XinjingdailyBot.Service.Bot.Handler.ChannelPostHandler>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IUserService, XinjingdailyBot.Service.Data.UserService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IImageHelperService, XinjingdailyBot.Service.Helper.ImageHelperService>(); services.AddSingleton<XinjingdailyBot.Interface.Data.INameHistoryService, XinjingdailyBot.Service.Data.NameHistoryService>(); services.AddScoped<XinjingdailyBot.Command.NormalCommand>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IMediaGroupService, XinjingdailyBot.Service.Data.MediaGroupService>(); services.AddScoped<XinjingdailyBot.Command.ObsoleteCommand>(); services.AddScoped<XinjingdailyBot.Command.PostCommand>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAdvertisePostService, XinjingdailyBot.Service.Data.AdvertisePostsService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAdvertiseService, XinjingdailyBot.Service.Data.AdvertiseService>(); services.AddSingleton<XinjingdailyBot.Repository.GroupRepository>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IHttpHelperService, XinjingdailyBot.Service.Helper.HttpHelperService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.ITextHelperService, XinjingdailyBot.Service.Helper.TextHelperService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IMessageHandler, XinjingdailyBot.Service.Bot.Handler.MessageHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.ICmdRecordService, XinjingdailyBot.Service.Data.CmdRecordService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IInlineQueryHandler, XinjingdailyBot.Service.Bot.Handler.InlineQueryHandler>(); services.AddTransient<XinjingdailyBot.Repository.PostRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IUserTokenService, XinjingdailyBot.Service.Data.UserTokenService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IMarkupHelperService, XinjingdailyBot.Service.Helper.MarkupHelperService>(); services.AddScoped<XinjingdailyBot.Command.AdminCommand>(); services.AddScoped<XinjingdailyBot.Command.SuperCommand>(); services.AddTransient<XinjingdailyBot.Interface.Data.IReviewStatusService, XinjingdailyBot.Service.Data.ReviewStatusService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IGroupMessageHandler, XinjingdailyBot.Service.Bot.Handler.GroupMessageHandler>(); } } 调试源生成器 首先删除源生成器项目的所有调试目标，然后新增 Roslyn Component 目标，并在右边将 Target Project 设为主项目，然后将启动项目改为生成器 然后在生成器的 Initialize 方法中开启调试器 public void Initialize(GeneratorInitializationContext context) { // 无需处理 if (!Debugger.IsAttached) { Debugger.Launch(); } } 这样就可以动态调试生成器代码了 实际使用 将启动项目改为主功能，去掉源生成器的调试代码，然后重新生成一遍解决方案，如果一切正常，将会在分析器中看到生成的代码文件 如果后续有新的服务，重新执行 scan_service.ps1 即可更新配置文件
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] [post cid="1365"/] [post cid="1383"/] 前情提要 距离上一篇文章已经过去一年多了，前两天心血来潮找了最新版本研究了一下，熬了两天夜也没整明白，不打算继续研究下去了，做个笔记记录下分析过程。 @> 本文适用于小黑盒 1.3.191，小黑盒历史版本参考我的仓库 @> 全文评论后可见
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] 距离上一篇文章已经过了半年了，最近也是刚好在学逆向相关的内容，就拿以前的工程来练手。 静态分析 还是接着上文，寻找hkey计算方式 可以看到，生成hkey的过程中调用了Native函数encode，这个函数在libnative-lib.so里 解压出so文件，拖进IDA，可以看到伪代码，可以看到一共有5个参数，但是在Java代码中是3个参数，前2个参数是不需要的。 尝试直接Hook，但是只能得到看不懂的结果，应该都是字符串地址。 我们换个思路，从return开始分析 int __fastcall encode(int a1, int a2, int a3, int a4, int a5) { const char *v5; // r10 int v6; // r4 int v7; // r6 int v8; // r8 const char *v9; // r11 int v10; // r0 bool v11; // zf size_t v12; // r5 char *v13; // r5 char *v14; // r0 size_t v15; // r0 char *v16; // r0 int v17; // r5 int result; // r0 int v19; // [sp+0h] [bp-30h] int *v20; // [sp+4h] [bp-2Ch] int v21; // [sp+8h] [bp-28h] int v22; // [sp+Ch] [bp-24h] v19 = a4; v6 = a1; // v6 是第一个参数 v7 = a4; if ( j_check_signature(a1) == 1 ) // 看名字应该是验证签名的方法 { v8 = 0; v9 = (const char *)(*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)v6 + 676))(v6, v7, 0); v10 = (*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)v6 + 676))(v6, a5, 0); v11 = v9 == 0; if ( v9 ) { v5 = (const char *)v10; v11 = v10 == 0; } if ( !v11 ) { v21 = a5; v12 = strlen(v9); v20 = &v19; v13 = (char *)&v19 - ((strlen(v5) + v12 + 21) & 0xFFFFFFF8); v14 = strcpy(v13, v9); v15 = strlen(v14); _aeabi_memcpy(&v13[v15], "/bfhdkud_time=", 15); v16 = strcat(v13, v5); v17 = j_MDString(v16); // v16 被赋值 (*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v6 + 680))(v6, v7, v9); (*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v6 + 680))(v6, v21, v5); v8 = j_charToJstring(v6, v17); // v8 被赋值，值与v6，v17有关 } if ( _stack_chk_guard == v22 ) return v8; // 返回值是 v8 } //正常逻辑应该走不到这里 result = _stack_chk_guard - v22; if ( _stack_chk_guard == v22 ) result = j_j_charToJstring(v6, UNSIGNATURE[0]); return result; } 不难看出，返回值跟j_charToJstring和j_MDString有关，j_大概是一个修饰符，真实函数应该是charToJstring和MDString 找到目标，接下来上Hook。 动态分析 接下来使用 Frida 进行分析，Frida 安装与使用可以参考： [post cid="1301"/] Python 加载器 保存为run.py import sys import frida process_name = 'com.max.xiaoheihe' # 发送信息回调函数 def on_message(message, data): if message['type'] == 'send': print(f"[*] {message['payload']}") else: print(message) if __name__ == '__main__': try: device = frida.get_usb_device() except: device = frida.get_remote_device() if not device: print("* 连接到Frida Server失败") else: process = device.attach(process_name) # 加载JS脚本 js = open('hook.js', encoding='utf-8').read() script = process.create_script(js) script.on('message', on_message) script.load() # 读取返回输入 input() script.unload() Hook代码 保存为hook.js console.log("脚本载入成功"); Java.perform(function () { var charToJstringAddr = Module.findExportByName("libnative-lib.so", "charToJstring"); console.log(charToJstringAddr); if (charToJstringAddr != null) { Interceptor.attach(charToJstringAddr, { onEnter: function (args) { var arg0 = args[0]; var arg1 = args[1]; console.log('charToJstring - Enter'); console.log(arg0, Memory.readCString(arg0)); console.log(arg1, Memory.readCString(arg1)); }, onLeave: function (retval) { //retval函数返回值 console.log('charToJstring - Leave'); console.log(retval.toString()); console.log('======'); } }); } var MDStringAddr = Module.findExportByName("libnative-lib.so", "MDString"); console.log(MDStringAddr); if (MDStringAddr != null) { Interceptor.attach(MDStringAddr, { onEnter: function (args) { var arg0 = args[0]; var arg1 = args[1]; console.log('MDString - Enter'); console.log(arg0, Memory.readCString(arg0)); console.log(arg1, Memory.readCString(arg1)); }, onLeave: function (retval) { console.log('MDString - Leave'); console.log(retval.toString()); console.log('======'); } }); } }) 不难看出，MDString的输入是特定的拼接字符串，MDString的输出是charToJstring的输入，而且是一个32位哈希值，推测是MD5 试了一下，果真是MD5 所以encode的逻辑是把url的路径部分，拼上/bfhdkud_time=，再加上秒级时间戳，然后返回整个字符串的MD5哈希 Python 实现 稍微整理了一下Java代码： String path = get_url_path(str); String time = (System.currentTimeMillis() / 1000) + ""; if (path.endsWith("/")) { path = path.substring(0, i.length() - 1); //去除path结尾的"/" } String hkey = get_MD5(NDKTools.encode(HeyBoxApplication.f(), path, time).replaceAll("a", "app").replaceAll("0", "app")).substring(0, 10); 接下来换成Python实现 import time import hashlib from urllib.parse import urlparse def gen_hkey(url: str,t:int) -> str: def url_to_path(url: str) -> str: path = urlparse(url).path if path and path[-1] == '/': path = path[:-1] return(path) def get_md5(data: str): md5 = hashlib.md5() md5.update(data.encode('utf-8')) result = md5.hexdigest() return(result) h = f'{url_to_path(url)}/bfhdkud_time={t}' h = get_md5(h) h = h.replace('a', 'app') h = h.replace('0', 'app') h = get_md5(h) h = h[:10] return(h) t=time.time() h=gen_hkey('https://api.xiaoheihe.cn/bbs/app/feeds/news',t) print('time: ',t) print('hkey: ',h) 后续 [post cid="1365"/]