Android反调试笔记

Posted by Jieming Gu on 2017-01-04

Android反调试常常出现在init.arrayJNI_Onload中。除了反调试外,应用还可能在上述两个地方初始化变量。

1)代码执行时间检测

读取系统时间,检测关键代码执行耗时。类似函数:timegettimeofday

1
2
3
4
5
6
7
time_t t1, t2;
time (&t1);
/* Parts of Important Codes */
time (&t2);
if (t2 - t1 > 2) {
puts ("debugged");
}

2)运行环境检测

进程的状态信息能通过procfs系统反馈给用户空间,调试会使进程状态发生变化。

  1. TCP端口/android_server检测
  2. 读取/proc/%d/wchanep_poll表示未调试,ptrace_stop表示调试状态
  3. 读取/proc/pid/status:State属性值T表示调试状态,TracerPid属性值表示正在调试此进程的pid。非调试情况下State为SR, TracerPid为0

3)ptrace

ptrace()是Linux的系统调用,也是IDA等调试器实现的基础。它提供了一个进程跟踪另一个进程寄存器、内存等的能力,一个进程只能被一个进程ptrace()

  1. ptrace()应用自身
  2. fork()子进程相互ptrace()

使用形式:

  1. ptrace(PTRACE_TRACEME, 0, 0, 0)
  2. syscall(__NR_ptrace, 0, 0, 0)

简单的反调试会直接调用ptrace(),只需在进程被ptrace()前让调试器挂载即可;有的反调试会根据ptrace()的返回值(0表示成功)来做进一步的处理。

4)模拟器检测

用户层行为和数据检测、模拟器特有属性值,以及模拟器体系结构特征。

  1. 电池状态和电流、模拟器默认电话号码检测,检测设备IDS是不是000000000000000、imsi id是不是310260000000000,手机运营商等
  2. API Demo,Dev tool:一般模拟器上才有的应用
  3. 读取/system/build.prop文件
  4. 检查模拟器特有文件:/dev/socket/qemud、/dev/qemu_pipe、/sysrtem/bin/qemud、/dev/qemu_pipe、/dev/qemu_trace等

5)断点检测

ARM程序下断点,调试器完成两件事:

  1. 保存目标地址处指令
  2. 将目标地址处指令替换成断点指令
指令集 指令
ARM 0x01, 0x00, 0x9f, 0xef
Thumb 0x01,0xde

当命中断点时,系统产生SIGTRAP信号,调试器收到信号后完成下面操作:

  1. 恢复断点处原指令
  2. 回退被跟踪进程的当前PC

这时控制权回到被调试程序,正好执行断点位置指令。这就是ARM平台断点的基本原理。

断点指令会替换内存中原有指令,因此通过检测内存中的断点指令,可以检测调试:

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
void checkBreakPoint ()
{
int i, j;
unsigned int base, offset, pheader;
Elf32_Ehdr *elfhdr;
Elf32_Phdr *ph_t;
base = getLibAddr ("libdemo.so");
if (base == 0) {
LOGI ("getLibAddr failed");
return;
}
elfhdr = (Elf32_Ehdr *) base;
pheader = base + elfhdr->e_phoff;
for (i = 0; i < elfhdr->e_phnum; i++) {
ph_t = (Elf32_Phdr*)(pheader + i * sizeof(Elf32_Phdr)); // traverse program header
if ( !(ph_t->p_flags & 1) ) continue;
offset = base + ph_t->p_vaddr;
offset += sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) * elfhdr->e_phnum;
char *p = (char*)offset;
for (j = 0; j < ph_t->p_memsz; j++) {
if(*p == 0x01 && *(p+1) == 0xde) {
LOGI ("Find thumb bpt %p", p);
} else if (*p == 0xf0 && *(p+1) == 0xf7 && *(p+2) == 0x00 && *(p+3) == 0xa0) {
LOGI ("Find thumb2 bpt %p", p);
} else if (*p == 0x01 && *(p+1) == 0x00 && *(p+2) == 0x9f && *(p+3) == 0xef) {
LOGI ("Find arm bpt %p", p);
}
p++;
}
}
}

6)inotify文件系统监控

inotify是内核用于通知用户态文件系统变化的机制,当文件被访问、修改、删除等时,用户态可以快速感知。

1.调用inotify_init()初始化实例并返回文件描述符,每个文件描述符关联一个事件队列:

1
int fd = inotify_init();

2.拿到这个文件描述符后告诉内核,哪些文件发生哪些事件时需要通知。通过inotify_add_watch()实现:

1
int wd = inotify_add_watch(fd, path, mask);

fd是文件描述符,path表示监控项的目标路径(可以是文件目录等)。mask表示关注事件的掩码,例如:IN_ACCESS代表访问,IN_MODIFY代表修改等。

与之对应,可以通过inotify_rm_watch()来删除一个watch:

1
int ret = inotify_rm_watch(fd, wd);

每当监视的文件发生变化时,内核便给fd关联的事件队列里面塞一个文件事件。文件事件用inotify_event 结构表示,可以通过read()来读取:

1
2
3
4
5
6
7
struct inotify_event {
__s32 wd; /* watch descriptor */
__u32 mask; /* watch mask */
__u32 cookie; /* cookie to synchronize two events */
__u32 len; /* length (including nulls) of name */
char name[0]; /* stub for possible name */
};
1
size_t len = read(fd, buf, LEN);

通过监视/proc/pid/maps文件的打开事件,可防针对360加固的dump脱壳。