DLL注入实践

Windows系统大量使用dll作为组件复用,应用程序也会通过dll实现功能模块的拆分。DLL注入技术是向一个正在运行的进程插入自有DLL的过程。

Window下的代码注入

常见的Windows代码注入方法如下:

  • 注册表注入
    编译注册表中的AppInit_DLLs选项,凡是使用GUI的进程,都会读取AppInit_DLLs内容,加载这些Dll。

  • Windows Hook注入
    使用 SetWindowsHookEx、UnHkkkWindowsHookEx 来进行,为目标进程安装钩子,在注入dll中监听目标进程消息。

  • 远程线程注入

    使用 CreateRemoteThread 函数在目标进程中创建线程,在该线程中加载注入dll。

  • DLL函数转发

    使用伪造的dll来替换目标dll,两个dll的导出符号完全相同,在自定义DLL中,先利用函数转发器将请求转发到真实dll中,然后进行自己的一些处理。

在本篇文章中,主要介绍 Windows Hook注入 这一种方式。在具体介绍之前,先介绍下Dll的加载顺序、加载过程。

DLL加载顺序

系统在搜索加载指定DLL之前,按照如下顺序做检查:

  1. 如果同名DLL已在内存中加载,则直接使用
  2. 如果DLL在系统DLL列表中,系统直接使用已知DLL的拷贝
  3. 如果DLL依赖其他DLL,系统会按照名字搜索并加载依赖的DLL,待依赖的DLL加载完毕后再加载自身。

系统已知DLL列表配置位于注册表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs选项中,本机上的内容如下:

系统DLL列表.jpg

标准的DLL搜索顺序取决于系统安全DLL搜索模式,该模式默认使能。按照如下顺序搜索DLL:

  1. 进程对应的应用程序目录,可通过 GetModuleFileName 获得,程序启动后为固定值。
  2. Windows系统目录,一般为 C:\Windows\system32
  3. Windows目录,一般为 C:\Windows
  4. 当前进程目录,通过 GetCurrentDirectory 获得,程序可通过 SetCurrentDirectory 进行修改。
  5. PATH环境变量中的目录

DLL加载过程

DLL加载分为隐式加载和显示加载。

隐式加载既为在编译链接选项中增加导入库,在程序运行目录存放待加载的dll。双击程序启动时,由系统加载程序根据exe中的导入表加载对应dll到进程空间,若dll有依赖其他dll的,会递归加载直到加载完成所有必需的dll,然后进行exe的导入函数地址重定位,使得能够调用dll的导出函数。

显示加载指的是由应用程序按需加载,具体为调用 LoadLibraryFreeLibraryGetProcAddress这三个API函数,加载并获取dll的导出函数地址。

加载器执行流程

  1. 为进程创建虚拟地址空间
  2. 把可执行模块映射到进程地址空间中
  3. 检查可执行模块的导入段,根据DLL搜索规则,找到所需的DLL并加载。如在此阶段,未找到需要的dll,弹出"无法启动,因为计算机中缺失XXX.dll"
  4. 检查dll的导入表(IAT),如果该dll还依赖其他的dll,那么继续去定位所需的dll并加载.
  5. 修复导入符号的引用符号。具体做法,遍历所有模块的导入段,针对每个导入符号,加载程序在导出段中检查是否存在匹配的导入符号,若存在,则取导出符号的RVA,加上对应dll的基址,得到导出符号的真实地址,填入对应的导入表中。如在此阶段未找到对应的导出符号,弹出"程序入口点XXX无法定位到动态链接库xxx.dll上"
  6. 可执行模块运行期间如果调用到某个dll的导出函数,则会跳转到IAT,得到导出函数的地址,然后进行调用。

使用CreateRemoteThread注入DLL

使用CreateRemoteThread可使得目前进程创建线程,但要加载注入dll,需要在目标线程的虚拟地址空间申请内存,用于保存目标dll的名称,通过 GetProcAddress 函数在远程线程中 kernel32.dll 模块的LoadLibrary函数地址,用于远程线程的入口函数,主要流程如下:

  1. 获取 LoadLibrary 函数的地址
  2. 调用 VirtualAllocEx 函数在远程进程中申请一段虚拟内存
  3. 调用 WriteProcessMemory 将 待加载的dll名称写入虚拟内存 ReadProcessMemory 读写进程地址内容
  4. 调用 CreateRemoteThread 创建远程线程,回调函数为 LoadLibrary,参数为对应字符串地址
  5. 调用 VirtualFreeEx 释放远程虚拟内存

