red1y 发表于 2022-9-21 21:32

xx度灰app加密算法分析还原

本帖最后由 red1y 于 2022-9-21 22:26 编辑

# xx度灰app加密算法分析还原

### 本文包括

- java层分析
- so层加密算法分析
- java层签名分析
- 发包验证
- b站视频连接 https://www.bilibili.com/video/BV1id4y1g7G1/?spm_id_from=333.999.0.0&vd_source=23b9de401c27e819abddbd5551eddbda

### 一. 修改测试

1. 重新打包签名生成可调试版本后运行闪退
2. 使用mt管理修改dex文件重新编译后正常运行,访问相关资源会提示正版维护信息
3. 使用mt管理器重新签名后运行闪退

### 二、Java层静态分析

1. 定位启动`Activity`

   ```xml
   <activity android:name="com.tencent.mm.ui.LaunchActivity" android:screenOrientation="1" android:theme="@style/AppTransparentTheme">
         <intent-filter>
         <action android:name="android.intent.action.MAIN"/>
         <category android:name="android.intent.category.LAUNCHER"/>
         </intent-filter>
       </activity>
   ```

2. 分析`onCreate`函数

   `LauncheActivity`继承自`BaseActivity`,`BaseActivity`中调用了几个函数,经分析确定了`w5()`为关键函数:

   ```java
   @Override// android.support.v7.app.AppCompatActivity
   protected void onCreate(@nullable Bundle arg2) {
       this.Z5();
       super.onCreate(arg2);
       try {
         this.a = android.databinding.f.l(this.O5(), this.Q5());
         this.V5();
         com.tencent.mm.base.f.c().a(this);
         this.W5(); // 关键函数
         this.U5();
         this.b6();
       }
       catch(Exception v2) {
         v2.printStackTrace();
       }
   }
   ```

3. 跟踪`w5()`

   在前面测试过程中有一个信息是:修改了签名后的app并不会直接闪退,而是在申请完相关使用权限后才会退出,如果没有通过权限的申请,会自动正常退出,而不是闪退;在`w5()`中找到了相关权限申请的函数`T6()`

   ```java
   @Override// com.tencent.mm.base.BaseActivity
   public void W5() {
       /* other code */
       int v0_1 = this.checkSha1(this) ? 1 : 2;
       com.tencent.mm.network.d.h = this.D6(this) + ":" + v0_1;
       org.greenrobot.eventbus.c.f().t(this);
       this.I = new LaunchModel(this);
       this.z6();
       this.T6(); // 在T6中进行权限申请
       String v0_2 = i1.k().E();
       if(!TextUtils.isEmpty(v0_2)) {
         com.tencent.mm.l.j.d().v(((UserInfoBean)JSON.parseObject(v0_2, UserInfoBean.class)));
       }
   }
   ```

   在`T6()`中如果没有赋予应用相关权限,则会结束应用,否则进入`B6()`

   ```java
   public void T6() {
       /* other code */
       // if no permission, return and eixt
       LaunchActivity.this.B6();
   ```

   跟踪`B6()`后续的一系列函数调用,最终定位到一个向服务器发送请求的函数

   ```java
   public void q(String arg6) {
       d.D1().N4(arg6);
       d.D1().d4("http://xxxx/.../xxx", d.D1().x1(), new b("/api/xxx/xxx") {
       }
   }
   ```

   开启`Fiddler`抓报后,发现应用自启动到闪退没有发送任何请求,`d4()`函数中在发送请求前进行了一系列的数据操作

   ```java
   public void d4(String arg2, HttpParams arg3, com.tencent.mm.network.b arg4) {
       ((PostRequest)((PostRequest)((PostRequest)((PostRequest)((PostRequest)OkGo.post(arg2).tag(arg4.b())).upJson(this.s2(arg3).toJSONString())).headers("token", i1.k().w())).cacheKey(this.C0(arg4.a()))).cacheMode(CacheMode.FIRST_CACHE_THEN_REQUEST)).execute(arg4);
   }
   ```

   `upJson()`参数即为上传的数据,经过了`s2()`的处理,跟进`s2()`,最终数据的加密封装在`com.szcx.lib.encrypt.c.k()`中进行

   ```java
   public String k(String arg5) throws JSONException {
       String v5 = this.e(arg5);
       JSONObject v2 = new JSONObject();
       v2.put("timestamp", "1663503240");
       v2.put("_ver", "v1");
       v2.put("data", v5);
       v2.put("sign", this.j(a.e("_ver=v1&data=" + v5 + "×tamp=" + "1663503240" + this.e)));
       return v2.toString();
   }
   ```

   经过对正常app运行时的抓包比较,此处的参数与实际一致,在`e()`中队数据进行了加密,最终调用`native`函数进行加密

   ```java
   public String f(String arg1, String arg2) {
       return EncryptUtil.encrypt(arg1, arg2);
   }
   ```

   ```java
   public static native String encrypt(String arg0, String arg1) {
   }
   ```

   同时在代码中发现了多个密钥,包括但不限于,第一个`base64`编码的密钥在跟踪流程中传递给了`native`函数

   - `BwcnBzRjN2U/MmZhYjRmND4xPjI+NWQwZWU0YmI2MWQ3YjAzKw8cEywsIS4BIg==`
   - `81d7beac44a86f4337f534ec9332837`

