安卓逆向 — 某对app算法分析
快对算法分析
1. 概述
版本:6.29.0
包名:com.kuaiduizuoye.scan
加固:未加固
-
此样本未加固,接口是登录接口,抓包应该也没什么检测;frida的话不能attath,不知道问题所在;
-
同样,此样本需要了解des算法流程,默认你有;
-
整体样本不算特别复杂,但分析过程绝对值得一看;是一个综合性非常强的样本,会涉及很多unidbg相关的使用,以及简单的魔改des算法;
-
此文章用作技术交流,侵权可下架;中间若有错别字请见谅;
-
若是需要我附上样本可评论,我会贴出;
2. 抓包定位
-
目标是登录,感觉重要的参数大概是这些:
复制代码 隐藏代码data GicW//OsDspTHVDzbe8DYNOGZZ9/W/fyY767oimlgc6BsBQlpY90kzUPY2RNostXg4XTuWNY+QYDcuid E0DA888804E90201780A9C0D17D0B050|0token 1_XPXQH3c5HRPtFHkSwi3sCCURmT25QfxMphoneDevice orioleadid 50e3e6225e071bb7a2bbd4c38faa336d23f51a3bdid 042a7996f140000253909500400000bfsign b90be668c7209699eefcfda24d97e6c1_t_ 1763087460

-
主要是sign参数,定位依然采用搜索大法;这里尝试搜索sign”、”sign、”sign”、=sign、sign=,最终 sign= 有比较明显的字眼;

-
直接去看最后一个,这里定位也只是经验之谈,往往这种朴素的方法在普通样本是最适用的;往里再跟一步就可以跟到native的位置;

-
hook一下看看位置对不对;
复制代码 隐藏代码functionhook_java() {Java.perform(function () {letNativeHelper = Java.use("com.zuoyebang.baseutil.NativeHelper");NativeHelper["nativeGetSign"].implementation = function (str) {console.log(`NativeHelper.nativeGetSign is called: str=${str}`);let result = this["nativeGetSign"](str);console.log(`NativeHelper.nativeGetSign result=${result}`);return result; }; })}hook_java()
-
位置没错,打印结果如下:
复制代码 隐藏代码[Pixel 6::com.kuaiduizuoye.scan ]-> NativeHelper.nativeGetSign is called: str=X3RfPTE3NjMwODg2NjZhYmlzPTFhZGlkPTUwZTNlNjIyNWUwNzFiYjdhMmJiZDRjMzhmYWEzMzZkMjNmNTFhM2JhcHBCaXQ9MzJhcHBJZD1zY2FuY29kZWFyZWE9YnJhbmQ9Z29vZ2xlY2hhbm5lbD10YW9iYW96aHVzaG91Y2l0eT1jdWlkPUUwREE4ODg4MDRFOTAyMDE3ODBBOUMwRDE3RDBCMDUwfDBkYXRhPUdpY1cvL09zRHNwVEhWRHpiZThEWU5PR1paOS9XL2Z5WTc2N29pbWxnYzZCc0JRbHBZOTBrelVQWTJSTm9zdFhnNFhUdVdOWStRWURkZXZpY2U9UGl4ZWwgNmRpZD0wNDJhNzk5NmYxNDAwMDAyNTM5MDk1MDA0MDAwMDBiZmtha29ycmhhcGhpb3Bob2JpYT0zMTg4MzA5MDVudD13aWZpb3BlcmF0b3JpZD1vcz1hbmRyb2lkb3NWZXJzaW9uPTEycGhvbmVEZXZpY2U9b3Jpb2xlcGtnTmFtZT1jb20ua3VhaWR1aXp1b3llLnNjYW5wcm92aW5jZT1zZGs9MzJ0b2tlbj0xX1hQWFFIM2M1SFJQdEZIa1N3aTNzQ0NVUm1UMjVRZnhNdmM9OTgwdmNuYW1lPTYuMjkuMA==NativeHelper.nativeGetSign result=003030578ba3bb549a4fe86793664f92

-
和抓包结果也对上了,后续就开始分析了;so的名字在上一层也可以看到:libbaseutil.so;
3. 算法分析
-
在看算法之前先看看参数,从上一层可以看出来是做了base64的,解一下;
复制代码 隐藏代码_t_=1763088666abis=1adid=50e3e6225e071bb7a2bbd4c38faa336d23f51a3bappBit=32appId=scancodearea=brand=googlechannel=taobaozhushoucity=cuid=E0DA888804E90201780A9C0D17D0B050|0data=GicW//OsDspTHVDzbe8DYNOGZZ9/W/fyY767oimlgc6BsBQlpY90kzUPY2RNostXg4XTuWNY+QYDdevice=Pixel 6did=042a7996f140000253909500400000bfkakorrhaphiophobia=318830905nt=wifioperatorid=os=androidosVersion=12phoneDevice=oriolepkgName=com.kuaiduizuoye.scanprovince=sdk=32token=1_XPXQH3c5HRPtFHkSwi3sCCURmT25QfxMvc=980vcname=6.29.0
-
实际上就是请求体的其他参数,做了排序编码一下传过来;试了一下果然不是标准的md5;
3.1 unidbg模拟执行
-
这里还是使用unidbg来辅助算法还原,先搭一个基本的框架;这里样本只提供了32位的so;
复制代码 隐藏代码publicclasskuaiduiextendsAbstractJniimplementsIOResolver {privatefinal AndroidEmulator emulator;privatefinal VM vm;privatefinal Module module;@Overridepublic FileResult resolve(Emulator emulator, String pathname, int oflags) { System.out.println("pathName:" + pathname);returnnull; }publickuaidui() { emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.kuaiduizuoye.scan").build();Memorymemory= emulator.getMemory(); memory.setLibraryResolver(newAndroidResolver(23)); vm = emulator.createDalvikVM(newFile("src/test/java/com/Samples/KuaiDui/file/快对v6.29.0.apk")); emulator.getSyscallHandler().addIOResolver(this); emulator.getSyscallHandler().setEnableThreadDispatcher(true); vm.setVerbose(true); vm.setJni(this);DalvikModuledm= vm.loadLibrary(newFile("src/test/java/com/Samples/KuaiDui/file/libbaseutil.so"), true);module = dm.getModule(); dm.callJNI_OnLoad(emulator); }publicstaticvoidmain(String[] args) {kuaiduidemo=newkuaidui(); }}
-
运行后没有报错,并且输出了对应函数的偏移;

-
留个心眼,nativeInitBaseUtil、nativeSetToken这种函数极有可能是需要我们执行的,也就是初始化函数,先不着急看这些,直接call目标函数;
复制代码 隐藏代码public String call_nativeGetSign(){ List<Object> list = newArrayList<>(10); list.add(vm.getJNIEnv()); list.add(0);StringObjectstr1=newStringObject(vm,"X3RfPTE3NjMwODg2NjZhYmlzPTFhZGlkPTUwZTNlNjIyNWUwNzFiYjdhMmJiZDRjMzhmYWEzMzZkMjNmNTFhM2JhcHBCaXQ9MzJhcHBJZD1zY2FuY29kZWFyZWE9YnJhbmQ9Z29vZ2xlY2hhbm5lbD10YW9iYW96aHVzaG91Y2l0eT1jdWlkPUUwREE4ODg4MDRFOTAyMDE3ODBBOUMwRDE3RDBCMDUwfDBkYXRhPUdpY1cvL09zRHNwVEhWRHpiZThEWU5PR1paOS9XL2Z5WTc2N29pbWxnYzZCc0JRbHBZOTBrelVQWTJSTm9zdFhnNFhUdVdOWStRWURkZXZpY2U9UGl4ZWwgNmRpZD0wNDJhNzk5NmYxNDAwMDAyNTM5MDk1MDA0MDAwMDBiZmtha29ycmhhcGhpb3Bob2JpYT0zMTg4MzA5MDVudD13aWZpb3BlcmF0b3JpZD1vcz1hbmRyb2lkb3NWZXJzaW9uPTEycGhvbmVEZXZpY2U9b3Jpb2xlcGtnTmFtZT1jb20ua3VhaWR1aXp1b3llLnNjYW5wcm92aW5jZT1zZGs9MzJ0b2tlbj0xX1hQWFFIM2M1SFJQdEZIa1N3aTNzQ0NVUm1UMjVRZnhNdmM9OTgwdmNuYW1lPTYuMjkuMA=="); list.add(vm.addLocalObject(str1));Numbernumber=module.callFunction(emulator, 0x1054 + 1, list.toArray());StringObjectstr= vm.getObject(number.intValue());return str.getValue();}
-
这里的参数还是之前那一组,直接执行会出现问题;

-
需要去看一下so里的实现;

-
很久没遇到这种清新的so了,很显然是可能是dword_A044这一块出的错,它应该就是一个初始化相关的东西,我们按下X去查看交叉引用,看看哪里可能是赋值的位置;

-
一共有三个函数位置在调用,但是看了一下,只有sub_DD0有在赋值,那我们就看它;函数内部经过一堆有的没的,最终给dwoed_A404这块内容赋值为1,那就再去看看谁调用了sub_DD0;

-
这两个函数或许不太熟悉,但是对比下面这张图;

-
其实就是另外的两个native函数,我们需要知道谁先执行;我们可以去把这三个函数全部hook上,然后清除数据或者重装apk来看谁先执行;
复制代码 隐藏代码functionhook_java() {Java.perform(function () {letNativeHelper = Java.use("com.zuoyebang.baseutil.NativeHelper");NativeHelper["nativeInitBaseUtil"].implementation = function (context, str) {console.log(`NativeHelper.nativeInitBaseUtil is called: context=${context}, str=${str}`);let result = this["nativeInitBaseUtil"](context, str);console.log(`NativeHelper.nativeInitBaseUtil result=${result}`);return result; };NativeHelper["nativeSetToken"].implementation = function (context, str, str2, str3) {console.log(`NativeHelper.nativeSetToken is called: context=${context}, str=${str}, str2=${str2}, str3=${str3}`);let result = this["nativeSetToken"](context, str, str2, str3);console.log(`NativeHelper.nativeSetToken result=${result}`);return result; };NativeHelper["nativeGetSign"].implementation = function (str) {console.log(`NativeHelper.nativeGetSign is called: str=${str}`);let result = this["nativeGetSign"](str);console.log(`NativeHelper.nativeGetSign result=${result}`);return result; }; })}functionhook_dlopen() {var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");Interceptor.attach(android_dlopen_ext, {onEnter: function (args) {var path_ptr = args[0];var path = ptr(path_ptr).readCString();if (path && path.indexOf("libbaseutil.so") !== -1) {console.log("[android_dlopen_ext:]", path);this.flag = true; } },onLeave: function (retval) {if (this.flag) {constmodule = Process.findModuleByName("libbaseutil.so");let jni_onload = module.findExportByName("JNI_OnLoad")console.log("JNI_OnLoad--->>>" + jni_onload);Interceptor.attach(jni_onload, {onEnter: function (args) {console.log("JNI_OnLoad onEnter") },onLeave: function (retval) {console.log("JNI_OnLoad onLeave")hook_java() } }) } } });}setImmediate(hook_dlopen)
-
这里需要把握好时机的问题,一般来说直接用常规hook没输出的话就要考虑自己hook的时机是不是够早了,不行的话还可以往前找更早的时机;我这里是hook JNI_OnLoad后,时机是比较早的了; 
复制代码 隐藏代码NativeHelper.nativeInitBaseUtil is called: context=com.kuaiduizuoye.scan.base.BaseApplication@90993f0, str=E0DA888804E90201780A9C0D17D0B050|0NativeHelper.nativeInitBaseUtil result=0d040c0d0e0f01050c02040a0f0707040207050e05050a0c0b090d0f090f030f020400030a0f030903070e0708020300070f0f080306060b00000f0d04070d0d02070b0f020a01010c0d030a09080e090b040f02080c0b040c0e0e060c0d02010e0b0c050b02020c07050a06020a0c0e0c030b000e0f0f0907080c0c0e0b0404000d0b0903010f05080c040f0c0f0f060102080a050509040606040a0d0f0f0e0f05050a05000e0001010b020701000eNativeHelper.nativeSetToken is called: context=com.kuaiduizuoye.scan.base.BaseApplication@90993f0, str=E0DA888804E90201780A9C0D17D0B050|0, str2=0d040c0d0e0f01050c02040a0f0707040207050e05050a0c0b090d0f090f030f020400030a0f030903070e0708020300070f0f080306060b00000f0d04070d0d02070b0f020a01010c0d030a09080e090b040f02080c0b040c0e0e060c0d02010e0b0c050b02020c07050a06020a0c0e0c030b000e0f0f0907080c0c0e0b0404000d0b0903010f05080c040f0c0f0f060102080a050509040606040a0d0f0f0e0f05050a05000e0001010b020701000e, str3=0c000a01090d0e04020c0a0e06010b0d050a080e0c0f03050c0a0602060e0c0207090c0a0d090f060a060e08030a0003NativeHelper.nativeSetToken result=true
-
初步观察一下,init函数执行后得到一个值,nativeSetToken用到了它,并且init的参数settoken也用到了,那么实际上只需要执行nativeSetToken就可以跑通了,这里为了固定数据,我这里先只call nativeSetToken函数;
复制代码 隐藏代码publicvoidcall_nativeSetToken(){ List<Object> list = newArrayList<>(10); list.add(vm.getJNIEnv()); list.add(0);StringObjectstr1=newStringObject(vm,"E0DA888804E90201780A9C0D17D0B050|0");StringObjectstr2=newStringObject(vm,"0d040c0d0e0f01050c02040a0f0707040207050e05050a0c0b090d0f090f030f020400030a0f030903070e0708020300070f0f080306060b00000f0d04070d0d02070b0f020a01010c0d030a09080e090b040f02080c0b040c0e0e060c0d02010e0b0c050b02020c07050a06020a0c0e0c030b000e0f0f0907080c0c0e0b0404000d0b0903010f05080c040f0c0f0f060102080a050509040606040a0d0f0f0e0f05050a05000e0001010b020701000e");StringObjectstr3=newStringObject(vm,"0c000a01090d0e04020c0a0e06010b0d050a080e0c0f03050c0a0602060e0c0207090c0a0d090f060a060e08030a0003|0"); DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);// context list.add(vm.addLocalObject(context)); list.add(vm.addLocalObject(str1)); list.add(vm.addLocalObject(str2)); list.add(vm.addLocalObject(str3));Numbernumber=module.callFunction(emulator, 0xe90 + 1, list.toArray());StringObjectstr= vm.getObject(number.intValue());}
-
不需要不环境,直接得到结果:

-
但是call_nativeSetToken的三个参数我们均未知,所以先去看看到底是什么;先看init函数;

-
参数一自然不用多说,参数二实际上是cuid,和抓包上传的也对得上;这里我清除数据发现依旧是这个值,可能与设备有关,这里我不去追究来源了;返回值怎么算的这里先不管,去看nativeSetToken函数;

-
前面说了,它的参数是context、cuid,还有a2、a3,从代码语义上看,a2、a3原先是没有值的,a2就是init函数算出来的结果,后续应该是存在了xml文件里,因为上面在取;a3是plutoAntispam.data;看起来postSync可能是和请求有关,问了问ai说是同步网络POST请求,那我们去搜一下,这里幸好抓包没停;

-
确实找到了,参数还是那一些,另外多说一句,我是重装+清数据过的,但是前面有疑问的adid、token之类的请求体还是固定的,那就证明与设备有关,所以后面不会再提这些参数了,统一当做已知数据; -
现在大概清楚了数据来源,现在暂时停下脚步,看看我们还有什么是未知的; -
一共出现了三个函数,我们称为init、setToken、getsign; -
init有两个参数context、cuid均为已知,结果来源未知,而结果后续用到了,所以需要分析算法,记为init_res; -
而后发起一个请求,参数主要有init_res,响应记为req_res; -
setToken函数有四个参数context、cuid、init_res、req_res,依旧需要解决init_res,结果为true; -
getsign函数参数已知,结果来源未知,需要分析; -
所以需要分析的有init函数的返回值、getsign函数的返回值;
3.2 nativeInitBaseUtil
-
开始分析第一部分,nativeInitBaseUtil函数,它的偏移是0xd15,我们跳过去看看;

-
这里的符号是我修改过的,看起来依旧清爽,我稍微修改了一样参数1的类型;函数不多,看样子需要先看sub_3C6C;

-
生成随机10位字符串,再看第二个函数 sub_1C18;

-
这个函数熟悉的读者可能会很熟悉,有点像说废话了,实际上是一个md5函数;MD5初始常量摆在那里,后续的两个函数分别是md5_update和final函数;标不标准我们先不管,继续往下看,现在只是预分析而已; -
sub_1EF0不去看了,基本上是格式化字符串的函数,sub_3688可能与des有关;

-
最后大概是这些函数;

-
下面开始分析整个算法,先call吧;
复制代码 隐藏代码publicvoidcall_init() { List<Object> list = newArrayList<>(10); list.add(vm.getJNIEnv()); list.add(0); list.add(vm.addLocalObject(vm.resolveClass("android/content/Context").newObject(null))); list.add(vm.addLocalObject(newStringObject(vm, "E0DA888804E90201780A9C0D17D0B050|0")));Numberret=module.callFunction(emulator, 0xd14 + 1, list.toArray());StringObjectstr= vm.getObject(ret.intValue()); System.out.println("call_init result-->>" + str);}
-
这里开始需要补环境了,报错如下:

-
这是一个系统调用相关的错误,clock_gettime这个系统调用,不熟悉的读者可以留言我后面可以单独写一篇文章;我们可以从第一行报错点进去;

-
可以发现unidbg已经实现了一部分,去看看是哪一部分;

-
很好,没有id为2的情况,所以需要我们自己去补,正常补的话是一段苦人的差事,我选择偷懒;

-
非常非常不建议这么补,我这里只是偷懒,分析完我还要恢复的,但是这样执行就没问题了;另外,对于系统调用相关的报错可以多多结合ai,它比我们见多识广;

-
前面分析了,有随机数rand的参与,结果一直在变,给它固定住;
复制代码 隐藏代码publicvoidhook_rand() {IHookZzhookZz= HookZz.getInstance(emulator); hookZz.wrap(module.findSymbolByName("lrand48"), newWrapCallback<HookZzArm32RegisterContext>() {@OverridepublicvoidpreCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { }@OverridepublicvoidpostCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { ctx.setR0(1); } }); hookZz.wrap(module.findSymbolByName("srand48"), newWrapCallback<HookZzArm32RegisterContext>() {@OverridepublicvoidpreCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { }@OverridepublicvoidpostCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { ctx.setR0(1); } });}
-
这部分hook代码可以留着,后续需要直接参考就可以,现在结果固定住了;回到我们刚进入函数的时候;

-
hook一下rand函数,看看固定后的结果是什么,这也是为了分辨后续哪一部分值是随机出来的;
复制代码 隐藏代码emulator.attach().addBreakPoint(module.base + 0x3C6C);
-
断住后先在控制台敲 blr再按c查看返回值R0;

-
这里不理解的我也可以单独说一下,随机值需要记住;

-
这里rand的结果,再看md5函数;
复制代码 隐藏代码emulator.attach().addBreakPoint(module.base + 0x1C18);

-
很长的数据,先看是不是标准的;这里有一些地方需要注意,参数1的长度有点长,尽量读的时候多读一些;最后加密对比是这样的:

-
首先证明了他是一个标准的md5,其次、参数还是整个apk的签名,首先看日志可以看出来这一点;

-
这两个值是一样的,其次,如果去分析md5_1C18(*(dword_A0D8 + 20))的话,也是可以找到证据的;

-
在sub_DD0函数就可以发现,从偏移20处开始取的,应用签名存的偏移就是这么多,那自然是对得上的;

-
所以这个md5的结果就是固定的,继续看sub_1EF0的参数和返回值;
复制代码 隐藏代码emulator.attach().addBreakPoint(module.base + 0x1EF0);
-
参数有好几个,我们写成持久化的风格;
复制代码 隐藏代码emulator.attach().addBreakPoint(module.base + 0x1EF0, newBreakPointCallback() {@OverridepublicbooleanonHit(Emulator<?> emulator, long address) {RegisterContextcontext= emulator.getContext();UnidbgPointerpointer1= context.getPointerArg(0); Inspector.inspect(pointer1.getByteArray(0, 0xe), "参数0");UnidbgPointerpointer2= context.getPointerArg(1); Inspector.inspect(pointer2.getByteArray(0, 0x5), "参数1");UnidbgPointerpointer3= context.getPointerArg(2); Inspector.inspect(pointer3.getByteArray(0, 0xa), "参数2 随机数");UnidbgPointerpointer4= context.getPointerArg(3); Inspector.inspect(pointer4.getByteArray(0, 0x20), "参数3 md5签名");// 3. 获取栈指针UnidbgPointersp= context.getStackPointer();intarg5= sp.getInt(0); // 栈顶第一个32位值是第5个参数UnidbgPointerarg555= UnidbgPointer.pointer(emulator, arg5); Inspector.inspect(arg555.getByteArray(0, 0x22), "参数4");// int arg6 = sp.getInt(4); // 下一个32位值是第6个参数// int arg7 = sp.getInt(8); // 再下一个32位值是第7个参数returntrue; }});
-
这里引申出一个知识点,在arm32下,参数一般是放在R0-R3中,如果参数超过则逐个放在栈中,以小端序存储,并且一般在一个函数进来之后会把所有的参数都放在栈中保存起来,返回值通常都是通过R0返回; -
看看结果;
复制代码 隐藏代码>-----------------------------------------------------------------------------<[16:47:10 476]参数0, md5=d688b762594d55ece22fa8bfcaeb226d, hex=2573232325732323257323232573size: 140000: 25 73 23 23 25 73 23 23 25 73 23 23 25 73 %s##%s##%s##%s^-----------------------------------------------------------------------------^>-----------------------------------------------------------------------------<[16:47:10 478]参数1, md5=6560c614ae407a4cf1f1c069e1f028c3, hex=382625642asize: 50000: 38 26 25 64 2A 8&%d*^-----------------------------------------------------------------------------^>-----------------------------------------------------------------------------<[16:47:10 478]参数2 随机数, md5=38b18761d3d0c217371967a98d545c2e, hex=42424242424242424242size: 100000: 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBB^-----------------------------------------------------------------------------^>-----------------------------------------------------------------------------<[16:47:10 478]参数3 md5签名, md5=34bbdff4e0bedd62d3d0bede8c05e251, hex=3266623533646536643338656666373130396631396436386530343731323362size: 320000: 32 66 62 35 33 64 65 36 64 33 38 65 66 66 37 31 2fb53de6d38eff710010: 30 39 66 31 39 64 36 38 65 30 34 37 31 32 33 62 09f19d68e047123b^-----------------------------------------------------------------------------^>-----------------------------------------------------------------------------<[16:47:10 478]参数4, md5=0be9dbcbe36e5f2cc0d0ecabc9c737f9, hex=45304441383838383034453930323031373830413943304431374430423035307c30size: 340000: 45 30 44 41 38 38 38 38 30 34 45 39 30 32 30 31 E0DA888804E902010010: 37 38 30 41 39 43 30 44 31 37 44 30 42 30 35 30 780A9C0D17D0B0500020: 7C 30 |0^-----------------------------------------------------------------------------^
-
参数1好像不太认识,它是来自byte_A0DC,按下X没有交叉引用,但是他实际上是A0D8偏移4位存储的数据,但是我进去看发现不太对得上;

-
对应的位置是在获取ro.build.version.sdk,hook发现确实是;

-
那这里就并不是我们分析的那样,换一个ida版本看看;

-
解决问题,是有交叉引用的,在sub_DD0函数里;

-
传进来的参数就是byte_A0DC,这个函数就是真正赋值的地方;从aFda893hflEsi这个内容里取数据,一共循环5次,我们可以打印一下2010函数的返回值 ;
复制代码 隐藏代码emulator.attach().addBreakPoint(module.base + 0x2010);
-
返回值确实是这个,这里不贴图了,我们去看分析一下它的生成; -
重点肯定就在于v3的值,这里反编译很差,所以去看汇编;

-
可以发现,MUL.W R0, R5, R1是乘法指令,也就是说R1就是v3的值,我们去hook打印一下;
复制代码 隐藏代码emulator.attach().addBreakPoint(module.base + 0x5868, newBreakPointCallback() {@OverridepublicbooleanonHit(Emulator<?> emulator, long address) {RegisterContextcontext1= emulator.getContext();intpointer2= context1.getIntArg(1); System.out.println("0x5868 参数1:" + pointer2); emulator.attach().addBreakPoint(context1.getLRPointer().peer, newBreakPointCallback() {@OverridepublicbooleanonHit(Emulator<?> emulator, long address) {RegisterContextcontext= emulator.getContext();intpointer1= context.getIntArg(1); System.out.println("0x5868 返回值:" + pointer1);returntrue; } });returntrue; }});
-
输出如下:

-
返回值分别是:0、0、2、0、5;根据这段算法还原一下;
复制代码 隐藏代码aFda893hflEsi = "fda&8^%$#)93hfl_esi.*"defsub_2010(): a1 = [0, 0, 0, 0, 0] v3 = [0, 0, 2, 0, 5]for i inrange(0, 5):# v3 = sub_5868(0xC, (i + 3)) a1[i] = aFda893hflEsi[i * v3[i] - i + 4]return a1print(sub_2010())
-
生成的结果就是:8&%d*,和hook的结果是对得上的;这里的v3跟进去实际上也不是一个多复杂的算法,感兴趣可以去还原一下,实际上没有进行随机化,每个设备都可能是这个值,就说到这里; -
回到正题,接下来看看sub_3688函数;参数1是这样的:

复制代码 隐藏代码emulator.attach().addBreakPoint(module.base + 0x3688);
-
其实就是前面的函数拼接起来,两个参数没有问题,但是返回值有些不对劲,请看:

-
这是在函数结束时读的r0,看不出什么东西,不太像结果,但是伪代码很清晰的说: v7 = sub_3688(v6, “@fG2SuLA”),返回值就是r0,所以只能看汇编了;

-
没看出什么内容,那就进去sub_3688函数看看返回的可能是什么类型;

-
v13进行了三次赋值,第三次才是真正的结果,前两次是一致的都是v14,感觉是长度,所以读第三个位置的数据应该就是结果;这里的结构体类似于这样:
复制代码 隐藏代码structDesResult { DWORD size; // 第一个字段,v14 DWORD size_copy; // 第二个字段,v14(与第一个相同) DWORD* data; // 第三个字段,v6,指向加密后的数据};
-
那我们下断点后去读第三个字段,也就是这一部分;

-
小端序读,m0x401e8114,长度正好是0x58,对得上;

-
还记得它说他是des,我们尝试去看看是不是标准的;

-
和结果完全不一样,这里我先是猜测是ecb模式,因为只传了一个key,结果是对不上的,但是位数是一样的,所以有可能是魔改,也有可能是cbc模式,还需要继续分析;
-
首先来看是什么模式,这里分享一个知识,ECB模式是最简单的加密模式,每个明文块都使用相同的密钥独立加密,没有任何反馈或交互,相同的明文块一定会加密成相同的密文块;
-
那么我们就可以修改明文来进行测试,给定相同的明文,看看对应加密出来是否一致,一致则是ecb模式,不一致则是cbc或其他;
-
这里肯定不能修改init的入参了,需要去修改des的入参,代码参考如下:
复制代码 隐藏代码emulator.attach().addBreakPoint(module.base + 0x3688, newBreakPointCallback() {@OverridepublicbooleanonHit(Emulator<?> emulator, long address) {StringInput="111111111111111111111111111111111111111111111111111111111111111111111";intlength= Input.length();MemoryBlockfakeInputBlock= emulator.getMemory().malloc(length, true); fakeInputBlock.getPointer().write(Input.getBytes(StandardCharsets.UTF_8));// 修改r0为指向新字符串的新指针 emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, fakeInputBlock.getPointer().peer);returnfalse; }});
-
这里断下来之后可以去查看是否改成功了;

-
然后输入blr再按c,去读对应的返回结果;

-
肉眼可见的一致,去cyberchef对比一下;

-
最后应该是填充的事情,所以它应该是一个ecb模式的des,这里被魔改了,但是比较简单,后续就知道改了哪里了; -
对于填充比较好判断,我们前面知道了sub_3250是分组加密的位置,所以去hook一下,这里我不给代码了,可以手动的按c一直跑,自己估摸着最后一组停下来,就会是这样:

-
这是pkcs7的填充应该没跑了,接下来看具体魔改了哪里; -
这里需要知道des算法的流程,我实在是不知道怎么写,因为几乎没有魔改算法,改的全是常量表; -
先看看秘钥编排的过程,sub_3620函数;

-
引入眼帘的是sub_3554函数,点进去看看;

-
我们知道,des秘钥编排首先就是与PC1表进行置换,pc1表长度是56;这里也是56次循环置换,所以这是pc1置换,那dword_8180处的数据就是pc1表;

-
长度也很合理,但是数据完全不一样,去看一下正常的pc1表应该是多少;

-
基本上都完全不一样了,接下来应该是循环左移,这里又有个表SHIFT;
复制代码 隐藏代码SHIFT = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]# 长度:16
-
正好这里有16次循环,且dword_8320也是这样的数据;

-
他俩貌似是一致的,再看sub_3578函数;

-
一个道理,这里对应的应该是PC_2表,后续我不贴了,把对应的表都扣下来就行,s盒是标准的,剩下的改的基本上就是这些常量; -
但还有一个重要的魔改点,他把字节序翻转了,byte转bit的时候高位低位是相反的;

-
这个函数将1个字节( a1)转换为8个位,但转换顺序低位在前的位序反转;
复制代码 隐藏代码i=0: 取第0位(最低位)i=1: 取第1位...i=7: 取第7位(最高位)
-
sub_2E28是和他对应的翻转函数,其余的就正常了,我们手动改一下这些魔改的位置,然后加密对应的明文;
复制代码 隐藏代码defbytes_to_bits_reverse(self, data: bytes) -> List[bool]:"""将字节转换为位列表,注意高位低位反转""" bits = []for byte in data:# 将字节转换为位,但顺序反转(低位在前)for i inrange(8): bits.append((byte >> i) & 1)return bitsdefbits_to_bytes_reverse(self, bits: List[bool]) -> bytes:"""将位列表转换为字节,注意高位低位反转""" result = bytearray()for i inrange(0, len(bits), 8): byte = 0for j inrange(8):if i + j < len(bits) and bits[i + j]: byte |= (1 << j) # 注意这里j是从低位开始 result.append(byte)returnbytes(result)

-
加密结果对比:
复制代码 隐藏代码本地:8df33e23f867646595a8e3a2b3f019693b82fd90bbf6ae3afe1f6cd600bfe2bbe4fd5488b35c19972d4f312d7367b384d7a34d34ae655473c30df79f1e33d722b09d8caf31f2f36f4851aa296652fb7faf5a0a07884d8e70正确:8df33e23f867646595a8e3a2b3f019693b82fd90bbf6ae3afe1f6cd600bfe2bbe4fd5488b35c19972d4f312d7367b384d7a34d34ae655473c30df79f1e33d722b09d8caf31f2f36f4851aa296652fb7faf5a0a07884d8e70
-
是对应上的,所以这个函数也就分析结束,需要读者熟悉des的加密流程; -
这一部分的分析太长了,回到最外层先看看;

-
目前还需要分析sub_2EA0这个函数,他是一个str转hex的函数,但也是需要自己还原的;重点就在这个函数,可以自行hook验证一下,入参就是des的结果,出参就是最后的结果;
-
也可以借助ai帮助还原,例如:
复制代码 隐藏代码defsub_2EA0_detailed(byte_array): hex_str = [''] * (len(byte_array) * 2 + 1)# 转换每个字节for i, byte_val inenumerate(byte_array):# 调用 sub_2F18 和 sub_2E44 的等效操作# 直接将字节转换为十六进制字符串 hex_chars = f"{byte_val:02x}"# 写入到输出数组 hex_str[i * 2] = hex_chars[0] hex_str[i * 2 + 1] = hex_chars[1]# 添加换行符 hex_str[len(byte_array) * 2] = '\n'# 合并为字符串return''.join(hex_str)
-
测试一下;

-
结果对了,至此整个init函数才算是分析完成;

-
稍微做个小总结,这里我们得到的是nativeSetToken函数的参数2,涉及到了md5、魔改des等;
3.3 nativeSetToken
-
先看看大体的代码吧;

-
实际上,它和init函数呈现一种逆序的感觉,后者是加密,他其实是在解密,这里也有加密用的key; -
这里不去分析了,在unidbg call的时候传入的参数是与主动调用的时候不同的,我们测试解一下看看;

-
这里注意,hex2byte这个方法是前面的反写,解密的话让ai帮你写就行; -
成功的解出来数据了,和我们的明文是一个情况,在这里回想一下,前面提到了参数3是一个请求返回的,并且3.2部分我们已经解释了它的来源,这里我们应思考响应是否也可以解密?

-
很显然下方还有一次调用,key的话是第一次解密结果的7-11位,后续再拼接 “#G4“;测试一下;
复制代码 隐藏代码key2 = b'95b8L#G4'ciphertext2 = hex2byte("0c000a01090d0e04020c0a0e06010b0d050a080e0c0f03050c0a0602060e0c0207090c0a0d090f060a060e08030a0003")decrypted2 = des.decrypt(ciphertext2, key2)print(f"Decrypted2: {decrypted2}")

-
解出来了一段明文:95b8LzKrr3##t2BOaKTul1,前面是随机数那部分,也就是key的一部分,后面的可能就是需要设置的内容,毕竟函数并没有返回值; -
这时候请回想分析getsign函数的时候,dword_A0D4也有判断,并且md5还需要加密使用到它;

-
那这个函数刚好也有设置值的位置;

-
这个函数的目的应该就是设置这个返回值解密后的数据了; -
并且参数我们也都解决了,算法主要就是des解密,和init的魔改函数是同一对;
3.4 nativeGetSign
-
首先把unidbg的call打开,别忘了call_nativeSetToken;到这个算法其实就简单了,直接hook对应的md5函数先;
复制代码 隐藏代码emulator.attach().addBreakPoint(module.base + 0x1C18);
-
第一次端下来是之前获取应用签名的位置,第二次是这样的:

-
这个着实有些眼熟,是setToken函数解密出来的后半部分,原来是用在这里了;继续往下跟;

-
这个结果有点熟悉;

-
就是主动调用的结果,所以算法很简单,就是标准的md5,参数的话是这种组成形式;

-
第一部分在前面详细的说了来源,f061614527adba41881c85c075b6bf52这一部分是前面 t2BOaKTul1 md5结果,后面一部分是传进来的参数,至此算法分析完毕;
4. 总结
-
整体的算法不算太难,但是分析过程属实花了不少时间,des扣表的时候请仔细,因为我就踩了不久的坑; -
最后对整体我们做了什么做个总结,针对三个方面:入参、返回值、算法; -
nativeInitBaseUtil函数: -
入参:context、cuid,总体来说cuid与设备有关,可能是设备id; -
返回值:一组16进制结果,后续会用做nativeSetToken函数的入参; -
算法:魔改des加密、md5等;des的入参也分析的很透彻了; -
nativeSetToken函数: -
入参:cuid、nativeInitBaseUtil返回值、一个请求的响应结果; -
返回值:没有返回值,函数用于给全局变量赋值,此结果会在nativeGetSign函数使用; -
算法:魔改des解密、md5等;与init函数紧密联系; -
nativeGetSign函数: -
入参:base64形式的明文,前面也分析过; -
返回值:最终的sign; -
算法:md5等; -
总体的算法难度非常小,但综合性非常强,分析每一个明文的来源才是这篇文章的目的;
· 今 日 推 荐 ·

本文内容来自网络,如有侵权请联系删除

夜雨聆风
