间接跳转是通过将原本的 jmp addr 指令,替换成 jmp reg,从而混淆块与块之间的跳转关系的方法。
# 编写
一个最基础的间接跳转 pass 如下
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 #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<> 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 赋值。一下就能看出跳转的地址
为了让混淆的效果更好,可以对地址进行加解密操作
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 #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); 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 来定位间接跳转部分
1 2 3 auto jmp_insn = print_insn_mnem (current_addr);auto jmp_op = print_operand (current_addr,0 );if (jmp_insn == "jmp" && jmp_op == "rax" )
然后计算出全局变量的值和加的值
1 2 3 4 5 6 7 8 9 10 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
1 2 3 NopCode (mov_addr,jmp_addr + 2 - mov_addr);PatchByte (mov_addr,0xE9 );PatchDword (mov_addr + 1 ,jmp_offset);
完整脚本如下:
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 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 批量下断点,再执行一遍,直接获取到跳转的值
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include <idc.idc> 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); } static SetHandlerToBpt (Address, HandlerFuncName, IsBreak) { auto Cond = sprintf ("return %s();" , HandlerFuncName); if (check_bpt (Address) == (BPTCK_NONE)) { add_bpt (Address); } SetBptBreak (Address, IsBreak); 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 ); if (insn_name == "jmp" && op == "rax" ) { SetHandlerToBpt (current_addr, "Handler1" , 0 ); } current_addr = next_head (current_addr,end_addr); } }
因为有循环,所以重复内容占了很多,可以写一个 python 脚本去重
1 2 3 4 5 6 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)
结果如下
1 2 3 4 5 6 7 8 9 10 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 脚本来匹配特征去混淆
脚本如下
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #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 ; 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; 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 ); if ( asm_name == 0x9C && call_asm_name == 0xE8 && call_op == 0 ) { auto pop_addr = current_addr + 7 ; auto xor_addr = current_addr + 16 ; auto xor_data = Dword (xor_addr + 2 ); auto offest = ecx_data ^ 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_data = Dword (ecx_addr + 1 ); msg ("ecx_data : %X\n" ,ecx_data); NopCode (current_addr,39 ); PatchByte (ecx_addr,0xE9 ); PatchDword (ecx_addr + 1 ,jmp_offest); 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,只需要将刚才的脚本稍加修改
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 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); } }
就可以得到跳转表。部分跳转表如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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