导语:这篇文章的目的是对动态二进制插桩的原理和基本实现过程进行全面的介绍,其中,我会选择一些最知名和最常用的动态二进制插桩框架进行具体说明,其中包括Pin,DynamoRIO和Frida。
前言
动态二进制插桩(dynamic binary instrumentation ,DBI)技术是一种通过注入插桩代码,来分析二进制应用程序在运行时的行为的方法。
动态二进制插桩技术,可以在不影响程序动态执行结果的前提下,按照用户的分析需求,在程序执行过程中插入特定分析代码,实现对程序动态执行过程的监控与分析。目前,应用广泛的动态二进制分析平台有Pin,DynamoRIO和Frida等。
最常用动态二进制插桩框架的平台
这篇文章的目的是对动态二进制插桩的原理和基本实现过程进行全面的介绍,其中,我会选择一些最知名和最常用的动态二进制插桩框架进行具体说明,其中包括Pin,DynamoRIO和Frida。而这三个里面,我会主要关注Intel公司的Pin平台,该平台提供了丰富的编程接口,开发者可以通过调用编程接口方便的获取程序动态执行期间的指令、内存和寄存器等信息,实现细粒度的动态监控,因此本文选取Pin平台进行动态分析。
Pin提供了指令、基本块和系统调用等多个层次插桩分析,其中,RTN是函数级的插桩机制,能够自动识别API函数;Trace是轨迹层次的插桩机制,能够自动识别单一入口、多出口的指令轨迹块。
不过使用动态二进制插桩框架的可不止Pin,DynamoRIO和Frida,还有比如Valgrind,Triton(使用Pin),Q动态二进制插桩,BAP,Dyninst以及许多其他框架。其中有些更成熟,有些则不太成熟。有些功能更多,有些功能更少。尽管Valgrind是最广为人知且较为成熟的动态二进制插桩框架之一,但它仅适用于Linux。所以,我根本不会用它作为分析对象。
在我插桩发现漏洞的过程中,我一直比较专注Windows上的漏洞。不过,我在本文提供的有关Windows平台的代码,你一样可以在Linux上构建,应该非常简单。而反过来则不可以,原因是在Windows上构建Pin或DynamoRIO时会失败。请注意,我写本文的目的是让你学会编写references。
为什么我只选择动态二进制插桩进行分析
根据维基百科的介绍,插桩指的是监控或测量产品性能水平、诊断错误和编写跟踪信息的能力。程序员以代码指令的形式实现插桩,监控系统中的特定组……当应用程序包含插桩代码时,可以使用管理工具对其进行管理。必须使用插桩来检查应用程序的性能,插桩方法可以有两种类型:源插桩(Source instrumentation)和二进制插桩。
顾名思义,源插桩会要求你掌握软件应用程序的源代码,否则无法进行插桩。而二进制插桩,可以与任何软件应用程序一起使用。事实证明,你在Windows操作系统上运行的大多数程序源代码都是封闭的。这意味着,在这篇文章中,我将只谈论二进制插桩。二进制插桩通常又被称为动态二进制插桩或动态二进制修改(Dynamic Binary Modification)。
在单行语句中,动态二进制插桩是一种将插桩代码注入正在运行的进程中的技术,这意味着插桩代码对注入的应用程序来说是完全透明的。
使用动态二进制插桩框架,我们可以一步一步地对目标二进制执行过程进行拆解。但请注意,分析仅适用于已执行的代码。
为何我只选择动态程序分析
目前总共有两种类型的程序分析:静态程序分析和动态程序分析。在不运行程序的情况下进行的分析就是静态分析,当我们运行程序时所进行的分析就是动态分析。
根据维基百科的介绍,动态程序分析是通过在真实或虚拟处理器上执行程序,来达到分析目的。而要使动态程序分析有效,必须让测试程序进行多次且多样的行为运行,以全方位分析。这时,你可以使用诸如代码覆盖之类的软件测试办法,以确保观察到程序的所有行为。
如前面提到的框架那样,动态二进制修改工具会在正在运行的程序和底层操作系统之间重新引入了一个层,该层会提供一些在程序执行时检查和修改用户级程序指令的机会。
不管是多么复杂的内部系统,只要通过API让其简单实用化,允许任何用户快速构建大量工具来辅助软件分析,而这正是我将在这篇文章中尝试展示的内容。
我们经常会出于各种原因来观察和修改程序的运行行为,这其中就包括软件或硬件开发人员、系统工程师、漏洞捕获者、恶意软件分析师、终端用户等。动态二进制插桩框架可以提供对每个执行的用户级指令的访问,除了可能发生的少量运行时刻和内存开销之外,该程序将与本机执行相同地运行。
静态分析的主要优点是它可以确保100%的代码覆盖率,通过动态分析,为了确保代码覆盖率更高,我需要多次运行程序,并使用不同的输入,以便分析采用不同的代码路径。但是,在某些情况下,由于软件应用程序太大,执行静态分析的成本太高。
正如我之前提到的,动态二进制插桩框架会直接在二进制文件或可执行文件中运行,这样我们就不需要程序的源代码,也不需要重新编译或重新链接程序,这个优势就允许我们分析许多软件。
动态二进制系统不但会和“guest” 执行的程序同时运行,还会在运行中执行所有请求或所需的修改。这种动态方法还可以处理动态生成代码的程序(其分析的工作量相当大),即自修改代码(self-modifying code)。如果你上网搜一下,就会发现很多情况下,动态二进制插桩框架被用于分析具有自修改代码的恶意软件。例如,请查看Blackhat Europe 2017的一篇演示文稿,或者,关于如何用Pin解压Skype的帖子。
动态二进制插桩框架被用于解决计算机体系结构问题,大量用于软件工程、程序分析和计算机安全。比如,软件工程师希望用它来深入了解他们开发的软件,系统的分析其性能和运行行为。动态二进制插桩框架的一个常见用途是模拟新的CPU指令。由于动态二进制系统在执行之前可以访问每条指令,因此硬件工程师实际上可以使用这些系统来测试当前不受硬件支持的新指令。他们可以模拟新的指令行为,而不是执行特定的指令。同样,他们也可以使用相同的方法来替换错误指令,并正确模拟所需行为。无论如何,从计算机安全的角度来看,动态二进制插桩系统可用于流程分析、污点分析、模糊测试、代码覆盖、生成测试历程、逆向工程、调试、漏洞插桩,甚至是修补漏洞以及自动利用。
使用动态二进制系统的两种主要方式和三种执行模式
使用动态二进制系统有两种主要方式,其中第一个方式是最常见的,至少在计算机安全中,是在动态二进制系统的控制下从头到尾执行程序。当我们想要实现完整的系统模拟或仿真时,我由于需要完全控制并进行代码覆盖,就要在动态二进制系统的控制下从头到尾执行程序。第二个方式就是动态二进制系统可以被附加到一个已经运行的程序中,且以完全相同的方式被调试器从正在运行的程序中附加或分离。如果我们想知道某个程序在特定时刻正在做什么,那么第二个方式就会非常有用。
此外,大多数动态二进制插桩框架都有三种执行模式:解释模式( Interpretation mode)、探测模式(probe mode)和JIT模式(just-in-time mode)。JIT模式是最常见的实现方式,也是最常用的模式,即使动态二进制插桩系统支持多种执行模式。在JIT模式下,原始二进制文件或可执行文件实际上从未被修改或执行过。因为,此时二进制文件被视为数据,修改后的二进制文件副本将在新的内存区域中生成(但只针对二进制文件的执行部分,而不是整个二进制文件),此时执行的就是这个修改后的文件副本。而在解释模式中,二进制文件也被视为数据,每条指令都被用作具有相应功能的替代指令的查找表(由用户实现) 。在探测模式中,二进制文件实际上是通过使用新指令来覆盖旧的指令,来达到修改目的的,不过这会导致运行开销增大,但在某些体系结构(如x86)中,该方式却很好用。
无论采用哪种执行模式,一旦我们通过动态二进制插桩框架控制了程序的执行,就能够将插桩添加到执行程序中。我们可以在代码块之前和之后插入想要的代码,甚至也可以完全替换它们。
你可以在下图中看到执行过程:
其中还包含不同程度的运行权限级别:
1.指令级(Instruction level);
2.基本块级(Basic block level);
3.函数级(Function level);
可以看出,运行权限级别的选择将决定你对程序执行所控制的程度。显然,这会对性能的分析产生直接影响。另外,请注意,在大多数情况下,对整个程序进行插桩是不切实际的。