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