调试与反调试

篇幅有限

完整内容及源码关注公众号:ReverseCode,发送

ANDROID调试检测技术汇总

1. IDA调试端口检测

原理: 调试器远程调试时,会占用一些固定的端口号。

做法: 读取/proc/net/tcp,查找IDA远程调试所用的23946端口,若发现说明进程正在被IDA调试。 (也可以运行netstat -apn结果中搜索23946端口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CheckPort23946ByTcp(){ 
FILE* pfile=NULL;
char buf[0x1000]={0};
// 执行命令
char* strCatTcp= "cat /proc/net/tcp |grep :5D8A";
//char* strNetstat="netstat |grep :23946";
pfile=popen(strCatTcp,"r");
if(NULL==pfile) {
LOGA("CheckPort23946ByTcp popen打开命令失败!\n");
return;
}

// 获取结果
while(fgets(buf,sizeof(buf),pfile)) {
// 执行到这里,判定为调试状态
LOGA("执行cat /proc/net/tcp |grep :5D8A的结果:\n");
LOGB("%s",buf);
}//while
pclose(pfile);
}

2. 调试器进程名检测

原理:远程调试要在手机中运行android_server gdbserver gdb等进程。

做法: 遍历进程,查找固定的进程名,找到说明调试器在运行。

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
void SearchObjProcess() { 
FILE* pfile=NULL;
char buf[0x1000]={0};
// 执行命令
//pfile=popen("ps | awk '{print $9}'","r"); // 部分不支持awk命令
pfile=popen("ps","r");
if(NULL==pfile) {
LOGA("SearchObjProcess popen打开命令失败!\n");
return;
}

// 获取结果
LOGA("popen方案:\n");
while(fgets(buf,sizeof(buf),pfile)) {
// 打印进程
LOGB("遍历进程:%s\n",buf);
// 查找子串
char* strA=NULL,strB=NULL,strC=NULL,strD=NULL;
strA=strstr(buf,"android_server");
strB=strstr(buf,"gdbserver");
strC=strstr(buf,"gdb");
strD=strstr(buf,"fuwu");
if(strA || strB ||strC || strD) {
// 执行到这里,判定为调试状态
LOGB("发现目标进程:%s\n",buf);
}//if
}//while
pclose(pfile);
}

3 父进程名检测

原理: 有的时候不使用apk附加调试的方法进行逆向,而是写一个.out可执行文件直接加载so进行 调试,这样程序的父进程名和正常启动apk的父进程名是不一样的。

测试发现:

(1)正常启动的apk程序:父进程是zygote

(2)调试启动的apk程序:在AS中用LLDB调试发现父进程还是zygote

(3)附加调试的apk程序:父进程是zygote

(4)vs远程调试 用可执行文件加载so:父进程名为gdbserver 结论:父进程名非zygote的,判定为调试状态。

读取/proc/pid/cmdline,查看内容是否为zygote

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
void CheckParents() 
{
///////////////////
// 设置buf
char strPpidCmdline[0x100]={0};
snprintf(strPpidCmdline, sizeof(strPpidCmdline), "/proc/%d/cmdl ine", getppid());
// 打开文件
int file=open(strPpidCmdline,O_RDONLY);
if(file<0) {
LOGA("CheckParents open错误!\n");
return;
}
// 文件内容读入内存
memset(strPpidCmdline,0,sizeof(strPpidCmdline));
ssize_t ret=read(file,strPpidCmdline,sizeof(strPpidCmdline));
if(-1==ret) {
LOGA("CheckParents read错误!\n");
return;
}

// 没找到返回0
char sRet=strstr(strPpidCmdline,"zygote");
if(NULL==sRet) {
// 执行到这里,判定为调试状态
LOGA("父进程cmdline没有zygote子串!\n");
return;
}
int i=0;
return;
}

4 自身进程名检测

原理: 和上条一样,也是写个.out加载so来脱壳的场景, 正常进程名一般是apk的com.xxx这样的格式。

代码: 略

5 apk线程检测

原理:

同样.out加载so来脱壳的场景, 正常apk进程一般会有十几个线程在运行(比如会有jdwp线程), 自己写可执行文件加载so一般只有一个线程, 可以根据这个差异来进行调试环境检测。

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
void CheckTaskCount()
{
char buf[0x100]={0};
char* str="/proc/%d/task";
snprintf(buf,sizeof(buf),str,getpid());

// 打开目录:
DIR* pdir = opendir(buf);
if (!pdir) {
perror("CheckTaskCount open() fail.\n");
return;
}
// 查看目录下文件个数:
struct dirent* pde=NULL;
int Count=0;
while ((pde = readdir(pdir))) {
// 字符过滤
if ((pde->d_name[0] <= '9') && (pde->d_name[0] >= '0')) {
++Count;
LOGB("%d 线程名称:%s\n",Count,pde->d_name);
}
}
LOGB("线程个数为:%d",Count);
if(1>=Count) {
// 此处判定为调试状态.
LOGA("调试状态!\n");
}
int i=0;
return;
}

6 apk进程fd文件检测

原理: 根据/proc/pid/fd/路径下文件的个数差异,判断进程状态。 (apk启动的进程和非apk启动的进程fd数量不一样) (apk的debug启动和正常启动,进程fd数量也不一样)

代码: 略

7 安卓系统自带调试检测函数

// android.os.Debug.isDebuggerConnected();

原理: 分析android自带调试检测函数isDebuggerConnected()在native的实现, 尝试在native使用。

做法:

(1)dalvik模式下: 找到进程中libdvm.so中的dvmDbgIsDebuggerConnected()函数, 调用他就能得知程序是否被调试。

dlopen(/system/lib/libdvm.so) dlsym(_Z25dvmDbgIsDebuggerConnectedv)

(2)art模式下: art模式下,结果存放在libart.so中的全局变量gDebuggerActive中, 符号名为_ZN3art3Dbg15gDebuggerActiveE。 但是貌似新版本android不允许使用非ndk原生库,dlopen(libart.so)会失败。 所以无法用dalvik那样的方法了。 有一种麻烦的方法,手动在内存中搜索libart模块,然后手工寻找该全局变量符号。

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
// 只写了dalvik的代码,art的就不写了 
typedef unsigned char wbool;
typedef wbool (*PPP)();

void NativeIsDBGConnected()
{
void* Handle=NULL;
Handle=dlopen("/system/lib/libdvm.so", RTLD_LAZY);
if(NULL==Handle) {
LOGA("dlopen打开libdvm.so失败!\n");
return;
}
PPP Fun = (PPP)dlsym(Handle, "_Z25dvmDbgIsDebuggerConnectedv");
if(NULL==Fun) {
LOGA("dlsym获取_Z25dvmDbgIsDebuggerConnectedv失败!\n");
return;
} else {
wbool ret = Fun();
if(1==ret) {
// 此处判定为调试模式
LOGA("dalvikm模式,调试状态!\n");
return;
}
}
return;
}

8 ptrace检测

原理: 每个进程同时刻只能被1个调试进程ptrace,再次p自己会失败。

做法:

1 主动ptrace自己,根据返回值判断自己是否被调试了。

2 或者多进程ptrace。

1
2
3
4
5
6
7
8
9
10
11
12
// 单线程ptrace 
void ptraceCheck()
{
// ptrace如果被调试返回值为-1,如果正常运行,返回值为0 int iRet=ptrace(PTRACE_TRACEME, 0, 0, 0);
if(-1 == iRet) {
LOGA("ptrace失败,进程正在被调试\n");
return;
} else {
LOGB("ptrace的返回值为:%d\n",iRet);
return;
}
}

9 函数hash值检测

原理: so文件中函数的指令是固定,但是如果被下了软件断点,指令就会发生改变(断点地址被改 写为bkpt断点指令),可以计算内存中一段指令的hash值进行校验,检测函数是否被修改或 被下断点。

代码: 略

10 断点指令检测

原理: 上面说了,如果函数被下软件断点,则断点地址会被改写为bkpt指令, 可以在函数体中搜索bkpt指令来检测软件断点。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// IDA 6.8 断点扫描 
// 参数1:函数首地址 参数2:函数size
typedef uint8_t u8;
typedef uint32_t u32;

void checkbkpt(u8* addr,u32 size){
// 结果
u32 uRet=0;
// 断点指令
// u8 armBkpt[4]={0xf0,0x01,0xf0,0xe7};
// u8 thumbBkpt[2]={0x10,0xde};
u8 armBkpt[4]={0};
armBkpt[0]=0xf0;
armBkpt[1]=0x01;
armBkpt[2]=0xf0;
armBkpt[3]=0xe7;
u8 thumbBkpt[2]={0};
thumbBkpt[0]=0x10;
thumbBkpt[1]=0xde;
// 判断模式
int mode=(u32)addr%2;
if(1==mode) {
LOGA("checkbkpt:(thumb mode)该地址为thumb模式\n");
u8* start=(u8*)((u32)addr-1);
u8* end=(u8*)((u32)start+size);
// 遍历对比
while(1) {
if(start >= end) {
uRet=0;
LOGA("checkbkpt:(no find bkpt)没有发现断点.\n");
break;
}
if( 0==memcmp(start,thumbBkpt,2) ) {
uRet=1;
LOGA("checkbkpt:(find it)发现断点.\n");
break;
}
start=start+2;
}//while
}//if
else {
LOGA("checkbkpt:(arm mode)该地址为arm模式\n");
u8* start=(u8*)addr;
u8* end=(u8*)((u32)start+size);
// 遍历对比
while(1) {
if (start >= end) {
uRet = 0;
LOGA("checkbkpt:(no find)没有发现断点.\n");
break;
}
if (0 == memcmp(start,armBkpt , 4)){
uRet = 1;
LOGA("checkbkpt:(find it)发现断点.\n");
break;
}
start = start + 4;
}//while
}//else
return;
}

11 系统源码修改检测

原理: 安卓native下最流行的反调试方案是读取进程的status或stat来检测tracepid,原理是调试状 态下的进程tracepid不为0。

对于这种调试检测手段,最彻底的绕过方式是修改系统源码后重新编译,让tracepid永远为 0。

对抗这种bypass手段,我们可以创建一个子进程,让子进程主动ptrace自身设为调试状态, 此时正常情况下,子进程的tracepid应该不为0。此时我们检测子进程的tracepid是否为0, 如果为0说明源码被修改了。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
bool checkSystem() 
{
// 建立管道
int pipefd[2];
if (-1 == pipe(pipefd)) {
LOGA("pipe() error.\n");
return false;
}
// 创建子进程
pid_t pid = fork();
LOGB("father pid is: %d\n",getpid());
LOGB("child pid is: %d\n",pid);
// for失败
if(0 > pid) {
LOGA("fork() error.\n");
return false;
}
// 子进程程序
int childTracePid=0;
if ( 0 == pid ) {
int iRet = ptrace(PTRACE_TRACEME, 0, 0, 0);
if (-1 == iRet) {
LOGA("child ptrace failed.\n");
exit(0);
}
LOGA("%s ptrace succeed.\n");
// 获取tracepid
char pathbuf[0x100] = {0};
char readbuf[100] = {0};
sprintf(pathbuf, "/proc/%d/status", getpid());
int fd = openat(NULL, pathbuf, O_RDONLY);
if (-1 == fd) {
LOGA("openat failed.\n");
}
read(fd, readbuf, 100);
close(fd);
uint8_t *start = (uint8_t *) readbuf;
uint8_t des[100] = {0x54, 0x72, 0x61, 0x63, 0x65, 0x72, 0x5 0, 0x69, 0x64, 0x3A,0x09};
int i = 100;
bool flag= false;
while (--i) {
if( 0==memcmp(start,des,10)) {
start=start+11;
childTracePid=atoi((char*)start);
flag= true;
break;
}else {
start=start+1;
flag= false;
}
}//while
if(false==flag) {
LOGA("get tracepid failed.\n");
return false;
}
// 向管道写入数据
close(pipefd[0]); // 关闭管道读端
write(pipefd[1], (void*)&childTracePid,4); // 向管道写端写入 数据
close(pipefd[1]); // 写完关闭管道写 端
LOGA("child succeed, Finish.\n");
exit(0);
} else {
// 父进程程序
LOGA("开始等待子进程.\n");
waitpid(pid,NULL,NULL); // 等待子进程 结束
int buf2 = 0;
close(pipefd[1]); // 关闭写端
read(pipefd[0], (void*)&buf2, 4); // 从读端读取 数据到buf
close(pipefd[0]); // 关闭读端
LOGB("子进程传递的内容为:%d\n", buf2); // 输出内容 // 判断子进程ptarce后的tracepid
if(0 == buf2) {
LOGA("源码被修改了.\n");
}else{
LOGA("源码没有被修改.\n");
}
return true;
}
}

void smain()
{
bool bRet=checkSystem();
if(true==bRet)
LOGA("check succeed.\n");
else
LOGA("check failed.\n");
LOGB("main Finish pid:%d\n",getpid()); return;
}

12 单步调试陷阱

原理:调试器从下断点到执行断点的过程分析:

  • 1 保存:保存目标处指令
  • 2 替换:目标处指令替换为断点指令
  • 3 命中断点:命中断点指令(引发中断 或者说发出信号)
  • 4 收到信号:调试器收到信号后,执行调试器注册的信号处理函数。 5 恢复:调试器处理函数恢复保存的指令
  • 6 回退:回退PC寄存器
  • 7 控制权回归程序.

主动设置断点指令/注册信号处理函数的反调试方案:

  • 1 在函数中写入断点指令
  • 2 在代码中注册断点信号处理函数
  • 3 程序执行到断点指令,发出信号

分两种情况:

(1)非调试状态 进入自己注册的函数,NOP指令替换断点指令,回退PC后正常指令。 (执行断点发出信号—进入处理信号函数—NOP替换断点—退回PC)

(2)调试状态 进入调试器的断点处理流程,他会恢复目标处指令失败,然后回退PC,进入死循环。

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
42
43
44
45
#!cpp 

char dynamic_ccode[] = {0x1f,0xb4, //push {r0-r4}
0x01,0xde, //breakpoint
0x1f,0xbc, //pop {r0-r4}
0xf7,0x46};//mov pc,lr

char *g_addr = 0;

void my_sigtrap(int sig){
char change_bkp[] = {0x00,0x46}; //mov r0,r0
memcpy(g_addr+2,change_bkp,2);
__clear_cache((void*)g_addr,(void*)(g_addr+8)); // need to clea r cache
LOGI("chang bpk to nop\n");
}

void anti4(){//SIGTRAP
int ret,size;
char *addr,*tmpaddr;
signal(SIGTRAP,my_sigtrap);
addr = (char*)malloc(PAGESIZE*2);
memset(addr,0,PAGESIZE*2);
g_addr = (char *)(((int) addr + PAGESIZE-1) & ~(PAGESIZE-1));
LOGI("addr: %p ,g_addr : %p\n",addr,g_addr);
ret = mprotect(g_addr,PAGESIZE,PROT_READ|PROT_WRITE|PROT_EXEC);
if(ret!=0) {
LOGI("mprotect error\n");
return ;
}
size = 8;
memcpy(g_addr,dynamic_ccode,size);
__clear_cache((void*)g_addr,(void*)(g_addr+size)); // need to c lear cache
__asm__("push {r0-r4,lr}\n\t"
"mov r0,pc\n\t" //此时pc指向后两条指令
"add r0,r0,#4\n\t"//+4 是的lr 地址为 pop{r0-r5}
"mov lr,r0\n\t"
"mov pc,%0\n\t"
"pop {r0-r5}\n\t"
"mov lr,r5\n\t" //恢复lr
:
:"r"(g_addr)
:);

LOGI("hi, i'm here\n"); free(addr);
}

13 利用IDA先截获信号特性的检测

原理: IDA会首先截获信号,导致进程无法接收到信号,导致不会执行信号处理函数。将关键流程 放在信号处理函数中,如果没有执行,就是被调试状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h> 
#include <signal.h>
#include <unistd.h>
void myhandler(int sig) {
//signal(5, myhandler);
printf("myhandler.\n");
return;
}

int g_ret = 0;
int main(int argc, char **argv) {
// 设置SIGTRAP信号的处理函数为myhandler()
g_ret = (int)signal(SIGTRAP, myhandler);
if ( (int)SIG_ERR == g_ret ) {
printf("signal ret value is SIG_ERR.\n");
}

// 打印signal的返回值(原处理函数地址)
printf("signal ret value is %x\n",(unsigned char*)g_ret);
// 主动给自己进程发送SIGTRAP信号 raise(SIGTRAP); raise(SIGTRAP); raise(SIGTRAP);
kill(getpid(), SIGTRAP);
printf("main.\n");
return 0;
}

14 利用IDA解析缺陷反调试

原理: IDA采用递归下降算法来反汇编指令,而该算法最大的缺点在于它无法处理间接代码路径, 无法识别动态算出来的跳转。而arm架构下由于存在arm和thumb指令集,就涉及到指令集 切换,IDA在某些情况下无法智能识别arm和thumb指令,进一步导致无法进行伪代码还原。

在IDA动态调试时,仍然存在该问题,若在指令识别错误的地点写入断点,有可能使得调试 器崩溃。( 可能写断点 ,不知道写ARM还是THUMB ,造成的崩溃)

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#if(JUDGE_THUMB) 

#define GETPC_KILL_IDAF5_SKATEBOARD \
__asm __volatile( \
"mov r0,pc \n\t" \
"adds r0,0x9 \n\t" \
"push {r0} \n\t" \
"pop {r0} \n\t" \
"bx r0 \n\t" \
\
".byte 0x00 \n\t" \
".byte 0xBF \n\t" \
\

".byte 0x00 \n\t" \
".byte 0xBF \n\t" \
\

".byte 0x00 \n\t" \
".byte 0xBF \n\t" \
:::"r0" \ );

#else
#define GETPC_KILL_IDAF5_SKATEBOARD \
__asm __volatile( \
"mov r0,pc \n\t" \
"add r0,0x10 \n\t" \
"push {r0} \n\t" \
"pop {r0} \n\t" \
"bx r0 \n\t" \
".int 0xE1A00000 \n\t" \
".int 0xE1A00000 \n\t" \
".int 0xE1A00000 \n\t" \
".int 0xE1A00000 \n\t" \
:::"r0" \ );

#endif

// 常量标签版本

#if(JUDGE_THUMB)

#define IDAF5_CONST_1_2 \
__asm __volatile( \
"b T1 \n\t" \
"T2: \n\t" \
"adds r0,1 \n\t" \
"bx r0 \n\t" \
"T1: \n\t" \
"mov r0,pc \n\t" \
"b T2 \n\t" \
:::"r0" \

);

#else

#define IDAF5_CONST_1_2 \
__asm __volatile( \
"b T1 \n\t" \
"T2: \n\t" \
"bx r0 \n\t" \

"T1: \n\t" \
"mov r0,pc \n\t" \

"b T2 \n\t" \
:::"r0" \ );

#endif

15 五种代码执行时间检测

第一类:

原理: 一段代码,在a处获取一下时间,运行一段后,再在b处获取下时间, 然后通过(b时间­a时间)求时间差,正常情况下这个时间差会非常小, 如果这个时间差比较大,说明正在被单步调试。

做法: 五个能获取时间的api:

time()函数 time_t结构体
clock()函数 clock_t结构体
gettimeofday()函数 timeval结构 timezone结构
clock_gettime()函数 timespec结构
getrusage()函数 rusage结构

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <sys/time.h> 
#include <sys/types.h>
#include <sys/resource.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>

static int _getrusage(); //Invalid
static int _clock(); //Invalid
static int _time();
static int _gettimeofday();
static int _clock_gettime();

int main () {
_getrusage();
_clock();
_time();
_gettimeofday();
_clock_gettime();
return 0;
}

int _getrusage() {
struct rusage t1;

/* breakpoint */
getrusage (RUSAGE_SELF, &t1);
long used = t1.ru_utime.tv_sec + t1.ru_stime.tv_sec;
if (used > 2) {
puts("debugged");
}
return 0;
}
int _clock(){
clock_t t1, t2;
t1 = clock ();

/* breakpoint */
t2 = clock ();
double used = (double)(t2 - t1) / CLOCKS_PER_SEC;
if (used > 2) {
puts("debugged");
}
return 0;
}

int _time() {
time_t t1, t2;
time (&t1);

/* breakpoint */
time (&t2);
if (t2 - t1 > 2) {
puts("debugged");
}
return 0;
}

int _gettimeofday() {
struct timeval t1, t2;
struct timezone t;
gettimeofday (&t1, &t);

/* breakpoint */
gettimeofday (&t2, &t);
if (t2.tv_sec - t1.tv_sec >2 ){
puts("debugged");
}
return 0;
}

int _clock_gettime(){
struct timespec t1, t2;
clock_gettime (CLOCK_REALTIME, &t1);
/* breakpoint */
clock_gettime (CLOCK_REALTIME, &t2);
if (t2.tv_sec - t1.tv_sec > 2) {
puts("debugged");
}
return 0;
}

16 三种进程信息结构检测

原理: 一些进程文件中存储了进程信息,可以读取这些信息得知是否为调试状态。

做法:

第一种:/proc/pid/status /proc/pid/task/pid/status TracerPid非0 statue字段中写入t(tracing stop)

第二种:/proc/pid/stat /proc/pid/task/pid/stat 第二个字段是t(T)

第三种:/proc/pid/wchan /proc/pid/task/pid/wchan ptrace_stop

代码: 略。

17 Inotify事件监控dump

原理: 通常壳会在程序运行前完成对text的解密,所以脱壳可以通过dd与gdb_gcore来dump /proc/pid/mem或/proc/pid/pagemap,获取到解密后的代码内容。

可以通过Inotify系列api来监控mem或pagemap的打开或访问事件, 一旦发生时间就结束进程来阻止dump。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
void thread_watchDumpPagemap() { 
LOGA("-------------------watchDump:Pagemap------------------ - \n");
char dirName[NAME_MAX]={0};
snprintf(dirName,NAME_MAX,"/proc/%d/pagemap",getpid());
int fd = inotify_init();
if (fd < 0) {
LOGA("inotify_init err.\n");
return;
}
int wd = inotify_add_watch(fd,dirName,IN_ALL_EVENTS);
if (wd < 0) {
LOGA("inotify_add_watch err.\n");
close(fd);
return;
}
const int buflen=sizeof(struct inotify_event) * 0x100;
char buf[buflen]={0};
fd_set readfds;
while(1) {
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
int iRet = select(fd+1,&readfds,0,0,0); // 此处阻塞
LOGB("iRet的返回值:%d\n",iRet);
if(-1==iRet)
break;
if (iRet) {
memset(buf,0,buflen);
int len = read(fd,buf,buflen);
int i=0;
while(i < len) {
struct inotify_event *event = (struct inotify_even t*)&buf[i];
LOGB("1 event mask的数值为:%d\n",event->mask);
if( (event->mask==IN_OPEN) ) {
// 此处判定为有true,执行崩溃.
LOGB("2 有人打开pagemap,第%d次.\n\n",i);
//__asm __volatile(".int 0x8c89fa98");
}
i += sizeof (struct inotify_event) + event->len; }
LOGA("-----3 退出小循环-----\n");
}
}
inotify_rm_watch(fd,wd);
close(fd);
LOGA("-----4 退出大循环,关闭监视-----\n");
return;
}

void smain() {
// 监控/proc/pid/mem
pthread_t ptMem,t,ptPageMap;
int iRet=0;
// 监控/proc/pid/pagemap
iRet=pthread_create(&ptPageMap,NULL,(PPP)thread_watchDumpPagema p,NULL);
if (0!=iRet) {
LOGA("Create,thread_watchDumpPagemap,error!\n"); return;
}
iRet=pthread_detach(ptPageMap);
if (0!=iRet) {
LOGA("pthread_detach,thread_watchDumpPagemap,error!\n"); return;
}
LOGA("-------------------smain-------------------\n");
LOGB("pid:%d\n",getpid());
return;
}

Frida反反调试

应用崩溃退出

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
function show_native_stacks(flag){ 
console.log(`${flag} \tBacktrace:\n\t` + Thread.backtrace(this.context,Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n\t'));
}

function anti_exit() {
const exit_ptr = Module.findExportByName(null, 'exit');
console.log('anti_exit: ', exit_ptr);
if (null == exit_ptr) {
return;
}
Interceptor.replace(exit_ptr, new NativeCallback(function (flag) {
if (null == this) {
return 0;
}
console.log('anti_exit lr: ',this.context.lr);
show_native_stacks('anti_exit');
return 0;
}, 'int', ['int', 'int']));
}

function anti_kill() {
const kill_ptr = Module.findExportByName(null, 'kill');
console.log('anti_kill: ', kill_ptr);
if (null == kill_ptr) {
return;
}
Interceptor.replace(kill_ptr, new NativeCallback(function (pid, flag) {
if (null == this) {
return 0;
}
console.log(`anti_kill: lr=${this.context.lr} pid=${pid} flag=${flag}`);
show_native_stacks('anti_kill');
return 0;
}, 'int', ['int', 'int']));
}

检测是否被调试

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
function anti_isDebuggerConnected(){
Java.perform(function() {
var Debug = Java.use('android.os.Debug');
Debug.isDebuggerConnected.implementation = function() {
console.log('isDebuggerConnected Bypassed !');
return false;
}
});
}

//开发者模式
function anti_developer_mod(){
Java.perform(function () {
var SSecure = Java.use("android.provider.Settings$Secure");
SSecure.getStringForUser.overload('android.content.ContentResolver', 'java.lang.String', 'int').implementation = function (contentResolver, name, flag) {
if (name.indexOf("development_settings_enabled") >= 0) {
console.log(name);
return this.getStringForUser.call(this, contentResolver, 'fuckyou', flag);
}
return this.getStringForUser(contentResolver, name, flag);
}
})
}

//long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
function anti_ptrace() {
var ptrace_ptr = Module.findExportByName(null, "ptrace");
console.log('ptrace_ptr: ', ptrace_ptr);
if (null != ptrace_ptr) {
Interceptor.replace(ptrace_ptr.or(1), new NativeCallback(function (request, pid, addr, data) {
console.log('anti_ptrace entry');
return 1;
}, 'long', ['int', "int", 'pointer', 'pointer']));
}
}

//每日优鲜
function anti_fork() {
var fork_ptr = Module.findExportByName(null, "fork");
console.log('fork_ptr: ', fork_ptr);
if (null != fork_ptr) {
Interceptor.replace(fork_ptr, new NativeCallback(function () {
console.log('fork_ptr', 'entry');
return -1;
}, 'int', []));
}
}


function getModuleByAddr(addr) {
var result = null;
Process.enumerateModules().forEach(function (module) {
if (module.base <= addr && addr <= (module.base.add(module.size))) {
result = JSON.stringify(module);
return false; // 跳出循环
}
});
return result;
}

//char *fgets(char *str, int n, FILE *stream)
function anti_fgets() {
const tag = 'anti_fgets';
const fgets_ptr = Module.findExportByName(null, 'fgets');
console.log('fgets_ptr: ', fgets_ptr);
if (!fgets_ptr) {
return;
}
var fgets = new NativeFunction(fgets_ptr, 'pointer', ['pointer', 'int', 'pointer']);
Interceptor.replace(fgets_ptr, new NativeCallback(function (buffer, size, fp) {
if (null == this) {
return 0;
}
var logTag = null;
// 进入时先记录现场
const lr = this.context.lr
// 读取原 buffer
var retval = fgets(buffer, size, fp);
var bufstr = Memory.readUtf8String(buffer);
if (null != bufstr) {
if (bufstr.indexOf("TracerPid:") > -1) {
buffer.writeUtf8String("TracerPid:\t0");
logTag = 'TracerPid';
}
//State: S (sleeping)
else if (bufstr.indexOf("State:\tt (tracing stop)") > -1) {
buffer.writeUtf8String("State:\tS (sleeping)");
logTag = 'State';
}
// ptrace_stop
else if (bufstr.indexOf("ptrace_stop") > -1) {
buffer.writeUtf8String("sys_epoll_wait");
logTag = 'ptrace_stop';
}

//(sankuai.meituan) t
else if (bufstr.indexOf(") t") > -1) {
buffer.writeUtf8String(bufstr.replace(") t", ") S"));
logTag = 'stat_t';
}

// SigBlk
else if (bufstr.indexOf('SigBlk:') > -1) {
buffer.writeUtf8String('SigBlk:\t0000000000001204');
logTag = 'SigBlk';
}
if (logTag) {
console.log(tag + " " + logTag, bufstr + " -> " + buffer.readCString() + ' lr: ' + lr +
"(" + getModuleByAddr(lr) + ")");
show_native_stacks(tag)
}
}
return retval;
}, 'pointer', ['pointer', 'int', 'pointer']));
}

frida检测

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//梆梆检测点
//int readlink(const char *path, char *buf, size_t bufsiz);
function anti_readlink(){
const readlink_ptr = Module.findExportByName(null, "readlink");
console.log('readlink_ptr: ', readlink_ptr);
if(!readlink_ptr){
return
}
const readlink = new NativeFunction(readlink_ptr, 'int', ['pointer', 'pointer', 'pointer'])
Interceptor.replace(readlink_ptr, new NativeCallback(function(path, buf, size){
var retval = readlink(path, buf, size);
var bufstr = buf.readCString();
if (bufstr.indexOf('frida')!==-1 ||
bufstr.indexOf('gum-js-loop')!==-1 ||
bufstr.indexOf('gmain')!==-1 ||
bufstr.indexOf('linjector')!==-1){
console.log(`\nreadlink(path=${path.readCstring()}, buf=${bufstr}, size=${size}`);
this.buf.writeUtf8String("/system/framework/boot.art")
console.log("replce with: "+ this.buf.readCString())
return 0x1A;
}
return retval;
}, 'int', ['pointer', 'pointer', 'pointer']))
}

//char *strstr(const char *haystack, const char *needle)
function anti_strstr(){
const strstr_ptr = Module.findExportByName(null, "strstr");
console.log('strstr_ptr: ', strstr_ptr);
if(!strstr_ptr){
return
}
const strstr = new NativeFunction(strstr_ptr, 'pointer', ['pointer', 'pointer'])
Interceptor.replace(strstr_ptr, new NativeCallback(function(haystack, needle){
var retval = strstr(haystack, needle);
var haystackstr = haystack.readCString();
var needle_content = haystack.readCString();
if (haystackstr.indexOf('frida')!==-1 ||
needle_content.indexOf('frida')!==-1 ||
haystackstr.indexOf('gum-js-loop')!==-1 ||
needle_content.indexOf('gum-js-loop')!==-1 ||
haystackstr.indexOf('gmain')!==-1 ||
needle_content.indexOf('gmain')!==-1 ||
haystackstr.indexOf('linjector')!==-1 ||
needle_content.indexOf('linjector')!==-1){
console.log(`\nstrstr(haystack=${haystackstr}, needle=${needle_content}`);
this.buf.writeUtf8String("/system/framework/boot.art")
console.log("replce with: "+ this.buf.readCString())
return 0;
}
return retval;
}, 'pointer', ['pointer', 'pointer']))
}

//线程检测
//int pthread_create(pthread_t* thread_out, pthread_attr_t const* attr,
//void* (*start_routine)(void*), void* arg)
function anti_pthread_create_detect_frida_loop(){
const pthread_create_ptr = Module.findExportByName(null, "pthread_create");
console.log('pthread_create_ptr: ', pthread_create_ptr);
if(!strstr_ptr){
return
}
const pthread_create = new NativeFunction(pthread_create_ptr, 'int', ['pointer', 'pointer','pointer','pointer']);
Interceptor.replace(pthread_create_ptr, new NativeCallback(function (thread_out, attr, start_routine, arg) {
var retval;
var detect_frida_loop = Module.findExportByName(null, "_Z17detect_frida_loopPv");
if (String(detect_frida_loop) === String(start_routine)){
retval = 0;
console.log('Prevent detect_frida_loop Thread start')
} else{
retval = pthread_create(thread_out, attr, start_routine, arg);
}
return retval
},'int', ['pointer', 'pointer','pointer','pointer']))
}

环境检测

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
const openPtr = Module.getExportByName('libc.so', 'open');
const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);

var readPtr = Module.findExportByName("libc.so", "read");
var read = new NativeFunction(readPtr, 'int', ['int', 'pointer', "int"]);
var fakePath = "/data/data/com.xingin.xhs/maps";//这里要改包名
var file = new File(fakePath, "w");
var buffer = Memory.alloc(512);

var fakePath2 = "/data/data/com.xingin.xhs/task";
var file2 = new File(fakePath2, "w");
var buffer2 = Memory.alloc(512);

Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
var pathname = Memory.readUtf8String(pathnameptr);
var realFd = open(pathnameptr, flag);
console.log("open:", pathname)
//路径是否包含maps 和task
if (pathname.indexOf("maps") >= 0 || pathname.indexOf("status") >= 0 || pathname.indexOf("cmdline") >= 0) {
var temp = pathname.indexOf("maps") >= 0 ? 1:2;
//包含maps则map为1 task为2
switch (temp) {
case 1://maps
{
// console.log("open maps:", pathname);
while (parseInt(read(realFd, buffer, 512)) !== 0) {
var oneLine = Memory.readCString(buffer);
// if(pathname=="/proc/self/maps"){
// console.log("maps 打印 oneLine: ",oneLine);
// }

if (oneLine.indexOf("tmp") === -1) {
// === 比== 更加严格 ==类型不匹配再转化匹配值 ===类型不匹配就是false
// 就是online 不包含tmp 则写入/data/data/com.wujie.chengxin/maps中
// 因为Frida在运行时会先确定/data/local/tmp路径下是否有re.frida.server文件夹,
// 若没有则创建该文件夹并存放fridaagent.so等文件
// console.log("write :",oneLine);
file.write(oneLine);
} else {

}
}
console.log("外打印 maps oneLine: ",oneLine)
var filename = Memory.allocUtf8String(fakePath);
return open(filename, flag);
break;
}
case 2://task
{
console.log("open task:", pathname);
while(parseInt(read(realFd, buffer2, 512)) !== 0){
var oneLine = Memory.readCString(buffer2);
console.log("打印 oneLine: ",oneLine)

if(oneLine.indexOf("gum-js-loop")!=-1){
var replaceStr = "AAAAAAAAAA"
oneLine = oneLine.replace("gum-js-loop", replaceStr)
//console.log(oneLine)
}
if(oneLine.indexOf("pool-frida")!=-1){
var replaceStr = "BBBBBBB"
oneLine = oneLine.replace("pool-frida", replaceStr)
//console.log(oneLine)
}
if(oneLine.indexOf("gmain")!=-1){
var replaceStr = "CCCCCCC" //最终只有这里匹配上了
oneLine = oneLine.replace("gmain", replaceStr)
//console.log(oneLine)
}
file2.write(oneLine);
}
console.log("外打印 oneLine: ",oneLine)
var filename = Memory.allocUtf8String(fakePath2);
return open(filename, flag);//把伪造的路径打开返回
break;
}
}
}
var fd = open(pathnameptr, flag);
// Thread.sleep(1)
return fd;
}, 'int', ['pointer', 'int']));

