逆核系列No.11--代码注入

代码注入

代码注入是一种想目标进程插入具备独立运行能力的代码并使之运行的技术,由于一般是通过调用CreateRemoteThread APi以远程线程形式运行插入的代码,因此也被称为线程注入

例子,如若需要将下列用于弹出Windows消息框的代码注入的到目标进程:

1
2
3
4
5
DOWRD WINAPI ThreadProc(LPVOID lparam)
{
MessageBox(NULL, "www.reversecore.com", "ReverseCore", MB_OK);
return 0;
}

使用DLL注入的方式实现

在DLL注入技术里,会将代码放入某个DLL文件,在将整个DLL文件注入到目标进程中,使用OD载入目标程序,开启调试选项中的 “ 中断在新模块载入处” 的选项,使程序处于运行状态,使用DLL注入器将DLL注入,会来到如下代码:

在0x10001002 - 0x10001007处的两条指令

1
2
3
4
0x10001002 push 10009290	//字符串ReverseCore的起始位置
0x10001007 push 1000929c //字符串www.reversecore.com的位置
//...
0x1000100E call DOWRD ptr ds:[100080F0] //API入口

关注DLL注入方式中有关地址的部分,可以发现:DLL代码使用的所有数据都是位于DLL自己载入内存时的数据区域。这样有什么坏处?

  • 需要将整个DLL的内容装进内存,占用内存大
  • 痕迹明显,很容易被察觉

使用代码注入的方式实现

而代码注入则是指注入必要的代码,并且代码所使用的数据一同注入,并且在完成注入代码的运行时需要明确指出数据的地址。 克服了一定的DLL注入的缺点

demo演示

使用代码注入器–CodeInjection.exe输入目标进程对应的pid完成代码注入,实现弹窗。


源码剖析

先贴一下CodeInjection的完整代码,后面展开分析

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
// CodeInjection.cpp
// reversecore@gmail.com
// http://www.reversecore.com

#include "windows.h"
#include "stdio.h"

typedef struct _THREAD_PARAM
{
FARPROC pFunc[2]; // LoadLibraryA(), GetProcAddress()
char szBuf[4][128]; // "user32.dll", "MessageBoxA", "www.reversecore.com", "ReverseCore"
} THREAD_PARAM, *PTHREAD_PARAM;

typedef HMODULE (WINAPI *PFLOADLIBRARYA)
(
LPCSTR lpLibFileName
);

typedef FARPROC (WINAPI *PFGETPROCADDRESS)
(
HMODULE hModule,
LPCSTR lpProcName
);

typedef int (WINAPI *PFMESSAGEBOXA)
(
HWND hWnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType
);

DWORD WINAPI ThreadProc(LPVOID lParam)
{
PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam;
HMODULE hMod = NULL;
FARPROC pFunc = NULL;

// LoadLibrary()
hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]); // "user32.dll"
if( !hMod )
return 1;

// GetProcAddress()
pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]); // "MessageBoxA"
if( !pFunc )
return 1;

// MessageBoxA()
((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);

return 0;
}

BOOL InjectCode(DWORD dwPID)
{
HMODULE hMod = NULL;
THREAD_PARAM param = {0,};
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
LPVOID pRemoteBuf[2] = {0,};
DWORD dwSize = 0;

hMod = GetModuleHandleA("kernel32.dll");

// set THREAD_PARAM
param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
strcpy_s(param.szBuf[0], "user32.dll");
strcpy_s(param.szBuf[1], "MessageBoxA");
strcpy_s(param.szBuf[2], "www.reversecore.com");
strcpy_s(param.szBuf[3], "ReverseCore");

// Open Process
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, // dwDesiredAccess
FALSE, // bInheritHandle
dwPID)) ) // dwProcessId
{
printf("OpenProcess() fail : err_code = %d\n", GetLastError());
return FALSE;
}

// Allocation for THREAD_PARAM
dwSize = sizeof(THREAD_PARAM);
if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess, // hProcess
NULL, // lpAddress
dwSize, // dwSize
MEM_COMMIT, // flAllocationType
PAGE_READWRITE)) ) // flProtect
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}

if( !WriteProcessMemory(hProcess, // hProcess
pRemoteBuf[0], // lpBaseAddress
(LPVOID)&param, // lpBuffer
dwSize, // nSize
NULL) ) // [out] lpNumberOfBytesWritten
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}

// Allocation for ThreadProc()
dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess, // hProcess
NULL, // lpAddress
dwSize, // dwSize
MEM_COMMIT, // flAllocationType
PAGE_EXECUTE_READWRITE)) ) // flProtect
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}

if( !WriteProcessMemory(hProcess, // hProcess
pRemoteBuf[1], // lpBaseAddress
(LPVOID)ThreadProc, // lpBuffer
dwSize, // nSize
NULL) ) // [out] lpNumberOfBytesWritten
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}

if( !(hThread = CreateRemoteThread(hProcess, // hProcess
NULL, // lpThreadAttributes
0, // dwStackSize
(LPTHREAD_START_ROUTINE)pRemoteBuf[1], // dwStackSize
pRemoteBuf[0], // lpParameter
0, // dwCreationFlags
NULL)) ) // lpThreadId
{
printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
return FALSE;
}

WaitForSingleObject(hThread, INFINITE);

CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}

BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)
{
TOKEN_PRIVILEGES tp;
HANDLE hToken;
LUID luid;

if( !OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&hToken) )
{
printf("OpenProcessToken error: %u\n", GetLastError());
return FALSE;
}

if( !LookupPrivilegeValue(NULL, // lookup privilege on local system
lpszPrivilege, // privilege to lookup
&luid) ) // receives LUID of privilege
{
printf("LookupPrivilegeValue error: %u\n", GetLastError() );
return FALSE;
}

tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
if( bEnablePrivilege )
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
else
tp.Privileges[0].Attributes = 0;

// Enable the privilege or disable all privileges.
if( !AdjustTokenPrivileges(hToken,
FALSE,
&tp,
sizeof(TOKEN_PRIVILEGES),
(PTOKEN_PRIVILEGES) NULL,
(PDWORD) NULL) )
{
printf("AdjustTokenPrivileges error: %u\n", GetLastError() );
return FALSE;
}

if( GetLastError() == ERROR_NOT_ALL_ASSIGNED )
{
printf("The token does not have the specified privilege. \n");
return FALSE;
}

return TRUE;
}

int main(int argc, char *argv[])
{
DWORD dwPID = 0;

if( argc != 2 )
{
printf("\n USAGE : %s <pid>\n", argv[0]);
return 1;
}

// change privilege
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
return 1;

// code injection
dwPID = (DWORD)atol(argv[1]);
InjectCode(dwPID);

return 0;
}


main

首先是CodeInjection的main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(int argc, char *argv[])        //注入器需要的参数,注入目标进程的pid
{
DWORD dwPID = 0;

if( argc != 2 )
{
printf("\n USAGE : %s <pid>\n", argv[0]);
return 1;
}

// change privilege
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
return 1;

// code injection
dwPID = (DWORD)atol(argv[1]); //数字字符转化为long int类型
InjectCode(dwPID);

return 0;
}

无非做了参数合法验证,由于涉及跨进程内存操作,因此需要提升权限SetPrivilege,进程权限提升在之前有做过文章描述。随后是转化传递进来的pid,转化为int类型,然后进入InjectCode函数

1
2
3
4
5
6
HMODULE         hMod            = NULL;
THREAD_PARAM param = {0,};
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
LPVOID pRemoteBuf[2] = {0,};
DWORD dwSize = 0;
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
//上述几行代码在CodeInject.cpp中相关的内容
typedef HMODULE (WINAPI *PFLOADLIBRARYA)
(
LPCSTR lpLibFileName
);

typedef struct _THREAD_PARAM //传递给线程的参数
{
FARPROC pFunc[2]; // LoadLibraryA(), GetProcAddress()
char szBuf[4][128];
// "user32.dll", "MessageBoxA", "www.reversecore.com", "ReverseCore"
} THREAD_PARAM, *PTHREAD_PARAM;

typedef FARPROC (WINAPI *PFGETPROCADDRESS)
(
HMODULE hModule,
LPCSTR lpProcName
);

typedef int (WINAPI *PFMESSAGEBOXA) //根据API原型定义的
(
HWND hWnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType
);
线程参数写入

接着InjectCode函数的源码分析,下边完成的是线程参数的写入

1
2
3
4
5
6
7
8
hMod = GetModuleHandleA("kernel32.dll");		
// set THREAD_PARAM
param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
strcpy_s(param.szBuf[0], "user32.dll");
strcpy_s(param.szBuf[1], "MessageBoxA");
strcpy_s(param.szBuf[2], "www.reversecore.com");
strcpy_s(param.szBuf[3], "ReverseCore");

重点关注线程参数的设置:

  • 线程中需要用到的kernel32.LoadLibraryA & kernel32.GetProcAddress
  • 线程中需要用到的一些字符串,user32.dll,MessageBoxA,www.reversecore.com,ReverseCore。

根据线程参数也猜得出大致线程的行为,获取kernel32.LoadLibraryA 后将user32.dll载入,kernel32.GetProcAddress获取user32.MessageBoxA API地址,完成MessageBoxA调用,实现弹窗

1
2
3
4
5
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) ) 
{
printf("OpenProcess() fail : err_code = %d\n", GetLastError());
return FALSE;
}

以PROCESS_ALL_ACCESS打开目标进程,打开成功后返回目标进程的句柄到hProcess

1
dwSize = sizeof(THREAD_PARAM);		

计算将要传递进目标进程中作为线程参数的结构体大小,计算结果存放在dwSize中

1
2
3
4
5
6
if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, dwSize,
MEM_COMMIT, PAGE_READWRITE)) )
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}

在目标进程hProcess开辟一段大小为dwSize的内存空间,对于这块内存的详情为MEM_COMMIT,保护形式为PAGE_READWRITE,也就是具备读写权限。

关于MEM_COMMIT的描述:

为指定地址空间提交物理内存。这个函数初始化内在为零,试图提交已提交的内存页不会导致函数失败。这意味着您可以在不确定当前页的当前提交状态的情况下提交一系列页面。如果尚未保留内存页,则设置此值会导致函数同时保留并提交内存页。

函数执行成功后在目标进程中开辟的内存空间起始地址放入pRemoteBuf[0]

往下分析:

1
2
3
4
5
if( !WriteProcessMemory(hProcess, pRemoteBuf[0], (LPVOID)&param, dwSize, NULL) ) 
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}

往先前在目标进程中开辟出来的内存空间写入线程参数param

至此完成线程参数写入目标进程空间

线程代码写入

接下来需要写入线程的代码,同样使用VirtualAllocEx API

需要注意的点

在目标进程中分配线程代码所需要的空间需要进行计算得出。

源码中计算方式,dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;

之所以能够这样计算是由于作者使用的MS Visual C++中使用Release模式编译程序源码,源码中函数顺序和二进制代码中的前后顺序是一致的,源码文件是按照ThreadProc、InjectCode顺序编写的,所以生成InjectCode.exe中两函数也按照这个顺序排列。(特性,记住,会用就好)

因此我们得到了需要在目标进程中开辟另外一块空间,写入线程的代码,在目标进程中使用CreateRemoteThread设置回调函数为即将写入的ThreadProc,在传入ThreadProc需要的参数param,如此一来,当在目标进程中开辟成功线程,新线程会自行调用ThreadProc

言归正传,得到线程代码所需要的空间大小dwSize ,接下来就是申请空间了:

1
2
3
4
5
6
if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, 
MEM_COMMIT, PAGE_EXECUTE_READWRITE)) )
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}

方式类似于第一次调用VirtualAllocEx进行线程所需参数空间一样,区别在于开辟出来的空间的权限,这块空间是需要执行权限的,因为这是代码的位置。开辟出来的空间起始地址放在pRemoteBuf[1].

1
2
3
4
5
6
if( !WriteProcessMemory(hProcess, pRemoteBuf[1], 
(LPVOID)ThreadProc, dwSize, NULL) )
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}

这里需要理解一下,ThreadProc + dwSize 这两个内容配合,完成ThreadProc代码的完整写入

1
2
3
4
5
if( !(hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0,   NULL)) )
{
printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
return FALSE;
}

一切准备就绪,在目标进程中开启新线程,线程中执行写入的ThreadProc代码。

ThreadProc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DWORD WINAPI ThreadProc(LPVOID lParam)
{
PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam;
HMODULE hMod = NULL;
FARPROC pFunc = NULL;


hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]);
if( !hMod )
return 1;

pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]);
if( !pFunc )
return 1;

// MessageBoxA()
((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);

return 0;
}

参数只有一个lParam是一个结构体

1
2
3
4
5
typedef struct _THREAD_PARAM 
{
FARPROC pFunc[2];
char szBuf[4][128];
} THREAD_PARAM, *PTHREAD_PARAM;

其中THREAD_PARAM.pFunc存放的是两个API的调用地址:LoadLibraryA(), GetProcAddress(),都是kernel32.dll的API。

THREAD_PARAM.szBuf存放的是线程执行需要用到的4个字符串

分别是:user32.dllMessageBoxAwww.reversecore.comReverseCore

因此:

1
hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]);

完成的是使用kernel32.LoadLibraryA将user32.dll进行载入,载入后模块句柄存放在hMod中。

1
pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]);

完成的是使用kernel32.GetProcAddress获取user32.MessageBoxA的地址,存放到pFunc

1
((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);

这一步完成的是对MessageBoxA的调用。可以看到,上述许多位置都用到了类型转换,保证程序的运行。

执行完线程内容,回到InjectCode,完成痕迹擦出

1
2
3
4
5
6
WaitForSingleObject(hThread, INFINITE);	

CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;

回顾

动手调试,很容易可以看出代码注入区别于DLL的方式在于所有完成ThreadPro过程的重要数据都是从[ebp+8]接受使用的,使用的地址是相对地址而不是DLL那样的硬编码地址。另外CodeInject中的ThreadProc也是可以独立运行的代码。

Author: Victory+
Link: https://cvjark.github.io/2022/05/06/逆核系列No-11-代码注入/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.