Shift Browser - When a Password Importer Looks Exactly Like an InfoStealer
Valid code-signing certificates now sit on enough real malware that a green signature answers nothing, so when a client sent me an EV-signed installer that renamed itself on every download and tripped a sandbox as an infostealer, I unpacked and read all 536 MB of it.
Overview
A client sent me a 173 MB executable with three things stacked against it. The filename changed on every download, a sandbox scored it as stealer activity, and seventeen of sixty eight engines flagged it, тhe expectation was a targeted attack.
The file is also signed with a valid Extended Validation certificate issued by DigiCert to a real registered Canadian company, and that is exactly the part I could not lean on. Signed malware has stopped being remarkable, an EV certificate has become a distribution mechanism rather than a guarantee, and the last case I worked was a Colorado shell company that bought one fourteen days before shipping a Python C2 agent through it. A signature tells you who built something, it does not tell you what that something does, so the only honest way to answer the client was to open the package and read every byte inside it.
The installer was built with Inno Setup 7, so reaching the contents meant working out the container format first.
The Certificate
status Valid, signature verified
subject CN=Shift Technologies Inc, O=Shift Technologies Inc
location Victoria, British Columbia, CA, Private Organization
registration SERIALNUMBER=BC1497351
issuer DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1
serial 0B08348496F9EA0BFADC2F629570407C
valid 2025-11-13 to 2028-11-15
signed 2026-08-27 12:30:00 UTC, compiled 2026-08-10 09:40:38 UTCChain validation with online revocation checking across the whole chain returned zero errors, so the certificate is live and DigiCert has not pulled it.
One structural detail decides how much that signature is worth here. The certificate table sits at offset 181,540,352 with a size of 8,992 bytes, which sums to exactly the 181,549,344 byte file, so the security directory runs to the last byte and nothing is appended past it. The Authenticode hash therefore covers the entire 179 MB payload, and nobody has modified this file since it left the vendor.
That narrows the question usefully, this is not a case of an attacker bolting a payload onto a signed binary, whatever is inside was put there by Shift Technologies and signed by them, which turns a hidden-implant question into a vendor-integrity question.
The Filename That Changes
The strongest argument was that every download produced a different name, which is a reasonable thing to distrust, since server-side repacking is how families defeat hash-based detection.
The names are real enough, Shift_t1u5a1.exe, Shift_m9nhmk.exe, Shift_kq3jew.exe, Shift_48m2dg.exe and about thirty more. They all resolve to one SHA256, and one of the names in that same set is Shift-Browser-147.1.3-Production.exe, the official release filename.
So the name rotates and the bytes do not, which is a different thing entirely. The mechanism is visible in the binary’s own network configuration, an install-attribution service at attribution[.]shiftapis[.]com that stamps each download for affiliate tracking, and the PKCS#7 confirms it from the other direction, carrying a single unauthenticated attribute that is just the DigiCert timestamp token, with no per-download tag and no slack in the certificate table.
A build repacked per download would need a fresh signature and a fresh timestamp each time, this one was signed once.
Inno Setup 7
The installer is a Delphi MZP stub with 99.07 percent of the file sitting in the overlay, and innoextract refuses it.
Warning: Unexpected setup loader revision: 2
Warning: Setup loader checksum mismatch!
Could not determine setup data version!That reads like tampering until you find the version string in the overlay, Inno Setup Setup Data (7.0.0.3), which is a byte-exact match for the SetupID constant in the current jrsoftware/issrc main branch. The tool is not being defeated, it is simply older than the format, since innoextract 1.9 supports up to 6.0.5.
Two changes matter when parsing it.
TCompressedBlockHeader = packed record
StoredSize: Int64; { LongInt in Inno 6, widened in 7 }
Compressed: Boolean;
end;The first is that widened field, the second is a new TSetupEncryptionHeader inserted between SetupID and the first compressed block. Rather than guess its size I used the CRC as an oracle, brute-forcing lengths until CRC32 over the candidate matched the stored value, which landed on 49 bytes and gave up its contents at the same time, EncryptionUse = euNone, a zeroed KDF salt and 220,000 iterations, so the package is not password protected.
With that, the block reader is small. A four byte CRC, the nine byte header, then chunks of at most 4,096 bytes each preceded by their own CRC32, and every checksum validated on the first run. The header block at offset 179,571,603 stored 175,269 bytes and decompressed under LZMA1 to 724,726 bytes of install manifest, and the data section at offset 1,692,672 opened with the zlb magic and standard LZMA properties into a single solid stream of 536,141,101 bytes that ended on a clean end-of-stream marker.
What Is Actually Inside
The manifest reads as a Chromium tree, and the component names map one to one onto a stock build, shift.exe for chrome.exe, shift.dll for chrome.dll, shift_elf.dll for chrome_elf.dll, shift_wer.dll for chrome_wer.dll, alongside notification_helper.exe, shift_pwa_launcher.exe and several hundred locale .pak files carrying the gendered variants modern Chromium ships. A path to resources\wexond\injection\ names the upstream project the fork is built on.
Extracting the decompressed bit found fourteen PE files, and the signer column is the answer to the client’s question.
13 Shift Technologies Inc, all on serial 0B08348496F9EA0BFADC2F629570407C
1 Microsoft Windows Code Signing PCA 2024
0 unsigned
0 signed by anyone elseIts section layout settles the authenticity question better than the signature does.
.text 241,443,603 .rdata 29,607,128 .pdata 6,817,860
CPADinfo LZMADEC malloc_h prot .fptable _RDATACPADinfo, LZMADEC, malloc_h and prot are emitted by Chromium's own build system, and you do not get those by wrapping something else in a browser-shaped package.
The Injection Layer
A browser fork that injects its own JavaScript into pages is where this kind of product monetises, so the wexond/injection path was the first thing I read properly.
There is one injection helper, it targets one Google domain, and its entire job is hiding a banner.
const CHROME_WEB_STORE_URL = "https://chromewebstore.google.com/";
const DEFAULT_PROMO_HIDE_SELECTORS = [
'div[role="dialog"][aria-labelledby="promo-header"]',
'div[aria-label="Info"]:has(button[aria-label*="Switch to Chrome to install extensions and themes"])'
];Google shows non-Chrome Chromium browsers an upsell when you try to install an extension, and this hides it, which is a competitive UI decision that Brave, Vivaldi and Opera all make in their own ways.
Get devmihaylov’s stories in your inbox
Join Medium for free to get updates from this writer.
The selectors can be overridden from the server, which sounds like a remote code path until you read the guard sitting next to the schema.
const UNSAFE_CSS_SELECTOR_CHARS = /[{}]/;Braces are rejected, the payload is schema-validated, and the server can therefore supply a CSS selector and nothing else, which is a deliberate constraint on their own backend rather than an oversight.
Why a Sandbox Called It a Stealer
This is the part worth keeping, because it generalises well past this sample.
NSS_Init 1
PK11SDR_Decrypt 1
logins.json 1
Login Data 2
importer 301
ProfileImport 33That first trio is verbatim upstream Chromium firefox_importer.cc, the code that reads Firefox's saved passwords so a newly installed browser can import them on first run. It ships in Chrome, Edge, Brave and every other derivative.
To a behavioural sandbox it is indistinguishable from an infostealer, because it is the same API sequence against the same files for the same data, and the only difference is intent, which a sandbox cannot see. Any Chromium fork with an importer will earn a stealer tag from heuristics that key on credential-store access, and that is what happened here.
The things a real stealer needs were all absent.
wallet.dat 0 Electrum 0 api.telegram.org 0 X-Secret 0
/gate.php 0 chat_id 0 AmsiScanBuffer 0 EtwEventWrite 0
Add-MpPreference 0 ExclusionPath 0Dynamic Analysis
Static analysis of a 241 MB custom fork cannot prove a negative, so the last step was a controlled run in virtual environment with the telemetry hosts redirected to a forwarding TLS proxy holding a throwaway CA, which logs the requests and relays them to the genuine servers by pinned address so the real responses come back too.
One hundred and seventeen requests, all HTTP 200, no upstream errors.
POST api.mixpanel.com/track 69
POST api.mixpanel.com/engage 41
GET config.shiftapis.com/ip 6
POST config.shiftapis.com/config 1The single /config call is the one that matters, returning 6,927 bytes of feature flags, search engine definitions and A/B assignments, with no commands, no download URLs and no executable content of any kind. The injection selectors it pushed were the two already recovered statically, plus a sync_sign_in pair that selects email inputs and a login button, scoped to the vendor's own Cognito sign-in flow at login[.]shift[.]com.
Nothing sensitive left the machine, every captured body was searched for credential stores, cookie values, card fields, wallet artifacts, clipboard contents, keystrokes and screenshots, and the only hit on password across all 117 requests was a UI telemetry field reading "passwords_button":"absent".
The Business Model
The config response is where the adware classification stops being a label and becomes a mechanism.
"engine_yahoo": { "search_url": "...hspart=shift&hsimp=yhs-shift...", "monetized": true }
"engine_startpage": { "search_url": "...segment=startpage.shift...", "monetized": true }
"engine_1": { "name": "Google", "monetized": false }
"amazon_url": "https://wbd_ol.ampxdirect.com/amazon?sub1=default&sub2=amazon"Search revenue through partner tags, an affiliate redirect for Amazon, and a campaign identifier, type=0_2000_100_1000_100_260907, threaded through each monetised engine.
The split tests running against users are the more telling half.
split_st4573_improve_bring_to_foreground variation2
split_st5063_bring_to_foreground_maximized variation
split_st1232_rename_shortcuts_shift_browser variation
split_st1236_pin_profile_app_to_taskbar control
split_st6488_amazon_monetized_prepinned controlExperiments on forcing the window to the foreground, renaming the user’s existing shortcuts, pinning itself to the taskbar and pre-pinning a monetised Amazon link, all assigned server side, per install.
Persistence
HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
ShiftAutoLaunch_AEB9266706744FDEDD3B5B1030D174D8 =
"%LOCALAPPDATA%\Shift\chromium\shift.exe" --no-startup-window /prefetch:5--no-startup-window is the detail, since the browser starts at every logon and stays resident with no window to tell the user it is running. Alongside it the product registers under Clients\StartMenuInternet, claims http, https, ftp and mailto, and takes .html, .pdf, .svg and .webp.
Indicators
sha256 a0e01cca56080aaa58a4694559a84d5af3e9c07541c26be722569940ff27ac95 installer
sha256 ebfb3146488e77741d8c7b8aeb588c2f72e6b7833bd1a99692990517c5549b8c shift.dll
cert serial 0B08348496F9EA0BFADC2F629570407C
cert thumbprint 0C9A1B5FD117CB11BF7D5E624B20E458F6BCFBF4
cert CN=Shift Technologies Inc, Victoria, British Columbia, BC1497351
domain config[.]shiftapis[.]com feature flags, split tests, /ip
domain attribution[.]shiftapis[.]com install attribution, randomised download names
domain api[.]mixpanel[.]com product analytics
domain wbd_ol[.]ampxdirect[.]com Amazon affiliate redirect
domain shiftntp[.]com new tab target for monetised engines
reg HKCU\...\CurrentVersion\Run\ShiftAutoLaunch_<32 hex>
reg HKCU\SOFTWARE\Shift\pv = 147.1.3.414
reg HKCU\SOFTWARE\Shift\BLBeacon\version = 147.0.7727.56
path %LOCALAPPDATA%\Shift\chromium\
tag type=0_2000_100_1000_100_260907, hspart=shift, segment=startpage.shiftDetection
Hashes are the weakest signal here, because the vendor ships a new build whenever Chromium moves and the filename is randomised at the CDN anyway. The durable markers are structural.
On the endpoint the autostart value name is distinctive, ShiftAutoLaunch_ followed by 32 hex characters under the user's Run key, pointing at a browser launched with --no-startup-window. The install tree is equally recognisable, a Chromium layout under %LOCALAPPDATA% whose binaries are named shift.dll, shift_elf.dll and shift_wer.dll rather than their upstream equivalents.
On the wire the split-test names are the cleanest fingerprint, since split_st prefixed keys posted to config[.]shiftapis[.]com appear nowhere else, and the monetised search URLs carry hspart=shift and segment=startpage.shift in plain sight.
The certificate serial identifies every build the vendor has shipped on it, which is the right granularity for an inventory question, though it is a publisher identifier rather than a threat one.
Summary
Three signals pointed at malware, and all three had ordinary explanations. The rotating filename was affiliate attribution wrapped around one unchanging hash, the stealer verdict was Chromium’s own Firefox password importer doing the job it ships to do, and the seventeen detections were adware and PUA labels from every engine that named a family at all.
What is left after the unpacking is a real Chromium browser with a real certificate, monetised search partners, an affiliate redirect, silent autostart and a set of live experiments on how aggressively it can put itself in front of you.
That.. is in fact an adware.
Bytes never lie.









