Detecting fileless execution in Linux
はじめに
本記事はSecurity Camp Online 2020のLTで発表したスライドを元に書いています.
内容が古い上,検証時の私の実力も現在には到底満たないものです.
大目に見てください.
Agenda
fileless_malware
- file less
- evidence less
- detection less
maliciousな実行ファイルが証跡として直接残らない為,ディスクベースのanti malwareでは検知しづらい.
example
Invoke mimikatz
mimikatz
- base64
Invoke-Command
mimikatz
をbase64でエンコードし,Invoke-Command
を用いてメモリ上に展開,実行するマルウェアのテクニックの一つです.
mimikatz
のバイナリが直接ファイルとして残らない為,検知しづらかったようです.
Linux
Linuxでも,ファイルを作成せずに実行することはできます.
exec
familyfexecve
memfd_create
exec
系の関数の一つであるfexecve
とmemfd_create
というシステムコールを使う.
memfd_create
- anonymous file
- on RAM
- volatile
anonymous file
という形式のファイルを生成し,そのファイルディスクリプタを返すシステムコールです.
作成されたファイルはメモリ上にのみ置かれ,参照するfdが無くなると消えます.
fexecve
int fd
char *const argv[]
char *const envp[]
execve(2)
と異なるのは実行するファイルのパスではなくfdを受け取る点です.
detection
LD_PRELOAD
ld
dlsym
shared object
リンカ(ld)が実行ファイルに動的にリンクされたライブラリの解決を行う際に優先的に使用されるパスを指定する為の環境変数です.
自作のshared object
を渡す事で,printf
を始めとする既存のライブラリ関数を再コンパイル無しで乗っ取る事ができます.
sample
検出させる為のサンプルとして
memfd_create
write
fexecve
の順で操作を行う疑似マルウェアを用意しました.
maliciousな挙動は一切無いプログラムですが,ファイルを作成せずに実行しています.
#include <stdio.h>
#include <sys/syscall.h>
#include <linux/memfd.h>
#include <unistd.h>
#include <err.h>
#include <string.h>
#define HELLO "#!/bin/bash\necho \"hello fileless\""
int main() {
int fd;
if ((fd = syscall(SYS_memfd_create, "test", 0)) == -1)
err(1, "memfd_create");
write(fd, HELLO, strlen(HELLO));
if (fexecve(fd, (char *[]){"script", NULL}, (char*[]){NULL}) == -1 )
err(1, "fexecve");
}
memfd_create
で作成したファイルに対して,生のシェルスクリプトを書き込み,実行しています.
(おまけ)Rust版
use nix::sys::memfd;
use nix::unistd;
use std::ffi::CString;
use std::io::Write;
use std::os::unix::io::FromRawFd;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let fd = memfd::memfd_create(&CString::new("").unwrap(), memfd::MemFdCreateFlag::empty())?;
let mut f = unsafe { std::fs::File::from_raw_fd(fd) };
write!(&mut f, "#!/bin/bash\necho \"hello fileless with rust\"").unwrap_or_else(|e| {
eprintln!("{}", e);
std::process::exit(1);
});
unistd::fexecve(
fd,
&[&CString::new("").unwrap()],
&[&CString::new("").unwrap()],
)?;
Ok(())
}
detector
検知する側のコードはこんな感じ
extern crate libc;
#[macro_use]
extern crate redhook;
use env_logger;
use libc::{c_char, c_int};
use log::{error, info, warn};
use std::io::{Read, Write};
use std::{env, fs, process};
hook! {
unsafe fn fexecve(fd: c_int, argv: *mut *mut c_char, envp: *mut *mut c_char) -> c_int => detect_fexecve {
if let Err(_) = env::var("RUST_LOG") {
env::set_var("RUST_LOG", "warn");
}
if let Err(_) = env::var("ACTION") {
env::set_var("ACTION", "dump");
}
env_logger::init();
info!("hook fexecve!");
let path = format!("/proc/self/fd/{}", fd);
match fs::read_link(&path) {
Ok(link) => {
let filename = String::from(link.iter().last().unwrap().to_str().unwrap());
if filename.starts_with("memfd:") {
warn!("detected fileless fexecve!!");
}
match &env::var("ACTION").unwrap()[..] {
"abort" => {
warn!("aborting process");
std::process::abort();
},
"dump" => {
let mut buf = Vec::new();
let mut f = fs::File::open(path).unwrap();
f.read_to_end(&mut buf).unwrap();
let dump = format!("{}.dump", process::id());
warn!("binary dump to -> {}", dump);
let mut f = fs::File::create(dump).unwrap_or_else(|e| { error!("{}", e); process::exit(1); });
f.write_all(&buf).unwrap_or_else(|e| { error!("{}", e); process::exit(1); });
process::exit(0);
},
_ => info!("detected fileless exec"),
}
},
Err(e) => {
error!("error in read_link: {}", e);
}
}
real!(fexecve)(fd, argv, envp)
}
}
hook
hook! {
unsafe fn fexecve(fd: c_int, argv: *mut *mut c_char, envp: *mut *mut c_char) -> c_int => detect_fexecve {
fexecve
を自作の関数に置き換える.
check
let path = format!("/proc/self/fd/{}", fd);
match fs::read_link(&path) {
Ok(link) => {
let filename = String::from(link.iter().last().unwrap().to_str().unwrap());
if filename.starts_with("memfd:") {
warn!("detected fileless fexecve!!");
}
action
match &env::var("ACTION").unwrap()[..] {
"abort" => {
warn!("aborting process");
std::process::abort();
},
"dump" => {
let mut buf = Vec::new();
let mut f = fs::File::open(path).unwrap();
f.read_to_end(&mut buf).unwrap();
let dump = format!("{}.dump", process::id());
warn!("binary dump to -> {}", dump);
let mut f = fs::File::create(dump).unwrap_or_else(|e| { error!("{}", e); process::exit(1); });
f.write_all(&buf).unwrap_or_else(|e| { error!("{}", e); process::exit(1); });
process::exit(0);
},
環境変数ACTION
の内容によって,実行をabortさせたり,実行しようとしているファイルの内容を保存したりできます.
memfd_create
で作成されたファイルは,procfs
上にあるファイルディスクリプタのリンクを読むとmemfd:
がprefixに付いているので,これを元に判断します.
demo
bypass
検知ができたら,次はそのバイパスです.
これは簡単で,LD_PRELAOD
で置き換えられるのはライブラリ関数だけなので,システムコールを直接叩きます.
syscall
asm
- etc…
thank you
マルウェアの検知とその回避といういたちごっこみたいな事を一人でできて楽しかったです.
この記事はIPFactory Advent Calendar 2021の12/03分です.
IPFactoryというサークルについてはこちらをご覧ください.
明日はy0d3nによる,なにかです.
Comments