android-native保护整理实践

前言

本篇收集整理常见native保护措施,并有一定实践。
尚未完成,持续更新

阻止静态分析

so整体加密

目标:so整体加密,不落地&落地加密、内存解密加载
实现:自定义linker实现

段加密

目的:加密部分so
原理:加载时使用的是头表,因此节头表很多无用的部分,可以用来变形并添加自己的信息。
将任意部分加密后运行时获取信息解密即可

实现:
https://xn--74q78i15hxv3arigm4e.cn/2018/07/19/apk%E5%8A%A0%E5%9B%BA-%E5%8A%A0%E5%AF%86so-native%E5%B1%82%E8%A7%A3%E5%AF%86/

函数加密,运行时解密

目标:加密指定函数
原理:通过符号表定位函数位置、大小。加密即可。解密时可以直接使用函数名当指针解密。

实现:
https://xn--74q78i15hxv3arigm4e.cn/2018/07/19/apk%E5%8A%A0%E5%9B%BA-%E5%8A%A0%E5%AF%86so-native%E5%B1%82%E8%A7%A3%E5%AF%86/

elf加壳

目的:对elf加壳

实现:
常见的有UPX:https://github.com/upx/upx 可以自己修改源码,同时可以压缩elf大小

花指令

目标:干扰反编译器的自动分析
原理:针对常见反汇编算法,手动构造容易引起误会的汇编代码。

实现:

1
2
3
4
5
6
__asm
{
push label;
retn;
}
label:

hook更改流程

目的:动态改变执行流程
原理:通过hook要运行的函数,改变程序流程,使程序以不同于静态分析结果。

实现:
如inline hook下Jni_OnLoad实现解密等操作。

混淆

目的:替换等价汇编指令为难以分析的指令
原理:花指令、指令乱序、指令替换、llvm

阻止动态调试

干扰动态调试过程,有效增加逆向成本

基于时间的检测

目的:进程自我检测一定区域的断点调试
原理:通过在一段代码区间获取时间值,如果时间相差过大,则可能是有断点在调试。检测退出时机为区间最后时间判断时,当该区域没有断点时不会退出。

实现:

1
2
3
4
5
6
7
8
9
10
#include<time.h>
start = clock();
...
(部分代码逻辑)
...
end = clock();
if(end-start>10000){
//debug处理
}

基于文件的检测

目的:通过系统信息检测是否被调试
原理:查看系统特定文件的信息,获取调试信息
/proc/pid/status 和 /proc/pid/task/pid/status:普通状态下,TracerPid这项应该为0;调试状态下为调试进程的PID。
/proc/pid/stat 和 /proc/pid/task/pid/stat:cpu活跃信息;调试状态下,括号后面的第一个字母应该为t。表示traced状态(追踪)
/proc/pid/wchan 和 /proc/pid/task/pid/wchan:当进程sleep时,kernel当前运行的函数;调试状态下,里面内容为ptrace_stop。

实现:

1
2
3
4
5
6
7
8
9
10
#include<fcntl.h>
int fd=open("/proc/self/status",O_RDONLY);//或指定pid
char* buf = new char[500];
read(fd, buf, 500);
char *p=strstr(buf,"TracerPid");
if(memcpy(p+11,"0",1)!=0)//pid不可能以0开头,不是0就是被调试了
{
//debug处理
}

针对常见调试器检测

目的:干扰常用调试器调试
原理:通过检测常用调试器痕迹,进行反调试。如
程序-android_server,gdb,gdbserver
端口-23946等,信息来源可以是/proc/net/tcp

虚拟机内部相关字段

目的:通过虚拟机信息判断调试
原理:虚拟机保存了是否被调试的相关数据。

实现:

1
2
3
if(android.os.Debug.isDebuggerConnected()){
//debug处理
}

ptrace

目的:阻止调试器附加
原理:linux下的调试是使用ptrace(跟踪)调用,linux每个进程只能被一个跟踪,因此可以先ptrace自己阻止附加。

实现:

1
2
3
4
int check = ptrace(PTRACE_TRACEME,0 ,0 ,0);//用来指示此进程将由其父进程跟踪。父fork出子。
if(check != 0){//成功返回0
//处理debug
}

断点扫描

目的:直接查找断点的存在
原理:调试器原理是向断点处插入断点汇编指令,触发断点进行系统调用,调试器获得控制权从而处理。此方法暴力查找断点

实现:

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
bool checkBreakPoint ()
{
int i, j;
unsigned int base, offset, pheader;
Elf32_Ehdr *elfhdr;
Elf32_Phdr *ph_t;
//获取段表
base = getLibAddr ("libnative-lib.so");
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);
return true;
} else if (*p == 0xf0 && *(p+1) == 0xf7 && *(p+2) == 0x00 && *(p+3) == 0xa0) {
LOGI ("Find thumb2 bpt %p", p);
return true;
} else if (*p == 0x01 && *(p+1) == 0x00 && *(p+2) == 0x9f && *(p+3) == 0xef) {
LOGI ("Find arm bpt %p", p);
return true;
}
p++;
}
}
return false;
}

