android反调试技术
检测调试相关文件android_server等#
int SearchFile(std::string file_path)
{
int ret = 0;
// fork进程检测
std::string command = "cat ";
command.append(file_path.c_str());
FILE *fd1 = popen(command.c_str(), "r");
if(NULL != fd1){
char m_buffer[10] = {0};
if(fgets(m_buffer, sizeof(m_buffer), fd1)){
return -1;
}
pclose(fd1);
}
// 直接打开
int fd2 = open(file_path.c_str(), O_RDONLY);
if(fd2 != -1){
close(fd2);
return -1;
}
//fd反查
DIR *dir = opendir("/proc/self/fd/");
if (dir != NULL) {
struct dirent *entry = NULL;
while ((entry = readdir(dir)) != NULL) {
char buf[0x1000] = {0};
char filePath[0x1000] = {0};
snprintf(filePath, sizeof(filePath), "/proc/self/fd/%s", entry->d_name);
readlinkat(AT_FDCWD, filePath, buf, 0x1000);
if (NULL != strstr(buf, file_path.c_str())) {
ret = -1;
break;
}
}
closedir(dir);
}
return ret;
}
这种检测方法很好绕过,直接修改文件名称即可。
检测特定的字段#
调试状态下/proc/pid/stat 和/proc/pid/task/pid/stat的第三个字段会变成t
调试状态下/proc/pid/wchan 和/proc/pid/task/pid/wchan的值会变成ptrace_stop
/proc/pid/status文件的TracerPid的值为调试器进程的pid
检测android_server等调试器进程默认端口号#
android_server进程默认监听的是23946端口,gdbserver默认监听的是27042端口
通过/proc/net/tcp文件获取正在被监听的端口
int SearchPort()
{
int ret = 0;
char m_buffer[0x1000] = {0};
FILE *fd = fopen("/proc/net/tcp","r");
if(NULL == fd){
return -1;
}
while(fgets(m_buffer, sizeof(m_buffer), fd)){
if( strstr(m_buffer, "5D8A") ||
strstr(m_buffer, "69A2")){
printf("check debug!\n");
ret = -1;
break;
}
}
fclose(fd);
return ret;
}
android 10之后/proc/net
文件系统的访问权限实施了限制,可以直接建立socket连接来确认是否存在端口占用。
inotify监控文件#
通过linux提供的inotify来监控文件/proc/pid/mem
和/proc/pid/pagemap
是否被读写,防止一些dump操作。
检测指令间运行时间差#
程序正常执行两天指令执行的时间差是很短的,如果被调试的话时间差会大大增加。
扫描内存断点#
内存断点是通过修改程序指令实现的,通过遍历内存中的各个段并判断是否为bkpt指令可以发现是否存在内存断点。
bool check_break_point (uint32_t module_base)
{
Elf32_Ehdr *elf_header;
uint32_t program_offset, program_header;
if (module_base == 0) {
return false;
}
elf_header = (Elf32_Ehdr *) module_base;
program_header = module_base + elf_header->e_phoff;
for (int i = 0; i < elf_header->e_phnum; i++) {
Elf32_Phdr *ph_t;
ph_t = (Elf32_Phdr*)(program_header + i * sizeof(Elf32_Phdr));
if ( !(ph_t->p_flags & 1) ) continue;
program_offset = module_base + ph_t->p_vaddr;
program_offset += sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) * elf_header->e_phnum;
char *p = (char*)program_offset;
for (int j = 0; j < ph_t->p_memsz; j++) {
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;
}
主动抛出SIGTRAP异常反调试#
程序主动抛出SIGTRAP异常信号,操作系统在得到SIGTRAP信号后会检查是否存在调试器,如果存在调试器会将信号先传递给调试器。这样程序中的异常处理程序就得不到执行,调试器就会陷入死循环。
//利用ida先截获信号的特性进行反调试
//异常处理函数
void my_signal(int sig)
{
printf("not find debugger!\n");
}
int SetSignal()
{
//设置异常对应的异常处理函数
sighandler_t ret = signal(SIGTRAP, my_signal);
if(SIG_ERR == ret){
return -1;
}
//主动抛出异常
raise(SIGTRAP);
return 0;
}
bkpt指令反调试#
在程序中malloc申请内存硬编码存放bkpt指令,然后设置异常处理函数来恢复程序执行。如果程序被调试的话操作系统会先将异常信号传递给调试器,异常处理函数就得不到执行,程序无法恢复执行陷入死循环。
//利用bkpt指令反调试
uint32_t pBkpt;
//SIGTRAP信号处理函数
void my_sigtrap(int sig)
{
//将bkpt指令覆盖填充为arm模式的mov pc,r1指令,执行此指令后返回
*((uint32_t*)pBkpt) = 0xE1A0F001;
}
int SetBkpt()
{
pBkpt = (uint32_t)malloc(0x10);
if(NULL == pBkpt){
return -1;
}
if(mprotect((void*)((uint32_t)pBkpt / PAGE_SIZE * PAGE_SIZE), PAGE_SIZE,PROT_READ|PROT_WRITE|PROT_EXEC)){
return -1;
}
*((uint32_t*)pBkpt) = 0xE1200070; //填充arm模式的bkpt指令
signal(SIGTRAP, my_sigtrap); //设置SIGTRAP异常处理函数
__asm__ volatile(
"mov r0, %[pBkpt]\n\t"
"mov r1,pc\n\t" //保存返回地址
"BX r0\n\t" //执行bkpt指令
: //在内联汇编代码中修改的变量列表
:[pBkpt] "r"(pBkpt) //在内联汇编代码中使用的变量列表
: "cc", "r0","r1","pc" //在内联汇编中使用的寄存器列表
);
return 0;
}
rtld_db_dlactivity#
linker的rtld_db_dlactivity
函数在非调试模式下为空函数,如果程序被调试则会被修改为对应的断点指令,可以对此函数进行检测。
ptrace反调试#
因为一个程序只能被一个调试进程调试,所以有其他进程ptrace的话就会失败。可以创建一个守护进程,并通过ptarce轮询附加主进程,如果失败则证明主进程被调试。
int ret = ptrace(PTRACE_ATTACH, pid, NULL, NULL);
if(ret){
return -1;
}
return 0;
双进程反调试#
- 可以通过主进程fork子进程,然后子进程ptarce调试父进程并监控父进程所有的线程(ptrace 附加的单位是线程),同时父进程通过进程间通讯(信号量等)检测子进程是否存活,这样其他进程就无法ptrace调试主进程。
针对上述方法可以ptrace注入子进程并在子进程中调用ptrace PTRACE_DETACH 父进程解除保护,这样就可以正常调试父进程了。针对这种方法可以对双进程保护逻辑进行优化,子进程 ptrace父进程的所有线程,但是需要保留一个守护线程。此守护线程ptrace 附加子进程,这样子进程与父进程都无法被ptrace。https://blog.csdn.net/u011247544/article/details/78248427 ,https://xz.aliyun.com/t/9815。
反ida静态分析#
- 因为ida使用的递归下降的反汇编引擎,而这种引擎有个最大的缺点就是无法识别间接跳转指令。在函数中穿插间接跳转指令可以反ida F5分析
int OrderObscure()
{
__asm__ volatile( \
"mov r0,pc \n\t" \
"adds r0,0xc \n\t" \
"push {r0} \n\t" \
"pop {r0} \n\t" \
"bx r0 \n\t"
:::"r0" \
);
printf("is ok");
return 0;
}
上面的代码在函数中使用的间接跳转指令bx r0,ida查看此汇编代码bx r0后的指令是没有识别的。
IDA F5也是无法正确分析的
但是还是ida还是可以正确查看整个函数对应的反汇编代码的
因为ida无法同时解析arm与thumb指令,所以进一步的在代码中硬编码穿插thumb指令可以使ida连汇编代码也无法解析。
//1. 利用代码中穿插BX R0间接跳转指令
//2. 利用代码中混合使用thumb和arm指令
int OrderObscure()
{
__asm__ volatile( \
"mov r0,pc \n\t" \
"adds r0,0xd \n\t" \
"push {r0} \n\t" \
"pop {r0} \n\t" \
"bx r0 \n\t"
//thhub指令
".byte 0x00 \n\t" \
".byte 0xBF \n\t" \
\
".byte 0x78 \n\t" \
".byte 0x46 \n\t" \
\
".byte 0x02 \n\t" \
".byte 0x30 \n\t" \
\
".byte 0x00 \n\t" \
".byte 0x47 \n\t" \
:::"r0" \
);
//nop
//mov r0,pc
//adds r0,2
//bx r0
printf("is ok");
return 0;
}
ida查看对应的反汇编代码,发现无法正确解析thumb指令集的汇编代码以及后续的代码。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探