首页
社区
课程
招聘
[原创]windows攻防对抗-dll侧载篇
2024-5-18 01:23 6476

[原创]windows攻防对抗-dll侧载篇

2024-5-18 01:23
6476

介绍

最近来聊一聊在实战攻防对抗中比较常用的技术——dll侧载。从功能实现上来讲,该技术的实现就是倚托进程的白签名,通过劫持白进程的执行流来达到恶意代码执行的目的。但在实际应用的过程中会有一些坑点和骚操作的利用手法。而且作为安全产品对dll侧载的检测也没有很好的手段,一般都是类似设立黑名单的机制,无法对未知的样本进行一个有效的防御。可能对安全产品来讲也许是一个老生常谈的问题,设置强规则(诸如dll签名啊,文件落地啊等)就会产生大量误报,很难找到一个权衡误报与真实告警事件的一个边界。。。跑远了,这不是这篇文章该讨论的问题。

前置知识

动态库的加载方式

隐式加载

隐式加载动态库(也称为静态加载或预加载)是指在程序启动时,操作系统加载器自动加载程序所依赖的动态链接库。这种加载方式是在编译时确定的,因为依赖的库会在程序的链接阶段被指定。
隐式加载的优点是使用简单,程序启动时按照系统中一定的搜索顺序自动加载所有依赖,不需要在代码中进行额外的加载操作。缺点是程序启动时会加载所有依赖的库,即使这些库中的一些函数可能永远不会被调用,这可能会导致额外的内存消耗。此外,如果依赖的库在系统上不存在,程序将无法启动。
从PE文件格式对应的就是导入表,注意,导入表内的动态库在程序初始化的时候会被加载进内存,如果没找到就会报错。但动态库里面的导出函数并不一定会被调用。

显示加载

与隐式加载相对的是显式加载(也称为动态加载或运行时加载),这是指程序在运行时根据需要动态地加载和卸载动态链接库。在Windows上,这通常通过使用 LoadLibrary和GetProcAddress函数来实现。显式加载允 许程序更加灵活地控制何时加载库,以及如何处理库不可用的情况。

动态库的加载顺序


● 检测dll是否已被加载进内存
● 检测加载的dll是否在Known DLLs内
● 检测应用当前目录下是否存在dll
● 检测System32目录下是否存在dll
● 检测当前执行目录下是否存在dll
● 检测%PATH%环境变量下是否存在dll
其实在大多数的利用场景中(银狐黑灰,攻防演练,apt等)可以总结出,大部分利用都是依靠应用当前目录来做dll侧载的。
这里再介绍下Known DLLs
Known DLLs标记的dll默认会从system32路径下加载,注册表位置
\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs

动态库被加载的执行点

dllmain

参考文章:https://elliotonsecurity.com/perfect-dll-hijacking/
当动态库被加载时,会执行动态库中的dllmain函数。但当程序进入dllmain函数时,会被施加一个锁的状态,该锁的存在就是微软为了限制dllmain的行为做了一些安全限制。

后面就引用原文了
You should never perform the following tasks from within DllMain:
● Call LoadLibrary or LoadLibraryEx (either directly or indirectly). This can cause a deadlock or a crash.
● Call GetStringTypeA, GetStringTypeEx, or GetStringTypeW (either directly or indirectly). This can cause a deadlock or a crash.
● Synchronize with other threads. This can cause a deadlock.
● Acquire a synchronization object that is owned by code that is waiting to acquire the loader lock. This can cause a deadlock.
● Initialize COM threads by using CoInitializeEx. Under certain conditions, this function can call LoadLibraryEx.
● Call the registry functions.
● Call CreateProcess. Creating a process can load another DLL.
● Call ExitThread. Exiting a thread during DLL detach can cause the loader lock to be acquired again, causing a deadlock or a crash.
● Call CreateThread. Creating a thread can work if you do not synchronize with other threads, but it is risky.
● Call ShGetFolterPathW. Calling shell/known folder APIs can result in thread synchronization, and can therefore cause deadlocks.
● Create a named pipe or other named object (Windows 2000 only). In Windows 2000, named objects are provided by the Terminal Services DLL. If this DLL is not initialized, calls to the DLL can cause the process to crash.
● Use the memory management function from the dynamic C Run-Time (CRT). If the CRT DLL is not initialized, calls to these functions can cause the process to crash.
● Call functions in User32.dll or Gdi32.dll. Some functions load another DLL, which may not be initialized.
● Use managed code.
换算成攻击者可以理解场景就是你无法在dllmain中完成C2的上线。(具体的利用方式后面介绍)

导出函数

通过重写动态链接库中的导出函数劫持可执行程序的劫持流是一个非常有效的手段。但前提是可执行程序确实会调用你的导出函数。这个的利用场景也很多。

隐式加载的劫持方式

OEP劫持

对于需要隐式加载的dll,由于其加载过程由系统接管。即隐式加载的dll是在exe主体模块加载过程中进来的,执行顺序上是先通过PE文件的导入表加载隐式的dll,然后再运行EXE的入口点。玩过runpe的应该知道pe在内存中的装载流程中,会填充iat,而iat的填充就是需要动态链接库里面的函数地址。所以导入表内的动态链接库会在可执行文件初始化的过程中被装载进内存,从而触发dllmain函数的执行。
具体利用方式参考:https://github.com/9bie/iatHijackGenerate (很粗暴)
伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        MODULEINFO moduleInfoe;
        SIZE_T bytesWritten;
        GetModuleInformation(GetCurrentProcess(), GetModuleHandle(NULL), &moduleInfoe, sizeof(moduleInfoe));
        unsigned char shellcode[] = { xx, xx...}
        int shellcode_size = xx;
        HANDLE currentProcess = GetCurrentProcess();
        WriteProcessMemory(currentProcess, moduleInfoe.EntryPoint, (LPCVOID)&shellcode, shellcode_size, &bytesWritten);
    }
    else if (dwReason == DLL_PROCESS_DETACH){}
    return TRUE;
}

● 缺点:直接覆盖OEP,如果使用stageless(极大的shellcode),有可能覆盖到其他区段,导致PE文件执行出现问题。
● 改良方案:申请新内存,将shellcode代码拷贝过去,修改oep处代码跳转执行或找到E8、E9指令,修改偏移地址劫持执行流

导入函数劫持

应用程序运行时,调用了恶意动态链接库中的导出函数,从而劫持程序流。多场景中都较为常见。以最简单的version.dll为例



显示加载的劫持方式

导入函数劫持

由于显示加载是程序运行时通过LoadLibrary动态加载进内存,所以即使通过dllmain修改oep也难以劫持程序执行流。但导入函数劫持依旧可行,程序会通过GetProcAddress获取需要调用的函数地址进行调用。该方式的利用较隐式加载中的函数劫持稍微隐蔽了一些,但也没啥技术含量。




ghost dll hijacking

参考文章:https://itm4n.github.io/windows-dll-hijacking-clarified/

在Windows的不同系统版本上,存在着某些服务加载动态库失败的情况。加载失败的原因就是dll不存在了。程序按照右图顺序查找动态链接库当Application目录没有时,将继续在其他 Windows 目录中进行搜索。如果仍然找不到,它会尝试从当前目录加载。如果还是找不到,它最终会在 %PATH% 环境变量中查找。%PATH%的环境变量给了我们机会。
注意:不同的系统版本可利用此方式劫持的动态库不同。

挖掘思路

