はじめに

本記事はSecurity Camp Online 2020のLTで発表したスライドを元に書いています.

内容が古い上,検証時の私の実力も現在には到底満たないものです.

大目に見てください.

Agenda

fileless_malware

maliciousな実行ファイルが証跡として直接残らない為,ディスクベースのanti malwareでは検知しづらい.

example

Invoke mimikatz

mimikatzをbase64でエンコードし,Invoke-Commandを用いてメモリ上に展開,実行するマルウェアのテクニックの一つです.

mimikatzのバイナリが直接ファイルとして残らない為,検知しづらかったようです.

Linux

Linuxでも,ファイルを作成せずに実行することはできます.

exec系の関数の一つであるfexecvememfd_createというシステムコールを使う.

memfd_create

anonymous fileという形式のファイルを生成し,そのファイルディスクリプタを返すシステムコールです.

作成されたファイルはメモリ上にのみ置かれ,参照するfdが無くなると消えます.

fexecve

execve(2)と異なるのは実行するファイルのパスではなくfdを受け取る点です.

detection

LD_PRELOAD

リンカ(ld)が実行ファイルに動的にリンクされたライブラリの解決を行う際に優先的に使用されるパスを指定する為の環境変数です.

自作のshared objectを渡す事で,printfを始めとする既存のライブラリ関数を再コンパイル無しで乗っ取る事ができます.

sample

検出させる為のサンプルとして

  1. memfd_create
  2. write
  3. 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

demo

bypass

検知ができたら,次はそのバイパスです.

これは簡単で,LD_PRELAODで置き換えられるのはライブラリ関数だけなので,システムコールを直接叩きます.

thank you

マルウェアの検知とその回避といういたちごっこみたいな事を一人でできて楽しかったです.

repo


この記事はIPFactory Advent Calendar 2021の12/03分です.

IPFactoryというサークルについてはこちらをご覧ください.

明日はy0d3nによる,なにかです.