← PC Reverse Engineering

Reverse Engineering a Browser's Encrypted Credential Database

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.


Reverse Engineering a Browser’s Encrypted Credential Database

Foreword

The goal

In some security engagements you need to perform forensics on a target host, and the browser is often a goldmine of information. I also wanted to sharpen my reverse engineering skills, so this writeup came out of that. I’m still early in my RE journey — corrections are welcome.

The target

The target browser

First layer of decryption

SQLite3 basics

The functions you need to know from the SQLite3 library:

  1. sqlite3_open:
int sqlite3_open(const char *filename,sqlite3 **ppDb);
  1. sqlite3_key: if the database is encrypted, you must call this first with the correct key before doing anything else. If the database isn’t encrypted, calling this and then operating on the DB instead produces “this database is encrypted or is not a database file” errors.
int sqlite3_key( sqlite3 *db, const void *pKey, int nKey)

The MachineGuid key

Searching strings across all modules, chrome.dll contains "sqlite3_open/assis2.db" and a series of related SQL statements. I started from sqlite3_open.

The key thing: in SQLite3, if a database was encrypted at creation, you must run sqlite3_key to decrypt it before any SQL operation. So sqlite3_open is followed by sqlite3_key to decrypt the database:

sqlite3_open followed by sqlite3_key

Why do I identify sub_105C03A3 as sqlite3_open? If you step inside you’ll find the same "main" / "temp" string features as the SQLite3 source — the default database name is always "main":

The “main” / “temp” string features

And sqlite3_open in the SQLite3 library has these signature features:

sqlite3_open signature

Next, sub_105C03BA does the SQLite3 decryption. It takes the database handle, and both guid and guid_length show up:

sub_105C03BA(**sqlite3_obj, guid, guid_length);

The decrypt function with guid and guid_length

Under dynamic debugging you can see the same thing: the arguments are the database handle and a machine-serial string (the MachineGuid) plus its length, used for decryption:

Dynamic view of the MachineGuid argument

With the API monitor tool you can also see this string is a serial-number value from the registry:

The registry serial value

Second layer of decryption

That wraps up the first layer. The second layer is Base64 encoding plus AES128-ECB encryption.

Locating the key analysis point

I located the SQLite3 insert statement. The idea: at insert time the program must first encrypt the password, then assemble everything and execute the SQL, so an encryption call is bound to appear around here:

798497D7  |.  68 AEA1A77B   push chrome.7BA7A1AE;

Insert into [tb_account (domain,username,password,last_modify_time)values(?,?,passencode(?, ?), ?);

Breakpoint at the insert statement

Then I set a breakpoint on the SQL statement and stepped through. It first validates the domain length format, then the username length format, then the password — one check after another, along with SQLite’s own transaction rollback handling. Worth stepping through yourself:

Domain length validation

Username length validation

Only further down does the actual password encryption begin:

553A98CA    E8 0D1E1EFF     call chrome.5458B6DC ; password encryption starts here

The password encryption call

Stepping in, right before the encryption it pushes a fixed string, which is later concatenated with another string and used for encryption.

Odd/even password obfuscation

Keep single-stepping and you’ll find this CALL:

553A47B3    E8 AA020000     call chrome.553A4A62; password obfuscation CALL

The obfuscation call

The and here makes the lowest bit 0/1, which decides between two branches:

082D4E40    81E1 FE000000   and ecx,0xFE                                    ; take the low 2 bits
080B4E34    E8 A7A20302     call chrome.0A0EF0E0                     ; compute eax
080B4E39    89C1            mov ecx,eax                              ; ecx = eax
080B4E3B    C1E9 1F         shr ecx,0x1F                             ; ecx = ecx >> 0x1f
080B4E3E    01C1            add ecx,eax                              ; ecx = ecx + eax
080B4E40    81E1 FE000000   and ecx,0xFE                             ; ecx = ecx & 0xFE
080B4E46    29C8            sub eax,ecx                              ; eax = ah
080B4E48    89C3            mov ebx,eax                              ; ebx = ah
080B4E4A    8945 D8         mov dword ptr ss:[ebp-0x28],eax
080B4E4D    8D443F 02       lea eax,dword ptr ds:[edi+edi+0x2]       ; password length * 2 + 2
080B4E51    FEC3            inc bl                                   ; ebx = ebx + 1
080B4E53    8945 DC         mov dword ptr ss:[ebp-0x24],eax          ; push password length
080B4E56    50              push eax                                 ; push password length
080B4E57    E8 E0740002     call chrome.0A0BC33C
080B4E5C    83C4 04         add esp,0x4
080B4E5F    89C1            mov ecx,eax
080B4E61    8945 E0         mov dword ptr ss:[ebp-0x20],eax
080B4E64    FF75 DC         push dword ptr ss:[ebp-0x24]             ; push password length
080B4E67    31C0            xor eax,eax
080B4E69    50              push eax
080B4E6A    51              push ecx
080B4E6B    E8 80940102     call chrome.0A0CE2F0
080B4E70    83C4 0C         add esp,0xC
080B4E73    8B45 E0         mov eax,dword ptr ss:[ebp-0x20]
080B4E76    85FF            test edi,edi                             ; is the password length non-zero?
080B4E78    885D DC         mov byte ptr ss:[ebp-0x24],bl
080B4E7B    8818            mov byte ptr ds:[eax],bl
080B4E7D    7E 65           jle short chrome.080B4EE4
080B4E7F    8B5D E0         mov ebx,dword ptr ss:[ebp-0x20]          ; where the encrypted password is stored; each iteration puts a \x01 or \x02 in ebx

The final value at [ebp-0x28] drives the branch below and decides how the obfuscation byte is generated. It goes through an odd/even check: if [ebp-0x28] is \x02, it reads the even-indexed bytes; if \x01, the odd-indexed bytes:

08404E83    807D D8 00      cmp byte ptr ss:[ebp-0x28],0x0
08404E87    74 2E           je short chrome.08404EB7
08404E89    807D DC 02      cmp byte ptr ss:[ebp-0x24],0x2
08404E8D    75 51           jnz short chrome.08404EE0

The odd/even branch

AES128-ECB encryption

Next comes key generation. Here it pushes another serial-like string; together with the one pushed earlier during encryption, the two get concatenated to produce the AES key:

553A47E8    E8 77FEFFFF     call chrome.553A4664    ; generates a 32-byte value, likely the key

Key generation call

Stepping into this call, you see the two strings being concatenated:

Concatenating the two strings

Then the key generation begins:

Key generation

There are two main calls: one turns the concatenated string into a byte array, the other converts that byte array’s hex values into a string. The algorithm takes the last eight bytes of the two concatenated fixed serials and encrypts them into a byte array:

08784713    E8 C82B12FF     call chrome.078A72E0                            ; produces the byte array used to derive the AES key

This call converts the byte array’s hex into a string:

Hex-to-string conversion

08784720    E8 CBC47F00     call chrome.08F80BF0                            ; produces the AES key

Then the AES encryption call — it pushes the obfuscated password and the AES128-ECB key:

553A4860    E8 66E2FFFF     call chrome.553A2ACB ; AES encryption CALL

The AES encryption call

Base64 encoding

After AES, it generates a Base64 encoding (needed because the result is stored in the database):

553A4933    E8 988120FF     call chrome.545ACAD0  ; produces the base64 encoding

The Base64 encoding uses the default alphabet:

The Base64 alphabet

At this point AES128-ECB and Base64 are done. Finally it concatenates the fixed string (4B01F200ED01) with the Base64 result:

553A495A    E8 458613FF     call chrome.544DCFA4; concatenate the (4B01F200ED01) serial and the base64 output

Concatenating the fixed prefix and Base64

Summary

Simulating the decryption

The local machine’s MachineGuid:

5cbfe6e6-21aa-40df-8b1e-XXXXXXXXXXXXX

MachineGuid

Decryption result

Reading every other byte

At this point the output looks like garbage — and it is. The browser itself adds a layer of obfuscation, the same one described above. Read every other byte and you get the example password 222222.

The first byte’s \x01 / \x02 handling matters:

The \x01 / \x02 first-byte handling

Implementing the decryption

The whole decryption chain:

MachineGuid -> Base64 -> aes128 ecb -> odd/even de-obfuscation

Get the MachineGuid

MachineGuid = pomocna.getKey(@"SOFTWARE\Microsoft\Cryptography", "MachineGuid", 2);

dbPath = pomocna.getKey(@"[redacted]SeSES\DefaultIcon", null, 0).Split(',')[0].Replace(@"[redacted]se6\Application\[redacted]se.exe", "") + @"[redacted]se6\User Data\Default\apps\LoginAssis\assis2.db";

Base64 decode

Convert.FromBase64String

AES decrypt

        public string DecryptAes(string text)
        {

            byte[] src = Convert.FromBase64String(text);
            RijndaelManaged aes = new RijndaelManaged();
            byte[] key = Encoding.ASCII.GetBytes("");
            aes.KeySize = 128;
            //aes.IV = Encoding.UTF8.GetBytes("");//
            //aes.Padding = PaddingMode.Zeros;//
            //aes.BlockSize = 128;//
            aes.Padding = PaddingMode.None;
            aes.Mode = CipherMode.ECB;
            using (ICryptoTransform decrypt = aes.CreateDecryptor(key, null))
            {
                byte[] dest = decrypt.TransformFinalBlock(src, 0, src.Length);
                decrypt.Dispose();
                return Encoding.UTF8.GetString(dest);
            }
        }

Odd/even detection and reading

                if (passItem[0] == '\x02')
                {
                    for (int p = 0; p < passItem.Length; p++)
                    {
                        if (p % 2 == 1)
                        {
                            _stringb.Append(passItem[p]);
                        }
                    }
                    dr["password"] = _stringb;
                }
                else
                {
                    for (int p = 1; p < passItem.Length; p++)
                    {
                        if (p % 2 != 1)
                        {
                            _stringb.Append(passItem[p]);
                        }
                    }
                    dr["password"] = _stringb;
                }
            }

A helper tool on GitHub

There’s a one-click helper for grabbing this browser’s passwords: a Cobalt Strike script plus a decryption utility designed to save red-team effort by downloading the browser database and recording the key for offline decryption. 360SafeBrowsergetpass.cna records the information needed for offline decryption, and 3Bpass does the password decryption (supporting both local auto-export and offline decryption).

GitHub: https://github.com/hayasec/360SafeBrowsergetpass

The GitHub tool

This writeup is for technical sharing only — stay legal, and nothing here is the author’s responsibility.

The tools are for authorized penetration testing only — stay legal, and nothing here is the author’s responsibility.


Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch