Android trap攻防
图/文 h_one
0x01 反编译出错
1.插入无效指令是部分逆向工具崩溃
原理:大部分逆向工具都是线性读取字节码并解析, 如dex2jar,baksmali,apktool等,当遇到无效字节码时,就会引起反编译工具解析失败。
例如:新版的dex2jar 遇到这种情况任然没法转化成jar,在新版本的baksmail和apktool已修复此问题。
010editor查看,红色框中就是加入的陷阱类,绕过方法很简单,只要将这个三个类删除,重编译即可
![](http://img.e-com-net.com/image/info8/da4148b257a04148b44b59adb82d2545.jpg)
2.利用apk包本质上是zip/jar包进行保护
a.伪加密
在Android4.2.x前的一种保护方 式,通过APK(压缩文件)进行伪加密,其修改原理是修改连续4位字节标记为“P K 01 02”后的第5字节(ps:一般在文件末尾有多处),奇数表示加密偶数不加密,这种保护方式一般只会出现在一些cm里,因为:
1.对系统不兼容;
2.伪加密 处理后的apk市场也无法对其进行安全检测,部分市场会拒绝这类APK上传市场
b. 文件名长度操作
Android平台对文件名的长度是没有限制的,但是操作系统要求不能大于255;于是可以构建超长字符的类名达到反编译错误目的。
3.axml文件保护
目的:防止/检测二次打包
android在解析AXML文件时,是用属性的资源id,而不是资源名,当android系统遇到非法资源id时,并不会做解析,所以可以对axml文件解析,添加无用的属性。但是对于破解者来说,一般会在Java层增加log信息,然后打包apk, 此时如apktool之类的逆向工具,就会无法解析无效属性,或进入trap类(检测二次打包)
0x02 运行环境检测
环境检测主要包括
- ▏运行在调试状态下
- ▏系统代码被hook
- ▏以及运行环境在模拟器中
目的:为了保护关键代码被逆向分析,一般放在应用程序初始化过程中,如init_array,或jni_onload函数里进行检查代码执行。、
1.调试检测
对调试器的检测(ida,gdb,strace, ltrace,android_server等调试工具)
a.父进程检测
b.当前运行进程检测
例如,对android_server进程检测。针对这种检测只需将android_server改名就可绕过
- pid_t GetPidByName(const charchar *as_name) {
- DIR *pdir = NULL;
- struct dirent *pde = NULL;
- FILEFILE *pf = NULL;
- char buff[128];
- pid_t pid;
- char szName[128];
-
- pdir = opendir("/proc");
- if (!pdir) {
- perror("open /proc fail.\n");
- return -1;
- }
- while ((pde = readdir(pdir))) {
- if ((pde->d_name[0] < '0') || (pde->d_name[0] > '9')) {
- continue;
- }
- sprintf(buff, "/proc/%s/status", pde->d_name);
- pf = fopen(buff, "r");
- if (pf) {
- fgets(buff, sizeof(buff), pf);
- fclose(pf);
- sscanf(buff, "%*s %s", szName);
- pid = atoi(pde->d_name);
- if (strcmp(szName, as_name) == 0) {
- closedir(pdir);
- return pid;
- }
- }
- }
- closedir(pdir);
- return 0;
- }
c.读取进程状态(/proc/pid/status)
State属性值T 表示调试状态,TracerPid 属性值正在调试此进程的pid,在非调试情况下State为S或R, TracerPid等于0
![Android trap攻防思路整理_第2张图片](http://img.e-com-net.com/image/info8/af386ee0891746668e09f28e2a57a660.png)
d.读取 /proc/%d/wchan
下图中第一个红色框值为非调试状态值,第二个红色框值为调试状态值:
![Android trap攻防思路整理_第3张图片](http://img.e-com-net.com/image/info8/4124a6909ea24adcb083c4376f84b71a.png)
- static void get_process_status(pid_t pid,const char* info,charchar *outline)
- {
- FILEFILE *fp;
- char filename;
- char line = {0};
- snprintf( filename, sizeof(filename), "/proc/%d/status", pid );
- fp = fopen( filename, "r" );
- if ( fp != NULL )
- {
- while ( fgets( line, sizeof(line), fp ) )
- {
- if ( strstr( line, info ) )
- strcpy(outline,line);
- }
- fclose( fp ) ;
- }
- return ;
- }
- static int getProcessStatus(int pid)
- {
- char readline = {0};
- int result = STATUS_ELSE;
- get_process_status(pid,"State",readline);
- if(strstr(readline,"R"))
- result = STATUS_RUNNING;
- else if(strstr(readline,"S"))
- result = STATUS_SLEEPING;
- else if(strstr(readline,"T"))
- result = STATUS_TRACING;
- return result;
- }
- static int getTracerPid(int pid)
- {
- char readline = {0};
- int result = INVALID_PID;
- get_process_status(pid,"TracerPid",readline);
- charchar *pidnum = strstr(readline,":");
- result = atoi(pidnum + 1);
- return result;
- }
- static int getWchanStatus(int pid)
- {
- FILEFILE *fp= NULL;
- char filename;
- char wchaninfo = {0};
- int result = WCHAN_ELSE;
- char cmd = {0};
- sprintf(cmd,"cat /proc/%d/wchan",pid);
- LOGANTI("cmd= %s",cmd);
- FILEFILE *ptr; if((ptr=popen(cmd, "r")) != NULL)
- {
- if(fgets(wchaninfo, 128, ptr) != NULL)
- {
- LOGANTI("wchaninfo= %s",wchaninfo);
- }
- }
- if(strncasecmp(wchaninfo,"sys_epoll\0",strlen("sys_epoll\0")) == 0)
- result = WCHAN_RUNNING;
- else if(strncasecmp(wchaninfo,"ptrace_stop\0",strlen("ptrace_stop\0")) == 0)
- result = WCHAN_TRACING;
- return result;
- }
e. ptrace 自身或者fork子进程相互ptrace(梆梆加固之类的反调试)
- ptrace me
- if (ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) {
- printf("DEBUGGING... Bye\n");
- return 1;
- }
- void anti_ptrace(void)
- {
- pid_t child;
- child = fork();
- if (child)
- wait(NULL);
- else {
- pid_t parent = getppid();
- if (ptrace(PTRACE_ATTACH, parent, 0, 0) < 0)
- while(1);
- sleep(1);
- ptrace(PTRACE_DETACH, parent, 0, 0);
- exit(0);
- }
- }
f. 防止内存dump
利用Inotify机制,对/proc/pid/mem和/proc/pid/pagemap文件进行监视。inotify API提供了监视文件系统的事件机制,可用于监视个体文件,或者监控目录。具体原理可参考:http://man7.org/linux/man- pages/man7/inotify.7.html
伪代码:
- void __fastcall anitInotify(int flag)
- {
- MemorPagemap = flag;
- char *pagemap = "/proc/%d/pagemap";
- char *mem = "/proc/%d/mem";
- pagemap_addr = (charchar *)malloc(0x100u);
- mem_addr = (charchar *)malloc(0x100u);
- ret = sprintf(pagemap_addr, &pagemap, pid_);
- ret = sprintf(mem_addr, &mem, pid_);
- if ( !MemorPagemap )
- {
- ret = pthread_create(&th, 0, (void *(*)(void *)) inotity_func, mem_addr);
- if ( ret >= 0 )
- ret = pthread_detach(th);
- }
- if ( MemorPagemap == 1 )
- {
- ret = pthread_create(&newthread, 0, (void *(*)(void *)) inotity_func, pagemap_addr);
- if(ret > 0)
- ret = pthread_detach(th);
- }
- }
- void __fastcall __noreturn inotity_func(const char *inotity_file)
- {
- const charchar *name;
- signed int fd;
- bool flag;
- bool ret;
- ssize_t length;
- ssize_t i;
- fd_set readfds;
- char event;
- name = inotity_file;
- memset(buffer, 0, 0x400u);
- fd = inotify_init();
-
- inotify_add_watch(fd, name, 0xFFFu);
- while ( 1 )
- {
- do
- {
- memset(&readfds, 0, 0x80u);
- }
- while ( select(fd + 1, &readfds, 0, 0, 0) <= 0 );
- length = read(fd, event, 0x400u);
- flag = length == 0;
- ret = length < 0;
- if ( length >= 0 )
- {
- if ( !ret && !flag )
- {
- i = 0;
- do
- {
- inotity_kill((int)&event);
- i += *(_DWORD *)&event + 16;
- }
- while ( length > i );
- }
- }
- else
- {
- while ( *(_DWORD *)_errno() == 4 )
- {
- length = read(fd, buffer, 0x400u);
- flag = length == 0;
- ret = length < 0;
- if ( length >= 0 )
- }
- }
- }
- }
g. 对read做hook
因为一般的内存dump都会调用到read函数,所以对read做内存hook,检测read数据是否在自己需要保护的空间来阻止dump
h. 设置单步调试陷阱
- int handler()
- {
- return bsd_signal(5, 0);
- }
- int set_SIGTRAP()
- {
- int result;
- bsd_signal(5, (int)handler);
- result = raise(5);
- return result;
- }
2. 模拟器检测
用户层行为和数据检测,模拟器特有属性值,以及模拟器体系结构特征
- ▏电池状态和电流,模拟器默认电话号码检测,检测设备IDS 是不是“000000000000000”, 检测imsid是不是“310260000000000“,手机运营商等
- ▏API Demo,Dev tool一般模拟器上才有的应用,检测安装应用,短信箱,通信录,相册等
- ▏读取/system/build.prop文件
- ▏调用__system_property_get或反射调用Systemproperty.get获取系统属性值
- ▏通过执行shell命令检测模拟器,如getprop
- ▏检查模拟器特有文件如 /dev/socket/qemud","/dev/qemu_pipe","/sysrtem/bin/qemud",/dev/qemu_pipe,/dev/qemu_trace等
- ▏模拟器cpu信息值差异,如hardware,Revision等
- ▏系统属性值等(android.os.build)
- ▏基于Qemu二进制翻译技术(ps:真机具有真正的物理CPU,在执行一段指令的时候只能一条一条的去执行指令(编译器没有对指令进行优化的前提下)。模拟器没有真正的物理CPU,所以,他在执行一段指令的时候,这段指令已经被人为的优化掉)http://www.dexlabs.org/blog/btdetect
- ▏通过观察低级别的缓存行为。检测方法: 默认情况下,Android模拟器提供了Android SDK是基于QEMU,仿真器不具备分裂缓存。而在真实设备上存在两个不同的缓存,一个用于数据访问,一个用于指令。https://bluebox.com/technical/android-emulator-detection-by-observing-low-level-caching-behavior/
0x03防app运行环境被hook(*)
hook代码肯定是在app自身模块加载之前运行的,那么在app的maps表里会首先加载hook框架的dex,我们只要对此dex做简单的校验,就会检测到app被注入了。
编码思路:
遍历maps表,查看子串是否存在“[email protected]”的字符串,若存在--获取该模块的startAddr和endAddr, 然后检验此odex的头部是否为真正的dex文件。
0x4抗静态分析(ida F5 以及执行流程图)
1.arm 指令插花
![Android trap攻防思路整理_第4张图片](http://img.e-com-net.com/image/info8/a6450fc881f04f8fbef80090bf149f4f.png)
2.通过栈修改程序调用过程
STDFD保存寄存器值到栈上,LDMFD将栈上数据赋值到寄存器中,这个过程修改了函数返回的地址。
![Android trap攻防思路整理_第5张图片](http://img.e-com-net.com/image/info8/da464dd12edc4d828f4d23375b47d7e7.jpg)
0x05如何绕过app环境检测
1.调试过程中修改代码
hook检测点(path检测点的函数代码,函数返回值修改等),修改源码(改变字段属性主要针对模拟器仿真,修改函数返回值,例如绕过签名校验等)
eg:
ida patch 线程退出函数: patch 地址48D9668A处的函数调用
![Android trap攻防思路整理_第6张图片](http://img.e-com-net.com/image/info8/a9e058e038c240cb8beb939bc4419d2a.png)
a.在数据窗口定位到机器码位置
b.F2 编辑机器码 00 00 0A EF (movs R0, R0)
c.F2保存修改
![Android trap攻防思路整理_第7张图片](http://img.e-com-net.com/image/info8/d9d88ea102714dfdac67e4f2b6f8611a.png)
2.fopen函数相关的检测
由于/proc/pid/status,/proc/pid/wchan,/proc/pid/mem等都是针对文件状态的检测,入口点函数一般都为fopen, 我们可以事先拦截fopen,查看app是否有这方面的防护。
以某某app为例:如下图此app fopen了这些文件,我们就能猜测这是对调试检测。
![](http://img.e-com-net.com/image/info8/c8b2a13c7bce4191b850de766a0aea89.png)
绕过方法:在指定目录下(/data/local/tmp)新建一文件alimolisec, hook fopen函数检测到文件名子串有/proc/self时,就重定位打开alimolisec文件
- FILEFILE * MyOpen( const charchar * filename, const charchar * mode ) {
- FILEFILE *file = NULL;
- if (strstr(filename, "/proc/self")){
- LOGI("fileName:%s", filename);
- file = oldFopen("/data/local/tmp/hone", mode );
- }else
- file = oldFopen( filename, mode );
- return file;
- }
0x06 CM中的那些壳子
加固手法:
1.不替换源classes.dex,也没有做任何加密的处理,对classes.dex中的Activity,service, receiver等的oncreate,onReceive加密替换,壳首先拿到执行权,在自身so里完成对源dex还原。
2.不替换源classes.dex,也没有做任何加密的处理, 修改原Dex的Class_Data,将MyContentProvider,Application,Activity,service类的入口函数 onCreate方法标记为native方法,但是原始字节码仍然未加密保存在dex文件中。
3.对classes.dex整包加密,使用壳加载器内存解密classes.dex并替换原始成源classes.dex
4.对classes.dex整包加密,并将原dex拆分成两部分,在内存中分两块区域存储(阿里移动安全比赛加固)。
脱壳手法:
1.对部分整包加密的可以通过运行时内存dump,部分通过拦截dvmDexFileOpenPartial函数即可获取完整dex。(dex连续)
2.对于修改了dex的Class_data和classes.dex做了拆分的,可以通过找到dex对应的pDvmDex
结构,重建dex(dex不连续或不完整的)
0x07 android平台常用的hook框架
▏cydia substrate
原理:框架注入zygote进程,采用inline hook( 修改目标函数前N字节,跳转到自定义函数入口,备份目标函数前N个字节,跳转回目标函数)
▏Xposed
原理:替换app_process,将需要hook的java函数替换成JNI函数,所有需要HOOK的函数首先由xposedCallHandler处理,xposedCallHandler负责调用注册的beforeHookMethod和afterHookedMethod函数
▏adbi
原理:利用ptrace()函数attach到一个进程上,然后在其调用序列中插入一个调用dlopen()函数的步骤,将一个事先预先备好的.so文件加载到要hook的进程内存中,最终由这个加载的.so文件在初始化函数中hook指定的函数。
参考文档
http://man7.org/linux/man-pages/man7/inotify.7.html
http://www.dexlabs.org/blog/btdetect
https://bluebox.com/technical/android-emulator-detection-by-observing-low-level-caching-behavior/
https://github.com/crmulliner/adbi
http://www.cydiasubstrate.com/
转载地址:http://blog.csdn.net/zihao2012/article/details/44238273#0-tsina-1-93113-397232819ff9a47a7b7e80a40613cfe1