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: [*const u16; 2] = [
237            windows_sys::w!("\\??\\ROOT#SYSTEM#0001#{1abc05c0-c378-41b9-9cef-df1aba82b015}\0"),
238            windows_sys::w!("\\??\\ROOT#SYSTEM#0002#{1abc05c0-c378-41b9-9cef-df1aba82b015}\0"),
239        ];
240
241        for buffer in buffers {
242            if self.device_initialize(buffer) == STATUS_SUCCESS {
243                return true;
244            }
245        }
246
247        false
248    }
249
250    /// Initializes the device by opening a handle to it.
251    ///
252    /// # Arguments
253    /// * `device_name` - A `PCWSTR` representing the path to the device.
254    ///
255    /// # Returns
256    /// An `NTSTATUS` indicating the success or failure of the operation.
257    fn device_initialize(&mut self, device_name: PCWSTR) -> NTSTATUS {
258        let mut name = UNICODE_STRING::default();
259        let mut attr = OBJECT_ATTRIBUTES::default();
260        let mut iostatusblock = IO_STATUS_BLOCK::default();
261
262        // SAFETY: RtlInitUnicodeString requires a valid pointer to a UNICODE_STRING and a valid PCWSTR.
263        unsafe {
264            RtlInitUnicodeString(&raw mut name, device_name);
265        };
266        InitializeObjectAttributes(&mut attr, &raw const name, 0, ptr::null_mut(), ptr::null());
267
268        // SAFETY: NtCreateFile requires properly initialized pointers and structures as per API contract.
269        unsafe {
270            NtCreateFile(
271                &raw mut self.filehandle,
272                GENERIC_WRITE | SYNCHRONIZE,
273                &raw const attr,
274                &raw mut iostatusblock,
275                ptr::null::<i64>(), // AllocationSize (optional)
276                FILE_ATTRIBUTE_NORMAL,
277                FILE_SHARE_NONE,
278                FILE_OPEN_IF, // CreateDisposition (OPEN_EXISTING)
279                FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
280                ptr::null(),
281                0,
282            )
283        }
284    }
285
286    /// Closes the handle to the device.
287    ///
288    /// This method safely closes the device handle if it's currently open,
289    /// and sets the handle to null to prevent double-closing.
290    fn close(&mut self) {
291        if !self.filehandle.is_null() {
292            // SAFETY: ZwClose is only called if filehandle is not null, and filehandle is set to null after closing to prevent double-closing.
293            unsafe {
294                ZwClose(self.filehandle);
295            };
296            self.filehandle = ptr::null_mut();
297        }
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn open_close() {
307        let mut device = Device {
308            filehandle: HANDLE::default(),
309        };
310
311        assert!(device.open());
312        device.close();
313        assert!(device.filehandle.is_null());
314    }
315}