llvm
LLVM Passllvm
一套编译器和工具链技术,用于为任何编程语言开发前端,为任何指令集架构开发后端,基于中间表示ir设计(可移植高级汇编),最初作为动态编译技术,能提供一个完整编译器系统的中间层,从编译器中获取中间表示ir,新的ir可以被转化为平台依赖于机器的汇编语言代码。还能在编译时,链接时,运行时生成可重定位的机器代码,或者二进制机器代码
- 重要命令行工具
llvm-as:把LLVM IR从人类能看懂的文本格式汇编成二进制格式。注意:此处得到的不是目标平台的机器码。
llvm-dis:llvm-as的逆过程,即反汇编。 不过这里的反汇编的对象是LLVM IR的二进制格式,而不是机器码。
opt:优化LLVM IR。输出新的LLVM IR。
llc:把LLVM IR编译成汇编码。需要用as进一步得到机器码。
lli:解释执行LLVM IR。
前端把源代码编译为中间表示(IR),后端把IR编译为目标平台的机器码,ir也可以给解释器解释执行。
llvm设计了一个叫llvm ir的中间表示,以库(library)的方式来提供接口,提供后端服务,llvm pass就是,遍历一遍ir,同时对它进行一些操作,实现上,llvm核心库提供一些pass类去继承,修改一些实现它的方法,最后使用llvm编译器把它翻译得到ir传入pass进行遍历和修改。然后使用就是,可以拿来插桩,优化代码,静态分析一些clang
编译器前端(驱动),llvm编译器基础设施项目一部分(当gcc替代品看,主要在unix类系统上,官方这么介绍?)主要是一个库集合,libclang(提供c接口,api稳定)和libtooling(c++库,提供对ast控制—就那个语法结构图有点像—编写分析器,构建源代码重构工具,创建代码生成工具)还有协议啥的。
ir过程
中间层ir会经过优化,常见代码优化方式:删除无用代码,删除公共子表达式,常量合并等
- 删除无用代码,发现有计算结果不会被计算的就删除
- 删除公共子表达式,对重复计算结果不重复计算,复用计算结果,如
int fff(int x,int y){
int d=x*y;
int f=x*y+2;
int c=x*y+3;
}
优化成
int fff(int x,int y){
int d=x*y;
int f=d+2;
int c=d+3;
}
- 常量合并,常量计算拿结果直接编译被计算得到的,直接写了结果
- 除此之外还有循环展开,寄存器分配等优化方式
pass
![[Pasted image 20260103232526.png]]
pass是在中间ir阶段对中间代码进行优化,混淆等操作,而且是链式,前一个pass的结果可以作为下一个pass的输入进去
这里的pass分为两类,一类分析analysis以提供信息为主,一类是transform。以优化中间代码为主,ollvm的主要代码就在这个transform目录下
注册一个pass的格式
#include "llvm/Pass.h" // 引入定义 Pass 基类及相关接口的头文件
#include "llvm/IR/Function.h" // 引入 LLVM IR 中 Function 类型的定义
#include "llvm/Support/raw_ostream.h" // 引入支持输出的原始流(errs,outs 等)
using namespace llvm;
// 将 Pass 实现在匿名命名空间中,可避免命名冲突
namespace {
//继承自functionPass
struct EncodeFunctionName : public FunctionPass {
static char ID; // 每个 Pass 都需要一个 ID 来标识自己
// 构造函数:向父类(FunctionPass)的构造函数传递 ID
EncodeFunctionName() : FunctionPass(ID) {}
// runOnFunction 方法是 FunctionPass 的核心接口,每当 Pass 在一个 Function 对象上运行时调用
bool runOnFunction(Function &F) override {
// 使用 write_escaped 输出函数名称,可以将一些特殊字符转义输出
errs().write_escaped(F.getName()) << '\n';
// 返回 false,表示该 Pass 并未改变当前函数的 IR(如果有更改 IR,需返回 true)
return false;
}
}; e
}
// 在匿名命名空间之外定义静态成员变量 ID
char EncodeFunctionName::ID = 0;
static RegisterPass<EncodeFunctionName> X("encode", "Hello EncodeFunctionName Pass",
false /* Only looks at CFG */,
false /* Analysis Pass */);
ir语法
ir是各种pass类的目标
有两种表现形式,.ll和方便机器处理的.bc
![[Pasted image 20260103234955.png]]
llvm将编译过程分为三步
前端,高级语言转ir
中端,ir优化
后端,把ir转化为对应的硬件平台的汇编语言
ir包含三部分指令格式、Basic Block & CFG,SSA
指令格式
LLVM IR提供了一种类似于汇编语言的三地址码式的指令格式。下面的代码片段是一个非常简单的用LLVM IR实现的函数,该函数的输入是5个i32类型(int32)的整数,函数的功能是计算这5个数的和并返回。
define i32 @ir_add(i32, i32, i32, i32, i32){
%6 = add i32 %0, %1
%7 = add i32 %6, %2
%8 = add i32 %7, %3
%9 = add i32 %8, %4
ret i32 %9
}Basic Block & CFG
llvm ir 支持一些基本数据类型,如i1(布尔值),i8(8位整数,对应c语言中byte或char),i16(短整数)i32(32位整数,对应c语言中int),i64(long或64位系统的指针大小),i128(超大整数,编译器内部支持),浮点数(16位half,32位float 64位double)指针类型,数组和结构体等。
llvm ir中,所有变量和寄存器都必须带前缀,用来区分作用域,
- %开头 (Local Variables):表示局部变量(或者叫虚拟寄存器)它们只在当前函数内有效。LLVM IR中的变量的命名是以”%”开头,默认%0是函数的第一个参数,%1是第二个参数,依次类推。
- @开头 (Global Variables):表示 全局变量或函数名。
在 LLVM IR 中,变量(寄存器)有两种命名方式: - 有名字的:比如%a,%retval(通常是Debug模式下生成的方便人看)
- 没名字的(编号):比如%0,%1,%2(Release模式或优化后常见)
举例
c语言代码
// 一个简单的加法函数
int add(int a, int b) {
return a + b;
}
对应ir无名称模式
define i32 @add(i32 %0, i32 %1) {
; %0 代表第一个参数 (即 C代码里的 a)
; %1 代表第二个参数 (即 C代码里的 b)
%3 = add i32 %0, %1 ; 把 %0 和 %1 相加,结果存入新的寄存器 %3
ret i32 %3 ; 返回 %3 的值
}
对应ir有名称模式
define i32 @add(i32 %a, i32 %b) {
%result = add i32 %a, %b
ret i32 %result
}
注:每个%变量只能被赋值一次,因为下文要讲的ssa
LLVM IR的指令格式包括操作符、类型、输入、返回值。例如 “%6 = add i32 %0, %1″的操作符号是”add”、类型是”i32″、输入是”%0″和“%1”、返回值是”%6″。总的来说,IR支持一些基本的指令,然后编译器通过这些基本指令的来完成一些复杂的运算。
基本块(basic block)和控制流图CFG
汇编语言通常通过有条件跳转和无条件跳转两种跳转指令来实现逻辑运算,llvm ir同理,有无条件跳转br label,有条件跳转br i1…,比如在LLVM IR中”br label %7″意味着无论如何都跳转到名为%7的label那里,这是一条无条件跳转指令。”br i1 %10, label %11, label %22″是有条件跳转,意味着这如果%10是true则跳转到名为%11的label,否则跳转到名为%22的label。基本块,从第一条指令(Leading instruction)到最后一条(Terminator Instruction)跳转指令(如br,ret,switch等),CFG就是把跳转指令当作连接的路线,每个基本块当成一个块连成的执行路径图。控制流平坦化就是把所有基本块都塞在一个swicth分发器下,原本的a到b变成了a,返回分发器,计算,分发器决定去b
以一个简单c语言代码举例
int test(int a) {
int b = a + 5;
if (b > 10) {
return 1;
} else {
return 0;
}
}
这段代码变成ir后对应三个基本块,入口块(entry):做加法,判断大小,true块(if.then):返回1,false块(if.else):返回0
入口块
define i32 @test(i32 %a) {
entry: ; <--- 1. 名字 (Label)
%add = add nsw i32 %a, 5 ; <--- 2. 入口 (Leading Instruction)
%cmp = icmp sgt i32 %add, 10
br i1 %cmp, label %if.then, label %if.else ; <--- 3. 出口 (Terminator)
if.then: ; 下一个基本块的名字
ret i32 1
if.else: ; 再下一个基本块的名字
ret i32 0
}
SSA
静态单赋值,核心思想就是,在ir代码列出看,每个变量只会被赋值一次,从编译器角度来看,编译器不关心变量,编译器是以数据为中心设计的,每个变量的每次写入,都生成了一个新的数据版本,编译器的优化是围绕数据版本展开的,相当于同一变量,每次被赋值,都会产生一个新的变量版本。
但是会出现一个问题,变量不能重写,那遇到分支怎么办
假设有一个这样的c
int a;
if (condition) {
a = 10; // 路径A
} else {
a = 20; // 路径B
}
// 那在这里a等于多少?
return a;
路径a会生成%a1 = 10,路径b会生成%a2 = 20
那后续用a的时候,该用哪一个,这时候Phi节点(通常位于基本块最开头),根据前面的是从哪个基本块跳过来的,根据块给对应值
; ... 前面是分支判断 ...
if.true: ; 对应路径 A
%a1 = add i32 0, 10 ; a1 = 10
br label %merge ; 跳到汇合点
if.false: ; 对应路径 B
%a2 = add i32 0, 20 ; a2 = 20
br label %merge ; 跳到汇合点
merge: ; 汇合点
; Phi 节点
%final_a = phi i32 [ %a1, %if.true ], [ %a2, %if.false ]
ret i32 %final_a
在修改控制流时,phi必须严格放在第一行,覆盖所有前驱(假设一个基本块有三个入口,那phi节点要全部写出),再遇到循环处理,循环作为一个回环结构,头部的变量值,第一次进入时是初始值,第二次进入是累加后的值,需要phi节点同时处理从外面进来和从循环尾部跳回的情况
LLVM IR基础工具与格式
LLVM IR 在磁盘上有两种存储格式:Bitcode(.bc)和汇编文本 (.ll)。Bitcode是紧凑的二进制格式,适合机器处理;汇编文本是人类可读的格式,适合调试和阅读。
常用工具链如下:
Clang(前端编译器):将C/C++源码转为IR。
生成Bitcode(.bc)
clang sum.c -emit-llvm -c -o sum.bc
生成汇编表示 (.ll)
clang sum.c -emit-llvm -S -c -o sum.ll
llvm-as/llvm-dis (格式转换):在二进制和文本之间互转。
将汇编文本转换为Bitcode
llvm-as sum.ll -o sum.bc
将Bitcode反汇编为汇编文本
llvm-dis sum.bc -o sum.ll
llvm-extract (提取工具):从模块中剥离出特定的函数或全局变量。
提取sum函数
llvm-extract -func=sum sum.bc -o sum-fn.bc
opt -load LLVMObfuscator.so -hlw -S hello.ll -o hello_opt.ll
//opt是读取bitcode和执行pass的工具
-hlw 是 LLVM Pass 中自定义的参数,用来指定使用哪个 Pass 进行优化
注:-S就是人类可读
LLVM内存模型 (C++ API)
LLVM在内存中通过C++类层级来表示代码,这套结构对应了LLVM语言的语法。
- Module类
聚合了整个翻译单元(Translation Unit)的数据。它是最高层级的容器,包含函数列表和全局变量。可以通过begin() 和 end() 方法遍历其中的Function。 - Function类
包含函数定义和声明。如果是一个函数定义,它包含了一系列BasicBlock。可以通过begin()和end()方法遍历这些基本块。它还包含参数列表。 - BasicBlock类
代表控制流图 (CFG) 中的一个节点。它包含一个指令序列。其核心特征是单入口单出口,只有最后一条指令(Terminator)可以改变控制流。可以通过getTerminator()获取终结指令,或通过begin()和end()遍历其中的Instruction。 - Instruction类
代表LLVM IR的运算原子,即单条指令。可以通过getOpcode()获取操作码(如add,sub,load)。指令继承自User类,因为它使用操作数;同时也是Value的子类,因为它产生结果。
Value与User接口
这是LLVM处理数据流的核心机制,基于SSA形式设计。它允许直接操作Use-Def(使用-定义)链。
- Value类
代表一个可以被其他指令使用的结果。函数参数、指令的运算结果、基本块的标签都是Value。
它定义了use_begin()和use_end()方法,用于遍历所有使用该值的地方(User)。
replaceAllUsesWith(Value *New) 方法可以将所有使用当前值的地方替换为新值,常用于代码优化。 - User类
代表使用了一个或多个Value的实体。
它定义了op_begin()和op_end()方法,用于快速访问它所依赖的操作数。
PASS基类
LLVM Pass基类与其运行粒度
常见的Pass主要分为三类,区别在于LLVM遍历代码的粒度和范围:
- ModulePass
针对整个Module运行。通常对应一个源文件(翻译单元)。
特点:可进行跨函数分析,查看全局变量与函数之间的关系。 - FunctionPass
针对Function运行。
特点:LLVM在IR层遍历模块中的每个函数,并对每个函数调用一次runOnFunction方法。这是常用的Pass类型之一。 - BasicBlockPass
针对每一个BasicBlock运行。
定义:基本块是函数中的基本单位,表示一组顺序执行的指令。
结构:每个基本块以一个标签(Label)开始,并以一个终止指令(Terminator,如ret或br)结束。
应用:这种粒度的Pass在控制流平坦化(Control Flow Flattening)等混淆源码中常见。
Module类成员列表(C++结构)
Module类作为最高层容器,内部维护多种列表管理不同类型的全局对象:
Module {
GlobalListType GlobalList; // 全局变量列表
FunctionListType FunctionList; // 函数列表
AliasListType AliasList; // 别名列表
IFuncListType IFuncList; // "IFunc"列表(间接函数)
NamedMDListType NamedMDList; // 命名元数据列表
...
}
LLVM类型查询与安全转换模板函数
![[0d9e1ae5f21f0064f74774c4aaed3238.png]]
运行时检查x是不是属于T类型,有种C++泛型编程的感觉,返回值是boo类型,如果是这个类型就true,否则就false
if (isa<CallInst>(inst)) {
// inst 确实是一个 CallInst 或其子类
}
![[8f67086242e1ab9a42d0dd4c3ecab39d.png]]
在运行的时候将x向下转换成T类型
返回值是当isatx为true时就返回一个指向T的指针,不然就返回null
if (auto *CI = dyn_cast<CallInst>(inst)) {
// 只有当 inst 是 CallInst 时,CI 非空
CI->getCalledFunction()->print(errs());
} else {
// inst 不是 CallInst
}
![[841aebb2aedb0d5664e0e5c3a20777b0.png]]
运行时断言x是T或T的子类,然后进行转换
// 确信 inst 是 CallInst,否则会在调试模式下断言失败
auto *CI = cast<CallInst>(inst);
Function *callee = CI->getCalledFunction();
c语言编译程序阶段
📄 hello.c
(源程序 / 文本)
⬇️
🔲 预处理器 (cpp)
⬇️
📄 hello.i
(修改了的源程序 / 文本)
⬇️
🔲 编译器 (cc1)
⬇️
📄 hello.s
(汇编程序 / 文本)
⬇️
🔲 汇编器 (as)
⬇️
📄 hello.o
(可重定位目标程序 / 二进制)
⬇️ ➕ (并入 printf.o)
🔲 链接器 (ld)
⬇️
🚀 hello
(可执行目标程序 / 二进制)
ir的生成在第二个步骤,编译器(cc1)内部,内部流程
hello.i (文本)
⬇️
┌─────────────────────────────┐
│ 编译器 (cc1) 内部 │
│ │
│ 1. 词法/语法分析 (Parser) │
│ ⬇️ │
│ 2. 生成 IR (中间代码) 👈 这里 │
│ ⬇️ │
│ 3. 优化 IR (Optimizer) │
│ ⬇️ │
│ 4. 代码生成 (Code Gen) │
└─────────────────────────────┘
⬇️
hello.s (汇编文本)
LLVM/Clang: 它的IR就叫LLVM IR(也就是.ll或.bc文件)
代码控制流混淆
条件控制流混淆
添加无关条件语句,交换条件判断顺序,插入冗余条件语句
控制流平坦化
流程转为平坦结构,重排转换,流程就是,分发循环,状态变量,基本块调度,正常的控制流的就是线性分支和跳转
函数内联
常见代码控制流混淆,通过将函数调用替换为函数体的复制来改变程序的控制流程
简单示例
// 原始代码
int add(int a, int b) {
return a + b;
}
int result = add(2, 3);
// 混淆后的代码
int result = 2 + 3;
控制流图重构
改变程序控制流结构,通常使用图转换和图重排操作,将控制流图变成更复杂混乱的结构
// 原始代码
if (condition1) {
// code block 1
} else if (condition2) {
// code block 2
} else {
// code block 3
}
// 混淆后的代码
if (condition1) {
// code block 1
} else {
if (condition2) {
// code block 2
} else {
// code block 3
}
}
字符串加密
对字符串进行加密和解密操作,通常使用对称加密算法和密钥对字符串进行加解密操作
// 原始代码
String username = "admin";
String password = "123456";
// 混淆后的代码
String encryptedUsername = decrypt("encrypted_username");
String encryptedPassword = decrypt("encrypted_password");
ollvm反混淆
去指令替换混淆(SUB)
llvm提供的一些优化pass可以简化和优化编译后的ir,去除无意义的混淆指令,尤其使用llvm-dis反汇编指令后,可以通过opt工具结合-03优化级别来简化程序,利用llvm编译工具链中内置优化技术自动去替换
或者使用miasm框架进行匹配再优化,通过模拟二进制代码的执行,自动化识别程序中的混淆模式,通过去除冗余·指令来简化程序,可以进行二进制分析,控制流恢复,混淆去除
- d810去除
- GAMBA 简化复杂的 SUB 表达式
反字符串加密
- 字符串加密存在解密函数,通过某些算法将加密字符串还原成明文,例如命名为datadiv_decode或其他的类似函数,在二进制中搜索特定解密函数快速定位
- init_array中解密,在程序启动时执行的代码区域,通常用于初始化操作,因此通过模拟程序启动过程可以提取解密后字符串,也可以通过动态分析工具或模拟执行来查看解密过程
- jni_onload解密,在jni库加载时被调用,在jni库加载时,解密操作可能会在jni_onload中执行,通常准备一些加密数据给java层,可以hook或者用unicorn模拟执行
反虚假控制流(BCF)
一般思路为出去不可达块和不透明谓词,难点在于不透明谓词,存在永真永假型,可真可假型,也要考虑死循环问题
永真/假型:插入的后续基本块中必有一个不被执行
可真可假型:插入的两个后继基本块的语义应相同
思路:
(1)不直接处理不透明谓词,通过让不透明谓词的变量地址可读,则IDA便可以优化
找到参与不透明谓词计算的全局变量,修改变量所在的内存段(segment)的属性,通常位于data段或者bss段,修改为只读,或者通过快捷键alt加s编辑,或者用idapy修改
(2)直接将不透明谓词赋值为0或者将不透明谓词中变量x,y赋值为0
找到不透明谓词对应的汇编指令(通常是条件跳转指令,如jz, jnz)。
通过动态调试或分析确定该谓词的结果是永真还是永假。
直接修改二进制代码。如果条件必为真,将jnz target改为jmp target;如果必为假,将跳转指令修改为nop。或者,在汇编层面将比较指令前的变量赋值指令直接修改为常数赋值(如mov eax, 0),迫使条件跳转变为确定跳转。
(3)编译器优化去干掉不透明谓词
获取LLVM IR(如果是逆向工程,需使用RetDec或McSema等工具将二进制提升为IR)。使用LLVM的opt工具运行优化Pass。
各Pass作用:
-mem2reg:将内存访问提升为寄存器操作,这是后续优化的基础。
-sccp (Sparse Conditional Constant Propagation):稀疏条件常量传播,能计算出很多复杂的常量表达式。
-simplifycfg:简化控制流图,它会删除那些确定的条件跳转生成的死分支,并合并基本块。
-adce (Aggressive Dead Code Elimination):激进的死代码消除,移除对结果无影响的计算指令。
不可达块,指控制流无法到达基本快,可以用符号执行或者模拟执行来除去一些基本不可达块
idapython脚本
import ida_xref
import ida_idaapi
from ida_bytes import get_bytes, patch_bytes
from ida_segment import get_segm_by_name
# 将 mov 寄存器, 不透明谓词 修改为 mov 寄存器, 0
def do_patch(ea):
"""
检查并替换不透明谓词的 mov 操作,将其替换为 mov 寄存器, 0
"""
# 获取指令字节
opcode = get_bytes(ea, 1)
# 判断是否为 mov 寄存器, [寄存器/内存] 指令 (例如 mov eax, edi)
if opcode == b"\x8B":
# 获取目标寄存器
reg = (ord(get_bytes(ea + 1, 1)) & 0b00111000) >> 3
# 将原始的 mov 指令替换为 mov 寄存器, 0 (即 mov eax, 0)
patch_bytes(ea, (0xB8 + reg).to_bytes(1, 'little') + b'\x00\x00\x00\x00\x90')
else:
print(f"Unsupported instruction at {hex(ea)}")
def get_bss_segment():
"""
获取 BSS 段的地址范围
"""
seg = get_segm_by_name('.bss')
if seg is None:
print("BSS segment not found.")
return None, None
return seg.start_ea, seg.end_ea
def patch_control_flow(start, end):
"""
对指定的地址范围内的虚假控制流进行修复
"""
for addr in range(start, end, 4):
# 获取所有对该地址的交叉引用
ref = ida_xref.get_first_dref_to(addr)
print(f"Processing references for address {hex(addr)}".center(40, '-'))
# 遍历所有交叉引用
while ref != ida_idaapi.BADADDR:
print(f"Patch reference at {hex(ref)}")
do_patch(ref)
ref = ida_xref.get_next_dref_to(addr, ref)
print('-' * 40)
def main():
# 获取 .bss 段的地址范围
start, end = get_bss_segment()
if start is None or end is None:
return # 如果找不到 .bss 段则退出
# 对虚假控制流进行修复
patch_control_flow(start, end)
if __name__ == "__main__":
main()
反控制流平坦化
![[Pasted image 20260103030401.png]]
真实逻辑在于,序言块,真实块,retn块中
核心在于区分真实块和分发器,恢复真实块顺序,通过特征匹配和动态执行相结合。修补过程中优先使用简单的跳转逻辑,必要时整体重构函数
- 先保存所有基本块
控制流平坦化重构执行流分为三链
- 入口链:原始函数入口到主分发器路径
- 循环链:主分发器之间的循环跳转路径
- 返回链:主分发器到函数结束的路径
重点关注分发器识别
- 区分真实块和分发器 分发器:引导流程跳转到下一个基本块 引用次数较高,结构固定(如switchcase或复杂ifelse) 真实块:存在内存操作(如内存访问指令ldr,str),函数调用(出现bl或blx指令),确定性跳转(明确条件跳转指令,如beq,bne) 方法:
- 遍历所有基本块,统计引用次数
- 通过特征匹配识别
- 连接真实块顺序,一般静态通过ida trace然后编写idapython脚本,动态通过符号执行,模拟执行
静态:使用trace功能获取执行路径,用idapython解析连接关系,判断分支条件
动态:使用符号执行工具如angr模拟执行代码,跟踪执行结果,结合人工分析调整
特例:真实块包含双路径(movwne/movtne r1)的情况,需要分别处理两条路径并连接至对应的真实块。 - 编写patch修复,对目标函数进行修复,恢复原始逻辑
一. 直接patch
- 清理无用块:将分发器或虚假块nop掉
- 修补无分支块:真实块最后一条指令改为无条件跳转(jmp)到下一个真实块
- 修补分支块:将条件跳转指令(如cmovz)替换为明确的条件跳转(如jz),并添加无条件跳转指令调香=向另一分支
二.重构函数逻辑 - 提取所有真实块指令,根据关系排列
- 计算每个真实块相对偏移,形成新函数代码
- 替换混淆后的目标函数
idapython寻找真实块
#idapython寻找真实块
import idaapi
import idc
target_func=0x401CE0#要处理的函数地址
preprocess_block=0x402057#预处理块的地址
#寻找所有预处理器的前驱
True_Block=[]
Fake_Block=[]
'''FlowChart
Constructor
@param f: A func_t type, use get_func(ea) to get a reference
@param bounds: A tuple of the form (start, end). Used if "f" is None
@param flags: one of the FC_xxxx flags.
这个函数就是返回ida流程图中的所有块中的操作权,比如查看函数起始地址,结束地址等等
'''
f_block=idaapi.FlowChart(idaapi.get_func(target_func),flags=idaapi.FC_PREDS)
for block in f_block:
#print(block)
#预处理器的前驱都是真实块
if block.start_ea==preprocess_block:
Fake_Block.append((block.start_ea,idc.prev_head(block.end_ea)))
print("Find True Bolcks\n")
tbs=block.preds()
for tb in tbs:
True_Block.append((tb.start_ea,idc.prev_head(tb.end_ea)))
print(True_Block)
elif not [x for x in block.succs()]:
print("find ret")
True_Block.append((block.start_ea,idc.prev_head(block.end_ea)))
elif block.start_ea!=target_func:
Fake_Block.append((block.start_ea,idc.prev_head(block.end_ea)))
print('True block')
print(True_Block)
print("Fake Block")
print(Fake_Block)
OLLVM环境搭建
ollvm4.0源码
git clone -b llvm-4.0 --depth=1 https://github.com/obfuscator-llvm/obfuscator.git
安装docker
sudo apt install docker.io
安装编译 ollvm 的 docker 环境
sudo docker pull nickdiego/ollvm-build
编译ollvm
下载编译脚本
git clone --depth=1 https://github.com/oacia/docker-ollvm.git
编译
ollvm-build.sh` 后面跟的参数是 `ollvm的源码目录
sudo docker-ollvm/ollvm-build.sh /home/xw/Desktop/ollvm/obfuscator/
创建软链接
#!/usr/bin/env bash
# 一键部署 OLLVM 可执行文件到 /usr/local/ollvm/bin,并加 -ollvm 后缀
set -e
SRC_DIR="/home/xw/Desktop/ollvm/obfuscator/build_release/bin"
DEST_DIR="/usr/local/ollvm/bin"
sudo mkdir -p "${DEST_DIR}"
echo ">>> 正在批量创建软链接 ..."
for f in "${SRC_DIR}"/*; do
base=$(basename "$f")
sudo ln -sf "$f" "${DEST_DIR}/${base}-ollvm"
done
echo ">>> 已完成链接到 ${DEST_DIR}"
vim ~/.bashrc
export PATH="/usr/local/ollvm/bin:$PATH"
source ~/.hasbrc
一些使用
trace
- 首先需要设置Tracing options。Debugger->Tracing->Tracing options。
- 打开后设置buffer size,和Trace file。如果需要buffer size无限大,可以设置为0。(如下图所示)
- Tracing 支持 函数Tracing 和 指令Tracing。可以在动态调试时,点击工具栏中的按钮开启。
unicorn
Unicorn 是一个模拟 CPU 执行的框架,Unicorn 在 OLLVM 反混淆中的作用为计算 BR 寄存器的值以及控制流平坦化中作为 SWITCH ID 的寄存器的值
import unicorn # 导入 Unicorn 库
import capstone # 导入 Capstone 库
import binascii # 导入 binascii 库
# 定义一个函数 print_regs,用于打印寄存器的值
def print_regs(mu):
for i in range(unicorn.arm64_const.UC_ARM64_REG_X0, unicorn.arm64_const.UC_ARM64_REG_X30 + 1): # 遍历 X0-X30
print('X%d: 0x%x' % (i - unicorn.arm64_const.UC_ARM64_REG_X0, mu.reg_read(i)))
# 定义一个回调函数 hook_code,用于在代码执行时触发
def hook_code(mu, addr, size, user_data):
print('-------hook code start-------')
code = mu.mem_read(addr, size) # 读取内存中的代码
cp = capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM) # 初始化 Capstone 模块
for i in cp.disasm(code, addr): # 反汇编代码
print('[addr:0x%x]: %s %s' % (i.address, i.mnemonic, i.op_str))
print_regs(mu) # 打印寄存器的值
# 定义一个回调函数 hook_mem_write,用于在内存写入时触发
def hook_mem_write(mu, type, addr, size, value, user_data):
print('-------hook mem write-------')
if type == unicorn.UC_MEM_WRITE: # 如果是内存写入事件
print('memory write addr:0x%x size:%x value:0x%x' % (addr, size, value))
# 定义一个回调函数 hook_intr,用于在中断发生时触发
def hook_intr(mu, intno, user_data):
print('-------hook intr start-------')
print_regs(mu) # 打印寄存器的值
# 定义一个测试函数 test_arm64
def test_arm64():
# 定义一段 ARM64 指令集的代码
code = b'\xe0\x03\x1f\xaa' # mov x0, xzr (示例指令,可替换)
# 初始化 Unicorn 模块
mu = unicorn.Uc(unicorn.UC_ARCH_ARM64, unicorn.UC_MODE_ARM)
# 定义内存地址、大小
addr = 0x1000
size = 0x1000
# 映射内存并将代码写入内存
mu.mem_map(addr, size)
mu.mem_write(addr, code)
# 读取内存中的代码并打印
code_bytes = mu.mem_read(addr, len(code))
print('addr:0x%x, content:%s' % (addr, binascii.b2a_hex(code_bytes)))
# 设置寄存器的值
mu.reg_write(unicorn.arm64_const.UC_ARM64_REG_X0, 0x100)
mu.reg_write(unicorn.arm64_const.UC_ARM64_REG_X1, 0x200)
mu.reg_write(unicorn.arm64_const.UC_ARM64_REG_X2, 0x300)
mu.reg_write(unicorn.arm64_const.UC_ARM64_REG_X3, 0x400)
# 监听代码执行事件
mu.hook_add(unicorn.UC_HOOK_CODE, hook_code)
# 监听内存写入事件
mu.hook_add(unicorn.UC_HOOK_MEM_WRITE, hook_mem_write)
# 监听中断事件
mu.hook_add(unicorn.UC_HOOK_INTR, hook_intr)
try:
# 开始模拟执行代码
mu.emu_start(addr, addr + len(code))
except unicorn.UcError as e:
print(e)
# 读取内存中的数据并打印
stack_bytes = mu.mem_read(addr, 4)
print("mem:0x%x, value:%s" % (addr, binascii.b2a_hex(stack_bytes)))
if __name__ == '__main__':
test_arm64()
flare-emu
flare-emu的核心功能是通过 Unicorn 的仿真能力,为 IDA 二进制分析提供强大的动态仿真支持。它支持多种架构(包括x86、x86_64、ARM和ARM64),并提供了五种主要的仿真接口,以及一系列相关的辅助和实用函数。
- emulateRange
用于仿真指定范围内的指令或函数。支持用户定义的指令钩子和“调用”指令钩子。用户可以指定寄存器和堆栈参数的值。如果提供字节字符串,会将其写入仿真器的内存,并将指针写入寄存器或堆栈变量。提供了从仿真内存或寄存器读取数据的实用函数。 - iterate
强制仿真进入指定函数内的特定分支,以达到给定的目标地址。
用户可以指定目标地址列表,或从函数的交叉引用中获取目标地址。
提供了用户定义的指令和“调用”指令钩子。 - iterateAllPaths
尝试找到并仿真目标函数的所有路径。
适用于需要覆盖函数中所有基本块的代码分析场景。 - emulateBytes
用于仿真一段独立的代码(如 shellcode)。提供的字节不会添加到 IDB 中,而是直接仿真。 - emulateFrom
从指定地址开始仿真,直到没有更多指令可仿真,或在钩子中停止仿真。支持动态代码发现,适用于边界不明确的函数或混淆代码。
网址
示例使用
import flare_emu # 导入flare_emu模块,用于模拟执行
import idc # 导入idc模块,用于与IDA交互
import idaapi # 导入idaapi模块,用于获取函数信息等
import keypatch # 导入keypatch模块,用于汇编指令的修复
# 定义一个函数,用于修复指定地址的指令
def patch_one(address: int, new_instruction: str):
"""
使用Keypatch修复指定地址的指令。
参数:
address (int): 要修复的指令地址。
new_instruction (str): 新的汇编指令。
返回:
bool: 如果修复成功返回True,否则返回False。
"""
kp_asm = keypatch.Keypatch_Asm() # 初始化Keypatch汇编对象
if kp_asm.arch is None: # 检查Keypatch是否支持当前架构
print("ERROR: Keypatch无法处理此架构")
return False
# 解析新的汇编指令
assembly = kp_asm.ida_resolve(new_instruction, address)
(encoding, count) = kp_asm.assemble(assembly, address) # 将汇编指令转换为机器码
if encoding is None: # 如果没有生成机器码,说明无需修复
print("Keypatch: 无需修复")
return False
# 将机器码转换为字节数据
patch_data = ''.join(chr(c) for c in encoding)
patch_len = len(patch_data) # 获取机器码长度
kp_asm.patch(address, patch_data, patch_len) # 使用Keypatch进行修复
print(f"修复完成: {assembly}") # 输出修复的指令
def myTargetCallBack(emu, address, argv, userData):
"""
模拟执行时的回调函数,用于记录间接调用的目标地址。
参数:
emu (flare_emu.EmuHelper): 模拟器对象。
address (int): 当前指令地址。
argv (list): 当前指令的参数。
userData (dict): 用户数据,用于存储间接调用的映射。
"""
# 获取当前指令的反汇编字符串
code_str = idc.GetDisasm(address)
# 提取BR指令使用的寄存器名
register_name = code_str.split(" ")[-1]
# 输出当前地址和寄存器值
print(f"address = {hex(address)}, X* = {hex(emu.getRegVal(register_name))}")
# 将当前地址和寄存器值存储到用户数据中
userData["br_map"][address] = emu.getRegVal(register_name)
def anti_icall():
"""
主函数,用于处理间接调用(indirect call)的修复。
"""
br_addr_list = [0x4006A0, 0x400724] # 初始化间接调用地址列表
eh = flare_emu.EmuHelper() # 初始化flare_emu模拟器
# 模拟执行指定范围内的代码,并记录间接调用的目标地址
eh.iterate(br_addr_list, targetCallback=myTargetCallBack, hookData={"br_map": {}})
# 遍历记录的间接调用地址,并修复为直接调用
for br_addr in eh.hookData["br_map"]:
print(f"br_addr = {hex(br_addr)} {hex(eh.hookData['br_map'][br_addr])}")
patch_one(br_addr, f"bl {hex(eh.hookData['br_map'][br_addr])}")
arm架构下未映射脚本
(针对平坦化)
import flare_emu
import idaapi
import idc
import unicorn
import unicorn.arm64_const as uc_arm64
import keypatch
import struct
# ==========================================
# 辅助类:处理 ARM64 条件码与 NZCV 的关系
# ==========================================
class ConditionHandler:
def __init__(self):
# 映射条件码到满足该条件的 NZCV 设置 (N, Z, C, V)
# 这里的设置是为了让条件 100% 成立
self.cond_map = {
"EQ": (0, 1, 0, 0), # Z=1
"NE": (0, 0, 0, 0), # Z=0
"CS": (0, 0, 1, 0), # C=1 (HS)
"CC": (0, 0, 0, 0), # C=0 (LO)
"MI": (1, 0, 0, 0), # N=1
"PL": (0, 0, 0, 0), # N=0
"VS": (0, 0, 0, 1), # V=1
"VC": (0, 0, 0, 0), # V=0
"HI": (0, 0, 1, 0), # C=1, Z=0
"LS": (0, 1, 0, 0), # C=0 or Z=1
"GE": (0, 0, 0, 0), # N=V
"LT": (1, 0, 0, 0), # N!=V
"GT": (0, 0, 0, 0), # Z=0, N=V
"LE": (0, 1, 0, 0), # Z=1 or N!=V
}
# 映射条件码到对应的跳转指令
self.b_map = {k: f"B.{k}" for k in self.cond_map.keys()}
def get_flags_for_true(self, cond_str):
"""返回让条件成立的 NZCV"""
return self.cond_map.get(cond_str, (0,0,0,0))
def get_flags_for_false(self, cond_str):
"""返回让条件不成立的 NZCV (取反)"""
# 简单处理:对于 EQ(Z=1),反面就是 Z=0。
# 这里为了简化,针对不同条件取反逻辑不同,但通常翻转关键位即可
# 实际上模拟执行时,我们会强行覆盖 PSTATE,所以只要给出一组必假的值即可
n, z, c, v = self.cond_map.get(cond_str, (0,0,0,0))
if cond_str in ["EQ", "LE", "LS"]: return (n, 0, c, v) # Z=0
if cond_str in ["NE", "GT", "HI"]: return (n, 1, c, v) # Z=1
if cond_str in ["GE", "LT"]: return (1-n, z, c, v) # 翻转 N 导致 N!=V 变化
return (0, 0, 0, 0) # 默认
# ==========================================
# 核心逻辑
# ==========================================
class BlockInfo:
def __init__(self, start_addr, end_addr):
self.start_addr = start_addr
self.end_addr = end_addr
self.behind_block = {True: None, False: None} # True: 条件成立跳转, False: 条件不成立
self.csel_instr = None # 记录 (address, condition_string)
def is_conditional(self):
return self.csel_instr is not None
class OLLVMDeobfuscator:
def __init__(self, func_start):
self.func_start = func_start
self.func_end = idc.find_func_end(func_start)
self.real_blocks = {} # addr -> BlockInfo
self.fake_blocks = []
self.visited_paths = set() # 记录已探索的 (block_addr, branch_bool)
self.cond_handler = ConditionHandler()
self.path_queue = [] # 待探索队列
def set_nzcv(self, uc, n, z, c, v):
pstate = uc.reg_read(uc_arm64.UC_ARM64_REG_PSTATE)
# 清除 NZCV (bits 31, 30, 29, 28)
pstate &= ~(0xF0000000)
pstate |= (n << 31) | (z << 30) | (c << 29) | (v << 28)
uc.reg_write(uc_arm64.UC_ARM64_REG_PSTATE, pstate)
def find_csel_backwards(self, start_addr, end_addr):
"""倒序查找 CSEL 指令,不再依赖固定偏移"""
curr = end_addr
# 向前回溯最多 8 条指令
for _ in range(8):
curr = idc.prev_head(curr)
if curr < start_addr: break
mnem = idc.print_insn_mnem(curr)
# 匹配 CSEL, CSEL.EQ 等
if mnem.startswith("CSEL"):
# 获取条件码,例如 CSEL EQ -> EQ; CSEL.EQ -> EQ
# IDA 显示通常是 CSEL Rd, Rn, Rm, COND
op_str = idc.generate_disasm_line(curr, 0)
parts = op_str.replace(",", "").split()
cond = parts[-1] # 最后一个通常是条件
return curr, cond
return None, None
def is_real_block(self, start, end):
"""判断逻辑:不是分发器,通常以 RET 或 B 结尾,且包含 CSEL 更新状态"""
if start == self.func_start: return True
last_mnem = idc.print_insn_mnem(end)
if last_mnem == "RET": return True
# 真实块通常在计算完后跳转回分发器
if last_mnem == "B":
csel_addr, _ = self.find_csel_backwards(start, end)
if csel_addr: return True
return False
def hook_code(self, uc, address, size, user_data):
# 这里的 user_data 是当前正在模拟的 BlockInfo
current_block = user_data
# 1. 检查是否遇到了 CSEL
mnem = idc.print_insn_mnem(address)
if mnem.startswith("CSEL"):
# 解析条件
op_line = idc.generate_disasm_line(address, 0)
cond = op_line.replace(",", "").split()[-1]
current_block.csel_instr = (address, cond)
# 检查我们是否需要强制改流
# 这里的逻辑是:如果这是一个新发现的分支点,我们需要把另一条路加入队列
# 并在当前执行中强制走其中一条路
pass
def run_emulation(self):
print("[*] 开始模拟执行...")
# 使用栈或队列来管理待探索的路径
# 初始:从函数头开始
queue = [(self.func_start, None, None)] # (addr, set_flags_func, parent_block)
processed_blocks = set()
eh = flare_emu.EmuHelper()
# 这里的逻辑比较复杂,为了简化脚本,我们采用
# "多次模拟 + 强制分支" 的策略,类似你原脚本的 range(0,3) 或者是 DFS
# 我们使用一个简单的递归 DFS 模拟器
self.dfs_emulate(eh, self.func_start)
def dfs_emulate(self, eh, start_addr):
# 找到当前基本块范围
end_addr = idc.get_item_end(start_addr)
while True:
mnem = idc.print_insn_mnem(idc.prev_head(end_addr))
if mnem in ["B", "RET"] or end_addr > self.func_end:
end_addr = idc.prev_head(end_addr) # 指向最后一条指令地址
break
end_addr = idc.next_head(end_addr)
block_key = start_addr
if block_key in self.real_blocks:
return # 已处理
# 判定是否真实块
if not self.is_real_block(start_addr, end_addr):
# 如果是分发器,我们通常不记录,但需要让模拟器跑过去
return
print(f"发现真实块: {hex(start_addr)} - {hex(end_addr)}")
block_info = BlockInfo(start_addr, end_addr)
self.real_blocks[start_addr] = block_info
# 寻找 CSEL
csel_addr, csel_cond = self.find_csel_backwards(start_addr, end_addr)
if csel_addr:
block_info.csel_instr = (csel_addr, csel_cond)
print(f" 包含条件跳转: {csel_cond} at {hex(csel_addr)}")
# 模拟两次:一次强制 True,一次强制 False
# 1. Force True
print(f" >> 模拟 True 分支 ({csel_cond})...")
next_true = self.get_next_block(eh, start_addr, end_addr, csel_cond, force_true=True)
block_info.behind_block[True] = next_true
if next_true and next_true != start_addr: # 避免死循环
self.dfs_emulate(eh, next_true)
# 2. Force False
print(f" >> 模拟 False 分支 (!{csel_cond})...")
next_false = self.get_next_block(eh, start_addr, end_addr, csel_cond, force_true=False)
block_info.behind_block[False] = next_false
if next_false and next_false != start_addr:
self.dfs_emulate(eh, next_false)
else:
# 无条件跳转 (只有一条路)
print(f" >> 模拟无条件跳转...")
next_addr = self.get_next_block(eh, start_addr, end_addr, None, None)
block_info.behind_block[True] = next_addr # 默认放 True
if next_addr and next_addr != start_addr:
self.dfs_emulate(eh, next_addr)
def get_next_block(self, eh, start, end, cond, force_true):
"""执行一段代码并获取跳出的目标地址"""
def hook(uc, address, size, user_data):
# 到了 CSEL 指令,强制改 PSTATE
if cond and address == user_data['csel_addr']:
n, z, c, v = (0,0,0,0)
if force_true:
n, z, c, v = self.cond_handler.get_flags_for_true(cond)
else:
n, z, c, v = self.cond_handler.get_flags_for_false(cond)
self.set_nzcv(uc, n, z, c, v)
# 寻找 CSEL 地址
csel_addr = 0
if cond:
csel_addr, _ = self.find_csel_backwards(start, end)
user_data = {'csel_addr': csel_addr}
# 模拟执行,直到跳出当前块范围,或者跑了一定步数
# 这里利用 flare_emu 的 emulateRange,但我们需要获取最终停在哪
# 由于 flare_emu 封装较多,这里简化为:跑完该块后,再单步跑几步直到遇到比较大的跳转(跳回真实块)
# 注意:这里为了简化代码,假设 emulateRange 能正确处理。
# 实际上 OLLVM 跑完真实块后会进入 Dispatcher,我们需要让它跑过 Dispatcher 停在下一个真实块开头。
# 定义一个停止 hook:当 PC 指向一个新的真实块候选时停止
final_pc = [None]
def stop_hook(uc, address, size, user_data):
# 如果跑出了当前块,且看起来像是个代码块开头
if address != start and address > self.func_start and address < self.func_end:
# 简单的启发式:如果是 dispatcher 的中间,继续跑
# 如果是 real block,停止
# 实际操作中,可以设置步数限制
pass
# 这里的实现难点在于如何精确截获 "下一个真实块"。
# 简单方案:使用 unicorn 直接跑,不依赖 flare_emu 的高级封装,以便控制循环
uc = eh.uc
try:
# 初始化寄存器 context (flare_emu 应该已经做好了环境)
# 仅做局部模拟
eh._init_emulation() # 这是一个 hack,实际需参考 flare_emu 源码
# 重新设置 PC
uc.reg_write(uc_arm64.UC_ARM64_REG_PC, start)
# 注册 hook
h1 = uc.hook_add(unicorn.UC_HOOK_CODE, hook, user_data)
# 跑!限制步数防止死循环 (例如 500 条指令足够跑过 dispatcher)
uc.emu_start(start, self.func_end, count=500)
final_pc[0] = uc.reg_read(uc_arm64.UC_ARM64_REG_PC)
uc.hook_del(h1)
except unicorn.UcError as e:
# 通常是因为跳到了不可读内存或跑飞了
final_pc[0] = uc.reg_read(uc_arm64.UC_ARM64_REG_PC)
return final_pc[0]
def patch(self):
print("[*] 开始 Patch...")
kp = keypatch.Keypatch_Asm()
for addr, block in self.real_blocks.items():
if block.end_addr is None: continue
# 1. 只有一条路
if not block.is_conditional():
target = block.behind_block[True]
if target:
print(f"Patching Unconditional: {hex(block.end_addr)} -> {hex(target)}")
self.apply_patch(kp, block.end_addr, f"B {hex(target)}")
# 2. 有两条路 (CSEL)
else:
csel_addr, cond = block.csel_instr
target_true = block.behind_block[True]
target_false = block.behind_block[False]
if target_true and target_false:
print(f"Patching Conditional ({cond}): {hex(csel_addr)}")
# 逻辑:
# CSEL Rd, Rn, Rm, COND
# 如果 COND 成立 -> 选 Rn -> 对应 target_true
# 故:B.COND target_true
patch_asm = f"B.{cond} {hex(target_true)}"
self.apply_patch(kp, csel_addr, patch_asm)
# 紧接着 CSEL 后面(或者在 Block 结尾 B 的位置)Patch 跳转到 False
# 原地 CSEL 被改成 B.COND 了 (4字节),下一条指令如果是 STR 或 B,改成 B target_false
# 通常我们在 Block 结尾的 B 处 patch False 路径
self.apply_patch(kp, block.end_addr, f"B {hex(target_false)}")
# 中间的垃圾指令(如 STR Wx, [SP])可以 NOP 掉,也可以不管,反正跳走了
def apply_patch(self, kp, addr, asm):
try:
encoding, count = kp.assemble(asm, addr)
if encoding:
idc.patch_bytes(addr, bytes(encoding))
print(f" Success: {asm}")
else:
print(f" Fail to assemble: {asm}")
except Exception as e:
print(f" Error: {e}")
# 用法
# d = OLLVMDeobfuscator(0x123456)
# d.run_emulation()
# d.patch()
x86
eflags辅助类,用更直观的方式来修改 CPU 的状态。
class X86Flags:
# EFLAGS 位的掩码
CF = 1 << 0
PF = 1 << 2
AF = 1 << 4
ZF = 1 << 6
SF = 1 << 7
OF = 1 << 11
@staticmethod
def get_flags_for_cond(cond_str):
"""
根据条件码返回 (mask, value)
mask: 需要修改的位
value: 修改后的值
"""
f = X86Flags
# 仅列举常用
mapping = {
"E": (f.ZF, f.ZF), # Equal (ZF=1)
"Z": (f.ZF, f.ZF),
"NE": (f.ZF, 0), # Not Equal (ZF=0)
"NZ": (f.ZF, 0),
"S": (f.SF, f.SF), # Sign (SF=1)
"NS": (f.SF, 0),
"G": (f.ZF|f.SF|f.OF, 0),# Greater (ZF=0, SF=OF) -> 也就是 ZF=0 且 SF=0,OF=0
"GE": (f.SF|f.OF, 0), # Greater or Equal (SF=OF)
"L": (f.SF|f.OF, f.SF), # Less (SF!=OF) -> SF=1, OF=0
"LE": (f.ZF|f.SF|f.OF, f.ZF|f.SF), # Less or Equal (ZF=1 or SF!=OF)
"A": (f.CF|f.ZF, 0), # Above (CF=0, ZF=0)
"B": (f.CF, f.CF), # Below (CF=1)
}
# 注意:这里简化了 G/L 的设置,只要满足一种情况即可让条件成立
return mapping.get(cond_str.upper(), (0, 0))
根据上一个arm改的
import flare_emu
import idaapi
import idc
import unicorn
import unicorn.x86_const as uc_x86
import keypatch
class X86OLLVMDeobfuscator:
def __init__(self, func_start):
self.func_start = func_start
self.func_end = idc.find_func_end(func_start)
self.real_blocks = {}
# 映射 conditions 到 EFLAGS 修改逻辑
self.cond_map = {
"e": (1<<6, 1<<6), # ZF=1
"z": (1<<6, 1<<6),
"ne": (1<<6, 0), # ZF=0
"nz": (1<<6, 0),
"g": (1<<6|1<<7|1<<11, 0), # ZF=0, SF=0, OF=0 (简单满足 G)
"le": (1<<6, 1<<6), # ZF=1 (简单满足 LE)
# ... 其他需要根据具体汇编补充
}
def set_eflags(self, uc, mask, value):
"""修改 EFLAGS 寄存器"""
eflags = uc.reg_read(uc_x86.UC_X86_REG_EFLAGS)
eflags &= ~mask # 清除相关位
eflags |= value # 设置新值
uc.reg_write(uc_x86.UC_X86_REG_EFLAGS, eflags)
def is_real_block(self, start, end):
"""判断是否为真实块"""
if start == self.func_start: return True
# x86 块通常以 JMP 结尾跳回分发器,或者 RET
last_mnem = idc.print_insn_mnem(end)
if last_mnem.startswith("ret"): return True
if last_mnem.startswith("jmp"):
# 检查块内是否有 CMOV 指令
curr = end
for _ in range(10): # 向前回溯
curr = idc.prev_head(curr)
if curr < start: break
mnem = idc.print_insn_mnem(curr)
if mnem.startswith("cmov"):
return True
return False
def get_cmov_condition(self, addr):
"""从指令中提取条件后缀,例如 cmovne -> ne"""
mnem = idc.print_insn_mnem(addr)
if mnem.startswith("cmov"):
return mnem[4:] # 返回 ne, e, g 等
return None
def run_emulation(self):
print("开始 x86 模拟...")
eh = flare_emu.EmuHelper()
# 寻找所有真实块 (简化版:遍历所有块进行判断)
flow = idaapi.FlowChart(idaapi.get_func(self.func_start))
for block in flow:
# 获取块的最后一条指令地址
end_addr = idc.prev_head(block.end_ea)
if self.is_real_block(block.start_ea, end_addr):
print(f"分析真实块: {hex(block.start_ea)}")
# 寻找 CMOV
cmov_addr = 0
cond = None
curr = end_addr
for _ in range(10):
curr = idc.prev_head(curr)
mnem = idc.print_insn_mnem(curr)
if mnem.startswith("cmov"):
cmov_addr = curr
cond = self.get_cmov_condition(curr)
break
if cmov_addr:
print(f" 发现条件分支: cmov{cond} at {hex(cmov_addr)}")
# 这里的处理逻辑与 ARM 类似:
# 1. 强制条件成立 (修改 EFLAGS) -> 跑出 True 分支目标
# 2. 强制条件失败 (修改 EFLAGS) -> 跑出 False 分支目标
# 模拟获取 True Target
mask, val = self.cond_map.get(cond, (0,0))
target_true = self.emulate_path(eh, block.start_ea, cmov_addr, mask, val)
# 模拟获取 False Target (取反比较麻烦,通常只要让条件不满足即可)
# 简单做法:如果 True 是 ZF=1,False 就设 ZF=0
# 注意:mask 不变,val 取反需要位运算技巧,或者手动硬编码
val_false = val ^ mask # 简单的翻转尝试
target_false = self.emulate_path(eh, block.start_ea, cmov_addr, mask, val_false)
print(f" True -> {hex(target_true) if target_true else 'None'}")
print(f" False -> {hex(target_false) if target_false else 'None'}")
# 记录下来准备 Patch...
pass
else:
# 无条件跳转,直接跑
target = self.emulate_path(eh, block.start_ea, 0, 0, 0)
print(f" Unconditional -> {hex(target) if target else 'None'}")
def emulate_path(self, eh, start_addr, hook_addr, flag_mask, flag_val):
"""模拟执行并返回下一跳地址"""
result = [None]
def hook(uc, address, size, user_data):
# 在 CMOV 处修改标志位
if address == hook_addr and flag_mask != 0:
self.set_eflags(uc, flag_mask, flag_val)
# 停止条件:跳出了当前函数范围,或者跳到了另一个已知块
# 这里简化处理:如果是 JMP 指令且目标不在当前块内,记录目标
pass
# 实际需要结合 flare_emu 的 iterate 或者手动控制 step
# 这里只是伪代码示意,实际需要像 ARM 版那样设置 uc.emu_start
return 0xDEADBEEF # 占位符
def patch(self):
# x86 Patch 逻辑
# CMOVcc -> Jcc (条件跳转)
# JMP Dispatcher -> NOP
pass
# 注意:x86 的指令长度不定长!Patch 时非常痛苦。
# CMOV (3-4 bytes) 可能比 JZ (2 bytes short, 6 bytes near) 长或短。
# 必须使用 Keypatch 重新汇编,并确保不覆盖后续指令,或者用 NOP 填充多余字节。
注:容易指令不等长,要patch
angr
angr速通
文章栏:
文章,环境搭建加实战去除
源码分析多一些
挺全的,包括unicorn配置加用法
大框架
好理解很多
代码控制流混淆,有点杂,参考
文章,上面某一篇的参考
unicorn使用










太强了,re大手
我驳回