首页
论坛
课程
招聘
[原创]Android APP漏洞之战(14)——Ollvm混淆与反混淆
2022-9-25 14:45 15523

[原创]Android APP漏洞之战(14)——Ollvm混淆与反混淆

2022-9-25 14:45
15523

Android APP漏洞之战(14)——Ollvm混淆与反混淆

目录

前言

本文为Android漏洞之战技巧篇的最后一个篇幅,前面我们依次讲了Hook、脱壳、反调、过签等,本文主要初步讲解Ollvm混淆与反混淆,本文收集整理了网络上开源的实验样本和脚本,实验脚本上传至github,实验样本上传至知识星球:安全后厨。

 

本文收集整理了网上已有大佬们使用的一些开源脚本和样例,帮助初学者初步了解和学习Ollvm混淆和反混淆,脚本的原理和具体讲解参考文章链接或知识星球中的内容,本文考虑篇幅不做过多讲解。本文的结构主要分为:

 

第一节介绍Ollvm混淆

 

第二节介绍Ollvm反混淆的常见方法

 

第三节进行Ollvm反混淆实操

1.Ollvm

1.1 ollvm简介

LLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度。github上地址是https://github.com/obfuscator-llvm/obfuscator ,只不过仅更新到llvm的4.0,2017年开始就没在更新。

 

想了解OLLVM,首先需要明白LLVM是什么。简而言之,LLVM是模块化和可重用的编译器和工具链技术的集合。具体的过程如下图所示:

 

 

即需要完成如下的步骤:

1
源代码(c/c++)经过clang--> 中间代码(经过一系列的优化,优化用的是Pass) --> 机器码

llvm正是因为将不同的语言转换为中间语言IR,然后通过编写一系列的Pass进行优化,最后在针对不同平台进行转换,所以可以支持不同平台

 

 

我们前面提到了Pass,而llvm通过编写Pass将中间语言IR进行优化,而Ollvm则是通过编写更加复杂的Pass,将代码复杂化,这样就达到了混淆的目的。

1.2 ollvm分类

前面我们已经知晓了ollvm的原理,那么这里我们理解最原始的ollvm的分类就变得更加容易。官方的Ollvm更新到llvm的4.0,目前主要分为:指令替换、虚假控制流、控制流平坦化。当然后面越来越多的人编写了更加复杂的Pass,这里我们就不具体深究了。

 

image-20220921201852889

 

下面我们简单的介绍每个Pass的实现原理

1.2.1 指令替换(Sub)

指令替换,将一条运算指令,替换为多条等价的运算指令。例如:y=x+1变为y=x+1-1

1.2.2 虚假控制流(bcf)

虚假控制流混淆主要通过加入包含不透明谓词的条件跳转和不可达的基本块,来干扰IDA的控制流分析和F5反汇编

 

image-20220921202830658

 

不透明谓词:在跳转前就已经确定的不等式,但是IDA无法分析,例如y > 10 || x * (x + 1) % 2 == 0这个不等式,大家都知道x * (x + 1) % 2 == 0这个式子是恒成立的,因此刚才这个不等式是恒成立的,当然这是我们知道,而IDA就不知道了,IDA不确定x,y的值,因此无法识别出来,因此就可以增加虚假控制流。

 

不透明谓词是研究反虚假控制流的重点,因此不透明谓词除了永真/永假型、还有可真可假型,而现在的研究都在此基础上进行了深入研究,所以不透明谓词的研究也成为难点。

 

不可达基本块,是指在虚假控制流中一些基本块是永远不可能执行的,这也是进一步去增加代码的复杂程度

1.2.3 控制流平坦化(Fla)

控制流平坦化,主要通过一个主分发器来控制程序基本块的执行流程。该方法将所有基本代码放到控制流最底部,然后删除原理基本块之间跳转关系,添加次分发器来控制分发逻辑,然后过新的复杂分发逻辑还原原来程序块之间的逻辑关系。

 

image-20220921210110215

 

我们在阅读源码后,会发现逻辑十分清晰,Ollvm的基本流程:

  • ​ 添加一个随机数种子 blockID
  • ​ 保存所有的基本块
  • ​ 将代码中含有switch改为if
  • ​ 删除第一个基本块,第一个需要特殊处理
  • ​ 识别main中的if,并且删除跳转指令
  • ​ 插入一个switch指令
  • ​ 第一个块跳转到loopEntry块
  • ​ 把所有的block保存到switch语句
  • 重新计算switch变量的值
  • ​ 处理不是条件跳转 直接删除jump 跳转到loopEnd 进行下一轮循环
  • ​ 处理条件跳转 对真分支和假分支进行相应处理 真则选择真的ID
1
先将里面含switch的改为if-else,再将所有的if-else变为Switch的结果,所以多次进行控制流平坦化就会变得越来越复杂

1.2.4 字符串加密

字符串加密的原理很简单,编写一个pass将其中的字符串信息使用一些加密算法进行加密,然后特定的时间进行还原。一般含有字符串混淆、函数名混淆、不在init_array解密等

2.Ollvm反混淆

ollvm反混淆主要思路基本上就是静态分析和动态分析,基本的流程大体都是:

1
2
3
4
5
1)找到所有基本块(特征匹配或机器学习) 控制流平坦化中区分虚假块和真实块是难点
2)动静态分析找到真实块的联系
    静态分析:符号执行/反编译器提供的IL的API
    动态分析:模拟指令/IDA trace            
3)编写相应的patch脚本,进行还原原程序

2.1 指令替换

指令替换一般可以使用llvm的pass进行优化,或者使用Miasm框架进行匹配,然后优化处理即可,由于指令替换一般不会影响程序整体逻辑,这里我们不进行深究。

2.2 反字符串加密

字符串加密的的常规解决方式:

 

(1)特征搜索

 

一般在so中可以直接搜索datadiv_decode,一般很多编写解密函数进行操作是这个函数,针对这种情况,一般可以通过frida hook就可以拿到解密后的值,然后进行patch

 

(2)init_array中解密

 

字符串解密操作在init_arrray中进行,一般可以通过模拟执行init_array,然后将解密后的字符串全部保存下来

 

(3)jni_onload解密

 

在jni_onload函数中进行解密操作,这时候就要进行inlinehook拿到解密后寄存器的值,也可以进行hook,也可以使用unicorn进行操作

2.3 反虚假控制流

虚假控制流去除的思路一般为除去不可达块和不透明谓词。但是难点在于不透明谓词,现在不透明谓词的研究不断发展,有永真/永假型不透明谓词,也有可真可假型不透明谓词。当然针对复杂的虚假控制流,在反混淆过程中还需要考虑死循环等问题

 

不透明谓词:

1
2
永真/假型:插入的后续基本块中必有一个不被执行
可真可假型:插入的两个后继基本块的语义应相同

针对简单的控制流混淆,去不透明谓词的思想主要是:

1
2
3
1)不直接处理不透明谓词,通过让不透明谓词的变量地址可读,则IDA便可以优化
2)直接将不透明谓词赋值为0或者将不透明谓词中变量x,y赋值为0
3)编译器优化去干掉不透明谓词

不可达块:

 

不可达块是指控制流永远无法到达的基本块,一般我们可以使用符号执行或模拟执行来除去不可达基本块

2.4 反控制流平坦化

一般通用的反控制流平坦化思路:

1
2
3
4
1)先保存所有的基本块
2)区分真实块和分发器(虚假块) 一般通过规则匹配来做,但是并无法使用所有情况 (难点)
3)连接真实块的顺序  一般静态可以通过IDA trace然后编写IDApython脚本,动态可以通过符号执行、模拟执行
4)编写patch修复  对目标函数进行修复、恢复原始逻辑

(1)保存所有的基本块

 

控制流平坦化本质逻辑是把原始的基本块都碎片化,再通过switch-case语句,对函数的执行流进行重建。那么反混淆的时候,可以尝试根据主分发器将这些执行链给一条条的拆解出来,具体划分为三类:

1
2
3
入口链:原始函数代码的入口逻辑链,为序言到主分发器的执行路径
循环链:入口及出口均为主分发器的流程链,对应混淆过程中的循环体
Return链:指代入口为主分发器,出口为目标函数结束地址的流程链

(2)区分真实块和分发器

 

其中入口链没有分发器这样的控制块,所以代码全是真实指令。所以主要是找出循环链及Return链中的真实块,一般有几种思路:

1
2
3
1)通过特征匹配来找真实块
2)真实块的入口跳转地址必须为绝对的比较指令,如beq、bne,首次匹配到这种绝对的跳转指令,就能定位对应流程链中的真实入口地址,然后识别真是快
3)凡是有内存操作的以及有bl/blx函数调用的都是真实块,其中有一些指向主分发器的虚拟块也会包含内存操作

主分发器的确定:直接遍历目标函数下的所有基本块,并计算每一个Block的引用次数,数量最多的那个就是主分发器

 

(3)连接真实块的顺序

 

通过判断movwne/movtne r1指令的地址是否大于mov r1,如果大于就说明此真实块会有两条路径去指向两个基本块。对于有两条路径的真实块就需要寻找两次分别去寻找两条路径下对应的真实块,而对于没有两条路径的真实块就直接寻找一次路径就ok了

 

一般我们使用模拟执行能解决该问题,但遇到上面情况并需要相应的手动修改

 

(4)编写patch

1
2
1.打patch,即通过jump指令或者一些条件跳转指令试图将它们重新连接起来
2.提取出所有真实块的指令,并根据它们之间的关系,计算相对偏移,据此对函数进行重构

前者简单,但是适用性不强,后者复杂,但是工作量大。这里我们主要介绍第一种patch

1
2
3
(1)把无用块都改成nop指令
(2)针对没有产生分支的真实块把最后一条指令改成jmp指令跳转到下一真实块
(3)产生分支的真实块把CMOV指令改成相应的条件跳转指令跳向符合条件的分支,例如CMOVZ 改成JZ ,再在这条之后添加JMP 指令跳向另一分支

3.Ollvm反混淆实操

这里收集了一些网上开源的脚本和样例,便于大家学习

3.1反字符串加密

首先我们打开一个字符串加密的样本

 

image-20220922172044918

 

我们可以发现导出函数中有datadiv_decode字段,我们初步判定就是在该函数中完成对字符串的加密

 

image-20220922172239875

 

image-20220922172318658

 

很明显发现这些函数进行了字符串加密

 

我们在查看加密函数datadiv_decode的交叉引用,可以很明显的发现在init_array中完成了对字符串的解密

 

image-20220922172516725

 

下面我们使用模拟执行的方法来去除ollvm 字符串加密

3.1.1 AndroidNativeEmu

AndroidNativeEmu是基于Unicorn的框架,主要解决Unicorn不支持第三方库、JNI_Onload调用等问题

 

反混淆思路:

1
我们分析得知,字符串解密一定在init_array运行完结束,将字符串读取到内存,因此我们运行init_array,然后将内存中解密的字符串保存下来即可

运行环境:

1
2
1)不使用AndroidNativeEmu项目,可以直接安装 pip install androidemu
2)也可以去原仓库进行下载

这里我采用第一种环境,解密脚本:

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
import logging
import sys
 
from unicorn import *
import struct
 
from androidemu.emulator import Emulator
 
logging.basicConfig( # 设置日志
    stream=sys.stdout,
    level=logging.DEBUG,
    format='%(asctime)s %(levelname)7s %(name)34s | %(message)s'
)
logger = logging.getLogger(__name__)
emulator = Emulator(vfp_inst_set=True, vfs_root='vfs') # 创建模拟器
str_datas = {}
def hook_mem_write(uc,type,address,size,value,userdata): # 当发生写内存操作时进行Hook
    try:
        curdata = struct.pack("I", value)[:size]
        str_datas[address] = curdata
    except:
        print(size)
 
 
emulator.mu.hook_add(UC_HOOK_MEM_WRITE,hook_mem_write) # 添加Hook,需在加载so前
 
#emulator.load_library('libc.so',do_init=False) # 补充加载libc.so,不然可能会有问题
 
lib_module = emulator.load_library('obf.so',do_init=True)
 
 
base_addr = lib_module.base
sodata = open('obf.so', 'rb').read()
for address,value in str_datas.items():
    if base_addr < address < base_addr + lib_module.size: # 判断是否是我们需要的so文件内存空间
        offset = address - base_addr -0x1000 # 加载的基址还多了个0x1000
        print('address:0x%x data:%s offset:0x%x ' % (address, value, offset+0x1000))
        sodata = sodata[:(offset)] + value + sodata[offset+len(value):] # 将解密后的字符串写回
#print("sodata"+str(sodata[:0x3810]))
 
with open('obf_new.so','wb') as file:
    file.write(sodata)
file.close()
1
2
3
4
上面脚本中:
obf.so为目标样本
如果在框架中运行,需要导入libc.so
最重要需要注意由于IDA偏移和绝对地址不对 需要offset-0x1000

image-20220922173658071

 

运行脚本,得到结果

 

image-20220922173829174

 

修复前后的so对比:

 

原so:

 

image-20220922173934780

 

修复so:

 

image-20220922174035699

3.1.2 IDA插件

上面我们使用Unicorn进行了模拟调用,很显然我们还可以制作成相应的IDApython脚本,通过插件来进行动态的查看

 

参考链接:使用unicorn对ollvm字符串进行解密

 

环境支持:

1
2
3
4
pip install unicorn  IDApython安装unicorn
Ida 7.x
python3
uEmu.py

脚本:uEmu.py 代码太长 https://github.com/alexhude/uEmu(或者微信公众号回复:Ollvm,所有脚本自取)

 

脚本导入操作:复制uEmu.py到IDA_Pro_7.5\plugins\下,重启ida

 

首先我们找到加密函数,设置起始和结束断点:

 

image-20220922175139399

 

image-20220922175209878

 

image-20220922175235173

 

然后我们右键,此时出现uEmu

 

image-20220922175347552

1
2
3
4
5
6
7
8
9
10
11
12
start命令:通过映射所有段并设置 Unicorn 来初始化模拟器
run命令: 模拟指令,直到到达断点或发生错误
Step模拟一条或 N 条指令(按住ALT/OPTION指定一个数字)
stop:中断仿真
Reset:重置仿真引擎并取消映射所有内存区域
Jump to Pc: 只是跳转到当前 PC
Change CPU context: 可以手动或通过 JSON 文件更新 CPU 上下文(见下文)
Show Controls  显示带有 Start/Run/Step/Stop 按钮的窗口
Show CPU Context 显示带有可用寄存器的窗口
Show CPU Extended Context 显示带有扩展寄存器 (FP/SIMD) 的窗口
Show Stack 显示带有当前 Stack 的窗口
Show Memory Range 允许显示特定的内存区域

我们点击start启动初始化模拟器,并不断确定

 

image-20220922175704258

 

image-20220922175725288

 

image-20220922175805710

 

我们可以使用快捷键ctrl+shfit+alt+s进行快速单步步过

 

image-20220922180009607

 

可以选择一次步过多少条指令

 

image-20220922184945143

 

此时我们查看0x4004处的字符串,右键uEMu-->Show Memory Range

 

image-20220922185053241

 

image-20220922185122178

 

可以看见此时的还是加密的QUR,还未解密,我们也可以在so中看到

 

image-20220922185226245

 

我们继续调试,按照我们上面的结果,这里应该为jni.go into _ini,这里我们可以直接下断点,然后run过去

 

image-20220922191223988

 

image-20220922190331570

 

可以发现很好的进行了解密,其实原理和上面一样

3.2 反虚假控制流

3.2.1 符号执行

环境要求:

1
安装angr pip install angr==8.19.4.5   angr版本为8.19.4.5

我们打开一个虚假控制流混淆后的样本

 

image-20220922200516763

 

很明显我们在这里看到了一些不透明谓词,这是一个虚假控制流混淆处理后的代码

1
python debogus.py -f target_arm_bogus --addr 0x83B4

image-20220922200812611

 

image-20220922200840974

 

这里我们打开修复后的样本

 

image-20220922201047300

 

可以看到一些不可达块填充为nop

 

image-20220922201128282

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
# 去除虚假控制流 idapython脚本
import ida_xref
import ida_idaapi
from ida_bytes import get_bytes, patch_bytes
 
# 将 mov 寄存器, 不透明谓词 修改为 mov 寄存器, 0
def do_patch(ea):
    if get_bytes(ea, 1) == b"\x8B": # mov eax-edi, dword
        reg = (ord(get_bytes(ea + 1, 1)) & 0b00111000) >> 3
        patch_bytes(ea, (0xB8 + reg).to_bytes(1,'little') + b'\x00\x00\x00\x00\x90')
    else:
        print('error')
 
# 不透明谓词在.bss段的范围
start = 0x0011028
end = 0x0011030
 
for addr in range(start,end,4):
    ref = ida_xref.get_first_dref_to(addr)
    print(hex(addr).center(20,'-'))
    # 获取所有交叉引用
    while(ref != ida_idaapi.BADADDR):
        do_patch(ref)
        print('patch at ' + hex(ref))
        ref = ida_xref.get_next_dref_to(addr, ref)
    print('-' * 20)

3.2.2 模拟执行

使用基于Unicorn的框架可以去虚假控制流,思路如下:

1
2
1)通过Unicorn模拟执行目标函数,并记录可达块和不可达块的地址,将可达块的地址全部报错
2)编写IDApython脚本,将不可达块地方全部填充为nop,就可以除去虚假控制流

参考文章:http://missking.cc/2021/05/14/ollvm3/

3.3 反控制流平坦化

3.3.1 符号执行

环境:

1
安装angr pip install angr==8.19.4.5   angr版本为8.19.4.5

我们先打开一个控制流平坦化的样本

 

image-20220922213932982

 

这是一个标准的控制流平坦化的样本,我们可以看见序言、主分发器、次分发器、结束块等

 

我们通过符号执行来除去:

1
python deflat.py -f samples/bin/check_passwd_arm_flat --addr 0x83B0(目标函数地址)

image-20220922214629123

 

我们将修复后的样本打开:

 

原样本:

 

image-20220922214902748

 

去除混淆后的样本:

 

image-20220922214827404

3.3.2 模拟执行

环境要求:

 

image-20220922220425663

 

我们打开另外一个样本

 

image-20220922215820915

 

定位到函数的起始和终止

 

image-20220922215854085

 

然后启动脚本

 

image-20220922215709565

 

image-20220922215934225

 

打开修复后的样本

 

image-20220922220136390

 

image-20220922220154180

4.总结

本文初步的简单介绍了当前Ollvm反混淆中的一些方法并进行了复现,样本的实例都来自于当前研究该领域的各个大佬开源脚本,这里就不一一提及了,感兴趣朋友可以查看参考文献。

 

本实验的实验样本全部上传至知识星球:安全后厨

 

本实验的全部脚本全部上传至github:WindXaa或微信公众号:安全后厨

5.参考文章

1
2
3
https://bbs.pediy.com/thread-268108.htm#msg_header_h1_0
http://missking.cc/2020/11/03/unicorn2/
https://bbs.pediy.com/thread-266005.htm

虚假控制流:

1
2
3
4
https://bbs.pediy.com/thread-257213.htm
https://github.com/llxiaoyuan/llux
https://bbs.pediy.com/thread-257213.htm
https://github.com/cq674350529/deflat

控制流平坦化:

1
2
3
4
https://www.cnblogs.com/revercc/p/16339476.html
https://www.wireghost.cn/2020/08/28/OLLVM%E9%80%9A%E7%94%A8%E5%8F%8D%E5%B9%B3%E5%9D%A6%E5%8C%96%E7%A0%94%E7%A9%B6/
https://security.tencent.com/index.php/blog/msg/112
https://bbs.pediy.com/thread-271557.htm

看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~

最后于 2022-10-4 21:52 被随风而行aa编辑 ,原因:
收藏
点赞12
打赏
分享
最新回复 (10)
雪    币: 1622
活跃值: 活跃值 (1309)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
huangjw 活跃值 2022-9-26 14:29
2
0
干货啊,写的太好了,学习了谢谢
雪    币: 6127
活跃值: 活跃值 (17265)
能力值: ( LV12,RANK:530 )
在线值:
发帖
回帖
粉丝
随风而行aa 活跃值 10 2022-9-26 18:22
3
0
huangjw 干货啊,写的太好了,学习了谢谢
感谢支持
雪    币: 798
活跃值: 活跃值 (790)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
yuhan梓 活跃值 2022-9-26 21:01
4
0
脚本上传了吗,怎么没看到
雪    币: 798
活跃值: 活跃值 (790)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
yuhan梓 活跃值 2022-9-26 21:07
5
0
已在公众号找到了
雪    币: 6127
活跃值: 活跃值 (17265)
能力值: ( LV12,RANK:530 )
在线值:
发帖
回帖
粉丝
随风而行aa 活跃值 10 2022-9-26 21:07
6
0
yuhan梓 脚本上传了吗,怎么没看到[em_5]
脚本上传github了哦 https://github.com/WindXaa/Android-Vulnerability-Mining
雪    币: 297
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_yqjtfroq 活跃值 2022-9-27 16:46
7
0
终于等到大佬更新了。安卓逆向就是5月开始,跟着您的文章一路学习过来的,写得很好
雪    币: 6127
活跃值: 活跃值 (17265)
能力值: ( LV12,RANK:530 )
在线值:
发帖
回帖
粉丝
随风而行aa 活跃值 10 2022-9-27 16:58
8
0
mb_yqjtfroq 终于等到大佬更新了。安卓逆向就是5月开始,跟着您的文章一路学习过来的,写得很好
感谢支持
雪    币: 821
活跃值: 活跃值 (3890)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
suuuuu 活跃值 2022-9-28 17:24
9
0
雪    币: 6127
活跃值: 活跃值 (17265)
能力值: ( LV12,RANK:530 )
在线值:
发帖
回帖
粉丝
随风而行aa 活跃值 10 2022-9-28 19:52
10
0
suuuuu [em_63]
感谢支持
雪    币: 221
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_qvxqjytp 活跃值 2022-9-29 17:00
11
0
大佬,可以提供下样本吗?
游客
登录 | 注册 方可回帖
返回