因为一直忙于考试,很久没有学习了,今天随意分析了一个app就当是恢复训练了
主要分析的是该APP的手机验证登录协议,首先启动Fillder开始抓包,打开该app,输入相关手机号和验证码
抓到的包如下:
POST https://api.gotokeep.com/account/v2/sms HTTP/1.1
Content-Type: application/json; charset=UTF-8
Content-Length: 163
Host: api.gotokeep.com
Connection: Keep-Alive
Accept-Encoding: gzip
x-os-version: 5.1.1
x-geo: 0.0,0.0
x-channel: adhub_cpa__siruifan_04
x-ads: gYdZHMFkdTLC1JvfK4AsSmouEN3+S+OsjkFGaZ9kygpo/FGeJ5N7iJdMyIBtadqqUBgJnRBFi+Ri6KYKwFCVqFVPgWMaqbAAp5JiY9yFM4rGVByS9EsGHY0DB9ekNkSw+9v/Xu/BsbMxxEm9Et4MJT4QbLGf9ifhngIO3aDdE3bN+3aB801nvmOb8ZYsJ2U4Mw2sAGcnJrcDFU2kX3ASbUfhJYGWCkELV8Y6iKoPrPr/wNfkDmtuWUNYUSpSdYlPWWrANkgrPW5YL4wWIQwfVf7cDX5ItxTOGl00XYlKUcnwOsm6JAjdF5Voc0u9mmiT
x-locale: zh--CN
x-screen-height: 800
x-is-new-device: true
x-carrier: 70120
User-Agent: Keep+6.43.0%2FAndroid+5.1.1-24528+Xiaomi+MI+9
x-manufacturer: Xiaomi
x-keep-timezone: Asia/Shanghai
x-timestamp: 1594278593217
x-screen-width: 450
x-connection-type: 4
x-app-platform: keepapp
x-os: Android
x-device-id: 865166020644319111111111111111119da6a866
x-version-name: 6.43.0
x-user-id:
x-version-code: 24528
x-model: MI+9
sign: 5257caf9b35de7f6f9805f3e814773036f6c73e1
首先分析x-ads:
x-ads: gYdZHMFkdTLC1JvfK4AsSmouEN3+S+OsjkFGaZ9kygpo/FGeJ5N7iJdMyIBtadqqUBgJnRBFi+Ri6KYKwFCVqFVPgWMaqbAAp5JiY9yFM4rGVByS9EsGHY0DB9ekNkSw+9v/Xu/BsbMxxEm9Et4MJT4QbLGf9ifhngIO3aDdE3bN+3aB801nvmOb8ZYsJ2U4Mw2sAGcnJrcDFU2kX3ASbUfhJYGWCkELV8Y6iKoPrPr/wNfkDmtuWUNYUSpSdYlPWWrANkgrPW5YL4wWIQwfVf7cDX5ItxTOGl00XYlKUcnwOsm6JAjdF5Voc0u9mmiT
我们通直接搜索字符串,收到了x-ads,
显然b()返回的就是我们要分析的字符串,直接进去
可以看到b方法也就是将一些参数进行序列化,然后调用m.b()处理后返回
继续跟进
到这里我们就可以得到这个加密算法是AES算法,通过AES算法进行加密后再进行base64编码,对应AES算法,我们可以看到IV已经给出2346892432920300
,key是由一个c(CypLib.a())
返回的
继续分析发现CypLib.a()有两种返回值,若为true则进入native方法进行计算,若为false,则直接返回一个key。
那么怎么判断这个key呢,大概有下面几种思路
(1)查找a(Context context)的交叉引用,判断在对字符串进行加密处理时,是否调用了该方法,若没有调用
,就会返回false,直接对"Pl*Rxe76fx'fWWqR"加密,反之"Pl*Rxe76fx'fWWqR"就会进入native层进行处理。
(2)由于只有这两个key,我们可以通过穷举的方式,直接分析得出这两个key,当然也是比较复杂的
(3)动态调试smali代码,直接定位到返回的地方,获取c()方法的返回值
(4)frida Hook直接找到c()方法返回值
显然通过分析,使用动态调试或者frida hook会简单很多,如果采用(1),(2)分析的话就很有可能需要分析so文件,这里可能需要手动copy一下so层伪c代码,然后修改运行,得到返回值。
这里直接采用frida hook,关于frida hook的教程很多,这里不多赘述,启动frida-server,运行脚本如下
import frida, sys
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)
jscode = """
Java.perform(function () {
var m = Java.use('l.q.a.y.p.m');
m.c.implementation = function (param1) {
send("Hook Start...");
send(param1);
var result=this.c(param1);
send("AESKey is :"+result);
return result;
}
});
"""
process = frida.get_usb_device().attach('com.gotokeep.keep')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()
最后可以得到key
到这一步,x-ads算法的主要逻辑就已经弄清楚了,
key=56fe59;82g:d873c,iv=2346892432920300
那么直接开始解密操作:
import base64
from Crypto.Cipher import AES
def base64_encode(bdate):
return base64.b64encode(bdate).decode()
def base64_decode(date_str):
return base64.b64decode(date_str)
def aes_decrypt(Key,iv,crypted):
decryptor=AES.new(Key,AES.MODE_CBC,iv=iv)
return decryptor.decrypt(crypted)
def aes_encrypt(Key,iv,raw_data):
if isinstance(raw_data,str):
raw_data=raw_data.encode()
#不满足8的倍数补齐
if len(raw_data)%8 !=0:
pad_len=8-(len(raw_data)%8)
raw_data+=bytes([pad_len]*pad_len)
encryptor=AES.new(Key,AES.MODE_CBC,iv=iv)
return encryptor.encrypt(raw_data)
if __name__=='__main__':
import json
key='56fe59;82g:d873c'.encode()
iv='2346892432920300'.encode()
data='gYdZHMFkdTLC1JvfK4AsSmouEN3+S+OsjkFGaZ9kygpo/FGeJ5N7iJdMyIBtadqqUBgJnRBFi+Ri6KYKwFCVqFVPgWMaqbAAp5JiY9yFM4rGVByS9EsGHY0DB9ekNkSw+9v/Xu/BsbMxxEm9Et4MJT4QbLGf9ifhngIO3aDdE3bN+3aB801nvmOb8ZYsJ2U4Mw2sAGcnJrcDFU2kX3ASbUfhJYGWCkELV8Y6iKoPrPr/wNfkDmtuWUNYUSpSdYlPWWrANkgrPW5YL4wWIQwfVf7cDX5ItxTOGl00XYlKUcnwOsm6JAjdF5Voc0u9mmiT'
crypted=base64_decode(data)
decrypt=aes_decrypt(key,iv,crypted)
print(decrypt.decode())
运行python脚本,得到结果
{"imei":"865166020644319","adua":"Mozilla\/5.0 (Linux; Android 5.1.1; MI 9) AppleWebKit\/537.36 (KHTML, like Gecko) Version\/4.0 Chrome\/70.0.3538.64 Mobile Safari\/537.36","androidId":"e2437690c1181ef5","device":"phone","oaid":""}
同理,可以分析得出x-device-id
我们全局搜索sign字段,发现了一个极有可能的地方
通过分析我们可以得出,sb和各种值进行的拼接,然后通过l.q.a.y.p.b0.a(sb.toString()),进行了一次MD5得到32位的值,再进入CrypLib.a()方法中进行判断
这里我们不妨进入native层看看
jstring __fastcall Java_com_gotokeep_keep_common_utils_CrypLib_getEncryptDeviceId(JNIEnv *a1, jclass a2, jstring a3)
{
int v3; // r3
int v4; // r0
int v5; // r3
jstring v7; // [sp+4h] [bp-70h]
JNIEnv *v8; // [sp+Ch] [bp-68h]
int v9; // [sp+14h] [bp-60h]
signed int i; // [sp+18h] [bp-5Ch]
char *s; // [sp+1Ch] [bp-58h]
int v12; // [sp+24h] [bp-50h]
int v13; // [sp+28h] [bp-4Ch]
int v14; // [sp+2Ch] [bp-48h]
int v15; // [sp+34h] [bp-40h]
int v16; // [sp+38h] [bp-3Ch]
int v17; // [sp+3Ch] [bp-38h]
int v18; // [sp+40h] [bp-34h]
int v19; // [sp+44h] [bp-30h]
int v20; // [sp+48h] [bp-2Ch]
int v21; // [sp+4Ch] [bp-28h]
int v22; // [sp+50h] [bp-24h]
int v23; // [sp+54h] [bp-20h]
int v24; // [sp+58h] [bp-1Ch]
int v25; // [sp+5Ch] [bp-18h]
char v26; // [sp+60h] [bp-14h]
char v27[12]; // [sp+68h] [bp-Ch]
v8 = a1;
v7 = a3;
if ( getSignHashCode(a1) != 1580769512 )
return v7;
s = ((*v8)->GetStringUTFChars)(v8, v7, 0);
v16 = 0;
v17 = 0;
v18 = 0;
v19 = 0;
v20 = 0;
v21 = 0;
v22 = 0;
v23 = 0;
v24 = 0;
v25 = 0;
v26 = 0;
if ( strlen(s) == 32 )
{
v9 = 0;
for ( i = 7; i >= 0; --i )
{
v27[i - 48] = s[i];
v27[i - 40] = s[i + 8];
v27[i - 32] = s[i + 16];
v27[i - 24] = s[i + 24];
v12 = get_int(s[i]);
v13 = get_int(s[i + 8]);
v14 = get_int(s[i + 16]);
v4 = get_int(s[i + 24]);
v15 = v12 + v13 + v14 + v4 + v9 + 929;
v5 = v12 + v13 + v14 + v4 + v9 + 929;
if ( v5 < 0 )
v5 = v12 + v13 + v14 + v4 + v9 + 944;
v9 = v5 >> 4;
v27[i - 16] = get_char(v15 % 16);
}
((*v8)->ReleaseStringUTFChars)(v8, v7, s);
v3 = ((*v8)->NewStringUTF)(v8, &v16);
}
else
{
((*v8)->ReleaseStringUTFChars)(v8, v7, s);
v3 = v7;
}
return v3;
}
是一个特别简单的算法,让人不解的是
这里居然进行了一次签名校验,而且居然直接把签名校验值返回了,看到这里如果想要修改相关资源文件就变得很容易了
然后,我们分析一下sb字段的拼接结果,直接使用frida hook l.q.a.y.p.b0.a(sb.toString())
获取到入参即可,代码还是和上一个差不多,但这里有一个小小的不同点,a是一个重载方法,要记得用overload(‘入参类型’)来区别
import frida, sys
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)
jscode = """
Java.perform(function () {
var b0 = Java.use('l.q.a.y.p.b0');
b0.a.overload('java.lang.String').implementation = function (param1) {
send("Hook Start...");
send("sb is"+param1);
var result=this.a(param1);
send("ret is :"+result);
return result;
}
});
"""
process = frida.get_usb_device().attach('com.gotokeep.keep')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()
打印结果为:
[*] Hook Start...
[*] sb is{"captcha":"1111","countryCode":"86","countryName":"CHN","mobile":"18202870881","type":"login"}/account/v3/login/smsV1QiLCJhbGciOiJIUzI1NiJ9
总的来说,这个app比较简单,而且很多的逻辑写的也不是很好,通过frida Hook就能很快的获取关键值