Reference

http://paper.seebug.org/271/
https://sploitfun.wordpress.com/2015/05/08/bypassing-nx-bit-using-chained-return-to-libc/


0x00 前言

栈溢出可以说是最最基础的漏洞了,同时它也被很多人认为是最简单的漏洞。早在1988年诞生的世界上第一种蠕虫病毒:“莫里斯蠕虫”就利用了栈溢出漏洞。尽管被认为很简单,但栈溢出涉及到了底层的很多方面,在这里做了小结,也当复习一下。(部分图片来自网络)


0x10 综述

栈溢出是什么?举个简单的例子,下面是一份示例代码

运行一下

上面的程序存在一个缓冲区溢出漏洞。
这个程序给buff分配了64字节空间,但由于argv[1]由用户自行输出,而strcpy函数并未对字符串的长度做检查,因此当用户输入超过64字节长的字符串的时候就会在main函数中引发缓冲区溢出。
这里的重点在于,当输入超过64字节,程序的运行结果将变得不可预测。这就是一个漏洞。
对于栈溢出来说,只要满足

  • 程序有向栈内写入数据的行为
  • 程序并不限制写入数据的长度。

这个漏洞就可能被利用


0x11 前置--栈帧

要了解栈溢出我们必须先知道的机制和函数调用的机制。(函数调用主要讲栈帧,在这里我假设你有一定的汇编语言基础)栈是一个通常被描述为串列的数据结构,它遵循后进先出(LIFO, Last In First Out)的原理,就像一叠盘子,你总是只能拿走最上面的那一个或者把新的盘子放在这一叠的顶端。栈的主要功能是实现函数的调用。
一个函数调用另一个函数时,程序将在调用函数(caller)的低地址开一块新的区域做被调用函数(callee)的栈空间,在调用完成return的时候,这一块内存就会被释放。(其实有可能残留数据)
sto00

在此之上我们要了解关于函数调用的三个寄存器

  • ESP 寄存器为 Stack Pointer ,它始终指向栈顶的位置。
  • EIP 寄存器指向CPU下一条执行指令,在函数调用中为返回地址,它是调用函数( Caller )在执行完 Call 指令后的下一条指令的地址。
  • EBP 寄存器为 Frame Pointer( 亦称 Base Pointer ),它指向栈的基地址,被用作在当前的栈帧中寻址所有的函数参数以及局部变量。
    在调用过程中,程序会依次把 被调函数的参数 被调函数返回地址 调用函数基址(ebp) 被调函数局部变量 给压入栈中。
    sto01

同样的,在函数执行完毕准备返回的时候会按照栈的规则讲数据依次弹出,并且把调用函数的ebp存入ebp寄存器,返回地址存入eip寄存器。这样,函数调用的机制就完成了。


0x20 Stack Overflow

栈溢出从原理上大致可分为四种攻击方式:

  • 修改返回地址,让其指向溢出数据中的一段指令(shellcode)
  • 修改返回地址,让其指向内存中已有的某个函数(return2libc)
  • 修改返回地址,让其指向内存中已有的一段指令(ROP)
  • 修改某个被调用函数的地址,让其指向另一个函数(hijack GOT)

下面会分别介绍


0x21 Shellcode

通过上面描述栈空间的那张图我们可以看到,对于Sample1这个程序来说,buff的内存空间是分配在Local Variables这个区域的,其大小为64字节,如果用户的输入超过了64个字节,超出的部分就会覆盖Caller's ebp和Return Address。如果我们能覆盖Return Address,我们就能让程序跳转到任意地址。我们可以先准备一段代码(shellcode),再让程序跳转过去。这样就成功达到了攻击的目的。
关于shellcode,我在隔壁的文章做了介绍,这里不赘述。//隔壁文章传送门

根据我们上面讲到的原理,我们可以构造出攻击的payload : padding1 + address of shellcode + padding2 + shellcode

