Android加固和混淆[加固、反调试、llvm]
加固
先来一张android classloader的图
一:整体加固 特点
在原有apk的基础上套一层
呈现出:
–> 壳BootClassloader加载系统核心库
–> 壳PathClassLoader加载dex
–> 壳调用Application的attachBaseContext() 对原dex进行动态加载解密
–> 壳调用Application的onCreate() 对dex进行动态加载解密
–> 主MainActivity attachBaseContext()
–> 主MainActivity onCreate()
的效果
Notice 因为实现动态加载dex文件需要用自定义的dexClassLoader,因此要进行类加载器的修正,方法如下:
类加载器替换
替换系统组件类加载器为我们的DexClassLoader,同时设置DexClassLoader的parent为系统组件加载器
类加载器插入
打破原有的双亲委派关系,在系统组件类加载器PathClassLoader和BootClassLoader的中间插入我们自己的DexClassLoader
脱壳 思路
内存加载思路很简单,拿到dex释放在内存中的起始地址和大小,直接dump下来就行
如果是文件加载的话重点就是要找到加载dex的目录,定位解密文件
总而言之要关注dex的加载和解密流程,包括整个流程中产出的odex、vdex等编译优化文件
工具
frida dexdump
xposed FDex2
BlackDex
如果现有的工具不行的话就仿照上面的思路,用frida、xposed hook,ida动态调试利用脱壳点dump
二:函数抽取【不落地加载】 特点
落地加载的缺点就是会将dex文件释放到本地,这个过程是会造成dex文件泄露的
那么不落地加载就出现了,我们想要直接将dex文件加载到内存中
如何实现呢?
脱壳
函数抽取通常会在类加载和函数执行前自解密,或者在函数执行过程中动态自解密
因此重点关注被抽取函数的执行流程,定位抽取函数解密时机
三:dex、java2c | VMP dex、java2c
将java层代码变为c代码,是一种基于编译原理的语义等价替换。不仅提高了运行效率,也增加了逆向分析的难度
脱壳 :
VMP
缺心眼儿~
脱壳 :关键在于定位解释器,找到对应的映射关系
三代壳重点要关注JNI相关api的调用
so加固 基于 init_array 函数加密
处理: 模拟执行init_array
,然后将解密后的字符串全部保存
基于 JNI_Onload 函数加密 处理: hook
或者 unicorn
拿到解密后寄存器的值
自定义linker加固so 加密 既然是自定义,所以格式是自由的,原则就是壳so能够正确识别格式并且解密被加固的so就行
运行过程
通常来讲壳so和被加固so不会合并成一个so
壳代码加载加密so文件
,进行解密 –> 解析加载,链接重定位 –> 修正soinfo为加固so
对抗 通过soinfo_list获取对应的soinfo,得到soinfo中的program header table,基地址,映射大小等内存信息,然后去dump内存再进行修复
具体细节可以看:
ollvm 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 clang -emit-llvm -S hello_clang.c -o hello_clang.ll lli hello_clang.ll llvm-as hello_clang.ll -o hello_clang.bc llc hello_clang.bc -o hello_clang.s llvm-dis hello_clang.bc –o hello_clang.ll llvm-link test1.bc test2.bc –o output.bc clang hello_clang.s -o hello
常见手法 计算混淆、指令替代
符号混淆
控制流混淆
控制流平坦化
虚假、随机控制流
基本块拆分,基本块克隆,虚拟、随机跳转
ollvm的处理方法 虚假控制流
对于虚假控制流来讲有一个特点就是 假的逻辑不会被执行
利用这个特点网上公开的思路就已经是非常有用的:
–>模拟执行拿到指令级执行流【unidbg|unicorn】
–>在一个特定地址范围内对执行过的指令做标记
–>将未被执行的指令nop,然后重新反编译
具体可以参考:https://missking.cc/2021/05/04/ollvm2/
贴一个ida后续处理的脚本: Set color + nop
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 import ida_bytesimport idaapiimport idcdef set_address_color (ea, color ): idc.set_color(ea, idc.CIC_ITEM, color) print (f"Set color at address {hex (ea)} to {hex (color)} " ) def patch_nop (ea, cons ): nop = 0x90 if cons == 'arm64' : nop = 0xD503201F ida_bytes.patch_dword(ea, nop) print (f"Patched address {hex (ea)} with NOP" ) addresses = [] start_address = 0x124CC end_address = 0x12B68 for address in addresses: set_address_color(address, 0x00FF00 ) for j in range (start_address,end_address,4 ): if idc.get_color(j,idc.CIC_ITEM) != 0xFF00 : patch_nop(j,'arm64' )
控制流平坦化 先把解决方案放在最前面:
对于对抗来讲,一句话概括就是去除没用的代码块,用实际的执行流把真实代码块的顺序串起来
控制流平坦化本质上是一种代码块级别的混淆,它会加入分发器等等元素,打乱原本直观的代码块连接逻辑,原始的控制流平坦化长这样
这个图来自文章:https://www.52pojie.cn/thread-1488350-1-1.html
用IDA来实现traceBlock,这也是一种去混淆的方案,当然,这个适合用来学习学习,对于简单的这么做还行
Angr deflat脚本:https://umvfx1bvaw50.github.io/angrDeflat/#more
相对来讲angr现在用的会比较少了,更多是用在特定系统的执行分析上
d810:https://github.com/joydo/d810
直接逆向 现成工具
插桩、打印调用栈【逆向逻辑】
重点去分析输入参数 –> 数据流向、加密过程
br间接跳转
这里不做介绍,思路就是把正确的代码块顺序连接起来,把br x patch br realAddress
Android反调试 常规 root检测 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 public static boolean isRooted () { String[] rootFiles = { "/system/su" , "/system/xbin/su" , "/system/bin/su" , "/sbin/su" , "/system/sbin/su" , "/data/local/su" , "/data/local/xbin/su" , "/data/local/bin/su" , "/su/bin/su" }; for (String rootFile : rootFiles) { if (new File (rootFile).exists ()) { return true ; } } try { Process process = Runtime.getRuntime ().exec ("su" ); OutputStream outputStream = process.getOutputStream (); outputStream.write ("exit\n" .getBytes ()); outputStream.flush (); int exitCode = process.waitFor (); return exitCode == 0 ; } catch (IOException | InterruptedException e) { return false ; } }
文件检测 检测某一路径下是否存在调试文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 dir = opendir ("/data/local/tmp" ); pid = getpid (); v2 = pid; if ( dir ){ while ( 1 ){ v3 = readdir (dir); v4 = v3 == 0 ; filename = (const char *)(v3 + 19 ); if ( v4 ) break ; if ( !strncmp (filename, "android_server" , 0 xEu) ) kill (v2, 9 ); } pid = closedir (dir, "android_server" , 14 ); }
端口检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ("fail\n" ); return ; } while (fgets (buf,sizeof (buf),pfile)){ int ret=kill (pid,SIGKILL); } pclose (pfile);
进程名称检测 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 const int bufsize = 1024 ;char filename[bufsize]; char line[bufsize]; char name[bufsize]; char nameline[bufsize]; int pid = getpid ();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);
轮循检测
同样是对tracepid值的检测
缺点是如果tracepid不为0会一直消耗内存
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 void anti_debugger () { pthread_t tid; pthread_create (&tid,NULL ,&anti_debug_thread,NULL ); } void *anti_debugger_thread (void *data) { pid_t pid=getpid (); while (true ) { check_debugger (pid) } } 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 break ; } } fclose (fp); } return result; }
isDebuggerConnected() 1 2 3 4 5 6 7 8 9 10 protected void attachBaseContext (Context context) { super.attachBaseContext (context); this.mContext = context; AppInfo.APKPATH = context.getApplicationInfo ().sourceDir; AppInfo.DATAPATH = context.getApplicationInfo ().dataDir; if (!Debug.isDebuggerConnected ()) { loadLibrary (); A.a (AppInfo.PKGNAME, AppInfo.APPNAME, AppInfo.APKPATH, AppInfo.DATAPATH, Build.VERSION.SDK_INT, AppInfo.REPORT_CRASH); } }
Frida对抗 端口检测
绕过 :自定义端口启动
1 2 fs -l 0.0 .0 .0 :1234 adb forward tcp:1234 tcp:1234
进程列表/proc目录检测
绕过 :自然就可改frida-server名字绕过
D-BUS协议检测
frida使用D-BUS协议,因此可以向每个开放的端口发送 D-Bus 认证消息,哪个端口回复了 REJECT,哪个端口上就运行了 frida-server
绕过 :把检测函数hook掉就行
maps映射文件检测
frida注入后,目标进程 的maps文件中会有frida-agent[/proc/self/maps]
绕过 :备份一个正常的maps
文件,然后用frida去hook open
函数,在匹配到字符串maps
时将字符串重定向到我们备份的maps
文件
内存搜索
扫描frida特征:“LIBFRIDA”、“rpc”等
绕过 :把检测函数hook掉就行
扫描 task 目录
绕过 :把检测函数hook掉就行
Inline hook
父子进程保护
利用同一时刻一个进程只能被一个进程附加,自己创建一个子进程调试自己,从而阻止其他调试
ASM级别防护
因为无论是native还是java层只要是函数的都有可能被hook绕过
Xposed对抗 maps映射文件检测
检测proc/self/maps,寻找相关dex、jar、so文件【XposedBridge.jar】
反射读取
检测java方法变native 利用异常扫描堆栈 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 int __fastcall Java_com_example_dogpro_MainActivity_detectHookTool (_JNIEnv *a1) { const char *StringUTFChars; int v3; int v4; int ObjectClass; int ObjectArrayElement; int i; int ArrayLength; int v9; int v10; int v11; int MethodID; int Class; size_t n; size_t v16; char v17[24 ]; char v18[36 ]; Class = _JNIEnv::FindClass(a1, "java/lang/Throwable" ); MethodID = _JNIEnv::GetMethodID(a1, Class, "<init>" , "()V" ); v11 = _JNIEnv::NewObject(a1, Class, MethodID); v10 = _JNIEnv::GetMethodID(a1, Class, "getStackTrace" , "()[Ljava/lang/StackTraceElement;" ); v9 = _JNIEnv::CallObjectMethod(a1, v11, v10); ArrayLength = _JNIEnv::GetArrayLength(a1, v9); strcpy(v18, "de.robv.android.xposed.XposedBridge" ); strcpy(v17, "com.saurik.substrate" ); for ( i = 0 ; i < ArrayLength; ++i ) { ObjectArrayElement = _JNIEnv::GetObjectArrayElement(a1, v9, i); ObjectClass = _JNIEnv::GetObjectClass(a1, ObjectArrayElement); v4 = _JNIEnv::GetMethodID(a1, ObjectClass, "getClassName" , "()Ljava/lang/String;" ); v3 = _JNIEnv::CallObjectMethod(a1, ObjectArrayElement, v4); StringUTFChars = (const char *)_JNIEnv::GetStringUTFChars(a1, v3, 0 ); n = _strlen_chk(v18, 0x24u); if ( !strncmp(StringUTFChars, v18, n) ) { _android_log_print(6 , "lilac" , "%s" , StringUTFChars); _android_log_print(6 , "lilac" , byte_389E); } v16 = _strlen_chk(v17, 0x15u); if ( !strncmp(StringUTFChars, v17, v16) ) { _android_log_print(6 , "lilac" , "%s" , StringUTFChars); _android_log_print(6 , "lilac" , byte_38AE); } } return _stack_chk_guard; }
unidbg对抗 进程名检测
如果模拟执行时候没设置进程名的话会出现unidbg字符串,so里可以做检测
methodID
uname系统调用 /proc/cpuinfo检测 重打包对齐处理 all
生成签名
keytool -genkeypair -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-alias
签名
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA-1 -keystore my-release-key.jks my-app.apk my-key-alias
对齐(sdk build-tools)
zipalign -v 4 my-app.apk my-app-aligned.apk
原因
ZIP未对齐的 APK 文件可能会导致性能问题或安装失败
处理
参考文章: