1.分析目标 要分析的字段:shield。
2.抓包 这个字段在很多请求中有出现,比如登录验证码。
(ps:其实一般先抓包,然后确定分析目标,只不过在网上能找到分析shield的博客,不用孤军奋战!第一次分析大厂app,找个有资料分析的,hhhh,当然~虽然有其它人的博客,但我还是先自己分析,有不会的再看博客~)
3.基本分析 通过frida-ps获得包名。
从Manifest文件中获取App类名。
看了一下,似乎没有加壳,因为什么内容都看得见,比如IndexActivityV2。
先直接在jadx中搜索,观察能否找到协议字段shield。
并没有找到,但感觉有一些类名、字段名比较可疑。——先暂时放着,如果其它方法用不了再来查看这些类与字段。
再尝试使用frida去hook hashMap,一般开发的时候,会用hashMap存储通信协议字段。
打印的调用链有点长。
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 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 [shield] shield [value] XYAAQAAgAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG434Je+FUTaRHxIzkyONuSp3/qeYJz8Mt3Jx+2fc6EAwYGGHebLP31H5m0rFOBPBvvrx/G4l575l4LPk3 java.lang.Throwable at java.util.HashMap.put(Native Method) at qd9.h1.intercept(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at wd8.a.intercept(SourceFile:5) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at wd8.m.intercept(SourceFile:6) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at wd8.d.intercept(SourceFile:7) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at wd8.h.intercept(SourceFile:4) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at sf3.b.b(SourceFile:1) at sf3.b.intercept(SourceFile:25) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at wd8.r.b(SourceFile:1) at wd8.r.intercept(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at com.xingin.shield.http.XhsHttpInterceptor.intercept(Native Method) at com.xingin.shield.http.XhsHttpInterceptor.intercept(SourceFile:8) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at ae8.b.c(SourceFile:2) at ae8.a.intercept(SourceFile:3) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at wd8.l.intercept(SourceFile:13) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at wd8.s.intercept(SourceFile:13) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at wd8.u.intercept(SourceFile:6) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at rd9.c.intercept(SourceFile:15) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at ne7.c.intercept(SourceFile:11) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at wd8.c.intercept(SourceFile:2) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at qe7.b.intercept(SourceFile:3) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at ve9.d.intercept(SourceFile:8) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at yy8.m.intercept(SourceFile:26) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at on1.b.intercept(SourceFile:2) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at on1.a.intercept(SourceFile:2) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at jf9.c.intercept(SourceFile:1) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at qd9.k.intercept(SourceFile:3) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at yd8.a.intercept(SourceFile:1) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at qd9.p1.intercept(SourceFile:1) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at nj8.j.intercept(SourceFile:5) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at nj8.g.intercept(SourceFile:5) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at hw5.b.intercept(SourceFile:2) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at rd9.e.intercept(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at rd9.b.intercept(SourceFile:12) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at rd9.d.intercept(SourceFile:26) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at yy8.b.intercept(SourceFile:2) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at wd8.o.intercept(SourceFile:33) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at ve9.e$a.intercept(SourceFile:4) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at jf9.l.intercept(SourceFile:8) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at wd8.w.intercept(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at okhttp3.RealCall.getResponseWithInterceptorChain(SourceFile:13) at okhttp3.RealCall.execute(SourceFile:8) at retrofit2.OkHttpCall.execute(SourceFile:8) at wc8.b.e(SourceFile:14) at io.reactivex.Observable.subscribe(SourceFile:14) at wc8.a.e(Unknown Source:23) at io.reactivex.Observable.subscribe(SourceFile:14) at ms9.n0.e(Unknown Source:17) at io.reactivex.Observable.subscribe(SourceFile:14) at ms9.l3$b.run(Unknown Source:6) at kd8.a.run(SourceFile:3) at ud7.a.run(SourceFile:2) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at uh8.h.F(Unknown Source:12) at uh8.h.run(SourceFile:9) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) at java.lang.Thread.run(Thread.java:920)
除此之外,进程还在执行一段时间后终止了。
我决定先解决反调。
4.反调 先用常规办法,看是否能解决。
先hook库的加载,观察哪个库加载后会导致进程退出。
下面这个结果是通过hook System.loadLibrary的结果。
感觉不太对,就打印了一条hook记录。
换一个方式hook,这回hook dlopen和android_dlopen_ext。
在加载了libredpreload.so、libmsaoaidsec.so后,退出了。还记得bilibili和豆瓣用的也是libmsaoaidsec.so做反调。
当时的解决方式是:hook掉检测线程或者删掉这个库(如果小红薯没额外对这个库做检查的话),考虑到重新签名太麻烦了,还是hook线程吧。
将这3个线程的目标函数替换掉,等待了一段时间,发现进程不崩溃了。
然后整理一个绕过检测的hook脚本模板,方便之后写其它脚本,这里就不展示了。
5.追踪字段shield 过完反调,回来继续分析shield字段。
避开sdk的函数,先追踪qd9.h1.intercept。
在函数intercept中,找到2个调用put的位置。
这里的intercept代码可以分成2块,上面一块在拦截和修改请求 (Request),下面一块在拦截和处理响应 (Response)。
hook一下函数intercept,观察原本的请求体和修改后的请求体。
我的脚本只获得了输入参数,输出结果却打印不出来——说明hook引发了某些问题,导致调用原本的intercept的时候,陷入了死循环,导致方法stack溢出。
又或者是下面这样的报错。(搜了一下,大致原因是:有不可序列化的内容在输出结果里)
但不管怎么说,通过输入参数,我们发现shield已经在传入时就加密好了,说明qd9.h1.intercept并不是我们要找的加密点。
观察了一下前面的调用栈,发现存在shield的字段,追踪过去看看。
1 2 at com.xingin.shield.http.XhsHttpInterceptor.intercept(Native Method) at com.xingin.shield.http.XhsHttpInterceptor.intercept(SourceFile:8)
hook后发现输入参数没有shield。
而hook wd8.r.intercept,发现输入参数存在shield参数。
说明,大概率是在com.xingin.shield.http.XshHttpInterceptor.intercept添加了字段shield。
至此,锁定了加密点。
6.加密分析 猜测应该是根据this.cPtr生成shield,然后往chain上添加。
而this.cPtr的来头有点复杂,咱们不管,直接hook JNI函数intercept,查看j10的值。
多次打印,发现j10不会轻易发生改变,j10=494045072144。
hook RegisterNatives,观察动态注册的函数地址,找到intercept在libxyass.so的偏移是0x9e184。
PS:一开始脚本写错逻辑了,我还以为是静态注册的,,ԾㅂԾ,,然后写了一个遍历文件夹下所有so的脚本,获得导出表,然后筛选我们的intercept——就当作在这里提供一个找静态注册JNI函数的思路吧。
PPS:要找动态注册的函数,还有一个思路,根据JNI在Java的方法名,拿到ArtMethod,然后根据ArtMethod获得JNI函数在Native层的地址,然后判断这个地址在哪个模块,而偏移量则是Native层的地址 - 模块基址。
用IDA打开后,修改一下参数名与类型。
不知道到底在调用哪个函数,大概率是关于JNIEnv的函数。
根据计算,off_AD430 + 0x753016A0 == 0x6E13C,看样子是封装了CallObjectMethodV。
问题来了,参数传递的时候,chain为什么会是a2?a2应该是jobject类型。
而且,qword_B1740等全局变量尚未被初始化,因此,我决定dump一个so文件下来看看。
然后使用工具修复dump下来的elf文件。
打开IDA,来到0x9e184,可以发现qword_B1740等全局变量已经回填了。
然而,这些jmethodID咱肯定是看不懂的。——这里有3个思路。
将jmethodID转换成对应的函数名。(难点:先获取jmethodID,再考虑如何转换)
hook JNI函数,打印JNI函数调用序列。(难点:如何筛选sub_9E184中执行的JNI函数)
Unidbg的模拟执行可以打印调用的JNI函数序列。(难点:补环境)
网上的博客都是基于Unidbg进行分析,既然别人用Unidbg,那这里我就用frida吧。——自己分析才更具挑战性嘛。
获取JNI调用链 Ⅰ.通过jmethodID获得函数名 jmethodID在内存中实际上是指向ArtMethod结构的指针,也就是说,通过jmethodID可以获得ArtMethod在内存的位置。在libart中有一个函数叫PrettyMethod,往这个函数输入ArtMethod对象,可以打印出对应的函数名,而frida中就内置了这个函数的api。
至此,还有一个问题需要解决——PrettyMethod返回函数名存储在c++的std::string对象中,要对这个string进行合理的解析,才能获得最终的函数名。
安卓的arm64下,std::string类型一般占24字节,也就是3* pointerSize。
需要存储小的字符串时,一般会将字符串直接存在std::string的内存空间中,最多可以存24字节;而需要存储大的字符串时,会另外开辟一处堆空间,用来专门存放字符串的内容,而std::string的原24字节,会用来存储这样的结构:
一个指向堆上字符数据的指针 (char* data)。
字符串的当前长度 (size_t length)。
当前分配的内存容量 (size_t capacity)。
总结一下,我们输入一个jmethodID后,把它作为参数传给PrettyMethod后,需要对返回的string进行解析,获得函数名字符串地址,然后打印。
PS:至于如何区分小字符串存储还是大字符串存储,这里就不说咯。
下图是一个实现效果。
在IDA中添加注释。
Ⅱ.hook JNI调用 github上有大佬写过hook jni调用的frida脚本了,其原理是:hook上libart库中的函数——ArtMethod::Invoke。
ArtMethod::Invoke是ART中负责调用方法的核心函数,无论是Java方法还是JNI方法,最终都会通过Invoke执行。通过对这个函数的hook,可以获得整个进程的方法调用日志。
然而日志太杂乱了,需要筛选,我们只针对libxyass.so中的方法调用,通过反射,可以获取某个类的某个方法的jmethodID,然后反射执行;因此,我们只需要打印调用栈,只要在调用栈中,某个调用的地址在libxyass.so的映射范围内,就将这条日志记录下来。
大佬写的源代码没有过滤的代码,无过滤的、大量的console.log会导致进程卡死,无法获得我们想要的内容,于是我做了一些改动。
其中地址0x6e1b4在CallObjectMethodV_函数的映射范围内。
和我们第1个方法的日志结果一模一样。
分析shield加密点 为了获得加密函数的trace,这里有3种思路:
使用IDA的trace功能;
使用frida-stalker的实现trace;
使用unidbg实现trace。
我已经尝试过了第1、2种方式,耗时一天,无论怎么修改脚本,脚本始终跑得不稳定,最多一次trace了20w条指令,进程就崩溃了。
在这20w条指令中,没有看到shield字段的影子,那只能使用unidbg了,麻了。
Unidbg补环境 先照着其它项目的模板,搭建基础环境。
1.获得ShieldLogger对象 跑起来后,遇到的第一个问题。
Unidbg的JNI实现调用AbstractJni子类中的getStaticObjectField来解析此字段,但Unidbg的原生实现并不能正确解析,需要子类进行覆写,也就是在我们创建的类中进行覆写。
找到getStaticOjectField的实现,然后模仿它的写法。
1 2 3 4 5 6 7 8 @Override public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) { switch (signature){ case "com/xingin/shield/http/ContextHolder->sLogger:Lcom/xingin/shield/http/ShieldLogger;" : return vm.resolveClass("com/xingin/shield/http/ShieldLogger" ).newObject(null ); } return super .getStaticObjectField(vm, dvmClass, signature); }
然后JNI_OnLoad顺利执行完了。
还记得这张图,在调用intercepte之前,需要先后执行initializeNative和initialize。
通过上述这2个函数,对so中的静态变量和传入的cPtr进行初始化。
因此,先要调用initializeNative和initialize。通过hook,发现initialize的参数始终是main。
先创建3个变量。
然后在构造函数中进行初始化。
然后创建函数initializeNative和initialize。
调用这2个函数,接着进行补环境。
2.模拟nativeInitializeStart执行
Unidbg 尝试调用 ShieldLogger 类中定义的原生方法 nativeInitializeStart (一个无参数的 void 方法),但该方法未在您的 AbstractJni 子类中实现或正确处理。
不做处理,直接返回。
3.模拟defaultCharset执行 AbstractJni的函数callStaticObjectMethodV未能对java.nio.charset.Charset类上的静态方法defaultCharset()正确解析,因此需要补环境,模拟defaultCharset。
在这里,我们直接调用Charset的defaultCharset,然后转换成DvmObject的类型传回去。
4.获得sDeviceID 我们需要获得设备id,然后转换成DvmObject类型传过去。
通过全局搜索,查找类com.xingin.shield.http.ContextHolder。
由于sDeviceId是一个静态变量,可以直接通过类名获取。——518dfe96-7f6f-370a-8e80-f94f8838e6de
然后覆盖AbstractJni的getStaticObjectField。
对于常用的java内置的数据结构,unidbg封装了特别的函数,比如StringObject,就不需要dvmClass.newObject来转换。
5.获得sAppId
与第4个补环境的流程一样。——sAppId = -319115519
覆写。
6.模拟nativeInitializeEnd执行
同样不做处理,直接返回。
7.模拟initializeStart()执行
8.获得SharedPreferences对象
根据报错提示,我们要返回构造好一个SharedPreferences对象。
SharedPreferences 是 Android 提供的一种轻量级的数据存储机制。它允许应用程序以键值对 (key-value pairs) 的形式持久化存储一些简单的数据类型(如布尔值、浮点数、整数、长整数和字符串)。每个 Android 应用都可以有自己的 SharedPreferences 文件。这些文件存储在应用的私有目录(shared_prefs目录)下,默认情况下其他应用无法访问。它是一个接口 (Interface) 。这意味着它定义了一套操作规范(比如如何读取数据 getString(), getInt(),如何写入数据 edit().putString().commit() 等),但具体的实现则由 Android 系统提供。
Context.getSharedPreferences(name, mode) 是获取或创建特定名称的 SharedPreferences 实例的标准途径 。你调用这个方法,传入你想要操作的配置文件的名字,然后系统会返回一个实现了 SharedPreferences 接口的对象,你可以通过这个对象来读写数据。
补环境的代码如下。
注意,这里的newObject(getSharedPreferences_arg0)并不是调用构造函数,含义如下图所示。
9.实现函数SharedPreferences->getString()
上一步中,获得了SharedPreferences对象,这里需要通过getString获得value。
通过unidbg,可以打印arg0的值,是“s”。
因此,我们去看看xhs私有目录下有没有名为s的文件。——这些文件在原apk中没有,属于服务器下发给客户端的。
android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;的逻辑是这样的:输入2个字符串arg0、v2,输出1个字符串v1;这个函数会获取键arg0对应的值,如果没有,默认返回v2,如果找到了建arg0的值,返回v1。
先通过unidbg获取访问了哪些键。
已知main是不存在,s.xml里就放了一个main_hmac,所以可以这么写。
为了方便观察,我把返回的结果也打印了。
10.模拟Base64Helper->decode()执行
需要返回解码后的字节数组。
可以调用Base64类进行解码,然后通过unidbg对Byte数组的封装函数,返回回去。
11.模拟initializedEnd()执行 一样对initializedEnd()不做处理。
12.至此,完成了对initializeNative()和initialize()的调用,接下来对intercepte进行主动调用
13.构造一个空的chain对象 添加chain。
调用get_shield()。
补环境。
15.下载okhttp3框架,模拟okhttp3/Interceptor$Chain->request()执行 遇到报错。
在pom.xml中,添加okhttp3的包,注意缩进。
1 2 3 4 5 <dependency > <groupId > com.squareup.okhttp3</groupId > <artifactId > okhttp</artifactId > <version > 4.12.0</version > </dependency >
保存,并构造request对象。
补环境。这里的request是一种标记,可以通过DvmObject.getValue()取出来request的值。
16.模拟okhttp3/Interceptor$Chain->request()执行
这里的request.url也是一种标记,可以通过DvmObject.getValue()取出来request.url()的值。
17.模拟执行okhttp3/HttpUrl->encodedPath()
需要通过getValue取出具体的值,然后调用okhttp3.HttpUrl的encodedQuery(),将返回值转换为DvmObject类型返回去,由于这里是返回字符串,有专门的封装函数StringObject。
18.模拟执行okhttp3/HttpUrl->encodedPath()
一样的原理。
19.空字符串异常
根据提示,这里的httpUrlObj_encodedQuery.encodedQuery()返回的是空字符串,即null。
方法encodedQuery是用来返回url请求?后面参数用的,而我们的url没有添加?。
20.模拟执行okhttp3/Request->body()
复杂对象都用newObject打上标记即可。
1 2 case "okhttp3/Request->body()Lokhttp3/RequestBody;" : return vm.resolveClass("okhttp3/RequestBody" ).newObject(request.body());
1 2 case "okhttp3/Request->headers()Lokhttp3/Headers;" : return vm.resolveClass("okhttp3/Headers" ).newObject(request.headers());
22.模拟构造函数okio/Buffer-><init>()
在AbstractJni.newObjectV中,实现了很多构造函数。
现在需要往里面添加okio.Buffer类的构造函数。
观察原有的构造函数实现,其实是往newObject里面存放构造函数后的初始值。
而okio/Buffer-><init>()是无参构造函数,所以代码应该这么写。
23.模拟okio/Buffer->writeString(Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/Buffer;
函数writeString的返回值是Lokio/Buffer,也就是说,我们可以取出Buffer实例,取出2个参数,然后调用writeString,最后将结果转换成DvmObject类型。
补环境。
已经熟练补环境了。
补环境。
27.模拟执行okhttp3/RequestBody->writeTo(Lokio/BufferedSink;)V
这里不能像以前一样直接返回return了,得先完成目标操作,再return。
补环境。
补环境。
30.模拟执行okio/Buffer->clone()Lokio/Buffer;
补环境。
31.模拟执行okio/Buffer->read([B)I
补环境。
补环境的代码如下。
33.模拟执行okhttp3/Request->newBuilder()Lokhttp3/Request$Builder;
补环境的代码如下。
1 2 3 4 5 6 case "okhttp3/Request$Builder->header(Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder;" : Request.Builder builderObj_header = (Request.Builder) dvmObject.getValue(); String header_arg0 = vaList.getObjectArg(0 ).getValue().toString(); String header_arg1 = (String)vaList.getObjectArg(1 ).getValue(); builderObj_header.header(header_arg0,header_arg1); return dvmObject;
34.模拟执行okhttp3/Request->newBuilder()Lokhttp3/Request$Builder;
补环境的代码如下。
1 2 3 case "okhttp3/Request->newBuilder()Lokhttp3/Request$Builder;" : Request requestObj_newBuilder = (Request) dvmObject.getValue(); return vm.resolveClass("okhttp3/Request$Builder" ).newObject(requestObj_newBuilder.newBuilder());
35.模拟执行okhttp3/Request$Builder->build()Lokhttp3/Request;
补环境的代码如下。
36.模拟执行okhttp3/Interceptor$Chain->proceed(Lokhttp3/Request;)Lokhttp3/Response;
这里不需要模拟proceed的执行,因为proceed用于发送请求,说明字段shield已经添加好了,我们不关注发包,只关注shield是如何加密的。
37.模拟状态码
直接返回200。
至此,总算是模拟完成了——37个要补充的地方,快累死了。
38.获得shield
然后打印的是null。
注意到,之前我们在模拟环境的时候,总会使用newObject创建一个新的Request对象返回去,request的值其实始终没改变,因此,我们需要知道,最后一次返回Request类型的对象是哪个函数。
来到这个代码的地方,添加一句为request赋值的语句,更新request的值。
再次查看shield,这回有结果了。
全部代码如下:
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 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 package com.xhs;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Module;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.*;import com.github.unidbg.linux.android.dvm.api.Binder;import com.github.unidbg.linux.android.dvm.api.Bundle;import com.github.unidbg.linux.android.dvm.api.ServiceManager;import com.github.unidbg.linux.android.dvm.api.Signature;import com.github.unidbg.linux.android.dvm.array.ByteArray;import com.github.unidbg.linux.android.dvm.wrapper.DvmBoolean;import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger;import com.github.unidbg.memory.Memory;import okhttp3.*;import okio.Buffer;import okio.BufferedSink;import org.apache.commons.codec.binary.Base64;import javax.crypto.Cipher;import javax.crypto.NoSuchPaddingException;import javax.crypto.spec.SecretKeySpec;import java.io.ByteArrayInputStream;import java.io.File;import java.io.IOException;import java.io.UnsupportedEncodingException;import java.nio.charset.Charset;import java.security.*;import java.security.cert.CertificateException;import java.security.cert.CertificateFactory;import java.util.List;import java.util.Locale;import java.util.Map;import java.util.UUID;public class XHS_shield extends AbstractJni { public static AndroidEmulator emulator; public static Memory memory; public static VM vm; public static Module module ; public static DvmClass XhsInterceptor = null ; public static DvmObject XhsInterceptorObject; public static long cPtr; public static DvmObject chain = null ; private String url; public Request request; public XHS_shield () { emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("XHS" ).build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver (23 )); vm = emulator.createDalvikVM(new File ("unidbg-android/src/test/resources/xhs_pack/xhs.apk" )); vm.setJni(this ); DalvikModule dm = vm.loadLibrary("xyass" , true ); dm.callJNI_OnLoad(emulator); module = dm.getModule(); XhsInterceptor = vm.resolveClass("com/xingin/shield/http/XhsHttpInterceptor" ); url = "https://edith.xiaohongshu.com/api/sns/v4/user/login/password?" ; request = new Request .Builder() .url(url) .addHeader("xy-direction" ,"88" ) .addHeader("X-B3-TraceId" ,"4b4bdb48881d080f" ) .addHeader("x-xray-traceid" ,"cb1441a76a4a081ecd469a26f3706ad0" ) .addHeader("xy-scene" ,"fs=0&point=1932" ) .addHeader("x-legacy-did" ,"f10f91f68-7db7-3a36-80c9-703abad8fee2" ) .addHeader("x-legacy-fid" ,"17442981081013d20c9158a0a9b29abbcbb1ed726f5b" ) .addHeader("x-legacy-sid" ,"session.1744300626655701629150" ) .addHeader("x-mini-gid" ,"7c0d68e8647a5438a2394bbad364c2bdf31b98794735944077bbfc55" ) .addHeader("x-mini-s1" ,"ABsAAAABKjIUDN/qnVyFrjdjCBjTNhmOGE0SjnzqstjuHWyj5wVhUjSgHTzsuqoPKuiiOhD4JesIeK1AX8w=" ) .addHeader("x-mini-sig" ,"a619346fd57b0f2494eb8e197cf8fe6bb33cd62241a67e03343700de0ab8b8ff" ) .addHeader("x-mini-mua" ,"eyJhIjoiRUNGQUFGMDEiLCJjIjo0MSwiayI6ImQzNjkxYjYzMGJhODNmODljZTM0M2Q5NDlmODU2NTcyNzY5NTg2YTA5YzhmNTJhMjgzNWNiYjhjZjYxZTAwMTciLCJwIjoiYSIsInMiOiJkZmQ0OTI4ZWZlNTA5NDg1MTg5ZmJjOGEwMGRmMmMwYyIsInQiOnsiYyI6NDgsImQiOjQsImYiOjAsInMiOjQwOTgsInQiOjE3NDY3MTQwLCJ0dCI6WzFdfSwidSI6IjAwMDAwMDAwMmMwOGQwYWI0ZDdlNjExMjM4NmU2YmEyOTljZjAzNzIiLCJ2IjoiMi44LjUifQ.r2k40ztxFkHb9YD9W-r8ktVwWcyCuRLf0MmPFBWxV-9voigXNb_Bkvc3Vf8X3qs2wwcX2CpuPWIpylpfB9dwGR_CB3GAZrG1NpfKMjoDZh3Q6VAtuROrlVc7eAi-SqcGE-Z7GoU5tDth4LC9Fg0lbTIa6w6T6WTD_7QUlGroY8huQKkU26M7Whhv7nIFZQtkleznZtw276pAfbuvqyM9zMJb6TN4vqSrPeRRBBIn-7vgTNYF5pS7sF3yeshHP9BbS5QgFACF2I5iBo-z2dSMJkPeyPltMlabkqqJcf4fAjbqECYOCZusmkHXTfFk7-bwTYTW2TzeFFOBYMXNwBWKeOXe4aRqCcqbkjQQzPnJ3EHL-rzXCnKPv7Uf63MfEVxq9LqG398kwYNbUfkRdiGeM_X3OrwBLQ54cmA9jQa25L5jNzUo_2s1dS4Heg1EsEkLtNhAD9zDYDmjRpY6lbe94i_5oWLux8USIomK6_-TbvoEm1w3A51MwcJMVtiop-klqFRHEkazDUQA-76XS1Ogptxo9JpFSSYQt-S6Wet8nvg344UHUuhmXx7bb-gPYepYR6CQOt6EuCNOmLUIDLo0GgniHf2VJNQieSDxuYUIO3EGOXiwzxxB4Lv0TeSRTdqc8U7bLOb-GPeh49my4YWFHVcWEsqRO55EawZF0MSL4aekkiC5ZMm1SpoaJ60XKKVhNfG7sYf7LmIs6kMd3tNFFjvs3CFNlFgu58falTh13_W-sGsMZS3e-Cvlh6PKHic4l36rF2NH1xa2XmbY_VJzQyPm6tlD57IX89HocmwL1emORGPUx6EkhEkgbU-ZiXKcXmEC4W3njsliIc4HY18MurQmn2szKA4Kyl0Yo05igx5DlpJedTiALaaczSuaPr9Ko5xbcrSeFzFPc682BPkwZ1z8H-Hb5ljHDi2wqdvHepBVf8f96SbPEobd52wRjKh4hgdwxuUrKiM4JD3fxlCheGkLvMByCEXL3pP3oMMk7Bjblf-a_qw8cPdjzj_XOzrlcr-gWq1y79AyoyIF3CEri04uJEvnauaNNqLYOjWUYidJiuHdfpj0hQ3o-tEv19hiA4Qx_Bn_qG8BQEpVgksCTu9iwrIaeCjCZ486-waE09M." ) .addHeader("xy-common-params" ,"fid=17442981081013d20c9158a0a9b29abbcbb1ed726f5b&gid=7c0d68e8647a5438a2394bbad364c2bdf31b98794735944077bbfc55&device_model=phone&tz=Asia%2FShanghai&channel=Vivo&versionName=8.77.0&deviceId=10f91f68-7db7-3a36-80c9-703abad8fee2&platform=android&sid=session.1744300626655701629150&identifier_flag=4&project_id=ECFAAF&x_trace_page_current=login_full_screen_pwd_page&lang=zh-Hans&app_id=ECFAAF01&uis=light&teenager=0&active_ctry=CN&cpu_name=Qualcomm+Technologies%2C+Inc+SM8150&dlang=zh&launch_id=1744436392&overseas_channel=0&mlanguage=zh_cn&folder_type=none&auto_trans=0&t=1744436385&build=8770299&holder_ctry=CN&did=2d351bbef1db838c5fcd6b67a4aa1db5" ) .addHeader("User-Agent" ,"Dalvik/2.1.0 (Linux; U; Android 13; Pixel 4 XL Build/TP1A.221005.002.B2) Resolution/1440*3040 Version/8.77.0 Build/8770299 Device/(Google;Pixel 4 XL) discover/8.77.0 NetType/WiFi" ) .addHeader("Referer" ,"https://app.xhs.cn/" ) .build(); } @Override public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) { switch (signature){ case "com/xingin/shield/http/ContextHolder->sLogger:Lcom/xingin/shield/http/ShieldLogger;" : return vm.resolveClass("com/xingin/shield/http/ShieldLogger" ).newObject(null ); case "com/xingin/shield/http/ContextHolder->sDeviceId:Ljava/lang/String;" : return new StringObject (vm, "518dfe96-7f6f-370a-8e80-f94f8838e6de" ); } return super .getStaticObjectField(vm, dvmClass, signature); } @Override public void callVoidMethodV (BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { if ("com/xingin/shield/http/ShieldLogger->nativeInitializeStart()V" .equals(signature)){ System.out.println("nativeInitializeStart() trap!" ); return ; } if ("com/xingin/shield/http/ShieldLogger->nativeInitializeEnd()V" .equals(signature)){ System.out.println("nativeInitializeEnd() trap" ); return ; } if ("com/xingin/shield/http/ShieldLogger->initializeStart()V" .equals(signature)){ System.out.println("initializeStart() trap" ); return ; } if ("com/xingin/shield/http/ShieldLogger->initializedEnd()V" .equals(signature)){ System.out.println("initializedEnd() trap" ); return ; } if ("com/xingin/shield/http/ShieldLogger->buildSourceStart()V" .equals(signature)){ System.out.println("buildSourceStart trap" ); return ; } if ("okhttp3/RequestBody->writeTo(Lokio/BufferedSink;)V" .equals(signature)){ System.out.println("writeTo trap" ); RequestBody requestBody = (RequestBody) dvmObject.getValue(); BufferedSink bufferedSink = (BufferedSink) vaList.getObjectArg(0 ).getValue(); if (requestBody == null ){ return ; } try { requestBody.writeTo(bufferedSink); } catch (IOException e) { throw new RuntimeException (e); } return ; } if ("com/xingin/shield/http/ShieldLogger->buildSourceEnd()V" .equals(signature)){ return ; } if ("com/xingin/shield/http/ShieldLogger->calculateStart()V" .equals(signature)){ return ; } if ("com/xingin/shield/http/ShieldLogger->calculateEnd()V" .equals(signature)){ return ; } super .callVoidMethodV(vm, dvmObject, signature, vaList); } @Override public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature){ case "java/nio/charset/Charset->defaultCharset()Ljava/nio/charset/Charset;" : { return dvmClass.newObject(Charset.defaultCharset()); } case "com/xingin/shield/http/Base64Helper->decode(Ljava/lang/String;)[B" :{ String getString_arg0 = vaList.getObjectArg(0 ).getValue().toString(); System.out.println("Base64Helper arg0: " + getString_arg0); return new ByteArray (vm, Base64.decodeBase64(getString_arg0)); } } return super .callStaticObjectMethodV(vm, dvmClass, signature, vaList); } @Override public int getStaticIntField (BaseVM vm, DvmClass dvmClass, String signature) { if ("com/xingin/shield/http/ContextHolder->sAppId:I" .equals(signature)){ return -319115519 ; } return super .getStaticIntField(vm, dvmClass, signature); } @Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList){ switch (signature){ case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;" :{ String getSharedPreferences_arg0 = vaList.getObjectArg(0 ).getValue().toString(); System.out.println("arg0 = " + getSharedPreferences_arg0); return vm.resolveClass("android/content/SharedPreferences" ).newObject(getSharedPreferences_arg0); } case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;" :{ if (dvmObject.getValue().toString().equals("s" )){ String getString_arg0 = vaList.getObjectArg(0 ).getValue().toString(); String getString_arg1 = vaList.getObjectArg(1 ).getValue().toString(); System.out.println("key: " + getString_arg0); System.out.println("default_value: " + getString_arg1); if (getString_arg0.equals("main_hmac" )) { System.out.println("SharedPreferences -> getString(): " + "Msm7d4eAmQVoQie97ci+UCY3fL6YKxBwT2qSuID0IOxYqWkpyRFdN6bM2LvQTlPzdQ5qkkJx/T2QMa8V/z+zkKAqh1mZevC+GYm8UJDPuZKbSI/ROWnKwlTiCvIMHEyY" ); return new StringObject (vm,"Msm7d4eAmQVoQie97ci+UCY3fL6YKxBwT2qSuID0IOxYqWkpyRFdN6bM2LvQTlPzdQ5qkkJx/T2QMa8V/z+zkKAqh1mZevC+GYm8UJDPuZKbSI/ROWnKwlTiCvIMHEyY" ); }else { System.out.println("SharedPreferences -> getString(): " + getString_arg1); return new StringObject (vm, getString_arg1); } } } case "okhttp3/Interceptor$Chain->request()Lokhttp3/Request;" : return vm.resolveClass("okhttp3/Request" ).newObject(request); case "okhttp3/Request->url()Lokhttp3/HttpUrl;" : return vm.resolveClass("okhttp3/HttpUrl" ).newObject(request.url()); case "okhttp3/HttpUrl->encodedQuery()Ljava/lang/String;" :{ HttpUrl httpUrlObj_encodedQuery = (HttpUrl) dvmObject.getValue(); return new StringObject (vm, httpUrlObj_encodedQuery.encodedQuery()); } case "okhttp3/HttpUrl->encodedPath()Ljava/lang/String;" :{ HttpUrl httpUrl = (HttpUrl) dvmObject.getValue(); return new StringObject (vm, httpUrl.encodedPath()); } case "okhttp3/Request->body()Lokhttp3/RequestBody;" : return vm.resolveClass("okhttp3/RequestBody" ).newObject(request.body()); case "okhttp3/Request->headers()Lokhttp3/Headers;" : return vm.resolveClass("okhttp3/Headers" ).newObject(request.headers()); case "okio/Buffer->writeString(Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/Buffer;" :{ Buffer buffer = (Buffer) dvmObject.getValue(); String arg0 = vaList.getObjectArg(0 ).getValue().toString(); Charset arg1 = (Charset) vaList.getObjectArg(1 ).getValue(); return vm.resolveClass("okio/Buffer" ).newObject(buffer.writeString(arg0, arg1)); } case "okhttp3/Headers->name(I)Ljava/lang/String;" :{ int arg0 = vaList.getIntArg(0 ); return new StringObject (vm, ((Headers)dvmObject.getValue()).name(arg0)); } case "okhttp3/Headers->value(I)Ljava/lang/String;" :{ int arg0 = vaList.getIntArg(0 ); return new StringObject (vm, ((Headers)dvmObject.getValue()).value(arg0)); } case "okio/Buffer->clone()Lokio/Buffer;" :{ return vm.resolveClass("okio/Buffer" ).newObject(((Buffer)dvmObject.getValue()).clone()); } case "okhttp3/Request$Builder->header(Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder;" :{ Request.Builder builderObj_header = (Request.Builder) dvmObject.getValue(); String header_arg0 = vaList.getObjectArg(0 ).getValue().toString(); String header_arg1 = (String)vaList.getObjectArg(1 ).getValue(); builderObj_header.header(header_arg0,header_arg1); return dvmObject; } case "okhttp3/Request->newBuilder()Lokhttp3/Request$Builder;" :{ Request requestObj_newBuilder = (Request) dvmObject.getValue(); return vm.resolveClass("okhttp3/Request$Builder" ).newObject(requestObj_newBuilder.newBuilder()); } case "okhttp3/Request$Builder->build()Lokhttp3/Request;" :{ Request.Builder builderObj_build= (Request.Builder) dvmObject.getValue(); Request requestObj_build = builderObj_build.build(); request = requestObj_build; return vm.resolveClass("okhttp3/Request" ).newObject(requestObj_build); } case "okhttp3/Interceptor$Chain->proceed(Lokhttp3/Request;)Lokhttp3/Response;" : return vm.resolveClass("okhttp3/Response" ).newObject(null ); } return super .callObjectMethodV(vm, dvmObject, signature, vaList); } @Override public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature){ case "okio/Buffer-><init>()V" :{ return dvmClass.newObject(new Buffer ()); } } return super .newObjectV(vm, dvmClass, signature, vaList); } @Override public int callIntMethodV (BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature){ case "okhttp3/Headers->size()I" :{ return ((Headers) dvmObject.getValue()).size(); } case "okio/Buffer->read([B)I" :{ Buffer buffer = (Buffer) dvmObject.getValue(); byte [] bytes = (byte []) vaList.getObjectArg(0 ).getValue(); return buffer.read(bytes); } case "okhttp3/Response->code()I" :{ return 200 ; } } return super .callIntMethodV(vm, dvmObject, signature, vaList); } public void initializeNative () { XhsInterceptor.callStaticJniMethod(emulator, "initializeNative()V" ); } public void initialize () { XhsInterceptorObject = XhsInterceptor.newObject(null ); cPtr = XhsInterceptorObject.callJniMethodLong(emulator, "initialize(Ljava/lang/String;)J" , "main" ); System.out.println("cPtr = " + cPtr); } public void get_shield () { chain = vm.resolveClass("okhttp3/Interceptor$Chain" ).newObject(null ); DvmObject<?> response = XhsInterceptorObject.callJniMethodObject(emulator, "intercept(Lokhttp3/Interceptor$Chain;J)Lokhttp3/Response;" , chain, cPtr); System.out.println("response = " + response); } public static void main (String[] args) { XHS_shield shield = new XHS_shield (); shield.initializeNative(); shield.initialize(); shield.get_shield(); System.out.println("shield = " + shield.request.headers().get("shield" )); } }
补环境补了好久,幸好有大佬们的博客,在补环境的过程中,我也逐渐学会了各式各样的unidbg补环境的方法,今天先歇一歇,明天开始trace指令流,分析加密点。——2025.6.2,晚上20:36。
Unidbg trace 获得日志 代码如下:
1 2 3 4 5 6 7 8 9 10 11 static void traceCode () { String traceFile = "unidbg-android/src/test/java/com/xhs/traceCode.log" ; PrintStream traceStream = null ; try { traceStream = new PrintStream (new FileOutputStream (traceFile), true ); } catch (FileNotFoundException e) { throw new RuntimeException (e); } emulator.traceCode(module .base, module .base + module .size).setRedirect(traceStream); }
然后从JNI_OnLoad开始trace的话,日志存了120w条指令;而从sub_9E184开始trace,只有1700多行,我们主要关注的是sub_9E184的逻辑。
注意到,这里把寄存器变化的值打印出来了,假如某寄存器存放着字符串的地址,这里也只是打印地址,而不会打印字符串,这样的日志不便于观察。
因此,我们需要修改traceCode的逻辑。
修改Unidbg,获得带有字符串信息的日志 注意到,日志会出现符号“=>”,于是在整个项目中搜索这个符号。然后发现,文件RegAccessPrinter.java就是我要们要修改的文件。
我在这里设定,每次读取寄存器的内容,当初字符串地址进行访问,如果读取后发现了连续3个可视字符,就当成字符串进行处理,打印整个字符串。
在Unidbg中,读取指定内存区域的字节又该怎么做呢?——我记得Unidbg动调可以访问内存地址,指令是“m”,于是在整个项目搜索“m”,然后发现是用Pointer类型进行访问,于是代码如下。
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 private void checkAndPrintString (long address, StringBuilder builder) { try { Pointer pointer = UnidbgPointer.pointer(emulator, address); if (pointer == null ) { return ; } byte [] bytes = pointer.getByteArray(0 , 256 ); if (isPrintableString(bytes)) { String str = extractString(bytes); builder.append(" (string: \"" ).append(str).append("\")" ); } } catch (Exception e) { } } private boolean isPrintableString (byte [] bytes) { if (bytes.length < 3 ) return false ; for (int i = 0 ; i < 3 ; i++) { if (bytes[i] < 32 || bytes[i] > 126 ) { return false ; } } return true ; } private String extractString (byte [] bytes) { StringBuilder sb = new StringBuilder (); for (byte b : bytes) { if (b >= 32 && b <= 126 ) { sb.append((char ) b); } else { break ; } } return sb.toString(); }
将checkAndPrintString移动到函数print内部。
同时,由于调用UnidbgPointer.Pointer需要Emulator对象,所以还得往类RegAccessPrinter添加成员变量。
既然改了构造函数,还需要在调用构造函数的地方,把Emulator对象传进去。
重新运行,程序能正常跑了。
日志中出现了我们想看到的字符串。
寻找加密点 通过带有字符串信息的日志可以知道:
shield最早出现在哪条指令;
shield的缓冲区地址在哪。
于是,有2个思路去寻找加密点:
根据shield最早出现的指令,朝着之前的指令进行溯源;
在shield的缓冲区地址设置读写断点。
怎么看都是第2种方式更简单。
找到缓冲区地址:0x1240c000。——因为是unidbg模拟的缓冲区,所以缓冲区地址不会变。
我们的shield长度是134个字节,内容为:
XYAAQAAgAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG434Je+FUTaRHxIzkyONuSp3/qeYJz8Mt3Jx+2fc6EAwYGGHebLP31H5m0rHnmtyNNSyC55+4O2fp4TDk
unidbg提供了api,可以对写操作进行监控。——traceWrite。
可以发现,偏移0x4b024处在将正确的值写回去了。
分析加密算法 先在IDA中,来到0x4b024看看。
用frida hook一下,查看memcpy复制的内容,发现与最后的shield相差了2个字节,有2个字节(XY)不见了。
根据之前traceWrite日志,可以发现0x4af74单独对0x1240c000前2个字节进行了修改。
根据观察,0x4af70对前2个字节做了赋值,而0x4b020对132个字节进行了赋值。
而且,对a1进行交叉引用,发现没有任何修改a1的地方;查看a1被传入的地方,只发现了简单的加解密,也就是说,前2个字节是固定的,根据直接的traceWrtie日志,固定为XY,因此我们分析的重点在v16。
而v16似乎涉及加密。
在Unidbg中,对v16涉及的memcpy下断点,查看源地址是什么。(已知目的地址是0x1240c002)
可以看到,源地址是0x1240c0a0,于是在0x1240c0a0下写断点。
发现,0x1240c0a0在libxyass.so偏移0x82890的地方被写入数据。
注意到,shield有点像是base64编码后的结果,而且0x82890的代码又是每轮取出v136的3个字节,然后赋给v138的4个字节,这是base64编码的特征。
在IDA中,来到汇编层,X10应该是:指向未经过base64编码的数据的地址。
在Unidbg中下个断点,然后查看这里的值。
对这里的内容进行base64编码,跟我们最终的shield后134字节一模一样,坐实了这里是base64编码。
注意到,这个数据的地址是0x12411000,对这里下一个写断点。(编码前是99字节)
注意到,99个字节的前16个字节的LR指向libxyass.so偏移0x49da0的位置,而后面83个字节来自偏移0x49db4的位置。
来到IDA中。
先后查看两个memcpy的源地址(下断点),然后对源地址下写断点。
先看前16字节的memcpy的x1在哪,由于我hook的地址是0x49D9C,有些数据不是我们想要的,我们只需要前16个字节是00 04 00 02…的源地址。
第4次进入该函数后,终于找到前16个字节的源地址是0xe4ffefd9。
再来找后83个字节的x1在哪。——找到后83个字节在0x123dc060。
0x10个字节(I) 通过IDA的伪代码分析,v15是前16个字节,它来自于a1+1或者a1+16的位置。
hook函数sub_49CE4,查看a1内容。
确定v15来自a1+1。
查看交叉引用,从哪一个过来的呢。
根据trace日志,可以发现是从0x4a2e0跳转过来的。
地址0x4a2e0属于函数sub_4a278,hook一下函数的入参,可以发现x0和x1分别指向16字节和83字节,而x2代表x1的长度。
这里继续观察 16个字节,16个字节位于栈0xe4ffefd8上。
因此对0xe4ffefd8下一个写断点,注意到0x8271C、0x8272C等地址会往里写目标字节。
来到0x8271C,位于函数sub_81F00的范围内。
下图,我对17个字节进行了分析(第0个字节是0x20,0x20并不在目标16字节内,它另有作用 )。
第0、3-4、5-8、9-12字节分析 v124用来描述v127(53个字节)的长度,所以v124是0x53,经过bswap32后变成0x53 00 00 00。
因此,这17个字节的格式是这样的:
20 ?? ?? ?? ?? 00 00 00 01 53 00 00 00 ?? ?? ?? ??
再来判断第1-4个字节是如何来的。
假设v155是0xAB CD EF GH,左移16位后,0xEF GH 00 00,或上一个2后,0xEF GH 00 02,然后swap32交换次序,0x02 00 GH EF,也就是说,有2个字节是固定的,GH EF相当于原来的v155的低2个字节做了交换。
当前的v157是这样的:
20 ?? ?? 00 02 00 00 00 01 53 00 00 00 ?? ?? ?? ??
第13-16字节分析 暂时先不分析第1-2字节,这个难度有点高,先来看最后4个字节。
对v161进行交叉引用,发现v161 = v35。
阅读下图,v161获得了sub_4A278的返回值,我们上面的图中,也出现了sub_4A278。
对sub_4A278进行分析,发现它是一个类似memcpy的函数。
不同的是,target是std::string类型,而src是char数组。
string类型是24个字节,之前提到过string类型会对短字符串做sso优化,存储短字符串和长字符串时,string采用的结构不同。
v3 & 1,即target的最低位与1做&,如果(v3&1 != 0)为true,则说明当前当前的string存储着长字符串,采用__long结构;而反之为false,则采用__short结构。
当遇到一个string类型,先判断第0位是否为1,如果是1,则采取__long结构,不用担心第0位的1会影响__cap_的值,__cap_被规定需要2字节对齐,所以第0位单纯是flag;而如果第0位是0,则采取__short结构,该结构会使用高7位表示data的大小,舍弃掉flag的1位,因为data就23个字节,最后一个字节还要存\x00,所以data的实际存储空间大小就22字节,用7位足够表示了。
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 +--------+----------------+----------+ |address | __short | __long | +--------+----------------+----------+ | 0 | __size_ / __lx | | +--------+----------------+ | | 1 | | | | 2 | | | | 3 | | __cap_ | | 4 | | | | 5 | | | | 6 | | | | 7 | | | +--------| |----------+ | 8 | | | | 9 | | | | 10 | | | | 11 | __data_ | __size_ | | 12 | | | | 13 | | | | 14 | | | | 15 | | | +--------| |----------+ | 16 | | | | 17 | | | | 18 | | | | 19 | | __data_ | | 20 | | | | 21 | | | | 22 | | | | 23 | | | +--------+----------------+----------+
注意到,在函数sub_81F00中,一共调用了4次sub_4A278,而v34 = sub_4A278是第3次调用,也就是说,v34里面累积存放了4个字符串——原本有1个,再通过sub_4A278追加了3个。
在Unidbg中模拟执行,对函数sub_4A278进行hook,可以发现:一共就调用了4次sub_81F00,所以基本可以确认,这4次调用都来自于sub_81F00。
第一次调用sub_4A278。
x0指向string类型对象,x1指向char数组,x2是char数组的长度。
第1个字节是0x21,它最低位是1,说明是__long类型;
第1个8字节是0x21,由于对齐,算作0x20,也就是说有32个字节的缓冲区;
第2个8字节说明缓冲区已经用了0x18个字节,也就是用掉了24个字节;
第3个8字节是缓冲区(堆)的地址。
查看堆里面存放的内容,正好24个字节。
待添加的字符串存在x1指向的地址中。
第二次调用sub_4A278。
x0指向string类型对象,x1指向char数组,x2是char数组的长度。
查看x0的内容。
第1个8字节是0x21,由于对齐,算作0x20,也就是说有32个字节的缓冲区。——因为之前32个字节,原本的字符串长度是24个字节,第一次调用时需要添加了7个字节,而缓冲区剩下的8个字节足够容纳,不需要重新申请空间。
第2个8字节由于添加了7个字节,由0x18 + 0x7变成了0x1F。
第3个8字节由于不用重新申请空间,所以空间地址和第一次调用时的堆地址一样,没变。
z
查看堆里面存放的内容,可以看到已经添加上8770299了。
第三次调用sub_4A278。
第一个字节是0x51,说明是__long类型。
第1个8字节,0x51视作0x50,说明新申请的缓冲区有80字节的大小。
第2个8字节,0x43表示已经使用了0x43个字节。
第3个8字节,0x12407000是新缓冲区的地址。
18dfe96-7f6f-370a-8e80-f94f8838e6de已经被复制到缓冲区里了。
sub_4A278的返回值是string对象的地址,也就是说,v161指向这4个字符串。
再也就是说,这里的v123实际上是在获得长度,而4个字符串的长度分别是0x18 + 0x7 + 0x24 + 0x10,和为0x53。
所以16字节的第9-12和13-16都是53 00 00 00,都代表着v127的长度。
为什么v156是17字节,而非16个字节,第0个字节为什么是0x20?很明显了,0x20说明是短字符串存储,经过sso优化,直接存储在对象内部,不用开辟堆空间。
砍去一个0不用,剩下的7位表示data部分使用的字节数,0x0010000就是是十进制16,也就是目标16个字节的长度。
言归正传,前17个字节的格式是这样的。
20 ?? ?? 00 02 00 00 00 01 53 00 00 00 53 00 00 00
只剩下第1-2字节未解决了。
第1-2字节分析 第1-2字节与v154有关。
而v154与a4有关。
查看函数0x81F00的引用,来到sub_6EFF0。
v5与a3相关。
函数sub_6EFF0有2个引用。
先不着急追踪引用,先在Unidbg中hook函数sub_6EFF0。
发现*a3 = 0x4。
搜索trace记录,判断函数sub_6EFF0来自于哪个函数调用。
跳转到0x9ef04。
v134是传入的a3。
在0x9ef04下断点,查看x2,因为v134是一个指针,所以用mx2查看里面的内容。
v134与v13有关,v13通过多个调用获得初始化。
我之前实现过:通过jmethodID获得函数名,因此可以识别这里调用了什么。
经过判断,调用了类的初始化函数okio.Buffer.<init>,还调用了writeString。
尝试分析,这里究竟写入了什么。这里我hook上了0x6E13C,0x6E13C封装了CallObjectMethodV,同时根据args[2](jmethodID)筛选调用的函数,打印写入的字符串。
1 2 3 4 5 6 7 8 9 10 11 12 function getStr ( ){ var module = Process .getModuleByName ("libxyass.so" ); Interceptor .attach (module .base .add (0x6E13C ), { onEnter : function (args ){ if (prettyMethod (args[2 ], 0 ) == ("okio.Buffer.writeString" )){ var va1 = args[3 ]; var env = Java .vm .getEnv (); console .log ("[str]" , ptr (env.getStringUtfChars (va1)).readCString ()); } } }); }
写了很多东西。
用Unidbg也可以实现类似的效果,写的时候打印一下即可。
然后下个断点,判断执行前后能否打印字符串。
成功打印字符串“/api/sns/v4/user/login/password”,但发现了点小问题。
观察我补的环境,发现这里传入了一个新的dvmObject,可能是因为这个原因。
改成这样子。
然后再跑一次,这回jobject的值一致了。
最后v134应该指向栈上地址:0xe4fff640,但没找到v134被修改的地方。
只能说,只看伪代码果然不行,接下来看汇编了,前面的内容供读者图一乐,错误经验也是经验,主要是展示我个人的分析流程。
往栈地址0xe4fff640下一个写断点,发现是0x9eddc往0xe4fff640里写了0x4。
在0x9eddc下一个断点,判断一下Q3、Q2哪个存放了0x4。
q3存放0x4。
而Q3 = [X24 + 252],往0x9EDC4下断点,查看X24+252和是多少,然后给X24+252下一个写断点。
x24 = 0x123df000,因此在0x123df0fc下一个写断点。
下了断点后,发现是0x920AC的指令再往0x123df0fc里写0x4。
而w8的值由LDR w8,[x8]得来的。
而x8的值是0x123dc060。
再在0x123dc060下一个写断点,找是谁往0x123dc060里写入的0x4。
地址0x9aab8的指令往0x123dc060里写0x4。
分析一波,如图所示。
可以下个断点在0x9AAA4,查看每次取的16字节来自于哪里。
Q1的16字节来自栈上基址(且x19指向的)0xe4fff200、Q0的16字节来自栈上基址(且x22指向的)0xe4fff148。
Q1和Q0只有低字节不一样,Q1是0x31,Q0是0x35。
先为0xe4fff200下一个写断点,判断这16个字节(31 01 32…17 66 39)是什么时候写入的。
发现是0x92830的指令在往里面写,跳转过去看看。
Q0 = [x8 + x10],在0x92820下一个断点,查看x8与x10的值。
得出0x12019A30。
Unidbg的映射基地址是0x1200000,所以这里的偏移指向so文件的0x19A30。
发现这里是.rodata节的内容,也就是说,Q1的16字节,来自于文件中硬编码的16字节。
再追踪Q0的16字节从何而来,对0xe4fff148下一个写断点。
注意到,似乎对每个字节赋值的地址不同。
这里先追踪Q0最低字节,因为Q1和Q0只在这里有所不同。
来到0x9A064。
对这一块代码进行还原,发现有点复杂啊。
借鉴了一下其它人的分析,Q0的16个字节来自于变种AES加密,因此这里先放一放,先去分析另外53个字节。
0x53个字节 前面提到,0x53个字节其实是分为4块,每块的字节数分别是0x18、0x7、0x24、0x10,
通过hook函数sub_4A278便可获得这4块内容。
下图是第3次调用sub_4A278的位置,可以在这边下一个断点,观察4个字符串未加密前是什么。
这是加密前的内容,位于0x1240c000。
在对第四次调用sub_4A278的位置下断点,这个时候里面的内容虽然也是0x53个字节,但已经加密了。
加密点应该在第3次调用与第四次调用之间。
在0x123dc060下写断点,指令0x8235C处往里面写了0x35,0x8235C位于函数sub_81F00内。
跳转到0x8235C,看不出加密算法。
慢慢分析。
先对sub_81F00进行hook,打印入参。
(x0)a1 = 1。(疑似固定值)
(x1)a2是个字符数组,代表build,如下。
0xE表示sso存储,占用了7个字节。
(x2)a3来自于AppId。
(x3)a4为0x4;——之前q1、q2异或的0x4。
(x4)a5是个string类型;
存放了device_id。
(x5)a6=0xe4fff521,似乎存放了16个字节。
(x6)a7 = 0x10,似乎代表a6的大小。
(x7)a8=0x38663439662d3038,存放了8个字符“80-f94f8”。
似乎是device_id的一部分。
改一下变量名。
结合入参进行改名。
插入一个结构体,方便分析。
接着往下分析。
点进xmmword_19AA0,0x21说明了是长字符串存储,缓冲区大小是0x20个字节,已经占用了0x18个字节。(v157的前16个字节来自于xmmword_19AA0,缓冲区的地址并不是通过v157=xmmword_19AA0得到的)
v157指向红色框中的这18个字节。
接着往下分析。
观察变量“ptr_18bytes_AND_build_id_AND_device_id_b”的值,它的值其实在之前就清空了,这里应该是复用了,因为不知道它在加密部分是什么含义,暂时没改变量名。
hook上0x821B0的位置,访问x9所指向的内存地址,查看内容有什么变化。(根据计算,有32次循环,记作0-31)
第0次循环。
在执行了几次循环后,如下图所示。
从第8个字节开始,0x0、0x1…依次递增,一次循环可以初始化8个4字节,而执行32次,也就是说,从0一直到(32*8 - 1),也就是0x0到0xff。
而!RC4流密码中,在初始化状态向量时,SBox就是这么得来的,SBox的初始化的伪代码如下。
一般的RC4中,SBox是字节数组,而这里是4字节为单位的数组。
1 2 3 for (int i = 0 ; i < 256 ; i++) { S[i] = i; }
如果猜想是对的话,待会应该还有一段用密钥对S盒进行置换的过程,暂时先将“ptr_18bytes_AND_build_id_AND_device_id_b”重命名为“SBox”。
继续往下分析。
待会再去hook获得密钥,先来看看这个S盒置换算法。
下面这段代码基本可以说明,就是RC4算法,而且密钥长度为13——0到12。
循环64次,说明每次循环交换4次。
hook一下,得到密钥看看。
x8=0x1201b48d,说明密钥在文件偏移0x1b48d的位置。
试图用字符串“std::abort();”作掩护,hhhh。
拿去试一下,发现是个非常标准的rc4加密。
至此,0x53个字节分析完了。
0x10个字节(Ⅱ) 还记得,在**16个字节(Ⅰ)**中,我们分析得出,16个字节的格式如下:
20 ?? ?? 00 02 00 00 00 01 53 00 00 00 53 00 00 00
当时最大的问题是:?? ??的值由Q1与Q0异或得来,Q1是硬编码在文件中的,而Q0的值,当时只追踪了最低字节。
当时为了得出Q0的最低字节从何而来,我逆了一下算法,但涉及到的缓冲区有点多。
所以在这一小节,我们的目标之一是还原Q0 ,还有一个目标是分析绿色框里的16个字节 从何而来。
(我好像没解释过红色框,红色框的前4个字节是函数sub_81F00传入的a1,是固定值;再4个字节是app_id,0x2是根据传Q1与Q0异或的0x4判断得到的;0x7代表build的大小,0x24代表device_id的大小,0x10代表未知的16个字节)
HMAC-MD5 这16个字节是函数sub_81F00的入参a6,对应的缓冲区地址是0xe4fff521。
在0xe4fff521下一个写断点。
地址0x6f768处的指令对0xe4fff521进行了写。
0xe4fff521的值由Q0得到,Q0的值由[X29,#-0x18]得到,所以在0x6F758下断点(或者看日志)。
x29=0xe4fff470,所以X29 - 0x18是0xE4FFF458。
在0xE4FFF458下写断点,发现是0x92fd8。
地址0x92fd8位于函数sub_92F2C范围内。
我们需要的16字节存储在a2中,a2由v2赋值。
w8是上图的a2,a2的值地址0x92FCC获得,所以在0x92FCC下一个断点,查看X20的值。
x20的值是0x123df480。
在x20下写断点。
可以看到,偏移0x93f74对0x123df480进行了写操作。
来到0x93f74。
对应于汇编的伪代码。
现在0x93F74下个断点,发现这个函数执行了很多次,迟迟等不到我们要看的内容,于是统计一下这个函数执行的次数。
同时,分析算法,发现一个很有意思的点。
假设HIDWORD(v4) = R,那么LODWORD(v4)也等于R,也就是说64位的高32位和低32位是一样的,都为R。
而v4 >> 17可以写成(R << 32 | R) >> 17,进一步可以写成((R << 32) >> 17) | R >> 17。
(R << 32) >> 17的结果是 R 左移(32 - 17 = 15)位,并占据了结果的高位部分。
R >> 17的结果是 R 右移17位,占据了结果的低位部分。
v5是一个32位的数据类型,所以当上述的两者通过或组合在一块,就相当于将R循环右移了17位。
而循环右移17位,等同于循环左移15位。
(注意这里的<<都是逻辑左移和逻辑右移,而不是循环左移和循环右移)
md5算法有4轮运算:F、G、H、I,每一轮运算要执行16次(因为有16个子分组),在I运算中,涉及循环左移6、10、15、21位;除此之外,I运算涉及异或、取反、或等操作。
而在sub_93F14中,发现了循环左移15位,还有循环左移21位,同时发现了一些异或、取反的操作,又考虑到这个函数执行了16次,这大概率是md5算法的II运算。
还发现了md5的魔数,按常理来说,II运算是第4轮运算,魔数ABCD早被修改了,但编译器似乎没对寄存器x0进行操作,魔数一直存在x0中。
更加坐实了这是一个魔改的md5算法。——为什么说是魔改,之后会说。
分析trace日志中,0x93F74出现的地方,对应上调用了16次函数sub_93F14。
在第16次的时候,w28出现了未知16字节中的4个字节。
在第16次调用的trace记录,w27是魔数,w28的值来自于[0xe4fff2c0 - 0x34],即[0xE4FFF28C]。
要同时追踪w27和w28吗?其实不用。
下图是第1次调用的trace记录,w27是一个魔数,所以只追踪w28。
第三轮运算最终的a、b、c、d的第四轮运算的初始a、b、c、d。
在0xe4fff1f0-0x34处下一个写断点,观察里面的值来自于哪,大概率是md5的HH轮运算传过来的。
偏移0x94534位于函数sub_944A8内,符合md5的特征,看样子还是II运算。
结合上图和下图,有点像是MD5的第60、61、62、63、64轮运算。
结合标准的md5算法,md5的63、64轮明显被魔改了。
有一个疑惑,既然这里是md5的第63、64轮加密,为什么函数sub_93F14调用了16次?我原本以为,这里魔改的II运算,结果似乎只涉及63、64轮加密,前面的分析有些误打误撞,不过确认了,使用的加密算法是md5,涉及一些变异。
回到函数sub_92F2C,对函数进行hook,判断v2是从什么变成16个字节。
发现要加密的内容是16个字节。
对函数sub_92F2C进行分析,静态分析下,可能会调用了2次sub_93390,第1次调用是为了处理空间不足的情况,因为md5的每个分组是512位,要留出最后64位记录信息长度,所以实际能用来存储的空间只有56字节。
创建结构体,对变量进行修改。
0x80是填充值,数据长度要小于等于55,因为至少有1个字节是填充值。
接下来,分析函数sub_93390,它大概率是在进行md5加密,根据统计,一共执行了5次sub_93390。——这也无法解释为什么调用了sub_93F14共计16次,再放一会吧。
而我hook函数sub_92F2C,这个函数执行了2次,而且每一次数据的量都没有超过55字节,说明还有其它3处地方调用了sub_93390,进行md5加密。
搜索trace记录,从第1次调用开始看起。
第一次调用来自于0x92eec。
这里的sub_93390没有看到参数列表。
根据汇编,大概有3个参数,因此可以修改函数签名。
而且这里的v3,一看就是之前分析的那个结构,修改变量类型。
hook函数sub_93390,看看返回值和入参。——返回值可以通过入参a1的前16字节获得。
根据大量的0x36,可以判断这是一个hmac-md5算法。
整理一下,5次调用sub_93390的入参、返回值。
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 103 104 105 106 107 108 109 // 第1次调用 // x0 0000: 76 54 32 10 FE DC BA 98 89 AB CD EF 01 23 45 67 vT2..........#Eg // x1(key与0x36的结果) 0000: 6A 3E 23 D6 BC E6 B7 B9 C6 D2 4B D7 6C 78 02 D9 j>#.......K.lx.. 0010: 72 84 55 82 03 93 5F 6E 20 B3 32 87 75 82 F9 8D r.U..._n .2.u... 0020: 38 43 90 71 D4 5C B9 1A 8D 63 39 3D F7 94 59 C7 8C.q.\...c9=..Y. 0030: 7C 27 5A FD 1D 68 77 A5 BE 4D 91 14 E5 8D 0E 51 |'Z..hw..M.....Q // x2 0x1 // 返回值 0000: 39 0A 80 0F EC FE 50 43 EE 54 1F 5B 06 9C 47 8B 9.....PC.T.[..G. // 第2次调用 // x0 0000: 76 54 32 10 FE DC BA 98 89 AB CD EF 01 23 45 67 vT2..........#Eg // x1(key与0x5C的结果) 0000: 00 54 49 BC D6 8C DD D3 AC B8 21 BD 06 12 68 B3 .TI.......!...h. 0010: 18 EE 3F E8 69 F9 35 04 4A D9 58 ED 1F E8 93 E7 ..?.i.5.J.X..... 0020: 52 29 FA 1B BE 36 D3 70 E7 09 53 57 9D FE 33 AD R)...6.p..SW..3. 0030: 16 4D 30 97 77 02 1D CF D4 27 FB 7E 8F E7 64 3B .M0.w....'.~..d; // x2 0x1 // 返回值 0000: 29 9A 8E 4B 77 48 EA 98 E3 E3 DF 8C C7 D2 4B 4A )..KwH........KJ // 第3次调用 // x0 0000: 39 0A 80 0F EC FE 50 43 EE 54 1F 5B 06 9C 47 8B 9.....PC.T.[..G. // x1 0000: 2F 61 70 69 2F 73 6E 73 2F 76 34 2F 75 73 65 72 /api/sns/v4/user 0010: 2F 6C 6F 67 69 6E 2F 70 61 73 73 77 6F 72 64 66 /login/passwordf 0020: 69 64 3D 31 37 34 34 32 39 38 31 30 38 31 30 31 id=1744298108101 0030: 33 64 32 30 63 39 31 35 38 61 30 61 39 62 32 39 3d20c9158a0a9b29 0040: 61 62 62 63 62 62 31 65 64 37 32 36 66 35 62 26 abbcbb1ed726f5b& 0050: 67 69 64 3D 37 63 30 64 36 38 65 38 36 34 37 61 gid=7c0d68e8647a 0060: 35 34 33 38 61 32 33 39 34 62 62 61 64 33 36 34 5438a2394bbad364 0070: 63 32 62 64 66 33 31 62 39 38 37 39 34 37 33 35 c2bdf31b98794735 0080: 39 34 34 30 37 37 62 62 66 63 35 35 26 64 65 76 944077bbfc55&dev 0090: 69 63 65 5F 6D 6F 64 65 6C 3D 70 68 6F 6E 65 26 ice_model=phone& 00A0: 74 7A 3D 41 73 69 61 25 32 46 53 68 61 6E 67 68 tz=Asia%2FShangh 00B0: 61 69 26 63 68 61 6E 6E 65 6C 3D 56 69 76 6F 26 ai&channel=Vivo& 00C0: 76 65 72 73 69 6F 6E 4E 61 6D 65 3D 38 2E 37 37 versionName=8.77 00D0: 2E 30 26 64 65 76 69 63 65 49 64 3D 31 30 66 39 .0&deviceId=10f9 00E0: 31 66 36 38 2D 37 64 62 37 2D 33 61 33 36 2D 38 1f68-7db7-3a36-8 00F0: 30 63 39 2D 37 30 33 61 62 61 64 38 66 65 65 32 0c9-703abad8fee2 0100: 26 70 6C 61 74 66 6F 72 6D 3D 61 6E 64 72 6F 69 &platform=androi 0110: 64 26 73 69 64 3D 73 65 73 73 69 6F 6E 2E 31 37 d&sid=session.17 0120: 34 34 33 30 30 36 32 36 36 35 35 37 30 31 36 32 4430062665570162 0130: 39 31 35 30 26 69 64 65 6E 74 69 66 69 65 72 5F 9150&identifier_ 0140: 66 6C 61 67 3D 34 26 70 72 6F 6A 65 63 74 5F 69 flag=4&project_i 0150: 64 3D 45 43 46 41 41 46 26 78 5F 74 72 61 63 65 d=ECFAAF&x_trace 0160: 5F 70 61 67 65 5F 63 75 72 72 65 6E 74 3D 6C 6F _page_current=lo 0170: 67 69 6E 5F 66 75 6C 6C 5F 73 63 72 65 65 6E 5F gin_full_screen_ 0180: 70 77 64 5F 70 61 67 65 26 6C 61 6E 67 3D 7A 68 pwd_page&lang=zh 0190: 2D 48 61 6E 73 26 61 70 70 5F 69 64 3D 45 43 46 -Hans&app_id=ECF 01A0: 41 41 46 30 31 26 75 69 73 3D 6C 69 67 68 74 26 AAF01&uis=light& 01B0: 74 65 65 6E 61 67 65 72 3D 30 26 61 63 74 69 76 teenager=0&activ 01C0: 65 5F 63 74 72 79 3D 43 4E 26 63 70 75 5F 6E 61 e_ctry=CN&cpu_na 01D0: 6D 65 3D 51 75 61 6C 63 6F 6D 6D 2B 54 65 63 68 me=Qualcomm+Tech 01E0: 6E 6F 6C 6F 67 69 65 73 25 32 43 2B 49 6E 63 2B nologies%2C+Inc+ 01F0: 53 4D 38 31 35 30 26 64 6C 61 6E 67 3D 7A 68 26 SM8150&dlang=zh& 0200: 6C 61 75 6E 63 68 5F 69 64 3D 31 37 34 34 34 33 launch_id=174443 0210: 36 33 39 32 26 6F 76 65 72 73 65 61 73 5F 63 68 6392&overseas_ch 0220: 61 6E 6E 65 6C 3D 30 26 6D 6C 61 6E 67 75 61 67 annel=0&mlanguag 0230: 65 3D 7A 68 5F 63 6E 26 66 6F 6C 64 65 72 5F 74 e=zh_cn&folder_t 0240: 79 70 65 3D 6E 6F 6E 65 26 61 75 74 6F 5F 74 72 ype=none&auto_tr 0250: 61 6E 73 3D 30 26 74 3D 31 37 34 34 34 33 36 33 ans=0&t=17444363 0260: 38 35 26 62 75 69 6C 64 3D 38 37 37 30 32 39 39 85&build=8770299 0270: 26 68 6F 6C 64 65 72 5F 63 74 72 79 3D 43 4E 26 &holder_ctry=CN& 0280: 64 69 64 3D 32 64 33 35 31 62 62 65 66 31 64 62 did=2d351bbef1db 0290: 38 33 38 63 35 66 63 64 36 62 36 37 61 34 61 61 838c5fcd6b67a4aa 02A0: 31 64 62 35 38 38 70 6C 61 74 66 6F 72 6D 3D 61 1db588platform=a 02B0: 6E 64 72 6F 69 64 26 62 75 69 6C 64 3D 38 37 37 ndroid&build=877 02C0: 30 32 39 39 26 64 65 76 69 63 65 49 64 3D 35 31 0299&deviceId=51 02D0: 38 64 66 65 39 36 2D 37 66 36 66 2D 33 37 30 61 8dfe96-7f6f-370a 02E0: 2D 38 65 38 30 2D 66 39 34 66 38 38 33 38 65 36 -8e80-f94f8838e6 02F0: 64 65 66 73 3D 30 26 70 6F 69 6E 74 3D 31 39 33 defs=0&point=193 0300: 32 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 2............... // x2 0xc // 返回值 0000: 81 97 7E 83 36 B7 43 2D 01 34 A6 A4 34 6C 32 06 ..~.6.C-.4..4l2. // 第4次调用 // x0 0000: 81 97 7E 83 36 B7 43 2D 01 34 A6 A4 34 6C 32 06 ..~.6.C-.4..4l2. // x1 0000: 32 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 2............... 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0030: 00 00 00 00 00 00 00 00 08 1A 00 00 00 00 00 00 ................ // x2 0x1 // 返回值 0000: 8D 67 06 8C 91 18 85 7B FB 78 DB 01 59 F2 09 69 .g.....{.x..Y..i // 第5次调用 // x0 0000: 29 9A 8E 4B 77 48 EA 98 E3 E3 DF 8C C7 D2 4B 4A )..KwH........KJ // x1 0000: 8D 67 06 8C 91 18 85 7B FB 78 DB 01 59 F2 09 69 .g.....{.x..Y..i 0010: 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0030: 00 00 00 00 00 00 00 00 80 02 00 00 00 00 00 00 ................ // x2 0x1 // 返回值 0000: 72 4B A0 52 97 97 13 0E 0B C3 00 2F BF C2 C7 5F rK.R......./..._
注意到,第5次调用sub_93390的返回值正是我们需要的16字节。
同时发现:
第1次调用的返回值,是第3次加密的魔数值——0x36与key异或作为输入,魔数是标准md5魔数;
第2次调用的返回值,是第5次加密的魔数值——0x5C与key异或作为输入,魔数是标准md5魔数;
第3次调用的返回值,是第4次加密的魔数值——明文url + xy-common-params + xy-direction + platform + build + deviceId + xy-scene作为输入,魔数是第1次调用的返回值;
第4次调用的返回值,是第5次加密的输入——第4次调用的输入是第3次加密中,末尾不足64字节的部分;如果第3次调用刚好满足每组都是64字节,第4次调用的输入将是0x80开头,然后在最后的8个字节中,会描述前面的所有块的大小。
第5次加密就是将第4次的16字节进行了填充得到最终的16字节。
单独看每次调用,其实每次调用都是魔改MD5算法,而将5次魔改MD5的调用合在一起,就是一个HMAC-MD5调用,整个流程应该这样子理解:
第1次调用魔改MD5,是对**(密钥 ^ 0x36)的结果进行的魔改MD5运算,这一次调用的目的是获得用作第3次调用的魔数(16字节),这16字节并非是用来作为 K’**,再次说明:密钥是64字节,不需要经过MD5运算获得16字节,再填充至64字节,这里进行MD5仅仅是为了获得第3次调用的魔数,因为第3次调用是第1次调用的延续。
暂时不管第2次调用。
第3次调用魔改MD5,是对明文进行计算MD5的值,明文长度为0x301字节,魔数是第1次调用的结果(16字节)。
按照HMAC-MD5计算方式,K’是密钥,ipad是64个字节的0x36,两者异或后还是64字节,然后拼接上明文,再进行魔改的MD5运算,这时候,输入MD5运算的长度是0x301+0x40,即0x341字节。
而MD5运算,一次处理64字节,如果最后一组不满64字节,则填充448位,最后8字节用来表示这一次输入MD5运算的位数是多少。0x341个字节,即输入了0x1A08位数据。
第4次调用,单独处理1个字节,因为0x341个字节,可以分为13个64字节分组和单独1个字节,第4次调用就是处理这个字节的,注意下图,0x32后填充了一个0x80(即后面填充一个1和“无数”个0),而最后8字节是0x1A08,代表着处理的位数。
将第1、3、4次调用放在一块看,最后获得了HMAC-MD5 这一步的结果。↓
第2次调用是获得(K’ ^ 0x5C)的MD5运算后的结果(16字节),用来作为第5次调用的魔数(因为在附加连接的时候,是K’在前面,说白了,魔数代表着前面几次MD5运算的结果)。
第5次调用,是计算H(K’ ^ 0x5C || H(K’ ^ 0x36 || 明文)) ,输入是(K’ ^ 0x5C)|| (第1、3、4次调用的结果),已知(K’ ^ 0x5C)是64字节,而(第1、3、4次调用的结果)肯定是16字节,所以一共是80字节。
下图中0x80是填充,最后8字节是0x280,即80字节。
到这里,也能理解之前为什么调用了16次sub_93F14。因为第1-5次调用sub_93390,分别执行了1、1、12、1、1次魔改MD5运算。
之所以说这是魔改MD5,是因为在个别轮次的运算,不是标准MD5的FF、GG、HH、II。
点开sub_93390。
根据日志,对sub_93390调用链进行分析,MD5一共64轮运算,将每轮运算依次与标准MD5进行对比。
在日志中,追踪BR跳转,sub_94970是第1个较为“完整”的函数。(这里的完整指的是:有些函数的伪代码在IDA的分析下,显示为直接BR跳转;而这个函数会先执行一些操作再跳转)
根据动调,发现前4行代码加载了魔数,第5行代码加载了待加密数据的地址,具体内容写在注释上了…函数sub_94970似乎在为MD5的第一轮运算做准备。
接下来,来到函数sub_95080,这应该是md5的第一轮加密 。循环左移7位变成了6位。
hook了一下v7+132的位置应该是T盒,但并不是标准的T盒。
函数sub_95654,为明文的第2、3个字做准备。
函数sub_93DD8中,MD5第二轮加密 。循环左移12位变成了循环左移13位。
具体的不在这里一一分析,做个总结。
魔改左移操作数 第43轮的左移操作数被改,在地址0x93AC8处,原操作数为 16,被改为 11
第42轮的左移操作数被改,在地址0x93A90处,原操作数为11 ,被改为 16
第41轮的左移操作数被改,在地址0x93A78处,原操作数为 4,被改为 23
第40轮的左移操作数被改,在地址0x94368处,原操作数为 23,被改为 4
第14轮的左移操作数被改,在地址0x9480C处,原操作数为 12,被改为 13
第11轮的左移操作数被改,在地址0x94E00处,原操作数为 17,被改为 16
第8轮的左移操作数被改,在地址0x93888处,原操作数为 22,被改为 20
第4轮的左移操作数被改,在地址0x952E4处,原操作数为 22,被改为 21
第2轮的左移操作数被改,在地址0x93E28处,原操作数为 12,被改为 13
第1轮的左移操作数被改,在地址0x950F0处,所属函数sub_95080,原操作数为 7 ,被改为 6
魔改T值 标准MD5的T值是:
1 2 3 4 5 6 7 8 9 const unsigned int t[] = { 0xd76aa478 , 0xe8c7b756 , 0x242070db , 0xc1bdceee , 0xf57c0faf , 0x4787c62a , 0xa8304613 , 0xfd469501 , 0x698098d8 , 0x8b44f7af , 0xffff5bb1 , 0x895cd7be , 0x6b901122 , 0xfd987193 , 0xa679438e , 0x49b40821 , 0xf61e2562 , 0xc040b340 , 0x265e5a51 , 0xe9b6c7aa , 0xd62f105d , 0x02441453 , 0xd8a1e681 , 0xe7d3fbc8 , 0x21e1cde6 , 0xc33707d6 , 0xf4d50d87 , 0x455a14ed , 0xa9e3e905 , 0xfcefa3f8 , 0x676f02d9 , 0x8d2a4c8a , 0xfffa3942 , 0x8771f681 , 0x6d9d6122 , 0xfde5380c , 0xa4beea44 , 0x4bdecfa9 , 0xf6bb4b60 , 0xbebfbc70 , 0x289b7ec6 , 0xeaa127fa , 0xd4ef3085 , 0x04881d05 , 0xd9d4d039 , 0xe6db99e5 , 0x1fa27cf8 , 0xc4ac5665 , 0xf4292244 , 0x432aff97 , 0xab9423a7 , 0xfc93a039 , 0x655b59c3 , 0x8f0ccc92 , 0xffeff47d , 0x85845dd1 , 0x6fa87e4f , 0xfe2ce6e0 , 0xa3014314 , 0x4e0811a1 , 0xf7537e82 , 0xbd3af235 , 0x2ad7d2bb , 0xeb86d391 };
而魔改的T值是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0000 : 56 B7 C9 E9 79 A4 1B D7 DB 81 10 24 D9 88 10 68 V ...y......$...h0010 : AF F7 14 9B B1 5B 1F FF BE D7 1C 88 22 61 66 66 .....[......"aff 0020: 93 61 66 F6 9E 63 19 A6 21 09 14 49 EE CE 1D C1 .af..c..!..I.... 0030: AF 0F 1C F5 2A C6 17 47 13 46 10 A9 01 95 16 FD ....*..G.F...... 0040: 00 25 00 F6 53 14 74 02 91 E6 21 D2 C9 11 00 E2 .%..S.t...!..... 0050: E6 CD 61 22 97 0D D5 F2 ED 14 5A 42 05 E9 77 A2 ..a" ......ZB..w.0060 : F9 A3 77 F2 D9 12 6F 62 9A 4C 2A 92 48 F2 39 12 ..w...ob.L*.H.9 .0070 : 00 00 00 00 40 B3 40 C0 51 5A 5E 26 00 00 10 E9 ....@.@.QZ^&.... 0080 : 5D 10 3F D6 D6 07 57 C3 42 39 FC FF 91 D6 7C 97 ].?...W.B9....|.0090 : 44 EA BC A4 A9 CF DC 4B 70 BC BC BE C6 7E 8C 28 D ......Kp....~.(00 A0: 60 4B CC F6 95 10 EC D4 FA 27 AC EA E5 88 DC E6 `K.......'...... 00B0: 39 D0 DC D9 05 1D 8C 04 F9 7C A2 1F 90 F2 39 12 9........|....9. 00C0: 00 00 00 00 22 61 9D 6D 65 56 AC C4 1C 39 E5 FD ...."a.meV...9.. 00D0: 44 22 29 F4 A7 23 94 AB 39 A0 93 F5 C3 59 5B 65 D")..#..9....Y[e 00E0: 97 FF 2A 45 7D 24 EF F5 D1 5D 84 85 92 CC 0C 85 ..*E}$...]...... 00F0: E0 26 99 F9 C0 F2 39 12 00 00 00 00 D6 A9 97 EF .&....9......... 0100: 4F 7E 99 F9 14 43 99 A9 82 7E 53 C5 A1 11 08 45 O~...C...~S....E 0110: A6 11 08 45 35 F2 3A BD 91 D3 86 EB
为什么魔改后的T表比标准T表大呢?——多了7个4字节。
在函数sub_93BA4中,执行了第26、27、28轮运算,v0[59]是第27轮的t,v0[62]是第28轮的t,中间跳过了v0[60]、v0[61],也就是2个4字节。
在函数sub_951A8中,执行了第45、46、47轮运算。
而在函数sub_93A4C中,执行了第42、43、44轮运算,第45轮的t是v1[79]。
v1和v5基址一样,v1[79]到v5[82],跳过了2个4字节,也就是8字节。
在函数sub_93CB4中,执行了第56、57、58轮运算,又跳过了3个4字节。
总共跳过了2 + 2 + 3个4字节,共计7个4字节,因此魔改后的T表多了7个4字节。
入参顺序修改 假设HH的签名是HH(A,B,C,D,Mi,s,Ti)。
40轮:37轮结果对应为A位置,36轮结果对应B,38-39分别对应C-D,明文块取下标13
41轮:36轮结果对应为A位置,39轮结果对应B,38-40分别对应C-D,明文块取下标10
42轮:39轮结果对应为A位置,38轮结果对应B,40-41分别对应C-D,明文块取下标3
43轮:38轮结果对应为A位置,40轮结果对应B,41-42分别对应C-D,明文块取下标0
以第40轮的入参为例子。
标准MD5的情况下,原本第40轮的bcda分别来自于第36、39、38、37轮。
在函数sub_957D4中,v1-200存放着第36轮的结果b。
在函数sub_936C0中:
v2-196存放着第37轮的结果a。
v2-192存放着第38轮的结果d。
v2-188存放着第39轮的结果c。
正常的第40轮,应该是:HH(v-200, v-188, v-192, v-196,M10,23,tj),展开后应该是这样子:
a = (v-188) + (((v-200) + H(v-188, v-192, v-196) + M10 + tj) <<23)。
在函数sub_94344中是:HH(v-196, v-200, v-192, v-188, M13, 4, tj)。
同时发现,如果对第40轮取的明文进行打印,对应的M是第13块(下标),而非第10块,这里不过多赘述了。
魔改AES,还原Q0 现在只剩下最后2个问题:
一是 之前Q0与Q1异或的结果是0x4,Q1是硬编码在文件中的,而Q0则涉及很多内容,对栈、堆下断点,一点一点追踪Q0的内容十分困难。
二是 HMAC-MD5的密钥是哪里来的?
根据后来的结果,只要解决问题二,问题一就顺带解决了。
x1指向的内容,是密钥与0x36异或的结果。
1 2 3 4 5 6 7 8 9 10 11 12 // 第1次调用 // x0 0000: 76 54 32 10 FE DC BA 98 89 AB CD EF 01 23 45 67 vT2..........#Eg // x1(key与0x36的结果) 0000: 6A 3E 23 D6 BC E6 B7 B9 C6 D2 4B D7 6C 78 02 D9 j>#.......K.lx.. 0010: 72 84 55 82 03 93 5F 6E 20 B3 32 87 75 82 F9 8D r.U..._n .2.u... 0020: 38 43 90 71 D4 5C B9 1A 8D 63 39 3D F7 94 59 C7 8C.q.\...c9=..Y. 0030: 7C 27 5A FD 1D 68 77 A5 BE 4D 91 14 E5 8D 0E 51 |'Z..hw..M.....Q // x2 0x1 // 返回值 0000: 39 0A 80 0F EC FE 50 43 EE 54 1F 5B 06 9C 47 8B 9.....PC.T.[..G.
再次异或0x36,获得key。
1 2 3 4 5c 08 15 e0 8a d0 81 8f f0 e4 7d e1 5a 4e 34 ef 44 b2 63 b4 35 a5 69 58 16 85 04 b1 43 b4 cf bb 0e 75 a6 47 e2 6a 8f 2c bb 55 0f 0b c1 a2 6f f1 4a 11 6c cb 2b 5e 41 93 88 7b a7 22 d3 bb 38 67
存放(密钥^0x36)的栈地址是0xe4fff320,在这里下一个写断点。
偏移0x91708的位置。
说明密钥来自于[x25, #0x24]往后的64个字节。
地址是0x123e7024。
对0x123e7024下一个写断点。
偏移0x916d0。
在0x926B8下断点,获得X24的值。
再对0x123e2000下一个写断点。
这里的偏移是0x911c8,0x1c1f8是libc的偏移,所以这里看LR寄存器。
hook 0x911C4,获取源地址。
再在0xe4fff644下写断点,看看哪里往里写。
偏移0x9eddc。
在0x9edc4下断点,查看x24和x8.
在0x123df100下写断点。
偏移0x91c84。
在0x91C80下断点,查看src。
在0x123dc070下个写断点。
偏移0x9aab8,之前分析第1-2字节的时候,遇到过这个偏移。
密钥来源于Q1、Q0的16字节异或的结果。
在0x9AAB4下断点,查看Q0、Q1的值。
一共经历了6次地址0x9AAB4。
第1轮异或的结果是0x4,是我们之前想探究的2个字节的来源。
第2、3、4、5轮异或的结果合在一块,是密钥的64字节。
第6轮异或的结果像是PKCS7 填充方式。
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 // 1 q0=0x3966170766667a666108020434320135 q1=0x3966170766667a666108020434320131 // 结果 q0=0x4 // 2 q0=0xbf8a86b75c5aa6988a18500d97aec16e q1=0x50bec8edbd2742680599808777bbc932 // 结果 q0=0xef344e5ae17de4f08f81d08ae015085c // 3 q0=0x57ef40c30996ef5928798ead0a1f8562 q1=0xec20f480b8926a4f70102b98be7c3726 // 结果 q0=0xbbcfb443b10485165869a535b463b244 // 4 q0=0x23cec11b0d7991d1bd27b2b6ecfdc56 q1=0xf3534ed0bbd8cca6375d11c92969a958 // 结果 q0=0xf16fa2c10b0f55bb2c8f6ae247a6750e // 5 q0=0xf78b842c37084a18aebc2f6959061f3f q1=0x90b33fff15af31903dfd7142926a0e75 // 结果 q0=0x6738bbd322a77b8893415e2bcb6c114a // 6 q0=0x82a9df8040ac9909aee06a8949973ab0 q1=0x92b9cf9050bc8919bef07a9959872aa0 // 结果 q0=0x10101010101010101010101010101010
注意到,x19里面的地址,都是从x24里面取出来的。
x24指向的内容如下。
监控一下0x120b4000。
发现监控不到。。。
在IDA的伪代码中进行追踪。
打印一下result。
后面发现,这里的x24源于hmac_main的值,hmac_main进行base64解码后的内容和它一模一样。
前面0x40字节在异或后是hmac-md5的密钥,而0x40->0x50字节抑或后是pkcs7填充的内容。
再对x22指向的0xe4fff148进行监控。(Q0)
之前追踪过0x9a064,找不到结果,这回追踪LR。
写的操作都发生在sub_994AC中,sub_994AC累积执行了6次。
偏移0x9a9cc位于函数sub_9A728,对这个函数分析,发现这个函数实现了一个对称分组密码 的加密和解密 操作,使用模式是CBC。
if(a6)分支实现了CBC加密模式。
if(a3)分支实现了CBC解密模式。
根据分析,a5是IV,即初始化向量(盐),这是之前Q1的值 。
hook函数sub_994AC,查看它的入参。
注意到,第1次执行,对“32 C9 BB 77 87 80 99 05 68 42 27 BD ED C8 BE 50”解密,获得了“35 01 32 34 04 02 08 61 66 7A 66 66 07 17 66 39”,而“35 01 32 34 04 02 08 61 66 7A 66 66 07 17 66 39”正是0x9AAB4第一轮异或的Q0!
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 第1次执行 X0 32 C9 BB 77 87 80 99 05 68 42 27 BD ED C8 BE 50 X1 58 97 27 12 0A 00 00 00 00 00 00 00 00 00 00 00 返回值(第2次执行时的X1) 35 01 32 34 04 02 08 61 66 7A 66 66 07 17 66 39 第2次执行 X0 26 37 7C BE 98 2B 10 70 4F 6A 92 B8 80 F4 20 EC X1 35 01 32 34 04 02 08 61 66 7A 66 66 07 17 66 39 返回值(第3次执行时的X1) 6E C1 AE 97 0D 50 18 8A 98 A6 5A 5C B7 86 8A BF 第3次执行 X0 58 A9 69 29 C9 11 5D 37 A6 CC D8 BB D0 4E 53 F3 X1 6E C1 AE 97 0D 50 18 8A 98 A6 5A 5C B7 86 8A BF 返回值(第4次执行时的X1) 62 85 1F 0A AD 8E 79 28 59 EF 96 09 C3 40 EF 57 第4次执行 X0 75 0E 6A 92 42 71 FD 3D 90 31 AF 15 FF 3F B3 90 X1 62 85 1F 0A AD 8E 79 28 59 EF 96 09 C3 40 EF 57 返回值(第5次执行时的X1) 第5次执行 X0 A0 2A 87 59 99 7A F0 BE 19 89 BC 50 90 CF B9 92 X1 56 DC CF 6E 2B 7B D2 1B 1D 99 D7 B0 11 EC 3C 02 返回值(第6次执行时的X1) 3F 1F 06 59 69 2F BC AE 18 4A 08 37 2C 84 8B F7 第6次执行 X0 9B 48 8F D1 39 69 CA C2 54 E2 0A F2 0C 1C 4C 98 X1 3F 1F 06 59 69 2F BC AE 18 4A 08 37 2C 84 8B F7 返回值 b0 3a 97 49 89 6a3 e0 ae 09 99 ac 40 80 df a9 82
sub_9A728采用了CBC模式,而函数sub_994AC每次处理又是16字节,这大概率是一个AES加解密。sub_994AC的第一个参数的待解密的16字节,第二个参数存放结果,第三个参数是轮密钥。
s.xml文件里存放的是密文。
第1个16字节作为密文0,在AES解密后生成中间值[0] ,中间值[0]就是35 01 32 34 04 02 08 61 66 7A 66 66 07 17 66 39,中间值[0]再与盐IV “31 01 32 34 04 02 08 61 66 7A 66 66 07 17 66 39”进行异或,生成明文[0]——0x4。
所以说,这里的明文0x4是固定的。
第2个16字节作为密文1,在AES解密后生成中间值[1],中间值1就是6E C1 AE 97 0D 50 18 8A 98 A6 5A 5C B7 86 8A BF,中间值[1]再与密文0(32 C9 BB 77 87 80 99 05 68 42 27 BD ED C8 BE 50)进行异或,生成明文[1]——0xef344e5ae17de4f08f81d08ae015085c。
…以此类推,最后获得的内容是:
1 2 3 4 5 6 7 8 9 10 11 // 明文[0],固定字节 q0=0x4 // 明文[1-4],用作HMAC-MD5的密钥 q0=0xef344e5ae17de4f08f81d08ae015085c q0=0xbbcfb443b10485165869a535b463b244 q0=0xf16fa2c10b0f55bb2c8f6ae247a6750e q0=0x6738bbd322a77b8893415e2bcb6c114a // PKCS7填充 q0=0x10101010101010101010101010101010
也就是说,xhs服务器那边发来的xml文件,其实藏了0x4和MD5的密钥。
接下来还有2个疑惑AES是否魔改?密钥是什么?
为了解答疑惑,需要追踪函数sub_994AC,这个函数专门负责AES解密,难点在于,这个函数与魔改MD5一样,频繁通过BR跳转到别的函数,代码片段零零散散。
根据分析,每一次执行sub_994AC,最终都会调用下面这些函数。
0-3次循环都一样,第4次循环出现变化;
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 994AC 9a260 // 0-3字节:轮密钥加 99e6c // 4-7字节:轮密钥加 9a40c // 8-11字节:轮密钥加 9a574 // 12-15字节:轮密钥加 995e8 99c40<----循环0 996d8 // 查表优化 9a620 // 查表优化 99a7c 99564 9a6ac 9a2cc 9991c<> 9a0a0 9a364 99898 9978c 9a160 99ee0 99f74 9969c 99c40<----循环1 996d8 9a620 99a7c 99564 9a6ac 9a2cc 9991c<> 9a0a0 9a364 99898 9978c 9a160 99ee0 99f74 9969c 99c40<----循环2 996d8 9a620 99a7c 99564 9a6ac 9a2cc 9991c<> 9a0a0 9a364 99898 9978c 9a160 99ee0 99f74 9969c 99c40<----循环3 996d8 9a620 99a7c 99564 9a6ac 9a2cc 9991c<> 9a0a0 9a364 99898 9978c 9a160 99ee0 99f74 9969c 99c40<----循环4 996d8 9a620 99a7c 99564 9a6ac 9a2cc 9991c<> 9a4a0<差异点> 9984c 9a00c
同时,之前提到的0x9a064位于函数sub_9A00C内部,0x9a064会在每次循环中,会修改下图的X22寄存器指向的地址,这间接改变了Q0的内容,而Q0指向解密后的中间值(之所以称作中间值,是因为CBC模式下,中间值还要和上一轮密文进行异或,才能得到最后的明文),这说明了执行到sub_9A00C的时候,已经完成了AES解密。
开始分析上述函数——从sub_9A260开始。
hook函数sub_994AC的时候,第3个参数是轮密钥。
hook函数sub_9A260,发现下面的情况。
这说明sub_9A260在对密文进行“轮密钥加”。
初始密钥 加密与解密的流程是反过来的,既然a3是轮密钥,可以倒推回初始密钥。
在0xE4FFF398下一个写断点,观察到偏移0x981b4。
在偏移0x981A0处下一个断点,观察x2+x8。
往0xe4fff2f8下一个写断点。
偏移0x96a44,位于函数sub_96A14中。
hook一下函数sub_96A14,打印a1,a1是device_id的前16字节。
因此,aes密钥的种子是device_id。
解密——Td表(未改动) 稍微分析了一下,AES解密采取的方式是查T表,查表的话AES的解密速率会快很多,但没有一般的特征了(行位移、字节替换等),所以不好分析。
来到函数sub_996d8,0x6a5fcc9b是标准Td[0]表的4个字节,打印一下T表。
和标准的Td[0]表一模一样。
另外3个表会基于Td[0]进行生成,简单来说就是对基础表中的每一个32位数值执行字节的循环右移 ,因此可以根据Td[0]生成另外3个T表。经过对比,4个T表都是标准的。
密钥扩展 这里,我们为了追踪密钥扩展的函数,可以追踪初始密钥。
追踪sub_96A14的执行流,寻找对device_id进行处理加密,最终获得初始密钥的位置。
根据分析,在函数sub_96A14、sub_95A3C、sub_97100、sub_960F8中,完成了对初始密钥的构建。
已知,初始密钥是W[3:0],是根据device_id异或4个固定的4字节生成的;(0-3轮密钥)
而接下来的第4-39轮密钥是根据查表的方式生成的。
在日志中,发现了一个规律——最左边框框里面的轮密钥,统一由函数sub_980ac查表获得;左2的框框统一由函数sub_97490查表获得,左3的框框统一由函数sub_97980获得、左4的框框统一由sub_973a8查表获得。
这4个函数都只执行9次,生成了W[39:4],此时,还未知W[43:40]的生成方式,但应该是正常的标准扩展方式。
正常的密钥扩展如下图所示。
我们在日志中搜索W[43:40]的轮密钥——比如:搜索0x1070c115,然后来到第1次出现的地方。注意到,eor的w9和w12中并没有出现0xf0106B39,如果是标准的扩展算法,应该会出现0xf0106B39的。
可以看到,T函数明显被修改了,除此之外,W[43:40]的结果本来是两个轮密钥异或的结果,但在上图中,由一个轮密钥和一个未知的4字节异或得到的。
密钥扩展中最后4轮密钥是如何生成的?涉及到查S盒、字节替换、Rcon等——需要交叉引用96b08、96d5c。
这里简单过一下,具体算法还原先不做,快秋招了,得做点其它的事情,补充一下开发能力QwQ,害,怎么感觉这么难呢,我好菜。
以W[40]为例,W[40] = *(_DWORD *)(a1 + 60) = *(_DWORD *)(a1 + 52) ^ *(_DWORD *)(a1 + 56);
需要hook一下,获得a1+52、a1+44、a1+48等值。
它们的值又来源于sub_97230。
然后又来源于sub_97100。
又可以追回到sub_96A14。
大概意思是,W[43-40]与初始密钥相关,涉及S盒、字节交换、rcon。
具体关于逆T表的工作,等到以后吧,算法还原耗时但一定能解开,hhhhh。
总结 2025/0610/22:23,脑子有点晕,希望能早点找到工作。
参考 jidong大佬的公众号。