爬虫之- 某生鲜APP 加密参数逆向分析
本文由 简悦
SimpRead 转码, 原文地址 www.yuanrenxue.com
本文是跟我学习爬虫的小伙伴:彭良怀的投稿,稿费是
500 。
PS:他在北京,有看上的老板可以私信我,为人也不错。
学了一段时间
使用到的工具如下:
- 一部
root 后的安卓手机,模拟器也可以 - 抓包工具:Charles
- 查壳工具:APK Messenger
APK 反编译工具:jadx-gui 1.1SO 文件分析工具:IDA_Pro_v7.0Hook 框架:frida
二、抓包分析
首先手机配置好代理,打开

可以看到很多的请求参数,翻页再抓包一次,把两次抓到的参数进行比对,看看哪些参数固定,哪些是变化的。这么多参数,要是自己用肉眼看,那就太费劲了,而且还容易看漏,所以直接用在线文本对比工具吧,我用的是这个网站:https://qqe2.com/word/diff,把两次抓包的参数复制上去,如下图所示:

不同的参数都高亮出来了,一目了然。我简单分析如下:
- signKey:密文,长度
32 位,可能为MD5 、HmacMD5 加密或随机UUID - signKeyV1:密文,长度
64 位,可能为SHA256 、HmacSHA256 加密 - t:
13 位时间戳 - traceId:等于
deviceId (固定的设备ID )加两个13 位时间戳 - currentPage:页码
- lastStoreId:上一页最后一家店铺
ID
其他固定参数很好理解,我就不阐述了。通过模拟请求验证,修改任意参数的值都无法获取数据,所以推测
三、Java 层分析
1. 查壳
在反编译

结果疑似无壳,接下来就可以使用
2. 分析关键Java 代码
用

这段代码很好理解,明显是在组装参数,可以从中得出以下信息:
t 为当前时间戳;subVersion 为当前APP 版本号;signKey 是由k 方法生成的;signKeyV1 等于KEY_NEW_SIGN ,KEY_NEW_SIGN 又是由k2 方法生成的;- 传入方法
k2 的参数为formatQueryParaMap 方法的返回值; - 方法
k 和k2 都在native 层,加载的是libjdpdj.so 文件;
3. 分析formatQueryParaMap 方法

这段代码也好理解,传入该方法的第一个参数为
def formatQueryParaMap(param: dict) -> str:
return '&'.join(param[k] for k in sorted(param.keys()) if k != 'functionId')
4. Hook formatQueryParaMap 方法
如果看不懂或不想分析
Java.perform(function () {
var util = Java.use('jd.net.ASCIISortUtil');
util.formatQueryParaMap.implementation = function (arg1, arg2) {
console.log('param1: ', arg1);
console.log('param2: ', arg2);
var result = this.formatQueryParaMap(arg1, arg2);
console.log('return: ', result);
return result;
};
})
打印结果如下:

很明显,
5. 关于反调试
这里提一下,该
四、Native 层分析
通过
1. 静态注册和动态注册
因为
-
静态注册:
静态注册是通过固定格式方法名进行关联,命名规则如下:
native 函数名= Java + 包名+ 类名+ 方法名例如,包名
: com.example.test ,类名:jd.net.z,方法名:k如果是静态注册的话,那么
native 中的函数名就该为:Java_com_example_test_jd_net_z_k -
动态注册:
动态注册是通过
RegisterNative() 这个JNI 函数动态添加映射关系来进行关联的,这种方式可以随便命名函数名,比较灵活。其申明示例如下: -
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod* methods, jint nMethods)
第
1 个参数是JNIEnv 指针,所有JNI 函数第一个参数都是它;第
2 个参数clazz 是注册方法对应Java 层中的类,由FindClass 函数获取;第
3 个参数methods 是一个数组,其中包含了注册方法结构体信息,我们可以从中找到注册前后的方法名,所以我们注意这个参数就行了;第
4 个参数nMethods 是动态注册方法的数量。
2. 找到k 、k2 对应的native 函数
知道了
因为
凡是看到类似

根据前面的介绍,我们只需要看第

从
搜搜看,这就很容易找到了:

不过有些
3. JNI 静态调试的一些技巧
在分析
(1) 批量还原JNI 函数名
- 按
Ctrl + F9 ,选择jni.h 头文件导入 - 导入成功后,鼠标左键点击其中一个
JNI 函数的参数,然后右键选择Convert to Struct * - 在弹出的
Select a structure 窗口中 选择_JNIEnv ,点击OK
这样就可以把当前打开的
(2) 强制调出函数参数
有时会遇到

