本文章内容仅用于逆向学习,请勿用于黑产行为,如有侵权,请联系本人删除,未经本人允许,不可转载。
抓包分析 QUIC降级:
QUIC是基于 UDP 协议 的
主流抓包工具,本质上是建立一个 HTTP/HTTPS 代理服务器 。它们主要工作在 TCP 协议 之上
因此需要让它放弃使用UDP而使用TCP,因此要先降级处理
正常抓包只有okhttpd走代理,要先降级抓包,jadx搜索cronetConfig找到对应设置配置的地方,打开libageon.so 并搜索字符串enable_quic可以找到处理配置的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 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放进去,例如下面这段代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ); });
1 2 [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获取的,拿一个栗子
1 [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可以初步恢复成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public String c (String str, boolean z, String str2) throws KSException { if (PatchProxy.isSupport(e.class)) { return PatchProxy.accessDispatch(...); } try { n.a builder = n.b(); builder.a(b.i().j().a()) .a((Map) null ) .b(0 ); builder.a(z); builder.e(str2); builder.b("" ); byte [] strBytes = str.getBytes(Charset.forName("UTF-8" )); builder.a(strBytes); n requestObj = builder.a(); e dispatch = e.a(this .context); d pluginInterface = dispatch.g(); if (pluginInterface == null ) { throw new KSException ("SecurityGuardManager... return null" ); } boolean success = pluginInterface.f(requestObj, "0335" ); if (success && requestObj.g() != null && requestObj.g().a().length > 0 ) { } else { success = pluginInterface.f(requestObj, "0335" ); } if (success && requestObj.g() != null ) { byte [] resultBytes = requestObj.g().a(); if (resultBytes.length > 0 ) { String resultStr = new String (resultBytes); return resultStr; } } throw new KSException ("signPlus return enull final" ); } catch (Exception e) { throw new KSException (...); } }
1 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方法,
1 2 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
最后走到了这个函数
1 public static native Object doCommandNative (int i4, Object[] objArr) ;
是一个so层实现的函数
1 2 [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函数获取
1 2 3 4 5 6 7 8 9 10 [+] 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
1 2 3 4 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
1 2 3 protected long currentTimeMillis () { return 1960949932368L ; }
这样每次运行的结果都是c5d4a4xxxxxxxxx77e3909c9284
unidbg配合静态分析 目标函数存在大量ollvm混淆的代码,D-810貌似有点bug,所以我写了一个配合unidbg模拟执行然后nop掉一些无用逻辑的IDA插件
https://github.com/s1nec-1o/TraceClean
如果有用,球球star⭐
sub_11BDC函数
调用前:
1 x0=0x404d80e0 x1=0x404d3240 x2=0x30 x3=0xbffff528 x4=0xbffff540
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 >-----------------------------------------------------------------------------< [19 :44 :54 838 ]x0=RW@0x404d80e0 , md5=388 eb550700dae4d7b8c77fce6fd7868, 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 4 E 4 E 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 4 D 40 00 00 00 00 8. .@....P.M@....0050 : E0 81 4 D 40 00 00 00 00 00 82 4 D 40 00 00 00 00 ..M@......M@....0060 : D8 02 07 40 00 00 00 00 80 40 4 E 40 00 00 00 00 ...@.....@N@....^-----------------------------------------------------------------------------^ >-----------------------------------------------------------------------------< [19 :46 :25 591 ]RW@0x404e4e40 , md5=bd4xxxx4102fcf47, hex=2f 7265xxxx65637xxxxx4343634623738xxxx000000000000000xxxx33930396339323834 size: 112 0000 : 2F 72 65 73 74 2F 6 E 2F 66 65 65 64 2F 73 65 6 C /rest/n/feed/selxxx 0030 : 32 37 62 61 32 37 00 00 00 00 00 00 00 00 00 00 27b a27..........0040 : 63 35 64 34 61 34 38 37 39 64 33 34 32 63 66 63 c5d4a4879d342cfcxxx 0060 : 33 38 32 64 37 37 65 33 39 30 39 63 39 32 38 34 382 d77e3909c9284^-----------------------------------------------------------------------------^ >-----------------------------------------------------------------------------< [19 :45 :00 748 ]x1=RW@0x404d3240 , md5=27b xxxxxxf6de3, hex=63356 xxxxxx43865386637323965383736663338326437376533393039633932383430663933386334663039393561383363396266333166306336343332323538390000000000000000000000000000000034623462383431373330666131313837 size: 112 0000 : 63 35 64 34 61 34 38 37 39 64 33 34 32 63 66 63 c5d4a4879d342cfcxxxxx 0040 : 39 62 66 33 31 66 30 63 36 34 33 32 32 35 38 39 9b f31f0c643225890050 : 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 4b 4b841730fa1187^-----------------------------------------------------------------------------^ >-----------------------------------------------------------------------------< [19 :45 :11 154 ]x3=unidbg@0xbffff528 , md5=6b 63f389a162b301ff80879932d5cf04, hex=b2cbbfab951b87030ee89e8c9d6e9a5fec65610e885c1f09010000000300000000000000040000000000000000000000002007400000000000200740000000003c0000000000000000404e400000000000000000000000000000000000000000000000000000000000cf0700eec9b64f size: 112 0000 : B2 CB BF AB 95 1B 87 03 0 E E8 9 E 8 C 9 D 6 E 9 A 5F .............n._0010 : EC 65 61 0 E 88 5 C 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 : 3 C 00 00 00 00 00 00 00 00 40 4 E 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=3e1 e49c8021db65491c22c54bd370bb3, hex=010000000300000000000000040000000000000000000000002007400000000000200740000000003 c0000000000000000404e400000000000000000000000000000000000000000000000000000000000cf0700eec9b64f220000000000000000f6ffbf000000003100000000000000 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 3 C 00 00 00 00 00 00 00 . .@....<.......0030 : 00 40 4 E 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....... ^-----------------------------------------------------------------------------^
调用后:
1 2 3 4 5 6 7 8 9 10 11 >-----------------------------------------------------------------------------< [19 :47 :13 553 ]x0=RW@0x404d3300 , md5=6f c600b2f4392f46ab880d69b03eb31f, hex=6335 xxxxx6438653866xxxxx37376533393039633932383400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 size: 112 0000 : 63 35 64 34 61 34 38 37 39 64 33 34 32 63 66 63 c5d4a4879d342cfcxxxxx 0020 : 33 38 32 64 37 37 65 33 39 30 39 63 39 32 38 34 382 d77e3909c92840030 : 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检测一下这个地址附近的值
1 emulator.traceWrite(0x404e4e80 ,0x404e4e80 +0x30 );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [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可以找到调用逻辑
1 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
然后就能写出下面的脚本:
1 2 3 4 5 6 7 import hmacimport hashlibsecret_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: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 __int64 *sub_2BD20 () { unsigned __int8 v0; 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参数
1 2 x0= cab68c9xxxxxxxxxb3ebe0ed8ea4aab0c1d5f4519f8d19c4948f
这个x0是第二次toHex的入参
x1是0x30就是len
x2是CRC32b_poly_Constant_57C78
那就先看x0是怎么获取的
通过sub_1E2C4函数获取
1 2 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看看
1 2 3 4 5 6 7 8 9 [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个字节的密文
简单补个环境
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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 ; module .callFunction(emulator, 0x2636C , arg1Ptr, inputPtr.share(offset), outputPtr.share(offset) ); } 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次
这个得详细分析一下
1 2 3 4 5 *((_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次,然后详细分析就确定是行移位 了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 long double __fastcall sub_25938 (_OWORD *a1) { __int64 v1; long double result; long double v3; __int64 v4; 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行移位函数,将其第一个参数随机替换一位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 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(); emulator.attach().addBreakPoint(module .base+0x2636C ); module .callFunction(emulator, 0x2636C , arg1Ptr, inputPtr, outputPtr ); 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 )); } 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轮密钥:https://github.com/SideChannelMarvels/JeanGrey/tree/master/phoenixAES
获取真正的密钥:https://github.com/SideChannelMarvels/Stark
成功获取68455xxxxxxxx5A5476
成功获取密钥
因此可以得知CRC的x0就是通过标准AES ECB 128 加密 HMAC-SHA256 结果的值
cab68c9fxxxxxxx88de61509c5dxxxxxxxxc4948fe0ed8ea4aab0c1d5f4519f8d19c4948f
结果截取96个字符就是CRC的x0了
cab68c9fxxxxxxx88de61509c5dxxxxxxxxc4948f
CRC32 1 2 3 4 5 6 .rodata:0000000000057 C78 CRC32b_poly_Constant_57C78 DCD 0x4C11DB7 .rodata:0000000000057 C78 ; DATA XREF: sub_11BDC+BC↑o .rodata:0000000000057 C78 ; sub_11BDC+D4↑o ... .rodata:0000000000057 C7C DCD 0xFFFFFFFF .rodata:0000000000057 C80 DCD 0xFFFFFFFF .rodata:0000000000057 C84 DCD 0x101
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 __int64 __fastcall sub_120C4 (unsigned __int8 *a1, __int64 a2, unsigned int *a3) { int v3; unsigned int v4; int v5; int v6; int v7; __int64 v8; unsigned __int8 v9; __int64 v10; int v11; int v12; int v14; int v15; __int64 v16; unsigned int *v17; int v18; unsigned __int8 *v19; 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 < 0x20 u ); 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 < 0x20 u ); } return v11 ^ a3[2 ]; }
根据a3的配置,可以知道这里执行的CRC32是标准的CRC32
即首先将a3的第一个4字节先进行32位的镜像反转
1 2 3 4 5 6 7 8 9 10 11 v4 = *a3; v5 = 0 ; v6 = 0 ; do { ++v5; v7 = v4 & 1 | (2 * v6); v4 >> = 1 ; v6 = v7; } while ( (unsigned __int16)v5 < 0x20 u );
这里就是将4字节铺平成32bit,然后第0bit和第31bit进行交换……
0x04C11DB7 -> 0xEDB88320
然后逐字节处理->逐位计算:
1 2 3 4 5 6 7 8 9 10 11 12 for ( ; a2; ++a1 ){ v3 ^= *a1; v8 = 8 ; do { --v8; v3 = (v3 << 31 >> 31 ) & v7 ^ ((unsigned int )v3 >> 1 ); } while ( v8 ); --a2; }
模拟二进制除法(其实就是移位和异或)
1 2 v11 = v3; return v11 ^ a3[2 ];
最后直接return v3 ^ 0xFFFFFFFF
可以直接调用python的zlib-crc32库,但是发现其代码量并不多,直接写逻辑即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 def sub_120C4_simulation (hex_string ): CONFIG_POLY = 0x04C11DB7 CONFIG_INIT = 0xFFFFFFFF CONFIG_XOR_OUT = 0xFFFFFFFF 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()} " )
1 2 输入 Hex: cab68c9f7xxxxxxxa4aab0c1d5f4519f8d19c4948f 计算结果: 0XE40xx7FA
经过调试确认结果的一致!
4:
看着就像是时间戳
调试发现是
就是上述我们填入的时间戳1960949932368L / 1000
2:
1 2 LDAXR W9, [X8] STLXR W10, W9, [X8]
LD和ST分别是加载和载入,X是独占的语义,说明在这个期间X8指向的内存只有这条汇编可以改写,如果写入期间发现有其他的线程修改了这个内存地址,那么会返回1,继续读取然后自增,如果没有就直接返回0退出这个循环
其实说白了,就是一个自增器,确保内容的唯一性
而v68就是一个原子递增后的新序列号,这样每发一次包,就会生成一个唯一的请求ID!
猜测服务端接收时会按照这个ID来进行对抗
5:
这里调试发现结果都是0xd00(QwQ这里感觉如果是固定的就很简单,估计不同的功能这里是不一样的,但是有点复杂,这里只考虑当前情况)
可以做个简单的小总结
1 2 3 4 5 6 7 8 struct SignatureBlock { int magic; int user_id; int seq_id; int crc32; int timestamp; int feature_mask; };
之前以为到这里还没结束,其实是已经结束了,因为之前说是需要48个字节是因为默认其通过编码存储,但是发现都是通过Hex存储的,之后只需要整理一下写个纯算python即可
这里就是异或,还原一下即可,没什么难度。之后就可以按照这个写出sig3的纯算python了
贴一下这部分的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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)根据逆向可知其不会对最后一位进行异或处理
1 2 3 4 5 6 7 8 9 10 11 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