0%

加固与混淆

  • Android加固和混淆[加固、反调试、llvm]

加固

  • 先来一张android classloader的图

img

一:整体加固

特点

在原有apk的基础上套一层

呈现出:

–> 壳BootClassloader加载系统核心库

–> 壳PathClassLoader加载dex

–> 壳调用Application的attachBaseContext() 对原dex进行动态加载解密

–> 壳调用Application的onCreate() 对dex进行动态加载解密

–> 主MainActivity attachBaseContext()

–> 主MainActivity onCreate()

的效果

Notice

因为实现动态加载dex文件需要用自定义的dexClassLoader,因此要进行类加载器的修正,方法如下:

类加载器替换

替换系统组件类加载器为我们的DexClassLoader,同时设置DexClassLoader的parent为系统组件加载器

类加载器插入

打破原有的双亲委派关系,在系统组件类加载器PathClassLoader和BootClassLoader的中间插入我们自己的DexClassLoader

img

脱壳

思路

  1. 内存加载思路很简单,拿到dex释放在内存中的起始地址和大小,直接dump下来就行
  2. 如果是文件加载的话重点就是要找到加载dex的目录,定位解密文件
  3. 总而言之要关注dex的加载和解密流程,包括整个流程中产出的odex、vdex等编译优化文件

工具

如果现有的工具不行的话就仿照上面的思路,用frida、xposed hook,ida动态调试利用脱壳点dump

二:函数抽取【不落地加载】

特点

落地加载的缺点就是会将dex文件释放到本地,这个过程是会造成dex文件泄露的

那么不落地加载就出现了,我们想要直接将dex文件加载到内存中

如何实现呢?

  • 上图有提到Android 8.0+提供了InMemoryDexClassLoader,通过这个就可以实现不落地加载

  • 至于Android8.0之前有时间再说吧

  • https://github.com/Frezrik/Jiagu

脱壳

函数抽取通常会在类加载和函数执行前自解密,或者在函数执行过程中动态自解密

因此重点关注被抽取函数的执行流程,定位抽取函数解密时机

三: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
# C to IR
clang -emit-llvm -S hello_clang.c -o hello_clang.ll

## exec
lli hello_clang.ll

# IR to bcCode
llvm-as hello_clang.ll -o hello_clang.bc

# bcCode to asm
llc hello_clang.bc -o hello_clang.s

# bcCode to IR
llvm-dis hello_clang.bc –o hello_clang.ll

# bcCode link
llvm-link test1.bc test2.bc –o output.bc

# asm to exec
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
# 这个是ida python38环境
import ida_bytes
import idaapi
import idc


def 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):
# patch 没有执行的指令
if idc.get_color(j,idc.CIC_ITEM) != 0xFF00:
patch_nop(j,'arm64')

控制流平坦化

先把解决方案放在最前面:

  • 对于对抗来讲,一句话概括就是去除没用的代码块,用实际的执行流把真实代码块的顺序串起来

控制流平坦化本质上是一种代码块级别的混淆,它会加入分发器等等元素,打乱原本直观的代码块连接逻辑,原始的控制流平坦化长这样

img

这个图来自文章: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() {
// 检查常见的root文件
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;
}
}

// 检查是否可以执行su命令
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");       //check dir
pid = getpid(); //获取pid
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", 0xEu) )
kill(v2, 9); //kill debug process
}
pid = closedir(dir, "android_server", 14);
}

端口检测

  • frida 27042
  • ida 23946
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"; //check tcp
char* strNetstat="netstat |grep :23946"; //check the state of port
//calls popen(), executes a shell and runs commands
pfile=popen(strCatTcp,"r");
int pid=getpid();
if(NULL==pfile){
printf("fail\n");
return;
}
//kill debug process
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();
//先读取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);

轮循检测

  • 同样是对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) //如果tracepid不为0就死循环阻止程序继续执行
{
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()) { //isDebuggerConnected()
loadLibrary();
A.a(AppInfo.PKGNAME, AppInfo.APPNAME, AppInfo.APKPATH, AppInfo.DATAPATH, Build.VERSION.SDK_INT, AppInfo.REPORT_CRASH);
}
}

Frida对抗

端口检测

  • 默认端口27042

绕过:自定义端口启动

1
2
fs -l 0.0.0.0:1234
adb forward tcp:1234 tcp:1234

进程列表/proc目录检测

  • frida-server、fs字符串

绕过:自然就可改frida-server名字绕过

D-BUS协议检测

  • frida使用D-BUS协议,因此可以向每个开放的端口发送 D-Bus 认证消息,哪个端口回复了 REJECT,哪个端口上就运行了 frida-server

绕过:把检测函数hook掉就行

maps映射文件检测

  • frida注入后,目标进程的maps文件中会有frida-agent[/proc/self/maps]

frida

绕过:备份一个正常的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】

反射读取

  • 反射读取XposedHelper关键字

检测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; // [sp+28h] [bp-A0h]
int v3; // [sp+2Ch] [bp-9Ch]
int v4; // [sp+30h] [bp-98h]
int ObjectClass; // [sp+34h] [bp-94h]
int ObjectArrayElement; // [sp+38h] [bp-90h]
int i; // [sp+3Ch] [bp-8Ch]
int ArrayLength; // [sp+40h] [bp-88h]
int v9; // [sp+44h] [bp-84h]
int v10; // [sp+48h] [bp-80h]
int v11; // [sp+4Ch] [bp-7Ch]
int MethodID; // [sp+50h] [bp-78h]
int Class; // [sp+54h] [bp-74h]
size_t n; // [sp+6Ch] [bp-5Ch]
size_t v16; // [sp+7Ch] [bp-4Ch]
char v17[24]; // [sp+80h] [bp-48h] BYREF
char v18[36]; // [sp+98h] [bp-30h] BYREF

//反射找到异常处理机制核心类java/lang/Throwable
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);

//检测xposed、Substrate框架
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);

//cmp
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

  • 方法签名是字符串的hashCode

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 文件可能会导致性能问题或安装失败

处理

  • 使用 Android 提供的 zipalign 检查 APK 文件的对齐状态

    zipalign -c 4 your_app.apk

  • 对齐APK

    zipalign -v 4 input.apk output.apk

    -v: 启用详细输出

    4: 以 4 字节边界对齐

参考文章: