安卓逆向-unidbg辅助算法还原
标签搜索

安卓逆向-unidbg辅助算法还原

sana
2025-01-17 / 0 评论 / 34 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2025年01月17日,已超过137天没有更新,若内容或图片失效,请留言反馈。

一、搜狗unidbg

1. 前置

  • 方法定位为如下位置:

image-20250115142106843

  • 并不以整个案例为目的,只是执行unidbg进行算法还原,前置不聊了;
  • 目标函数就是encrypt,so文件为libSCoreTools.so;
  • 直接开始unidbg;

2. unidbg模拟执行

  • 初始的代码如下:
package com.xyt.sana;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;

import java.io.File;
import java.nio.charset.StandardCharsets;


public class sougou extends AbstractJni {
    public static AndroidEmulator emulator;
    public static Memory memory;
    public static VM vm;
    public static Module module;


    sougou() {
        emulator = AndroidEmulatorBuilder
                .for32Bit()
                .addBackendFactory(new Unicorn2Factory(true))
                .build();

        memory = emulator.getMemory();

        memory.setLibraryResolver(new AndroidResolver(23));

        vm = emulator.createDalvikVM(new File("apks/sougou/sougou.apk"));
        vm.setJni(this);
        vm.setVerbose(true);
        DalvikModule dm = vm.loadLibrary(new File("apks/sougou/libSCoreTools.so"), true);
        dm.callJNI_OnLoad(emulator);
        module = dm.getModule();

    }

    public static void main(String[] args) {
        sougou demo = new sougou();
    }
}
  • 直接运行没有什么问题,就可以开始调用函数了,它的地址可以去ida查看,参数是有三个,这边固定入参;
public void call_encrypt(){
    List<Object> list = new ArrayList<>(10);
    list.add(vm.getJNIEnv());
    list.add(0);
    list.add(vm.addLocalObject(new StringObject(vm,"http://app.weixin.sogou.com/api/searchapp")));
    list.add(vm.addLocalObject(new StringObject(vm,"type=2&ie=utf8&page=1&query=%E5%A5%8B%E9%A3%9E%E5%AE%89%E5%85%A8&select_count=1&tsn=1&usip=")));
    list.add(vm.addLocalObject(new StringObject(vm,"sana")));
    Number number = module.callFunction(emulator,0x9ca1,list.toArray());
    String result = vm.getObject(number.intValue()).getValue().toString();
    System.out.println("result--->>>"+result);
}
  • 直接运行发现并没有得到我们想要的输出;

image-20250115142940192

  • 这是为什么呢,它也没有报错,我们去ida看看;

image-20250115143333828

  • 这里可以看到是有一个init函数的,在java层也是可以看到,那这里有没有可能是函数的初始化问题呢?我们去调用一下init函数;
public void call_init(){
    List<Object> list = new ArrayList<>(10);
    list.add(vm.getJNIEnv());
    list.add(0);
    DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null); // context
    list.add(vm.addLocalObject(context));
    module.callFunction(emulator, 0x9565, list.toArray());
};
  • 直接执行发现出结果了,连环境都不用补;

image-20250115143538838

  • 接下来重点就是算法还原;

3. 算法还原

3.1 定位&Hook

  • 进入函数,将基本的参数修改一下就是如下的效果:

image-20250115144314336

  • 这里的关键函数肯定就是j_Sc_EncryptWallEncode了,参数都进去了,第四个参数v8应该是一个缓冲区,按照经验来说应该就是返回值的存放位置,c或者c++开发都很喜欢这么干,最后的返回值会于1比较,可能是用来表示函数执行情况的,这里可以hook验证一下,看看参数情况;
  • 首先使用hookzz来hook,;
public void hookzz_hook() {
    // 获取HookZz对象
    IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz
    hookZz.wrap(module.base + 0xA284 + 1, new WrapCallback<HookZzArm32RegisterContext>() {
        Pointer buffer;

        @Override
        // 方法执行前 onenter
        public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
            Pointer input1 = ctx.getPointerArg(0);
            Pointer input2 = ctx.getPointerArg(1);
            Pointer input3 = ctx.getPointerArg(2);
            // getString的参数i代表index,即input[i:]
            System.out.println("参数1--->>>" + input1.getString(0));
            System.out.println("参数2--->>>" + input2.getString(0));
            System.out.println("参数3--->>>" + input3.getString(0));

            buffer = ctx.getPointerArg(3);
        }

        @Override
        // 方法执行后 onleave
        public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
            // getByteArray参数1是起始index,参数2是长度,我们不知道结果多长,就先设置0x100吧
            byte[] outputhex = buffer.getByteArray(0, 0x100);
            Inspector.inspect(outputhex, "EncryptWallEncode output");
        }
    });

}
  • 大体没有什么需要注意的点,需要在方法调用前hook;

image-20250115153206020

  • 可以看到位置是没有问题的,参数和返回值都在这里,那么这个函数就是加密函数;
  • 那这里想要实现inline hook怎么实现?就看这条指令的时候的参数和返回值,因为它是有可能运行多次的,同样使用hookzz实现,这里顺便到下一条指令处打印一下返回值,为何到下一条指令的时候地址没有变成其他值?这是因为inline hook的时机是目标指令执行前;
private Pointer buffer;
// hookzz的inline hook
public void hookzz_inline() {
    IHookZz hookZz = HookZz.getInstance(emulator);
    hookZz.enable_arm_arm64_b_branch();

    hookZz.instrument(module.base + 0x9d24 + 1, new InstrumentCallback<Arm32RegisterContext>() {
        @Override
        public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) {
            Pointer input1 = ctx.getPointerArg(0);
            Pointer input2 = ctx.getPointerArg(1);
            Pointer input3 = ctx.getPointerArg(2);
            // getString的参数i代表index,即input[i:]
            System.out.println("参数1--->>>" + input1.getString(0));
            System.out.println("参数2--->>>" + input2.getString(0));
            System.out.println("参数3--->>>" + input3.getString(0));

            buffer = ctx.getPointerArg(3);
        }
    });

    hookZz.instrument(module.base + 0x9d28 + 1, new InstrumentCallback<Arm32RegisterContext>() {
        @Override
        public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) {
            Inspector.inspect(buffer.getByteArray(0, 0x100), "inline hook EncryptWallEncode");
        }
    });
}
  • 输出结果是一样的,这里就不贴图了;
  • 接下来使用unicorn原生的hook实现inline hook;
public void Unicorn_hook() {
    // 指令级hook
    emulator.getBackend().hook_add_new(new CodeHook() {

        @Override
        public void onAttach(UnHook unHook) {
        }

        @Override
        public void detach() {
        }

        @Override
        // 指令级hook
        public void hook(Backend backend, long address, int size, Object user) {
            if (address == (module.base + 0x9d24)) {
                RegisterContext ctx = emulator.getContext();
                Pointer input1 = ctx.getPointerArg(0);
                Pointer input2 = ctx.getPointerArg(1);
                Pointer input3 = ctx.getPointerArg(2);
                // getString的参数i代表index,即input[i:]
                System.out.println("hook in unicorn参数1--->>>" + input1.getString(0));
                System.out.println("hook in unicorn参数2--->>>" + input2.getString(0));
                System.out.println("hook in unicorn参数3--->>>" + input3.getString(0));

                buffer = ctx.getPointerArg(3);
            }
            if (address == (module.base + 0x9d28)) {
                Inspector.inspect(buffer.getByteArray(0, 0x100), "hook in unicorn result");
            }

        }
    }, module.base + 0x9d24, module.base + 0x9d28, null);
}
  • hook结果依旧是正确的;

image-20250115163650032

  • 对比上面的三份hook代码,两个是第三方的,一个是原生的,代码量都不算特别小,那么为何辅助算法还原还是要使用unidbg呢,那就要请出唯一真神——Console Debugger;
  • 下两个断点非常的容易,只需要一句代码;
public void consoledebugger_hook(){
    emulator.attach().addBreakPoint(module.base+0x9d24);
    emulator.attach().addBreakPoint(module.base+0x9d28);
}

