内容纲要

Android反调试常见方法

ptrace

ptrace自己,使得android_server附加不上

void anti_ptrace()
{
    ptrace(PTRACE_TRACEME, 0, 0, 0);
}

由于ptrace_me方法在JNI_OnLoad方法中调用,若是以《IDA Pro动态调试so文件》中的动态调试方法调试,则在调用JNI_OnLoad之前IDA就附加了程序,所以以上方法并不能达到反调试的目的。

既然如此,可以检测ptrace的返回值,若附加成功则返回0,失败则返回-1。若ptrace函数返回了-1,说明当前程序正在被调试。将ptrace方法修改为如下代码:

void ptrace_me(){
    int ch=ptrace(PTRACE_TRACEME,0,NULL,NULL);
    if (ch == -1){
        int pid=getpid();
        kill(pid,SIGKILL);
    }
}

检测Tracepid的值

查看/proc/self/status

image-20220724141205013

程序正常状态下 其 TracerPid 为 0,当处于调试状态时候, TracerPid 为调试那个进程pid, 也就是非0。

void anti_Tracepid(){
    try{
        const int bufsize = 1024;
        char filename[bufsize];
        char line[bufsize];
        int pid = getpid();
        sprintf(filename, “/proc/%d/status”, pid);
        FILE* fd = fopen(filename, “r”);
        if (fd !=NULL){
            while (fgets(line, bufsize, fd)){
                if (strncmp(line, “TracerPid”, 9) == 0){
                    int statue = atoi(&line[10]);
                    if (statue != 0){
                        fclose(fd);
                        int ret = kill(pid, SIGKILL);
                    }
                    break;
                }
            }
            fclose(fd);
        } 
        else{
            // LOGD(“open %s fail…”, filename);
        }
    } catch (…){
    }
}

在native下实现,并在JNI_Onload下调用

对android_server端口号进行检测

ida的android_server

image-20220724152935026

void anti_serverport() {
    const int bufsize=512;
    char filename[bufsize];
    char line[bufsize];
    int pid =getpid();
    sprintf(filename,"/proc/net/tcp");
    FILE* fd=fopen(filename,“r”);
    if(fd!=NULL){
        while(fgets(line,bufsize,fd)){
            if (strncmp(line, “5D8A”, 4)==0){
                int ret = kill(pid, SIGKILL);
            }
        }
    }
    fclose(fd);
}

当然只需将端口设为其他即可绕过

./android_server -p 24000

-p选项指定android_server的运行端口,这里指定了24000端口。

检测调试进程的名字

调试器的进程名则被存储在/proc/<pid>/cmdline文件中

void anti_processstatus(){
    const int bufsize = 1024;
    char filename[bufsize];
    char line[bufsize];
    char name[bufsize];
    char nameline[bufsize];
    int pid = getpid();
    //先读取Tracepid的值
    sprintf(filename, “/proc/%d/status”, pid);
    FILE *fd=fopen(filename,“r”);
    if(fd!=NULL){
        while(fgets(line,bufsize,fd)){
            if(strstr(line,“TracerPid”)!=NULL)
            {
                int statue =atoi(&line[10]);
                if(statue!=0){
                    sprintf(name,"/proc/%d/cmdline",statue);
                    FILE *fdname=fopen(name,“r”);
                    if(fdname!= NULL){
                        while(fgets(nameline,bufsize,fdname)){
                            if(strstr(nameline,“android_server”)!=NULL){
                                int ret=kill(pid,SIGKILL);
                            }
                        }
                    }
                    fclose(fdname);
                }
            }
        }
    }
    fclose(fd);
}

对常规放置目录进行检测

比较常见的android_server一般放在/data/local/tmp下,对该目录下的文件名进行检测

void anti_localtmp(){
    int pid=getpid();
    const int bufsize=1024;
    char line[bufsize];
    char filename[bufsize];
    sprintf(filename,"/data/local/tmp");
    FILE *fd=fopen(filename,“r”);
    if(fd!=NULL){
        while(fgets(line,bufsize,fd)){
            if(strstr(line,“android_server”)!=NULL){
                int ret=kill(pid,SIGKILL);
            }
        }
    }
    fclose(fd);
}

时间差异检测

正常情况下,一段程序在两行代码之间的时间差是很短的,而对于调试程序来说,单步调试时两行代码之间的时间差会比较大,检测两条代码之间的时间差,可以大概率判断程序是否被调试。

在C语言中可以使用函数gettimeofday()来得到时间,其精度可以达到微秒。

这个方法一个不好的特点是需要一定的代码跨度,因此可能需要暴露部分代码逻辑。因为如果两个时间的取值点非常非常近,那很可能调试者在两者之间没有断点从而迅速跳过。

#include<sys/time.h>
int gettimeofday(struct timeval *tv,struct timezone *tz )
void check_time(){
    int pid=getpid();
    struct timeval start;
    struct timeval end;
    struct timezone tz;
    gettimeofday(&start,&tz);
    gettimeofday(&end,&tz);
    int timeoff=end.tv_sec-start.tv_sec;
    if (timeoff>1){
        kill(pid,SIGKILL);
    }
}

Java层内置函数检测

Android的android.os.Debug类提供了isDebuggerConnected()方法,用于检测是否有调试器挂载到程序上。

由于是Java层的系统调用,所以相比于Native层,本方法会更容易被发现,且被Hook篡改返回值也会更简单。

if (!Debug.isDebuggerConnected()){
    System.loadLibrary("native-lib");
}else {
    Process.killProcess(Process.myPid());
}

检测break point指令

IDA等调试器在动态调试时会向断点地址插入breakpoint断点指令,而把原来的指令暂时备份到别处,所以可以通过扫描代码中是否有breakpoint汇编指令即可。

一般来说,Android的汇编代码有ARM和Thumb,因此需要都检测一下:

- Arm:0x01,0x00,0x9f,0xef
- Thumb16:0x01,0xde
- Thumb32:0xf0,0xf7,0x00,0xa0

#include <elf.h>

...

unsigned int getLibAddr(){
    unsigned int ret=0;
    char name[]="libnative-lib.so";
    char buf[4096],*tmp;
    int pid=getpid();
    FILE *fp;
    sprintf(buf,"/proc/%d/maps",pid);
    fp=fopen(buf,"r");
    if (fp!=NULL){
        while (fgets(buf,sizeof(buf),fp)!=NULL){
            if (strstr(buf,name)){
                tmp=strtok(buf,"-");
                ret=strtoul(tmp,NULL,16);
                break;
            }
        }
    }
    fclose(fp);
    return ret;
}
bool check_break_point(){
    unsigned int base,offset,pheader;
    Elf32_Ehdr *elfhdr;   // ELF_Header
    Elf32_Phdr *elfphdr;  // Program_Header
    base=getLibAddr();
    if (base == 0){
        return false;
    }
    elfhdr=(Elf32_Ehdr*) base;
    pheader=base+elfhdr->e_phoff;     // e_phoff:程序头偏移

    for (int i=0;i<elfhdr->e_phnum;i++){    // e_phnum:程序头表中元素的个数
        elfphdr=(Elf32_Phdr*)(pheader+i*sizeof(Elf32_Phdr));
        if (!(elfphdr->p_flags & 1)){
            continue;
        }
        offset=base+elfphdr->p_vaddr;      // p_vaddr:段的数据映射到虚拟地址空间中的位置
        offset+=sizeof(Elf32_Ehdr)+sizeof(Elf32_Phdr)*elfhdr->e_phnum;

        char *p=(char *)offset;
        for (int j = 0; j < elfphdr->p_memsz; ++j) {    // p_memsz:段在虚拟地址空间中的长度
            if (*p == 0x01 && *(p+1) == 0xde){          // Thumb16
                return true;
            } else if (*p == 0xf0 && *(p+1) == 0xf7 && *(p+2) == 0x00 && *(p+3) == 0xa0){    // Thumb32
                return true;
            } else if (*p == 0x01 && *(p+1) == 0x00 && *(p+2) == 0x9f && *(p+3) == 0xef){    // ARM
                return true;
            }
            p++;
        }
    }
    return false;
}

jint JNI_OnLoad(JavaVM* vm, void* reserved){
    if (check_break_point()){
        int pid=getpid();
        kill(pid,SIGKILL);
    }
    ...
}

通过使用Linux inotify特性来对文件的读写,以及打开等权限进行监控

在动态调试的过程中,一般会查看调试进程的虚拟地址空间或者是dump内存,这时就会涉及到对文件的读写以及打开的权限,这时候对它们进行检测就能发现是否正在被破解。Linux下的Inotify就可以实现对文件系统事件的打开,读写的监管。如果通过Inotify收到事件的变化,我们就Kill掉进程。

void anti_debug06() {
    int ret, len, i;
    int pid6 = getpid();
    const int MAXLEN = 2048;
    char buf[1024];
    char readbuf[MAXLEN];
    int fd, wd;
    fd_set readfds;
    fd = inotify_init();
    sprintf(buf, “/proc/%d/maps”, pid6);
    wd = inotify_add_watch(fd, buf, IN_ALL_EVENTS);
    if(wd>=0){
        while (1) {
            i = 0;
            FD_ZERO(&readfds);//使得readfds清零
            FD_SET(fd, &readfds);//将fd加入readfds集合
            ret = select(fd + 1, &readfds, 0, 0, 0);
            if(ret==-1){
                break;
            }
            if (ret) {
                len = read(fd, readbuf, MAXLEN);
                while (i < len) {
                    struct inotify_event *event = (struct inotify_event *) &readbuf[i];
                    if ((event->mask & IN_ACCESS) || (event->mask & IN_OPEN)) {
                        int ret = kill(pid6, SIGKILL);
                        return;
                    }
                    i += sizeof(struct inotify_event) + event->len;
                }
            }
        }
    }
    inotify_rm_watch(fd, wd);
    close(fd);
}

参考

断点详解

inotify补充