其他
对安卓反调试和校验检测的一些实践与结论
在学习过程中对网上的一些反调试手段进行了实践,以下为产生的结论和一些问题。
反调试来源参考:https://bbs.pediy.com/thread-223324.htm
测试机器:小米6x开发版
1 • 调试端口检测
实现代码:
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 • 调试进程名检测
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 • 父进程名检测
int CheckParents()//检查父进程是不是zygote
{
// 设置buf
char strPpidCmdline[0x100]={0};
FILE *file;
snprintf(strPpidCmdline, sizeof(strPpidCmdline), "/proc/%d/cmdline", getppid());
// 打开文件
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,已经失效,由于安全性更新无法访问此目录。
这里感谢pareto的指正,app层在实机下是只能访问自身/proc/pid下的文件,并非完全无法访问,这使得我们可以通过检查Tracerpid这个属性进行反调试。
4 • APK线程检测
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;
}
测试效果:可以先测试自身app产生的线程数目,再判断是否被调试,如果线程数目显著大于测试得到的结果,那可以认为遭到了注入或者调试。
5 • 安卓系统自带调试检测函数
分析android自带调试检测函数isDebuggerConnected(),返回是否处于调试。
public boolean getDebugEvent()
{
return Debug.isDebuggerConnected();
}
测试效果:系统自带函数,很好用,几行代码就可以实现,简单粗暴。
结论:由于是在java层实现的,容易被静态去除掉,当然,也可以hook系统的这个方法直接绕过。
6 • ptrace检测
int ptrace_protect()//ptrace附加自身线程 会导致此进程TracerPid 变为父进程的TracerPid 即zygote
{
return ptrace(PTRACE_TRACEME,0,0,0);;//返回-1即为已经被调试
}
在实机上面效果:
结论:在之前做题的时候就发现了,ptrace反调试的效果一下有一下没有,不是很稳定,也容易被静态修改,所以有使用内联汇编的方式使用。而且由于其被大量使用,许多修改的镜像直接在安卓源码层面对ptrace进行了反反调试。不过ptrace占坑的思路非常有用,也就衍生出了一些利用占坑的方式的反调试。总的来说有好过没有。
7 • TracerPid检测
int get_app_TracerPid()
{
char strPpidCmdline[0x300]={'0'};
FILE *file;
snprintf(strPpidCmdline, sizeof(strPpidCmdline), "/proc/%d/status", getpid());
file = fopen(strPpidCmdline,"r");
if(!file)
{
//printf("文件不存在\n");
return 1;
}
memset(strPpidCmdline,0,sizeof(strPpidCmdline));//清空之前使用的数据
fread(strPpidCmdline,1,sizeof(strPpidCmdline),file);
int TracerPid_pos=0;
int i=0;
while(i!=300)
{
if(strPpidCmdline[i]=='T')
{
if(strPpidCmdline[i+1]=='r')
{
if(strPpidCmdline[i+2]=='a')
{
if(strPpidCmdline[i+3]=='c')
{
if(strPpidCmdline[i+4]=='e')
{
if(strPpidCmdline[i+5]=='r')
{
if(strPpidCmdline[i+6]=='P')
{
if(strPpidCmdline[i+7]=='i')
{
if(strPpidCmdline[i+8]=='d')
{
TracerPid_pos=i;
break;
}
}
}
}
}
}
}
}
}
i++;
}
//TracerPid_pos = TracerPid_pos+10;//+10进入到末尾
int digits=0;//记录一共有几位
int on=0;//是否找到数字
int result=0;
i=0;
int j=0;
while(on==0)
{
if(strPpidCmdline[TracerPid_pos+i]>47&&strPpidCmdline[TracerPid_pos+i]<58)
{
digits++;
if(strPpidCmdline[TracerPid_pos+i+1]<47||strPpidCmdline[TracerPid_pos+i]>58)
{
on=1;
while(j!=digits)
{
result += (strPpidCmdline[TracerPid_pos+i-j]-0x30)*pow(10.0,j);
j++;
}
break;
}
}
i++;
if(i>200)
{
break;
}
}
return result;
}
8 • 断点指令检测
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;
}
结论:效果不错,但是对性能开销较大。这里有必要提一下和这种方法有点相似的方法,即smc自解密,如果在自解密的代码上面下了断点,被解密之后还原出来的代码就是错误的。
9 • 签名校验
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;
}
10 • debuggable属性
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先截获信号特性的检测
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;
}
13 • 利用IDA解析缺陷反调试
参考题目:https://ctf.pediy.com/game-season_fight-174.htm
参考wp:https://bbs.pediy.com/thread-267627.htm
14 • 代码执行时间检测
原理:
一段代码,在a处获取一下时间,运行一段后,再在b处获取下时间,然后通过(b时间a时间)求时间差,正常情况下这个时间差会非常小,如果这个时间差比较大,说明正在被单步调试。
写在最后:
看雪ID:Dyingchen
https://bbs.pediy.com/user-home-857117.htm
*本文由看雪论坛 Dyingchen 原创,转载请注明来自看雪社区# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!