この脆弱性は明らかにヒープオーバーフローでした。
どうすれば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
eax
registerはセグメントサイズを指し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!!!
これで、必要な場所、必要な場所に書き込むことができます...