Sterra Security Tech Blog

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

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でも有効です。

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

参考資料

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

取締役CTOの小竹(aka tkmru)です。 ディスアセンブルを妨害するアンチディスアセンブルという耐タンパ性を高めるための技術があります。 本記事では、32ビットのARM環境(以下、ARM32)のみで有効なアンチディスアセンブルのテクニックを紹介します。

静的解析を妨害するアンチディスアセンブル

リバースエンジニアリングの方法の1つに、バイナリをディスアセンブル(逆アセンブル)した結果を読み解く静的解析があります。 ディスアセンブルというのは、バイナリを機械語からアセンブリ言語に変換することです。 静的解析では、アセンブリ言語の命令を読み解くことで、プログラムの挙動を明らかにします。

アンチディスアセンブル(Anti-Disassembly)は、静的解析を妨害するために、ディスアセンブルを正常に行えなくする技術です。 解析を逃れるために、攻撃者がマルウェアに用いることもあれば、チートを防ぐためにゲームに用いられることや、 不正なコピー防止のためにソフトウェア製品に用いられることもあります。

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

x86/x64では、アセンブリ言語の命令は可変長です。 x86/x64の場合、命令が可変長であることを利用し、命令の途中のバイトを分岐先として解釈させるなどして、 命令間の境界をディスアセンブラに誤認させることで、比較的容易にアンチディスアセンブルを実現できます。

しかし、ARMの命令は固定長です。 ARM32では、命令の長さは32ビット(4バイト)で固定です。 例外として、16ビット(2バイト)のThumb命令セットがあります。 これは、プログラムのサイズを削減し、消費電力を低減するために設計された命令セットです。 Thumb命令を使用するには、動作モードを切り替える必要があります。 ARMでは、このようにほとんどの命令が固定長であるため、命令間の境界を誤認させることが難しく、アンチディスアセンブルを実現するのが難しくなります。

PLD命令を活用したアンチディスアセンブル

PLD(PreLoad Data)命令は、メインメモリからL1/L2キャッシュにデータを読み込む命令です。 メモリアクセスの高速化のために用意されています。 公式リファレンスには、次のシンタックスで利用できると記載されています。 レジスタで指定されたアドレスにオフセットを加算して、そのアドレスにあるデータを読み込みます。

PLD [レジスタ, オフセット]

PLD命令に指定されたアドレスを、IDA Proなどのリバースエンジニアリングツールは、データとして解釈します。 そのため、PLD命令にコードが存在するアドレスを指定すると、そのアドレスにある命令はディスアセンブルされません。 このように、PLD命令を活用して、アンチディスアセンブルを実現できます。 L1/L2キャッシュを前後で使用していなければ、PLD命令を挿入してもプログラムの動作に影響はありません。 PLD命令に指定できるアドレスにはアライメントの制限がない点、変なアドレスを読み込んでしまっても例外を発生させない点も良い点です。

PLD命令を挿入する

シンプルなHello, world!という文字列を出力するだけのプログラムにPLD命令を挿入し、IDA Pro、Ghidraでディスアセンブルしてみます。 PLD命令のオペランドには、次に実行する命令を指すPCレジスタを指定します。

#include <stdio.h>

int main() {
    __asm__ volatile(
        "pld [pc]\n"
    );
    puts("Hello, world!\n");
    return 0;
}

コンパイルと実行

x64のUbuntu 22.04でARM32のクロスコンパイル環境を構築し、QEMUというエミュレータを使ってコンパイルしたプログラムを実行してみます。 実行したいプログラムのアーキテクチャとPCのアーキテクチャが異なる場合は、QEMUのようなエミュレータを使わないと実行できません。

まず、aptコマンドを使って、コンパイラQEMUをインストールします。

$ sudo apt install -y gcc-arm-linux-gnueabihf qemu-user-static

インストールが完了したら、次のコマンドでプログラムをコンパイルします。 先ほどのプログラムはinsert-pld.cというファイルに保存してあるとします。