### 三、Java层动态跟踪、Hook分析

1. 将前面重新打包签名生成的可调试的`apk`安装到手机上,为防止应用直接闪退,拒绝其相关权限的申请,同时在程序判断权限申请结果处下断,动态修改权限申请的结果,使后续流程继续下去

2. 调试跟踪函数,最终定位发现程序在加载上述`native`加密`so`库时闪退

   ```smalltalk
   .method static constructor <clinit>()V
             .registers 1
   00000000const-string      v0, "sojm"
   00000004invoke-static       System->loadLibrary(String)V, v0
   0000000Areturn-void
   .end method
   ```

3. 同时在上面的跟踪过程中还可以得到程序生成的一系列请求参数,包含了大量系统、设备信息,但没有`hash`相关的参数

4. 使用 `frida` `hook` `encrypt`函数,主动调用其多次加密相同数据,可以发现每次得到的结果都不同,应该使用了某种随机量

### 四、so层静态分析

1. 使用`ida pro`分析`sojm` 库,通过观察函数名可以得到其是通过静态注册的,这里的四个参数也符合常规的`jni`函数

   ```c
   // JNIEnv* env
   // jclass _clazz
   // jstring a3
   // jstring a4
   int __fastcall Java_com_qq_lib_EncryptUtil_encrypt(int a1, int a2, int a3, int a4)
   {
   int v8; // r4
   int v10; // BYREF
   int v11; //
   
   v8 = cgo_wait_runtime_init_done();
   v10 = a4; // jstring
   v10 = a3; // jstring
   v10 = a2; // _clazz
   v10 = a1; // env
   v11 = 0;
   crosscall2(cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt, v10, 20, v8);
   cgo_release_context(v8);
   return v11; // jstring
   }
   ```

2. 但是后续的操作就不太常规了,可以看出它把参数依次赋给了一个数组;同时调用了`crosscall2`,其参数为:

   1. 一个函数地址
   2. 参数数组
   3. 应该是参数数组的长度,size
   4. `init`函数的返回值

   值得注意的是,`v10`明明只有四个元素,但是传入的参数size却是`20 = 5 * 4`,同时`v11`被置`0`后又没有显式的赋值,最终却被返回,猜测应该是在`cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt`中被赋值了

