Patching Electron app menu behavior
For a few weeks now, I’ve been trying out Kinto – an application for Linux (and Windows) that remaps keyboard keys so you can use macOS shortcuts on your Linux computer. While not perfect, I’ve been surprised by how well it worked.
On macOS, the shortcut to delete a word is Alt-Backspace. When you press it on a Linux computer with Kinto installed, it correctly remaps that to Ctrl-Backspace. This works well in almost all applications, except for Electron apps. Electron apps activate the main menu when you release Alt.
Apparently, this is a feature that comes from the way most applications on Windows work: if you press Alt without pressing anything else, it will focus the first menu item. This is not a common behavior on Linux however – neither GTK nor Qt apps do this.
Even though this can be worked around in Kinto with varying levels of success, the real fix needs to happen in the Electron framework. With this in mind, I’ve set out to dig into this issue.
Looking into Electron’s source code
After going through the pull request that introduced this, I realized there doesn’t seem to be a way to disable this behavior. The culprit is this piece of code. Since a proper fix won’t come anytime soon, let’s binary patch compiled executables ✨.
Analyzing the binary
First, we need to find where the binary is. Let’s run Signal and list running processes:
$ ps aux | grep signal
[...]
user 211853 2.5 0.8 6837048 267392 ? SLl 01:08 0:02 /opt/Signal/signal-desktop --no-sandbox
user 211865 0.0 0.1 208664 45708 ? S 01:08 0:00 /opt/Signal/signal-desktop --type=zygote --no-zygote-sandbox --no-sandbox
user 211866 0.0 0.1 208664 45932 ? S 01:08 0:00 /opt/Signal/signal-desktop --type=zygote --no-sandbox
[...]
There it is! Let’s open /opt/Signal/signal-desktop
in IDA Pro. There’s a free version that will do just fine for our needs. Signal binary is huge (~130 MB), so the analysis takes a looong time.
After the analysis is finished, our main goal is to find where that piece of code is in the binary. Sadly, the executable doesn’t have any debugging symbols, so we can’t just search for the function by its name. Without compiling Electron ourselves (which I guess takes like half a day), we need to find something unique about this piece of code. String literals work best, however, there seem to be no string literals around.
I then found the following function:
const int kMenuBarHeight = 25;
// ...
int RootView::GetMenuBarHeight() const {
return kMenuBarHeight;
}
While not perfect, we can search for this value in IDA Pro by pressing Alt-I. Enter 25, press Enter. 32265 results. Oof. Let’s try to narrow them down.
If we reference the standard calling conventions we should expect the function to store the constant into the rax
/eax
/ax
/al
register. So let’s press Ctrl-F and enter
mov eax, 19h
57 results. That’s better. Let’s go through all of them and find a really short function. To my surprise, the function I needed was the first in the list. Let’s rename it to GetMenuBarHeight
:
.text:0000000001A5FAA0 GetMenuBarHeight proc near
.text:0000000001A5FAA0 push rbp
.text:0000000001A5FAA1 mov rbp, rsp
.text:0000000001A5FAA4 mov eax, 19h
.text:0000000001A5FAA9 pop rbp
.text:0000000001A5FAAA retn
.text:0000000001A5FAAA GetMenuBarHeight endp
This function by itself is not very important, but if we go through adjacent functions, we’ll realize the compiled binary follows the source code pretty closely. I figured out the next functions were: SetAutoHideMenuBar
, IsMenuBarAutoHide
, IsMenuBarVisible
and then HandleKeyEvent
, the one we need to patch!
Patch idea
If we pretend that the Alt key press never happened, the menu will not be activated. We need to make a patch as if this line was
menu_bar_alt_pressed_ = false;
Analyzing HandleKeyEvent function
If we look at the source code again, the function has an early return which ensures that menu_bar_
is set:
void RootView::HandleKeyEvent(const content::NativeWebKeyboardEvent& event) {
if (!menu_bar_)
return;
// ...
}
Before we go further, I need to mention that methods in C++ have an implicit first parameter which is a pointer to the object (this
), so the method above actually has two (RootView *this, NativeWebKeyboardEvent &event
). This is what the method looks like in the disassembly:
HandleKeyEvent proc near
push rbp ; init stack frame, save register values, etc.
mov rbp, rsp
push r14
push rbx
mov r14, rdi ; rdi stores the first argument (`this` keyword)
; it is then copied to r14
mov rdi, [rdi+288h] ; rdi = this->menu_bar_;
test rdi, rdi ; if (!rdi)
jz loc_1A5FBE7 ; return;
This gives us some important information:
r14
stores the pointer to theRootView
object.288h
is the offset in the object formenu_bar_
variable.
Now, if we now look at the header file, we can find the offset for menu_bar_alt_pressed_
:
class RootView : public views::View {
public:
// ...
// Menu bar.
std::unique_ptr<MenuBar> menu_bar_; // r14+288h
bool menu_bar_autohide_ = false; // r14+288h+8 (8 is sizeof(std::unique_ptr<MenuBar>)
bool menu_bar_visible_ = false; // r14+288h+8+1 (1 is sizeof(bool))
bool menu_bar_alt_pressed_ = false; // r14+288h+8+1+1 = r14+292h
Now we need to look for [r14+292h]
in the disassembly. There are 4 matches:
.text:0000000001A5FB39 mov byte ptr [r14+292h], 1 ; set to true
.text:0000000001A5FB50 cmp byte ptr [r14+292h], 0 ; compare to 0
.text:0000000001A5FB5E mov byte ptr [r14+292h], 0 ; set to false
.text:0000000001A5FBDF mov byte ptr [r14+292h], 0 ; set to false
The first one is exactly what we were looking for. If you now go to the mov byte ptr [r14+292h], 1
line, IDA Pro will tell you the offset where the instruction in the input file is. In my case, it was 01A5EB39
.
Digging into the instruction
If you open the hex view in IDA Pro while the instruction is selected, it will highlight instruction’s machine code:
41 C6 86 92 02 00 00 01 mov byte ptr [r14+292h], 1
Using x86-64 opcode reference we can figure out what each byte means:
41 register extension prefix (REX.B), allows access to r14
C6 instruction: mov r/m8, imm8
86 ModR/M byte which corresponds to [R14/R14D]+disp32
92 02 00 00 disp32 value: 0x292 (encoded as little endian)
01 imm8 value (0x01)
The last byte is the one we need to patch.
Patching
We’re very close now! Let’s open this file in GHex:
$ sudo ghex /opt/Signal/signal-desktop
Press Ctrl-J and type the offset. At offset 0x01A5EB40, there is the value 1 that corresponds to true
. Change it to 00
(false
) and save.
Run Signal, press and release Alt, and there it is, the menu no longer gets activated! 🎉🎉🎉
Next steps
The biggest issue is that this patch needs to be reapplied after each update. The other obvious issue is that each Electron app needs to be patched.
Hang on until the next blog post where I’ll show you how to write a generic patcher that works with any Electron version and any Electron app – until developers make changes to that part of the code, of course.