某运动APP登录协议分析

因为一直忙于考试,很久没有学习了,今天随意分析了一个app就当是恢复训练了

某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:

x-ads: gYdZHMFkdTLC1JvfK4AsSmouEN3+S+OsjkFGaZ9kygpo/FGeJ5N7iJdMyIBtadqqUBgJnRBFi+Ri6KYKwFCVqFVPgWMaqbAAp5JiY9yFM4rGVByS9EsGHY0DB9ekNkSw+9v/Xu/BsbMxxEm9Et4MJT4QbLGf9ifhngIO3aDdE3bN+3aB801nvmOb8ZYsJ2U4Mw2sAGcnJrcDFU2kX3ASbUfhJYGWCkELV8Y6iKoPrPr/wNfkDmtuWUNYUSpSdYlPWWrANkgrPW5YL4wWIQwfVf7cDX5ItxTOGl00XYlKUcnwOsm6JAjdF5Voc0u9mmiT

我们通直接搜索字符串,收到了x-ads

在这里插入图片描述

在这里插入图片描述

显然b()返回的就是我们要分析的字符串,直接进去

在这里插入图片描述
可以看到b方法也就是将一些参数进行序列化,然后调用m.b()处理后返回

继续跟进

扫描二维码关注公众号,回复: 11858807 查看本文章

在这里插入图片描述

到这里我们就可以得到这个加密算法是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

我们全局搜索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就能很快的获取关键值

猜你喜欢

转载自blog.csdn.net/weixin_43632667/article/details/107236629
今日推荐