web-dev-qa-db-ja.com

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

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

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

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

サンプルコード:

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

3行目のmalloc(0)は、ヒープ上の未割り当てメモリへのポインタを返すことに注意してください。 _0XFFFFFFFE_バイト(_4GB_ !!!!)を書き込むと、プログラムがクラッシュしない可能性があります。これは、ヒープ領域を超えて、他のプログラムとOSのスペースに書き込みますか?それではどうなりますか?

memcpyを理解しているので、単にn文字を宛先からソースにコピーします。この場合、ソースはスタック上にあり、宛先はヒープ上にあり、nは_4GB_です。

94
Rafa

この脆弱性は間違いなく heap overflow でした。

0XFFFFFFFEバイト(4 GB !!!!)を書き込むと、プログラムがクラッシュしない可能性がありますか?

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

Memcpy()が開始されると、コピーは他のヒープブロックまたはヒープ管理構造の一部(空きリスト、ビジーリストなど)を上書きします。

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

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

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

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

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

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

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

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

要するに、プロセスは別のプロセスのメモリにアクセスできません(何らかのサービス/ APIを介してカーネルに要求する場合を除き、呼び出し元にアクセス権があるかどうかをカーネルが確認します)。


今週末にこの脆弱性をテストすることにしました。そのため、純粋な推測ではなく、何が起こっているのかを知ることができました。この脆弱性は10年が経過しているため、この回答で悪用の部分については説明していませんが、この脆弱性について書くことは問題ないと考えました。

計画

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

次に、以下に示すように、単一のピクセルのみで構成される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バイトの長さパラメーターです。

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

_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バイトの数字)で挿入しました。これは、脆弱性を「悪用」するときに便利になります。

デバッグ

画像をダブルクリックすると、GpJpegDecoder::read_jpeg_marker()という名前の関数のWindowsシェル(別名「Explorer.exe」)、_gdiplus.dll_のバグがすぐにトリガーされます。

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

ここで機能の開始:

_.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
_

eaxレジスタはセグメントサイズを指し、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
_

このコードは、セグメントサイズ全体(この場合は0)からsegment_sizeサイズ(セグメントの長さは2バイトの値)を単純に減算し、整数アンダーフローで終わります:0-2 = 0xFFFFFFFE

次に、コードは、イメージに解析するバイトが残っていることを確認し(これが正しい)、コピーにジャンプします。

_.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(Structured Exception Handler;これはtry/except低レベル)がコードのこの部分で例外をキャッチしていることです。より正確には、最初の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!!!
_

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

94
Neitsa

私はGDIのコードを知らないので、以下は推測にすぎません。

念頭に置いて思い浮かぶことの1つは、一部のOS(Windows noticeXPにこれがあったかどうかはわかりません)で気づいた1つの動作です。new/ mallocで割り当てるとき、そのメモリに書き込まない限り、RAM。

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

Www.kernel.orgから:

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

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

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

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

時々、実際にRAM(プログラムは4GBを使用しません)で実際の割り当てを行いません。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);

これは連続メモリの4GBセグメントのようなものがないため、バッファ後の書き込みにつながります。

4GBのメモリ全体をダーティにするためにpに何も入れなかったので、memcpyが一度にメモリをダーティにするのか、ページごとにダーティになるのかわかりません(ページごとだと思います)。

最終的には、スタックフレームが上書きされます(スタックバッファーオーバーフロー)。

もう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がそのサイズを割り当てなかった場合、プログラムはクラッシュしません。

3
MichaelCMS