10

[原创]对安卓反调试和校验检测的一些实践与结论

 3 years ago
source link: https://bbs.pediy.com/thread-268155.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在学习过程中对网上的一些反调试手段进行了实践,以下为产生的结论和一些问题

反调试来源参考:https://bbs.pediy.com/thread-223324.htm

测试机器:小米6x开发版 

1.调试端口检测

原理:读取/proc/net/tcp,查找IDA远程调试所用的23946端口,若发现说明进程正在被IDA调试。

这里需要说明的是由于安卓安全性更新,/proc等目录在app里面已经没法直接访问了,导致通过查找/proc/net/tcp这个方法直接宣告无效,但是通过别的办法查找端口还是可行的

实现代码:

public static  boolean isPortUsing(String host,int port) throws UnknownHostException
{
boolean flag = false;
InetAddress Address = InetAddress.getByName(host);
try
{
Socket socket = new Socket(Address,port);  //建立一个Socket连接
if(socket.isClosed()==true)
{
flag = true;
}
else//不管断没断开连接都认为是已经有一个Socket存在了
{
flag =true;
}
}
catch (IOException e)
{
e.printStackTrace();
}
return flag;
}

这里需要注意的是对网络相关使用不能放在主线程里面否则会报错,建议配合线程使用

new Thread(new Runnable()
{
@Override
public void run()
{
try
{
portEvent = getPortUsingEvent.isPortUsing("127.0.0.1",23946);
outputView.setText("端口23946占用状态"+String.valueOf(portEvent));
catch (UnknownHostException e)
{
e.printStackTrace();
}
}
}).start();

相关文章:https://bbs.pediy.com/thread-268080.htm

结论:尽管可以通过检测端口的方式进行反调试,但是IDA的服务端也是可以更改端口的

2.调试进程名检测

遍历进程,查找固定的进程名,找到说明调试器在运行。(比如说固定的进程名 android_server gdb_server等等)

实现代码:

int SearchObjProcess()
{
FILE* pfile=NULL;
char buf[0x1000]={0};
pfile=popen("ps","r");
if(NULL==pfile)
{
//LOGA("SearchObjProcess popen打开命令失败!\n");
return -1;
}
// 获取结果
//LOGA("popen方案:\n");
while(fgets(buf,sizeof(buf),pfile))
{
char* strA=NULL;
char* strB=NULL;
char* strC=NULL;
char* strD=NULL;
strA=strstr(buf,"android_server");//通过查找匹配子串判断
strB=strstr(buf,"gdbserver");
strC=strstr(buf,"gdb");
strD=strstr(buf,"fuwu");
if(strA || strB ||strC || strD)
{
return 1;
// 执行到这里,判定为调试状态
}
}
pclose(pfile);
return 0;
}

测试效果:无论是打开了android_server 还是没有打开都返回了0,即没有检测到,关掉seLinux之后可行

结论:可能是写法比较过时了,这个代码实现需要关掉seLinux才有效那便形同虚设,待补充可行的方案

3.父进程名检测

有的时候不使用apk附加调试的方法进行逆向,而是写一个.out可执行文件直接加载so进行

调试,这样程序的父进程名和正常启动apk的父进程名是不一样的。

读取/proc/pid/cmdline,查看内容是否为zygote

实现代码:

int CheckParents()//检查父进程是不是zygote
{
// 设置buf
char strPpidCmdline[0x100]={0};
FILE *file;
snprintf(strPpidCmdline, sizeof(strPpidCmdline), "/proc/%d/cmdline", getpid());
// 打开文件
file = fopen(strPpidCmdline,"r");
if(!file)
{
//LOGA("CheckParents open错误!\n");
return 1;
}
// 文件内容读入内存
memset(strPpidCmdline,1,sizeof(strPpidCmdline));
ssize_t ret=fread(strPpidCmdline,1,sizeof(strPpidCmdline),file);
if(-1==ret)
{
//LOGA("CheckParents read错误!\n");
return 2;
}
// 没找到返回0
char *sRet=strstr(strPpidCmdline,"zygote");
if(NULL==sRet)
{
// 执行到这里,判定为调试状态
//LOGA("父进程cmdline没有zygote子串!\n");
return 3;
}
return 4;//正常
}

测试效果:模拟器上面测试正常,但是实机就无效了

同1,已经失效,由于安全性更新无法访问此目录

结论:基本来说直接读取/proc实现的反调试已经无法使用了,有没有其他的办法访问暂且不知道,有了解的师傅可以说一下

4.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;
}

测试效果:同上,已经失效

5.APK进程FD文件检测

