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