逆向工程核心原理-23

逆向工程核心原理-第二十三章-DLL注入

里面函数分析部分很多的内容都参考了SYJ学长的看雪博客: https://bbs.pediy.com/thread-266901.htm

23.1 DLL注入

DLL注入是指向运行中的其他进程强制插入特定的DLL文件.

从技术细节来说, DLL注入命令其他进程自行调用LoadLibrary()API, 加载(Loading)用户指定的DLL文件.

DLL注入与一般DLL加载的区别在于, 加载的目标进程是其自身或其他进程.

Untitled

如上图所示, myhack.dll被注入到notepad.exe中, 关键在于myhack.dll拥有与其他DLL一样的权限, 比如访问notepad进程内存, 这样就可以做任何想做的事情了.

关于DllMain()函数的分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BOOL WINAPI DllMain(HINSTANCE instDLL, DWORD dwReason, LPVOID lpvReserved)
{
switch(weReason)
{
case DLL_PROCESS_ATTACH:
//添加像执行的代码
break;

case DLL_THREAD_ATTACH:
break;

case DLL_THREAD_DETACH:
break;

case DLL_PROCESS_DETACH:
break;
}

return TRUE
}

参数

  • instDLL 指向自身的句柄
  • dwReason 调用的原因
  • lpvReserved 隐式加载和显式加载

关键参数-dwReason的四种类型

  • DLL_PROCESS_ATTACH(进程映射(附加)): 当一个程序需要调用一个DLL中的函数, 首先要把DLL文件映射到进程的地址空间(前面钩子处理函数KeyboardProc()就是这样). 要把一个DLL文件映射到进程的地址空间, 有两种方法: 静态链接和动态链接的LoadLibrary或LoadLibraryEx. 同时, 当一个DLL文件被映射到进程的地址空间时, 系统会调用该DLL的DllMain函数, 此时dwReason的值为DLL_PROCESS_ATTACH, 这种调用只发生在第一次映射时, 如果同一个进程后来为已经映射进来的DLL再次调用LoadLibrary或LoadLibraryEx, 操作系统只会增加DLL的使用次数, 不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数.不同进程用LoadLibrary同一个DLL时, 每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用DllMain函数. 总而言之, 以DLL_PROCESS_ATTACH调用DllMain只发生在一个进程第一次调用该DLL时发生
  • DLL_PROCESS_DETACH(进程卸载): 当DLL从进程的地址空间中被解除映射时, 系统使用参数DLL_PROCESS_DETACH调用DllMain(), 使用该值的DllMain执行的是DLL的清理工作. DLL被解除映射的两种情况: 1. FreeLibrary解除DLL映射 2. 进程结束而解除DLL映射. 注意: 当使用DLL_PROCESS_ATTACH调用DLL的DllMain时, 如果返回FALSE, 说明初始化失败, 系统会调用DLL_PROCESS_DETACH再次调用DllMain进行清理, 因此, 必须保证清理那些没有成功初始化的东西(会占用资源)
  • DLL_THREAD_ATTACH(线程映射): 当进程创建一个线程的时候, 系统查看当前进程地址空间中的所有DLL文件映像, 并用DLL_THREAD_ATTACH调用DLL的DllMain函数, 新创建的线程负责执行这次DLL的DllMain函数, 只有所有的DLL都处理完这一个通知后, 系统才允许进程开始执行它的线程函数. 注意跟DLL_PROCESS_ATTACH的区别, DLL_PROCESS_ATTACH只执行一次, 但是DLL_PROCESS_ATTACH只要有一个新线程在该进程中创建都会执行, 哪怕在线程中建立线程也是一样的.
  • DLL_THREAD_DETACH(线程卸载): 如果线程调用ExitThread来结束线程(线程函数返回时, 系统也会自动调用ExitThread), 系统查看当前映射到进程空间中的所有DLL文件映像, 并用DLL_THREAD_DETACH来调用DllMain函数, 并通知所有的DLL去执行线程级的清理工作.

23.2 DLL注入示例

使用LoadLibrary()API加载某个DLL时, 该DLL中的DllMain()函数就会调用执行.

DLL注入的工作原理就是从外部促使目标进程调用LoadLibrary()API, 所以会强制调用执行DLL的DLLMain()函数.

  • 改善功能和修复Bug
  • 消息钩取
  • API钩取
  • 其他应用程序
  • 恶意代码

23.3 DLL注入的实现方法

三种方法:

  • 创建远程线程(CreateRemoteThread()API)
  • 使用注册表(AppInit_DLLs值)
  • 消息钩取(SetWindowsHookEx()API)

23.4 CreateRemoteThread()创建远程线程

23.4.1 练习示例myhack.dll

过程时将myhack.dll注入notepad.exe进程, 被注入的没有hack.dll用来联网并下载一个html文件.

首先把文件都复制到一个文件夹中

Untitled

运行notepad.exe

Untitled

使用ProcessExplore查看

可以看到notepad的PID为2008

Untitled

运行DebugView

Untitled

myhack.dll注入

InjectDll.exe用来向目标进程注入DLL文件的实用小程序.打开命令行输入相应参数即可运行InjectDll.exe

Untitled

确定DLL注入成功

我们查看debugview

Untitled

然后查看ProcessExplore

Untitled

可以看到myhack.dll已经注入到了notepad.exe中

结果确认

Untitled

可以看到网站已经下载

23.4.2 分析示例源代码

myhack.cpp

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>
#include <tchar.h> //导入宽字节头文件

#pragma comment(lib, "urlmon.lib") //将静态库链接到项目中

#define DEF_URL (L"http://www.naver.com/index.html") //define我们要保存的网页
#define DEF_FILE_NAME (L"index.html") //define我们保存的网页的文件名

HMODULE g_hMod = NULL;

DWORD WINAPI ThreadProc(LPVOID lParam) //LPVOID是一个没有类型的指针,可以将LPVOID类型的变量赋值给任意类型的指针
{
TCHAR szPath[_MAX_PATH] = { 0, }; //定义一个数组来储存路径
if (!GetModuleFileName(g_hMod, szPath, MAX_PATH))//这里就是获取当前模块的路径,因为该模块跟我们要存储html文件的路径是一样的, 所以我们在这里用这个函数获取前面的路径
{
return FALSE;
}
TCHAR *p = _tcsrchr(szPath, '\\'); //在szPath储存的路径中从右到左寻找字符\\并返回字符串指针
if (!p)
{
return FALSE;
}
_tcscpy_s(p + 1, _MAX_PATH, DEF_FILE_NAME); //生成文件保存的路径, 将index.html保存在szPath路径最后的'\'字符之后
URLDownloadToFile(NULL, DEF_URL, szPath, 0, NULL); //下载文件保存到szPath所描述的文件中

return 0;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{//hinstDLL: 指向自身的句柄
HANDLE hTread = NULL; //定义一个通用句柄
g_hMod = (HMODULE)hinstDLL; //将dll自身的句柄赋给g_hMod
switch(fdwReason)
{
case DLL_PROCESS_ATTACH://这里就是首次加载DLL是运行的标签
OutputDebugString(L"myhack.dll Injection!!!"); //调用OutputDebugString函数和调试器交流,并在调试器中输出字符串
hTread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);//创建一个线程来执行ThreadProc()从而在这个Work文件夹中下载index.html
CloseHandle(hTread);//再清除这个线程
break;
}
return TRUE;
}

InjectDll.cpp

用到的函数和指针分析

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
#include "windows.h"
#include "tchar.h

BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath) //L是长指针,P代表指针的意思,C代表const常量的意思,T代表通用类型的意思
{
HANDLE hProcess = NULL, hThread = NULL;
HMODULE hMod = NULL;
LPVOID pRemoteBuf = NULL; //void型的长指针

//确定路径需要占用的缓冲区大小, _tcslen测量字符串的长度,然后加上结尾的空字符,再乘sizeof(TCHAR)
DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc; //LPTHREAD_START_ROUTINE等价于typedef DWORD (__stdcall *LPTHREAD_START_ROUTINE) (LPVOID lpThreadParameter);

//OpenProcess函数获取目标进程句柄(PROCESS_ALL_ACCESS权限)
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError()); //如果打开失败,使用GetLastError()来获取错误信息
return FALSE;
}

//使用VirtualAllocEx函数在目标进程中分配内存,大小为Dll路径的大小
//VirtualAllocEx函数返回的是hProcess指向的目标进程的分配所得缓冲区的内存的首地址
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE); //pRemoteBuf就是分配的内存的首地址

//将myhack.dll路径 ("c:\\myhack.dll")写入目标进程中分配到的内存
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);

hMod = GetModuleHandle(L"kernel32.dll"); //直接获取kernel32.dll的句柄
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");//为之前定义的函数指针赋成kernel32.dll中LoadLibraryW函数的地址

hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL); //hThread为新线程句柄
WaitForSingleObject(hThread, INFINITE);

//关闭句柄
CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}

int _tmain(int argc, TCHAR *argv[])
{
if( argc != 3) //检测doc输入的参数个数是否是3个
{
_tprintf(L"USAGE : %s <pid> <dll_path>\n", argv[0]); //printf的通用类型
return 1;
}

// change privilege
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) ) //提升权限,以便kill进程等操作
return 1;

// inject dll
if( InjectDll((DWORD)_tstol(argv[1]), argv[2]) ) //argv[1]为进程的pid, argv[2]为dll的路径
_tprintf(L"InjectDll(\"%s\") success!!!\n", argv[2]);
else
_tprintf(L"InjectDll(\"%s\") failed!!!\n", argv[2]);

return 0;
}

下面逐个分析API

OpenProcess()

用来打开一个**已经存在的(注意这个条件)**进程对象, 并返回进程的句柄

原型

1
2
3
4
5
6
7
8
9
10
11
12
13
HANDLE OpenProcess(


DWORD dwDesiredAccess, //渴望得到的访问权限(标志)


BOOL bInheritHandle, // 是否继承句柄


DWORD dwProcessId// 进程标示符


);

参数

dwDesiredAccess

  • PROCESS_ALL_ACCESS:获取所有权限
  • PROCESS_CREATE_PROCESS:创建进程
  • PROCESS_CREATE_THREAD:创建线程
  • PROCESS_DUP_HANDLE:使用DuplicateHandle()函数复制一个新句柄
  • PROCESS_QUERY_INFORMATION:获取进程的令牌、退出码和优先级等信息
  • PROCESS_QUERY_LIMITED_INFORMATION:获取进程特定的某个信息
  • PROCESS_SET_INFORMATION:设置进程的某种信息
  • PROCESS_SET_QUOTA:使用SetProcessWorkingSetSize函数设置内存限制
  • PROCESS_SUSPEND_RESUME:暂停或者恢复一个进程
  • PROCESS_TERMINATE:使用Terminate函数终止进程
  • PROCESS_VM_OPERATION:在进程的地址空间执行操作
  • PROCESS_VM_READ:使用ReadProcessMemory函数在进程中读取内存
  • PROCESS_VM_WRITE:使用WriteProcessMemory函数在进程中写入内存
  • SYNCHRONIZE:使用wait函数等待进程终止

bInheritHandle

  • TRUE
  • FALSE

dwProcessld

  • 已存在进程的PID

返回值

  • 成功, 则返回指定进程的句柄
  • 失败, 则返回NULL, 并可用GetLastError获取错误代码

VirtualAllocEx

在指定进程(注意是指定, 在这里我们所指定的就是notepad的内存区域, 而不是我们运行的Injectdll)的虚拟空间保留或提交内存区域, 除非指定MEM_RESET参数, 否则将该内存区域初始化为0

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LPVOID VirtualAllocEx(


HANDLE hProcess,//即将申请内存的进程句柄, 这里是notepad的进程句柄, 是用OpenProcess()获得的


LPVOID lpAddress,//保留页面的内存地址, 一般设为NULL让系统自动分配


SIZE_T dwSize,//想分配的内存大小, 以字节为单位, 实际分配的内存大小是业内存大小的整数倍


DWORD flAllocationType,//分配方式


DWORD flProtect//设置该内存保护属性


);

参数

hProcess

  • 申请内存所在的句柄

lpAddress

  • 保留页面的内存地址, 一般设为NULL让系统自动分配

dwSize

  • 想分配的内存大小, 以字节为单位, 实际分配的内存大小是业内存大小的整数倍

flAllocationType

  • MEM_COMMIT:为特定的页面区域分配内存中或磁盘的页面文件中的物理存储
  • MEM_PHYSICAL :分配物理内存(仅用于地址窗口扩展内存
  • MEM_RESERVE:保留进程的虚拟地址空间,而不分配任何物理存储。保留页面可通过继续调用VirtualAlloc()而被占用
  • MEM_RESET :指明在内存中由参数lpAddress和dwSize指定的数据无效
  • MEM_TOP_DOWN:在尽可能高的地址上分配内存(Windows 98忽略此标志)
  • MEM_WRITE_WATCH:必须与MEM_RESERVE一起指定,使系统跟踪那些被写入分配区域的页面(仅针对Windows 98)

flProtect

  • PAGE_READONLY: 该区域为只读。如果应用程序试图访问区域中的页的时候,将会被拒绝访问
  • PAGE_READWRITE: 区域可被应用程序读写
  • PAGE_EXECUTE: 区域包含可被系统执行的代码。试图读写该区域的操作将被拒绝。
  • PAGE_EXECUTE_READ :区域包含可执行代码,应用程序可以读该区域
  • PAGE_EXECUTE_READWRITE: 区域包含可执行代码,应用程序可以读写该区域。
  • PAGE_GUARD: 区域第一次被访问时进入一个STATUS_GUARD_PAGE异常,这个标志要和其他保护标志合并使用,表明区域被第一次访问的权限
  • PAGE_NOACCESS: 任何访问该区域的操作将被拒绝
  • PAGE_NOCACHE: RAM中的页映射到该区域时将不会被微处理器缓存(cached)

返回值

  • 成功, 则返回分配的内存的首地址
  • 是被, 则返回NULL

WriteProcessMemory

将数据写入内存的函数, 但是前提是这段内存是可写的, 这就是前面的VirtualAllocEx的参数flProtect的值PAGE_READWRITE的作用了, 它使得我们分配的那段内存是可读可写的.

原型

1
2
3
4
5
6
7
8
9
10
11
12
BOOL WriteProcessMemory(

HANDLE hProcess,//该内存所在的进程的句柄

LPVOID lpBaseAddress,//要**被写入**的内存首地址

LPVOID lpBuffer,//**要写入**的数据的指针

DWORD nSize,//要写入的字节数

LPDWORD lpNumberOfBytesWritten//实际数据的长度
);

返回值

  • 成功, 则返回一个非零值
  • 失败, 则返回0, 可以用GetLastError获取跟多错误详细信息

CreateRemoteThread, 关键函数

创建一个在其他进程(注意是在其他进程中)地址空间中运行(注意是运行的)的线程(即远程线程)

原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HANDLE WINAPI CreateRemoteThread(

__in HANDLE hProcess,

__in LPSECURITY_ATTRIBUTES lpThreadAttributes,

__in SIZE_T dwStackSize,

__in LPTHREAD_START_ROUTINE lpStartAddress,

__in LPVOID lpParameter,

__in DWORD dwCreationFlags,

__outSECURITY_ATTRIBUTES

参数

hProcess

创建的要执行的线程所属的进程的句柄

lpThreadAttributes

一个指向SECURITY_ATTRIBUTES结构的指针, 该结构指定了线程的安全属性.

dwStackSize

线程栈初始大小,以字节为单位,如果该值设为0,那么使用系统默认大小.

lpStartAddress

在远程进程的地址空间中, 该线程的线程函数的起始地址

在这里传入的是LoadLibraryW库函数的地址

lpParameter

传给线程函数的参数

在这里传入的是myhack.dll的路径

dwCreationFlags

线程的创建表示

lpThreadId

指向所创建线程ID的指针, 如果创建失败, 该参数为NULL

返回值

  • 成功, 则返回新线程的句柄
  • 失败, 则返回NULL

再回来分析InjectDll.exe

  • 首先获取目标进程句柄, 使用OpenProcess()实现
  • 在notepad的地址空间中分配内存来存储myhack.dll的路径, 通过VirtualAllocEx()实现
  • 将myhack.dll的路径写入notepad的内存空间中并获取该地址, 通过WriteProcessMemory实现
  • 获取LoadLibraryW的地址, 由于DLL共享所以可以直接使用GetProcAddress获得
  • 使用CreateRemoteThread()在notepad中创建一个进程调用LoadLibraryW()从而加载myhack.dll
  • 在myhack.dll中执行了DllMain函数, 在其中调用ThreadProc()(及线程过程函数)下载了网页

CreateRemoteThread()实现DLL注入的原理

首先我们一定一定要知道的是InjectDll.exe调用了CreateRemoteThread(), 而CreateRemoteThread()作用的进程却是notepad.exe

而CreateRemoteThread()对notepad.exe做了什么呢, 就是在notepad.exe中创建了一个线程, 且这个线程调用了LoadLibraryW()这个API, 而这个LoadLibraryW()加载的就是myhack.dll. 在这里停下, DLL就是这样注入到了notepad当中.

再具体一点就是, 我们先来看看具体的Inject DLL.cpp源代码

hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);

注意第四个参数和第五个参数

  • pThreadProc, 这个参数是线程函数的起始地址, 意即线程开始时在那个函数那里执行, 我们传入的是LoadLibraryW()函数的地址, 所以该线程会执行该函数在notepad.exe中注入某个DLL, 但是我们现在不知道要注入的是哪个DLL, 所以还需要给LoadLibraryW()一个参数
  • pRemoteBuf, 这个参数就是为了弥补上面的空缺, 告诉线程中执行的LoadLibraryW()要注入哪个DLL, 在这个实例中我们给的值是myhack.dll的路径, 所以LoadLibraryW()会注入myhack.dll, 并执行其DllMain(), 剩下的事情就跟消息钩子一样了.

23.4.3 调试方法

首先打开notepad.exe

Untitled

然后打开OD, 使用附加功能, 将OD附加到notepad上

Untitled

然后就可以调试notepad.exe了

Untitled

然后我们设置选项, 使得调试会在DLL注入时在DLL的EP处暂停

Untitled

AppInit_DLLs注入

该方法使用的是注册表修改, 修改AppInit_DLLs 和 LoadAppInit_DLLs, 使得每个加载了user32.dll的进程都会加载我们添加的DLL, 从而实现DLL注入

源代码分析

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
// myhack.cpp. 作用是以隐藏模式运行IE,连接到指定网站

#include "windows.h"
#include "tchar.h"

#define DEF_CMD L"c:\\Program Files\\Internet Explorer\\iexplore.exe" //定义要打开的浏览器的路径
#define DEF_ADDR L"http://www.naver.com" //要打开的网址
#define DEF_DST_PROC L"notepad.exe" //要用于比较的进程的名称

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
TCHAR szCmd[MAX_PATH] = {0,};//我们控制台的输入
TCHAR szPath[MAX_PATH] = {0,};//DLL文件路径
TCHAR *p = NULL;
STARTUPINFO si = {0,}; //定义一个STARTUPINFO类型的结构体,用于指定新进程的主窗口特性的一个结构
PROCESS_INFORMATION pi = {0,}; //定义一个PROCESS_INFORMATION(进程信息)类型的结构体,

si.cb = sizeof(STARTUPINFO); //包含STARTUPINFO结构中的字节数
si.dwFlags = STARTF_USESHOWWINDOW; //使用(感觉用激活更好些)该结构体的wShowWindow成员
si.wShowWindow = SW_HIDE; //以隐藏方式打开窗口,即隐藏, 并且任务栏也没有最小化图标

switch( fdwReason )
{
case DLL_PROCESS_ATTACH ://DLL首次载入时执行的语句
if( !GetModuleFileName( NULL, szPath, MAX_PATH ) ) //获取该dll加载到的进程的全路径
break;

if( !(p = _tcsrchr(szPath, '\\')) ) //获取"\\"字符之后的进程名指针
break;

if( _tcsicmp(p+1, DEF_DST_PROC) ) //比较进程是否是指定的进程notepad.exe
break;

wsprintf(szCmd, L"%s %s", DEF_CMD, DEF_ADDR); //将浏览器地址和网址格式化打印到szCmd数组中
if( !CreateProcess(NULL, (LPTSTR)(LPCTSTR)szCmd,
NULL, NULL, FALSE,
NORMAL_PRIORITY_CLASS,
NULL, NULL, &si, &pi) ) //创建进程,第二个参数用于指向一个以NULL结尾的字符串,该字符串指定要执行的命令行。
break; //FALSE表示新进程没有从调用进程处继承了句柄。
if( pi.hProcess != NULL ) //pi.hProcess返回新进程的句柄
CloseHandle(pi.hProcess);

break;
}

return TRUE;
}

实际操作

运行 → 注册表编辑器regedit.exe

随后进入如下路径

Untitled

可以看到我们要找的AppInit_DLLs , 但是我的虚拟机里没有LoadAppInit_DLLs选项, 但是其实就是一个DWORD值, 所以新建一个试试

Untitled

同时修改其值, 如下:

Untitled

重启电脑后, 查看ProcessExplorer

Untitled

Untitled

发现所有加载了user32.dll的进程都加载了myhack2.dll

但是myhack2.dll只对notepad.exe生效, 所以我们打开notepad.exe查看效果, 由于窗口为隐藏, 只能使用ProcessExplorer查看

Untitled

下面就是复原重启即可

最后的DLL再上章讲过了, 看21章内容即可

文章作者: LamのCrow
文章链接: http://example.com/2022/04/05/逆向工程核心原理-第 83e0b/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LamのCrow