其中,padding1的长度可以通过IDA, gdb, edb等工具来确定。
除此之外,在写入返回地址的时候,我们需要确定shellcode的起始地址。我们可以在调试工具里查看返回地址的位置,可是在调试工具里的这个地址和正常运行时并不一致,这是运行时环境变量等因素有所不同造成的。所以这种情况下我们只能得到大致但不确切的 shellcode 起始地址,解决办法是在 padding2 里填充若干长度的 “\x90”。这个机器码对应的指令是 NOP (No Operation),也就是告诉 CPU 什么也不做,然后跳到下一条指令。有了这一段 NOP 的填充,只要返回地址能够命中这一段中的任意位置,都可以无副作用地跳转到 shellcode 的起始处,所以这种方法被称为 NOP Sled(中文含义是“滑雪橇”)。这样我们就可以通过增加 NOP 填充来配合试验 shellcode 起始地址。

到此为止,我们的payload就构造完毕了

sto02

看起来很简单对不对,理论上我们很轻松的就能利用这个栈溢出漏洞拿到shell。但是实际上这份payload要生效必须满足两个条件:

  • 操作系统关闭了内存布局随机化,即Address Space Layout Randomization (ASLR) ,否则程序每次运行时函数返回地址会随机变化。
  • 函数调用栈上的数据(shellcode)要有可执行的权限(NX bit)

在实际的场景中,上述两个条件不能满足,所以我们还有以下的几种条件要求更低的栈溢出方法。

0x22 Return2libc

Return2libc的核心是修改返回地址,让其指向内存中已有的某个函数。这样我们可以绕过NX bit的限制。
例如,如果攻击者想要拿到一个shell,那么他用system()地址覆盖返回地址,并在堆栈中准备system()所需的相应参数来成功调用它就可以。

顺便一提,要在程序中关闭NX保护只需要给gcc传入“-z execstack”,检测一个程序NX bit的状态可以用readelf查看GNU_STACK的权限,也可以直接用peda看。

payload: padding1 + address of system() + padding2 + address of “/bin/sh”

padding1和上面提到的一样就可以(不要写\x00),padding2 处的数据长度为4(32位机)作system的返回地址(因为相比正常的函数调用,直接跳转过来少了一个push eip的操作,所以会有4为的padding)。"/bin/sh"是system的参数

在这里我们要解决的问题就是

  • system函数的地址是多少?
    在ASLR关闭的情况下,我们可以通过计算libc基地址 + system偏移地址来得到system函数地址,也可以直接在调试器里查看。
    你可以用"cat /proc/$pid/maps"来查看libc基地址,"readelf -s /lib/i386-linux-gnu/libc.so.6 | grep system"来查看system偏移地址。

  • "/bin/sh"在哪里?
    二进制文件里面找找,没有的话可以想办法利用用户输入或者环境变量来解决。

看起来问题很完美的解决了,但是有时候你无法通过这种方式拿到root的shell,具体原因可以参见构造shellcode这里,由于userID的问题,你可能只能得到一个普通权限的shell,所以你最好在调用system之前调用一个seteuid(0)或者setreuid(0, 0),然后再把seteuid/setreuid的返回地址改到system。

其实你可以发现,照这个方法我们可以一直添加我们要调用的函数。只要控制好每个函数的返回地址即可。
这个想法看起来很完美,但是仔细思考一下
比如我们依次调用seteuid, system, exit三个函数,此时的栈空间是怎样的?

可以看到seteuid的参数和exit的地址挤在一起了,在真实环境下这肯定是做不到的。所以我们需要改变栈上数据的布局。
在最开始的地方本文介绍了栈帧的机制,
栈帧的结构如下

我们只需要伪造一个栈帧,就可以控制程序执行我们想要的函数链。
此时的栈空间如下

在这里每一个"leave ret"会调用其上方的第一个函数,但是遇到seteuid的时候又会遇到一个问题,我们要给这个函数传的参数是0,strcpy函数会在这里停掉。
所以在调用seteuid函数之前我们应该调用其它函数把0给写进去。
在这里可以用strcpy函数或者sprintf函数来实现,检查一下这两个函数在库里的地址有没有0,如果没有就可以放心的拿来用了。

综合以上就是return2libc的技巧。

0x23 ROP

有空填坑,先扔这里。