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.

CFF bugs in processing managed resources

kao

Users on tuts4you quite often ask questions like "Can you identify which obfuscator was used". When I was analyzing one such assembly, my CFF Explorer started to act erratically. New tabs would not open and on exit CFF Explorer crashed with access violation.
CFF acting up
That's weird, I said to myself and decided to figure out what's causing it.

Uninitialized buffers and unchecked return values

First bug is a classic. In pseudo-code it looks like this:

bool ReadResourceHeader(ManagedResource resource, ResourcesInfo *info)
{
   DWORD MagicNumber = ReadDword();
   if (MagicNumber != RESOURCES_MAGIC_NUMBER)
      return FALSE;

   if (FAILED(ReadSomething(&xyz)))
      return FALSE;
   info.FieldX = xyz;
   ...
   return TRUE;
}

void BuggyFunction()
{
   ResourcesInfo info;
   foreach (ManagedResource resource in exefile)
   {
      ReadResourceHeader(resource, &info);
      for (int i = 0; i < info.EntryCount; i++)
      {
          ...
      }
   }
}

First issue is that nobody initialized ResourcesInfo structure, so all fields will initially contain random garbage. As soon as ReadResourceHeader fails to read or validate something, it returns, and lots of fields will still contain random garbage.

It wouldn't be a big problem, if Daniel checked the return value of the function. But his code just continues processing even if the data initialization failed. And, to make matters worse, it just hides all exceptions by putting try-except handlers around most of the code. No wonder CFF is occasionally acting weird! πŸ™‚

This bug is quite hard to demonstrate, as it needs to have few lucky coincidences in the uninitialized data. But I'm sure that a skilled person would convert it into arbitrary code execution in no time. Not me, though... πŸ™‚

Buffer overflow

Second bug is also a classic. In pseudo-code it looks like this:

WCHAR Str[MAX_PATH];
ZeroMemory(Str, MAX_PATH * sizeof (WCHAR));
if (!DecodeInt(ptr, &NameSize, &ValueSize))
   return FALSE;
memcpy(Str, ptr, NameSize);

So, what's wrong here? Daniel takes a fixed-size buffer and initializes it with all zeros (unlike previous case). Then he reads size of data (NameSize). And then he copies NameSize bytes into a fixed-size buffer - without checking if that's gonna overflow or not.. Yikes!

Example file demonstrating this bug: https://www.mediafire.com/?x4idsa21toh0t36 (you need to click on Resource Editor -> .NET Resources to trigger the buggy code. Afterwards, CFF Explorer will start acting weird).

Solution

Just like in a previous case where I added support for ConfuserEx and undocumented fields, I had to make few binary patches to CFF Explorer.

Fixed exe file is here: Please get latest version from this post

Have fun and stay safe!

Dancing pigs – or how I won my fight with Google Chrome updates

kao

I think removing NPAPI support from Google Chrome was a really stupid decision from Google. Sure, Java and some other plugins were buggy and vulnerable. But there is a huge group of users that need to have NPAPI for perfectly legit reasons. Certain banks use NPAPI plugins for 2-factor authentication. Certain countries have made their digital government and signatures based on NPAPI plugins. And the list goes on.

I have my reasons too. If I have to run older version of Chrome for that, I will do so - and no amount of nagging will change my mind.

That’s a well known fact in security circles, named "dancing pigs":

If J. Random Websurfer clicks on a button that promises dancing pigs on his computer monitor, and instead gets a hortatory message describing the potential dangers of the applet β€” he's going to choose dancing pigs over computer security any day

Unfortunately pointy-haired managers at Google fail to understand this simple truth. Or they just don't give a crap.

Hello, I am AutoUpdate, I just broke your computer

Imagine my reaction one day when my NPAPI plugin suddenly stopped working. It just wouldn't load. It turned out that Google Chrome was silently updated by Google Update. It broke my plugin in the process and - officially - there is no way of going back.

