JPEG of Deathの脆弱性はどのように機能しますか?


94

私が取り組んでいるプロジェクトの死のJPEGと呼ばれる、Windows XPおよびWindows Server 2003の GDI +に対する古いエクスプロイトについて読んでいます。

エクスプロイトについては、次のリンクで詳しく説明されています。http//www.infosecwriters.com/text_resources/pdf/JPEG.pdf

基本的に、JPEGファイルには、COMと呼ばれる(空の可能性がある)コメントフィールドを含むセクションと、COMのサイズを含む2バイトの値が含まれています。コメントがない場合、サイズは2です。リーダー(GDI +)はサイズを読み取り、2を引いて、適切なサイズのバッファーを割り当てて、ヒープにコメントをコピーします。攻撃で0は、フィールドに値を配置します。GDI +はを減算し2、その値はによって-2 (0xFFFe)符号なし整数に変換さ0XFFFFFFFEmemcpyます。

サンプルコード:

unsigned int size;
size = len - 2;
char *comment = (char *)malloc(size + 1);
memcpy(comment, src, size);

ことを観察するmalloc(0)3行目のヒープ上に割り当てられていないメモリへのポインタを返すべきです。どのようにして0XFFFFFFFEバイト(4GB!!!!)を書き込むとプログラムがクラッシュしないのでしょうか?これは、ヒープ領域を超えて、他のプログラムやOSの領域に書き込みますか?次に何が起こりますか?

私が理解しているようにmemcpy、それは単にn宛先からソースに文字をコピーします。この場合、ソースはスタックにあり、宛先はヒープにありnます4GB


mallocはヒープからメモリを割り当てます。悪用はmemcpyの前とメモリが割り当てられた後に行われたと思います
iedoc

余談ですが、値を符号なし整数(4バイト)にプロモートするの memcpyではなく、減算です。
rev

1
以前の回答を実際の例で更新しました。mallocEDの大きさは2バイトではなく、です0xFFFFFFFE。この巨大なサイズは、コピーサイズにのみ使用され、割り当てサイズには使用されません。
Neitsa

回答:


96

この脆弱性は明らかにヒープオーバーフローでした。

どうすれば0XFFFFFFFEバイト(4 GB !!!!)を書き込んでもプログラムがクラッシュしないのですか?

おそらくそうなるでしょうが、場合によっては、クラッシュが発生する前に悪用する時間がありました(場合によっては、プログラムを通常の実行に戻し、クラッシュを回避できます)。

memcpy()が開始すると、コピーは他のいくつかのヒープブロックまたはヒープ管理構造の一部(たとえば、フリーリスト、ビジーリストなど)を上書きします。

ある時点で、コピーは割り当てられていないページに遭遇し、書き込み時にAV(アクセス違反)をトリガーします。その後、GDI +はヒープに新しいブロックを割り当てようとします(ntdll!RtlAllocateHeapを参照)...しかし、ヒープ構造はすべて混乱しています。

その時点で、JPEG画像を注意深く作成することで、ヒープ管理構造を制御されたデータで上書きできます。システムが新しいブロックを割り当てようとすると、おそらく(フリーの)ブロックがフリーリストからリンク解除されます。

ブロックは(特に)flink(Forward link;リストの次のブロック)とblink(Backward link;リストの前のブロック)ポインターで管理されます。flinkとblinkの両方を制御する場合、書き込み可能なものと書き込み可能な場所を制御する可能性のあるWRITE4(What / Where条件の書き込み)がある可能性があります。

その時点で、関数ポインター(SEH [構造化例外ハンドラー]ポインターは、2004年当時は選択の対象でした)を上書きして、コードを実行できます。

ブログ投稿「ヒープの破損:ケーススタディ」を参照してください。

注:フリーリストを使用した悪用について書きましたが、攻撃者は他のヒープメタデータを使用して別のパスを選択する可能性があります(「ヒープメタデータ」はシステムがヒープを管理するために使用する構造です。flinkとblinkはヒープメタデータの一部です)。リンク解除の悪用はおそらく「最も簡単な」悪用です。「ヒープの悪用」をグーグルで検索すると、これに関する多数の調査結果が返されます。

これは、ヒープ領域を超えて、他のプログラムやOSの領域に書き込みますか?

