Tenho lido sobre um antigo exploit contra GDI + no Windows XP e Windows Server 2003 chamado JPEG da morte para 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 do COM. Se não houver comentários, o tamanho é 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 -2 (0xFFFe)
que é convertido em um 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
!!!!) não pode travar o programa? Isso grava além da área de heap 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 no heap e n
está 4GB
.
malloc
tamanho ed é de apenas 2 bytes em vez de0xFFFFFFFE
. Este tamanho enorme é usado apenas para o tamanho da cópia, não para o tamanho da alocação.Respostas:
Essa vulnerabilidade foi definitivamente um estouro de heap .
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.
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):
Uma imagem JPEG é composta por marcadores binários (que introduzem segmentos). Na imagem acima,
FF D8
é o marcador SOI (Start Of Image), enquantoFF 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.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 chamadaGpJpegDecoder::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:
eax
register aponta para o tamanho do segmento eedi
é 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):
E o byte menos significativo:
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:
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:
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:
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: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:
Agora podemos escrever o que quisermos, onde quisermos ...
fonte
Como não conheço o código da GDI, o que está abaixo é apenas especulação.
Bem, uma coisa que me vem à mente é um comportamento que notei em alguns sistemas operacionais (não sei se o Windows XP tinha isso) foi ao alocar com novo /
malloc
, você pode realmente alocar mais do que sua RAM, desde que você não escreve para aquela memória.Este é realmente um comportamento do kernel do Linux.
De www.kernel.org:
Para entrar na memória residente, uma falha de página deve ser acionada.
Basicamente, você precisa sujar a memória antes de realmente ser alocada no sistema:
Às vezes, ele não fará uma alocação real na RAM (seu programa ainda não usará 4 GB). Eu sei que já vi esse comportamento em um Linux, mas não posso, entretanto, replicá-lo agora na minha instalação do Windows 7.
Partindo desse comportamento, o cenário a seguir é possível.
Para fazer com que essa memória exista na RAM, você precisa sujá-la (basicamente memset ou algum outro tipo de gravação):
No entanto, a vulnerabilidade explora um estouro de buffer, não uma falha de alocação.
Em outras palavras, se eu tivesse isso:
Isso levará a uma gravação após o buffer, 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 não sei se
memcpy
suja a memória de uma vez, ou apenas página por página (acho que é página por página).Eventualmente, ele acabará sobrescrevendo o quadro de pilha (Stack Buffer Overflow).
Outra vulnerabilidade mais possível seria se a imagem fosse mantida na memória como um array de bytes (ler o arquivo inteiro no buffer) e o tamanho dos comentários fosse usado apenas para pular informações não vitais.
Por exemplo
Como você mencionou, se o GDI não alocar esse tamanho, o programa nunca travará.
fonte
malloc(-1U)
certamente falhará, retornaráNULL
ememcpy()
quebrará.