专栏名称: 愿做一名渗透小学徒
分享渗透,安服方面的知识,从浅到深,循序渐进。在渗透的路上,让我们从学徒出发。 此公众号提供的任何工具仅供实验使用,如用于其它用途使用,本公众号概不承担任何责任。
目录
相关文章推荐
内蒙古政府办公厅  ·  王莉霞在自治区国资委调研助企行动开展情况时强 ... ·  昨天  
中国电信  ·  距2026年春节还有376天,开工大吉! ·  昨天  
国资小新  ·  新春走基层 见证新动能 | ... ·  2 天前  
国资小新  ·  新春走基层 见证新动能 | ... ·  2 天前  
中国能建  ·  家乡的腔调,安排! ·  3 天前  
51好读  ›  专栏  ›  愿做一名渗透小学徒

十年未解之痛:DLL劫持的暗夜,我们该如何破局?

愿做一名渗透小学徒  · 公众号  ·  · 2024-09-26 12:10

正文




01
DLL介绍


DLL 劫持是一种强制合法应用程序运行恶意代码的技术,至少已经使用了十年左右。在本文中,我们简要介绍了 DLL 劫持技术,然后总结了 MITRE 在过去十年中记录的数十种该技术的使用情况。重点包括被滥用的特定可执行文件、有关劫持实施具体方式的统计数据,以及一些涉及的恶意 DLL 的内部结构。然后,我们讨论了应用程序开发人员可以使用哪些工具来防止恶意行为者以这种方式滥用其合法应用程序,并为其中一种工具提供了一个概念验证,该工具可以利用数字签名的部分功能,而无需与证书颁发机构打交道。

02
什么是DLL劫持

各种来源对“DLL 劫持”以及相关术语“DLL 侧载”的定义不同。这两个术语的不同定义部分重叠,可能会造成一些混淆。例如,MITRE认为侧载“利用加载程序使用的 DLL 搜索顺序,将受害应用程序和恶意负载并排放置”,而信息安全公司 Mandiant 至少在一份报告中将DLL 侧载定义为 WinSxS 的滥用,具体如下:

“Dll 侧加载 [..] 从 SxS 列表 [在清单中] 加载恶意 DLL,并将其作为 XML 数据嵌入可执行文件中 [..] [这] 旨在让开发人员能够灵活地更新二进制文件,通过在同一位置轻松替换旧二进制文件,[但] 对加载的 DLL 几乎没有验证。”

在本文中,我们将“DLL 劫持”定义为任何滥用良性可执行文件的动态库依赖项的执行流劫持技术,无论这些依赖项是在某种可执行文件清单中声明的还是在运行时加载的。这种技术至少从 2013 年就开始出现;上文提到的 Mandiant 报告指出,2013 年的一次鱼叉式网络钓鱼攻击针对的是中国政治权利活动家,该攻击利用 Windows ActiveX 控件中的漏洞 (CVE-2012-0158) 从 Office 2003 Service Pack 2 更新中删除良性可执行文件,然后使其加载恶意 DLL。从那时起,以 DLL 劫持为特征的攻击链就一直稳步发展 - 主要由国家支持的行为者(例如Lazarus Group和Tropic Trooper)使用,偶尔也被网络犯罪行业使用,与QBot 信息窃取程序和Dridex 银行木马等结合使用。


03
DLL劫持的目的

DLL 劫持的三个主要用例是逃避、持久性和权限提升。

  • 逃避检测 可能是因为侧载 DLL 将作为最初源自良性可执行文件的进程映像的一部分运行。乍一看,该进程似乎不那么可疑;在某些病态情况下,它甚至可能在某种允许列表中,从而免于审查。根据进程的声誉而不是行为来判断进程的安全过滤器可能会将被劫持的进程错误地归类为良性,而事实并非如此。这是一个一般原则的例子:当你信任某物(或某人)时,你不仅需要担心它会故意背叛你,还需要担心它会被操纵和混淆。

  • 如果在受害系统正常运行期间定期执行良性可执行文件,则可能导致持久性 。作为攻击者,自然的想法是使用在启动时自动启动的程序,但稍加思考就会发现,针对受害机器上的默认 Web 浏览器或其他一些常用软件也可以奏效。

  • 如果良性可执行文件具有普通进程所不具备的权限,则可以实现权限提升 。首先想到的例子是管理员权限:正如微软所说,“UAC 中的同桌面提升不是安全边界”;DLL 劫持是攻击者滥用这一事实的一种方式。在其他情况下,某些软件可能会在文件、驱动程序和其他对象周围实施临时安全边界,只有特定进程才允许读取或修改。劫持这些特定进程将允许绕过该限制。

