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}