What do you think I did next?

That's right - I disabled Google Update from services, patched GoogleUpdate.exe to terminate immediately and restored previous version of Google Chrome from the backup. Dancing pigs, remember?

Your Google Chrome is out-of-date

It worked well for few months. But this week, Chrome started nagging me again.
chrome_nag1
Quick Google search lead me to this answer: you need to disable Chrome updates using Google's administrative templates.

Let's ignore the fact that the described approach works only for XP (for Windows 7 you need to use ADMX templates which you need to copy manually to %systemroot%\PolicyDefinitions) and now there are like 4 places related to Google Chrome updates in the policies.

So, I set the policies and it seemed to work. For a day.

Your Google Chrome is still out-of-date

Imagine my joy the next day when I saw yet-another-nagscreen. Like this:
chrome_nag2

No, I don't need that update. Really!

I can close the nag, but 10 minutes later it will pop up again. And it looks like the only way to get rid of the nag is to patch chrome.dll. I really didn't want to do that but dumb decisions by Google managers are forcing my hand here.

Reversing Google Chrome

Since Chrome is more or less open-source, you can easily find the nagware message:

      
        Chrome could not update itself to the latest version, so you are missing out on awesome new features and security fixes. You need to update Chrome.
      

From here, we can find which dialog is responsible for the nag:

void OutdatedUpgradeBubbleView::Init() {
  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
  accept_button_ = new views::LabelButton(
      this, l10n_util::GetStringUTF16(
          auto_update_enabled_ ? IDS_REINSTALL_APP : IDS_REENABLE_UPDATES));
  accept_button_->SetStyle(views::Button::STYLE_BUTTON);
  accept_button_->SetIsDefault(true);
  accept_button_->SetFontList(rb.GetFontList(ui::ResourceBundle::BoldFont));
  elevation_icon_setter_.reset(new ElevationIconSetter(
      accept_button_,
      base::Bind(&OutdatedUpgradeBubbleView::SizeToContents,
                 base::Unretained(this))));

  later_button_ = new views::LabelButton(
      this, l10n_util::GetStringUTF16(IDS_LATER));
  later_button_->SetStyle(views::Button::STYLE_BUTTON);

  views::Label* title_label = new views::Label(
      l10n_util::GetStringUTF16(IDS_UPGRADE_BUBBLE_TITLE));
  title_label->SetFontList(rb.GetFontList(ui::ResourceBundle::MediumFont));
  title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);

  views::Label* text_label = new views::Label(l10n_util::GetStringUTF16(
      auto_update_enabled_ ? IDS_UPGRADE_BUBBLE_TEXT
                           : IDS_UPGRADE_BUBBLE_REENABLE_TEXT));

From there we can find NOTIFICATION_OUTDATED_INSTALL which comes from UpgradeDetector. And finally we arrive at CheckForUpgrade() procedure:

void UpgradeDetectorImpl::CheckForUpgrade() {
  // Interrupt any (unlikely) unfinished execution of DetectUpgradeTask, or at
  // least prevent the callback from being executed, because we will potentially
  // call it from within DetectOutdatedInstall() or will post
  // DetectUpgradeTask again below anyway.
  weak_factory_.InvalidateWeakPtrs();

  // No need to look for upgrades if the install is outdated.
  if (DetectOutdatedInstall())
    return;

  // We use FILE as the thread to run the upgrade detection code on all
  // platforms. For Linux, this is because we don't want to block the UI thread
  // while launching a background process and reading its output; on the Mac and
  // on Windows checking for an upgrade requires reading a file.
  BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
                          base::Bind(&UpgradeDetectorImpl::DetectUpgradeTask,
                                     weak_factory_.GetWeakPtr()));
}

This is what I want to patch! But how?

You could load Chrome DLL in IDA and try to find the offending call on your own. But I'm willing to bet that it will take you hours, if not days. Well, PDB symbols to the rescue!

Symbols for Chrome are stored at https://chromium-browser-symsrv.commondatastorage.googleapis.com and you will need to add that path to your _NT_SYMBOL_PATH. Something like this:

set _NT_SYMBOL_PATH=SRV*F:\Symbols*https://msdl.microsoft.com/download/symbols;SRV*F:\Symbols*https://chromium-browser-symsrv.commondatastorage.googleapis.com

_NT_SYMBOL_PATH is a very complex beast, you can do all sorts of things with it. If you want a more detailed explanation how it works, I suggest that you read Symbols the Microsoft Way.

After that, you can load chrome.dll in IDA, wait until IDA downloads 850MB of symbols, and drink a coffee or two while IDA is analyzing the file. After that it's all walk in the park. This is the place:

.02CF3BDF: 55                             push         ebp
.02CF3BE0: 8BEC                           mov          ebp,esp
.02CF3BE2: 83EC20                         sub          esp,020 ;' '
.02CF3BE5: 8365FC00                       and          d,[ebp][-4],0
.02CF3BE9: 56                             push         esi
.02CF3BEA: 8BF1                           mov          esi,ecx
.02CF3BEC: 57                             push         edi
.02CF3BED: 8DBE10010000                   lea          edi,[esi][000000110]

And one retn instruction makes my day so much better..

Final words

Unfortunately for me, this world is changing. You are no more the sole owner of your devices, all the big corporations want to make all the decisions for you.

Luckily for me, it is still possible to achieve a lot using a disassembler and debugger. And reverse engineering for interoperability purposes is completely legal in EU. πŸ™‚

Have fun!

Improved CFF Explorer

kao

CFF Explorer is another invaluable tool for .NET reversers. Unfortunately it is closed-source and is not actively maintained anymore.

One of the most annoying problems is that it cannot correctly process .NET metadata in some assemblies protected by ConfuserEx (and few other protectors).
CFF shows garbage
As you can see, Module data make no sense and Methods also look weird.

Cause of the problem

The problem is caused by obscure and undocumented field in Metadata Table Stream. DNLib is one of the very few tools/libraries that properly supports it:

/// 
/// MDStream flags
/// 
[Flags]
public enum MDStreamFlags : byte {
	/// #Strings stream is big and requires 4 byte offsets
	BigStrings = 1,
	/// #GUID stream is big and requires 4 byte offsets
	BigGUID = 2,
	/// #Blob stream is big and requires 4 byte offsets
	BigBlob = 4,
	/// 
	Padding = 8,
	/// 
	DeltaOnly = 0x20,
	/// Extra data follows the row counts
	ExtraData = 0x40,
	/// Set if certain tables can contain deleted rows. The name column (if present) is set to "_Deleted"
	HasDelete = 0x80,
}

...

/// 
/// Gets the  bit
/// 
public bool HasExtraData {
	get { return (flags & MDStreamFlags.ExtraData) != 0; }
}

...

ulong valid = validMask;
var sizes = new uint[64];
for (int i = 0; i < 64; valid >>= 1, i++) {
	uint rows = (valid & 1) == 0 ? 0 : imageStream.ReadUInt32();
	if (i >= maxPresentTables)
		rows = 0;
	sizes = rows;
	if (i < mdTables.Length)
		mdTables[i] = new MDTable((Table)i, rows, tableInfos[i]);
}

if (HasExtraData)
	extraData = imageStream.ReadUInt32();

This extraData field is causing us troubles.. Oh, well, it's time to fix it! πŸ™‚

Solution

Since CFF Explorer is closed-source, I had to reverse-engineer parts of it. Then I created a small code cave and added extra code that checks flag value and skips over extraData field, if necessary. If you're interested how exactly it was done, check address 004689CC and added code at 00589800.

CFF works fine
Much better, isn't it?

Download link for patched EXE: Please get latest version from this post