BruceFan's Blog

Stay hungry, stay foolish

0%

Android逆向之反调试

为了防止通过调试将内存中的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) {
// error assumes emulator
}
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()
{
...
// 获取tracerpid之后
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;
//从maps中读取elf文件在内存中的起始地址
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); // 使得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(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安全攻防实战》