electron_hardener/
patcher.rs

1//! A configurable and fast implementation of [electron-evil-feature-patcher]'s binary patching capabilites.
2//!
3//! [electron-evil-feature-patcher]: https://github.com/antelle/electron-evil-feature-patcher
4
5use crate::{BinaryError, ElectronApp, PatcherError};
6use regex::bytes::Regex;
7
8#[cfg(test)]
9use enum_iterator::IntoEnumIterator;
10
11/// A flag inside an Electron application binary that can be patched to disable it.
12pub trait Patchable: private::Sealed {
13    #[doc(hidden)]
14    /// Disables the option.
15    ///
16    /// You are probably looking for [patch_option](ElectronApp::patch_option).
17    fn disable(&self, binary: &mut [u8]) -> Result<(), PatcherError>;
18}
19
20#[allow(deprecated)]
21mod private {
22    use super::{DevToolsMessage, ElectronOption, NodeJsCommandLineFlag};
23
24    pub trait Sealed {}
25
26    impl Sealed for NodeJsCommandLineFlag {}
27    impl Sealed for ElectronOption {}
28    impl Sealed for DevToolsMessage {}
29}
30
31/// List of known command line debugging flags that can be disabled
32///
33/// See the [Node.JS documentation] for details on what each flag does.
34///
35/// [Node.JS documentation]: https://nodejs.org/en/docs/guides/debugging-getting-started/#command-line-options
36#[deprecated(
37    since = "0.2.2",
38    note = "This has been superseded by the NodeCliInspect fuse."
39)]
40#[allow(missing_docs)]
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[non_exhaustive]
43pub enum NodeJsCommandLineFlag {
44    Inspect,
45    InspectBrk,
46    InspectPort,
47    Debug,
48    DebugBrk,
49    DebugPort,
50    InspectBrkNode,
51    InspectPublishUid,
52}
53
54#[allow(deprecated)]
55impl NodeJsCommandLineFlag {
56    const fn search_string(&self) -> &'static str {
57        match self {
58            Self::Inspect => "\0--inspect\0",
59            Self::InspectBrk => "\0--inspect-brk\0",
60            Self::InspectPort => "\0--inspect-port\0",
61            Self::Debug => "\0--debug\0",
62            Self::DebugBrk => "\0--debug-brk\0",
63            Self::DebugPort => "\0--debug-port\0",
64            Self::InspectBrkNode => "\0--inspect-brk-node\0",
65            Self::InspectPublishUid => "\0--inspect-publish-uid\0",
66        }
67    }
68
69    const fn fallback_search_string(&self) -> Option<&'static str> {
70        // Electron 13 Windows binaries have flags laid out differently.
71        if matches!(self, Self::Inspect) {
72            Some(r"(?-u)\xAA--inspect\x00")
73        } else {
74            None
75        }
76    }
77}
78
79#[allow(deprecated)]
80impl Patchable for NodeJsCommandLineFlag {
81    fn disable(&self, binary: &mut [u8]) -> Result<(), PatcherError> {
82        let search = Regex::new(self.search_string()).expect("all regex patterns should be valid");
83        let found = search
84            .find(binary)
85            .or_else(|| {
86                self.fallback_search_string().and_then(|s| {
87                    let search = Regex::new(s).expect("all regex patterns should be valid");
88                    search.find(binary)
89                })
90            })
91            .ok_or(BinaryError::NodeJsFlagNotPresent(*self))?
92            .range();
93
94        for b in &mut binary[found] {
95            if *b == b'-' {
96                *b = b' '
97            }
98        }
99
100        Ok(())
101    }
102}
103
104/// List of known Electron command line flags that can be disabled.
105///
106/// See the [Electron documentation] for details on what each flag does.
107///
108/// [Electron documentation]: https://www.electronjs.org/docs/api/command-line-switches
109#[allow(missing_docs)]
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111#[cfg_attr(test, derive(IntoEnumIterator))]
112#[non_exhaustive]
113pub enum ElectronOption {
114    JsFlags,
115    RemoteDebuggingPipe,
116    RemoteDebuggingPort,
117    WaitForDebuggerChildren,
118}
119
120impl ElectronOption {
121    const fn search_string(&self) -> &'static str {
122        match self {
123            Self::JsFlags => "\0js-flags\0",
124            Self::RemoteDebuggingPipe => "\0remote-debugging-pipe\0",
125            Self::RemoteDebuggingPort => "\0remote-debugging-port\0",
126            Self::WaitForDebuggerChildren => "\0wait-for-debugger-children\0",
127        }
128    }
129}
130
131impl Patchable for ElectronOption {
132    fn disable(&self, binary: &mut [u8]) -> Result<(), PatcherError> {
133        let search = Regex::new(self.search_string()).expect("all regex patterns should be valid");
134        let found = search
135            .find(binary)
136            .ok_or(BinaryError::ElectronOptionNotPresent(*self))?
137            .range();
138
139        let replacement = b"\0xx\r\n"
140            .iter()
141            .copied()
142            .chain(std::iter::repeat(0))
143            .take(found.len());
144
145        for (old, new) in binary[found].iter_mut().zip(replacement) {
146            *old = new;
147        }
148
149        Ok(())
150    }
151}
152
153/// List of known developer tool command line messages that can be
154/// written to stdout by Node.JS during debugging.
155///
156/// ### Warning
157///
158/// Disabling these is a worst-case fallback protection against internal changes to the way
159/// that Chromium/Electron/Node.JS handle parsing command line arguments. If something is changed
160/// and a debugging flag slips through, modifying one of these will cause the application to trigger a segemntation fault
161/// and be terminated by the OS, exiting immediately.
162#[deprecated(
163    since = "0.2.2",
164    note = "This is no longer necessary due to the NodeCliInspect fuse's functionality."
165)]
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167#[non_exhaustive]
168pub enum DevToolsMessage {
169    /// The message printed to standard out when Node.JS listens on TCP port.
170    ///
171    /// Ex: `Debugger listening on 127.0.0.1:9229/uuid`
172    Listening,
173    /// The message printed to standard out when Node.JS listens on a websocket.
174    ///
175    /// Ex: `Debugger listening on ws://127.0.0.1:9229/uuid`
176    ListeningWs,
177}
178
179#[allow(deprecated)]
180impl DevToolsMessage {
181    const fn search_string(&self) -> &'static str {
182        match self {
183            #[allow(deprecated)]
184            Self::Listening => "\0Debugger listening on %s\n\0",
185            Self::ListeningWs => "\0\nDevTools listening on ws://%s%s\n\0",
186        }
187    }
188}
189
190#[allow(deprecated)]
191impl Patchable for DevToolsMessage {
192    fn disable(&self, binary: &mut [u8]) -> Result<(), PatcherError> {
193        let search = Regex::new(self.search_string()).expect("all regex patterns should be valid");
194        let found = search
195            .find(binary)
196            .ok_or(BinaryError::MessageNotPresent(*self))?
197            .range();
198
199        let mut replacement = Vec::with_capacity(found.len());
200        replacement.push(b'\0');
201        let str_len = found.len() - 3;
202        for _ in (0..str_len).step_by(2) {
203            replacement.push(b'%');
204            replacement.push(b's');
205        }
206        replacement.extend_from_slice(b"\n\0");
207
208        for (old, new) in binary[found].iter_mut().zip(replacement) {
209            *old = new;
210        }
211
212        Ok(())
213    }
214}
215
216impl ElectronApp<'_> {
217    /// Disables the ability to use this command line flag in the application.
218    ///
219    /// After being disabled, the flag will no longer be processed by the application. The removal
220    /// is a best-effort attempt. See the [crate documentation on effectiveness](crate).
221    pub fn patch_option<P: Patchable>(&mut self, to_disable: P) -> Result<(), PatcherError> {
222        to_disable.disable(self.contents)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    const TEST_DATA: &[u8] = include_bytes!("../examples/fake_electron_flags.bin");
231
232    #[test]
233    #[allow(deprecated)]
234    fn disabling_nodejs_flags_works() {
235        use NodeJsCommandLineFlag::*;
236        let mut data = TEST_DATA.to_vec();
237
238        const ALL_FLAGS: &[NodeJsCommandLineFlag] = &[
239            Inspect,
240            InspectBrk,
241            InspectPort,
242            Debug,
243            DebugBrk,
244            DebugPort,
245            InspectBrkNode,
246            InspectPublishUid,
247        ];
248
249        // Remove all the flags supported.
250        for flag in ALL_FLAGS {
251            flag.disable(&mut data).unwrap();
252
253            if flag.fallback_search_string().is_some() {
254                let _ = flag.disable(&mut data);
255            }
256        }
257
258        // Ensure they no longer exist
259        for flag in ALL_FLAGS {
260            assert_eq!(
261                flag.disable(&mut data),
262                Err(PatcherError::Binary(BinaryError::NodeJsFlagNotPresent(
263                    *flag
264                )))
265            );
266        }
267    }
268
269    #[test]
270    fn disabling_electron_options_works() {
271        let mut data = TEST_DATA.to_vec();
272
273        // Remove all the options supported.
274        for opt in ElectronOption::into_enum_iter() {
275            opt.disable(&mut data).unwrap();
276        }
277
278        // Ensure they no longer exist
279        for opt in ElectronOption::into_enum_iter() {
280            assert_eq!(
281                opt.disable(&mut data),
282                Err(PatcherError::Binary(BinaryError::ElectronOptionNotPresent(
283                    opt
284                )))
285            );
286        }
287    }
288
289    #[allow(deprecated)]
290    #[test]
291    fn disabling_debugging_messages_works() {
292        let mut data = TEST_DATA.to_vec();
293
294        const ALL_MESSAGES: &[DevToolsMessage] =
295            &[DevToolsMessage::ListeningWs, DevToolsMessage::Listening];
296
297        // Remove all the options supported.
298        for msg in ALL_MESSAGES.iter().copied() {
299            msg.disable(&mut data).unwrap();
300        }
301
302        // Ensure they no longer exist
303        for msg in ALL_MESSAGES.iter().copied() {
304            assert_eq!(
305                msg.disable(&mut data),
306                Err(PatcherError::Binary(BinaryError::MessageNotPresent(msg)))
307            );
308        }
309    }
310}