属于到处收集的资料和工作中碰到的,在此感谢文末出处链接的作者
我把他们分为三大部分,由浅至深,实际上检测点实在太多了,肯定不全的,如果有漏掉的麻烦大佬在评论区指出
Java
在java层检测的实际很容易就能找到,因为app反调试的反映只有几种
- 闪退
- 弹窗
- 卡启动页
实际上甚至可以无视上面几种,无脑hook所有函数,再过滤掉启动必须的函数就能找到,java层能实现的,native层也能实现
0.检查应用列表
String packageName = context.getPackageName();
PackageManager pm = context.getPackageManager();
List<PackageInfo> pkgs = pm.getInstalledPackages(0);
改面具名字就绕过了
1.应用主动申请root权限
Runtime.getRuntime().exec("su")
如果提权成功的话就是root了
2.获取系统build.prop属性(native也能检测)
如ro.build.tags/ro.build.type/ro.secure/ro.debuggable
如果开了系统debugger或root了,分别为 userdebugger 和test-keys
以下方案都能获取到
SystemPropertiesProxy.get(this, "ro.build.version.release")
android.os.Build.TAGS.contains("test-keys")
后者hook string的contains就行
3.是否存在XXX提权app或链接库例如magisk,su之类(native也能检测)
String[] paths = {"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su"};
for (String path : paths) {
file = new File(path);
if (file.exists()) return true;//可以继续做可执行判断
}
4.执行指令/which命令找su/busybox(native也能检测)
ArrayList<String> execResult = executeCommand(new String[] {"/system/xbin/which","su"});
ArrayList<String> execResult = executeCommand(new String[] {"busybox","df"});
5.写入文件到/data或者/etc之类(native也能检测)
FileOutputStream fout = new FileOutputStream(fileName);
byte [] bytes = message.getBytes();
fout.write(bytes);
fout.close();
这个估计还有很多类似的方法
6. 检测app本身是否debugger模式
(context.getApplicationInfo().flags
& ApplicationInfo.FLAG_DEBUGGABLE) != 0
android.os.Debug.isDebuggerConnected()
7.扫描常用逆向应用端口
InetAddress theAddress = InetAddress.getByName(host);
try {
Socket socket = new Socket(theAddress, port);
flag = true;
} catch (IOException e) {
}
8.加载常用逆向应用的类
类名->de.robv.android.xposed.XposedHelpers/de.robv.android.xposed.XposedBridge
Object xpHelperObj = ClassLoader
.getSystemClassLoader()
.loadClass(类名)
.newInstance();
9.检查堆栈
try {
throw new Exception("gg");
} catch (Exception e) {
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
if (stackTraceElement.getClassName().contains("de.robv.android.xposed.XposedBridge")) return true;
}
return false;
}
10.扫描/proc/{pid}/maps 是否有hook相关so
public boolean hasReadProcMaps(String paramString) {
try {
Object localObject = new HashSet();
BufferedReader localBufferedReader =
new BufferedReader(new FileReader("/proc/" + Process.myPid() + "/maps"));
for (; ; ) {
String str = localBufferedReader.readLine();
if (str == null) {
break;
}
if ((str.endsWith(".so")) || (str.endsWith(".jar"))) {
((Set) localObject).add(str.substring(str.lastIndexOf(" ") + 1));
}
}
localBufferedReader.close();
localObject = ((Set) localObject).iterator();
while (((Iterator) localObject).hasNext()) {
boolean bool = ((String) ((Iterator) localObject).next()).contains(paramString);
if (bool) {
return true;
}
}
} catch (Exception fuck) {
}
return false;
}
这个可以用来扫描frida的注入噢 ,这里还能扫描系统的其他目录,可以获取到很多信息,但是主要还是得Filereader~
java层的大概就这些了,实际ndk层的也差不多,但是实现方式更加曲折.
然而java层的其实很容易就找的出来,除非真的同时全用上了,又hook不上,又挂不上root,那就只能一层一层hook.
有个专搞aosp编译系统的朋友,直接发了个改过对应特征的系统,可以免root+hook直接随意翱翔于这一层甚至ndk层了
NDK
Frida
当然不是说只有这个,本文的其它也能检测frida,举一反三
https://github.com/b-mueller/frida-detection-demo
https://github.com/darvincisec/DetectFrida
Xposed
https://github.com/w568w/XposedDetectLib
https://github.com/KagurazakaHanabi/XposedHider
https://github.com/vvb2060/XposedDetector
1.端口检测
int num = 54321;
//检测常用的端口
void check()
{
FILE* pfile=NULL;
char buf[0x1000]={0};
// 执行命令
char* strCatTcp= "cat /proc/net/tcp |grep :5D8A";
char* strNetstat="netstat |grep :23946";
pfile=popen(strCatTcp,"r");
int pid=getpid();
if(NULL==pfile)
{
printf("打开命令失败!\n");
return;
}
// 获取结果
while(fgets(buf,sizeof(buf),pfile))
{
// 执行到这里,判定为调试状态
printf("执行cat /proc/net/tcp |grep :5D8A的结果:\n");
printf("%s",buf);
int ret=kill(pid,SIGKILL);
}
pclose(pfile);
}
无限查端口并杀掉来防止调试
通过hook fopen或者popen可以绕过
2.进程名称检测
//进程名称检测
void coursecheck(){
const int bufsize = 1024;
char filename[bufsize];
char line[bufsize];
char name[bufsize];
char nameline[bufsize];
int pid = getpid();
//先读取Tracepid的值
sprintf(filename, "/proc/%d/status", pid);
FILE *fd=fopen(filename,"r");
if(fd!=NULL)
{
while(fgets(line,bufsize,fd))
{
if(strstr(line,"TracerPid")!=NULL)
{
int statue =atoi(&line[10]);
if(statue!=0)
{
sprintf(name,"/proc/%d/cmdline",statue);
FILE *fdname=fopen(name,"r");
if(fdname!= NULL)
{
while(fgets(nameline,bufsize,fdname))
{
if(strstr(nameline,"android_server")!=NULL)
{
int ret=kill(pid,SIGKILL);
}
}
}
fclose(fdname);
}
}
}
}
fclose(fd);
}
这个可以通过hook strstr来看看对比了啥,然后加个判断来绕过
2.5.进程名检测
void CheckParents()
{
///
// 设置buf
char strPpidCmdline[0x100] = { 0};
snprintf(strPpidCmdline, sizeof(strPpidCmdline), "/proc/%d/cmdl
ine", getppid());
// 打开文件
int file = open(strPpidCmdline, O_RDONLY);
if (file < 0) {
LOGA("CheckParents open错误!\n");
return;
}
// 文件内容读入内存
memset(strPpidCmdline, 0, sizeof(strPpidCmdline));
ssize_t ret = read(file, strPpidCmdline, sizeof(strPpidCmdline));
if (-1 == ret) {
LOGA("CheckParents read错误!\n");
return;
}
// 没找到返回0
char sRet = strstr(strPpidCmdline, "zygote");
if (NULL == sRet) {
// 执行到这里,判定为调试状态
LOGA("父进程cmdline没有zygote子串!\n");
return;
}
int i = 0;
return;
}
有的时候不使用apk附加调试的方法进行逆向,而是写一个.out可执行文件直接加载so进行
调试,这样程序的父进程名和正常启动apk的父进程名是不一样的。
读取/proc/pid/cmdline,查看内容是否为zygote
2.75进程数量检测
APK线程检测
正常apk进程一般会有十几个线程在运行(比如会有jdwp线程),
自己写可执行文件加载so一般只有一个线程,
可以根据这个差异来进行调试环境检测
void CheckTaskCount()
{
char buf[0x100] = { 0};
char * str="/proc/%d/task";
snprintf(buf, sizeof(buf), str, getpid());
// 打开目录:
DIR * pdir = opendir(buf);
if (!pdir) {
perror("CheckTaskCount open() fail.\n");
return;
}
// 查看目录下文件个数:
struct dirent * pde=NULL;
int Count = 0;
while ((pde = readdir(pdir))) {
// 字符过滤
if ((pde -> d_name[0] <= '9') && (pde -> d_name[0] >= '0')) {
++Count;
LOGB("%d 线程名称:%s\n", Count, pde -> d_name);
}
}
LOGB("线程个数为:%d", Count);
if (1 >= Count) {
// 此处判定为调试状态.
LOGA("调试状态!\n");
}
int i = 0;
return;
}
3.检查时间差
这个跟time有关,hook所有时间的堆栈应该会有收获
五个能获取时间的api:
time()函数
time_t结构体
clock()函数
clock_t结构体
gettimeofday()函数
timeval结构
timezone结构
clock_gettime()函数
timespec结构
getrusage()函数
rusage结构
4.ptrace
void anti_ptrace(void)
{
pid_t child;
// 创建子进程
child = fork();
if (child)
{
// 返回在父进程中
wait(NULL);
}
else
{
// 获取父进程的pid
pid_t parent = getppid();
// ptrace附加父进程
if (ptrace(PTRACE_ATTACH, parent, 0, 0) < 0)
{
while(1);
sleep(1);
}
// 释放附加的进程
ptrace(PTRACE_DETACH, parent, 0, 0);
// 结束当前进程
exit(0);
}
}
zygote通过fork()系统调用,fork出一个app
app内通过ptrace(PTRACE_TRACEME,0,0,0);将父进程zygote做为自己的tracer
这样其他进程就无法ptrace()到app进程了
一些进程文件中存储了进程信息,可以读取这些信息得知是否为调试状态。
第一种:
/proc/pid/status
/proc/pid/task/pid/status
TracerPid非0
statue字段中写入t(tracing stop)
第二种:
/proc/pid/stat
/proc/pid/task/pid/stat
第二个字段是t(T)
第三种:
/proc/pid/wchan
/proc/pid/task/pid/wchan
ptrace_stop
5. 断点调试指令检测
void checkbkpt(u8* addr,u32 size)
{
//结果
u32 uRet=0;
//断点指令
u8 armBkpt[4]={0xf0,0x01,0xf0,0xe7};
u8 thumbBkpt[2]={0x10,0xde};
int mode=(u32)addr%2;
if(1==mode)
{
u8* start=(u8*)((u32)addr-1);
u8* end=(u8*)((u32)start+size);
while(1)
{
if(start>=end)
{
uRet=0;
return;
}
if(0==memcmp(start,thumbBkpt,2))
{
uRet=1;
break;
}
start=start+2;
}
} else{
//arm
u8* start=(u8*)addr;
u8* end=(u8*)((u32)start+size);
while (1)
{
if(start>=end)
{
uRet=0;
return;
}
if(0==memcmp(start,armBkpt,4))
{
uRet=1;
break;
}
start=start+4;
}
}
}
如果被下了断点的话,指令会被替换成(bkpt断点指令),那么在内存搜索一下不就完事了吗,注意arm和thumb指令有所区别
6.系统源码检测
bool checkSystem()
{
// 建立管道
int pipefd[2];
if (-1 == pipe(pipefd)) {
LOGA("pipe() error.\n");
return false;
}
// 创建子进程
pid_t pid = fork();
LOGB("father pid is: %d\n", getpid());
LOGB("child pid is: %d\n", pid);
// for失败
if (0 > pid) {
LOGA("fork() error.\n");
return false;
}
// 子进程程序
int childTracePid = 0;
if (0 == pid) {
int iRet = ptrace(PTRACE_TRACEME, 0, 0, 0);
if (-1 == iRet) {
LOGA("child ptrace failed.\n");
exit(0);
}
LOGA("%s ptrace succeed.\n");
// 获取tracepid
char pathbuf[0x100] = { 0};
char readbuf[100] = { 0};
sprintf(pathbuf, "/proc/%d/status", getpid());
int fd = openat(NULL, pathbuf, O_RDONLY);
if (-1 == fd) {
LOGA("openat failed.\n");
}
read(fd, readbuf, 100);
close(fd);
uint8_t * start = (uint8_t *) readbuf;
uint8_t des[100] = {
0x54, 0x72, 0x61, 0x63, 0x65, 0x72, 0x5
0, 0x69, 0x64, 0x3A, 0x09};
int i = 100;
bool flag = false;
while (--i) {
if (0 == memcmp(start, des, 10)) {
start = start + 11;
childTracePid = atoi((char *)start);
flag = true;
break;
} else {
start = start + 1;
flag = false;
}
}//while
if (false == flag) {
LOGA("get tracepid failed.\n");
return false;
}
// 向管道写入数据
close(pipefd[0]); // 关闭管道读端
write(pipefd[1], (void*) & childTracePid, 4); // 向管道写端写入
数据
close(pipefd[1]); // 写完关闭管道写
端
LOGA("child succeed, Finish.\n");
exit(0);
}
else {
// 父进程程序
LOGA("开始等待子进程.\n");
waitpid(pid, NULL, NULL); // 等待子进程
结束
int buf2 = 0;
close(pipefd[1]); // 关闭写端
read(pipefd[0], (void*) & buf2, 4); // 从读端读取
数据到buf
close(pipefd[0]); // 关闭读端
LOGB("子进程传递的内容为:%d\n", buf2); // 输出内容
// 判断子进程ptarce后的tracepid
if (0 == buf2) {
LOGA("源码被修改了.\n");
} else {
LOGA("源码没有被修改.\n");
}
return true;
}
}
这个比较6,是检查自己附加之后traceid有没有变,没变就是改源码了
7.Inotify事件监控dump
void thread_watchDumpPagemap()
{
LOGA("-------------------watchDump:Pagemap-------------------
\n");
char dirName[NAME_MAX] = { 0};
snprintf(dirName, NAME_MAX, "/proc/%d/pagemap", getpid());
int fd = inotify_init();
if (fd < 0) {
LOGA("inotify_init err.\n");
return;
}
int wd = inotify_add_watch(fd, dirName, IN_ALL_EVENTS);
if (wd < 0) {
LOGA("inotify_add_watch err.\n");
close(fd);
return;
}
const int buflen = sizeof(struct inotify_event) * 0x100;
char buf[buflen] = { 0};
fd_set readfds;
while (1) {
FD_ZERO(& readfds);
FD_SET(fd, & readfds);
int iRet = select(fd + 1,& readfds, 0, 0, 0); // 此处阻塞
LOGB("iRet的返回值:%d\n", iRet);
if (-1 == iRet)
break;
if (iRet) {
memset(buf, 0, buflen);
int len = read(fd, buf, buflen);
int i = 0;
while (i < len) {
struct inotify_event * event = (struct inotify_even
t *)& buf[i];
LOGB("1 event mask的数值为:%d\n", event -> mask);
if ((event -> mask == IN_OPEN)) {
// 此处判定为有true,执行崩溃.
LOGB("2 有人打开pagemap,第%d次.\n\n", i);
//__asm __volatile(".int 0x8c89fa98");
}
i += sizeof(struct inotify_event) + event -> len;
}
LOGA("-----3 退出小循环-----\n");
}
}
inotify_rm_watch(fd, wd);
close(fd);
LOGA("-----4 退出大循环,关闭监视-----\n");
return;
}
void smain()
{
// 监控/proc/pid/mem
pthread_t ptMem, t, ptPageMap;
int iRet = 0;
// 监控/proc/pid/pagemap
iRet = pthread_create(& ptPageMap, NULL, (PPP)thread_watchDumpPagema
p, NULL);
if (0 != iRet) {
LOGA("Create,thread_watchDumpPagemap,error!\n");
return;
}
iRet = pthread_detach(ptPageMap);
if (0 != iRet) {
LOGA("pthread_detach,thread_watchDumpPagemap,error!\n");
return;
}
LOGA("-------------------smain-------------------\n");
LOGB("pid:%d\n", getpid());
return;
}
通常壳会在程序运行前完成对text的解密,所以脱壳可以通过dd与gdb_gcore来dump
/proc/pid/mem或/proc/pid/pagemap,获取到解密后的代码内容。
可以通过Inotify系列api来监控mem或pagemap的打开或访问事件,
一旦发生时间就结束进程来阻止dump。
8.扫描目录文件适配关键词
interceptor
这个是我最近碰到的,实际原理就是root的权限比较大,可以扫描隔壁的文件夹,这个app就真的全扫了,然后匹配文字,属实🐮
当然以上都只是检测点冰山一角,很多内存中的异常都是检测点
9…基于信号的检测
signal() raise() 发送信号
利用IDA先截获信号特性的检测
IDA总是先于我们的应用程序截获信号
signal(SIGTRAP, myhandler); SIGTRAP:调试信号 myhandler:信号处理函数(自己实现的)
信号处理函数 可以设置一个全局变量
终止进程方式可以kill进程 或者 sleep()
先给程序设置signal 并实现信号处理函数
在关键函数里或者开一个线程 隔时 发送信号 即 raise()
若信号未收到 则程序被调试
信号 消息机制 被捕获就会消失 一次性
10./task/pid/status
扫描task目录下所有/task/pid/status中的Name字段寻找是否存在frida注入的特征,具体线程名为
gmain 、 gdbus 和 gum-js- loop ,一般情况下这三个线程在第11–13的位置,此外在frida运行脚本过
程中,还会存在一个Name字段为 pool-frida 的线程。
11.readlink
通过readlink查看 /proc/self/fd 和 /proc/self/task/pid/fd 下所有打开的文件,检测是否有frida
相关文件。
12.D-BUS
Frida是通过D-Bus协议进行通信的,所以可以遍历/proc/net/tcp文件,向每个开放的端口发送 D-Bus 的
认证消息 AUTH ,如果端口回复了 REJECT ,那么这个端口就是frida-server
绕过脚本参考
-
anti_fgets();
anti_exit();
anti_fork();
anti_kill();
anti_ptrace();
https://github.com/deathmemory/FridaContainer/blob/011c205d5ef1372b697ed04a66f69b8fd75680ed/utils/android/Anti.ts -
签名校验之类
https://github.com/axhlzy/Il2CppHookScripts/blob/862b39b695540008ff06ed2b15e1a1ec98b44fa3/Others/FTS/fts.js
系统
- frida syscalls https://github.com/FrenchYeti/frida-syscall/blob/main/index.js
- frida syscalls https://github.com/AeonLucid/frida-syscall-interceptor
- 内联汇编https://www.jianshu.com/p/7f5081b9f000
- io重定向http://www.yxfzedu.com/rs_show/1447
写的修改系统源码
https://blog.csdn.net/weixin_42453905/article/details/122462984?spm=1001.2014.3001.5501
参考文章
https://blog.csdn.net/lintax/article/details/70988565
https://www.jianshu.com/p/c37b1bdb4757
https://forum.butian.net/share/776
http://pwn4.fun/2017/04/15/Android%E9%80%86%E5%90%91%E4%B9%8B%E5%8F%8D%E8%B0%83%E8%AF%95/
https://coar.wang/article/42
https://www.freebuf.com/articles/mobile/291894.html
https://www.jianshu.com/p/55d7bb3ba5af