3. 进入`cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt`后发现参数个数很奇怪,而且`sub_BC3C4658`传入了很多重复的参数;

   ```c
   int __fastcall cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt(int a1, int a2, int a3, int a4, int a5, int a6)
   {
   int v6; // r10
   int v7; // lr
   int v9; //
   int v10; //
   
   while ( (unsigned int)&a5 <= *(_DWORD *)(v6 + 8) )
       sub_BC360D10();
             sub_BC3C4658( // 通过这个函数可以推测a6为前面传入的数组地址
         a6,
         *(_DWORD *)a6,
         *(_DWORD *)(a6 + 4),
         *(_DWORD *)(a6 + 8),
         v7,
         *(_DWORD *)a6,
         *(_DWORD *)(a6 + 4),
         *(_DWORD *)(a6 + 8),
         *(_DWORD *)(a6 + 12),
         v9);
       // sub_BC3C4658函数没有返回值,局部变量v10也没有被显式地赋值
   *(_DWORD *)(a6 + 16) = v10; // 在这里对a6,也就是上面v11的地址处进行了赋值
   sub_BC301DF8();
   return sub_BC2FBDAC();
   }
   ```

4. 通过上面的观察分析,可以察觉到这不是常规的函数调用约定,而且肯定不是`fastcall`;注意到函数中出现了`cgo`字样,且在该`so`库的函数表中也有大量的`cgo`函数

5. 分析:

   1. 这个`so`库的调用约定与常规的不同,很可能是全部通过栈进行的,包括参数的传递以及返回值的传递
   2. `golang`是可以和`c`进行交叉调用的,而且可以编译成`so`库
   3. 这个so库的核心加密部分应该是由`golang`编写的,`C`接口函数就起到个连接、转发的作用

6. 定位关键加密函数

   虽然看起来有点奇怪,但是这并不妨碍定位到关键函数,跟进上面的`sub_BC3C4658()`函数:

   ```c
   int __fastcall sub_BC3C4658(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10)
   {
   int v10; // r10
   int v11; // lr
   int result; // r0
   int v13; //
   _DWORD *v14; //
   int v15; //
   int v16; //
   int v17; //
   int v18; //
   int v19; // BYREF
   
   while ( (unsigned int)&a5 <= *(_DWORD *)(v10 + 8) )
       sub_BC360D10();
   v19 = a6;
   sub_BC3BED84();
   v19 = v13;
   sub_BC3BED84();
   sub_BC3C0D0C(v13, (int)v14, v16, v16, v11, (int)v19, v13, (int)v14, v16, v13, v14, v16, v17, v18);
   sub_BC3BEC54();
   result = v15;
   a10 = v15;
   return result;
   }
   ```

   跟进`sub_BC3C0D0C()`,发现了`package_name`,`pakcage_hash`字样,且进行了大量函数调用,将动态调试的目光先锁定在它身上

   ```c
   int __fastcall sub_BC3C0D0C(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, _DWORD *a11, int a12, int a13, int a14)
   {
   while ( (unsigned int)&a5 <= *(_DWORD *)(v14 + 8) )
       sub_BC360D10();
   v34 = v15;
   a13 = 0;
   a14 = 0;
   v72 = &off_BC3DFD34;
   sub_BC3C42D8();
   v38 = a11;
   sub_BC3C21F8();
   v16 = v46;
   if ( v61 )
   {
       a13 = 0;
       a14 = 0;
       result = sub_BC3C0CDC();
   }
   else
   {
       v68 = v49;
       v69 = v57;
       sub_BC304C38();
       v71 = v38;
       sub_BC305928();
       if ( dword_BC47A460 )
       {
         sub_BC36294C(dword_BC47A460, v17, v71, &unk_BC3D2F68);
         v18 = v19;
       }
       else
       {
         v18 = v71;
         *v71 = &unk_BC3D2F68;
       }
       v50 = (int)v18;
       sub_BC3A70F8();
       if ( v55 )
       {
         v20 = a9;
         v21 = a8;
         v22 = a7;
       }
       else
       {
         v23 = v65;
         do
         *v23++ = 0;
         while ( (int)v23 <= (int)&v65 );
         qmemcpy(v65, "__package_name__", sizeof(v65));
         sub_BC34E438((int)&v65, (int)v23, 0, 95, v15, 0, (unsigned __int8 *)v65, 16, (int)&unk_BC3CF818, v50);
         v67 = v47;
         v64 = v51;
         v24 = v65;
         do
         *v24++ = 0;
         while ( (int)v24 <= (int)&v65 );
         qmemcpy(v65, "__package_hash__", sizeof(v65));
         sub_BC34E438(v47, (int)v24, v51, 0, v35, 0, (unsigned __int8 *)v65, 16, v47, v51);
         v66 = v48;
         v63 = v52;
         sub_BC3C4318();
         sub_BC34E438(0, v25, v26, v27, v36, 0, v39, v41, v48, v52);
         sub_BC301F40();
         v28 = (unsigned __int8 *)*v71;
         v70 = v42;
         v40 = v28;
         v43 = v67;
         sub_BC309480();
         *v53 = &unk_BC3D2A70;
         if ( dword_BC47A460 )
         sub_BC36294C(v53 + 1, v29, v53 + 1, v70);
         else
         v53 = v70;
         sub_BC3C440C();
         sub_BC34E438(0, v30, v31, v32, v37, 0, v40, v43, v64, (int)v53);
         sub_BC3C094C();
         sub_BC301F40();
         v70 = v44;
         v45 = v66;
         sub_BC309480();
         *v54 = &unk_BC3D2A70;
         if ( dword_BC47A460 )
         sub_BC36294C(v54, dword_BC47A460, v54 + 1, v70);
         else
         v54 = v70;
         sub_BC3AEB0C();
         v22 = v45;
         v21 = v63;
         v20 = (int)v54;
       }
       if ( v16 )
       {
         a13 = 0;
         a14 = 0;
       }
       else
       {
         sub_BC3C05EC(v56, v22, v21, v20, v34, v22, v21, v20, v69, v58, v59, v68, v55, v56, v59, 0);
         a13 = v60;
         a14 = v62;
       }
       result = sub_BC3C0CDC();
   }
   return result;
   }
   ```

