我已经开始写下有关我观看的安全性视频的注释(作为快速召回的方式)。
这些可能对初学者更有用。
这里的笔记顺序不是按难度顺序排列的,而是按照我的编写方式的反向时间顺序(即,最新的第一)。
这项工作是根据创意共享归因于非商业共享4.0国际许可证的许可。
撰写于2017年8月12日
受Gynvael的信心CTF 2017年的直播的影响;以及他的Google CTF Quals 2017 Rivestream在这里
有时,挑战可能通过实施VM来实现复杂的任务。并非总是有必要完全逆转VM并致力于解决挑战。有时,您可以稍微恢复一点,一旦知道发生了什么事,就可以挂接到VM,并访问所需的东西。此外,基于计时的侧通道攻击在VM中变得更加容易(主要是由于执行了更多的“真实”指令。
仅通过寻找常数并在线搜索它们,就可以识别并快速重新识别二进制文件中的密码有趣的功能。对于标准加密函数,这些常数足以快速猜测功能。可以更轻松地识别更简单的加密功能。如果您看到很多XOR和类似的事情发生,并且不容易识别的常数,则可能是手工滚动的加密货币(也可能损坏)。
有时,在将IDA与十六进制一起使用时,拆卸视图可能比倒数式观点更好。如果您注意到在解码视图中似乎发生了很多并发症,但是您会在拆卸视图中注意到重复性模式,尤其如此。 (您可以使用太空栏快速切换两者)。例如,如果实现了(固定尺寸的)大型库,那么解码视图很糟糕,但是拆卸视图很容易理解(并且由于重复的“带有携带”的说明,例如adc并且可以易于识别)。此外,在这样的分析时,使用IDA图表视图中的“组节点”功能非常有用,对于快速降低图形的复杂性非常有用,因为您了解每个节点的作用。
对于怪异的体系结构,拥有良好的仿真器非常有用。特别是,一旦您将内存从仿真器中拿出来,可以使用可以给您带来内存的模拟器来快速弄清楚发生了什么,并识别有趣的部分。此外,使用以舒适的语言实现的模拟器(例如Python),这意味着您可以准确地运行自己喜欢的东西。例如,如果您可能希望多次运行的代码中有一些有趣的部分(例如,要蛮力或其他东西),则使用模拟器,您可以快速编码仅执行代码那部分的内容,而不是必须运行完整的程序。
懒惰时,懒惰是好的。不要浪费时间逆向工程,而要花足够的时间进行侦察(即使在重新挑战中!),以便能够减少实际完成更艰巨的雷(Reing)的时间。在这种情况下,侦察的意思是,只需快速查看不同的功能,而无需花费太多时间来彻底分析每个功能。您只需快速衡量该功能的意义(例如“看起来像加密货币的东西”或“看起来像记忆管理的事物”,等等)
对于未知的硬件或体系结构,请花足够的时间在Google上查找它,您可能会使用大量有用的工具或文档来幸运,这些工具或文档可能会帮助您更快地构建工具。通常,您会发现玩具模拟器等实现可能是可以从一开始就可以从中开始的。另外,您可能会获得一些有趣的信息(例如如何存储位图,或者如何存储字符串或其他内容),您可以使用这些信息来编写一个快速的“修复”脚本,然后使用普通工具来查看是否有有趣的内容。
GIMP(图像操纵工具)具有非常酷的打开/负载功能,可以查看原始像素数据。您可以使用它快速寻找原始二进制数据中的资产或重复结构。一定会花时间弄乱设置,以查看是否可以从中收集更多信息。
写于2017年7月2日
受 @p4n74和 @h3rcul35的讨论的影响。我们正在讨论有时初学者如何从更大的挑战开始,尤其是当它被剥离时。
要么解决重新挑战,要么能够在PWN上进行PWN,必须首先分析给定的二进制文件,以便能够有效利用它。由于二进制文件可能会被剥离等(使用file找到),因此必须知道在哪里开始分析,以使立足点从中构成。
在寻找二进制文件的漏洞时(从我收集到的漏洞中,不同的CTF团队都有不同的偏好):
1.1。将完整的代码转换为C
这种分析很少见,但对于较小的二进制文件非常有用。这个想法是进入倒数工程师的整个代码。每个功能均在IDA中打开(使用反编译器视图),并使用重命名(快捷方式:N)和重新启动(快捷方式:Y)来快速使分解代码更具可读性。然后,将所有代码复制/导出到一个单独的.c文件中,可以将其编译以获得与原始的等效(但不相同)的二进制文件。然后,可以进行源代码级别的分析,找到漏洞等。一旦找到了脆弱点,然后通过在IDA中的良好倒数源来构建的原始二进制构建中,并与拆卸视图并肩(使用选项卡(使用选项卡)(使用tab)快速切换两者之间的空间;并在图形和文本视图之间快速切换空间,以换取分配视图)。
1.2。最小化分析
这是经常进行的,因为大多数二进制都是相对毫无用处的(从攻击者的角度来看)。您只需要分析可疑的功能,或者可能导致您进入vuln。为此,有一些方法可以开始:
1.2.1。从Main开始
现在,对于被剥离的二进制文件,即使是MAIN也没有标记(尽管IDA 6.9开始为您标记为您),但是随着时间的流逝,您将学会识别如何从入口点到达主(IDA在其中打开的位置默认打开)。您可以跳到这一点,然后从那里开始分析。
1.2.2。找到相关的字符串
有时,您知道可能会输出的一些特定字符串,您知道可能很有用(例如“恭喜,您的标志为%s”,以进行重新挑战)。您可以跳到字符串视图(快捷方式:Shift+F12),找到字符串,并使用Xrefs(快捷方式:X)向后工作。 XREF可让您通过在该链中的所有功能上使用XREF来找到该字符串的功能路径,直到到达MAIM(或您知道的某个点)。
1.2.3。从某些随机功能
有时,没有特定的字符串可能有用,并且您不想从Main开始。因此,相反,您迅速浏览整个功能列表,寻找看起来可疑的功能(例如有很多常数,很多XOR等)或调用重要功能(Malloc的Xrefs,free等),然后从那里开始,然后向前启动并向前(遵循函数)和向后(函数)(Xrefs of the函数)(XREFS)
1.3。纯拆卸分析
有时,您无法使用偏这次视图(由于怪异的结构,反编译技术,或者手写的组装,或者看起来太复杂)。在这种情况下,纯粹看一下拆卸视图是完全有效的。 (对于新体系结构)打开自动注释非常有用,该评论显示了说明每个说明的评论。另外,节点着色和组节点功能非常有用。即使您不使用任何一种,拆卸中的定期标记评论很有帮助。如果我个人这样做,我更喜欢写下类似python的评论,这样我就可以快速地手动转移到python中(对于重新挑战尤其有用,您可能必须使用Z3等)。
1.4。使用BAP等平台,等等。
这种分析是(半)自动化的,通常对于更大的软件更有用,很少直接在CTF中使用。
模糊可能是快速进入漏洞的有效技术,而无需最初实际理解它。通过使用模糊器,人们可以获得许多低悬挂的漏洞风格,然后需要对其进行分析和分析以进入实际的vuln。有关更多信息,请参阅我关于模糊和遗传模糊基础的笔记。
在使用静态分析找到VULN后,可以使用动态分析,以帮助快速构建漏洞。另外,它可以用来找到vuln本身。通常,一个人在调试器内部启动可执行文件,并尝试沿触发错误的代码路径。通过在正确的位置放置断点,并分析寄存器/堆/堆栈/等的状态,可以很好地了解正在发生的事情。人们还可以使用调试者快速识别有趣的功能。例如,可以通过最初在所有功能上设置临时断点来完成这一点。然后继续进行2次步行 - 一个穿过所有无趣的代码路径;一条只有一条有趣的道路。第一次步行旅行所有无趣的功能并禁用这些断点,从而使有趣的功能在第二次步行期间出现为断点。
我的个人分析方式是从静态分析开始,通常是从主(或基于非辅助的应用程序,来自字符串)开始,然后努力快速找到一个看起来很奇怪的函数。然后,我花时间并从这里开始向前和向后分支,定期写下评论,并不断重命名和重新定型变量以改善不合情可及。像其他人一样,我确实使用诸如Apple,Banana,Carrot等的名称似乎有用,但是截至尚不清楚的功能/变量/等,它可以更易于分析(跟踪Func_123456名称样式对我来说太难了)。我还定期使用IDA中的结构视图来定义结构(和枚举),以使解构更加更好。一旦找到vuln,我通常会使用pwntools编写脚本(并用它来调用gdb.attach() )。这样,我可以控制正在发生的事情。在GDB内部,我通常使用普通的GDB,尽管我添加了一个命令peda ,该命令在需要时立即加载PEDA。
不过,我的风格肯定在不断发展,因为我对工具变得更加满意,并且使用了为加快速度加快速度的定制工具。我很高兴听到其他分析样式的声音,以及可能会更改我的风格,这可能有助于我更快地变得更快。对于您所拥有的任何评论/批评/赞美,我可以通过Twitter @jay_f0xtr0t与我联系。
写于2017年6月4日
受Gynvael Coldwind的真棒现场直播的影响,他在那里讨论了ROP的基础知识,并提供了一些技巧和技巧
面向返回的编程(ROP)是经典的开发技术之一,用于绕过NX(非可执行内存)保护。微软已将NX纳入DEP(数据执行预防)。甚至Linux等,都具有有效的效果,这意味着有了此保护,您再也无法将ShellCode放在堆/堆栈上,并仅通过跳到它来执行它。因此,现在,为了执行代码,您可以跳入预先存在的代码(Main Binary或其库 - Linux上的LIBC,LDD等; Windows上的kernel32,ntdll等)。 ROP通过重新使用已经存在的代码的片段而出现,并找出一种将这些片段结合起来做您想做的事情的方法(当然,这是破解星球!!!)。
最初,ROP从RET2LIBC开始,然后通过使用更多的小型代码随着时间的推移而变得更加先进。有人可能会说,由于其他保护措施,ROP现在已经“死了”,但是在许多情况下仍然可以利用它(对于许多CTF来说绝对必要)。
ROP最重要的部分是小工具。小工具是“ ROP的可用代码”。这通常意味着以ret结束的代码(但是其他类型的小工具也可能有用;例如以pop eax; jmp eax等结尾的小工具)。我们将这些小工具链在一起以形成利用,即被称为ROP链。
ROP最重要的假设之一是您可以控制堆栈(即,堆栈指针指向您控制的缓冲区)。如果这不是真的,那么您将需要应用其他技巧(例如堆栈旋转)以在构建ROP链之前获得此控制。
您如何提取小工具?使用可下载的工具(例如Ropgadget)或在线工具(例如Ropshell)或编写自己的工具(有时可能会对更困难的挑战更有用,因为如果需要的话,您可以将其调整为特定的挑战)。基本上,我们只需要可以跳到这些小工具的地址即可。这是ASLR等问题的地方(在这种情况下,您会在继续进行ROP之前会泄漏地址)。
因此,现在,我们如何使用这些小工具制作ropchain?我们首先寻找“基本小工具”。这些是可以为我们完成简单任务的小工具(例如pop ecx; ret ,可通过放置小工具将值加载到ecx中,然后放置要加载的值,然后是链的其余链,在值加载后返回到该值之后)。通常最有用的基本小工具通常是“设置寄存器”,“存储寄存器值在地址指向寄存器”,等等。
我们可以从这些原始功能中建立以获得更高级别的功能(类似于我的帖子标题为“剥削抽象”)。例如,使用Set-Register和Att-At-At-At-Ad-At-At-Ad-At-Atdress小工具,我们可以提出一个“ oke”功能,使我们可以设置具有特定值的任何特定地址。使用此功能,我们可以构建一个“ oke string”功能,使我们可以将任何特定字符串存储在内存中的任何特定位置。现在我们已经有了戳弦,我们基本上已经完成了,因为我们可以在内存中创建任何想要的结构,并且还可以用所需的参数调用我们想要的任何功能(因为我们可以设置注册并可以将值放在堆栈上)。
从这些低阶原语构建到更复杂的事情的较大功能的最重要原因之一是减少犯错的机会(否则在ROP中很常见)。
ROP有更多复杂的想法,技巧和技巧,但这可能是不同时间的单独的主题:)
PS:Gyn有一个关于返回剥削的博客文章,可能值得一读。
撰写于2017年5月27日;延长于2017年5月29日
受Gynvael Coldwind的惊人现场直播的影响,他在那里谈论了遗传模糊背后的基本理论,并开始建立一个基本的遗传绒毛。然后,他继续完成此直播中的实现。
“高级”模糊(与我的“模糊基本”注释中描述的盲绒毛相比)。它也会修改/突变字节等,但比盲人“ Dumb” Fuzzer更聪明。
为什么我们需要遗传绒毛?
某些程序可能对愚蠢的模糊剂“讨厌”,因为漏洞可能需要满足一大堆条件才能达到。在愚蠢的魔力中,我们对这种情况的可能性很低,因为它是否不知道它是否取得了任何进展。作为一个特定示例,如果我们有代码if a: if b: if c: if d: crash! (我们将其称为崩溃代码),然后在这种情况下,我们需要4个条件才能满足使程序崩溃。但是,愚蠢的模糊器可能无法超越a条件,仅仅是因为所有4个突变a , b , c , d ,同时发生的可能性很小。实际上,即使仅通过a进行进展,下一个突变也可能会恢复!a只是因为它对程序一无所知。
等等,这种“坏案例”程序何时出现?
以文件格式解析器非常普遍,以一个例子为例。要达到某些特定的代码路径,可能需要超越多个检查“这个值必须就是这个,并且该值必须就是那个,而其他一些值必须是其他东西”等等。此外,几乎没有现实世界的软件“简单”,并且大多数软件都有许多可能的代码路径,其中一些可能只有在该州的许多内容正确设置后才能访问。因此,其中许多程序的代码路径基本上是愚蠢的模糊基本上无法访问的。此外,有时,由于没有足够的突变,某些路径可能完全无法访问(而不是疯狂地不可能)。如果这些路径中的任何一个都有错误,那么愚蠢的伪造者将永远无法找到它们。
那么,我们如何比愚蠢的模糊剂做得更好?
考虑上述碰撞器代码的控制流程图(CFG)。如果偶然的愚蠢的魔力突然得到正确的a ,那么它也不会认识到它达到了一个新节点,但是它会继续忽略它,丢弃样本。另一方面,AFL(以及其他遗传或“智能”模糊器)是他们将其视为新信息(“新近到达的路径”),并将此样本存储为语料库的新初始点。这意味着现在,模糊器可以从a块开始并进一步移动。当然,有时候,它可能会返回b a样品中的!a这再次是一个新的节点,因此在语料库中添加了一个新样本。这继续存在,允许检查越来越多的路径,最后到达了crash! 。
为什么这起作用?
通过将突变的样品添加到语料库中,可以探索图形更多(即以前未探索的部分),我们可以到达以前无法到达的区域,因此可以掩盖此类区域。由于我们可以掩盖此类区域,因此我们也许可以在这些地区发现错误。
为什么被称为遗传模糊?
这种“智能”模糊类似于遗传算法。标本的突变和交叉引起新标本。我们保留更适合经过测试条件的标本。在这种情况下,条件是“图中有多少个节点?”。可以保留越多的人。这并不像遗传算法,而是一种变化(因为我们将所有标本都保留在遍历未开发的领域,并且我们不进行跨界),但是足够相似,可以得到相同的名称。基本上,从预先存在的人群中进行选择,其次是突变,然后进行健身测试(无论是看到新区域)和重复。
等等,所以我们只是跟踪未触及的节点吗?
不,不是真的。 AFL跟踪图表中的边缘遍历,而不是节点。此外,它不仅说“边缘旅行或不旅行”,还跟踪了边缘穿越多少次。如果边缘遍历0、1、2、4、8、16,...时间,它被认为是“新路径”,并导致添加到语料库中。之所以这样做,是因为查看边缘而不是节点是区分应用程序状态的更好方法,并且使用指数增加的边缘遍历计数可提供更多信息(边缘遍历一次与遍历两次完全不同,但是遍历10与11次没有太大不同)。
那么,您在遗传爆炸器中需要什么?
我们需要两件事,第一部分称为示踪剂(或示踪仪器)。它基本上告诉您在应用程序中执行了哪些说明。 AFL通过在编译阶段之间跳入简单的方式来做到这一点。生成组件后,但是在组装程序之前,它会寻找基本块(通过查看结局,检查跃升/分支类型的指令),并将代码添加到每个块中,以将块/边缘标记为执行的块/边缘(可能是某些阴影存储器之类的东西)。如果我们没有源代码,我们可以使用其他技术进行跟踪(例如PIN,调试器等)。事实证明,即使是Asan也可以提供覆盖信息(请参阅此文件)。
然后,在第二部分中,我们使用示踪剂给出的覆盖范围信息来跟踪新路径的显示,并将这些生成的样本添加到语料库中,以便将来随机选择。
有多种机制可以制作示踪剂。它们可以基于软件,也可以基于硬件。对于基于硬件的情况,例如,存在一些英特尔CPU功能,其中给定存储器中的缓冲区,它记录了所有基本块的信息。它是一个内核功能,因此内核必须支持它并将其作为API(Linux具有)。对于基于软件的,我们可以通过添加代码或使用调试器(使用临时断点,或通过单个步进)来做到这一点,或使用地址消毒器的跟踪能力,或使用挂钩或模拟器,或其他许多其他方法。
区分机制的另一种方法是通过Black-Box跟踪(您只能使用未修改的二进制文件)或软件White-Box跟踪(您可以访问源代码,并修改代码本身以添加跟踪代码)。
AFL在编译过程中使用软件仪器作为追踪方法(或通过QEMU仿真)。 Honggfuzz支持基于软件和基于硬件的跟踪方法。其他智能模糊可能会有所不同。 Gyn构建的一种使用地址消毒剂(ASAN)提供的跟踪/覆盖范围。
一些模糊器使用“ Speedhacks”(即提高模糊速度),例如通过提出叉子或其他此类想法。可能值得研究这些:)
写于2017年4月20日
受Gynvael Coldwind的真棒现场直播的影响,他在那里谈论了什么模糊,还从头开始建立了基本的模糊!
首先,什么是模糊器?我们为什么要使用它?
考虑到我们有一个获取输入数据的库/程序。输入可以以某种方式结构(例如PDF,PNG或XML等;但不需要任何“标准”格式)。从安全的角度来看,如果输入和过程 /库 /程序之间存在安全边界,那么有趣的是,我们可以传递一些“特殊输入”,这会导致超出该边界以外的意外行为。模糊器就是这样做的。它是通过“突变”输入中的事物(从而使其损坏)来实现这一目标,以导致正常执行(包括安全处理错误)或崩溃。由于边缘案例逻辑不能很好地处理,因此可能发生这种情况。
崩溃是错误条件的最简单方法。可能还有其他。例如,使用ASAN(地址消毒剂)等可能也导致检测更多内容,这可能是安全问题。例如,缓冲区的单个字节溢出可能不会自行造成崩溃,但是通过使用ASAN,我们甚至可以使用Fuzzer捕获它。
fuzzer的另一个可能用途是,通过模糊一个程序生成的输入也可以在另一个库/程序中使用,并查看是否存在差异。例如,这样的一些高精度数学库错误是这样的。但是,这通常不会导致安全问题,因此我们不会专注于这么多。
模糊如何工作?
fuzzer基本上是一种突变的执行循环,它探讨了应用程序的状态空间,以尝试“随机”查找崩溃 /安全性vuln的状态。它找不到漏洞,只是一个vuln。模糊的主要部分是突变器本身。稍后再详细介绍。
来自fuzzer的输出?
在模糊器中,(有时)将调试器附加到应用程序中,以从崩溃中获取某种报告,以便稍后将其分析为安全液体与良性(但可能很重要的)崩溃。
如何首先确定哪些程序最能陷入模糊?
当模糊时,我们通常希望专注于一组或一组程序。这通常主要是为了减少要执行的执行量。通常,我们只专注于解析和处理。同样,安全边界在确定哪些部分对我们很重要时很重要。
类型的模糊?
给出的输入样本称为copus 。在Oldschool Fuzzers(又名“盲人”/“ Dumb” Fuzzzers)中,有必要进行大型语料库。较新的(例如,又名“遗传”模糊器,例如AFL),不一定需要如此大的语料库,因为它们会自己探索国家。
模糊如何有用?
模糊剂主要用于“低悬挂果”。它不会发现复杂的逻辑错误,但是很容易找到错误(实际上,在手动分析中,这实际上很容易错过)。虽然我可能会在整个注释中说输入,并且通常是指输入文件,但不仅仅是这样。 Fuzzer可以处理可能是stdin或输入文件或网络套接字的输入。但是,如果没有过多的一般性损失,我们可以将其视为目前的文件。
如何编写(基本)绒毛?
同样,它只需要是一个突变的重复循环即可。我们需要能够经常调用目标( subprocess.Popen )。我们还需要能够将输入传递到程序(例如:文件)中并检测崩溃( SIGSEGV等)导致例外,可以捕获)。现在,我们只需要为输入文件编写一个突变器,然后继续在突变文件上调用目标。
突变器?什么?!?
可能有多个可能的突变器。容易(即易于实现)可能是突变,突变字节或突变为“魔术”值。为了增加崩溃的机会,而不是仅更改1位或其他东西,我们可以更改多个(也许是某些参数化百分比?)。我们还可以(而不是随机突变),将字节/单词/dwords/等更改为某些“魔术”值。魔术值可能为0 , 0xff , 0xffff , 0xffffffff (32位INT_MIN ), 0x7fffffff (32- 0x80000000 INT_MAX )等。基本上,选择与引起安全问题的共同点(因为它们可能触发某些边缘情况)。如果我们知道有关该程序的更多信息,我们可以编写更智能的突变器(例如,对于基于字符串的整数,我们可能会写一些将整数字符串更改为"65536"或-1等的内容)。基于块的突变器可能会移动零件(基本上是重新组织输入)。添加/附加突变器也有效(例如,导致更大的输入到缓冲区中)。截断器也可能起作用(例如,有时EOF可能无法很好地处理)。基本上,尝试一系列创造性的方式来弄脏事物。关于该程序的经验(一般而言)的经验越多,可能会有更多有用的突变器。
但是,这种“遗传”模糊是什么?
这可能是以后的讨论。但是,一些指向某些现代(开源)模糊的链接是AFL和Honggfuzz。
写于2017年4月7日
受到2017年Picoctf的挑战的影响(由于比赛仍在进行中,因此挑战的名称)
警告:对于某些读者来说,这张注释似乎很简单/显而易见,但这需要说,因为直到最近我才清楚地层。
当然,当编程时,我们所有人都使用抽象,无论它们是类,对象,功能,元功能,多态性,单子或单调或函数,或所有爵士乐。但是,在剥削过程中,我们真的可以有这样的事情吗?显然,我们可以利用实施上述抽象时犯的错误,但是在这里,我谈论的是不同的事情。
在多个CTF中,每当我以前写过exploit时,它一直是一个掉落的脚本,它会丢弃外壳。我将惊人的pwntools用作一个框架(用于连接到服务,转换事物以及Dynelf等),但仅此而已。每个漏洞利用往往是为实现任意代码执行目标的临时方式。但是,当前的挑战以及我先前关于“高级”格式弦剥削的注释,使我意识到我可以以一致的方式分层漏洞,并通过不同的抽象层进行移动以最终达到必要的目标。
例如,让我们将漏洞视为逻辑错误,这使我们可以进行4个字节的读/写入,但缓冲区后的范围很小。我们想一直滥用这一点,以获得代码执行,最后是标志。
在这种情况下,我认为这种抽象是一个short-distance-write-anything原始的。有了这个本身,显然我们不能做太多。但是,我制作了一个小的python函数vuln(offset, val) 。但是,由于在缓冲区之后,可能会有一些数据/元数据可能有用,因此我们可以滥用它来构建任何read-anywhere ,又write-anything-anywhere内容。这意味着,我编写了简短的Python函数,该函数调用先前定义的vuln()函数。这些get_mem(addr)和set_mem(addr, val)函数仅通过使用vuln()函数覆盖指针而简单地(在当前示例中)制作,然后可以在二进制中的其他地方重新定位。
现在,在使用这些get_mem()和set_mem()抽象之后,我通过从get_mem()中泄漏了2个地址,并与libc数据库进行比较(感谢@niklasb的数据库)。这些来自这些的偏移使我有一个可靠的libc_base ,这使我可以用libc的另一个功能替换got中的任何功能。
从本质上讲,这使我可以控制EIP(当我可以在我想要的时候准确地“触发”其中一个功能的那一刻)。现在,剩下的就是用正确的参数调用触发器。因此,我将参数设置为单独的抽象,然后调用trigger() ,并且在系统上具有shell访问权限。
tl; dr:一个人可以建立小的剥削原语(没有太多的功率),并且通过将它们结合并建立更强原始的层次结构,我们可以完全执行。
写于2017年4月6日
受Gynvael Coldwind的真棒现场直播的影响,他在那里谈论格式剥削
简单格式字符串利用:
您可以使用%p查看堆栈上的内容。如果格式字符串本身在堆栈上,则可以将地址(例如foo )放在堆栈上,然后使用位置指定符n$进行搜索(例如, AAAA %7$p可能返回AAAA 0x41414141 ,如果7是堆栈上的位置)。然后,我们可以使用它来构建一个读取的原始词,而是使用%s格式指定符(例如, AAAA %7$s将在地址0x41414141上返回该值,以继续上一个示例)。 We can also use the %n format specifier to make it into a write-what-where primitive. Usually instead, we use %hhn (a glibc extension, iirc), which lets us write one byte at a time.
We use the above primitives to initially beat ASLR (if any) and then overwrite an entry in the GOT (say exit() or fflush() or ...) to then raise it to an arbitrary-eip-control primitive, which basically gives us arbitrary-code-execution .
Possible difficulties (that make it "advanced" exploitation):
If we have partial ASLR , then we can still use format strings and beat it, but this becomes much harder if we only have one-shot exploit (ie, our exploit needs to run instantaneously, and the addresses are randomized on each run, say). The way we would beat this is to use addresses that are already in the memory, and overwrite them partially (since ASLR affects only higher order bits). This way, we can gain reliability during execution.
If we have a read only .GOT section, then the "standard" attack of overwriting the GOT will not work. In this case, we look for alternative areas that can be overwritten (preferably function pointers). Some such areas are: __malloc_hook (see man page for the same), stdin 's vtable pointer to write or flush , etc. In such a scenario, having access to the libc sources is extremely useful. As for overwriting the __malloc_hook , it works even if the application doesn't call malloc , since it is calling printf (or similar), and internally, if we pass a width specifier greater than 64k (say %70000c ), then it will call malloc, and thus whatever address was specified at the global variable __malloc_hook .
If we have our format string buffer not on the stack , then we can still gain a write-what-where primitive, though it is a little more complex. First off, we need to stop using the position specifiers n$ , since if this is used, then printf internally copies the stack (which we will be modifying as we go along). Now, we find two pointers that point ahead into the stack itself, and use those to overwrite the lower order bytes of two further ahead pointing pointers on the stack, so that they now point to x+0 and x+2 where x is some location further ahead on the stack. Using these two overwrites, we are able to completely control the 4 bytes at x , and this becomes our where in the primitive. Now we just have to ignore more positions on the format string until we come to this point, and we have a write-what-where primitive.
Written on 1st April 2017
Influenced by this amazing live stream by Gynvael Coldwind, where he explains about race conditions
If a memory region (or file or any other resource) is accessed twice with the assumption that it would remain same, but due to switching of threads, we are able to change the value, we have a race condition.
Most common kind is a TOCTTOU (Time-of-check to Time-of-use), where a variable (or file or any other resource) is first checked for some value, and if a certain condition for it passes, then it is used. In this case, we can attack it by continuously "spamming" this check in one thread, and in another thread, continuously "flipping" it so that due to randomness, we might be able to get a flip in the middle of the "window-of-opportunity" which is the (short) timeframe between the check and the use.
Usually the window-of-opportunity might be very small. We can use multiple tricks in order to increase this window of opportunity by a factor of 3x or even up to ~100x. We do this by controlling how the value is being cached, or paged. If a value (let's say a long int ) is not aligned to a cache line, then 2 cache lines might need to be accessed and this causes a delay for the same instruction to execute. Alternatively, breaking alignment on a page, (ie, placing it across a page boundary) can cause a much larger time to access. This might give us higher chance of the race condition being triggered.
Smarter ways exist to improve this race condition situation (such as clearing TLB etc, but these might not even be necessary sometimes).
Race conditions can be used, in (possibly) their extreme case, to get ring0 code execution (which is "higher than root", since it is kernel mode execution).
It is possible to find race conditions "automatically" by building tools/plugins on top of architecture emulators. For further details, http://vexillium.org/pub/005.html
Written on 31st Mar 2017
Influenced by this amazing live stream by Gynvael Coldwind, where he is experimenting on the heap
Use-after-free:
Let us say we have a bunch of pointers to a place in heap, and it is freed without making sure that all of those pointers are updated. This would leave a few dangling pointers into free'd space. This is exploitable by usually making another allocation of different type into the same region, such that you control different areas, and then you can abuse this to gain (possibly) arbitrary code execution.
Double-free:
Free up a memory region, and the free it again. If you can do this, you can take control by controlling the internal structures used by malloc. This can get complicated, compared to use-after-free, so preferably use that one if possible.
Classic buffer overflow on the heap (heap-overflow):
If you can write beyond the allocated memory, then you can start to write into the malloc's internal structures of the next malloc'd block, and by controlling what internal values get overwritten, you can usually gain a read-what-where primitive, that can usually be abused to gain higher levels of access (usually arbitrary code execution, via the GOT PLT , or __fini_array__ or similar).