# 摘要
分析某乎 是因为有一老乡,刚学unidbg遇到某乎一直没办法正常运行、报错。问了我一句,我出于好奇于是想着帮他看看是哪里出的问题。
# 1.抓包看看某乎在接口上有啥需要特别关注的地方

多次抓包发现每次这个x-zse-96一直是变化的 于是去代码里找找看看具体在哪里。

很好,找了个寂寞,果然没那么简单。看了下网络框架用的okhttp,那么请求头正常来说是放在拦截器中添加的,于是搜索了一下。

找到个感觉有点像的某乎包名下的net包,正常没有混淆的网络请求包基本上也采用这种命名方式,而且也有使用拦截器,点进去看看。最终在这个net包下找到这个类名为j


这个方法为添加请求头方法,怎么定位到的呢?这里的把请求头key做了一个加密。通过frida 去hook 这个方法拿到了x-zse-96 对应的加密串为G51CEEF09BA7DF27F frida hook结果如下:

所以可以定位到了这个位置就是添加请求头的位置 从这里开始找该key 对应的value 是如何生成的。
value的值为 1.0_S85l858ic01VjwtX/xa0+BWZPtg5JRUKVGz3uz0XgD+JXTufPwrDK4sClacJhLk3 由两部分组成,
```
H.d("G38CD8525") + new String(this.f61745c.encode(encrypt)))
```
G38CD8525 代表的值为1.0_
后面这串字符串是上面这个encrypt方法返回,具体分析这个方法。

具体加密为这个c类 的a 方法
参数1:待加密的数组
参数2:一串固定的字符串 通过H.d()方法解密后得到 Aes 的key值
541a3a5896fbefd351917c8251328a236a7efbf27d0fad8283ef59ef07aa386dbb2b1fcbba167135d575877ba0205a02f0aac2d31957bc7f028ed5888d4bbe69ed6768efc15ab703dc0f406b301845a0a64cf3c427c82870053bd7ba6721649c3a9aca8c3c31710a6be5ce71e4686842732d9314d6898cc3fdca075db46d1ccf3a7f9b20615f4a303c5235bd02c5cdc791eb123b9d9f7e72e954de3bcbf7d314064a1eced78d13679d040dd4080640d18c37bbde
参数3:固定的byte数组 16位 看着像Aes 的IV值
```
new byte[]{102, 48, 53, 53, 49, 56, 53, 54, 97, 97, 53, 55, 53, 102, 97, 97}
```
接下来开始分析 b.b() 的入参1 也就是CryptoTool.laesEncryptByteArr() 方法的返回值


这里可以到b.b()方法 嵌套了几层
1.首先先b.a()方法计算CryptoTool.laesEncryptByteArr() 入参1
2.CryptoTool.laesEncryptByteArr()返回值 又作为b.b()方法的入参1
这里其实看看b.a() 、b.b() 方法都可以看到完整的java 实现代码 这里就不分析了,主要看CryptoTool.laesEncryptByteArr()这个方法 因为是 native方法 所以这里直接看下frida hook的结果
把上面这三个方法都hook 一遍看看结果吧 frida hook 代码也送上

~~~
function hookB(){
Java.perform(function(){
var B = Java.use("com.bangcle.b")
B.a.overload('[B', 'java.lang.String', '[B').implementation = function(arg1,arg2,arg3){
// b.a()方法的返回值 是 native方法CryptoTool.laesEncryptByteArr()入参 1
console.log('B.a 参数1 ',bytes2hexstr_1(arg1));
var result = this.a(arg1,arg2,arg3)
console.log("B.a 返回值为 : ",bytes2hexstr_1(result))
return result
}
var CryptoTool = Java.use('com.bangcle.CryptoTool');
CryptoTool.laesEncryptByteArr.overload('[B','java.lang.String','[B').implementation = function (arg1,arg2,arg3) {
console.log('---hook CryptoTool.laesEncryptByteArr---');
//printstack();
// arg:传入的是加密前数据
console.log('CryptoTool 参数1 ',bytes2hexstr_1(arg1));
var ret = this.laesEncryptByteArr(arg1,arg2,arg3);
console.log('CryptoTool 返回值 ',bytes2hexstr_1(ret));
return ret;
}
B.b.overload('[B', 'java.lang.String', '[B').implementation = function(arg1,arg2,arg3){
// b.b()方法的arg1 是 native方法CryptoTool.laesEncryptByteArr()的返回值
console.log("B.b 入参1为 : ",bytes2hexstr_1(arg1))
var result = this.b(arg1,arg2,arg3)
console.log("B.b 结果为 : ",bytes2hexstr_1(result))
return result
}
})
}
function bytes2hexstr_1(arrBytes) {
var str = "";
for (var i = 0; i < arrBytes.length; i++) {
var tmp;
var num = arrBytes[i];
if (num < 0) {
tmp = (255 + num + 1).toString(16);
} else {
tmp = num.toString(16);
}
if (tmp.length == 1) {
tmp = "0" + tmp;
}
str += tmp;
}
return str;
}
~~~
因为项目需要使用到unidbg 黑盒调用,所以这里我们需要使用unidbg来调用 so 的 CryptoTool.laesEncryptByteArr()这个方法返回值给上面截图的b.b()的入参 1进行比对 也就是返回值 必须为下面的值才是正确的
~~~
4d4e2f66e5e7d7cfb4602ddfdf8f82531e8f31be8534921f2df2a281685b8e68ab29cdc87c9a8ac8d4883dd6e865d73d
~~~
# 2.unidbg 调用
ida 打开libbangcle_crypto_tool.so 找到 laesEncryptByteArr() 很明显不是动态注册的,双击然后到混编区域 ,全是这种%1 加密了 。如下图:
上大杀器,用yang神的frida-dump工具重新导入修复 后 的 so包