执行完自己的函数后,就要远程卸载dll,思路与注入类似,函数变为FreeLibrary,传入参数对对应dll的句柄。
获取已加载的模块句柄(通过EnumProcessModule实现)

注入代码示例:


// 获得指定进程名称的进程PID
int GetProcessId(const char* pName)
{
    PROCESSENTRY32 pe;
    DWORD id = 0;
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    pe.dwSize = sizeof(PROCESSENTRY32);
    if (!Process32First(hSnapshot, &pe))
        return 0;

    while (1)
    {
        pe.dwSize = sizeof(PROCESSENTRY32);
        if (Process32Next(hSnapshot, &pe) == FALSE)
            break;

        if (strcmp(pe.szExeFile, pName) == 0)
        {
            id = pe.th32ProcessID;
            break;
        }
    }

    CloseHandle(hSnapshot);
    return id;
}

//利用远程线程来注入dll
bool RemoteThreadDllInject()
{
    提权代码,在Windows Vista 及以上的版本需要将进程的权限提升,否则打开进程会失败
    if (!SetDebugPrivilege(TRUE))
    {
        printf("提升权限失败\n");
        return false;
    }

    int nPid = GetProcessId(INJECT_EXE_NAME);

    // 打开目标进程
    HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, nPid);
    if (NULL == hRemoteProcess)
    {
        printf("OpenProcess failed! %d", GetLastError());
        return false;
    }
    
    // 获得 LoadLibraryW 在 kernel32.dll 中的地址
    typedef HMODULE(WINAPI *pfnLoadLibrary)(LPCWSTR);
    // 这里注意要载入宽字节版本还是普通版本的 LoadLibrary  
    pfnLoadLibrary pfnThreadRtn2 = (pfnLoadLibrary)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "LoadLibraryA");
    // 在远程进程中申请内存空间,用于保存远程线程的参数
    LPVOID lpRemoteMemory = VirtualAllocEx(hRemoteProcess, 0, MAX_PATH, MEM_COMMIT, PAGE_READWRITE);

    string strInjectDllName(INJECT_DLL_NAME);
    DWORD nWritten = 0;
    BOOL bRet = WriteProcessMemory(hRemoteProcess, lpRemoteMemory, strInjectDllName.c_str(), strInjectDllName.length() + 1, &nWritten);

    HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pfnThreadRtn2, lpRemoteMemory, 0, NULL);
    WaitForSingleObject(hRemoteThread, INFINITE);
    VirtualFreeEx(hRemoteProcess, lpRemoteMemory, 0, MEM_RELEASE);

    CloseHandle(hRemoteThread);
    CloseHandle(hRemoteProcess);

    return true;
}

远程卸载dll示例代码:

// 获得指定进程内指定模块信息
bool GetProcessModule(DWORD dwPid, string strModuleName, LPMODULEENTRY32 lpMe32, DWORD cbMe32)
{
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPid);
    if (INVALID_HANDLE_VALUE == hSnapshot)
    {
        printf("CreateToolhelp32Snapshot Error");
        return false;
    }

    bool bFind = false;
    MODULEENTRY32 moduleInfo = {0};
    moduleInfo.dwSize = sizeof(MODULEENTRY32);
    if (Module32First(hSnapshot, &moduleInfo))
    {
        do 
        {
            if (strModuleName == string(moduleInfo.szModule))
            {
                memcpy(lpMe32, &moduleInfo, cbMe32);
                bFind = true;
                break;
            }
        } while (!bFind && Module32Next(hSnapshot, &moduleInfo));
    }
    
    CloseHandle(hSnapshot);

    return bFind;
}


bool RemoteDllUnLoad()
{
    //在远程线程执行结束后,注入的 dll 仍然存在与目标进程中,我们需要再次使用 CreateRemoteThread,执行 FreeLibrary ,将之前注入的 dll 卸载掉
    // 实现思路:枚举进程的模块,根据模块名称找到对应模块的句柄。
    int nPid = GetProcessId(INJECT_EXE_NAME);

    提权代码,在Windows Vista 及以上的版本需要将进程的权限提升,否则打开进程会失败
    if (!SetDebugPrivilege(TRUE))
    {
        printf("提升权限失败\n");
        return false;
    }

    MODULEENTRY32 moduleInfo = {0};
    if (!GetProcessModule(nPid, INJECT_DLL_NAME, &moduleInfo, sizeof(moduleInfo)))
    {
        printf("can't find %s dll in %s", INJECT_DLL_NAME, INJECT_EXE_NAME);
        return false;
    }
    
    typedef BOOL(*pfnFreeLibrary)(HMODULE);
    pfnFreeLibrary pFreeLibrary = (pfnFreeLibrary)GetProcAddress(GetModuleHandle("kernel32.dll"), "FreeLibrary");

    HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, nPid);
    if (hRemoteProcess == NULL)
    {
        printf("OpenProcess Error");
        return false;
    }

    HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFreeLibrary, moduleInfo.hModule, 0, NULL);
    WaitForSingleObject(hRemoteThread, INFINITE);

    CloseHandle(hRemoteThread);
    CloseHandle(hRemoteProcess);
    return true;
}

