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

查看 166|回复 11
作者:red1y   
xx度灰app加密算法分析还原
本文包括
  • java层分析
  • so层加密算法分析
  • java层签名分析
  • 发包验证
  • b站视频连接 https://www.bilibili.com/video/BV1id4y1g7G1/?spm_id_from=333.999.0.0&vd_source=23b9de401c27e819abddbd5551eddbda

    一. 修改测试
    [ol]
  • 重新打包签名生成可调试版本后运行闪退
  • 使用mt管理修改dex文件重新编译后正常运行,访问相关资源会提示正版维护信息
  • 使用mt管理器重新签名后运行闪退
    [/ol]
    二、Java层静态分析
    [ol]

  • 定位启动Activity
         
          
          
         

       

  • 分析onCreate函数
    LauncheActivity继承自BaseActivity,BaseActivity中调用了几个函数,经分析确定了w5()为关键函数:
    @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();
       }
    }

  • 跟踪w5()
    在前面测试过程中有一个信息是:修改了签名后的app并不会直接闪退,而是在申请完相关使用权限后才会退出,如果没有通过权限的申请,会自动正常退出,而不是闪退;在w5()中找到了相关权限申请的函数T6()
    @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()
    public void T6() {
       /* other code */
       // if no permission, return and eixt
       LaunchActivity.this.B6();
    跟踪B6()后续的一系列函数调用,最终定位到一个向服务器发送请求的函数
    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()函数中在发送请求前进行了一系列的数据操作
    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()中进行
    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函数进行加密
    public String f(String arg1, String arg2) {
       return EncryptUtil.encrypt(arg1, arg2);
    }
    public static native String encrypt(String arg0, String arg1) {
    }
    同时在代码中发现了多个密钥,包括但不限于,第一个base64编码的密钥在跟踪流程中传递给了native函数
  • BwcnBzRjN2U/MmZhYjRmND4xPjI+NWQwZWU0YmI2MWQ3YjAzKw8cEywsIS4BIg==
  • 81d7beac44a86f4337f534ec9332837

    [/ol]
    三、Java层动态跟踪、Hook分析
    [ol]

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

  • 调试跟踪函数,最终定位发现程序在加载上述native加密so库时闪退
    .method static constructor ()V
             .registers 1
    00000000  const-string        v0, "sojm"
    00000004  invoke-static       System->loadLibrary(String)V, v0
    0000000A  return-void
    .end method

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

  • 使用 frida hook encrypt函数,主动调用其多次加密相同数据,可以发现每次得到的结果都不同,应该使用了某种随机量
    [/ol]
    四、so层静态分析
    [ol]

  • 使用ida pro分析sojm 库,通过观察函数名可以得到其是通过静态注册的,这里的四个参数也符合常规的jni函数
    // 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[4]; // [sp+4h] [bp-2Ch] BYREF
    int v11; // [sp+14h] [bp-1Ch]
    v8 = cgo_wait_runtime_init_done();
    v10[3] = a4; // jstring
    v10[2] = a3; // jstring
    v10[1] = a2; // _clazz
    v10[0] = a1; // env
    v11 = 0;
    crosscall2(cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt, v10, 20, v8);
    cgo_release_context(v8);
    return v11; // jstring
    }

  • 但是后续的操作就不太常规了,可以看出它把参数依次赋给了一个数组;同时调用了crosscall2,其参数为:
    [ol]
  • 一个函数地址
  • 参数数组
  • 应该是参数数组的长度,size
  • init函数的返回值
    [/ol]
    值得注意的是,v10明明只有四个元素,但是传入的参数size却是20 = 5 * 4,同时v11被置0后又没有显式的赋值,最终却被返回,猜测应该是在cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt中被赋值了

  • 进入cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt后发现参数个数很奇怪,而且sub_BC3C4658传入了很多重复的参数;
    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; // [sp+14h] [bp-4h]
    int v10; // [sp+14h] [bp-4h]
    while ( (unsigned int)&a5

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

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

  • 定位关键加密函数
    虽然看起来有点奇怪,但是这并不妨碍定位到关键函数,跟进上面的sub_BC3C4658()函数:
    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; // [sp+Ch] [bp-2Ch]
    _DWORD *v14; // [sp+10h] [bp-28h]
    int v15; // [sp+10h] [bp-28h]
    int v16; // [sp+14h] [bp-24h]
    int v17; // [sp+20h] [bp-18h]
    int v18; // [sp+24h] [bp-14h]
    int v19[2]; // [sp+30h] [bp-8h] BYREF
    while ( (unsigned int)&a5
    跟进sub_BC3C0D0C(),发现了package_name,pakcage_hash字样,且进行了大量函数调用,将动态调试的目光先锁定在它身上
    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
    [/ol]
    五、so层动态调试、分析调用约定
    [ol]

  • 在jni接口处下断,符合fastcall调用约定


    image-20220921175805969.png (102.07 KB, 下载次数: 1)
    下载附件
    2022-9-21 21:50 上传

  • 传递给cgo的参数


    image-20220921180034269.png (75.33 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:51 上传



    image-20220921180136723.png (40.66 KB, 下载次数: 1)
    下载附件
    2022-9-21 21:51 上传

  • 进入cgo函数,首先观察栈平衡循环


    image-20220921180451141.png (83.75 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:52 上传

    结束后


    image-20220921180715293.png (109.45 KB, 下载次数: 1)
    下载附件
    2022-9-21 21:53 上传

  • 观察从哪里取得参数


    image-20220921181020664.png (104.08 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:53 上传

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


    image-20220921181238897.png (132.09 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:53 上传

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


    image-20220921181412980.png (103.19 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:54 上传

  • 总结得出函数调用:参数完全通过栈传递,返回值存储在参数往下的地址中
    [/ol]
    六、so层加密算法还原
    [ol]

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

  • 需要说明的是,这个函数中对java层传入的key进行了base64解码,并得到两个密钥:
    [ol]
  • key1: 4c7e?2fab4f4>1>2>5d0ee4bb61d7b03
  • key2: mIZUjjghGd
    [/ol]

  • 在sub_C2DC05EC处下断,分析参数


    image-20220921182713971.png (33.09 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:54 上传

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


    image-20220921182838936.png (70.15 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:54 上传

  • 再对key进行了两次转换,得到
    第一次得到


    image-20220921182913411.png (20.14 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:54 上传

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


    image-20220921183107882.png (20.25 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:55 上传

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


    image-20220921183027646.png (66.45 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:55 上传

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


    image-20220921183450895.png (76.12 KB, 下载次数: 1)
    下载附件
    2022-9-21 21:55 上传

    其中指针的内容是


    image-20220921183648512.png (103.95 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:55 上传

    到这里的话,因为前面已经猜测这是一个golang编写的so库,此时基本可以确定这是使用的go的crypto/cipher加密库了;
    通过查看go加密的源码,能发现其newCipher最终会生成两个长度为0x3C即60的密钥,分别用来加密和解密;
    又由于是对称加密,因此使用的是用一个密钥,这里生成的两个密钥刚好是逆序的关系,可能是因为方便实现的原因


    image-20220921195430035.png (33.27 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:56 上传



    image-20220921195522728.png (17.1 KB, 下载次数: 1)
    下载附件
    2022-9-21 21:56 上传



    image-20220921195620953.png (28.55 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:56 上传

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

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


    image-20220921190408627.png (136.95 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:59 上传

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


    image-20220921190708165.png (64.11 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:57 上传



    image-20220921190906871.png (90.51 KB, 下载次数: 1)
    下载附件
    2022-9-21 21:57 上传

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


    image-20220921191342058.png (48.45 KB, 下载次数: 1)
    下载附件
    2022-9-21 21:57 上传

  • 最终得到的密文为


    image-20220921191405844.png (53.37 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:57 上传

  • 算法还原

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

  • 确定加密算法,根据几个特征,推测应该是一种有初始向量的流加密,最终确定为CFB模式的aes加密
    [ol]
  • go的加密库
  • 有初始向量的加密算法
  • 没有padding操作
    [/ol]

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

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


    image-20220921191855157.png (13.15 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:58 上传

  • 最后验证python和go的加密结果一致即可
    [/ol]
    七、Java层签名算法分析还原
    [ol]

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

  • sha256:根据post参数的格式及各个参数生成输入,经过sha256得到一个十六进制字符串
      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 "";
          }
      }

  • md5:将上面的到的sha256字符串经过md5变换得到最终的sign值
      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 "";
          }
      }
    [/ol]
    [/ol]
    八、发包验证算法正确性
    [ol]

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


    image-20220921194210696.png (385.57 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:58 上传

  • 服务器正常返回数据,解密得到数据:
    [/ol]


    image-20220921194419585.png (133.61 KB, 下载次数: 0)
    下载附件
    2022-9-21 21:58 上传

    下载次数, 函数

  • archosaur   

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

    期待成品发布                             
    孤灯独饮   

    加个精,期待大佬后续佳作
    博爵   

    啥APP  是好看的APP么?
    一介书生   

    支持,可惜没有成品
    hjw01   

    老哥,apk可以放个链接么,不是成品
    泥河湾メ~晓亮﹀   

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

    感谢楼主分享
    CA99588   

    高 太高了
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部