Zscalerのブログ
Zscalerの最新ブログ情報を受信
購読するPikabotの変遷
はじめに
Pikabotは、2023年初めに登場したマルウェア ローダーです。ThreatLabzはこの1年間、Pikabotの開発とその手口を追跡してきました。FBIによるQakbotの解体を受け、2023年下半期にはPikabotの使用が大幅に増加しました。これは、Black Bastaのランサムウェア アフィリエイトがこれまで初期アクセスに使用していたQakbotをPikabotに置き換えたためと考えられます。しかし、Pikabotは2023年のクリスマス直後に活動を停止しています(その時点でのバージョン番号は1.1.19)。
2024年2月に開始されたキャンペーンで、Pikabotはコード ベースと構造に大きな変更を加えて再登場しました。新しい開発サイクルとテスト段階にあるようですが、開発者は高度な難読化技術を廃止し、ネットワーク通信を変更することで、コードの複雑さを軽減しています。
要点
- Pikabotは、2023年の前半に初めて確認されたマルウェア ローダーで、2023年8月のQakbot解体後、非常に活発になりました。
- Pikabotの活動は2023年12月に停止しましたが、これはQakbotの新しいバージョンが登場したためと考えられています。2024年2月には、大幅にアップデートされた新しいバージョンのPikabotがリリースされました。
- Pikabotの以前のバージョンでは高度な文字列暗号化技術が使用されていましたが、現在はよりシンプルなアルゴリズムに置き換えられています。
- Pikabotは、Qakbotと同様にすべての構成要素を1つのメモリー ブロックに保存するようになりました。以前のバージョンでは、必要な場合にのみ必要な構成要素を復号していました。
- Pikabotは、コマンド&コントロールにHTTPを使用し続けていますが、ネットワーク コマンドIDや暗号化アルゴリズムなどのネットワーク プロトコルは変更されています。
技術分析
前回のブログ(Pikabotの技術分析)でも解説したように、Pikabotはローダーとコア モジュールの2つのコンポーネントで構成されており、コア モジュールはコマンドの実行とコマンド&コントロール サーバーからのペイロードの注入を担当します。このマルウェアはコード インジェクターでコア モジュールを復号し、注入します。また、さまざまな分析回避技術と文字列の難読化を採用しています。PikabotはQakbotと同様の配信方法、キャンペーン、振る舞いを使用しており、バックドアとして機能します。このマルウェアにより、攻撃者は感染したシステムを制御し、Cobalt Strikeなどの悪意のあるペイロードを配信することができます。
以降のセクションでは、Pikabotの最新バージョンの機能や以前のバージョンからの変更点などを詳しく説明します。今回の分析は、バージョン1.8.32のPikabotバイナリーで実行しました。
分析回避技術
Pikabotの最新バージョンは、以前のバージョンと同様、分析に長い時間がかかるようなさまざまな分析回避技術を採用しています。なお、以下の手法のいずれにも、大幅に高度化した機能は見られません。さらに、Pikabotの以前のバージョンのマルウェアでは、ローダー コンポーネントにより高度な検出機能が使用されていました。
文字列の暗号化
最も顕著な変更点は、文字列の難読化に関するものです。Pikabotの以前のバージョンでは、RC4アルゴリズムとAES-CBCを組み合わせて各文字列を難読化していました。この手法は、特に自動構成抽出に関して分析を防ぐうえで非常に効果的でした。Pikabotを分析するには、アナリストは暗号化された文字列だけでなく、その一意のRC4キーも検出する必要がありました。さらに、Pikabotの各ペイロードに固有のAESキーと初期化ベクトルも抽出しなければなりませんでした。なお、Pikabotマルウェアの開発者が従ったアプローチは、ADVobfuscatorに類似したものです。
最新バージョンでは、大半の文字列は各文字を取得してスタックにプッシュすることによって構築されます(図1)。まれに、一部の文字列は今でもRC4アルゴリズムのみを使用して暗号化されています。
図1. 文字列のスタック構造
ジャンク コードによる命令
この分析回避技術は、Pikabotの以前のバージョンにも実装されていました。Pikabotは、有効な命令の間にジャンク コードを挿入します。ジャンク コードは、関数内でインライン化されているか、ジャンク コードを含む関数に対して呼び出しが行われます(図2)。
図2. ジャンク コード
デバッグ回避手法
Pikabotは、デバッグ セッションを検出するために2つの手法を使用します。その手法は以下のとおりです。
- PEB (Process Environment Block)からBeingDebuggedフラグを読み取る
- Microsoft Windows API関数のCheckRemoteDebuggerPresentを呼び出す
Pikabotは、コードの特定の部分で上記のデバッグ チェックを常に実行しています。たとえば、ネットワーク データをエンコード/デコードする場合や、ネットワーク コマンドの受信をリクエストする場合などです。
サンドボックス回避手法
上記のデバッグ防止のチェックに加えて、Pikabotは以下の手法により、セキュリティ製品とサンドボックスを回避します。
- ネイティブのWindows APIコールを利用する
- コードのさまざまな段階でその実行を遅らせる。タイマーは毎回ランダムに生成されます
- 必要なすべてのWindows API関数をAPIハッシュで動的に解決する
Pythonのアルゴリズム表現は以下のとおりです。
api_name = b""
checksum = 0x113B
for c in api_name:
if c > 0x60:
c -= 0x20
checksum = (c + (0x21 * checksum)) & 0xffffffff
print(hex(checksum))
言語の検出
Pikabotは以前のバージョンと同様、OSの言語が以下のいずれかである場合、実行を停止します。
- ロシア語(ロシア)
- ウクライナ語(ウクライナ)
これは、Pikabotの背後に潜む脅威アクターがロシア語を話し、ウクライナやロシアに住んでいる可能性が高いことを示しています。言語チェックにより、これらの地域で法執行機関から刑事訴追される可能性を抑えることができます。
ボット初期化フェーズ
Pikabotの最新バージョンは、以前のバージョンとは異なり、すべての設定と情報をグローバル アドレスの1つの構造に保存します(Qakbotと同様の手法)。分析した構造を以下に示します。わかりやすくするために、構造の重要でない項目(Windows API名など)は編集しています。
struct bot_structure
{
void *host_info;
WINHTTPAPI winhttp_session_handle;
bool bot_error_init_flag;
FARPROC LdrLoadDll;
FARPROC LdrGetProcedureAddress;
FARPROC RtlAllocateHeap;
FARPROC RtlReAllocateHeap;
FARPROC RtlFreeHeap;
FARPROC RtlDecompressBuffer;
FARPROC RtlGetVersion;
FARPROC RtlRandomEx;
---redacted—
wchar_t* bot_id;
bool registered_flag;
int process_pid;
int process_thread_id;
int* unknown_unused_1;
unsigned short os_arch;
unsigned short dlls_apis_loaded_flag;
int unknown_unused_2;
unsigned char* host_rc4_key;
int number_of_swap_rounds;
int beacon_time_ms;
int delay_time_ms; // Pikabotの初期化フェーズでのみ使用されます。
int delay_seed_mul;
wchar_t* bot_version;
wchar_t* campaign_tag;
wchar_t* unknown_registry_key_name;
cncs_info* active_cnc_info;
cncs_info* cncs_list;
int num_of_cncs;
int unknown_unused_3;
int max_cnc_attempts;
wchar_t* user_agent;
void* uris_array;
void* request_headers_array;
TEB* thread_environment_block;
};
struct cncs_info
{
wchar_t* cnc;
int cnc_port;
int http_connection_settings; // 1に設定すると、サーバーの証明書の検証は無視され、WINHTTP_FLAG_SECURE | WINHTTP_FLAG_BYPASS_PROXY_CACHEのフラグが設定されます。
int connection_attempts;
bool is_cnc_unavailable;
cncs_info* next_cnc_ptr;
};
ボットの構成
Pikabotの最新バージョンでは、すべての構成がプレーンテキストで1つのアドレスに保存されますが、これは大きな欠点です。なぜなら、以前のバージョンでは、必要な各要素を実行時に必要な場合のみ復号していたからです。さらに、多くの構成要素(コマンド&コントロールURIなど)がランダム化されました。
アナリストの注記:ランダム化されているにもかかわらず、すべての構成要素はサーバー側で有効でした。ボットが誤った情報を送信した場合、そのボットはコマンド&コントロール サーバーによって拒否または禁止されます。
この構成の構造は以下のとおりです。
struct configuration
{
int number_of_swap_rounds_number_of_bytes_to_read_from_end; // ボットの初期化プロセス中、このメンバーは構成ブロックの最後から読み取るバイト数を表します。
size_t len_remaining_structure; // 残りの構造のデータから最後の要素を引いたサイズです。
wchar_t* bot_minor_version; // 32-betaなど。一部のサンプルでは、このメンバーにはボットのメジャー バージョンとマイナー バージョンの両方が含まれています。
size_t len_campaign_name;
wchar_t* campaign_name;
size_t len_unknown_registry_key_name;
wchar_t* unknown_registry_key_name; // ネットワーク コマンド0x246Fでのみ使用されます。
size_t len_user_agent;
wchar_t* user_agent;
size_t number_of_http_headers;
wchar_string request_headers[number_of_http_headers];
int number_of_cnc_uris;
wchar_string cnc_uris[number_of_cnc_uris];
int number_of_cncs;
cnc cns[number_of_cns];
int beacon_time_ms;
int delay_time_ms;
int delay_seed_mul; // この値に演算の計算値であるdelay_seed_mul * 1000を掛けます。
int maximum_cnc_connection_attempts;
size_t len_bot_version // メジャー バージョン + マイナー バージョン
wchar_t* major_version; // 1.8。
int len_remaining_bytes_to_read; // 最初のメンバーに追加され、「len_remaining_structure」の直後に読み取るバイト数を示します。
};
struct wchar_string
{
size_t length;
wchar_t* wstring;
};
struct cnc
{
size_t len_cnc;
wchar_t* cnc;
int cnc_port;
int connection_attempts;
bool http_connection_settings;
};
Pikabotはプレーンテキストの構成を解析すると、すべてのバイトをゼロに設定して消去します。Zscaler ThreatLabzは、これを構成の抽出自動化を回避するためのダンプ防止手法と考えています。
最後に、Pikabotは残りの必要なWindows API関数をロードし、侵害されたホストのボット識別子を生成します。このアルゴリズムは以前のバージョンと似ており、以下のPythonのコードで再現できます。
def checksum(input: int) -> int:
return (0x10E1 * input + 0x1538) & 0xffffffff
def generate_bot_id_set_1(host_info: bytes, volume_serial_number: int) -> int:
for current_character in host_info.lower():
volume_serial_number *= 5
volume_serial_number += current_character
bot_id_part_1 = checksum(volume_serial_number & 0xffffffff)
return bot_id_part_1
def generate_bot_id_set_2(volume_serial_number: int) -> int:
bot_id_part_2 = checksum(volume_serial_number)
bot_id_part_2 = checksum(bot_id_part_2)
return bot_id_part_2
def generate_bot_id_set_3(bot_id_part_2: int) -> int:
out = []
for i in range(8):
bot_id_part_2 = checksum(bot_id_part_2)
out.append(bot_id_part_2 & 0xff)
out = bytes(out[-4:])
return int.from_bytes(out, byteorder='little')
host_info = b"username|hostname"
volume_serial_number = int("",16)
bot_id_part_1 = generate_bot_id_set_1(host_info, volume_serial_number)
bot_id_part_2 = generate_bot_id_set_2(volume_serial_number)
bot_id_part_3 = generate_bot_id_set_3(bot_id_part_2)
bot_id = f"{bot_id_part_1:07X}{bot_id_part_2 & 0xffff:09X}{bot_id_part_3}"
アナリストの注記:一部のサンプルでは、Pikabotはボリューム シリアル番号を読み取りません。これは、コードのバグによってGetVolumeInformationWを呼び出すときにエラーが起こるためです。
ネットワーク通信
Pikabotはコマンド&コントロール サーバーに接続し、ネットワーク コマンドをリクエストおよび受信します。最新バージョンでは、ネットワーク プロトコルが大幅に変更されています。Pikabotは、侵害されたホストをサーバーに登録することから始めます。
まず、Pikabotは侵害されたホストから以下のような情報を収集します。
- モニターの表示設定
- Windowsのバージョン
- ホスト名/ユーザー名とOSのメモリー サイズ
- ビーコンと遅延の設定
- プロセスID、親プロセスID、スレッド数などのプロセス情報(すべての項目についてはネットワーク コマンド0x985の説明を参照)
- ボットのバージョンとキャンペーン名
- ドメイン コントローラー名
その後、Pikabotは登録パケットに以下の情報を追加します。
- ネットワークで使用される32バイトのRC4キー(ホストごとに一意)。セッション中は変わりません。以前のバージョンでは、リクエストごとにランダムなキー/IVでAES-CBCを使用していました。
- 不明なレジストリー キー名。IDが0x246Fのネットワーク コマンドでのみ使用されていることが確認されました。
- データのエンコーディングに使用されるスワップ ラウンド数。セッション中、変更されることはありません。
次に、PikabotはRC4アルゴリズムによってデータを暗号化し、暗号化された出力をエンコードし、リストからランダムなURIを選択します。そして、POSTリクエストで、データをコマンド&コントロール サーバーに送信します。
エンコーディングには、N回のバイト スワッピングが含まれます。Nには、0から25までのランダムに生成された数値が入ります。
アナリストの注記:構成に端数のない数字が設定されても(構成の構造を参照)、この値は無視され、Pikabotがランダムな値に置き換えます。さらに、Pikabotはネットワーク パケットのJSON形式を完全に削除し、すべてを生データの形式で挿入します。
ボットの登録が成功すると、Pikabotはコマンドをリクエストして実行するための無限ループを開始します。
各着信ネットワーク コマンド(ID 0x164のネットワーク コマンド以外)には、タスクIDがあり、このIDは(復号された)パケットの先頭にQWORD値として配置されます。以下の表1に、識別されたネットワーク コマンドとその機能の説明を示します。
コマンドID | 概要 |
---|---|
0x164 | コマンド&コントロール サーバーからコマンドをリクエストします。パケットには、コマンドID、ボットIDのサイズ、ボットIDが含まれます。ボットが実行するネットワーク コマンドがない場合、サーバーは同じコマンドIDで応答します。 |
0x555 | 実行されたネットワーク コマンドの出力をコマンド&コントロール サーバーに報告します。 |
0x1291 | ボットを登録します。不明な整数値(0x1687)がパケットのオフセット8に追加されます。 |
0x1FED | ビーコン タイムを更新します。 |
0x1A5A | ボットを終了/強制終了させます。 |
0x2672 | 実装されていません。 |
0x246F | ファイルをディスクに書き込み、構成で指定された値の名前(unknown_registry_key_name)を使用し、レジストリー データを追加します。 |
0xACB | システム コマンドを実行し、出力を送り返します。出力がない場合は、エラー コード0x1B3を含めます。 |
0x36C | ダウンロードしたPEファイルのコードを挿入します。対象となるプロセス情報は、ネットワーク パケットで指定されます。 |
0x792 | ダウンロードしたシェルコードのコードを挿入します。対象となるプロセス情報は、ネットワーク パケットで指定されます。 |
0x359 | システム コマンドを実行し、出力を送り返します。 注記:0xACBと同じですが、エラー コードは送信されません。 |
0x3A6 | システム コマンドを実行し、出力を送り返します。 注記:0xACBと同じですが、エラー コードは送信されません。 |
0x240 | システム コマンドを実行し、出力を送り返します。 注記:0xACBと同じですが、エラー コードは送信されません。 |
0x985 | プロセスの情報を収集します。収集する情報は以下のとおりです。
|
0x982 | 実装されていません。 |
表1. Pikabotのネットワーク コマンド
まとめ
Pikabotの活動は現在活発ではないものの、依然として重大なサイバー脅威であり、常に開発が行われています。しかし、開発者は別のアプローチを採用して高度な難読化機能を廃止し、Pikabotのコードの複雑さのレベルを下げています。また、当社のコード分析から、特定の機能やネットワーク コマンドはまだ実装されておらず、現在も進行中であることがわかっています。
Zscaler ThreatLabzはこの脅威を引き続き追跡し、お客様を保護するために検出機能を追加していく予定です。
侵害の痕跡(IoC)
SHA256 |
概要 |
---|---|
555687ca3149e23ee980a3acf578e0572da556cf34c87aecf48596834d6b496f |
Pikabotサンプル(バージョン1.8.32のベータ版) |
ca5fb5814ec62c8f04936740aabe2664b3c7d036203afbd8425cd67cf1f4b79d |
Pikabotサンプル(バージョン1.8.32のベータ版) |
IOC |
概要 |
---|---|
104.129.55[.]103:2224 |
コマンド&コントロール サーバー |
178.18.246[.]136:2078 |
コマンド&コントロール サーバー |
158.220.80[.]167:2967 |
コマンド&コントロール サーバー |
104.129.55[.]104:2223 |
コマンド&コントロール サーバー |
23.226.138[.]161:5242 |
コマンド&コントロール サーバー |
37.60.242[.]85:9785 |
コマンド&コントロール サーバー |
23.226.138[.]143:2083 |
コマンド&コントロール サーバー |
37.60.242[.]86:2967 |
コマンド&コントロール サーバー |
85.239.243[.]155:5000 |
コマンド&コントロール サーバー |
158.220.80[.]157:9785 |
コマンド&コントロール サーバー |
65.20.66[.]218:5938 |
コマンド&コントロール サーバー |
95.179.191[.]137:5938 |
コマンド&コントロール サーバー |
139.84.237[.]229:2967 |
コマンド&コントロール サーバー |
Zscalerの防御策
Zscalerの多層クラウド セキュリティ プラットフォームは、サンドボックス検知に加え、以下の脅威名でPikabotに関連する指標をさまざまなレベルで検知します。