Skip to content

Android应用反调试 #15

@NazcaLines

Description

@NazcaLines

给Android应用加壳是保护Android应用的常见方法。侠义上的Android加壳,只是将Android源代码隐藏起来,骇客仍然可以通过静态或者动态调试的方法获取到Android应用的源代码。因此,在加壳的同时增加反调试功能能够有效的对抗骇客的入侵行为。
常见的反调试思路有:1.禁止读取应用内存 2.禁止调试器attach 3.采用陷阱使调试器崩溃。本文给出两种实现方法,第一种方法实践了思路1和思路2,第二种方法实践了思路3.

Ptrace

Ptrace是Linux系统下用于进程跟踪的利器。它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。
Linux下的调试器同样使用Ptrace提供的丰富的调试功能进行对进程的调试,并且一个进程同一时间只能被Ptace一次。知道了以上两点,我们就可以让进程ptrace自己以达到不被调试attach的效果。调用ptrace(PTRACE_TRACEME, 0, 0, 0);即可。详细的用法请参考: https://linux.die.net/man/2/ptrace

Inotify

Inotify是Linux提供的用于监视文件系统事件的机制。它可以用来监视文件或者文件夹的变化情况。它可以监视ACCESS(文件访问),CREATE(文件创建),MODIFY(文件修改),OPEN(文件打开)等事件。基本的使用请参考:http://www.man7.org/linux/man-pages/man7/inotify.7.html

结合Ptrace和Inotify反调试

结合上面两种方法,我们可以编写反调试代码。基本的思路:程序进入Native层之后,由父进程fork()子进程,然后子进程ptrace自己,并且在子进程中注册文件监视事件,监视父进程的/proc/ppid/maps文件的访问打开和访问事件和/dev/mem文件的访问和打开事件,一旦发现这两个文件产生了相应事件,就立刻杀死父进程。因为/proc/ppid/maps完整保存了进程中各个ELF文件在内存中的位置,/dev/mem文件是内存映射文件,由此信息,入侵者可以使用dd命令完成内存dump。

void antidebug(pid_t parent_pid) {
    int fd = inotify_init();
    int used = 0;
    char buf[256], readbuf[2048];
    sprintf(buf, "/proc/%d/maps", parent_pid);

    int watch_fd = inotify_add_watch(fd, buf, IN_ALL_EVENTS);
    while(1){
	used = 0;
	FD_ZERO(readbuf);
	FD_SET(fd, readbuf);
	int ret = select(fd + 1, readbuf, 0, 0, 0);
	if (ret == -1)
		break;
	else if(ret) {
		int len = read(fd, readbuf, 2048);
		while(used < len) {
			struct inotify_event* event = (struct inotify_event*)readbuf + used;
					
			if(event->mask & IN_ACCESS || event->mask && IN_OPEN) {
				kill(parent_pid, SIGKILL);
			}
			used += sizeof(struct inotify_event) + event->len;
		}
	}
    }
    inotify_rm_watch(fd, watch_fd);
    close(fd);
}

这段代码将会运行在子进程中。首先设置/proc/pid/maps的所有文件事件,一旦有事件发生,inotify会收到inotify_event数据结构。在此数据结构中,我们可以获取到发生的具体事件,如果出现了ACCESS或者OPEN,则立即杀死父进程。
Select函数是Linux下的IO复用函数,通过轮询机制可以同时监听多个文件描述符,本demo只有一个文件描述符,所以使用select是多此一举,但是当我们需要同时监视多个文件时,select函数就显得非常有必要了。

调试器工作原理简述

