间接跳转是通过将原本的 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对象,在终结指令前插入
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);
// 转为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 来定位间接跳转部分

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>

// 设置断点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 脚本去重

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;
// 初始保存的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,只需要将刚才的脚本稍加修改

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