決して。最新のOSは仮想アドレス空間の概念に基づいているため、各プロセスには独自の仮想アドレス空間があり、32ビットシステムで最大4ギガバイトのメモリをアドレス指定できます(実際には、ユーザーランドでその半分しか取得できませんが、残りはカーネル用です)。

つまり、プロセスは別のプロセスのメモリにアクセスできません(サービス/ APIを介してカーネルに要求する場合は例外ですが、カーネルは呼び出し元がそうする権利を持っているかどうかをチェックします)。


この脆弱性を今週末テストすることにしたので、純粋な推測ではなく、何が起こっているのかについて良い考えを得ることができました。この脆弱性は10年前のものなので、この回答では悪用の部分については説明していませんが、この問題について書いても大丈夫だと思いました。

企画

最も困難な作業は、2004年のように、SP1のみを備えたWindows XPを見つけることでした:)

次に、以下に示すように、1ピクセルのみで構成されるJPEG画像をダウンロードしました(簡潔にするためにカットしています)。

File 1x1_pixel.JPG
Address   Hex dump                                         ASCII
00000000  FF D8 FF E0|00 10 4A 46|49 46 00 01|01 01 00 60| ÿØÿà JFIF  `
00000010  00 60 00 00|FF E1 00 16|45 78 69 66|00 00 49 49|  `  ÿá Exif  II
00000020  2A 00 08 00|00 00 00 00|00 00 00 00|FF DB 00 43| *          ÿÛ C
[...]

JPEG画像は、セグメントを挿入するバイナリマーカーで構成されます。上の画像でFF D8は、はSOI(イメージの開始)マーカーFF E0ですが、たとえばはアプリケーションマーカーです。

マーカーセグメントの最初のパラメーター(SOIなどの一部のマーカーを除く)は、長さパラメーターを含み、2バイトマーカーを除く、マーカーセグメントのバイト数をエンコードする2バイトの長さパラメーターです。

FFFEマーカーには厳密な順序がないため、SOIの直後にCOMマーカー(0x )を追加しただけです。

File 1x1_pixel_comment_mod1.JPG
Address   Hex dump                                         ASCII
00000000  FF D8 FF FE|00 00 30 30|30 30 30 30|30 31 30 30| ÿØÿþ  0000000100
00000010  30 32 30 30|30 33 30 30|30 34 30 30|30 35 30 30| 0200030004000500
00000020  30 36 30 30|30 37 30 30|30 38 30 30|30 39 30 30| 0600070008000900
00000030  30 61 30 30|30 62 30 30|30 63 30 30|30 64 30 30| 0a000b000c000d00
[...]

COMセグメントの長さは00 00、脆弱性をトリガーするように設定されています。また、COMマーカーの直後に0xFFFCバイトを挿入しました。このパターンは16進数の4バイトの数字で、脆弱性を「悪用」するときに便利になります。

デバッグ

画像をダブルクリックすると、Windowsシェル(別名 "explorer.exe")のどこかgdiplus.dllにある関数のバグがすぐにトリガーされGpJpegDecoder::read_jpeg_marker()ます。

この関数は、画像内の各マーカーに対して呼び出されます。マーカーセグメントサイズを読み取り、セグメントサイズの長さのバッファーを割り当て、セグメントのコンテンツをこの新しく割り当てられたバッファーにコピーします。

ここで関数の開始:

.text:70E199D5  mov     ebx, [ebp+arg_0] ; ebx = *this (GpJpegDecoder instance)
.text:70E199D8  push    esi
.text:70E199D9  mov     esi, [ebx+18h]
.text:70E199DC  mov     eax, [esi]      ; eax = pointer to segment size
.text:70E199DE  push    edi
.text:70E199DF  mov     edi, [esi+4]    ; edi = bytes left to process in the image

eaxregisterはセグメントサイズを指しedi、画像に残っているバイト数です。

次に、コードはセグメントサイズの読み取りを開始し、最上位バイトから始めます(長さは16ビット値です)。

.text:70E199F7  xor     ecx, ecx        ; segment_size = 0
.text:70E199F9  mov     ch, [eax]       ; get most significant byte from size --> CH == 00
.text:70E199FB  dec     edi             ; bytes_to_process --
.text:70E199FC  inc     eax             ; pointer++
.text:70E199FD  test    edi, edi
.text:70E199FF  mov     [ebp+arg_0], ecx ; save segment_size

そして最下位バイト:

.text:70E19A15  movzx   cx, byte ptr [eax] ; get least significant byte from size --> CX == 0
.text:70E19A19  add     [ebp+arg_0], ecx   ; save segment_size
.text:70E19A1C  mov     ecx, [ebp+lpMem]
.text:70E19A1F  inc     eax             ; pointer ++
.text:70E19A20  mov     [esi], eax
.text:70E19A22  mov     eax, [ebp+arg_0] ; eax = segment_size

これが完了すると、次の計算に従って、セグメントサイズを使用してバッファが割り当てられます。

alloc_size = segment_size + 2

これは以下のコードによって行われます:

.text:70E19A29  movzx   esi, word ptr [ebp+arg_0] ; esi = segment size (cast from 16-bit to 32-bit)
.text:70E19A2D  add     eax, 2 
.text:70E19A30  mov     [ecx], ax 
.text:70E19A33  lea     eax, [esi+2] ; alloc_size = segment_size + 2
.text:70E19A36  push    eax             ; dwBytes
.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)

この例では、セグメントサイズが0であるため、バッファに割り当てられるサイズは2バイトです。

脆弱性は割り当て直後です。

.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)
.text:70E19A3C  test    eax, eax
.text:70E19A3E  mov     [ebp+lpMem], eax ; save pointer to allocation
.text:70E19A41  jz      loc_70E19AF1
.text:70E19A47  mov     cx, [ebp+arg_4]   ; low marker byte (0xFE)
.text:70E19A4B  mov     [eax], cx         ; save in alloc (offset 0)
;[...]
.text:70E19A52  lea     edx, [esi-2]      ; edx = segment_size - 2 = 0 - 2 = 0xFFFFFFFE!!!
;[...]
.text:70E19A61  mov     [ebp+arg_0], edx

このコードは、セグメントサイズ全体(セグメントの長さは2バイトの値)をセグメントサイズ全体(この場合は0)から差し引いて、整数のアンダーフローで終了します:0-2 = 0xFFFFFFFE

次にコードは、画像内に解析するバイトが残っているかどうかを確認し(true)、次にコピーにジャンプします。

.text:70E19A69  mov     ecx, [eax+4]  ; ecx = bytes left to parse (0x133)
.text:70E19A6C  cmp     ecx, edx      ; edx = 0xFFFFFFFE
.text:70E19A6E  jg      short loc_70E19AB4 ; take jump to copy
;[...]
.text:70E19AB4  mov     eax, [ebx+18h]
.text:70E19AB7  mov     esi, [eax]      ; esi = source = points to segment content ("0000000100020003...")
.text:70E19AB9  mov     edi, dword ptr [ebp+arg_4] ; edi = destination buffer
.text:70E19ABC  mov     ecx, edx        ; ecx = copy size = segment content size = 0xFFFFFFFE
.text:70E19ABE  mov     eax, ecx
.text:70E19AC0  shr     ecx, 2          ; size / 4
.text:70E19AC3  rep movsd               ; copy segment content by 32-bit chunks

上記のスニペットは、コピーサイズが0xFFFFFFFE 32ビットチャンクであることを示しています。ソースバッファーは制御され(画像のコンテンツ)、宛先はヒープ上のバッファーです。

書き込み条件

コピーは、メモリページの最後に到達したときにアクセス違反(AV)例外をトリガーします(これは、ソースポインターまたは宛先ポインターのいずれかから可能性があります)。AVがトリガーされると、ヒープは既に脆弱な状態にあります。これは、マップされていないページが検出されるまで、後続のすべてのヒープブロックがコピーによって既に上書きされているためです。

このバグを悪用できるのは、3 SEH(構造化例外ハンドラー、これはtry /低レベルを除く)がコードのこの部分で例外をキャッチしていることです。より正確には、1番目のSEHはスタックを巻き戻し、別のJPEGマーカーを解析するために戻って、例外をトリガーしたマーカーを完全にスキップします。

SEHがなければ、コードはプログラム全体をクラッシュさせるだけでした。したがって、コードはCOMセグメントをスキップし、別のセグメントを解析します。GpJpegDecoder::read_jpeg_marker()新しいセグメントに戻り、コードが新しいバッファを割り当てると、次のようになります。

.text:70E19A33  lea     eax, [esi+2] ; alloc_size = semgent_size + 2
.text:70E19A36  push    eax             ; dwBytes
.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)

システムはブロックをフリーリストからリンク解除します。メタデータ構造が画像のコンテンツによって上書きされたことが起こります。したがって、制御されたメタデータを使用してリンク解除を制御します。次のコードは、ヒープマネージャのシステム(ntdll)のどこかにあります。

CPU Disasm
Address   Command                                  Comments
77F52CBF  MOV ECX,DWORD PTR DS:[EAX]               ; eax points to '0003' ; ecx = 0x33303030
77F52CC1  MOV DWORD PTR SS:[EBP-0B0],ECX           ; save ecx
77F52CC7  MOV EAX,DWORD PTR DS:[EAX+4]             ; [eax+4] points to '0004' ; eax = 0x34303030
77F52CCA  MOV DWORD PTR SS:[EBP-0B4],EAX
77F52CD0  MOV DWORD PTR DS:[EAX],ECX               ; write 0x33303030 to 0x34303030!!!

これで、必要な場所、必要な場所に書き込むことができます...


3

GDIのコードがわからないので、以下は推測にすぎません。

まあ、心に浮かぶことの1つは、一部のOSで気付いた動作の1つです(Windows XPにこれがあったかどうかはわかりません)は、新しい/ mallocで割り当てるときでした。そのメモリには書き込みません。

これは実際にはLinuxカーネルの動作です。

www.kernel.orgから:

プロセス線形アドレス空間のページは、必ずしもメモリに常駐しているとは限りません。たとえば、vm_area_struct内で領域が予約されるだけなので、プロセスのために行われた割り当てはすぐには満たされません。

常駐メモリに入るには、ページ違反をトリガーする必要があります。

基本的に、実際にシステムに割り当てられる前に、メモリをダーティにする必要があります。

  unsigned int size=-1;
  char* comment = new char[size];

RAMに実際の割り当てが実際に行われない場合があります(プログラムは4 GBを使用しません)。Linuxでこの動作を見たことがあるのはわかっていますが、Windows 7のインストール環境では再現できません。

この動作から開始して、次のシナリオが可能です。

そのメモリをRAMに存在させるには、ダーティにする必要があります(基本的にmemsetまたは他の何らかの書き込み)。

  memset(comment, 0, size);

ただし、この脆弱性はバッファオーバーフローを悪用するものであり、割り当ての失敗ではありません。

言い換えれば、私がこれを持っているとしたら:

 unsinged int size =- 1;
 char* p = new char[size]; // Will not crash here
 memcpy(p, some_buffer, size);

連続メモリの4 GBセグメントなどは存在しないため、これはバッファ後の書き込みになります。

4 GBのメモリ全体をダーティにするためにpに何も入力しなかったし、メモリmemcpyを一度にダーティにするか、ページごとにダーティにするかはわからない(ページごとだと思う)。

最終的には、スタックフレームを上書きすることになります(Stack Buffer Overflow)。

もう1つの可能性のある脆弱性は、画像がバイト配列としてメモリに保持され(ファイル全体をバッファーに読み込み)、重要でない情報をスキップするためにsizeofコメントが使用された場合です。

例えば

     unsigned int commentsSize = -1;
     char* wholePictureBytes; // Has size of file
     ...
     // Time to start processing the output color
     char* p = wholePictureButes;
     offset = (short) p[COM_OFFSET];
     char* dataP = p + offset;
     dataP[0] = EvilHackerValue; // Vulnerability here

あなたが述べたように、GDIがそのサイズを割り当てなかった場合、プログラムは決してクラッシュしません。


4
これは、4 GBが大したことではない64ビットシステムの場合にも当てはまります(アドレス空間について言えば)。しかし、32ビットシステムでは(これらも脆弱であるように見えます)、4 GBのアドレススペースを予約することはできません。したがって、a malloc(-1U)は必ず失敗して戻りNULLmemcpy()クラッシュします。
ロドリゴ

9
私はこの行が真実ではないと思います:「最終的には、別のプロセスアドレスに書き込むことになります。」通常、1つのプロセスが別のプロセスのメモリにアクセスすることはできません。MMUの利点を参照してください。
キューx 2015

@MMU利点はい、そうです。私はそれが通常のヒープ境界を越えてスタックフレームを上書きし始めると言うつもりでした。回答を指摘していただき、ありがとうございます。
MichaelCMS 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.