
Accflyカメラの脆弱性の開示: CVE-2020-25782、CVE-2020-25783、CVE-2020-25784、CVE-2020-25785。
2020年の初め、以前の職場で社内のpwn2own形式のイベントに参加する機会がありました。いくつかのターゲットがありましたが、私が最も興味を持ったのはAccfly Wireless Security cameraでした。残念ながら実際のイベントまでに研究を終えることはできませんでしたが、このデバイスに対する他の試みがなかったため、研究を続けました。
研究の主な焦点は、リモートコード実行(RCE)につながる脆弱性でした。この種の脆弱性により、攻撃者はデバイスの完全な制御を乗っ取ることができ、ビデオカメラの場合には所有者のプライバシーが完全に侵害される可能性があります。残念ながら、このデバイスのファームウェアにはこのような問題が多数存在することが判明しました。
まず、このデバイスは一切の認証を提供しません。その結果、接続できる攻撃者は自由にアクセスして再設定することができます。最も単純な形では、デバイスを継続的に再起動させることができ、正規ユーザーにとっては完全に使用不能になります。この攻撃の範囲は多少限定されています。なぜなら、このデバイスはWiFiネットワーク内で使用されるように設計されており、通常はNATの背後にあるため、インターネットから直接到達できないからです。しかし、デバイスと所有者のスマートフォンアプリ間の通信に暗号化がないこと、さらにベンダーのサーバーが通信のプロキシとして使用されることにより、WiFi NATの制限を突破できるMitM攻撃やDNS操作攻撃の機会が生じます。
さらに、このアプリケーションは通信に独自のバイナリプロトコルを使用しています。CとC++の混合で実装されており、安全でない文字列処理関数が多数含まれていることが判明しました。メインの実行ファイルには大量の未使用コードが含まれており、他のデバイスでも再利用されていることを示唆しています。これによりメンテナンスが困難になり、攻撃対象領域が拡大しています。このアプリケーションは、一般的な多くのエクスプロイト技術から保護する最新のセキュリティメカニズムを一切有効にしておらず、さらにユーザー権限を制限すらしておらず、最高の権限であるrootで実行されています。
この研究の結果、以下の4つの脆弱性が文書化されました。
CNetClientManage::ServerIP_Proto_Set の
認証なしスタックベースのバッファオーバーフローCNetClientTalk::OprMsg の
認証なしヒープベースのバッファオーバーフローCNetClientGuard::SubOprMsg の
認証なしスタックベースのバッファオーバーフローCFtpProtocol::FtpLogin の
認証なしスタックベースのバッファオーバーフローそのうち3つについては、攻撃者がデバイスの完全な制御を獲得できるRCEエクスプロイトを開発しました。しかし、脆弱性の報告試行に対するベンダーの応答がなかったため、このリポジトリにはアプリケーションをクラッシュさせるだけの限定されたPoCエクスプロイトのみが含まれています。
これらの問題はソフトウェアバージョン V3.10.73 で発見され、本公表時点(2021年1月26日)で利用可能な最新版であるソフトウェアバージョン V4.15.77 で検証されました。
ご質問がある場合は、メール(git commitを参照)またはGitHub issuesでお気軽にご連絡ください。ハッキングに興味深いかもしれないIoTデバイスをお持ちの方、セキュリティリサーチャーを探している方、または単にご挨拶したい方、ぜひお知らせください。コーヒーをごちそうする こともできます!
対象デバイスは、付属のモバイルアプリから制御されるビデオカメラデバイスです。私の分析はカメラのネットワークトラフィックから始まり、カメラのファームウェアへと進みました。すべての通信には独自のバイナリプロトコルが使用されています。コマンドは、同じネットワーク内にある場合はモバイルデバイスに直接送信されるか、デバイスメーカーのサーバーを経由します。カメラソフトウェア自体は複数のポートで待ち受けています(TCP 23456、34567、UDP 34568、34569)。ネットワークトラフィックには暗号化も認証もないため、MitM攻撃や、カメラがネットワーク上に露出している場合の直接アクセスが可能です。ビデオストリームへのアクセスも認証なしで可能である可能性が高いと思われますが、これを試すために独自プロトコルのリバースエンジニアリングを十分に行うには至っていません。
簡単な通信の概要を把握した後、次のステップはデバイスのファームウェアへのアクセスを試みることでした。最初の試みは、デバイスの更新プロセスをハイジャックして直接ダウンロードすることでしたが、ネットワークトラフィックにはそのような動きは見られませんでした。同僚がフラッシュメモリからファームウェアを抽出してくれるという非常に必要な助けがなければ、この段階で行き詰まっていたでしょう。そのおかげでこの研究を続けることができました。
ファームウェアはMIPSリトルエンディアンCPU上でLinuxを実行していることが判明しました。興味深いプロセスは Alloca という名前のちょうど1つだけで、ビデオキャプチャを担当し、すべてのネットワーク通信も処理します。アプリケーションはC++で作成されており、このデバイスでは使用されないコードが多数含まれています。これは、同じソフトウェアが他のデバイスでも使用されていることを示しています。
この問題は最後に発見されたものですが、他のほとんどの脆弱性の実際の悪用にとって極めて重要です。なぜなら、それらは安全でないC言語の文字列関数の使用に起因するからです。同様のシナリオでコード実行を成功させるために使用できるテクニックはいくつか存在しますが、このアプリケーションはそれらがほとんど役に立たないような作りになっています。主な問題は、Alloca のコードとデータが低いアドレス( < 0x01000000)に静的に割り当てられていることです。そのため、既存のコードを再利用する試み(すなわちROPなど)は、プログラムのメモリにアドレスを書き込む能力が必要となるため、役に立ちません。C言語の文字列は \x00 を終端文字として使用し、文字列関数は最初のそのようなバイトで処理を終了するため、複数のNULLバイトを使用することはできません。さらに、スタックの位置はランダム化されており、アプリケーションは高度にマルチスレッド化されているため、他のテクニックの信頼性ははるかに低くなります。
この脆弱性は、複数のスレッド間でのデータ共有と、安全でない strcpy の使用の結果です。この特定の問題を長い間分析してきましたが、この公表のわずか数週間前まで、これをデータ漏洩ベクトルとして使用する機会に気づきませんでした。興味深いことに、C++オブジェクトのヒープアドレスを漏洩できるおかげで、この脆弱性はリモートコード実行も可能にします。ただし、この攻撃は本レポートには含まれていません。
Alloca アプリケーションはFTP経由で自身を更新できます。この操作はサーバーから要求でき、必要なユーザー名、パスワード、ファイル名もサーバーが提供します。更新を開始する関数を以下に示します:

strcpy への3つの呼び出しは明らかに安全ではなく、ftpUpgrade オブジェクトが動的に割り当てられるため、ヒープオーバーフローにつながります。残念ながら、コピーが実行される順序と ftpUpgrade 構造体のレイアウトにより、データを漏洩するスレッドを実際に起動することは不可能です。受信パケットを詳しく調べると、次の構造が明らかになります:
struct ftp_upgrade_pkt {
struct pktHeader;
char username[16];
char password[16];
char filename[128];
}
一方、ftpUpgrade オブジェクトは次のような構造です:
struct CNetClientFtpUpgrade {
// ... something here
char filename[128];
char unknown[6];
char username[16];
char password[16];
CFtpDownlad *;
CNetClientConnect*;
int something[5];
bool threadRunning;
// ... and more
}
リークは、内部ポインタ(CFtpDownload*、CNetClientConnect*)のいずれかがアプリケーションによって設定された後に発生する可能性があります。さらに、ユーザー名とパスワードは、そのポインタがリーク可能な場所に格納される前に、新しく作成されたオブジェクトに(もう一度、今回は安全に)コピーされるため、リークは filename でのみ発生し得ます。その結果、ファイル名は非常に長くなければなりませんが、strcpy の順序と終端の挙動により、十分に長いファイル名はさらに長いユーザー名とパスワードを生み出し、実際には threadRunning を上書きしてスレッドをまったく起動しないことになります。
このコードがシングルスレッドであれば、できることはあまりありません。しかし、新しい FtpDownload スレッドが生成され DownloadFile 関数を実行すると、受信パケットを処理するスレッドと CNetClientFtpUpgrade オブジェクトを共有するため、興味深い機会が生じます。外部から制御可能な複数のIO操作(DNSリクエスト、FTP接続処理)があるだけでなく、FTPへの接続を最大10回試行します(これは DownloadFile の呼び出し元で行われます)。これにより、FtpDownload スレッドの実行を(IO操作でブロックすることにより)制御でき、メッセージ処理スレッドが他のリクエストを処理する時間を確保できます。

要するに、複数の更新リクエストを送信するだけで、既に実行中の FtpDownload スレッドが使用する filename(および他のパラメータ)を変更し、漏洩したヒープアドレスを受け取ることが可能です。おまけとして、FtpSize 関数(緑色でマーク)は、漏洩したアドレスが参照するオブジェクト内のバッファを使用して filename 自体を格納するため、最初のステージのシェルコードを簡単に注入できます。ここでの唯一の制限は、strcpy の使用による長さとNULLバイトの欠如です。デバイスからヒープアドレスを漏洩するだけのサンプル PoC が提供されています。
関数 CNetClientManage::ServerIP_Proto_Set における認証なしスタックベースのBO
受信トラフィックの処理に認証がまったくないことから、パケットハンドラを探すようになりました。興味深い関数の1つが ServerIP_Proto_Set です。これはDNS解決のための静的オーバーライドを作成するために使用されるようです。この方法でトラフィックをリダイレクトする方法は見つかりませんでしたが、ここには別のバッファオーバーフローがあります(オレンジ色でマーク)。

パケットから直接読み取られるデータは、sprintf 関数内で使用されます。この場合、パケットからのデータが16バイトのバッファに収まると想定されていますが、単純な %s フォーマットを使用すると、NULLを含まない限り、任意の数のバイトを書き込むことができます。
この脆弱性はかなり限定的です。スタックに大量のデータを書き込むことは可能なので、NOPスレッジを使用すれば機能する可能性がありますが、NULLバイトを書き込むことはできません。単一のNULLバイトを書き込もうとしても、sprintf 関数のフォーマットがその前に \n を置くため、失敗します。もう1つの障害は、バッファの後に格納される CMutex オブジェクトです。オーバーフローの試みは、このミューテックスを正しい値(または少なくとも CGuard デストラクタを満たす値 - 赤色でマーク)で埋めなければなりません。これは問題です。デストラクタは渡された変数を2回デリファレンスし、その値を pthread_mutex_unlock 呼び出しで使用するからです。いくつかのテストの後、NULLで埋められたバッファで pthread_mutex_unlock から正しく戻るのに十分であることがわかりましたが、それでも適切なメモリアドレスにデリファレンスされる必要がありました。
リークが救いになります。この攻撃は少し複雑です。NULLバイトを含まないヒープアドレスが必要だからです。幸いなことに、このデバイスはリモートでの認証なし再起動の機能を提供するため、検索は容易になります。再起動のたびに異なるヒープアドレス空間の割り当てが提供されます。そのため、デバイスをリセットして、適切なアドレスが見つかるまでアドレスをリークするだけで済みます。便利なことに、これによりシェルコードの短い最初のステージを格納することもできます。ミューテックスの問題(ポインタへのポインタが必要)を克服する必要があるため、別のアドレスをリークします。今回は、以前にリークしたアドレスをファイル名として渡します。次の図は、期待されるメモリレイアウトを示しています:

すべてが計画通りに進めば、2番目のアドレスをミューテックスとして、1番目のアドレスを戻りアドレスとして渡すことが可能です。ただし、PoC が行うように単にアプリケーションをクラッシュさせるだけなら、これは必要ありません。
関数 CNetClientTalk::OprMsg における認証なしヒープベースのBO
このデバイスは双方向の音声通信を可能にするはずです。受信パケット用の別のハンドラは、オーディオの受信と再生を担当しているようです。audio_pkt_hdr ネットワークパケットは次の構造で記述されます:
struct audio_pkt_hdr {
struct pktHeader field_0x0;
int field_0x14
int field_0x18
int field_0x1c
char audioBuff[0x140];
}
pktHeader 構造体のフィールドの1つは、パケットの長さ(ネットワーク経由で転送される)です。このフィールドは送信者が自由に設定できます。脆弱な部分は、受信パケットヘッダーで提供される信頼できない長さの値を使用して、受信パケットから直接データをコピーすることです。

見てわかるように、CNetClientTalk オブジェクトは次のコンストラクタで作成されます:

そのため、上記の memcpy 呼び出しはヒープ上でバッファオーバーフローを引き起こします。残念ながら、この問題の実際の悪用はかなり困難です。ヒープを繰り返し上書きすることは可能ですが、オーバーフローするバッファの後にヒープ上に格納されるデータを制御する方法は見つかりませんでした。このアプリケーションには50を超えるアクティブなスレッドがあり、その一部はオーディオとビデオの処理を担当しているため、常にメモリの割り当てと解放が行われています。これによりヒープデータが常に変化するため、バッファの後に何が格納されているかを予測して正しく上書きすることが困難になります。
関数 CNetClientGuard::SubOprMsg における認証なしスタックベースのBO
ここで、もう1つの受信パケットハンドラが出てきます。今回は、ネットワークパケットの構造は次のとおりです(共通パケットヘッダーは省略):
struct pkt_hdr_sub_cliGuard {
dword deviceId;
dword userId;
dword magic;
dword subCmd;
dword field_0x10;
dword field_0x14;
dword field_0x18;
dword field_0x1c;
dword guard_icommand;
dword moreThenRandomStackValue;
dword itemCnt;
char array_of_0x18[24];
};
ここでも興味深い部分は最後の配列です(このパケットは好きなだけ拡張できるため)。この配列にはサイズ24の内部構造体が含まれます。この脆弱性は、受信した itemCnt が6を超えないという想定に起因します。コピー先のバッファのサイズが144(=24*6)であるためです。これは次のリストで確認できます(オレンジ色のハイライト):

今回はコピーに memcpy(緑色のハイライト)が使用されているため、使用可能な文字に制限はありません。コピーはwhileループ(青色でマーク)によってチャンク単位で実行されます。注目すべきは、カウンタ cnt_v0 がループ内で減少するため、チャンクが逆順でコピーされることです。脆弱なバッファ buf に続く変数を含めると、オーバーフローには256バイト、次に4つのレジスタ($s0-$s3)、そして $ra が必要です。メモリレイアウトが不明なため、PoC コードはROPテクニックを使用しています。デバイスの内蔵サウンドの1つを再生する(そしてクラッシュする)単一のガジェットが使用されています。
関数 CFtpProtocol::FtpLogin における認証なしスタックベースのバッファオーバーフロー
私の分析の最初の方向性の1つは、更新手順を探すことでした。調査の結果、このデバイスにはFTP更新機能があり、更新リクエストを送信することで開始され、外部のFTPサイトからファームウェアをダウンロードします。他の脆弱性と同様に、デバイスの更新をリクエストする前に認証する必要はありません。FTP機能の詳細な分析により、CFtpProtocol::FtpLogin 関数にスタックベースのバッファオーバーフローがあることが判明しました。以下の逆コンパイルされたリストでわかるように、この関数はサイズ256の char 配列を FtpPwd 関数に渡します。

FtpPwd はFTPサーバーから現在の作業ディレクトリを取得するために使用されます。内部バッファに最大1500バイトの応答を読み込み、それらを提供されたバッファにコピーします。この一連の呼び出しにより、1242バイトのオーバーフローが発生します。この場合、"(二重引用符)を使用すると入力文字列が短くなるため(strchr がC文字列内の char を検索するために使用される)、使用可能な文字は非常に限られており、バッファをオーバーフローさせることができません。幸い、コード実行がリダイレクトされる単一のアドレスを配信するだけで済みます。

この脆弱性を悪用するには、DNSを制御するか、FTPサーバーへの接続をリダイレクト(またはMitM)する必要があります。アプリケーションには最新の保護機構が一切ないため、スタックから直接コードを実行することが可能です。アドレスのリークがない場合、できる最善の方法はスタックの位置を推測するか、実行を単一の関数にリダイレクトしてアプリケーションをクラッシュさせることです。私の最初の試みはまさにそれで、内蔵サウンドの1つを再生するものであり、PoC として提供されています。リークを使用することで、デバイスの完全な制御を獲得することが可能です。