第一步:既然是LoadLibrary函数加载失败,首先肯定是hook LoadLibrary函数,保存原始LoadLibrary函数的参数,并判断是否加载成功。将加载失败的结果保存到磁盘。
第二步:选择一种在开机启动时可以动态全局hook所有三环进程的方式。
方案一:从内核做,由于PG的存在,想要在内核做模块加载的监控,首选肯定是LoadImage内核回调,但又大概率认为三环模块加载失败时是不会触发LoadImage的内核回调(可恶,应该去测一下。插一嘴,这个点对后续绕LoadImage回调触发调用栈检测给了我不一样的思路。我就不再这里赘述了)。后续和其他大哥们聊天知道了很多安全产品从零环做三环UMH(User Mode Hook)的思路与接触了InfinityHook拦截系统调用等方式,后续有时间开帖子讲。
方案二:在三环通过Appinit方式注入dll从而hook LoadLibrary函数。方案可行,但有缺陷。凡是加载appinit注册表中dll的程序前提是会加载user32.dll。(这个点在我做测试的时候是没有想到的),所以依靠三环还是无法做到全局hook所有进程的api调用,欢迎师傅们提供三环全局hook的思路。
来看一下方案二实现的伪代码

并将编写好的动态链接库文件放到指定位置,修改注册表
64位 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs
32位 HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs


重启机器等待进程的加载

查看日志,找到可以利用的dll,伪造dll并写入系统环境变量中


通用dllmain利用

利用难题

回到dllmain的讨论。打攻防的兄弟肯定遇到过的情况,dllmain中起C2的shellcode会发现C2无论如何也上不了线。我也简单做了测试。

  • 链接库代码
  • 加载器代码

    只展示测试的一种方案。线程没有启动。后续与朋友的共同测试中也会发现(远程dll注入的方式),有时线程启动了但是卡在shellcode发送请求的步骤。这个有兴趣的朋友可以参考https://elliotonsecurity.com/perfect-dll-hijacking/ 中的详细原因,这点在上文前置知识中也有提及。

解决思路

参考文章:https://elliotonsecurity.com/perfect-dll-hijacking/
第一种解决思路肯定是参考文章所述通过一定方式去解锁,不详细赘述。
再来说说第二种思路——覆盖返回地址。这里感谢wwh同学给予的帮助。
既然程序在退出前会进行解锁,那我们不必纠结于在dllmain解锁的问题,我们直接劫持栈上的返回地址。其中的难点就是在dllmain中遍历自己的栈空间,找到应用模块的返回地址进行覆盖。随后便完成劫持,等待函数返回即可。
这里附上遍历调用栈的逻辑。

  • 演示效果



    通过这种方式可以极大的扩大劫持面,增加dll侧载的攻击广度。

总结

dll侧载并不是新鲜的技术,现在也在各种攻防演练、黑灰产、apt等攻击场景中多有涉猎。本篇文章从攻击视角给出了dll侧载的挖掘与攻击思路。后续也会持续更新windows攻防对抗的其他篇章,比如cobaltstrike攻防篇、byovd致盲篇、域渗透篇等。希望与各位师傅们互相学习,探讨攻防技术。


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 8
打赏
分享
最新回复 (5)
雪    币: 378
活跃值: (3020)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
appview 2024-5-24 10:18
2
0
一行代码修改返回地址:
*(size_t *)_AddressOfReturnAddress() = (size_t)ExecutionFlowHijackTrampoline;
雪    币: 3499
活跃值: (725)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
F4our444 1 2024-5-24 14:42
3
0
appview 一行代码修改返回地址: *(size_t *)_AddressOfReturnAddress() = (size_t)ExecutionFlowHijackTrampoline;
这个需要遍历调用栈,判断返回地址所在模块,根据所在模块来获取真正要被修改的返回地址。而不是直接覆盖当前函数的返回地址
雪    币: 189
活跃值: (168)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wangjietx 2024-5-28 17:37
4
0
就是dll注入技术
雪    币: 4023
活跃值: (5669)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
badboyl 2 2024-5-29 22:05
5
0
AppInit_DLLs 现在都被安全启动了,要从BIOS里关闭。
雪    币: 3499
活跃值: (725)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
F4our444 1 2024-5-29 23:06
6
0
badboyl AppInit_DLLs 现在都被安全启动了,要从BIOS里关闭。
其实appinit只是次选方案,并不能做到全进程hook。我记得好像只有加载user32的进程才会加载appinit注册的dll
游客
登录 | 注册 方可回帖
返回