为了了解 DLL 劫持的现状,我们分析了不同活动对此技术的几十种使用,MITRE 将其归类为“DLL 侧加载”,其中包括所使用的具体劫持技术,例如恶意 DLL 的位置、加载方式、滥用了哪些良性可执行文件,以及恶意 DLL 的构建方式的内部位和字节。

到目前为止,这些活动中记录的最常见策略是将已知的良性应用程序和恶意 DLL 捆绑在一起,然后将它们放在同一个文件夹中并执行良性应用程序。超过一半的受访活动使用了这种技术。我们在下表中提供了这些劫持实例以及每个实例中被滥用的良性可执行文件。


图 1. 通过可执行文件和恶意 DLL 的“简单捆绑”进行 DLL 劫持的案例示例


该表格中第一个引人注目的特征是攻击者对“听起来可信”的应用程序的迷恋:Google、Microsoft、Adobe。毕竟,攻击者没有防御者如何行动的精确威胁模型,但也许他们认为,如果他们滥用这些知名供应商的应用程序,他们就能获得优势。当处理具有广泛安装基础的流行应用程序时,防御者自然会更加担心误报(根据传说,在遥远的过去,“受信任”的应用程序和协议通常完全免于检查)。基本上,防御者越重视可执行文件的信誉,这种技术就越有价值。

如果我们抛开那些备受推崇和广受欢迎的应用程序的持续滥用,剩下的数据中还是有一些小趋势的。首先是 AV 产品被反复滥用,但另一个令人好奇的现象是应用程序被滥用,而不太在意它们的状态或来源,这导致了如下场景:

  • Proofpoint 报告称, 2016 年发生的一次攻击滥用了 AV 产品 Norman Safeground。该公司两年前被 AVG收购,而 AVG 又在攻击发生时被 Avast Software 收购;原始产品被纳入更名后的 AVG 产品。

  • Forcepoint描述的另一起 2016 年攻击滥用了 Java 6 运行时中的一个文件 — java-rmi.exe — 一个被错误编译到 Java 运行时中的二进制文件,该文件的存在自 2007 年以来一直被视为一个错误。2013 年,Java 6正式终止使用;然后在 2015 年,有关存在的错误报告 java-rmi.exe 得到解决,该文件从 Java 的所有未来版本中删除。尽管如此,攻击者在一年后仍毫无顾忌地打包该文件并滥用它来加载恶意 DLL。

另一个趋势是捆绑可执行文件,这些可执行文件是 Windows 操作系统的一部分,或者在受害者机器文件中非常常见。这可以在攻击链滥用Windows 凭据备份和恢复向导或RFS重新密钥向导的副本的活动中看到。

Red Canary 的Dridex 报告似乎认为,过于关注特定的被滥用的良性可执行文件可能会带来不利影响:

“除了最初的交付之外,我们观察到 Dridex 全年使用的最常见技术之一是劫持各种合法 Windows 可执行文件的 DLL 搜索顺序。Dridex 操作员在进行搜索顺序劫持时不会坚持使用单个 Windows 可执行文件,因此需要进行多次检测分析才能捕获此行为。”

一个自然而然的问题是如何寻找易于被动态库劫持的可执行文件。一个众所周知的可靠技巧是通过进程监视工具运行可执行文件,并专门监视某个位置不存在的 DLL 文件的查找失败事件。这表明有人可以在那里插入他们自己的恶意版本供应用程序查找。这个审查程序可以使用例如 ProcMon、过滤器 Path ends with .dll Result is NAME NOT FOUND 或一些等效程序来完成。不幸的是,这种方法的扩展性并不好,即使存在一些自动化方法,比如Spartacus 项目。特定情况可能易于利用 EDR 遥测进行搜索——例如,过滤进程从同一目录加载缺少已知正确签名的 DLL 的情况,如此处所建议的。并非所有动态库劫持都满足这些条件,但很多都满足。最后,另一种选择是利用hijacklibs存储库中提供的优秀资源。这些目录对可劫持的 DLL 进行分类,包括其版本信息、预期签名信息等。该信息可用于搜寻,例如通过查询声明特定发布者或版本信息但具有可疑修改的哈希值或缺少签名的文件。


