X86指令混淆之函数分析和代码块粉碎
字数 1083 2025-08-24 20:49:31
X86指令混淆之函数分析和代码块粉碎教学文档
一、指令混淆概述
指令混淆是一种通过改变代码结构而不改变其功能的技术,主要用于增加逆向分析的难度。本文档将详细介绍基于X86架构的函数分析和代码块粉碎技术。
二、函数分析技术
2.1 基本概念
函数分析是指从二进制代码中识别出函数的起始地址和大小,类似于IDA的sub_xxx标注功能。由于不同编译器生成的函数特征各异,准确识别函数具有挑战性。
2.2 函数识别算法
2.2.1 基本规则
-
向下跳转记录:
- 记录所有jcc指令跳转目的地址
- 保留跳转目的地址最大的一个
-
ret结尾识别:
- 遇到ret指令时比较当前地址与最大跳转地址
- 当前地址>跳转地址:函数结束
- 当前地址<跳转地址:从跳转地址继续分析
-
其他结尾判断条件:
- 向上跳转的无条件jmp指令
- nop指令(0x90)
- 连续两个以上int3指令(0xCC)
- add [eax], al指令(0x00,0x00....)
- 第一条指令是无条件jmp
- 下一条指令是另一个函数的开始特征
-
Call指令处理:
- 遇到call立即数指令,将目的地址加入待分析集合
-
其他启发式规则:
- 根据编译器特定特征
- 利用调试信息(如果存在)
2.2.2 代码实现
FunctionNode X86Analysis::AnalysisFunction(DWORD64 begin, DWORD bufferSize,
map<DWORD64, FunctionNode>* functionMap,
map<DWORD64, FunctionNode>* excludeMap,
DWORD64 pc) {
ud_t ud;
ud_init(&ud);
ud_set_mode(&ud, 32);
ud_set_syntax(&ud, UD_SYN_INTEL);
ud_set_input_buffer(&ud, (uint8_t*)begin, bufferSize);
ud_set_pc(&ud, pc);
InstrFlowNode jcc_max, jcc_flow;
memset(&jcc_max, 0, sizeof(jcc_max));
while(ud_disassemble(&ud)) {
jcc_flow = GetInstrNode(&ud);
if(jcc_flow.isInvalid()) {
// 处理无效指令(可能花指令)
FunctionNode function;
function.memoryFileAddress = begin;
function.loadImageAddress = pc;
function.size = begin - (jcc_flow.memoryFileAddress - jcc_flow.insnLen);
return function;
}
if(jcc_flow.isJcc && (jcc_flow.operatorType == UD_OP_JIMM)) {
if(jcc_flow.jmpMemoryFileAddress > jcc_max.jmpMemoryFileAddress) {
jcc_max = jcc_flow;
}
}
switch(jcc_flow.type) {
case UD_Ijmp:
// 处理无条件跳转
break;
case UD_Icall:
// 处理call指令
break;
case UD_Iret:
// 处理ret指令
break;
case UD_Inop:
case UD_Iint3:
// 处理nop和int3
break;
case UD_Iadd:
// 处理add指令
break;
default:
// 其他指令
}
// 检查下一条指令是否是函数开始特征
DWORD64 ptr = jcc_flow.memoryFileAddress + jcc_flow.insnLen;
if(LookNextBegin(ptr)) {
// 添加到待分析集合
if((excludeMap != nullptr) && (!excludeMap->empty())) {
if(excludeMap->find(ptr) == excludeMap->end()) {
FunctionNode node(ptr, jcc_flow.loadImageAddress + jcc_flow.insnLen);
functionMap->operator[](ptr) = node;
}
} else {
FunctionNode node(ptr, jcc_flow.loadImageAddress + jcc_flow.insnLen);
functionMap->operator[](ptr) = node;
}
return FunctionNode(begin, pc, jcc_flow.memoryFileAddress + jcc_flow.insnLen - begin);
}
}
return FunctionNode(begin, pc, bufferSize);
}
三、代码块粉碎技术
3.1 基本思路
- 以函数为单位进行混淆
- 抽取函数所有指令
- 申请新空间并随机乱序放置指令
- 增加指令保证执行顺序不变
- 修复跳转关系和重定位表
3.2 实现步骤
3.2.1 指令预处理
vector<InstrFlowNode> instrbox = this->m_Analysis.InstrExtract(
function.memoryFileAddress,
function.size,
function.loadImageAddress);
3.2.2 指令混淆块生成
class ObfuscateInstr {
public:
DWORD64 memoryAddress = 0; // 混淆块在新空间的地址
DWORD64 virtulAddress = 0; // 混淆块的VA
DWORD prexCodeSize = 0; // 原始功能指令前面的指令长度
char* code = nullptr; // 混淆块数据
DWORD size = 0; // 混淆块大小
};
vector<ObfuscateInstr> chunkbox;
x86::Gp registers[] = {x86::eax, x86::ebx, x86::ecx, x86::edx, x86::esi, x86::edi};
bool first = true;
for(auto instr : instrbox) {
CodeHolder code;
code.init(CodeInfo(ArchInfo::kIdX86));
x86::Assembler assember(&code);
// 原始功能指令前面
if(!first) {
assember.pop(registers[index]);
assember.popfd(); // 保护标志位
} else {
first = false;
}
DWORD prexCodeSize = code.sectionById(0)->buffer().size();
// 处理原始功能指令
if((instr.isJcc || instr.isCall) && (instr.operatorType == UD_OP_JIMM)) {
// 处理跳转和call指令
char* new_jcc = new char[6];
DWORD jcc_padding = 0xAAAAAAAA; // 跳转偏移占位
WORD jcc_opcode = this->jcc_long_opcode[instr.type];
// 特殊处理call和jmp指令
// ...
assember.embed(new_jcc, insn_len);
delete[] new_jcc;
} else {
assember.embed((char*)instr.memoryFileAddress, instr.insnLen);
}
// 原始功能指令后面
assember.pushfd(); // 保护标志位
index = this->GetRandomKey() % sizeof(registers)/sizeof(x86::Gp);
assember.mov(x86::dword_ptr(x86::esp, -4), registers[index]);
assember.add(x86::esp, -4);
Label label = assember.newLabel();
assember.call(label);
assember.bind(label);
assember.pop(registers[index]);
int num = -(13 + prexCodeSize + insn_len);
assember.add(registers[index], num);
assember.add(registers[index], 0xdeadbeaf); // 占位,需要修复
assember.push(registers[index]);
assember.ret();
// 保存混淆块
CodeBuffer& buffer = code.sectionById(0)->buffer();
ObfuscateInstr instrchunk;
instrchunk.code = new char[buffer.size()];
::memcpy(instrchunk.code, buffer.data(), buffer.size());
instrchunk.size = buffer.size();
instrchunk.prexCodeSize = prexCodeSize;
chunkbox.push_back(instrchunk);
}
3.2.3 随机分配指令块
DWORD buffer_index = 0;
DWORD buffer_size = function.size * 100; // 申请100倍原始函数空间
char* buffer = new char[buffer_size];
map<DWORD64, DWORD64> orign_chunk_map; // 原始地址到混淆后地址的映射
vector<int> indexTable; // chunkbox的索引表
// 随机分配指令块
while(!indexTable.empty()) {
int key = this->GetRandomKey() % indexTable.size();
int index = indexTable[key];
memcpy(buffer + buffer_index, chunkbox[index].code, chunkbox[index].size);
DWORD64 addr = (DWORD64)buffer + buffer_index;
DWORD64 va = VirtulAddress + (addr - (DWORD64)buffer);
chunkbox[index].memoryAddress = addr;
chunkbox[index].virtulAddress = va;
orign_chunk_map.insert(pair<DWORD64, DWORD64>(
instrbox[index].memoryFileAddress, addr));
buffer_index += chunkbox[index].size;
// 添加随机垃圾数据
int junk_size = this->GetRandomKey() % 16 + 6;
this->GetRandomBytes(buffer + buffer_index, junk_size);
buffer_index += junk_size;
// 从索引表中移除已处理的指令块
auto iter = indexTable.begin();
iter += key;
indexTable.erase(iter);
}
3.2.4 修复指令执行顺序和跳转
DWORD offset_flag = 0xdeadbeaf;
DWORD jcc_flag = 0xAAAAAAAA;
for(int i = 0; i < chunkbox.size(); i++) {
char* begin = (char*)chunkbox[i].memoryAddress;
char* end = (char*)chunkbox[i].memoryAddress + chunkbox[i].size;
DWORD offset;
if(i < chunkbox.size() - 1) {
offset = chunkbox[i+1].memoryAddress - chunkbox[i].memoryAddress;
} else {
// 最后一条指令随机跳转到前面的指令块
int k = this->GetRandomKey() % (chunkbox.size() - 1);
offset = chunkbox[k].memoryAddress - chunkbox[i].memoryAddress;
}
// 修复相邻指令执行顺序
char* ptr = this->SearchBytes(begin, end, (char*)&offset_flag, sizeof(DWORD));
memcpy(ptr, &offset, 4);
// 修复jcc指令跳转偏移
if((instrbox[i].isJcc || instrbox[i].isCall) &&
(instrbox[i].operatorType == UD_OP_JIMM)) {
char* ptr = this->SearchBytes(begin, end, (char*)&jcc_flag, sizeof(DWORD));
DWORD64 addr = 0;
if((instrbox[i].type == UD_Icall) ||
(orign_chunk_map.count(instrbox[i].jmpMemoryFileAddress) <= 0)) {
// 处理call或跳转到函数外的情况
addr = instrbox[i].jmpLoadImageAddress;
DWORD64 va = VirtulAddress + ((DWORD64)begin + chunkbox[i].prexCodeSize - (DWORD64)buffer);
offset = addr - va - 5; // jmp/call指令长度是5
} else {
// 处理函数内跳转
addr = orign_chunk_map[instrbox[i].jmpMemoryFileAddress] + chunkbox[i].prexCodeSize;
if(instrbox[i].type == UD_Ijmp) {
offset = addr - ((DWORD64)begin + chunkbox[i].prexCodeSize) - 5;
} else {
offset = addr - ((DWORD64)begin + chunkbox[i].prexCodeSize) - 6;
}
}
memcpy(ptr, &offset, 4);
}
}
3.2.5 重定位修复
// 保存新的重定位项
for(auto relocInstr : relocInstrBox) {
DWORD index = relocInstr.index;
DWORD64 relocVa = chunkbox[index].virtulAddress + chunkbox[index].prexCodeSize + relocInstr.off;
DWORD rva = relocVa - this->m_pefile.getOptionHeader()->ImageBase;
DWORD orignRva = instrbox[index].loadImageAddress + relocInstr.off -
this->m_pefile.getOptionHeader()->ImageBase;
WORD typeOffset = rva % 0x1000; // 新的typeOffset
DWORD newPage = rva - typeOffset;
typeOffset |= ((WORD)(relocInstr.type << 12));
RelocFixer fixer;
fixer.orignRva = orignRva;
fixer.newPage = newPage;
fixer.typeOffset = typeOffset;
relocFixBox.push_back(fixer);
}
3.2.6 修改原函数入口
char* begin = (char*)function.memoryFileAddress;
char opcode[] = {0xe9, 00, 00, 00, 00, 0xc3}; // jmp imm + ret
DWORD64 firstCodeVa = VirtulAddress + (chunkbox[0].memoryAddress - (DWORD64)buffer);
DWORD jmpoffset = firstCodeVa - function.loadImageAddress - 5;
memcpy(opcode+1, &jmpoffset, 4);
memcpy(begin, opcode, 6);
begin += 6;
this->GetRandomBytes(begin, function.size - 6); // 剩余空间填充随机数据
四、PE文件处理
4.1 段合并与新增
- 找到reloc段的前一个段
- 向下合并reloc段
- 创建新的text段存放混淆代码
- 在新text段后面创建reloc段
- 修复重定位信息
4.2 重定位处理
混淆前必须扫描记录函数的所有重定位信息,混淆过程中将原始重定位信息和新重定位信息关联,以便后续修复。
五、总结
本文介绍了一种基于X86架构的函数分析和代码块粉碎技术,主要包括:
- 函数识别算法:通过多种启发式规则识别二进制文件中的函数
- 指令粉碎技术:将函数指令随机乱序并保证执行顺序不变
- 跳转修复:维护原始跳转关系并在混淆后修复
- 重定位处理:正确处理混淆后的重定位信息
- PE文件处理:合理组织段结构以容纳混淆代码
该技术可以有效增加逆向分析的难度,阻止IDA等工具的F5伪代码分析功能,但需要注意这只是一种基础的混淆方法,高级逆向工程师仍然可以通过动态分析等手段还原原始逻辑。