另一个趋势是捆绑可执行文件,这些可执行文件是 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 进行分类,包括其版本信息、预期签名信息等。该信息可用于搜寻,例如通过查询声明特定发布者或版本信息但具有可疑修改的哈希值或缺少签名的文件。
在主流操作系统中,应用程序声明动态库依赖项的惯用方法是通过其标头中的某种静态编译数据,例如 Windows 操作系统过去使用的 PE 格式中包含的导入表。这些格式很简单,只允许开发人员命名他们想要加载的库,而不需要进行任何额外的验证。从那里开始,操作系统会处理所有事情并指示允许劫持的行为——标准搜索顺序和加载具有正确名称的第一个库,而无需任何进一步的验证。
useopenssl::x509::X509;useopenssl::sign::Verifier;useopenssl::pkey::PKey;useopenssl::sign::Signer;useopenssl::rsa::Rsa;useopenssl::hash::MessageDigest;usestd::fs;uselibloading::{Library, Symbol};useclap::{Command, Arg};useanyhow::{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