某运动APP的登录协议分析

前言

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

抓包

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

Sign分析

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

sign: b61df9a8bce7a8641c5ca986b55670e633a7ab29

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

js 复制代码
   // 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,脚本的关键代码如下:

js 复制代码
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来看下都可以做哪些改变。

opensource.apple.com/source/ppp/...

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

scss 复制代码
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我们也可以改变其中的某几个数据:

css 复制代码
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等静态防护手段,再加上反调试等安全手段,对于攻击的门槛也相应的提高。

相关推荐
用户962377954483 天前
VulnHub DC-3 靶机渗透测试笔记
安全
叶落阁主4 天前
Tailscale 完全指南:从入门到私有 DERP 部署
运维·安全·远程工作
用户962377954486 天前
DVWA 靶场实验报告 (High Level)
安全
数据智能老司机6 天前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机6 天前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
用户962377954486 天前
DVWA 靶场实验报告 (Medium Level)
安全
red1giant_star6 天前
S2-067 漏洞复现:Struts2 S2-067 文件上传路径穿越漏洞
安全
用户962377954486 天前
DVWA Weak Session IDs High 的 Cookie dvwaSession 为什么刷新不出来?
安全
cipher8 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
一次旅行11 天前
网络安全总结
安全·web安全