首页
关于
案例站
tools
记录
推荐
我的B站
Search
1
安卓逆向-unidbg辅助算法还原
34 阅读
2
安卓逆向-Bilibili播放量接口
28 阅读
3
JS逆向-知网滑块
21 阅读
4
安卓逆向-五菱白盒AES
16 阅读
5
安卓逆向-瑞幸咖啡白盒AES
13 阅读
JS逆向
安卓逆向
小程序逆向
登录
Search
标签搜索
安卓逆向
JS逆向
密码学
unidbg
sana
累计撰写
12
篇文章
累计收到
6
条评论
首页
栏目
JS逆向
安卓逆向
小程序逆向
页面
关于
案例站
tools
记录
推荐
我的B站
搜索到
1
篇与
的结果
2025-01-17
安卓逆向-unidbg辅助算法还原
一、搜狗unidbg1. 前置方法定位为如下位置:并不以整个案例为目的,只是执行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); }直接运行发现并没有得到我们想要的输出;这是为什么呢,它也没有报错,我们去ida看看;这里可以看到是有一个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()); };直接执行发现出结果了,连环境都不用补;接下来重点就是算法还原;3. 算法还原3.1 定位&Hook进入函数,将基本的参数修改一下就是如下的效果:这里的关键函数肯定就是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;可以看到位置是没有问题的,参数和返回值都在这里,那么这个函数就是加密函数;那这里想要实现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结果依旧是正确的;对比上面的三份hook代码,两个是第三方的,一个是原生的,代码量都不算特别小,那么为何辅助算法还原还是要使用unidbg呢,那就要请出唯一真神——Console Debugger;下两个断点非常的容易,只需要一句代码;public void consoledebugger_hook(){ emulator.attach().addBreakPoint(module.base+0x9d24); emulator.attach().addBreakPoint(module.base+0x9d28); }断在对应的位置了,这时候可以去查看寄存器的值,mr0 mr1 mr2分别是我们的参数;后续的图不放了,那返回值呢?mr3吗?这里存的并不是我们的结果,但是在上面分析明明最后一个参数是返回值这里怎么又不是了?这是因为此时函数并未执行,断在了此处,记住r3的地址,在函数结束的时候打印一下;按下c让程序继续走,这里会在第二个断点停下来,此时再看看刚刚的地址,0x40203000可以查看地址,后面可以跟长度;回到算法分析的路程,去看看函数里什么样;大概看一眼,三个函数比较重要,优先去看最后一个,一是良好的习惯,如果hook最后一个函数发现参数就是明文,那就说明其他的位置根本不关键,这样能节省大量的时间,再一个就是传进来的参数感觉都聚焦在这个方法,进去看看;事实上,逆向这门学问猜是占有很大的比重的,大胆猜测小心求证即可,有时候会帮助我们节省非常多的时间的;代码比较多,放一些关键的,符号还在,所以难度其实非常低了,前面有一个RSA、后面是Base64和AES,往下看看会看到比较关键的东西;这里能看出我们的结果中的一部分来自于这里,分别是k、v、u;但对比发现结果中还有r、g、p三个结果,这里是没看见拼接的,继续往下看;下面有非常多代码在做判断,这些也看不懂啊,按下r将它转为char,看看怎么回事;缺失的数据在这里做的拼接,那么 逐步来看吧;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); }断下来了,这里我们的目的就是看看参数;看看关键的参数;r2是参数3,r3是0x40,这应该就是它的长度,这里可以修改参数3验证,那剩下的怎么看?上面提到了,后五个参数在栈中,那就先看看栈;查看堆栈SP寄存器的内存,msp;每四个字节就代表栈中的一个值,且为小端序,那么解析一下:参数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?m0x40203000,看不出来是什么,但我们在前面也看到过这个位置,或许是初始化的东西也或许是别的原因;其他的也不看了,都看不出啥东西,但是参数我们已经是知道了,这里主要是学习多余的参数应该怎么查看;再次回到刚开始提出的观点,我们这里的入参和明文一致,那他前面的方法还有必要去看吗?那这里需要看的大概就是rsa、base64、aes这三个方法,先看看rsa;这里熟悉js逆向的可能会见的多一点,10001和AQAB都是rsa的显著标识,那么此处另外一个大数应该就是模数了,有了模数和指数,即得到了公钥,然后用公钥完成加密,这里就不多说了;接着看看base64,稍微跟一下能看到它的码表;码表一致不代表它就是标准的,还是去hook一下比较合适,去看看在哪里hook比较好;就hook这里吧,前面我们就提到过inline hook的事情,这里很显然hook这里的指令是最合适的,下两个断点;emulator.attach().addBreakPoint(module.base + 0xB372); emulator.attach().addBreakPoint(module.base + 0xB376);首先看看参数;然后按下c运行,看看r0里面存储了什么;使用cyberchef验证一下;这里长度没完全打印出来,但是无伤大雅,可以看出是一个标准的base64编码;ⅠAES接下来就是分析这个aes,那么一个aes我们需要知道的东西无非就是模式、填充、key、iv;一个一个分析就行;先进去看看;先找一下它是什么模式的,从EncSym开始跟,可以跟到如下位置;符号看出来是cbc,这只是猜测,在这里是很好使,符号抹去了就傻了,所以需要保持怀疑态度,暂且认为它是cbc;那么我们知道aes是有区分的,分为128、192、256三个,那这里应该是哪一个呢?那从SetEncKeySym进去看看,这里看着像密钥编排;为何要从这里进去看?前面加密的位置为什么就会突然去看密钥编排呢,这里需要熟悉aes的细节,128或者256他们的差距具体是体现在密钥长度上和加密轮数的区别,这里自然是要去看密钥编排的位置了,而且它的结果或者某个参数就应该是key才对;符号都没去,再进去看看;这里有些关键信息,那我们去hook一下看看究竟是哪一种;emulator.attach().addBreakPoint(module.base + 0x114B2); emulator.attach().addBreakPoint(module.base + 0x114B6);看看参数情况;r2就不是地址了,0x100不就是256嘛,那它应该是一个aes-256,那他的密钥长度是32字节的,那r1里的值应该就是密钥了,它的前32字节就是初始密钥;接下来再找填充,还是去看关键函数;这应该是关键的填充函数了,不进去看了,直接hook;emulator.attach().addBreakPoint(module.base + 0x114D6); emulator.attach().addBreakPoint(module.base + 0x114DA);看看参数情况;r0就应该是返回的结果存储的位置,看看r1;看不明白,再看看r2;根据伪代码,这个也许就是加密的明文,只是这里我们不认识,后面全是0,即使这里判断不出也没关系,还记得r0存储着返回值,那就执行到下一条指令看看结果,这里r0的地址是:0x4021e090;可以看到,刚刚的分析是没错的,后面填充的是08,那这个是什么填充方式?这里的块大小以及填充后的数据均满足16字节倍数,所以这里是pkcs7填充;到这里来分析一下我们还有什么是未知的;已知:模式、填充、密钥;未知:iv;如何分析iv呢?自然是要去看关键的加密函数了;从EncSym往里面跟;在这里hook一下这个函数,就不在外面hook了;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同样是看参数,第一个参数是加密的数据,也是上面填充出现过的;第二个参数是buffer,用来存放加密结果的,那么我们最后等这个函数运行结束后要看的地址就是它了,把它记住;参数3是0x30,应该不是地址,而且应该是明文的长度,也就是48嘛,参数4大概看不懂,猜的话应该是密钥,也就是编排后的密钥;根据堆栈来看后面两个参数的值;参数5地址是0xbffff5e0,参数6是数字1,大概是一种代表性的值,加密或者解密之类的;参数五暂时不知道是什么,那我们分析一下,明文、key等等都有了,那它可能是iv吗?去看看伪代码结合分析,我们都知道,有无iv的区别或者iv在加密的参与位置是在哪里;那我们去看看有没有什么能佐证这个观点;这里也是比较明显的,所以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继续执行,他会断住,此时再看地址;这就是它的返回值,如下:45e9ca6dd4e0c6dbf7a54d39a348e5f974948e3b2d8b7d56fd511bcec005d319c7322ec5b68dad81b772a9d212c707ef接下来就可以去测试了;结果如下:// cyberchef结果: 45e9ca6dd4e0c6dbf7a54d39a348e5f974948e3b2d8b7d56fd511bcec005d319c7322ec5b68dad81b772a9d212c707ef // 原结果: 45e9ca6dd4e0c6dbf7a54d39a348e5f974948e3b2d8b7d56fd511bcec005d319c7322ec5b68dad81b772a9d212c707ef对上了,那就证明我们的分析是没有问题的,aes就分析完毕了;4. 总结此样本符号都在,主要是锻炼unidbg 辅助时hook的能力,另外aes的明文其实是经过了处理,这里我也就懒得花时间去看了;
2025年01月17日
34 阅读
0 评论
1,033 点赞