飘云阁

 找回密码
 加入我们

QQ登录

只需一步,快速开始

查看: 12941|回复: 38

[Android] 恋恋APK之登录时DES加密分析

  [复制链接]
  • TA的每日心情
    慵懒
    2022-1-10 21:59
  • 签到天数: 109 天

    [LV.6]常住居民II

    发表于 2018-4-6 22:27:20 | 显示全部楼层 |阅读模式
    本帖最后由 Bu弃 于 2018-4-6 22:27 编辑

    大家好呀,好久都没在论坛发帖了(好像我一共也没发几个帖,尴尬)。因为最近对Android的逆向有兴趣,所以去网上找了下教程。好像论坛也有。叫《无名Android逆向》,感兴趣的可以去看看。
    此次发帖算是笔记吧。记录自己怎么跟着视频一步一步的分析,也算是一个作业。毕竟 看懂了 !=  你会了!
    教程第一课是分析一款恋恋的交友APK。具体是分析它登录时是如何把信息加密,然后发送到服务器的。废话不多说,开搞吧。我尽量写详细点,以下有些思路和内容是视频里面的,我借鉴下,希望不会被喷。另外有些词把握得不是很准。有些地方或许说法有些错误,希望大家多多指正。毕竟我也是个小白,正在学习ing。。。。
       一、需要的工具
           JEB、IDA(分析so文件)、Fiddler(抓包)、任意一款Android模拟器/手机
       二、开整吧
           1.首先使用抓包工具Fiddler,抓下此APK登录时发送的数据包。设置如下。
                                 

    设置抓包工具1

    设置抓包工具1


             

    设置抓包工具2

    设置抓包工具2

                        2. 设置模拟器网络
                              

    模拟器设置

    模拟器设置

                      3.登录后的封包(用户名为123456789  密码为!@#$%^)
                            

    抓到的登录封包

    抓到的登录封包

                      4.从上面的封包中,我们看到了请求的url(POST后面跟着的那串),还有加密后的用户名和密码。下面我们就使用url字符串(http://mob.imlianai.com/call.do?cmd=mobileUser.login )搜索,定位到关键点。
                      5.打开JEB,把APK拖入,然后切换到字符串。搜索上面抓到的url(http://mob.imlianai.com/call.do?cmd=mobileUser.login )其实我们这里只需要搜索mobileUser.login 就行了。
                      6.双击找到的字符串,来到汇编界面。
                         

    汇编界面

    汇编界面

                    7.点击任意一行,按TAB键,到java代码界面
                      

    java代码页面

    java代码页面

                   8.这里是switch语句。主要应该是根据参数来选择操作的连接地址。我们往下找找。看看有没有什么我们需要的。
                   9.在下面很多地方,我们都发现了类似这样的代码:
    [Java] 纯文本查看 复制代码
               JSONObject v1_1 = new JSONObject();   //创建一个构建JSON字符串的对象
                v1_1.put("xxxx", xxxx);   //往里面加入key/value形式的键值对
                v1_1.put("xxxx", xxxx);
                v0 = com.a.a.a.f.a.a(v1_1.toString()).getBytes(); //com.a.a.a.f.a.a(v1_1.toString()) 就是具体的加密逻辑了
    

                   10.com.a.a.a.f.a.a()这个方法就是具体的加密逻辑了。接下来我们着重分析下。我们双击这个方法,进入到方法实现位置,具体代码如下:
    [Java] 纯文本查看 复制代码
    public class a {
        private static final byte[] a;
        private static final IvParameterSpec b;
        private static String c;
        private static Key d;
    
        static {
            a.a = new byte[]{1, 2, 3, 4, 5, 6, 7, 8};
            a.b = new IvParameterSpec(a.a);
            a.c = "hqi/FjjcBxA=";
            a.d = null;
        }
    
        public static String a(String arg1) { //这就是加密的方法体
            return Jni.getInstance().encryptString(arg1);
        }
    }
    
                                     
                    11.观察上面的代码,发现其又调用了Jni中的encryptString方法。把Json字符串传递过去。我们照样双击这个方法。得到如下代码
    [Java] 纯文本查看 复制代码
        public String encryptString(String arg9) {
            String v0_1;
            if(arg9 == null || arg9.length() == 0) { // 如果什么都没输入的话,就把v0_1置为空串,返回。否则则进行加密操作
                v0_1 = "";
            }
            else {
                String v2 = this.encode(arg9);  //调用本类中的encode方法。这个方法主要是把字符串的每个字符的字节码转换成16进制形式。例如 ‘a’ 的ASCII是[color=#333333]97,经过这个方法后,就变成了61[/color]
                int v3 = v2.length();
                StringBuffer v4 = new StringBuffer();
                if(v3 < 500) {    //如果v2的长度小于500.则执行下面的逻辑。否则执行else中的。我们现在就只看下面的逻辑,先不看else中的逻辑
                    v4.append(this.getEncryptString(v2, true)); //调用this.getEncryptString(v2, true)加密。具体细节我们后面说
                }
                else {
                    int v0;
                    for(v0 = 1; v0 < v3 / 500 + 1; ++v0) {
                        if(v3 % 500 == 0 && v0 == v3 / 500) {
                            v4.append(this.getEncryptString(v2.substring((v0 - 1) * 500, v0 * 500), true));
                            break;
                        }
    
                        v4.append(this.getEncryptString(v2.substring((v0 - 1) * 500, v0 * 500), false));
                    }
    
                    if(v3 % 500 == 0) {
                        goto label_15;
                    }
    
                    v4.append(this.getEncryptString(v2.substring((v0 - 1) * 500, v3), true));
                }
    
            label_15:
                v0_1 = DESencryption.getEncString(v4.toString(), this.getEncryptString("a", true).substring(0, 8));  //使用DES对加密后的数据再进行加密。DES的key是一个固定的值。因为用到了getEncryptString()方法,所以等分析完这个方法我们再提
            }
            return v0_1;
        }
    
    

                三、分析this.getEncryptString(v2, true)                  

                       1.getEncryptString代码如下:
    [Java] 纯文本查看 复制代码
    private native String getEncryptString(String arg1, boolean arg2) {}  //native为原生态方法,一般是调用C++、C语言代码
    

                       2.上面代码是通过JNI调用C++或者C语言代码。有关于这方面的知识。我也是小白,所以也不怎么懂。大家可以去百度下。既然这里是通过JNI调用,那么我们怎么知道他调用的是哪个so文件呢?在这个类上面的static静态代码块中声明出来了。如下:
    [Java] 纯文本查看 复制代码
        static {
            Jni.hexString = "0123456789ABCDEF";  //这个先不管。这个是用于上面说的encode方法把字符串的每个字符的字节码转换成16进制形式的。
            System.loadLibrary("jni"); //这个就是加载so的库了。so的名字是libjni.so。字符串jni前面再加上lib。
        }
    
    

                        3.在APK中的\lib\armeabi下有个libjni.so文件。我们现在把它拖入ida中。
                        4.在IDA载入完成后,进入Exports中。如果没有Exports视图,则需要进行如下操作就行了。Exports中会显示所有的导出函数

                             7.png
                                      8.png
                        5.因为供Java调用的API有个明显的特征:Java_包名_方法名。我们Ctrl+F搜索下Java开头的。很幸运,这个里面只有一个。而且发现方法名也是和Java中声明的native方法名是一样的。
                                   9.png
                         6.双击进去吧。就进入了汇编页面。汇编代码页面我就不贴了。直接按F5变成C代码,我们再分析吧。总体分析如下。
    [C++] 纯文本查看 复制代码
    int __fastcall Java_com_jni_Jni_getEncryptString(_JNIEnv *a1, JNIInvokeInterface *a2, int inputStr, int inputBool) //你们一点进去可能方法定义不是这样的。你只需要导入下jni.h就行。具体操作,在第7点中给出
    {
      _JNIEnv *v4; // r5@1
      int str; // r4@1
      int v6; // r2@1
      const char *v7; // r7@1
      size_t v8; // r4@1
      _JNIEnv *v9; // r0@2
      char *v10; // r1@2
      jstring (__cdecl *v11)(JNIEnv *, const char *); // r3@2
      int result; // r0@6
      char *s; // [sp+0h] [bp-828h]@1
      int v14; // [sp+4h] [bp-824h]@1
      char dest; // [sp+Ch] [bp-81Ch]@3
      int v16; // [sp+80Ch] [bp-1Ch]@1
    
      v4 = a1;
      str = inputStr;
      v14 = inputBool;
      v16 = _stack_chk_guard;
      g_env = a1;
      s = (char *)initAddStr();                     // 初始化一个字符串。待会我们再分析
      v7 = (const char *)jstringTostring((int)v4, str, v6);// 调用JNI的方法,把Java中的String变成C中的char *
      v8 = strlen(s);                               // 求出初始化字符串的长度
      if ( strlen(v7) + v8 <= 0x7FF )               // 转成C的inputStr的长度和s的长度<0x7ff(2047)如果小于则拼上s.否则不拼
      {
        memset(&dest, 0, 0x7FFu);                   // 往dest这个地址填充0x7FF个0
        strcat(&dest, v7);                          // 这里是把v7的值,也就是inputStr转成Char的值赋给dest
        if ( v14 )                                  // 第2个传参也就是inputBool为true的时候,就在后面跟上初始值s。否则不跟
          strcat(&dest, s);
        v9 = v4;                                    // v9 = JNIEnv
        v10 = &dest;
        v11 = v4->functions->NewStringUTF;          // v11 =  NewStringUTF:把C的char* 转换成Java中的String
      }
      else
      {
        v9 = v4;
        v10 = (char *)v7;
        v11 = v4->functions->NewStringUTF;          // v11 =  NewStringUTF:把C的char* 转换成Java中的String
      }
      result = ((int (__fastcall *)(_JNIEnv *, char *))v11)(v9, v10);// 调用NewStringUTF方法,把v10转换成String
      if ( v16 != _stack_chk_guard )
        _stack_chk_fail(result);
      return result;
    }
    

                       7.导入jni.h文件。此文件在java_home/jdk/lib下。当然你直接导会报错。我们需要修改一下。具体修改方法百度下吧。我待会会提供个。导入操作如下:
                          10.png
                     8.现在我们逐步来分析下。首先看方法声明中的参数。int __fastcall Java_com_jni_Jni_getEncryptString(_JNIEnv *a1, JNIInvokeInterface *a2, int inputStr, int inputBool)。参数列表函数如下:                              1)_JNIEnv * :这个参数是固定的。传入的是Dalvik虚拟的函数表。具体可参考   https://www.cnblogs.com/gavanwanggw/p/6907893.html
                                  2)JNInvokeInterface * :这个参数也是固定的。传入的是正在调用这个方法的类。
                                  3)int :这个是函数的参数,也就是java传递过来的参数,因为在Java中第一个参数传的是String,而C中没有String。所以这个应该是char*指针。
                                  4)int:这个同样是Java传递过来的参数。在Java中传的是boolean。C中没有。所以用int代替。
                    9.分析 s = (char *)initAddStr(); 。我们点开这个函数。他内部如下
    [C++] 纯文本查看 复制代码
    int initAddStr()
    {
      int v0; // r0@2
      int v1; // r2@2
    
      if ( !isInit ) //这里是如果初始化一次了,就不需要再执行了。也就是这里只会执行一次。
      {
        v0 = initInflect((int)jniStr);  //在第10点中分析
        key = jstringTostring((int)g_env, v0, v1); //调用方法,把java的String变成C语言中的char*
        isInit = 1;
      }
      return key;
    }
    
    

                     10.分析initInflect((int)jniStr)。蚕食为jniStr。那么jniStr是什么呢?我们双击下jniStr。发现如下:
                          11.png
                           那么,jniStr = “/key=i im lianai” 。接下来我们看看initInflect方法的内部吧
    [C++] 纯文本查看 复制代码
    int __fastcall initInflect(int a1)
    {
      int *v1; // r5@1
      int v2; // r0@1
      bool v3; // zf@1
      int v4; // r7@1
      int v5; // r0@1
      int (__fastcall *v6)(int *, const char *); // r3@3
      const char *v7; // r1@3
      int v8; // r0@2
      int v9; // r3@4
      int v10; // r4@4
      int v12; // [sp+Ch] [bp-1Ch]@1
    
      v12 = a1;
      v1 = g_env;
      v2 = (*(int (__fastcall **)(_DWORD *, const char *))(*g_env + 24))(g_env, "com/Reflect");
      v4 = v2;
      v3 = v2 == 0;
      v5 = *v1;
      if ( v3 )
      {
        v6 = *(int (__fastcall **)(int *, const char *))(v5 + 668);
        v7 = "jclass";
        return v6(v1, v7);
      }
      v8 = (*(int (__fastcall **)(int *, int, const char *, const char *))(v5 + 452))(
             v1,
             v4,
             "func",
             "(ILjava/lang/String;)Ljava/lang/String;");
      v9 = *v1;
      v10 = v8;
      if ( !v8 )
      {
        v6 = *(int (__fastcall **)(int *, const char *))(v9 + 668);
        v7 = "method";
        return v6(v1, v7);
      }
      (*(void (__fastcall **)(int *, int))(v9 + 668))(v1, v12);
      return _JNIEnv::CallStaticObjectMethod(v1, v4, v10, 10);
    }
    
    

                          好吧。这里我就不一行一行分析了,因为我也不会。但是大致意思是看懂了。意识是调用com.Reflect类中的func方法。参数就是“/key=i im lianai”我们接下来回到JEB看吧。
                     11. 代码如下:
    [Java] 纯文本查看 复制代码
    package com;
    
    public class Reflect {
        private static String hexString;
        public static String tmp;
    
        static {
            Reflect.tmp = " alien";
            Reflect.hexString = "0123456789ABCDEF";
        }
    
        public Reflect() {
            super();
        }
    
        private static String encode(String arg5) {//arg5 = “/key=i im lianai alien”   这里就是上面的把字符串中的字母变为16进制的代码了。我就不一行一行读了
            byte[] v1 = arg5.getBytes();
            StringBuilder v2 = new StringBuilder(v1.length * 2);
            int v0;
            for(v0 = 0; v0 < v1.length; ++v0) {
                v2.append(Reflect.hexString.charAt((v1[v0] & 240) >> 4));
                v2.append(Reflect.hexString.charAt((v1[v0] & 15) >> 0));
            }
    
            return v2.toString();
        }
    
        public static String func(int arg2, String arg3) {//这个就是IDA中调用的方法
            return Reflect.encode(String.valueOf(arg3) + Reflect.tmp);  //这里调用了此类中的encode方法,传入的是我们传递过来的参数“/key=i im lianai” 加上此类提供的一个静态字符串" alien"<注意前面有个空格>,综合起来也就是“/key=i im lianai alien”
        }
    }
    
    

                         12.总结下initInflect((int)jniStr)的作用:传入“/key=i im lianai alien”把其中的每个字母转成16进制字符串。最后的结果是“2F6B65793D6920696D206C69616E616920616C69656E” 这个是固定的。
                         13.其他的都分析完了。现在我们总结下getEncryptString()这个方法.这个方法说的就是把传入的字符串,如果长度小于0x7FF的就在后面拼上“2F6B65793D6920696D206C69616E616920616C69656E”。
                         14.so部分到此结束,这个so挺容易。做的事也容易。就是拼下字符串。然后我们回到JEB中的java代码中。看类Jni的encryptString的最后。
    [Java] 纯文本查看 复制代码
     v0_1 = DESencryption.getEncString(v4.toString(), this.getEncryptString("a", true).substring0, 8));   //这个就是最终的结果了。v4就是经过so处理过的字符串,第二个参数是“a”去让so处理,在截取他的0~8位作为DES加密的key。加密后就是结果了。
    
    

                        15.DES算法我不懂。所以里面的就不看了。我们可以直接把这些代码拷出来到Eclipse中。然后使用Java代码验证我们的分析是否正确。Java代码如下:
    [Java] 纯文本查看 复制代码
    public class Test01 {
        public static void main(String[] args) throws Exception {
           String jsonHex = getHexString("{\"pwd\":\"!@#$%^\",\"number\":\"1234567890\"}"); //登录时的JSON字符串
           String initStringHex = getHexString("/key=i im lianai alien");//固定的字符串
           String soString = jsonHex+initStringHex;
           String ret = DESencryption.getEncString(soString, "a2F6B657");
           System.out.println(ret);
        }
    
        /**
         * 这里是复制JEB中的把字符串中每个字符转换成16进制字符串
         * @param str
         * @return
         * @throws Exception
         */
        public static String getHexString(String str) throws Exception {
            byte[] v1 = str.getBytes("UTF-8");
            String hexString = "0123456789ABCDEF";   
            StringBuilder v2 = new StringBuilder(v1.length * 2);
            int v0;
            for(v0 = 0; v0 < v1.length; ++v0) {
                v2.append(hexString.charAt((v1[v0] & 240) >> 4));
                v2.append(hexString.charAt((v1[v0] & 15) >> 0));
            }
           return v2.toString();
        }
    }
    
    

                     16.最后结果如下:
                
                      13.png
                    17.总结下流程:
                            1)用户输入用户名和密码。把他么封装成JSON字符串
                            2)把JSON字符串的每个字符,变成16进制形式
                            3)调用so库。拼接固定字符串"2F6B65793D6920696D206C69616E616920616C69656E"
                            4)调用DES加密。密码是固定的"a2F6B6579",要加密的数据就是第3)步拼接出来的。
                            5)我也是小白,各位亲喷。有地方说错的,欢迎批评指正。
                            6)这个能吐槽下论坛的这个帖子的富文本编辑器么?为嘛不支持TAB键。。。。还有排版好蛋疼。。。。
                            6)收工,洗澡睡觉。谢谢大家。
                   考虑到有些小伙伴需要《无名Android逆向》所以这里就直接给出了。按道理不会违规吧。如果有违规,麻烦版主大人@下我,我改正。
                    《无名Android逆向》链接: https://pan.baidu.com/s/1yb-1OJWJPkDFjJQZ5Fh-4w 密码: 39y5


                   第一课的课件里面也有。有的不需要这个课程,我也直接给下课件的分享吧。
                    课件 链接: https://pan.baidu.com/s/1N-GYBBS4x9hZeKu_T3pjPg 密码: phgi






    评分

    参与人数 5威望 +30 飘云币 +36 收起 理由
    nociabx + 2 PYG有你更精彩!
    losers + 4 很给力!
    Rooking + 16 很给力!
    gfjykldd + 4 PYG有你更精彩!
    wai1216 + 20 + 20 赞一个!

    查看全部评分

    本帖被以下淘专辑推荐:

    PYG19周年生日快乐!
  • TA的每日心情
    慵懒
    2022-1-10 21:59
  • 签到天数: 109 天

    [LV.6]常住居民II

     楼主| 发表于 2018-4-6 22:29:34 | 显示全部楼层
    沙发自己来
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2016-6-16 14:07
  • 签到天数: 10 天

    [LV.3]偶尔看看II

    发表于 2018-4-6 23:26:33 | 显示全部楼层
    666 就喜欢看协议分析 精了!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2024-3-18 17:05
  • 签到天数: 1824 天

    [LV.Master]伴坛终老

    发表于 2018-4-7 13:55:34 | 显示全部楼层
    小白前来学习了
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2 小时前
  • 签到天数: 1791 天

    [LV.Master]伴坛终老

    发表于 2018-4-7 16:56:39 | 显示全部楼层
    谢谢大神提供这么好的学习资料好好学习天天向上
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    3 天前
  • 签到天数: 670 天

    [LV.9]以坛为家II

    发表于 2018-4-7 19:59:36 | 显示全部楼层

    厉害啊楼主,谢谢楼主分享
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    3 天前
  • 签到天数: 670 天

    [LV.9]以坛为家II

    发表于 2018-4-7 20:00:46 | 显示全部楼层

    谢谢大神提供这么好的学习资料
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    2020-8-23 17:12
  • 签到天数: 660 天

    [LV.9]以坛为家II

    发表于 2018-4-7 22:51:21 | 显示全部楼层
    围观大神 受益匪浅
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    2024-2-17 14:19
  • 签到天数: 160 天

    [LV.7]常住居民III

    发表于 2018-4-9 17:02:49 | 显示全部楼层
    Bu弃师傅厉害呀,研究加密算法了。

    点评

    我是一渣渣,算法部分都跳过了  详情 回复 发表于 2018-4-9 22:40
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-1-10 21:59
  • 签到天数: 109 天

    [LV.6]常住居民II

     楼主| 发表于 2018-4-9 22:40:07 | 显示全部楼层
    666888tzq 发表于 2018-4-9 17:02
    Bu弃师傅厉害呀,研究加密算法了。

    我是一渣渣,算法部分都跳过了
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 加入我们

    本版积分规则

    快速回复 返回顶部 返回列表