首页
论坛
课程
招聘
[原创]对安卓反调试和校验检测的一些实践与结论
2021-6-20 20:44 12354

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

2021-6-20 20:44
12354

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

反调试来源参考: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", 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线程检测

正常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;
}


测试效果:可以先测试自身app产生的线程数目,再判断是否被调试,如果线程数目显著大于测试得到的结果,那可以认为遭到了注入或者调试



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

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

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

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

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


6.ptrace检测

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

实现代码:

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


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


然而

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

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


在实机上面效果:

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

关闭seLinux之后

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

无法附加,效果正常


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


7:TracerPid检测

被成功ptrace的app的/proc/pid/status文件中的Tracerpid会变为ptrace它的父进程(一般来说是zygote),而在正常情况下,这个值为0


实现代码

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


结论:这个反调试由于之前提到的ptrace的不稳定,使用这个反调试和ptrace反调试共同使用的情况可能会遇到ptrace成功,Tracerpid变为zygote的进程号,ptrace失败,Tracerpid为0,这里就需要使用者自行判断应用场景了,根据不同的场景进行不同的检测



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找到关键点



写在最后:

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






[注意] 欢迎加入看雪团队!base上海,招聘安全工程师、逆向工程师多个坑位等你投递!

最后于 2021-6-23 18:03 被Dyingchen编辑 ,原因:
收藏
点赞7
打赏
分享
最新回复 (13)
雪    币: 2208
活跃值: 活跃值 (698)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
木志本柯 活跃值 2021-6-21 10:24
2
0
听说 模拟器都是修改了Android内核源码 始终返回正常的值 来 过一些签名检测  和 反调试
雪    币: 2365
活跃值: 活跃值 (1118)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Dyingchen 活跃值 2021-6-21 10:33
3
0
木志本柯 听说 模拟器都是修改了Android内核源码 始终返回正常的值 来 过一些签名检测 和 反调试
是吗,这个不太清楚,不过由于有些在源码层面上面对ptrace等检测进行了始终返回0的情况,那么自己主动多次调用ptrace都会返回0,从这一点上进行检测是否可行
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
playSCforeve 活跃值 2021-6-21 11:05
4
0
木志本柯 听说 模拟器都是修改了Android内核源码 始终返回正常的值 来 过一些签名检测 和 反调试
已经检测出模拟器了,就不用管是不是调试了
雪    币: 441
活跃值: 活跃值 (635)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
pareto 活跃值 2021-6-23 10:23
5
0
"结论:基本来说直接读取/proc实现的反调试已经无法使用了,有没有其他的办法访问暂且不知道,有了解的师傅可以说一下"
这个失效了,是什么原因?
雪    币: 2365
活跃值: 活跃值 (1118)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Dyingchen 活跃值 2021-6-23 12:25
6
0
pareto "结论:基本来说直接读取/proc实现的反调试已经无法使用了,有没有其他的办法访问暂且不知道,有了解的师傅可以说一下" 这个失效了,是什么原因?
可以去搜索一下,安卓版本更新之后在app层就没法访问/proc里面的内容了,其实我本身代码写了很多关于这个的反调试,但在真机上面发现无效之后就都没有放出来了
雪    币: 441
活跃值: 活跃值 (635)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
pareto 活跃值 2021-6-23 14:46
7
0
Dyingchen 可以去搜索一下,安卓版本更新之后在app层就没法访问/proc里面的内容了,其实我本身代码写了很多关于这个的反调试,但在真机上面发现无效之后就都没有放出来了
/proc/下进程私有的,还是可以访问的吧,还是这里你读取的是其他进程的文件。
雪    币: 2365
活跃值: 活跃值 (1118)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Dyingchen 活跃值 2021-6-23 17:34
8
0
pareto /proc/下进程私有的,还是可以访问的吧,还是这里你读取的是其他进程的文件。
感谢指正,不过虽然可以访问自身进程的文件,由于没法访问其他进程的cmdline和status 得到的信息还是少了很多,我着手准备修改一下文章
雪    币: 5425
活跃值: 活跃值 (3255)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
GitRoy 活跃值 3 2021-6-23 20:15
9
0
mark
雪    币: 4475
活跃值: 活跃值 (2262)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
[軍] 活跃值 1 2021-6-24 14:12
10
0
isDebuggerConnected()在android6.0之上已经不能通过手动绕过了,并且在java层hook时ida会卡死。尚不清楚在native的hook能不能绕过。
雪    币: 1439
活跃值: 活跃值 (949)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
TUGOhost 活跃值 2021-6-25 15:34
11
0
应该是/proc/pid/net/tcp,app可以访问自己的pid的这个目录,还有一些防注入的手段也可以在/proc/pid/中其他文件进行实现
雪    币: 4
活跃值: 活跃值 (24)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
维达3层加厚 活跃值 2021-6-26 16:04
12
0
mark
雪    币: 1454
活跃值: 活跃值 (2576)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lhxdiao 活跃值 2021-6-26 16:11
13
0
吃鸡、王者荣耀外挂里面使用了一堆ptrace反检测技术,可以参考一下里面对应的检测、反调试内容,比较有趣。
雪    币: 233
活跃值: 活跃值 (599)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kakasasa 活跃值 2021-6-28 18:39
14
0
mark
游客
登录 | 注册 方可回帖
返回