信号处理

目的:干扰调试过程中的信号处理
原理:断点指令使被调试进程发出信号SIGTRAP,传递到要处理的地方,通常调试器会截获Linux系统内给被调试进程的各种信号,由调试者可选地传递给被调试进程。但是SIGTRAP是个例外,因为通常的目标程序中不会出现breakpoint,因为这会使得程序自己奔溃。SIGTRAP会被认为是调试器自己设置的-这样会造成不同的错误
在信号处理中处理解密逻辑,此处难以调试
实现:

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
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){
LOGI("my_sigtrap\n");
char change_bkp[] = {0x00,0x46}; //mov r0,r0
memcpy(g_addr+2,change_bkp,2);
__builtin___clear_cache(g_addr,(g_addr+8)); // need to clear cache
LOGI("chang bpk to nop\n");
}
void anti4(){//SIGTRAP
int ret,size;
char *addr,*tmpaddr;
signal(SIGTRAP,my_sigtrap); //注册信号处理
addr = (char*)malloc(PAGE_SIZE*2);
memset(addr,0,PAGE_SIZE*2);
g_addr = (char *)(( (long)addr + PAGE_SIZE-1) & ~(PAGE_SIZE-1));
LOGI("addr: %p ,g_addr : %p\n",addr,g_addr);
ret = mprotect(g_addr,PAGE_SIZE,PROT_READ|PROT_WRITE|PROT_EXEC);
if(ret!=0)
{
LOGI("mprotect error\n");
return ;
}
size = 8;
memcpy(g_addr,dynamic_ccode,size);
__builtin___clear_cache(g_addr,(g_addr+size)); // need to clear cache
LOGI("start stub\n");
__asm__("push {r5}\n\t"
"push {r0-r4,lr}\n\t"
"mov r0,pc\n\t" //此时pc指向后两条指令
"add r0,r0,#6\n\t"//cjh:这里的add是add.w,所以会占32位,因此需要+6才对。 原文:+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
"pop {r5}\n\t"
:
:"r"(g_addr)
:);
LOGI("hi, i'm here\n");
free(addr);
LOGI("hi, i'm here2\n");
}

调试器的错误理解

目的:干扰调试器对汇编指令的解析
原理:Thumb-2模式是Thumb16和Thumb32指令的混合执行,切换依靠跳转时的地址数值,可动态确定。因此难以被判断。
不过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
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
int pipefd[2];
int childpid;
void *anti3_thread(void *){
int statue=-1,alive=1,count=0;
close(pipefd[1]);
while(read(pipefd[0],&statue,4)>0)
break;
sleep(1);
//这里改为非阻塞 fcntl(pipefd[0], F_SETFL, O_NONBLOCK); //enable fd的O_NONBLOCK
LOGI("pip-->read = %d", statue);
while(true) {
LOGI("pip--> statue = %d", statue);
read(pipefd[0], &statue, 4);
sleep(1);
LOGI("pip--> statue2 = %d", statue);
if (statue != 0) {
kill(childpid,SIGKILL);
kill(getpid(), SIGKILL);
return NULL;
}
statue = -1;
}
}
void anti3(){
int pid,p;
FILE *fd;
char filename[MAX];
char line[MAX];
pid = getpid();
sprintf(filename,"/proc/%d/status",pid);// 读取proc/pid/status中的TracerPid p = fork();
if(p==0) //child {
LOGI("Child");
close(pipefd[0]); //关闭子进程的读管道 int pt,alive=0;
pt = ptrace(PTRACE_TRACEME, 0, 0, 0); //子进程反调试 while(true)
{
fd = fopen(filename,"r");
while(fgets(line,MAX,fd))
{
if(strstr(line,"TracerPid") != NULL)
{
LOGI("line %s",line);
int statue = atoi(&line[10]);
LOGI("########## tracer pid:%d", statue);
write(pipefd[1],&statue,4);//子进程向父进程写 statue值
fclose(fd);
if(statue != 0)
{
LOGI("########## tracer pid:%d", statue);
return ;
}
break;
}
}
sleep(1);
}
}else{
LOGI("Father");
childpid = p;
}
}
extern "C"
JNIEXPORT jstring
JNICALL
Java_com_sec_gtoad_antidebug_MainActivity_stringFromFork(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from fork";
pthread_t id_0;
id_0 = pthread_self();
pipe(pipefd);
pthread_create(&id_0,NULL,anti3_thread,(void*)NULL);
LOGI("Start");
anti3();
return env->NewStringUTF(hello.c_str());
}

防止hook

防止注入

VMP

其它针对性措施

检测frida

fridaserver:进程列表、tcp端口检测
检测注入:检测加载的so中是否有frida特征的

参考

https://gtoad.github.io/2017/06/25/Android-Anti-Debug/