0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Android] Arm64でAnti-Disassembly/Decompile

Posted at

昨日、このような素晴らしい記事を見かけました

こちらの記事ではPLDという命令を使用してアンチディスアセンブルを施していましたが、こちらでも言及されている通り、Arm64では存在しない命令です。

そこで、Arm64ではアンチディスアセンブルを施すことはできないのかと気になったので自分なりに調べてまとめてみました。

初めに

ArmにはDCBディレクティブという1バイト以上のメモリを割り当て、メモリの実行時の初期内容を定義するものがあります。

The DCB directive allocates one or more bytes of memory, and defines the initial runtime contents of the memory.

そのほかにも種類があり、

DCB DCW DCD DCQ
BYTE (1 Bytes) WORD (2 Bytes) DWORD (4 Bytes) QWORD (8 bytes)

というものがあります。

(以後これらを総じてDCXと呼びます)

DCX命令の使用例としては次の通りです

LOAD
C_string   DCB  "C_string",0
.got.plt
off_XXXXX     DCQ fprintf
.data.rel.ro
`vtable for'Sample
            DCQ `typeinfo for'
off_XXXX    DCQ sub_XXXXX    

(あまり意識してなかったけどそこら中にいるな.....)

DCXは一般的にReadOnlyなデータ領域に置かれるようですが、どうやらtext領域にも設置できるようです。
本来置かれるはずのないtext領域に設置することによりIDAのディスアセンブルを妨害するということですね。

データ埋め込み (DCQ)

インラインアセンブラを使用してDCQを設置することが出来るようです。
.longに任意の4バイトを続けて直接書きます

extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    __asm__(R"(
        .long 0x12345678
        .long 12345678
    )");

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}

コンパイルしてIDAにかけてみましょう
dcb_demo.png

無事にデータ扱いされてますね。F5もCreate Functionも効きません。

Ghidraにもかけてみましょう

スクリーンショット 2024-04-20 233838.png

Ghidraは頑張ってデコンパイルしてくれますが、何だかダメそうです。

しかしながら、これではとある問題が発生します。

関数内に埋め込んだデータは他の命令と同じように実行されます。
が、埋め込んだデータは有効な命令ではありません。
なので実行するとクラッシュします。

Fatal signal 4 (SIGILL), code 1 (ILL_ILLOPC), fault addr 0x7c70bd523c in tid 8419 (.arm64_studying), pid 8419 (.arm64_studying)

B命令 + DCQ

そこで、無条件分岐命令であるB命令を使用して不正な命令を飛ばしてみます。

extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    __asm__(R"(
        b 0xc
        .long 0x12345678
        .long 12345678
    )");

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}

早速IDAにかけてみましょう

スクリーンショット 2024-04-21 014543.png

なんと、関数として解釈されてしまっています。

スクリーンショット 2024-04-21 130322.png

デコンパイルも出来てしまいました。

どうやら、B命令がIDAに解析のヒントを与えてしまったみたいです。(Ghidraも同じ結果でした)

単純なデータ埋め込みはIDAの解析を妨害することが出来ますが、実行するとクラッシュしてしまいます。
その問題を回避するためにB命令を使ってみましたが、ジャンプ先が分かってしまうためIDAに解析されてしまいました。
もう他に方法ないのでしょうか?

偽の制御フロー + DCQ

もちろんあります。DCQを決して実行されない分岐に配置すれば良いのです。

extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    constexpr volatile bool flag = false;

//    __asm__(R"(
//        .long 0x12345678
//        .long 12345678
//    )");

    if (flag){
        __asm__(R"(
            .long 0x12345678
            .long 12345678
        )");
    }
    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}

最適化されてDCQを消されないようにflag変数に最適化抑止をつけます

IDAにかけてみると....

スクリーンショット 2024-04-21 155451.png

きちんと妨害できてますね。

もちろん、ちゃんと実行も出来ます。

7671.jpg

さて、これでアンチディスアセンブルを施すことが出来ましたが、
これらを組み合わせることで更に強固なディスアセンブルを施すことが出来ます。

偽の制御フロー + B + DCQ

extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    constexpr volatile bool flag = false;
    

    if (flag){
        __asm__(R"(
            B 0x4
            .long 123456789
        )");
    }

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}

スクリーンショット 2024-04-21 165532.png

なんとも歪な形になりましたね

さて、今までの手法は関数の範囲内にデータを埋め込んでそもそもデコンパイルさせないというものでしたが、実は少し工夫すると簡単に突破されてしまいます。

そこで、次はデコンパイルにより解析されることを前提として対策を講じていきたいと思います。

BR命令による間接ジャンプ

さっきB命令でジャンプしてたやん、と思われる方もいらっしゃるかと思いますが、先程はジャンプ先がIDAにばれてしまっていたので解析されてしまいました。
なので、今回はBR命令(Branch to Register)を使用してみます。
静的解析はレジスタの実行時の値を取得できないため、ジャンプ先のアドレスをレジスタに格納することでIDAに解析されないようにするといったところです。

extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    __asm__(R"(
        mov x8, #0x1
        adr x9, #0x10
        mul x8, x9, x8
        .long 0x12345678
        br x8
    )");

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}

アドレス構造としてはこんな感じ

アドレス 命令
~000 adr x9, 0x10 x9に現在のアドレス + 0x10した値を格納
~004 mul x8, x9, x8 x8にx9とx8の乗算結果を格納
~008 .long 0x12345678 フェイクの命令(DCQ)
~00C br x8 x8に格納されているアドレス(下の命令)にジャンプ
~010 (続きの命令) ジャンプしてきた😊

IDAのディスアセンブル結果はこんな感じ

スクリーンショット 2024-04-21 221012.png

うまく騙されてくれていますね
BR命令のところでstringFromJNI関数の終わりと判断され、次の命令からは別の関数として扱われています。

デコンパイル結果はこんな感じ

スクリーンショット 2024-04-21 225448.png

BR先の関数の呼び出しは表示されているものの、インラインアセンブリ埋め込み箇所より上の部分のデコンパイルは失敗したようです。

このケースはうまくIDAを騙せたものの、関数の参照を切ることは出来ませんでした。

ここまで来たらデコンパイル結果を空にしたい、ということで次はret命令を使用してみましょう。

ret命令

今回はx30、関数呼び出し命令時にリターンアドレスを保持するリンクレジスタを使用します。

extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    __asm__(R"(
        adr x8, #0xc
        mov x30, x8
        ret
    )");

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}

アドレス構造としてはこんな感じ

アドレス 命令
~000 adr x8, #0xc x8に現在のアドレス + 0x10した値を格納
~004 mov x30, x8 リンクレジスタにx8に格納されているアドレスを格納
~008 ret リンクレジスタが指すアドレスにリターン
~010 (続きの命令) リターンしてきた😊

勝手にリンクレジスタを書き換えていいの? と考える方もいらっしゃると思いますが、
大丈夫です。コンパイラが破壊される前の値をスタックに保存、関数の終了時にリンクレジスタに戻す処理を挿入してくれます。

IDAのディスアセンブル結果はこんな感じ

スクリーンショット 2024-04-22 011053.png

素晴らしい、傍から見たら完全に別々の関数です

デコンパイル結果はこんな感じ

スクリーンショット 2024-04-22 011309.png

x8レジスタにアドレスを代入しているのにIDAがデコンパイル結果に関数の呼び出しを表示しないのは、ret命令だからでしょう。
ただレジスタにアドレスを代入していると判断されたみたいですね。

さて、これで遂に関数を空っぽにすることに成功しました。

終わり

いかがだったでしょうか。
何通りかの方法をお教えしましたが、私のお気に入りはret命令を使用したアンチディスアセンブルです。
他の命令を使用したり、組み合わせによっては更に強固なディスアセンブルを施すことが出来そうですね。
皆様も是非お試しください!!

参考文献

本記事はこれで終わりとなります。ご覧いただきありがとうございました!

おまけ

ret命令を使用した場合のアンチディスアセンブルでは、ret命令の前後で分断され別々の関数として認識されていました。
それが原因でデコンパイル結果が空っぽになったのですが、関数を元に繋ぎ直したらデコンパイル結果が正常に戻るのかと気になったのでやってみたところ....

スクリーンショット 2024-04-23 110326.png
スクリーンショット 2024-04-22 011309.png

なんと、変わりませんでした。 (やり方が悪かっただけ...?)

では、Ghidraはどうでしょう?
スクリーンショット 2024-04-23 113151.png

ふむ、Ghidraもほとんど空っぽの関数を吐き出していますね。

retの下を見てみると...

スクリーンショット 2024-04-23 113311.png

どうやらretの下の命令達はただのデータとして扱われているようです。

Dissassembleをして命令を元に戻してみましょう
スクリーンショット 2024-04-23 113726.png

まだデコンパイル結果は変わりませんが...

retをnopに書き換えると....
スクリーンショット 2024-04-23 114108.png

お!!
スクリーンショット 2024-04-23 114125.png

キタ―――(゚∀゚)―――― !!

(ん、待てよ? IDAでも出来るんじゃ..?)

これを
スクリーンショット 2024-04-23 114539.png

こうして...スクリーンショット 2024-04-23 114554.png

デコンパイルするとスクリーンショット 2024-04-23 114718.png
(全てが崩れる音)

結論

もう何も意味をなさない。こいつらに取り込まれた時点で我々は既に負けているのである。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?