某运动app的登陆协议分析
sign分析#
首先对https进行抓包,密码验证接口的http头部有一个x-sign
字段的值为3bb776e25f1f4f7334f706d723adae79e26a6a56
hook libc.so
的字符串和内存相关处理函数,然后进行栈回溯。可以hookmemcpy, memmove, memcmp, strlen, strstr, strncpy, strncmp, strncat
,通过栈回溯可以看到libcryp.so+0x1e14
位置会调用NewStringUTF
生成sign字符串3bb776e25f1f4f7334f706d723adae79e26a6a56
libcryp.so+0x1e14
位于导出函数Java_com_gotokeep_keep_common_utils_CrypLib_getEncryptDeviceId
,此函数首先通过jni接口反射获取PackageManagerService
中的apk签名信息并进行验证是否为0x5E38A0E8
然后获取从java层传进来的一个0x20长度字符串参数,对这个字符串调用自定义的算法生成8个字节的数据并放置在原字符串尾部,最后调用NewStringUTF
并返回。
因为最后生成的sign是3bb776e25f1f4f7334f706d723adae79e26a6a56
,所以前0x20个字节是3bb776e25f1f4f7334f706d723adae79
,经过上述算法后生成了最后8个字节e26a6a56
。此算法是将原0x20个字符串分成4组,每次倒着取每一组的一个字符参与计算最后生成一个字符,共循环8次,共生成8个字符e26a6a56
。重写算法验证如下。
void getTail(const char* input, char* output) {
int sum = 0;
int value = 0;
int index = 0;
int v1 = 0, v2 = 0, v3 = 0, v4 = 0, v5 = 0, v6 = 0;
for (index = 0; index + 8 >= 0; index--) {
sum = 0;
for (int i = 0; i < 4; i++) {
value = input[index + 7 + i * 8];
v1 = value - '0';
if (v1 > 9) {
if (value - 'a' > 5) {
if (value - 'A' >= 6) {
v1 = 0;
}
else {
v1 = value - '7';
}
}
else {
v1 = value - 'W';
}
}
sum = sum + v1;
}
sum = sum + v2;
v3 = sum + 0x3A1;
v4 = sum + 0x3B0;
if (v3 >= 0)
v5 = v3;
else
v5 = v4;
v2 = v5 >> 4;
v6 = v3 - (v5 & 0xFFFFFFF0);
if (v6 >= 10)
output[index + 7] = 0x57 + v6;
else
output[index + 7] = 0x30 + v6;
}
}
那么java层传入的0x20大小的字符串参数是什么呢!继续分析java层代码,因为Java_com_gotokeep_keep_common_utils_CrypLib_getEncryptDeviceId
是静态注册,直接找到所有getEncryptDeviceId
函数的引用(可以hook此函数查看栈回溯寻找),看到一个intercept
函数。
查看intercept
函数,看到sb4字符串经过四次拼接后调用MessageDigest.getInstance(EvpMdRef.MD5.JCA_NAME)
进行MD5
得到hash值,用hash值作为参数调用getEncryptDeviceId
函数。
通过hook四次拼接中的相关函数得到每一次拼接的字符串。
Java.perform(function(){
//com.gotokeep.keep.common.utils.n h()
Java.use("com.gotokeep.keep.common.utils.n").h.implementation = function (str, num) {
var result = this.h(str, num)
console.log("string4 : ", str, " >> ", num, " to ", result)
return result
};
//js.b.intercept(okhttp3.j.a aVar)
Java.use("js.b").intercept.overload('okhttp3.j$a').implementation = function(aVar){
//vk3.c.B0() String
Java.use("vk3.c").B0.implementation = function(){
var ret = this.B0()
console.log("string1 : ", ret)
return ret;
}
//js.b.b(hashMap) String
Java.use("js.b").b.implementation = function(hashMap){
var ret = this.b(hashMap)
console.log("string2 : ", ret)
return ret;
}
//hk3.m.d() String
Java.use("hk3.m").d.implementation = function(){
var ret = this.d()
console.log("string3 : ", ret)
return ret;
}
//com.gotokeep.keep.common.utils.h0.e(str)
Java.use("com.gotokeep.keep.common.utils.h0").e.implementation = function(str){
var ret = this.e(str)
console.log("target string : ", ret)
return ret;
}
return this.intercept(aVar)
}
})
最后的拼接过程如下:第一次拼接的数据是body,第二次拼接的数据为空,第三次拼接的数据是url,第四次拼接的数据是字符串vQiLcJhbGcioijIUzI1NiJ9
(T/OgJAHf`Eag
每个字符加2 + KeFEQvE-JeF5
每个字符加4)。最后经过md5 hash后生成0x20个字节就是sign的前0x20个字节。
sign的算法就是MD5(body + url + vQiLcJhbGcioijIUzI1NiJ9) + getTail(MD5(body + url + vQiLcJhbGcioijIUzI1NiJ9))
body分析#
分析sign的时候看到第一次拼接的数据就是body,body是通过调用hk3.b.a()
函数得到的。
hk3.b.a()
获取的值是在hk3.q.$init
构造函数中初始化的。
hook hk3.q.$init
构造函数并进行栈回溯,可以看到其使用的是okhttp网络框架,定位到关键函数com.gotokeep.keep.fd.business.account.login.mvp.pnresenter.LoginNainActionPresenter.e()
com.gotokeep.keep.fd.business.account.login.mvp.pnresenter.LoginNainActionPresenter.e()
会设置mobile
, countryCode
, countryName
, password
, oaid
, android_id
这些LoginParams
登陆参数,然后调用com.gotokeep.keep.common.utils.n.i
进行加密,加密之后调用retrofit2.g$b.enqueue()
进行异步网络请求。
查看com.gotokeep.keep.common.utils.n.i
是如何对LoginParams
登陆参数进行加密的,首先将LoginParams
登陆参数格式化为js格式,然后调用com.gotokeep.keep.common.utils.n.g
对js数据进行加密
查看com.gotokeep.keep.common.utils.n.g
发现是一个AES CBC
加密。调用Java_com_gotokeep_keep_common_utils_CrypLib_getCrypKey
将Pl*Rxe76fx'fWWqR
生成一个hash值,然后调用com.gotokeep.keep.common.utils.n.h(str, 2)
将hash值的每一个字符都加2得到key,iv为2346892432920300
,最后调用AES CBC加密后进行base64编码。
看一下Java_com_gotokeep_keep_common_utils_CrypLib_getCrypKey
函数发现是一个hash算法。
unidbg
跑一下,其会调用两次自定义的hash,每次hash会生成16个字节的散列值,最后一次hash的散列值的5-12个字节就是最后的返回结果34dc37960e8b651a
hook整个加密过程中的相关函数
Java.perform(function(){
//com.gotokeep.keep.common.utils.CrypLib getCrypKey()
Java.use("com.gotokeep.keep.common.utils.CrypLib").getCrypKey.implementation = function(str){
var result = this.getCrypKey(str)
console.log(str, " to ", result)
return result
}
//com.gotokeep.keep.common.utils.n h()
Java.use("com.gotokeep.keep.common.utils.n").h.implementation = function (str, num) {
var result = this.h(str, num)
console.log(str, " >> ", num, " to ", result)
return result
};
//javax.crypto.spec.SecretKeySpec
Java.use("javax.crypto.spec.SecretKeySpec").$init.overload('[B', 'java.lang.String').implementation = function(key, str){
console.log("AES key : ", byteToString(key))
return this.$init(key, str)
}
//javax.crypto.spec.IvParameterSpec
Java.use("javax.crypto.spec.IvParameterSpec").$init.overload('[B').implementation = function(iv){
console.log("AES iv : ", byteToString(iv))
return this.$init(iv)
}
//javax.crypto.Cipher doFinal(bytes3)
Java.use("javax.crypto.Cipher").doFinal.overload('[B').implementation = function(data){
console.log("data : ", byteToString(data))
var result = this.doFinal(data)
console.log("AES encrypy data : ", result)
return result
}
//android.util.Base64 encode()
Java.use("android.util.Base64").encode.overload('[B', 'int').implementation = function(data, flag){
var result = this.encode(data, flag)
console.log("BASE64(AES encrypt data) : ", byteToString(result))
return result
}
})
得到body的加密过程如下。
key:调用Java_com_gotokeep_keep_common_utils_CrypLib_getCrypKey
将Pl*Rxe76fx'fWWqR
生成的hash值为34dc37960e8b651a
,34dc37960e8b651a
的每个字符加2得到Key为56fe59;82g:d873c
iv:2346892432920300
data:{"androidId":"14c5efodfe4e773f","countrycode":"86","countrywame":"CHN","mobile":"19137031751", "password":"qqq123456789"}
BASE64(AES encrypt data):/6jBElbU5ddJH9AiLtUWf370Drsp+BVsNaLsHbO3Twk7uu86z4a8T+vavaL99SXObrBGJI94g4MSOFPb05KQ6MrwTX0xQyNJpZwMmHgmXvRCGgI5hQLYNsO+XVTIZypSApnIMqT5qRgI/ed1I21XfV2SB2VkHOn8EB/3Le4Pbh8=
网络请求定位数据包和相关字段加解密关键位置#
-
通过
url
以及请求的字段去搜索组包位置,注意通过一些此url
特有的字段进行筛选,同时字段组包的顺序也是一个重要的筛选条件。 -
hook 字符串和内存相关处理函数
memcpy, memmove, memcmp, strlen, strstr, strncpy, strncmp, strncat
,然后进行栈回溯。因为范围较大不好筛选,效果不是很好。 -
对字符串相关jni接口函数进行hook,然后进行栈回溯。(
libart.so
的NewStringUTF
等) -
对java层加解密接口进行hook,对比被加密的数据和加密后的数据进行栈回溯。(
javax.crypto.spec.SecretKeySpec
:key,javax.crypto.spec.IvParameterSpec
:iv,javax.crypto.Cipher getInstance
:加密方式,javax.crypto.Cipher init
:初始化设置key和iv,javax.crypto.Cipher doFinal
:加密,android.util.Base64 encode
:BASE64编码) -
对java层网络框架(
OKHttpCall.enqueue/execute
),native层socket
,ssl_write
和ssl_read
等进行hook,与抓包数据进行对比并进行栈回溯。
总结:apk java层和so层均为加壳,so层没有ollvm等混淆,无反调试和反frida,使用标准加解密算法,没有防抓包,整体分析较简单,主要是逆向思路的熟悉。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
2022-10-10 android反调试技术
2022-10-10 android hook之ELF hook