因此,在简单的情况下并利用堆栈缓冲区溢出(在其中我可以在堆栈上执行代码)时,我的有效负载就是:

NOP Sled -> Shellcode -> Padding -> New RET Address (Pointing to the NOP Sled)
需要在shellcode中包含一个指向NOP Sled的内存地址,这是我们需要的,因为我们“不确定”返回地址是什么。

为什么我们不能确定吗?

诸如argv [0]之类的简单事物是否可能不同?在一个系统上,文件可能是“ / vuln”,而在另一个“ / home / user / vuln”上,由于这个原因,堆栈已经不同了?
环境变量是否起作用?

基本上我的问题是:每当我们在另一台机器上执行/执行vuln程序时,哪些因素导致堆栈不同?

#1 楼

在现代系统上,最明显的罪魁祸首可能是地址空间布局随机化,但是即使在ASLR广泛实施之前,堆栈框架布局可变性对于漏洞利用开发也是一个问题。这在AlephOne古老的“砸碎堆栈以获得乐趣和利润”中提到过:



我们试图在溢出另一个缓冲区时遇到的问题
程序试图找出缓冲区(以及我们的
代码)将位于哪个地址。答案是,对于每个程序,堆栈将
从同一地址开始。大多数程序在任何时候都不会将数百或几千个字节压入堆栈。因此,通过了解堆栈从哪里开始,我们可以尝试猜测要尝试溢出的缓冲区的位置。的生命几乎是不可能的。我们最多需要一百次尝试,而最坏则需要几千次尝试。问题是我们需要准确猜测代码地址的起始位置。如果我们或多或少地减少了一个字节,我们只会得到分段违规或无效指令。增加机会的一种方法是用NOP指令填充溢出缓冲区的前部。


因此,即使未启用ASLR,这也是一个问题。造成这种情况的原因包括以下(据我所知):


编译器/编译器工具链和ABI运行时环境
编译器, ABI和堆栈框架布局

有趣的是,管理运行时堆栈的代码是由编译器生成的,并且编译器无法知道程序运行时堆栈帧的绝对位置将是什么:


尽管无法在编译时预测堆栈帧的位置(编译器通常无法告知堆栈中可能还有其他帧),但是通常可以静态确定帧中对象的偏移量。此外,编译器可以安排(按调用顺序或序言)特定的寄存器(称为帧指针)以始终指向当前子例程的帧内的已知位置。需要访问当前帧内的局部变量或调用帧顶部附近的参数的代码可以通过向帧指针中的值添加预定偏移量来实现。1


为堆栈帧分配和释放内存的方式可能会因编译器以及同一编译器工具链的不同版本而异。为什么会出现这种情况,在System V i386体系结构处理器增补(第36页)中提到了:堆栈是按字对齐的。尽管体系结构不需要堆栈的任何对齐方式,但是软件约定和操作系统
要求堆栈在字边界上对齐。


默认情况下,GCC对齐在i386机器(x86)上将堆栈限制为16个字节的边界:


-mpreferred-stack-boundary=num
尝试使堆栈边界与2对齐,并升至num字节边界。如果未指定-mpreferred-stack-boundary,则默认值为4(16个字节或128位)。


这意味着即使局部变量或函数参数的长度为4个字节(例如作为int),将在堆栈帧上为该功能分配至少16个字节的空间。由于这是gcc的命令行参数,因此可以更改。不用说。



其他区域取决于编译器和正在编译的代码。标准调用序列没有定义最大堆栈框架大小,也没有限制语言系统如何使用标准堆栈框架的“未指定”区域。


此“未指定”堆栈帧的区域”包括除返回地址和前一帧的已保存基本指针以外的所有帧:



写入该区域的数据可以包括保存的寄存器,局部变量,临时变量和下一个函数的参数2。 GCC将分配在一个帧中存储此数据所需的内存,以及在二进制文件编译时保持堆栈对齐至指定边界所需的空间:




这是所有这些如何联系在一起的方法:


在运行时之前无法确定堆栈上数据的绝对虚拟内存地址将是什么,因此使用间接寻址(例如,使用%ebp%esp计算偏移量)。但是,要控制EIP,必须将可执行指令的绝对存储地址写入EIP。因此,现在必须在拆卸二进制文件时弄清楚该地址是什么,而没有绝对地址可以使用。

堆栈帧的精确布局以及扩展的运行时堆栈由所使用的编译器确定。创建二进制文件试图利用。


这意味着分配给诸如缓冲区之类的变量的空间量至少取决于编译器将堆栈帧对齐的边界(4个字节?8个字节?16个字节?等等)。
这会导致堆栈帧中出现“松弛空间”,而没有与局部变量关联的值写入其中(包含垃圾数据)。
除此之外,编译器还确定变量在堆栈框架中的排列顺序。
优化编译可能会对特定于体系结构的ABI中指定的调用约定以及堆栈帧的布局方式产生不利影响
带有堆栈保护的编译将进一步影响编译器如何在堆栈帧内安排数据
<总而言之,缓冲区溢出和目标返回地址位置之间的偏移量可能会根据编译器工具链(GCC,TCC,MSVC等),编译器版本(3.x, 4.x等)以及用于编译二进制文件的特定选项(对齐值,优化级别,堆栈保护等):


返回地址与起始地址之间的确切距离由于不同的编译器版本和不同的优化标志,[buffer]可能会更改。只要缓冲区的开始与堆栈上的DWORD对齐,就可以通过简单地重复多次返回地址来解决这种可变性。这样,至少一个实例将覆盖返回地址,即使它由于编译器优化而发生偏移。3


运行时环境

在其他地方,有人建议堆栈布局的不确定性可能是由环境变量和程序参数引起的:


如果您没有充分考虑导致不兼容的因素,漏洞利用开发可能会导致严重的麻烦。确定性进入调试过程。特别是,调试器中的堆栈地址在正常执行期间可能与地址不匹配。之所以会出现这种伪像,是因为操作系统加载程序将环境变量和程序参数都放置在堆栈的开始之前


如果我们查看堆栈在虚拟内存中的布局方式,这是有道理的:


如果环境或参数之间的执行之间存在差异,则堆栈基的位置也可能会更改。

为了证明这不仅仅是闲置的猜测,我们可以看一个具体的例子。首先,我们像这样禁用ASLR:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

然后我们在C中编写一个快速程序:

#include <stdio.h>

int test(int a, int b)
{
        printf("%p\n", &a);
        return a*a + b*b;
}

int main(void)
{
        return test(3, 5);
}


在编译后以稳定增加的参数长度运行几次后,可以观察到局部变量之一在堆栈上的位置在虚拟内存中会越来越低:

$ gcc -m32 simple.c -o simple
$ ./simple 
0xffffd210
$ ./simple AAAAAAAA
0xffffd200
$ ./simple AAAAAAAAAAAAAAAA
0xffffd200
$ ./simple AAAAAAAAAAAAAAAAAAAAAAAA
0xffffd1f0
$ ./simple AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xffffd1f0
$ ./simple AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xffffd1e0
$ ./simple AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xffffd1e0
$ ./simple AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xffffd1d0
$ ./simple AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xffffd1d0
$ ./simple AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xffffd1c0


,这表明更改传递给execve的参数的大小也将更改运行时堆栈在虚拟内存中的位置。这个极其琐碎的示例的缺点在于,它无法准确反映实际程序的复杂性,也无法捕获运行时堆栈在整个复杂过程执行过程中的变化方式。

影响运行时堆栈布局的另一个变量是二进制文件是否动态链接。如果它是动态链接的,则进程启动由glibc处理。在x86 Linux系统上,C运行时库函数调用序列类似于以下内容:


这将取决于二进制文件所针对的glibc版本,并且运行时环境会有所不同机器。这意味着在进程启动过程中的函数调用顺序也会因系统而异。


另请参见:


https:// www。 win.tue.nl/~aeb/linux/hh/hh-10.html
https://www.win.tue.nl/~aeb/linux/hh/bof-eng.txt
https://0xax.gitbooks.io/linux-insides/content/Misc/program_startup.html


1。斯科特,迈克尔·L。《编程语言语用学》。第三版。页面117

2。科比,奥哈拉隆。计算机系统:程序员的观点。第二版。页面220

3。乔恩·埃里克森。黑客:剥削的艺术。第二版。第136页

#2 楼

正如您提到的,许多事情可能会使您的内存布局与您期望的布局不同:


二进制文件的名称是一个。
二进制文件获得的环境变量是另一个。
如果正在调试(与上一点有关)
安全措施,例如堆栈cookie
Etc

使用指向堆栈的硬编码地址为EIP将采用的地址是错误的方法。早期就是这样。粗略的方法,没有适当的DEP / NX。

更稳定的方法是,您可以直接跳到将要执行的代码,而不必跳到堆栈的中间。有多种不同的技术可以执行此操作,例如返回libc / library或ROP。

另一种更简单的方法是在溢出发生时(实际上,当段错误被触发)。假设ESP指向0x12345678。如果在段错误时可以稳定地控制0x12345678的内容,则可以使用JMP ESP -kind指令的地址,如EIP所用。


为什么这种方法更好?

因为您确定二进制文件本身或执行二进制文件时被加载的库(因此,它们的指令)将始终存储在同一地址中。 (1)长话短说,座右铭是:“如果您的漏洞包含NOP,则您做错了!”。

要回答标题中的问题,就不需要NOP雪橇。


只要没有ASLR或模块重新安装底座即可。