Sterra Security Tech Blog

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

ARM環境でディスアセンブルを妨害するテクニック(32ビット編)

取締役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環境で有効なアンチディスアセンブルのテクニックを紹介する予定です。

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

参考文献