### 五、so层动态调试、分析调用约定

1. 在`jni`接口处下断,符合`fastcall`调用约定

   
         
         

2. 传递给`cgo`的参数

   

   

3. 进入`cgo`函数,首先观察栈平衡循环

   

   结束后

   

4. 观察从哪里取得参数

   

5. 在下一个函数调用前下断,观察参数传递

   

6. f8步过,观察栈变化以及从哪里取的返回值

   

7. 总结得出函数调用:参数完全通过栈传递,返回值存储在参数往下的地址中

### 六、so层加密算法还原

1. 前置工作分析:可以跟踪调试`sub_BC3C0D0C`函数,发现这里只是进行了一些参数以及其他操作,真正的加密处理函数在这个函数的结尾处调用,即:`sub_C2DC05EC`

2. 需要说明的是,这个函数中对`java`层传入的`key`进行了`base64`解码,并得到两个密钥:

   1. `key1`: `4c7e?2fab4f4>1>2>5d0ee4bb61d7b03`
   2. `key2`: `mIZUjjghGd`

3. 在`sub_C2DC05EC`处下断,分析参数

   

4. 首先对传入的两个`key`进行了异或得到一个新`key`

   

5. 再对`key`进行了两次转换,得到

   第一次得到

   

   第二次得到,此时密钥已经成为一个不可读的字节序列,这也是最终加密算法使用的密钥

   

6. 之后生成了一个长度为`0x10`的随机串,这是最终加密算法使用的初始向量

   

