首页
社区
课程
招聘
3
有毒的学Pin记录(二)
发表于: 2021-10-18 09:18 22733

有毒的学Pin记录(二)

2021-10-18 09:18
22733

4. Callbacks

这部分主要介绍几个Pin的用于注册回调函数的API:

  • 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()函数放弃对应用程序的控制权限之前执行的函数,一个进程只调用一次,可以被任何线程调用,此时Pin的内存并没有释放。

对于每个注册函数的第二个参数val将在“回调”时传递给回调函数。如果在实际的场景中不需要传递第二个参数,为了保证安全,可以传递将val的值设置为0进行传递。val的理想使用方式是传递一个指向类实例的指针,这样回调函数在取消引用该指针前需要将其转换回一个对象。

 

所有的注册函数都会返回一个PIN_CALLBACK对象,该对象可以在后续过程中用于操作注册的回调的相关属性。

PIN callbacks manipulation API

在注册函数返回PIN_CALLBACK对象后,可以使用PIN_CALLBACKAPI对其进行操作,来检索和修改在Pin中已注册的回调函数的属性。

 

声明:

1
typedef COMPLEX_CALLBACKVAL_BASE *     PIN_CALLBACK

函数:

  1. CALLBACK_GetExecutionOrder()

    声明:

    1
    VOID     CALLBACK_GetExecutionOrder (PIN_CALLBACK callback)

    作用:获取已注册回调函数的执行顺序。越靠前,越早被执行。

    参数:callback,从*_Add*Funcxtion()函数返回的注册的回调函数

  2. CALLBACK_SetExecutionOrder()

    声明:

    1
    VOID     CALLBACK_SetExecutionOrder (PIN_CALLBACK callback, CALL_ORDER order)

    作用:设置已注册回调函数的执行顺序。越靠前,越早被执行。

    参数:callback,从*_Add*Funcxtion()函数返回的注册的回调函数;order,新设置的回调函数的执行顺序。

  3. PIN_CALLBACK_INVALID()

    声明:

    1
    const PIN_CALLBACK PIN_CALLBACK_INVALID(0)

    PIN回调的无效值。

CALL_ORDER

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会按照插入的顺序进行调用。

5. Mopdifying Application Instructions

虽然Pin的主要用途是对二进制程序进行插桩,但是它也可以实现对程序指令的修改。

5.1 实现方式

最简单的实现方式是插入一个分析routine来模拟指令执行,然后调用INS_Delete()来删除指令。也可以通过直接或间接插入程序执行流分支(使用INS_InsertDirectJumpINS_InsertIndirectJump)实现,这种方式会改变程序的执行流,但是会更容易实现指令模拟。

  1. INS_InsertDirectJump()

    声明:

    1
    VOID INS_InsertDirectJump(INS ins, IPOINT ipoint, ADDRINT tgt)

    参数:

    • ins:输入的指令
    • ipoint:与ins相关的location(仅支持IPOINT_BEFORE和IPOINT_AFTER)
    • tgt:target的绝对地址

    作用:插入相对于给定指令的直接跳转指令,与INS_Delete()配合使用可以模拟控制流转移指令。

  2. INS_InsertIndirectJump()

    声明:

    1
    VOID INS_InsertIndirectJump    (    INS     ins, IPOINT     ipoint, REG     reg)

    参数:

    • ins:输入的指令
    • ipoint:与ins相关的location(仅支持IPOINT_BEFORE和IPOINT_AFTER
    • reg:target的寄存器

    作用:插入相对于给定指令的间接跳转指令,与INS_Delete()配合使用可以模拟控制流转移指令。

5.2 指令内存修改

对于原始指令使用到的内存的访问,可以通过使用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));  // 重写内存指令的操作数
   }

6. Applying a Pintool to an Application

命令行:

1
pin [pin-option]... -t [toolname] [tool-options]... -- [application] [application-option]..

6.1 Pin Cmdline Options

如下是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 健全性检查

6.2 Instrumenting Applications on Intel(R) 64 Architectures

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>后面进行指定。

6.3 Injection

选项-injection仅在UNIX平台下可以使用,该选项控制着Pin注入到目标程序进程的方式。

 

默认情况下,建议使用dynamic模式。在该模式下,使用的是对父进程注入的方式,除非是系统内核不支持。子进程注入方式会创建一个pin的子进程,所以会看到pin进程和目标程序进程同时运行。使用父进程注入方式时,pin进程会在注入完成后退出,所以相对来说比较稳定。在不支持的平台上使用父进程注入方式可能出现意料之外的问题。

7. Writing a Pintool

7.1 Logging Messages from a Pintool

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" );

7.2 Performance Considerations When Writing a Pintool

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函数。


[注意]看雪招聘,专注安全领域的专业人才平台!

收藏
免费 3
支持
分享
赞赏记录
参与人
雪币
留言
时间
伟叔叔
为你点赞~
2023-3-18 05:53
PLEBFE
为你点赞~
2022-7-28 00:09
哈桑
为你点赞~
2021-10-18 16:13
最新回复 (5)
雪    币: 277
活跃值: (1677)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
太棒了,楼主辛苦
2021-10-18 16:13
0
雪    币: 15618
活跃值: (16997)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
3
哈桑 太棒了,楼主辛苦[em_13]
希望能对大家的学习起点作用
2021-10-19 14:30
0
雪    币: 576
活跃值: (2035)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
问下,pin能对加了VMP这类强壳的程序进行二进制插桩吗
2021-10-24 16:18
0
雪    币: 15618
活跃值: (16997)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
5
kakasasa 问下,pin能对加了VMP这类强壳的程序进行二进制插桩吗

pin并不能识别壳的相关逻辑,我的一个思路是可以找到解密逻辑,在解密完成后进行插桩。只要是规范的二进制可执行文件,它都可以。个人对VMP了解不多,仅是猜测参考,欢迎讨论一下~

最后于 2021-10-25 09:10 被有毒编辑 ,原因:
2021-10-25 09:10
0
雪    币: 576
活跃值: (2035)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
明白了,感谢回复。同样不杂了解VMP,只是工作偶尔需要扣算法,目前都是调试器trace+模拟执行。看到pin的指令插桩就想到了trace,就好奇问一问。
2021-10-25 18:35
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册