可以正常看了,F5一波看伪代码
接下来unidbg 开始调用so包 补环境过程就不说了 很简单就是补一个包名就好 具体包名在清单文件中可以找到
```
@Override
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature){
case "android/app/ActivityThread->currentPackageName()Ljava/lang/String;":{
return new StringObject(vm,"com.xxx.android");
}
}
return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
}
```
开始调用这个laesEncryptByteArr()这个方法

提示报错,把日志设置为Debug以后在运行一遍

也是运行到这里开始报错了 ,那么分析就从0xa7d4开始。看ida 跳转到这个地址 F5查看伪代码。
我这边是先 Y键修改下参数类型,这样代码好看些。主要看看这个加密方法调用的时候哪个位置出现的问题。


这里主要看sub_8E74 函数 里的init()方法 因为刚才unidbg调用执行到 这里时候出现的问题怎么看出来的呢?上面截图有执行 unidbg调用的执行过程。如下图:

init方法里面检测包名和检测md5签名

因为我们之前补环境的时候已经补了包名了,但是这里调用记录提示是在获取包名的时候导致程序出错的,这里先断点看看check_package_name方法执行过程

通过不断的s 单步调试找到了出问题的点 如图


这有点懵逼了这个位置报错,找了很久也没发现是出错的原因、所以这里采用了patch 的方式直接nop掉这个init方法,代码如下:
~~~
private void patchInit(){
try (Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb)){
KeystoneEncoded assemble = keystone.assemble("nop;nop");
byte[] machineCode = assemble.getMachineCode(); emulator.getMemory().pointer(module.base+0x8EBC).write(0,machineCode,0,machineCode.length);
}catch (Exception e){
e.printStackTrace();
}
}
~~~
重新运行一次在看看情况、如下图:



成功断下来后 s 继续单步执行 发现程序走的loc_91FC,接下来分析程序走的哪个case代码块。

当执行到R3=0x4的时候这时候遇到b跳转指令那么可知道是执行case 4 的判断,那么继续分析
查看伪代码找到case4 调用的方法如下,前面程序返回错误码也是这个方法返回的,那么具体分析下这个方法。
根据分析ida 汇编执行只有当0x4E88 R3不等于0的情况下程序才会继续往下走,否则直接返回错误码6 具体
如下图:



那么这里最快的解决办法就是把这个R3的值改为大于0 比如说1,程序是不是就可以继续执行了呢?尝试下,因为这if有两个判断条件那么需要 patch两个位置将R3的值都统一改为1,patch 代码如下:
~~~
KeystoneEncoded encoded = keystone.assemble("mov r3, 1");
byte[] patchCode = encoded.getMachineCode();
androidEmulator.getMemory().pointer(moduleModule.base + 0x4e70).write(0, patchCode, 0, patchCode.length);
KeystoneEncoded encoded1 = keystone.assemble("mov r3, 1");
byte[] patchCode1 = encoded1.getMachineCode();
androidEmulator.getMemory().pointer(moduleModule.base + 0x4e84).write(0, patchCode1, 0, patchCode1.length);
~~~
patch后重新运行能正确拿到结果了截图如下:
至于后面的具体计算也就简单了有现成的java代码copy一下就算出来,这里就不详细说明了.这里验证有个问题就是最好是hook拿到b.a()方法的入参1后用copy出来的b.a() java代码进行计算拿到返回值后在调用CryptoTool.laesEncryptByteArr() 这样后方便些。我也是踩坑了几次才知道用这种方式验证结果最快。
至于算法还原,就留到以后在出吧。
安卓逆向入门
最后于 2022-4-5 18:29
被那年没下雪编辑
,原因: