Analysis of a sports APP login protocol

Because I have been busy with exams and haven’t studied for a long time, I analyzed an app at random today and regarded it as resume training.

Analysis of an APP login protocol

1. Packet capture

The main analysis is the mobile phone verification login protocol of the APP. First, start Fillder to start capturing packets, open the app, and enter the relevant mobile phone number and verification code.

Insert picture description here

The captured packages are as follows:

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

Two, X-ads

First analyze x-ads:

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

We searched the string directly and received x-ads ,

Insert picture description here

Insert picture description here

Obviously b() returns the string we want to analyze, go directly in

Insert picture description here
You can see that the b method is to serialize some parameters, and then call mb() to process and return

Follow up

Insert picture description here

At this point we can get that the encryption algorithm is the AES algorithm. After encrypting by the AES algorithm, base64 encoding is performed. Corresponding to the AES algorithm, we can see that the IV has been given 2346892432920300and the key is c(CypLib.a())returned by one .

Insert picture description here

Continue to analyze and find that CypLib.a() has two return values. If it is true, enter the native method for calculation, if it is false, it will directly return a key.
So how to judge this key, there are probably the following ideas

(1)查找a(Context context)的交叉引用,判断在对字符串进行加密处理时,是否调用了该方法,若没有调用
,就会返回false,直接对"Pl*Rxe76fx'fWWqR"加密,反之"Pl*Rxe76fx'fWWqR"就会进入native层进行处理。

(2)由于只有这两个key,我们可以通过穷举的方式,直接分析得出这两个key,当然也是比较复杂的

(3)动态调试smali代码,直接定位到返回的地方,获取c()方法的返回值

(4)frida Hook直接找到c()方法返回值

Obviously through analysis, it is much easier to use dynamic debugging or frida hook. If (1) and (2) are used for analysis, it is very likely that the so file needs to be analyzed. Here you may need to manually copy the so layer pseudo-c code, and then modify and run it. Get the return value.

The frida hook is used directly here. There are many tutorials about frida hook, so I won’t go into details here. Start frida-server and run the script as follows

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()

Finally you can get the key

Insert picture description here

At this point, the main logic of the x-ads algorithm has been figured out,
key=56fe59;82g:d873c,iv=2346892432920300

Then start the decryption operation directly:

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())

Run the python script and get the result

{"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":""}

In the same way, x-device-id can be analyzed

Three, sign

We searched the sign field globally and found a very likely place

Insert picture description here

Through analysis, we can conclude that sb and various values ​​are spliced, and then through lqaypb0.a(sb.toString()), MD5 is performed once to obtain a 32-bit value, and then enter the CrypLib.a() method for judgment

Insert picture description here

Insert picture description here

Here we might as well enter the native layer to see

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;
}

Is a particularly simple algorithm. What is puzzling is

Insert picture description here
A signature verification was actually performed here, and the signature verification value was returned directly. If you want to modify the relevant resource file, it becomes very easy.

Then, we analyze the splicing result of the sb field and directly use frida hook to l.q.a.y.p.b0.a(sb.toString())get the input parameters. The code is still similar to the previous one, but there is a small difference here, a is an overload method, remember to use overload ('Input type') to distinguish

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()

The print result is:

[*] Hook Start...
[*] sb is{"captcha":"1111","countryCode":"86","countryName":"CHN","mobile":"18202870881","type":"login"}/account/v3/login/smsV1QiLCJhbGciOiJIUzI1NiJ9

to sum up

In general, this app is relatively simple, and a lot of logic is not very well written. The key value can be obtained quickly through frida Hook.

Guess you like

Origin blog.csdn.net/weixin_43632667/article/details/107236629