最近想起以前学习过的一款 app 非常有代表性,其采用爱加密加固,且内部具有 java 及 native 两层反调试,故将之前的学习笔记重新整理如下
本文将按顺序分别分析其 java 层反调试及 native 层反调试原理,并给出绕过方案
--> java 反调试原理分析
本文参考了 hypnos 的文章《使用Android ART进行反调试》
其实在 hypnos 大神的文章里讲的已经很清楚了,在此仅仅补充一点个人的想法
由 hypnos 大神的文章可知, java 层反调试主要由 Jdwp 线程相关的 JdwpSocketState 和 JdwpAdbState 全局符号决定
反调试的方案是直接覆盖掉 vtable 中 ProcessIncoming 的地址来完成,该方案报错信息少且几乎没有任何有用反馈来让人分析出可能存在的反调试手段
但实际上搞清楚原理之后发现可行的反调试手段并不止以上的一种
如对建立调试链接时调用栈上任意函数(若要求通用性则需要导出函数)进行 inline hook 均可达成反调试的目的
以某正常 demo 进程为例,在建立调试连接时的函数调用栈如下,以函数 ProcessIncoming 为目标, 以下函数均为导出函数,完成对任意函数的 inline hook 均可完成反调试... art::Dbg::GoActive | art::JDWP::JdwpState::ProcessRequest | art::JDWP::JdwpState::HandlePacket | art::JDWP::JdwpAdbState::ProcessIncoming ...
--> JAVA 反调试绕过方案
首先看到应用由爱加密加固,打开应用后,发现 java 层无法附加调试,现象就是 jdb 一挂载进程就崩溃
接下来实施具体绕过步骤,首先以调试模式启动应用am start -D -n com.liuli.yun/com.e4a.runtime.android.mainActivity
jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=8700
可以成功连接上,因为调试模式启动时应用内部反调试模块还未初始化,但走着走着进程很快会挂掉,所以需要注入编写好的 so 来排查进程反调试的方案
OK,接下来注入 so 并 inline hook dlopen ,用于打印其加载的 so 及顺序(以下方案完全适用于 Frida 操作)
发现在完成 liblog.so 的加载后进程会挂掉,并且日志中出现了加载 libart.so 及 libdvm.so 的记录,应该是对应不同的 Android 版本做的适配
此时在加载 liblog.so 时暂停一下,并打印 log 看看 ProcessIncoming & ShutDown 函数地址是否一致
直接使用 hypnos 大神的代码,仅仅打印 log 来检查函数地址void jdwp_func1() { void* lib = dlopen("libart.so", RTLD_NOW); if (lib == NULL) { LOGD(ANDROID_LOG_ERROR, "[-] loading libart.so\n"); dlerror(); }else{ struct VT_JdwpAdbState *vtable = ( struct VT_JdwpAdbState *)dlsym(lib, "_ZTVN3art4JDWP12JdwpAdbStateE"); if (vtable == 0) { LOGD(ANDROID_LOG_ERROR, "[-] Couldn't resolve symbol '_ZTVN3art4JDWP12JdwpAdbStateE'.\n"); }else { LOGD(ANDROID_LOG_INFO, "[+] Vtable for JdwpAdbState at: %08x\n", vtable); LOGD(ANDROID_LOG_INFO, "[+] vtable->ProcessIncoming at: %p\n", vtable->ProcessIncoming); LOGD(ANDROID_LOG_INFO, "[+] vtable->ShutDown at: %p\n", vtable->ShutDown); } } }打印的结果出乎意料,两个函数地址并不一致,说明该加固方案并不是直接覆盖了 vtable 中 ProcessIncoming 的地址来完成 java 反调试的
那么继续暂停在加载 liblog.so 的位置,看看此时的内存布局是什么样的
看到在 libart.so 中出现了一段 rwxp 的内存布局,这点着实让人在意,dump 下来整个 libart.so 对应的内存布局来检查下吧
与原始 libart.so 对比发现如下点被加固方案做了 inline hook
可以清楚看到这些位置做了 inline hook,被修改后的字节码是一段跳转指令;那么如何绕过 java 反调试已经很明显了,最简单的方案是直接还原 hook 的指令字节码就 OK 了void jdwp_func1() { void* lib = dlopen("libart.so", RTLD_NOW); if (lib == NULL) { LOGD(ANDROID_LOG_ERROR, "[-] loading libart.so\n"); dlerror(); }else{ void *ptr_GoActive = ( void *)dlsym(lib, "_ZN3art3Dbg8GoActiveEv"); if (ptr_GoActive == 0) { LOGD(ANDROID_LOG_ERROR, "[-] Couldn't resolve symbol '_ZN3art3Dbg8GoActiveEv'.\n"); }else { u_int8_t ori_code[16] = {0x2D, 0xE9, 0xF0, 0x4F, 0x9B, 0xB0, 0xDF, 0xF8, 0xA4, 0x0D, 0x78, 0x44, 0x00, 0x68, 0x00, 0x68}; u_int32_t addr_GoActive = (u_int32_t)ptr_GoActive & 0xFFFFFFFE; memcpy(( void *)addr_GoActive, ori_code, 16); } } }直接注入 so 来还原这段指令字节码,然后再次 jdb 附加调试,成功附加并且应用不会崩溃了
p.s. 另外图中其他的 inline hook 点应该与指令抽取有关,如 AddImageSpace LoadMethod OpenImageSpace,感兴趣的小伙伴可以自己研究下
--> native层反调试原理及绕过
该应用 native 层反调较为简单,网上针对此类技术文章也很丰富,其中最多也最基础的应该是针对 ptrace 调试标志位检测来完成反调试
p.s. 若通过刷机修改了 ptrace 标志位的大神们可以忽略了
ptrace 会在调试进程与被调试进程间构建父子关系,此时被调试进程的一切行为诸如指令执行、信号捕获等均可以被父进程监视
并且同时会在 /proc/[pid]/status(pid 为被调试进程号)文件内将 TracerPid 字段置为调试进程的 pid
该应用反调试的点存在于 dlopen 加载 libexec.so 时 .init 段的某个函数中,考虑到这种反调试方案依然具有一定代表性,所以还是做了如下总结记录
首先 ida 挂起该应用,开始调试,发现仅仅在加载 libexec.so 后进程就崩溃退出了,所以需要在 linker 中的 _dl__ZN6soinfo13call_functionEPKcPFvvE 函数下断点
该函数负责调用被加载 so 的 .init 段函数,如下图所示,v3(v7) 是 .init 段函数调用语句
p.s. 该函数内部包含众所周知的特征字符串 "[ Calling %s @ %p for \"%s\" ]"
p.p.s. 该函数所处于 [dlopen] 流程调用栈如下所示(1+5 — Android 7.1.1)_dl_android_dlopen_ext | _dl__ZL10dlopen_extPKciPK17android_dlextinfoPv | _dl__Z9do_dlopenPKciPK17android_dlextinfoPv | _dl__ZN6soinfo17call_constructorsEv | _dl__ZN6soinfo13call_functionEPKcPFvvE在这个位置下断点,大概断点重复至40次时(发现第40次就会挂掉),发现一函数较为可疑,如图所示,
首先 dump 下来 libexec.so 解密后的内存(libexec.so 会在加载后将自己的指令所在内存解密)
使用 ida 分析之,该函数如下图
直接给出结论,红框内函数通过构造 "/proc/%d/status" 字符串并检测 TracerPid 字段来进行调试检测,并且检测到调试器后将 r0 寄存器置为1,后续 BNE 跳转会调用 exit 退出进程
(最后测试发现,可以采用简便的方法直接在 libc.so 的 _exit 导出函数上下断点来快速发现其退出流程来定位反调试函数)
尝试直接把 r0 寄存器置为0,强行续命,但后续加载 libart.so 时出现 sigsegv 信号且无法绕过,依然导致程序退出。。。
猜测可能还有其他检测机制,实在懒得寻找了,最后使用刷过 ptrace 调试标志位的机器挂载调试无任何异常
关于如何刷机绕过 ptrace 请参见 倔强石头 的文章 ([原创]记一次安卓内核源码编译刷机过程(修改反调试标志位))
--> 总结
本文仅针对部分常见反调试手段进行记录,个人认为该 app 作为实战反调试系列的极佳 app ,其包含的知识点较为全面,作为本小白的首次发帖,希望各路大神多加指正,感谢~