Android反调试常见方法
ptrace
ptrace自己,使得android_server附加不上
void anti_ptrace()
{
ptrace(PTRACE_TRACEME, 0, 0, 0);
}
由于ptrace_me方法在JNI_OnLoad方法中调用,若是以《IDA Pro动态调试so文件》中的动态调试方法调试,则在调用JNI_OnLoad之前IDA就附加了程序,所以以上方法并不能达到反调试的目的。
既然如此,可以检测ptrace的返回值,若附加成功则返回0,失败则返回-1。若ptrace函数返回了-1,说明当前程序正在被调试。将ptrace方法修改为如下代码:
void ptrace_me(){
int ch=ptrace(PTRACE_TRACEME,0,NULL,NULL);
if (ch == -1){
int pid=getpid();
kill(pid,SIGKILL);
}
}
检测Tracepid的值
查看/proc/self/status
程序正常状态下 其 TracerPid 为 0,当处于调试状态时候, TracerPid 为调试那个进程pid, 也就是非0。
void anti_Tracepid(){
try{
const int bufsize = 1024;
char filename[bufsize];
char line[bufsize];
int pid = getpid();
sprintf(filename, “/proc/%d/status”, pid);
FILE* fd = fopen(filename, “r”);
if (fd !=NULL){
while (fgets(line, bufsize, fd)){
if (strncmp(line, “TracerPid”, 9) == 0){
int statue = atoi(&line[10]);
if (statue != 0){
fclose(fd);
int ret = kill(pid, SIGKILL);
}
break;
}
}
fclose(fd);
}
else{
// LOGD(“open %s fail…”, filename);
}
} catch (…){
}
}
在native下实现,并在JNI_Onload下调用
对android_server端口号进行检测
ida的android_server
void anti_serverport() {
const int bufsize=512;
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 (strncmp(line, “5D8A”, 4)==0){
int ret = kill(pid, SIGKILL);
}
}
}
fclose(fd);
}
当然只需将端口设为其他即可绕过
./android_server -p 24000
-p选项指定android_server的运行端口,这里指定了24000端口。
检测调试进程的名字
调试器的进程名则被存储在/proc/<pid>/cmdline
文件中
void anti_processstatus(){
const int bufsize = 1024;
char filename[bufsize];
char line[bufsize];
char name[bufsize];
char nameline[bufsize];
int pid = getpid();
//先读取Tracepid的值
sprintf(filename, “/proc/%d/status”, pid);
FILE *fd=fopen(filename,“r”);
if(fd!=NULL){
while(fgets(line,bufsize,fd)){
if(strstr(line,“TracerPid”)!=NULL)
{
int statue =atoi(&line[10]);
if(statue!=0){
sprintf(name,"/proc/%d/cmdline",statue);
FILE *fdname=fopen(name,“r”);
if(fdname!= NULL){
while(fgets(nameline,bufsize,fdname)){
if(strstr(nameline,“android_server”)!=NULL){
int ret=kill(pid,SIGKILL);
}
}
}
fclose(fdname);
}
}
}
}
fclose(fd);
}
对常规放置目录进行检测
比较常见的android_server一般放在/data/local/tmp下,对该目录下的文件名进行检测
void anti_localtmp(){
int pid=getpid();
const int bufsize=1024;
char line[bufsize];
char filename[bufsize];
sprintf(filename,"/data/local/tmp");
FILE *fd=fopen(filename,“r”);
if(fd!=NULL){
while(fgets(line,bufsize,fd)){
if(strstr(line,“android_server”)!=NULL){
int ret=kill(pid,SIGKILL);
}
}
}
fclose(fd);
}
时间差异检测
正常情况下,一段程序在两行代码之间的时间差是很短的,而对于调试程序来说,单步调试时两行代码之间的时间差会比较大,检测两条代码之间的时间差,可以大概率判断程序是否被调试。
在C语言中可以使用函数gettimeofday()
来得到时间,其精度可以达到微秒。
这个方法一个不好的特点是需要一定的代码跨度,因此可能需要暴露部分代码逻辑。因为如果两个时间的取值点非常非常近,那很可能调试者在两者之间没有断点从而迅速跳过。
#include<sys/time.h>
int gettimeofday(struct timeval *tv,struct timezone *tz )
void check_time(){
int pid=getpid();
struct timeval start;
struct timeval end;
struct timezone tz;
gettimeofday(&start,&tz);
gettimeofday(&end,&tz);
int timeoff=end.tv_sec-start.tv_sec;
if (timeoff>1){
kill(pid,SIGKILL);
}
}
Java层内置函数检测
Android的android.os.Debug类提供了isDebuggerConnected()方法,用于检测是否有调试器挂载到程序上。
由于是Java层的系统调用,所以相比于Native层,本方法会更容易被发现,且被Hook篡改返回值也会更简单。
if (!Debug.isDebuggerConnected()){
System.loadLibrary("native-lib");
}else {
Process.killProcess(Process.myPid());
}
检测break point指令
IDA等调试器在动态调试时会向断点地址插入breakpoint断点指令,而把原来的指令暂时备份到别处,所以可以通过扫描代码中是否有breakpoint汇编指令即可。
一般来说,Android的汇编代码有ARM和Thumb,因此需要都检测一下:
- Arm:0x01,0x00,0x9f,0xef
- Thumb16:0x01,0xde
- Thumb32:0xf0,0xf7,0x00,0xa0
#include <elf.h>
...
unsigned int getLibAddr(){
unsigned int ret=0;
char name[]="libnative-lib.so";
char buf[4096],*tmp;
int pid=getpid();
FILE *fp;
sprintf(buf,"/proc/%d/maps",pid);
fp=fopen(buf,"r");
if (fp!=NULL){
while (fgets(buf,sizeof(buf),fp)!=NULL){
if (strstr(buf,name)){
tmp=strtok(buf,"-");
ret=strtoul(tmp,NULL,16);
break;
}
}
}
fclose(fp);
return ret;
}
bool check_break_point(){
unsigned int base,offset,pheader;
Elf32_Ehdr *elfhdr; // ELF_Header
Elf32_Phdr *elfphdr; // Program_Header
base=getLibAddr();
if (base == 0){
return false;
}
elfhdr=(Elf32_Ehdr*) base;
pheader=base+elfhdr->e_phoff; // e_phoff:程序头偏移
for (int i=0;i<elfhdr->e_phnum;i++){ // e_phnum:程序头表中元素的个数
elfphdr=(Elf32_Phdr*)(pheader+i*sizeof(Elf32_Phdr));
if (!(elfphdr->p_flags & 1)){
continue;
}
offset=base+elfphdr->p_vaddr; // p_vaddr:段的数据映射到虚拟地址空间中的位置
offset+=sizeof(Elf32_Ehdr)+sizeof(Elf32_Phdr)*elfhdr->e_phnum;
char *p=(char *)offset;
for (int j = 0; j < elfphdr->p_memsz; ++j) { // p_memsz:段在虚拟地址空间中的长度
if (*p == 0x01 && *(p+1) == 0xde){ // Thumb16
return true;
} else if (*p == 0xf0 && *(p+1) == 0xf7 && *(p+2) == 0x00 && *(p+3) == 0xa0){ // Thumb32
return true;
} else if (*p == 0x01 && *(p+1) == 0x00 && *(p+2) == 0x9f && *(p+3) == 0xef){ // ARM
return true;
}
p++;
}
}
return false;
}
jint JNI_OnLoad(JavaVM* vm, void* reserved){
if (check_break_point()){
int pid=getpid();
kill(pid,SIGKILL);
}
...
}
通过使用Linux inotify特性来对文件的读写,以及打开等权限进行监控
在动态调试的过程中,一般会查看调试进程的虚拟地址空间或者是dump内存,这时就会涉及到对文件的读写以及打开的权限,这时候对它们进行检测就能发现是否正在被破解。Linux下的Inotify就可以实现对文件系统事件的打开,读写的监管。如果通过Inotify收到事件的变化,我们就Kill掉进程。
void anti_debug06() {
int ret, len, i;
int pid6 = 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”, pid6);
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(pid6, SIGKILL);
return;
}
i += sizeof(struct inotify_event) + event->len;
}
}
}
}
inotify_rm_watch(fd, wd);
close(fd);
}