本文为Android漏洞之战技巧篇的最后一个篇幅,前面我们依次讲了Hook、脱壳、反调、过签等,本文主要初步讲解Ollvm混淆与反混淆,本文收集整理了网络上开源的实验样本和脚本,实验脚本上传至github,实验样本上传至知识星球:安全后厨。
本文收集整理了网上已有大佬们使用的一些开源脚本和样例,帮助初学者初步了解和学习Ollvm混淆和反混淆,脚本的原理和具体讲解参考文章链接或知识星球中的内容,本文考虑篇幅不做过多讲解。本文的结构主要分为:
第一节介绍Ollvm混淆
第二节介绍Ollvm反混淆的常见方法
第三节进行Ollvm反混淆实操
LLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度。github上地址是https://github.com/obfuscator-llvm/obfuscator ,只不过仅更新到llvm的4.0,2017年开始就没在更新。
想了解OLLVM,首先需要明白LLVM是什么。简而言之,LLVM是模块化和可重用的编译器和工具链技术的集合。具体的过程如下图所示:
即需要完成如下的步骤:
llvm正是因为将不同的语言转换为中间语言IR,然后通过编写一系列的Pass进行优化,最后在针对不同平台进行转换,所以可以支持不同平台
我们前面提到了Pass,而llvm通过编写Pass将中间语言IR进行优化,而Ollvm则是通过编写更加复杂的Pass,将代码复杂化,这样就达到了混淆的目的。
前面我们已经知晓了ollvm的原理,那么这里我们理解最原始的ollvm的分类就变得更加容易。官方的Ollvm更新到llvm的4.0,目前主要分为:指令替换、虚假控制流、控制流平坦化。当然后面越来越多的人编写了更加复杂的Pass,这里我们就不具体深究了。
下面我们简单的介绍每个Pass的实现原理
指令替换,将一条运算指令,替换为多条等价的运算指令。例如:y=x+1
变为y=x+1-1
虚假控制流混淆主要通过加入包含不透明谓词的条件跳转和不可达的基本块,来干扰IDA的控制流分析和F5反汇编
不透明谓词:在跳转前就已经确定的不等式,但是IDA无法分析,例如y > 10 || x * (x + 1) % 2 == 0
这个不等式,大家都知道x * (x + 1) % 2 == 0
这个式子是恒成立的,因此刚才这个不等式是恒成立的,当然这是我们知道,而IDA就不知道了,IDA不确定x,y的值,因此无法识别出来,因此就可以增加虚假控制流。
不透明谓词是研究反虚假控制流的重点,因此不透明谓词除了永真/永假型、还有可真可假型,而现在的研究都在此基础上进行了深入研究,所以不透明谓词的研究也成为难点。
不可达基本块,是指在虚假控制流中一些基本块是永远不可能执行的,这也是进一步去增加代码的复杂程度
控制流平坦化,主要通过一个主分发器来控制程序基本块的执行流程。该方法将所有基本代码放到控制流最底部,然后删除原理基本块之间跳转关系,添加次分发器来控制分发逻辑,然后过新的复杂分发逻辑还原原来程序块之间的逻辑关系。
我们在阅读源码后,会发现逻辑十分清晰,Ollvm的基本流程:
字符串加密的原理很简单,编写一个pass将其中的字符串信息使用一些加密算法进行加密,然后特定的时间进行还原。一般含有字符串混淆、函数名混淆、不在init_array解密等
ollvm反混淆主要思路基本上就是静态分析和动态分析,基本的流程大体都是:
指令替换一般可以使用llvm的pass进行优化,或者使用Miasm框架进行匹配,然后优化处理即可,由于指令替换一般不会影响程序整体逻辑,这里我们不进行深究。
字符串加密的的常规解决方式:
(1)特征搜索
一般在so中可以直接搜索datadiv_decode
,一般很多编写解密函数进行操作是这个函数,针对这种情况,一般可以通过frida hook就可以拿到解密后的值,然后进行patch
(2)init_array中解密
字符串解密操作在init_arrray中进行,一般可以通过模拟执行init_array,然后将解密后的字符串全部保存下来
(3)jni_onload解密
在jni_onload函数中进行解密操作,这时候就要进行inlinehook拿到解密后寄存器的值,也可以进行hook,也可以使用unicorn进行操作
虚假控制流去除的思路一般为除去不可达块和不透明谓词。但是难点在于不透明谓词,现在不透明谓词的研究不断发展,有永真/永假型不透明谓词,也有可真可假型不透明谓词。当然针对复杂的虚假控制流,在反混淆过程中还需要考虑死循环等问题
不透明谓词:
针对简单的控制流混淆,去不透明谓词的思想主要是:
不可达块:
不可达块是指控制流永远无法到达的基本块,一般我们可以使用符号执行或模拟执行来除去不可达基本块
一般通用的反控制流平坦化思路:
(1)保存所有的基本块
控制流平坦化本质逻辑是把原始的基本块都碎片化,再通过switch-case语句,对函数的执行流进行重建。那么反混淆的时候,可以尝试根据主分发器将这些执行链给一条条的拆解出来,具体划分为三类:
(2)区分真实块和分发器
其中入口链没有分发器这样的控制块,所以代码全是真实指令。所以主要是找出循环链及Return链中的真实块,一般有几种思路:
主分发器的确定:直接遍历目标函数下的所有基本块,并计算每一个Block的引用次数,数量最多的那个就是主分发器
(3)连接真实块的顺序
通过判断movwne/movtne r1指令的地址是否大于mov r1,如果大于就说明此真实块会有两条路径去指向两个基本块。对于有两条路径的真实块就需要寻找两次分别去寻找两条路径下对应的真实块,而对于没有两条路径的真实块就直接寻找一次路径就ok了
一般我们使用模拟执行能解决该问题,但遇到上面情况并需要相应的手动修改
(4)编写patch
前者简单,但是适用性不强,后者复杂,但是工作量大。这里我们主要介绍第一种patch
这里收集了一些网上开源的脚本和样例,便于大家学习
首先我们打开一个字符串加密的样本
我们可以发现导出函数中有datadiv_decode
字段,我们初步判定就是在该函数中完成对字符串的加密
很明显发现这些函数进行了字符串加密
我们在查看加密函数datadiv_decode
的交叉引用,可以很明显的发现在init_array中完成了对字符串的解密
下面我们使用模拟执行的方法来去除ollvm 字符串加密
AndroidNativeEmu是基于Unicorn的框架,主要解决Unicorn不支持第三方库、JNI_Onload调用等问题
反混淆思路:
运行环境:
这里我采用第一种环境,解密脚本:
运行脚本,得到结果
修复前后的so对比:
原so:
修复so:
上面我们使用Unicorn进行了模拟调用,很显然我们还可以制作成相应的IDApython脚本,通过插件来进行动态的查看
参考链接:使用unicorn对ollvm字符串进行解密
环境支持:
脚本:uEmu.py 代码太长 https://github.com/alexhude/uEmu(或者微信公众号回复:Ollvm,所有脚本自取)
脚本导入操作:复制uEmu.py到IDA_Pro_7.5\plugins\下,重启ida
首先我们找到加密函数,设置起始和结束断点:
然后我们右键,此时出现uEmu
我们点击start启动初始化模拟器,并不断确定
我们可以使用快捷键ctrl+shfit+alt+s
进行快速单步步过
可以选择一次步过多少条指令
此时我们查看0x4004处的字符串,右键uEMu-->Show Memory Range
可以看见此时的还是加密的QUR,还未解密,我们也可以在so中看到
我们继续调试,按照我们上面的结果,这里应该为jni.go into _ini,这里我们可以直接下断点,然后run过去
可以发现很好的进行了解密,其实原理和上面一样
环境要求:
我们打开一个虚假控制流混淆后的样本
很明显我们在这里看到了一些不透明谓词,这是一个虚假控制流混淆处理后的代码
这里我们打开修复后的样本
可以看到一些不可达块填充为nop
使用基于Unicorn的框架可以去虚假控制流,思路如下:
参考文章:http://missking.cc/2021/05/14/ollvm3/
环境:
我们先打开一个控制流平坦化的样本
这是一个标准的控制流平坦化的样本,我们可以看见序言、主分发器、次分发器、结束块等
我们通过符号执行来除去:
我们将修复后的样本打开:
原样本:
去除混淆后的样本:
环境要求:
我们打开另外一个样本
定位到函数的起始和终止
然后启动脚本
打开修复后的样本
本文初步的简单介绍了当前Ollvm反混淆中的一些方法并进行了复现,样本的实例都来自于当前研究该领域的各个大佬开源脚本,这里就不一一提及了,感兴趣朋友可以查看参考文献。
本实验的实验样本全部上传至知识星球:安全后厨
本实验的全部脚本全部上传至github:WindXaa或微信公众号:安全后厨
虚假控制流:
控制流平坦化:
源代码(c
/
c
+
+
)经过clang
-
-
> 中间代码(经过一系列的优化,优化用的是Pass)
-
-
> 机器码
源代码(c
/
c
+
+
)经过clang
-
-
> 中间代码(经过一系列的优化,优化用的是Pass)
-
-
> 机器码
先将里面含switch的改为
if
-
else
,再将所有的
if
-
else
变为Switch的结果,所以多次进行控制流平坦化就会变得越来越复杂
先将里面含switch的改为
if
-
else
,再将所有的
if
-
else
变为Switch的结果,所以多次进行控制流平坦化就会变得越来越复杂
(
1
)找到所有基本块(特征匹配或机器学习) 控制流平坦化中区分虚假块和真实块是难点
(
2
)动静态分析找到真实块的联系
静态分析:符号执行
/
反编译器提供的IL的API
动态分析:模拟指令
/
IDA trace
(
3
)编写相应的patch脚本,进行还原原程序
(
1
)找到所有基本块(特征匹配或机器学习) 控制流平坦化中区分虚假块和真实块是难点
(
2
)动静态分析找到真实块的联系
静态分析:符号执行
/
反编译器提供的IL的API
动态分析:模拟指令
/
IDA trace
(
3
)编写相应的patch脚本,进行还原原程序
永真
/
假型:插入的后续基本块中必有一个不被执行
可真可假型:插入的两个后继基本块的语义应相同
永真
/
假型:插入的后续基本块中必有一个不被执行
可真可假型:插入的两个后继基本块的语义应相同
(
1
)不直接处理不透明谓词,通过让不透明谓词的变量地址可读,则IDA便可以优化
(
2
)直接将不透明谓词赋值为
0
或者将不透明谓词中变量x,y赋值为
0
(
3
)编译器优化去干掉不透明谓词
(
1
)不直接处理不透明谓词,通过让不透明谓词的变量地址可读,则IDA便可以优化
(
2
)直接将不透明谓词赋值为
0
或者将不透明谓词中变量x,y赋值为
0
(
3
)编译器优化去干掉不透明谓词
(
1
)先保存所有的基本块
(
2
)区分真实块和分发器(虚假块) 一般通过规则匹配来做,但是并无法使用所有情况 (难点)
(
3
)连接真实块的顺序 一般静态可以通过IDA trace然后编写IDApython脚本,动态可以通过符号执行、模拟执行
(
4
)编写patch修复 对目标函数进行修复、恢复原始逻辑
(
1
)先保存所有的基本块
(
2
)区分真实块和分发器(虚假块) 一般通过规则匹配来做,但是并无法使用所有情况 (难点)
(
3
)连接真实块的顺序 一般静态可以通过IDA trace然后编写IDApython脚本,动态可以通过符号执行、模拟执行
(
4
)编写patch修复 对目标函数进行修复、恢复原始逻辑
入口链:原始函数代码的入口逻辑链,为序言到主分发器的执行路径
循环链:入口及出口均为主分发器的流程链,对应混淆过程中的循环体
Return链:指代入口为主分发器,出口为目标函数结束地址的流程链
入口链:原始函数代码的入口逻辑链,为序言到主分发器的执行路径
循环链:入口及出口均为主分发器的流程链,对应混淆过程中的循环体
Return链:指代入口为主分发器,出口为目标函数结束地址的流程链
(
1
)通过特征匹配来找真实块
(
2
)真实块的入口跳转地址必须为绝对的比较指令,如beq、bne,首次匹配到这种绝对的跳转指令,就能定位对应流程链中的真实入口地址,然后识别真是快
(
3
)凡是有内存操作的以及有bl
/
blx函数调用的都是真实块,其中有一些指向主分发器的虚拟块也会包含内存操作
(
1
)通过特征匹配来找真实块
(
2
)真实块的入口跳转地址必须为绝对的比较指令,如beq、bne,首次匹配到这种绝对的跳转指令,就能定位对应流程链中的真实入口地址,然后识别真是快
(
3
)凡是有内存操作的以及有bl
/
blx函数调用的都是真实块,其中有一些指向主分发器的虚拟块也会包含内存操作
1.
打patch,即通过jump指令或者一些条件跳转指令试图将它们重新连接起来
2.
提取出所有真实块的指令,并根据它们之间的关系,计算相对偏移,据此对函数进行重构
1.
打patch,即通过jump指令或者一些条件跳转指令试图将它们重新连接起来
2.
提取出所有真实块的指令,并根据它们之间的关系,计算相对偏移,据此对函数进行重构
(
1
)把无用块都改成nop指令
(
2
)针对没有产生分支的真实块把最后一条指令改成jmp指令跳转到下一真实块
(
3
)产生分支的真实块把CMOV指令改成相应的条件跳转指令跳向符合条件的分支,例如CMOVZ 改成JZ ,再在这条之后添加JMP 指令跳向另一分支
(
1
)把无用块都改成nop指令
(
2
)针对没有产生分支的真实块把最后一条指令改成jmp指令跳转到下一真实块
(
3
)产生分支的真实块把CMOV指令改成相应的条件跳转指令跳向符合条件的分支,例如CMOVZ 改成JZ ,再在这条之后添加JMP 指令跳向另一分支
我们分析得知,字符串解密一定在init_array运行完结束,将字符串读取到内存,因此我们运行init_array,然后将内存中解密的字符串保存下来即可
我们分析得知,字符串解密一定在init_array运行完结束,将字符串读取到内存,因此我们运行init_array,然后将内存中解密的字符串保存下来即可
(
1
)不使用AndroidNativeEmu项目,可以直接安装 pip install androidemu
(
2
)也可以去原仓库进行下载
(
1
)不使用AndroidNativeEmu项目,可以直接安装 pip install androidemu
(
2
)也可以去原仓库进行下载
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):
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)
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:
offset
=
address
-
base_addr
-
0x1000
print
(
'address:0x%x data:%s offset:0x%x '
%
(address, value, offset
+
0x1000
))
sodata
=
sodata[:(offset)]
+
value
+
sodata[offset
+
len
(value):]
with
open
(
'obf_new.so'
,
'wb'
) as
file
:
file
.write(sodata)
file
.close()
import
logging
import
sys
from
unicorn
import
*
import
struct
from
androidemu.emulator
import
Emulator
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-10-4 21:52
被随风而行aa编辑
,原因: