-
-
[原创]LLVM Pass编写及去除 —— 间接跳转
-
发表于: 1天前 609
-
间接跳转是通过将原本的jmp addr指令,替换成jmp reg,从而混淆块与块之间的跳转关系的方法。
编写
一个最基础的间接跳转pass如下
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Transforms/Utils/ValueMapper.h"
#include "llvm/Transforms/Utils/Cloning.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/IRBuilder.h"
#include "SplitBasicBlock.h"
#include "Utils.h"
#include <vector>
#include <cstdlib>
#include <ctime>
using std::vector;
using namespace llvm;
static cl::opt<bool> enableIndBr("indbr_num",cl::init(1),cl::desc("Indirect branch obfuscation"));
namespace{
class IndirectBranch : public FunctionPass{
public:
static char ID;
IndirectBranch() :FunctionPass(ID){}
bool runOnFunction(Function &F);
void applyIndirectJump(BasicBlock *BB);
};
}
bool IndirectBranch::runOnFunction(Function &F)
{
vector<BasicBlock *> origBB;
for(BasicBlock &BB : F)
{
origBB.push_back(&BB);
}
for(BasicBlock *BB : origBB)
{
applyIndirectJump(BB);
}
return true;
}
void IndirectBranch::applyIndirectJump(BasicBlock *BB)
{
Instruction *terminator = BB->getTerminator();
BranchInst *br = dyn_cast<BranchInst>(terminator);
if(!br) return;
// 创建IRBuilder对象,在终结指令前插入
IRBuilder<> builder(terminator);
// 无条件跳转
if(br->isUnconditional())
{
// 获取目标块
BasicBlock *targetBB = br->getSuccessor(0);
// 获取目标地址
BlockAddress *targetAddr = BlockAddress::get(targetBB);
// 创建间接跳转指令(目标地址,可能地址数)
IndirectBrInst *indirectBr = builder.CreateIndirectBr(targetAddr,1);
// 添加有效目标块
indirectBr->addDestination(targetBB);
// 删除原指令
br->eraseFromParent();
}
// 有条件跳转
else if(br->isConditional())
{
Value *cond = br->getCondition();
// 获取真假目标块
BasicBlock *trueBB = br->getSuccessor(0);
BasicBlock *falseBB = br->getSuccessor(1);
// 真假目标地址
BlockAddress *trueAddr = BlockAddress::get(trueBB);
BlockAddress *falseAddr = BlockAddress::get(falseBB);
// 条件选择指令
Value *selectedAddr = builder.CreateSelect(cond,trueAddr,falseAddr);
// 创建间接跳转指令
IndirectBrInst *indirectBr = builder.CreateIndirectBr(selectedAddr,2);
// 添加可能的目标块
indirectBr->addDestination(trueBB);
indirectBr->addDestination(falseBB);
// 删除原指令
br->eraseFromParent();
}
}
char IndirectBranch::ID = 0;
static RegisterPass<IndirectBranch> X("indbr","Indirect Branch Obfuscation Pass");
经典的寄存器跳转,直接给rax赋值。一下就能看出跳转的地址

为了让混淆的效果更好,可以对地址进行加解密操作
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Transforms/Utils/ValueMapper.h"
#include "llvm/Transforms/Utils/Cloning.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/IRBuilder.h"
#include "SplitBasicBlock.h"
#include "Utils.h"
#include <vector>
#include <cstdlib>
#include <ctime>
#include <string>
using std::vector;
using namespace llvm;
static cl::opt<bool> enableIndBr("indbr_num",cl::init(1),cl::desc("Indirect branch obfuscation"));
namespace{
class IndirectBranch : public FunctionPass{
public:
static char ID;
IndirectBranch() :FunctionPass(ID){
srand(time(0));
}
bool runOnFunction(Function &F);
void applyIndirectJump(BasicBlock *BB);
Value *createDecodedBlockAddress(IRBuilder<> &builder, BasicBlock *targetBB, uint64_t secretKey);
};
}
bool IndirectBranch::runOnFunction(Function &F)
{
INIT_CONTEXT(F);
vector<BasicBlock *> origBB;
for(BasicBlock &BB : F)
{
origBB.push_back(&BB);
}
for(BasicBlock *BB : origBB)
{
applyIndirectJump(BB);
}
return true;
}
Value *IndirectBranch::createDecodedBlockAddress(IRBuilder<> &builder, BasicBlock *targetBB, uint64_t secretKey)
{
Module *M = targetBB->getModule();
const DataLayout &DL = M->getDataLayout();
IntegerType *intPtrTy = DL.getIntPtrType(*CONTEXT);
Type *int8PtrTy = Type::getInt8PtrTy(*CONTEXT);
// 目标地址
BlockAddress *targetAddr = BlockAddress::get(targetBB);
// 转为Int便于计算
Constant *targetInt = ConstantExpr::getPtrToInt(targetAddr,intPtrTy);
Constant *keyVal = ConstantInt::get(intPtrTy,secretKey);
// 加密后的地址
Constant *encodedInit = ConstantExpr::getAdd(targetInt,keyVal);
// 生成全局变量名称
std::string globalName = targetBB->getParent()->getName().str() + ".indbr.addr";
// 创建全局变量 初始值为加密后的值
GlobalVariable *encodedSlot = new GlobalVariable(
*M,
intPtrTy,
false,
GlobalValue::PrivateLinkage,
encodedInit,
globalName
);
// 加载全局变量值
LoadInst *encodedVal = builder.CreateLoad(intPtrTy,encodedSlot);
// 设置不优化
encodedVal->setVolatile(true);
// 创建减法指令
Value *decodedVal = builder.CreateSub(encodedVal,keyVal);
return builder.CreateIntToPtr(decodedVal,int8PtrTy);
}
void IndirectBranch::applyIndirectJump(BasicBlock *BB)
{
Instruction *terminator = BB->getTerminator();
BranchInst *br = dyn_cast<BranchInst>(terminator);
if(!br) return;
IRBuilder<> builder(terminator);
// 条件跳转
if(br->isUnconditional())
{
BasicBlock *targetBB = br->getSuccessor(0);
uint64_t secretKey = rand();
Value *decPtr = createDecodedBlockAddress(builder,targetBB,secretKey);
IndirectBrInst *indirectBr = builder.CreateIndirectBr(decPtr,1);
indirectBr->addDestination(targetBB);
br->eraseFromParent();
}
// 非条件跳转
else if(br->isConditional())
{
Value *cond = br->getCondition();
// 获取真假目标块
BasicBlock *trueBB = br->getSuccessor(0);
BasicBlock *falseBB = br->getSuccessor(1);
uint64_t secretKey = rand();
// 获取解密后的值
Value *decPtr_true = createDecodedBlockAddress(builder,trueBB,secretKey);
Value *decPtr_false = createDecodedBlockAddress(builder,falseBB,secretKey);
// 创建选择指令
Value *selectedAddr = builder.CreateSelect(cond,decPtr_true,decPtr_false);
// 创建间接跳转指令
IndirectBrInst *indirectBr = builder.CreateIndirectBr(selectedAddr,2);
// 添加可能的两个目标块
indirectBr->addDestination(trueBB);
indirectBr->addDestination(falseBB);
br->eraseFromParent();
}
}
char IndirectBranch::ID = 0;
static RegisterPass<IndirectBranch> X("indbr","Indirect Branch Obfuscation Pass");
现在给寄存器赋的值需要通过全局变量去运算

当然间接跳转混淆的方式多种多样,这里只是基础的两种Pass编写。
去除
关于去除:
有两种去除混淆的方式
1.通过idc脚本计算进行去除
和之前去除控制流平坦化一样,通过idc脚本匹配特征再进行patch,以上面的样本为例,读取全局变量的值后和一个64位值进行简单运算,可以根据特征jmp rax来定位间接跳转部分
auto jmp_insn = print_insn_mnem(current_addr);
auto jmp_op = print_operand(current_addr,0);
if(jmp_insn == "jmp" && jmp_op == "rax")
然后计算出全局变量的值和加的值
auto add_addr = prev_head(current_addr,start_addr);
auto add_val = Dword(add_addr + 2);
add_val = add_val | 0xFFFFFFFF00000000;
msg("add val = %X\n",add_val);
auto mov_addr = prev_head(add_addr,start_addr);
auto rand_offset = Dword(mov_addr + 3);
auto rand_addr = mov_addr + rand_offset + 7;
msg("rand addr = %X\n",rand_addr);
auto rand_val = Dword(rand_addr);
auto jmp_addr = add_val + rand_val;
最后patch
NopCode(mov_addr,jmp_addr + 2 - mov_addr);
PatchByte(mov_addr,0xE9);
PatchDword(mov_addr + 1,jmp_offset);
完整脚本如下:
static NopCode(Addr, Length)
{
auto i;
for (i = 0; i < Length; i++)
{
PatchByte(Addr + i, 0x90);
}
}
static main()
{
auto current_addr = ;
auto end_addr = ;
auto start_addr = current_addr;
while (current_addr < end_addr && current_addr != BADADDR)
{
auto jmp_insn = print_insn_mnem(current_addr);
auto jmp_op = print_operand(current_addr,0);
if(jmp_insn == "jmp" && jmp_op == "rax")
{
auto add_addr = prev_head(current_addr,start_addr);
auto add_val = Dword(add_addr + 2);
add_val = add_val | 0xFFFFFFFF00000000;
msg("add val = %X\n",add_val);
auto mov_addr = prev_head(add_addr,start_addr);
auto rand_offset = Dword(mov_addr + 3);
auto rand_addr = mov_addr + rand_offset + 7;
msg("rand addr = %X\n",rand_addr);
auto rand_val = Dword(rand_addr);
auto jmp_addr = add_val + rand_val;
auto jmp_offset = jmp_addr - mov_addr - 5;
msg("jmp addr = %X\n",jmp_addr);
msg("jmp offset = %X\n",jmp_offset);
NopCode(mov_addr,jmp_addr + 2 - mov_addr);
PatchByte(mov_addr,0xE9);
PatchDword(mov_addr + 1,jmp_offset);
}
current_addr = next_head(current_addr,end_addr);
}
}
(示例脚本仅为非条件跳转)
运行后可自动将间接跳转改为直接跳转

2.自动下断点trace去除
无论是怎样的间接跳转,执行到最后jmp指令的时候都会计算出值,所以动态获取值无疑是很好的办法。
但是在一个混淆样本中jmp指令往往过多,调试去看值是不行的,所以可以使用idc批量下断点,再执行一遍,直接获取到跳转的值
#include <idc.idc>
// 设置断点Break属性
static SetBptBreak(Address, Enable)
{
auto OldFlag = get_bpt_attr(Address, BPTATTR_FLAGS);
if (Enable == 1)
{
if ((OldFlag & BPT_BRK) == 0)
{
msg("%d\n", OldFlag & BPT_BRK);
OldFlag = OldFlag | BPT_BRK;
}
}
else
{
if ((OldFlag & BPT_BRK) != 0)
{
OldFlag = OldFlag & (~BPT_BRK);
}
}
set_bpt_attr(Address, BPTATTR_FLAGS, OldFlag);
}
// 设置断点Handler
static SetHandlerToBpt(Address, HandlerFuncName, IsBreak)
{
// return回调到Handler
auto Cond = sprintf("return %s();", HandlerFuncName);
if (check_bpt(Address) == (BPTCK_NONE))
{
add_bpt(Address);
}
// 设置Break属性
SetBptBreak(Address, IsBreak);
// 设置断点Conditional
auto Status = set_bpt_cond(Address, Cond);
if (Status == 1)
{
msg("Successfully set conditional bpt at 0x%x -> call %s\n", Address, HandlerFuncName);
}
else
{
msg("Failed to set conditional bpt at 0x%x\n", Address);
}
}
static Handler1()
{
auto fp = fopen("dump.txt", "a");
fprintf(fp, "jmp %X -> %X\n",rip,rax);
fclose(fp);
return 0;
}
static main()
{
auto start_addr = ;
auto end_addr = ;
auto current_addr = start_addr;
while(current_addr < end_addr && current_addr != BADADDR)
{
auto insn_name = print_insn_mnem(current_addr);
auto op = print_operand(current_addr,0);
// 检查到jmp rax指令就对其下断 断点函数为Handler1
if(insn_name == "jmp" && op == "rax")
{
SetHandlerToBpt(current_addr, "Handler1", 0);
}
current_addr = next_head(current_addr,end_addr);
}
}
因为有循环,所以重复内容占了很多,可以写一个python脚本去重
file_path = "dump.txt"
with open(file_path,'r') as f:
lines = [line.rstrip('\n') for line in f]
set_line = set(lines)
for s in set_line:
print(s)
结果如下
jmp 401581 -> 401583
jmp 4015AA -> 4015AC
jmp 4013D4 -> 4013D6
jmp 4015E6 -> 401583
jmp 4013A5 -> 4013A7
jmp 4015CE -> 4015D0
jmp 4014FA -> 4013A7
jmp 4014D9 -> 4014DB
jmp 4013D4 -> 4014FC
jmp 4015AA -> 4015E8
这样我们就获取到了关键的跳转关系表,可通过脚本或ai辅助patch
实例
样本为strange_xor.exe,混淆方式如图

这一段汇编中只有中间的push ebp是真正有用的程序代码,其余全部为间接跳转服务。
最终跳转的计算公式为 0x77 ^ 0x9908 + 0x393017 = 0x39C996
虽然是通过复杂的方式对最终跳转的值进行计算,但因为混淆的方式是固定的:赋值 + 取值 + 赋值 + 跳转,所以可以写idc脚本来匹配特征去混淆
脚本如下
#include <idc.idc>
static NopCode(Addr, Length)
{
auto i;
for (i = 0; i < Length; i++)
{
PatchByte(Addr + i, 0x90);
}
}
static main()
{
auto current_addr = 0x40C996;
auto end_addr = current_addr + 0x10000;
// 初始保存的ecx值
auto ecx_data = 0x35;
while (current_addr != BADADDR && current_addr < end_addr)
{
auto insn_name = Byte(current_addr);
auto op = print_operand(current_addr, 0);
auto op2 = print_operand(current_addr, 1);
auto i;
///msg("addr: %X\n",current_addr);
// 找到pusha这条指令
if( insn_name == 0x60)
{
auto asm_name = Byte(current_addr + 1);
auto call_asm_name = Byte(current_addr + 2);
auto call_op = Dword(current_addr + 3);
//msg("pusha\n");
// jmp ebx
// 接下来的两条汇编是否为pushf + call $5
if( asm_name == 0x9C && call_asm_name == 0xE8 && call_op == 0)
{
auto pop_addr = current_addr + 7;
auto xor_addr = current_addr + 16;
//msg("xor_addr : %X\n",xor_addr);
auto xor_data = Dword(xor_addr + 2);
auto offest = ecx_data ^ xor_data;
//msg("xor_data : %X\n",xor_data);
// 计算出跳转的值
auto jmp_addr = (offest + pop_addr) & 0xFFFFFFFF;
msg("jmp_addr : %X\n",jmp_addr);
auto ecx_addr = xor_addr + 9;
// 计算出跳转偏移
auto jmp_offest = jmp_addr - ecx_addr - 5;
// 顺带保存下一次运算的ecx值
ecx_data = Dword(ecx_addr + 1);
msg("ecx_data : %X\n",ecx_data);
NopCode(current_addr,39);
//msg("nop_addr: %X len: %X\n",current_addr,39);
PatchByte(ecx_addr,0xE9);
PatchDword(ecx_addr + 1,jmp_offest);
//msg("jmp %X\n",jmp_addr);
for(i = 0; i < 54; i++)
{
create_insn(jmp_addr + i);
}
current_addr = jmp_addr;
msg("current_addr: %X\n",current_addr);
auto next_call_insn = Byte(current_addr);
auto next_call_op = Dword(current_addr + 1);
if( next_call_insn == 0xE8 && next_call_op == 0 )
{
auto pop_byte = Byte(current_addr + 5);
if( pop_byte == 0x5B)
{
NopCode(current_addr - 1,12);
}
}
}
}
auto check_byte = Byte(current_addr);
if( check_byte == 0x9D || check_byte == 0x61 || check_byte == 0x60 || check_byte == 0x9C)
{
NopCode(current_addr,1);
}
current_addr = current_addr + 1;
}
}
效果如下

