Frida Hook Java层还原Android客户端签名算法

news2026/5/24 8:46:12
1. 这不是“调用API”而是拆解签名生成的完整逻辑链你有没有遇到过这种情况App每次请求都带一个叫api-sign的字段值像一串随机字符串长度固定、格式规整但无论你怎么翻网络请求日志、抓包重放、甚至改参数重发只要这个字段错一位后端就直接返回401 Unauthorized或sign invalid我第一次在做某电商类App的自动化测试时就被卡在这儿——明明所有参数都对Header也照搬就是过不了签名校验。后来才明白这不是简单的“加个时间戳再MD5”而是一整套嵌在Java层里的、带上下文依赖的签名生成逻辑它可能读取设备ID、拼接特定顺序的请求体字段、参与计算的密钥藏在so里、甚至还会根据当前Activity状态动态调整盐值。Frida Hook在这里的价值从来不是为了“绕过”签名而是为了把黑盒变成白盒看清签名函数的输入从哪来、中间怎么变换、输出怎么封装。这篇文章讲的就是如何用Frida精准定位到那个generateApiSign()方法拿到它真正接收的原始参数不是你看到的JSON字符串而是它内部解析后的Map对象观察它调用的每一个子函数比如encryptWithDeviceKey()、buildSignatureString()最终还原出完整的签名算法。它适合正在做Android App安全评估、协议逆向、自动化接口调试的工程师也适合想深入理解“客户端签名防篡改”实际落地方式的开发者。关键词Frida Hook、Android逆向、Java层签名、api-sign分析、Java函数调用链追踪。2. 为什么必须从Java层入手——签名逻辑的三层嵌套陷阱很多人一上来就想用Frida HookOkHttpClient或Retrofit的网络请求方法以为截住请求就能改api-sign。这思路本身没错但实操中90%会失败。原因在于签名生成和网络发送之间存在三道天然屏障它们共同构成了“Hook Java层”的必要性。第一道是逻辑前置性。签名值不是在网络层生成的而是在业务逻辑层就计算完毕作为普通参数传给网络模块。比如一个典型的调用链是OrderActivity.onClick()→OrderService.createOrder()→SignHelper.generateApiSign(Map params)→NetworkClient.post(/order, params)。你HookNetworkClient.post时params里api-sign字段早已是最终结果你根本看不到它怎么算出来的。就像你只看到厨师端上来的菜却不知道他切了几刀、放了几克盐、火候几成。第二道是参数抽象化。generateApiSign()接收的往往不是原始JSON字符串而是一个HashMapString, Object或自定义的RequestModel对象。这个对象里可能包含timestamp: 1715823456,nonce: a1b2c3,bodyMd5: d41d8cd98f00b204e9800998ecf8427e等字段但这些字段的值很多是在generateApiSign()内部调用其他工具类实时生成的。比如bodyMd5不是你传进去的而是它自己把params里除api-sign外的所有键值对按字典序拼接后计算的。你Hook网络层只能看到最终的bodyMd5字符串而Hook Java层你能看到它拼接前的原始Map、看到它调用DigestUtils.md5Hex()的完整入参、甚至能看到它读取SharedPreferences里存储的设备指纹的过程。第三道是密钥隔离性。真正的签名密钥几乎从不以明文字符串形式出现在Java代码里。它可能被拆成两段一段藏在BuildConfig.API_KEY_PREFIX里另一段通过NativeLib.getSecretKeySuffix()从so里动态获取然后在generateApiSign()里拼接。你Hook Java层能清晰看到prefix suffix的拼接过程而如果只Hook网络层你只会看到一个最终的、无法反推的密钥哈希值。我曾经在一个金融类App里发现它的密钥生成逻辑里还嵌套了三次System.currentTimeMillis() % 1000的取模运算用来动态选择密钥片段——这种细节只有在Java层单步跟踪时才能捕捉到。提示不要试图用正则从Java代码里搜索api-sign或generateSign。混淆后的代码里方法名可能是a(),b(String, int),c$123()类名可能是com.a.b.c.d.e.f。静态分析只能给你一个模糊范围真正的定位必须靠动态Hook行为验证。3. Frida脚本的核心设计从“找到函数”到“看清输入”的四步闭环写一个能真正还原签名逻辑的Frida脚本绝不是简单地Java.use(xxx.SignHelper).generateApiSign.implementation ...就完事。它必须形成一个“定位→拦截→观察→验证”的闭环。下面是我经过二十多个App实战打磨出的标准四步法每一步都对应一个关键决策点。3.1 第一步宽泛定位——用类名/包名线索缩小搜索范围别一上来就猜函数名。先看APK结构用jadx-gui打开APK搜索关键词api-sign、sign、signature重点看onCreate()、onClick()、网络请求发起处附近的代码。你会发现签名逻辑往往集中在几个固定位置com.xxx.network.、com.xxx.util.、com.xxx.security.这些包下。比如我分析的一个社交App所有签名相关类都在com.social.core.security包里主类叫SecurityManager。这时你的Frida脚本第一行就该是Java.perform(function () { var SecurityManager Java.use(com.social.core.security.SecurityManager); // 后续Hook逻辑 });为什么不用Java.choose动态遍历因为Java.choose在App启动初期执行时目标类可能还没加载。而Java.use是惰性加载只要类被首次引用Frida就会自动绑定。这是经验之谈宁可多写几行Java.use也不要依赖Java.choose去找“可能存在的类”。3.2 第二步智能拦截——不只Hook函数更要Hook“函数调用前”的瞬间很多教程教你这样写SecurityManager.generateApiSign.implementation function (params) { console.log([] generateApiSign called with: , params); return this.generateApiSign.call(this, params); };这只能看到最终入参但丢失了最关键的信息这个params是谁传进来的它之前长什么样正确做法是Hook函数的“入口点”也就是JVM执行到该函数第一行代码前的瞬间。Frida提供了onEnter钩子它能在函数执行前捕获所有寄存器和局部变量。对于Java方法onEnter的args数组就是该方法的全部参数。但要注意args[0]是this当前对象实例args[1]才是第一个业务参数。所以一个更健壮的写法是SecurityManager.generateApiSign.overload(java.util.Map).implementation function (paramsMap) { console.log([*] generateApiSign entered. this: , this.$className); console.log([*] paramsMap keys: , Array.from(paramsMap.keySet())); // 关键打印每个key对应的value类型和简略内容 var keys paramsMap.keySet().toArray(); for (var i 0; i keys.length; i) { var key keys[i]; var value paramsMap.get(key); console.log([*] key: key - type: value.getClass().getName() , value: (value.toString ? value.toString().substring(0, 50) : N/A)); } var result this.generateApiSign.call(this, paramsMap); console.log([*] generateApiSign returned: , result); return result; };这段代码的威力在于它不假设paramsMap里一定有timestamp或nonce而是把整个Map的结构、每个值的类型是String是JSONObject还是自定义的RequestBody对象都打出来。我曾在一个新闻App里发现它的paramsMap里body字段的值类型是okhttp3.RequestBody而不是String——这意味着签名逻辑里必然调用了RequestBody.string()或类似方法去读取原始字节流。这个发现直接指引我下一步去HookRequestBody的相关方法。3.3 第三步深度观察——顺着调用栈逐层Hook子函数当你看到generateApiSign()的入参里有个deviceInfo字段值是一个DeviceInfo对象你不能只满足于打印DeviceInfo.toString()。你要知道DeviceInfo的getDeviceId()方法很可能就是密钥的一部分。这时你需要“顺藤摸瓜”// 先Hook DeviceInfo类 var DeviceInfo Java.use(com.social.core.security.DeviceInfo); DeviceInfo.getDeviceId.implementation function () { var deviceId this.getDeviceId.call(this); console.log([*] DeviceInfo.getDeviceId() returned: , deviceId); return deviceId; }; // 再Hook它可能调用的加密方法 var CryptoUtil Java.use(com.social.core.security.CryptoUtil); CryptoUtil.hmacSha256.overload(java.lang.String, java.lang.String).implementation function (data, key) { console.log([*] CryptoUtil.hmacSha256 called with data: , data.substring(0, 30), and key: , key.substring(0, 20)); var result this.hmacSha256.call(this, data, key); console.log([*] hmacSha256 result: , result); return result; };这个过程就像剥洋葱每一层Hook都揭示一层新的逻辑。你可能会发现hmacSha256的key参数其实是CryptoUtil类里一个静态字段sKey的值而sKey又是在CryptoUtil.clinit()静态初始化块里通过NativeLib.getDynamicKey()设置的。于是你的Hook链条就延伸到了so层——但这正是我们后续文章要讲的内容。目前Java层的完整调用链已经清晰generateApiSign()→DeviceInfo.getDeviceId()→CryptoUtil.hmacSha256()→NativeLib.getDynamicKey()。3.4 第四步交叉验证——用真实请求数据反向检验Hook结果所有Hook日志都是“旁观者视角”必须用真实数据验证它是否准确。最有效的方法是构造一个已知结果的请求看Hook日志能否复现该结果。比如你抓到一个成功的请求它的api-sign是a1b2c3d4e5f67890。你在Hook脚本里除了打印日志还要把关键中间变量存下来var debugData {}; SecurityManager.generateApiSign.overload(java.util.Map).implementation function (paramsMap) { // ... 日志打印 ... // 关键把核心中间变量存入全局debugData供后续分析 debugData.rawParams JSON.stringify(paramsMap); // 如果能转JSON debugData.timestamp paramsMap.get(timestamp); debugData.deviceId DeviceInfo.getDeviceId.call(DeviceInfo.$new()); // 调用一次获取 var result this.generateApiSign.call(this, paramsMap); debugData.finalSign result; return result; }; // 暴露一个JS接口方便在控制台手动调用 rpc.exports { getDebugData: function () { return debugData; } };然后在Frida CLI里你执行frida -U -f com.social.app -l hook.js --no-pause等App启动后手动触发一次签名生成再在控制台输入rpc.exports.getDebugData()就能拿到一个包含所有关键变量的JSON对象。你可以把它复制到Python脚本里用同样的算法比如hmac.new(key.encode(), data.encode(), hashlib.sha256).hexdigest()尝试复现a1b2c3d4e5f67890。如果复现成功说明你的Hook完全准确如果失败就说明你还漏掉了某个隐藏步骤比如时间戳被转换成了毫秒、或者某个字段被URL编码了两次。这就是“验证”环节不可替代的价值。4. 实战排错那些让你怀疑人生的Hook失效场景与根因Frida Hook Java层听起来很美但实操中你会遇到一堆“日志没打印”、“函数根本没被调用”、“Hook后App直接闪退”的问题。这些问题背后往往藏着对Android运行机制的深层误解。下面是我踩过的五个典型坑每个都附带了定位方法和解决方案。4.1 坑一“函数根本没被调用”——类加载时机与Hook时机的错位现象你确认SecurityManager类存在generateApiSign()方法也找到了但无论怎么操作AppHook日志就是不出现。根因Java.use()绑定的是类模板但generateApiSign()方法可能只在某个特定Activity比如LoginActivity里被调用。而你的Frida脚本是在App进程启动时就注入的此时LoginActivity根本没创建SecurityManager类也没被加载。Java.use(xxx)成功了但SecurityManager.generateApiSign.implementation还没被设置因为JVM还没解析到这个方法。解决方案把Hook逻辑放到Java.performNow()里并监听Activity生命周期。标准写法Java.perform(function () { // 先Hook Activity确保在目标页面出现时再执行具体Hook var Activity Java.use(android.app.Activity); Activity.onResume.implementation function () { this.onResume.call(this); // 判断当前Activity是否是我们关心的 var currentClassName this.getClass().getName(); if (currentClassName com.social.ui.LoginActivity) { console.log([] LoginActivity resumed, now hooking SecurityManager...); var SecurityManager Java.use(com.social.core.security.SecurityManager); SecurityManager.generateApiSign.overload(java.util.Map).implementation function (params) { console.log([*] generateApiSign called!); return this.generateApiSign.call(this, params); }; } }; });这个方案的核心思想是不要期待“一次Hook处处生效”而要“按需加载精准打击”。它牺牲了一点通用性但换来的是100%的可靠性。4.2 坑二“日志打印了但参数是null”——混淆导致的参数类型误判现象你Hook了generateApiSign(String json)日志里args[1]总是null但你知道传进去的肯定不是空字符串。根因代码被ProGuard混淆后方法签名可能从generateApiSign(String)变成了a(String)但更重要的是它的实际参数类型可能被改成了java.lang.Object或自定义的a类。你用overload(java.lang.String)去匹配自然找不到Frida就默认调用overload()无参版本导致args[1]是undefined。解决方案用getDeclaredMethods()动态获取真实方法签名。在Java.perform里添加var SecurityManager Java.use(com.social.core.security.SecurityManager); console.log([] SecurityManager methods: ); SecurityManager.class.getDeclaredMethods().forEach(function (method) { console.log( - method.getName() - method.getGenericReturnType() ( method.getParameterTypes().map(t t.getName()).join(, ) )); });运行后你可能会看到- a - java.lang.String (java.lang.Object) - b - java.lang.String (java.util.Map)这说明真正的签名方法是a(Object)而不是generateApiSign(String)。于是你改成SecurityManager.a.overload(java.lang.Object).implementation function (obj) { console.log([*] a(Object) called with: , obj); // 尝试转换为String if (obj obj.toString) { console.log([*] as String: , obj.toString()); } return this.a.call(this, obj); };4.3 坑三“Hook后App闪退”——线程安全与UI线程阻塞现象Hook脚本加上去App一进入目标页面就ANR或直接崩溃。根因generateApiSign()很可能在主线程UI线程被调用。而你的Hook函数里如果做了耗时操作比如console.log()打印一个超大JSON、或者调用Java.array()转换大量数据就会阻塞UI线程触发系统ANR机制。解决方案所有耗时操作必须异步化。Frida提供了setTimeout但更推荐用Java.scheduleOnMainThread如果需要在UI线程执行或直接用setTimeout放到JS线程SecurityManager.generateApiSign.overload(java.util.Map).implementation function (paramsMap) { // 立即返回不阻塞 var result this.generateApiSign.call(this, paramsMap); // 异步打印避免阻塞 setTimeout(function () { console.log([*] Async log: params size , paramsMap.size()); // 这里可以做更重的日志处理 }, 0); return result; };4.4 坑四“同一个方法Hook日志有时出现有时不出现”——多实例与单例模式的干扰现象你在Application.onCreate()里Hook了SecurityManager但发现只有第一次登录时日志出现之后切换账号再登录日志就没了。根因SecurityManager很可能是一个单例但它的实例在不同账号登录时会被重新初始化。你Hook的是类模板但implementation是绑定在具体实例上的。当旧实例被GC回收新实例创建时你的Hook就失效了。解决方案Hook类的构造函数确保每个新实例都被“染色”。标准写法var SecurityManager Java.use(com.social.core.security.SecurityManager); SecurityManager.$init.overload().implementation function () { console.log([] New SecurityManager instance created); // 在这里对新实例进行Hook或者设置一个标记 this._hooked true; return this.$init.call(this); }; // 然后在generateApiSign里检查标记 SecurityManager.generateApiSign.overload(java.util.Map).implementation function (paramsMap) { if (!this._hooked) { console.log([!] This instance is not hooked! Re-hooking...); // 这里可以动态重新Hook该实例的方法 } console.log([*] generateApiSign called on hooked instance); return this.generateApiSign.call(this, paramsMap); };4.5 坑五“日志里看到密钥但用它算不出签名”——密钥的动态变形与二次加工现象你Hook到hmacSha256(data, key)key打印出来是my_secret_123但用Pythonhmac.new(bmy_secret_123, data.encode(), hashlib.sha256).hexdigest()算出的结果和App里真实的api-sign对不上。根因密钥在参与HMAC计算前很可能被做过变形。常见变形包括Base64解码key实际是bXlfc2VjcmV0XzEyMw需要先base64.b64decode()。AES解密key是一个密文需要用硬编码的AES密钥去解。字符串拼接key是prefix但真实密钥是prefix device_id suffix。哈希摘要key是raw_key但真实密钥是sha256(raw_key.encode()).hexdigest()。解决方案不要只看hmacSha256的入参要看它的调用栈上游。在hmacSha256的onEnter里打印Thread.currentThread().getStackTrace()找到是谁调用了它。你可能会看到at com.social.core.security.CryptoUtil.hmacSha256(CryptoUtil.java:45) at com.social.core.security.SignHelper.buildKey(SignHelper.java:128) // 关键 at com.social.core.security.SignHelper.generateApiSign(SignHelper.java:89)然后你就知道要去SignHelper.buildKey()里看密钥是怎么组装的。这才是真正的根因定位。5. 从Hook到复现一份可直接运行的签名算法还原指南前面所有步骤最终都要服务于一个目标在你的本地环境Python/Node.js里100%复现出和App一模一样的api-sign值。这不仅是技术验证更是后续自动化、测试、甚至合规审计的基础。下面我以一个真实案例某外卖App的订单签名为例展示从Frida日志到本地Python脚本的完整还原流程。5.1 步骤一整理Frida日志提取核心要素运行Hook脚本后你得到的关键日志如下[*] generateApiSign entered. this: com.social.core.security.SignHelper [*] paramsMap keys: [timestamp, nonce, order_id, user_id, body] [*] key: timestamp - type: java.lang.Long, value: 1715823456 [*] key: nonce - type: java.lang.String, value: a1b2c3d4 [*] key: body - type: org.json.JSONObject, value: {address:beijing,items:[{id:1,count:2}]} [*] SignHelper.buildKey() called, deviceId: 861234567890123 [*] CryptoUtil.hmacSha256 called with data: 1715823456|a1b2c3d4|{address:beijing,items:[{id:1,count:2}]}|861234567890123 and key: sec_key_v2 [*] hmacSha256 result: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855从中我们提取出四个核心要素基础参数timestamp1715823456,noncea1b2c3d4业务参数body是一个JSON对象需要序列化为字符串设备标识deviceId861234567890123签名算法HMAC-SHA256(data, key)其中data是各字段按|拼接keysec_key_v25.2 步骤二编写Python验证脚本逐行对照import hashlib import hmac import json import base64 def generate_api_sign(timestamp: int, nonce: str, body_dict: dict, device_id: str) - str: # 1. 序列化body为紧凑JSON字符串无空格 body_str json.dumps(body_dict, separators(,, :)) # 2. 拼接data字符串按日志中的顺序 data_str f{timestamp}|{nonce}|{body_str}|{device_id} print(f[DEBUG] data_str: {data_str}) # 3. 定义密钥注意这里是明文实际中可能需要解密 key_str sec_key_v2 # 4. 计算HMAC-SHA256 signature hmac.new( key_str.encode(utf-8), data_str.encode(utf-8), hashlib.sha256 ).hexdigest() print(f[DEBUG] signature: {signature}) return signature # 测试数据来自Frida日志 if __name__ __main__: test_timestamp 1715823456 test_nonce a1b2c3d4 test_body {address: beijing, items: [{id: 1, count: 2}]} test_device_id 861234567890123 result generate_api_sign(test_timestamp, test_nonce, test_body, test_device_id) print(f[RESULT] api-sign: {result})运行此脚本输出的result必须和Frida日志里hmacSha256 result完全一致。如果不一致就回到日志检查data_str的拼接顺序、JSON序列化格式是否需要排序键、编码方式UTF-8UTF-16。5.3 步骤三处理真实世界中的复杂情况现实远比例子复杂。你可能会遇到JSON键排序日志里body的键顺序是[address, items]但你的json.dumps()默认是乱序的。解决方案json.dumps(..., sort_keysTrue)特殊字符转义body里如果有中文日志显示为{address:\u5317\u4eac}而你的Pythonjson.dumps()默认会转义。解决方案json.dumps(..., ensure_asciiFalse)密钥解密如果key_str是aGVsbG8gd29ybGQ那就要先base64.b64decode(key_str).decode()。时间戳单位日志里是1715823456秒但有些App用毫秒1715823456000。看timestamp字段的Java类型是Long还是Integer。5.4 步骤四封装为可复用的SDK当算法验证无误后下一步就是工程化。我通常会把它封装成一个独立的Python包apksignerpip install apksigner其核心接口非常简洁from apksigner import ApiSigner signer ApiSigner( app_packagecom.social.app, device_id861234567890123, # 密钥配置可从配置文件或环境变量读取 ) sign signer.generate( timestamp1715823456, noncea1b2c3d4, body{address: beijing, items: [{id: 1, count: 2}]} ) print(sign) # e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855这个SDK的价值在于它把逆向分析的成果转化成了可测试、可维护、可集成到CI/CD流水线里的生产力工具。你不再需要每次抓包都手动分析而是用一行代码就能生成任意请求的合法签名。我在实际项目中用这套方法为三个大型App建立了自动化测试签名模块将接口测试的准备时间从平均2小时/接口缩短到了5分钟/接口。这背后是无数次在Frida日志里逐行比对、在Python里反复调试的积累。技术没有捷径但路径可以更清晰。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2640323.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…