[IOV安全入门] 十. Android安全之DDMS反调试检测说明

[IOV安全入门] 十. Android安全之DDMS反调试检测说明,第1张

[IOV安全入门] 十. Android安全之DDMS反调试检测说明

欢迎新同学的光临
… …
人若无名,便可专心练剑


我不是一条咸鱼,而是一条死鱼啊!


0x01 前言

反调试检测是为了应对现在很多破解者使用IDA进行动态方式调试so文件,从而获取重要的信息,知道IDA进行so动态调试是基于进程的注入技术,然后使用Linux中的ptrace机制,进行调试目标进程的附加 *** 作。ptrace机制有一个特点,如果一个进程被调试了,在它进程的status文件中有一个字段TracerPid会记录调试者的进程id值

adb shell
cat /proc/[myPid]/status

在第六行,有一个TracerPid字段,就是记录了调试者的进程id,可以轮询遍历自己进程的status文件,然后读取TracerPid字段值,如果发现它大于0,就代表着自己的应用在被人调试,所以就立马退出程序

地址:https://github.com/GToad/Android_Anti_Debug/releases/tag/1.0.0

0x02 十六种反调试 2.1 IDA调试端口检测
  • 原理:调试器远程调试时,会占用一些固定的端口

  • 做法:读取/proc/net/tcp,查找IDA远程调试所用的端口23946端口,监测android_server文件端口信息 默认的23946(5D8A),若发现说明进程正在被IDA动态调试

void CheckPort23946ByTcp()
{
    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("CheckPort23946ByTcp popen打开命令失败!n");
       return;
    } 
    // 获取结果
    while(fgets(buf,sizeof(buf),pfile))
    {
        // 执行到这里,判定为调试状态
        LOGA("执行cat /proc/net/tcp |grep :5D8A的结果:n");
        LOGB("%s",buf);
     }//while
     pclose(pfile);
}
  • 解决办法
    • 运行IDA调试时,更改端口
    • 运行调试文件:/data/local/tmp/android_server -p123456
    • 端口转发:adb forward tcp:123456 tcp:123456
2.2 调试器进程名检测
  • 原理:远程调试要在手机中运行android_server、gdbserver、gdb等进程.

  • 做法:遍历进程查找固定的进程名,如果找到,说明调试器在运行(通过ps命令来监测有没有相应的进程信息)

void SearchObjProcess()
{
    FILE* pfile=NULL;
    char buf[0x1000]={0};
    // 执行命令
    //pfile=popen("ps | awk '{print }'","r"); // 部分不支持awk命令
    pfile=popen("ps","r");
    if(NULL==pfile)
    {
        LOGA("SearchObjProcess popen打开命令失败!n");
        return;
    } 
    // 获取结果
    LOGA("popen方案:n");
    while(fgets(buf,sizeof(buf),pfile))
    {
        // 打印进程
        LOGB("遍历进程:%sn",buf);
        // 查找子串
        char* strA=NULL,strB=NULL,strC=NULL,strD=NULL;
        strA=strstr(buf,"android_server");
        strB=strstr(buf,"gdbserver");
        strC=strstr(buf,"gdb");
        strD=strstr(buf,"fuwu");
        if(strA || strB ||strC || strD)
        {
            // 执行到这里,判定为调试状态
            LOGB("发现目标进程:%sn",buf);
        }//if
    }//while
    pclose(pfile);
}
  • 解决办法
    • 修改进程名(更改android_server文件名),随便改一个
2.3 父进程名检测
  • 原理:有的时候不使用apk附加调试的方法进行逆向,而是写一个.out可执行文件直接加载so进行调试,这样程序的父进程名和正常启动apk的父进程名是不一样的

  • 测试发现如下问题

    • a. 正常启动的apk程序 : 父进程是zygote
    • b. 调试启动的apk程序 : 在AS中用LLDB调试发现父进程还是zygote
    • c. 附加调试的apk程序 : 父进程是zygote
    • d. vs远程调试 用可执行文件加载so : 父进程名为gdbserver
    • 结论:父进程名非zygote的,判断为调试状态.
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;
}
  • 解决方法
    • 附加调试,查看读取/proc/pid/cmdline,查看内容是否为zygote
2.4 自身进程名检测
  • 原理:和父进程名检测,也是写个.out加载so来脱壳的场景,正常进程名一般是apk的com.xxx这样的格式

  • 解决方法

    • 附加调试,查看读取/proc/pid/cmdline,查看内容是否为zygote