image-20250115164712697

  • 断在对应的位置了,这时候可以去查看寄存器的值,mr0 mr1 mr2分别是我们的参数;

image-20250115164813734

  • 后续的图不放了,那返回值呢?mr3吗?

image-20250115164917072

  • 这里存的并不是我们的结果,但是在上面分析明明最后一个参数是返回值这里怎么又不是了?这是因为此时函数并未执行,断在了此处,记住r3的地址,在函数结束的时候打印一下;
  • 按下c让程序继续走,这里会在第二个断点停下来,此时再看看刚刚的地址,0x40203000可以查看地址,后面可以跟长度;

image-20250115165406650

  • 回到算法分析的路程,去看看函数里什么样;

image-20250115170856769

  • 大概看一眼,三个函数比较重要,优先去看最后一个,一是良好的习惯,如果hook最后一个函数发现参数就是明文,那就说明其他的位置根本不关键,这样能节省大量的时间,再一个就是传进来的参数感觉都聚焦在这个方法,进去看看;
  • 事实上,逆向这门学问猜是占有很大的比重的,大胆猜测小心求证即可,有时候会帮助我们节省非常多的时间的;

image-20250115171757122

  • 代码比较多,放一些关键的,符号还在,所以难度其实非常低了,前面有一个RSA、后面是Base64和AES,往下看看会看到比较关键的东西;

image-20250115172016791

  • 这里能看出我们的结果中的一部分来自于这里,分别是k、v、u;但对比发现结果中还有r、g、p三个结果,这里是没看见拼接的,继续往下看;

image-20250115172300671

  • 下面有非常多代码在做判断,这些也看不懂啊,按下r将它转为char,看看怎么回事;

image-20250115172428906

  • 缺失的数据在这里做的拼接,那么 逐步来看吧;

3.2 算法分析

  • 首先看看EncryptHttpRequest3这个函数的入参,这里承接上面我的逻辑,看看参数,万一就是我们传的呢;
  • ida反汇编出来是9个参数,根据ATPCS调用约定,后五个参数在栈中,我们该怎么在Console Debugger中看后面五个值是啥呢?先下个断点,这里的地址是0xB300;
参数1~参数4 分别保存到 R0~R3 寄存器中 ,剩下的参数从右往左依次入栈,被调用者实现栈平衡,返回值存放在 R0 中;
public void consoledebugger_hook(){
    // emulator.attach().addBreakPoint(module.base+0x9d24);
    // emulator.attach().addBreakPoint(module.base+0x9d28);
    emulator.attach().addBreakPoint(module.base + 0xB300);
}
  • 断下来了,这里我们的目的就是看看参数;

image-20250115173752638

  • 看看关键的参数;

image-20250115173503553

image-20250115173525951

  • r2是参数3,r3是0x40,这应该就是它的长度,这里可以修改参数3验证,那剩下的怎么看?上面提到了,后五个参数在栈中,那就先看看栈;查看堆栈SP寄存器的内存,msp;

image-20250115174410798

  • 每四个字节就代表栈中的一个值,且为小端序,那么解析一下:

    • 参数5:00 80 22 40 --->>> 0x40228000
    • 参数6:00 30 20 40 --->>> 0x40203000
    • 参数7:40 80 21 40 --->>> 0x40218040
    • 参数8:00 00 00 00 --->>> 0x00
    • 参数940 00 00 00 --->>> 0x40
  • 这里的参数也就全部找到了,去看看这几个像指针的数据,0x00很明显就不是指针;
  • m0x40228000,看着像是buffer?

image-20250115174823972

  • m0x40203000,看不出来是什么,但我们在前面也看到过这个位置,或许是初始化的东西也或许是别的原因;

image-20250115175438861

  • 其他的也不看了,都看不出啥东西,但是参数我们已经是知道了,这里主要是学习多余的参数应该怎么查看;
  • 再次回到刚开始提出的观点,我们这里的入参和明文一致,那他前面的方法还有必要去看吗?
  • 那这里需要看的大概就是rsa、base64、aes这三个方法,先看看rsa;

