Lockbit 3.0で見つけたアンチデバッグテクニック(その1) | reverse-eg-mal-memoのブログ

reverse-eg-mal-memoのブログ

サイバーセキュリティに関して、あれこれとメモするという、チラシの裏的存在。
medium(英語):https://sachiel-archangel.medium.com/

はじめに(読み飛ばし推奨)

 

2022年に利用がみられているランサムウェアのLockbit 3.0を解析中です。

Lockbit 3.0は、ビルダーが流出した話があるなど、攻撃そのもの以外でもちょっと物議を醸していますね。

Lockbit 2.0に比べると大分中身を変えていて・・・というより、これ全部作り直したんじゃない?というくらい別物になっている印象です。

Lockbit 2.0は、今年のCode BlueのOpen Talksでもネタにして、散々ディスったのですが、Lockbit 3.0はプログラムとしての出来は大分良くなった感じです(マルウェアを褒めてどーする、とかイワナイ)

 

さて、解析ではやはり解析回避や検知回避テクニックがいくつか見つかっており、解析回避テクニックを回避しないと解析ができないようになっています。

Lockbit 2.0でもいくつかのテクニックがみられましたが、さらに追加されているようなので、気づいたものをメモっておきます。

 

なお、Lockbit 2.0については、解析回避テクニックを含めてMBSDさんが詳細な記事を書いているので、そっちを参考にしたほうがいいです(他所に丸投げする技術ブログ)。

 

ランサムウェア「LockBit2.0」の内部構造を紐解く

 

 

 

マルウェア解析回避テクニックも色々あり、それを知って対策しながら解析する必要があるので、「マルウェア解析回避テクニック&対策テクニック集」なんてあると面白いかも?・・・と思ったけど、仮に作ったとして、そもそもそんなニッチなもの誰が買うんだよw

同人誌にしてコミケで売るくらいが関の山カナー。(ウ=ス異本どころか、結構な厚みの本になりそうだなぁ・・・)

 

なお、今回の Lockbit 3.0 は 32bit アプリケーションだったので、解説はそれが前提となります。(64bit の場合、色々読み替えが必要)

 

 

RtlCreateHeap を用いたデバッガ回避テクニック

 

そして脈絡を無視して唐突に始まる本編。

 

個人ブログとはいえ、文章の構成はもうちょっと考えた方がいいんじゃないかと言われそうです。

Lockbit 3.0ではいくつかの解析回避テクニックが見つかっていますが、デバッガ回避について特徴と回避策を紹介します。

1つめとして、RtlCreateHeapのリターン値を利用した回避テクニックを見つけたので解説します。

 

 

なんか変な処理があるぞ?

 

Lockbit 3.0を解析していると、下図のようなコードが見つかりました。

 

 

0x004063C5の call eax は、「RtlCreateHeap」APIのコールであることは解析で分かっていました。

なお、このAPIのアドレスも、Importテーブルに乗せずDLLからアドレスを引っ張ってくるよく見る難読化(いつものアレ)がされているものです。

 

RtlCreateHeap の結果は、0x004063C7 の段階で eax にハンドルが入っているはずです。

実際は、このハンドルはヒープに関するアドレスとなっています。

 

さて、問題はここから。

①の部分(青紫の枠)では、このハンドルをアドレスとして、オフセット 0x40 (つまり、[ハンドル + 0x40]の領域)を参照しています。

この場合32ビット(4バイト)の値を読み込んでいます。

このケースでは RtlCreateHeap の結果のハンドル値は 0x044E0000 だったので、0x044E0040 を読み込んでいます。(下図参照)

 

 

なお、x86はリトルエンディアンのため、①青紫枠の0x004063D1 の mov によって eax 読み込まれた結果は、

 

 eax = 0x40041062

 

であることに注意が必要です。

(リトルエンディアンが分からないひとは、ちょっとググってね)

そのあと eax を shr 1C していますが、これは 28ビットの右シフトであり、残った4ビット値を 0x004063D7 で判定しています。

 

つまり、①青紫枠の処理は、

 

「[ハンドル + 0x40]の領域のデータの先頭4ビットの値が4(2進数で「0100」)かどうか判定している」

 

という処理なのです。

 

判定対象も1ビットだけなので、これは「DWORDのデータ列のフラグチェック」ということが推測できます。

方法は様々ありますが、こういったフラグチェックのような処理はちょくちょく見かけるので、この手の解析では知っておく必要があるテクニックですね(まあ、ここの記事を読む人は大半知ってると思うけど・・・)。

 

 

で、問題はここです。

そもそも、「[ハンドル + 0x40]」に何の値が入っていて、どうしてこのフラグチェックをしたのか?」というハナシです。

 

判定の結果、このフラグが立っていた場合、②の赤枠の処理に行きます。

これは、esi にコピーされた RtlCreateHeap の結果ハンドル値を 1ビット 左にローテートするという処理です。

 

この目的は何か?を考えたとき、私は「値の難読化」を試みていると考えました。

RtlCreateHeap の結果を 1ビット ローテートさせて本来の値と違うものにすることで、解析者が値を探しにくくしたり、何の値か分からなくするということを目的としたのではないか、ということです。

と、いうことは、このハンドル値を使うときに 右に1ビットローテート(ror 1)することで元の値に戻して使うことが予想されます。

これは、xorを使った簡易な難読化と同様に使われるテクニックですね。

 

解析を進めると、案の定 RtlAllocateHeap をコールする処理が見つかりました。

このとき、 RtlCreateHeap で取得したハンドルを使うことになります。

この処理を実行させると・・・

 

値の不正により領域確保失敗でプログラムクラッシュ!!

 

いや、ror 1で値戻さへんのかーいwww

 

なに??

ここまで「テクニック見破ったで(ドヤァ)」って解析して、あまつさえ偉そうに解説してたおいらの面目丸つぶれなんですけど?www

いや、「それが Sachiel クオリティw」と期待されているからやったんだけどね!?

・・・いやまあ、解析時に見事に読みが外されたのは事実でしたw

 

 

となると、

 

「フラグチェックにヒットして rol 1 したことがそもそも問題だった」

 

ってことなんですよね。

やっぱり、「[ハンドル + 0x40]」に何の値が入っていたのか?」ってことが気になりますね。

 

 

RtlCreateHeapのハンドルの中身

 

では、 RtlCreateHeap のリターン値のハンドルがどのような中身になっているか調べてみましょう。

まずは、Microsoft公式のHPです。

やはり、公式は仕様に基づいて可能な限り正しい情報を提供してくれますし、Microsoft はドキュメントも豊富ですからね。

 

RtlCreateHeap 関数 (ntifs.h)

 

読んでみると。

 

戻り値の型は「PVOID」になっていますね。

これは、「型がないポインタ」という意味ですね。

構造体のポインタであれば話は早かったのですが・・・。

 

戻り値の説明を見てみると。

「RtlCreateHeap は、作成されたヒープへのアクセスに使用するハンドルを返します。」

以上。

 

キ w サ w マ w

 

これじゃ何もわからんじゃないかぼけぇええええええwww

 

 

・・・いや、ここは落ち着け。

Microsoft はアメリカの会社で、もちろん原本は英語。

日本語に訳されてないドキュメントも結構あるし、日本語に訳したときに省略されてるケースもある。これは仕方ない。

やはり、原文の記載が最も情報が多いハズだ。

では、英語の原文をチェックしよう!

 

RtlCreateHeap function (ntifs.h)

 

Return value
RtlCreateHeap returns a handle to be used in accessing the created heap.

 

以上。

 

きしゃまあああああぁぁぁぁああああwww

 

「ハンドルとしてvoid型のポインタが返ってくる」ってだけじゃ、その領域にどんなデータがあるか分からんではないか!

公式でこの程度の説明しかないんじゃ、もう内部の仕様書でも出てなければ情報ないんじゃないかね・・・。

 

 

 _SEGMENT_HEAP 構造体

 

・・・と、諦め悪く色々ググっていたら、ようやく見つけました。

(もはや、どんな検索ワードで見つけたか憶えてない・・・)

どうも、このvoidのポインタは、「_SEGMENT_HEAP」と呼ばれる構造体の模様。

 

(2022/11/12 訂正)

この場合、「_HEAP」構造体でした!

このため、旧記事は小文字で残しつつ、以下で訂正します。

 

(旧記事)

-----------------------------------------------------------------------

WINDOWS 10 SEGMENT HEAP INTERNALS

https://www.blackhat.com/docs/us-16/materials/us-16-Yason-Windows-10-Segment-Heap-Internals-wp.pdf

 

P8に「HeapBase and _SEGMENT_HEAP Structure」という項目があり、そこに構造体といくらかの情報が書かれていました。

問題のオフセット0x40を見てみると・・・?

 

_RTL_RB_TREE型の LargeAllocMetadata の値の一部?

パラメータはポインタのはず?

 

まったくしっくりこないパラメータで、資料が違うのか、Windowsのバージョンの変化でメモリの構造が変わったのか、と頭を抱えてさらに調べることになりました。

まあ、結論をいうと、このドキュメントは64ビットアプリケーションの場合のオフセット値が書かれており、解析している32ビットアプリケーションではオフセット値が異なる、というのが理由でした。

動いているOSは確かに64ビットですが、アプリケーションが32ビット版だと、こういうOSで使われている構造体も32ビットになる、ということを見落としていましたね。

 

32ビット、64ビット双方のオフセットがかかれているページも発見しました。

 

Terminus Project - _SEGMENT_HEAP - combined view

 

左側が32ビット版アプリケーションのオフセット、右側が64ビット版アプリケーションのオフセットとなっています。

 

例えば、「Signature」は64ビットではオフセット +0x10 ですが、32ビットでは +0x08 です。

32ビットの後述する検証プログラムのダンプやデバッガでの参照でも、32ビットアプリケーションでは +0x08 に「EE FF EE FF」が格納されていることから、32ビットではこれが「Signature」の固定値と考えられます。

 

32ビット版のオフセットで0x40を見てみると・・・。

使っているバージョンから考えて、「SegmentCount」が相当するようにみえます。

 

うーん、カウンタのようだけど、ポインタよりはマシかな・・・。

実際のところ、絶対使わないような桁の部分に、隠し仕様でパラメータを入れている可能性もあります。

また、そもそも公式の資料ではないと思うので、実は SegmentCount は3バイトで、4バイト目は別の意味の値、という可能性も微レ存ってとこですかね。

-----------------------------------------------------------------------

(旧記事:ここまで)

 

(2022/11/12 訂正:続き)

 

後日、色々調べた結果、RtlCreateHeap で返ってくるアドレスは、 _HEAP 構造体らしいということが判明しました。

原因は、私が調べていた際に RtlCreateHeap で返ってくる値が Segment Heap だと勘違いしていたためです。

公式のドキュメントにはやっぱり書いていないみたい?ですが、色々調べていると研究者のブログなどで出ていますね!

リサーチ不足&勉強不足でした! m(_ _)m

 

以下、私の反省文となります!

 

_HEAP 構造体は以下に掲載されています。

 

Terminus Project - _HEAP - combined view

 

 

この場合、32ビットの0x0040を見ると、「Flags」となっています。

・・・うーん、公式にこの構造体が無い以上、「Flags」と書かれてもフラグの意味が分からない?

 

調べていくと、以下の記事の「Heap Flags and ForceFlags」で触れられているのを発見しました。

 

 

Anti Debugging Protection Techniques with Examples

 

 

今回の件に限らず、色々なアンチデバッグテクニックが書かれており、知らないものも他にあったので、後でしっかり読んで置こう・・・。

 

とりあえず、今回の _HEAP に関しては、「Flags(32ビットの場合 0x0040)」と「ForceFlags(32ビットの場合 0x0044)」にデバッグのパラメータが含まれるということが書かれています。

 

このフラグの意味については、以下の記事に書かれていました。

 

 

Heap Flag

 

 

 

この記事を読む限りでは、デバッグ中の場合、以下のフラグが立つようです。

 

  • HEAP_TAIL_CHECKING_ENABLED (0x20)
  • HEAP_FREE_CHECKING_ENABLED (0x40)
  • HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)

 

これらのフラグが全部立つとすると、HEAP_TAIL_CHECKING_ENABLED | HEAP_FREE_CHECKING_ENABLED | HEAP_VALIDATE_PARAMETERS_ENABLED のため、「0x40000060」と or を取った値が Flags および ForceFlags に設定されていることが予想されます。

 

(2022/11/12 訂正:ここまで)

 

 

IDAおよびx64dbgで実験

 

では、実際に同じAPIを使ってどのようなパラメータになっているか検証してみましょう。

RtlCreateHeap で得られたハンドルをアドレスとし、一定サイズを参照します。

デバッガはIDAとx64dbgの2種類を使い、特定のデバッガのみで起きる現象かどうかを確認します。

つまり、特定のデバッガが仕様として故意にフラグを設定している可能性も考慮した、ということです。

 

また、デバッガを使っていないときのダンプの値も欲しいので、 Visual Studio C++ でちょこちょこっとコードを書いちゃいました。

このコードで通常のコマンドラインで実行してダンプした結果と、同じプログラムをデバッガで参照した結果を比較して、違いがあるかを確認しやすくすることも狙いです。

 

ソースコードは、以下のような至って単純なものです。

 

#include "pch.h"
#include <iostream>
#include <Windows.h>

typedef PVOID(CALLBACK* PROCRTLCREATEHEAP)(UINT, UINT, UINT, UINT, UINT, UINT);
typedef PVOID(CALLBACK* PROCRTLALLOCATEHEAP)(PVOID, ULONG, SIZE_T);

int main()
{
    HMODULE hNtdll = LoadLibrary(L"ntdll.dll");
    PROCRTLCREATEHEAP procRtlCreateHeap = (PROCRTLCREATEHEAP)GetProcAddress(hNtdll, "RtlCreateHeap");
    PROCRTLALLOCATEHEAP procRtlAllocateHeap = (PROCRTLALLOCATEHEAP)GetProcAddress(hNtdll, "RtlAllocateHeap");

    int i;

    if (procRtlCreateHeap == NULL || procRtlCreateHeap == NULL) {
        printf("Get function address error\r\n");
        return -1;
    }

    PVOID pHeapBase = (PVOID)procRtlCreateHeap(0x41002, 0, 0, 0, 0, 0);
    if (pHeapBase == NULL) {
        printf("RtlCreateHeap error\r\n");
        return -1;
    }

    PVOID pHeapAlloc = procRtlAllocateHeap(pHeapBase, 0, 0x10);
    if (pHeapAlloc == NULL) {
        printf("RtlAllocateHeap error\r\n");
        return -1;
    }

    // RtlCreateHeapの先頭 0x100 サイズ分のデータの表示
    unsigned char *pOutput = (unsigned char *)pHeapBase;
    printf("RtlCreateHeapの先頭サイズ0x100のダンプ:\r\n");
    for (i = 0; i < 0x100; i++) {
        printf("%02X ", pOutput[i]);
        if ((i+1) % 16 == 0) {
            printf("\r\n");
        }
    }

    printf("\r\n\r\n");
    printf("RtlAllocateHeapの先頭サイズ0x20のダンプ(うち、サイズ0x10はAllocate対象):\r\n");
    pOutput = (unsigned char *)pHeapAlloc;
    for (i = 0; i < 0x20; i++) {
        printf("%02X ", pOutput[i]);
        if ((i + 1) % 16 == 0) {
            printf("\r\n");
        }
    }

    return 0;
}

 

 

このプログラムを実行し、RtlCreateHeap の結果のダンプ、特にオフセット 0x40を見てみましょう。

以下の通りの結果になりました。

 

 

RtlCreateHeap で得られたハンドルのオフセット +0x40 の4バイトの値は、これをリトルエンディアンとすると、

 

 0x00041002

 

となります。

 

 

一方、同じプログラムをIDA Proで実行し、RtlCreateHeap の結果のダンプ、特にオフセット 0x40を見てみましょう。

以下の通りの結果になりました。

 

 

RtlCreateHeap で得られたハンドルのオフセット +0x40 の4バイトの値は、これをリトルエンディアンとすると、

 

 0x40041062

 

となります。

SegmentCount という名前から、実行時に多少のパラメータの違いがでることは予想されるものの、カウント数としては桁が大きく違っていることがわかります。

 

 

さらに、同じプログラムをx64dbgで実行し、RtlCreateHeap の結果のダンプ、特にオフセット 0x40を見てみましょう。

以下の通りの結果になりました。

 

 

RtlCreateHeap で得られたハンドルのオフセット +0x40 の4バイトの値は、これをリトルエンディアンとすると、

 

 0x40041062

 

となります。

これは、IDA Proで実行したときと全く同じ値であり、デバッガを使わなかった場合と異なる値となりました。

 

以上の検証から、

 

RtlCreateHeap で得られたハンドルのオフセット +0x40の4バイト値「SegmentCount」「Flags」は、デバッガの場合は最上位から2番目のビットが立つことで「0x40」となり、通常実行とは値が異なる。

(2022/11/12 追記)(= HEAP_VALIDATE_PARAMETERS_ENABLED(0x40000000)を判定している、ということでした。)

 

ことが確認できました。

(なお、末尾の0x62もなんだかアヤシイ気がする。)

(2022/11/22 訂正)

デバッガの場合、「通常の値 | 0x40000060」を取ることが前述の Heap Flag の記事の内容から分かったため、末尾の0x60(0x40、0x20と片方のフラグの場合も含む)の値を利用される可能性も考えられると思います。

 

 

Lockbit 3.0の作成者はこれを知っていたか、このテクニックによるデバッガ検知のライブラリを利用するなどして、デバッガの検知と解析回避利用していたということだと思います。

 

 

 

このケースでの回避法

 

今回の考察の結果、「RtlCreateHeap で得られたハンドルのオフセット +0x40の値チェックにヒット」した場合の「rol 1」は攻撃者のデバッガ解析対策であることが分かりました。

では、解析妨害を回避することにしましょう。

 

つまり、「RtlCreateHeap で得られたハンドルのオフセット +0x40の値チェックにヒットしても、rol 1しない」ようにすれば万事解決なのです。

 

そうであれば、このコードにパッチを当てるまでです。

「rol 1」のコード部分を、「nop」(0x90) に置き換えてしまいましょう。

 

 

マルウェアにとっては無駄処理になる nop に置き換えることで、デバッガ対策を回避できるようになりました。

 

Code Blue では、このテクニックを「無駄無駄ラッシュ」とボケたんですが、見事にスベりましたね。

(他にも混ぜていたジョルノ・ジョバーナネタも全てスベりました。ちくしょうw)

 

 

 

(2022/11/22 追記)

このケースでの回避法(その2)

 

私はまだこの方法は未検証ですが、別の方法として、RtlCreateHeap や HeapCreate の結果得られた_HEAP 構造体の Flags および ForceFlags の値を書き換えることで、デバッガを使用していないよう振る舞うことができると考えられます。

この場合、Flags および ForceFlags の値をそれぞれ 0x40000060 のフラグを倒して(XOR)上書きすることで、デバッガを使用していないようにパラメータを偽装することは可能だと思います。

今回の場合、Flags(オフセット0x0040)の 0x40041062 を 0x00041002、ForceFlags (オフセット0x0044)の0x40000060 を 0x00000000 にすることになります。

これにより、デバッガ回避のためにこの値を参照するテクニックは回避できると思います。

 

ただし、_HEAP 構造体上のパラメータではデバッガを使用していないことになるため、OSやデバッガがこの値を参照した場合にもデバッガを使用していないと判定されると思われるため、それが原因となって予想外の動作をする可能性が生じることに注意してください。

(余裕があったら、この方法も試してみたいなぁ)

 

 

 

まとめ

 

今回見つけたテクニックは、「RtlCreateHeap で得られたハンドルの特定の値を参照することで、デバッガを使用しているかを判定し、デバッガと判定した場合はハンドルの値をおかしくすることで、後の処理を失敗させる」というものでした。

最初は何のための判定か分からなかったものの、明らかに値をおかしくしているのは見えていたので、早い段階で特定と対処ができました。

むしろ、根拠となるドキュメント捜索や見つかった解析資料の読み込みと理解のほうがよっぽど時間がかかりましたよ!

 

今回は公式のドキュメントが見つからなかったケース(頑張って探せばあるのかもしれないけれども)でしたが、研究によってデバッガ使用時に特定のパラメータに違いが出ていることが発見されていたりします。

そして、それを利用したデバッガ回避に利用していることを発見した、というものでした。

 

コードを読めればそこまで難しいテクニックではないものの、デバッガで一気に処理を進めるとこのトラップに引っかかりやすくなります。

解析に時間をかけさせる、あるいはレベルの低い解析者を振るい落とす目的なんじゃないかと推測しています。

 

デバッガでの解析を勉強し始めたばかりの人などは苦労しそうなテクニックだったため、今回は対策も含めて紹介しました。

また、なにより本人が折角調べたのに数か月もすると忘れそうなのでメモっておいた次第です。

 

さて、今回はまだ続きがある・・・んですが、長くなったので一旦ここまでとします。

またボチボチ書き足す予定なので、マルウェアのバイナリ解析が大好きな同類さんは今後もお楽しみにw