我就我看到的安全视频做了一些笔记(作为一种快速回忆的方法)。
这些笔记对于初学者来说可能更有用。
这些笔记并非以难易程度排序,而是以时间顺序(也就是说,最近的在前面)。
本文全部文字在“ 知识共享署名 -非商业性使用 -相同共享方式 4.0国际 ”( CC-BY-NC-SA )条款执行提供。
写于 2017 / 7 / 2
有感于 @p4n74 和 @h3rcul35 在 InfoSecIITR #bin 的讨论。我们讨论的是让新手觉得很困惑的如何着手处理比较大的二进制文件,尤其是精简过的( stripped )。
不管是要逆向还是要 pwn 它,为能有效进行漏洞利用,你都要先分析拿到的二进制文件。由于该二进制文件可能被精简过(显示为 file ),所以要我们得知道从哪开始分析。
在寻找二进制文件里的漏洞的过程中,有几种不同的分析方式(就我收集到,不同的 CTF 战队有不同的风格):
1. 静态分析
1.1 将代码全部转换( Transpiling )成 C
这种分析方式比较少见,但是对于小型的二进制文件来说挺有用的。这种方法就是将整个文件都拖进逆向工具中。在 IDA 中打开每一个函数(用 decomplier view ,反编译视图),然后重命名(快捷键: n ) , 重新编辑(快捷键: y )这些函数使反编译代码更具可读性。然后,将这些代码复制 / 导出成单独的 .c 文件,这些 C 文件可以经过编译也能得到和最初那个文件差不多的二进制文件。至此,对源文件的分析就算做好了,可以用来查找漏洞或者其他有用信息了。只要找到漏洞点,利用 IDA 处理好的汇编和伪码(用 Tab 实现两者的快速切换,用空格切换函数的图形视图和文本视图)就可以进行 exploit 的编写了。
1.2 最小化对反编译的分析
我们常常得做这件事,因为二进制文件里的大部分是相对没用的(从攻击者的角度)。你只需要分析可疑函数或者能帮你找到漏
洞的函数。为达此目的,可以从以下几个方面着手:
1.2.1 从 main 函数开始
现在,很多精简过的二进制文件( stripped binary ),对 main 函数并没有标记(虽然 IDA 6.9 以后的版本已经帮你标记了),但是,时间久了你也得自己学会如何从进入点(也就是 IDA 默认打开的地方)到达 main ,并从那里开始分析。
1.2.2 查找相关的字符串
有的时候,你知道程序会输出某些有用的字符串(比如说,逆向中的“恭喜, flag 是 %s ”) . 你可以跳转到到字符串视图(快捷键: Shift+F12 ),找到该字符串,用 XRefs 进行逆向操作(快捷键: x ) .XRefs 能帮你找到该字符串的函数路径,对路径中的函数使用 XRefs ,直到你找到 main (或者你知道的其他点)。
1.2.3 从一些随机函数开始
有的时候,没有特别有用的字符串,然后你又不想从 main 开始。这个时候,你可以快速浏览整个函数表,查找看起来比较可疑的函数(比如有很多常量,很多 xor 等等)或者调用了重要函数(对 malloc , free 等等进行 XRefs ),然后你就可以从那里开始了,向前(顺着调用它的函数)或者向后( XRef 该函数)均可。
1.3 纯汇编分析
有时,你无法使用反编译视图(原因可能是:奇怪的架构,或者反汇编技术,或者手动汇编,或者反汇编看起来过于复杂)。这种情况下,只看反汇编视图是最有效的法子了。对于未知的架构,打开自动备注功能是一个非常有用的方法,因为它可以给每一条指令进行注释。另外,节点着色( node colorization )和群节点功能( group nodes functionalities )也超有用。即使你都没有用到这些,只是简单的给汇编语言加注释就能帮到很多。就我个人而言,我更喜欢写 python 形式的注释,这样我稍后就可以手动将它们转换成 python 了(对不得不用 Z3 库的逆向尤其有用)。
1.4 使用类似 BAP 的平台
这种分析是(半)自动的,而且对于大多数软件来说通常是有用的,但是很少能直接用在 CTF 中。
2.Fuzzing 模糊测试
Fuzzing 是获取漏洞很有效的技术。你都不需要完全了解它,只需使用漏洞检查工具( Fuzzer )就可以得到很多触手可及的漏洞,之后再对它们进行分析分类得到真正的漏洞。更多信息参看我的笔记 basics of fuzzing 和 genetic fuzzing 。
3. 动态分析
动态分析可以在使用静态分析找到漏洞之后进行,以便更快的写出 exploit 。当然,也可以用动态分析来发现漏洞。一般是这样,在调试器中运行可执行程序,尝试着走一遍代码以期找到触发 bug 的地方。在合适的地方设置断点,然后分析寄存器 / 堆 / 栈的状态,我们就可以知道发生了什么。我们也可以用调试器快速确定重要函数。可以这么做:比如,在最开始的时候对所有函数都暂时插入断点,然后分两步走:一步走所有不重要的函数,一步只走重要的函数。第一步经过所有不重要的函数然后取消端点,最后只留下第二步中的重要函数还有断点。
我个人的分析方式是,先用静态分析,通常从 main (或者,对于不是基于 console 的应用程序,从字符串)开始,快速找到可疑函数。然后从这里开始向前向后扩展,通常会写注释,重命名,重编辑变量以便反编译。和很多人一样,对于可能有用但现在还未知的 函数 / 变量等 我会用 Apple , Banana , Carrot 等这些名称,为便于分析(对我来说,跟踪 func_123456 这种风格的名字有点难)。我也会用 IDA 中的结构体视图来定义结构体(和枚举)来使反编译的效果更好。一旦我找到漏洞,我会用 pwntools( 并用它来调用 gdb.attach() ) 来写一个脚本。这样我对于发生了什么就有更多的控制权。在 gdb 中,我通常使用 gdb 的简单版本,虽然我有添加 peda 命令,如果需要就可以即时加载 peda 。
我的风格也一直在变化,因为对于我的工具我用得越来越顺手了,再加上自己写的工具。非常乐意听到其他的分析方式, 以助我更快分析。如果您对本文有任何评论 / 批评 / 赞扬,请移步我的 Twitter @jay_f0xtr0t .
写于 2017 / 6 / 4
有感于 Gynvael Coldwind 的 这个 超赞的视频。在这个视频中他谈及 ROP 的基础,并提了一些技巧。
面向返回编程( Return Oriented Programming , ROP )是最典型的漏洞利用技术之一,用来避开 NX ( non executable memory, 不可执行内存)保护。微软的 NX 是 DEP ( data execution prevention, 数据执行保护)。 Linux 也有 NX ,这就意味着,在这种保护机制下,你不再可以将 shellcode 放到堆栈中并使之得以执行。现在,要执行代码,你得进入之前已经存在的代码区( main binary 主库 , 或者它的库—— Linux 的 libc 、 ldd 等; Windows 的 kernel32 、 ntdll 等)。 ROP 是通过复用已经存在的代码片段产生的,并指出如何将你想要执行的代码融入到这些代码片段中。
起初, ROP 是从 ret2libc 开始的,然后通过可以使用更多的小代码块而逐步发展。有人说, ROP 已“死”,因为新增的保护技术。但是在很多场合中依然有用(而且在很多 CTF 中是必须的)。
ROP 最重要的部分是 gadgets (指令片段)。 Gadget 是“可用于 ROP 的代码块”。通常是以 ret 结尾(其他类型的 gadget 可能也会有用,比如以 pop eax; jmp eax 等结尾的)。我们将这些 gadget 串在一起形成 exploit ,成为 ROP 链。
ROP 最重要的前提是你对栈有控制权(也就是说,栈指针指向你能控制的缓冲区)。如果这个条件不成立,你需要使用一些技巧(比如, stack pivoting 栈转)来获取控制。
你要如何提取 gadget ?使用可以下载的工具(比如, ropgadget )或者在线工具(比如, ropshell )或者你自己写(对一些比较难的题目,这个可能更有用,因为你可以有针对性的设计)。最基本的,我们需要可以跳转到这些 gadget 的地址。这里就有一个 ASLR (地址空间配置随机加载)的问题,在这种情况下,在真正可以执行 ROP 之前你会遭遇地址泄露。
现在是,我们要如何利用这些 gadget 形成 rop 链?我们首先寻找“基础 gadget ”。这类 gadget 可以帮我执行一些简单的任务(例如, pop ecx; ret ,通过放置这个 gadget 可以将某个值加载到 ecx 中,在加载完该值后,就返回到链尾)最有用的基础 gadget 通常是“指向寄存器”,“将寄存器的值放置在指向某个寄存器的地址中”。
我们可以从这些最基础的函数开始逐步获取高级功能(类似下面的文章, exploitation abctionstra )。例如,利用“指向寄存器”,和“在某个地址存值”这个 gadget ,我们可以构造函数实现将特定的值传入特定的地址。用这个函数,我们可以将任意的字符串传到内存的任意位置。有了这么一个函数我们就差不多大功告成了。因为我们可以在内存中构造任意数据结构,也可以用任意的参数调用任意的函数(因我没有可以指向寄存器,将值保存到栈中)。
从基础函数开始而不是从可以实现复杂功能的函数开始的一个重要原因是,减少出错的几率(这在 ROP 中很常见)。
还有很多关于 ROP 的想法,工具,技巧,但这可能是另一篇笔记的内容了,下次吧 : )
PS: Gyn 的博文 Return-Oriented Exploitation 值得一读。
写于 2017 / 3 / 27 ,增加于 2017 / 3 / 29
有感于 Gynvael Coldwind 的 这个 视频。在视频中他谈论了遗传模糊背后的理论,并开始组建一个简易的遗传模糊测试工具。之后,他在 这个 视频中完成了其实现。
“高级的”模糊测试(相较于我在 "Basics of Fuzzing" 提到的盲测试器, blind fuzzer )也修改 / 变异字节,但是比起盲测,它做得更智能一些。
我们为什么需要一个遗传模糊测试器呢?
对于盲测试器来说,有些程序可能会比较难搞,因为有的漏洞可能需要几个条件同时满足才会触发。在盲测试器中,发生这种事的可能性非常低,因为它不知道它是否有些许进化。举个具体的例子,如果有这么一段代码: if a: if b: if c: if d: crash! (让我们称之为 CRASHER ),这段代码表示需要满足四个条件才能 crash 该程序。然而,盲测试器可能就无法通过条件 a, 因为四个变异 a,b,c,d 同时发生的可能性太低了。事实上,即使它做了 a 进化了,下一个变异可能有回到 !a ,因为它对程序一无所知。
稍等,那么什么时候这种情况才会出现?
举个例子,这在文件格式解析器中就很常见啊。为获取某些特定的代码路径,必须同时检查好几项“这个必须是这个值,那个必须是那个值,其他的也必须是规定好的”等等。还有,没有一款现实世界的软件是“简单的”,大多数软件有很多很多可能的路径,有些路径只有同时满足几个条件才能到达。因此,很多这类程序路径对于盲测试器来说是不可达的。另外,有些路径是真的不可达,因为没有完成足够的变异。如果这些路径由 bug ,盲测试器将无法将其找出。
那我们可以怎样可以比盲测试器做得更好?
考虑上面那段 CRASHER 代码的流程图。如果某个盲测试器机缘之下满足 a, 那它也不会意识到自己到达了新的节点,所以它会将其忽略。另一方面, AFL (和其他遗传或“ smart ”模糊测试器)会做的是,它们能意识到这是新的信息(“一条全新的路径”)然后将这个样本作文一个新的点保存到资料库中。这就意味着,现在这个测试器可以从 a 开始。当然,从 a 开始有时也会返回到 !a ,但更多时候,它不会,而是更可能到达 b 。这样又到达了一个新的节点,将其加入资料库中。继续这个操作,将会有越来越多的路径得到检查,最后到达 crash !。
为什么要这样做?
将变异的样本保存到资料库中,就能到达之前未能到达的区域,也就能对这些区域进行模糊测试。那既然我们可以对这些区域进行测试,自然能找到这些区域的 bug 。
为什么称之为遗传模糊?
这种“ smart ”模糊测试类似于遗传算法。样本的交叉、变异能产生新的样本。我们保留能适应我们测试条件的样本。在这个例子中,条件是“它能到达流程图中的多少个节点?”。保存能到达更多节点的。跟遗传算法不是很像,但是它的一个变体(因为我们保存了所有到达未探查领域的节点,并且没有做交叉)。基本上就是,从已经存在的样本中选择,进行变异,之后进行适应性测试(看它是否能到达新领域),再重复。
等一下,所以我们只是记录未到达节点?
不,并非如此。 AFL 记录的是图中的边,而不是点。还有,它不仅仅是说“边被遍历过或没有”,它记录该边遍历了多少次。如果一条边遍历了 0 , 1 , 2 , 4 , 8 , 16 ,…次,它就可以当作是一条新的路径并添加到资料库中。这么做是因为,查看边比查看点更能区分应用程序的状态,用指数级增长来记录遍历的次数能够给到给多的信息(一条边被遍历一次很遍历两次有很大的不同,但是遍历 10 次和遍历 11 次的区别就没有那么大了)。
那么,在遗传模糊测试器中你要做什么?
我们需要两样东西。第一部分叫做追踪器 tracer (或者,追踪设备)。它主要是告诉你执行了哪条指令。 AFL 用一种很简单的方法做到这一点。它跳到编译阶段,在形成汇编语言之后,编译之前,它会寻找基础代码块(通过找结束点,检查跳转 / 分支指令),并添加代码到每一个代码块以标志该代码已经执行(可能会进入 shadow memory 影子内存或其他)如果我们没有源代码,我们用其他技术来进行追踪(比如: pin, 调试器等)。事实证明,即使是 ASAN 也能给出覆盖率信息(具体看查看相关文档)。
第二部分是,我们利用追踪器提供的覆盖率信息去追踪刚出现的新路径,并将这些刚形成的样本增加到资料库中,以便将来进行随机选择。
追踪器有多种机制,有基于软件的,也有基于硬件的。基于硬件的,拿 Intel CPU 来说,其内存中有一个缓冲区,它记录了所有进入该缓冲区的基本代码块。这属于内核特征,所以内核不得不支持它并提供 API ( Linux 是这样的)。基于软件的,我们通过增加代码,或是用调试器(用临时断点,或者通过单步),或者用内存检测工具( Address Sanitizer , ASAN )的追踪功能,或者用钩子,或者虚拟机,或者其他的方法来达到。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)