首页
社区
课程
招聘
[原创]某运动APP的登录协议分析
2023-8-9 14:55 8784

[原创]某运动APP的登录协议分析

2023-8-9 14:55
8784

前言

最近在尝试逆向方向相关的探索,针对某款运动APP的登录协议进行了分析,此文记录一下分析过程与结果,仅供学习研究,使用的工具较少,内容也比较简单,新手项,大佬请跳过。针对密码登录模块进行分析,随便输入一个手机号与密码,后续使用抓包工具分析,针对登录协议的几个字段从学习角度还是值得看下实现逻辑的。

抓包

  1. 抓包使用 Charles,请自行安装并配置证书
  2. 抓取登陆接口,点击密码登陆。使用假账密测试抓包,能够抓包成功

图片描述

Sign分析

首先能看到请求头里面有sign字段,针对该字段进行分析:

sign: b61df9a8bce7a8641c5ca986b55670e633a7ab29

整体长度为40,常用的MD5长度为32,第一反应不太像,但是也有可能md5以后再拼接其它字段,sha1散列函数的长度是40,正好吻合。那我们就一一验证,先看下是否有MD5的痕迹,直接写脚本frida试着跑下。 脚本内容比较明确,针对MD5的Init、Update、Final分别hook打印看下输入与输出,下面给到关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// hook CC_MD5
// unsigned char * CC_MD5(const void *data, CC_LONG len, unsigned char *md);
Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_funcName), {
    onEnter: function(args) {
        console.log(g_funcName + " begin");
        var len = args[1].toInt32();
        console.log("input:");
        dumpBytes(args[0], len);
        this.md = args[2];
    },
    onLeave: function(retval) {
        console.log(g_funcName + " return value");
        dumpBytes(this.md, g_funcRetvalLength);
 
        console.log(g_funcName + ' called from:\n' +
            Thread.backtrace(this.context, Backtracer.ACCURATE)
            .map(DebugSymbol.fromAddress).join('\n') + '\n');
    }
});
 
// hook CC_MD5_Update
// int CC_MD5_Update(CC_MD5_CTX *c, const void *data, CC_LONG len);
Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_updateFuncName), {
    onEnter: function(args) {
        console.log(g_updateFuncName + " begin");
        var len = args[2].toInt32();
        console.log("input:");
        dumpBytes(args[1], len);
    },
    onLeave: function(retval) {
        console.log(g_updateFuncName + ' called from:\n' +
            Thread.backtrace(this.context, Backtracer.ACCURATE)
            .map(DebugSymbol.fromAddress).join('\n') + '\n');
    }
});
 
// hook CC_MD5_Final
// int CC_MD5_Final(unsigned char *md, CC_MD5_CTX *c);
Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_finalFuncName), {
    onEnter: function(args) {
        //console.log(func.name + " begin");
        finalArgs_md = args[0];
    },
    onLeave: function(retval) {
        console.log(g_finalFuncName + " return value");
        dumpBytes(finalArgs_md, g_funcRetvalLength);
 
        console.log(g_finalFuncName + ' called from:\n' +
            Thread.backtrace(this.context, Backtracer.ACCURATE)
            .map(DebugSymbol.fromAddress).join('\n') + '\n');
    }
});

很幸运,在打印中明显看到了sign相关的内容打印,但是缺少sign的后面一部分,那就明确sign值的构成为32(md5)+8,先看下md5的数据构造过程。

b61df9a8bce7a8641c5ca986b55670e6 33a7ab29

图片描述

通过打印可以明确的看到,sign的MD5由三部分数据组成,分别为:bodyData+Url+Str,body数据也可从Charles获取到。

  • {"body":"5gJEXtLqe3tzRsP8a/bSwehe0ta3zQx6wG7K74sOeXQ6Auz1NI1bg68wNLmj1e5Xl7CIwWelukC445W7HXxJY6nQ0v0SUg1tVyWS5L8E2oaCgoSeC6ypFNXV2xVm8hHV"}

  • /account/v4/login/password

  • V1QiLCJhbGciOiJIUzI1NiJ9

图片描述

到这里有一个疑问,数据的第三部分:V1QiLCJhbGciOiJIUzI1NiJ9,该值是固定的字符串还是每次都变化的?猜测应该是固定的字符串,作为MD5的Salt值来使用,我们再次请求验证一下。

图片描述

新的sign值为:131329a5af4ecb025fb5088615d5e5c526dbd1a3,通过脚本打印的数据能确认第三部分为固定字符串。

MD5({"body":"12BcOSg50nLxdbt++r7liZpeyWAVpmihTy8Zu8BmpA6a1hqdevS5PPYwnbtpjN05xgeyReSihh9idyfriR6qx1Fbo8AA0k8HQt6gJ3spWITI21GhLTzh9PDUkgjCtrEK"}/account/v4/login/passwordV1QiLCJhbGciOiJIUzI1NiJ9)

