某运动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,第四次拼接的数据是字符串vQiLcJhbGcioijIUzI1NiJ9T/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_getCrypKeyPl*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_getCrypKeyPl*Rxe76fx'fWWqR生成的hash值为34dc37960e8b651a34dc37960e8b651a的每个字符加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=

网络请求定位数据包和相关字段加解密关键位置

  1. 通过url以及请求的字段去搜索组包位置,注意通过一些此url特有的字段进行筛选,同时字段组包的顺序也是一个重要的筛选条件。

  2. hook 字符串和内存相关处理函数memcpy, memmove, memcmp, strlen, strstr, strncpy, strncmp, strncat,然后进行栈回溯。因为范围较大不好筛选,效果不是很好。

  3. 对字符串相关jni接口函数进行hook,然后进行栈回溯。(libart.soNewStringUTF等)

  4. 对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编码)

  5. 对java层网络框架(OKHttpCall.enqueue/execute),native层socketssl_writessl_read等进行hook,与抓包数据进行对比并进行栈回溯。

总结:apk java层和so层均为加壳,so层没有ollvm等混淆,无反调试和反frida,使用标准加解密算法,没有防抓包,整体分析较简单,主要是逆向思路的熟悉。

posted @ 2023-10-10 16:19  怎么可以吃突突  阅读(155)  评论(0编辑  收藏  举报