看雪论坛
主题:186038  回帖:1211011  会员:739282  在线:1597

七级
注册:2014-4
帖子:197
精华:7

【原创】基于HOOK的Anti-debug调用点trace和Anti-anti

ThomasKing 2015-4-16 18292
发一个之前写的辅助调试SO的小东西,限于水平难免会有错误之处,请各位大大批评斧正,小弟感激不尽。

同时,也欢迎各位大大讨论下反调试的技术和Anti方法,让也小弟学学:D:
-------------------------------------------------------------------------------
基于HOOK的Anti-debug调用点trace和Anti-anti
一、  概述
相信动态调试过SO的坛友对Anti并不陌生,比如读取/proc/self/status,/proc/self/task/xxx/status、stat文件查看状态TracePid和PPid,读取wchan查看进程等待,添加notify,模拟器检测等等。经过各种掉坑之后,虽然知道了这些检测方法,但如果elf文件被畸形不能静态分析(修复又是另外一回事了),也只能动态调试在这些关键API处下断点,也免不了掉坑调试崩溃,再来点指令混淆的话,调试起来确实比较恶心。虽然在Android平台同样也存在linux常用的ltrace(库函数trace)和strace(系统调用和信号trace)工具,但不能满足调试需求,还有检测ltrace的调试。废话不多说,直接进入正题(限于水平,难免会有疏漏和错误之处,请各位大大斧正,小弟感激不尽)。