根据/proc/pid/fd/路径下文件的个数差异,判断进程状态。

测试效果:同上,已经失效

6.安卓系统自带调试检测函数

分析android自带调试检测函数isDebuggerConnected(),返回是否处于调试

public boolean getDebugEvent()
{
return Debug.isDebuggerConnected();
}

测试效果:系统自带函数 很好用 几行代码就可以实现 简单粗暴

结论:由于是在java层实现的,容易被静态去除掉,当然,也可以hook系统的这个方法直接绕过

7.ptrace检测

每个进程同时刻只能被1个调试进程ptrace
主动ptrace本进程可以使得其他调试器无法调试

实现代码:

int ptrace_protect()//ptrace附加自身线程 会导致此进程TracerPid 变为父进程的TracerPid 即zygote
{
return ptrace(PTRACE_TRACEME,0,0,0);;//返回-1即为已经被调试
}

非常简单粗暴,通过占坑的方式使得其他调试器无法调试,应该是用得最多的反调试之一了

测试效果:使用ptrace对自身进行附加,在模拟器上效果

857117_R2XQ8JMUKQU2PUQ.jpg

返回值为0,正常(一般调试so也不会用模拟器阿)

在实机上面效果:spacer.gifspacer.gif

857117_83E44RPG4E65A6B.jpg

返回值为-1,为了排除返回值的问题,在此状态下使用IDA进行调试,发现可以调试

关闭seLinux之后

857117_X58ZDJFQ6TX5E8U.jpg

返回值变为0,尝试使用IDA附加

spacer.gifspacer.gif857117_2QA2SU6GDPAAGFX.jpg

无法附加,效果正常

结论:在之前做题的时候就发现了,ptrace反调试的效果一下有一下没有,不是很稳定,也容易被静态修改,所以有使用内联汇编的方式使用,而且由于其被大量使用,许多修改的镜像直接在安卓源码层面对ptrace进行了反反调试。不过ptrace占坑的思路非常有用,也就衍生出了一些利用占坑的方式的反调试。总的来说有好过没有

8.断点指令检测

如果函数被下软件断点,则断点地址会被改写为bkpt指令,

可以在函数体中搜索bkpt指令来检测软件断点

代码实现:

typedef uint8_t u8;
typedef uint32_t u32;
int checkbkpt(u8* addr,u32 size)
{
// 结果
u32 uRet=0;
// 断点指令
// u8 armBkpt[4]={0xf0,0x01,0xf0,0xe7};
// u8 thumbBkpt[2]={0x10,0xde};
u8 armBkpt[4]={0};
armBkpt[0]=0xf0;
armBkpt[1]=0x01;
armBkpt[2]=0xf0;
armBkpt[3]=0xe7;
u8 thumbBkpt[2]={0};
thumbBkpt[0]=0x10;
thumbBkpt[1]=0xde;
// 判断模式
int mode=(u32)(size_t)addr%2;
if(1==mode)
{
//LOGA("checkbkpt:(thumb mode)该地址为thumb模式\n");
u8* start=(u8*)((u32)(size_t)addr-1);
u8* end=(u8*)((u32)(size_t)start+size);
// 遍历对比
while(1)
{
if(start >= end)
{
uRet=0;
// LOGA("checkbkpt:(no find bkpt)没有发现断点.\n");
break;
}
if( 0==memcmp(start,thumbBkpt,2) )
{
uRet=1;
//LOGA("checkbkpt:(find it)发现断点.\n");
return -1;
}
start=start+2;
}//while
}//if
else
{
//LOGA("checkbkpt:(arm mode)该地址为arm模式\n");
u8* start=(u8*)addr;
u8* end=(u8*)((u32)(size_t)start+size);
// 遍历对比
while(1)
{
if (start >= end)
{
uRet = 0;
//LOGA("checkbkpt:(no find)没有发现断点.\n");
break;
}
if (0 == memcmp(start,armBkpt , 4))
{
uRet = 1;
//LOGA("checkbkpt:(find it)发现断点.\n");
return -1;
}
start = start + 4;
}//while
}//else
return 0;
}

测试效果:通过查找有没有断点指令进行反调试,反调试效果很不错,但是性能上面就有点问题了。其实现就是不断遍历对比,这里出现两个问题

1:会花费大量资源进行这个查找的行为

2:什么时候进行查找?--如果只在相对较早的时机进行一次,那么后面的时机下了断点则没有效果,这意味着需要进行循环查找--性能花销太大

