这部分主要介绍几个Pin的用于注册回调函数的API:
对于每个注册函数的第二个参数val
将在“回调”时传递给回调函数。如果在实际的场景中不需要传递第二个参数,为了保证安全,可以传递将val
的值设置为0进行传递。val
的理想使用方式是传递一个指向类实例的指针,这样回调函数在取消引用该指针前需要将其转换回一个对象。
所有的注册函数都会返回一个PIN_CALLBACK
对象,该对象可以在后续过程中用于操作注册的回调的相关属性。
在注册函数返回PIN_CALLBACK
对象后,可以使用PIN_CALLBACK
API对其进行操作,来检索和修改在Pin中已注册的回调函数的属性。
声明:
1 |
typedef COMPLEX_CALLBACKVAL_BASE * PIN_CALLBACK
|
函数:
-
CALLBACK_GetExecutionOrder()
声明:
1 |
VOID CALLBACK_GetExecutionOrder (PIN_CALLBACK callback)
|
作用:获取已注册回调函数的执行顺序。越靠前,越早被执行。
参数:callback
,从*_Add*Funcxtion()函数返回的注册的回调函数
-
CALLBACK_SetExecutionOrder()
声明:
1 |
VOID CALLBACK_SetExecutionOrder (PIN_CALLBACK callback, CALL_ORDER order)
|
作用:设置已注册回调函数的执行顺序。越靠前,越早被执行。
参数:callback
,从*_Add*Funcxtion()函数返回的注册的回调函数;order
,新设置的回调函数的执行顺序。
-
PIN_CALLBACK_INVALID()
声明:
1 |
const PIN_CALLBACK PIN_CALLBACK_INVALID( 0 )
|
PIN回调的无效值。
CALL_ORDER
是一个枚举类型,预定义了IARG_CALL_ORDER
的值。其作用就是当指令有多个分析函数调用时,控制每个分析函数的调用顺序,默认值为CALL_ORDER_DEFAULT
。
- CALL_ORDER_FIRST:首先执行该调用,整数值为100.
- CALL_ORDER_DEFAULT:未指定
IARG_CALL_ORDER
时的默认值,整数值为200.
- CALL_ORDER_LAST:最后执行该调用,整数值为300.
在进行数值设定时,可以使用类似CALL_ORDER_DEFAULT + 5
的格式来设置。
针对在相同插桩回调环境中的针对同一指令的、具备同样CALL_ORDER
的多个分析调用,Pin会按照插入的顺序进行调用。
虽然Pin的主要用途是对二进制程序进行插桩,但是它也可以实现对程序指令的修改。
最简单的实现方式是插入一个分析routine来模拟指令执行,然后调用INS_Delete()
来删除指令。也可以通过直接或间接插入程序执行流分支(使用INS_InsertDirectJump
和INS_InsertIndirectJump
)实现,这种方式会改变程序的执行流,但是会更容易实现指令模拟。
-
INS_InsertDirectJump()
声明:
1 |
VOID INS_InsertDirectJump(INS ins, IPOINT ipoint, ADDRINT tgt)
|
参数:
- ins:输入的指令
- ipoint:与ins相关的location(仅支持IPOINT_BEFORE和IPOINT_AFTER)
- tgt:target的绝对地址
作用:插入相对于给定指令的直接跳转指令,与INS_Delete()
配合使用可以模拟控制流转移指令。
-
INS_InsertIndirectJump()
声明:
1 |
VOID INS_InsertIndirectJump ( INS ins, IPOINT ipoint, REG reg)
|
参数:
- ins:输入的指令
- ipoint:与ins相关的location(仅支持IPOINT_BEFORE和IPOINT_AFTER
- reg:target的寄存器
作用:插入相对于给定指令的间接跳转指令,与INS_Delete()
配合使用可以模拟控制流转移指令。
对于原始指令使用到的内存的访问,可以通过使用INS_RewriteMemoryOperand
来引用通过分析routine计算得到的值来替代。
需要注意的是,对于指令的修改操作,会在所有的指令插桩操作完成后进行,因此在进行指令插桩时,插桩routine看到的都是原始的、没有经过修改的程序指令。
INS_RewriteMemoryOperand()
声明:
1 |
VOID INS_RewriteMemoryOperand(INS ins, UINt32 memindex, REG newBase)
|
参数:
- ins:输入指令
- memindex:控制需要重写的内存操作数(0,1,...)
- newBase:包含新操作数地址的寄存器,通常是通过
PIN_ClainToolRegister
分配的临时寄存器
作用:更改此内存访问指令以饮用包含在给定特定寄存器中的虚拟内存地址。
在IA-32和Intel 64平台上,修改后的操作数仅使用具有新基址寄存器newBase的基址寄存器进行寻址。原始指令中该操作数的任何index, scale或者offset filed都会被删除。
该函数可以用于重写内存操作数,包括隐式的(如call、ret、push、pop),唯一不能重写的指令是第二个操作数大于0的enter
。
newBase中的地址是中是该操作数将访问的最低地址,如果操作数在内存访问之前被指令修改,如push,则newBase中的值将不是堆栈指针,而是指令访问的内存地址。
用于内存地址重写的一个样例插桩代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/ / 映射originalEa到一个翻译后的地址
static ADDRINT ProcessAddress(ADDRINT originalEa, ADDRINT size, UINT32 access);
...
for (UINT32 op = 0 ; op+ + ) / / 首先遍历内存操作指令进行计数
{
UINT32 access = (INS_MemoryOperandIsRead(ins,op) ? 1 : 0 ) | / / 判断是内存读还是内存写
(INS_MemoryOperandIsWritten(ins,op) ? 2 : 0 );
INS_InsertCall(ins, IPOINT_BEFORE,
AFUNPTR(ProcessAddress),
IARG_MEMORYOP_EA, op,
IARG_MEMORYOP_SIZE, op,
IARG_UINT32, access,
IARG_RETURN_REGS, REG_INST_G0 + i,
IARG_END); / / 在指令处进行插桩
INS_RewriteMemoryOperand(ins, i, REG(REG_INST_G0 + i)); / / 重写内存指令的操作数
}
|
命令行:
1 |
pin [pin - option]... - t [toolname] [tool - options]... - - [application] [application - option]..
|
如下是Pin的命令行的完整option列表:
Option |
Description |
-follow_execv |
使用Pin执行由execv类系统调用产生的所有进程 |
-help |
帮助信息 |
-pause_tool |
暂停并打印PID以可以在tool加载后attach到debugger,处理过程在‘n’秒后重启 |
-logfile |
指定log文件的名字和路径,默认路径为当前工作目录,默认文件名为pin.log |
-unique_logfile |
添加pid到log文件名中 |
-error_file |
指定error文件的名字和路径,默认路径为当前工作目录。如果设置了error文件,则所有error都会写入到文件中,并且不会在console中显示。如果没有指定,则不创建文件。 |
-unique_error_file |
添加pid到error文件名中 |
-injection |
的选项为dynamic, self, child, parent,只能在UNIX中使用,详看Injection,默认使用dynamic。 |
-inline |
内联简单的分析routine |
-log_inline |
在pin.log文件中记录哪些分析routine被设置成了内联 |
-cc_memory_size |
最大代码缓存,字节为单位。0为默认值,表示不做限制。必须设置为代码缓存块大小的对齐倍数。 |
-pid |
使用Pin和Pintool attach一个正在运行的进程 |
-pin_memory_range |
限制Pin到一个内存范围内,0x80000000:0x90000000 or size: 0:0x10000000. |
-restric_memory |
阻止Pin的动态加载器使用该地址范围:0x10000000:0x20000000 |
-pin_memory_size |
限制Pin和Pintool可以动态分配的字节数。Pin分配的字节数定义为Pin分配的内存页数乘以页大小。 |
-tool_load_option |
加载有附加标志的tool。 |
-t |
指定加载的Pintool。 |
-t64 <64-bit toolname> |
指定针对Intel 64架构的64-bit的Pintool。 |
-p32 |
指定IA-32架构下的Pintool |
-p64 |
指定针对Intel 64架构的Pintool |
-smc-support |
是否开启app的SMC功能,1开启,0关闭。默认开启 |
-smc_strict |
是否开启基本块内部的SMC,1开始,0关闭。默认关闭 |
-appdebug |
调试目标程序,程序运行后立即在debugger中断下 |
-appdebug_enable |
开启目标程序调试功能,但是在程序运行后不暂停 |
-appdebug_silent |
当程序调试功能开启时,Pin打印消息告知如何连接外部debugger。但是在-appdebug_connection选项开启时不打印。 |
-appdebug_exclude |
当程序调试功能开启,并指定了-follw_execv时,默认在所有子进程上启用调试。 |
-appdebug_allow_remote |
允许debugger与Pin不运行在同一系统上,而是以远程方式进行连接。指定 -appdebug_connection 时会忽略该选项的值,因为 -appdebug_connection 明确指定了运行debugger的machine。 |
-appdebug_connection |
当程序开启调试时,Pin默认会开启一个TCP端口等待debugger的连接。在开启该选项时,会在debugger中开启一个TCP端口来等待Pin的连接,相当于反置了默认的机制。该选项的格式为"[ip]:port",“ip”以点十进制格式表达,如果省略了ip,则会连接本地的端口,端口号为十进制表示。需要注意的是,debugger为GDB时,不使用该选项。 |
-detach_reattach |
允许在probe模式下进行detach和reattach,仅在Windows平台下使用。 |
-debug_instrumented_processes |
允许debugger对经过插桩的进程进行attach,仅在Windows平台下使用。 |
-show_asserts |
健全性检查 |
此外,还支持如下的tool options,它们需要跟在tool名字后面,但是要在--
符号前:
Option |
Description |
-logifle |
指定log文件的名字和路径,默认路径为当前工作目录,默认文件名为pintool.log |
-unique_logfile |
添加pid到log文件名中 |
-discard_line_info |
忽略特定模块的信息,模块名应该为没有路径的短文件名,不能是符号链接 |
-discard_line_info_all |
忽略所有模块的信息 |
-help |
帮助信息 |
-support_jit_api |
启用托管平台支持 |
-short_name |
使用最短的RTN名称。 |
-symbol_path
|
指定用分号分隔的路径列表,用于搜索以查找符号和行信息。仅在Windows平台下使用。 |
-slow_asserts |
健全性检查 |
IA-32和Intel(R) 64架构的Pin kit是一个组合kit,均包含32-bit和64-bit的版本。这就为复杂的环境提供了极高的可运行性,例如一个稍微有点复杂的运行如下:
1
2
|
pin [pin - option]... - t64 < 64 - bit toolname> - t < 32 - bit toolname> [tool - options]...
- - [application - option]..
|
需要注意的是:
- -t64选项需要用在-t选项的前面
- 当-t64和-t一起使用时,-t后面跟的时32-bit的tool。不推荐使用不带-t的-t64,因为在这种情况下,当给定32-bit应用程序时,Pin将在不应用任何工具的情况下运行该应用程序。
- [tool-option]会同时作用于64-bit和32-bit的tool,并且必须在-t <32-bit toolname>后面进行指定。
选项-injection仅在UNIX平台下可以使用,该选项控制着Pin注入到目标程序进程的方式。
默认情况下,建议使用dynamic模式。在该模式下,使用的是对父进程注入的方式,除非是系统内核不支持。子进程注入方式会创建一个pin的子进程,所以会看到pin进程和目标程序进程同时运行。使用父进程注入方式时,pin进程会在注入完成后退出,所以相对来说比较稳定。在不支持的平台上使用父进程注入方式可能出现意料之外的问题。
Pin提供了将Pintool的messages写入到文件的机制——LOG()
api,在合适的获取message的位置使用即可。默认的文件名为pintool.log,存储路径为当前工作目录,可以使用-logfile选项来改变log文件的路径和名字。
1
2
3
|
LOG( "Replacing function in " + IMG_Name(img) + "\n" );
LOG( "Address = " + hexstr( RTN_Address(rtn)) + "\n" );
LOG( "Image ID = " + decstr( IMG_Id(img) ) + "\n" );
|
Pintool的开发质量会很大程度上决定tool的性能如何,例如在进行插桩时的速度问题。将通过一个例子来介绍一些提高tool性能的技巧。
首先是插桩部分代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
VOID Instruction(INS ins, void * v)
{
...
if ( [ins is a branch or a call instruction] )
{
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR) docount2,
IARG_INST_PTR,
IARG_BRANCH_TARGET_ADDR,
IARG_BRANCH_TAKEN,
IARG_END);
}
...
}
|
然后是分析代码:
1
2
3
4
5
6
|
VOID docount2( ADDRINT src, ADDRINT dst, INT32 taken )
{
if (!taken) return ;
COUNTER * pedg = Lookup( src,dst );
pedg - >_count + + ;
}
|
该工具的目的是计算控制流图中每个控制流变化的边界被遍历的频率。工作原理如下:插桩组件通过调用docount2对每个分支进行插桩。传入的参数为源分支和目标分支以及分支是否被执行。源分支和目标分支代表来控制流边界的源和目的。如果没有执行分支,控制流不会发生改变,因此分析routine会立即返回。如果执行了分支,就使用src和dst参数来查找与此边界相关的计数器,并增加计数器的值。
Shifting Computation for Analysis to Instrumentation Code
在一个典型的应用程序中,大概每5条指令构成一个分支,在这些指令执行时会调用Lookup
函数,造成性能下降。我们思考这个过程可以发现,在指令执行时,每条指令只会调用一次插桩代码,但会多次调用分析代码。所以,可以想办法将计算工作从分析代码转移到插桩代码,这样就可以降低调用次数,从而提升性能。
首先,就大多数分支而言,我们可以在Instruction()
中找到目标分支。对于这些分支,我们可以在Instruction()
内部调用Lookup()
而不是docount2()
,对于相对较少的间接分支,我们仍然需要使用原来的方法。
因此,我们增加一个新的函数docount
,原来的docount2
函数保持不变:
1
2
3
4
5
|
VOID docount( COUNTER * pedg, INT32 taken )
{
if ( !taken ) return ;
pedg - >_count + + ;
}
|
相应地,修改插桩函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
VOID Instruction(INS ins, void * v)
{
...
if (INS_IsDirectControlFlow(ins))
{
COUNTER * pedg = Lookup( INS_Address(ins), INS_DirectControlFlowTargetAddress(ins) );
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR) docount,
IARG_ADDRINT, pedg,
IARG_BRANCH_TAKEN,
IARG_END);
}
else
{
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR) docount2,
IARG_INST_PTR,
IARG_BRANCH_TARGET_ADDR,
IARG_BRANCH_TAKEN,
IARG_END);
}
...
}
|
在插桩函数内部根据不同的情况,执行不同的分析代码,避免对所有类型的指令都笼统地调用性能要求高docount2
函数。
[注意]看雪招聘,专注安全领域的专业人才平台!