二、  调用点trace
注入Hook API函数,当然首选zygote进程是最方便的了(如果zygote不能注入的话,那就关文档去修改ROM吧)。HooK住函数之后,通常采用下面这种模式添加代码:
Xxx new_fun(xxx){
  Before_call();
  Old_fun();
  After_call();
}
相信许多都知道ARM函数的调用流程,这里再啰嗦下 ARM汇编如何调用函数:
直接调用:BL/BLX  _xxfun
函数指针调用:BL/BLX  Rx
某些混淆代码:mov lr, [pc, #xx], 计算Rx,Ldr PC, Rx等
外部函数调用:BL/BLX _plt表项,plt表中load got表存在的函数地址到PC
被调函数一般模式:
Stmxx {Rx-Rx, lr}  /push

ldmxx {Rx-rx, pc}  /pop

函数的返回地址保存在lr寄存器中,之后被被调函数存在放栈上。只要lr的值未被修改,那么就保存着调用点的下一条指令地址!只要获取lr的值,那么就能跟踪到调用点。
知道了lr保存了调用点的值,获取lr的值也要注意时机。调用函数时lr的值已经被存在放栈上,修改lr也无关紧要。无法确定被调函数是否存在显式地给lr赋值或者函数调用,那要保证lr不被修改,故在函数入口点就保存lr的值是最佳也是最简单的选择。获取lr的值可以通过汇编来实现:
#define GETLR(store_lr)  \
  __asm__ __volatile__(  \
    "mov %0, lr\n\t"  \
    :  "=r"(store_lr)  \
  )
则Hook函数模式:
Xxx new_fun(xxx){
  Unsigned lr;
  GETLR(lr)
  Before_call();
  Old_fun();
  After_call();
}

来个实例,简单起见只Hook fopen函数:
FILE* new_fopen(const char *path,const char * mode){
  unsigned lr;
  GETLR(lr);

  if(strstr(path, "status") != NULL){
    LOGD("
  •  Traced-fopen Call function: 0x%x\n", lr);
  •     if(strstr(path, "task") != NULL){
          LOGD("
  •  Traced-anti-task/status");
  •     }else
          LOGD("
  •  Traced-anti-status");
  •   }else if(strstr(path, "wchan") != NULL){
        LOGD("
  •  Traced-fopen Call function: 0x%x\n", lr);
  •     LOGD("
  •  Traced-anti-wchan");
  •   } 
      return old_fopen(path, mode);
    }
    注入后,某APK输出:

    图 1 调用点
    Logcat:
     
    图2  trace
    Trace到了这个检测点,大概就知道这个函数想干什么了。动态调试时,也好准备”跨坑”而不是”掉坑”了。
    除了直接获得这些调用点过坑外,还可以组合一些其他功能。比如监控APK启动时mmap分配内存,直接dd,便于大致分析某段代码做了什么,配合mprotect能起到一定的效果。当然,不必担心会监控到linker加载SO时mmap的无用信息,因为linker自身实现了mmap函数。
    static void* new_mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset){
      unsigned lr;
      void* base = NULL;
      
      GETLR(lr);
      base = old_mmap(start, length, prot, flags, fd, offset);
      if((flags & MAP_ANONYMOUS) == 0){  //文件映射
        char file_name[256];
        char buf[256];
        memset(buf, 0, 256);
        sprintf(file_name, "/proc/self/fd/%d", fd);
        if(readlink(file_name, buf, 256) < 0){
          LOGD("[E] Traced-mmap --> readlink %s error\n", file_name);
          goto _done;
        }
        LOGD("
  •  Traced-mmap --> [file] start = %p, length = 0x%x, filename = %s, offset = 0x%x", 
  •       start, length, buf, offset);
      }else{  //内存映射
        LOGD("
  •  Traced-mmap --> [mem] start = %p, length = 0x%x", 
  •       start, length);
      }
      LOGD("
  •  Traced-mmap Call function: 0x%x, Ret address: 0x%x\n", lr, (unsigned)base);
  • _done:
      return base;
    }

    图 3
    至于还能做什么,那就靠各位读者自行研究了吧。附上一些常见anti trace。

    三、  Anti-anti
    讨论一些常见的基于Hook的Anti-anti方法,欢迎讨论。
    1、  status和stat
    status和stat的Anti-anti方式类似,通过Hook fopen实现重定向到/data/local/tmp目录下:
    sprintf(re_path, "/data/local/tmp/status");
    if(!HasGenFile(re_path)){
      char buffer[BUFFERSIZE];
      FILE *fpr, *fpw;
      fpr = old_fopen(path, "r");
      fpw = old_fopen(re_path, "w");
      if(fpr == NULL || fpw == NULL){
        LOGD("[E] re-path [%s]failed", path);
        return old_fopen(path, mode);
      }
      while(fgets(buffer, BUFFERSIZE, fpr) != NULL){
        if(strstr(buffer, "State") != NULL){
          fputs("State:\tS (sleeping)\n", fpw);
        }
        if(strstr(buffer, "TracerPid") != NULL){
          fputs("TracerPid:\t0\n", fpw);
        }else{
          fputs(buffer, fpw);
        }
      }
      fclose(fpr);
      fclose(fpw);
    }
    2、  wchan
    和status类似,只是重定向时,将等待事件设置为sys_epoll_wait:
    if(strstr(path, "wchan") != NULL){
      LOGD("
  •  Anti-anti-wchan!");
  •   strcpy(re_path, "data/local/tmp/wchan");
      if(!HasGenFile(re_path)){
        FILE *fpw;
        fpw = old_fopen(re_path, "w");
        if(fpw == NULL){
          LOGE("[E] re-path wchan failed!");
          goto __normal;
        }
        fputs("sys_epoll_wait", fpw);
        fclose(fpw);
      }
      return old_fopen(re_path, mode);

    3、  inotify_add_watch
    检测mem、pagemem、task等读取事件。可以直接让其返回-1,但不推荐这么做。如果检测代码并未对返回值做判断,直接使用,这样Anti-anti会导致程序崩溃,我的做法是改变mask的值:

    //监控打开和读事件
    if(strstr(pathname, "mem") != NULL){
      LOGD("
  •  inotify_add_watch --> patch mem");
  •   return old_inotify_add_watch(fd, pathname, 0x00000200);    //mem永远不会被删除,改为0也可以
    //监控打开和读事件,防获取反调试线程信息
    }else if(strstr(pathname, "task") != NULL){
      LOGD("
  •  inotify_add_watch --> patch task");
  •   return old_inotify_add_watch(fd, pathname, 0x00000200);  
    4、  ptrace
    这个函数算是用得比较多的吧,简单的检测代码是调用PTRACE_TRACEME,直接返回0就可以,不过现在这种方式很少了吧。某加固fork了进程去作ptrace,并写入一些数据。通过对ptrace hook,可以监控到写入数据。模拟这个通信过程就可以Anti-anti。具体就不展开了吧,相信各位读者可以做到了。
    当然,还有一些Anti-anti方法,限于篇幅,就不展开了吧(让小弟学学各位大牛的Anti-anti方法吧)。

    四、总结
    做好Anti-trace和Anti-anti有些时候能大大节省时间,将精力专注于算法或者其他逻辑上。但也存在一些问题:
    1、  HOOK检测
    导出表HOOK检测:读取/system/lib/libc.so特定函数偏移,再获取本进程libc.so的基地址来检测是否有HOOK。(Patch方法:Hook fopen、dlopen和open函数)
    Inline HooK检测:Inline Hook时要替换函数起始的字节码,可以检测一些比较奇怪的二进制码(BX pc等字节码)。
    2、  LOGCAT
    许多反调试都会起一个进程或者线程循环监控,此时anti-trace的输出会比较多,比如:

    图 4
    这看起来比较烦,而且很多都是相同的。可以利用调用点trace地址的唯一性进行过滤,让同一信息只输出一次即可,也可以写个apk,将信息通过AF_UNIX本地套接字发送给apk,利用数据库的优势来存储等等。
    3、  SVC
    有些关键性的API,为了隐藏,直接通过汇编实现系统调用。ARM系统调用时的调用号通过r7寄存器来传递的。如果编写时,没有手写花指令,直接通过扫描SVC字节码和附近的关于r7的寄存器赋值操作,可以获得一些搜索结果。不过为了隐藏,手写点花指令也是可以的。
    比如:常用来刷cache的代码
    static void clearcache(char* begin, char *end)
    {
      const int syscall = 0xf0002;
      __asm __volatile (
        "mov   r0, %0\n"
        "mov   r1, %1\n"
        "mov   r7, %2\n"
        "mov     r2, #0x0\n"
        "svc     0x00000000\n"
        :
        :  "r" (begin), "r" (end), "r" (syscall)
        :  "r0", "r1", "r7"
        );
    }
    最新回复 (20)
    六级 mingxuan三千 2015-4-16
    1
    感谢分享  学习了
    一级 ShadoWWinL 2015-4-16
    2
    入门还是不错的。。。
    精华:3
    五级 我是小三 2015-4-16
    3
    来学习的,多谢分享。
    一级 peterchen 2015-4-17
    4
    TK牛很nice,技术也nice... :cool:
    最近都有撤贴风,马上本地备份一份:p:
    一级 wangzehua 2015-4-17
    5
    好东西,支持一个
    精华:1
    三级 鬼谷子c 2015-4-17
    6
    前排顶一顶,好资料,感谢分享~~~~~这个是substrate来做的hook,还是自己实现的框架呢?
    精华:7
    七级 ThomasKing 2015-4-17
    7


    鬼哥,上次实现的Hook库
    二级 cacorothuo 2015-4-17
    8
    现在已经工作了吗。还是在某地实习。
    一级 OnlyEnd 2015-4-17
    9
    我用substrate做了个过这些反调试的。  远远没有你这个这么强大呀 哈哈哈  学习学习
    一级 vVv一 2015-4-17
    10
    学习一下,感谢分享~
    一级 guxing罗 2015-4-17
    11
    居然是。。。支持大牛。。。。。。。。。。。
    一级 wule 2015-4-18
    12
    火速围观。楼主真好人。
    一级 ztxb 2015-4-20
    13
    感谢楼主分享.下载研究研究.
    一级 deadxing 2015-6-2
    14
    tk
    是tk教主么。。哈哈
    一级 暴强 2016-3-27
    15
    刚学安卓,就碰到各种奇葩的调试问题,这篇资料非常重要完整,谢谢!
    一级 lidagogo 2016-4-9
    16
    意思是HOOK zygote这个进程吗? 这样能获取到 我需要获取的进程的 执行吗?
    不是HOOK我要获取的进程吗?
    一级 jltxgcy 2016-5-4
    17
    请教楼主是通过什么方式注入的?我用的古河的注入方式,不过需要注入两个so,一个是libTKHooklib.so,一个是libtrace_anti.so,第一个so可以成功注入到Zygote,但是第二个so就不能成功注入了,我在java层system.loadlibrary是可以的。
    一级 jltxgcy 2016-5-4
    18
    终于弄出来了,本文中没有写如何注入到Zygote,我下面简单说下:
    参考http://blog.csdn.net/jinzhuojun/article/details/9900105。
    这里要先dlopen libTKHooklib.so ,然后再dlopen libtrace_anti.so。
    再加载之前要在java层先加载一遍,如果加载无误,再去用dlopen加载。如果出现错误,就修改对应的代码,我遇到一个错误,找不到android::AndroidRuntime::findClass(env, JNIREG_CLASS);这个可能和系统有关,注释掉即可。
    static {
                    System.load("/data/data/com.example.anti_antidebug/lib/libTKHooklib.so");
                    System.loadLibrary("trace_anti");
            }
    一级 jltxgcy 2016-5-4
    19
    真心感谢分享。
    一级 yy虫子yy 2016-5-4
    20
    路过留mark
    返回