function get_self_process_name() {
var openPtr = Module.getExportByName('libc.so', 'open');
var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);

var readPtr = Module.getExportByName("libc.so", "read");
var read = new NativeFunction(readPtr, "int", ["int", "pointer", "int"]);

var closePtr = Module.getExportByName('libc.so', 'close');
var close = new NativeFunction(closePtr, 'int', ['int']);

var path = Memory.allocUtf8String("/proc/self/cmdline");
var fd = open(path, 0);
if (fd != -1) {
var buffer = Memory.alloc(0x1000);

var result = read(fd, buffer, 0x1000);
close(fd);
result = ptr(buffer).readCString();
console.log("进程的名字是:"+result);
return result;
}

return "-1";
}

//int __system_property_get(const char *name, char *value);
function anti_system_property_get(){
const system_property_get_ptr = Module.getExportByName('libc.so', '__system_property_get');
if (!system_property_get_ptr){
return
}
var system_property_get = new NativeFunction(system_property_get_ptr, 'int', ['pointer', 'pointer'])
Interceptor.replace(system_property_get, new NativeCallback(function (name, value) {
console.log(`\n__system_property_get(name=${name.readCString()}, value=${value.readCString()}`);
var retval = system_property_get(name, value);
return retval;
}, 'int', ['pointer', 'pointer']))
}

