乐于分享
好东西不私藏

安卓逆向 — 某对app算法分析

安卓逆向 — 某对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, {onEnterfunction (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;            }        },onLeavefunction (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, {onEnterfunction (args) {console.log("JNI_OnLoad onEnter")                    },onLeavefunction (retval) {console.log("JNI_OnLoad onLeave")hook_java()                    }                })            }        }    });}setImmediate(hook_dlopen)
  • 这里需要把握好时机的问题,一般来说直接用常规hook没输出的话就要考虑自己hook的时机是不是够早了,不行的话还可以往前找更早的时机;我这里是hook JNI_OnLoad后,时机是比较早的了;
可以发现,顺序分别是nativeInitBaseUtil、nativeSetToken、nativeGetSign;分别去unidbg调用,这里放出一份日志:
 复制代码 隐藏代码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 + 0x1EF0newBreakPointCallback() {@OverridepublicbooleanonHit(Emulator<?> emulator, long address) {RegisterContextcontext= emulator.getContext();UnidbgPointerpointer1= context.getPointerArg(0);        Inspector.inspect(pointer1.getByteArray(00xe), "参数0");UnidbgPointerpointer2= context.getPointerArg(1);        Inspector.inspect(pointer2.getByteArray(00x5), "参数1");UnidbgPointerpointer3= context.getPointerArg(2);        Inspector.inspect(pointer3.getByteArray(00xa), "参数2 随机数");UnidbgPointerpointer4= context.getPointerArg(3);        Inspector.inspect(pointer4.getByteArray(00x20), "参数3 md5签名");// 3. 获取栈指针UnidbgPointersp= context.getStackPointer();intarg5= sp.getInt(0); // 栈顶第一个32位值是第5个参数UnidbgPointerarg555= UnidbgPointer.pointer(emulator, arg5);        Inspector.inspect(arg555.getByteArray(00x22), "参数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 + 0x5868newBreakPointCallback() {@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 = [00000]    v3 = [00205]for i inrange(05):# 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 + 0x3688newBreakPointCallback() {@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(0len(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等;
  • 总体的算法难度非常小,但综合性非常强,分析每一个明文的来源才是这篇文章的目的;

· 今 日 推 荐 ·

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

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 安卓逆向 — 某对app算法分析

评论 抢沙发

9 + 2 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