Fix Backspace in Google Chrome

kao

I've written about my fight with Google Chrome updates and broken features in the past. This time let's talk about the brain-dead decision to disable Backspace.

This was their rationale for the change:

We have UseCounters showing that 0.04% of page views navigate back via the backspace button and 0.005% of page views are after a form interaction. The latter are often cases where the user loses data. Years of user complaints have been enough that we think it's the right choice to change this given the degree of pain users feel by losing their data and because every platform
has another keyboard combination that navigates back.

So, just because 50 persons out of each 1'000'000 are f*king idiots, all the others have to suffer? Makes no sense to me.

To prove my point, let's look at the simple Google search: "Google Chrome backspace". It gives 238'000+ results. First few results are: "Backspace to go Back - Chrome Web Store", "Go Back With Backspace - Chrome Web Store", "Back to Backspace - Chrome Web Store", "How to restore the backspace key as a keyboard shortcut to go back in ...", "So where's the Chrome flag to RE-ENABLE BACKSPACE going back a...".

Apparently, I'm not the only one who is hurt by this change.

Hidden BackspaceGoesBack feature

When the change was first introduced in Google Chrome, developers also created a hidden feature that you could set and make Backspace work as it used to. To use it, you just need to launch chrome.exe with a command-line like this:

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --enable-features=BackspaceGoesBack

But in commit 0fe1505a this feature was removed as well.

If you enter the commit number in Chromium Find Releases tool, you'll see that in went out in public in v61.0.3116.0. Another check in Chrome Channel Releases tool will tell you that as of this moment the change is already out for both Canary and Dev channels, and will hit Beta and Stable channels in next months:

So, let's fix this issue for good! And by "fixing it" I don't mean some stupid JavaScript-based Chrome extension (which doesn't work when JavaScript is disabled and in hundreds of other cases..), I mean a proper fix in the code.

Patching Google Chrome again

If you've read my previous post, you know the drill. Set the symbol path, load chrome.dll in IDA, get yourself some coffee and wait. Wait a lot. And after 20-30 minutes you'll be able to start working.

This is the commit that's causing our headaches: commit 0fe1505a and the corresponding place in disassembly of Chrome 58:

.text:0001819AB871      mov     ecx, edi
.text:0001819AB873      sub     ecx, 33000
.text:0001819AB879      jz      short locIDC_BACK
.text:0001819AB87B      sub     ecx, 1
.text:0001819AB87E      jz      short locIDC_FORWARD

...

.text:0001819AB8D9 locIDC_FORWARD:              ; CODE XREF: chrome::BrowserCommandController::ExecuteCommandWithDisposition(int,WindowOpenDisposition)+BEj
.text:0001819AB8D9      mov     rcx, [rbx+0F0h]
.text:0001819AB8E0      mov     rax, [rcx]
.text:0001819AB8E3      call    qword ptr [rax+110h]
.text:0001819AB8E9
.text:0001819AB8E9 doGoForward:                 ; CODE XREF: chrome::BrowserCommandController::ExecuteCommandWithDisposition(int,WindowOpenDisposition)+1C3j
.text:0001819AB8E9      mov     rcx, [r14+18h]  ; browser
.text:0001819AB8ED      mov     edx, r15d       ; disposition
.text:0001819AB8F0      call    ?GoForward@chrome@@YAXPEAVBrowser@@W4WindowOpenDisposition@@@Z ; chrome::GoForward(Browser *,WindowOpenDisposition)
.text:0001819AB8F5      jmp     loc_1819AC7C2
.text:0001819AB8FA ; ---------------------------------------------------------------------------
.text:0001819AB8FA
.text:0001819AB8FA locIDC_BACK:                 ; CODE XREF: chrome::BrowserCommandController::ExecuteCommandWithDisposition(int,WindowOpenDisposition)+B9j
.text:0001819AB8FA      mov     rcx, [rbx+0F0h]
.text:0001819AB901      mov     rax, [rcx]
.text:0001819AB904      call    qword ptr [rax+110h]
.text:0001819AB90A
.text:0001819AB90A doGoBack:                    ; CODE XREF: chrome::BrowserCommandController::ExecuteCommandWithDisposition(int,WindowOpenDisposition)+1DBj
.text:0001819AB90A      mov     rcx, [r14+18h]  ; browser
.text:0001819AB90E      mov     edx, r15d       ; disposition
.text:0001819AB911      call    ?GoBack@chrome@@YAXPEAVBrowser@@W4WindowOpenDisposition@@@Z ; chrome::GoBack(Browser *,WindowOpenDisposition)
.text:0001819AB916      jmp     loc_1819AC7C2

...

.text:0001819AB954      mov     ecx, edi
.text:0001819AB956      sub     ecx, 33007
.text:0001819AB95C      jz      loc_1819AB9FC
.text:0001819AB962      sub     ecx, 2
.text:0001819AB965      jz      short loc_1819AB9BC
.text:0001819AB967      sub     ecx, 1
.text:0001819AB96A      jz      short locIDC_BACKSPACE_BACK
.text:0001819AB96C      cmp     ecx, 1
.text:0001819AB96F      jnz     loc_1819AC729
.text:0001819AB975
.text:0001819AB975 locIDC_BACKSPACE_FORWARD:               ; feature
.text:0001819AB975      lea     rcx, ?kBackspaceGoesBackFeature@features@@3UFeature@base@@B
.text:0001819AB97C      call    ?IsEnabled@FeatureList@base@@SA_NAEBUFeature@2@@Z ; base::FeatureList::IsEnabled(base::Feature const &)
.text:0001819AB981      test    al, al
.text:0001819AB983      jnz     doGoForward
.text:0001819AB989      mov     dl, 1
.text:0001819AB98B      jmp     short loc_1819AB9A3
.text:0001819AB98D ; ---------------------------------------------------------------------------
.text:0001819AB98D
.text:0001819AB98D locIDC_BACKSPACE_BACK:       ; CODE XREF: chrome::BrowserCommandController::ExecuteCommandWithDisposition(int,WindowOpenDisposition)+1AAj
.text:0001819AB98D      lea     rcx, ?kBackspaceGoesBackFeature@features@@3UFeature@base@@B ; feature
.text:0001819AB994      call    ?IsEnabled@FeatureList@base@@SA_NAEBUFeature@2@@Z ; base::FeatureList::IsEnabled(base::Feature const &)
.text:0001819AB999      test    al, al
.text:0001819AB99B      jnz     doGoBack
.text:0001819AB9A1      xor     edx, edx
.text:0001819AB9A3
.text:0001819AB9A3 loc_1819AB9A3:               ; CODE XREF: chrome::BrowserCommandController::ExecuteCommandWithDisposition(int,WindowOpenDisposition)+1CBj
.text:0001819AB9A3      mov     rax, [r14+18h]
.text:0001819AB9A7      mov     rcx, [rax+0F0h]
.text:0001819AB9AE      mov     rax, [rcx]
.text:0001819AB9B1      call    qword ptr [rax+108h]   ; window()->MaybeShowNewBackShortcutBubble()
.text:0001819AB9B7      jmp     loc_1819AC7C2

What a mess!

Luckily for us, compiler decided to emit nice switch table in version v61.0.3153.2:

.text:0001806CE0C3 loc_1806CE0C3:                          ; CODE XREF: sub_1806CDF28+CFj
.text:0001806CE0C3                 lea     eax, [rbx-33000] ; jumptable 00000001806CE00B default case
.text:0001806CE0C9                 cmp     eax, 0Bh        ; switch 12 cases
.text:0001806CE0CC                 ja      loc_1806CEACC   ; jumptable 00000001806CE03B cases 2-11,13
.text:0001806CE0D2                 lea     rcx, off_1806CEC30
.text:0001806CE0D9                 movsxd  rax, dword ptr [rcx+rax*4]
.text:0001806CE0DD                 add     rax, rcx
.text:0001806CE0E0                 jmp     rax             ; switch jump

...

.text:0001806CEC30 off_1806CEC30:   
.text:0001806CEC30  B2 F4 FF FF    dd offset locIDC_BACK - 1806CEC30h
.text:0001806CEC34  5B FD FF FF    dd offset locIDC_FORWARD - 1806CEC30h ; jumptable 00000001806CE0E0 case 1
.text:0001806CEC38  91 FD FF FF    dd offset loc_1806CE9C1 - 1806CEC30h ; jumptable 00000001806CE0E0 case 2
.text:0001806CEC3C  B3 FD FF FF    dd offset loc_1806CE9E3 - 1806CEC30h ; jumptable 00000001806CE0E0 case 3
.text:0001806CEC40  D5 FD FF FF    dd offset loc_1806CEA05 - 1806CEC30h ; jumptable 00000001806CE0E0 case 4
.text:0001806CEC44  9C FE FF FF    dd offset loc_1806CEACC - 1806CEC30h ; jumptable 00000001806CE0E0 default case
.text:0001806CEC48  F5 FD FF FF    dd offset loc_1806CEA25 - 1806CEC30h ; jumptable 00000001806CE0E0 case 6
.text:0001806CEC4C  21 FE FF FF    dd offset loc_1806CEA51 - 1806CEC30h ; jumptable 00000001806CE0E0 case 7
.text:0001806CEC50  9C FE FF FF    dd offset loc_1806CEACC - 1806CEC30h ; jumptable 00000001806CE0E0 default case
.text:0001806CEC54  15 FE FF FF    dd offset loc_1806CEA45 - 1806CEC30h ; jumptable 00000001806CE0E0 case 9
.text:0001806CEC58  43 FE FF FF    dd offset locIDC_BACKSPACE_BACK - 1806CEC30h ; jumptable 00000001806CE0E0 case 10
.text:0001806CEC5C  65 FE FF FF    dd offset locIDC_BACKSPACE_FORWARD - 1806CEC30h ; jumptable 00000001806CE0E0 case 11

To make Backspace work as intended, we can simply overwrite 2 entries in jump table.

.text:0001806CEC30 off_1806CEC30:   
.text:0001806CEC30  B2 F4 FF FF    dd offset locIDC_BACK - 1806CEC30h
.text:0001806CEC34  5B FD FF FF    dd offset locIDC_FORWARD - 1806CEC30h ; jumptable 00000001806CE0E0 case 1
.text:0001806CEC38  91 FD FF FF    dd offset loc_1806CE9C1 - 1806CEC30h ; jumptable 00000001806CE0E0 case 2
.text:0001806CEC3C  B3 FD FF FF    dd offset loc_1806CE9E3 - 1806CEC30h ; jumptable 00000001806CE0E0 case 3
.text:0001806CEC40  D5 FD FF FF    dd offset loc_1806CEA05 - 1806CEC30h ; jumptable 00000001806CE0E0 case 4
.text:0001806CEC44  9C FE FF FF    dd offset loc_1806CEACC - 1806CEC30h ; jumptable 00000001806CE0E0 default case
.text:0001806CEC48  F5 FD FF FF    dd offset loc_1806CEA25 - 1806CEC30h ; jumptable 00000001806CE0E0 case 6
.text:0001806CEC4C  21 FE FF FF    dd offset loc_1806CEA51 - 1806CEC30h ; jumptable 00000001806CE0E0 case 7
.text:0001806CEC50  9C FE FF FF    dd offset loc_1806CEACC - 1806CEC30h ; jumptable 00000001806CE0E0 default case
.text:0001806CEC54  15 FE FF FF    dd offset loc_1806CEA45 - 1806CEC30h ; jumptable 00000001806CE0E0 case 9
.text:0001806CEC30  B2 F4 FF FF    dd offset locIDC_BACK - 1806CEC30h    ; jumptable 00000001806CE0E0 case 10
.text:0001806CEC34  5B FD FF FF    dd offset locIDC_FORWARD - 1806CEC30h ; jumptable 00000001806CE0E0 case 11

Mission accomplished! 🙂

In the next part of this blog series, I'll show you how to make this patch more user friendly and a few ways how to automate the patching (so that you can receive automatic Google Chrome updates, if you wish).

Till next time!
kao.