注意事项

  • 远程进程可能以Unicode编码编译,也可能是MBSC编码编译,因此,要区分 LoadLibraryW 还是 LoadLibraryA 版的函数,FreeLibrary函数无需区分不同版本。
  • 在获得目标进程的句柄时,需要对自身程序进程提权操作。一般需要当前登陆用户属于Administrator组成员,才有能力提升至调试权限。Administrator组成员的access token中会含有一些可以执行系统级操作,注意,非Administrator组成员创建的进程 无法提升自身的权限。如果提权失败,建议通过管理权权限打开VS开发环境。
  • 在注入时,需要将待注入的dll放在远程进程同一目录下。
  • 验证注入,可通过在dllMain函数的加载和卸载分支中弹框提示,也可以通过 procexp64.exe 程序来查看程序当前加载的dll信息。

工程实践

如何获得运行过程中不符合预期的dll

  1. 通过读取exe的PE头,获得依赖的dll列表,遍历读取dll依赖的dll,得到集合1
  2. 在程序运行过程中,调用 EnumProcessModules 得到当前加载所有dll,得到集合2
  3. 从集合2中排除掉集合1的内容,得到生成集合3,即为不符合预期的dll。

一般来说,输入法(搜狗输入法的皮肤组件PicFace.dll、资源组件Resource.dll等)、监控软件的dll会自动加载到所有进程的dll中去。

模块基址重定位

每个模块(exe和dll)在编译输出时,都有一个首选基址,它指示加载器将模块映射到进程地址空间中的首选位置,一般exe的基址设定为 0x00400000,dll模块为 0x10000000.当一个exe依赖于多个dll时,第一个dll被正确的加载到 0x10000000上,随后的dll就不能在加载到0x10000000上,加载程序会对随后的dll进行基址重定位,把它放到别的地方。基址重定位会增加程序初始化时间,因此,如果将多个模块载入同一进程空间,可以给不同模块指定不同的基址。在VS开发环境中,基址配置方式:DLL工程的配置属性-->链接器-->高级-->基址。

在所有dll编译完成后,使用 Rebase.exe 工具,对需要载入进程地址空间的所有模块进行基址重定位,将结果写回到dll文件中。

一旦一个dll模块的基址已知,那么可直接推算出exe在使用该DLL的导出函数的真实地址。这种预先将exe和所依赖的dll绑定在一起的做法,可以提高应用程序的启动速度。

VS提供 Bind.exe程序提供了绑定可执行文件与dll的功能。工作原理为,读取所有dll的基址和导出符号的RVA,计算后填充到可执行文件的导入表中。

参考文档:多种DLL注入技术原理介绍

