本文为看雪论坛优秀文章
前段时间想玩下这个游戏,模拟器运行时候发现有root检测,想看下它的实现,准备调试过掉,但是没有X86库,又发现之前的调试手机版本太低了,手上有个小米8,然后经历重新下载源码,编译LineageOs17.1,刷机,调试,根据调试情况修改系统源码,中间碰到盲区,又要去翻对应知识点,中间还被其它事打断过,断断续续持续了很长时间,写个文档做下记录。LineageOs 17.1 (android 10)直接翻了下各个信息,有函数名称混淆,字符串加密等,看起来是有壳的,直接搜相关信息,搜到这篇:https://blog.csdn.net/weixin_30512785/article/details/99559394。这个是旧版本的Nprotect,新版本跟这个比起来,dex和so的保护强度都加强了,包括so文件(抹去了文件头等信息,干扰静态分析),so的加载(libengine.so没有使用系统API装载),符号表的加密,信息流也不再直来直去,还有使用了Ollvm(Obfuscator-LLVM clang version 4.0.1)等。新旧版本虽然有很多不同了,并且跟我看的游戏也不同,但是仍然有很多相似的地方,这篇文章还是有所帮助的,省了不少时间,非常感谢这位作者。我是想看下这个root检测,那篇没提到,可能是旧版本没有,就准备自己看了,这中间穿插了编译系统刷机,也是一番波折,还好系统都刷好了。用uiautomatorviewerStrong看了下,没找到切入点,想通过提示字符串查找关键点,搜不到,字符串都加密了,并且直接打开的APK JAVA代码也是不全的,接着就准备dump class文件了,这个用github上的frida_dump脚本就行,会得到8个dex文件:然后就是反编译,翻代码,对我这边相关代码是在class2.dex中,发现字符串都是加密的。加密代码都是比较简单的(so里面的字符串也是加密的,不过是AES的),不过要注意的是,这个加密函数不止一个,不同的字符串可能用的函数不同的,算法类似,只是里面的2个XOR常量不同: static public String IiIIiiIiii(String p0){
int vi;
int ilength = p0.length();
char[] ocharArray = new char[ilength];
ilength = ilength-1;
while (ilength >= 0) {
vi = ilength-1;
ocharArray[ilength]=(char)(p0.charAt(ilength)^0x3c);
if (vi >= 0) {
ilength = vi-1;
ocharArray[vi]=(char)(p0.charAt(vi)^0x60);
}else {
break ;
}
}
return new String(ocharArray);
}
后面就是找到感兴趣的类,写程序解密字符串了,最后找到这个提示点:package com.inca.security.Core;
public class AppGuardEngine implements WeakRefHandler$IOnHandleMessage, BaseEventInvoker
This app will be terminated because a security policy violation has been detected!
这里要说下GDA确实强,下面这个JEB反编译不出来的:这里面包括各个检测码的定义,还原字符串后是这样的:根据这个看,34正好就是DETECT_ROOTING_ENVIRONMENT,表示检测到root环境了,顺便提下,翻代码过程中,发现几个反调试的地方:类名:com.inca.security.IiIIiiiiIiDebug.isDebuggerConnected():
public boolean iiIIIiiiIi() {
return Debug.isDebuggerConnected();
}
通过执行时间差Debug.threadCpuTimeNanos(),判断是否被调试做100W次加法计算,看时间是否超过100ms (100000000纳秒) public boolean iIIIiiiIII() {
boolean v0 = false;
long v4 = Debug.threadCpuTimeNanos();
int v1 = 0;
int v2;
for(v2 = 0; v1 < 1000000; v2 = v1) {
v1 = v2 + 1;
}
if(Debug.threadCpuTimeNanos() - v4 >= 100000000) {
v0 = true;
}
return v0;
}
public void conditionCallback(int arg23, int arg24, byte[] arg25)
MainActivity = Java.use('com.inca.security.Core.AppGuardEngine');
if (MainActivity != null) {
MainActivity.conditionCallback.implementation = function (arg0, arg1, arg2) {
//send('Statr! Hook!'); //python call back
console.log("call conditionCallback");
console.log(arg0);
console.log(arg1);
console.log(arg2);
showStacks();
return this.conditionCallback(arg0, arg1, arg2);
}
}
frida -U -l E:\node_proj\TcpsocketTest\fridaHook2.js -f com.bluepotiongames.eosm
Spawned `com.bluepotiongames.eosm`. Use %resume to let the main thread start executing!
[MI 8::com.bluepotiongames.eosm]-> hook_eos();
[MI 8::com.bluepotiongames.eosm]-> %resume
发现很快就退出了,到不了提示窗口那,那就是被检测到了,要上动态调试了。这个启动后是3个进程的,互相ptrace,还是用frida启动进程方式,然后通过IDA附加游戏进程调试。libcompatible.so
libstub.so
libengine-hlp.so
libengine.so
首先上来肯定就是找JNI_OnLoad了,直接跑IDC脚本: //android 10(lineage 17.1)
//LoadNativeLibrary偏移: 0000007BAE70AC70 - 0000007BAE395000 = 375C70
auto soBase=0;
soBase=getModuleBase("libart.so");
auto addrArtBp=soBase + 0x375C70;
MakeComm(addrArtBp,"LoadNativeLibrary");
auto addrArtCallOnload=soBase + 0x376910;
AddBpt(addrArtCallOnload);
MakeComm(addrArtCallOnload,"call JNI_ONLOAD");
进到libcompatible.so的JNI_OnLoad:有些代码是运行中解压的,这种可以调试时候dump对应的数据下来,然后合并到原so文件中,可以方便IDA分析。JNI_OnLoad里面包含反调试的相关处理,检查status信息,fork子进程互相ptrace,注册inotify_add_watch,包含下面几个检测:/dev/input 这里下面3个是调试libengine.so发现的这些检测,直接改内核过滤掉了,status的相关修改网上都很多,关于这个inotify_add_watch的修改,在fs/notify/下:/fs/notify/inotify/inotify_user.c中的inotify_add_watch
这里修改给自己挖了个坑,只过滤了了主进程的,导致线程触发的还是被检测了,下面会提到,根本原因是子线程的current->parent task_struct结构直接是主进程的parent的了,导致得到的进程名称不是预想的本进程名称,而是父进程的父进程的名称。bionic/libc/bionic/ptrace.cpp
对svc调用的要改内核部分ptrace.c中的ptrace_attach,对这个进程都直接返回就行。这里实际调试花了点时间,中间也输出过so里面各个jni接口函数(包括后续libstub.so)这里提下,libstub.so中的.init_proc会调用libcompatible.so中的导出函数SoLibraryStart来解密代码,后来发现这里不是主要关注点,就换了个思路。处理完上面检测后,就可以继续跑hook conditionCallback脚本了,这个时候就可以显示堆栈了:java.lang.Exception
at com.inca.security.Core.AppGuardEngine.conditionCallback(Native Method)
GetMethodID Pid: 11681Path: conditionCallback
Backtrace:
0x76046fe75c
0x7635df2808
0x7635df2808
然后IDA附加上去,去到0x76046fe75c,通过对比分析,确定了这就是libengine.so的代码空间,可以dump出来用IDA打开,然后动态静态结合看,内存中文件头都没了,IDA里面定位有点麻烦,这里可以把调试得到的相关偏移写到idc脚本中,每次新的调试,跑下脚本,就可以识别出之前已经分析的点。双击输出的地址,就可以到达对应代码点,还是很方便的。通过0x76046fe75c这个点只是搞清楚了检测码的读取,写入是另外的线程,这里读取到后,就会调用java显示提示窗口了。因为前面修改inotify_add_watch的坑,准备这里入手找切入点的时候,发现会跑飞。后来尝试了几种方法,没找到切入点,还是回到系统修改上,直接在pthread_create那输出了线程入口地址,然后结合上面堆栈输出的地址,确定了几个相关的线程地址,然后修改修改libengine.so文件的入口地址为00 00 00 14,直接入口循环,然后附加,还原入口代码并断点,确定相关的入口点:Root相关检测就是这个线程了,跑起来后遇到新的问题,就是不是提示检测码34,而是9,查找之前的码表: stringArray[7]=("DETECT_INVALID_LIBDVM_SO");
stringArray[8]=("detectinvVALID_LIBRUNTIME_SO");
stringArray[9]=("DETECT_INVALID_APPLIB_SO");
stringArray[10]=("DETECT_INVALID_LIBENGINE_SO");
9就是DETECT_INVALID_APPLIB_SO,看起来是修改了libengine.so导致被检测到了,不过现在已经确定了线程入口点了,后面直接顺着调试了。主要就是hash比较,这个文件校验会涉及到assets\appguard目录下的3个文件:对于这个检测,通过修改内核的目录列表返回,过滤掉了libengine.so。对应修改点fs/readdir.c中的filldir64,相关的修改都是一些字符串比较,就是过滤,并且不同系统内核也不同,就不复制代码占篇幅了。现在就可以继续调试了,找到root检测点,包括su文件检测和apk包检测:3、 调用pm list packages,查找是否有:回过头来看,其实也可以用frida hook java.lang.Runtime.exec找到app调用的命令:hookAllOverloads: exec
arguments: pm,path,com.bluepotiongames.eosm
arguments: which,su
arguments: pm,list,packages
处理方法,也是直接在内核中过滤su相关路径,pm list中过滤包返回。顺便提下,还有个读取__system_property相关,这个有JAVA层,也有native层的,可以根据情况过滤。对于通过SystemProperties.get读取build.prop文件信息的,这个我是直接在内核open函数那,重定向到/data/local/tmp/build2.prop了,后面直接改这个文件就好了。在处理完文件校验,找到线程切入点后,基本后面各需求都可以顺着调试了,提下模拟器相关检测:/system/bin/nox
/system/bin/ttVM-prop
/system/app/MOMOStore/MOMOStore.apk
/system/lib/vboxsf.ko
/system/lib/vboxguest.ko
/system/lib/vboxvideo.ko
/system/bin/nemuVM-nemu-service
boolean isEmulator = SystemProperties.get("ro.kernel.qemu").equals("1");
处理完这个root检测后,安装建 行的app(ver 5.0.2)试了下,发现还是被检测(这个看网上很多也是说新版本用面具隐藏模块也不行,梆梆保护),看了下,是多了下面路径访问检测的,比如/data目录非root是访问不了的:看雪ID:xwtwho
https://bbs.pediy.com/user-home-44250.htm
*本文由看雪论坛 xwtwho 原创,转载请注明来自看雪社区。