logitech_cve/device.rs
1use core::{mem, ptr};
2
3use windows_sys::{
4 Wdk::{
5 Foundation::OBJECT_ATTRIBUTES,
6 Storage::FileSystem::{FILE_NON_DIRECTORY_FILE, FILE_OPEN_IF, FILE_SYNCHRONOUS_IO_NONALERT, NtCreateFile},
7 System::{IO::NtDeviceIoControlFile, SystemServices::ZwClose},
8 },
9 Win32::{
10 Foundation::{GENERIC_WRITE, HANDLE, NTSTATUS, STATUS_SUCCESS, UNICODE_STRING},
11 Storage::FileSystem::{FILE_ATTRIBUTE_NORMAL, FILE_SHARE_NONE, SYNCHRONIZE},
12 System::{IO::IO_STATUS_BLOCK, WindowsProgramming::RtlInitUnicodeString},
13 },
14 core::PCWSTR,
15};
16
17use crate::{keyboard::Key, mouse::MouseButton, util::InitializeObjectAttributes};
18
19/// I/O structure used to communicate mouse actions to the device driver.
20#[repr(C)]
21struct MouseIO {
22 /// The mouse button state/action to perform.
23 button: u8,
24 /// The X-axis movement delta.
25 x: i8,
26 /// The Y-axis movement delta.
27 y: i8,
28 /// The mouse wheel movement delta.
29 wheel: i8,
30 /// Unknown field, always set to 0.
31 unk1: i8,
32}
33
34impl MouseIO {
35 /// Creates a new `MouseIO` instance with the specified parameters.
36 ///
37 /// # Arguments
38 /// * `button` - The mouse button state/action to perform
39 /// * `x` - The X-axis movement delta
40 /// * `y` - The Y-axis movement delta
41 /// * `wheel` - The mouse wheel movement delta
42 ///
43 /// # Returns
44 /// A new `MouseIO` instance with `unk1` set to 0
45 #[inline]
46 const fn new(button: u8, x: i8, y: i8, wheel: i8) -> Self {
47 let unk1 = 0;
48 Self {
49 button,
50 x,
51 y,
52 wheel,
53 unk1,
54 }
55 }
56}
57
58/// I/O structure used to communicate keyboard button states to the device driver.
59#[repr(C)]
60struct KeyboardIO {
61 /// Unknown field, always set to 0.
62 unknown1: u8,
63 /// Unknown field, always set to 0.
64 unknown2: u8,
65 /// State of keyboard button 1.
66 button1: u8,
67 /// State of keyboard button 2.
68 button2: u8,
69 /// State of keyboard button 3.
70 button3: u8,
71 /// State of keyboard button 4.
72 button4: u8,
73 /// State of keyboard button 5.
74 button5: u8,
75 /// State of keyboard button 6.
76 button6: u8,
77}
78
79impl KeyboardIO {
80 /// Creates a new `KeyboardIO` instance with the specified button states.
81 ///
82 /// # Arguments
83 /// * `button1` through `button6` - The states of keyboard buttons 1-6
84 ///
85 /// # Returns
86 /// A new `KeyboardIO` instance with unknown fields set to 0
87 #[inline]
88 const fn new(button1: u8, button2: u8, button3: u8, button4: u8, button5: u8, button6: u8) -> Self {
89 let unknown1 = 0;
90 let unknown2 = 0;
91 Self {
92 unknown1,
93 unknown2,
94 button1,
95 button2,
96 button3,
97 button4,
98 button5,
99 button6,
100 }
101 }
102}
103
104/// Represents a handle to the virtual input device.
105/// Represents a handle to the virtual input device.
106pub struct Device {
107 /// Handle to the device file.
108 filehandle: HANDLE,
109 /// I/O status block used for device operations.
110 iostatusblock: IO_STATUS_BLOCK,
111}
112
113impl Drop for Device {
114 #[inline]
115 fn drop(&mut self) {
116 self.close();
117 }
118}
119
120impl Device {
121 /// Attempts to open the device and return a [`Device`] instance.
122 ///
123 /// # Errors
124 /// Returns an error if the device cannot be opened (e.g., G HUB not installed or incompatible version).
125 #[inline]
126 pub fn try_new() -> Result<Self, &'static str> {
127 let filehandle = HANDLE::default();
128 let iostatusblock = IO_STATUS_BLOCK::default();
129
130 let mut device = Self {
131 filehandle,
132 iostatusblock,
133 };
134
135 if !device.open() {
136 return Err("Device not found. Consider to download Logitech G HUB 2021.11.1775");
137 }
138
139 Ok(device)
140 }
141
142 /// Calls the device IOCTL.
143 ///
144 /// # Arguments
145 /// * `button` - The mouse button action to perform (e.g., left click, right click, release)
146 /// * `x` - Horizontal movement delta in pixels. Positive values move right, negative values move left
147 /// * `y` - Vertical movement delta in pixels. Positive values move down, negative values move up
148 /// * `wheel` - Mouse wheel scroll delta. Positive values scroll up, negative values scroll down
149 ///
150 /// # Returns
151 /// `true` if the IOCTL call was successful, `false` otherwise.
152 #[expect(
153 clippy::must_use_candidate,
154 reason = "This function is used to send mouse input commands"
155 )]
156 #[inline]
157 pub fn call_mouse(&self, button: MouseButton, x: i8, y: i8, wheel: i8) -> bool {
158 #[expect(clippy::cast_possible_truncation, reason = "MouseIO is only 5 bytes")]
159 const INPUTBUFFERLENGTH: u32 = mem::size_of::<MouseIO>() as u32;
160 let mut iostatusblock = IO_STATUS_BLOCK::default();
161 let inputbuffer = MouseIO::new(button.into(), x, y, wheel);
162
163 // SAFETY: All pointers passed to NtDeviceIoControlFile are either valid, null, or point to properly initialized structures as required by the API.
164 let status = unsafe {
165 NtDeviceIoControlFile(
166 self.filehandle,
167 ptr::null_mut(),
168 None,
169 ptr::null(),
170 &raw mut iostatusblock,
171 0x002A_2010,
172 (&raw const inputbuffer).cast(),
173 INPUTBUFFERLENGTH,
174 ptr::null_mut(),
175 0,
176 )
177 };
178 status == STATUS_SUCCESS
179 }
180
181 /// Calls the device IOCTL.
182 ///
183 /// # Arguments
184 /// * `button1` through `button6` - The states of keyboard buttons 1-6. Each parameter represents
185 /// the desired state of a specific key position. Use `Key::None`
186 /// for keys that should not be pressed.
187 ///
188 /// # Returns
189 /// `true` if the IOCTL call was successful, `false` otherwise.
190 #[expect(
191 clippy::must_use_candidate,
192 reason = "This function is used to send keyboard input commands"
193 )]
194 #[inline]
195 pub fn call_keyboard(
196 &self,
197 button1: Key,
198 button2: Key,
199 button3: Key,
200 button4: Key,
201 button5: Key,
202 button6: Key,
203 ) -> bool {
204 #[expect(clippy::cast_possible_truncation, reason = "KeyboardIO is only 8 bytes")]
205 const INPUTBUFFERLENGTH: u32 = mem::size_of::<KeyboardIO>() as u32;
206 let mut iostatusblock = IO_STATUS_BLOCK::default();
207 let inputbuffer = KeyboardIO::new(
208 button1.into(),
209 button2.into(),
210 button3.into(),
211 button4.into(),
212 button5.into(),
213 button6.into(),
214 );
215
216 // SAFETY: All pointers passed to NtDeviceIoControlFile are either valid, null, or point to properly initialized structures as required by the API.
217 let status = unsafe {
218 NtDeviceIoControlFile(
219 self.filehandle,
220 ptr::null_mut(),
221 None,
222 ptr::null(),
223 &raw mut iostatusblock,
224 0x002A_200C,
225 (&raw const inputbuffer).cast(),
226 INPUTBUFFERLENGTH,
227 ptr::null_mut(),
228 0,
229 )
230 };
231 status == STATUS_SUCCESS
232 }
233
234 /// Tries to open the device by testing multiple known device paths.
235 ///
236 /// # Returns
237 /// `true` if a device was successfully opened, `false` otherwise.
238 fn open(&mut self) -> bool {
239 let buffers: [Vec<u16>; 2] = [
240 "\\??\\ROOT#SYSTEM#0001#{1abc05c0-c378-41b9-9cef-df1aba82b015}\0"
241 .encode_utf16()
242 .collect(),
243 "\\??\\ROOT#SYSTEM#0002#{1abc05c0-c378-41b9-9cef-df1aba82b015}\0"
244 .encode_utf16()
245 .collect(),
246 ];
247
248 for buffer in buffers {
249 if self.device_initialize(buffer.as_ptr()) == STATUS_SUCCESS {
250 return true;
251 }
252 }
253
254 false
255 }
256
257 /// Initializes the device by opening a handle to it.
258 ///
259 /// # Arguments
260 /// * `device_name` - A `PCWSTR` representing the path to the device.
261 ///
262 /// # Returns
263 /// An `NTSTATUS` indicating the success or failure of the operation.
264 fn device_initialize(&mut self, device_name: PCWSTR) -> NTSTATUS {
265 let mut name = UNICODE_STRING::default();
266 let mut attr = OBJECT_ATTRIBUTES::default();
267
268 // SAFETY: RtlInitUnicodeString requires a valid pointer to a UNICODE_STRING and a valid PCWSTR.
269 unsafe {
270 RtlInitUnicodeString(&raw mut name, device_name);
271 };
272 InitializeObjectAttributes(&mut attr, &raw const name, 0, ptr::null_mut(), ptr::null());
273 // SAFETY: NtCreateFile requires properly initialized pointers and structures as per API contract.
274 unsafe {
275 NtCreateFile(
276 &raw mut self.filehandle,
277 GENERIC_WRITE | SYNCHRONIZE,
278 &raw const attr,
279 &raw mut self.iostatusblock,
280 ptr::null::<i64>(), // AllocationSize (optional)
281 FILE_ATTRIBUTE_NORMAL,
282 FILE_SHARE_NONE,
283 FILE_OPEN_IF, // CreateDisposition (OPEN_EXISTING)
284 FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
285 ptr::null(),
286 0,
287 )
288 }
289 }
290
291 /// Closes the handle to the device.
292 ///
293 /// This method safely closes the device handle if it's currently open,
294 /// and sets the handle to null to prevent double-closing.
295 fn close(&mut self) {
296 // SAFETY: ZwClose is only called if filehandle is not null, and filehandle is set to null after closing to prevent double-closing.
297 unsafe {
298 if !self.filehandle.is_null() {
299 ZwClose(self.filehandle);
300 self.filehandle = ptr::null_mut();
301 }
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn open_close() {
312 let mut device = Device {
313 filehandle: HANDLE::default(),
314 iostatusblock: IO_STATUS_BLOCK::default(),
315 };
316 assert!(device.open(), "Device not opened");
317 device.close();
318 assert!(device.filehandle.is_null());
319 }
320}