04
恶意DLL的技术剖析

我们研究了劫持中使用的一些恶意制作的 DLL 的内部汇编,并确定了技术主题和模式。例如,对 DLL 使用混淆工具(“打包程序”、“加密程序”)的现成支持较少。通常情况下,当威胁行为者认为某些代码或数据没有达到应有的混淆程度时,他们会使用 XOR 循环。


图 2. 恶意 DLL 中使用的反混淆例程。


上图所示的特定 DLL 是恶意制作的 dbgeng.dll 和 版本,导出相对较少。如果仔细观察,您会发现,在底层,其中两个( DebugConnect DebugCreate )实际上指向同一个函数。


图 3. DebugConnect 和 DebugCreate 导出指向同一地址。



图 4. DLL 中两个导出指向的函数是一个调用恶意逻辑的简短存根。


如果这看起来很奇怪,那么就有一个恶意制作的版本 jli.dll ,其中不只是 2 个函数 — — 几乎 每个 函数都指向恶意代码:


图 5. 所有 JLI_ 导出都指向调用攻击者制作的逻辑的同一个存根。


恶意制作的版本 lbtserv.dll 也有许多指向同一目标的导出。它们不是指向恶意代码,而是全部指向空函数存根。您可以自行判断哪个看起来更可疑:


图 6. 所有 LGBT_ 导出都指向同一个函数存根。



图 7. 这是一个不执行任何操作的空存根。


最后,恶意精心制作的 version.dll 包含微妙的调用 vresion 而不是 version


05
防止DLL劫持的开发人员工具

在本节中,我们将深入探讨 应用程序开发人员 可用的预防工具和方法,以防止恶意行为者使用此技术成功滥用他们的应用程序。

在主流操作系统中,应用程序声明动态库依赖项的惯用方法是通过其标头中的某种静态编译数据,例如 Windows 操作系统过去使用的 PE 格式中包含的导入表。这些格式很简单,只允许开发人员命名他们想要加载的库,而不需要进行任何额外的验证。从那里开始,操作系统会处理所有事情并指示允许劫持的行为——标准搜索顺序和加载具有正确名称的第一个库,而无需任何进一步的验证。

需要明确的是,解决此类问题所需的技术是存在的。人们很容易开始考虑空想的系统改革:如果每个人都对其 DLL 进行数字签名,如果每个可执行文件都知道它试图加载的 DLL 是谁的,并验证签名……当然,没有什么比空想的系统改革真正发生并让一整类安全问题消失更好的了,但在此之前,我们必须使用我们现有的适度工具来处理当前未改革世界中的问题。

由于通过可执行头声明依赖项会立即允许劫持,因此在开发人员级别处理此问题似乎需要在运行时加载所有可能的库。当然,如果您调用,这只会再次调用通常的搜索顺序。这里 LoadLibrary("some.dll") 记录了一种适用于 Windows 的快速而肮脏的解决方法,即让应用程序首先调用。这会从 DLL 搜索路径中删除当前工作目录,因此如果 确实 需要从当前目录加载任何 DLL,则必须获取其完全限定路径并明确提供给它。相关的黑客是调用。这会将当前目录移到搜索顺序的底部。如果您足够偏执,担心在当前目录之外的搜索顺序中的某处存在恶意制作的 DLL,则可能希望在所有对的调用中使用完全限定路径,或者使用允许您指定可以从哪些库加载 DLL。 SetDllDirectory("") LoadLibrary SetSearchPathMode (BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE | BASE_SEARCH_PATH_PERMANENT) LoadLibrary LoadLibraryEx

不幸的是,即使你控制了 DLL 的加载位置,仍然有足够的空间被劫持。其根本原因是,加载的库在文件系统中的位置并不能保证任何事情。虽然很可能没有人会直接篡改 C:\Windows\System32\ws2_32.dll ,但如果加载的库是应用程序定制的,并且通常从当前目录加载,那就另当别论了。在这种情况下,基于路径的验证将无法在同一目录中捕获良性应用程序和恶意库的典型恶意“捆绑包”。毕竟,恶意库正好位于应用程序期望的位置。

要解决 这个 问题, 需要处理数字签名或等效解决方案 。显然,操作系统提供了一些便利措施来做到这一点(例如, LoadLibraryEx 有一个标志要求对目标 DLL 进行签名 — LOAD_LIBRARY_REQUIRE_SIGNED_TARGET ),但作为开发人员,这似乎仍然是一个重大障碍,需要花费大量时间和资源在证书颁发机构进行注册。幸运的是,可以找到一种解决方法。创建 DLL 时,您可以使用自签名证书中的私钥对其进行签名,然后将证书与 DLL 一起发布。从可执行文件加载该 DLL 的任何人(包括您)都可以首先验证证书链是否在内部签出。

请注意 ,攻击者仍然可以制作自己的恶意可执行文件版本(这可能是微软在介绍 .NET 的类似功能“强名称程序集”时表示“不要依赖强名称来确保安全。它们仅提供唯一身份”的原因之一);但伪造的可执行文件将具有未知的哈希值,并且不会享有原始可执行文件的良好声誉。另 请注意 ,替换证书将破坏与以前编译的可执行文件和 DLL 的兼容性。

为了演示其工作原理,我们在下面提供了一个概念验证程序,该程序可以 sample.dll 使用非常简化的自制 Authenticode 对应程序对简单的 DLL 进行签名 — 它使用 OpenSSL 计算整个文件内容的签名,然后将签名添加为覆盖。 frontloaded.exe 然后,一个单独的可执行文件 ( ) 对签名的 DLL ( ) 执行安全加载 sample.dll ,首先提取 DLL 签名并验证它,然后才正确加载 DLL。如果签名验证失败,则可执行文件会崩溃(抛出异常)。


图 9。良性可执行文件嵌入了自签名证书,与之关联的私钥已用于对原始 DLL 进行签名,并将生成的签名添加为覆盖层(绿色)。 secure_load 可执行文件中的函数在加载 DLL 之前会验证签名(蓝色)。精心设计的恶意 DLL 将无法通过此验证,并会被拒绝 secure_load (红色)。


用于签名程序的 Rust 代码如下。

use openssl::x509::X509;use openssl::sign::Verifier;use openssl::pkey::PKey;use openssl::sign::Signer;use openssl::rsa::Rsa;use openssl::hash::MessageDigest;use std::fs;use libloading::{Library, Symbol};use clap::{Command, Arg};use anyhow::{Result,anyhow};
const SIG_LEN : usize = 512;
pub fn secure_load_library(cert: &str, dll_name: &str) -> Result { match verify_file(cert,dll_name)? { true => unsafe { Ok(Library::new(dll_name)?) }, false => Err(anyhow!("Signature verification failed")) }}
pub fn sign(pem_key: &str, data: &[u8]) -> Result> { let pkey = Rsa::private_key_from_pem(pem_key.as_bytes()) .and_then(|x| PKey::from_rsa(x))?; let signature = Signer::new(MessageDigest::sha256(), &pkey) .and_then(|mut x| {x.update(data)?; Ok(x)}) .and_then(|x| {x.sign_to_vec()})?; Ok(signature)}
pub fn verify(cert: X509, data: &[u8], sig: &[u8]) -> Result { let public_key = cert.public_key()?; let verification_status = Verifier::new(MessageDigest::sha256(), &public_key) .and_then(|mut x| {x.update(data)?; Ok(x)}) .and_then(|x| x.verify(sig))?; Ok(verification_status)}
pub fn sign_file(key_path: &str, fname: &str, fname_new: &str) -> Result { let data = fs::read(fname)?; let sig = sign( &fs::read_to_string(key_path)?, &data)?; let signed_data : Vec = data.into_iter().chain(sig.into_iter()).collect(); fs::write(fname_new, signed_data)?; Ok(())}
pub fn verify_file(cert: &str, fname: &str) -> Result { let cert = X509::from_pem(cert.as_bytes())?; let mut data = fs::read(fname)?; let sig = data.split_off(data.len()-SIG_LEN); verify(cert, &data, &sig)}

CLI:

fn main() -> Result {    let matches = Command::new("Signing Tool")        .version("1.0")        .about("Simple homebrew implementation of file signing using an overlay.")        .arg(            Arg::new("source_path")                .short('s')                .long("source")                .value_name("SOURCE_PATH")                .help("Path to the unsigned file")                .required(true)        )        .arg(            Arg::new("target_path")                .short('t')                .long("target")                .value_name("TARGET_PATH")                .help("Signed file will be written to this path.")                .required(true






请到「今天看啥」查看全文