图片描述

Sign尾部分析

接下来我们针对Sign的尾部数据进行分析,单纯盲猜或者挂frida脚本已经解决不了问题了,我们用IDA看下具体的实现逻辑,当然上面的MD5分析也可以直接从IDA反编译入手,通过搜索sign关键字进行定位,只是我习惯先钩一下脚本,万一直接命中就不用费时间去分析了...

通过MD5的脚本打印,我们也能看到相关的函数调用栈,这对于我们快速定位也提供了很大的方便。我们直接搜索[KEPPostSecuritySign kep_signWithURL: body:]方法,可以看到明显的字符串拼接的痕迹,IDA还是比较智能的,已经识别出了MD5的salt值。

图片描述

通过分析,定位到[NSString kep_networkStringOffsetSecurity]函数,在内部进行了字符串的处理,在循环里面进行了各种判断以及移位操作,不嫌麻烦的话可以分析一下逻辑,重写一下处理流程。
图片描述

我这边处理比较暴力,发现kep_networkStringOffsetSecurity是NSString的Catetory,那就直接调用验证一下吧,使用frida挂载以后,找到NSString类,调用方法传入md5之后的值,然后就会发现经过该函数,神奇的sign值就给到了。

图片描述

x-ads分析

分析完sign以后,观察到还有一个x-ads的字段,按照惯例,先用脚本试着钩一下,经常采用的加密大致就是DES、AES或RC4这些算法。

图片描述

针对 AES128、DES、3DES、CAST、RC4、RC2、Blowfish等加密算法进行hook,脚本的关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
var handlers = {
    CCCrypt: {
        onEnter: function(args) {
            var operation = CCOperation[args[0].toInt32()];
            var alg = CCAlgorithm[args[1].toInt32()].name;
            this.options = CCoptions[args[2].toInt32()];
            var keyBytes = args[3];
            var keyLength = args[4].toInt32();
            var ivBuffer = args[5];
            var inBuffer = args[6];
            this.inLength = args[7].toInt32();
            this.outBuffer = args[8];
            var outLength = args[9].toInt32();
            this.outCountPtr = args[10];
            if (this.inLength < MIN_LENGTH || this.inLength > MAX_LENGTH){
                return;
            }
            if (operation === "kCCEncrypt") {
                this.operation = "encrypt"
                console.log("***************** encrypt begin **********************");
            } else {
                this.operation = "decrypt"
                console.log("***************** decrypt begin **********************");
            }
            console.log("CCCrypt(" +
                "operation: " + this.operation + ", " +
                "CCAlgorithm: " + alg + ", " +
                "CCOptions: " + this.options + ", " +
                "keyBytes: " + keyBytes + ", " +
                "keyLength: " + keyLength + ", " +
                "ivBuffer: " + ivBuffer + ", " +
                "inBuffer: " + inBuffer + ", " +
                "inLength: " + this.inLength + ", " +
                "outBuffer: " + this.outBuffer + ", " +
                "outLength: " + outLength + ", " +
                "outCountPtr: " + this.outCountPtr + ")"
            );
 
            //console.log("Key: utf-8 string:" + ptr(keyBytes).readUtf8String())
            //console.log("Key: utf-16 string:" + ptr(keyBytes).readUtf16String())
            console.log("key: ");
            dumpBytes(keyBytes, keyLength);
 
            console.log("IV: ");
            // ECB模式不需要iv,所以iv是null
            dumpBytes(ivBuffer, keyLength);
 
            var isOutput = true;
            if (!SHOW_PLAIN_AND_CIPHER && this.operation == "decrypt") {
                isOutput = false;
            }
 
            if (isOutput){
                // Show the buffers here if this an encryption operation
                console.log("In buffer:");
                dumpBytes(inBuffer, this.inLength);
            }
             
        },
        onLeave: function(retVal) {
            // 长度过长和长度太短的都不要输出
            if (this.inLength < MIN_LENGTH || this.inLength > MAX_LENGTH){
                return;
            }
            var isOutput = true;
            if (!SHOW_PLAIN_AND_CIPHER && this.operation == "encrypt") {
                isOutput = false;
            }
            if (isOutput) {
                // Show the buffers here if this a decryption operation
                console.log("Out buffer:");
                dumpBytes(this.outBuffer, Memory.readUInt(this.outCountPtr));
            }
            // 输出调用堆栈,会识别类名函数名,非常好用
            console.log('CCCrypt called from:\n' +
                Thread.backtrace(this.context, Backtracer.ACCURATE)
                .map(DebugSymbol.fromAddress).join('\n') + '\n');
        }
    },
};
 
 
if (ObjC.available) {
    console.log("frida attach");
    for (var func in handlers) {
        console.log("hook " + func);
        Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", func), handlers[func]);
    }
} else {
    console.log("Objective-C Runtime is not available!");
}

查看脚本的输出日志,直接命中了AES128的加密算法,并且输出的Base64数据完全匹配,只能说运气爆棚。

图片描述

拿到对应的key跟iv,尝试解密看下也是没问题的。x-ads分析结束,都不用反编译看代码:)

图片描述

Body的分析

最后看下sign值的组成部分,body数据是怎么计算的,抱着试试的想法,直接用x-ads分析得到的算法以及对应的key、iv进行解密:

{
"body": "5gJEXtLqe3tzRsP8a/bSwXDiK0VslZZZyOEj1jBDBhtYTGGdWltuIjLbzwZ2OxMcb3mFX7bJtgH3WlqGET5W34P4dTEIDhLH6FkT3HSLaDnEXYHvEl9IZRQKf19wMG/t"
}

图片描述

这次说不上什么运气爆棚了...只能说开发者比较懒或者安全意识有点差了,使用了AES-CBC模式,iv都不改变一下的...

总结

这次分析整体来看,没什么技术含量,大部分都是脚本直接解决了,从结果来看,也是使用的常规的加密、签名算法,这也从侧面给我们安全开发提个醒,是不是可以有策略性的改变一下,比如我们拿MD5来看下都可以做哪些改变。

https://opensource.apple.com/source/ppp/ppp-37/ppp/pppd/md5.c.auto.html

首先针对MD5Init,我们可以改变它的初始化数据:

1
2
3
4
5
6
7
8
9
10
11
12
void MD5Init (mdContext)
MD5_CTX *mdContext;
{
  mdContext->i[0] = mdContext->i[1] = (UINT4)0;
 
  /* Load magic initialization constants.
   */
  mdContext->buf[0] = (UINT4)0x67452301;
  mdContext->buf[1] = (UINT4)0xefcdab89;
  mdContext->buf[2] = (UINT4)0x98badcfe;
  mdContext->buf[3] = (UINT4)0x10325476;
}

其次针对Transform我们也可以改变其中的某几个数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static void Transform (buf, in)
UINT4 *buf;
UINT4 *in;
{
  UINT4 a = buf[0], b = buf[1], c = buf[2], d = buf[3];
 
  /* Round 1 */
#define S11 7
#define S12 12
#define S13 17
#define S14 22
  FF ( a, b, c, d, in[ 0], S11, UL(3614090360)); /* 1 */
  FF ( d, a, b, c, in[ 1], S12, UL(3905402710)); /* 2 */
  FF ( c, d, a, b, in[ 2], S13, UL( 606105819)); /* 3 */
  FF ( b, c, d, a, in[ 3], S14, UL(3250441966)); /* 4 */
  FF ( a, b, c, d, in[ 4], S11, UL(4118548399)); /* 5 */
  FF ( d, a, b, c, in[ 5], S12, UL(1200080426)); /* 6 */
  FF ( c, d, a, b, in[ 6], S13, UL(2821735955)); /* 7 */
  FF ( b, c, d, a, in[ 7], S14, UL(4249261313)); /* 8 */
  FF ( a, b, c, d, in[ 8], S11, UL(1770035416)); /* 9 */
  FF ( d, a, b, c, in[ 9], S12, UL(2336552879)); /* 10 */
  FF ( c, d, a, b, in[10], S13, UL(4294925233)); /* 11 */
  FF ( b, c, d, a, in[11], S14, UL(2304563134)); /* 12 */
  FF ( a, b, c, d, in[12], S11, UL(1804603682)); /* 13 */
  FF ( d, a, b, c, in[13], S12, UL(4254626195)); /* 14 */
  FF ( c, d, a, b, in[14], S13, UL(2792965006)); /* 15 */
  FF ( b, c, d, a, in[15], S14, UL(1236535329)); /* 16 */
 
  /* Round 2 */
#define S21 5
#define S22 9
#define S23 14
#define S24 20
  GG ( a, b, c, d, in[ 1], S21, UL(4129170786)); /* 17 */
  GG ( d, a, b, c, in[ 6], S22, UL(3225465664)); /* 18 */
   
  ...
  

简单的变形以后,即使脚本能hook到对应的函数,但是想直接脱机调用结果还是不可以的,此时就要不得不进行反编译分析或者动态调试,此时配合代码混淆、VMP等静态防护手段,再加上反调试等安全手段,对于攻击的门槛也相应的提高。


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
点赞3
打赏
分享
最新回复 (2)
雪    币: 1993
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
赛文奥特曼 2023-8-9 16:51
2
0
感谢
雪    币: 19323
活跃值: (28938)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-8-10 09:21
3
1
感谢分享
游客
登录 | 注册 方可回帖
返回