Sterra Security Tech Blog

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

クロスコンパイルできる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やプルリクエストをお待ちしております。

書籍「ポートスキャナ自作ではじめるペネトレーションテスト」が9/20に発売されます

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

オライリー・ジャパンより拙書「ポートスキャナ自作ではじめるペネトレーションテスト -Linux環境で学ぶ攻撃者の思考(通称: カワウソ本)」が9/20に発売されます。本記事では構成、見どころを紹介します。

表紙はかわいいカワウソ🦦です!

本書の構成

次のように、6章の本編と2章の付録より、構成されています。 ポートスキャンの原理から、ポートスキャンによって見つけたサービスへの攻撃、 攻撃を成功させた後はさらなる被害拡大へと徐々にステップアップして理解できる構成になっています。

  • 1章:攻撃者はいかにしてシステムを攻撃するのか
  • 2章:Scapyでポートスキャナを自作し動作原理を知ろう
  • 3章:デファクトスタンダードのポートスキャナNmap
  • 4章:既知脆弱性を発見できるネットワークスキャナNessus
  • 5章:攻撃コードを簡単に生成できるMetasploit Framework
  • 6章:攻撃者はどのように被害を拡大するか
  • 付録A:ペンテスターが安全にキャリアを形成する方法
  • 付録B:ペンテスターと良好な関係を築く方法

1章では攻撃者がシステムを攻撃していくプロセスをUnified Kill Chainを題材に解説しています。 2章から6章では、Dockerコンテナの演習環境を用いて具体的なテクニックを学べます。 2章ではScapyを用いてポートスキャナとARPスプーフィングのexploit(攻撃コード)を実装し、ネットワークへの理解を深めます。 3章から5章ではNmap、Nessus、Metasploit Frameworkといったペンテストにおけるデファクトスタンダードのツールの解説をしています。 6章では、Linux環境で攻撃を成功させた後にさらに被害を拡大する、Post-Exploitationに使える技術について解説しています。

付録Aでは、誤解を招く行動を取れば逮捕されることもあるペンテスターが安全にキャリアを形成する方法を、 付録Bでは、ペンテストの依頼者の方やペンテスターの上司の方向けにペンテスターと良好な関係を築く方法をそれぞれ紹介しています。

こだわりポイント

説明の際には、ツールの紹介をするだけではなく、コーディングや脆弱性を攻撃する演習を設けることで、ツールの使い方だけを紹介しているマニュアルのような書籍にならないよう心がけました。 また、ツールの説明をするパートでは、業務でどのようにツールを活用しているのかを説明し、実務に役立つ知識を記載するように心がけました。 ツールは便利であるものの万能ではなく、結局は手を動かして脆弱性を検証する必要があるという脆弱性診断、ペネトレーションテストを行う上での本質に本書を通して触れることができます。 最後に頼りにできるのは、自分の手で行った作業だけです。 「ツールは役にたたない」というメッセージ性が強い書籍になったと思います。

Unified Kill Chainやレジデンシャルプロキシ、細かいPost-Exploitationのテクニックといったあまり日本語文献がないような概念、他の書籍では紹介していないようなマニアックなツールの機能を紹介し、初学者の方だけではなく実務に従事している方が読んでも楽しめるようにしました。 付録Bの「ペンテスターと良好な関係を築く方法」には、実務を行う上で遭遇するトラブルについて記載しており、こちらも実務をやっている方に、楽しんでもらえる内容だと思います。

攻撃者の思考を知ることはセキュリティエンジニアのみならず開発者にとっても大切です。 開発者は日々発見される、OSやライブラリなどの大量の脆弱性の対応を日常的に迫られていますが、 攻撃者の思考を分かっていなければ、何が脅威なのか分からず脆弱性のリスクレベルを評価できません。 開発者の方が読んでも楽しめるよう、攻撃手法だけでなく対策についても記載しています。 また、セキュリティエンジニアが当然のように使うものの、開発者の方には馴染みがない単語は平易な言葉に置き換えるよう心がけました。 例えば、「バイパス(Bypass)」は「突破」「迂回」といった単語に置き換えています。 開発者の方にも読んでほしいです。

表紙のカワウソ🦦が可愛いのでカワウソが好きな方にもおすすめです。

電子版はO'Reilly Japan Ebook Storeより購入できます

おわりに

本書の執筆にあたり、多数の方にご協力いただきました。 オライリー・ジャパンの方々、本書のレビューに協力していただいた皆様には、この場を借りて心からお礼申し上げます。

似た構成の本は世界中を探してもなさそうな、 初学者の方にも実務に従事している方にも面白いと思ってもらえる書籍に仕上がったと思います。 広く多くの方に読んでもらえるとうれしいです。

Nmapの日本語ドキュメントを更新した話&その方法の解説

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

先日、Nmapというポートスキャナの日本語ドキュメントにコントリビュートしました。 この記事では、日本語ドキュメントの現状とコントリビュート方法の解説を行います。

デファクトスタンダードのポートスキャナNmap

Nmap(Network Mapper)は、セキュリティ業界でデファクトスタンダードのポートスキャナです。 ポートスキャンを行えるだけではなく、Luaスクリプトによって機能を拡張でき、多数の脆弱性を攻撃するためのスクリプトがデフォルトで含まれています。

1997年から存在するツールで、「マトリックス リローデッド」や「ダイ・ハード4.0」などの映画の中のハッキングシーンにもたびたび登場しており、セキュリティエンジニアであれば誰もが知る有名ツールです。 Nmapが登場した映画は次のURLにまとめられているので興味がある方は見てください。

nmap.org

日本語ドキュメントは古く役に立たない...

そんな有名なNmapですが、日本語ドキュメントは長年更新されていませんでした。 Nmapの最新バージョンは7.93ですが、日本語ドキュメントはバージョン4.501相当のものになっています。 そのため、コマンドライン引数に実際とは違うものが記載されているオプションが多数あり、それを鵜呑みにしていると、正しく使うことができないという状況です。

というわけで、コントリビュートしました!

以前より、時間を指定するオプションの引数が現在のバージョンではtimeなのに、millisecondsと日本語ドキュメントに記載されているのが気になっていました。例えば、スキャンに時間がかかるホストをスキャンした際に、一定時間経過したらスキャンを停止する--host-timeoutオプションの引数の説明がこれに該当します。timeは、<数値><単位>というフォーマットを意味します。例えば、30分と指定する場合は30m、10秒と指定する場合は10sというふうに記述します。

そこで、このような間違った説明が頻出する「タイミングとパフォーマンス」のページにコントリビュートを行い、引数の表記を正しいものへと修正を行いました。 該当コミットは次のURLです。 github.com

引数の説明が修正されている様子

コントリビュートするには

日本語ドキュメントにどのようにしてコントリビュートを行うのか解説します。 NmapにはNmap Man Page Translation FAQという翻訳に関するガイドラインが存在します。 NmapのドキュメントはXMLで文書を記述できるDocBook XMLというフォーマットで記述されており、そのフォーマットの説明が書かれています。

nmap.org

日本語ドキュメントのファイルはdocs/man-xlate/nmap-man-ja.xmlに、英語ドキュメントのファイルはdocs/refguide.xmlに存在します。 ファイルに変更を加えたら、作者にメールするか、プルリクエストを送ることで反映してもらえます。 上記ガイドラインには、メールするように書かれていますが、実際はプルリクエストでも大丈夫です。

まとめ

Nmapの日本語ドキュメントの問題点とコントリビュート方法を紹介しました。 日本語ドキュメントに問題を感じているものの、コントリビュート方法が分からなかった人も多いのではないかと思います。

今回、私が更新した部分はほんの一部で、まだまだ直さないといけないところが残されています。 世界平和のために、日本語話者のNmapユーザーのみなさんにコントリビュートしていただけるとうれしいです。


  1. 日本語ドキュメントが準拠しているバージョン情報については以前GitHubのIssueで問い合わせました。https://github.com/nmap/nmap/issues/2243