Android反调试手段收集
恶意应用可以用来躲避杀软监测,正常应用也可以用来保护代码不被窃取。
1,ptrace检测
一个进程只能被一个进程追踪,程序中添加追踪自身的代码导致ida附加失败,无线实现调试。
void ptraceCheck()
{
ptrace(PTRACE_TRACEME, 0, 0, 0);
}
或者检测返回值,
void ptraceCheck()
{
int ck=ptrace(PTRACE_TRACEME, 0, 0, 0);
if(ck == -1)
{
LOGA("进程正在被调试\n");
return;
}else
{
LOGB("ptrace的返回值为:%d\n",ck);
return;
}
}
2,检测TracerPid的值
原理同1,正常情况下TracePid为0,进程被追踪时TracePid的值会变成追踪进程的Pid
void anti_debug02(){
const int bufsize=1024;
char filename[bufsize];
char line [bufsize];
int pid=getpid();//获取目前进程的进程的Pid
FILE *fp;
sprintf(filename,"proc/%d/status",pid);
fp=fopen(filename,"r");//
if (fp!= NULL){
while(fgets(line,bufsize,fp)){
if(strncmp(line,"TracerPid",9)==0){
int status=atoi(&line[10]);
if(status!=0){
fclose(fp);//先关闭
LOGD("%s","antidebug02 run exit");
int ret=kill(pid,SIGKILL);
}
break;
}
LOGD("%s","no antidebug02 run");
}
}
对抗方法:修改内核代码编译刷机,代码修改位置kernel/msm/fs/proc/base.c
和kernel/msm/fs/proc/array.c
如果不想编译内核也可以修改手机内核绕过反调试https://www.jianshu.com/p/91aa37f3a972
反对抗方法:创建一个子进程,让子进程主动ptrace自身设为调试状态,此时的子进程的tracepid应该不为0。如果检测到子进程的tracepid为0,说明源码被修改了。
3,检测ida常用端口
ida的默认端口是23946,检测端口是否被占用确认是否是调试状态。
void CheckPort23946()
{
FILE* pfile=NULL;
char buf[0x1000]={0};
char* strCatTcp= "cat /proc/net/tcp |grep :5D8A";
//char* strNetstat="netstat |grep :23946";
pfile=popen(strCatTcp,"r");
if(NULL==pfile)
{
LOGA("未发现23946端口占用\n");
return;
}
while(fgets(buf,sizeof(buf),pfile))
{
// 检测到23946被占用
LOGA("执行cat /proc/net/tcp |grep :5D8A的结果:%s\n",buf);
}//while
pclose(pfile);
}
对抗方法:ida调试时可通过-p
命令修改端口
tips:可直接修改ida中的android_server文件,免去每次都要加参数的麻烦。
4,检测Android_server文件是否存在
void checkAndroid_serverFile(){
const char* rootPath = "/data/local/tmp";
DIR* dir;
dir = opendir(rootPath);
if (dir!= NULL) {
dirent *currentDir;
while ((currentDir = readdir(dir)) != NULL) {
if(strncmp(currentDir->d_name,"android_server",14)==0){
LOGD("%s",currentDir->d_name);
LOGD("%s","发现android_server");
}
}
closedir(dir);
} else{
LOGD("%s","dir not access");
}
}
对抗方法:换个文件名即可;
5,检测android_server进程是否存在
执行ps命令获取进程列表查找到android_server名即可确认在调试
void SearchObjProcess()
{
FILE* pfile=NULL;
char buf[0x1000]={0};
pfile=popen("ps","r");
if(pfile == NULL)
{
LOGA("ps命令失败!\n");
return;
}
while(fgets(buf,sizeof(buf),pfile))
{
LOGB("遍历进程:%s\n",buf);
char* strA=NULL;
strA=strstr(buf,"android_server");
if(strA != NULL)
{
LOGB("发现调试进程:%s\n",buf);
}
}
pclose(pfile);
}
6,检测apk中的线程数量
正常apk运行加载so时会由多个线程,写可执行文件加载so的时候只有一个线程,可以检测线程数量来判断运行环境是否正常。
void CheckThreadNum()
{
char buf[0x100] = {0};
char* str = "/proc/%d/task";
snprintf(buf, sizeof(buf), str, getpid());
DIR* pdir = opendir(buf);
if (!pdir)
{
LOGA("任务文件打开失败。\n");
return;
}
struct dirent* pde=NULL;
int Num=0;
while ((pde = readdir(pdir)))
{
if ((pde->d_name[0] <= '9') && (pde->d_name[0] >= '0'))
{
++Count;
LOGB("%d 线程名称:%s\n",Num,pde->d_name);
}
}
LOGB("线程个数为:%d",Num);
if(Num<=1)
{
LOGA("只有一个线程,确定是调试状态!\n");
}
int i=0;
return;
}
7,检测调试状态下的软件断点
调试时在函数中下了断点,地址就会被改成bkpt指令,可以通过在函数中搜索bkpt指令来检测断点。
void checkBreakPoint(){
Elf32_Ehdr *elfhdr;
Elf32_Phdr *pht;
unsigned int size, base, offset,phtable;
int n, i,j;
char *p;
base = GetLibAddr();
if(base == 0){
LOGD("find base error/n");
return;
}
elfhdr = (Elf32_Ehdr *) base;
phtable = elfhdr->e_phoff + base;
for(i=0;i<elfhdr->e_phnum;i++){
pht = (Elf32_Phdr*)(phtable+i*sizeof(Elf32_Phdr));
if(pht->p_flags&1){
offset = pht->p_vaddr + base + sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr)*elfhdr->e_phnum;
LOGD("offset:%X ,len:%X",offset,pht->p_memsz);
p = (char*)offset;
size = pht->p_memsz;
for(j=0,n=0;j<size;++j,++p){
if(*p == 0x10 && *(p+1) == 0xde){
n++;
LOGD("### find thumb bpt %X /n",p);
}else if(*p == 0xf0 && *(p+1) == 0xf7 && *(p+2) == 0x00 && *(p+3) == 0xa0){
n++;
LOGD("### find thumb2 bpt %X /n",p);
}else if(*p == 0x01 && *(p+1) == 0x00 && *(p+2) == 0x9f && *(p+3) == 0xef){
n++;
LOGD("### find arm bpt %X /n",p);
}
}
LOGD("### find breakpoint num: %d/n",n);
}
}
}
8,检测代码运行时间差
利用调试时函数的运行时间差来检测,过长则判定为调试
int gettimeofday(struct timeval *tv, struct timezone *tz);
void checkTimeDiff()
{
int pid = getpid();
struct timeval t1;
struct timeval t2;
struct timezone tz;
gettimeofday(&t1, &tz);
gettimeofday(&t2, &tz);
int timeoff = (t2.tv_sec) - (t1.tv_sec);
if (timeoff > 1) {
int ret = kill(pid, SIGKILL);
return ;
}
}
9,单步调试陷阱
调试器从下断点到执行断点的过程分析:
- 保存:保存目标处指令
- 替换:目标处指令替换为断点指令
- 命中断点:命中断点指令(引发中断 或者说发出信号)
- 收到信号:调试器收到信号后,执行调试器注册的信号处理函数。
- 恢复:调试器处理函数恢复保存的指令
- 回退:回退PC寄存器
- 控制权回归程序.
主动设置断点指令/注册信号处理函数的反调试方案:
- 在函数中写入断点指令
- 在代码中注册断点信号处理函数
- 程序执行到断点指令,发出信号
分两种情况:
- 非调试状态
进入自己注册的函数,NOP指令替换断点指令,回退PC后正常指令。
(执行断点发出信号—进入处理信号函数—NOP替换断点—退回PC) - 调试状态
进入调试器的断点处理流程,他会恢复目标处指令失败,然后回退PC,进入死循环。
10,利用ida先截获信号的特性
IDA会首先截获信号,导致进程无法接收到信号,导致不会执行信号处理函数。将关键流程
放在信号处理函数中,如果没有执行,就是被调试状态。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
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("信号返回错误!\n");
printf("signal ret value is %x\n",(unsigned char*)g_ret);
raise(SIGTRAP);
raise(SIGTRAP);
raise(SIGTRAP);
kill(getpid(), SIGTRAP);
printf("main.\n");
return 0;
}
参考:http://zt.360.cn/1101061855.php?dtid=1101061451&did=210078060