2.5 apk线程数量检测
  • 原理:同.out加载so来脱壳的场景,正常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)))
    {
        //字符过滤,每个文件都是一个线程id
        if((pde->d_name[0]<='9')&&(pde->d_name[0]>='0'))
        {
            count++;
            printf("%d 线程名称:%sn",count,pde->d_name);
        }
        if(count<=1)
        {
            //说明被调试了
            printf("被调试了");
        }
        return;
    }

}
  • 解决方法
    • 找到检测点nop掉
2.6 apk进程fd文件检测
  • 原理:根据/proc/pid/fd/路径下文件的个数差异,判断进程状态
    • apk启动的进程和非apk启动的进程fd数量不一样
    • apk的debug启动的正常启动,进程fd数量也不一样

检测apk是否以debug模式启动 即使进行ida调试了 程序的大部分功能依然没有运行 所以fd文件较正常偏少

  • 解决方法
    • 找到检测点nop掉
2.7 安卓系统自带调试检测函数
  • //android.os.Debug.isDebuggerConnected();

  • 原理:分析android自带调试检测函数isDebuggerConnected()在native的实现,尝试在native使用

  • 做法

    • a. dalvik模式下:
      • 找到进程中libdvm.so中的dvmDbglsDebuggerConnected()函数,调用它就能知道程序是否被调试
      • dlopen(/system/lib/libdvm.so)
      • dlsym(_Z25dvmDbgIsDebuggerConnectedv)
typedef unsigned char wbool;
typedef wbool (*ppp)();
void NativeIsDBGConnected()
{
    void* Handle=NULL;
    Handle=dlopen("/system/lib/libdvm.so",RTLD_LAZY);
    if(Handle==NULL)
    {
        return;
    }
    ppp Fun=(ppp)dlsym(Handle,"_Z25dvmDbgIsDebuggerConnect"); //根据动态链接库的句柄和符号名,返回地址
    if(Fun==NULL) {
        printf("获取函数地址失败");
        return;
    } else
    {
        wbool ret=Fun();
        if(ret==1)
        {
            printf("被调试了");
            return;
        }
    }
}
  • b. art模式下:
    • art模式下,结果存放在libart.so中的全局变量gDebuggerAvtive中,符号名为_ZN3art3Dbg15gDebuggerActiveE
    • 但是貌似新版本android不允许使用非ndk原生库,dlopen(libart.so)会失败
    • 方法,手动在内存中搜索artlib模块,然后手工寻找该全局变量符号
void checkPtrace()
{
    int iRet;
    iRet=ptrace(PTRACE_TRACEME,0,0,0);
    if(iRet==-1)
    {
        //说明父进程调试失败,说明进程已经被别的进程ptrace了
        printf("已经被调试了!");
        return;
    } else
    {
        printf("还没被调试");
    }
}
  • 解决方法
    • 修改系统源码,将ptrace返回值直接返回0
    • hook ptrace
    • nop掉这个函数,或者汇编级修改寄存器绕过
2.8 ptrace检测
  • 原理:每个进程同时只能被一个调试进程ptrace,再次p自己会失败

  • 做法

    • a. 主动prtace自己,根据返回值判断自己是否被调试了
    • b. 或者多进程ptrace
// 单线程ptrace
void ptraceCheck()
{
    // ptrace如果被调试返回值为-1,如果正常运行,返回值为0
    int iRet=ptrace(PTRACE_TRACEME, 0, 0, 0);
    if(-1 == iRet)
    {
        LOGA("ptrace失败,进程正在被调试n");
        return;
    }
    else
    {
        LOGB("ptrace的返回值为:%dn",iRet);
        return;
    }
}

注:IDA gdb 都要附加,一个app只能被附加一次,在自己app中开一个线程 然后附加本应用 如果附加失败 证明应用不能被调试,先于别的附加,自己附加自己,那么本应用就不能被附加,从而避免被调试

  • 解决方法
    • 可以在ptrace函数做手脚,hook函数 ida动态调试时打断点做处理 或者直接干掉反调试
    • 用IDA工具给JNI_OnLoad函数下断点,然后进行调试,找到检测轮询代码,使用nop指令,替换检测指令,就相当于把检测代码给注释了
2.9 函数hash值检测
  • 原理:so文件中函数的指令是固定的,但是如果被下了软件断点,指令就会发生改变(断点地址被改写为bkpt断点指令),可以计算内存中一段指令的hash值进行校验,检测函数是否被修改或者被下断点

  • 解决方法

    • nop掉这个函数,或者汇编级修改寄存器绕过
2.10 断点指令检测
  • 原理:跟函数hash值检测的原理一样,如果函数被下软件断点,则断点地址会被改写为bkpt指令,可以在函数体中搜索bkpt指令来检测软件断点

  • 如果函数被下软件断点,则断点地址会被改写为bkpt指令,可以在函数体中搜索bkpt指令来检测软

  • 参数1:函数首地址 参数2:函数size

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;
        }
        
    }
}
2.11 系统源码修改检测
  • 原理:安装native下最流行的反调试方案是读取进程的status或stat来检测tracepid,原理是调试状态下的进程tracepid不为0

对于这种调试检测手段,最彻底的绕过方法是修改系统源码后重新编译,让tracepid永远为0

对抗这种bypass手段,我们可以创建一个子进程,让子进程主动ptrace自身设为调试状态,此时正常情况下,子进程的tracepid应该不为0,此时我们检测子进程的tracepid是否为0,如果为0源码被修改了

tracePid检测 _ZN3art3Dbg15gDebuggerActiveE函数,查看 /proc/pid/status 目录下的 tracePid 是否为0 若为0 则未被调试 若不为0 则被调试

bool checkSystem()
{
    // 建立管道
    int pipefd[2];
    if (-1 == pipe(pipefd))
    {
        LOGA("pipe() error.n");
        return false;
    }
    // 创建子进程
    pid_t pid = fork();
    LOGB("father pid is: %dn",getpid());
    LOGB("child pid is: %dn",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("子进程传递的内容为:%dn", buf2); // 输出内容
        // 判断子进程ptarce后的tracepid
        if(0 == buf2) {
        LOGA("源码被修改了.n");
        }else
        {
            LOGA("源码没有被修改.n");
        }
        return true;
    }
}
void smain()
{
    bool bRet=checkSystem();
    if(true==bRet)
    LOGA("check succeed.n");
    else
    LOGA("check failed.n");
    LOGB("main Finish pid:%dn",getpid());
    return;
}
  • 解决办法
    • 可以修改系统源码实现 tracePid 为0,不论是否被调试
    • 动态调试的反调试检测点
    • 程序直接挂掉,直接可以nop掉或者fopen函数下断 打开/proc/pid/status目录时 修改内存中 tracePid的值
2.12 单步调试陷阱
  • 原理:调试器从下断点到执行断点的过程分析

    • a. 保存:保存目标处指令
    • b. 替换:目标处指令替换为断点指令
    • c. 命中断点:命中断点指令(引发中断或者说发出信号)
    • d. 收到信号:调试器收到信号后,执行调试器注册的信号处理函数
    • e. 恢复:调试器处理函数恢复保存的指令
    • f. 回退:回退PC寄存器
    • g. 控制权回归程序
  • 主动设置断点指令/注册信号处理函数的反调试方案

    • ①. 在函数中写入断点指令
    • ②. 在代码中注册断点信号处理函数
    • ③. 程序执行到断点指令,发出信号
  • 分两种情况

    • Ⅰ. 非调试状态:
    • 进入自己注册的函数,NOP指令替换为断点指令,回退PC后正常指令(执行断点发出信号 - 进入处理信号函数 - NOP替换断点 - 退回PC)
    • Ⅱ. 调试状态:
    • 进入调试器的断点处理流程,他会恢复目标处指令失败,然后回退PC,进入死循环
char dynamic_ccode[]={
        0x1f,0xb4, //push {r0-r4}
        0x01,0xde, //breakpoint
        0x1f,0xbc, //pop {r0-r4}
        0xf7,0x46};//mov pc,lr
char*g_addr=0;
void my_sigtrap(int sig){
        char change_bkp[]={0x00,0x46}; //mov r0,r0
        memcpy(g_addr+2,change_bkp,2);
        __clear_cache((void)g_addr,(void)(g_addr+8)); // need to clear cache
        LOGI("chang bpk to nopn");
}