7. 之后传入密钥,调用一个函数后返回了一个全局地址和一个指针

   

   其中指针的内容是

   

   到这里的话,因为前面已经猜测这是一个`golang`编写的`so`库,此时基本可以确定这是使用的`go`的`crypto/cipher`加密库了;

   通过查看`go`加密的源码,能发现其`newCipher`最终会生成两个长度为`0x3C`即`60`的密钥,分别用来加密和解密;

   又由于是对称加密,因此使用的是用一个密钥,这里生成的两个密钥刚好是逆序的关系,可能是因为方便实现的原因

   

   

   

   

8. 说明:这里usb断了一次连接,因此下面一些参数的地址可能和上面不同

9. 接着,调用了一个保存在寄存器的地址,并将明文和前面密钥生成的结构当作参数传了进去

   

10. 函数返回后,那片内存空间里已经由全0填充为了字节序列,可以确定其为加密函数

   

   

11. 最后,又对加密生成的序列进行了一次字母表映射,字母表为16进制的16个字符

   

12. 最终得到的密文为

   

12. 算法还原

1. 找到最后一步密钥转换后生成的字节序列,这就是真正的加密密钥

2. 确定加密算法,根据几个特征,推测应该是一种有初始向量的流加密,最终确定为CFB模式的aes加密

   1. go的加密库
   2. 有初始向量的加密算法
   3. 没有padding操作

3. 之后,首先用go还原一下,验证加密算法无误

4. 之后用python重现,这里要注意的是默认的python和go的CFB加密结果是不同的,需要在python加密中设置以下属性:

   

5. 最后验证python和go的加密结果一致即可

### 七、Java层签名算法分析还原

1. 接下来就是java层sign字段的生成了,这个比较简单,它依次调用了两个哈希算法

   1. sha256:根据post参数的格式及各个参数生成输入,经过sha256得到一个十六进制字符串

      ```java
          public static String e(String arg2) {
            try {
                  MessageDigest v0 = MessageDigest.getInstance("SHA-256");
                  v0.update(arg2.getBytes("UTF-8"));
                  return a.b(v0.digest()); // 将字节数组转换为16进制字符串
            }
            catch(NoSuchAlgorithmException v2_1) {
                  v2_1.printStackTrace();
                  return "";
            }
            catch(UnsupportedEncodingException v2) {
                  v2.printStackTrace();
                  return "";
            }
          }
      
      ```

   2. md5:将上面的到的sha256字符串经过md5变换得到最终的sign值

      ```java
          public static String b(String arg2) {
            try { // a() 将字节数组转换为16进制字符串
                  return c.a(MessageDigest.getInstance("MD5").digest(arg2.getBytes("UTF-8")));
            }
            catch(Exception v2) {
                  v2.printStackTrace();
                  return "";
            }
          }
      ```

### 八、发包验证算法正确性

1. 用python实现它的数据加解密以及签名、封装过程,生成请求数据,并向其服务器的一个接口发起请求验证:

   

2. 服务器正常返回数据,解密得到数据:


ggjj20082008 发表于 2022-11-12 13:26

期待成品发布                           

archosaur 发表于 2022-9-30 14:08

49+1?,果然这才是生产力

正己 发表于 2022-9-21 22:48

加个精,期待大佬后续佳作{:301_997:}

孤灯独饮 发表于 2022-9-21 23:14

啥APP是好看的APP么?

博爵 发表于 2022-9-21 23:34

支持,可惜没有成品

一介书生 发表于 2022-9-22 00:46

老哥,apk可以放个链接么,不是成品

hjw01 发表于 2022-9-22 00:47

很不错哦,还有一种方法是直接脱壳解析所有类,重新生成dex,修正androidmanifest应该是可行的。有空再试试

泥河湾メ~晓亮﹀ 发表于 2022-9-22 05:04

感谢楼主分享

mozhongzhou 发表于 2022-9-22 07:39

高 太高了

CA99588 发表于 2022-9-22 08:56

感谢分享!

娃娃菜啊 发表于 2022-9-22 08:59

你这是纯技术型的,虽然知道软件无奈看不懂过程,祝早日突破技术更上一层
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: xx度灰app加密算法分析还原