为了防止通过调试将内存中的DEX文件dump出来,APP一般都会采用各种反调试来进行保护,这篇文章主要对常见的一些反调试技术进行介绍。
ptrace自身或子进程相互ptrace
代码非常简单,在JNI_Onload最开始加上这个函数即可:
1 2 3 4
| void anti_ptrace(void) { ptrace(PTRACE_TRACEME, 0, 0, 0); }
|
其中PTRACE_TRACEME代表:本进程被自身进程ptrace。一般一个进程只能被附加一次,如果应用已被自身进程附加,后面的调试附加就会失败。
检查调试状态
这种方式是通过Android中的API进行检验,有两种方法:
检查应用是否属于debug模式
调用Android中的flag属性:ApplicationInfo.FLAG_DEBUGGABLE
,判断是否属于debug模式:
1 2 3
| public static boolean isDebuggable(Context context) { return (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; }
|
这个是为了防止破解者在应用的AndroidManifest.xml中添加:android:debuggable=true
属性,然后对应用进行调试。解决方法是通过将/default.prop中的Android系统属性ro.debuggable
修改为1即可使得所有应用可以被调试。
检查应用是否处于调试状态
这种方法也是通过系统的API:android.os.Debug.isDebuggerConnected();
判断当前应用有没有被调试。用jdb进行连接操作(jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700)时,这段代码就会返回true。所以可以通过这个API判断当前应用是否处于调试状态来进行反调试。
检查端口
破解逆向时通常需要借助IDA,使用IDA时,在设备中要启动android_server来进行通信,android_server会默认占用23946端口:
1 2 3
| root@hammerhead:/data/local/tmp # ./android_server IDA Android 32-bit remote debug server(ST) v1.19. Hex-Rays (c) 2004-2015 Listening on port #23946...
|
查看设备的TCP端口使用情况:
1 2 3
| root@hammerhead:/ # cat /proc/net/tcp sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode 0: 00000000:5D8A 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 63939 1 00000000 100 0 0 10 -1
|
其中5D8A转换成十进制就是23946,而看到uid是0,因为运行android_server是root身份。只要在JNI_Onload最开始加上下面的函数即可检测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void check_port() { const int bufsize = 256; 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 (strstr(line, "5D8A") != NULL) { int ret = kill(pid, SIGKILL); } } } fclose(fd); }
|
解决办法就是换一个端口:
1 2 3
| root@hammerhead:/data/local/tmp # ./as -p6666 IDA Android 32-bit remote debug server(ST) v1.19. Hex-Rays (c) 2004-2015 Listening on port #6666...
|
读取进程状态
在使用IDA进行调试的时候,需要在设备端启动android_server进行通信,那么被调试的进程就会被附加,TracerPid就是android_server进程的pid值了。所以可以在应用中的native层加上一个循环检查status中的TracerPid字段的值,如果非0或是非本进程pid(如果采用了第一种方法),那么就认为被附加调试了。还可以检查进程列表中有没有android_server进程。
1 2 3 4 5 6 7 8 9 10 11
| root@hammerhead:/ # cat /proc/12730/status Name: com.bruce State: t (tracing stop) <-非调试状态为S或R Tgid: 12730 Pid: 12730 PPid: 179 TracerPid: 32354 <- 非0非本进程pid,被附加 Uid: 10128 10128 10128 10128 Gid: 10128 10128 10128 10128 FDSize: 256 Groups: 50128
|
将下面这个函数加到JNI_Onload中,或者在JNI_Onload中新建一个线程不停地进行检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void check_tracerpid() { int pid = getpid(); int bufsize = 256; char filename[bufsize]; char line[bufsize]; int tracerpid; FILE *fp; sprintf(filename, "/proc/%d/status", pid); fp = fopen(filename, "r"); if (fp != NULL) { while (fgets(line, bufsize, fp)) { if (strstr(line, "TracerPid") != NULL) { tracerpid = atoi(&line[10]); if (tracerpid != 0) { int ret = kill(pid, SIGKILL); } break; } } fclose(fp); } }
|
解决方法:
1.修改内存中的值,但是有时候是循环检查,每次都修改不方便;
2.patch so文件,将循环检查的函数NOP掉;
3.一种比较复杂的方法,但是可以一劳永逸:修改内核信息
读取/proc/pid/wchan:
1 2
| root@hammerhead:/ # cat /proc/12730/wchan ptrace_stop <- 非调试状态为ep_poll
|
模拟器检测
检测应用是不是运行在一台模拟器中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static boolean isEmulator() { try { Class systemPropertyClazz = Class.forName("android.os.SystemProperties"); boolean kernelQemu = getProperty(systemPropertyClazz, "ro.kernel.qemu").length() > 0; boolean hardwareGoldfish = getProperty(systemPropertyClazz, "ro.hardware").equals("goldfish"); boolean modelSdk = getProperty(systemPropertyClazz, "ro.product.model").equals("sdk"); if (kernelQemu || hardwareGoldfish || modelSdk) return true; } catch (Exception e) { } return false; }
private static String getProperty(Class clazz, String propertyName) throws Exception { return (String) clazz.getMethod("get", new Class[] {String.class}).invoke(clazz, new Object[] {propertyName}); }
|
检测android_server关键字以及文件目录
调试进程的时候,这个进程会被IDA中的android_server附加,而在/proc/pid/cmdline
中会有进程的进程名,因此通过android_server的进程号可以找到它的进程名:
1 2
| root@hammerhead:/ # cat /proc/3514/cmdline ./android_server
|
android_server的进程号可以通过TracerPid来获得。如果有这个名字说明应用正在被调试。
一般情况下,android_server都会放在/data/local/tmp目录下,因此还可以检测这个目录中有没有android_server,如果有就退出程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void check_andser() { ... if (tracerpid != 0) { sprintf(filename, "/proc/%d/cmdline", tracerpid); FILE *fd = fopen(filename, "r"); if (fd != NULL) { while (fgets(nameline, bufsize, fd)) { if (strstr(nameline, "android_server") != NULL) { int ret = kill(pid, SIGKILL); } } } } }
|
解决方法:将android_server重命名一下,如果有目录检查就换一个目录。
检测调试状态下的软件断点
调试时下断点是利用的ptrace系统函数,在调试器设置断点的时候,首先完成两件事:
- 保存目标地址上的数据
- 将目标地址上的头几个字节替换为breakpoint指令,命中断点触发breakpoint,这时程序向操作系统发送SIGRAP信号,调试器收到SIGRAP信号后,调试器会回退被跟踪进程的当前PC值,当控制权回到原进程时,PC就恰好指向了断点所在位置,这就是调试器设置断点的基本原理。
软件断点通过改写目标地址的头几字节为breakpoint指令,接下来类似于前面所讲的几种方法,直接进行检测文件,可以遍历so中可执行segment,查找是否出现breakpoint指令即可。
关键函数实现如下:
1.读取ELF文件在内存中的地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| unsigned long GetLibAddr() { unsigned long ret = 0; char name[] = "libxxx.so"; char buf[4096], *temp; int pid; FILE *fp; pid = getpid(); sprintf(buf, "/proc/%d/maps", pid); fp = fopen(buf, "r"); if (fp == NULL) { puts("open failed"); goto _error; } while (fgets(buf, sizeof(buf), fp)) { if (strstr(buf, name)) { temp = strtok(buf, "-"); ret = strtoul(temp, NULL, 16); break; } } _error: fclose(fp); return ret; }
|
2.读取完以后,根据其对应的偏移地址进行检测,有没有ARM、Thumb、Thumb2的断点指令,如果有就kill进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| void check_breakpoint() { int pid = getpid(); Elf32_Ehdr *elfhdr; Elf32_Phdr *pht; unsigned int size, base, offset, phtable; int n, i,j; char *p; base = GetLibAddr(); if (base == 0) { return; } elfhdr = (Elf32_Ehdr *) base; phtable = elfhdr->e_phoff + base; for (i = 0; i < elfhdr->e_phnum; i++) { pht = (Elf32_Phdr *)(phtable + i * sizeof(Elf32_Phdr)); if (pht->p_flags & 1) { offset = pht->p_vaddr + base + sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr)*elfhdr->e_phnum; p = (char*)offset; size = pht->p_memsz; for (j=0, n=0; j < size; ++j, ++p) { if (*p == 0x10 && *(p+1) == 0xde) { n++; int ret1 = kill(pid, SIGKILL); break; } else if (*p == 0xf0 && *(p+1) == 0xf7 && *(p+2) == 0x00 && *(p+3) == 0xa0) { n++; int ret2 = kill(pid, SIGKILL); break; } else if(*p == 0x01 && *(p+1) == 0x00 && *(p+2) == 0x9f && *(p+3) == 0xef) { n++; int ret3 = kill(pid, SIGKILL); break; } } } } }
|
解决方法:一般这种函数可以在JNI_Onload、com_java_XX这类函数中调用,经过几次动态调试就可以找出关键函数,将其NOP掉就可以了。
使用Inotify对文件进行监控
在动态调试的过程中,一般会查看调试进程的虚拟地址空间或者是dump内存,这时就会涉及到对文件的读写以及打开的权限,这时候对它们进行检测就能发现是否正在被破解。Linux下的Inotify就可以实现对文件系统事件的打开,读写的监管。如果通过Inotify收到事件的变化,我们就Kill掉进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| void check_inotify() { int ret, len, i; int pid = 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", pid); wd = inotify_add_watch(fd, buf, IN_ALL_EVENTS); if (wd >= 0) { while (1) { i = 0; FD_ZERO(&readfds); FD_SET(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(pid, SIGKILL); return; } i += sizeof(struct inotify_event) + event->len; } } } } inotify_rm_watch(fd, wd); close(fd); }
|
代码执行时间差检测
动态调试的时候关键代码的前后时间差比正常执行的时候要大许多,因此可以计算代码执行时间,如果超出一般正常情况下设定值,就认为代码被调试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int gettimeofday(struct timeval *tv, struct timezone *tz); void check_time() { int pid = getpid(); struct timeval t1; struct timeval t2; struct timezone tz; gettimeofday(&t1, &tz); gettimeofday(&t2, &tz); int timeoff = (t2.tv_sec) - (t1.tv_sec); if (timeoff > 1) { int ret = kill(pid, SIGKILL); return ; } }
|
reference
Android反调试方法总结以及源码实现之检测篇(一)
Android安全防护之旅—Android应用”反调试”操作的几种方案解析
《Android安全攻防实战》