# FortiClientの隠しダウンロードプロトコルのリバースエンジニアリング
Fortinetは無料のFortiClient製品への直接ダウンロードリンクを公開していませんが、オンラインインストーラーまたはサポートポータル(ライセンスが必要?)からダウンロードできます。オンラインインストーラーは実行時にソフトウェアをダウンロードするため、無料製品としては不必要に複雑に思えました。そこで、裏で何が起こっているのか調べてみることにしました。
調査の結果、カスタムバイナリプロトコル、OpenSSL実装、そしてエンタープライズグレードのネットワークを備えたマルチサーバーインフラストラクチャが見つかりました。これは、アクセス制御がユーザーの利便性よりも重要である場合にのみ意味をなす過剰なエンジニアリングです。以前は直接ダウンロードできたことから、その通りであると思われます。
## Ghidra による初期バイナリ解析
`FortiClientVPNInstaller.exe` を Ghidra にロードすると、すぐにネットワーク動作に変化が見られました。インポートテーブルには、ソケットレベルの関数がいくつか表示されていました。
![[b29bbdd669d7595454c9b2b01199c35b.png]]
`WININET.DLL`、`WINHTTP.DLL`、`URLMON.DLL` といった標準的な Windows HTTP ライブラリが欠けています。このインストーラは、raw ソケットレベルでネットワーク処理を実装していますが、ファイルのダウンロードには過剰な機能に思えます。
文字列解析により、さらに興味深い事実が判明しました。バイナリには、OpenSSL 内部への参照が随所に散在していました。
![[e5a0060bd9afd71eb42c20d7ccb9967f.png]]
これらの文字列は、FortiClient がネットワークに OpenSSL の BIO (Binary Input/Output) 抽象化レイヤーを使用していることを示しています。
逆コンパイルされたコードは、接続プロセス全体を管理する複雑なステートマシンを示していました。
```c
int FUN_00719270(int param_1, int *param_2)
{
code *pcVar1;
uint uVar2;
int iVar3;
int iVar4;
int iVar5;
undefined8 uVar6;
undefined4 uVar7;
undefined4 uVar8;
iVar5 = -1;
iVar4 = *param_2;
pcVar1 = (code *)param_2[7];
do
{
if (6 < iVar4 - 1U)
{
LAB_00719652:
if (pcVar1 == (code *)0x0)
{
return iVar5;
}
iVar4 = (*pcVar1)(param_1, *param_2, iVar5);
return iVar4;
}
switch (iVar4 - 1U)
{
case 0:
if ((param_2[2] == 0) && (param_2[3] == 0))
{
FUN_005a2510();
FUN_005a2610("crypto\\bio\\bss_conn.c", 0x5a, "conn_state");
FUN_005a26c0(0x20, 0x90, "hostname=%s service=%s", param_2[2], param_2[3]);
goto LAB_00719652;
}
*param_2 = 2;
break;
case 1:
iVar4 = param_2[1];
if (iVar4 == 4)
{
uVar8 = 2;
}
else if (iVar4 == 6)
{
uVar8 = 0x17;
}
else
{
if (iVar4 != 0x100)
{
FUN_005a2510();
FUN_005a2610("crypto\\bio\\bss_conn.c", 0x7b, "conn_state");
FUN_005a26c0(0x20, 0x92, 0);
goto LAB_00719652;
}
uVar8 = 0;
}
iVar4 = FUN_005cb430(param_2[2], param_2[3], 0, uVar8, 1, param_2 + 5);
if (iVar4 == 0)
goto LAB_00719652;
if (param_2[5] == 0)
{
FUN_005a2510();
FUN_005a2610("crypto\\bio\\bss_conn.c", 0x84, "conn_state");
FUN_005a26c0(0x20, 0x8e, 0);
goto LAB_00719652;
}
param_2[6] = param_2[5];
*param_2 = 3;
break;
case 2:
uVar7 = 0;
uVar8 = FUN_005cb120();
FUN_005cb170(param_2[6], uVar8, uVar7);
uVar8 = FUN_005cb0d0(param_2[6]);
iVar5 = FUN_00622a80(uVar8);
if (iVar5 == -1)
{
FUN_005a2510();
FUN_005a2610("crypto\\bio\\bss_conn.c", 0x90, "conn_state");
iVar4 = param_2[3];
uVar6 = CONCAT44(param_2[2], "calling socket(%s, %s)");
iVar3 = WSAGetLastError();
FUN_005a26c0(2, iVar3, uVar6, iVar4);
FUN_005a2510();
FUN_005a2610("crypto\\bio\\bss_conn.c", 0x93, "conn_state");
FUN_005a26c0(0x20, 0x76, 0);
goto LAB_00719652;
}
*(int *)(param_1 + 0x24) = iVar5;
*param_2 = 4;
break;
case 3:
FUN_005957f0();
FUN_005a3ff0();
uVar2 = param_2[4] | 4;
uVar8 = FUN_005cb0c0();
iVar5 = FUN_006224f0(*(undefined4 *)(param_1 + 0x24), uVar8, uVar2);
*(undefined4 *)(param_1 + 0x20) = 0;
if (iVar5 == 0)
{
iVar4 = FUN_00622230(0);
if (iVar4 != 0)
{
FUN_00596c30();
*param_2 = 6;
*(undefined4 *)(param_1 + 0x20) = 2;
FUN_005a3dc0();
goto LAB_00719652;
}
iVar4 = FUN_005cb110(param_2[6]);
param_2[6] = iVar4;
if (iVar4 == 0)
{
FUN_005a2f70();
FUN_005a2510();
FUN_005a2610("crypto\\bio\\bss_conn.c", 0xb1, "conn_state");
iVar4 = param_2[3];
uVar6 = CONCAT44(param_2[2], "calling connect(%s, %s)");
iVar3 = WSAGetLastError();
FUN_005a26c0(2, iVar3, uVar6, iVar4);
*param_2 = 7;
}
else
{
FUN_006224d0(*(undefined4 *)(param_1 + 0x24));
*param_2 = 3;
FUN_005a3dc0();
}
}
else
{
FUN_005a2f70();
LAB_00719486:
*param_2 = 5;
}
break;
case 4:
iVar5 = 1;
goto LAB_00719652;
case 5:
FID_conflict:
__time64((__time64_t *)0x0);
iVar4 = FUN_005caf50(*(undefined4 *)(param_1 + 0x24), 0);
if (iVar4 != 0)
{
iVar4 = FUN_005cacb0(*(undefined4 *)(param_1 + 0x24));
if (iVar4 == 0)
goto LAB_00719486;
uVar8 = 0xf;
iVar3 = param_1;
FUN_005957f0();
iVar3 = FUN_005cb110(param_2[6], iVar3, uVar8);
param_2[6] = iVar3;
if (iVar3 == 0)
{
FUN_005a2510();
FUN_005a2610("crypto\\bio\\bss_conn.c", 0xcd, "conn_state");
FUN_005a26c0(2, iVar4, "calling connect(%s, %s)", param_2[2], param_2[3]);
FUN_005a2510();
FUN_005a2610("crypto\\bio\\bss_conn.c", 0xd0, "conn_state");
FUN_005a26c0(0x20, 0x6e, 0);
iVar5 = 0;
goto LAB_00719652;
}
FUN_006224d0(*(undefined4 *)(param_1 + 0x24));
*param_2 = 3;
}
break;
case 6:
FUN_005a2510();
FUN_005a2610("crypto\\bio\\bss_conn.c", 0xe2, "conn_state");
FUN_005a26c0(0x20, 0x67, 0);
iVar5 = 0;
goto LAB_00719652;
}
if ((pcVar1 != (code *)0x0) && (iVar5 = (*pcVar1)(param_1, *param_2, iVar5), iVar5 == 0))
{
return 0;
}
iVar4 = *param_2;
} while (true);
}
```
この関数は複数の接続状態を管理し、エラーを処理し、再試行ロジックを実装します。パラメータから、`param_1` には対象のホスト名が、`param_2` にはポート番号が含まれていることが示唆されました。
## x64dbg による動的解析
静的解析では潜在的な動作しか表示されません。実行時に実際に何が起こるかを確認するために、インストーラーを x64dbg に読み込み、ブレークポイントを設定しました。
![[4dfe64864232d2aef8c4d524a69ec4b6.png]]
```plaintext
bp ws2_32.send # 送信リクエストをキャプチャ
bp ws2_32.recv # サーバーの応答を監視
bp ws2_32.connect # 接続試行を追跡
bp ws2_32.getaddrinfo # DNS 解決の監視
```
インストーラーを実行すると、単一のサーバーに接続するのではなく、負荷分散機能を備えたCDNエンドポイントのリストを保持していることがわかりました。実行中のスタックメモリの表示は以下の通りです。
![[d2bf7a63c92439af2a0db59c649713ba.png]]
```plaintext
スタックメモリ - サーバー1:
02F4F078 "POST /fdsupdate HTTP/1.1\r\nUser-Agent: Mozilla/4.0 (compatible; FCT 7.4.3; Windows NT 5.1)\r\nHost: 173.243.138.76\r\nCache-Control: no-cache\r\nContent-Length: 480\r\n\r\n"
スタックメモリ - サーバー2:
03469990 "POST /fdsupdate HTTP/1.1\r\nUser-Agent: Mozilla/4.0 (compatible; FCT 7.4.3; Windows NT 5.1)\r\nHost: 209.40.106.66\r\nCache-Control: no-cache\r\nContent-Length: 464\r\n\r\n"
```
2つの異なるIPアドレスが、どちらも「/fdsupdate」へのPOSTリクエストを受信しています。インストーラーは、複数のCDNサーバー間で何らかのフェイルオーバーを実装しているようです。
![[8991d1da05f82a7be970a0f53a05283d.png]]
実際に送信されたペイロードをキャプチャしたところ、これは標準的なHTTPリクエストではありませんでした。
![[e2f8f6b51006f034353daf436107c2f6.png]]
![[e55d1840cbde3807caf39dc9e72080db.png]]
![[895d3e27f3d890a7b8dae96d31ef70b2.png]]
インストーラーはPOST本体として464~480バイトの構造化バイナリデータを送信していました。デコードの結果、以下の文字列が埋め込まれていることが判明しました。
```plaintext
デコードされたバイナリコンテンツ:
プロトコルマーカー: "FCPCFCP Command"
応答タイプ: "FCPR"
セッションID: "00000000002509012243"
+ ~400 追加のバイナリデータのバイト
```
これは、Fortinet のサーバーとのハンドシェイクを行う、HTTP 経由で実行されるカスタム バイナリ プロトコルでした。
## カスタムプロトコルの理解
このプロトコル交換で何が起こっているのかを理解するのは難しい作業でした。`/fdsupdate` エンドポイントは明らかに特定のバイナリデータを期待していました。サーバーがどのように応答するかを確認するため、キャプチャしたリクエストを再生してみました。
まず、バイナリペイロードを正しく抽出する必要がありました。
![[1bdd273210831e9a321a662e86cf6bbb.png]]
```powershell
$base64Data = "hHhImK6W3Je6HBS2CABFAAG4i..."
[IO.File]::WriteAllBytes("$currentDir\postdata.bin", [System.Convert]::FromBase64String($base64Data))
curl -X POST http://209.40.106.66/fdsupdate -H "User-Agent: Mozilla/4.0 (compatible; FCT 7.4.3; Windows NT 5.1)" -H "Cache-Control: no-cache" -H "Content-Type: application/octet-stream" --data-binary "@postdata.bin"
```
サーバーは空のレスポンスを返しました。リプレイされたリクエストは受け付けなかったため、バイナリプロトコルにはセッション検証、時間ベースのトークン、あるいはその両方が含まれていたことになります。ファイルダウンロードに対するこのレベルの保護は…予想外でした。
動的分析を続けると、最終的に第2段階が明らかになりました。
```plaintext
Runtime Log Output:
"Mon Sep 1 23:33:23 2025 - downloading image from server: 209.40.106.66"
File System Operations:
L"FortiClientVPN.exe" # 実際のインストーラファイル
L"obj_1_b12352" # 一時オブジェクト識別子
```
プロセスは2段階に分かれており、まず「/fdsupdate」との複雑なバイナリプロトコルハンドシェイクが行われ、次に実際のファイルがダウンロードされます。インストーラはファイルを受信する前に許可をネゴシエートします。
## なぜこれほど複雑なシステムを構築するのか?
セキュアな通信のためのカスタムOpenSSL BIO実装、自動フェイルオーバー機能を備えたマルチサーバーCDNインフラストラクチャ、セッション管理機能を備えた独自のバイナリプロトコル、クライアント認証とリクエスト検証、リプレイ攻撃を防ぐ時間ベースのトークンシステム。
企業がダウンロードを複雑化するためにこれほど多額の投資を行うには、強力なビジネス上の正当性が必要です。その理由はおそらく管理にあります。ユーザーにこのシステムを強制することで、フォーティネットは誰がいつソフトウェアをダウンロードし、どのシステムにインストールするかを完全に把握できます。リモートからダウンロードを無効化したり、地理的制限を設けたり、ダウンロードを許可する前に利用規約の更新を要求したりすることも可能です。
ユーザーの観点から見ると、これは問題を引き起こします。エアギャップシステムへのオフラインインストール用にFortiClientをダウンロードすることはできません。エンタープライズ展開用のローカルミラーを維持することはできません。配信はセッションベースであるため、全員が同一のインストーラを受け取っていることを確認することはできません。
より根本的に、これは「フリー」ソフトウェアに対する期待に反します。ベンダーが自社製品を無料ダウンロードできると謳う場合、ユーザーは実際にダウンロードできることを期待します。ダウンロードを複雑な認証(しかも取り消し可能なもの)に依存させるのは、真の意味での「無料配布」とは言えません。
## シンプルな回避策
リバースエンジニアリングの結果、簡単な回避策が明らかになりました。FortiClientがインストーラファイルを書き込むタイミングと場所を正確に把握できたため、それを傍受することができました。
![[b4cd5dd13dfd4ff32552b7b63e8b79b1.png]]
このアプローチは、必要なファイルを取得しながらプロトコル全体をバイパスします。皮肉なことに、フォーティネットの精巧な制御システムでは、仕組みを理解すればインストーラーを簡単に抽出できます。
重要なのは、エンジニアリングの複雑さがユーザーにとって不利な状況を認識することです。シンプルなソフトウェア配布は数十年前から解決されています。ベンダーが基本的なタスクに対して精巧なカスタムソリューションを実装する場合、その複雑さから誰が利益を得るのかを問う価値があります。
このような配布メカニズムは、ソフトウェアベンダーを評価する際に、シグナルとして捉えるべきです。必ずしも悪意があるわけではありませんが、ユーザーの利便性よりも運用管理を優先していることを示しています。信頼が重要となるセキュリティソフトウェアにおいて、不要なアクセス障壁はベンダーの姿勢に問題があることを露呈させる可能性があります。
人為的な複雑さへの最善のアプローチは、それを完全にバイパスすることである場合もあります。