本文章内容仅用于逆向学习,请勿用于黑产行为,如有侵权,请联系本人删除,未经本人允许,不可转载。
前言 这是笔者第一篇关于APP逆向的文章,文笔过于青涩QwQ,还望海涵,属于入门系列文章(大师傅们请绕道QvQ)
抓包分析 QUIC降级:
QUIC是基于 UDP 协议 的
主流抓包工具,本质上是建立一个 HTTP/HTTPS 代理服务器 。它们主要工作在 TCP 协议 之上
因此需要让它放弃使用UDP而使用TCP,因此要先降级处理
正常抓包只有okhttpd走代理,要先降级抓包,jadx搜索 cronetConfig找到对应设置配置的地方,打开 libageon.so 并搜索字符串 enable_quic可以找到处理配置的地方
clazz .c .implementation = function (a ,b ){ showStacks (); console .log ("[c]args1-> " ,a ," " ,"args2-> " ,b ); if (a == "cronetConfig" ){ var replace = "{\"enable_quic\": false, \"enable_http2\": false}" var retval1 = this .c (a ,replace ); console .log ("[replace]retval-> " ,retval1 ); return retval1 ; } var retval3 = this .c (a ,b ); console .log ("[original]retval-> " ,retval3 ); return retval3 ; } 然后就可以正常抓到带sig3的包了
使用reqable进行抓包
Java层浅析 字符串搜索sig3很容易找到传入逻辑,就不多说了(在其他例子中可能会出现没找到的情况,可以通过hook hash.put或者其他可能的java方法,很喜欢通过这些方法将header放进去,例如下面这段代码)
Java .perform (function () { var HashMap = Java .use ("java.util.HashMap" ); HashMap .put .overload ('java.lang.Object' , 'java.lang.Object' ).implementation = function (key , value ) { if (key !== null && key .toString () === "__NS_sig3" ) { showStacks (); console .log ("[+] Value (__NS_sig3): " + value ); return this .put (key , value ); } return this .put (key , value ); }; console .log ("hook win -> " ,HashMap ); }); [vd6 .r .b ]r .b is called : str =/ rest / n / feed / selectionfb4e77xxxxxxx2541 [vd6 .r .b ]r .b result = cedfaf8xxxxx59b97998f Java层入口传入 /rest/n/feed/selectionfb4e77xxxxx2541
其中 /rest/n/feed/selection是API接口, fb4e77xxxxxxx2541是sig的值
sig指纹 sig是通过传入的request得到的
sig是通过 ce6.d$d.d获取的,拿一个栗子
[ce6.d$d.d]ce6.d$d.d is called: abi=androidApiLevel=android_os=app=appver=boardPlatform=bottom_navigation=browseType=c=cdid_tag=clientRealReportData=client_key=cold=coldStart=cold_launch_time_ms=count=country_code=cs=darkMode=ddpi=deviceBit=device_abi=did=did_gt=did_tag=earphoneMode=edgeRecoBit=edgeRerankConfigVersion=egid=ftt=grant_browse_type=hotfix_ver=injectTask=isOpenAutoPlay=is_background=isp=iuid=kcv=keyconfig_state=kpf=kpn=language=max_memory=mod=nbh=net=newOc=newUserAction=newUserRefreshTimes=oDid=oc=os=page=pcursor=pv=rdid=realShowPhotoIds=recoReportContext=sbh=seid=sh=slh=socName=source=sw=sys=teenageAge=thermal=totalMemory=uQaTag=ud=userRecoBit=ver=videoModelCrowdTag=result= 扔给gemini分析一下
第一部分:设备唯一标识 (Device Identifiers)
这些参数用于唯一标识一台设备,是风控最关注的部分。
Key Value 说明 abi ******CPU 架构指令集 (Application Binary Interface)。 boardPlatform ******芯片平台代号 (这里指 Google Tensor 芯片)。 cdid_tag ******设备 ID 相关的标签,用于区分 ID 类型或状态。 ddpi ******屏幕像素密度 (Density DPI)。 deviceBit ******设备位宽标识 (可能指 32/64 位或其他特性)。 device_abi ******同 abi,设备支持的指令集。 did ******核心设备 ID (Device ID),最关键的风控标识。 did_gt ******Device ID 生成的时间戳 (Generate Time)。 did_tag ******另一个设备 ID 标签。 egid ******扩展全局 ID (Extended Global ID),长指纹字符串。 max_memory ******App 可使用的最大内存限制 (MB)。 mod ******手机型号 (Model)。 oDid ******原始设备 ID (Original Device ID),可能指未重置前的 ID。 rdid ******随机设备 ID (Random Device ID)。 socName ******芯片型号名称 (System on Chip)。 sw ******屏幕宽度 (Screen Width)。 sh ******屏幕高度 (Screen Height)。 thermal ******热状态/温度信息。 totalMemory ******设备总内存 (MB)。
第二部分:系统与环境信息 (System & Env)
这些参数描述 App 运行的软件环境。
Key Value 说明 androidApiLevel ******Android SDK 版本 (33 对应 Android 13)。 android_os ******操作系统标识 (0 可能代表原生 Android)。 app ******App 类型标识 (0 通常指主 App)。 appver ******App 详细版本号 ,签名计算的关键。 channel (c) ******App 安装渠道 (VIVO 应用商店)。 country_code ******国家代码 (中国)。 cs ******可能指 Cold Start (冷启动) 的缩写,或者是某种开关状态。 darkMode ******深色模式状态。 earphoneMode ******耳机模式状态 (0: 未插入)。 hotfix_ver ******热修复版本号。 is_background ******是否在后台运行 (1: 是)。 isp ******运营商信息 (Internet Service Provider)。 keyconfig_state ******键值配置状态。 kpf ******平台标识 (Kwai Platform Form - Android Phone)。 kpn ******产品标识 (Kwai Product Name - 快手主站)。 language ******系统语言。 net ******网络状态。 newOc ******新渠道标识 (New Original Channel)。 oc ******原始渠道 (Original Channel)。 os ******操作系统名称。 sbh ******状态栏高度 (Status Bar Height)。 sys ******系统版本名称。 ver ******App 大版本号。
第三部分:业务与请求参数 (Business Logic)
这些参数随用户的具体操作(如刷新首页)而变化。
Key Value 说明 bottom_navigation ******是否显示底部导航栏。 browseType ******浏览类型 (4 可能指推荐流/发现页)。 clientRealReportData ******客户端实时上报数据容器。 client_key ******客户端密钥/标识。 cold ******是否冷启动状态。 coldStart ******明确的冷启动标记。 cold_launch_time_ms ******冷启动时间戳。 count ******请求返回的数据条数 (这里指请求 6 个视频)。 edgeRecoBit ******边缘推荐相关位标识。 edgeRerankConfigVersion ******边缘重排配置版本。 ftt ******First Time Token 或类似首次标识。 grant_browse_type ******授权浏览类型 (初始化)。 injectTask ******注入任务标识。 isOpenAutoPlay ******是否开启自动播放。 iuid ******可能是 IMEI 或其他用户唯一 ID。 kcv ******Key Config Version (配置版本)。 nbh ******Navigation Bar Height (导航栏高度)。 newUserAction ******JSON数据 :新用户的交互行为记录(点击、关注、点赞均为空)。 newUserRefreshTimes ******新用户刷新次数。 page ******当前页码。 pcursor ******分页游标 (Page Cursor),通常配合 feed 流使用。 pv ******Page View 标记。 realShowPhotoIds ******真实展示过的视频 ID 列表 (逗号分隔),用于去重。 recoReportContext ******JSON数据 :推荐上报上下文,包含 GPS 权限、刷新 ID、电池电量等详细环境信息。 seid ******Session ID (会话 ID)。 slh ******可能指搜索栏高度或其他布局高度。 source ******来源标识。 teenageAge ******青少年模式年龄设置。 uQaTag ******QA 测试标签。 ud ******User Data 或某种用户状态标识。 userRecoBit ******用户推荐位标识。 videoModelCrowdTag ******视频模型人群标签。
然后就是一个标准MD5就能获取到 649xxxxx879d(之所以跟第一次的不同是因为这不是同一个例子????)
sig3传入
第一个是监控计算签名耗时(可能会拿来做检测?)
第二个是关键计算sig3函数
最后走的是 com.kuaishou.android.security.internal.crypto.e.c方法,但是从jadx看该java层代码被严重混淆,jadx无法正确反汇编,因此直接阅读smali
通过gemini可以初步恢复成:
public String c (String str , boolean z , String str2 ) throws KSException { // 1. 热修复检查 (PatchProxy) - Smali 开头部分 if (PatchProxy .isSupport (e .class )) { return PatchProxy .accessDispatch (...); } try { // 2. 构造请求对象 (Request Builder) // 对应 Smali: Lcom/kuaishou/android/security/internal/plugin/n;->b() n .a builder = n .b (); // 设置 AppKey, Map, Int 等基础参数 builder .a (b .i ().j ().a ()) .a ((Map ) null ) .b (0 ); // 设置传入的参数 z 和 str2 builder .a (z ); // 对应 p2 builder .e (str2 ); // 对应 p3 builder .b ("" ); // 设置某个默认为空的字段 // 【关键】将输入的 str 转为 byte[] 放入请求对象 // 对应 Smali: getBytes("UTF-8") byte [] strBytes = str .getBytes (Charset .forName ("UTF-8" )); builder .a (strBytes ); // 构建最终请求对象 n n requestObj = builder .a (); // 3. 获取插件接口 (Plugin Interface) // 对应 Smali: Lcom/kuaishou/android/security/internal/plugin/bundle/d; e dispatch = e .a (this .context ); d pluginInterface = dispatch .g (); if (pluginInterface == null ) { throw new KSException ("SecurityGuardManager... return null" ); } // 4. 【核心调用】 调用接口方法 f,传入命令字 "0335" // 对应 Smali: const-string v0, "0335" // invoke-interface {p3, p1, v0}, ...->f(L.../n;Ljava/lang/String;)Z boolean success = pluginInterface .f (requestObj , "0335" ); // 5. 第一次尝试获取结果 if (success && requestObj .g () != null && requestObj .g ().a ().length > 0 ) { // 成功,跳转到返回逻辑 } else { // 失败,记录日志 "signPlus return enull" // 6. 【重试逻辑】 再次调用一次 f success = pluginInterface .f (requestObj , "0335" ); } // 7. 处理最终结果 if (success && requestObj .g () != null ) { byte [] resultBytes = requestObj .g ().a (); if (resultBytes .length > 0 ) { // 将结果字节数组转回 String 返回 String resultStr = new String (resultBytes ); return resultStr ; } } // 失败抛出异常 throw new KSException ("signPlus return enull final" ); } catch (Exception e ) { // 异常上报逻辑... throw new KSException (...); } } invoke-interface {p3, p1, v0}, Lcom/kuaishou/android/security/internal/plugin/bundle/d;->f(Lcom/kuaishou/android/security/internal/plugin/n;Ljava/lang/String;)Z 经过分析调用的是 com.kuaishou.android.security.internal.crypto.j.f方法,
j .f is called : nVar = SecurityGuardParamContext {paramMap = null , appKey = d7b7d042 - d4f2 - 4012 - be60 - d97ff2429c17 , requestType = 0 , reserved1 = null , reserved2 = null , input = [47 , 114 , 101 , 115 , xxxxQwQxxxx ,99 , 101 , 52 , 101 , 99 , 100 , 49 , 102 , 97 , 100 , 48 , 51 , 99 ], output = com .kuaishou .android .security .internal .plugin .n$b @fd4eede , errorCode = 0 , privateKey = [], sdkId = , isInnerInvoke = false , did = }, str = 0335 j .f result = true
最后走到了这个函数
public static native Object doCommandNative(int i4, Object[] objArr); 是一个so层实现的函数
[com.kuaishou.android.security.internal.plugin.k.a]k.a is called: i4=10418, objArr=[Ljava.lang.String;@8d18d16,d7b7d042-d4f2-4012-be60-d97ff2429c17,-1,false,com.yxcorp.gifshow.App@9da18de,,false,
[com.kuaishou.android.security.internal.plugin.k.a]k.a result=5c4d3d1xxxxxe81366709050b1d 快手采用了非常经典的“通用分发”模式。它没有为每个功能写一个 JNI 函数
而是写了一个通用的 doCommandNative,通过整数 ID ( i4) 来区分要执行什么功能
Ljava.lang.String;@8d18d16这里存储了处理过的字符串MD5(sig)
d7b7d042-d4f2-4012-be60-d97ff2429c17是快手AppKey
com.yxcorp.gifshow.App@9da18de为Native层留一个调用Java方法的对象
Native层浅析 入口 Hook registerNative函数获取
[+] Captured RegisterNatives for: com.kuaishou.android.security.internal.dispatch.JNICLibrary
Method count: 5
-----------------------------------------
[TARGET FOUND] Method: doCommandNative
Signature: (I[Ljava/lang/Object;)Ljava/lang/Object;
Absolute Address: 0x75f4981680
Module: libkwsgmain.so
Base Address: 0x75f4940000
!!! OFFSET: 0x41680 !!!
----------------------------------------- 在 libkwsgmain.so
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
JUMPOUT(0x4631C);
} 其实就是一个goto指令,直接patch BR X9-> B 0x4631C,IDA就会自己生成一个 sub_4631C函数
该软件的花指令基本都是这个,非常简单,只需要patch一个指令即可,可以写个脚本批量匹配特征值进行patch,但是我懒,所以都是手动patch
字符串没找到但是可以通过Hook的结果直接得知对应函数是 sub_41680
先unidbg搭架子,把函数运行起来先
unidbg部分在网上就很多了,基本拿来都能直接用,这里就不贴了
64位想正常运行read需要将
unidbg-android/src/main/java/com/github/unidbg/virtualmodule/android/AndroidModule.java
里的 throw new BackendException();改为 return read(emulator, vm);才可以正常运行
发现多次运行相同的参数结果却不相同,猜测在运行过程中调用了获取时间戳的函数,增加了随机性,在分析之前需要先固定随机
修改gettimeofday64
protected long currentTimeMillis() {
return 1960949932368L;//System.currentTimeMillis();
} 这样每次运行的结果都是 c5d4a4xxxxxxxxx77e3909c9284
unidbg配合静态分析 目标函数存在大量ollvm混淆的代码,D-810貌似有点bug,所以我写了一个配合unidbg模拟执行然后nop掉一些无用逻辑的IDA插件
32dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6K6x3h3&6W2j5#2)9J5k6o6q4G2i4K6u0r3g2s2u0S2j5$3g2o6L8r3g2S2L8R3`.`.
如果有用,球球star⭐
sub_11BDC函数
调用前:
x0=0x404d80e0 x1=0x404d3240 x2=0x30 x3=0xbffff528 x4=0xbffff540 >-----------------------------------------------------------------------------<
[19:44:54 838]x0=RW@0x404d80e0, md5=388eb550700dae4d7b8c77fce6fd7868, hex=d8fd06400000000050f6ffbf0000000090f5ffbf00000000670000000000000041000000000000003600000000000000404e4e40000000000000000000000000380307400000000050904d4000000000e0814d400000000000824d4000000000d80207400000000080404e4000000000
size: 112
0000: D8 FD 06 40 00 00 00 00 50 F6 FF BF 00 00 00 00 ...@....P.......
0010: 90 F5 FF BF 00 00 00 00 67 00 00 00 00 00 00 00 ........g.......
0020: 41 00 00 00 00 00 00 00 36 00 00 00 00 00 00 00 A.......6.......
0030: 40 4E 4E 40 00 00 00 00 00 00 00 00 00 00 00 00 @NN@............
0040: 38 03 07 40 00 00 00 00 50 90 4D 40 00 00 00 00 8..@....P.M@....
0050: E0 81 4D 40 00 00 00 00 00 82 4D 40 00 00 00 00 ..M@......M@....
0060: D8 02 07 40 00 00 00 00 80 40 4E 40 00 00 00 00 ...@.....@N@....
^-----------------------------------------------------------------------------^
>-----------------------------------------------------------------------------<
[19:46:25 591]RW@0x404e4e40, md5=bd4xxxx4102fcf47, hex=2f7265xxxx65637xxxxx4343634623738xxxx000000000000000xxxx33930396339323834
size: 112
0000: 2F 72 65 73 74 2F 6E 2F 66 65 65 64 2F 73 65 6C /rest/n/feed/sel
xxx
0030: 32 37 62 61 32 37 00 00 00 00 00 00 00 00 00 00 27ba27..........
0040: 63 35 64 34 61 34 38 37 39 64 33 34 32 63 66 63 c5d4a4879d342cfc
xxx
0060: 33 38 32 64 37 37 65 33 39 30 39 63 39 32 38 34 382d77e3909c9284
^-----------------------------------------------------------------------------^
>-----------------------------------------------------------------------------<
[19:45:00 748]x1=RW@0x404d3240, md5=27bxxxxxxf6de3, hex=63356xxxxxx43865386637323965383736663338326437376533393039633932383430663933386334663039393561383363396266333166306336343332323538390000000000000000000000000000000034623462383431373330666131313837
size: 112
0000: 63 35 64 34 61 34 38 37 39 64 33 34 32 63 66 63 c5d4a4879d342cfc
xxxxx
0040: 39 62 66 33 31 66 30 63 36 34 33 32 32 35 38 39 9bf31f0c64322589
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 34 62 34 62 38 34 31 37 33 30 66 61 31 31 38 37 4b4b841730fa1187
^-----------------------------------------------------------------------------^
>-----------------------------------------------------------------------------<
[19:45:11 154]x3=unidbg@0xbffff528, md5=6b63f389a162b301ff80879932d5cf04, hex=b2cbbfab951b87030ee89e8c9d6e9a5fec65610e885c1f09010000000300000000000000040000000000000000000000002007400000000000200740000000003c0000000000000000404e400000000000000000000000000000000000000000000000000000000000cf0700eec9b64f
size: 112
0000: B2 CB BF AB 95 1B 87 03 0E E8 9E 8C 9D 6E 9A 5F .............n._
0010: EC 65 61 0E 88 5C 1F 09 01 00 00 00 03 00 00 00 .ea..\..........
0020: 00 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 20 07 40 00 00 00 00 00 20 07 40 00 00 00 00 . .@..... .@....
0040: 3C 00 00 00 00 00 00 00 00 40 4E 40 00 00 00 00 <........@N@....
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 CF 07 00 EE C9 B6 4F ...............O
^-----------------------------------------------------------------------------^
>-----------------------------------------------------------------------------<
[19:45:13 548]x4=unidbg@0xbffff540, md5=3e1e49c8021db65491c22c54bd370bb3, hex=010000000300000000000000040000000000000000000000002007400000000000200740000000003c0000000000000000404e400000000000000000000000000000000000000000000000000000000000cf0700eec9b64f220000000000000000f6ffbf000000003100000000000000
size: 112
0000: 01 00 00 00 03 00 00 00 00 00 00 00 04 00 00 00 ................
0010: 00 00 00 00 00 00 00 00 00 20 07 40 00 00 00 00 ......... .@....
0020: 00 20 07 40 00 00 00 00 3C 00 00 00 00 00 00 00 . .@....<.......
0030: 00 40 4E 40 00 00 00 00 00 00 00 00 00 00 00 00 .@N@............
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 CF 07 00 EE C9 B6 4F 22 00 00 00 00 00 00 00 .......O".......
0060: 00 F6 FF BF 00 00 00 00 31 00 00 00 00 00 00 00 ........1.......
^-----------------------------------------------------------------------------^ 调用后:
>-----------------------------------------------------------------------------<
[19:47:13 553]x0=RW@0x404d3300, md5=6fc600b2f4392f46ab880d69b03eb31f, hex=6335xxxxx6438653866xxxxx37376533393039633932383400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: 63 35 64 34 61 34 38 37 39 64 33 34 32 63 66 63 c5d4a4879d342cfc
xxxxx
0020: 33 38 32 64 37 37 65 33 39 30 39 63 39 32 38 34 382d77e3909c9284
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
^-----------------------------------------------------------------------------^ 可以发现甚至在调用这个函数之前就已经存在结果了,那么肯定在之前就有赋值操作了!
会发现其实就是v100,但是v100没有任何直接赋值的操作!
第二个显然就不可能,第一个就是之前的原语!
我开始怀疑是不是我插件的BUG了?但是这么简单的插件怎么可能有问题呢,unidbg检测一下这个地址附近的值
emulator.traceWrite(0x404e4e80,0x404e4e80+0x30); [20:12:51 483] Memory WRITE at 0x404e4e80, data size = 1, data value = 0x63, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e81, data size = 1, data value = 0x35, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e82, data size = 1, data value = 0x64, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e83, data size = 1, data value = 0x34, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e84, data size = 1, data value = 0x61, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
xxxxxxx
[20:12:51 483] Memory WRITE at 0x404e4e8f, data size = 1, data value = 0x63, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e90, data size = 1, data value = 0x38, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
xxxxxx
[20:12:51 483] Memory WRITE at 0x404e4e96, data size = 1, data value = 0x38, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e97, data size = 1, data value = 0x66, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e98, data size = 1, data value = 0x37, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e99, data size = 1, data value = 0x32, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
xxxx
[20:12:51 484] Memory WRITE at 0x404e4ea5, data size = 1, data value = 0x37, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 484] Memory WRITE at 0x404e4ea6, data size = 1, data value = 0x65, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 484] Memory WRITE at 0x404e4ea7, data size = 1, data value = 0x33,
xxxx
[20:12:51 484] Memory WRITE at 0x404e4eaf, data size = 1, data value = 0x34, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 484] Memory WRITE at 0x404e4eb0, data size = 1, data value = 0x00, PC=RX@0x40013c34[libkwsgmain.so]0x13c34, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 485] Memory WRITE at 0x404e4e80, data size = 8, data value = 0x3738346134643563, PC=RX@0x401dc17c[libc.so]0x1c17c, LR=RX@0x4000d908[libkwsgmain.so]0xd908
[20:12:51 485] Memory WRITE at 0x404e4e88, data size = 8, data value = 0x6366633234336439, PC=RX@0x401dc17c[libc.so]0x1c17c,
xxxxx
[20:12:51 485] Memory WRITE at 0x404e4ea8, data size = 8, data value = 0x3438323963393039, PC=RX@0x401dc18c[libc.so]0x1c18c, LR=RX@0x4000d908[libkwsgmain.so]0xd908 根据这个trace可以找到调用逻辑
sub_A720->sub_1E5B8->sub_3D5F4->sub_13B1C 其中有几个函数的部分函数调用是不会调用的,直接可以nop
大致知道调用顺序了,那么开始分析吧,从尾巴开始往上分析
HMAC-SHA256 sub_3D5F4
这里的v31十分可疑
是通过23578的a2来赋值
这里会发现调用的是下面的两个函数
通过插件能首先发现具有SHA256特征,因此尝试SHA256,但与结果存在较大差异性,然后发现这个函数多传入了两个可疑参数,经过比对分析,发现是HMAC的性能优化的实现,为了避免每次计算签名都重复进行Key ^ 0x36和Key ^ 0x5C的运算,程序预先计算好了这两个状态保存在内存中,经过unidbg打印出对应的值就能看出来,密钥: vWqd4fRXxXxxxxxxxxxeRitxT7VwbK
然后就能写出下面的脚本:
import hmac
import hashlib
secret_key = b"vWqd4fRxxxxxxxitxT7VwbK"
data = b"/rest/n/feed/selection62ccedxxxxxxba27"
signature = hmac.new(secret_key, data, hashlib.sha256).hexdigest()
print(f"Calculated: {signature}")
assert signature == "e46ed2xxxxxxxxxx52c08" 一般来说如果看到是SHA256但输出却不是标准SHA256,有以下几种情况:
HMAC-SHA256(概率最高)
输入被加Salt或者对输入进行了预处理(前后加Salt、转hex、大小端、特殊字符)
魔改初始向量(最好判断)
魔改轮常量(修改K表,找0x428A2F98找不到就是K表被改了)
魔改逻辑/位移量(修改Sigma或Ch/Maj的位移数,很难判断,需要一行一行比对汇编逻辑)
输出后处理(trace一下,也好判断)
.......
然后看到03D5F4函数
这个函数感觉就是C++的某个库函数,涉及到流的操作,但是其实就是一个toHex函数
将类似 0xC5->0x63 0x35
通过跟踪trace发现这是较早出现结果的地方
执行过三次这个toHex函数,第一次是HMAC-SHA256的写入,第二次也是某个算法,第三次就是最后的结果了,从最后一次看
但是发现结果是24位的显然是不对的,结果应该是48位才对,应该是后面还有拼接吧
不过这24位确实是结果
0: __int64 *sub_2BD20()
{
unsigned __int8 v0; // w8
v0 = atomic_load((unsigned __int8 *)&byte_72808);
if ( (v0 & 1) == 0 && __cxa_guard_acquire((__guard *)&byte_72808) )// 线程安全,确保只执行一次初始化操作
{
qword_727A0 = (__int64)&off_702A8;
xmmword_727F8 = 0u;
xmmword_727E8 = 0u;
xmmword_727D8 = 0u;
xmmword_727C8 = 0u;
__cxa_atexit((void (*)(void *))&sub_301A4, &qword_727A0, &off_71000);
__cxa_guard_release((__guard *)&byte_72808);
}
return &qword_727A0;
} 这个函数是对全局this指针的初始化,并返回this指针,经过trace发现这里返回结果[3]+8的值都是固定的22
因此0是固定的0x51412200
1: 貌似也是固定的0x1db5ae7f
3: wbAES 先dump参数
x0=
cab68c9xxxxxxxxxb3ebe0ed8ea4aab0c1d5f4519f8d19c4948f 这个x0是第二次toHex的入参
x1是 0x30就是len
x2是 CRC32b_poly_Constant_57C78
那就先看x0是怎么获取的
通过sub_1E2C4函数获取
sub_26E44(v20, *a1);
sub_26A14(v20, HMAC_SHA256_final_1_1, HMAC_SHA256_final_len, src, src_len); v20依赖全局变量,然后调用26A14
所以v24存储的就是我们在寻找的字符串,然后看到v24是malloc出来的堆内存 0x404d3240trace看看
[16:15:11 186] Memory WRITE at 0x404d3240, data size = 1, data value = 0xca, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 186] Memory WRITE at 0x404d3241, data size = 1, data value = 0xb6, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 186] Memory WRITE at 0x404d3242, data size = 1, data value = 0x8c, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 186] Memory WRITE at 0x404d3243, data size = 1, data value = 0x9f, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 186] Memory WRITE at 0x404d3244, data size = 1, data value = 0x78,
xxx
PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 188] Memory WRITE at 0x404d326e, data size = 1, data value = 0x94, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 188] Memory WRITE at 0x404d326f, data size = 1, data value = 0x8f, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c 位于2636C函数中,初步判断为白盒AES
根据调试可以知道大致流程如下:
首先在进入这个函数之前会将HMAC-SHA256的结果进行填充\x10到0x30个字节
然后每0x10个字节进去这个函数一次,输出0x10个字节的密文
简单补个环境
private void call_AES() {
String hexInput = "E46ED2xxxxxxxxB349C97C5BF21F44519D2E6452C0810101010101010101010101010101010";
byte[] inputData = hexToByteArray(hexInput);
UnidbgPointer inputPtr = emulator.getMemory().malloc(inputData.length, true).getPointer();
inputPtr.write(inputData);
UnidbgPointer outputPtr = emulator.getMemory().malloc(inputData.length, true).getPointer();
UnidbgPointer arg1Ptr = emulator.getMemory().malloc(16, true).getPointer();
System.out.println("=== 开始分块加密 (3轮 x 16字节) ===");
for (int i = 0; i < 3; i++) {
int offset = i * 16; // 每次偏移 0, 16, 32
module.callFunction(emulator, 0x2636C,
arg1Ptr, // Arg1: 固定
inputPtr.share(offset), // Arg2: 输入指针偏移
outputPtr.share(offset) // Arg3: 输出指针偏移 (结果会自动拼接)
);
}
System.out.println("Full Ciphertext (Arg3):");
Inspector.inspect(outputPtr.getByteArray(0, inputData.length), "AES Result");
}
public static byte[] hexToByteArray(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
} $$ \begin{array}{l}\text{state} \leftarrow \text{plaintext} \\\text{AddRoundKey}(\text{state}, k_0) \\\text{for } r = 1 \dots 9 \\\quad \text{SubBytes}(\text{state}) \\\quad \text{ShiftRows}(\text{state}) \\\quad \text{MixColumns}(\text{state}) \\\quad \text{AddRoundKey}(\text{state}, k_r) \\\text{SubBytes}(\text{state}) \\\text{ShiftRows}(\text{state}) \\\text{AddRoundKey}(\text{state}, k_{10}) \\\text{ciphertext} \leftarrow \text{state}\end{array} $$
判断白盒AES-128的逻辑如下:
这里有10轮的循环,符合AES-128的标准轮数
看到v3,经过简单分析,发现主循环中是4次循环,而我们输入的字节数是0x10,4*4矩阵,处理4次
这个得详细分析一下
*((_BYTE *)a1 + v10) = v33[v23] | (0x10 * v32[v20]);
*((_BYTE *)a1 + v9) = v27 | (16 * v21);
*((_BYTE *)a1 + v8) = v16 | (16 * v15);
v3 = v40 + 1;
*((_BYTE *)a1 + v7) = v24 | (16 * v31); 最后结果是通过两个半字节也就是一个hex拼接成一个字节
其中v33和v32是int8类型,但是前面的逻辑,它的值只会在0x00-0x0F,v27等虽然是int64但在此之前做过LOBYTE处理了,LOBYTE就是BYTE的低位,也就是半个字节
然后看到这个5AB70的表:
显然这是一个异或表,可以将a ^ b 转换成 第a行的第b列的值
可以掩盖AES的xor特征
sub_25938通过unidbg执行可以确定,一次执行AES执行了10次,然后详细分析就确定是 行移位 了
long double __fastcall sub_25938(_OWORD *a1)
{
__int64 v1; // x9
long double result; // q0
long double v3; // [xsp+8h] [xbp-18h]
__int64 v4; // [xsp+18h] [xbp-8h]
v1 = 0;
v4 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
do
{
*((_BYTE *)&v3 + v1) = *((_BYTE *)a1 + dword_5AB30[v1]);
++v1;
}
while ( v1 != 16 );
result = v3;
*(long double *)a1 = v3;
return result;
} 那么上述的查表操作就是字节代换+列混合+轮密钥加了
那么直接通过DFA Hook行移位函数,将其第一个参数随机替换一位
private void call_AES() {
String hexInput = "E46ED2A1xxxxxxxxx1F44519D2E6452C0810101010101010101010101010101010";
byte[] inputData = hexToByteArray(hexInput);
UnidbgPointer inputPtr = emulator.getMemory().malloc(inputData.length, true).getPointer();
inputPtr.write(inputData);
UnidbgPointer outputPtr = emulator.getMemory().malloc(inputData.length, true).getPointer();
UnidbgPointer arg1Ptr = emulator.getMemory().malloc(16, true).getPointer();
// System.out.println("=== 开始分块加密 (3轮 x 16字节) ===");
emulator.attach().addBreakPoint(module.base+0x2636C);
module.callFunction(emulator, 0x2636C,
arg1Ptr, // Arg1: 固定
inputPtr, // Arg2: 输入指针偏移
outputPtr // Arg3: 输出指针偏移
);
String res = bytesToHex(outputPtr.getByteArray(0,0x10));
System.out.println(res);
}
public void callDfa(){
Debugger debugger = emulator.attach();
debugger.addBreakPoint(module.base+0x25938,new BreakPointCallback() {
int num = 1;
@Override
public boolean onHit(Emulator<?> emulator, long address) {
UnidbgPointer pointer;
RegisterContext context = emulator.getContext();
pointer = context.getPointerArg(0);
if(num%9==0){
pointer.setByte(randint(0,15),(byte) randint(0,0xff));
}
// System.out.println("callDfa num=" + num);
num+=1;
return true;
}
});
}
public static int randint(int min,int max){
Random rand = new Random();
return rand.nextInt((max-min)+1)+min;
}
public static byte[] hexToByteArray(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
int unsignedInt = b & 0xff;
String hex = Integer.toHexString(unsignedInt);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
} 然后在main中200次循环获取
剩余的就是使用工具获取密钥了
获取第10轮密钥: a7bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6e0K9h3c8W2b7$3S2S2L8X3&6W2L8p5#2S2M7Y4k6W2L8s2y4Q4x3V1k6v1k6h3q4F1c8%4u0W2P5g2)9J5c8Y4c8J5k6h3g2Q4x3V1k6E0j5i4y4@1k6i4u0Q4x3V1k6H3K9r3!0W2L8X3W2^5b7f1g2e0
获取真正的密钥: e90K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6e0K9h3c8W2b7$3S2S2L8X3&6W2L8p5#2S2M7Y4k6W2L8s2y4Q4x3V1k6e0N6r3q4J5K9H3`.`.
成功获取 68455xxxxxxxx5A5476
成功获取密钥
因此可以得知CRC的x0就是通过标准AES ECB 128 加密 HMAC-SHA256 结果的值
cab68c9fxxxxxxx88de61509c5dxxxxxxxxc4948fe0ed8ea4aab0c1d5f4519f8d19c4948f
结果截取96个字符就是CRC的x0了
cab68c9fxxxxxxx88de61509c5dxxxxxxxxc4948f
CRC32 .rodata:0000000000057C78 CRC32b_poly_Constant_57C78 DCD 0x4C11DB7
.rodata:0000000000057C78 ; DATA XREF: sub_11BDC+BC↑o
.rodata:0000000000057C78 ; sub_11BDC+D4↑o ...
.rodata:0000000000057C7C DCD 0xFFFFFFFF
.rodata:0000000000057C80 DCD 0xFFFFFFFF
.rodata:0000000000057C84 DCD 0x101 __int64 __fastcall sub_120C4(unsigned __int8 *a1, __int64 a2, unsigned int *a3)
{
int v3; // w9
unsigned int v4; // w12
int v5; // w10
int v6; // w11
int v7; // w8
__int64 v8; // x10
unsigned __int8 v9; // w8
__int64 v10; // x11
int v11; // w11
int v12; // w10
int v14; // w0
int v15; // w8
__int64 v16; // [xsp+0h] [xbp-30h]
unsigned int *v17; // [xsp+8h] [xbp-28h]
int v18; // [xsp+14h] [xbp-1Ch]
unsigned __int8 *v19; // [xsp+18h] [xbp-18h]
v3 = a3[1];
if ( *((_BYTE *)a3 + 12) )
{
v4 = *a3;
v5 = 0;
v6 = 0;
do
{
++v5;
v7 = v4 & 1 | (2 * v6);
v4 >>= 1;
v6 = v7;
}
while ( (unsigned __int16)v5 < 0x20u );
for ( ; a2; ++a1 )
{
v3 ^= *a1;
v8 = 8;
do
{
--v8;
v3 = (v3 << 31 >> 31) & v7 ^ ((unsigned int)v3 >> 1);
}
while ( v8 );
--a2;
}
}
else
{
v9 = atomic_load((unsigned __int8 *)&byte_72638);
if ( (v9 & 1) == 0 )
{
v19 = a1;
v16 = a2;
v17 = a3;
v18 = a3[1];
v14 = __cxa_guard_acquire((__guard *)&byte_72638);
v3 = v18;
a2 = v16;
a3 = v17;
v15 = v14;
a1 = v19;
if ( v15 )
{
dword_72634 = 24;
__cxa_guard_release((__guard *)&byte_72638);
v3 = v18;
a1 = v19;
a2 = v16;
a3 = v17;
}
}
for ( ; a2; ++a1 )
{
v3 ^= *a1 << dword_72634;
v10 = 8;
do
{
--v10;
v3 = *a3 & (v3 >> 31) ^ (2 * v3);
}
while ( v10 );
--a2;
}
}
if ( *((unsigned __int8 *)a3 + 12) == *((unsigned __int8 *)a3 + 13) )
{
v11 = v3;
}
else
{
v12 = 0;
v11 = 0;
do
{
++v12;
v11 = v3 & 1 | (2 * v11);
v3 = (unsigned int)v3 >> 1;
}
while ( (unsigned __int16)v12 < 0x20u );
}
return v11 ^ a3[2];
} 根据a3的配置,可以知道这里执行的CRC32是标准的CRC32
即首先将a3的第一个4字节先进行32位的镜像反转
v4 = *a3;
v5 = 0;
v6 = 0;
do
{
++v5;
v7 = v4 & 1 | (2 * v6);
v4 >> = 1;
v6 = v7;
}
while ( (unsigned __int16)v5 < 0x20u ); 这里就是将4字节铺平成32bit,然后第0bit和第31bit进行交换......
0x04C11DB7 -> 0xEDB88320
然后逐字节处理->逐位计算:
for ( ; a2; ++a1 )
{
v3 ^= *a1;
v8 = 8;
do
{
--v8;
v3 = (v3 << 31 >> 31) & v7 ^ ((unsigned int)v3 >> 1);
}
while ( v8 );
--a2;
} 模拟二进制除法(其实就是移位和异或)
v11 = v3;
return v11 ^ a3[2]; 最后直接return v3 ^ 0xFFFFFFFF
可以直接调用python的zlib-crc32库,但是发现其代码量并不多,直接写逻辑即可
def sub_120C4_simulation(hex_string):
CONFIG_POLY = 0x04C11DB7 # *a3
CONFIG_INIT = 0xFFFFFFFF # a3[1]
CONFIG_XOR_OUT = 0xFFFFFFFF # a3[2]
FLAG_REF_IN = True
FLAG_REF_OUT = True
try:
data = bytes.fromhex(hex_string)
except ValueError:
return
v3 = CONFIG_INIT
if FLAG_REF_IN:
v4 = CONFIG_POLY
v5 = 0
v6 = 0
for _ in range(32):
v5 += 1
v7 = (v4 & 1) | ((v6 << 1) & 0xFFFFFFFF)
v4 >> = 1
v6 = v7
poly_reversed = v6
for byte in data:
v3 ^ = byte
v8 = 8
while v8 > 0:
v8 - = 1
if (v3 & 1) == 1:
v3 = (v3 >> 1) ^ poly_reversed
else:
v3 = (v3 >> 1)
else:
pass
if FLAG_REF_IN == FLAG_REF_OUT:
v11 = v3
else:
v11 = v3
result = v11 ^ CONFIG_XOR_OUT
return result & 0xFFFFFFFF
input_hex_std = "cab68c9fxxxxxxxxd8ea4aab0c1d5f4519f8d19c4948f"
crc_val_std = sub_120C4_simulation(input_hex_std)
print(f"输入 Hex: {input_hex_std}")
print(f"计算结果: {hex(crc_val_std).upper()}") 输入 Hex: cab68c9f7xxxxxxxa4aab0c1d5f4519f8d19c4948f
计算结果: 0XE40xx7FA 经过调试确认结果的一致!
4:
看着就像是时间戳
调试发现是
就是上述我们填入的时间戳 1960949932368L / 1000
2:
LDAXR W9, [X8]
STLXR W10, W9, [X8] LD和ST分别是加载和载入,X是独占的语义,说明在这个期间X8指向的内存只有这条汇编可以改写,如果写入期间发现有其他的线程修改了这个内存地址,那么会返回1,继续读取然后自增,如果没有就直接返回0退出这个循环
其实说白了,就是一个自增器,确保内容的唯一性
而v68就是一个原子递增后的新序列号,这样每发一次包,就会生成一个唯一的请求ID!
猜测服务端接收时会按照这个ID来进行对抗
5:
这里调试发现结果都是0xd00(QwQ这里感觉如果是固定的就很简单,估计不同的功能这里是不一样的,但是有点复杂,这里只考虑当前情况)
可以做个简单的小总结
struct SignatureBlock {
int magic; // [0] 0x4151 (ASCII 'AQ') - 头部魔数 0x41512200
int user_id; // [1] 用户ID 或 盐值 (从 qword_72998 获取) 0x1dbxxe7f
int seq_id; // [2] 序列号 (原子自增 v68)
int crc32; // [3] 数据的校验和 (CRC32_120C4)
int timestamp; // [4] 当前时间戳 (gettimeofday)
int feature_mask;// [5] <--- 这里就是 v72 (特性掩码) 0xd00
};
之前以为到这里还没结束,其实是已经结束了,因为之前说是需要48个字节是因为默认其通过编码存储,但是发现都是通过Hex存储的,之后只需要整理一下写个纯算python即可
这里就是异或,还原一下即可,没什么难度。之后就可以按照这个写出sig3的纯算python了
贴一下这部分的代码:
def simulate_obfuscation(packed_data_bytes):
final_result = bytearray(packed_data_bytes)
v71 = 0
for i in range(23):
v73 = final_result[i]
v71 += v73
if v71 > 0xFF:
v71 = -v71
v72 = 0xd00
v71_32bit = v71 & 0xFFFFFFFF
shifted_v71 = (v71_32bit << 24) & 0xFFFFFFFF
new_int_val = v72 | shifted_v71
struct.pack_into('<I', final_result, 20, new_int_val)
v71_low_byte = v71 & 0xFF
for v74 in range(23):
xor_key = v71_low_byte ^ (v74 & 0xFF)
final_result[v74] ^= xor_key
return final_result 也不会特别困难,其中v72是之前的0xd00,有个可能会出现的问题就是range(23)而不是range(24)根据逆向可知其不会对最后一位进行异或处理
hmac = HMAC_SHA256(input_val)
aes_result = aes_ecb_encrypt(hmac, aes_key)
crc32_result = sub_120C4_simulation(aes_result)
packed_data = struct.pack('<6I',
magic,
user_id,
seq_id,
crc32_result,
time,
feature_mask)
result1 = simulate_obfuscation(packed_data) 总结 这个算法总的来说就是 HMAC-SHA256 -> AES-128 -> CRC32
总体难度不高QwQ
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 3天前
被s1nec-1o编辑
,原因: