android逆向奇技淫巧三十一:unidbg常见功能代码

  逆向so,unidbg这种模拟器必不可少,其优势:

  •   ida、frida遇到了严重的反调试
  •        生产环境生成sign字段(配合springboot尤其方便,有现成的框架可以直接拿来用了:https://github.com/anjia0532/unidbg-boot-server)
  •        可以打印JNIEnv成员函数的调用日志,比如registerNatives、GetStringUTFChars等;

  这里列举一些常见的unidbg功能供大伙逆向的时候参考;

  1、hook代码:这是逆向最基本的功能之一,frida的hook代码都不陌生吧?unidbg底层用了hookZz的框架,所以hook的代码长这样的:

public void hook(){
        //unidbg集成了HookZz框架
        HookZz hook = HookZz.getInstance(emulator);
        //直接hook add函数的地址,比通过符号hook更具有“普适性”
        hook.replace(module.base + 0x3DC + 1, new ReplaceCallback() {
            @Override
            public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
                //R2和R3才是参数,R0是env,R1是object
                System.out.println(String.format("R2: %d, R3: %d",context.getIntArg(2),context.getIntArg(3)));
                //把第二个参数R3改成5
                emulator.getBackend().reg_write(Unicorn.UC_ARM_REG_R3,5);
                return super.onCall(emulator, context, originFunction);
            }

            @Override
            public void postCall(Emulator<?> emulator, HookContext context) {
                emulator.getBackend().reg_write(Unicorn.UC_ARM_REG_R0,10);
                //返回值放R0,这里直接修改返回值
                super.postCall(emulator, context);
            }
        }, true);
    }

   代码整体的结构和frinda的hook是不是很类似了?onCall就是刚进入函数时候的回调(本质就是在函数入口处hook),onPost就是在函数ret前的hook回调!

   2、打patch方法:hook本质也是patch,还有很多关键的跳转代码(android下的B、BL等)可能也要NOP掉才能按照我们自己的逻辑执行!最原始打patch的办法就是在IDA或010editor更改,为了更好地逆向so,unidbg也提供了打patch的方法,如下:

  public void patch(){
        UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x3E8);
        byte[] code = new byte[]{(byte) 0xd0, 0x1a};//直接用硬编码改原so的代码:subs r0,r2,r3
        pointer.write(code);
    }
    public void patch2(){
        UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x3E8);
        Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb);
        String s = "subs r0, r2, r3";
        byte[] machineCode = keystone.assemble(s).getMachineCode();
        //byte[] code = ;
        pointer.write(machineCode);
    }

      代码很简单,可以直接在目标位置写硬编码,也可以借助keystone写汇编代码!

   3、hook的时候需要知道so的基址和代码偏移,unidbg提供的方法如下:

// 加载so到虚拟内存
DalvikModule dm = vm.loadLibrary("libnative-lib.so", true);
// 得到模块对象,然后根据导出的函数名找到函数入口偏移,比直接在代码写死地址灵活一些
module = dm.getModule();
int address = (int) module.findSymbolByName("funcNmae").getAddress();

   4、有一点可能会超出初学入门者的想象和预期,就是unidbg也支持单步调试,叫console debug,就是在console下输入各种命令调试!操作也简单:

  (1)先下个断点:当然这里也能制定特定的偏移地址

emulator.attach().addBreakPoint(module.findSymbolByName("funName").getAddress());

  (2)代码运行到断点后正常情况下会停下,然后逆向人员就可以在console下输入各种命令操作了,原理和hyperpwn、gbd等类似,如下:   

         

   比如r是删除断点,b是增加断点,n是步过等!其他写方面的操作命令如下:

wr0-wr7, wfp, wip, wsp <value>: write specified register
wb(address), ws(address), wi(address) <value>: write (byte, short, integer) memory of specified address, address must start with 0x
wx(address) <hex>: write bytes to memory at specified address, address must start with 0x

  如果命中断点后想做一个个性化的操作,但是又觉得在console上挨个敲命令麻烦,也可以写代码固化下来,比如这样:

public void ReplaceArgByConsoleDebugger(){
    emulator.attach().addBreakPoint(module.findSymbolByName("funName").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext context = emulator.getContext();
            String fakeInput = "hello world";
            int length = fakeInput.length();
            // 修改r1值为新长度
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R1, length);
            MemoryBlock fakeInputBlock = emulator.getMemory().malloc(length, true);
            fakeInputBlock.getPointer().write(fakeInput.getBytes(StandardCharsets.UTF_8));
            // 修改r0为指向新字符串的新指针
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, fakeInputBlock.getPointer().peer);

            Pointer buffer = context.getPointerArg(2);
            // OnLeave
            emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
                @Override
                public boolean onHit(Emulator<?> emulator, long address) {
                    String result = buffer.getString(0);
                    System.out.println("base64 result:"+result);
                    return true;
                }
            });
            return true;
        }
    });
}

    个人觉得和hook某个地址本质上是一样的,这种方式供参考!

   5、内存检索:搜索某些sign字段、字符串的时候特别重要,如下: 

private Collection<Pointer> searchMemory(long start, long end, byte[] data) {
    List<Pointer> pointers = new ArrayList<>();
    for (long i = start, m = end - data.length; i < m; i++) {
        byte[] oneByte = emulator.getBackend().mem_read(i, 1);
        if (data[0] != oneByte[0]) {
            continue;
        }
        if (Arrays.equals(data, emulator.getBackend().mem_read(i, data.length))) {
            pointers.add(UnidbgPointer.pointer(emulator, i));
            i += (data.length - 1);
        }
    }
    return pointers;
}

  6、条件断点:为了避免被过多信息干扰,很多时候的断点或hook是需要设置条件的,符合了条件才需要进一步打印出来查看结果,unidbg也不例外,也是这个思路。举个例子:比如strcat、strstr、strcmp这种函数,每时每刻都在被大量的模块调用,直接hook打印会产生大量无用日志,严重影响排查。同时大量日志得打印也会严重拖慢运行速度,所以需要自己写条件判断是否需要打印日志!比如这种:

  (1)只打印某个特定so调用的strcat函数:

public void hookstrcmp(){
    long address = module.findSymbolByName("strcat").getAddress();
    emulator.attach().addBreakPoint(address, new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext registerContext = emulator.getContext();
            String arg1 = registerContext.getPointerArg(0).getString(0);
            String moduleName = emulator.getMemory().findModuleByAddress(registerContext.getLRPointer().peer).name;
            if(moduleName.equals("libxxx.so")){
                System.out.println("strcat arg1:"+arg1);
            }
            return true;
        }
    });
}

  (2)只打印某个特定函数中调用的strcat函数:

// 早先声明全局变量 public boolean show = false;

public void hookstrcat(){
    emulator.attach().addBreakPoint(module.findSymbolByName("targetfunName").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext registerContext = emulator.getContext();

            show = true;//进入目标函数就把show设置为true,下面才好打印日志
            emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() {
                @Override
                public boolean onHit(Emulator<?> emulator, long address) {
                    show = false;//离开目标函数就把show设置为false,下面才知道不打印日志
                    return true;
                }
            });
            return true;
        }
    });

    emulator.attach().addBreakPoint(module.findSymbolByName("strcat").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext registerContext = emulator.getContext();
            String arg1 = registerContext.getPointerArg(0).getString(0);
            if(show){
                System.out.println("strcmp arg1:"+arg1);
            }
            return true;
        }
    });
}

 

总结:

1、个人感受,逆向调试时还是IDA的图形化界面更方便,所以不到万不得已,我一般首选IDA调试分析!

2、一旦后期要在生产线上生成sign字段,这时再用unidbg就更合适了!

3、逆向思路整理和总结

 

 

 

 

 

参考:

1、unidbg常见方法和frida对照: https://reao.io/archives/90/

2、frida长用的脚本代码:https://codeshare.frida.re/

                                         https://github.com/iddoeldor/frida-snippets  

posted @ 2022-06-28 21:10  第七子007  阅读(4912)  评论(0编辑  收藏  举报