案例

查看加载所有的module

1
2
3
4
5
6
7
8
9
10
function fridaProcess() {
Java.perform(function () {
var enumMoudle = Process.enumerateModules();
for (var i = 0; i < enumMoudle.length; i++) {
console.log("", enumMoudle[i].name)
}
});
}

setImmediate(fridaProcess, 0)

查看底层打开的所有文件

1
2
3
4
5
6
7
8
9
10
11
12
var pth = Module.findExportByName(null, "open");
Interceptor.attach(ptr(pth), {
onEnter: function (args) {
this.filename = args[0];
console.log("", this.filename.readCString())
if (this.filename.readCString().indexOf(".so") != -1) {
}

}, onLeave: function (retval) {
return retval;
}
})

最终闪退时打开的文件maps,备份一个正常maps文件用来替换

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
function main() {
const openPtr = Module.getExportByName('libc.so', 'open');
const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
var readPtr = Module.findExportByName("libc.so", "read");
var read = new NativeFunction(readPtr, 'int', ['int', 'pointer', "int"]);
var fakePath = "/data/data/com.app/maps";
var file = new File(fakePath, "w");
var buffer = Memory.alloc(512);
Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
var pathname = Memory.readUtf8String(pathnameptr);
var realFd = open(pathnameptr, flag);
if (pathname.indexOf("maps") >= 0) {
while (parseInt(read(realFd, buffer, 512)) !== 0) {
var oneLine = Memory.readCString(buffer);
if (oneLine.indexOf("tmp") === -1) {
file.write(oneLine);
}
}
var filename = Memory.allocUtf8String(fakePath);
return open(filename, flag);
}
var fd = open(pathnameptr, flag);
return fd;
}, 'int', ['pointer', 'int']));
}
setImmediate(main)

端口检测

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
Interceptor.attach(Module.findExportByName("libc.so", "connect"), {
onEnter: function (args) {
var memory = Memory.readByteArray(args[1], 64);
var b = new Uint8Array(memory);
if (b[2] == 0x69 && b[3] == 0xa2 && b[4] == 0x7f && b[5] == 0x00 && b[6] == 0x00 && b[7] == 0x01) {
this.frida_detection = true;
}
},
onLeave: function (retval) {
if (this.frida_detection) {
console.log("Frida Bypassed");
retval.replace(-1);
}
}
});

Interceptor.attach(Module.findExportByName(null, "connect"), {
onEnter: function (args) {
var family = Memory.readU16(args[1]);
if (family !== 2) {
return
}
var port = Memory.readU16(args[1].add(2));
port = ((port & 0xff) << 8) | (port >> 8);
if (port === 27042) {
console.log('Frida port check');
Memory.writeU16(args[1].add(2), 0x0101);
}
}
});
文章作者: J
文章链接: http://onejane.github.io/2022/10/19/调试与反调试/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