随着网络环境的不断变化,恶意软件与病毒变种开始层出不穷,给安全研究和病毒分析工作带来了不小的挑战。比如在病毒逆向分析过程中,有高效代码保护能力的强壳加密技术,这种技术通常通过加密、压缩和虚拟化等手段隐藏或混淆程序代码,导致分析人员难以获得有效的解密方法,进而使得对病毒样本的调试、分析和修改工作变得复杂。常见的强壳如Themida、VMProtect,以及越来越广泛的自定义混淆壳(代码虚拟化),通常采用多种复杂的保护策略,包括但不限于代码加密、API钩子、反调试、反虚拟机技术等,这些都一定程度上增加了病毒逆向分析的难度。
病毒在升级,但我们的技术手段也在不断升级。比如在病毒逆向分析的过程中,分析者可通过追踪解析被保护程序或加壳程序。通过追踪程序的执行过程,分析者可以观察到程序运行时的行为,逐步揭示其加壳或加密的方式。追踪可通过多种方法进行,例如调试器、动态分析工具、代码注入等,这些方法有助于分析者跟踪程序的调用栈、数据流以及了解加壳后的解密过程,进而破除壳保护,恢复程序的原始功能和代码。
一、
Pin
在 JIT 模式下,实际运行的是新生成的中间代码。原始代码仅作为参考,在生成代码时,Pin 给用户提供了注入自己代码(插桩)的机会。
回调函数
INS_AddInstrumentFunction (INSCALLBACK fun, VOID *val) 注册以指令粒度插桩的函数
TRACE_AddInstrumentFunction (TRACECALLBACK fun, VOID *val) 注册以 trace 粒度插桩的函数 (基本块插桩)
RTN_AddInstrumentFunction (RTNCALLBACK fun, VOID *val) 注册以 routine 粒度插桩的函数,函数级别的插桩需要符号信息
IMG_AddInstrumentFunction (IMGCALLBACK fun, VOID *val) 注册以 image 粒度插桩的函数
PIN_AddFiniFunction (FINICALLBACK fun, VOID *val) 注册在应用程序退出前执行的回调函数
PIN_AddDetachFunction (DETACHCALLBACK fun, VOID *val) 注册在 Pin 通过PIN_Detach()函数放弃对应用程序的控制权限之前执行的函数,一个进程只调用一次,可以被任何线程调用
BLL(基本块)
BBL(Basic Block)即基本块,是程序执行中最小的执行单元之一。它通常由一系列连续的指令组成,这些指令之间不存在跳转,因此在基本块内程序控制流程是顺序执行的。在动态分析中,基本块被视为程序的一个“原子”执行单元。
Trace Instrumentation 通过 TRACE_AddInstrumentFunction API 进行注册 Trace 回调。
功能:在基本块插入回调函数
IPointBefore:在基本块开始执行之前调用回调函数
IPointAfter:在基本块执行结束之后调用回调函数
Pintools
基于 Pin 开发的 Pintools 是一种动态二进制插桩工具,也相当于动态程序分析工具,可用于对Linux、Windows 上的用户空间应用程序进行程序分析。因其能够实现无需重新编译源代码,即可在程序运行时进行插桩,所以 Pintools 也支持对动态生成代码进行插桩。Pintools 包括Intel® VTune™ Amplifier、Intel® Inspector、Intel® Advisor以及Intel®软件开发模拟器(Intel® SDE)。
通过编写简单的示例代码,可以对代码覆盖率进行简单分析,但这种方法在处理大量执行流的明文字符串时,会涉及到频繁的 IO 操作,特别是在分析那些被 VMProtect、Themida 等强壳保护的程序时,如果虚拟化的指令数量达到千万条时,追踪效率会大幅降低。为了优化这一过程,我们可以利用 ProtoBuf 库对数据进行序列化,并通过对 BBL 进行白名单标记,取消对重复执行代码块的插桩,从而提高追踪效率。
static void OnTrace(TRACE trace, void* v) {
auto& context = *reinterpret_cast<ToolContext*>(v);
if (!context.m_tracing_enabled ||
!context.m_images->isInterestingAddress(TRACE_Address(trace))) {
return;
}
// 获取线程ID和线程本地数据
auto tid = PIN_ThreadId();
ThreadData* data = context.GetThreadLocalData(tid);
// 处理第一个基本块
auto bbl = TRACE_BblHead(trace);
auto firstBlockAddr = BBL_Address(bbl);
data->m_blocks[firstBlockAddr] = static_cast<uint16_t>(BBL_Size(bbl));
// 遍历trace中的后续基本块
for (bbl = BBL_Next(bbl); BBL_Valid(bbl); bbl = BBL_Next(bbl)) {
ADDRINT blockAddr = BBL_Address(bbl);
if (data->m_blocks.find(blockAddr) != data->m_blocks.end()) {
continue;
}
BBL_InsertCall(
bbl,
IPOINT_ANYWHERE,
reinterpret_cast<AFUNPTR>(OnBasicBlockHit),
IARG_FAST_ANALYSIS_CALL,
IARG_THREAD_ID,
IARG_ADDRINT, blockAddr,
IARG_UINT32, BBL_Size(bbl),
IARG_PTR, v,
IARG_END
);
}
}
追踪数据
利用 IDA 可以进一步增强代码覆盖率分析的准确性。通过统计每条指令的执行次数,可以计算出每个函数的执行覆盖率。即使在缺乏符号信息的情况下,借助 IDA 的函数识别功能,也能协助逆向分析人员精确分析出每个函数的代码覆盖率。注:绿色表示代码块已被执行。
代码覆盖率
代码覆盖率通过 Pin 对指令进行插桩,从而实现对执行地址的追踪。通过获取 RIP 寄存器的值,Pin 可以记录指令的执行情况,从而分析出哪些指令被执行过,哪些指令没有被执行过。此外,Pin 可通过对事件的监控来检测映像和模块的加载情况,并对相关模块进行追踪。通过对指令的插桩,Pin 可实时 Dump内存,查看内存状态,并对内存修改进行断点分析。同时,它也能对指令执行过程进行详细分析。
代码覆盖率分析通过追踪已执行的指令来判断哪些指令被执行过,哪些未被执行,并将这些信息记录到日志中。这些日志对于逆向分析人员来说非常有用,尤其是在判断 JCC 指令执行情况时,可以更准确的进行静态分析。
回溯分析
通过使用 IDA 插件分析日志中的 RIP 值,可以回溯 JCC 指令的相关执行流程,并实现内存断点执行的效果。
以下以指令粒度插桩进行打印 RIP 的例子:
#include <stdio.h>
#include "pin.H"
FILE* trace;
// This function is called before every instruction is executed
// and prints the IP
VOID printip(VOID* ip) { fprintf(trace, "%p\n", ip); }
// Pin calls this function every time a new instruction is encountered
VOID Instruction(INS ins, VOID* v)
{
// Insert a call to printip before every instruction, and pass it the IP
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)printip, IARG_INST_PTR, IARG_END);
}
// This function is called when the application exits
VOID Fini(INT32 code, VOID* v)
{
fprintf(trace, "#eof\n");
fclose(trace);
}
/* ===================================================================== */
/* Print Help Message */
/* ===================================================================== */
INT32 Usage()
{
PIN_ERROR("This Pintool prints the IPs of every instruction executed\n" + KNOB_BASE::StringKnobSummary() + "\n");
return -1;
}
/* ===================================================================== */
/* Main */
/* ===================================================================== */
int main(int argc, char* argv[])
{
trace = fopen("itrace.out", "w");
// Initialize pin
if (PIN_Init(argc, argv)) return Usage();
// Register Instruction to be called to instrument instructions
INS_AddInstrumentFunction(Instruction, 0);
// Register Fini to be called when the application exits
PIN_AddFiniFunction(Fini, 0);
// Start the program, never returns
PIN_StartProgram();
return 0;
}
首先,通过 INS_AddInstrumentFunction(Instruction, 0) 注册一个以指令粒度为单位的回调函数。接着,通过 INS_InsertCall 插入一个 pre 回调,用于在指令执行前记录并打印 RIP 寄存器的值。
采用上述方法,可以进行简单的代码覆盖率分析:通过记录 RIP 寄存器的值,可以追踪程序的指令流。RIP 寄存器的值有助于逆向分析人员分析哪些代码被执行过,以及识别程序中的“死代码”。此外,还可以统计执行的指令总数,为他们提供有价值的参考,帮助他们更高效地进行代码分析。
#include "pin.H"
#include <iostream>
#include <fstream>
#include <unordered_set>
std::unordered_set<ADDRINT> covered_instructions;
UINT64 covered_count = 0;
std::ofstream output_file;
// 指令覆盖回调函数
VOID OnInstructionExecuted(THREADID tid, ADDRINT rip) {
// 记录覆盖的指令 RIP 地址
if (covered_instructions.find(rip) == covered_instructions.end()) {
covered_instructions.insert(rip);
covered_count++;
}
}
// 在基本块上插桩,插入指令回调
VOID InstructionCallback(INS ins, VOID *v) {
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)OnInstructionExecuted,
IARG_THREAD_ID,
IARG_INST_PTR,
IARG_END);
}
// 程序结束时输出覆盖的指令信息
VOID FiniCallback(INT32 code, VOID *v) {
output_file << "Total covered instructions: " << covered_count << std::endl;
output_file << "Covered instruction addresses:" << std::endl;
for (auto &rip : covered_instructions) {
output_file << "0x" << std::hex << rip << std::dec << std::endl;
}
output_file.close();
}
int main(int argc, char *argv[]) {
output_file.open("coverage_output.txt");
// 初始化 Pin
if (PIN_Init(argc, argv)) {
std::cerr << "PIN initialization failed!" << std::endl;
return -1;
}
// 注册指令插桩回调函数
INS_AddInstrumentFunction(InstructionCallback, nullptr);
// 注册程序结束时的回调函数
PIN_AddFiniFunction(FiniCallback, nullptr);
// 启动程序执行
PIN_StartProgram();
return 0;
}
import idaapi
import idautils
import idc
# 用于存储Pin报告中的RIP地址(即已执行的指令)
covered_rips = set()
# 读取Pin生成的文件
def read_pin_output(file_path):
try:
with open(file_path, 'r') as f:
for line in f:
rip = line.strip()
if rip.startswith('0x'):
covered_rips.add(int(rip, 16)) # 转换为整数并添加到集合中
print(f"Loaded {len(covered_rips)} covered RIP addresses from {file_path}")
except Exception as e:
print(f"Error reading file {file_path}: {e}")
# 统计指令总数
def count_instructions():
instruction_count = 0
for ea in idautils.Heads(idc.get_segm_by_name('.text').startEA, idc.get_segm_by_name('.text').endEA):
if idc.isCode(idc.get_flags(ea)):
instruction_count += 1
return instruction_count
# 上色已覆盖的指令
def color_covered_instructions():
for rip in covered_rips:
if idc.isCode(idc.get_flags(rip)):
idc.set_color(rip, idc.CIC_ITEM, 0x00FF00) # 绿色
# 分析并输出指令统计信息
def run():
read_pin_output("pin_output.txt")
total_instructions = count_instructions()
print(f"Total instructions in the program: {total_instructions}")
color_covered_instructions()
if __name__ == '__main__':
run()
污点分析
污点分析是一种动态的信息流分析技术,它通过跟踪程序中不可信数据的流动来发现潜在的风险行为。在软件安全领域,有一个普遍认可的原则:“所有用户输入都是不可信的”。在动态污点分析中,用户输入的所有数据被视为“污点”,并对其在程序中的传播过程进行追踪。污点分析在逆向工程中被广泛应用于漏洞挖掘、软件破解等领域。通过动态污点分析,结合程序的执行轨迹和运行时的信息,能够追踪污点数据的传播路径,从而识别程序中的漏洞以及在软件破解中起到关键作用的 JCC (条件跳转指令)。污点分析的输入来源主要包括以下几个方面:
本地文件
网络报本
环境变量
程序消息事件响应
...
如果一条指令中的所有读操作均不涉及污染数据,则该指令的所有写操作都视为去污染。
add eax,ecx r:eax,ecx
w:eax,efl
mov eax,[esp+4] r:esp,[esp+4]
w:eax
如对程序进行指令级动态插桩,可以在每条指令执行之前分析出污点是否需要传播,以及涉及到的隐式读写过程中的污点是否需要传播。
确定污点传播记录数据结构每个线程的污点信息使用 thread_info 结构进行管理,每个寄存器的污点信息使用一个集合。
#include <iostream>
#include <set>
#include <unordered_map>
using namespace std;
typedef unsigned int THREADID;
typedef unsigned int REG32_INDEX;
// 用于存储每个寄存器的污点数据
struct ThreadInfo {
unordered_map<REG32_INDEX, set<int>> reg_taint;
};
unordered_map<THREADID, ThreadInfo> thread_info; // 存储每个线程的污点信息
污点传播回调函数。
// 检查指令是否有读操作数,且其中至少一个读操作数是污染的
bool has_read_taint(const INS& ins, THREADID tid) {
for (int i = 0; i < INS_OperandCount(ins); ++i) {
if (INS_OperandIsMemory(ins, i) || INS_OperandIsReg(ins, i)) {
REG32_INDEX reg = INS_OperandReg(ins, i);
if (!thread_info[tid].reg_taint[reg].empty()) {
return true;
}
}
}
return false;
}
// 污染写操作数
void contaminate_writes(const INS& ins, THREADID tid) {
for (int i = 0; i < INS_OperandCount(ins); ++i) {
if (INS_OperandIsMemory(ins, i) || INS_OperandIsReg(ins, i)) {
REG32_INDEX reg = INS_OperandReg(ins, i);
if (INS_OperandIsWrite(ins, i)) {
thread_info[tid].reg_taint[reg].insert(tid); // 使用tid作为污点源,代表当前线程的污点
}
}
}
}
// 清除写操作数的污点
void clear_writes(const INS& ins, THREADID tid) {
for (int i = 0; i < INS_OperandCount(ins); ++i) {
if (INS_OperandIsMemory(ins, i) || INS_OperandIsReg(ins, i)) {
REG32_INDEX reg = INS_OperandReg(ins, i);
if (INS_OperandIsWrite(ins, i)) {
thread_info[tid].reg_taint[reg].clear();
}
}
}
}
执行回调函数分析污点传播。
void insert_taint_callbacks(INS ins, THREADID tid) {
bool read_is_tainted = has_read_taint(ins, tid);
if (read_is_tainted) {
// 如果有污染的读操作数,污染所有的写操作数
contaminate_writes(ins, tid);
} else {
// 如果所有的读操作数都没有污染,去污染所有的写操作数
clear_writes(ins, tid);
}
REG32_INDEX reg_dst, reg_src;
if (INS_MemoryOperandCount(ins) == 0) { // 只处理寄存器操作
reg_dst = INS_OperandReg(ins, OP_0);
reg_src = INS_OperandReg(ins, OP_1);
if (REG_is_gr32(reg_dst)) {
switch (INS_Opcode(ins)) {
case XED_ICLASS_XOR:
case XED_ICLASS_SUB:
case XED_ICLASS_SBB:
if (reg_dst == reg_src) {
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)r_clrl, IARG_FAST_ANALYSIS_CALL,
IARG_UINT32, reg_dst, IARG_THREAD_ID, IARG_END);
}
break;
default:
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)r2r_binary_opl, IARG_FAST_ANALYSIS_CALL,
IARG_UINT32, reg_dst, IARG_UINT32, reg_src, IARG_THREAD_ID, IARG_END);
break;
}
}
}
}
通过简单的污点传播,可以找到污点传播过程中涉及到的关键跳转,以及那些被执行过且容易触发缓冲区的漏洞危险函数,这为进一步的 fuzz 执行提供了基础,有助于触发代码逻辑中的缺陷,污点分析详细过程可见下文Unicorn污点分析。
二、
Unicorn
Unicorn 是一种基于动态二进制翻译的开源模拟器,作为 QEMU 的轻量级子集,支持模拟多种架构的指令集,如 x86、ARM、MIPS、SPARC 等。它能够在不同平台上模拟程序执行,是二进制分析、逆向工程、漏洞研究、恶意软件分析等领域的强大工具。
在强壳虚拟化的条件下,一条指令可能被混淆并扩展为数千万条指令,相比之下,常规调试器,如 x64dbg ,其指令追踪速度通常被限制在每秒 3k 条指令,远远无法满足逆向分析的需求。然而,通过 Unicorn 模拟指令执行,模拟速度能够达到每秒 5w 条指令,并完整记录指令执行过程中的寄存器和内存状态,可为逆向工程提供更高效的支持。
追踪效率
记录完整追踪过程
REGDUMP lpRegDump;
if (DbgGetRegDumpEx(&lpRegDump, sizeof(lpRegDump)) == false)
{
MessageBoxW(hwndDlg, L"获取寄存器环境失败.", PLUGIN_NAME_utf16, MB_OK | MB_ICONERROR);
return;
}
bool Track::mem_map_range(uint64_t address)
{
bool bSuccess = false;
HANDLE hProcess = m_process_info.Process;
if (hProcess != NULL)
{
size_t max_mem_size = 0;
MEMORY_BASIC_INFORMATION mem_info;
MEMORY_BASIC_INFORMATION mem_info_1;
if (VirtualQueryEx(hProcess, (LPCVOID)address, &mem_info_1, sizeof(mem_info_1)) != NULL)
{
if (VirtualQueryEx(hProcess, (LPCVOID)mem_info_1.AllocationBase, &mem_info_1, sizeof(mem_info_1)) != NULL)
{
mem_info = mem_info_1;
do
{
max_mem_size += mem_info.RegionSize;
void* mem_buff = malloc(mem_info.RegionSize);
SIZE_T read_size = 0;
if (ReadProcessMemory(hProcess, (LPCVOID)mem_info.BaseAddress, mem_buff, mem_info.RegionSize, &read_size))
{
if (m_VME.sim_uc_mem_map((uint64_t)(uintptr_t)mem_info.BaseAddress, mem_info.RegionSize, mem_win_protect_to_uc_protect(mem_info.Protect)) == UC_ERR_OK)
{
if (m_VME.sim_uc_mem_write((uint64_t)(uintptr_t)mem_info.BaseAddress, mem_buff, mem_info.RegionSize) == UC_ERR_OK)
{
if ((uint64_t)(uintptr_t)mem_info.BaseAddress + mem_info.RegionSize > address)
{
bSuccess = true;
}
}
}
}
free(mem_buff);
mem_info.BaseAddress = (PVOID)((uint64_t)mem_info.BaseAddress + mem_info.RegionSize);
if (VirtualQueryEx(hProcess, (LPCVOID)mem_info.BaseAddress, &mem_info, sizeof(mem_info)) == NULL)
break;
} while (mem_info_1.AllocationBase == mem_info.AllocationBase);
}
}
}
return bSuccess;
}
追踪模拟至用户断点
bool is_dbg_bp = is_hit_user_bp(exit_msg); // 命中用户断点
if (is_dbg_bp)
{
_plugin_logprintf(u8"[%s] uc_err:[%d] exit_code:[%d] 结束地址:%llx 回溯地址:%llx -> %llx 本次跟踪数量:%d 总跟踪数量:%d \n", PLUGIN_NAME, ucerr, exit_msg.exit_code, exit_msg.exit_base, exit_msg.third_base, exit_msg.prev_base, exit_msg.track_num, m_Track.m_db_insn_size);
if (!bMapAllContextToProcess)
{
DbgCmdExecV(u8"go");
}
}
追踪模拟至 API 调用
uc_err Track::start_track(_track_exit_msg* exit_msg)
{
uc_err err = UC_ERR_OK;
bool is_continue = false;
do
{
#ifdef _WIN64
uint64_t lpCip = 0;
m_VME.sim_uc_reg_read(UC_X86_REG_RIP, &lpCip);
#else
uint32_t lpCip = 0;
m_VME.sim_uc_reg_read(UC_X86_REG_EIP, &lpCip);
#endif
m_track_write_mem_range.clear();
m_exit_msg.exit_code = track_exit_code::uc_exit;
m_exit_msg.user_exit_err = UC_ERR_OK;
m_last_insn.clear();
m_VME.sim_uc_ctl(UC_CTL_WRITE(UC_CTL_TB_FLUSH, 0));
size_t track_num = m_db_insn_size;
err = m_VME.sim_uc_emu_start(lpCip, -1, NULL, 0);
*exit_msg = m_exit_msg;
} while (is_continue);
return err;
}
追踪模拟到跨节区执行
根据获取到的区段大小信息,我们可以定位当前所在节区并获取相关信息。通过调用 x64dbg 的 API ,以及读取 PE 内存中的 PE 节表信息,可获取所在节区的具体数据,并在模拟执行过程中,当程序跳转到下一个节区时停止执行。
// 获取地址节区内存范围
bool get_addr_section_range(HANDLE hProcess, uint64_t address, _track_mem_range* mem_rage)
{
bool bSuccess = false;
if (hProcess != nullptr)
{
HMODULE lpBase = GetMainModuleBase(hProcess);
IMAGE_DOS_HEADER dos_header;
if (!ReadProcessMemory(hProcess, (LPCVOID)lpBase, &dos_header, sizeof(dos_header), NULL))
{
DWORD ERror = GetLastError();
return bSuccess;
}
// 读取NT头
IMAGE_NT_HEADERS32 nt_headers;
if (!ReadProcessMemory(hProcess, (LPCVOID)((DWORD)lpBase + dos_header.e_lfanew), &nt_headers, sizeof(nt_headers), NULL))
return bSuccess;
// 获取节表
DWORD section_count = nt_headers.FileHeader.NumberOfSections;
IMAGE_SECTION_HEADER* sections = new IMAGE_SECTION_HEADER[section_count];
if (!ReadProcessMemory(
hProcess,
(LPCVOID)((DWORD)lpBase + dos_header.e_lfanew + sizeof(IMAGE_NT_HEADERS32)),
sections,
section_count * sizeof(IMAGE_SECTION_HEADER),
NULL
))
{
delete[] sections;
return bSuccess;
}
// 遍历节区,找到包含目标地址的节
for (DWORD i = 0; i < section_count; ++i)
{
uint64_t section_start = (uint64_t)lpBase + sections[i].VirtualAddress;
uint64_t section_end = section_start + sections[i].Misc.VirtualSize;
if (address >= section_start && address < section_end)
{
mem_rage->base = section_start;
mem_rage->end = section_end;
bSuccess = true;
delete[] sections;
CloseHandle(hProcess);
return bSuccess;
}
}
delete[] sections;
}
}
根据 Unicorn 模拟执行到节区地址范围,我们可以更精准地确认脱壳过程中的 OEP,dump 出来更好的进行静态分析。
Unicorn 污点分析
Unicorn 污点分析与 Pin 污染分析的原理相同。Unicorn 可以通过 Capstone 反汇编引擎分析指令中的读写操作以及隐式的读写操作。如果指令中的读地址被标记为污染,那么相应的写操作地址也会被标记为污染;相反,如果读地址标记为非污染,写操作地址则也被标记为非污染。污染的地址会被记录并存储到动态数组中。除了这些基本规则,如有特殊指令涉及的污染地址则需要手动分析,具体包括:
jmp reg,jmp [mem],call reg,call [mem],ret,Jcc
栈指令(push,pop,pushad,popad,pushfd,popfd)
lea
xchg,xadd
使用被污染的寄存器寻找内存
串操作指令(stos *,scas*,...)
去污染(xor eax,eax)
.......
push eax将会读 eax,esp,写 esp,[esp-4]以一般原则中,如果 eax 是被污染的,则将 esp,[esp-4] 都给污染,人为分析可以发现 esp 是不用被污染的,只需要污染[esp - 4]。
if (!strcmp(insn->mnemonic, "push"))
{//push
cs_x86_op op = x86.operands[0];
do_taint_sp_push(op);
return g_taint_handled;
}
inline static void do_taint_sp_push(cs_x86_op &op)
{
DWORD esp_after = regs.u[reg_transfer_table[X86_REG_ESP]] - 4;
switch (op.type)
{
case X86_OP_MEM:
{
DWORD addr = get_mem_addr(op.mem);
if (is_addr_tainted(addr) || is_addr_tainted(addr+1) || is_addr_tainted(addr+2) || is_addr_tainted(addr+3)){
for (int i = 0; i<4; i++)
taint_addr(esp_after + i);
} else {
for (int i = 0; i<4; i++)
untaint_addr(esp_after + i);
}
}
break;
case X86_OP_REG:
{
x86_reg reg = op.reg;
if (is_reg_tainted(reg)){
for (int i = 0; i<4; i++)
taint_addr(esp_after + i);
} else {
for (int i = 0; i<4; i++)
untaint_addr(esp_after + i);
}
}
break;
case X86_OP_IMM:
for (int i = 0; i<4; i++)
untaint_addr(esp_after + i);
break;
default:
__asm int 3
}
}inline static void do_taint_sp_push(cs_x86_op &op)
{
DWORD esp_after = regs.u[reg_transfer_table[X86_REG_ESP]] - 4;
switch (op.type)
{
case X86_OP_MEM:
{
DWORD addr = get_mem_addr(op.mem);
if (is_addr_tainted(addr) || is_addr_tainted(addr+1) || is_addr_tainted(addr+2) || is_addr_tainted(addr+3)){
for (int i = 0; i<4; i++)
taint_addr(esp_after + i);
} else {
for (int i = 0; i<4; i++)
untaint_addr(esp_after + i);
}
}
break;
case X86_OP_REG:
{
x86_reg reg = op.reg;
if (is_reg_tainted(reg)){
for (int i = 0; i<4; i++)
taint_addr(esp_after + i);
} else {
for (int i = 0; i<4; i++)
untaint_addr(esp_after + i);
}
}
break;
case X86_OP_IMM:
for (int i = 0; i<4; i++)
untaint_addr(esp_after + i);
break;
default:
__asm int 3
}
}
以用户的输入为例,进行污点分析:
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
char buf[1204];
int advanced_strcmp(const char *str1, const char *str2) {
if (str1 == NULL && str2 == NULL) {
return 0;
}
if (str1 == NULL) {
return -1;
}
if (str2 == NULL) {
return 1;
}
while (*str1 && *str2) {
if (*str1 != *str2) {
return *(unsigned char *)str1 - *(unsigned char *)str2;
}
str1++;
str2++;
}
if (*str1) {
return 1;
}
if (*str2) {
return -1;
}
return 0;
}
void main()
{
while (1)
{
scanf("%s", buf);
if (advanced_strcmp(buf, (char *)"123"))
printf("ok\n");
else
printf("fail\n");
}
system("pause");
}
污点分析过程图
污点分析
mov ebp,esp ;esp流向了ebp
mov eax, dword ptr [ebp+8] ;参数一
mov edx, dword ptr [ebp+0xC] ;参数二
and ecx, eax ;判断1
je 0x40113b ;跳转1
cmp edx, ecx;判断2
je 0x401127 ;跳转2
test ecx,ecx;判断3
je 0x4011a6 ;关键跳转
• 通常是程序接收到外部输入的地方,例如函数参数、文件读取、网络数据包或用户输入。
• [EBP + 8] 和 [EBP + 0xC] 可以视为两个输入点。
• 分析输入数据在寄存器、内存和栈之间的流动,记录其影响的范围。
• mov eax, dword ptr [ebp + 8]:输入被加载到 EAX。
• mov ecx, byte ptr [eax]:EAX 的内容被用作指针,提取字节加载到 ECX。
• and ecx, eax:输入数据间接修改了 ECX 的值。
• 每一步都标记污点,并评估可能出现的影响。
3. 识别控制流相关操作
• 条件跳转(如 je、jne)等指令是控制流的核心。
• and ecx, eax 的结果可能影响后续的跳转指令,从而影响决定程序的执行路径。
4. 记录修改和分支点
• 通过寄存器和内存值的变化,构建数据依赖关系图。
三、
Intel PT 硬件追踪
英特尔处理器跟踪(Intel PT)是一种高性能的调试和性能分析工具,具备最新的英特尔 CPU 硬件辅助跟踪技术,旨在提供对程序执行行为的详细跟踪信息,广泛应用于调试、性能分析、安全研究等领域。目前,Intel Skylake 及更高版本的 CPU 均已配置此工具。Intel PT 能够在指令级别上触发和过滤代码执行的跟踪,通过仅存储以后重构程序控制流所必需的数据,用户可以根据需求选择需要跟踪的内容,例如特定线程、地址范围等,从而减少不必要的数据记录。对于病毒分析,通常使用 Filtering by CR3 对指定进程进行追踪,使用 Intel PT 进行追踪的效率以及内存的消耗可成指数级的加快。
条件跳转:用一位表示分支是否被执行("Taken" 或 "Not Taken")
间接调用和跳转:记录目标地址
无条件跳转:不会记录变化,因为目标地址可以从指令本身推断出
Intel PT 记录的指令指针(IP)会与之前的记录进行比较,并生成相关的数据包(如FUP、TIP、TIP.PGE或TIP.PGD)。如果地址的高位字节重叠,匹配的部分将被压缩存储。此外,对于"近返回"指令,如果返回目标正好是调用指令的下一条指令,则不会记录这一跳转,因为它可以根据控制流推断出来。
这种高效的记录机制使得 Intel PT 能够在消耗较少资源的情况下记录复杂的执行流程,同时为调试和性能分析提供了强有力的支持。
Intel Processor Trace Components
Trace Check
bool isIntelPTSupported() {
unsigned int check;
__asm
{
mov eax, 0x7
cpuid
mov check,ebx
}
bool isValue = check & 0x1000000;
return isValue;
}
bool isFiltering_by_CR3() {
unsigned int check;
__asm
{
mov eax, 0x14
cpuid
mov check,ebx
}
bool isValue = check & 0x1;
return isValue;
}
check Filtering by CR3
Filtering by CR3
Intel Processor Trace (Intel PT) 提供了一种基于 CR3 寄存器值的精确跟踪过滤机制,允许开发者通过选择性地启用或禁用数据包生成,实现对特定内存上下文的细粒度性能分析和代码执行追踪。
实时监控 CR3 寄存器的值
仅在 CR3 值与预设条件匹配时生成体系结构状态数据包
将目标 CR3 值写入 IA32_RTIT_CR3_MATCH MSR
设置 IA32_RTIT_CTL.CR3Filter 控制位
精确过滤单一进程的执行轨迹
最小化性能开销
通过精确配置 MSR 寄存器标志,Intel PT 实现了对单个进程执行可控且高效的追踪。
void ConfigureProcessorTraceSettings(ProcessTraceConfig* processTraceConfig, ProcessTraceCaps* processorTraceCaps) {
// 定义跟踪控制描述符
ProcessorTraceControlDescriptor traceControlDescriptor = {0};
ProcessorTraceStatusDescriptor traceStatusDescriptor = {0};
// 获取跟踪选项
TraceOptions& traceOptions = processTraceConfig->traceOptions;
// 配置基本跟踪设置
traceControlDescriptor.Fields.FabricEn = 0;
traceControlDescriptor.Fields.Os = (processTraceConfig->shouldTraceKernelSpace ? 1 : 0);
traceControlDescriptor.Fields.User = (processTraceConfig->shouldTraceUserSpace ? 1 : 0);
traceControlDescriptor.Fields.BranchEn = traceOptions.Fields.shouldTraceBranchPackets;
// 配置页表过滤
if (processTraceConfig->targetProcessPageTable) {
__writemsr(MSR_IA32_RTIT_CR3_MATCH, reinterpret_cast<ULONGLONG>(processTraceConfig->targetProcessPageTable));
assert(__readmsr(MSR_IA32_RTIT_CR3_MATCH) == reinterpret_cast<ULONGLONG>(processTraceConfig->targetProcessPageTable));
traceControlDescriptor.Fields.CR3Filter = 1;
} else {
__writemsr(MSR_IA32_RTIT_CR3_MATCH, 0);
assert(__readmsr(MSR_IA32_RTIT_CR3_MATCH) == 0);
traceControlDescriptor.Fields.CR3Filter = 0;
}
// 初始化地址范围配置
traceControlDescriptor.Fields.Addr0Cfg = 0;
traceControlDescriptor.Fields.Addr1Cfg = 0;
traceControlDescriptor.Fields.Addr2Cfg = 0;
traceControlDescriptor.Fields.Addr3Cfg = 0;
// 配置指令指针地址范围
auto configureAddressRange = [&](size_t rangeIndex) {
if (rangeIndex >= processTraceConfig->activeRangesCount) return;
auto& addressRange = processTraceConfig->instructionRanges[rangeIndex];
uint8_t* addressConfigField = nullptr;
DWORD msrStartAddr = 0, msrEndAddr = 0;
switch (rangeIndex) {
case 0:
addressConfigField = &traceControlDescriptor.Fields.Addr0Cfg;
msrStartAddr = MSR_IA32_RTIT_ADDR0_START;
msrEndAddr = MSR_IA32_RTIT_ADDR0_END;
break;
case 1:
addressConfigField = &traceControlDescriptor.Fields.Addr1Cfg;
msrStartAddr = MSR_IA32_RTIT_ADDR1_START;
msrEndAddr = MSR_IA32_RTIT_ADDR1_END;
break;
case 2:
addressConfigField = &traceControlDescriptor.Fields.Addr2Cfg;
msrStartAddr = MSR_IA32_RTIT_ADDR2_START;
msrEndAddr = MSR_IA32_RTIT_ADDR2_END;
break;
case 3:
addressConfigField = &traceControlDescriptor.Fields.Addr3Cfg;
msrStartAddr = MSR_IA32_RTIT_ADDR3_START;
msrEndAddr = MSR_IA32_RTIT_ADDR3_END;
break;
}
*addressConfigField = addressRange.shouldStopTrace ? 2 : 1;
__writemsr(msrStartAddr, reinterpret_cast<QWORD>(addressRange.startAddress));
__writemsr(msrEndAddr, reinterpret_cast<QWORD>(addressRange.endAddress));
};
for (size_t i = 0; i < 4; ++i) {
configureAddressRange(i);
}
// 配置MTC(Memory Trace Clock)
if (processorTraceCaps->isMtcSupported) {
traceControlDescriptor.Fields.MTCEn = traceOptions.Fields.shouldTraceMtcPackets;
if ((1 << traceOptions.Fields.mtcFrequency) & processorTraceCaps->mtcPeriodBitmap) {
traceControlDescriptor.Fields.MTCFreq = traceOptions.Fields.mtcFrequency;
}
}
// 配置周期性采样和循环计数
if (processorTraceCaps->isPsbAndCycSupported) {
traceControlDescriptor.Fields.CycEn = traceOptions.Fields.shouldTraceCyclePackets;
if ((1 << traceOptions.Fields.cycleThreshold) & processorTraceCaps->cycleThresholdBitmap) {
traceControlDescriptor.Fields.CycThresh = traceOptions.Fields.cycleThreshold;
}
if ((1 << traceOptions.Fields.psbFrequency) & processorTraceCaps->psbFrequencyBitmap) {
traceControlDescriptor.Fields.PSBFreq = traceOptions.Fields.psbFrequency;
}
}
// 额外配置
traceControlDescriptor.Fields.DisRETC = !traceOptions.Fields.isReturnCompressionEnabled;
traceControlDescriptor.Fields.TSCEn = traceOptions.Fields.shouldTraceTscPackets;
// 启用跟踪
traceControlDescriptor.Fields.TraceEn = 1;
__writemsr(MSR_IA32_RTIT_CTL, traceControlDescriptor.All);
assert(__readmsr(MSR_IA32_RTIT_CTL) == traceControlDescriptor.All);
// 读取状态寄存器
traceStatusDescriptor.All = __readmsr(MSR_IA32_RTIT_STATUS);
}
数据包解包
Intel PT 在追踪过程中生成的记录数据包需要使用官方的 Intel libipt 库来进行解包和分析。libipt 是解码 Intel PT 数据包的标准库,提供了如 ptdump 和 ptexd 等基本工具。通过对生成的 PSB 数据包进行解码,开发者可以分析关键跳转指令及其相关数据,从而获得更深入的执行过程理解和性能分析。
以下是使用Libipt库 API 实现对 PSB 进行解包的操作:
static int dump_packets(struct pt_packet_decoder *decoder,
struct ptdump_tracking *tracking,
const struct ptdump_options *options,
const struct pt_config *config)
{
uint64_t offset;
int errcode;
offset = 0ull;
for (;;) {
struct pt_packet packet;
//获取当前解码器位置作为偏移量进入 Intel PT 缓冲区
errcode = pt_pkt_get_offset(decoder, &offset);
if (errcode < 0)
return diag("error getting offset", offset, errcode);
errcode = pt_pkt_next(decoder, &packet, sizeof(packet));
if (errcode < 0) {
if (errcode == -pte_eos)
return 0;
if (pt_errcode(errcode) != pte_bad_packet)
return diag("error decoding packet", offset, errcode);
else
return errcode;
}
errcode = dump_one_packet(offset, &packet, tracking, options,
config);
if (errcode < 0)
return diag("error printing the packet", offset, errcode);
}
return 0;
}
以下是《Intel® 64 and IA-32 Architectures Software Developer Manuals》中的一些常用数据字段解析:
PSB(数据包流边界)
PSB 数据包作为追踪包解码的同步标志,定义了追踪日志中的一个边界。在此处,解压缩过程可以独立进行,不会产生其他影响。在 libipt 库中,这个偏移量被称为“同步偏移量”(sync offset),因为它标志着追踪文件中的一个位置,从这个位置开始可以安全地解码后续的数据包。
TIP(目标 IP )
TIP 数据包表示目标 IP 地址。????????压缩省略地址高四字节。
TNT(条件分支执行状态)
解包数据如下:
解包数据
通过解包数据,可以发现程序在执行过程中的 TIP 跳转到 7FFFFFFFF0a26c90 以及 0x405e0 偏移处存在的 TNT 跳转,这些信息有助于我们进一步借助代码实现更深入的分析。
数据包分析
class PtAnalyzer:
#解析转储文件中的基址
def parse_base_address(self, hDump):
try:
base_addr_line = hDump.readline().strip()
if not base_addr_line.lower().startswith("base address:"):
return False
# 提取基址字符串
base_addr_str = base_addr_line[14:].strip().split('-')[0].strip()
self.BaseAddr = int(base_addr_str, 16) if base_addr_str.startswith("0x") else int(base_addr_str, 10)
return True
except Exception as e:
print(f"基址解析错误: {e}")
return False
finally:
hDump.close()
# 使用IDA进行代码执行流程分析
def start_pt_analysis(self, start_ea):
current_ea = start_ea
line_number = 0
try:
while True:
# 读取下一个数据包
line = self.hTraceFile.readline()
if not line or line.strip() == 'END':
break
line = line.strip()
if not line:
continue
next_packet = self.get_packet(line)
line_number += 1
next_ea = self.analyse_next_chunk(current_ea, next_packet)
if next_ea == 0:
if next_packet[1][:3] not in ["fup", "tip"]:
print(f"内部错误。数据包ID: {hex(next_packet[0])}, 类型: {next_packet[1]}, 当前IP: {hex(current_ea)[:-1]}")
break
complete_pt_addr = self.get_pt_pck_full_ip(next_packet)
next_ea = self.get_ida_address(complete_pt_addr)
print(f"发现不相关的代码块。起始地址: {hex(next_ea)[:-1]},转储文件行号: {line_number}")
elif next_ea == -1:
break
self.last_pt_pck = next_packet
current_ea = next_ea
# 处理最后一个数据包
self._finalize_analysis(current_ea, next_packet)
return True
except Exception as e:
print(f"PT分析过程中发生错误: {e}")
return False
def _finalize_analysis(self, current_ea, last_packet):
last_ea = ida_funcs.get_fchunk(current_ea).endEA
if last_packet[1][-3:] == "pgd" and current_ea != last_ea:
while current_ea < last_ea:
mnem = idc.GetMnem(current_ea)
self.color_instruction(current_ea)
# 遇到特定指令则停止
if mnem[0] == "j" or mnem[:4] == "loop" or mnem in ["call", "ret", "retn"]:
break
current_ea = idc.NextHead(current_ea)
四、
火绒内部工具
虚拟沙盒
火绒虚拟沙盒实现了对超过数万个 API 的模拟,覆盖了绝大多数操作系统的核心机制,并支持多个操作系统平台:
Windows x86/x64
Linux x86/x64
火绒查杀引擎与调试器集合实现了对病毒的追踪,脱壳并自动修复 IAT,自动运行到调用 API,单步步过显示调用的 API。
模拟输出调用 API 演示
引擎追踪至跨节区演示
Dump 脱壳内存映像自动修复 IAT 演示
今天的技术分享到这里就结束啦,希望本篇文章,可以对大家探索病毒分析领域起到一定帮助。绒绒相信,这些工具和技术不仅仅是我们对抗恶意软件的利器,更是我们解码溯源、守卫网络安全的法宝。
在这个信息安全越来越被重视的时代,每一次技术的进步和应用都显得尤为重要,网络安全也不仅仅是专业人士的责任,更是与我们每个人的生活都息息相关。让我们一起筑起防护之盾,为整个网络空间的和谐与安全贡献力量。
最后,绒绒也要感谢大家的陪伴和支持,正是因为有了你们的好奇心和探索欲,我们的分享才更有意义。未来,绒绒会继续为大家挖掘更多网络安全的“宝藏”,分享更多有价值的技术知识,敬请期待吧~
END