image-20250115175808283

  • 这里熟悉js逆向的可能会见的多一点,10001和AQAB都是rsa的显著标识,那么此处另外一个大数应该就是模数了,有了模数和指数,即得到了公钥,然后用公钥完成加密,这里就不多说了;
  • 接着看看base64,稍微跟一下能看到它的码表;

image-20250115180320367

  • 码表一致不代表它就是标准的,还是去hook一下比较合适,去看看在哪里hook比较好;

image-20250116104227753

  • 就hook这里吧,前面我们就提到过inline hook的事情,这里很显然hook这里的指令是最合适的,下两个断点;
emulator.attach().addBreakPoint(module.base + 0xB372);
emulator.attach().addBreakPoint(module.base + 0xB376);
  • 首先看看参数;

image-20250116105011573

  • 然后按下c运行,看看r0里面存储了什么;

image-20250116105036465

  • 使用cyberchef验证一下;

image-20250116105201314

  • 这里长度没完全打印出来,但是无伤大雅,可以看出是一个标准的base64编码;
ⅠAES
  • 接下来就是分析这个aes,那么一个aes我们需要知道的东西无非就是模式、填充、key、iv;一个一个分析就行;
  • 先进去看看;

image-20250116110648827

  • 先找一下它是什么模式的,从EncSym开始跟,可以跟到如下位置;

image-20250116110729166

  • 符号看出来是cbc,这只是猜测,在这里是很好使,符号抹去了就傻了,所以需要保持怀疑态度,暂且认为它是cbc;那么我们知道aes是有区分的,分为128、192、256三个,那这里应该是哪一个呢?
  • 那从SetEncKeySym进去看看,这里看着像密钥编排;为何要从这里进去看?前面加密的位置为什么就会突然去看密钥编排呢,这里需要熟悉aes的细节,128或者256他们的差距具体是体现在密钥长度上和加密轮数的区别,这里自然是要去看密钥编排的位置了,而且它的结果或者某个参数就应该是key才对;

image-20250116111135510

  • 符号都没去,再进去看看;

image-20250116111818010

  • 这里有些关键信息,那我们去hook一下看看究竟是哪一种;

image-20250116112328097

emulator.attach().addBreakPoint(module.base + 0x114B2);
emulator.attach().addBreakPoint(module.base + 0x114B6);
  • 看看参数情况;

image-20250116112155819

image-20250116112545205

image-20250116112600359

  • r2就不是地址了,0x100不就是256嘛,那它应该是一个aes-256,那他的密钥长度是32字节的,那r1里的值应该就是密钥了,它的前32字节就是初始密钥;
  • 接下来再找填充,还是去看关键函数;

image-20250116142353131

  • 这应该是关键的填充函数了,不进去看了,直接hook;

image-20250116142446151

emulator.attach().addBreakPoint(module.base + 0x114D6);
emulator.attach().addBreakPoint(module.base + 0x114DA);
  • 看看参数情况;

image-20250116142509482

  • r0就应该是返回的结果存储的位置,看看r1;

image-20250116142554540

  • 看不明白,再看看r2;

image-20250116142614099

  • 根据伪代码,这个也许就是加密的明文,只是这里我们不认识,后面全是0,即使这里判断不出也没关系,还记得r0存储着返回值,那就执行到下一条指令看看结果,这里r0的地址是:0x4021e090;

image-20250116142754617

  • 可以看到,刚刚的分析是没错的,后面填充的是08,那这个是什么填充方式?

    • 这里的块大小以及填充后的数据均满足16字节倍数,所以这里是pkcs7填充;
  • 到这里来分析一下我们还有什么是未知的;

    • 已知:模式、填充、密钥;
    • 未知:iv;
  • 如何分析iv呢?自然是要去看关键的加密函数了;
  • 从EncSym往里面跟;

image-20250116151216269

  • 在这里hook一下这个函数,就不在外面hook了;

image-20250116151255194

emulator.attach().addBreakPoint(module.base + 0x12970); // 0x4021e0c0