void anti4(){//SIGTRAP
        int ret,size;
        char addr,tmpaddr;
        signal(SIGTRAP,my_sigtrap);
        addr=(char)malloc(PAGESIZE2);
        memset(addr,0,PAGESIZE*2);
        g_addr=(char*)(((int)addr+PAGESIZE-1)&~(PAGESIZE-1));
        LOGI("addr: %p ,g_addr : %pn",addr,g_addr);
        ret=mprotect(g_addr,PAGESIZE,PROT_READ|PROT_WRITE|PROT_EXEC);
        if(ret!=0)
        {
          LOGI("mprotect errorn");
          return;
        }
        size=8;
        memcpy(g_addr,dynamic_ccode,size);

        __clear_cache((void)g_addr,(void)(g_addr+size)); // need to clear cache
        __asm__(
                "push {r0-r4,lr}nt"
                "mov r0,pcnt" //此时pc指向后两条指令
                "add r0,r0,#4nt"//+4 是的lr 地址为 pop{r0-r5}
                "mov lr,r0nt"
                "mov pc,%0nt"
                "pop {r0-r5}nt"
                "mov lr,r5nt" //恢复lr
        :
        :"r"(g_addr)
        :);
        
        LOGI("hi, i'm heren");
        free(addr);
        }
2.13 利用IDA先截获信号特性的检测(基于信号的检测)
  • 原理:IDA会首先截获信号,导致进程无法接收到信号,导致不会执行信号处理函数,将关键流程放在信号处理函数中,如果没有执行,就是被调试状态

  • signal()

  • raise() 发送信号

  • signal(SIGTRAP, myhandler); SIGTRAP:调试信号 myhandler:信号处理函数(自己实现的)

PS:信号处理函数 可以设置一个全局变量

终止进程方式可以kill进程 或者 sleep(),先给程序设置signal 并实现信号处理函数,然后在关键函数里或者开一个线程,隔时发送信号即 raise(),若信号未收到,则程序被调试,信号消息机制,被捕获就会消失(一次性)

#include 
#include 
#include 
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 %xn",(unsigned char*)g_ret);
    // 主动给自己进程发送SIGTRAP信号
    raise(SIGTRAP);
    raise(SIGTRAP);
    raise(SIGTRAP);
    kill(getpid(), SIGTRAP);
    printf("main.n");
    return 0;
}
2.14 利用IDA解析缺陷反调试
  • 原理:IDA采用递归下降算法来反汇编指令,而该算法最大的缺点在于它无法处理间接代码路径,无法识别动态算出来的跳转,而arm架构下由于存在arm和thumb指令集,就涉及到指令集切换,IDA在某些情况下无法只能识别arm和thumb指令,进一步导致无法进行伪代码还原

在IDA动态调试时,仍然存在该问题,若在指令识别错误的地点写入断点 , 有可能使得调试器崩溃(可能写断点,不知道写ARM还是THUMB,造成的崩溃)

IDA解析是Thumb指令或Arm指令时可能出错,递归下降算法来反汇编指令(多发生于静态分析,动态调试指令执行基本不会出现)

__asm __volatile{"",“memory”},asm前缀从这里开始指令为汇编指令,volatile 会让汇编指令不要给我优化,按我的执行

#if(JUDGE_THUMB)
#define GETPC_KILL_IDAF5_SKATEBOARD 
__asm __volatile( 
"mov r0,pc nt" 
"adds r0,0x9 nt" 
"push {r0} nt" 
"pop {r0} nt" 
"bx r0 nt" 

".byte 0x00 nt" 
".byte 0xBF nt" 

".byte 0x00 nt" 
".byte 0xBF nt" 

".byte 0x00 nt" 
".byte 0xBF nt" 
:::"r0" 
);
#else
#define GETPC_KILL_IDAF5_SKATEBOARD 
__asm __volatile( 
"mov r0,pc nt" 
"add r0,0x10 nt" 
"push {r0} nt" 
"pop {r0} nt" 
"bx r0 nt" 
".int 0xE1A00000 nt" 
".int 0xE1A00000 nt" 
".int 0xE1A00000 nt" 
".int 0xE1A00000 nt" 
:::"r0" 
);
#endif
// 常量标签版本
#if(JUDGE_THUMB)
#define IDAF5_CONST_1_2 
__asm __volatile( 
"b T1 nt" 
"T2: nt" 
"adds r0,1 nt" 
"bx r0 nt" 
"T1: nt" 
"mov r0,pc nt" 
"b T2 nt" 
:::"r0" 
);
#else
#define IDAF5_CONST_1_2 
__asm __volatile( 
"b T1 nt" 
"T2: nt" 
"bx r0 nt" 
"T1: nt" 
"mov r0,pc nt" 
"b T2 nt" 
:::"r0" 
);
#endif
2.15 五种代码执行时间检测(时间反调试)
  • 原理:一段代码,在a处获取当前时间,运行一段后,再在b处获取时间,然后计算获得两个时间的时间差,正常情况下这个时间差会非常小,如果这个时间差比较大,说明正在被单步调试

  • 做法

    • 五个能获取时间的api
    • time()函数,time_t 结构体
    • clock()函数,clock_t 结构体
    • gettimeofday()函数,timeval 结构体,timezone 结构体
    • clock_gettime()函数,timespec 结构体
    • getrusage()函数,rusage 结构体

