DLL注入

0x00-远程线程注入

前置知识

线程注入,是通过开启远程线程的方式,将DLL加载到目标宿主进程中的常用方式。

由于WinNT系统下进程空间的独立性,获取其他进程的信息,就需要进入目标进程空间的方式,而使用线程注入可以轻松实现。

使用LoadLibrary动态加载DLL

使用GetProcAddress获取DLL中导出函数的指针

DLL的分类

在VS的编译环境下,DLL又分为三类:

非MFC的DLL——即使用SDK API进行编程,能被其他所有语言调用

MFC规则DLL——可以使用MFC进行编程,能被其他所有语言调用

MFC扩展DLL——可以使用MFC进行编程,但只能被用MFC编写的程序调用

MFC——Microsoft Foundation Class-Library是微软用C++对API进行的封装,全部封装成了类,简化了使用

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
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpReserved ) // reserved
{
// Perform actions based on the reason for calling.
switch( fdwReason )
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.进程第一次链接DLL并通过它的入口点会得到这个参数
// Return FALSE to fail DLL load.
break;

case DLL_THREAD_ATTACH:
// Do thread-specific initialization.进程在空间中取消DLL的映射时会得到这个参数
break;

case DLL_THREAD_DETACH:
// Do thread-specific cleanup.每当新线程创建时,系统会对所有映射的DLL传入此参数调用入口函数
break;

case DLL_PROCESS_DETACH:
// Perform any necessary cleanup.每当线程退出或者返回时时,系统会对所有映射的DLL传入此参数要求执行对应清理工作
break;
}
return TRUE; // Successful DLL_PROCESS_ATTACH.
}

DLL编写与导出

DLL的导出函数使用

extern “C” _declspec(dllexport)

DLL的导入函数使用

extern “C” _declspec(dllimport)

其中,extern "C"作为一种编译约定,表示按照C语言的方式导出

由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。 这样我们就可以直接通过函数名对DLL导出函数进行调用

DLL动态加载

既然我们要把DLL注入到进程中,那么需要先了解一下,进程是怎样调用DLL的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//使用LoadLibrary加载所需的DLL
//LoadLibraryA和LoadLibraryW分别对应ANSI编码下和Unicode编码下,因为一般都是Unicode,所以一般用w,路径记得改为\\
HMODULE hMod = LoadLibraryA(DLL路径);

//定义导入函数指针

typedef int (*ADD_IMPORT)(int a,int b);//定义一个返回值为int型的函数指针,这样ADD_IMPORT就是一个函数指针

//使用GetProcAddress获取函数入口点

ADD_IMPORT add_proc=(ADD_IMPORT)GetProcAddress(hMod,"ADD");

//直接调用

int result = add_proc(1,2);

//释放句柄
FreeLibrary(hMod);

线程注入

注入的可行性

kernel32.dll和user32.dll是两个在大部分程序上都会调用的DLL

同一个DLL,在不同进程中不一定被映射(加载)在同一个内存地址下

但是kernel32.dll和user32.dll除外,它们总是被映射到进程的内存首选地址

因此在所有使用这两个DLL的进程中,这两个DLL的内存地址是相同的

因此我们在本进程获取的kernel32.dll中函数的地址,在目标进程也是一样的

线程注入过程

目标进程->开辟并传入DLL地址->开启远程线程->加载DLL->实现DLL的注入

依次使用以下函数

1
2
3
4
5
6
7
OpenProcess()//获取目标进程的句柄
VirtualAllocEx()//在进程中申请空间
WriteProcessMemory()//向进程中写入DLL路径
GetProcAddress()//取得函数LoadLibrary在DLL中的地址
CreateRemoteThreadEx()//在目标进程中创建新线程使用LoadLibrary加载DLL
WaitForSingleObject()//挂起线程,可以传递INFINITE指明要无限期等待下去,等待线程执行完再执行下一步
CloseHandle()//关闭句柄

CreateRemoteThread()函数

1
2
3
4
5
6
7
8
HANDLE WINAPI CreateRemoteThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全相关的属性,常置为NULL
SIZE_T dwStackSize, //新线程的初始化栈在大小,可设置为0
LPTHREAD_START_ROUTINE lpStartAddress, //被线程执行的回调函数,也称为线程函数
LPVOID lpParameter, //传入线程函数的参数,不需传递参数时为NULL
DWORD dwCreationFlags, //控制线程创建的标志
LPDWORD lpThreadId //传出参数,用于获得线程ID,如果为NULL则不返回线程ID
);

目标其实就是让目标进程调用LoadLibrary()加载dll

将线程函数指定为LoadLibrary()函数,并且把DLL加载地址传递给线程参数,这样就在目标进程中成功调用了FreeLibrary函数

CreateRemoteThread()原意是在外部进程调用执行线程函数,不过这里的线程函数换成了FreeLibrary()

Inject.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
61
62
63
64
#include "windows.h"
#include "tchar.h"

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. 使用OpenProcess函数获取目标进程句柄(PROCESS_ALL_ACCESS权限)
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
return FALSE;
}

// #2. 使用VirtualAllocEx函数在目标进程中分配内存,大小为szDllName
// VirtualAllocEx函数返回的是hProcess指向的目标进程的分配所得缓冲区的内存地址,最后一个参数表示开辟内存的属性
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);

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

// #4. 获取LoadLibraryA() API的地址
// 这里主要利用来了kernel32.dll文件在每个进程中的加载地址都相同这一特点,所以不管是获取加载到
// InjectDll.exe还是notepad.exe进程的kernel32.dll中的LoadLibraryW函数的地址都是一样的。这里的加载地
// 址相同指的是在同一次系统运行中,如果再次启动系统kernel32.dll的加载地址会变,但是每个进程的
// kernerl32.dll的加载地址还是一样的。
hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");

// #5. 在目标进程notepad.exe中运行远程线程
// pThreadProc = notepad.exe进程内存中的LoadLibraryW()地址
// pRemoteBuf = notepad.exe进程内存中待加载注入dll的路径字符串的地址
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
WaitForSingleObject(hThread, INFINITE);

//同样,记得关闭句柄
CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}

int _tmain(int argc, TCHAR* argv[])
{
if (argc != 3)
{
_tprintf(L"USAGE : %s <pid> <dll_path>\n", argv[0]);
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;
}

inject.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
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include<windows.h>
#include<stdio.h>
extern "C" _declspec(dllexport) void Print()
{
printf("helloworld");
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxA(0, "注入成功", "Hint", MB_OK);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

效果

可以看到在notepad.exe的进程中注入了inject.dll

https://blog.csdn.net/xiewneqi/article/details/4683888?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~TopBlog-1.topblog&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~TopBlog-1.topblog&utm_relevant_index=1

有时候需要提升进程权限

在枚举/结束系统进程/或操作系统服务时,会出现自己权限不足而失败的情况,这时就需要提升自己进程到系统权限

前置知识

Windows的每个用户登录时,系统会产生一个访问令牌(access token),其中关联了当前用户的权限信息,用户登录后创建的每一个进程都含有用户access token的拷贝。当进程试图执行某些需要特殊权限的操作或者是访问受保护的内核对象时,系统会检查其access token中的权限信息以决定是否授权操作。

Administrator组成员的access token中会含有一些可以执行系统级操作的特权(privileges),如终止任意进程、关闭\重启系统、加载设备驱动和更改系统时间等,不过这些权限默认是被禁用的

当Administrator组成员创建的进程中包含一些需要特权的操作时,进程必须首先打开这些禁用的特权以提升自己的权限,否则系统将拒绝进程的操作。

windows以字符串的形式表示系统特权,如”SeCreatePageFilePrivilege“表示该特权用于创建页面文件,”SeDebugPrivilege“表示该特权可用于调试及更改其他进程的内存,为了方便使用这些字符串,微软在winnt.h定义了一组宏,如#define SE_DEBUG_NAME TEXT(“SeDebugPrivilege”)

权限列表

在vs的定义也可以看到,这些宏定义对应了不同的权限,可以使用LookupPrivilege函数得到对应权限的LUID

虽然Windows使用字符串表示特权,但查询或更改特权的API需要LUID来引用相应的特权,LUID表示 local unique identifier ,在系统中是唯一的。为了提升进程权限到指定的特权,我们必须找到特权对应的LUID,这时候就需要调用LookupPrivilege函数,获取到特权对应的LUID时,我们要打开该特权,此时要用到LUID_AND_ATTRIBUTES结构

其定义如下

1
2
3
4
typedef struct _LUID_AND_ATTRIBUTES {
LUID Luid;
DWORD Attributes;
} LUID_AND_ATTRIBUTES, * PLUID_AND_ATTRIBUTES;

当Attributes的值取SE_PRIVILEGE_ENABLED时将打开LUID对应的特权。设置完成后

AdjustTokenPrivileges函数通知操作系统将指定的access token权限中的特权置为打开状态,前面我们说过,进程执行需要特列权限的操作时,系统将检查其access token,因此更改了进程的access token特权设置,也就是更改了所属进程的特权设置

函数定义如下

1
2
3
4
5
6
7
8
BOOL WINAPI AdjustTokenPrivileges(
__in HANDLE TokenHandle,
__in BOOL DisableAllPrivileges,
__in_opt PTOKEN_PRIVILEGES NewState,
__in DWORD BufferLength,
__out_opt PTOKEN_PRIVILEGES PreviousState,
__out_opt PDWORD ReturnLength
);

TokenHandle是要更改特权设置的access token的句柄,DisableAllPrivileges表示是否禁用该access token的所有特权,NewState用来传递新的特权设置,注意它的类型是PTOKEN_PRIVILEGES,它是TOKEN_PRIVILEGES结构的指针

TOKEN_PRIVILEGES定义如下

1
2
3
4
typedef struct _TOKEN_PRIVILEGES {
DWORD PrivilegeCount;
LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];
} TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;

其中ANYSIZE_ARRAY被定义为1,可以看到TOKEN_PRIVILEGES中包含了用于设置特权信息的LUID_AND_ATTRIBUTES结构,在使用时,只需要将PrivilegeCount赋为1,然后把Privileges数组的第1个元素(Privileges[0])的Luid域设置为指定特权的Luid,再将其Attributes域设置为SE_PRIVILEGE_ENABLED,就可以完成TokenHandle表示的access token权限的提升了

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
BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)
{
TOKEN_PRIVILEGES tp;
HANDLE hToken;
LUID luid;
//获取当前进程的access token句柄
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&hToken))
{
_tprintf(L"OpenProcessToken error: %u\n", GetLastError());
return FALSE;
}
//查找newprivileges参数对应的Luid,并将结果写入tp.Privileges[0]的Luid域中
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的结构
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))
{
_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;
}

0x01-注册表注入

原理

Windows操作系统的注册表默认提供了AppInit_DLLs与LoadAppInit_DLLs两个注册表项

在注册表编辑器中,将要注入的DLL路径字符串写入AppInit_DLLs项目,然后把LoadAppInit_DLLs的项目值设置为1.重启后,指定DLL会注入所有进程。

0x02-SetWindowsHookEx()

在另一篇文章Hook写了,就不过多介绍

下面是更详细的解释

https://cloud.tencent.com/developer/article/1199648?msclkid=f79c9cc7b96011ecbdb2f47296f3b9d3

注入DLL的第三个方法就是消息钩取

0x03-还有其他方法

https://bbs.pediy.com/thread-253918.htm