$ arm-linux-gnueabihf-gcc -marm insert-pld.c -o insert-pld

コンパイルが完了したら、QEMUを使って実行します。 環境変数QEMU_LD_PREFIXを使って、プログラムが使う共有ライブラリのパスを指定する必要があることに注意してください。 指定しないと、/ld-linux-armhf.so.3が見つからないというエラーが発生します。 次のように問題なく実行できることを確認できるはずです。

$ export QEMU_LD_PREFIX='/usr/arm-linux-gnueabihf/'
$ qemu-arm-static insert-pld
Hello, world!

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

IDA Proでディスアセンブルした結果を確認してみましょう。 狙い通り、アンチディスアセンブルが成功しています。 PLD命令のオペランドに指定されたアドレスをデータとして解釈しているため、そのアドレスにある命令はディスアセンブルされていません。

ディスアセンブル結果を確認できないだけでなく、Graph viewを表示できないことも確認できます。

Ghidraでもディスアセンブル結果を確認してみましょう。 意外なことに、Ghidraではアンチディスアセンブルが成功していません。 これは、PLD命令のオペランドに指定されたアドレスを解釈していないからです。 IDA Proほど気が利かない多機能でないことが良い方向に作用していると言えるでしょう。

オフセットを調整し、任意のアドレスの命令にアンチディスアセンブルを施す

PLD命令ではレジスタで指定したベースアドレスに対するオフセットを指定できます。 これを用いて任意のアドレスのコードにアンチディスアセンブルを施すことができます。 アドレスを調整して、PLD命令があるアドレスの直後にある命令に対してアンチディスアセンブルを施してみましょう。

#include <stdio.h>

int main() {
    __asm__ volatile(
        "pld [pc, #-4]\n"
    );
    puts("Hello, world!\n");
    return 0;
}

今回のプログラムも先ほどのプログラムと同様の手順でコンパイル、実行できます。 IDA Proでディスアセンブルした結果を確認すると先ほどとはアンチディスアセンブルの対象が異なっていることが分かります。

まとめ

この記事では、ARM32のみで有効なアンチディスアセンブルのテクニックを紹介しました。 PLD命令を活用することで、ディスアセンブルを妨害することができます。 この記事で紹介したテクニックを応用し、NOP Sledを組み合わせたり、PLD命令とディスアセンブラが解釈できないよう一部のバイトを壊したりすることで、 解析をより困難にすることもできます。応用例についても機会があれば紹介したいと思います。

また、今回紹介したテクニックは、Ghidraには全く効かず、GhidraはIDA Proよりも優位でした。 しかし、リバースエンジニアリングを行っていると反対にIDA Proの方がディスアセンブルの精度が高いことの方が多いです。 耐タンパ性を高める取り組みをする際は、複数のリバースエンジニアリングツールで検証することが重要です。

PLD命令は、64ビットのARMアーキテクチャ(AArch64、ARM64)では使用できません。 次の記事では、64ビットのARM環境で有効なアンチディスアセンブルのテクニックを紹介する予定です。

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

参考文献

パッチを当てるPythonスクリプトを生成するIDAプラグイン「genpatch」を作った話

取締役CTOの小竹(aka tkmru)です。

先月、genpatchというIDA Pro上で当てたパッチと同じパッチを当てるPythonスクリプトを生成するIDAプラグインを公開しました。 この記事では、作成した「genpatch」の使い方、仕組みを紹介します。

困っていたこと

スマホアプリが対象の脆弱性診断では、準備段階でリバースエンジニアリングを行い、パッチを当てることがあります。 SSL Pinning(Certificate Pinning、証明書のピン留め)をお客様側で無効化していただけなかった場合には、解析を行い、無効化するパッチを当てた上で、APIとの通信を診断します。 SSL Pinningは、使用するSSL証明書をアプリ内にハードコートし、通信時にSSL証明書がアプリ内のものか検証を行うことで、SSL通信の中間者攻撃を防ぐ仕組みです。 Burp Suiteなどのプロキシツールを使ってSSL通信を閲覧する際には、PCにインストールしたプロキシツールの証明書を使用しています。 そのため、SSL Pinningが実装されているアプリに対して、プロキシツールを使用して通信を閲覧しようとすると、SSL証明書の検証に失敗し、通信が行えません。