转载于:https://www.cnblogs.com/cherishui/p/11205377.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
远程注入DLL方法有很多种,也是很多木马病毒所使用的隐藏进程的方法,因为通过程序加载的DLL在进程管理器是没有显示的.这里介绍一种用 CreateRemoteThread 远程建立线程的方式注入DLL. 首先,我们要提升自己的权限,因为远程注入必不可免的要访问到目标进程的内存空间,如果没有足够的系统权限,将无法作任何事.下面是这个函数是用来提升我们想要的权限用的. function EnableDebugPriv: Boolean; var hToken: THandle; tp: TTokenPrivileges; rl: Cardinal; begin Result := false; //打开进程令牌环 OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, hToken); //获得进程本地唯一ID if LookupPrivilegeValue(nil, 'SeDebugPrivilege', tp.Privileges[0].Luid) then begin tp.PrivilegeCount := 1; tp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED; //调整权限 Result := AdjustTokenPrivileges(hToken, false, tp, SizeOf(tp), nil, rl); end; end; 关于 OpenProcessToken() 和 AdjustTokenPrivileges() 两个 API 的简单介绍: OpenProcessToken():获得进程访问令牌的句柄. function OpenProcessToken( ProcessHandle: THandle; //要修改访问权限的进程句柄 DesiredAccess: DWORD; //指定你要进行的操作类型 var TokenHandle: THandle//返回的访问令牌指针 ): BOOL; AdjustTokenPrivileges() :调整进程的权限. function AdjustTokenPrivileges( TokenHandle: THandle; // 访问令牌的句柄 DisableAllPrivileges: BOOL; // 决定是进行权限修改还是除能(Disable)所有权限 const NewState: TTokenPrivileges; { 指明要修改的权限,是一个指向TOKEN_PRIVILEGES结构的指针,该结构包含一个数组, 数据组的每个项指明了权限的类型和要进行的操作; } BufferLength: DWORD; //结构PreviousState的长度,如果PreviousState为空,该参数应为 0 var PreviousState: TTokenPrivileges; // 指向TOKEN_PRIVILEGES结构的指针,存放修改前的访问权限的信息 var ReturnLength: DWORD //实际PreviousState结构返回的大小 ) : BOOL; 远程注入DLL其实是通过 CreateRemoteThread 建立一个远程线程调用 LoadLibrary 函数来加载我们指定的DLL,可是如何能让远程线程知道我要加载DLL呢,要知道在Win32系统下,每个进程都拥有自己的4G虚拟地址空间,各个进程之间都是相互独立的。所我们需要在远程进程的内存空间里申请一块内存空间,写入我们的需要注入DLL 的路径. 需要用到的 API 函数有: OpenProcess():打开目标进程,得到目标进程的操作权限,详细参看MSDN function OpenProcess( dwDesiredAccess: DWORD; // 希望获得的访问权限 bInheritHandle: BOOL; // 指明是否希望所获得的句柄可以继承 dwProcessId: DWORD // 要访问的进程ID ): THandle; VirtualAllocEx():用于在目标进程内存空间中申请内存空间以写入DLL的文件名 function VirtualAllocEx( hProcess: THandle; // 申请内存所在的进程句柄 lpAddress: Pointer; // 保留页面的内存地址;一般用nil自动分配 dwSize, // 欲分配的内存大小,字节单位;注意实际分 配的内存大小是页内存大小的整数倍 flAllocationType: DWORD; flProtect: DWORD ): Pointer; WriteProcessMemory():往申请到的空间中写入DLL的文件名 function WriteProcessMemory( hProcess: THandle; //要写入内存数据的目标进程句柄 const lpBaseAddress: Pointer; //要写入的目标进程的内存指针, 需以 VirtualAllocEx() 来申请 lpBuffer: Pointer; //要写入的数据 nSize: DWORD; //写入数据的大小 var lpNumberOfBytesWritten: DWORD //实际写入的大小 ): BOOL; 然后就可以调用 CreateRemoteThread 建立远程线程调用 LoadLibrary 函数来加载我们指定的DLL. CreateRemoteThread() //在一个远程进程中建立线程 function CreateRemoteThread( hProcess: THandle; //远程进程的句柄 lpThreadAttributes: Pointer; //线程安全描述字,指向SECURITY_ATTRIBUTES结构的指针 dwStackSize: DWORD; //线程栈大小,以字节表示 lpStartAddress: TFNThreadStartRoutine; // 一个TFNThreadStartRoutine类型的指针,指向在远程进程中执行的函数地址 lpParameter: Pointer; //传入参数的指针 dwCreationFlags: DWORD; //创建线程的其它标志 var lpThreadId: DWORD //线程身份标志,如果为0, 则不返回 ): THandle; 整个远程注入DLL的具体实现代码如下: function InjectDll(const DllFullPath: string; const dwRemoteProcessId: Cardinal): Boolean; var hRemoteProcess, hRemoteThread: THandle; pszLibFileRemote: Pointer; pszLibAFilename: PwideChar; pfnStartAddr: TFNThreadStartRoutine; memSize, WriteSize, lpThreadId: Cardinal; begin Result := false; // 调整权限,使程序可以访问其他进程的内存空间 if EnableDebugPriv then begin //打开远程线程 PROCESS_ALL_ACCESS 参数表示打开所有的权限 hRemoteProcess := OpenProcess(PROCESS_ALL_ACCESS, false, dwRemoteProcessId); try // 为注入dll文件路径分配内存大小,由于为WideChar,故要乘2 GetMem(pszLibAFilename, Length(DllFullPath) * 2 + 1); // 之所以要转换成 WideChar, 是因为当DLL位于有中文字符的路径下时不会出错 StringToWideChar(DllFullPath, pszLibAFilename, Length(DllFullPath) * 2 + 1); // 计算 pszLibAFilename 的长度,注意,是以字节为单元的长度 memSize := (1 + lstrlenW(pszLibAFilename)) * SizeOf(WCHAR); //使用VirtualAllocEx函数在远程进程的内存地址空间分配DLL文件名空间 pszLibFileRemote := VirtualAllocEx(hRemoteProcess, nil, memSize, MEM_COMMIT, PAGE_READWRITE); if Assigned(pszLibFileRemote) then begin //使用WriteProcessMemory函数将DLL的路径名写入到远程进程的内存空间 if WriteProcessMemory(hRemoteProcess, pszLibFileRemote, pszLibAFilename, memSize, WriteSize) and (WriteSize = memSize) then begin lpThreadId := 0; // 计算LoadLibraryW的入口地址 pfnStartAddr := GetProcAddress(LoadLibrary('Kernel32.dll'), 'LoadLibraryW'); // 启动远程线程LoadLbraryW,通过远程线程调用创建新的线程 hRemoteThread := CreateRemoteThread(hRemoteProcess, nil, 0, pfnStartAddr, pszLibFileRemote, 0, lpThreadId); // 如果执行成功返回 True; if (hRemoteThread 0) then Result := true; // 释放句柄 CloseHandle(hRemoteThread); end; end; finally // 释放句柄 CloseHandle(hRemoteProcess); end; end; end; 接下来要说的是如何卸载注入目标进程中的DLL,其实原理和注入DLL是完全相同的,只是远程调用调用的函数不同而已,这里要调用的是FreeLibrary,代码如下: function UnInjectDll(const DllFullPath: string; const dwRemoteProcessId: Cardinal): Boolean; // 进程注入和取消注入其实都差不多,只是运行的函数不同而已 var hRemoteProcess, hRemoteThread: THandle; pszLibFileRemote: PChar; pszLibAFilename: PwideChar; pfnStartAddr: TFNThreadStartRoutine; memSize, WriteSize, lpThreadId, dwHandle: Cardinal; begin Result := false; // 调整权限,使程序可以访问其他进程的内存空间 if EnableDebugPriv then begin //打开远程线程 PROCESS_ALL_ACCESS 参数表示打开所有的权限 hRemoteProcess := OpenProcess(PROCESS_ALL_ACCESS, false, dwRemoteProcessId); try // 为注入dll文件路径分配内存大小,由于为WideChar,故要乘2 GetMem(pszLibAFilename, Length(DllFullPath) * 2 + 1); // 之所以要转换成 WideChar, 是因为当DLL位于有中文字符的路径下时不会出错 StringToWideChar(DllFullPath, pszLibAFilename, Length(DllFullPath) * 2 + 1); // 计算 pszLibAFilename 的长度,注意,是以字节为单元的长度 memSize := (1 + lstrlenW(pszLibAFilename)) * SizeOf(WCHAR); //使用VirtualAllocEx函数在远程进程的内存地址空间分配DLL文件名空间 pszLibFileRemote := VirtualAllocEx(hRemoteProcess, nil, memSize, MEM_COMMIT, PAGE_READWRITE); if Assigned(pszLibFileRemote) then begin //使用WriteProcessMemory函数将DLL的路径名写入到远程进程的内存空间 if WriteProcessMemory(hRemoteProcess, pszLibFileRemote, pszLibAFilename, memSize, WriteSize) and (WriteSize = memSize) then begin // 计算GetModuleHandleW的入口地址 pfnStartAddr := GetProcAddress(LoadLibrary('Kernel32.dll'), 'GetModuleHandleW'); //使目标进程调用GetModuleHandleW,获得DLL在目标进程中的句柄 hRemoteThread := CreateRemoteThread(hRemoteProcess, nil, 0, pfnStartAddr, pszLibFileRemote, 0, lpThreadId); // 等待GetModuleHandle运行完毕 WaitForSingleObject(hRemoteThread, INFINITE); // 获得GetModuleHandle的返回值,存在dwHandle变量中 GetExitCodeThread(hRemoteThread, dwHandle); // 计算FreeLibrary的入口地址 pfnStartAddr := GetProcAddress(LoadLibrary('Kernel32.dll'), 'FreeLibrary'); // 使目标进程调用FreeLibrary,卸载DLL hRemoteThread := CreateRemoteThread(hRemoteProcess, nil, 0, pfnStartAddr, Pointer(dwHandle), 0, lpThreadId); // 等待FreeLibrary卸载完毕 WaitForSingleObject(hRemoteThread, INFINITE); // 如果执行成功返回 True; if hRemoteProcess 0 then Result := true; // 释放目标进程中申请的空间 VirtualFreeEx(hRemoteProcess, pszLibFileRemote, Length(DllFullPath) + 1, MEM_DECOMMIT); // 释放句柄 CloseHandle(hRemoteThread); end; end; finally // 释放句柄 CloseHandle(hRemoteProcess); end; end; end;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值