Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
CVE-2015-0057 — Análise técnica detalhada e implementação de exploit para CVE-2015-0057, uma vulnerabilidade de use-after-free no win32k.sys, cobrindo sistemas Windows de 32 e 64 bits do XP ao 8.1. | Kitploit
Ferramentas/GitHubGitHub/highandhigh/cve-2015-0057
Forensia de MemóriaAnálise de VulnerabilidadesExploraçãoEngenharia ReversaPapers e PesquisaAprendizado e EducaçãoExploração de Binários
GitHubhighandhigh/cve-2015-0057

CVE-2015-0057

Análise técnica detalhada e implementação de exploit para CVE-2015-0057, uma vulnerabilidade de use-after-free no win32k.sys, cobrindo sistemas Windows de 32 e 64 bits do XP ao 8.1.

Ver Repositório
83há 9 anosAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

CVE-2015-0057漏洞在32位和64位系统上的利用

Exploração da vulnerabilidade CVE-2015-0057 em sistemas de 32 e 64 bits

Autor: Aaron Adams

Tradução: 55-AA

Nota do tradutor: algumas partes deste artigo foram traduzidas livremente; em caso de dúvida, consulte o original.

Terminologia:

  • Primitiva (primitive): análoga a uma função, é um conjunto de operações de corrupção que realiza uma funcionalidade completa e reutilizável, como ler memória, gravar dados arbitrários etc.

Prefácio

No início deste ano, entrei em contato com uma vulnerabilidade interessante no win32k.sys (CVE-2015-0057) e implementei sua exploração estável em sistemas de 32 e 64 bits, variando do XP ao Windows 8.1 (com algumas exceções). Este artigo descreve em detalhes como concluí a exploração nessas duas plataformas; o final também traz algumas coisas extras. Além disso, descreve como realizar a exploração com privilégios de baixa integridade no Windows 8.1 com SMEP habilitado.

Este artigo é longo. Eu me esforcei para fornecer o máximo de detalhes possível, mostrando a complexidade de explorar essa vulnerabilidade em vez de escondê-la; naturalmente, também omiti alguns detalhes. Espero que estas informações sejam úteis para todos.

Introdução

Em 10 de fevereiro de 2015, a Microsoft divulgou os detalhes do MS15-010. Esse bug foi descoberto primeiramente por Udi Yavo, da enSilo. Udi fez uma excelente análise no breaking malware blog, "one bit rule-bypassing windows 10 protections using single bit". Recomendo uma leitura atenta desse artigo para entender o bug a fundo, embora eu vá fornecer aqui o máximo de detalhes possível sobre os obstáculos que precisam ser superados ao acionar a vulnerabilidade. A exploração dessa vulnerabilidade é muito interessante; muitos detalhes vêm do blog do Udi. Segue a declaração dele:

Divulgação responsável: embora este blog seja técnico, não revelaremos nenhum código nem detalhes completos, para impedir que qualquer técnico especialista reproduza essa exploração.

Como recompensa extra por explorar essa vulnerabilidade, tivemos uma evolução Pokémon: um gênio técnico. Acho que devo dar crédito ao Udi por ter descoberto o bug e por ter disponibilizado no blog as circunstâncias e os detalhes para explorá-lo — essas informações foram extremamente úteis.

Antes, eu nunca havia explorado uma vulnerabilidade no win32k.sys nem estava familiarizado com callbacks em modo usuário e muitas das APIs relacionadas. Por isso, também agradeço a alguns pesquisadores de segurança renomados pelos recursos inovadores que disponibilizaram on-line, como Skywing, Tarjei Mandt, Alex Ionescu e j00ru. Todas essas pessoas disponibilizaram uma quantidade enorme de informações técnicas e merecem reconhecimento. Minha principal referência foi o artigo de Tarjei Mandt, Win32k.sys exploitation paper.

Enquanto escrevia esta exploração, um excelente engenheiro reverso implementou a exploração estável do CVE-2015-1701; o código de exemplo sobre callbacks em modo usuário foi muito útil. Agradeço ao autor.

Vale notar que minha análise foi feita no Windows 7, pois parece ser a única versão em que todas as estruturas do win32k.sys possuem símbolos correspondentes. A maioria desses símbolos também se aplica às estruturas de outras versões do Win32k.sys. Por alguma razão, a Microsoft removeu esses símbolos a partir do Windows 8.

Por fim, gostaria de dizer que meu método de exploração é bastante complexo. É perfeitamente possível que exista uma forma mais simples que eu não tenha encontrado. Gostaria muito de ouvir se alguém usou um método diferente. De todo modo, espero que tudo isso seja útil para pesquisas sobre vulnerabilidades no win32k.sys.

O BUG

Abaixo, vemos o bug na desmontagem do win32k!xxxEnableWndSBArrows. É um bug bastante sutil:

Situação sem patch:

root@kitploit:~
.text:FFFFF97FFF1B157D mov r8d, r13d 
.text:FFFFF97FFF1B1580 mov rdx, r14 
.text:FFFFF97FFF1B1583 call xxxDrawScrollBar    ; 触发usermode callback 
.text:FFFFF97FFF1B1588 jmp short loc_FFFFF97FFF1B1519
 [...] 
.text:FFFFF97FFF1B1519 mov eax, [rbx]           ; 引用 tagSBINFO 指针而没有经过检查 
.text:FFFFF97FFF1B151B mov ebp, 0FFFFFFFBh 
.text:FFFFF97FFF1B1520 xor eax, esi

No código acima, Win32K!xxxdrawscrollbar pode, quando apropriado, fazer callback para o espaço do usuário. No código do modo usuário, o ponteiro para tagSBINFO pode ser liberado pelo atacante. Ao retornar ao código acima, a instrução em 0xFFFFF97FFF1B1519 fará referência a um ponteiro inválido.

Situação com patch:

root@kitploit:~
    .text:FFFFF97FFF1D69C3 xor r8d, r8d 
    .text:FFFFF97FFF1D69C6 mov rdx, rbp 
    .text:FFFFF97FFF1D69C9 call xxxDrawScrollBar        ; 触发usermode callback
    .text:FFFFF97FFF1D69CE cmp rbx, [rdi+0B0h]          ; 检查 tagSBINFO 指针是否正确 
 ---.text:FFFFF97FFF1D69D5 jz short loc_FFFFF97FFF1D69E4; 如果正确则继续原来的流程 
 |  .text:FFFFF97FFF1D69D7 mov rcx, rbp 
 |  .text:FFFFF97FFF1D69DA call _ReleaseDC 
 |  .text:FFFFF97FFF1D69DF jmp loc_FFFFF97FFF1D6958     ; 跳转到函数退出 
 |->.text:FFFFF97FFF1D69E4 mov eax, [rbx]               ; 放心地使用正确的 tagSBINFO 指针 
    .text:FFFFF97FFF1D69E6 xor eax, r14d

Na versão corrigida acima, vemos que é feita uma verificação antes de usar o ponteiro tagSBINFO. As informações relacionadas à estrutura serão fornecidas mais adiante.

Fundamentos — Corrupção de memória, fase 1

Ao implementar esta exploração, realizamos diversas corrupções. E foi durante uma dessas corrupções que acionamos a vulnerabilidade.

A causa técnica desse bug é um use-after-free (UAF) no heap da área de trabalho (desktop heap). No começo, isso me confundiu bastante, pois eu não conhecia o mecanismo de callbacks em modo usuário do win32k.sys nem como ele funcionava. Por isso, pensei que fosse uma condição de corrida com locks que causava o UAF. Na verdade, o lock da estrutura é usado corretamente, e o fluxo é exatamente o esperado. Em resumo, o verdadeiro problema é:

  1. A função win32k!xxxEnableWndSBArrows possui um ponteiro para um tagSBINFO no desktop heap, obtido a partir da estrutura tagWND associada à janela, usado para descrever uma barra de rolagem.
  2. win32k!xxxEnableWndSBArrows chama uma função que aciona um callback em modo usuário (função que pode ser hookada em modo usuário).
  3. Quando o código está em execução no espaço do usuário, as estruturas no desktop heap podem ser modificadas por outras chamadas de sistema do win32k, inclusive liberando a estrutura tagSBINFO do desktop heap.
  4. Ao retornar ao modo kernel, win32k!xxxEnableWndSBArrows não relê o tagSBINFO a partir do tagWND nem verifica se o ponteiro usado anteriormente ainda é válido; em vez disso, usa diretamente o ponteiro original (que na verdade já foi liberado).

É só isso. Sem considerar os callbacks em modo usuário, essa fase é bastante direta.

Entendendo como controlamos o fluxo

Mas como fazemos a corrupção e por quê? Como mencionado no blog do Udi, é possível definir ou limpar 2 bits em um local que o código do sistema trata como o campo WSBflags na estrutura tagSBINFO. Essa não é uma abordagem tradicional de exploração de UAF, mas o artigo dá uma dica de como fazer isso, e explicarei nas próximas seções. Primeiro, vamos entender como manipulamos e controlamos esses bits.

Estrutura tagSBINFO (idêntica em 32 e 64 bits):

root@kitploit:~
kd> dt -b !tagSBINFO 
win32k!tagSBINFO 
    +0x000 WSBflags     : Int4B 
    +0x004 Horz         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B 
    +0x014 Vert         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B

Esse UAF está na função win32k!xxxEnableWndSBArrows(), usada para ativar ou desativar as setas de um ou de ambos os controles de barra de rolagem (horizontal ou vertical). O controle de barra de rolagem é uma janela especial usada para manipular barras de rolagem. Ele pode ser criado chamando CreateWindow() com a classe de janela interna "SCROLLBAR".

Protótipo da função win32k!xxxEnableWndSBArrows():

root@kitploit:~
BOOL xxxEnableWndSBArrows(PWND wnd, UINT WSBflags, UINT wArrows);

O significado do parâmetro WSBflags é o mesmo definido no WinUser.h e indica quais barras de rolagem serão manipuladas:

root@kitploit:~
#define SB_HORZ 0 
#define SB_VERT 1 
#define SB_CTL  2 
#define SB_BOTH 3

O parâmetro wArrows indica se o estado das setas está habilitado ou desabilitado. Um bit setado significa que as setas estão desabilitadas; caso contrário, habilitadas. Os dois bits menos significativos de wArrows representam a barra horizontal; os próximos dois bits representam a barra vertical; os demais bits não são relevantes para o propósito desta exploração.

O código a seguir é retirado da função win32k!xxxEnableWndSBArrows() e define ou limpa os bits relacionados às setas horizontais se SB_HORZ ou SB_BOTH estiverem setados:

O bug existe ao definir os flags das barras de rolagem horizontal e vertical. Depois de atualizar a barra horizontal, uma vez que a janela correspondente esteja visível na área de trabalho, win32k!xxxEnableWndSBArrows() chama win32k!xxxDrawScrollBar(), que, como mencionado em artigos anteriores, pode acionar um potencial callback em modo usuário.

Antes de falarmos sobre o callback em modo usuário, continuemos discutindo o que acontece depois da chamada a win32k!xxxDrawScrollBar(). Na verdade, é a mesma lógica da barra horizontal, apenas com uma diferença de alguns bits. Se optarmos por desabilitar a barra vertical e considerarmos que acionamos o UAF, isso gravará 2 bits em algum lugar dentro do bloco de heap do tagSBINFO. Portanto, se o valor original for 0x2, ele se tornará 0xe. Como mostra a figura abaixo.

Essa mudança de bits já é suficiente para levar à execução de código. Não investigei como concluir a exploração limpando bits, mas é possível.

O ponto principal acima é que, para operar simultaneamente as barras horizontal e vertical, é necessário criar um controle de barra de rolagem que possua esses dois elementos. Isso é feito chamando CreateWindow() com os flags WS_HSCROLL e WS_VSCROLL. O código é o seguinte:

root@kitploit:~
g_hSBCtl = CreateWindowEx( 
    0,                  // No extended style 
    "SCROLLBAR",        // class 
    NULL,               // name 
    SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // 垂直+水平 
    10,                 // x 
    10,                 // y 
    100,                // width 
    100,                // height 
    g_hSpray[UAFWND],   // 无模式父窗口 
    (HMENU)NULL,
    NULL,               // window owner 
    NULL                // extra params 
    );

É possível garantir que a janela fique visível com o código abaixo (normalmente isso já é o padrão; aqui a chamada é explícita):

root@kitploit:~
result = ShowWindow(g_hSBCtl, SW_SHOW);

A barra de rolagem já vem habilitada por padrão. Quando estivermos prontos para tentar acionar o código vulnerável, podemos desabilitar a barra de rolagem para corromper os bits de que precisamos:

root@kitploit:~
result = EnableScrollBar(g_hSBCtl, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);

Acionando a vulnerabilidade

Embora tenhamos descrito os detalhes do bug e como acionar o código relacionado, ainda ignoramos a etapa mais importante: interceptar o callback em modo usuário iniciado por win32k!xxxDrawScrollBar() para podermos alterar o conteúdo do heap antes que win32k!xxxEnableWndSBArrows() continue a execução. Precisamos de fato acionar o bug, mas sem conhecer nada sobre o Win32k.sys nem sobre as APIs relacionadas — exatamente como eu estava no começo — isso é uma aventura por si só.

Em artigos anteriores havia um bom diagrama da pilha de chamadas mostrando esse processo em profundidade, com a ativação por meio de win32k!xxxDrawScrollBar(), a chamada subsequente a ClientLoadLibrary() e a distribuição via KeUserModeCallback(). Precisamos realmente descobrir como o KeUserModeCallback() é chamado para podermos fazer o HOOK em nosso próprio processo.

Encontrei alguns bons artigos que mencionam, em maior ou menor grau, os callbacks em modo usuário. As partes relacionadas ao win32k são muito úteis:

  • https://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf (artigo do Tarjei)
  • http://azimuthsecurity.com/resources/recon2012_mandt.pptx (PPT do Tarjei com muitas informações adicionais)
  • http://www.nynaeve.net/?p=204
  • http://www.cprogramdevelop.com/3825874/
  • http://www.zer0mem.sk/?p=410
  • https://www.reactos.org/wiki/Techwiki:RegisterUserApiHook
  • http://pasotech.altervista.org/windows_internals/Win32KSYS.pdf
  • http://j00ru.vexillium.org/?p=614
  • http://uninformed.org/index.cgi?v=10&a=2#SECTION00042000000000000000

Normalmente, cada processo possui uma tabela de ponteiros para funções de callback em modo usuário; PEB->KernelCallBackTable aponta para essa tabela. Quando o kernel quer chamar uma função em modo usuário, ele passa o índice da função para KeUserModeCallBack(). No exemplo acima, o índice aponta para a função __ClientLoadLibrary() no modo usuário.

O KeUserModeCallBack() consulta a função correspondente em PEB->KernelCallBackTable de acordo com o índice e a executa, chamando por fim KiUserModeCallbackDispatch() no modo usuário.

Para fazer HOOK no ponto de entrada desejado, devemos localizar o índice de __ClientLoadLibrary() em PEB->KernelCallBackTable e substituí-lo pela nossa própria função. Vale notar que esse índice varia de acordo com a versão do sistema operacional e está relacionado à plataforma de hardware.

Se quisermos ver PEB->KernelCallBackTable, podemos encontrar o endereço dessa tabela com o WinDbg. Comparando as plataformas de 32 e 64 bits, não notei grandes diferenças.

root@kitploit:~
kd> dt !_PEB @$peb 
ntdll!_PEB 
    +0x000 InheritedAddressSpace    : 0 '' 
    +0x001 ReadImageFileExecOptions : 0 '' 
    +0x002 BeingDebugged            : 0 '' 
    +0x003 BitField                 : 0x8 '' 
    +0x003 ImageUsesLargePages      : 0y0 
[...] 
    +0x02c KernelCallbackTable      : 0x76daf620 Void 
    
kd> dds 0x76daf620 
76daf620 76d96443 user32!__fnCOPYDATA 
76daf624 76ddf0e4 user32!__fnCOPYGLOBALDATA 
76daf628 76da736b user32!__fnDWORD 
76daf62c 76d9d603 user32!__fnNCDESTROY 
76daf630 76dc50f9 user32!__fnDWORDOPTINLPMSG 
76daf634 76ddf1be user32!__fnINOUTDRAG 
76daf638 76dc6cd0 user32!__fnGETTEXTLENGTHS 
76daf63c 76ddf412 user32!__fnINCNTOUTSTRING
76daf640 76d9ce49 user32!__fnINCNTOUTSTRINGNULL 
[...] 
76daf724 76da3962 user32!__ClientLoadLibrary 

kd> ?? (0x76daf724-0x76daf620)/4 int 0n65

No exemplo acima, sabemos que o índice de __ClientLoadLibrary é 65, e é aí que faremos o HOOK. Depois de fazer o HOOK, descobri que __ClientLoadLibrary é chamada várias vezes por código relacionado ao win32k! A primeira coisa a fazer é, antes de acionarmos a chamada que nos interessa, avisar nosso código de HOOK para sabermos que realmente chegamos ao ponto que queremos modificar. Por isso, o código do HOOK usa uma variável global como sinalizador; somente quando esse sinalizador está ativo é que as operações relevantes são executadas.

Agora temos dois obstáculos:

  1. Se deixarmos a __ClientLoadLibrary original executar normalmente, quando a vulnerabilidade fosse acionada no win32k, percebi que o fluxo não caía no modo usuário. Não investiguei a fundo; apenas suponho que a biblioteca dinâmica que essa chamada deveria carregar já tivesse sido carregada e, portanto, não haveria mais necessidade de chamar essa função de carregamento. Para fazer com que esse carregamento acontecesse, fiz com que a função de HOOK da __ClientLoadLibrary não retornasse resultado a cada chamada, forçando-a a tentar carregar repetidamente. Meu trabalho era apenas devolver NULL no bloco de parâmetros, o que foi feito revertendo a função __ClientLoadLibrary() na user32.dll.
  2. A execução de EnableScrollBar() acaba acionando __ClientLoadLibrary e depois chega ao ponto que queremos explorar por meio de win32k!xxxDrawScrollBar(). Por isso, precisamos saber quantas vezes essa função é chamada antes de aparecer o que nos interessa. Com uma contagem, consigo saber exatamente quando o bug foi acionado e o código de HOOK foi atingido. Felizmente, essa contagem é fixa entre plataformas e versões de sistema operacional.

Assim, a função de HOOK fica com a seguinte forma:

root@kitploit:~
void ClientLoadLibraryHook(void * p) 
{ 
    CHAR Buf[PGSZ]; 
    memset(Buf, 0, sizeof(Buf)); 
    if (g_PwnFlag) 
    { 
        dprintf("[+] __ClientLoadLibrary hook called\n"); 
        if (++g_HookCount == 2) 
        {
            g_PwnFlag = 0;      // 仅仅执行一次.. 
            ReplaceScrollBarChunk(NULL); 
        }
    } 
    fpClientLoadLibrary(&Buf); // 调用原始函数
}

Assim que confirmamos que a chamada veio de win32k!xxxDrawScrollBar(), podemos tentar acionar o bug. Por enquanto, considere apenas o acionamento: basta chamar DestroyWindow(g_hSBCtl). Isso fará com que a estrutura tagSBINFO da janela seja liberada. A estrutura da janela em si não é liberada imediatamente, pois seu contador de referências ainda está em uso pela chamada original, mas a tagSBINFO não possui esse mecanismo de contagem de referências e, portanto, é liberada imediatamente.

Nesse ponto, acionamos o bug. Embora não tenhamos realocado o bloco de heap que contém o tagSBINFO, ainda podemos gravar os 2 bits que representam a desabilitação no heap já liberado. O próximo passo é substituir esse bloco liberado por algo que desejamos, para podermos fazer algo mais interessante do que apenas definir alguns bits. Para isso, precisamos entender um pouco do histórico do desktop heap.

Desktop Heap

O win32k.sys usa o desktop heap para armazenar objetos GUI associados a uma determinada área de trabalho. Isso inclui objetos de janela e suas estruturas relacionadas, como listas de propriedades, textos de janela e barras de rolagem. O artigo do Tarjei menciona isso, mas é importante notar que o desktop heap é, na verdade, uma versão simplificada de um alocador de backend do modo usuário, e também usa RtlAllocateHeap() e RtlHeapFree() para operar. O desktop heap é mantido por uma estrutura _HEAP; por não haver alocador de front-end, não existem LFH (Low Fragmentation Heap) nem listas lookaside, entre outras coisas.

Cada área de trabalho criada possui um desktop heap correspondente que a atende. Isso significa que podemos alocar uma nova área de trabalho para obter um desktop heap "limpo", no qual nossas operações são mais previsíveis. No entanto, isso não faz sentido para processos com baixa integridade, pois esses processos não têm permissão para criar uma nova área de trabalho.

Agora, o principal problema é rastrear o processo de alocação (mais detalhes sobre metadados etc. serão abordados adiante).

Monitorando as alocações no desktop heap

Para observar as alocações e liberações no desktop heap, costumo usar scripts do WinDbg:

Monitoramento de heap em 64 bits

root@kitploit:~
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", @rcx, @edx, @r8; .echo ; gc";
ba e 1 nt!RtlAllocateHeap "r @$t2 = @r8; r @$t3 = @rcx; gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @rax; gc";

Monitoramento de heap em 32 bits

root@kitploit:~
ba e 1 nt!RtlAllocateHeap "r @$t2 = poi(@esp+c); r @$t3 = poi(@esp+4); gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @eax; gc"; 
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", poi(@esp+4), poi(@esp+8), poi(@esp+c); .echo ; gc"

Além desses scripts de depuração, como o desktop heap é apenas uma forma simplificada do alocador de backend do modo usuário, também podemos usar o comando !heap, nativo do WinDbg.

Preenchendo os buracos no heap

Para explorar esse bug, precisamos substituir o bloco de heap tagSBINFO liberado mais recentemente. E sabemos como explorar esses bugs típicos: corrompendo dados adjacentes. Isso nos impõe um requisito básico: pré-alocar blocos de heap próximos à estrutura que queremos corromper. Para prever onde um bloco de heap será alocado, precisamos controlar todo o layout do heap (ou o máximo possível). Para atender a isso, uma abordagem viável é alocar o maior número possível de blocos de heap para preencher os blocos já liberados; assim, os blocos recém-alocados ficam contíguos. Quando precisamos de um buraco, podemos cavá-lo em uma posição previsível (liberando um bloco já alocado).

Esta parte é uma compreensão simples dos fatores que afetam a alocação; os scripts do WinDbg acima nos ajudam. Tarjei, em seu PPT sobre win32k, mencionou os principais objetos alocados no desktop heap, exatamente o que observei. São eles:

  • Window
  • Menu
  • Hook
  • CallProcData
  • Input Context

O desktop heap é bastante interessante: a maioria das alocações está diretamente associada a objetos de janela e é gerenciada por meio da estrutura tagWND. Isso significa que, se quisermos alocar um bloco de heap de tamanho arbitrário (os chamados blocos pequenos para preencher buracos pequenos), primeiro precisamos alocar uma janela associada a ele. Você pode pensar na estrutura da janela como a interface para alocação no heap. Outro ponto interessante é que muitos blocos de heap alocados por operações de janela não podem ser liberados imediatamente, a menos que a própria janela seja destruída, o que obviamente afeta o heap. Por fim, vamos supor que alocamos um bloco de heap de tamanho N por meio de uma janela, como no exemplo acima. Será que precisamos alocar muitos blocos de tamanho N? Pode-se afirmar que as estruturas de janela alocadas, independentemente do tamanho, não são armazenadas em uma lista encadeada. Portanto, cada janela pode controlar uma alocação de heap de tamanho N. Ou seja, se você precisar alocar um grande número de blocos de tamanho N, será necessário primeiro criar um grande número de janelas, usando-as para auxiliar na alocação dos blocos.

Há também três tipos importantes de dados alocados no desktop heap que podemos explorar indiretamente por meio de objetos de janela para controlar os dados no heap. Usamos bastante esses tipos de dados para implementar a exploração e construir o heap feng shui. Os três tipos são:

  1. Estrutura tagPROPLIST: se for alocada pequena o suficiente, pode preencher buracos pequenos. Um objeto de janela contém uma tagPROPLIST; em sistemas de 32 bits, são 0x10 bytes; em sistemas de 64 bits, 0x18 bytes.
  2. Texto da janela: é uma string UNICODE de tamanho arbitrário alocada no desktop heap, armazenada por meio da estrutura _LARGE_UNICODE_STRING embutida no tagWND. Observe que o campo strName é uma estrutura, não um ponteiro, mas essa estrutura contém um ponteiro relacionado ao bloco de heap do texto da janela.
  3. Estrutura tagSBINFO: a raiz da vulnerabilidade, contém 4 campos ou todos os campos que podemos controlar.

A Figura 2 mostra as relações entre esses tipos de dados:

Para inicializar o heap, criei um grande número de estruturas tagWND (criando objetos de janela). Isso pode preencher muitos buracos grandes no heap e também nos fornece interfaces para alocar outros blocos de heap de que precisamos. No win8 e no win8.1, alocar uma nova janela faz com que uma estrutura tagPROPLIST seja alocada automaticamente (isso pode ser observado com os scripts do WinDbg mencionados anteriormente). No win7 e versões anteriores, alocamos nós mesmos uma nova tagPROPLIST para preencher buracos pequenos.

Aqui, todas as janelas que pulverizamos não possuem strings de texto de janela. No entanto, se necessário, ainda podemos usá-las para alocar ou liberar blocos de heap de tamanho arbitrário. Uma vez criada, você não pode remover uma lista de propriedades existente, a menos que a janela seja destruída, mas podemos controlar a realocação dessa lista para acomodar novas propriedades; esse mecanismo pode ser usado para cavar buracos em locais anteriormente ocupados. Tudo o que você precisa fazer é definir uma nova propriedade que não exista na lista original (distinguida pelo atomKey).

Verificando o layout do feng shui

Curiosamente, o desktop heap é mapeado no espaço do usuário, embora seja somente leitura. Isso significa que podemos verificar o layout de feng shui que construímos e garantir que ele funcione. Primeiro, precisamos determinar onde o desktop heap é mapeado para o modo usuário. O artigo do Tarjei sobre win32k menciona isso. Existe uma estrutura não documentada, Win32ClientInfo, no TEB, relacionada a isso. Sua definição aproximada é:

root@kitploit:~
typedef struct _CLIENTINFO { 
    ULONG_PTR CI_flags; 
    ULONG_PTR cSpins; 
    DWORD dwExpWinVer; 
    DWORD dwCompatFlags; 
    DWORD dwCompatFlags2; 
    DWORD dwTIFlags; 
    PDESKTOPINFO pDeskInfo; 
    ULONG_PTR ulClientDelta; // 不完整. 参见 reactos 
} CLIENTINFO, *PCLIENTINFO;

A estrutura PDESKTOPINFO é definida como:

root@kitploit:~
typedef struct _DESKTOPINFO { 
    PVOID pvDesktopBase; 
    PVOID pvDesktopLimit;   // 不完整. 参见 reactos  
} DESKTOPINFO, *PDESKTOPINFO;

O primeiro campo, pvDesktopBase, aponta para o endereço do desktop heap no modo kernel; anote isso. O campo ulClientDelta de Win32ClientInfo é a diferença entre o endereço no modo kernel e o endereço no modo usuário. Com essas informações, conseguimos o que queremos.

No entanto, não queremos analisar a estrutura do heap nós mesmos; em vez disso, queremos ter um handle do user32, como um valor de HWND, que possa ser convertido em um endereço mapeado no modo usuário, para podermos determinar se ele está associado a outras alocações de heap. Para encontrar esse handle, precisamos localizar uma estrutura chamada gShared; normalmente ela fica na user32.dll e é exportada no win7 e versões posteriores, portanto é fácil encontrá-la.

Na maioria dos sistemas, essa estrutura é definida como:

root@kitploit:~
kd> dt !tagSHAREDINFO 
win32k!tagSHAREDINFO 
    +0x000 psi                  : Ptr32 tagSERVERINFO 
    +0x004 aheList              : Ptr32 _HANDLEENTRY 
    +0x008 HeEntrySize          : Uint4B 
    +0x00c pDispInfo            : Ptr32 tagDISPLAYINFO 
    +0x010 ulSharedDelta        : Uint4B 
    +0x014 awmControl           : [31] _WNDMSG 
    +0x10c DefWindowMsgs        : _WNDMSG 
    +0x114 DefWindowSpecMsgs    : _WNDMSG

Na estrutura acima, aheList aponta para um array de _HANDLEENTRY; cada _HANDLEENTRY contém um handle que aponta para um endereço no modo kernel. Podemos obter um endereço utilizável no modo usuário por meio da "diferença entre o endereço do modo kernel e o modo usuário". Infelizmente, isso não é viável em versões anteriores ao win7, pois gSharedInfo não é exportado. O artigo do Tarjei diz que a função não documentada CsrClientConnectToServer pode ser usada para obter uma cópia do gSharedInfo, mas não encontrei um exemplo prático. O problema é que essa função, em sua implementação, exige uma estrutura cujo tamanho varia de acordo com o sistema; portanto, com base na minha experiência, você não pode confiar plenamente no que vê no ReactOS.

Assim que calculamos o local mapeado, podemos construir uma função que nos diga a posição do objeto de janela no desktop heap. Então, se quisermos saber onde o bloco de heap correspondente à lista de propriedades ou à string de texto foi alocado, basta analisar a estrutura no modo usuário.

Substituindo tagSBINFO por tagPROPLIST

Agora, finalmente chegamos perto da exploração. Já temos o método para controlar blocos de heap, um método para verificar se as posições dos blocos estão corretas e também sabemos como acionar o bug. Portanto, agora podemos finalmente determinar como substituir o bloco tagSBINFO liberado pela lista de propriedades tagPROPLIST escolhida. Observe que, como tagPROPLIST é apenas o cabeçalho de uma lista maior, podemos fazer com que o tamanho da lista corresponda ao tamanho do bloco da barra de rolagem. A parte seguinte à tagPROPLIST é basicamente um array de estruturas tagPROP, ou lista de propriedades; portanto, não farei distinção entre os termos array e lista. A definição da estrutura tagPROPLIST em sistemas de 64 bits é:

root@kitploit:~
kd> dt -b !tagPROPLIST 
win32k!tagPROPLIST 
+0x000 cEntries     : Uint4B 
+0x004 iFirstFree   : Uint4B 
+0x008 aprop        : tagPROP 
    +0x000 hData    : Ptr64 
    +0x008 atomKey  : Uint2B 
    +0x00a fs       : Uint2B

Como mencionado anteriormente, um objeto de janela possui uma lista de propriedades associada. Essa lista é criada pela função SetProp(). Ela é usada para localizar propriedades existentes por meio da correspondência com autoKey; se a propriedade não existir, um novo item de propriedade é criado na lista. Se não houver nenhuma lista de propriedades, uma lista é criada e anexada à estrutura tagWND.

Se já tivermos pulverizado um tagWND vulnerável e criado o item tagPROPLIST associado, o layout final será como na Figura 3:

Assim que isso estiver configurado, podemos alocar o controle de barra de rolagem que queremos explorar. Isso resultará no que mostra a Figura 4:

Em seguida, ao manipular a barra de rolagem, acionamos o HOOK do callback em modo usuário. Dentro da função de HOOK, tentamos destruir a janela para liberar a estrutura tagSBINFO. Isso leva à situação da Figura 5:

Em 64 bits, a estrutura tagSBINFO tem 0x28 bytes; um item de array tagPROPLIST tem 0x18 bytes, dos quais 0x10 bytes são o tagPROP padrão. Portanto, uma lista de propriedades com dois itens de array tem 0x28 bytes (0x8 + 0x10 + 0x10), o que é um encaixe perfeito. Supondo que já tenhamos pulverizado a memória para podermos preencher o buraco, precisamos apenas de uma janela com lista de propriedades; depois que essa janela liberar a estrutura tagSBINFO (como na figura anterior), adicionamos imediatamente um novo item à lista de propriedades. Esse processo primeiro libera o bloco tagPROPLIST anterior de 0x18 bytes; como o heap já foi pulverizado, não há blocos livres nas proximidades, portanto não há coalescência de blocos e não há espaço suficiente para acomodar os 0x28 bytes recém-alocados. Assim, a posição recém-liberada do tagSBINFO é usada (seu tamanho é exatamente 0x28 bytes), como mostra a Figura 6:

Ao retornar do callback com HOOK, o UAF será acionado e alguns bits serão gravados no campo cEntries da tagPROPLIST. O valor original de cEntries é 0x2, indicando que criamos dois itens na lista de propriedades. Após o overflow, ele se torna 0xe, com os bits 3 e 4 (contando a partir de 1) definidos como 1.

Nesse momento, concluímos o novo overflow no heap e aumentamos o número de itens da lista de propriedades para um valor maior que 0xc. Em seguida, vamos transbordar para o bloco de heap adjacente, que chamamos de corrupção da fase 2.

Abuso da tabela de propriedades — Corrupção da fase 2

No blog do Udi, a explicação vai até aqui. Antes disso, isso era chamado de "overflow de heap típico"; no entanto, com base na minha experiência, é difícil, a partir desse ponto, alcançar leitura/gravação em endereço arbitrário ou execução de código. Vejamos novamente a estrutura tagPROPLIST em 64 bits:

root@kitploit:~
kd> dt -b !tagPROPLIST 
win32k!tagPROPLIST 
+0x000 cEntries     : Uint4B 
+0x004 iFirstFree   : Uint4B 
+0x008 aprop        : tagPROP 
    +0x000 hData    : Ptr64 
    +0x008 atomKey  : Uint2B 
    +0x00a fs       : Uint2B

A corrupção da fase 1 nos deu uma lista tagPROPLIST corrompida, permitindo aumentar os itens do array tagPROP. A tagPROPLIST tem apenas dois campos:

  • cEntries: número de itens da lista
  • iFirstFree: índice do primeiro item livre. Uma lista cheia (indicando que é necessário solicitar uma nova) significa que iFirstFree == cEntries.

Quando um novo item é inserido na lista, uma função é chamada primeiro para percorrer cada item até encontrar o valor iFirstFree adequado. Se não encontrar, ela verifica se o valor de iFirstFree é maior que cEntries. Se o item correspondente ao atomKey não estiver na lista, verifica se iFirstFree != cEntries. Se não forem iguais, insere um item no índice iFirstFree; se forem iguais, aloca uma nova lista de propriedades capaz de conter a propriedade inserida, enquanto os itens originais são copiados e o novo item é inserido.

O campo atomKey corresponde a LPCTSTR lpString. Como documentado na função SetProp() no MSDN, o chamador pode passar um ponteiro de string ou um valor atom de 16 bits. Ao passar um ponteiro de string, ele é convertido automaticamente em um valor atom antes de ser armazenado na lista de propriedades. Como podemos passar qualquer valor atom arbitrário para SetProp(), isso nos dá a capacidade de controlar esses dois bytes, mas com algumas restrições. Ou seja, nossos dados corrompidos em atomKey não podem se repetir; caso contrário, ao definir um novo item de propriedade, ele substituirá o item original com o mesmo valor atom. Além disso, o campo fs não é controlável por nós; o valor 0 indica que atomKey < 0xBFFF, o que corresponde a um valor atom inteiro. O valor 2 em fs indica que atomKey >= 0xC000.

Outra observação é que tagPROP tem apenas 0xc bytes. Essa estrutura é alinhada em 0x10 bytes em sistemas de 64 bits; portanto, ao inserir um item tagPROP, há 4 bytes extras que não podem ser corrompidos. O último ponto importante é que os primeiros 8 bytes de um bloco de heap tagPROPLIST definem o tamanho dos itens da lista, o que significa que cada novo item tagPROP inserido sempre será gravado em uma posição alinhada a 8 bytes.

Para cada tagPROP inserido, em sistemas de 64 bits, a situação é:

root@kitploit:~
* Offset 0x0: 8 bytes de dados totalmente controláveis (hData) 
* Offset 0x8: 2 bytes de dados majoritariamente controláveis (atomKey) 
* Offset 0xa: 2 bytes de dados não controláveis (fs) 
* Offset 0xc: 4 bytes de dados não modificáveis (padding)

Essa situação é muito melhor do que 2 bits, mas ainda não é perfeita. A menos que possamos sobrescrever algo com os primeiros 8 bytes, que vêm do campo hData totalmente controlável, ficaremos bastante limitados. Se precisarmos gravar campos mais profundos em uma estrutura adjacente, não há como evitar a corrupção não controlável de alguns valores. Passei algum tempo procurando vários objetos no desktop heap e, considerando as restrições de corrupção anteriores, a única maneira que consegui pensar para contornar isso e obter leitura/gravação em endereço arbitrário foi corromper o campo strName do tagWND, que é uma estrutura _LARGE_UNICODE_STRING:

root@kitploit:~
kd> dt !_LARGE_UNICODE_STRING 
win32k!_LARGE_UNICODE_STRING 
    +0x000 Length           : Uint4B 
    +0x004 MaximumLength    : Pos 0, 31 Bits 
    +0x004 bAnsi            : Pos 31, 1 Bit 
    +0x008 Buffer           : Ptr64 Uint2B

Se conseguirmos corromper o campo Buffer dessa estrutura, poderemos ler/gravar MaximumLength bytes a partir de um endereço fornecido, manipulando o texto da janela. Foi isso que fiz. Você pode ter notado essa estrutura na seção anterior, sobre como criar blocos de heap de tamanho e valor arbitrários no desktop heap; a mesma situação se aplica aqui.

Até aqui, sabemos como usar os itens da tagPROPLIST para corromper dados, quais partes podemos controlar e, mais importante, quais limitações enfrentaremos; essas limitações diferem entre 32 e 64 bits. O que fizemos em 64 bits não funciona em 32 bits. Em breve, vamos passar da corrupção da fase 2 (ou seja, gravar dados por meio da estrutura tagPROP) para outra "primitiva" de corrupção, por meio da qual podemos gravar dados totalmente controláveis; é isso que chamo de corrupção da fase 3.

Construindo uma "primitiva de leitura/gravação" — Corrupção da fase 3

64 bits

O plano é corromper o campo strName em um tagWND adjacente. Já sabemos que ele é uma estrutura LARGE_UNICODE_STRING, mas vamos examinar mais detalhes da estrutura tagWND. Ela se parece com isto:kd> dt !tagWND win32k!tagWND +0x000 head : THRDESKHEAD +0x028 state : Uint4B +0x028 bHasMeun : Pos 0, 1 Bit +0x028 bHasVerticalScrollbar : Pos 1, 1 Bit +0x028 bHasHorizontalScrollbar : Pos 2, 1 Bit [SNIPPED FLAGS] +0x028 bDestroyed : Pos 31, 1 Bit +0x02c state2 : Uint4B [SNIPPED FLAGS] +0x02c bWMCreateMsgProcessed : Pos 31, 1 Bit +0x030 ExStyle : Uint4B +0x030 bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit +0x030 bUnused1 : Pos 1, 1 Bit +0x030 bWS_EX_NOPARENTNOTIFY : Pos 2, 1 Bit [SNIPPED FLAGS] +0x030 bUIStateFocusRectHidden : Pos 31, 1 Bit +0x034 style : Uint4B +0x034 bReserved1 : Pos 0, 16 Bits [SNIPPED FLAGS] +0x034 bWS_POPUP : Pos 31, 1 Bit +0x038 hModule : Ptr64 Void +0x040 hMod16 : Uint2B +0x042 fnid : Uint2B +0x048 spwndNext : Ptr64 tagWND +0x050 spwndPrev : Ptr64 tagWND +0x058 spwndParent : Ptr64 tagWND +0x060 spwndChild : Ptr64 tagWND +0x068 spwndOwner : Ptr64 tagWND +0x070 rcWindow : tagRECT +0x080 rcClient : tagRECT +0x090 lpfnWndProc : Ptr64 int64 +0x098 pcls : Ptr64 tagCLS +0x0a0 hrgnUpdate : Ptr64 HRGN +0x0a8 ppropList : Ptr64 tagPROPLIST +0x0b0 pSBInfo : Ptr64 tagSBINFO +0x0b8 spmenuSys : Ptr64 tagMENU +0x0c0 spmenu : Ptr64 tagMENU +0x0c8 hrgnClip : Ptr64 HRGN__ +0x0d0 hrgnNewFrame : Ptr64 HRGN__ +0x0d8 strName : LARGE_UNICODE_STRING +0x0e8 cbwndExtra : Int4B +0x0f0 spwndLastActive : Ptr64 tagWND +0x0f8 hImc : Ptr64 HIMC_ +0x100 dwUserData : Uint8B +0x108 pActCtx : Ptr64 _ACTIVATION_CONTEXT +0x110 pTransform : Ptr64 _D3DMATRIX +0x118 spwndClipboardListenerNext : Ptr64 tagWND +0x120 ExStyle2 : Uint4B +0x120 bClipboardListener : Pos 0, 1 Bit [SNIPPED FLAGS] +0x120 bChildNoActivate : Pos 11, 1 Bit

A estrutura acima é para 64 bits; pode-se ver que o deslocamento da estrutura _LARGE_UNICODE_STRING que queremos sobrescrever é 0xd8. Você também notará um campo importante no início dessa estrutura. Eu esperava poder abusar dele, mas há muitos ponteiros em _THRDESKHEAD que exigem nossa atenção e, infelizmente, não podemos controlar para onde escrevemos; a razão dessa limitação já foi discutida anteriormente.

Definição da estrutura _THRDESKHEAD:

root@kitploit:~
kd> dt !_THRDESKHEAD 
win32k!_THRDESKHEAD 
    +0x000 h        : Ptr64 Void 
    +0x008 cLockObj : Uint4B 
    +0x010 pti      : Ptr64 tagTHREADINFO 
    +0x018 rpdesk   : Ptr64 tagDESKTOP 
    +0x020 pSelf    : Ptr64 UChar

O problema com _THRDESKHEAD não apenas nos intrigou, mas também nos levou a reexaminar aquela restrição de alinhamento. Não importa em qual deslocamento o novo item da lista tagPROP esteja, nossa operação de escrita sobrescreverá diretamente o início de _LARGE_UNICODE_STRING:

root@kitploit:~
win32k!_LARGE_UNICODE_STRING 
    +0x000 Length           <-- hData (totalmente controlável) sobrescreve aqui 
    +0x004 MaximumLength    <-- e aqui 
    +0x004 bAnsi            <-- e aqui 
    +0x008 Buffer           <-- atomKey e fs (parcialmente controláveis) sobrescrevem aqui

É claro que queremos sobrescrever o ponteiro Buffer para acessar memória em endereços arbitrários; no entanto, mesmo que possamos atacar com segurança outros campos dessa estrutura, não podemos controlar o ponteiro de que precisamos.

Não conseguimos corromper dados arbitrários; resolver esse problema não é mais uma questão de corromper a tagPROPLIST, mas sim um mecanismo de corrupção totalmente diferente.

Em versões posteriores ao Windows XP, o cabeçalho do bloco de heap (ou seja, _HEAP_ENTRY) do alocador de backend user-mode (como o heap de desktop do kernel) é armazenado no heap, antes do conteúdo real do bloco. O heap de desktop em si é gerenciado pela estrutura _HEAP, o que nos dá certa liberdade ao explorar esse bloco de heap.

A estrutura _HEAP_ENTRY é definida como:

root@kitploit:~
kd> dt !_HEAP_ENTRY 
ntdll!_HEAP_ENTRY 
    +0x000 PreviousBlockPrivateData : Ptr64 Void 
    +0x008 Size             : Uint2B 
    +0x00a Flags            : UChar 
    +0x00b SmallTagIndex    : UChar 
    +0x00c PreviousSize     : Uint2B 
    +0x00e SegmentOffset    : UChar 
    +0x00f UnusedBytes      : UChar

O cabeçalho do bloco tem 0x10 bytes no total; os primeiros 8 bytes são PreviousBlockPrivateData, usados para acomodar dados reais do bloco anterior quando o tamanho solicitado excede os 0x10 normais (menos de 8 bytes são alinhados a 8 bytes). Isso é descrito resumidamente no Leviathan blog entry, e também em artigos anteriores sobre o heap user-mode. Size e PreviousSize representam o tamanho do bloco atual e do bloco anterior, em unidades de 0x10 bytes. Flags indica se o bloco está livre, etc. Se o modo seguro de _HEAP_ENTRY estiver ativado no _HEAP, SmallTagIndex conterá a soma de verificação XOR dos dados do bloco.

Embora a restrição de alinhamento jogue contra nós, ela realmente existe. Se você tiver uma tagPROPLIST que tenha sempre pelo menos 0x18 bytes e depois adicionar uma tagPROP de 0x10 bytes, para uma tagPROPLIST de 0x28 bytes com dois itens de lista, ela será colocada em um bloco de heap de 0x20 bytes, e os bytes extras representados por PreviousBlockPrivateData usam o bloco de heap adjacente. Isso significa que, quando adicionamos o terceiro item de lista, o bloco adjacente é corrompido, e os 8 bytes controláveis de hData sobrescrevem o topo de _HEAP_ENTRY.

O que queremos fazer é usar isso para, de alguma forma, escrever dados arbitrários no topo do Buffer. Primeiro, alteramos o layout do heap para nos aproximarmos do bloco tagPROPLIST corrompido; durante o controle, temos um pequeno bloco de heap contendo uma string de texto associada a uma janela, que chamaremos de "bloco de cobertura". Adjacente a esse "bloco de cobertura", colocamos uma tagWND para corromper essa tagWND. A Figura 7 abaixo ilustra o processo. Observe que começamos omitindo os blocos de heap anteriormente aspergidos para economizar espaço; portanto, eles devem agora ser considerados implícitos.

Em seguida, inserimos uma terceira tagPROP na lista tagPROPLIST, que sobrescreverá os últimos 8 bytes de _HEAP_ENTRY e os primeiros 8 bytes do "bloco de cobertura". Assim, podemos modificar o _HEAP_ENTRY do "bloco de cobertura" para que seu tamanho seja maior que o tamanho real e possa conter a estrutura tagWND adjacente.

Agora liberamos o "bloco de cobertura" corrompido para que o gerenciador de heap o coloque na lista livre; a lista correspondente ao tamanho do bloco (cada lista livre corresponde a um tamanho fixo) é maior que o tamanho real do "bloco de cobertura". Então reutilizamos esse bloco modificando o texto da janela (que controlamos completamente). No entanto, há um pequeno problema a resolver. Quando o "bloco de cobertura" é liberado, o gerenciador de heap tenta localizar o bloco adjacente anterior, dependendo do campo Size corrompido. O gerenciador de heap verifica se esse bloco adjacente está livre para poder coalescê-lo. De qualquer forma, queremos controlar essa referência e definir um indicador de "em uso". Uma pequena alteração no layout do heap resolve isso. Nesse ponto, colocamos um bloco falso cujo cabeçalho de heap tem o sinalizador "em uso" e cujo valor PreviousSize é definido como o valor do campo Size corrompido; assim, podemos implementá-lo simplesmente alocando o texto da janela de outra janela. O novo layout do heap é o seguinte (Figura 8):

Agora podemos liberar o "bloco de cobertura" corrompido atualizando a string de texto da janela associada para que seu comprimento seja maior que os 0x10 bytes originais. Dessa forma, o "bloco de cobertura" corrompido é liberado primeiro e colocado na lista livre; no entanto, seu tamanho foi corrompido e o tamanho declarado é maior que o real. Esse tamanho pode ser ajustado de acordo com nossa necessidade real. Assim, nossos dados de string são gravados no "bloco de cobertura" e então o usamos para corromper a tagWND adjacente com dados arbitrários. Isso é mostrado abaixo (Figura 9):

Esta é a corrupção do Estágio 3. Agora podemos sobrescrever o ponteiro strName.Buffer com quaisquer dados que desejarmos. No entanto, corromper outros dados da tagWND ainda é um pouco problemático, mas isso não é um problema porque o heap de desktop é mapeado no espaço do usuário! Portanto, antes de corromper tudo, já lemos todo o conteúdo da tagWND, modificamos o conteúdo da estrutura strName como queremos e enviamos todos os dados por meio da modificação do texto da janela.

Através de strName, não só obtemos uma "primitiva" de leitura/escrita arbitrária, mas também podemos modificar repetidamente strName, como o mecanismo de modificação do texto da janela permite. Contanto que o comprimento da string gravada não seja maior que o valor de MaximumLength, podemos continuar usando o mesmo bloco de heap. Portanto, cada vez que queremos modificar o endereço de strName para ler algum valor, atualizamos o "bloco de cobertura" com uma nova string anexada aos nossos dados. Essa reutilização é mostrada na Figura 10. Observe que aumentei novamente a granularidade da visão geral da figura para mostrar cada corrupção em detalhes.

Isso significa que, no final, só precisamos corromper duas coisas adicionais (além dos itens originais da lista tagPROPLIST):

  1. O cabeçalho do "bloco de cobertura". Podemos lê-lo antes da corrupção; portanto, sabemos como restaurá-lo após o uso. Curiosamente, podemos até mesmo reparar o bloco do terceiro item da lista tagPROPLIST simplesmente enviando o atomKey que usamos para corromper de volta para a posição original!
  2. A estrutura strName, que podemos modificar facilmente gravando o texto da janela. Durante a operação, o valor de retorno precisa ser definido como nulo.

Agora, se quisermos ler alguns bytes de algum lugar da memória, consultamos o texto da janela por meio da função InternalGetWindowText(), onde está a entrada strName corrompida. Podemos ler o número de bytes declarado pelo campo Length. Da mesma forma, se quisermos escrever em qualquer lugar da memória, usamos a função NtUserDefSetText() para atualizar o texto da janela corrompido, mas a quantidade gravada não deve ser maior que o valor declarado pelo campo MaximumLength (que também é configurável por nós). Assim, o buffer existente é reutilizado e aponta para o endereço de memória que desejamos.

Codificação de heap no Windows 8 e 8.1

Embora o alocador de backend user-mode use codificação de heap (heap encoding) desde o Windows Vista, o heap de desktop nunca a habilitou até o Windows 8. Portanto, em sistemas Windows 8 ou posteriores, isso cria um obstáculo ao sobrescrever o "bloco de cobertura". No entanto, como a estrutura _HEAP que contém o heap inclui esse cookie e o usa para codificar todo o cabeçalho do heap, podemos ler esse cookie do heap de desktop mapeado no espaço do usuário e, em seguida, usá-lo para codificar o cabeçalho do "bloco de cobertura"; ao imitar a operação obtida por engenharia reversa do código do alocador, o alocador aceitará essa operação.

Sistemas de 32 bits

A primeira coisa a observar é que, em sistemas de 32 bits, a estrutura tagPROP tem 8 bytes, em vez de 0xc bytes em sistemas de 64 bits, e o campo hData que controlamos tem apenas 4 bytes, em vez de 8 bytes em sistemas de 64 bits. Também não há bytes de preenchimento extras; em sistemas de 64 bits há 8 bytes de preenchimento, de modo que a estrutura inteira tem exatamente 8 bytes. Isso significa que não podemos corromper completamente o cabeçalho do bloco adjacente, se só pudermos controlar parcialmente os dados. Em algumas versões do Windows isso é possível, porque podemos controlar os campos mais importantes, mas no Windows 8 e 8.1 o cabeçalho do heap é codificado, e acabamos sobrescrevendo parte dele de forma insegura por meio do campo fs. O cabeçalho _HEAP_ENTRY em sistemas de 32 bits parece semelhante, mas não tem o campo PreviousBlockPrivateData.

Ainda não conseguimos corromper todas as partes da tagWND, pois não há como evitar ponteiros truncados. E ainda não encontrei um objeto que atenda a isso; visto que _LARGE_UNICODE_STRING funciona bem em sistemas de 64 bits, quero usá-lo também em sistemas de 32 bits.

Minha ideia é: se pudermos corromper o campo iFirstFree da estrutura tagPROPLIST (o índice do primeiro item de propriedade liberado na lista de propriedades) incrementando o valor do índice, podemos fazê-lo apontar para uma posição mais distante no heap. Por exemplo, podemos fazê-lo apontar para o topo de tagWND.strName. A Figura 11 ilustra essa ideia:

Para tornar o processo claro, agora usamos duas estruturas tagPROPLIST: a "Lista de Propriedades A", usada para o UAF, e outra, "Lista de Propriedades B". Precisamos saber exatamente quais partes das tagPROPs inseridas na "Lista de Propriedades A" sobrescreverão o campo iFirstFree da "Lista de Propriedades B". Também devemos lembrar que só podemos gravar 8 bytes por vez; portanto, precisamos inserir pelo menos uma tagPROP extra na "Lista de Propriedades A": a primeira corrompe o cabeçalho adjacente e a segunda atinge o campo tagPROPLIST da "Lista de Propriedades B". Isso pode variar dependendo do sistema operacional e do tamanho do bloco de heap, e meu exploit precisa se adaptar a vários layouts de heap. A Figura 12 mostra como realizamos a corrupção. Observe que a primeira tagPROPLIST na figura não é dividida em campos individuais; portanto, tagPROP[0] está implícita. Na segunda tagPROPLIST, no entanto, seus membros internos são divididos para mostrar nosso processo de corrupção. É por isso que tagPROP[0] é exibida:

Primeiro, observe que, se gravarmos 8 bytes para cada tagPROP, isso significa que só podemos controlar parcialmente a sobrescrita de iFirstFree (já que ele vem dos campos atomKey e fs), que é o que mais nos importa. Como podemos controlar totalmente pelo menos dois bytes-chave por meio do valor de atomKey, quando esse valor é pequeno o suficiente, o campo fs se torna 0. Portanto, usamos o valor de hData para sobrescrever cEntries com um valor razoável e usamos atomKey para fazer iFirstFree apontar para a tagWND, onde está o ponteiro strName.Buffer que queremos sobrescrever. Se não pudermos sobrescrever diretamente os valores de Length e MaximumLength, podemos pré-alocar uma string para a janela de destino, garantindo que seu comprimento já seja um valor definido.

Vamos dar uma olhada na estrutura tagWND de 32 bits para ver o que podemos obter. Observe que desta vez usei o parâmetro -b para que possamos calcular facilmente o deslocamento de Buffer em strName.

root@kitploit:~
kd> dt -b !tagWND 
win32k!tagWND 
    +0x000 head         : _THRDESKHEAD 
        +0x000 h        : Ptr32 
        +0x004 cLockObj : Uint4B 
        +0x008 pti      : Ptr32 
        +0x00c rpdesk   : Ptr32 
        +0x010 pSelf    : Ptr32 
    +0x014 state        : Uint4B 
    +0x014 bHasMeun     : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x014 bDestroyed   : Pos 31, 1 Bit 
    +0x018 state2       : Uint4B 
[SNIPPED FLAGS] 
    +0x018 bWMCreateMsgProcessed    : Pos 31, 1 Bit 
    +0x01c ExStyle                  : Uint4B 
    +0x01c bWS_EX_DLGMODALFRAME     : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x01c bUIStateFocusRectHidden  : Pos 31, 1 Bit 
    +0x020 style        : Uint4B 
    +0x020 bReserved1   : Pos 0, 16 Bits 
[SNIPPED FLAGS] 
    +0x020 bWS_POPUP    : Pos 31, 1 Bit 
    +0x024 hModule      : Ptr32 
    +0x028 hMod16       : Uint2B 
    +0x02a fnid         : Uint2B 
    +0x02c spwndNext    : Ptr32 
    +0x030 spwndPrev    : Ptr32 
    +0x034 spwndParent  : Ptr32 
    +0x038 spwndChild   : Ptr32 
    +0x03c spwndOwner   : Ptr32 
    +0x040 rcWindow     : tagRECT 
        +0x000 left     : Int4B 
        +0x004 top      : Int4B 
        +0x008 right    : Int4B 
        +0x00c bottom   : Int4B 
    +0x050 rcClient     : tagRECT 
        +0x000 left     : Int4B
        +0x004 top      : Int4B 
        +0x008 right    : Int4B 
        +0x00c bottom   : Int4B 
    +0x060 lpfnWndProc  : Ptr32 
    +0x064 pcls         : Ptr32 
    +0x068 hrgnUpdate   : Ptr32 
    +0x06c ppropList    : Ptr32 
    +0x070 pSBInfo      : Ptr32 
    +0x074 spmenuSys    : Ptr32 
    +0x078 spmenu       : Ptr32 
    +0x07c hrgnClip     : Ptr32 
    +0x080 hrgnNewFrame : Ptr32 
    +0x084 strName      : _LARGE_UNICODE_STRING 
        +0x000 Length   : Uint4B 
        +0x004 MaximumLength : Pos 0, 31 Bits 
        +0x004 bAnsi    : Pos 31, 1 Bit 
        +0x008 Buffer   : Ptr32 
    +0x090 cbwndExtra   : Int4B 
    +0x094 spwndLastActive  : Ptr32 
    +0x098 hImc         : Ptr32 
    +0x09c dwUserData   : Uint4B 
    +0x0a0 pActCtx      : Ptr32 
    +0x0a4 pTransform   : Ptr32 
    +0x0a8 spwndClipboardListenerNext   : Ptr32 
    +0x0ac ExStyle2                     : Uint4B 
    +0x0ac bClipboardListener           : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x0ac bChildNoActivate             : Pos 11, 1 Bit

strName está no deslocamento 0x84, e Buffer está no deslocamento 0x8c. Sabemos o índice do item da lista tagPROP e também sabemos que podemos gravar 8 bytes. Portanto, podemos saber facilmente se iFirstFree indexa o deslocamento 0x88 da janela, onde está MaximumLength. Como só podemos controlar dois bytes de Buffer, a operação de escrita não é viável; dado que nosso objetivo é usar isso como uma "primitiva" de leitura/escrita arbitrária, esse resultado é inaceitável. Se escrevermos um índice que aponte para 0x90, sobrescreveremos cbwndExtra, o que não é o que buscamos.

Revisitando o que conseguimos controlar durante o heap feng shui anterior, vamos ver se há algum deslocamento interessante na tagWND que possamos controlar. No deslocamento 0x70 da tagWND está o campo pSBInfo. Esse deslocamento é divisível por 8, portanto podemos sobrescrever esse ponteiro com parte dos dados de hData de uma tagPROP falsa.

Podemos sobrescrever pSBInfo para que aponte diretamente para strName na mesma estrutura tagWND? Talvez possamos usar a API de barra de rolagem para corromper strName e atingir nosso objetivo.

pSBInfo aponta para uma estrutura tagSBINFO, que foi mencionada no processo do UAF no início.

root@kitploit:~
kd> dt -b !tagSBINFO 
win32k!tagSBINFO 
    +0x000 WSBflags     : Int4B 
    +0x004 Horz         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B 
    +0x014 Vert         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B

Lembramos que WSBflags não nos dá muito controle, mas pelo menos sabemos que ele é definido como 1 quando a barra de rolagem é habilitada e como 0 quando é desabilitada. Esse campo de sinalizador não pode ser definido para um valor arbitrário; por meio de engenharia reversa das funções relacionadas, descobrimos que, se o estado da barra de rolagem não for alterado, esse campo de indicação permanece inalterado. Os valores na estrutura tagSBDATA parecem mais interessantes. Se lermos a documentação de SetScrollInfo(), entenderemos bem o significado desses valores. Parece que podemos passar parâmetros para SetScrollInfo() por meio da estrutura SCROLLINFO. Contanto que haja um controle de barra de rolagem próximo à janela que queremos corromper, podemos manipular diretamente o ponteiro pSBInfo (ele enviará uma mensagem especial de janela ao controle de janela associado). Obviamente, podemos controlar incondicionalmente os valores de posMin e posMax. Os campos page e pos são um pouco problemáticos, pois são restritos a determinados intervalos; por enquanto, tentamos evitá-los. Definimos o sinalizador SIF_RANGE na estrutura SCROLLINFO para declarar onde queremos definir os valores máximo e mínimo.

Queremos sobrescrever Buffer com dados arbitrários, o que significa que queremos que posMin o sobrescreva; portanto, podemos sobrescrever pSBInfo para que aponte para strName.MaximumLength. Contanto que não habilitemos ou desabilitemos a barra de rolagem, o campo WSBflags não será reescrito, o que garante a integridade de strName.MaximumLength. Isso significa que, não importa como definamos posMin (por meio de nMin em SCROLLINFO), Buffer será sobrescrito, e posMax será gravado em cbwndExtra. Isso não é um grande problema; em sistemas de 64 bits, podemos ler esse valor antecipadamente e restaurá-lo depois. A ideia geral do estouro é mostrada na Figura 13:

Agora usamos a Figura 14 para ilustrar o processo de ataque em sistemas de 32 bits. Antes de corromper qualquer endereço distante do UAF, vamos dar um passo atrás e examinar o layout do heap e dos blocos relevantes na figura. Agora que conhecemos mais detalhes, o que precisa ser feito a seguir fica óbvio.

Em seguida, inserimos dois itens de propriedade na "Lista de Propriedades A". Isso corromperá os dados próximos à "Lista de Propriedades A", graças à corrupção de UAF anterior, e fará com que o iFirstFree da "Lista de Propriedades A" aponte para pSBInfo. Observe que isso também corromperá o valor adjacente de pSBInfo, mas podemos lê-lo antecipadamente para restaurá-lo após a corrupção.

Inserimos uma nova tagPROP na "Lista de Propriedades B", com um identificador de atom diferente dos já existentes na lista, de modo que essa tagPROP seja inserida no próximo índice livre. Assim, pSBInfo é corrompido para apontar para strName.MaximumLength na mesma tagWND.

Por fim, atualizamos a barra de rolagem para corromper o campo strName.Buffer (como na Figura 17):

Observe que, diferentemente do caso de 64 bits, não podemos corromper os valores de comprimento de strName. Podemos pré-alocar uma string de texto de janela com um comprimento adequado para que seu valor já esteja em uso. Depois disso, quer queiramos ler ou gravar dados de/em um endereço do kernel, basta chamar SetScrollInfo() na janela de destino para atualizar o valor de Buffer e então usar a API de texto de janela para operar.

Agora temos uma "primitiva" reutilizável de leitura/escrita em endereços arbitrários em sistemas de 32 bits!

Execução de código

A partir de agora, presumimos que temos uma "primitiva" de leitura/escrita arbitrária. Portanto, quando digo vazar/ler algum valor ou sobrescrever algum valor, refiro-me a executar essa "primitiva" estabelecida na fase de corrupção anterior. Essa "primitiva" é mais ou menos a mesma nas duas plataformas. Tudo o que resta é sobrescrever um ponteiro de função e fazê-lo apontar para um payload de shellcode em algum lugar. O método comum é sobrescrever o segundo item de nt!HalDispatchTable, que corresponde à função HalQuerySystemInformation(). Em seguida, chamamos a função NtQueryInternalProfile() no modo usuário para acioná-lo.

Precisamos conhecer o endereço base de carregamento do módulo do kernel para calcular o endereço do kernel de nt!HalDispatchTable. Para isso, podemos chamar NtQuerySystemInformation() no modo usuário para obter informações do módulo, que incluem o endereço base do módulo.

root@kitploit:~
// 枚举值 11 代表 SystemModuleInformation , 这是未文档化的... 
rc = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)11, pModuleInfo, 0x100000, NULL);

Depois, carregamos ntoskrnl.exe no modo usuário para encontrar o deslocamento de nt!HalDispatchTable e, assim, obter seu endereço no espaço do kernel. Em seguida, usamos a "primitiva" de leitura para ler o endereço do kernel de HaliQuerySystemInformation() (essa função não é exportada) a fim de modificá-lo; depois, usamos a "primitiva" de escrita para corromper o ponteiro dessa função, fazendo-o apontar para o endereço do shellcode (que pode estar no espaço de endereço do kernel ou do usuário, como detalhado posteriormente). O número de bytes lidos/gravados é o mesmo em sistemas de 32 e 64 bits.

Contornando o SMEP

O Windows 8 e o 8.1 introduziram suporte a SMEP, e alguns produtos de segurança também podem habilitá-lo no Windows 7; portanto, presumimos que ele esteja presente. O SMEP impede que executemos código no espaço do usuário com privilégios de kernel, o que torna inviável modificar o item de nt!HalDispatchTable para apontar para um endereço no espaço do usuário. Portanto, queremos que ele aponte para um local controlável no espaço do kernel, onde o código possa modificar o valor do registro cr4 para desabilitar o SMEP, permitindo pular para o espaço de endereço do usuário. O artigo da MWR apresenta uma técnica interessante em 64 bits: mapeando entradas de tabela de páginas por conta própria, é possível obter um endereço válido em modo kernel para qualquer endereço virtual. Em seguida, por meio da "primitiva" de escrita, modificamos diretamente a entrada da tabela de páginas e alteramos seus bits de máscara. Portei essa técnica para sistemas de 32 bits, mas há algumas diferenças entre sistemas com e sem PAE.

Para conseguir isso, o método óbvio é mapear um endereço do modo usuário para o espaço do kernel e, em seguida, usar a "primitiva" de escrita para fazer a entrada da tabela de páginas ter privilégios de sistema em vez de privilégios de usuário. Esse é o nosso primeiro passo. Encontrei um problema interessante ao implementar isso no Windows 8. O gerenciador de área de trabalho (dwm.exe) do Windows 8 e versões posteriores examina periodicamente as janelas na área de trabalho e consulta seus nomes; não investiguei o motivo exato. Essa operação também não envia uma mensagem para a janela, mas há um manipulador de janela correspondente, e a função GetInternalWindowText() é chamada dentro dele. Portanto, o problema era usar o campo strName da estrutura da janela para sobrescrever a entrada da tabela de páginas da memória que contém o shellcode; essa entrada pertence à tabela de páginas do espaço do nosso próprio processo. Quando o dwm.exe obtém o nome da janela do kernel, a entrada de tabela de páginas modificada faz com que o kernel verifique se strName.Buffer é nulo e, assim, faça uma dereferência indireta desse endereço; se o endereço for inválido, o sistema travará.

Para satisfazer a consulta do dwm.exe, usei um endereço do kernel como payload. Assim, não importa como o processo atual carregue qualquer coisa, a entrada da tabela de páginas associada a esse endereço será sempre válida. Escolhi colocá-lo no heap de desktop, pois podemos calcular seu endereço de kernel usando o método mencionado anteriormente. Ainda usamos a técnica de mapear entradas de tabela de páginas por conta própria; nesse ponto, a tabela de páginas já está marcada como de alta privilégio, mas não como executável. Portanto, tudo o que precisamos fazer é definir o bit de execução.

Os passos são os seguintes:

  1. Criar um buffer de texto de janela contendo o payload do Estágio 1 e calcular seu endereço de kernel.
  2. Usar a técnica de "mapear entradas de tabela de páginas por conta própria" para calcular a entrada da tabela de páginas do endereço base do kernel obtido no Estágio 1.
  3. Usar a "primitiva" de escrita para definir o bit de máscara executável da entrada da tabela de páginas.
  4. Sobrescrever nt!HalDispatchTable para apontar para o endereço de kernel encontrado no Estágio 1.
  5. Chamar NtQueryInternalProfile() para pular para o payload.
  6. Desabilitar a máscara SMEP em cr4 e pular para o payload do espaço do usuário no Estágio 2.
  7. Executar o payload do espaço do usuário no Estágio 2 para escalar privilégios e retornar.
  8. Restaurar a máscara SMEP em cr4 para evitar detecção pelo PatchGuard e retornar após realizar a limpeza necessária.

Contornando a sandbox de baixa integridade

No Windows 8.1 há outro problema: NtQuerySystemInformation() verifica o valor do SID de baixa integridade, o que significa que apenas integridade média ou superior pode obter o endereço base do kernel. Isso pode ser facilmente implementado com a conhecida técnica da instrução sidt. Salvamos o endereço da IDT no modo usuário (isso não exige verificação de privilégio) e então usamos a "primitiva" de leitura para ler o índice da IDT de que precisamos, que geralmente aponta para o espaço de endereço do kernel; portanto, podemos vazar o endereço de kernel do manipulador de interrupção e, em seguida, localizar o deslocamento no arquivo PE correspondente ao módulo do kernel.

Uma vez obtido o endereço base de carregamento do kernel, podemos calcular o endereço de nt!HalDispatchTable.

O método usual é carregar o arquivo ntoskrnl.exe e traduzir os deslocamentos de seus símbolos, somando-os ao endereço base do kernel vazado. No entanto, isso não funciona em uma sandbox de modo aprimorado, porque há uma restrição do próprio sistema de arquivos: você não pode ler C:\windows\system32\ntoskrnl.exe. Para contornar essa limitação, usamos nossa "primitiva" de vazamento para resolver os endereços de símbolo de que precisamos a partir da imagem PE do kernel na memória.

Conclusão

É isso, todo o material. Obrigado por ler. Usando as técnicas apresentadas neste artigo, consegui explorações estáveis em sistemas de 32 e 64 bits – XP, Vista, 7, 8, 8.1 e Server 2012. No Windows 2003 e 2008, por padrão, isso não funciona, pois não é possível fazer hook de callbacks do modo usuário; portanto, esses dois sistemas não podem ser atacados, a menos que as condições que exigimos sejam atendidas. O processo de exploração é bastante complexo, com muitos obstáculos a superar, mas isso também nos proporcionou muita diversão e aprendizado; muitas das abordagens viáveis e resultados de pesquisa usados neste artigo já foram mencionados em artigos de outros pesquisadores. Até onde sei, existe apenas uma mitigação que pode impedir a exploração de win32k.sys: a que a sandbox do Google Chrome usa, que efetivamente bloqueia as chamadas de sistema do kernel do win32k em tempo de execução. Agradeço qualquer melhoria ou feedback; se houver deficiências em algumas das técnicas que apresentei, me avise e atualizarei este documento. Você pode entrar em contato comigo pelo twitter @fidgetingbits ou pelo e-mail [email protected].

Baixar ferramenta