查看原文
其他

N*Protect 分析记录

xwtwho 看雪学苑 2022-07-01

本文为看雪论坛优秀文章

看雪论坛作者ID:xwtwho



前段时间想玩下这个游戏,模拟器运行时候发现有root检测,想看下它的实现,准备调试过掉,但是没有X86库,又发现之前的调试手机版本太低了,手上有个小米8,然后经历重新下载源码,编译LineageOs17.1,刷机,调试,根据调试情况修改系统源码,中间碰到盲区,又要去翻对应知识点,中间还被其它事打断过,断断续续持续了很长时间,写个文档做下记录。
 
1. 软硬件环境
IDA 7.5
Frida 14.2.2
Gda3.86
JEB
LineageOs 17.1 (android 10)
小米8
 
2. 分析记录 (直接按实际经历的时间顺序来了)
 
最开始拿到APK后,用GDA打开。

 
直接翻了下各个信息,有函数名称混淆,字符串加密等,看起来是有壳的,直接搜相关信息,搜到这篇: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!

后面 code显示是10进制。
 
这里要说下GDA确实强,下面这个JEB反编译不出来的:


这里面包括各个检测码的定义,还原字符串后是这样的:
根据这个看,34正好就是DETECT_ROOTING_ENVIRONMENT,表示检测到root环境了,顺便提下,翻代码过程中,发现几个反调试的地方:

类名:com.inca.security.IiIIiiiiIi
 
Debug.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)
 
然后上frida,hook这个调用点。
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.eosmSpawned `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附加游戏进程调试。

保护相关的SO是下面几个:
libcompatible.solibstub.solibengine-hlp.solibengine.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,包含下面几个检测:

/proc/xxx/mem
/proc/xxx/maps
/dev/input      这里下面3个是调试libengine.so发现的
/system/bin/input
/system/bin/monkey

最后走的都是svc调用。
 
这些检测,直接改内核过滤掉了,status的相关修改网上都很多,关于这个inotify_add_watch的修改,在fs/notify/下:
/fs/notify/inotify/inotify_user.c中的inotify_add_watch

这里修改给自己挖了个坑,只过滤了了主进程的,导致线程触发的还是被检测了,下面会提到,根本原因是子线程的current->parent task_struct结构直接是主进程的parent的了,导致得到的进程名称不是预想的本进程名称,而是父进程的父进程的名称。
 
处理ptrace:
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: conditionCallbackBacktrace:0x76046fe75c0x7635df28080x7635df2808

然后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个文件:

81,936 sign.axml
256 sign.crt
510,368 sign.mf
涉及到的算法有sha256 ,RSA.
 
检测到修改后,会设置检测码9,如下:

 
对于这个检测,通过修改内核的目录列表返回,过滤掉了libengine.so。

对应修改点fs/readdir.c中的filldir64,相关的修改都是一些字符串比较,就是过滤,并且不同系统内核也不同,就不复制代码占篇幅了。

现在就可以继续调试了,找到root检测点,包括su文件检测和apk包检测:

 
相关的路径和包名:
/system/bin/su
/system/xbin/su
/sbin/su
/system/su
/system/xbin/ku.sud
/system/xbin/sutemp
/su/bin/su
/root/magisk
 
/sbin/adbd
daemonsu
eu.chainfire.supersu
 
com.topjohn
com.topjohnwu.magisk
 
1、  查找上面几个su文件路径。
2、  调用命令which su
3、  调用pm list packages,查找是否有:
com.topjohn
com.topjohnwu.magisk
 
回过头来看,其实也可以用frida hook java.lang.Runtime.exec找到app调用的命令:

hookAllOverloads: execarguments: pm,path,com.bluepotiongames.eosmarguments: which,suarguments: 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-serviceboolean isEmulator = SystemProperties.get("ro.kernel.qemu").equals("1");

附:

处理完这个root检测后,安装建 行的app(ver 5.0.2)试了下,发现还是被检测(这个看网上很多也是说新版本用面具隐藏模块也不行,梆梆保护),看了下,是多了下面路径访问检测的,比如/data目录非root是访问不了的:
/system/bin
/system/xbin
/system/product/bin
/odm/bin
/vendor/bin
/data/local/tmp
/postinstall
/data
这些路径对线程名称getprop过滤掉就可以了。

- End -




看雪ID:xwtwho

https://bbs.pediy.com/user-home-44250.htm

  *本文由看雪论坛 xwtwho 原创,转载请注明来自看雪社区。



《安卓高级研修班》2021年6月班火热招生中!


# 往期推荐





公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存