结论:效果不错,但是对性能开销较大。这里有必要提一下和这种方法有点相似的方法 即smc自解密,如果在自解密的代码上面下了断点,被解密之后还原出来的代码就是错误的

9:签名校验

顾名思义,app被重打包之后签名就会出现改变,可以检测签名防止重打包

public int getSignature(String packageName, PackageManager pm)
{
PackageInfo pi = null;
int sig=0;
try
{
pi = pm.getPackageInfo(packageName,PackageManager.GET_SIGNATURES);
Signature[] signatures = pi.signatures;
sig = signatures[0].hashCode();
}
catch (Exception errno)
{
sig =- 1;
errno.printStackTrace();
}
return sig;
}

结论:签名校验的效果很强,但是市面上已经有很多过签名的通用方法了(参考np管理器,mt管理器)

10:debuggable属性

这个属性是manifest文件中的属性,没有这个属性的话就没法调试java层,ddms也看不到没有这个属性的app进程(刷过机改过的镜像可以看到)

这里给出一个检测debuggable被设置为true的反调试实现代码:

public boolean getAppCanDebug(Context context)//上下文对象为xxActivity.this
{
boolean isDebug = context.getApplicationInfo() != null &&
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
return isDebug;
}

测试效果:我感觉这个也不能算反调试了应该,但是很多新手会踩到这个坑

结论:效果很强,即使知道这个属性也经常忘记导致找半天原因(主要还是调试机器有的直接在源码层面忽略了这个属性,而有的则没有),这里给出一种便捷的绕过方法

https://bbs.pediy.com/thread-267675.htm

11:哈希校验

对需要保护的一块区域的代码进行哈希检测

实现代码:待补充

相关文章:https://www.52pojie.cn/thread-1429241-1-1.html

结论:比起上面说到过的断点指令检测效果好一些,可以循环使用,但也容易被跟踪哈希函数调用找到关键位置从而静态修改

12.利用IDA先截获信号特性的检测

IDA会首先截获信号,导致进程无法接收到信号,导致不会执行信号处理函数。将关键流程
放在信号处理函数中,如果没有执行,就是被调试状态。

实现代码:

void myhandler(int sig)
{
//signal(5, myhandler);
printf("myhandler.\n");
return;
}
int g_ret = 0;
int main(int argc, char **argv)
{
// 设置SIGTRAP信号的处理函数为myhandler()
g_ret = (int)signal(SIGTRAP, myhandler);
if ( (int)SIG_ERR == g_ret )
printf("signal ret value is SIG_ERR.\n");
// 打印signal的返回值(原处理函数地址)
printf("signal ret value is %x\n",(unsigned char*)g_ret);
// 主动给自己进程发送SIGTRAP信号
raise(SIGTRAP);
raise(SIGTRAP);
raise(SIGTRAP);
kill(getpid(), SIGTRAP);
printf("main.\n");
return 0;
}

结论:这个我认为是比较靠谱的反调试,因为IDA在调试过程中可能会遇到大量的信号,尽管可以设置忽略,但真假难辨,需要很多时间进行分析

13.利用IDA解析缺陷反调试

IDA采用递归下降算法来反汇编指令,而该算法最大的缺点在于它无法处理间接代码路径,

无法识别动态算出来的跳转。而arm架构下由于存在arm和thumb指令集,就涉及到指令集

切换,IDA在某些情况下无法智能识别arm和thumb指令,进一步导致无法进行伪代码还原。

在IDA动态调试时,仍然存在该问题,若在指令识别错误的地点写入断点,有可能使得调试

参考题目: https://ctf.pediy.com/game-season_fight-174.htm  

参考wp:https://bbs.pediy.com/thread-267627.htm 

其他:待补充

14.代码执行时间检测

一段代码,在a处获取一下时间,运行一段后,再在b处获取下时间,

然后通过(b时间­a时间)求时间差,正常情况下这个时间差会非常小,

如果这个时间差比较大,说明正在被单步调试。

五个能获取时间的api:

time()函数

time_t结构体

clock()函数

clock_t结构体

gettimeofday()函数

timeval结构

timezone结构

clock_gettime()函数

timespec结构

getrusage()函数

rusage结构

代码实现:待补充

结论:效果很不错,但是容易被跟踪相关api找到关键点

写在最后:

参考借鉴了很多前辈的代码,不过更重要的是自己去跟着实现一遍,确定能不能用和问题所在才是关键,这便是学习的过程,过程难免有出现错误,如果有发现我的错误的话,希望大家能够给我指出,共同学习

[看雪官方培训] Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年秋季班火热招生!!

最后于 1天前 被Dyingchen编辑 ,原因:

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK