首页
关于
案例站
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站
搜索到
9
篇与
的结果
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 点赞
2025-01-07
安卓逆向-五菱白盒AES
一、五菱白盒AES1. 前置[!sana] 版本:V8.0.14包名:com.cloudy.linglingbang加固:梆梆加固so文件:libencrypt.so涉及到的知识点有白盒AES、DFA、unidbg补环境以及辅助算法还原、unidbg的hook与patch等;可供参考资料:https://bbs.kanxue.com/thread-280335.htmhttps://bbs.kanxue.com/thread-284073.htm文件相关链接:链接: https://pan.baidu.com/s/1xwBKsBhdgXGeAjEYcfWLSA?pwd=sana提取码: sana整体思路参考了杨如画大佬的思路,在此先表示感谢;2. 抓包分析直接点击app会有更新提示与root提示,不更新就会闪退,可以去hook一下dialog或者吐司,我这里图方便就使用算法助手了,同时抓包也有检测,也一同勾上即可抓包,也是可以进入app的;这里的屏蔽关键字弹窗写升级或者更新都行,总之是进入了第一步,可以开始抓包了;抓包结果如下:这里请求体和返回都是密文的,都需要去解决,请求头也有一个签名,后续如果有时间也会说,首先看请求体这两个密文;3. 定位分析首先这个apk是加壳的,所以直接反编译是毛都不会有的,找个大佬帮忙脱一下壳,我会把脱下来的东西放在附件里;直接搜索大法看看有没有效果;这里是一个变量,比较可疑,我们进去查看谁引用了它,按下X或右键查看交叉引用;这里随意选一个,我看最后一个比较炸眼,我选择进去看看;这里就可以看到一些关键词,可以hook看看是不是正确的位置;最开始我是选择直接attach附加的,发现hook不上,换成重启的方式也报错了,这里做了frida的检测,但是检测的不严格,换成魔改版本的就行,这里是启动的自己编译的魔改版frida,使用hluda好像也是能过去的;这里选择hook encrypt函数,看看是不是正确位置,hook代码如下:function hook_enc(){ Java.perform(function () { let CheckCodeUtils = Java.use("com.cloudy.linglingbang.model.request.retrofit2.CheckCodeUtils"); CheckCodeUtils["encrypt"].implementation = function (str,i) { console.log(`CheckCodeUtils.encrypt is called: str=${str},i=${i}`); let result = this["encrypt"](str); console.log(`CheckCodeUtils.encrypt result=${result}`); return result; }; }) }这里最好到要点登录的时候再调用这个函数,可能会多一些输出,不便于我们分析;[Pixel 4::com.cloudy.linglingbang ]-> CheckCodeUtils.encrypt is called: str=mobile=15211112222&password=111111&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=v8kTgUYjT8&response_type=token, i=2 CheckCodeUtils.encrypt result=MVNWGVFEpD18zqVt4x39yEZIE/y7BcfFhTbn9JUQ7bSrQ1EATn/MHXNQMOmE0Dlx0LYNvoPcIOPQlyRORq87sSsA5oUrlywzc6vjKJMlNb4gAL87KpYFQC9jznMlQVwFM2+fs59XMS6XehbNQ/yQHi9qidfcNyW/y379VFWCrN3Gy8T94fuqfA5L8Eb2Q3+m/5odBEjEYGCDAZgIxwz5K/JyV1rz/PctqINCcdSKozundoAkLwX7YUr+sPSlTcHa9dB4+yG/2jQQ6nDo0ckroqFRurapCUXP4MKTtMH05UQ5PdnXh6D6FyQ+LiUovi0P5iBBUwntGVn/n19e2xIG/KsiE0PEUL6uMdkXWUdoj2zftSXqHY38HaK+WCsy7gQGni5kIVzXNGRBgaQAc6HkGMg== 触发了,看看与抓包结果是否一致,对比发现大致是一样的,但是将+给换成了空格,这个记住就好;那么位置没错就继续深入看看;定位到具体位置了,很显然是native函数,checkcode是加密,那decheckcode就是解密了,一个个分析,先使用unidbg把算法跑出来再说;4. unidbg复现4.1 unidbg基本框架把unidbg跑起来之后对算法还原是非常有帮助的,并且本文需要dfa,我是更喜欢用unidbg来攻击的,比较省心,先搭一个架子;public class WuLing extends AbstractJni { public static AndroidEmulator emulator; public static Memory memory; public static VM vm; public static Module module; public WuLing() { emulator = AndroidEmulatorBuilder .for32Bit() .addBackendFactory(new Unicorn2Factory(true)) .build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("apks/whitebox/wuling/wuling.apk")); vm.setJni(this); vm.setVerbose(true); DalvikModule dm = vm.loadLibrary("encrypt", true); module = dm.getModule(); dm.callJNI_OnLoad(emulator); } public static void main(String[] args) { WuLing demo = new WuLing(); } }4.2 补环境基本框架是大致是通用的,改apk之类的就可以了,直接运行看看会不会成功;java.lang.UnsupportedOperationException: android/app/ActivityThread->currentActivityThread()Landroid/app/ActivityThread; at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:432) at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:421)一般来说没有进行主动调用的话问题不大,这里是报错了,那就直接进入补环境的主题;ActivityThread在Android框架中负责管理应用程序的生命周期和组件的创建与销毁等操作,这里直接返回一个空对象占位即可;@Override public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) { switch (signature){ case "android/app/ActivityThread->currentActivityThread()Landroid/app/ActivityThread;":{ return vm.resolveClass("android/app/ActivityThread").newObject(null); } } return super.callStaticObjectMethod(vm, dvmClass, signature, varArg); }接着往下运行;java.lang.UnsupportedOperationException: android/os/SystemProperties->get(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:432) at com.xyt.sana.WuLing.callStaticObjectMethod(WuLing.java:80)遇到这种有字符串参数的首先就应该去看看参数值,并且这里的方法调用的是get,估计是在获取什么东西;System.out.println(varArg.getObjectArg(0).getValue()); System.out.println(varArg.getObjectArg(1).getValue());看看会输出什么;ro.serialno unknown在Android系统中,ro.serialno是一个系统属性,用于存储设备的序列号;这里应该怎么模拟?通过adb命令获取即可,遇到这种拿不准的东西最好的方式就是google或者求助ai;adb shell getprop ro.serialno以自己的结果为准即可,并且最好是与真机保持一致;case "android/os/SystemProperties->get(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;": { System.out.println(varArg.getObjectArg(0).getValue()); return new StringObject(vm, ""); }继续走;java.lang.UnsupportedOperationException: android/app/ActivityThread->getSystemContext()Landroid/app/ContextImpl; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:932) at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:866)这里获取上下文,继续占位补;@Override public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) { switch (signature){ case "android/app/ActivityThread->getSystemContext()Landroid/app/ContextImpl;":{ return vm.resolveClass("android/app/ContextImpl").newObject(null); } } return super.callObjectMethod(vm, dvmObject, signature, varArg); }再次运行;java.lang.UnsupportedOperationException: android/app/ContextImpl->getPackageManager()Landroid/content/pm/PackageManager; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:932) at com.xyt.sana.WuLing.callObjectMethod(WuLing.java:94)这个也是相关的东西,感觉最后会是签名校验?同样处理;case "android/app/ContextImpl->getPackageManager()Landroid/content/pm/PackageManager;":{ return vm.resolveClass("android/content/pm/PackageManager").newObject(null); }看看下面的信息;java.lang.UnsupportedOperationException: android/app/ContextImpl->getSystemService(Ljava/lang/String;)Ljava/lang/Object; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:932) at com.xyt.sana.WuLing.callObjectMethod(WuLing.java:97)输出一下参数看看;sana getSystemService--->>>wifi那补一个wifi对象即可,它要获取wifi相关的信息吧;case "android/app/ContextImpl->getSystemService(Ljava/lang/String;)Ljava/lang/Object;":{ System.out.println("sana getSystemService--->>>" + varArg.getObjectArg(0).getValue()); String serviceName = (String) varArg.getObjectArg(0).getValue(); if ("wifi".equals(serviceName)) { // 创建模拟的WifiManager对象 DvmClass wifiManagerClass = vm.resolveClass("android/net/wifi/WifiManager"); DvmObject<?> wifiManager = wifiManagerClass.newObject(signature); return wifiManager; } }接着看报错;java.lang.UnsupportedOperationException: android/net/wifi/WifiManager->getConnectionInfo()Landroid/net/wifi/WifiInfo; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:932) at com.xyt.sana.WuLing.callObjectMethod(WuLing.java:108)这个也是补位即可;case "android/net/wifi/WifiManager->getConnectionInfo()Landroid/net/wifi/WifiInfo;":{ return vm.resolveClass("android/net/wifi/WifiInfo").newObject(signature); }继续看;java.lang.UnsupportedOperationException: android/net/wifi/WifiInfo->getMacAddress()Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:932) at com.xyt.sana.WuLing.callObjectMethod(WuLing.java:111)这就比较显然了,获取mac地址;case "android/net/wifi/WifiInfo->getMacAddress()Ljava/lang/String;": { return new StringObject(vm, "c6:2c:b7:2c:a4:47"); }可以使用adb获取;adb shell ip link show wlan0 或者 adb shell ip addr show运行到这里没有报错了,那我们开始call方法,先去拿一个参数和返回值,用于进行对比,这里hook三个参数的checkcode方法;function hook_enc() { Java.perform(function () { let CheckCodeUtil = Java.use("com.bangcle.comapiprotect.CheckCodeUtil"); CheckCodeUtil["checkcode"].overload('java.lang.String', 'int', 'java.lang.String').implementation = function (str, i, str2) { console.log(`CheckCodeUtil.checkcode is called: str=${str}, i=${i}, str2=${str2}`); let result = this["checkcode"](str, i, str2); console.log(`CheckCodeUtil.checkcode result=${result}`); return result; }; }) }拿到一份参数;[Pixel 4::com.cloudy.linglingbang ]-> hook_enc() [Pixel 4::com.cloudy.linglingbang ]-> CheckCodeUtil.checkcode is called: str=mobile=15211112222&password=111111&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=fhtTZiDQrY&response_type=token, i=2, str2=1736087879878 CheckCodeUtil.checkcode result= MVNWGVFEpD18zqVt4x39yEZIE/y7BcfFhTbn9JUQ7bSrQ1EATn/MHXNQMOmE0Dlx0LYNvoPcIOPQlyRORq87sSsA5oUrlywzc6vjKJMlNb4gAL87KpYFQC9jznMlQVwFM2+fs59XMS6XehbNQ/yQHi1NSGSeFGA9roRTajzYyacLDlMR9WZJqZ+mTUwylU3lZDcCYkian0uE+3Q5NAdMZ7fWFORCmhvWU5rBGBHrR17lccfF4Yrb6o4x+j3f3Unnfri1aCh9bnelkM2tvMrsd9xAmoy7wcwFRknsymvQzb8eHjkAFwtKQnBuMKARF3jQb5sp+85vd86jBnPUBQ3WI8xs1yDX97YqLWYq2WvDSisAzNO6P5RlvazEIoZHETjTfvqVhXubTSGx2Pdy7wTXD5Q==接下来直接使用地址去调用,我个人比较喜欢地址的方式,比较自由,给个偏移就行;首先去ida看看偏移信息,unidbg没有输出相关的信息,那可能是静态注册;它的偏移是13A18,这里是32位,所以地址要 + 1;public String call_checkcode() { List<Object> list = new ArrayList<>(5); list.add(vm.getJNIEnv()); list.add(0); // 三个参数 list.add(vm.addLocalObject(new StringObject(vm, "mobile=15211112222&password=111111&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=fhtTZiDQrY&response_type=token"))); list.add(2); list.add(vm.addLocalObject(new StringObject(vm, "1736087879878"))); Number number = module.callFunction(emulator, 0x13A18 + 1, list.toArray()); String result = vm.getObject(number.intValue()).getValue().toString(); System.out.println("sana res --->>>" + result); return result; }继续运行又开始补环境的路程;java.lang.UnsupportedOperationException: android/os/Build->MODEL:Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:103) at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:53)这是获取设备的型号名称,本地可以这样获取;adb shell getprop ro.product.model返回字符串即可;@Override public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) { switch (signature){ case "android/os/Build->MODEL:Ljava/lang/String;":{ return new StringObject(vm,"Pixel 4"); } } return super.getStaticObjectField(vm, dvmClass, signature); }接着补;java.lang.UnsupportedOperationException: android/os/Build->MANUFACTURER:Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:103) at com.xyt.sana.WuLing.getStaticObjectField(WuLing.java:139)这个是获取设备制造商的名称,这里也是直接获取;adb shell getprop ro.product.manufacturer返回即可;case "android/os/Build->MANUFACTURER:Ljava/lang/String;":{ return new StringObject(vm,"Google"); }接着看;java.lang.UnsupportedOperationException: android/os/Build$VERSION->SDK:Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:103) at com.xyt.sana.WuLing.getStaticObjectField(WuLing.java:142)这个也很明显获取sdk的版本,我是安卓10,也就是29;case "android/os/Build$VERSION->SDK:Ljava/lang/String;":{ return new StringObject(vm, "29"); }再次运行就跑出了结果;但是相同的参数结果却对不上,可能是有什么东西在随机,那我这里想验证一下这个结果的正确性应该如何验证?4.3 解密测试这里我们去主动call解密方法,看看我们补出来的结果正确与否;代码如下,地址在上面已经提到过了;public void call_decheckcode(String str){ List<Object> list = new ArrayList<>(5); list.add(vm.getJNIEnv()); list.add(0); list.add(vm.addLocalObject(new StringObject(vm, str))); Number number = module.callFunction(emulator, 0x165E0 + 1, list.toArray()); String result = vm.getObject(number.intValue()).getValue().toString(); System.out.println("sana 解密结果--->>>" + result); }直接将得到的结果传进去看看返回值如何,这里可以暂时把jni的打印去掉;这个结果很明显是不对的,可能是走了错误的逻辑,这里也有说在读文件,启动ida去看看情况,先静态分析一波再说;它是静态注册的,直接点进去看;jstring __fastcall Java_com_bangcle_comapiprotect_CheckCodeUtil_decheckcode(JNIEnv *env, int a2, void *input) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] if ( !input ) return 0; v5 = malloc(0x21u); v6 = sub_138AC(env, v5); v7 = *env; if ( !v6 ) return v7->NewStringUTF(env, v5); v9 = v7->GetStringUTFChars(env, input, 0); v10 = *v9; v11 = v9; if ( v10 == 'M' ) { v12 = &aes_decrypt1_ptr; } else { if ( v10 != 'N' ) { v13 = v9; aes_decrypt = aes_decrypt1; goto LABEL_10; } v12 = &aes_decrypt2_ptr; } v13 = v9 + 1; aes_decrypt = *v12; LABEL_10: v14 = v13; do { v15 = v14++; v16 = *v15; } while ( *v15 ); v20 = (v15 - v13); v17 = malloc(v15 - v13); v18 = &v20[v17]; for ( i = v17; i < v18; i += 4 ) { *i = v16; if ( v18 <= (i + 1) ) break; i[1] = v16; if ( v18 <= (i + 2) ) break; i[2] = v16; if ( v18 <= (i + 3) ) break; i[3] = v16; } v21 = (base64_decode)(v13, v17, v16); (aes_decrypt)(v17, &v21); v17[v21] = 0; free(v5); (*env)->ReleaseStringUTFChars(env, input, v11); return (*env)->NewStringUTF(env, v17); }最下面这个返回应该是正确的时候,我们用unidbg跑的时候结果是不正确的,那就找找其他的返回位置;感觉这里会比较像,去看看v5的生成;返回的位置大概会是v11,这里在做比较,那就去看看v5的位置;去看看ED04这个函数;非常长的一个函数,感觉是做了某种环境检测,这里也是有md5的特征,返回值也是32位的,感觉就有可能是这里了,那我们应该怎么做呢?比较合适的方式就是在判断的位置打patch,不让它走错误逻辑,也就是v6的位置;我们对它进行取反不就可以了吗,去看看这里的汇编代码;CBNZ 是一个条件分支指令,用于检查寄存器的值是否不为零,如果不为零则跳转到指定的标签位置;这条指令的作用是检查 R0 寄存器中的值是否为零,如果 R0 的值不为零,则程序将跳转到地址 0x00016610 处继续执行,如果 R0 的值为零,则程序将按照正常的顺序继续执行下一条指令;这里直接修改指令即可,要对 CBNZ 指令进行取反,即当寄存器的值为零时跳转,可以使用 CBZ 指令。CBZ 是“如果为零则分支”的指令,与 CBNZ 相反;这里可以用ida、010来修改,但既然用到了unidbg那就用它来patch,我觉得也是更省心的,方式比较多,先看看20 B9的指令是什么;那我们需要的指令就是:cbz r0, #0xc ,去掉n嘛,unidbg既可以传机器码,也可以传汇编指令,我们先看看传汇编指令;Ⅰpatch方式1public void patch() { try (Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb)) { KeystoneEncoded encoded = keystone.assemble("cbz r0, #0xc"); byte[] patchCode = encoded.getMachineCode(); emulator.getMemory().pointer(module.base + 0x16604).write(0, patchCode, 0, patchCode.length); } }Ⅱ patch方式2UnidbgPointer pointer = UnidbgPointer.pointer(emulator, module.base + 0x16604); byte[] code = new byte[]{(byte) 0x20, (byte) 0xB1}; pointer.write(code);两种都可以,区别也不大,这样就可以跑出正常的结果了;sana res --->>>MVNWGVFEpD18zqVt4x39yEZIE/y7BcfFhTbn9JUQ7bSrQ1EATn/MHXNQMOmE0Dlx0LYNvoPcIOPQlyRORq87sSsA5oUrlywzc6vjKJMlNb4gAL87KpYFQC9jznMlQVwFM2+fs59XMS6XehbNQ/yQHi1NSGSeFGA9roRTajzYyacLDlMR9WZJqZ+mTUwylU3lZDcCYkian0uE+3Q5NAdMZ7YqBhlEkjBKRN21UBsqHy8oLN/TIoTBXpy/TnI8DXxDdqb2Tv2SkR/YrDMO0Jnn4adjdrJvTEo0tEvXaI6Q1bRLspCZiSSVhsrQLs/HvRKTxMb2HV/xBafnJY7NxkXgoBTF7fe2I8m2QGxui8Q0aTv98KVj7p70Gcg2DStAkIV743nKmLp+XMMU8GhLDl4HI2g== sana 解密结果--->>>mobile=15211112222&password=111111&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=fhtTZiDQrY&response_type=token&ostype=ios&imei=99241FFAZ007VF&mac=c6:2c:b7:2c:a4:47&model=Pixel 4&sdk=29&serviceTime=1736087879878&mod=Google&checkcode=64e0d17b019b98a23f886f26a4b6f600明文看起来东西不多,正是我们输入的东西拼接了一些环境相关的内容,最后有一个checkcode是一直在变化的,多跑几次就可以发现,也就是为什么上面我们固定入参却依然有变化的原因;unidbg跑通之后就可以进行算法还原了,最后的checkcode后续再分析;5. 算法还原在最开始进来的地方能看到很多符号信息,告诉我们aes,那我们就朝着aes去看;这里具体去看哪一个呢?它这里做了判断,根据v10来判断,v10又等于v9,v9来自于输入的参数也就是密文,77和78按下r将它转换成字符;这样就会明了很多,我们的密文多次测试都是以M开头的那么走的就是aes_encrypt1这里,点进去看看;看符号的话,有很大的信息量,wb也就是白盒,模式可能是cbc,aes128加密,当然这些都只是猜测,我们就根据这些信息来进行求证;继续向里面推进,进入WBACRAES128_EncryptCBC这个方法;再次往里面进;再进;这里好像不太看得懂了,一般来说也没见过传值this,我们往回退,看看层层调用进来的时候最开始第一个参数是什么;我们发现第一个参数是传的v7,在上面有定义,我们进去看看;再点进去看看,进去之后按tab反编译;我把整体的代码贴一下;int __fastcall CWAESCipher_Auth::WBACRAES_EncryptOneBlock( CWAESCipher_Auth *this, CSecFunctProvider *a2, unsigned __int8 *a3, int a4) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] memset(s, 0, 0x20u); v46 = CSecFunctProvider::PrepareAESMatrix(a2, &word_10, s, v5); if ( !v46 ) { for ( i = 0; ; ++i ) { if ( i >= a4 ) goto LABEL_39; // 9轮 关键信息 if ( i == 9 ) break; v7 = 0; v8 = 0; v45 = 4 * i; do { v9 = &s[2 * v8 + 32]; for ( j = 0; j != 4; ++j ) { v11 = *(this + 6); if ( v11 == 1 ) { v15 = (*(&unk_18D8E + v7) + j) & 3; // 表 v51 = roundTables_auth1[256 * (v8 + 4 * (v15 + v45)) + *(v9 + v15 - 128)]; } else { v12 = (*(&unk_18D8E + v7) + j) & 3; v13 = *(v9 + v12 - 128) + ((v8 + 4 * (v12 + v45)) << 8); if ( v11 == 2 ) v14 = &roundTables_auth2_ptr; else v14 = &roundTables_auth1_ptr; v51 = *(*v14 + 4 * v13); } s[4 * v8 + 16 + j] = v51; } ++v8; v7 += 2; } while ( v8 != 4 ); for ( k = 0; k != 4; ++k ) { v17 = &s[16] + k; for ( m = 0; m != 4; ++m ) { v19 = 0; v20 = *v17; v50[1] = v17[16]; v21 = v17[32]; v22 = v20 & 0xF; v23 = 96 * i + 24 * m; v50[0] = v20; v50[2] = v21; v24 = v20 & 0xF0; v50[3] = v17[48]; v25 = 6 * k; do { v26 = v50[++v19]; if ( v11 == 2 ) v27 = &xorTables_auth2_ptr; else v27 = &xorTables_auth1_ptr; v28 = *(*v27 + (v22 | (16 * (v26 & 0xF))) + ((v23 + v25) << 8)); v29 = v25 + 1; v44 = v26 & 0xF0 | (v24 >> 4); v22 = v28 & 0xF; if ( v11 == 2 ) v30 = &xorTables_auth2_ptr; else v30 = &xorTables_auth1_ptr; v25 += 2; v24 = (16 * *(*v30 + v44 + ((v29 + v23) << 8))); } while ( v19 != 3 ); v17 += 4; *(&s[2 * k] + m) = v24 | v22; } } } // 第10轮单独处理 if ( a4 == 10 ) { s[8] = s[0]; s[9] = s[1]; s[10] = s[2]; s[11] = s[3]; s[12] = s[4]; s[13] = s[5]; s[14] = s[6]; s[15] = s[7]; v31 = 0; v32 = *(this + 6); do { v33 = 0; v34 = 0; for ( n = 0; n != 4; ++n ) { if ( v32 == 1 ) { v38 = *(&unk_18D8E + v34); } else { if ( v32 == 2 ) { v36 = &finalRoundTable_auth2_ptr; v37 = (*(&unk_18D8E + v34) + v31) & 3; goto LABEL_37; } v38 = *(&unk_18D8E + v34); } v36 = &finalRoundTable_auth1_ptr; v37 = (v38 + v31) & 3; LABEL_37: v34 += 2; v39 = &s[2 * n + 32] + v37; v40 = n + 4 * v37; *(s + v31 + v33) = *(*v36 + *(v39 - 96) + (v40 << 8)); v33 += 8; } ++v31; } while ( v31 != 4 ); } LABEL_39: for ( ii = 0; ii != 4; ++ii ) { for ( jj = 0; jj != 4; ++jj ) a3[4 * ii + jj] = *(&s[2 * jj] + ii); } } return v46; }我在里面标注了关键的信息,那么这里就很像aes的特征了,最后一轮没有列混合(MixColumns),所以单独处理是可以理解的;而且他这个表也非常大,符合白盒aes查表的特征;那么这里可能就是加密的位置了,接下来就可以hook看看情况,这里最好修改一下输入明文,比较方便分析,得出的结果不会特别长,但在输入的时候直接给短明文后续也是会拼接环境参数的,所以在拼接完之后再进行替换;5.1 keypublic void hook_debugger() { emulator.attach().addBreakPoint(module.base + 0x5A34, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { String fakeInput = "hello"; int length = fakeInput.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); return true; } }); }这里可以返回值改成false看看有没有替换成功;可以看到明文是替换成功了,然后看看密文是多少;那密文怎么看呢?LR寄存器存放了程序的返回地址,当函数跑到LR所指向的地址时,意味着函数结束跳转了出来;又因为断点是在目标地址执行前触发,所以在LR处的断点断下时,目标函数执行完且刚执行完,这就是Frida OnLeave 时机点的原理,所以我们在4DCC的位置下断后让他执行完,拿到LR的地址,在此处断下,也就是函数刚刚执行完的时机,那么此时应该如何查看返回值?public void hook_debugger() { emulator.attach().addBreakPoint(module.base + 0x5A34, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { String fakeInput = "hello"; int length = fakeInput.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); return true; } }); // 0xbffff51c就是返回值 emulator.attach().addBreakPoint(module.base + 0x4DCC, new BreakPointCallback() { RegisterContext context = emulator.getContext(); @Override public boolean onHit(Emulator<?> emulator, long address) { // 提前保存这个地址 final UnidbgPointer arg2 = context.getPointerArg(2); emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() { //onleave @Override public boolean onHit(Emulator<?> emulator, long address) { // 结束的时候读 Inspector.inspect(arg2.getByteArray(0, 100), "参数2、返回值"); return false; } }); return true; } }); }上述的代码中,第一处断点是为了修改明文,第二个断点在上面已经说过了,拿到返回值的地址下断,此时刚刚执行完整个函数,在这里打印返回值就是密文的结果,为何0xbffff51c就是返回值呢?这里去看ida的伪代码;这里可能是在做明文state化的矩阵运算,第一个参数暂且不知,第二个参数是明文吧,第三个参数应该就是返回值,那么我们就需要先记录一下r2寄存器的地址,为何不在结束后直接mr2呢?因为这个时候寄存器中的地址已经不是原先的用来存返回值的地址了,而我们只需要去读最开始的地址里面的值就可以了,这点和frida的hook是一个意思,应该能理解;当然我在代码里也提供了自动的方式,与frida是一样的思想;最后密文的结果如下:正确密文:57b0d60b1873ad7de3aa2f5c1e4b3ff6接下来就可以进行dfa攻击了,这个需要了解aes算法细节,我就不在这里提了,中心思想就是在也就是第八轮以及第九轮运算中两次列混淆之间进行;那我们怎么来攻击呢?首先看我们之前提到过的明文state化的矩阵运算,dfa攻击就需要去改变state的某些值来达到生成故障明文的目的;我们hook一下这个地址,看看state化后的明文到底在哪个地址,后续在合理的轮数进行攻击不就行了吗;emulator.attach().addBreakPoint(module.base + 0x4DF0);直接在state的位置下断点,看看现在的r0的值;很好,是明文,再看看r2的值,不知道为什么看r2的话去看看伪代码;是空的,很好,符合预期;这里我们需要去看它在这个函数完之后是不是我们的明文经过运算的结果,去找一下函数结束的位置;按照这个起始位置来下断点就行;emulator.attach().addBreakPoint(module.base + 0x4DF0); emulator.attach().addBreakPoint(module.base + 0x46C8);在结束位置再次查看结果即可(m0xbffff468);结果是我们推测的那样,那么我们要思考一个问题了,注入代码怎么写以及为什么要这么写;先看看注入一次故障的密文,我们要的是四个字节的差异,多了也就失去了意义,代码如下:public void dfaAttack() { // 0x4E26是i==9的位置,在这里下断点 emulator.attach().addBreakPoint(module.base + 0x4E26, new BreakPointCallback() { int round = 0; // 上述明文state块的位置 对它进行修改以达到dfa的目的 UnidbgPointer statePointer = memory.pointer(0xbffff468); @Override public boolean onHit(Emulator<?> emulator, long address) { round += 1; System.out.println("round:"+round); // dfa的时机 if (round % 9 == 0){ statePointer.setByte(randInt(0, 15), (byte) randInt(0, 0xff)); } return true; } }); }查看此处的密文结果;正确密文:57b0d6 0b 1873 ad 7de3 aa 2f5c 1e 4b3ff6 dfa后: 57b0d6 fd 1873 a0 7de3 7a 2f5c 57 4b3ff6可以看到确实是我们期望的那样,四个字节的偏差;接下来就是获取大量密文了,另外,你要是直接看结果是不可以的,因为它最后还做了其他的操作,我们需要在4DCC处拿到加密后的结果,就像上面的操作,但是这里会输出蛮多东西,我做了明文的判断依旧无果,所以在拿故障密文的时候需要自己去拿,我循环30次,这已经足够了,看看大概的代码; public void hook_debugger() { // m0xbffff468 // emulator.attach().addBreakPoint(module.base + 0x4DF0); // emulator.attach().addBreakPoint(module.base + 0x46C8); emulator.attach().addBreakPoint(module.base + 0x5A34, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { String fakeInput = "hello"; int length = fakeInput.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); return true; } }); // m0xbffff51c emulator.attach().addBreakPoint(module.base + 0x4DCC, new BreakPointCallback() { RegisterContext context = emulator.getContext(); Arm32RegisterContext context1 = emulator.getContext(); final Pointer input = context1.getR1Pointer(); @Override public boolean onHit(Emulator<?> emulator, long address) { Arm32RegisterContext context1 = emulator.getContext(); final Pointer input = context1.getR1Pointer(); String plaintext = input.getString(0); // 判断是否为目标明文 if ("hello".equals(plaintext)) { System.out.println("明文--->>> " + plaintext); // 保存第三个参数的地址 final UnidbgPointer arg2 = context.getPointerArg(2); // final Pointer arg2 = context1.getR2Pointer(); // hook LR 只在此情况下添加断点 emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { // 输出函数执行结果 // Inspector.inspect(arg2.getByteArray(0, 16), "AES密文--->>>"); // 也可以直接打印 arg2 地址和值 byte[] arg2Bytes = arg2.getByteArray(0, 16); // 读取 16 个字节 StringBuilder hexString = new StringBuilder(); for (byte b : arg2Bytes) { hexString.append(String.format("%02x", b)); } String result = hexString.toString().toLowerCase(); System.out.println("AES密文--->>>" + result.trim()); return true; } }); } return true; } }); } public void dfaAttack() { // 上面有 } public static int randInt(int min, int max) { Random rand = new Random(); return rand.nextInt((max - min) + 1) + min; } public static void main(String[] args) { WuLing demo = new WuLing(); demo.patch(); for (int i = 0; i < 30; i++) { demo.hook_debugger(); demo.dfaAttack(); demo.call_checkcode(); } }这里注入的时候需要注意上面说的问题,我们要的密文结果并不是最终的结果,需要在4DCC函数执行完后输出,在上面有提到,其次就是按照我的方式来输出会很多,没排查到具体原因,可能是地址存放的东西变了,但不影响dfa,最后把结果收集起来用phoenixAES推K10就行;import phoenixAES phoenixAES.crack_file('data.txt', [], True, False, 3)data.txt就是故障密文,第一行是正确的密文,推出结果如下;再使用轮密钥推到主密钥的工具去推K0就行;k0就是密钥,命令里的10代表轮数,密钥为:F6F472F595B511EA9237685B35A8F866;5.2 iv但是aes光有密钥是不够的,还需要看iv、模式、填充等;通过符号猜测可能是cbc模式,它是有iv的,那我们怎么去找这个iv?我们知道,iv在最开始的时候需要与处理过的明文进行异或操作,而异或操作是可以做逆运算的,在上述位置我们dfa注入的时候,明文的矩阵运算位置我们是知道的,那这里不正好是可以看到矩阵混合之后的明文吗,它就在这里面与iv进行了异或,换句话说,我们拿到它运算后的结果再与我们的明文进行异或就可以得到iv,这是异或的特点,a ^ b = c也可以是 a ^ c = b;根据这个思路我们去进行hook一下,前面正好打过断点,直接搬过来就行;public void hook_iv() { emulator.attach().addBreakPoint(module.base + 0x5A34, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { String fakeInput = "hello"; int length = fakeInput.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); return true; } }); emulator.attach().addBreakPoint(module.base + 0x4DF0); emulator.attach().addBreakPoint(module.base + 0x46C8); }在0x4DF0处断下后记得记住r3的地址,到0x46C8打印看就是它运算后的结果,不过之前的过程其实也知道这个地址是0xbffff468;首先我们的明文是hello,转化成矩阵应该是这样的;68 65 6C 6C 6F 00 00 00 00 00 00 00 00 00 00 00对比它的结果,貌似是没什么区别啊,难道我们输出的位置并不是明文处理完成的位置?也不应该啊,这时候就突然想到,异或还有一个特点啊,任何数值和0异或得到的结果都是本身,那这里有没有可能是iv全是0?带着疑惑我选择去测试一下,就拿之前的参数去加密一下看看;sana res --->>>MVNWGVFEpD18zqVt4x39yEZIE/y7BcfFhTbn9JUQ7bSrQ1EATn/MHXNQMOmE0Dlx0LYNvoPcIOPQlyRORq87sSsA5oUrlywzc6vjKJMlNb4gAL87KpYFQC9jznMlQVwFM2+fs59XMS6XehbNQ/yQHi1NSGSeFGA9roRTajzYyacLDlMR9WZJqZ+mTUwylU3lZDcCYkian0uE+3Q5NAdMZ7YqBhlEkjBKRN21UBsqHy8oLN/TIoTBXpy/TnI8DXxDdqb2Tv2SkR/YrDMO0Jnn4adjdrJvTEo0tEvXaI6Q1bRLspCZiSSVhsrQLs/HvRKTxMb2HV/xBafnJY7NxkXgoBTF7fe2I8m2QGxui8Q0aTv98KVj7p70Gcg2DStAkIV743nKmLp+XMMU8GhLDl4HI2g== sana 解密结果--->>>mobile=15211112222&password=111111&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=fhtTZiDQrY&response_type=token&ostype=ios&imei=99241FFAZ007VF&mac=c6:2c:b7:2c:a4:47&model=Pixel 4&sdk=29&serviceTime=1736087879878&mod=Google&checkcode=64e0d17b019b98a23f886f26a4b6f600测试结果发现是一样的,只是在开头拼接了一个M,且这里是做了base64,在最开始也看到过;那么这个iv我们也已经还原出来了,填充的话我是用cyberchef测试的,它默认是pkcs7填充,当然这里的填充也可以去验证,这里不细说了,所以我们就可以得到如下结论:加密方式:aes128-cbc模式-pkcs7填充;key:F6F472F595B511EA9237685B35A8F866;iv:00000000000000000000000000000000;结果需要base64后拼接M;到这里还没完,明文参数里有一个checkcode是动态的,也需要看一下;5.3 checkcode它是32位的参数,首当其冲就是猜测它是md5,我们都很清楚md5的套路,无非是把前面的参数做了签名,最多加盐,然后才是魔改,我们肯定希望直接能出结果,所以我们去测试一下;先拿一个密文解一下,然后再去测试;hook的密文:VNWGVFEpD18zqVt4x39yEZIE/y7BcfFhTbn9JUQ7bSrQ1EATn/MHXNQMOmE0Dlx0LYNvoPcIOPQlyRORq87sSsA5oUrlywzc6vjKJMlNb4gAL87KpYFQC9jznMlQVwFM2+fs59XMS6XehbNQ/yQHi1NSGSeFGA9roRTajzYyacLDlMR9WZJqZ+mTUwylU3lZDcCYkian0uE+3Q5NAdMZ7fWFORCmhvWU5rBGBHrR17lccfF4Yrb6o4x+j3f3Unnfri1aCh9bnelkM2tvMrsd9xAmoy7wcwFRknsymvQzb8eHjkAFwtKQnBuMKARF3jQb5sp+85vd86jBnPUBQ3WI8xs1yDX97YqLWYq2WvDSisAzNO6P5RlvazEIoZHETjTfvqVhXubTSGx2Pdy7wTXD5Q== 解密结果: mobile=15211112222&password=111111&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=fhtTZiDQrY&response_type=token&ostype=ios&imei=unknown&mac=02:00:00:00:00:00&model=Pixel 4&sdk=29&serviceTime=1736087879878&mod=Google&checkcode=379f0627ee7b3e895459a6d848219413 测试加密: 48219413ee7b3e895459a6d8379f0627直接看让我们很失望,好像并不一样,但仔细看看会发现一点端倪;48219413 ee7b3e895459a6d8 379f0627 379f0627 ee7b3e895459a6d8 48219413很显然了,它把前四个字节和最后四个字节进行了交换,实际上是一个标准的md5,在这里会引出一个小细节,我为什么要去拿一个hook到的密文呢?而不是用unidbg跑出来的密文?我在测试完成后去拿unidbg跑出的结果进行了测试,发现是和上面的理论对不上的,这是怎么回事?可上面确实是如此啊,我认为可能是环境的问题,或者其他的问题,咨询画神说unidbg跑出来对不上很正常,因为在之前的分析里md5特征是很明显的,而且是不止一处有特征,所以有些环境判断你有问题也是正常的,这里建议拿真机的结果来测试;6. 总结由于我时间不多,写这篇文章也是断断续续的,所以中间的分析过程我自己也不知道有没有讲清楚,如果有不清楚的可以自己多多研究,我的水平也比较有限;
2025年01月07日
16 阅读
0 评论
0 点赞
2025-01-06
安卓逆向-Bilibili播放量接口
一、Bilibili1. 前置[!sana] 版本:6.24.0包名:tv.danmaku.biliso文件:libbili.so目标:增加视频播放量算法2. 抓包分析使用socksdroid转发+charles抓包即可;一共两个接口比较重要:https://api.bilibili.com/x/report/heartbeat/mobile https://api.bilibili.com/x/report/click/android2抓包示例如下:第一个是心跳包,熟悉的朋友可能知道,控制完播率这方面的,剩下一个就是增加播放量的;3. click android2接口首先看一下接口需要哪些参数:请求地址: https://api.bilibili.com/x/report/click/android2 请求方式: POST 请求头: buvid XY88A4185B06248EA82BB0101C2ADC5DAD33B device-id bFpoC2lebA9uWm5ZJVkl fp_local d9c996c3fb98c1808bbf64ee054dda5420241222161236dee9d93eca085cd19e fp_remote d9c996c3fb98c1808bbf64ee054dda542024112221174303fb9f17f667dc29f3 session_id f815bbd9 请求体: ùouO\#âèhA^>o(](©ú······Ã<xxÚý\@aèyd 是一串乱码样式的内容3.1 请求体分析直接反编译搜索地址的后部分即可,这样比较唯一,搜索到后直接点过去;这里可以发现心跳接口其实也在这里,并且它是一个接口,那就去找它的实现,右键查找用例或者按x,也只有一个位置,点进去看看;这里其实也跟明了,看不懂的可以去了解一下retrofit,这里调用reportClick方法,传入了create这个参数,参考接口定义的位置,这个变量应该就是请求体;那么我们就应该去看create这个参数怎来的;public final void a() { long j2; long i = c2.f.f.c.j.a.i() / 1000; c2.f.b0.c.b.b.a.a E = c2.f.b0.c.b.b.a.a.E(); x.h(E, "EnvironmentPrefHelper.getInstance()"); long A = E.A(); if (A == -1) { c2.f.b0.c.b.b.a.a E2 = c2.f.b0.c.b.b.a.a.E(); x.h(E2, "EnvironmentPrefHelper.getInstance()"); E2.V(i); j2 = i; } else { j2 = A; } c0 create = c0.create(w.d(com.hpplay.sdk.source.protocol.h.E), d.this.H7(this.b.a(), this.b.b(), this.b.h(), i, j2, this.b.n(), this.b.m(), this.b.k(), this.b.c(), this.b.e(), this.b.l(), this.b.f())); x.h(create, "RequestBody.create(Media…ion/octet-stream\"), body)"); l<String> execute = ((tv.danmaku.biliplayerimpl.report.heartbeat.a) com.bilibili.okretro.c.a(tv.danmaku.biliplayerimpl.report.heartbeat.a.class)).reportClick(create).execute(); int b = execute.b(); String h = execute.h(); BLog.i("HeartBeatTracker", "player report click(vv): responseCode:" + b + ", responseMsg:" + h + ", responseBody:" + execute.a()); } 分析它的源码;x1 = w.d(com.hpplay.sdk.source.protocol.h.E) x2 = d.this.H7(this.b.a(), this.b.b(), this.b.h(), i, j2, this.b.n(), this.b.m(), this.b.k(), this.b.c(), this.b.e(), this.b.l(), this.b.f()) c0 create = c0.create(x1,x2);其实他就两个参数,第一个参数是一个请求格式的常量,那么第二个参数看着这么长,就有可能是我们需要的请求体参数了;进入H7方法看看;public final byte[] H7(long j2, long j4, int i, long j5, long j6, int i2, int i3, long j7, String str, int i4, String str2, String str3) throws Exception { long j8; int i5; Application f2 = BiliContext.f(); com.bilibili.lib.accounts.b client = com.bilibili.lib.accounts.b.f(f2); AccountInfo h = BiliAccountInfo.f.a().h(); if (h != null) { j8 = h.getMid(); i5 = h.getLevel(); } else { j8 = 0; i5 = 0; } TreeMap treeMap = new TreeMap(); treeMap.put("aid", String.valueOf(j2)); treeMap.put("cid", String.valueOf(j4)); treeMap.put("part", String.valueOf(i)); treeMap.put(EditCustomizeSticker.TAG_MID, String.valueOf(j8)); treeMap.put("lv", String.valueOf(i5)); treeMap.put("ftime", String.valueOf(j6)); treeMap.put("stime", String.valueOf(j5)); treeMap.put("did", com.bilibili.lib.biliid.utils.f.a.c(f2)); treeMap.put("type", String.valueOf(i2)); treeMap.put("sub_type", String.valueOf(i3)); treeMap.put("sid", String.valueOf(j7)); treeMap.put("epid", str); treeMap.put("auto_play", String.valueOf(i4)); x.h(client, "client"); if (client.r()) { treeMap.put("access_key", client.g()); } treeMap.put("build", String.valueOf(com.bilibili.api.a.f())); treeMap.put("mobi_app", com.bilibili.api.a.l()); treeMap.put("spmid", str2); treeMap.put("from_spmid", str3); StringBuilder sb = new StringBuilder(); for (Map.Entry entry : treeMap.entrySet()) { String str4 = (String) entry.getValue(); sb.append((String) entry.getKey()); sb.append('='); if (str4 == null) { str4 = ""; } sb.append(str4); sb.append('&'); } sb.deleteCharAt(sb.length() - 1); String sb2 = sb.toString(); x.h(sb2, "builder.toString()"); String b2 = t3.a.i.a.a.a.b.e.b(sb2); BLog.i("HeartBeatTracker", "player report click(vv), params: " + sb2 + " & sign=" + b2); sb.append("&sign="); sb.append(b2); String sb3 = sb.toString(); x.h(sb3, "builder.toString()"); return t3.a.i.a.a.a.b.e.a(sb3); }看起来很庞大,但是貌似也有些比较敏感的参数,这个方法的主要目的是构建一个包含多个参数的请求,对这些参数进行签名,然后将签名后的参数字符串转换为字节数组,它会把map的参数逐一添加到sb中,中间以&符号拼接;这里很明显就是看这个sign嘛,去看看b方法;public final String b(String params) { x.q(params, "params"); Charset charset = com.bilibili.commons.c.b; x.h(charset, "Charsets.UTF_8"); byte[] bytes = params.getBytes(charset); x.h(bytes, "(this as java.lang.String).getBytes(charset)"); String str = d; Charset charset2 = com.bilibili.commons.c.b; x.h(charset2, "Charsets.UTF_8"); if (str != null) { byte[] bytes2 = str.getBytes(charset2); x.h(bytes2, "(this as java.lang.String).getBytes(charset)"); String g = com.bilibili.commons.m.a.g(bytes, bytes2); x.h(g, "DigestUtils.sha256(param…yteArray(Charsets.UTF_8))"); Locale locale = Locale.US; x.h(locale, "Locale.US"); if (g != null) { String lowerCase = g.toLowerCase(locale); x.h(lowerCase, "(this as java.lang.String).toLowerCase(locale)"); return lowerCase; } throw new TypeCastException("null cannot be cast to non-null type java.lang.String"); } throw new TypeCastException("null cannot be cast to non-null type java.lang.String"); }那这里的主要内容就是这个g方法了,同样进去看看;public static String g(byte[] bArr, byte[] bArr2) { try { MessageDigest messageDigest = MessageDigest.getInstance(AaidIdConstant.SIGNATURE_SHA256); messageDigest.reset(); messageDigest.update(bArr); if (bArr2 != null) { messageDigest.update(bArr2); } return g.H(messageDigest.digest()); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } }打眼一看应该就是一个sha256算法;那么接下来就是需要hook验证了,先去hook H7看看位置对不对;function hook_h7() { Java.perform(function () { var ByteString = Java.use("com.android.okhttp.okio.ByteString"); let d = Java.use("tv.danmaku.biliplayerimpl.report.heartbeat.d"); d["H7"].implementation = function (j2, j4, i, j5, j6, i2, i3, j7, str, i4, str2, str3) { console.log(`d.H7 is called: j2=${j2}, j4=${j4}, i=${i}, j5=${j5}, j6=${j6}, i2=${i2}, i3=${i3}, j7=${j7}, str=${str}, i4=${i4}, str2=${str2}, str3=${str3}`); let result = this["H7"](j2, j4, i, j5, j6, i2, i3, j7, str, i4, str2, str3); console.log(`d.H7 result=${ByteString.of(result).hex()}`); return result; }; }) } hook_h7()hook的结果如下,转成16进制输出;[Pixel 4::哔哩哔哩 ]-> d.H7 is called: j2=1205801029, j4=1593015651, i=1, j5=1734867854, j6=1734855270, i2=3, i3=0, j7=0, str=, i4=0, str2=main.ugc-video-detail.0.0, str3=main.ugc-video-detail.0.0 d.H7 result=36c64b4e8b138184f3c14a734642b5a2168e1cd05181eed73655abdddca85f7d8d7379750746b64e55e8285d4ba87747f78439f6c9f629cc9fcd35edb2748d2ef7ad4e4221bb01363c86624dc7fc8256627fe6315ea001c3dcc701c6facd7dd46621225582b367c2ec7844035a20be2c9218c58c39d6641b90a7d2fb91e6dad2a1124c848d27cfcab1541714f1f8390e80de2a4ab0ce13dfb4210a75df6792286fe7029b7d11d1362add404442bedeee3e6911f36fa360a2d1cad53be0fce4aada0eb373f5f065a36e35db91242923ac23a3eac4b5c5d08f79298a840249a674b746af2273f97a3d3d07522061eb3e324824fe9701cf583e85c73ab4997ed06bcce957773dd909af1531e2bf2a477b4f2f7901137c7d96e82835d3a96b79952428a5f4474ae1b54ce901c9649eb397f12523b9b536d099acaa62043db34f3d59抓包的结果拿去对比一下;可以发现位置是没有错的,那么就可以去看sign怎么来的了,上面知道了sign怎么来的了,那我们也直接去hook一下;function hook_sign() { Java.perform(function () { let a = Java.use("com.bilibili.commons.m.a"); a["g"].implementation = function (bArr, bArr2) { console.log(`a.g is called: bArr=${Bytes2utf8String(bArr)}, bArr2=${Bytes2utf8String(bArr2)}`); let result = this["g"](bArr, bArr2); console.log(`a.g result=${result}`); return result; }; }) } hook_sign();输出结果如下:[Pixel 4::哔哩哔哩 ]-> a.g is called: bArr=aid=1002480418&auto_play=0&build=6240300&cid=1487958098&did=bFpoC2lebA9uWm5ZJVkl&epid=&from_spmid=main.ugc-video-detail.0.0&ftime=1734855270&lv=0&mid=0&mobi_app=android&part=1&sid=0&spmid=main.ugc-video-detail.0.0&stime=1734869003&sub_type=0&type=3, bArr2=9cafa6466a028bfb a.g result=9d5a27d1f98c1a9ae66558db3c2a3804419f696511cc9c1b037a6c86965267f4上面我们看源码觉得像sha256算法,这里它传了两个参数,本身是字节数组,我这里直接转成字符串,因为我们知道传进来的是什么,那么这个参数2在源码里是这样用的:if (bArr2 != null) { messageDigest.update(bArr2); }这毫无疑问就是盐嘛,也可以搜索一下看看;这里还有key iv等关键词,那么这个可以留个心眼了,那就看看是不是标准算法;没有问题,就是sha256加盐算法,是标准的,那这个java层的算法其实算法助手就解了;剩下的请求体加密也就没啥可细说的了,加密是AES;剩下的我就写了个主动调用,免得每次都去发请求;function ByteString2Hex(data) { var ByteString = Java.use("com.android.okhttp.okio.ByteString"); return ByteString.of(data).hex(); } function hook_aes() { Java.perform(function () { let b = Java.use("t3.a.i.a.a.a.b"); b["a"].implementation = function (body) { console.log(`hook_aes的参数--->>>${body}`); let result = this["a"](body); console.log(`hook_aes的结果--->>>${ByteString2Hex(result)}`); return result; }; }) } hook_aes() function call_H7() { Java.perform(function () { var ByteString = Java.use("com.android.okhttp.okio.ByteString"); let dClass = Java.use("tv.danmaku.biliplayerimpl.report.heartbeat.d"); let dInstance = dClass.$new(); let j2 = 1205801029; // long类型的值 let j4 = 1593015651; // long类型的值 let i = 1; // int类型的值 let j5 = 1734867854; // long类型的值 let j6 = 1734855270; // long类型的值 let i2 = 3; // int类型的值 let i3 = 0; // int类型的值 let j7 = 0; // long类型的值 let str = ""; // String类型的值 let i4 = 0; // int类型的值 let str2 = "main.ugc-video-detail.0.0"; // String类型的值 let str3 = "main.ugc-video-detail.0.0"; // String类型的值 // 调用H7方法 let result = dInstance.H7(j2, j4, i, j5, j6, i2, i3, j7, str, i4, str2, str3); result = ByteString.of(result).hex() console.log('主动调用--->>>' + result); }) } // callH7();直接拿固定的测试一下,主动调用一下看看结果;[Pixel 4::哔哩哔哩 ]-> call_H7() hook_aes的参数--->>>aid=1205801029&auto_play=0&build=6240300&cid=1593015651&did=bFpoC2lebA9uWm5ZJVkl&epid=&from_spmid=main.ugc-video-detail.0.0&ftime=1734855270&lv=0&mid=0&mobi_app=android&part=1&sid=0&spmid=main.ugc-video-detail.0.time=1734867854&sub_type=0&type=3&sign=8e5f7097273861c2141cff4e1da69fb05e876401c0884c718cf2e68a599676b5 hook_aes的结果--->>>36c64b4e8b138184f3c14a734642b5a2168e1cd05181eed73655abdddca85f7d8d7379750746b64e55e8285d4ba87747f78439f6c9f629cc9fcd35edb2748d2ef7ad4e4221bb01363c86624dc7fc8256627fe6315ea001c3dcc701c6facd7dd46621225582b367c2ec7835a20be2c9218c58c39d6641b90a7d2fb91e6dad2a1124c848d27cfcab1541714f1f8390e80de2a4ab0ce13dfb4210a75df6792286fe7029b7d11d1362add404442bedeee3e6911f36fa360a2d1cad53be0fce4aada0eb373f5f065a36e35db91242923ac23a3eac4b5c5d08f79298a840249a674b746af2273f97a3d3d07522061eb3e324824fe9701cf583e85c73ab4997ed06bcce957773dd909af1531e2bf2a477b4f2f7901137c7d96e82835d3a96b79952428a5f4474ae1b54ce901c9649eb397f12523b9b536d099acaa62043db34f3d59 主动调用--->>>36c64b4e8b138184f3c14a734642b5a2168e1cd05181eed73655abdddca85f7d8d7379750746b64e55e8285d4ba87747f78439f6c9f629cc9fcd35edb2748d2ef7ad4e4221bb01363c86624dc7fc8256627fe6315ea001c3dcc701c6facd7dd46621225582b367c2ec7844035a2c9218c58c39d6641b90a7d2fb91e6dad2a1124c848d27cfcab1541714f1f8390e80de2a4ab0ce13dfb4210a75df6792286fe7029b7d11d1362add404442bedeee3e6911f36fa360a2d1cad53be0fce4aada0eb373f5f065a36e35db91242923ac23a3eac4b5c5d08f79298a840249a674b7746af2273f97a3d3d07522061eb3e324824fe9701cf583e85c73ab4997ed06bcce957773dd909af1531e2bf2a477b4f2f7901137c7d96e82835d3a96b79952428a5f4474ae1b54ce901c9649eb397f12523b9b536d099acaa6203db34f3d59强烈建议把hook到的内容输出到文件里再测试,真的是害得我好苦。。。测试结果如下:是对的上的,那就证明没问题,只是提交的时候是原格式而不是hex;那么明文参数也需要看一看,aid和cid其实是视频相关的id,ftime是视频打开时间,stime是视频开始播放时间,剩下的就是一个did了;经过多次清除数据尝试,最终定位到其实就是在取mac地址;后续做加密的时候还需要拼接三个 | 符号,这是在我的设备上的方式;就类似这里,其实最理想的获取到应该为这样的组合:mac地址|蓝牙地址|设备总线|sn号 // 去跟一下java层的逻辑就知道了但在我的设备上hook多次都只是得到如下结果:c62cb72ca447|||,那么自行模拟即可;b方法就是加密的算法,明文就是上面说的东西,使用python复现b方法即可;import base64 def b(str): # 将字符串转换为字节 bytes_str = bytearray(str.encode()) # 进行位操作 bytes_str[0] = bytes_str[0] ^ (len(bytes_str) & 255) for i in range(1, len(bytes_str)): bytes_str[i] = (bytes_str[i - 1] ^ bytes_str[i]) & 255 try: return base64.b64encode(bytes(bytes_str)).decode() except Exception: return str # 使用示例 encoded_str = b("c62cb72ca447|||") print(encoded_str) # bFpoC2lebA9uWm5ZJVkl这里生成的did也是和参数里的对的上的,那么模拟就很容易了;那么请求体的分析就完成了;3.2 请求头分析具体参数如下:请求头: buvid XY88A4185B06248EA82BB0101C2ADC5DAD33B device-id bFpoC2lebA9uWm5ZJVkl fp_local d9c996c3fb98c1808bbf64ee054dda5420241222161236dee9d93eca085cd19e fp_remote d9c996c3fb98c1808bbf64ee054dda542024112221174303fb9f17f667dc29f3 session_id f815bbd9Ⅰbuvid先看buvid的生成,我这里直接hook了hashmap的put方法,可以定位到put的位置如下:往下稍微找一下即可发现关键的位置;定位的方法很多,能定位到都行,不需要拘泥于定位代码的方式,重点并不在此;然后就可以去跟了,最终大概能跟到这个位置;可以去hook一下,发现他确实是走了这里,但在hook之前需要先清除数据,并且它的结果是以XY开头的,那么就很显然是走的哪一个位置了;j2其实是mac地址,上面的分析也是走到了那个位置,java层都是很简单的,去跟一下就很明了了,a.d方法是一个md5,e方法需要python复现;def md5(input_string): md5_obj = hashlib.md5() md5_obj.update(input_string.encode('utf-8')) md5_hash = md5_obj.hexdigest() return md5_hash def e(str_input): sb = [] sb.append(str_input[2]) # Python索引从0开始,所以这里是第3个字符 sb.append(str_input[12]) # 第13个字符 sb.append(str_input[22]) # 第23个字符 return ''.join(sb) def main(): mac_id = "C6:2C:B7:2C:A4:47" data = md5(mac_id) res = "XY" + e(data) + data print(res) if __name__ == '__main__': main() # hook到:XY88a4185b06248ea82bb0101c2adc5dad33b # 运行结果:XY88a4185b06248ea82bb0101c2adc5dad33b这里的mac地址是没有去掉中间的分割符的,需要注意一下;Ⅱ sessionid接下来看sessionid的生成,直接去看代码;随便跟两步就能跟到一个接口,众所周知接口那就找实现;可以两个都去看一下,真实的就是下面这个,进去后找它实现的getSessionId方法,再次进入;又是一个接口,同样的操作继续去看;最终就能跟到这里,读一下逻辑可以发现他去读了,如果没有就返回L,那我们理想的肯定是L的生成才是最好的,那就去找L的生成位置;高亮选中即可找到,其实就是在随机,直接给出代码;def generate_random_hex(): bArr = [random.randint(0, 255) for _ in range(4)] hex_str = binascii.hexlify(bytearray(bArr)).decode('utf-8') return hex_strⅢ fp_local根据上述的逻辑可以跟到这个位置;还需要继续往下跟,但这里的方法告诉我们它可能是一个指纹信息;public final String c() { String str = ""; if (k()) { ReentrantReadWriteLock.ReadLock r = e; x.h(r, "r"); r.lock(); try { if (a != null && (str = a) == null) { x.Q("buvidLocal"); } } finally { r.unlock(); } } return str; }跟进来之后,str = a就是它赋值的位置,那么这里就需要找a的赋值位置,可以直接搜a =来定位;这个d2可能就是我们的fp_local的值,那么我们hook验证一下;function hook_local() { Java.perform(function () { let a = Java.use("com.bilibili.lib.biliid.internal.fingerprint.a.a"); a["a"].implementation = function (buvidLegacy, data) { console.log(`a.a is called: buvidLegacy=${buvidLegacy}, data=${data}`); let result = this["a"](buvidLegacy, data); console.log(`a.a result=${result}`); return result; }; }) } hook_local()这里清理内存再hook,可以发现确实是走了这里,位置是没错的;a.a is called: buvidLegacy=XY88A4185B06248EA82BB0101C2ADC5DAD33B, data=Data(main={str_brightness=31, app_version=6.24.0, cpuModel=ARMv8 Processor rev 14 (v8l), speed_sensor=1, adb_enabled=1, screen=1080,2236,440, ui_version=qd1a.190821.011, linear_speed_sensor=1, virtualproc=[], sensors_info=[{"name":"LSM6DSR ······· Accelerometer","vendor":"STMicro","version":"142083","type":"1","maxRange":"156.9064","resolution":"0.0047856453","power":"0.17","minDelay":"2404"}, {"name":"LIS2MDL Magnetometer","vendor":"STMicro","version":"262","type":"2","maxRange":"4915.2","resolution":"0.1","power["1230768000000,com.google.omadm.trigger,1,1.0,1,1230768000000","1230768000000,com.google.android.carriersetup,1,10,29,1230768000000","1230768000000,com.android.cts.priv.ctsshim,1,9-5374186,28,1230768000000","1230768000000,com.google.android.youtube,1,14.19.57,1419573400,1230768000000",t.sys.country=, ro.boot.serialno=, gsm.network.type=Unknown, net.eth0.gw=, net.dns1=, sys.usb.state=, http.agent=}, sys={product=flame, cpu_abi=armeabi-v7a, serial=unknown, display=QD1A.190821.011, fingerprint=google/flame/flame:10/QD1A.190821.011/5849216:user/release-keys, cpu_abi2=armeabi, device=flame, manufacturer=Google, hardware=flame}) a.a result=d9c996c3fb98c1808bbf64ee054dda5420241224203228adabc704bcb23630d6第一个参数是我们的buvidLegacy,其实就是buvid,第二个就是环境相关的;可以看到确实是很多环境信息,包括了很多东西,甚至还读了我们安装的所有apk的信息,此时抓包的fp_local也确实是我们hook到的,那生成位置也就确定了;继续跟到详细的位置;public static final String a(String buvidLegacy, com.bilibili.lib.biliid.internal.fingerprint.b.a data) { String str; // 这两句是log // x.q(buvidLegacy, "buvidLegacy"); // x.q(data, "data"); return (MiscHelperKt.a(f(buvidLegacy, data)) + h() + MiscHelperKt.a(g())) + b(str); }拆解一下,MiscHelperKt.a(f(buvidLegacy, data)) + h() + MiscHelperKt.a(g()),它的结果 + b(str)就是最终的fp_local;一个一个来看,首先看 g() :那么 g() 就是在随机8位,因为e.a方法是随机,且log也能看出;MiscHelperKt.a(g()):这段代码的目的是将一个字节数组转换为它的十六进制字符串表示,也就是转16进制返回;h():很明显时间戳相关的函数;f(buvidLegacy, data):f方法接收了两个参数,一个是buvid,一个是环境参数,它的返回值也需要拆解一下,但是也比较明了,buvid + 环境参数里的model + 环境参数里的band,后面这两个参数在刚刚hook到的内容也是可以找到的;最后再通过e方法再返回,看看e方法;是一个md5算法,就是上面提到过的md5;那么整体的加密如下:f(buvidLegacy, data): md5(buvid + 环境参数里的model + 环境参数里的band)MiscHelperKt.a(f(buvidLegacy, data)) + h() + MiscHelperKt.a(g())):看着也比较熟悉,应该也是将传入的内容转成16进制;b(str):这个看着比较晦涩,中间的逻辑暂时不去管,最终返回的也是一个16进制的数据;大致的逻辑看完了,整体理一下;fp_local: (MiscHelperKt.a(f(buvidLegacy, data)) + h() + MiscHelperKt.a(g())) + b(str); [hex(md5(buvid + 环境参数里的model + 环境参数里的band)) + 时间戳 + hex(随机8位)] + hex_b(str)hex_b使用python复现即可,里面有三个数字的参与,需要确定下来;剩余的内容就没有啥了,使用python复现即可,上面的b方法还缺了个参数,我hook之后发现其实就是前面的那一堆运算出去b方法的值,我没有找到具体位置,所以使用jeb反编译看看;结合起来看是可以证明的,另一个参数fp_remote就无所谓了,其实是差不多的形式;3.3 小结请求体涉及sha256加盐、aes算法,请求头buvid是mac地址得到的,sessinid是随机的,fp_local涉及md5、时间戳等;4. heartbeat接口接下来分析心跳包接口,心跳包是什么意思?在app端,打开一个视频会发送一个心跳包,视频播放完毕,或者说你停止视频后也会发一个心跳包;基础信息如下:请求地址: https://api.bilibili.com/x/report/heartbeat/mobile 请求方式: POST 请求头: buvid XY88A4185B06248EA82BB0101C2ADC5DAD33B device-id bFpoC2lebA9uWm5ZJVkl fp_local d9c996c3fb98c1808bbf64ee054dda542024122422130631012624a0802cf35b fp_remote d9c996c3fb98c1808bbf64ee054dda542024112221174303fb9f17f667dc29f3 session_id 492359b7接下来抓两次包进行对比,第一个是打开视频,第二个是视频播放结束;视频start参数如下:请求体: actual_played_time 0 aid 113454211927230 appkey 1d8b6e7d45233436 auto_play 0 build 6240300 c_locale zh-Hans_CN channel xxl_gdt_wm_253 cid 26690260642 epid 0 epid_status from 2 from_spmid main.ugc-video-detail.0.0 last_play_progress_time 0 list_play_time 0 max_play_progress_time 0 mid 0 miniplayer_play_time 0 mobi_app android network_type 1 paused_time 0 platform android play_status 0 play_type 1 played_time 0 quality 32 s_locale zh-Hans_CN session 80db3b980c3763fe5962aa35b2d42f380e7ead92 sid 0 spmid main.ugc-video-detail.0.0 start_ts 0 statistics {"appId":1,"platform":3,"version":"6.24.0","abtest":""} sub_type 0 total_time 0 ts 1735130848 type 3 user_status 0 video_duration 34 sign 58a6bdd705a1b1934ca151d23ee27934视频end参数如下:请求体: actual_played_time 35 aid 113454211927230 appkey 1d8b6e7d45233436 auto_play 0 build 6240300 c_locale zh-Hans_CN channel xxl_gdt_wm_253 cid 26690260642 epid 0 epid_status from 2 from_spmid main.ugc-video-detail.0.0 last_play_progress_time 34 list_play_time 0 max_play_progress_time 34 mid 0 miniplayer_play_time 0 mobi_app android network_type 1 paused_time 0 platform android play_status 0 play_type 1 played_time 35 quality 32 s_locale zh-Hans_CN session 80db3b980c3763fe5962aa35b2d42f380e7ead92 sid 0 spmid main.ugc-video-detail.0.0 start_ts 1735130823 statistics {"appId":1,"platform":3,"version":"6.24.0","abtest":""} sub_type 0 total_time 35 ts 1735130883 type 3 user_status 0 video_duration 34 sign adddc27c9e78a2318576c08ea6d19a66找个文本对比,看看哪些是不一样的;actual_played_time 0 actual_played_time 35 真正播放视频的时间--》要等于视频总时间,才是完播 last_play_progress_time 0 last_play_progress_time 34 max_play_progress_time 0 max_play_progress_time 34 played_time 0 played_time 35 start_ts 0 start_ts 1735130823 total_time 0 total_time 35 ts 1735130848 ts 1735130883 sign 58a6bdd705a1b1934ca151d23ee27934 sign adddc27c9e78a2318576c08ea6d19a66大致有区别的参数就是这些,看着需要逆的就是sign加上session,虽然session并没有变化,但是不同的心跳是不一样的;请求头的参数其实在第一个接口就已经全部完成了,那么大概就需要sign+session;4.1 session首先分析session的生成,这里我选择hook hashmap的put方法定位,代码如下:function showStacks() { console.log( Java.use("android.util.Log") .getStackTraceString( Java.use("java.lang.Throwable").$new() ) ); } Java.perform(function () { var hashMap = Java.use("java.util.HashMap"); hashMap.put.implementation = function (a, b) { // 键名 if (a.equals("session")) { showStacks(); console.log("hashMap.put: ", a, b); } return this.put(a, b); } })打印的输出如下:java.lang.Throwable at java.util.HashMap.put(Native Method) at com.bilibili.api.base.util.ParamsMap.putParams(BL:5) at tv.danmaku.biliplayerimpl.report.heartbeat.HeartbeatParams.<init>(BL:3) at tv.danmaku.biliplayerimpl.report.heartbeat.d.N7(BL:30) at tv.danmaku.biliplayerimpl.report.heartbeat.d.P7(BL:2) at tv.danmaku.biliplayerimpl.report.heartbeat.d.L7(BL:3) at tv.danmaku.biliplayerimpl.report.heartbeat.d.u7(BL:3) at tv.danmaku.biliplayerimpl.core.PlayerCoreServiceV2$l.onPrepared(BL:2) at t3.a.i.b.i$j.onPrepared(BL:6) at tv.danmaku.ijk.media.player.AbstractMediaPlayer.notifyOnPrepared(BL:2) at tv.danmaku.ijk.media.player.IjkMediaPlayer$EventHandler.handleMessage(BL:107) at android.os.Handler.dispatchMessage(Handler.java:107) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7356) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) hashMap.put: session bacd228eef6eb4c3f57621de97e557e9d895f372这里就可以很显然的看到很多关键的信息,就根据逻辑去找;看谁调用了它,根据堆栈去找就行,再往下跟一下,这里r1是调用的方法,那么找this.d的赋值即可;再去找t2是谁调用了;public final String a() { Random random = new Random(); StringBuilder sb = new StringBuilder(); c2.f.b0.c.b.b.a.a E = c2.f.b0.c.b.b.a.a.E(); x.h(E, "EnvironmentPrefHelper.getInstance()"); sb.append(E.t()); // 获取buvid sb.append(System.currentTimeMillis()); // 时间戳 sb.append(random.nextInt(1000000)); // 随机数 String sb2 = sb.toString(); String sha1 = com.bilibili.commons.m.a.i(sb2); // sha1加密 if (TextUtils.isEmpty(sha1)) { return sb2; } x.h(sha1, "sha1"); return sha1; }这里大概就已经定位到了,有sha1关键词,具体的参数是什么我也在上方解释了;这里hook一下这个i方法也就是sha1方法看看参数是不是正确;a.i is called: str=1735132995295639864 a.i result=26148cf9d1cdc70ea154d68ddb325e25bac5e2fd这里的参数可以发现貌似是没有buvid的参与的,只有后两个,加密算法是标准的;明文就是时间戳+随机数,加密方式为sha1算法;4.2 sign大致一看它是32位的,可能是一个md5,但也不能保证,只是猜测,心里留心眼即可;在上面那个居多参数的位置,并没有看到sign参数在,那么这个怎么定位?我是选择hook NewStringUTF方法,这是与native交互的,这里判断依据就是sign的长度,它是固定32的,hook脚本如下:var symbols = Module.enumerateSymbolsSync("libart.so"); var addrNewStringUTF = null; for (var i = 0; i < symbols.length; i++) { var symbol = symbols[i]; if (symbol.name.indexOf("NewStringUTF") >= 0 && symbol.name.indexOf("CheckJNI") < 0) { addrNewStringUTF = symbol.address; console.log("NewStringUTF is at ", symbol.address, symbol.name); break } } if (addrNewStringUTF != null) { Interceptor.attach(addrNewStringUTF, { onEnter: function (args) { var c_string = args[1]; var dataString = c_string.readCString(); // 你的参数的特征 if (dataString && dataString.length == 32) { console.log(dataString); console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n'); console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); } } }); }hook的结果如下:e4773e52b9f23f3c788191ce92763cfa 0xbc7061a5 libbili.so!0x31a5 java.lang.Throwable at com.bilibili.nativelibrary.LibBili.s(Native Method) at com.bilibili.nativelibrary.LibBili.g(BL:1) at com.bilibili.okretro.f.a.h(BL:1) at com.bilibili.okretro.f.a.c(BL:14) at com.bilibili.okretro.f.a.a(BL:6) at com.bilibili.okretro.d.a.execute(BL:24) at com.bilibili.okretro.d.a$a.run(BL:2) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) at java.lang.Thread.run(Thread.java:919) 0xbc7061a5 libbili.so!0x31a5根据堆栈去找,就可以找到生成它的native方法,so文件是libbili.so;去hook一下看看入参,hook脚本如下:function hook_heart() { Java.perform(function () { let LibBili = Java.use("com.bilibili.nativelibrary.LibBili"); var TreeMap = Java.use("java.util.TreeMap"); LibBili["s"].implementation = function (map) { var obj = Java.cast(map, TreeMap); console.log("map=", obj.toString()); // console.log(`LibBili.s is called: sortedMap=${sortedMap}`); let result = this["s"](map); console.log(`LibBili.s result=${result}`); return result; }; }) }结果如下:入参: {actual_played_time=0, aid=887018547, appkey=1d8b6e7d45233436, auto_play=0, build=6240300, c_locale=zh-Hans_CN, channel=xxl_gdt_wm_253, cid=309627230, epid=0, epid_status=, from=2, from_spmid=main.ugc-video-detail.0.0, last_play_progress_time=0, list_play_time=0, max_play_progress_time=0, mid=0, miniplayer_play_time=0, mobi_app=android, network_type=1, paused_time=0, platform=android, play_status=0, play_type=1, played_time=0, quality=32, s_locale=zh-Hans_CN, session=109275c491a0bdaa56ebda098ca39202f733dc53, sid=0, spmid=main.ugc-video-detail.0.0, start_ts=0, statistics={"appId":1,"platform":3,"version":"6.24.0","abtest":""}, sub_type=0, total_time=0, type=3, user_status=0, video_duration=111} 返回值: 63cb021860cbb7c5fa9c6c838e9b3937 这里的返回值有拼接入参接下来就可以去看so了,它不是静态注册,所以我们去看jnionload;这里会有一些问题,很明显看到我们c里面的函数并不是sub开头,而且跳转过去也没法反编译,这里应该是识别的问题,这里先跳过去,然后按下P键强制识别为函数,就可以反编译了;分析一下伪代码;int __fastcall sub_1C96(JNIEnv *a1, int a2, int a3) { const char *v5; // r1 const char *v6; // r2 int v8; // r0 JNIEnv v9; // r1 void *v10; // r0 void *v11; // r8 int v12; // r9 jobject v13; // r4 jstring v14; // r5 const char *v15; // r0 int v16; // r5 _DWORD *v17; // r9 int v18; // r10 _DWORD *v19; // r0 int v20; // r1 int v21; // r2 int v22; // r0 size_t v23; // r5 int i; // r4 char *v25; // r5 int j; // r4 jobject v27; // [sp+8h] [bp-D0h] char *v28; // [sp+Ch] [bp-CCh] char *v29; // [sp+10h] [bp-C8h] int v30; // [sp+14h] [bp-C4h] char s[40]; // [sp+18h] [bp-C0h] BYREF char v32[88]; // [sp+40h] [bp-98h] BYREF char v33[24]; // [sp+98h] [bp-40h] BYREF if ( sub_2E34(a1) ) { v5 = "java/lang/ClassNotFoundException"; v6 = "com.bilibili.nativelibrary.SignedQuery"; LABEL_3: sub_4414(a1, v5, v6); return 0; } if ( !a3 ) { v5 = "java/lang/NullPointerException"; v6 = "Null params!"; goto LABEL_3; } v8 = sub_6680(a1, a3); v9 = *a1; if ( v8 ) return v9->NewObject(a1, dword_B0FC, dword_B100, 0, 0); v30 = v9->NewStringUTF(a1, "appkey"); v10 = sub_64C4(a1, a3); v11 = v10; if ( v10 ) { v29 = (*a1)->GetStringUTFChars(a1, v10, 0); v12 = sub_34B8(v29); } else { v12 = -1; v29 = 0; } sub_3414(a1, a3); v13 = (*a1)->CallStaticObjectMethod(a1, dword_B0FC, dword_B104, a3); v14 = 0; if ( sub_4368(a1) ) v13 = 0; v15 = (*a1)->GetStringUTFChars(a1, v13, 0); if ( v12 != -1 ) { v28 = v15; v16 = v12; (*a1)->ReleaseStringUTFChars(a1, v11, v29); v17 = malloc(0x10u); if ( v17 ) { v27 = v13; v18 = dword_978C[v16]; v19 = &dword_978C[v16]; v20 = v19[7]; v21 = v19[14]; v22 = v19[21]; *v17 = v18; v17[1] = v20; v17[2] = v21; v17[3] = v22; memset(s, 0, 0x21u); // 初始化内存区域 v23 = strlen(v28); memset(v33, 0, sizeof(v33)); memset(v32, 0, sizeof(v32)); sub_227C(v32); sub_22B0(v32, v28, v23); sprintf(v33, "%08x", v18); sub_22B0(v32, v33, 8); for ( i = 1; i != 4; ++i ) { sprintf(v33, "%08x", v17[i]); sub_22B0(v32, v33, 8); } sub_2AE0(v33, v32); v25 = s; for ( j = 0; j != 16; ++j ) { sprintf(v25, "%02x", v33[j]); v25 += 2; } free(v17); v13 = v27; v14 = (*a1)->NewStringUTF(a1, s); } else { v14 = 0; } } sub_45C8(a1, v30); return (*a1)->NewObject(a1, dword_B0FC, dword_B100, v13, v14); // v14 sign }这里先看这一小段:依靠你灵敏的嗅觉,这里传了一个长度,这是一个很重要的信号,传长度又传了本身,那它就极有可能是在做加密,那么这个v28就应该是明文,我们去看看是不是;a3就是传进来的第三个参数,而a2没有使用到,那么a3就肯定是我们的明文,那么现在的目的肯定就是看sub_22B0这个方法;在这里结合一下之前的想法,他可能是一个md5,这是我们猜想的,那么来看一下md5的参与到加密的函数;MD5Update很符合我们这个sub_22B0吧,进去看看;这里我根据算法修改了一下名字,看起来会舒适一点,这里没有看到熟悉的64轮和k表,但有一个sub_2334函数,点进去看一眼;这个不用多说了吧,很明显的特征了,那么再回到最开始的位置,sub_227C就一定是init了吧,进去也一定能看到更加熟悉的东西;这几个数字是md5最显著的特征,这里我右键转成了16进制,更加明显;这里是可以魔改的,这也是md5魔改的最多的地方,其次就是k表里面,不熟悉的可以google一下这几个数字,也是会找到答案的;那么我们静态分析大概就是这些,接下来去hook看看情况;function hook_so() { Java.perform(function () { var libbili = Module.findBaseAddress("libbili.so"); var s_func = libbili.add(0x22b0 + 1); console.log(s_func); Interceptor.attach(s_func, { onEnter: function (args) { console.log("执行update长度是:", args[2].toInt32()); console.log(args[1].readUtf8String()) }, onLeave: function (args) { console.log("=======================结束==================="); } }); }); } hook_so()hook的结果如下:执行update长度是: 742 actual_played_time=1&aid=113561099573476&appkey=1d8b6e7d45233436&auto_play=0&build=6240300&c_locale=zh-Hans_CN&channel=xxl_gdt_wm_253&cid=27080001673&epid=0&epid_status=&from=2&from_spmid=main.ugc-video-detail.0.0&last_play_progress_time=1&list_play_time=0&max_play_progress_time=1&mid=0&miniplayer_play_time=0&mobi_app=android&network_type=1&paused_time=609&platform=android&play_status=0&play_type=1&played_time=1&quality=32&s_locale=zh-Hans_CN&session=e70b3e4d0b56bda8b4dc3875d760b4ad8ba1e6af&sid=0&spmid=main.ugc-video-detail.0.0&start_ts=1735219209&statistics=%7B%22appId%22%3A1%2C%22platform%22%3A3%2C%22version%22%3A%226.24.0%22%2C%22abtest%22%3A%22%22%7D&sub_type=0&total_time=610&ts=1735219844&type=3&user_status=0&video_duration=321 =======================结束=================== 执行update长度是: 8 560c52cc =======================结束=================== 执行update长度是: 8 d288fed0 =======================结束=================== 执行update长度是: 8 45859ed1 =======================结束=================== 执行update长度是: 8 8bffd973 =======================结束===================在明文填充之后,又增加了四次update,这里熟悉的肯定就知道了,这是加盐的过程,我们去测试一下看是不是标准的md5;运气比较好,它是标准的,如果魔改了还需要去进一步的分析,那么这里的sign也就分析完成了;5. 总结总体来说难度很低,但我非常建议新手朋友从头到尾来一遍,不管是hook还是主动调用,又或者是跟java层的动向,这篇解决了,那java层就没什么难度了;
2025年01月06日
28 阅读
0 评论
0 点赞
2025-01-03
安卓逆向-md5算法
一、md51. C实现md51.1 大致流程由三个函数来完成md5的加密操作;分别是MD5Init、MD5Update、MD5Final;void MD5Init (MD5_CTX *mdContext); void MD5Update (MD5_CTX *mdContext, char *inBuf, unsigned int inLen); void MD5Final (MD5_CTX *mdContext); void Transform (UINT4 *buf, UINT4 *in);MD5Init进行初始化,传入的参数是一个结构体;MD5Update第一个参数依然是结构体,第二个参数是明文,第三个参数是明文的长度;MD5Final第一个参数为结构体,第二个参数就是返回值;注:一个字节=8位(8个二进制位)1Byte=8bit;一个十六进制=4个二进制位。一个字节=2个十六进制。1.2 明文处理首先对明文进行Hex编码;xiaojianbang ==> 78 69 61 6f 6a 69 61 6e 62 61 6e 67注:Hex编码(也称为十六进制编码)是一种将数据转换为十六进制表示形式的编码方法。与Base64编码不同,Hex编码使用16个字符来表示数据,这16个字符分别是0-9的数字和A-F的字母(不区分大小写,即a-f和A-F是等价的)。填充把明文填充到448bit先填充一个1,后面跟对应个数的0附加消息长度用64bit表示消息长度 也就是明文长度00 00 00 00 00 00 00 60 转小端序 60 00 00 00 00 00 00 00如果内容过长,64个比特放不下。就取低64bit。所以MD5输入长度可以无限大,SHA3算法也是无限大,其他哈希算法不是;明文的结果:78 69 61 6f 6a 69 61 6e 62 61 6e 67 80 ...... 60 00 00 00 00 00 00 00 明文 填充 明文长度MD5输入数据无限大,不可能一起处理,需要分组 MD5分组长度为512bit,数据需要处理到512的倍数,因此需要填充 填充位数为1-512bit,如果明文长度刚好448bit,那么就填充512bit MD5算法使用的是小端字节序把处理后的明文分成16块 M1-M16,用于后续计算;1.3 初始化常量MD5的初始化常量一共有四个:A: 01 23 45 67 B: 89 ab cd ef C: fe dc ba 98 D: 76 54 32 10上面说了md5是小端序,所以输入的值不可能是01234567,所以需要传的值是:四个初始化常量: A: 0x67452301; B: 0xEFCDAB89; C: 0x98BADCFE; D: 0x10325476;常量是固定的,如果修改了那就不是一个标准算法,也就是魔改了,这也是魔改md5最多的位置。1.4 MD5Transform正式计算;流程图如下:关于流程解释如下: MD5总共64轮,每一轮都会把旧的D直接给新的A,旧的B直接给新的C,旧的C直接给新的D,也就是每一轮只计算一个新的B 图中的田代表相加 图中的F函数并不是一个函数,而是由四个函数组成 K表里面的值由公式计算的,但体现在代码中一般都是常量 大多数都是直接给的常量值 <<<s代表循环左移 最后把四个初始化常量不断变化后的值,拼接得到最终的摘要结果通过对上图的理解,每一轮的流程为:A + F(B、C、D) + Mi + Ki 循环左移 +BF是一个函数,但并不是固定的一个,而是四个,每16轮一个函数;在update中,并未直接进行计算,如果传入的参数大于64个字节,直接就去计算,如果没有超过则只进行拷贝,后续再进行填充;1.5 源码实现main.cpp#include <iostream> #include <memory.h> #include <string.h> #include "MD5.h" using namespace std; unsigned char PADDING[] = {0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; void MD5Init(MD5_CTX *context) { context->count[0] = 0; context->count[1] = 0; context->state[0] = 0x67452301; context->state[1] = 0xEFCDAB89; context->state[2] = 0x98BADCFE; context->state[3] = 0x10325476; } void MD5Update(MD5_CTX *context, unsigned char *input, unsigned int inputlen) { unsigned int i = 0, index = 0, partlen = 0; index = (context->count[0] >> 3) & 0x3F; partlen = 64 - index; context->count[0] += inputlen << 3; if (context->count[0] < (inputlen << 3)) context->count[1]++; context->count[1] += inputlen >> 29; if (inputlen >= partlen) { memcpy(&context->buffer[index], input, partlen); MD5Transform(context->state, context->buffer); for (i = partlen; i + 64 <= inputlen; i += 64) MD5Transform(context->state, &input[i]); index = 0; } else { i = 0; } memcpy(&context->buffer[index], &input[i], inputlen - i); } void MD5Final(MD5_CTX *context, unsigned char digest[16]) { unsigned int index = 0, padlen = 0; unsigned char bits[8]; index = (context->count[0] >> 3) & 0x3F; padlen = (index < 56) ? (56 - index) : (120 - index); MD5Encode(bits, context->count, 8); MD5Update(context, PADDING, padlen); MD5Update(context, bits, 8); printf("%.8x\n", context->state[0]); printf("%.8x\n", context->state[1]); printf("%.8x\n", context->state[2]); printf("%.8x\n", context->state[3]); MD5Encode(digest, context->state, 16); } // 大端序到小端序 void MD5Encode(unsigned char *output, unsigned int *input, unsigned int len) { unsigned int i = 0, j = 0; while (j < len) { output[j] = input[i] & 0xFF; output[j + 1] = (input[i] >> 8) & 0xFF; output[j + 2] = (input[i] >> 16) & 0xFF; output[j + 3] = (input[i] >> 24) & 0xFF; i++; j += 4; } } //分组 分成m1-16 void MD5Decode(unsigned int *output, unsigned char *input, unsigned int len) { unsigned int i = 0, j = 0; while (j < len) { output[i] = (input[j]) | (input[j + 1] << 8) | (input[j + 2] << 16) | (input[j + 3] << 24); i++; j += 4; } } void MD5Transform(unsigned int state[4], unsigned char block[64]) { unsigned int a = state[0]; unsigned int b = state[1]; unsigned int c = state[2]; unsigned int d = state[3]; unsigned int x[64]; MD5Decode(x, block, 64); FF(a, b, c, d, x[0], 7, 0xd76aa478); /* 1 */ FF(d, a, b, c, x[1], 12, 0xe8c7b756); /* 2 */ FF(c, d, a, b, x[2], 17, 0x242070db); /* 3 */ FF(b, c, d, a, x[3], 22, 0xc1bdceee); /* 4 */ FF(a, b, c, d, x[4], 7, 0xf57c0faf); /* 5 */ FF(d, a, b, c, x[5], 12, 0x4787c62a); /* 6 */ FF(c, d, a, b, x[6], 17, 0xa8304613); /* 7 */ FF(b, c, d, a, x[7], 22, 0xfd469501); /* 8 */ FF(a, b, c, d, x[8], 7, 0x698098d8); /* 9 */ FF(d, a, b, c, x[9], 12, 0x8b44f7af); /* 10 */ FF(c, d, a, b, x[10], 17, 0xffff5bb1); /* 11 */ FF(b, c, d, a, x[11], 22, 0x895cd7be); /* 12 */ FF(a, b, c, d, x[12], 7, 0x6b901122); /* 13 */ FF(d, a, b, c, x[13], 12, 0xfd987193); /* 14 */ FF(c, d, a, b, x[14], 17, 0xa679438e); /* 15 */ FF(b, c, d, a, x[15], 22, 0x49b40821); /* 16 */ /* Round 2 */ GG(a, b, c, d, x[1], 5, 0xf61e2562); /* 17 */ GG(d, a, b, c, x[6], 9, 0xc040b340); /* 18 */ GG(c, d, a, b, x[11], 14, 0x265e5a51); /* 19 */ GG(b, c, d, a, x[0], 20, 0xe9b6c7aa); /* 20 */ GG(a, b, c, d, x[5], 5, 0xd62f105d); /* 21 */ GG(d, a, b, c, x[10], 9, 0x2441453); /* 22 */ GG(c, d, a, b, x[15], 14, 0xd8a1e681); /* 23 */ GG(b, c, d, a, x[4], 20, 0xe7d3fbc8); /* 24 */ GG(a, b, c, d, x[9], 5, 0x21e1cde6); /* 25 */ GG(d, a, b, c, x[14], 9, 0xc33707d6); /* 26 */ GG(c, d, a, b, x[3], 14, 0xf4d50d87); /* 27 */ GG(b, c, d, a, x[8], 20, 0x455a14ed); /* 28 */ GG(a, b, c, d, x[13], 5, 0xa9e3e905); /* 29 */ GG(d, a, b, c, x[2], 9, 0xfcefa3f8); /* 30 */ GG(c, d, a, b, x[7], 14, 0x676f02d9); /* 31 */ GG(b, c, d, a, x[12], 20, 0x8d2a4c8a); /* 32 */ /* Round 3 */ HH(a, b, c, d, x[5], 4, 0xfffa3942); /* 33 */ HH(d, a, b, c, x[8], 11, 0x8771f681); /* 34 */ HH(c, d, a, b, x[11], 16, 0x6d9d6122); /* 35 */ HH(b, c, d, a, x[14], 23, 0xfde5380c); /* 36 */ HH(a, b, c, d, x[1], 4, 0xa4beea44); /* 37 */ HH(d, a, b, c, x[4], 11, 0x4bdecfa9); /* 38 */ HH(c, d, a, b, x[7], 16, 0xf6bb4b60); /* 39 */ HH(b, c, d, a, x[10], 23, 0xbebfbc70); /* 40 */ HH(a, b, c, d, x[13], 4, 0x289b7ec6); /* 41 */ HH(d, a, b, c, x[0], 11, 0xeaa127fa); /* 42 */ HH(c, d, a, b, x[3], 16, 0xd4ef3085); /* 43 */ HH(b, c, d, a, x[6], 23, 0x4881d05); /* 44 */ HH(a, b, c, d, x[9], 4, 0xd9d4d039); /* 45 */ HH(d, a, b, c, x[12], 11, 0xe6db99e5); /* 46 */ HH(c, d, a, b, x[15], 16, 0x1fa27cf8); /* 47 */ HH(b, c, d, a, x[2], 23, 0xc4ac5665); /* 48 */ /* Round 4 */ II(a, b, c, d, x[0], 6, 0xf4292244); /* 49 */ II(d, a, b, c, x[7], 10, 0x432aff97); /* 50 */ II(c, d, a, b, x[14], 15, 0xab9423a7); /* 51 */ II(b, c, d, a, x[5], 21, 0xfc93a039); /* 52 */ II(a, b, c, d, x[12], 6, 0x655b59c3); /* 53 */ II(d, a, b, c, x[3], 10, 0x8f0ccc92); /* 54 */ II(c, d, a, b, x[10], 15, 0xffeff47d); /* 55 */ II(b, c, d, a, x[1], 21, 0x85845dd1); /* 56 */ II(a, b, c, d, x[8], 6, 0x6fa87e4f); /* 57 */ II(d, a, b, c, x[15], 10, 0xfe2ce6e0); /* 58 */ II(c, d, a, b, x[6], 15, 0xa3014314); /* 59 */ II(b, c, d, a, x[13], 21, 0x4e0811a1); /* 60 */ II(a, b, c, d, x[4], 6, 0xf7537e82); /* 61 */ II(d, a, b, c, x[11], 10, 0xbd3af235); /* 62 */ II(c, d, a, b, x[2], 15, 0x2ad7d2bb); /* 63 */ II(b, c, d, a, x[9], 21, 0xeb86d391); /* 64 */ state[0] += a; state[1] += b; state[2] += c; state[3] += d; } int main(){ // void MD5Init(MD5_CTX *context); MD5_CTX context; MD5Init(&context); // void MD5Update(MD5_CTX *context,unsigned char *input,unsigned int inputlen); unsigned char* plainText = (unsigned char *) "xiaojianbang"; MD5Update(&context, plainText, strlen(reinterpret_cast<const char *>(plainText))); // void MD5Final(MD5_CTX *context,unsigned char digest[16]); unsigned char result[16]; MD5Final(&context, result); char temp[2] = {0}; char finalResult[33] = {0}; for(int i = 0; i < 16; i++){ int index = i; sprintf(temp, "%.2x", result[index]); strcat(finalResult, temp); } cout << finalResult << endl; return 0; }md5.h#ifndef HOOKDEMO_MD5_H #define HOOKDEMO_MD5_H typedef struct { unsigned int count[2]; unsigned int state[4]; unsigned char buffer[64]; } MD5_CTX; #define F(x,y,z) ((x & y) | (~x & z)) #define G(x,y,z) ((x & z) | (y & ~z)) #define H(x,y,z) (x^y^z) #define I(x,y,z) (y ^ (x | ~z)) #define ROTATE_LEFT(x,n) ((x << n) | (x >> (32-n))) #define FF(a,b,c,d,x,s,ac) \ { \ a += F(b,c,d) + x + ac; \ a = ROTATE_LEFT(a,s); \ a += b; \ } #define GG(a,b,c,d,x,s,ac) \ { \ a += G(b,c,d) + x + ac; \ a = ROTATE_LEFT(a,s); \ a += b; \ } #define HH(a,b,c,d,x,s,ac) \ { \ a += H(b,c,d) + x + ac; \ a = ROTATE_LEFT(a,s); \ a += b; \ } #define II(a,b,c,d,x,s,ac) \ { \ a += I(b,c,d) + x + ac; \ a = ROTATE_LEFT(a,s); \ a += b; \ } void MD5Init(MD5_CTX *context); void MD5Update(MD5_CTX *context,unsigned char *input,unsigned int inputlen); void MD5Final(MD5_CTX *context,unsigned char digest[16]); void MD5Transform(unsigned int state[4],unsigned char block[64]); void MD5Encode(unsigned char *output,unsigned int *input,unsigned int len); void MD5Decode(unsigned int *output,unsigned char *input,unsigned int len); #endif //HOOKDEMO_MD5_H2. IDA中的md52.1 md5init进入md5init函数,可以看到在上面提到的初始化常量;可以发现和我们上面说的是不太一样的,这里是因为默认显示的是十进制,选中数字按下h即可转成16进制,或者右键选择Hexadecimal转换;2.2 md5transform接着进到md5transform函数中可以发现k表有些值转换后和我们上面的值不太一样,可能是有无符号的问题,右键选择invert sign,转换一下就可以了;这里其实是f5插件识别的问题,在汇编中其实是正确的;__ROR4__代表的是循环右移,ROL则是循环左移,那么在上面提到了运算是循环左移,这里为何是右移,其实在这里是一样的,对于32位的数据来说,循环右移25位就等于循环左移7位;2.3 md5final函数参数处理技巧伪C/C++没有识别出函数参数的处理 rename lvar 修改变量名 set lvar type 修改变量类型 set item type 修改函数类型 force call type 重新识别函数
2025年01月03日
7 阅读
0 评论
0 点赞
2025-01-02
安卓逆向-jnitrace基本使用
一、jni-trace1. 前置jnitrace 是一个基于 Frida 的工具,用于跟踪 Android 应用程序中 JNI API 的使用;JNI 允许 Java 代码与 C/C++ 代码进行交互,而 jni-trace 通过动态分析,可以追踪这些交互过程中的 JNI 函数调用;它会记录并显示每一个 JNI 函数的调用,包括方法签名、参数、返回值等详细信息;项目地址:https://github.com/chame1eon/jnitrace安装命令:pip install jnitrace注意:在使用 jnitrace 之前需要先启动 frida-server各方面版本信息如下:frida 16.5.7 frida-tools 13.6.0 hexdump 3.3 jnitrace 3.3.12. 使用方式注意:在使用 jnitrace 之前需要先启动 frida-server;2.1 跟踪 JNI 函数调用执行下面命令附加到包名为 com.cyrus.exammple 的应用程序中,并开始跟踪 libnative-lib.so 中所有的 JNI 调用; jnitrace -l libnative-lib.so com.cyrus.exammpletrace的结果示例如下:Tracing. Press any key to quit... Traced library "libnative-lib.so" loaded from path "/data/app/com.cyrus.example-Jh9YgSVDqZ5bKxgv0f1E6w==/base.apk!/lib/arm64-v8a". /* TID 13912 */ 5973 ms [+] JNIEnv->NewStringUTF 5973 ms |- JNIEnv* : 0x7292c3c180 5973 ms |- char* : 0x7fc2a5b5d1 5973 ms |: Hello From Native 5973 ms |= jstring : 0x71 { Hello From Native } 5973 ms ------------------------------------Backtrace------------------------------------ 5973 ms |-> 0x719d3b1080: libnative-lib.so!0x25080 (libnative-lib.so:0x719d38c000) Stopping application (name=com.cyrus.example, pid=13912)...stopped.2.2 跟踪特定的 JNI 函数如果只想跟踪特定的 JNI 函数,例如 RegisterNatives,可以通过以下方式实现;jnitrace -l libGameVMP.so -i RegisterNatives com.shizhuang.duapp这个命令会仅跟踪 RegisterNatives 调用,从而帮助找到动态注册的 native 方法;Tracing. Press any key to quit... Traced library "libGameVMP.so" loaded from path "/data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64". /* TID 26932 */ 478 ms [+] JNIEnv->RegisterNatives 478 ms |- JNIEnv* : 0x757bdfe180 478 ms |- jclass : 0xe9 { lte/NCall } 478 ms |- JNINativeMethod* : 0x7fc96e3128 478 ms |: 0x747e51293c - dI(I)I 478 ms |- jint : 1 478 ms |= jint : 0 478 ms --------------------------------Backtrace-------------------------------- 478 ms |-> 0x747e50d998: libGameVMP.so!0x6998 (libGameVMP.so:0x747e507000) /* TID 26932 */ 493 ms [+] JNIEnv->RegisterNatives 493 ms |- JNIEnv* : 0x757bdfe180 493 ms |- jclass : 0xe9 { lte/NCall } 493 ms |- JNINativeMethod* : 0x7fc96e3140 493 ms |: 0x747e512aec - dS(Ljava/lang/String;)Ljava/lang/String; 493 ms |- jint : 1 493 ms |= jint : 0 493 ms --------------------------------Backtrace-------------------------------- 493 ms |-> 0x747e50d998: libGameVMP.so!0x6998 (libGameVMP.so:0x747e507000) /* TID 26932 */ 508 ms [+] JNIEnv->RegisterNatives 508 ms |- JNIEnv* : 0x757bdfe180 508 ms |- jclass : 0xe9 { lte/NCall } 508 ms |- JNINativeMethod* : 0x7fc96e3158 508 ms |: 0x747e512ad0 - dL(J)J 508 ms |- jint : 1 508 ms |= jint : 0 508 ms --------------------------------Backtrace-------------------------------- 508 ms |-> 0x747e50d998: libGameVMP.so!0x6998 (libGameVMP.so:0x747e507000) /* TID 26932 */ 522 ms [+] JNIEnv->RegisterNatives 522 ms |- JNIEnv* : 0x757bdfe180 522 ms |- jclass : 0xe9 { lte/NCall } 522 ms |- JNINativeMethod* : 0x7fc96e3170 522 ms |: 0x747e514fa8 - IV([Ljava/lang/Object;)V 522 ms |- jint : 1 522 ms |= jint : 0 522 ms --------------------------------Backtrace-------------------------------- 522 ms |-> 0x747e50d998: libGameVMP.so!0x6998 (libGameVMP.so:0x747e507000) /* TID 26932 */ 537 ms [+] JNIEnv->RegisterNatives 537 ms |- JNIEnv* : 0x757bdfe180 537 ms |- jclass : 0xe9 { lte/NCall } 537 ms |- JNINativeMethod* : 0x7fc96e3188 537 ms |: 0x747e514fa8 - IZ([Ljava/lang/Object;)Z 537 ms |- jint : 1 537 ms |= jint : 0 537 ms --------------------------------Backtrace-------------------------------- 537 ms |-> 0x747e50d998: libGameVMP.so!0x6998 (libGameVMP.so:0x747e507000) /* TID 26932 */ 609 ms [+] JNIEnv->RegisterNatives 609 ms |- JNIEnv* : 0x757bdfe180 609 ms |- jclass : 0xe9 { lte/NCall } 609 ms |- JNINativeMethod* : 0x7fc96e3200 609 ms |: 0x747e514fe8 - IF([Ljava/lang/Object;)F 609 ms |- jint : 1 609 ms |= jint : 0 609 ms --------------------------------Backtrace-------------------------------- 609 ms |-> 0x747e50d998: libGameVMP.so!0x6998 (libGameVMP.so:0x747e507000) /* TID 26932 */ 623 ms [+] JNIEnv->RegisterNatives 623 ms |- JNIEnv* : 0x757bdfe180 623 ms |- jclass : 0xe9 { lte/NCall } 623 ms |- JNINativeMethod* : 0x7fc96e3218 623 ms |: 0x747e514fa8 - IJ([Ljava/lang/Object;)J 623 ms |- jint : 1 623 ms |= jint : 0 623 ms --------------------------------Backtrace-------------------------------- 623 ms |-> 0x747e50d998: libGameVMP.so!0x6998 (libGameVMP.so:0x747e507000) /* TID 26932 */ 637 ms [+] JNIEnv->RegisterNatives 637 ms |- JNIEnv* : 0x757bdfe180 637 ms |- jclass : 0xe9 { lte/NCall } 637 ms |- JNINativeMethod* : 0x7fc96e3230 637 ms |: 0x747e515028 - ID([Ljava/lang/Object;)D 637 ms |- jint : 1 637 ms |= jint : 0 637 ms --------------------------------Backtrace-------------------------------- 637 ms |-> 0x747e50d998: libGameVMP.so!0x6998 (libGameVMP.so:0x747e507000) /* TID 26932 */ 652 ms [+] JNIEnv->RegisterNatives 652 ms |- JNIEnv* : 0x757bdfe180 652 ms |- jclass : 0xe9 { lte/NCall } 652 ms |- JNINativeMethod* : 0x7fc96e3248 652 ms |: 0x747e514fa8 - IL([Ljava/lang/Object;)Ljava/lang/Object; 652 ms |- jint : 1 652 ms |= jint : 0 652 ms --------------------------------Backtrace-------------------------------- 652 ms |-> 0x747e50d998: libGameVMP.so!0x6998 (libGameVMP.so:0x747e507000)2.3 附加到正在运行的应用通过 -m <spawn|attach> 指定 Frida 附加方式,默认为 spawn;执行下面命令通过 -m attach 附加到正在运行的应用上;jnitrace -m attach -l libnative-lib.so com.cyrus.example3. 详细参数说明参数说明:-l :指定要追踪的库,可以使用多次,例如 -l libnative-lib.so -l libanother-lib.so,或使用 -l * 追踪所有库:目标应用的包名,必须已安装在设备上;可选参数:-R ::指定远程 Frida server 的位置(默认是 localhost:27042);-m <spawn|attach>:指定 Frida 附加方式,默认为 spawn,推荐使用;-b <fuzzy|accurate|none>:控制追踪输出的堆栈信息,默认 accurate,可以选择 fuzzy 或 none 禁用;-i :指定方法名称的正则表达式进行追踪,可以多次使用,如 -i Get -i RegisterNatives;-e :排除指定方法名称的正则表达式,如 -e ^Find -e GetEnv;-I :指定库中的导出方法进行追踪,适用于只追踪部分导出方法,如 -I stringFromJNI;-E :排除指定导出方法的追踪,如 -E JNI_OnLoad -E nativeMethod;-o <path/output.json>:指定追踪数据保存路径,以 JSON 格式存储;-p <path/to/script.js>:加载自定义 Frida 脚本,jnitrace 加载前执行,用于绕过反调试;-a <path/to/script.js>:加载自定义 Frida 脚本,jnitrace 加载后执行;--hide-data:减少输出,隐藏 hexdump 和字符串引用;--ignore-env:隐藏通过 JNIEnv 的调用;--ignore-vm:隐藏通过 JavaVM 的调用;--aux <name=(string|bool|int)value>:传递自定义参数,例如 --aux='uid=(int)10';4. 输出说明Traced library "libGameVMP.so" loaded from path "/data/app/com.xxx.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64". /* TID 9541 */ # 表示线程 ID(TID)为 9541,当前的 JNI 调用是在这个线程上执行的。 613 ms [+] JNIEnv->RegisterNatives # 表示在 613 ms 时间点调用了 JNIEnv->RegisterNatives 方法 613 ms |- JNIEnv* : 0x757bdfe180 # 参数,JNIEnv 指针,0x757bdfe180 是该指针的内存地址。 613 ms |- jclass : 0xe9 { lte/NCall } # 参数,0xe9 为传递的 Java 类对象的句柄 613 ms |- JNINativeMethod* : 0x7fc96e3128 # 参数,指向 JNINativeMethod 数组的指针 0x7fc96e3128,这个数组定义了要注册的本地方法。 613 ms |: 0x747e54d93c - dI(I)I # 这是 JNINativeMethod 数组中的一项数据细节。0x747e54d93c 是本地函数的地址,dI(I)I 是方法的签名 613 ms |- jint : 1 # 参数,表示要注册的本地方法数量 613 ms |= jint : 0 # RegisterNatives 方法的返回值 613 ms --------------------------------Backtrace-------------------------------- 613 ms |-> 0x747e548998: libGameVMP.so!0x6998 (libGameVMP.so:0x747e542000) # 这是调用 RegisterNatives 方法时的调用堆栈信息。 # 0x747e548998 是调用地址。 # libGameVMP.so!0x6998 表示 libGameVMP.so 库中的偏移地址 0x6998。 # (libGameVMP.so:0x747e542000) 指示库的基地址 0x747e542000。
2025年01月02日
10 阅读
1 评论
0 点赞
1
2