Skip to main content

jopcall/
syscall.rs

1use core::ffi::c_void;
2use crate::helper::{search_bytes, JopcallError};
3use core::arch::global_asm;
4use core::ptr::slice_from_raw_parts;
5
6/// A struct representing a parsed system call. You are free to modify these if you wanted to do
7/// something like make a syscall to a neighbor ala hell's gate or hall or whichever one does that.
8/// This also tells you if the syscall is hooked (Some windows APIs are by default) if you'd like
9/// to act on that in any way.
10#[repr(C)]
11#[derive(Debug)]
12pub struct Syscall {
13    pub ssn:u16,
14    pub address:*const c_void,
15    pub hooked:bool,
16}
17
18/// A macro which makes it much easier to make an indirect syscall via JOP. You first need to
19/// provide it a slice of gadgets where the first gadget is used to jump to rcx and the rest are
20/// placed on the stack as return values after the syscall. There is a maximum of 5 gadgets. It
21/// then takes a syscall struct of the syscall you want to call, and variadic arguments for the
22/// NtApi arguments that the syscall takes. 
23///
24/// gadget_list format:
25/// 1: something that ends in jmp rcx without clobbering any registers or misaligning the stack 
26/// 2: The address that the syscall returns to 
27/// 3+ Any combination of gadgets you want as long as it ends in a ret
28#[macro_export]
29macro_rules! jopcall {
30    // Passed with gadget array as first argument.
31    ($gadget_list:expr, $syscall:expr $(,$args:expr)*) => {
32        {
33            fn enforce_syscall(value:&$crate::syscall::Syscall)->&$crate::syscall::Syscall{
34                &value
35        }
36            fn enforce_slice(value:&[*const c_void])->&[*const c_void]{
37                if value.len() > 5 {
38                    panic!("Too many gadgets! Provide fewer than 5");
39                }
40                value
41        }
42        
43        let gadget_list = enforce_slice($gadget_list);
44        let gadget_count = gadget_list.len() as u16;
45        let pgadget_list = (*(gadget_list.clone())).as_ptr() as *const c_void;
46        let syscall = enforce_syscall(&$syscall);
47        let ssn = syscall.ssn;
48        let address = syscall.address;
49        let mut arg_count:u16 = 0;
50        $(
51            let arg = $args;
52            arg_count += 1;
53        )*
54        let syscall_count = $crate::syscall::SyscallCount(gadget_count, arg_count);
55        $crate::syscall::isc(pgadget_list, ssn, address, syscall_count, $($args), *)
56        }
57    }
58}
59/// This is a macro to make a syscall without any return address obfuscation. Simply pass the
60/// syscall struct of the syscall you want to call and any arguments it takes.
61#[macro_export]
62macro_rules! syscall{
63    // Passed with no gadget list, so it just jumps directly to the syscall and returns 
64    // as normal
65    ($syscall:expr $(,$args:expr)*) => {
66        {
67        fn enforce_syscall(value:&$crate::syscall::Syscall)->&$crate::syscall::Syscall{
68            &value
69        }
70        let syscall = enforce_syscall(&$syscall);
71        let ssn = syscall.ssn;
72        let address = syscall.address;
73        let pgadget_list = [syscall.address;1];
74        let mut arg_count:u16 = 0;
75        $(
76            let arg = $args;
77            arg_count += 1;
78        )*
79        let syscall_count = $crate::syscall::SyscallCount(1 as u16, arg_count);
80        $crate::syscall::isc(pgadget_list.as_ptr() as *const c_void, ssn, address, syscall_count, $($args), *)
81    }
82}
83}
84/// A macro that makes it easier to build the syscall struct. You pass it the hashed name of a dll
85/// (Almost certainly ntdll.dll) and the name of the specific syscall (NtWhatever or ZwWhatever)
86/// and it will construct the struct for you.
87#[macro_export]
88macro_rules! get_syscall{
89    ($dll_name:expr, $syscall_name:expr) => {
90       $crate::syscall::lookup_syscall($crate::pfunction::get_function_pointer($crate::peb_walk::get_dll_base_address($dll_name).unwrap(), $syscall_name)) 
91    }
92}
93
94
95// Represents a field passed to the syscall (isc) assembly function below.
96// It is formatted this way to allow the counts to be accessible in one 32 bit 
97// value split along the E#X / #X delimeter ( so as to not require another register)
98// This should be abstracted away from the end user by the macro/functions used to call 
99// run_syscall
100// (gadget, arg_count)
101#[repr(C)]
102pub struct SyscallCount(pub u16, pub u16);
103
104/// A raw function to look up a syscall. It takes the memory address of a parsed Nt function and
105/// attempts to find the syscall stub and extract the SSN and syscall address from it by using the
106/// search_bytes function defined elsewhere in the program. If the function call provided isn't a
107/// syscall (For example if it's an RtlWhatever NTApi function) or if it is mangled in some way, it
108/// will return an error. This also looks for hooks and will reflect that in the Syscall struct.
109/// Note that some legitimate syscalls are hooked by default.
110pub unsafe fn lookup_syscall(function_address:*const c_void)->Result<Syscall, JopcallError>{
111
112    // We search for these to avoid EDR hooking. This appears immediately following the ntdll
113    // function call if it's a traditional syscall. Without this, it's either hooked or the end
114    // user typed in something incorrect
115    let ntdll_prefix:&[u8] = &[0x4C, 0x8B, 0xD1, 0xB8];
116    // This is a totally arbitrary number of 36 but it's enough to catch NtQuerySystemTime which is
117    // a naturally hooked "normal" nt syscall so I'm satisfied with it
118    let function_bytes:&[u8] = &*slice_from_raw_parts(function_address as *const u8, 36) as &[u8];
119    // I know that this is strange, but if the index returned by this is 0 (meaning it's right at
120    // the start), it's equivelant to false, otherwise it's true
121    let prefix_offset:usize = match search_bytes(&ntdll_prefix, function_bytes) {
122        Ok(index) => index,
123        Err(e) => {return Err(e)}
124    };
125    
126    // There are some naturally hooked ntdll syscalls such as NtQuerySystemTime, but this should
127    // return on anything that matches the prefix bytes within a size 16 array even if the first
128    // argument is a jmp
129    let hooked = if prefix_offset > 0 {
130        true
131    } else {
132        false
133    };
134
135    // Grabs the next 4 bytes following the ntdll prefix size + the offset to the first byte of the
136    // pattern. If not hooked and in most cases, this will just be next 4 bytes after (function_address+4)
137    let ssn_bytes:&[u8] = &*slice_from_raw_parts(function_address.cast::<u8>().offset((ntdll_prefix.len() + prefix_offset) as isize) as *const u8, 4) as &[u8];
138    
139    // Check to ensure there is a valid SSN format.
140    if ssn_bytes[2] != 0 && ssn_bytes[3] != 0 {
141        return Err(JopcallError::InvalidSSN);
142    }
143    
144    // This is some weird voodoo bullshit shamelessly stolen from hell's gate.
145    let ssn:u16 = (ssn_bytes[1] as u16) << 8 | ssn_bytes[0] as u16;
146 
147    // intel x64 syscall instruction
148    let syscall_instruction:&[u8] = &[0x0F, 0x05];
149    let syscall_offset:usize = match search_bytes(syscall_instruction,function_bytes) {
150        Ok(index) => index,
151        Err(e) => {return Err(e)}
152    };
153
154    let address:*const c_void = function_address.cast::<u8>().offset(syscall_offset as isize) as *const c_void; 
155
156    Ok(Syscall {
157        ssn,
158        address,
159        hooked
160    })
161}
162
163// I know that this function name is hardly descriptive, but the name has to match 
164// the label in the assembly below and that assembly gets copy pasted with labels included
165extern "C" { 
166/// Super voodoo bullshit that does a lot of things. I would encourage you not to call this
167/// directly, but if you do it takes a list of gadgets as defined in the jopcall macro, the ssn of
168/// a syscall, the address of a syscall, and a struct that contains the number of arguments and
169/// gadgets passed. It then takes whatever variadic arguments the syscall would take. Note that if
170/// you mess up any of these values it will probably crash horribly and the function itself is x64
171/// assembly. If you really want to play with this there are a lot of comments in the source code
172/// in src/syscall.rs i'd encourage you to look at instead.
173    pub fn isc(
174    gadget_list:*const c_void,
175    ssn:u16,
176    addr:*const c_void,
177    syscall_count:SyscallCount, 
178    ...)->i64;
179}
180
181global_asm!(
182"
183    .global isc
184    isc:
185    mov [rsp - 0x8], rsi
186    mov [rsp - 0x10], rdi 
187    mov [rsp - 0x18], r12
188    mov [rsp - 0x20], r14
189", // Moves gadget_list pointer into r11 and the arg_count struct into rcx
190" 
191    mov r11, rcx 
192    mov rcx, r9
193", // Calculates the offset of function parameters by pulling it out of cx
194   // register into r14 and bitshifting it left 3 times (equivelant to cx * 8)
195"    
196    xor r14, r14
197    mov r14w, cx
198    shl r14w, 3
199
200", // Extracts the number of syscall arguments to rcx
201"
202    shr ecx, 16 
203    movzx ecx, cx
204", // Moves r14 into rax and pushes all gadget addresses to the stack except for the first one
205"
206    mov rax, r14
207 
208    cmp rax, 0x08
209    je 2f
210    
211    sub rax, 0x08 
212
213    3:
214    push [r11 + rax];
215    sub rax, 0x08 
216    cmp rax, 0
217    jne 3b
218    
219    2:
220", // Dereferences the first gadget in the list (jmp rcx) 
221"   mov r11, [r11]    
222
223
224", // Places the SSN into the correct register
225"   
226    mov eax, edx
227    mov r12, r8
228
229", // Moves the first 4 arguments into the proper registers from the stack
230"   
231
232    sub r14, 0x08
233    mov r10, [rsp + 0x28 + r14]
234    mov rdx, [rsp + 0x30 + r14]
235    mov r8,  [rsp + 0x38 + r14]
236    mov r9,  [rsp + 0x40 + r14]
237    
238    sub rcx, 0x4 
239    jle 4f 
240
241", // Realigns stack arguments to the correct location to be passed to the syscall
242"   
243    
244    lea rsi, [rsp + 0x48 + r14]
245    lea rdi, [rsp + 0x28]
246
247    rep movsq
248
249    4:
250
251",  // Places the syscall address into rcx, restores callee registers, and jumps to the first gadget
252"   
253    mov rcx, r12
254    mov rsi, [rsp - 0x8]
255    mov rdi, [rsp - 0x10]
256    mov r12, [rsp - 0x18]
257    mov r14, [rsp - 0x20]
258    jmp r11
259"
260);
261