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.
105pub struct Device {
106    /// Handle to the device file.
107    filehandle: HANDLE,
108}
109
110impl Drop for Device {
111    #[inline]
112    fn drop(&mut self) {
113        self.close();
114    }
115}
116
117impl Device {
118    /// Attempts to open the device and return a [`Device`] instance.
119    ///
120    /// # Errors
121    /// Returns an error if the device cannot be opened (e.g., G HUB not installed or incompatible version).
122    #[inline]
123    pub fn try_new() -> Result<Self, &'static str> {
124        let filehandle = HANDLE::default();
125
126        let mut device = Self { filehandle };
127
128        if !device.open() {
129            return Err("Device not found. Consider to download Logitech G HUB 2021.11.1775");
130        }
131
132        Ok(device)
133    }
134
135    /// Calls the device IOCTL.
136    ///
137    /// # Arguments
138    /// * `button` - The mouse button action to perform (e.g., left click, right click, release)
139    /// * `x` - Horizontal movement delta in pixels. Positive values move right, negative values move left
140    /// * `y` - Vertical movement delta in pixels. Positive values move down, negative values move up
141    /// * `wheel` - Mouse wheel scroll delta. Positive values scroll up, negative values scroll down
142    ///
143    /// # Warning
144    /// The value `-128` for `x`, `y` or `wheel` is treated as `0`.
145    /// To avoid unexpected behavior, use `-127` instead.
146    ///
147    /// # Returns
148    /// `true` if the IOCTL call was successful, `false` otherwise.
149    #[expect(
150        clippy::must_use_candidate,
151        reason = "This function is used to send mouse input commands"
152    )]
153    #[inline]
154    pub fn call_mouse(&self, button: MouseButton, x: i8, y: i8, wheel: i8) -> bool {
155        #[expect(clippy::cast_possible_truncation, reason = "MouseIO is only 5 bytes")]
156        const INPUTBUFFERLENGTH: u32 = mem::size_of::<MouseIO>() as u32;
157        let mut iostatusblock = IO_STATUS_BLOCK::default();
158        let inputbuffer = MouseIO::new(button.into(), x, y, wheel);
159
160        // SAFETY: All pointers passed to NtDeviceIoControlFile are either valid, null, or point to properly initialized structures as required by the API.
161        let status = unsafe {
162            NtDeviceIoControlFile(
163                self.filehandle,
164                ptr::null_mut(),
165                None,
166                ptr::null(),
167                &raw mut iostatusblock,
168                0x002A_2010,
169                (&raw const inputbuffer).cast(),
170                INPUTBUFFERLENGTH,
171                ptr::null_mut(),
172                0,
173            )
174        };
175        status == STATUS_SUCCESS
176    }
177
178    /// Calls the device IOCTL.
179    ///
180    /// # Arguments
181    /// * `button1` through `button6` - The states of keyboard buttons 1-6. Each parameter represents
182    ///   the desired state of a specific key position. Use `Key::None`
183    ///   for keys that should not be pressed.
184    ///
185    /// # Returns
186    /// `true` if the IOCTL call was successful, `false` otherwise.
187    #[expect(
188        clippy::must_use_candidate,
189        reason = "This function is used to send keyboard input commands"
190    )]
191    #[inline]
192    pub fn call_keyboard(
193        &self,
194        button1: Key,
195        button2: Key,
196        button3: Key,
197        button4: Key,
198        button5: Key,
199        button6: Key,
200    ) -> bool {
201        #[expect(clippy::cast_possible_truncation, reason = "KeyboardIO is only 8 bytes")]
202        const INPUTBUFFERLENGTH: u32 = mem::size_of::<KeyboardIO>() as u32;
203        let mut iostatusblock = IO_STATUS_BLOCK::default();
204        let inputbuffer = KeyboardIO::new(
205            button1.into(),
206            button2.into(),
207            button3.into(),
208            button4.into(),
209            button5.into(),
210            button6.into(),
211        );
212
213        // SAFETY: All pointers passed to NtDeviceIoControlFile are either valid, null, or point to properly initialized structures as required by the API.
214        let status = unsafe {
215            NtDeviceIoControlFile(
216                self.filehandle,
217                ptr::null_mut(),
218                None,
219                ptr::null(),
220                &raw mut iostatusblock,
221                0x002A_200C,
222                (&raw const inputbuffer).cast(),
223                INPUTBUFFERLENGTH,
224                ptr::null_mut(),
225                0,
226            )
227        };
228        status == STATUS_SUCCESS
229    }
230
231    /// Tries to open the device by testing multiple known device paths.
232    ///
233    /// # Returns
234    /// `true` if a device was successfully opened, `false` otherwise.
235    fn open(&mut self) -> bool {
236        let buffers: [Vec<u16>; 2] = [
237            "\\??\\ROOT#SYSTEM#0001#{1abc05c0-c378-41b9-9cef-df1aba82b015}\0"
238                .encode_utf16()
239                .collect(),
240            "\\??\\ROOT#SYSTEM#0002#{1abc05c0-c378-41b9-9cef-df1aba82b015}\0"
241                .encode_utf16()
242                .collect(),
243        ];
244
245        for buffer in buffers {
246            if self.device_initialize(buffer.as_ptr()) == STATUS_SUCCESS {
247                return true;
248            }
249        }
250
251        false
252    }
253
254    /// Initializes the device by opening a handle to it.
255    ///
256    /// # Arguments
257    /// * `device_name` - A `PCWSTR` representing the path to the device.
258    ///
259    /// # Returns
260    /// An `NTSTATUS` indicating the success or failure of the operation.
261    fn device_initialize(&mut self, device_name: PCWSTR) -> NTSTATUS {
262        let mut name = UNICODE_STRING::default();
263        let mut attr = OBJECT_ATTRIBUTES::default();
264        let mut iostatusblock = IO_STATUS_BLOCK::default();
265
266        // SAFETY: RtlInitUnicodeString requires a valid pointer to a UNICODE_STRING and a valid PCWSTR.
267        unsafe {
268            RtlInitUnicodeString(&raw mut name, device_name);
269        };
270        InitializeObjectAttributes(&mut attr, &raw const name, 0, ptr::null_mut(), ptr::null());
271
272        // SAFETY: NtCreateFile requires properly initialized pointers and structures as per API contract.
273        unsafe {
274            NtCreateFile(
275                &raw mut self.filehandle,
276                GENERIC_WRITE | SYNCHRONIZE,
277                &raw const attr,
278                &raw mut iostatusblock,
279                ptr::null::<i64>(), // AllocationSize (optional)
280                FILE_ATTRIBUTE_NORMAL,
281                FILE_SHARE_NONE,
282                FILE_OPEN_IF, // CreateDisposition (OPEN_EXISTING)
283                FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
284                ptr::null(),
285                0,
286            )
287        }
288    }
289
290    /// Closes the handle to the device.
291    ///
292    /// This method safely closes the device handle if it's currently open,
293    /// and sets the handle to null to prevent double-closing.
294    fn close(&mut self) {
295        if !self.filehandle.is_null() {
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                ZwClose(self.filehandle);
299            };
300            self.filehandle = ptr::null_mut();
301        }
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn open_close() {
311        let mut device = Device {
312            filehandle: HANDLE::default(),
313        };
314
315        assert!(device.open());
316        device.close();
317        assert!(device.filehandle.is_null());
318    }
319}