it-swarm.dev

Como a vulnerabilidade JPEG of Death opera?

Eu tenho lido sobre uma exploração mais antiga contra o GDI + no Windows XP e Windows Server 20 chamado JPEG da morte de um projeto no qual estou trabalhando.

A exploração é bem explicada no seguinte link: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

Basicamente, um arquivo JPEG contém uma seção chamada COM contendo um campo de comentário (possivelmente vazio) e um valor de dois bytes contendo o tamanho de COM. Se não houver comentários, o tamanho será 2. O leitor (GDI +) lê o tamanho, subtrai dois e aloca um buffer do tamanho apropriado para copiar os comentários no heap. O ataque envolve colocar um valor de 0 No campo. GDI + subtrai 2, Levando a um valor de -2 (0xFFFe) que é convertido no número inteiro não assinado 0XFFFFFFFE Por memcpy .

Código de amostra:

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

Observe que malloc(0) na terceira linha deve retornar um ponteiro para a memória não alocada no heap. Como escrever 0XFFFFFFFE Bytes (4GB !!!!) possivelmente não trava o programa? Isso grava além da área de pilha e no espaço de outros programas e do sistema operacional? O que acontece depois?

Pelo que entendi memcpy, ele simplesmente copia n caracteres do destino para a fonte. Nesse caso, a origem deve estar na pilha, o destino na pilha e n é 4GB.

94
Rafa

Esta vulnerabilidade foi definitivamente um heap overflow .

Como pode escrever 0XFFFFFFFE bytes (4 GB !!!!) possivelmente não travar o programa?

Provavelmente sim, mas em algumas ocasiões você tem tempo para explorar antes que a falha aconteça (às vezes, você pode recuperar o programa para sua execução normal e evitar a falha).

Quando o memcpy () for iniciado, a cópia substituirá alguns outros blocos de heap ou algumas partes da estrutura de gerenciamento de heap (por exemplo, lista livre, lista de ocupado, etc.).

Em algum momento, a cópia encontrará uma página não alocada e acionará um AV (violação de acesso) na gravação. O GDI + tentará alocar um novo bloco no heap (consulte ntdll! RtlAllocateHeap ) ... mas as estruturas do heap agora estão todas desarrumadas.

Nesse ponto, criando 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 desvinculará um bloco (gratuito) da lista livre.

Os blocos são gerenciados com (notavelmente) um ponteiro (link para frente; o próximo bloco na lista) e ponteiros (piscada para trás; bloco anterior na lista). Se você controlar o flink e o piscamento, poderá ter uma possível WRITE4 (condição de gravação de quê/onde), onde você controla o que pode escrever e onde pode escrever.

Nesse ponto, você pode sobrescrever um ponteiro de função ( SEH [Structured Exception Handlers] ponteiros eram um alvo de escolha naquele momento em 2004) e obter a execução de código.

Ver postagem no blog Corrupção de Heap: 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 do heap ("heap metadata" são estruturas usadas pelo sistema para gerenciar o heap; flink e blink fazem parte dos metadados do heap), mas a exploração de desvinculação é provavelmente a mais "fácil". Uma pesquisa no Google por "exploração de pilha" retornará vários estudos sobre isso.

Isso grava além da área de pilha e no espaço de outros programas e do sistema operacional?

Nunca. Os sistemas operacionais modernos baseiam-se no conceito de espaço de endereço virtual, de modo que cada processo possui 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 metade disso no território do usuário, o resto é para o kernel).

Em resumo, um processo não pode acessar a memória de outro processo (exceto se solicitar ao kernel por algum serviço/API, mas o kernel verificará se o chamador tem o direito de fazê-lo).


Decidi testar essa vulnerabilidade neste final de semana, para que pudéssemos ter uma boa idéia 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 ainda 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 em 2004 :)

Em seguida, baixei uma imagem JPEG composta apenas por um único pixel, como mostrado abaixo (recortado por uma questão de concisão):

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 (início da imagem)), 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.

Eu simplesmente adicionei um marcador COM (0x FFFE) logo após o SOI, pois os marcadores não têm 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 está definido como 00 00 Para disparar a vulnerabilidade. Também injetei 0xFFFC bytes 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.

Depuração

Clicar duas vezes na imagem acionará imediatamente o bug no Windows Shell (também conhecido como "Explorer.exe"), em algum lugar em gdiplus.dll, Em uma função chamada GpJpegDecoder::read_jpeg_marker().

Essa função é chamada para cada marcador da figura: simplesmente: lê o tamanho do segmento do marcador, aloca um buffer cujo tamanho é o tamanho do segmento e copia o conteúdo do segmento para esse buffer recém-alocado.

Aqui 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 O registro aponta para o tamanho do segmento e edi é o número de bytes restantes na imagem.

O código passa a ler o tamanho do segmento, começando pelo byte mais significativo (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:

assign_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 é de 2 bytes .

A vulnerabilidade ocorre 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_size (o comprimento do segmento é um valor de 2 bytes) do tamanho inteiro do segmento (0 no nosso caso) e termina com um fluxo de número inteiro: 0 - 2 = 0xFFFFFFFE

O código então verifica se há bytes 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 snippet acima mostra que o tamanho da cópia é pedaços de 0xFFFFFFFE de 32 bits. O buffer de origem é controlado (conteúdo da imagem) e o destino é um buffer no heap.

Condição de gravação

A cópia acionará uma exceção de violação de acesso (AV) quando atingir o final da página de memória (pode ser do ponteiro de origem ou de destino). Quando o AV é acionado, 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 fosse encontrada.

O que torna esse bug explorável é que 3 SEH (manipulador de exceções estruturadas; isso é tentativa/exceto em nível baixo) está capturando exceções nesta parte do código. Mais precisamente, o 1º SEH desenrolará a pilha e voltará a analisar outro marcador JPEG, ignorando completamente o marcador que acionou a exceção.

Sem um SEH, o código teria travado todo o programa. Portanto, o código ignora o segmento COM e analisa outro segmento. Então, voltamos a 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 desvinculará um bloco da lista gratuita. Acontece que as estruturas de metadados foram substituídas pelo conteúdo da imagem; então 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 queremos, onde queremos ...

94
Neitsa

Como não conheço o código da GDI, o que se segue é apenas especulação.

Bem, uma coisa que me vem à mente é um comportamento que eu notei em alguns sistemas operacionais (não sei se o Windows XP tinha isso) foi ao alocar com o novo/malloc, você pode alocar mais do que sua RAM, desde que você não grave nessa memória.

Este é realmente um comportamento do Kernel Linux.

Em www.kernel.org:

As páginas no espaço de endereço linear do processo não são necessariamente residentes na memória. Por exemplo, as alocações feitas em nome de um processo não são satisfeitas imediatamente, pois o espaço é apenas reservado no vm_area_struct.

Para entrar na memória residente, uma falha de página deve ser acionada.

Basicamente, você precisa sujar a memória antes que ela seja realmente alocada no sistema:

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

Às vezes, ele não faz uma alocação real em RAM (seu programa ainda não usa 4 GB) .Eu sei que já vi esse comportamento em um Linux, mas não consigo replicá-lo agora na minha instalação do Windows 7.

A partir desse comportamento, o seguinte cenário é possível.

Para tornar a memória existente em RAM, você precisa torná-la suja (basicamente memset ou alguma outra gravação)):

  memset(comment, 0, size);

No entanto, a vulnerabilidade explora um estouro de buffer, não uma falha de alocação.

Em outras palavras, se eu tivesse isso:

 unsinged int size =- 1;
 char* p = new char[size]; // Will not crash here
 memcpy(p, some_buffer, size);

Isso levará a um buffer de gravação após, porque não existe um segmento de 4 GB de memória contínua.

Você não colocou nada em p para sujar os 4 GB de memória, e eu não sei se memcpy suja a memória de uma só vez, ou apenas página por página (acho que é página por página ).

Eventualmente, ele acabará substituindo o quadro da pilha (Estouro do buffer da pilha).

Outra vulnerabilidade mais possível era se a imagem fosse mantida na memória como uma matriz de bytes (leia o arquivo inteiro no buffer) e o tamanho dos comentários fosse usado apenas para ignorar informações não vitais.

Por exemplo

     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

Como você mencionou, se o GDI não alocar esse tamanho, o programa nunca travará.

3
MichaelCMS