Linux下有信号机制,当出现某一事件时,Linux系统会在响应进程的进程控制块(Task数据结构)中将对应的信号位置1,这就表明了内核通过信号机制通知了应用程序发生了特定事件。比较常见的有SIGTRAP(调试信号),SIGSEGV(内存非法访问),SIGKILL(中止了进程)等,这些信号量只能被消费一次,当进程接收了这些信号,这些信号也就过期了。
当某一进程被调试时,触发了断点,那么操作系统会就产生SIGTRAP信号,加入这个进程并没有被调试器调试,那么此进程就会崩溃,被操作系统杀死。如果此进程被调试器调试,那么程序控制权就会交给调试器,调试器因此可以通过ptrace获取寄存器信息等等调试信息。一个正常的出发断点的流程是这样的:调试器设置断点即将此处的二进制指令设置为CPU的断点指令,同时保存原有指令。当断点被触发后,调试器恢复此处的原有指令,并且回退pc值,继续执行原程序。
但是,一切都有意外。加入一段代码中原本就拥有一个断点指令会发生什么?如上文所说,该程序会接收到SIGTRAP信号,然后被杀死。那么调试器去调试这段代码又会发生什么呢?调试器失去工作能力。按照上文调试器的流程,它首先保存原程序的指令,很不幸的,这个指令是断点指令。但是调试器仍然保存了断点指令,然后又用断点指令代替了原指令,最后恢复原指令并回退PC。于是,调试器又遇到了断点指令,终于,它陷入了死循环。
这样的做法虽然可以对抗调试器,但是原程序也无法正常运行了。为了能让源程序能够正常运行,我们注册信号处理函数,专门处理SIGTRAP信号量。那么在正常情况下,原程序应当可以在执行流到达断点指令时,正确的替换断点指令。当有调试器的时候,调试器的优先级更高,能够在我们的信号处理函数之前获取信号量,因此无法正常调试。

demo:

char brkt_code[] = {0x01, 0xbe,   //breakpoint
                    0xc0, 0x46};  //mov r8, r8

void sigtrap_handler(int signal) {
	char continue_code[] = {0xc0, 0x46,  //mov r8, r8
	                        0xf7, 0x46}; //mov pc, lr

    memcpy(brkt_addr, continue_code, CODESIZE);
    __clear_cache((void*)brkt_addr,(void*)(brkt_addr + CODESIZE)); // need to clear cache
    LOGD("changed code.");
}

void antidebug() {
    char* addr = (char*) malloc(PAGESIZE * 2);
    memset(addr, 0, PAGESIZE * 2);
    brkt_addr = (char *)(((int) addr + PAGESIZE-1) & ~(PAGESIZE-1));

    __asm__  (
	"PUSH {R0-R4, LR} \n\t"
	"MOV R0, PC       \n\t"
	"ADD R0, R0, #4   \n\t"
	"MOV LR, R0       \n\t"
	"MOV PC, %0       \n\t"
	"POP {R0-R5}      \n\t"
	"MOV LR, R5       \n\t"
	:
	:"r"(brkt_addr)
	);
}

代码解析:
首先采用内嵌汇编的方式让程序执行流主动跳转到brkt_addr地址上,该地址上的代码正是0x01,0xbe ARM体系结构下的断点指令,则此时会出发SIGTRAP信号量,被我们注册的信号量处理函数sigtrap_handler接收,并将正常的代码continue_code复制到brkt_addr中。当程序处理完毕再次运行时,brkt_addr中的代码已经变成了continue_addr的代码,因此程序可以重新跳转到antidebug中执行。

ARM汇编代码解析:
PUSH {R0-R4, LR} \n\t保存基本的寄存器信息

MOV R0, PC \n\tADD R0, R0, #4 \n\t等同于 ADD R0, PC, #2但不知道为什么后者是非法的。我翻看了ARM文档,感觉没问题呀,但是我在测试的时候手机就会报错。因为ARM体系结构流水线执行指令,所以当执行到这句话的时候,真实的PC值ARM下4指令之后了,THUMB下2指令以后。
所以MOV R0, PC \n\t``R0保存的是MOV LR, R0语句的地址,执行ADD R0, R0, #4则R0保存的是POP {R0-R5} ```语句的地址。

MOV PC, %0将brkt_addr的值传递给PC,改变PC等同于改变程序执行流,所以程序此处进入了我们构造的堆中。

最后, 堆中代码执行完毕,回到POP {R0-R5} 恢复寄存器环境。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions