反调试,在我们脱壳的第一步。反调试虽然不能完全阻止攻击者,但是还是能加大攻击者的时间成本,一般与加壳结合使用,核心还是加壳部分。反调试可以分为两类:一类是检测,另一类是攻击。本文是对安卓逆向中的反调试进行案例分析。
IDA
AndroidKiller
jadx
一部安卓机
反调试时,第一个会检测android_server,文件名检测。
实战分析filecheck
文件。
.so
文件是可以找到JNl_onload,它是编译可执行文件,看具体的逻辑,在Exports模块下搜索start
,找main函数的入口函数,双击进去。
如果这个函数的参数超过了四个以上(>4),跳转的地址就得用其他的寄存器来替代。
libc_init
容纳的函数的参数是在四个以上(>4)
当它这里没有被替代的时候,往下找最大的寄存器只出现了R*3为止,没出现R4以上的寄存器
那么这里猜测传入的个数的参数为︰是三个
具体验证一下
这里TAB进来
可以看到 确实是三个参数
第三个参数是main函数
双击分析main函数
这就是我们要找的main函数了
或者左边直接双击main
也可以进入main函数
进入main函数之后 TAB 分析代码
右键进行隐藏类型
下面是一个if判断
双击 sub 进来
分析代码
v0 = opendir("/data/local/tmp");
opendir打开/data/local/tmp目录给v0,它是一个文件指针
result = getpid();
getpid是当前进程的id
简单讲,就是v0不等于0的时候,要执行while里面的循环逻辑
v3 = readdir(v0);
这里的v0就是打开的文件指针
!strncmp(v5, "android_server", 0xEu)
strncmp是一个字符串比较函数
如果文件底层存在android_server,就会直接kill结束程序
拿出我的安卓真机
adb上传filecheck
adb上传android_server
adb push C:\Users\12550\Desktop\filecheck data/local/tmp
adb push C:\Users\12550\Desktop\android_server data/local/tmp
加权
chmod 777 android_server
chmod 777 filecheck
执行
./filecheck
因为有android_server
所以 被kill掉了
这就是一个文件反调试逻辑思路
即我们改名字 即可
这里给各位师傅 贴上源码
checkTCP.c
#include <unistd.h>
#include <stdio.h>
int num = 54321;
//检测常用的端口
void check()
{
FILE* pfile=NULL;
char buf[0x1000]={0};
// 执行命令
char* strCatTcp= "cat /proc/net/tcp |grep :5D8A";
char* strNetstat="netstat |grep :23946";
pfile=popen(strCatTcp,"r");
int pid=getpid();
if(NULL==pfile)
{
printf("打开命令失败!\n");
return;
}
// 获取结果
while(fgets(buf,sizeof(buf),pfile))
{
// 执行到这里,判定为调试状态
printf("执行cat /proc/net/tcp |grep :5D8A的结果:\n");
printf("%s",buf);
int ret=kill(pid,SIGKILL);
}
pclose(pfile);
}
int main()
{
int x = 2;
int y = 3;
int key;
x = x ^ y;
y = x ^ y;
x = x ^ y;
int X = x ^ y;
int Y = x & y;
Y= Y << 1;
int X0 = X ^ Y;
int Y0 = X & Y;
Y0 = Y0 << 1;
if (Y0==0)
{
key = X0+4543;
}
int encrypt = num ^ key;
int decrypt = encrypt ^ key;
check();
printf("加密前:%d\n",num);
printf("加密后值:%d\n",encrypt);
printf("解密后值:%d\n", decrypt);
return 0;
}
源码分析
首先FILE文件指针定义字符数组(char buf[0x1000]={0};)
然后执行命令
cat /proc/net/tcp |grep :5D8A
这里grep过滤端口只查看5D8A端口,5D8A换算是:23946
android_server 默认的端口也是23946
如果有5D8A
就kill(pid,SIGKILL);
双击main函数
空白处TAB进入伪C代码
双击进入check()
函数
右键进行隐藏类型
然后查看即可
adb push C:\Users\12550\Desktop\checkTCP data/local/tmp
chmod 777 checkTCP
执行
可以看到是被kill掉了
启动时指定端口(-p
)即可
#include <stdio.h>
#include <string.h>
#include <unistd.h>
//进程名称检测
void coursecheck(){
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);
}
void order(int* p,int n)//n:表示数组的长度
{
int i,j;
int k;
for(i=0;i<n-1;i++)
{
for(j=0;j<n-1-i;j++)
{
if(*(p+j)>*(p+j+1))
{
k=*(p+j);//k=a;
*(p+j)=*(p+j+1);//a=b;
*(p+j+1)=k; //b=k;
}
}
}
printf("排序后的数组为:");
for(i=0;i<n;i++)
{
if(i%5==0)
printf("\n");
printf("%4d",*(p+i));
}
printf("\n");
}
int main()
{
int n;
printf("请输入数组元素的个数:");
scanf("%d",&n);
int sum[n];
printf("请输入各个元素:");
int i;
coursecheck();
for(i=0;i<n;i++)
{
//scanf("%d",sum+i);
scanf("%d",&sum[i]);
}
order(sum,n);//实现冒泡排序
return 0;
}
分析main函数
关注这个方法
获取(getpid)
pid值放到%d里面
然后以只读的方式(r)打开
进行获取当前进程的一个状态
文件打开成功就进入while循环
找到main函数进来
空白处TAB查看伪C代码
关注这个方法
右键进行隐藏类型
adb push C:\Users\12550\Desktop\BubbleSort data/local/tmp
运行程序
查看进程列表
ps | grep BubbleSort
程序的PID号:5760
执行 cat 遍历程序
cat /proc/5760/status
./a001 -p20365adb forward tcp:20365 tcp:3192
增加设置
当我们在动态调试的时候
找到Tracepid
赋值的地方,手动把它赋值改为0即可
1.函数类型:safe_attach函数handle_events函数
2.简介:轮询检测反调试技术基于循环检测进程的状态
3.目的:判断当前进程是否正在被调试
4.优点:实现比较简单
5.缺点:系统资源消耗大
6.原理:读取进程的/proc/[pid]/status文件,通过该文件得到调式当前进程的调式器(检测调式器的[pid])
7.实现:通过status文件内的TracerPid字段的值判断当前进程或线程是否正在被调式
8.status文件信息:Name:进程名称State:进程的状态Tgid:一般指进程的名称Pid:一般指进程Id,他的值与getting函数的返回值相等PPid:父进程的IdTraceerPid:实现调试功能的进程Id,值为0表示当前进程未被调试
9.反-反调试方案:动态调试时修改TraceerPid字段值为0修改内核,让TraceerPid字段值为负值
这里给师傅们 贴上源码
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
#define TRACERPID "TracerPid:"
#define TRACERPID_LEN (sizeof(TRACERPID) - 1)
void loop()
{
while(true)
{
sleep(60);
}
}
bool check_debugger(pid_t pid)
{
const int pathSize = 256;
const int bufSize = 1024;
char path[pathSize];
char line[bufSize];
snprintf(path, sizeof(path) - 1, "/proc/%d/status", pid);
bool result = true;
FILE *fp = fopen(path, "rt");
if (fp != NULL)
{
while (fgets(line, sizeof(line), fp))
{
if (strncmp(line, TRACERPID, TRACERPID_LEN) == 0)
{
pid_t tracerPid = 0;
sscanf(line, "%*s%d", &tracerPid);
if (!tracerPid) result = false;
printf("%s", line);
break;
}
}
fclose(fp);
}
return result;
}
void *anti_debugger_thread(void *data)
{
pid_t pid = getpid();
while (true)
{
check_debugger(pid);
sleep(1);
}
}
void anti_debugger()
{
pthread_t tid;
pthread_create(&tid, NULL, &anti_debugger_thread, NULL);
}
int main()
{
printf("pid: %d\n", getpid());
anti_debugger();
loop();
return 0;
}
从main函数入手
main函数中调用了anti_debugger()
函数
anti_debugger()
函数定义了t tid
参数
anti_debugger_thread
作为参数传入
然后继续往上看
分析check_debugger(pid);
1.fopen:打开文件操作的意思
2.path是上面传入path路径的256
3.权限是以rt模式打开
4.然后是一个if判断是否打开为空
5.执行while循环,whie循环有fgets,打开指向的fp文件,会进行条件遍历
6.if中根据传入的tracerPid进行strncmp比较
7.如果传入的tracerPid为0就直接打印8.if (!tracerPid)不为0就返回false9.最后关闭文件指针
拿出我的安卓机
上传
adb push C:\Users\12550\Desktop\poll_anti_debug data/local/tmp
adb push C:\Users\12550\Desktop\debugger data/local/tmp
赋予权限
chmod 777 debugger
chmod 777 poll_anti_debug
运行检测调试程序poll_anti_debug
可以看到在循环打印
TracerPid: 0
并且pid:5333
debugger调试程序
./debugger 5333
1.原理父进程创建一个子进程,通过子进程调试父进程
2.特点非常实用、高效的实时反调式技术
3.优点(可以作为受保护进程的主流反调试方案)消耗的系统资源比较少几乎不影响受保护进程性能可以轻易地阻止其他进程调式受保护的进程
4.缺点实现比较复杂
5.实现
5.1 核心ptrace函数
5.2进程的信号机制
6.注意进程暂停状态比较多
7.暂停状态
7.1 signal-delivery-stop状态调试器和被调试进程之间的关系
7.2 group-stop状态(难)sigcont信号同时满足两个条件:
进程/线程处于被调式状态
被调式进程/线程收到了暂停信号-->重置为0 sigstop sigtstp sigttin sigttou
7.3 sysco1l-stop状态7.4 ptrace-event-stop状态
8.反-反调式
8.1.让父进程不fork
8.2.把 while函数循环去掉
8.3.不能调试父进程,但可以调式子进程,配合双IDA调式,挂起子进程
fork是一个函数fork函数fork出一个子进程来调试自己,那么别的函数就无法进行调试了那么通过调试fork出来的子进程从而调试父进程同一时刻,我们调试的时候,一个进程只能被一个进程附加
上传
adb push C:\Users\12550\Desktop\debugger data/local/tmp
adb push C:\Users\12550\Desktop\self-debugging data/local/tmp
加权
chmod 777 debugger
chmod 777 self-debugging
找一个主进程进行调试
这里以找.com
为例
ps | grep .com
这里以nfc
进程进行调试
进程PID是:3294
debugger调试
./debugger 3*94
查看nfc进程的TracerPid
cat /proc/3294/status
我们在看看 这个TracerPid 具体对应哪个进程
ps | grep 9624
它对应我们的debugger
程序
所以
当我们用debugger
调试某个进程的时候,TracerPid
会变成调试器的Pid
运行self-debugging
主进程Pid:9842
子进程Pid:9843
子进程调用父进程
之前有提及到过
同一时刻,我们调试的时候,一个进程只能被一个进程附加
所以
综上所述:过掉self-debugger直接附加他的子进程即可绕过
JDWP协议动态调式
安卓程序动态调式条件:
1.在AndroidMainfest.xml中,application标签下
Android:debuggable=true
2.系统默认调式,在build.prop(boot.img),ro.debugable=1
Android SDK中有android.os.debug类提供了一个isDebuggerConnected方法,用于判断JDWP调式器是否正在工作
两个满足之一即可
jadx打开案例apk
我们先看AndroidMainfest.xml
文件
然后去看StubApplication
下的onCreate()
我们关注isDebuggerConnected
方法
因为它是:用于判断JDWP调式器是否正在工作
Debug.isDebuggerConnected
获取到一个值进行比较
如果为真就进行加载loadLibrary
库
继续往上看
所以只有符合条件成立才会执行if里面的逻辑,进行加载so库
这就是在java层进行反调试,也能用来保护代码
进行Androidkiller 反编译
这个APK是加壳的 这里不是我们的重点 先跳过
我们今天是要熟悉该APK 中Java层 反调试的逻辑
我们直接工程搜索isDebuggerConnected
方法
我们就找到了这个方法的Smali代码
代码分析
这里是判断
然后v0
返回给cond_0
那么我们取反
nez 改成 eqz 即可
拿到一个apk,我们一个简单的思路
第一步:先去查壳
第二步:APK反编译查看有没有签名
第三步:逆向分析逻辑
jadx反编译AntiDebug.apk
找onCreate()
开始分析
loadLibrary
直接加载了antidebug
,那么说明逻辑在so库里面
上IDA 分析
把AntiDebug.apk用解压软件打开 找到so库文件
拖入IDA 即可
找静态注册的动态函数:JNI_Onload
TAB伪代码进行查看
右键 进行隐藏类型
然后进行代码分析
两个赋值
然后是if判断
if判断中用了或运算符
后面四个参数 有一个 达成条件既满足if判断
需要将这四个函数 都为假 才能绕过
先查看anti_time()
参数 双击进来
代码分析
1.首先定义结构体类型
2.定义v0=getpid
3.然后调用了同一个函数gettimefday传入两个不同的值
4.v1 = tv.tv_sec - v3.tv_sec;
通过传入两个不同的参数调用tv_sec
做差值获取到v1
5.如果if判断,v1小于等于1返回0,否者就kill,这就是一个简单的时间检测
双击进来
我们关注 return 返回值
想要返回值不触发,只需要这里面函数的返回值都为0即可
这里很多if和while嵌套循环,那么最终不执行return 1,可以在多个if中进行修改条件即可
双击进来
分析代码
1.pthread_self创建子线程
2.pipe(&pipefd)
是管道意思是实现进程通信
3.pthread_create创建线程,传入了四个参数
查看第三个参数: anti_thread
if判断 然后return 0
这是一个获取线程
registerNativeMethod是一个无关的参数
获取主类
com.qianyu.antidebug/.MainActivity
上传安装apk
adb install C:\Users\12550\Desktop\AntiDebug.apk
挂起apk程序
adb shell am start -D -n com.qianyu.antidebug/.MainActivity
打开ddms
启动android_server
注:我更名且指定了端口
端口转发
adb forward tcp:7788 tcp:7788
IDA 连接
增加设置
F9 进入运行状态
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8600
IDA 运行一下 加载so库
找到JNI_OnLoad
双击进入
下断点
F9 运行
F7单步步入
可以看到状态寄存器 T=1
说明这里面是Thumb模式指令
0 2 4 6 8 A
两位数
F8 跳下来
注意R*3
R*3是GetEnv
注:这里实在截图不方便 ,手机拍照 师傅们见谅
如果Enν获取成功的话就会执行后面的三个函数
这三个函数会在这里进行反调试,如何跳过不执行呢?
同步PC寄存器
选中上面第一个函数
然后下面栈右键
F2 开始进行修改 为00
最后F2保存
把后面两个参数的栈地址一样的方法进行修改为00
前三个参数 就没有了看第四个参数
双击进入
静态代码也双击进入第四个参数
R5有四个参数
只需要看第三个参数即可
F4 跳到R5
然看第三个参数R2
点击箭头进入
识别成功后地址什么都没有
这里直接给师傅们贴出来
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/inotify.h>
#include <elf.h>
#include <pthread.h>
#include <sys/types.h>
#include <fcntl.h>
#include <signal.h>
#include <android/log.h>
#define LOG_TAG "qianyu"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN , LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG, __VA_ARGS__)
#define MAX 1024
//检测代码执行时间差
jint anti_time(){
int pid = getpid();
struct timeval t1;
struct timeval t2;
struct timezone tz;
gettimeofday(&t1, &tz);
gettimeofday(&t2, &tz);
int timeoff = (t2.tv_sec) - (t1.tv_sec);
LOGD("time %d",timeoff);
if (timeoff > 1) {
int ret = kill(pid, SIGKILL);
return 1;
}
return 0;
}
//inotify检测
jint anti_inotify(){
const int MAXLEN = 2048;
int ppid =getpid();
char buf[1024],readbuf[MAXLEN];
int pid, wd, ret,len,i;
int fd;
fd_set readfds;
//防止调试子进程
ptrace(PTRACE_TRACEME, 0, 0, 0);
fd = inotify_init();
sprintf(buf, "/proc/%d/maps",ppid);
wd = inotify_add_watch(fd, buf, IN_ALL_EVENTS);
if (wd < 0) {
LOGD("can't watch %s",buf);
return 0;
}
while (1) {
i = 0;
//注意要对fd_set进行初始化
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
//第一个参数固定要+1,第二个参数是读的fdset,第三个是写的fdset,最后一个是等待的时间
//最后一个为NULL则为阻塞
//select系统调用是用来让我们的程序监视多个文件句柄的状态变化
ret = select(fd + 1, &readfds, 0, 0, 0);
if (ret == -1)
break;
if (ret) {
len = read(fd,readbuf,MAXLEN);
while(i < len){
//返回的buf中可能存了多个inotify_event
struct inotify_event *event = (struct inotify_event*)&readbuf[i];
LOGD("event mask %d\n",(event->mask&IN_ACCESS) || (event->mask&IN_OPEN));
//这里监控读和打开事件
if((event->mask&IN_ACCESS) || (event->mask&IN_OPEN)){
LOGD("kill!!!!!\n");
//事件出现则杀死父进程
int ret = kill(ppid,SIGKILL);
LOGD("ret = %d",ret);
return 1;
}
i+=sizeof (struct inotify_event) + event->len;
}
}
}
inotify_rm_watch(fd,wd);
close(fd);
return 0;
}
/*
* 检测在调试状态下的软件断点(断点扫描)
* 读取其周围的偏移地址有没有ARM等指令集的断点指令
* 遍历so中可执行segment,查找是否出现breakpoint指令即可
* */
unsigned long GetLibAddr() {
unsigned long ret = 0;
char name[] = "libantidebug.so";
char buf[4096], *temp;
int pid;
FILE *fp;
pid = getpid();
sprintf(buf, "/proc/%d/maps", pid);
fp = fopen(buf, "r");
if (fp == NULL) {
puts("open failed");
goto _error;
}
while (fgets(buf, sizeof(buf), fp)) {
if (strstr(buf, name)) {
temp = strtok(buf, "-");//将buf由"-"参数分割成片段
ret = strtoul(temp, NULL, 16);//将字符串转换成unsigned long(无符号长整型数)
break;
}
}
_error: fclose(fp);
return ret;
}
jint anti_breakpoint(){
Elf32_Ehdr *elfhdr;
Elf32_Phdr *pht;
unsigned int size, base, offset,phtable;
int n, i,j;
char *p;
//从maps中读取elf文件在内存中的起始地址
base = GetLibAddr();
if(base == 0){
LOGD("find base error/n");
return 0;
}
elfhdr = (Elf32_Ehdr *) base;
phtable = elfhdr->e_phoff + base;
for(i=0;i<elfhdr->e_phnum;i++){
pht = (Elf32_Phdr*)(phtable+i*sizeof(Elf32_Phdr));
if(pht->p_flags&1){
offset = pht->p_vaddr + base + sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr)*elfhdr->e_phnum;
LOGD("offset:%#x ,len:%#x",offset,pht->p_memsz);
p = (char*)offset;
size = pht->p_memsz;
for(j=0,n=0;j<size;++j,++p){
if(*p == 0x10 && *(p+1) == 0xde){
n++;
LOGD("### find thumb bpt %#x /n",p);
return 1;
}else if(*p == 0xf0 && *(p+1) == 0xf7 && *(p+2) == 0x00 && *(p+3) == 0xa0){
n++;
LOGD("### find thumb2 bpt %#x /n",p);
return 1;
}else if(*p == 0x01 && *(p+1) == 0x00 && *(p+2) == 0x9f && *(p+3) == 0xef){
n++;
LOGD("### find arm bpt %#x /n",p);
return 1;
}
}
LOGD("### find breakpoint num: %d/n",n);
}
}
return 0;
}
//多进程/线程
int pipefd[2];
int childpid;
int isAnti=0;
void *anti_thread(void*){
int statue=-1,alive=1,conut=0;
close(pipefd[1]);
while(read(pipefd[0],&statue,4)>0)
break;
sleep(1);
//这里改为非阻塞
fcntl(pipefd[0],F_SETFL,O_NONBLOCK);
LOGI("pip-->read=%d",statue);
while(true){
LOGI("pip-->read=%d",statue);
read(pipefd[0],&statue,4);
sleep(1);
LOGI("pip-->read=%d",statue);
if(statue!=0){
if(isAnti==0)
return NULL;
kill(childpid,SIGKILL);
kill(getpid(),SIGKILL);
return NULL;
}
statue=-1;
isAnti=1;
}
}
void anti(){
int pid,p;
FILE *fd;
char filename[MAX];
char line[MAX];
pid=getpid();
//读取/proc/pid/status中的tracerPid
sprintf(filename,"/proc/%d/status",pid);
p=fork();
if(p==0){
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);
fclose(fd);
if(statue!=0){
LOGI("tracer pid:%d",statue);
return;
}
break;
}
}
sleep(1);
}
}else{
LOGI("father");
childpid=p;
}
}
jint anti_pthread(){
// id_0:新线程标识符
pthread_t id_0;
id_0=pthread_self();
pipe(pipefd);
pthread_create(&id_0,NULL,anti_thread,(void*)NULL);
LOGI("start");
anti();
return 0;
}
JNINativeMethod nativeMethod[]={};
jint registerNativeMethod(JNIEnv* env){
jclass clszz=env->FindClass("com/qianyu/antidebug/MainActivity");
if(env->RegisterNatives(clszz,nativeMethod,sizeof(nativeMethod)/sizeof(nativeMethod[0]))!=JNI_OK){
return JNI_ERR;
}
return JNI_OK;
}