Windows Dll InjectionProcess InjectionAPI Hook

Posted Han Zheng, Researcher of Compu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Windows Dll InjectionProcess InjectionAPI Hook相关的知识,希望对你有一定的参考价值。

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();
}
View Code
  • 打开待注入的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)
View Code 

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;
    

以上是关于Windows Dll InjectionProcess InjectionAPI Hook的主要内容,如果未能解决你的问题,请参考以下文章

windows 10 wps缺少dll文件

Windows C++:编译 DLL 时是不是可以“烘焙”Windows DLL 依赖项? [复制]

怎么解决.dll没有被指定在windows上运行

windows定位dll的搜索顺序

dll没有被指定在windows上运行该怎么办

学习:Windows API核心DLL文件