这时需要鼠标左键点击该函数,然后鼠标右键选择
(3) 常用快捷键
-
shift + F12:查看
so 文件中所有常量字符串的值; -
tab 键:汇编和伪C 代码之间相互切换; -
/ 键:添加注释; -
N 键:变量重命名; -
X 键:查看某变量的所有引用; -
= 键:消除冗余的中间变量;由于
IDA 反编译出来总是会有很多冗余的中间变量,如:v2 = v1;
result = encrypt(v2);选中
v2 ,按键盘上的= 键,再点击OK ,即可消除中间变量v2 :result = encrypt(v1);
(4) 静态调试思路
-
根据函数入参,至上而下分析
-
根据函数返回值,至下而上分析
-
寻找关键的函数进行分析,一般可以把函数分为以下几种:
① 标准库函数:如
strlen() ,计算字符串的长度,见名知意;②
JNI 函数:如FindClass() ,调用Java 中的类,JNI 函数一般也是见名知意;③ 用户自定义的函数:如
MD5::MD5() ,一看就知道是MD5 加密,这类需特别注意;④
IDA 命名的函数:如sub 567C() ,IDA 会对没有名字的函数自动命名,命名规则就是sub + 函数地址,这类函数也是重点。从追求效率的角度来说,最好先找关键函数,看看有没有常见的加密函数名,找到后直接用
frida hook ,一些简单的往往能够一击中的,快速破解。从学技术的角度来说,可以多尝试一行一行代码地分析,锻炼看代码的能力。当然复杂点的还不得不分析arm 指令,要是被混淆后就更加难了,难的我也不会,以后多练多学吧。
4. 静态分析gk 函数
接下来开始具体操作吧,双击

很明显是
获取导出函数的绝对地址
var onload_addr = Module.getExportByName('libjdpdj.so', 'JNI_OnLoad');
获取未导出函数的绝对地址,我列举以下
-
方式一:
-
var onload_addr = Module.getExportByName('libjdpdj.so', 'JNI_OnLoad'); var base_addr = parseInt(onload_addr ) - parseInt('0x34D6C'); var md5_update_addr = ptr(base_addr + parseInt('0x34E18'));
-
方式二:
-
var onload_addr = Module.getExportByName('libjdpdj.so', 'JNI_OnLoad'); var md5_update_addr = onload_addr.sub(0x34D6C).add(0x34E18);
-
方式三:
-
var md5_update_addr = Module.findBaseAddress("libjdpdj.so").add(0x34E18 + 1);
方式一看注释很好理解,方式二其实就是方式一的简化,用
frida 提供的的add() 和sub() 函数进行地址的加减。方式三是进一步简化,但是用这种方式一定要记得对地址+1 ,为什么要+1 呢?我引用赵四的原话解释吧:因为
thumb 和arm 指令的区分,地址最后一位的奇偶性来进行标志获取未导出函数地址的方式也完全适用于导出函数,所以不管导出还是未导出,我都用方式三获取,代码简单优雅。
那么我们
var pointer = Module.findBaseAddress("libjdpdj.so").add(0x34E18 + 1);
console.log('MD5Update pointer:', pointer);
Interceptor.attach(pointer, {
onEnter: function(args) {
console.log('参数1:', args[0]);
console.log('参数2:', Memory.readCString(args[1]));
console.log('参数3:', parseInt(args[2]));
console.log('----------------');
},
onLeave: function(retval) {
}
})
MD5Update pointer: 0xaed5ae19
参数1: 0xbef0eb8c
参数2: {"city":"重庆市","latitude":29.57252,"longitude":106.53355,"address":"观音桥", "coordType":"2","channelId":"4037","appVersion":"7.4.0","platform":"2","currentPage":1, "pageSize":10,"areaCode":4,"ref":"home","ctp":"channel"}923047ae3f8d11d8b19aeb9f3d1bc002
参数3: 259
—————-
可以看到参数
5. 静态分析gk2 函数
然后再来看

很明显是
var pointer = Module.findBaseAddress("libjdpdj.so").add(0x361B8 + 1);
console.log("hmac_sha256 pointer: ", pointer);
Interceptor.attach(pointer, {
onEnter: function(args) {
console.log("参数1:", Memory.readUtf8String(args[0]));
console.log("参数2:", parseInt(args[1]));
console.log("参数3:", Memory.readCString(args[2]));
console.log("参数4:", parseInt(args[3]));
console.log('---------------');
},
onLeave:function(retval){
}
});

参数
抱着学习的心态再去分析一下伪
- 先调用
java 层的getsign 方法获取基础key , - 对基础
key 每个字符的ASCII 码进行修改,同时拼接到输入参数的尾部, - 取出入参尾部的
32 位作为密钥, - 最后对输入参数进行
hmac_sha256 加密,通过指针返回加密结果。
逆向到这儿就结束了,后面用
6. native 函数的参数
我再啰嗦一下,

其中
五、总结
本篇文章的案例
再次跨一下这篇文章,非常不错,继续接受投稿,稿费还不错
PS,给自己广告一下:我继续在教爬虫,真正的爬虫技术。教
感兴趣的加我微信私聊,备注:爬虫。
最近打算建一个爬虫技术交流群,感兴趣的也可以加我。
