• 某车联网App 通讯协议加密分析(三) Trace Block


    一、目标

    之前我们已经用unidbg跑通了libencrypt.so,那么如何判断跑出来的结果是对是错?再如何纠正unidbg跑错误的流程,是我们今天的目标。

    v6.1.0

    二、步骤

    找到明显的接口来判断

    checkcode是加密,加密的结果确实不好判断是否正确。不过我们可以试试解密,能解密就是对的,简单粗暴。这里解密函数是 decheckcode 。

    public void callB() {
        String strA = "FlK6XicivmCwPSE3sk6b71m9WbWd/gYZtlajqGXhEXXjmWEZziR51rVWSEDwUUi4UN9RnoCGbLNmqI80Fiog4Sw==";
        String methodName = "decheckcode(Ljava/lang/String;)Ljava/lang/String;";
        DvmObject ret = dvmClass.callStaticJniMethodObject(emulator, methodName,strA);
        String strOut = (String)ret.getValue();
        System.out.println("call decheckcode: " + strOut);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    跑一下,这个结果明显不对,死心了

    call decheckcode: fac34ffa581987c7a1ffa5b876ca96ce
    
    • 1

    分析问题

    结果不对,肯定是过程不对。

    那么解决方案就是 分析对比unidbg运行的流程和app运行的流程 有哪里有不同?

    对比运行流程有三个粒度, 函数、代码块和代码。 (在ida里按空格,出现的流程图中的每个块就是代码块)

    我们今天主要要对比 decheckcode函数,所以先从代码块的粒度来做Trace。

    Trace Block

    unidbg提供一个BlockHook,每运行到一个代码块就触发这Hook,我们就利用他来做Trace Block

    // 保存需要用frida Hook的Block的地址
    public static Map subTraceMap = new HashMap();
    // 保存命中的Block地址的次数,命中次数太多的就忽略掉。
    public static Map calcMap = new HashMap();
    
    // 入参是so基地址,   需要Trace代码的开始地址和结束地址
    private void traceBlock(final long baseAddr, final long starAddr, final long endAddr) {
        emulator.getBackend().hook_add_new(new BlockHook() {
            @Override
            public void hookBlock(Backend backend, long address, int size, Object user) {
                // 代码块需要大于20个字节,  块太小 影响 frida的 hook
                if (size > 20) {
                    Instruction[] insns = emulator.disassemble(address, 4, 0);
    
                    int iSize = insns[0].getSize();
                    int iUseAddr = 0;
    
                    if (iSize == 4) {
                        // ARM模式 4字节
                        iUseAddr = (int) (address - baseAddr);
                    } else {
                        // THUMB模式 2 字节 ,hook的时候 + 1 ,
                        iUseAddr = (int) (address - baseAddr) + 1;
                    }
    
                    if (calcMap.containsKey(iUseAddr)) {
                    // 保存命中次数
                        int iValue = calcMap.get(iUseAddr);
                        calcMap.put(iUseAddr, iValue + 1);
    
                        // 4次以上的调用就不显示了, 也不用frida Trace了
                        if (iValue > 3) {
                            subTraceMap.remove(iUseAddr);
                        } else {
                            System.out.println("  " + "sub_" + Integer.toHexString((int) (address - baseAddr)) + " ");
                        }
    
                    } else {
                        calcMap.put(iUseAddr, 1);
                        subTraceMap.put(iUseAddr, 1);
    
                        System.out.println("  " + "sub_" + Integer.toHexString((int) (address - baseAddr)) + " ");
                    }
                }
            }
    
            @Override
            public void onAttach(UnHook unHook) {
    
            }
    
            @Override
            public void detach() {
    
            }
    
        }, starAddr, endAddr, 0);
    
    }
    
    • 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

    Trace Block结束之后,把命中的代码块地址都打印出来,用于在Frida中去hook

    public void PrintHookSubInfo(){
        System.out.println("subTrace len = " + subTraceMap.size());
        String strOut = "";
        for (Map.Entry entry : subTraceMap.entrySet()) {
            int iShow = entry.getKey();
            // 为了和unidbg显示一致这里处理下
            if(iShow % 2 != 0){
                iShow = iShow -1;
            }
            strOut = strOut + "  ,['sub_" + Integer.toHexString(iShow ) + "','0x" + Integer.toHexString((int)entry.getKey() ) + "']";
        }
        System.out.println(strOut);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    有了这两个函数就可以干活了

    public void callB() {
        traceBlock(module.base,module.base,module.base  + module.size);
      ...
            String methodName = "decheckcode(Ljava/lang/String;)Ljava/lang/String;";
            DvmObject ret = dvmClass.callStaticJniMethodObject(emulator, methodName,strA);
      ...
        PrintHookSubInfo();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在执行 decheckcode 之前去做Trace, 执行之后去打印所有命中的Block地址。

    Tip:

    这个样本没那么复杂,所以就直接Trace所有代码范围,讲究人是需要缩小范围,只Trace自己感兴趣的部分。

    Find native function Java_com_bangcle_comapiprotect_CheckCodeUtil_decheckcode => RX@0x4002b1bc[libencrypt.so]0x2b1bc
      sub_2b1bc
      sub_2b20c
      sub_2b238
      sub_2b254
      sub_2b270
      sub_2b28c
      sub_2b2a8
      sub_2b2c4
      sub_2b2e0
      sub_2b2fc
      sub_2b318
      sub_2b334
        ...
    
    subTrace len = 127
      ,['sub_21400','0x21400']  ,['sub_21c00','0x21c00']  ,['sub_22200','0x22200']  ,['sub_2b604','0x2b604']  ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    Trace的结果出来了,命中的地址列表也打印出来了,一共命中了127个地址块。

    frida Hook 对比

    把命中的地址列表导入到frida里面去hook,然后就可以对比出来 unidbg跑的流程和App跑的流程的差别了。

    function hook_suspected_function(targetSo) {
        const funcs = [
            ['sub_21400','0x21400']  ,['sub_21c00','0x21c00']  ,['sub_  ...
              ];
    
        for (var i in funcs) {
            let relativePtr = funcs[i][1];
            let funcPtr = targetSo.add(relativePtr);
            let describe = funcs[i][0];
            let handler = (function() {
                return function(args) {
                    // console.log("\n");
                    console.log(TAG + describe);
                };
            })();
            Interceptor.attach(funcPtr, {onEnter: handler});
        }
    }
    
    
    function traceNative() {
        var targetSo = Module.findBaseAddress('libencrypt.so');
        console.log(TAG +" Trace ############# libencrypt.so: " +targetSo);
        hook_suspected_function(targetSo);
    }
    
    // 然后在hook  decheckcode的时候打印Trace结果
    Interceptor.attach(targetSo.add(0x2B1BC ),{
        onEnter: function(args){
            traceNative();
    
            var strCls = Java.use('java.lang.String');
    
            var strA = Java.cast(this.context.x2, strCls);
            console.log(TAG + "-------- decheckcode a = " + strA);
    
        },
        onLeave: function(retval){
            var strCls = Java.use('java.lang.String');
            var strRc = Java.cast(retval, strCls);
            console.log(TAG + "-------- decheckcode rc = " + strRc);
    
        }
    });
    
    • 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

    对比结果

    对比的方法比较low,先把unidbg Trace Block的结果复制到文本文件1,然后把frida hook打印的结果复制到文本文件2。 最后开启 Beyond Compare 来对比

    cmp.png

    1:cmp

    过程虽然很low,但是结果可一点都不low,从对比的结果看,大家之前都是好朋友,不过 sub_18650 之后就开始分道扬镳了。

    这时候就需要问问ida了。

    fopen.png

    1:fopen

    这里fopen了一个文件,文件名是做了base64。

    base64谁不会呢,随便写两行代码就可以解出来了 /proc/%d/cmdline

    这又在考我们的android编程知识了,问了下谷哥,哥说了,这是在读进程名,对于apk来说,进程名就是他的包名。

    回想在 unidbg中 有个不起眼的报错

    [11:26:48 927]  INFO [com.github.unidbg.linux.ARM64SyscallHandler] (ARM64SyscallHandler:1309) - openat dirfd=-100, pathname=/proc/2256/cmdline, oflags=0x0, mode=0
    
    • 1

    这也是在提醒我们,读取进程名失败了。

    重定向io

    unidbg是支持这种情况的,先让 CaranywhereDemo 多继承一个 IOResolver 来做io重定向

    public class CaranywhereDemo  extends AbstractJni implements IOResolver {
        ...
    
        public Caranywhere(String apkFilePath) throws DecoderException, IOException {
          ...
                emulator.getSyscallHandler().addIOResolver(this);
          ...
        }
    
        FileResult f1;
    
        public FileResult getF1(String pathname, int oflags) {
            if (f1 == null) {
                f1 = FileResult.success(new ByteArrayFileIO(oflags, pathname, "com.xxx.aeri.caranywhere".getBytes()));
            }
            System.out.println("new f1==" + pathname + "===" + vm.getPackageName());
            return f1;
        }
    
        @Override
        public FileResult resolve(Emulator emulator, String pathname, int oflags) {
            System.out.println(pathname);
            if ("/proc/self/cmdline".equals(pathname) || ("/proc/" + emulator.getPid() + "/cmdline").equals(pathname)) {
                            return getF1(pathname, oflags);
            }
    
            return null;
        }
    }
    
    • 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

    ok了,这几步又和App跑的一样了,大家又是好朋友了。

    不过问题还是没有解决,跑出来的结果还是不对,木有解密成功。而且貌似这个app还有坑,hook点一多就摆烂,直接崩溃。

    得找新武器对付它了,期待下一章的大结局吧。

    三、总结

    何以解忧,唯有Trace。

    能下断点Debug的App,一定就逃不出手心了。所以现在App的关注点都是抵抗Debug,抵抗下断点。

    结果不对,就和正确的结果去对比流程,跑的和你一模一样,总没毛病吧?

    ffshow.jpeg

    1:ffshow

    凡说之难 在知所说之心 可以吾说当之

  • 相关阅读:
    vue3的双向绑定 v-model实现原理和案例
    XTU OJ 1381表格
    动态网页和前端技术基础知识
    c语言编程 结构(struct)
    Class类三种函数的区别:普通函数、get函数、 静态函数
    C++小程序——“靠谱”的预测器
    python+django+vue房屋租赁系统 8gwmf
    GPU使用和管理经验
    循环神经网络(RNN)
    两个数组的交集
  • 原文地址:https://blog.csdn.net/fenfei331/article/details/126826758