so保护总结
so反调试
检查Tracepid
由于调试进程附加到被调试进程后,目标进程会改变进程状态,将调试进程pid设为本进程的跟踪进程,在进程信息里可查看,/proc/[pid]/status
中的TracePid字段就是跟踪进程的pid,如果进程未被跟踪,那么该值为0。所以我们可以通过该值是否为0来判断进程是否被调试。
示例代码:
1 | void readtrace() |
思路讲解
通过开辟一个线程,在线程中执行检查代码,首先获取当前进程的pid,在获取该进程的status信息,遍历字段,找到TracePid字段,然后检查该值是否为0,如果不为0,即认为进程被调试,kill掉进程即可。
效果
运行程序,开始循环检查。
未被调试时,TracePid值为0.
我们用ida附加上去,观察效果。
点击运行后,发现检查到了被调试,TracePid的值时13615.
我们的调试进程的pid正好时13615.
利用ptrace实现双进程保护
我们发现ida附加上去后,进程停止了,这时候并不能实现检查,那么我们需要开一个进程去检查而不是线程,所以双进程保护是一个很好的方式。我们利用fork函数来开一个子进程,在子进程中检查父进程时候被调试,而子进程由父进程调试,由于linux下,进程只允许被一个进程调试,那么我们用ida附加到子进程是不被允许的,而如果附加到父进程又会被子进程检测到,所以双进程保护可以起到更好的效果。
示例代码
1 | void readtrace2() |
思路讲解
父进程fork后,在子进程中完成和之前通用的操作,而在父进程中,要完成对子进程的附加操作。我使用了ptrace,关于ptrace,可以看一下我之前遇到问题时找到的一篇文章,我把它翻译了——链接。
父进程wait(NULL);
是为了获取子进程SIGTRAP信号,子进程wait(NULL);
是为了通知父进程。因为执行PTRACE操作时,目标进程必须是停止状态。在子进程里PTRACE_TRACEME操作,在之前找到的别人的代码中只要子进程执行PTRACE_TRACEME就会被父进程跟踪,而我实验发现并不可行,必须要有这两个wait来满足被跟踪进程停止的条件。
效果
运行程序:
子进程的PTRACE_TRACE执行成功,子进程开始检查。
子进程的TracePid不为0,子进程已经被跟踪。
9998是父进程的一个线程。
这时候我们尝试用ida附加,先附加父进程。
子进程检查到调试进程,这次不用ida使其运行起来,父进程直接被子进程kill掉了。
我们尝试附加子进程。
发现根本无法附加上去。
执行时间校验
通过计算某部分代码的执行时间差来判断是否被调试,在Linux内核下可以通过time、gettimeofday,或者直接通过sys call来获取当前时间。另外,还可以通过自定义SIGALRM信号来判断程序运行是否超时。
文件监控
在Linux下,inotify可以实现监控文件系统事件(打开、读写、删除等),加固方案可以通过inotify监控apk自身的某些文件,某些内存dump技术通过/proc/pid/maps、/proc/pid/mem来实现内存dump,所以监控对这些文件的读写也能起到一定的反调试效果。
调试进程及端口检查
- 使用IDA动态调试APK时,android_server默认监听23946端口,所以通过检测端口号可以起到一定的反调试作用。具体而言,可以通过检测/proc/net/tcp文件,或者直接system执行命令netstat -apn等。
- 在对APK进行动态调试时,可能会打开android_server、gdb、gdbserver等调试相关进程,一般情况下,这几个打开的进程名和文件名相同,所以可以通过运行状态下的进程名来检测这些调试相关进程。具体而言,可以通过打开/proc/pid/cmdline、/proc/pid/statue等文件来获取进程名。当然,这种检测方法非常容易绕过――直接修改android_server、gdb、gdbserver的名字即可。
信号机制
调试器设置断点时会发出SIGTRAP信号,系统接受到SIGTRAP信号后,会用当前pc值处的原指令替换bp指令。当在代码中本来就存在bp指令时,就会陷入循环(bp指令替换bp指令)。通过先注册自己的signal函数处理breakpoint异常(SIGTRAP),然后在运行时调用该函数,即可触发自定义SIGTRAP的接管函数。而动态调试时,SIGTRAP会先被调试器接收,处理失败。
from other’s blog:
breakpoint命令会使被调试进程发出信号SIGTRAP。通常调试器会截获Linux系统内给被调试进程的各种信号,由调试者可选地传递给被调试进程。但是SIGTRAP是个例外,因为通常的目标程序中不会出现breakpoint,因为这会使得程序自己奔溃。因此,当调试器遇到SIGTRAP信号时会认为是自己下的断点发出的。这样一来当调试器给这个breakpoint命令插入断点breakpoint后,备份的命令也是breakpoint(原指令),这样当继续执行时,调试器将备份指令恢复并执行,结果误以为备份后这个位置发出的SIGTRAP又是自己下的断点造成的,这样一来就会使得调试器的处理逻辑出现错误,不同的调试器会导致各种不同的问题。
利用信号机制对抗,可以在代码中写好断点,自己实现对应的信号处理函数,在处理函数中加入代码解密等功能,使得调试中擅自去除断点指令会导致崩溃。再对处理函数加密或者混淆,使静态分析难度加大,而在动态调试中就必须跟入信号处理函数。
so加密
简述
section 加密
自定义section,把需要加密的函数定义在该section中,该section将会被加密,在init中将其解密。
加密操作是通过so的信息来获取到自定义section,通过offset和size来获取到指定section,把section所有字节加密,修改section修改ELFheader中几个无用的字段来方便解密时索引。
解密操作,通过之前放置的索引来获取到加密的区域,对其解密后,就可以调用了。
尝试了修改e_shoff,由于是动态注册函数,在加载的时候报错了
1 | 03-13 02:17:51.362 11931-11931/com.fancy.testsoshell E/linker: |
尝试将信息都放置e_entry中是可行的。
代码来源:http://www.wjdiankong.cn/archives/563
文章中使用静态注册+修改e_entry+e_shoff的方法,测试中出现问题,该用动态注册,加密代码稍作了修改。
ELF格式
ELF header
1 | typedef struct{ |
Program segment header table
1 | /* Program segment header. */ |
Section header table
1 | typedef struct{ |
特殊节区
section索引过程:
0+header.e_shoff=section_header_table_startaddress
section名称索引过程:
0+section_header_table[header.strndx].s_offset=string_table_startaddresss
从linker源码看so加载
首先会调用do_dlopen, 其中调用find_library来获取soinfo,find_library调用find_library_internal。
find_library_internal会调用find_loaded_library来查看程序so调用链solist中是否已存在需要加载的so,如果有直接返回soinfo,否则调用load_library来获取soinfo。
1 | static soinfo* load_library(const char* name) { // Open the file. int fd = open_library(name); if (fd == -1) { DL_ERR("library \"%s\" not found", name); return NULL; } // Read the ELF header and load the segments. ElfReader elf_reader(name, fd); if (!elf_reader.Load()) { return NULL; } const char* bname = strrchr(name, '/'); soinfo* si = soinfo_alloc(bname ? bname + 1 : name); if (si == NULL) { return NULL; } si->base = elf_reader.load_start(); si->size = elf_reader.load_size(); si->load_bias = elf_reader.load_bias(); si->flags = 0; si->entry = 0; si->dynamic = NULL; si->phnum = elf_reader.phdr_count(); si->phdr = elf_reader.loaded_phdr(); return si; } |
1 | ElfReader elf_reader(name, fd); |
ReadElfHeader():获取header
VerifyElfHeader():校验
magic
header_.e_ident[EI_CLASS]
header_.e_ident[EI_DATA]
header_.e_type
header_.e_version
header_.e_machine
ReadProgramHeader():读取Program segment header table
ReserveAddressSpace():根据program header 计算 SO 需要的内存大小并分配相应的空间
LoadSegments():遍历 program header table,找到类型为 PT_LOAD 的 segment,使用 mmap 将 segment 映射到内存
FindPhdr():获取内存中被加载的段地址
soinfo* si = soinfo_alloc(bname ? bname + 1 : name);
为so分配一个soinfo,并添加到solist中
链接:
a.定位动态节;
b.解析动态节;
c.加载依赖so;
d.重定位:
1 | phdr_table_get_dynamic_section(phdr, phnum, base, &si->dynamic, |
获取.dynamic section 在内存中的地址
1 | (void) phdr_table_get_arm_exidx(phdr, phnum, base, |
获取arm_exidx section 在内存中的地址
1 | uint32_t needed_count = 0; |
1 | typedef struct { |
由.dynamic section 各自的tag确定该节用途
d_tag = DT_SYMTAB表示该项存储的是符号表的信息
d_un.d_ptr 表示符号表的虚拟地址的偏移
d_tag = DT_RELSZ d_un.d_val 表示重定位表rel的项数
解析的过程就是遍历数组中的每一项,根据d_tag的不同,获取到不同的信息.
不同tag类型的节使用d_ptr和d_val之一,表达意义也是不一致的。
参考文章:http://blog.csdn.net/feibabeibei_beibei/article/details/53004525
http://blog.csdn.net/feibabeibei_beibei/article/details/52986326