找到
10
篇与
安卓逆向
相关的结果
- 第 2 页
-
安卓逆向-jnitrace基本使用 一、jni-trace 1. 前置 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.exammple trace的结果示例如下: 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。
-
安卓逆向-瑞幸咖啡白盒AES 一、瑞幸咖啡白盒AES 样本其实已经有很多大佬分享过了,之所以写这样一篇文档是希望理清整体的流程,加深逆向的思想; 仅作技术交流 请勿恶意请求 侵权麻烦联系删除1. 前置 [!sana] APP:瑞幸咖啡 包名:com.lucky.luckyclient 版本:5.0.01 加固:360加固 目标:q参数 这里选择的版本不是新版,因为新版有frida对抗,考虑到复现问题使用了旧版本,这个版本是没有frida检测的; 需要前置知晓的知识点大致如下: unidbg简单使用与补环境; AES算法详细流程; 参考文档:https://www.yuque.com/nanren-w8l2z/xgu63m/uoff9ovsmhqki9wh 白龙大佬的文章,请多多站在巨人的肩膀; 差分故障攻击(DFA); 在此默认读者已经有逆向环境所以不会提到环境的东西; 2. 抓包 抓包环境:charles + socksdroid转发; 在我的环境下直接就可以抓包,有对抗的话自行解决抓包问题; image-20241201111652996图片 这里主要的目标就是q参数,sign也是native层,篇幅足够的话会讨论; 关于壳在前置提到过,它是有一个360的加固,如图: image-20241201112405771图片 在这里不涉及脱壳的内容,其实也非常好脱,我在这里使用的是fart脱的壳; 3. 代码定位 脱完壳后直接搜索sign,但是并搜不到什么有价值的东西; image-20241201114539042图片 很明显这是人家的东西,所以这里用其他的方式定位; 在这里使用hook hashmap的put方法,可以直接定位到,hook代码如下: 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!=null && a.equals("q")){ showStacks(); console.log("hashMap.put: ", a, b); } return this.put(a, b); } }) 在这里可以先将打印的地方注释掉,因为这个app是有双进程保护的,所以需要重启注入,这样就可以避免还没点击登录却输出了过多信息,hook结果如下: java.lang.Throwable at java.util.HashMap.put(Native Method) at com.lucky.lib.http2.r.a(SourceFile:7) at com.lucky.lib.http2.AbstractLcRequest.getRequestParams(SourceFile:14) at com.lucky.lib.http2.m.b(SourceFile:14) at com.lucky.lib.http2.m.getRequest(SourceFile:1) at com.lucky.lib.http2.AbstractLcRequest.async(SourceFile:5) ···省略部分 at butterknife.internal.c.onClick(SourceFile:4) at android.view.View.performClick(View.java:7259) at android.view.View.performClickInternal(View.java:7236) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) hashMap.put: q 8pR_VhJIrWrz_t·····省略部分SLoqiKvmgkJGOXxo= 根据堆栈信息去找,发现getRequestParams的位置比较像put的位置,而且也有cid的字眼; image-20241201134318185图片 但这里并不知道它的键究竟是不是我们需要的,而它放入的是看不懂的东西,其实这里是加密了,如何进行测试呢?主动调用即可,getString2这个方法按理来说就应该是解密字符串的方法,传入相应的参数即可; 主动调用代码如下: function dec_str(str){ Java.perform(function (){ let StubApp = Java.use("com.stub.StubApp"); let result = StubApp["getString2"](str); console.log("字符串解密结果: " + result); }) } 将上图加密的字符串分别解密结果如下: image-20241201134509590图片 [Pixel 4::com.lucky.luckyclient ]-> dec_str(7719) 字符串解密结果: sign [Pixel 4::com.lucky.luckyclient ]-> dec_str(16944) 字符串解密结果: uid [Pixel 4::com.lucky.luckyclient ]-> dec_str(4005) 字符串解密结果: cid [Pixel 4::com.lucky.luckyclient ]-> dec_str(457) 字符串解密结果: q 当然直接hook这个解密方法输出参数和结果也是可以找到需要的结果的; 那么这里正是我们需要的位置,接下来就可以去找生成q参数的native方法到底在哪; 没有什么难度,一路向下跟即可; image-20241201135019629图片 4. hook验证 这里其实是有分支的,也就是说这两个圈起来的方法是都有可能走的,所以可以都hook一下,在这里测试过是走的下面的方法,也就不再复现了,hook代码如下: function call_aes() { Java.perform(function () { let ByteString = Java.use("com.android.okhttp.okio.ByteString"); let CryptoHelper = Java.use("com.luckincoffee.safeboxlib.CryptoHelper"); CryptoHelper["localAESWork4Api"].implementation = function (bArr, i2) { console.log('参数1:' + Java.use("java.lang.String").$new(bArr)); console.log('参数2:' + i2); let result = this["localAESWork4Api"](bArr, i2); console.log(`result=${ByteString.of(result).base64()}`); return result; }; }) } call_aes() hook的时候有几个需要注意的点,最后在刚启动时注释掉方法调用,否则有壳的防护可能会找不到类,其次注意只在点击登录之前将注释解掉,这样输出也会少很多方便分析,最后hook到的结果如下: 参数1:{"blackBox":"eyJvcyI6ImFuZHJvaWQiLCJ2ZXJzaW9uIjoiMy4zLjciLCJwYWNrYWdlcyI6ImNvbS5sdWNreS5sdWNreWNsaWVudComNS4wLjAxIiwicHJvZmlsZV90aW1lIjozNDQsImluddmFsX3RpbWUiOjc5OCwidG9rZW5faWQiOiJuT1wvZWFscmNCRjZZTnBQYXR0OThDaVNmWmZMdlEyMEFnWHNrUk9OdVVNUXE4dTV0RFl1RWN0aUk5WUM4MHRxSVlaME8zS3BFeUsrNWhvdnNCdjVTcnp6N0NIeFk1d0lhanYyUXVFM2pZNkk9In0=","uniqueCode":"DUStsTlxA16lKIyUaLR_SGOxaGnmBnMTXJ1dRFVTdHNUbHhBMTZsS0l5VWFMUl9TR094YUdubUJuTVRYSjFkc2h1","regionId":"CO0001","mobile":"15244448888","countryNo":"86","validateCode":"456921","regId":"","appversion":"5001","type":1,"deviceId":"android_lucky_beb03f432402baaa","systemVersion":"29","deviceBrand":"google"} 参数2:0 result=8pR/VhJIrWrz/tZtV6BCHViaW+VcsIem1HE6z7xNRYyVtK9SqWgrrgfb3iFkKIEGsfrSjR3dQhXSTLVGCRHEigsSIniYTUc1GbWaPMS1lOY9vZ2x3z0mK7gNza9lU9h8N+SzBmY1heejAwpKAaGGZXcpEjvXKst3PxekVny9uCld8NXdBRh0Ur37jcyK5Ik8PQ/w5pNwRmJY2om/+ilph/d5FkrbdUtZf3pAmSiTcCNKJoI6zCBs1RIXN4y0yFw59a/PpV1vy0giAu8DocmLRpQT/vDriJd+tbsWfV9LqgSieNuAUiK1TKKM01fusC8ByR/zFDoMhEMZKbj7+Ln4p0y/Jy/gYLANWarDm7WUES6zf3rrG+i sYx1JzigdgHk9B+9qVo1gf4ftktwvQV0YHsFOPF6qdOkAZcXDCwDwsNG+rXlSAnxEgRun7ZyumWdOIJqGaZKmleHTI/GnMuAPVk4epMt0Gc8K1WL2s3x1eK1AUcYuK6bksvVa7E6poqshlcHdrtthW9QR5AxOKAUew0708bBlhZO/93tA9b7796JdbeHt pyKviexhfiDpIqeJ4y0qdhsrkvTlIW7hx+/ku6IBdHCBWw4Q1p11i56orROO6YEcTXEmoRzjgZAKrGJUkmD5kLVw8E1ufsTbTfVIzwQ/BCOJJiUQe6/Badbcx4vkPJ6fR9MIeIVOA6N5vn5Jll+itsF+ptKuERsP9IH3gxzRozBQAdU3gvpJhukvv2y/UTfJs+yreXXUj8/mHlyiX6ugE0mhGw+BTdeMEjR1iJ2BiK3FBfLCpP9KVPPVnWG9UDybqS4WKiHACrAZmjJ38MmnSPga6XagdgggDjtZYAokm1WSLoqiKvmgkJGOXxo= 与抓包结果对比是对的上的,这也就证明走的方法是没问题的,而且在hook的同时我们可以发现有些参数2是0,而有些是1,当它是1的时候结果解码之后其实就是请求的结果,也就是响应解密的结果,可以大概看出这个参数可能代表的就是加密解密; 这里的结果并不完全对的上,在返回的时候做了一些处理; image-20241201151222590图片 参数中有个blackBox是同盾的风控,在这里不在讨论范畴; 5. 算法分析 5.1 定位so 接下来就应该去分析so文件了,同样的道理,可以去调用解密函数来知道so文件是哪一个; image-20241201152442195图片 另外直接hook也可以,我这里偷懒就直接选择hook动态注册了,脚本如下: function hook_RegisterNatives() { var RegisterNatives_addr = null; var symbols = Process.findModuleByName("libart.so").enumerateSymbols(); for (var i = 0; i < symbols.length; i++) { var symbol = symbols[i].name; if ((symbol.indexOf("CheckJNI") == -1) && (symbol.indexOf("JNI") >= 0)) { if (symbol.indexOf("RegisterNatives") >= 0) { RegisterNatives_addr = symbols[i].address; console.log("RegisterNatives_addr: ", RegisterNatives_addr); } } } Interceptor.attach(RegisterNatives_addr, { onEnter: function (args) { var env = args[0]; var jclass = args[1]; var class_name = Java.vm.tryGetEnv().getClassName(jclass); var methods_ptr = ptr(args[2]); var method_count = args[3].toInt32(); console.log("RegisterNatives method counts: ", method_count); for (var i = 0; i < method_count; i++) { var name = methods_ptr.add(i * Process.pointerSize * 3).readPointer().readCString(); var sig = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize).readPointer().readCString(); var fnPtr_ptr = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2).readPointer(); var find_module = Process.findModuleByAddress(fnPtr_ptr); console.log("类: ", class_name, "方法: ", name, "签名: ", sig, "函数地址: ", fnPtr_ptr, "模块名: ", find_module.name, "函数偏移: ", ptr(fnPtr_ptr).sub(find_module.base)); } }, onLeave: function (retval) {} }); } hook_RegisterNatives() hook结果如下,有报错不用管,也可以自行修复: RegisterNatives method counts: 4 方法: localAESWork 签名: ([BI[B)[B 函数地址: 0xc113684d 模块名: libcryptoDD.so 函数偏移: 0x1984d 方法: localConnectWork 签名: ([B[B)[B 函数地址: 0xc113678d 模块名: libcryptoDD.so 函数偏移: 0x1978d 方法: md5_crypt 签名: ([BI)[B 函数地址: 0xc1137981 模块名: libcryptoDD.so 函数偏移: 0x1a981 方法: localAESWork4Api 签名: ([BI)[B 函数地址: 0xc11381cd 模块名: libcryptoDD.so 函数偏移: 0x1b1cd5.2 白盒AES 涉及到的四个native函数都找到了,so是libcryptoDD.so,函数地址也都找到了,比较方便; 去看一下so长什么样,此样本只有32位的so,不过9.0也不区分了,没有关系; image-20241201153650696图片 可以看到不是平时看到的那些状态,这是做了混淆,但也不影响,而且也没用Java开头的函数,所以我们直接hook动态注册是正确的; 上面已经得到了函数的偏移,在IDA按下G键,跳转到对应的偏移位置; image-20241201154413068图片 字面意思可以看出一点端倪,wb也就是白盒的缩写,所以它有可能是一个白盒AES,当然这也只是一个猜测而已,不过逆向就是要大胆猜测小心求证,猜一下无伤大雅; 进入到方法里面,实际上比较关键的点也就是这一个方法而已; image-20241201154731808图片 可以发现通篇看下来无非就是这两个方法,虽然不同的位置,但方法是同一个,管他什么逻辑进去看看先,因为被混淆了所以两个位置有一个位置条件永远不成立也不是不可能; 进入方法后进行分析,截取几个重要的位置做参考; 位置1: image-20241201155548754图片 位置2: image-20241201155634822图片 位置3: image-20241201155654563图片 位置4: image-20241201155731224图片 通篇看下来其实是没多少内容的,而且这个样本之所以不算特别难,是因为它的符号大多都在,这大大减少了我们在分析的难度,甚至可以直接告诉我们很多信息; 大概重要的就是位置4,可以看出大概是一个ecb模式的加密,但不能完全相信这个结果,我们可以hook一下这个函数,hook代码如下,32位的so地址需要+1; function call_so() { var base_addr = Module.findBaseAddress("libcryptoDD.so"); let funcPtr = base_addr.add(0x17BD4 + 1) Interceptor.attach(funcPtr, { onEnter: function (args) { console.log("参数0:" + args[0].readCString()); console.log("参数1:" + args[1].toInt32()); this.len = args[1].toInt32() this.args2 = args[2]; console.log("参数3:" + args[3].toInt32()); }, onLeave: function (retval) { var hexdumpString = hexdump(this.args2, { offset: 0, length: this.len, header: true, ansi: true }); console.log("参数2返回时(hexdump):\n" + hexdumpString); } }); } call_so() hook结果如下: java入参: 参数1:{"blackBox":"eyJvcyI6ImFuZHJvaWQiLCJ2ZXJzaW9uIjoiMy4zLjciLCJwYWNrYWdlcyI6ImNvbS5sdWNreS5sdWNreWNsaWVudComNS4wLjAxIiwicHJvZmlsZV90aW1lIjozMDEsImludGVydmFsXbWUiOjc5NSwidG9rZW5faWQiOiJuT1wvZWFscmNCRjZZTnBQYXR0OThDbmZhSWVPTlBWaW92ZHBMb09CZG13eTdpRmk4Ukp2clhwNlVZZTBYTmRuYUxVY3R0QzlBczUreXBGdDNyOFRaWGxOOXFoZHNKUFdtbDBqWnJGWjE1K1k9In0=","uniqueCode":"DUStsTlxA16lKIyUaLR_SGOxaGnmBnMTXJ1dRFVTdHNUbHhBMTZsS0l5VWFMUl9TR094YUdubUJuTVRYSjFkc2h1","regionId":"CO0001","mobile":"15211115555","countryNo":"86","validateCode":"123456","regId":"","appversion":"5001","type":1,"deviceId":"android_lucky_beb03f432402baaa","systemVersion":"29","deviceBrand":"google"} 参数2:0 native入参: 参数0:{"blackBox":"eyJvcyI6ImFuZHJvaWQiLCJ2ZXJzaW9uIjoiMy4zLjciLCJwYWNrYWdlcyI6ImNvbS5sdWNreS5sdWNreWNsaWVudComNS4wLjAxIiwicHJvZmlsZV90aW1lIjozMDEsImludGVydmFsX3RpbWUiOjc5NSwidG9rZW5faWQiOiJuT1wvZWcmNCRjZZTnBQYXR0OThDbmZhSWVPTlBWaW92ZHBMb09CZG13eTdpRmk4Ukp2clhwNlVZZTBYTmRuYUxVY3R0QzlBczUreXBGdDNyOFRaWGxOOXFoZHNKUFdtbDBqWnJGWjE1K1k9In0=","uniqueCode":"DUStsTlxA16lKIyUaLR_SGOxaGnmBnMTXJ1dRFVTdHNUbHhBMTZsS0l5VWFMUl9TR094YUdubUJuTVRYSjFkc2h1","regionId":"CO0001","mobile":"15211115555","countryNo":"86","validateCode":"123456","regId":"","appversion":"5001","type":1,"deviceId":"android_lucky_beb03f432402baaa","systemVersion":"29","deviceBrand":"google"}������������������������������������������������ 参数1:656 参数3:0 参数2返回时(hexdump): 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF c297e100 f2 94 7f 56 12 48 ad 6a f3 fe d6 6d 57 a0 42 1d ...V.H.j...mW.B. ···省略部分内容 c297e380 ae 46 23 46 05 9e 4c 11 53 d3 ca 59 ab a5 51 08 .F#F..L.S..Y..Q. 经过验证返回的内容就是抓包的结果,也就证明了函数是对的方向,而这个明文与java层的明文大致是一样的,只是后面多了一些不可见字符,我推测可能是末尾的填充,后续留个心眼; 也就是说接下来重点就在于这个wbaes_encrypt_ecb方法; 依旧是找几个关键点做分析; 位置1: image-20241201171249278图片 位置2: image-20241201171309352图片 选择第一个进去观察一下,看看是否会有白盒的特征; 这里看见了一个比较重要的字眼,循环左移,这个需要了解aes的算法流程; image-20241201171546565图片 除了这个以外,还有很多的操作,很符合查表的特征; image-20241201171721179图片 去看看表的大小; image-20241201171749148图片 很显然普通的查表法aes的表是没有这么大的,这也很符合白盒aes的特征; DFA的攻击点在哪里?这个也需要读者了解,这里直接给出; image-20241201172645678图片 也就是图中的四个攻击点,原理这里不展开; 而在这里我们一共发现了两个关键函数,点进去查看它的循环左移符号均未抹去,在结合上图的攻击点,不是正好有个点是ShiftRows吗,那我们就可以对它做一些文章;循环左移的入参是state块,左移后结果也存在里面,所以DFA就可以攻击这个位置;如果你了解AES的算法细节,你会知道它的加密轮数有10轮(仅在此案例讨论,具体轮数不定),攻击点就在8-9轮中间,而这里循环左移是最合适的,因为它符号并没有抹去,就可以很畅快的注入DFA; 准备好以后就可以进行故障攻击了; 5.3 DFA 这里由于不需要补环境,所以在后面就使用unidbg来攻击; 首先搭好基本的框架; package com.xyt.sana; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; 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.linux.android.dvm.jni.ProxyDvmObject; import com.github.unidbg.memory.Memory; import java.io.File; public class RuiXin extends AbstractJni { public static AndroidEmulator emulator; public static Memory memory; public static VM vm; public static Module module; public RuiXin(){ emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.lucky.luckyclient").build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("apks/ruixin/rxkf5.0.01.apk")); vm.setJni(this); vm.setVerbose(true); DalvikModule dm = vm.loadLibrary("cryptoDD", true); dm.callJNI_OnLoad(emulator); module = dm.getModule(); } public static void main(String[] args) { RuiXin rxkf = new RuiXin(); } } 不需要补环境,直接上DFA代码,在这里需要读者了解unidbg的hook,unidbg完整代码如下: package com.xyt.sana; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Emulator; import com.github.unidbg.Module; import com.github.unidbg.arm.context.RegisterContext; import com.github.unidbg.debugger.BreakPointCallback; import com.github.unidbg.debugger.Debugger; 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.linux.android.dvm.jni.ProxyDvmObject; import com.github.unidbg.memory.Memory; import com.github.unidbg.memory.MemoryBlock; import com.github.unidbg.pointer.UnidbgPointer; import javax.xml.bind.DatatypeConverter; import java.io.File; import java.util.Random; public class RuiXin extends AbstractJni { public static AndroidEmulator emulator; public static Memory memory; public static VM vm; public static Module module; public RuiXin() { emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.lucky.luckyclient").build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("apks/ruixin/rxkf5.0.01.apk")); vm.setJni(this); vm.setVerbose(true); DalvikModule dm = vm.loadLibrary("cryptoDD", true); dm.callJNI_OnLoad(emulator); module = dm.getModule(); } public static String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { int unsignedInt = b & 0xff; String hex = Integer.toHexString(unsignedInt); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); } public static byte[] hexToBytes(String hexString) { return DatatypeConverter.parseHexBinary(hexString); } public static int randint(int min, int max) { Random rand = new Random(); return rand.nextInt((max - min) + 1) + min; } public void calldfa() { Debugger debugger = emulator.attach(); debugger.addBreakPoint(module.base + 0x14f98, new BreakPointCallback() { UnidbgPointer pointer; RegisterContext context = emulator.getContext(); int num = 1; @Override public boolean onHit(Emulator<?> emulator, long address) { pointer = context.getPointerArg(0); if (num % 9 == 0) { pointer.setByte(randint(0, 15), (byte) randint(0, 0xff)); } num += 1; return true; } }); } public void call_wbaes() { // 主动调用 MemoryBlock inputBlock = emulator.getMemory().malloc(16, true); UnidbgPointer inputPtr = inputBlock.getPointer(); MemoryBlock ouputBlock = emulator.getMemory().malloc(16, true); UnidbgPointer ouputPtr = ouputBlock.getPointer(); byte[] byteArray = hexToBytes("55669988555269996541441122554411"); assert byteArray != null; inputPtr.write(0, byteArray, 0, byteArray.length); module.callFunction(emulator, 0x17bd5, inputPtr, 16, ouputPtr, 0); String res = bytesToHex(ouputPtr.getByteArray(0, 0x10)); // System.out.println("白盒结果:" + res); System.out.println(res); inputBlock.free(); ouputBlock.free(); } public static void main(String[] args) { RuiXin rxkf = new RuiXin(); // rxkf.calldfa(); // rxkf.call_wbaes(); for (int i = 0; i < 300; i++) { rxkf.calldfa(); rxkf.call_wbaes(); } } } 首先主动调用一次工具,对比一下正确密文和故障密文有什么差别; 60 9b 2ab3 8e fb53e028d8b4 5a 191a f3 13 60 10 2ab3 3d fb53e028d8b4 3f 191a bf 13 可以发现有四个字节是不一样的,这个需要读者了解差分故障攻击,同时我会将涉及到的东西放在文件里; 接下来就是循环几百次,收集多个故障密文,再使用phoenixAES去推导第十轮的密钥,再去使用轮密钥推导主密钥,其中一些结果如下图: image-20241201180855488图片 使用phoenixAES库得到K10密钥,使用方法如下: # pip install phoenixAES import phoenixAES phoenixAES.crack_file('tracefile.txt', [], True, False, 3) tracefile.txt放得到的故障密文,第一行是正确的密文,其余的是故障密文; image-20241201181429228图片 它推导的k10:869D92BBB700D0D25BD9FD3E224B5DF2,再使用k10去推导主密钥; 推导结果如下: image-20241201181826009图片 所以它的原始密钥就是 644A4C64434A69566E44764D394A5570; 这里去测试结果是一样的,以下是加密的结果: image-20241201184058845图片 hook的结果: image-20241201184046888图片 对比来看也是对的上的; image-20241201184141560图片 至此q参数分析完毕,这篇文章的重点也就在此,一个简单的白盒AES; 本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除! 6. 总结 很少写长篇幅的东西,很多东西也来不及仔细的写到文档,如有讲解不到位的欢迎指出; 技术交流+vx:HeiYuKuaiDou23
-
安卓逆向-泡泡聊天 一、泡泡聊天 1. 逆向前置 1.1 逆向目标 目标:账号密码登录 版本:v1.7.4 时间:2024-06-10 1.2 简要分析 关于泡泡聊天比较重要的就是抓包证书以及360的壳,直接抓包结果如下: 这是做了服务端证书的校验,所以我们需要拿到证书并且导入到charles,具体步骤如下: - 获取 bks 或 p12证书 文件 - 获取证书相关密码 - 将证书导入到charles,可以实现抓包(bks格式需要转换p12格式) - 用requests发送请求时,携带证书去发送请求2. 逆向实现 2.1 获取证书&密码 使用frida脚本进行hook相应的证书与密码,这是上述步骤的前两步; 可以看到加载了不止一个证书,这是正常的,但是只有一个有密码,所以无疑就是这个了; 那么它的证书就在asset目录下的bks文件;那么此时我们并不知道具体是哪一个bks文件,所以在此直接保存当前获取到的流文件并且保存下来; Java.perform(function () { var KeyStore = Java.use("java.security.KeyStore"); var String = Java.use("java.lang.String"); KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (inputStream, v2) { var pwd = String.$new(v2); console.log('\n--------------------------------------------------------------') console.log("密码:" + pwd, this.getType()); if (this.getType() === "BKS") { var myArray = new Array(1024); for (var i = 0; i < myArray.length; i++) { myArray[i] = 0x0; } var buffer = Java.array('byte', myArray); var file = Java.use("java.io.File").$new("/sdcard/Download/paopao-" + new Date().getTime() + ".bks"); var out = Java.use("java.io.FileOutputStream").$new(file); var r; while ((r = inputStream.read(buffer)) > 0) { out.write(buffer, 0, r); } console.log("save success!") out.close() } var res = this.load(inputStream, v2); return res; }; }); // frida -U -f com.paopaotalk.im -l 2.hook_save.js 在进行这一步之前需将app赋予可读写的权限;随后去观察是否保存成功; 至此证书以及密码我们获取到了;后续就需要导出证书并且导入到charles; 2.2 导出&导入证书 扩展:有时进行hook时会失败,提示找不到类等,是因为有壳导致,可以加上延迟; 此时我们需要将bks证书转换成p12的格式,才支持;使用portecle-1.11工具; 将选择的文件导出,选择p12格式; 此时根据指引输入密码后即可导出; 接下来即可将证书导入到charles中; 选择客户端证书,并且添加; 导入p12证书; 端口等选择所有; 接下来再度抓包即可成功; 2.3 代码实现 具体逻辑比较简单,脱壳使用frida-dumpdex大致实现如下: frida-dexdump -U -f 包名# File:01-泡泡聊天登录实现.py # Author:下雨天 # Date:2024-06-10 21:50 import hashlib import random from requests_pkcs12 import get, post import json import uuid import urllib3 urllib3.disable_warnings() def md5_hash(password): md5_obj = hashlib.md5() md5_obj.update(password.encode('utf-8')) md5_hash = md5_obj.hexdigest() return md5_hash def double_md5_hash(password, openId): # 第一次MD5加密 md5_obj = hashlib.md5() md5_obj.update(password.encode('utf-8')) first_md5_hash = md5_obj.hexdigest() # 将第一次MD5加密结果与openId和"MOSGRAM"拼接起来 combined_string = first_md5_hash + openId + "MOSGRAM" # 第二次MD5加密 md5_obj = hashlib.md5() md5_obj.update(combined_string.encode('utf-8')) second_md5_hash = md5_obj.hexdigest() return second_md5_hash def login(): password = "aa12121323" openId = "008615222225555" timestamp = random.randint(100, 999) headers = { "bundle_id": "com.paopaotalk.im", "version": "1.7.4", "timestamp": str(timestamp), "sign": md5_hash(str(timestamp)), "app_id": "qiyunxin", "Accept-Language": "zh-CN", "package": "com.paopaotalk.im", "Content-Type": "application/json; charset=UTF-8", "Host": "api.vvchat.im", "User-Agent": "okhttp/4.8.1" } url = "https://api.vvchat.im/userservices/v2/user/login" device_id = md5_hash(str(uuid.uuid4()).replace("-", "")) data = { "device_type": "app", "username": openId, "password": double_md5_hash(password, openId), "device_id": device_id, "device_name": "Google Pixel 2 XL", "device_model": "Pixel 2 XL" } data = json.dumps(data, separators=(',', ':')) response = post(url, headers=headers, data=data, pkcs12_filename='Client.p12', pkcs12_password='111111', verify=False ) print(response.text) print(response) if __name__ == '__main__': login() 值得注意的就是发送请求时依然需要携带证书;
-
安卓逆向-Native层相关HOOK 一、Native层相关hook 1. hook_so层 hook_so层只需要得到它的函数地址,有函数地址就能hook与主动调用,而得到函数地址的方式有两种; 1.1 方式一 通过frida提供的api来得到,该函数必须有符号的才可以; 有符号是指此函数是否出现在导出、导入、符号表里; 1.2 方式二 通过计算得到地址:so基址+函数在so中的偏移[+1](32位+1) 2. 各种枚举 2.1 枚举导入表 通过枚举导入表,可以得到出现在导入表中的函数地址;(enumerateImports) var imports = Module.enumerateImports("libifeng_secure.so"); // console.log(JSON.stringify(imports[0])) for (var i = 0; i < imports.length; i++) { // if (imports[i].name == "atoi") { // console.log(JSON.stringify(imports[i])); console.log('导入函数名--->>>',imports[i].name); console.log('导入函数地址--->>>',imports[i].address); console.log('-----------------------------------------------------------------') // break; // } } 以某so为例,得到的结果如下: 2.2 枚举导出表 通过枚举导出表,可以得到出现在导出表中的函数地址,与导入表同理,api不同;(enumerateExports) var imports = Module.enumerateExports("libifeng_secure.so"); // console.log(JSON.stringify(imports[0])) for (var i = 0; i < imports.length; i++) { // if (imports[i].name == "atoi") { // console.log(JSON.stringify(imports[i])); console.log('导出函数名--->>>',imports[i].name); console.log('导出函数地址--->>>',imports[i].address); console.log('-----------------------------------------------------------------') // break; // } } 以某so为例结果如下: 2.3 枚举符号表 通过枚举符号表,可以得到出现在符号表中的函数地址; var symbols = Module.enumerateSymbols("libifeng_secure.so"); // console.log(JSON.stringify(symbols[0])) for (var i = 0; i < symbols.length; i++) { console.log('符号表函数名--->>>',symbols[i].name); console.log('符号表函数地址--->>>',symbols[i].address); console.log('-----------------------------------------------------------------') }2.4 枚举模块 通过枚举模块,再枚举模块里面的导出表,可以快速找到某个导入函数出自哪个so; // 枚举进程中已加载的模块 var modules = Process.enumerateModules(); var module = modules[0].enumerateExports() for(let i = 0; i < module.length; i++){ console.log('枚举进程函数名--->>>',module[i].name); console.log('枚举进程函数地址--->>>',module[i].address); console.log('-----------------------------------------------------------------') } // console.log(JSON.stringify(modules[0].enumerateExports())); 通过枚举模块得到的是一个数组,取第一个后就可以调用上述三个方法; 3. hook导出函数 在so导出表里的函数,可以通过frida提供的api来获取函数地址,Module.findExportByName("xxxx.so", "add"),函数名以汇编中出现的为准; // 导出函数的hook var funcAddr = Module.findExportByName("libencryptlib.so", "_ZN7MD5_CTX11MakePassMD5EPhjS0_"); console.log('函数地址--->>>',funcAddr); Interceptor.attach(funcAddr, { onEnter: function (args) { console.log("funcAddr onEnter args[1]: ", hexdump(args[1])); console.log("funcAddr onEnter args[2]: ", args[2].toInt32()); this.args3 = args[3]; }, onLeave: function (retval) { console.log("funcAddr onLeave args[3]: ", hexdump(this.args3)); } }); 在这里以口袋48为例: 在这里我们的前置条件是,传了三个参数,这里初始打印第三个参数时,发现结果全是0,这里大概就是一个缓冲区,这里是因为C语言里,非常喜欢将参数当作返回值使用,那么这里我们就需要在离开的时候再读取内存; this.args3 = args[3]; console.log("funcAddr onLeave args[3]: ", hexdump(this.args3)); 在这里我们知道它是MD5,我们对比一下结果; 可以发现与hook到的结果是一致的; 4. 获取模块基址 在此的前提是,我们需要的函数不在三个表里,我们就没办法直接使用frida提供的api来获取函数地址,在此需要计算它的函数地址; 计算公式如下: so基址+函数在so中的偏移[+1] 32位则 +1 因此,我们需要先得到so基址,也就是模块基址; 4.1 findModuleByName 使用findModuleByName时,指明so文件即可,得到的是一个module对象,可以进行转换,也可以直接.base取地址; var module1 = Process.findModuleByName("libencryptlib.so"); console.log("module1对象--->>>",JSON.stringify(module1)); console.log("module1基址--->>>", module1.base); 直接.base取的就是基址; 4.2 getModuleByName 与findModuleByName类似,得到的也是一个对象; var module2 = Process.getModuleByName("libencryptlib.so"); console.log("module2对象--->>>",JSON.stringify(module2)); console.log("module2基址--->>>", module2.base); 结果如下: 4.3 findBaseAddress(推荐) 与前两个有所不同,这里得到的直接就是函数地址,也就是基址; var soAddr = Module.findBaseAddress("libencryptlib.so"); console.log("soAddr基址--->>>", soAddr); 这里就无需再去操作,返回值就是地址; 4.4 enumerateModules 通过枚举所有模块,再判断是否与我们需要的模块一样,如果一致则输出地址等; var modules = Process.enumerateModules(); for(let i = 0; i < modules.length; i++){ if(modules[i].name == "libencryptlib.so"){ console.log(modules[i].name + " " + modules[i].base); } } 不过此方式用的较少; 也可以通过地址找模块,这也就可以调用一些方法; var module = Process.findModuleByAddress(Module.findBaseAddress("libencryptlib.so")); console.log("module " + module.name + " " + module.base);5. 函数地址计算 5.1 偏移 上述描述可知,函数地址计算如下: so基址+函数在so中的偏移[+1] [ 32位则 +1] 基址我们已经能够获取到了,那就剩下偏移; 以口袋48app为例,这里我们需要找某函数的偏移; 在其定义位置按下tab,则可转到汇编,界面如下: 实际上这就是它的偏移,它是相对so基址的偏移;得到这个偏移就可以计算出函数地址了; 在这里是否+1呢,在目前来说,大部分是64位的so,此时则不需要加,若为32位则需要加; 5.2 地址计算 依据公式,首先得到基址,再加上偏移; var soAddr = Module.findBaseAddress("libencryptlib.so"); console.log(ptr(soAddr).add(0xxx)); soAddr得到是基址,其实也就是指针,在这里通过add (sub方法为减) 加上偏移,ptr实际上就是指针,也可以不加ptr; 而add里面是一个数值,不是字符串,这里是十六进制,则需要加上0x; 何时加ptr呢,若你的soAddr为具体的数值,则需要加,如: var soAddr = Module.findBaseAddress("libencryptlib.so"); var so = 0x72777a6000; console.log("soAddr基址--->>>" + soAddr); console.log(ptr(so).add(0x1FA38)); // new NativePointer() == ptr() 在一个具体的数值add时,由于不是指针,则无法调用add方法,加上ptr即可,也是同样可以得到地址的; 6. hook任意函数 根据上述条件,我们已经可以hook任意的函数了,得到一个地址即可; var soAddr = Module.findBaseAddress("libencryptlib.so"); // var so = 0x72777a6000; console.log("soAddr基址--->>>" + soAddr); // console.log(ptr(so).add(0x1FA38)); // new NativePointer() var funcAddr = soAddr.add(0x1FA38); console.log("funcAddr函数地址--->>>" + funcAddr); Interceptor.attach(funcAddr, { onEnter: function (args) { console.log("funcAddr onEnter args[1]: ", hexdump(args[1])); console.log("funcAddr onEnter args[2]: ", args[2].toInt32()); this.args3 = args[3]; }, onLeave: function (retval) { console.log("funcAddr onLeave args[3]: ", hexdump(this.args3)); } }); 其余就与hook导出函数是类似的;