ShellCode --- 修改函数返回地址 , 让其指向溢出数据中的一段指令
该类攻击方法的主要任务是 : 在溢出数据内包含一段攻击指令 , 用攻击指令的起始地址覆盖函数的返回地址
攻击指令一般是用来打开Shell的 , 从而可以获得当前进程的控制权 , 所以这类指令片段也被称为 : ShellCode
ShellCode可以用汇编语言写后转换成对应的机器码 , 也可以从网上直接获得 . 但学习必须要知其所以然 , 因此下面分析溢出数据的组成 , 再确定对应的填充信息
- 溢出数据组成分析
在函数调用发生后 , 栈中的结构是这样的
这里最主要的任务是覆盖 callee 的返回地址 , 因此填充信息的组成应该如下图所示
栈中的结构是这样的padding1 : 可以用任意数据填充( 但如果利用字符串程序输入溢出数据时不要包含 " \x00 " , 因为 " \x00 " 会截断后面的数据) , 数据的长度应该刚好覆盖 callee 的基地址 address of shellcode : 是shellcode的起始地址 , 用它来覆盖返回地址. padding2 : 可以用任意数据填充 , 长度由 shellcode 的位置决定 shellcode : 应该为十六进制的机器码
根据上述内容 , 完整的 Payload 如下
Payload :
padding1 + address of shellcode + padding2 + shellcode
-
解决构造 Payload 的两个问题
- 在返回地址前的填充数据( padding1 )应该多长?
可以用调试工具( gdb , objdump , ... )查看汇编代码来确定这个距离 , 也可以在运行程序时不断修改输入长度来试探( 如果返回地址被无效地址( 比如 "AAAA" )覆盖 , 则程序会报错并终止 ) -
shellcode 的起始地址应该是多少?
可以在调试工具里查看返回地址的位置( 查看ebp的内容然后加4 , 这个值是不固定的 ) . 并且调试工具里的这个地址和正常运行时是不一致的 , 由运行时环境变量等因素有所不同造成 . , 这种情况下我们只能拿到大致但是不确定的shellcode起始地址.比较好的解决方法是在 padding2 中填充若干长度的 " \x90 " , 该机器码对应的指令为 NOP ( No Operation , 告诉CPU什么都不做 , 直接跳转到下一条指令 ) , 有了这一段 NOP 的填充 , 只需要返回地址能够命中这一段指令中的任意位置 , 都可以无副作用的跳转到shellcode的起始地址
这种方法被称为
NOP Sled
( 滑雪撬 ) , 通过增加 NOP 填充来配合试验shellcode的起始地址
操作系统可以将函数调用栈的起始地址设置为随机化( 这种技术被称为 " 内存空间布局随机化 , Address Space Layout Randomization( ASLR )" ) , 这样每次程序运行时函数的返回地址会随机变化
反之如果操作系统关闭了 ASLR 技术 , 那么程序每次运行时函数的返回地址都是相同的 , 这样才可以使用填充无效的溢出数据来生成 core 文件 , 再通过调试工具在 core 文件中找到返回地址的位置 , 从而确定shellcode的起始地址
因此 , 操作系统没有开启 ASLR 是填充无效溢出数据找出shellcode初始地址的第一个前提
- 在返回地址前的填充数据( padding1 )应该多长?
-
shellcode溢出数据的最终构造
通过上面的内容可以更进一步的构造 Shellcode 的溢出数据 , 如下图所示
该攻击方法生效需要两个前提
- 第一个前提是上文提到的操作系统关闭 ASLR( 地址空间布局随机化 )
-
第二个前提是在函数调用栈上的数据( 也就是攻击者构造的shellcode )有可执行权限
很多时候操作系统会关闭函数调用栈的可执行权限 , 这样shellcode的方法就失效了
不过可以尝试使用内存中已有的指令或者函数 , 毕竟这部分内容是本来就存在的 , 不会受到函数执行权限的限制
后续
上文提到在调用函数栈上数据没有执行权限时 , 可以使用内存中已有的数据 , 那么如何使用这些已有的指令或者函数呢 ? 这就涉及到 Return2libc
和 ROP
两种攻击技术了
-
Return2libc : 修改返回地址 , 让其指向内存中已有的某个函数
-
ROP : 修改返回地址 , 让其指向内存中已有的某段指令