診断期間中に診断対象のアプリがアップデートされた場合、パッチを当て直す必要があります。 その際に、IDA Proを起動し、パッチを当て直す1のは面倒です。 また、IDA Proの操作に慣れていないチームメンバーにパッチを当ててもらうのも、大変です2。 そのため、IDA Pro上で当てたのと同じパッチを当てるPythonスクリプトを生成するプラグインを作成しました。

作ったツール

genpatchというIDA Pro上で当てたパッチと同じパッチを当てるPythonスクリプトを生成するIDAプラグインを作成しました。 genpatchが生成したパッチを当てるPythonスクリプトを使用することで、 繰り返しバイナリにパッチを当てる際に、Pythonスクリプトを実行するだけで済みます。 このプラグインPythonで実装しています。

github.com

インストール方法

リポジトリ内のgenpatch.pypatch_script_template.txtをIDA Pluginフォルダにコピーし、IDA Proを再起動するとgenpatchが使えるようになります。 Windowsの場合、コピー先のフォルダはC:\Program FilesIDA 8.x\pluginsです。 macOSの場合、コピー先のフォルダは/Applications/IDA Pro 8.x/idaq.app/Contents/MacOS/pluginsにあります。

パッチを当てるPythonスクリプトを生成する

IDA Pro上でパッチを当てた後、Edit -> Plugins -> genpatchボタンをクリックすると、IDA Proで読み込んだバイナリがあるディレクトリにパッチスクリプトが生成されます。 スクリプトの名前は、解析中のバイナリに接尾辞 _patch.pyを付けたものになります。 例えば、バイナリ名が a.out の場合、パッチスクリプト名は a.out_patch.pyとなります。

Pythonスクリプトが正常に生成されると、以下のようなダイアログが表示されます。

生成したPythonスクリプトを実行する

コマンドライン第一引数にパッチを当てたいバイナリへのパスを指定して実行することで、パッチを当てることができます。

$ python a.out_patch.py <target_binary_path>

生成されたPythonスクリプトは次のようになります。16進数のデータ列を置換します。

#!/usr/bin/env python
# coding: UTF-8

import binascii
import os
import re
import sys

target_path = sys.argv[1]
target_data = None
with open(target_path, 'rb') as target_file:
    target_data = binascii.hexlify(target_file.read()).decode('utf-8')

# address: 0x100000ecb
# function name: __text: _main
# comment: Keypatch modified this from:   jz loc_100000EF6 Keypatch padded NOP to next boundary: 4 bytes
matches = re.findall('0f8425000000', target_data)
if len(matches) == 1:
    target_data = target_data.replace('0f8425000000', '752990909090')
else:
    print("Patch pattern isn't unique")
    sys.exit()

result_path = f'{target_path}_patched'
with open(result_path, 'wb') as result_file:
    if sys.version_info[0] >= 3:
        result_file.write(bytes.fromhex(target_data))
    else:
        result_file.write(target_data.decode('hex'))

print("Successfully generated patched binary to '%s'" % result_path)

おわりに

弊社ではこのように脆弱性診断中に遭遇した課題を解決するべく、適宜ツールを開発し、業界内に知見を共有していきます。 また、「genpatch」が脆弱性診断、マルウェア解析などリバースエンジニアリングを伴う仕事に従事するエンジニアに広く使われるよう育てていきたいと思っています。 ぜひ、気づいたことがあればフィードバックしてもらえるとうれしいです。 みなさまからのIssueやプルリクエストをお待ちしております。


  1. Unity製のスマホアプリではすべてのロジックがC++製の共有ライブラリに実装されるのでパッチを当てるのにIDA ProやGhidraを使う必要があります。Javaで実装されたAndroidアプリであればSmaliファイルを編集するだけで済みます。
  2. IDA Proのライセンスを用意するのも大変です。

アップロード機能の検証のためのファイルを作成するツール「dummy」を作った話

取締役CTOの小竹(aka tkmru)です。

dummyというファイルアップロード機能の検証用の静的ファイルを作成するコマンドラインツールを公開しました。 指定したテキストを書き込んだJPEGファイル、PNGファイル、PDFファイルを生成でき、PNGファイルに関してはサイズ(バイト数)の指定も行えます。 この記事では、作成した「dummy」の使い方、仕組みを紹介します。

アップロード機能の検証の際に困ること

ファイルアップロード機能を対象に脆弱性診断を行うとき、どんな画像をアップロードするか、いつも悩みます。 ペンテスターでなくともフリー素材ではないネットミームの画像や自分が食べた美味しいご飯の画像を業務で使用するのには少し気が引けるでしょう。 クライアント(顧客)のサーバにファイルをアップロードする脆弱性診断では、なおさらです。 そこで、誰が見ても悪い印象を受けない検証用の画像を作成するためにツールを作りました。

また、扱えるファイルのサイズに制限がある場合、その制限が有効になっているか確認したいときがあります。 その制限に引っかかるような大きいサイズのファイルを用意するのは少し大変です。 そういう場合にも対応できる、任意のサイズのファイルを作成できるツールが必要でした。

作ったツール

検証用の静的ファイルを作成する「dummy」というコマンドラインツールを作りました。 前述した通り、指定したテキストを書き込んだJPEGファイル、PNGファイル、PDFファイルを生成でき、PNGファイルに関してはサイズ(バイト数)の指定も行えます。 Pillowという画像処理ライブラリを使用したかったため、Pythonで実装しています。

github.com

使い方

インストール方法や各種パラメータの指定方法を紹介します。

インストール方法

pipコマンドでインストールできます。 Pythonの実行環境を整えた上で、次のコマンドを実行してください。

$ pip install git+ssh://git@github.com/sterrasec/dummy.git

ファイルを生成する

生成したいファイルのパスはオプションなしで指定できます。 また、生成するファイルの種類は拡張子によって自動で識別されます。 -tオプションでファイルに書き込むテキスト、-bオプションでバイト数を指定できます。 -bオプションはPNGファイルを生成するときのみ使用できます。

$ dummy -t abc -b 1MB test.png

指定したサイズの画像が生成される様子

-tオプション、-bオプションは指定しなくとも使用できます。 -tオプションでテキストを指定しない場合は、デフォルトの「dummy file」というテキストが書き込まれます。

$ dummy test.jpeg

デフォルトで生成される画像

どのようにして任意のサイズのPNGファイルを生成しているのか

小さすぎないバイト数を指定しない限り、「dummy」では任意のサイズのPNGファイルを作成できます。 これをどのように実現しているのか解説します。

PNGファイルはチャンクと呼ばれるデータブロックから構成されています。 IHDRや、IDATIENDなどのチャンクが知られています。 IHDRは、画像の幅や高さ、色深度、圧縮方式などの情報を持っています。 IDATは、画像データを持っています。 IENDは、PNGファイルの終端を表します。

独自のチャンクを作成することもできます。 チャンクは、チャンクタイプ、チャンクデータ、CRCという3つの要素から構成されています。 これらのデータを適切に挿入することで、PNGファイルの機能を損なわずにファイルを大きくすることができます。 「dummy」では、eXtrという無意味なデータ列から構成される独自のチャンクをIENDチャンクの手前に挿入することで、指定されたバイト数になるよう、データ量をかさ増ししています。

おわりに

「dummy」が脆弱性診断、QAに従事するエンジニアに広く使われるよう育てていきたいと思っています。 ぜひ、気づいたことがあればフィードバックしてもらえるとうれしいです。 みなさまからのIssueやプルリクエストをお待ちしております。