gettimeofday()、time() 比较常用,通过获取两个时间来计算出时间间隔 从而确定时候被反调试,如果我们进行调试时不按F8单步或者不停留,那么这种反调试是无用的,在gettimeofday函数下断点,最近的两次调用将其返回值改成一样的,直接找到反调试设置的地方,然后给nop掉

#include 
#include 
#include 
#include 
#include 
#include 
#include 
static int _getrusage (); //Invalid
static int _clock (); //Invalid
static int _time ();
static int _gettimeofday ();
static int _clock_gettime ();
int main ()
{
    _getrusage ();
    _clock ();
    _time ();
    _gettimeofday ();
    _clock_gettime ();
    return 0;
}
int _getrusage ()
{
    struct rusage t1;
    
    getrusage (RUSAGE_SELF, &t1);
    long used = t1.ru_utime.tv_sec + t1.ru_stime.tv_sec;
    if (used > 2) {
        puts ("debugged");
    }
    return 0;
}
int _clock ()
{
    clock_t t1, t2;
    t1 = clock ();
    
    t2 = clock ();
    double used = (double)(t2 - t1) / CLOCKS_PER_SEC;
    if (used > 2) {
        puts ("debugged");
    }
    return 0;
}
int _time ()
{
    time_t t1, t2;
    time (&t1);
    
    time (&t2);
    if (t2 - t1 > 2) {
    puts ("debugged");
    }
return 0;
}
int _gettimeofday ()
{
    struct timeval t1, t2;
    struct timezone t;
    gettimeofday (&t1, &t);
    
    gettimeofday (&t2, &t);
    if (t2.tv_sec - t1.tv_sec >2 ) {
    puts ("debugged");
    }
    return 0;
    }
    int _clock_gettime ()
    {
        struct timespec t1, t2;
        clock_gettime (CLOCK_REALTIME, &t1);
        
        clock_gettime (CLOCK_REALTIME, &t2);
        if (t2.tv_sec - t1.tv_sec > 2) {
        puts ("debugged");
    }
    return 0;
}
2.16 三种进程信息结构检测
  • 原理:一些进程文件中存储了文件信息,可以读取这些信息得知是否为调试状态

做法

  • 第一种
/proc/pid/status
/proc/pid/task/pid/status
TracePid非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
2.17 Inotify事件监控dump
  • 原理:通常壳会在程序运行完成对text的解密,所以脱壳可以通过dd与gdb_gcore来dump

  • /proc/pid/mem或/proc/pid/pagemap,获取到解密后的代码内容

  • 可以通过lnotify系列api来监控mem或pagemap的打开或访问事件,一旦发生事件就结束进程来组织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的返回值:%dn",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的数值为:%dn",event->mask);
                if( (event->mask==IN_OPEN) )
                {
                    // 此处判定为有true,执行崩溃.
                    LOGB("2 有人打开pagemap,第%d次.nn",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:%dn",getpid());
    return;
}

https://blog.csdn.net/darmao/article/details/78816964

https://www.jianshu.com/p/55d7bb3ba5af

0x03 案例APK

https://gtoad.github.io/2017/06/25/Android-Anti-Debug

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

安卓逆向学习入门之过反调试(一)

https://bbs.huaweicloud.com/blogs/detail/230620

0x04 安卓动态调试 4.1 安卓程序动态调试条件(满足1个即可)
    1. 在 AndroidMainfest.xml —> application 标签下,设置或者添加属性 android:debuggable=“true”
    1. 系统默认模式,在 build.prop(boot.img),ro.debuggable=1

Android SDK 中有 android.os.debug 类提供了一个isDebuggerConnected方法,用于判断 JDWP 调试器是否正在工作

4.2 DDMS 简介

DDMS 的全称是 Dalvik Debug Monitor Service。可以实现 IDE 与连接终端设备(包含 仿真器 与 真机 )的调试,DDMS 可以实现查询终端设备运行状态,终端设备进程状态,线程状态,文件系统,日志信息(logcat)等。以及控制终端设备完成一些 *** 作。还可以向目标机发送短信、打电话,发送地理位置信息等。可以像 gdb 一样 attach 某一个进程调试。 SDK tools 目录下提供了 ddms 的完整版,直接运行即可。总的来说它是一款性能分析工具,可以帮助开发者快速了APP的运行情况

4.3 DDMS 怎样与调试器交互?
  1. 每一个 Android 应用都运行在一个 Dalvik 虚拟机实例里,而每一个虚拟机实例都是一个独立的进程空间。虚拟机的 线程机制,内存分配和管理,Mutex等等都是依赖底层 *** 作系统而实现的。所有Android应用的线程都对应一个Linux线程,虚拟机因而可以更多的 依赖 *** 作系统的线程调度和管理机制
  2. DDMS 在 IDE与设备或模拟器之间的起着中间人的角色
  3. DDMS 启动时会与 adb 之间建立一个 device monitoring service 用于监控设备。当设备断开或链接时,这个 service 就会通知DDMS
  4. 当一个设备链接上时,DDSM 和 ADB 之间又会建立 VM monitoring service 用于监控设备上的虚拟机
  5. 通过 ADB Deamon 与设备上的虚拟机的 debugger 建立链接,这样 DDMS 就开始与虚拟机对话

在安卓平台上,每个应用都运行在自己的进程上,同时每个应用也都运行在自己的虚拟机(VM)上,每个VM公布了唯一的端口号以供调试器连接

当DDMS启动后,会连接到adb。当有设备连接上,VM监测服务就在adb和DDMS之间创建,它会通知DDMS 设备上的VM是启动了还是终止了。一旦VM是运行的,DDMS就获取VM的进程ID(pid),通过adb和设备上的adb守护进程(adbd)建立到 VM调试器的连接。到此,DDMS就可以使用约定的线协议与VM通信

DDMS给设备上的每个VM分配一个调试端口。通常,DDMS分配的第一个可调试端口号是8600,下一个是8601,依次往下类推。当调试器连接 到已分配的一个端口时,VM上的所有通信都会被关联到调试器。一个调试器只能连接一个单独的端口,但是DDMS同时可以处理多个连接的调试器

默认的,DDMS也会监听DDMS的“基本端口”(默认为8700)。基本端口是一个端口转发端口,可以通过8700端口接受来自VM所有调试端口的通信并可以发送信息到调试器。这就允许你将调试器连接到8700端口,然后可以调试所有设备上的虚拟机。在DDMS设备视图下,转发的通信可以被当前所选进程终止

接下来的屏幕截图会在Eclipse中显示标准的DDMS屏幕视图。如果你是从命令行启动的DDMS,截图会略有不同,但绝大部分功能是相同的。注意这个特殊进程,com.android.email 它在模拟器上运行时的调试端口是8700,而分配给它的端口是8606。这就表明 DDMS 当前将 8606 端口转发到静态调试端口 8700

4.4 高版本的DDMS问题解决?

使用Android Studio高版本的同学,这里的高版本主要是3.0以上的版本,低版本的同学可能还没有察觉到什么,但是高版本的同学可就不一定了。因为高版本的同学会发现,在他们的Android Studio中,tool目录下根本就找不到Android选项

3.0以上版本,点击Tools目录,目录中会有一个Android选项,在Android选项中存在着Android Device Monitor,也就是我们需要用到的设备监控器

高版本中,Tools目录下的Android选项已经被移除了,这里是谷歌故意去掉的。谷歌官方认为,在开发过程中几乎很少的使用到AMD,AMD设备监控器这个功能被开发者使用的太少了,与其留着它占用内存,增大编译器体积,还不如把它去掉,为编译器瘦瘦身,所以AMD在最新版的Android Studio中被放弃了

那么高版本中还能使用到AMD了呢?大家可继续使用的,只不过换了启动的地方,在Android Studio 安装路径的tools路径下的monitor.bat,这就是AMD的启动程序,点击这个程序就会启动AMD设备监控器啦,高版本的同学可以通过这种方式打开

PS:

3.0之后删除之前的了 Tools目录下的Android菜单,这样就导致以前通过Tools-->Android-->DeviceMonitor 打开DDMS的方式不行了,打开3.0之后的DDMS 文件,如下:
1.先找到AndroidStudio配置的SDK路径;
2.在SDK的android-sdk/tools/路径下【就是和配置ADB命令一样的路径】有个monitor.bat 的批处理文件;
3.鼠标连续点击两下monitor.bat这个批处理文件,在屏幕上会打开一个类似CMD的命令行中输入板,然后迅速自动关闭;
4.坐等1到3秒就会打开DDMS

  • DMMS小技巧(快捷方式)

每次都要先找的AndroidStudio配置SDK文件夹下找到android-sdk/tools/monitor.bat,再打开就有点麻烦了,两种方式:

  • bat批处理方式
@echo off
rem  color 0A :设置cmd背景颜色和字体颜色
color 0A
rem title:设置cmd标题
title Start Android Studio Mointor 
echo 请按任意键打开 Android Studio Mointor .....
pause>nul
rem “C:UsersxxxAppDataLocalAndroidSdktoolsmonitor.bat”是AndroidStudio配置SDK文件夹下monitor.bat的完整路径
call C:UsersxxxAppDataLocalAndroidSdktoolsmonitor.bat
  • Dos命令

1)打开Window命令窗口 :Win+R快捷键–>CMD–>Enter 输入如下命令:

call D:AndroidSDKtoolsmonitor.bat
// 然后回车执行,坐等1到3秒就会打开DDMS;

2)在AndroidStudio中底部Terminal中输入如下命令:

call D:AndroidSDKtoolsmonitor.bat
// 然后回车执行,坐等1到3秒就会打开DDMS;

3)DDMS连接模拟器的一些疑难问题

DDMS 显示模拟器的东西出现问题,可以先用如下命令尝试解决,解决不了的话再试试换一个模拟器或用真机测试

adb devices # 查看自身模拟器的adb连接端口
adb connect 127.0.0.1:xxxx # 建立连接
# 重新打开一次adb端口服务
adb kill-server   
adb start-server

逍遥模拟器能成功打开:

夜神模拟器进程名没显示:


PS:此处可能是模拟器安装多个不同版本的模拟器造成的,可删除安装模拟器后只安装一个模拟器即可。

4.5 DDMS(Monitor)功能详解

首先 DDMS 被分为三个部分。左上角为 Device 面板,详细罗列了与电脑相连的终端设备的信息。右上角为详细的功能选项卡,下方为日志信息以及终端信息

4.5.1 Devices 面板
  1. 左边显示了所有当前能找到的所有模拟器或设备列表和每个设备当前正在运行的虚拟机列表。虚拟机是按程序的包命来显示的
  2. 通过这些列表可以找到运行着想调试的activity的虚拟机。每个虚拟机旁边的是“debugger pass-through”端口,链接到其中一个端口就会链接到设备上对应的虚拟机。不管如何,在用DDMS时,只需要链接到8700端口,因为DDSM 转发所有的通信到当前选择的虚拟机。这样,就不用在每次切换虚拟机是重新配置debugger端口
  3. 当一个正在运行的程序调用waitForDebugger()函数时,客户端名字旁边会显示一个红色的icon,知道debugger连上对 应的虚拟机,这是debugger会变成绿色
  4. 如果看到叉icon,着意味着DDMS用于不能打开虚拟机的端口而不能建立debugger与虚拟机建立连接。如果看到所有的虚拟机是这样, 很可能是有另外一个DDSM实例在运行

Devices 面板包含了所有的与IDE相连的设备列表以及每个设备上运行的进程的列表,如下图所示:

device 窗口列出了 模拟器(或 真机)中所有的进程,显示进程时会显示进程ID (上图中online那一列显示的即是终端上运行的进程的ID) 以及与进程相关联的端口号,连接端口号从 8600 端口依次往下增加,8700 是 DDMS 接收所有连接终端返回信息的端口,即是 base 端口。Devices 面板顶端从左往右有多个按钮

上面一排的按钮功能,可以把鼠标放上面会自动显示按钮说明。如果你没有运行或调试程序的话,这些图标是不可用的!

当你选中某个进程,并按下调试进程按钮时,如果 eclipse 中有这个进程的代码,那就可以进行源代码级别的调试。有点像gdb attach。图片抓取按钮可以把当前android的显示桌面抓到你的机器上,也是非常有用

  • 开始方法分析

    • 在设备选项,选择要进行方法分析的进程
    • 点击 Start Method Profiling按钮
    • 与应用进行交互,开始要分析的方法
    • 点击 Stop Method Profiling按钮。DDMS停止分析应用并打开Traceview,它包含了在点击Start Method Profiling和Stop Method Profiling之间方法分析收集到的信息
  • 分析这些按钮的功能

    • Debug:实现使用DDMS对代码进行调试,使用该功能的前提是 IDE中具有该运行进程的源代码,否则该按钮为灰色,功能无法使用
    • Update heap:实现对进程中的堆进行更新的 *** 作。只有当选择这个按钮后,在右侧的功能面板中的heap选项卡中就能够看见当前进程的堆使用情况。

  • Dump HPROF file:将当前进程堆使用情况生成文档,使用这个功能可以更加详细的分析当前堆的情况,有利于查找内存泄等问题
  • Cause GC:触发垃圾回收机制,可以点击后查看当前进程的堆使用情况
  • Update thread:这个功能与update heap一样,当点击了这个按钮才能在右侧面板的thread选项卡中查看当前进程的所用的线程运行状态

  • Start Method Profiling:开始进行方法分析
  • Stop:终止当前选中的进程
  • Screen Capture:截屏按钮,捕获当前设备的屏幕状态,该功能具有一定的延时

4.5.2 功能面板

功能面板从左到右有多个选项卡分别是:

  • Threads: 表示当前进程中的所有线程状态。线程视图列出了此进程的所有线程
ID:       虚拟机分配的唯一的线程ID,在Dalvik里,它们是从3开始的奇数。 
Tid:     linux的线程ID,For the main thread in a process, this will match the process ID. 
Stauts:  线程状态, 
running:  正在执行程序代码 
sleeping:执行了Thread.sleep() 
monitor: 等待接受一个监听锁。 
wait:     Object.wait() 
native:  正在执行native代码 
vmwait:  等待虚拟机 
zombie:  线程在垂死的进程 
init:    线程在初始化(我们不可能看到) 
starting:线程正在启动(我们不可能看到) 
utime:   执行用户代码的累计时间 
stime:   执行系统代码的累计时间 
name:    线程的名字 
  • Heap:表示当前进程堆使用情况。展示一些堆的状态,在垃圾回收期间更新。当选定一个虚拟机时, VM Heap视图不能显示数据,可以点击右边面包上的带有绿色的”Show heap updates”按钮,然后在点击”Cause GC “实施垃圾回收更新堆的状态
  • Allocation Tracker:分配跟踪器。在这个视图里,可以跟踪每个选中的虚拟机的内存分配情况。点击”Start Tracking”后点击”Get Allocations “就可以看到
  • NetWork Statistics: 网络分析功能
  • File Explorer: 浏览终端的文件系统,进行文件相关 *** 作。通过 Device > File Explorer 就可以打开 File Explorer。这里可以浏览文件,上传上载删除文件,当然这是有相应权限限制的( 只有 root 权限才能查看 )。 在这里面可以进行将外部文件导入到终端中,或者将终端文件导出,或者删除终端文件,具体 *** 作是右上角三个按钮:

文件 *** 作还是比较重要的,比如一个应用涉及到了SQLite数据库使用,此时可以使用这个功能,导出数据库文件单独分析

  • Emulator Control: 可实现往模拟器中打电话,发送短信,发送地理位置坐标等功能
    • 可以模拟一些设备状态和行为
    • Telephony Status:改变电话语音和数据方案的状态,模拟不同的网络速度
    • TelePhony Actions:发送模拟的电话呼叫和短信到模拟器
    • Location Controls:发送虚拟的定位数据到模拟器里,我们就可以执行定位之类的 *** 作。可以收工的在Manual里输入经度纬度发送到模拟器,也可以通过 GPX和KML文件
  • System Information:这个选项卡里面可以查看终端的CPU负载以及内存使用情况
4.5.3 LogCat and Console

系统运行产生的日志信息以及终端打印的信息,LogCat可以帮助我们查看到相关信息:

使用 LogCat 可以根据程序中的运行日志判断当前程序运行的状态。终端设备一般运行较多的进程,每个进程运行都有大量的日志产生。因此一般需要使用 过滤器过滤其他进程信息。过滤器在 Saved Filter 中的以添加过滤器:


如果指定要观察某一个进程的日志信息,那过滤器就使用PID(进程ID)进行过滤即可,进程ID可以在Devices面板中得到,过滤的等级根据自己代码中写的等级酌情考虑,这样就能够实现只是观察一个进程的日志信息

  • "车来了"去掉APP广告

https://blog.csdn.net/freeking101/article/details/105759124

参考链接:

https://blog.csdn.net/qq_34149335/article/details/82491997

https://blog.csdn.net/freeking101/article/details/105759124

https://blog.csdn.net/freeking101/article/details/106879863

https://blog.csdn.net/black_bird_cn/article/details/79893688

http://www.520monkey.com/


我自横刀向天笑,去留肝胆两昆仑


欢迎分享,转载请注明来源:内存溢出

原文地址:https://www.54852.com/zaji/5695745.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-12-17
下一篇2022-12-17

发表评论

登录后才能评论

评论列表(0条)

    保存