// >>> r0=0x4021e090 r1=0x4021e0c0 r2=0x30 r3=0xbffff4ec r4=0xbffff4ec r5=0xbffff4c0 r6=0x4021e090 r7=0xbffff4c8 r8=0x0 sb=0x0 sl=0x0 fp=0x0 ip=0x40039c58
// >>> SP=0xbffff4b8 LR=RX@0x40010f09[libSCoreTools.so]0x10f09 PC=RX@0x40012970[libSCoreTools.so]0x12970 cpsr: N=1, Z=0, C=1, V=0, T=1, mode=0b10000
  • 同样是看参数,第一个参数是加密的数据,也是上面填充出现过的;

image-20250116151342076

  • 第二个参数是buffer,用来存放加密结果的,那么我们最后等这个函数运行结束后要看的地址就是它了,把它记住;

image-20250116151635851

  • 参数3是0x30,应该不是地址,而且应该是明文的长度,也就是48嘛,参数4大概看不懂,猜的话应该是密钥,也就是编排后的密钥;

image-20250116151727969

  • 根据堆栈来看后面两个参数的值;

image-20250116151852446

  • 参数5地址是0xbffff5e0,参数6是数字1,大概是一种代表性的值,加密或者解密之类的;

image-20250116151955598

  • 参数五暂时不知道是什么,那我们分析一下,明文、key等等都有了,那它可能是iv吗?去看看伪代码结合分析,我们都知道,有无iv的区别或者iv在加密的参与位置是在哪里;

image-20250116152216491

  • 那我们去看看有没有什么能佐证这个观点;

image-20250116152408936

  • 这里也是比较明显的,所以iv应该就是参数5,这里注意,iv是只有16字节的;
  • 分析到这里我们汇总一下已知的信息;

    • 明文:cb282929b0d2d74f2c28d02b4fcdacc8ccd32bce4fcf2fd54bcecf050a66ea17a72616256700a501;
    • 模式:cbc;
    • 填充:pkcs7;
    • key:9fce87072d0dc6789817468974b2ea51ee3944b8d7e0a88e4f16ebb80f03bd84;
    • iv:5231a01e6146841cbaef0134dcd9300d;
  • 该有的都有了,去cyberchef加密看看,再测试之前我们需要知道加密的结果,这里应该怎么看?

    • 我们知道,0x4021e0c0,这个地址在函数运行结束后会存着函数的返回值,问题是这里怎么看,肯定是要等函数运行结束,这里最好的办法就是使用持久化的方式hook,很容易就能获取到函数执行成功之后的位置,那我们在这里可以输入blr,在ARM编程中,LR寄存器存放了程序的返回地址,当函数跑到LR所指向的地址时,意味着函数结束跳转了出来;
    • 又因为断点是在目标地址执行前触发,所以在LR处的断点断下时,目标函数执行完且刚执行完,这就是Frida OnLeave 时机点的原理;在Console Debugger交互调试中,使用 blr 命令可以在 lr 处下一个临时断点,它只会触发一次;
    • 然后按下c继续执行,他会断住,此时再看地址;

image-20250116153514909

  • 这就是它的返回值,如下:
45e9ca6dd4e0c6dbf7a54d39a348e5f974948e3b2d8b7d56fd511bcec005d319c7322ec5b68dad81b772a9d212c707ef
  • 接下来就可以去测试了;

image-20250116153613570

  • 结果如下:
// cyberchef结果:
    45e9ca6dd4e0c6dbf7a54d39a348e5f974948e3b2d8b7d56fd511bcec005d319c7322ec5b68dad81b772a9d212c707ef
// 原结果:
    45e9ca6dd4e0c6dbf7a54d39a348e5f974948e3b2d8b7d56fd511bcec005d319c7322ec5b68dad81b772a9d212c707ef
  • 对上了,那就证明我们的分析是没有问题的,aes就分析完毕了;

4. 总结

  • 此样本符号都在,主要是锻炼unidbg 辅助时hook的能力,另外aes的明文其实是经过了处理,这里我也就懒得花时间去看了;
1,033

评论 (0)

取消