需要完整代码的小伙伴评论区Q我,将附上完整代码!
目录
1.什么是二进制插桩
2.静态二进制插桩
2.1. int3方法
用int3解决多字节跳转问题
2.2 跳板方法
3.动态二进制插桩
3.1pin引擎
3.2使用Pin进行分析
3.2.1命令行选项和数据结构
3.2.2 初始化Pin
3.2.3 注册插桩例程
3.2.4 注册系统调用入口函数
3.2.5 注册fini函数
3.2.6启动应用程序
3.2.7 测试Profiler
3.3用 Pin自动对二进制文件脱壳
3.3.1可执行文件加壳器简介
二进制插桩是指在现有二进制程序中的任意位置插入新代码,并以某种方式来观察或修改二进制程序的行为。添加新代码的位置成为插桩点,添加的代码则称为插桩代码。
目前有两类插桩平台:静态插桩(SBI)和动态插桩(DBI),它们采用不同的方法解决插入和重定位代码的问题:
例如,对二进制程序中的所有call指令进行插桩,统计二进制程序中调用最频繁的函数,该例仅仅观察二进制程序的行为,也可以对其进行修改实现更多功能,例如可以在所有间接的控制转移指令处插入代码,来检查控制流转移目标是否属于预期目标集合,如果不属于的话则中断程序执行并发出警告,从而提高二进制程序抵御控制流劫持攻击的能力,这种抵御控制流劫持攻击的方法称为控制流完整性(Control-Flow Intergrity,CFI)
SBI对二进制程序进行反汇编,然后按需添加插桩代码并将更新的二进制程序存入磁盘。SBI平台包括PEBIL和Dyninst,都是研究工具,没有详细的文档。SBI主要挑战是,在不破坏任何现有代码和数据引用的前提下,添加插桩代码并重写二进制程序。目前有两种流行的解决方法:
int3方法的名称来自x86架构的int3指令,调试器用来实现软件断点。
针对静态二进制插桩,一种朴素的想法是使用jmp指令覆盖插桩点处的指令,然后将控制流转移到插桩代码,该插桩代码存储在一个独立的位置,如一个新的代码段或者共享库,因为现有代码段没有存放任意数量的新代码的空间。如下图所示:
问题在于jmp指令会占用多字节跳转到插桩代码,通常需要一个5字节的长度,当对短指令插桩时,指向插桩代码的jmp指令可能比它替换的指令长,会覆盖并破坏下一条指令。
基于以上问题,需要用到int3指令,int3指令可以用来对不适用于多字节跳转的简短指令进行插桩。
在x86架构中,int3指令会生成一个软中断,用户空间的程序能够通过操作系统提供的SIGTRAP信号捕获中断。int3的关键在于它的长度只有1字节,可以覆盖任何指令。
从SBI的角度,使用int3对指令进行插桩,用0xcc覆盖该指令的第一字节。党SIGTRAP信号产生时,可以用linux系统的ptrace API找出中断发生的地址,从而获取插桩点地址,然后根据插桩点位置调用相应的插桩代码。
缺点:int3软中断速度很慢,导致插桩后的应用程序的运行开销过大。
跳板方法不会直接对原始代码进行插桩,相反,它创建一个原始代码的副本并只对这个副本进行插桩。
以下图右侧添加插桩代码的图为例说明:
DBI引擎通过监视和控制所有执行的指令来动态地插桩进程。DBI引擎公开了API接口,允许用户编写自定义的DBI工具,指定应该插桩哪些代码以及如何插桩。
作为最流行的DBI平台之一,Intel Pin是一个频繁更新、免费使用且有详细文档的平台,提供了相对容易使用的API套件。
Pin的DBI引擎和Pintool都运行于用户空间,因此只能插桩用户空间进程。
实现一个Pintool需要两种不同的函数:插桩例程和分析例程。DBI引擎Pin启动主程序后,开始监视指令流,插桩例程告诉pin要添加的插桩代码和位置,插桩例程安装指向分析例程的回调,分析例程包含了实际的插桩代码。
Pintool可以实现特定工具的命令行选项,这些选项在Pin术语中称为开关(knob),PinAPI中包括一个专用的KNOB类,用于创建命令行选项。在下图代码中,有两个布尔选项(KNOB
Profiler使用多个std::map数据结构和计数器来跟踪程序的运行时统计信息。
从main函数开始,调用的第一个Pin函数是PIN_InitSymbols,该函数表示Pin读取应用程序的符号表,接下来调用PIN_Init函数来初始化Pin
Profiler需要注册3个插桩例程,其中第一个是parse_funcsyms,进行img粒度的插桩,另外两个为instrument_trace和instrument_insn,分别进行踪迹和指令粒度的插桩。
Profiler使用PIN_AddSyscallEntryFunction函数注册一个名为log_syscall的函数
Profiler注册的最后一个回调函数是fini函数,该函数在应用程序退出时或者Pin从程序分离时被调用
每个Pintool初始化的最后一步都是调用了PIN_StartProgram函数来启动应用程序。
#include
#include
接下来,就上述TRACE_AddInstrumentFunction插桩例程为例进行介绍,该插桩例程的第一个函数instrument_trace代表Profiler注册的踪迹粒度trace的插桩例程。以下是详细代码:
static void
instrument_trace(TRACE trace, void *v)
{
IMG img = IMG_FindByAddress(TRACE_Address(trace));
if(!IMG_Valid(img) || !IMG_IsMainExecutable(img)) return;
for(BBL bb = TRACE_BblHead(trace); BBL_Valid(bb); bb = BBL_Next(bb)) {
instrument_bb(bb);
}
}
static void
instrument_bb(BBL bb)
{
BBL_InsertCall(
bb, IPOINT_ANYWHERE, (AFUNPTR)count_bb_insns,
IARG_UINT32, BBL_NumIns(bb),
IARG_END
);
}
static void
count_bb_insns(UINT32 n)
{
insn_count += n;
}
最终结果是,Pin用指向count_bb_insns的回调对主应用程序的实际执行的bb块进行插桩,count_bb_insns为profiler的指令计数器加上每个基本块中指令的数量。
命令行中的-c -s 用于打开函数调用和进行系统调用分析
Profiler输出关于执行指令的数量、控制转移、函数调用和系统调用的统计分析
可执行文件加壳器,简称加壳器,将二进制文件作为输入并将二进制代码和数据段’打包‘到一个压缩或者加密的数据区域中,然后生成一个新的、加壳的可执行文件。
如下图所示,当使用加壳器处理二进制文件时,它会生成一个新的二进制文件,其中所有的原始代码和数据都被压缩或加密到加壳区域,此外,加壳器插入一个包含引导代码的新代码段,并将二进制文件的入口点重定向到引导代码。
关于脱壳,当加载并执行加壳的二进制文件时,引导代码首先将原始代码和数据提取到内存中,然后把控制权交给二进制文件的原始入口点(Original Entry Point),从而恢复程序的正常运行。因此,自动脱壳Pintool的关键是检测引导代码将控制流转移到OEP,然后Pintool将脱壳的代码和数据转储到磁盘。
脱壳器的main函数首先初始化Pin,接下来打开日志文件,并注册名为instrument_mem_cflow的指令粒度的插桩例程和结束函数,最后运行加壳的应用程序。
数据结构的介绍稍后补充!!!
#include
#include
#include
#include
#include
static void
instrument_mem_cflow(INS ins, void *v)
{
if(INS_IsMemoryWrite(ins) && INS_hasKnownMemorySize(ins)) {
INS_InsertPredicatedCall(
ins, IPOINT_BEFORE, (AFUNPTR)queue_memwrite,
IARG_MEMORYWRITE_EA,
IARG_END
);
if(INS_HasFallThrough(ins)) {
INS_InsertPredicatedCall(
ins, IPOINT_AFTER, (AFUNPTR)log_memwrite,
IARG_MEMORYWRITE_SIZE,
IARG_END
);
}
if(INS_IsBranchOrCall(ins)) {
INS_InsertPredicatedCall(
ins, IPOINT_TAKEN_BRANCH, (AFUNPTR)log_memwrite,
IARG_MEMORYWRITE_SIZE,
IARG_END
);
}
}
if(INS_IsIndirectBranchOrCall(ins) && INS_OperandCount(ins) > 0) {
INS_InsertCall(
ins, IPOINT_BEFORE, (AFUNPTR)check_indirect_ctransfer,
IARG_INST_PTR, IARG_BRANCH_TARGET_ADDR,
IARG_END
);
}
}
脱壳器的目标是检测到控制流转移到原始入口点的时刻,然后脱壳器将脱壳的二进制文件转储到磁盘,为此instrument_mem_cflow函数通过回调check_indirect_ctransfer函数来插桩间接分支指令和调用指令。
static void
queue_memwrite(ADDRINT addr)
{
saved_addr = addr;
}
static void
log_memwrite(UINT32 size)
{
ADDRINT addr = saved_addr;
for(ADDRINT i = addr; i < addr+size; i++) {
shadow_mem[i].w = true;
PIN_SafeCopy(&shadow_mem[i].val, (const void*)i, 1);
}
}
脱壳器的最终目标是检测到向OEP的跳转并转储脱壳的代码
static void
check_indirect_ctransfer(ADDRINT ip, ADDRINT target)
{
mem_cluster_t c;
shadow_mem[target].x = true;
if(shadow_mem[target].w && !in_cluster(target)) {
/* control transfer to a once-writable memory region, suspected transfer
* to original entry point of an unpacked binary */
set_cluster(target, &c);
clusters.push_back(c);
/* dump the new cluster containing the unpacked region to file */
mem_to_file(&c, target);
/* we don't stop here because there might be multiple unpacking stages */
}
}