只留下了有用的汇编和跳转地址,其余全部被nop
函数可正常反编译

当然除写idc脚本之外还可以下断点动态trace,只需要将刚才的脚本稍加修改
static Handler1()
{
auto fp = fopen("dump.txt", "a");
fprintf(fp, "jmp %X -> %X\n",eip,ebx);
fclose(fp);
return 0;
}
static main()
{
auto start_addr = 0x403001;
auto end_addr = 0x413C98;
auto current_addr = start_addr;
while(current_addr < end_addr && current_addr != BADADDR)
{
auto insn_name = print_insn_mnem(current_addr);
auto op = print_operand(current_addr,0);
if(insn_name == "jmp" && op == "ebx")
{
SetHandlerToBpt(current_addr, "Handler1", 0);
}
current_addr = next_head(current_addr,end_addr);
}
}
就可以得到跳转表。部分跳转表如下
jmp 4068F4 -> 41207E
jmp 404D77 -> 40C281
jmp 40E202 -> 407CB0
jmp 404155 -> 410C62
jmp 40A019 -> 40AA11
jmp 4057F3 -> 4139A5
jmp 40BFA4 -> 41212A
jmp 40B340 -> 4036FB
jmp 413AC2 -> 406A8F
jmp 40C789 -> 40D40E
jmp 40E652 -> 407150
jmp 40FFC7 -> 40CBDA
jmp 40A6BC -> 404A8A
jmp 40B8CD -> 4076D9
jmp 406C5F -> 409DFB
jmp 408170 -> 40A32B
jmp 40C74F -> 4127E1
jmp 40DBE5 -> 412CFA
jmp 40D4EB -> 40C7FE
jmp 411949 -> 413489
符合我们刚才patch过的代码。
最后
以上就是关于间接跳转的Pass编写,还有混淆去除的方法,当然都是我个人拙见,如果有问题或其他思路欢迎各位大佬和我交流☆*: .。. o(≧▽≦)o .。.:*☆
最后的最后,虽然有这些去除混淆的方法,但是实际应用是还是要逐个样本分析,没有一把梭的方法。所以我想借助agent来辅助去混淆,这需要很多混淆样本。所以如果各位大佬有小型混淆样本 / CTF题目,都可以分享给我orz。
qq:2060824185
[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!
赞赏
- [原创]LLVM Pass编写及去除 —— 间接跳转 609
- [原创]Android风险环境检测 —— 签名校验 46586
- [原创]Android一二三代壳实现 40972
- [原创]LLVM Pass编写及去除——虚假控制流 9549
- [原创]LLVM Pass编写及去除 —— 控制流平坦化 12600