Oficina
Bloco de Notas com linhas numeradas
Seg 28 Mai 2007 20:28 |
- Detalhes
- Categoria: Oficina de Assembly
- Atualização: Quinta, 18 Junho 2009 11:44
- Autor: vovó Vicki
- Acessos: 23201
Vitaminar um programa existente exige um bom conhecimento da estrutura e do funcionamento de executáveis. A proposta deste tutorial, baseado no texto publicado por razzia em 19.08.97, é reforçar as noções básicas através de um exercício de re-engenharia de um velho conhecido: o Bloco de Notas do Windows.
O Bloco de Notas (Notepad), entre outras limitações, não indica números de linha - algo que seria muito útil em diversas situações. Mesmo que você não veja vantagens em adicionar o número das linhas ao programa, talvez seja interessante saber como modificar um executável sem possuir seu código fonte.
Escrevi este tutorial em 2001. Na época, a versão do bloco de notas era de 08/06/2000. Sugiro que faça o download desta versão do executável para poder acompanhar o texto. Você o encontra na seção de downloads em Informática / Programação.
O procedimento WndProc
Para determinar o código que será adicionado precisamos investigar um pouco o programa alvo. O "coração" de qualquer programa Windows é o procedimento WndProc. Este procedimento é chamado pelo Windows toda vez que o usuário interage com a janela do programa. O Windows passa alguns parâmetros para este procedimento (por exemplo, a mensagem da janela) para que o WndProc possa saber o que ocorreu e efetuar a atualização da janela.
No caso do bloco de notas, o programa principal gera uma janela filha da classe "edit", que sempre ocupa o tamanho total da janela principal. Se quisermos imprimir nosso próprio texto na janela principal, será necessário reduzir o tamanho da janela filha para que ela não cubra a área total da janela principal. Cada vez que o usuário alterar o tamanho da janela principal, o Windows chama WndProc com a mensagem WM_SIZE. O procedimento WndProc reage a este evento e ajusta o tamanho da janela filha "edit" ao novo tamanho da janela principal.
Se localizarmos o procedimento WndProc será possível avaliar como ele lida com a mensagem WM_SIZE. Entre as várias maneiras de fazer isto, a mais fácil é através da função da API RegisterClass. O bloco de notas usa a função RegisterClassExA, função que registra uma classe janela para uso subsequente em chamadas para as funções CreateWindow ou CreateWindowEx.
ATOM RegisterClassEx( CONST WNDCLASSEX *lpwcx );
O parâmetro lpwcx é um ponteiro que aponta para uma estrutura do tipo WNDCLASSEX. Esta estrutura precisa ser preenchida com os atributos de classe apropriados antes do parâmetro ser passado para a função. Caso a função seja executada com sucesso, o valor de retorno é um atom que identifica unicamente a classe que está sendo registrada. Em caso de falha, o valor de retorno é zero.
Conhecendo a função e suas características, podemos usar o W32Dasm ou o IDA para obter o bloco de código que se referente à função. No W32Dasm, após carregar o programa, clique no item de menu Functions / Imports para abrir a janela com todas as funções importadas. Localize nesta janela a função USER32.RegisterClassExA e dê um duplo clique sobre a referência para ser levado ao código correspondente:
Caso você queira testar o IDA, carregue o programa, selecione o item de menu Jump / Jump to name (ou simplesmente Ctrl+L) para obter a janela de nomes. Selecione RegisterClassExA e digite [enter]. Você será levado ao seguinte trecho de código, que é a referência na seção .idata (seção das importações) da função procurada:
.idata:0040644C extrn RegisterClassExA:dword ; DATA XREF: sub_403140+71^r
Feche a janela de nomes e dê um duplo clique sobre a referência "sub_403140+71^r". Você será levado para a seção .text no seguinte ponto:
Conhecendo a localização do WndProc, vamos analisar o código correspondente a esta chamada. No W32Dasm, selecione o item de menu Goto / Goto Code Location e indique o endereço 401C8C. No IDA, basta dar um duplo clique sobre "sub_401C8C" para ser levado à subrotina:
Na linha 401C93 o programa checa o tipo de mensagem recebida e garante que seu valor esteja entre 1 e 5. Na linha seguinte, caso o valor seja maior do que 5 (nenhuma mensagem válida), salta para 401CAC. Caso a mensagem seja 5, salta para 401DA4, caso contrário continua determinando o tipo da mensagem. As mensagens válidas estão descritas em WM_SIZE, que representa o tipo da mensagem enviada para uma janela após seu tamanho ter sido modificado.
Parâmetros | Observação>/td> | Valor | Hexa | Significado |
fwSizeType = wParam; | // flag de redimensionamento | SIZE_MAXHIDE | 1 | Mensagem enviada para todas as janelas popup quando qualquer outra é maximizada |
SIZE_MAXIMIZED | 2 | Janela foi maximizada | ||
SIZE_MAXSHOW | 3 | Mensagem enviada para todas as janelas popup quando qualquer outra tenha sido restaurada ao tamanho original. | ||
SIZE_MINIMIZED | 4 | Janela foi minimizada. | ||
SIZE_RESTORED | 5 | A janela foi redimensionada mas não minimizada ou maximizada. | ||
nWidth = LOWORD(lParam); | // largura da área cliente | |||
nHeight = HIWORD (lParam); | // altura da área cliente | |||
Valor de retorno: Se a mensagem foi processada, o valor de retorno é 0 (zero). |
De posse dessas informações, fica claro que o programa checa a validade e o valor da mensagem para decidir que caminho tomar. Nós queremos esclarecer o que é feito caso WM_SIZE seja igual a 5 (SIZE_RESTORED), ou seja, o tamanho da janela foi modificado sem que ela tenha sido maximizada ou minimizada. Para isto, precisamos rastrear o salto que o programa efetua para o endereço 401DA4. No IDA, basta um duplo clique sobre "loc_401DA4" (no W32Dasm selecione Goto):
Analisando este trecho de código, nos deparamos com uma chamada à subrotina 40121B:
Note que aparentemente o manipulador (handle) da janela filha "edit" é armazenado no endereço (virtual) de nome hWnd. Para encontrar o valor de hWnd usando o IDA, selecione o item de menu View / Open subviews / Names (Shift+F4) para obter a janela de nomes. Nesta, selecione Search / Search (Alt+T) e digite o nome do endereço (hWnd): você encontrará hWndMain = 405000 e hWnd = 405004. Anote cuidadosamente estes endereços pois ainda vamos precisar muito deles!
Também podemos verificar que, para cada mensagem WM_SIZE, a janela filha "edit" é atualizada com uma chamada para MoveWindow com o novo tamanho da janela principal.
Criando o código adicional
Vimos que, para cada mensagem WM_SIZE, a janela-filha "edit" é atualizada através de uma chamada a MoveWindow de acordo com o novo tamanho da janela principal. Porém, queremos evitar que a janela-filha "edit" ocupe a área total da janela mãe, ou seja, teremos que criar uma rotina que gerencie a altura das janelas com valores diferentes. Lembrando do trecho de código onde estes parâmetros são utilizados:
Colocando um salto (jump) na linha 40122D, podemos fazer com que o programa faça um "desvio" para o nosso código, o qual irá alterar nHeight de acordo com nossas instruções antes de alcançar a chamada que atualiza o tamanho da janela-filha "edit". Para gerenciar o tamanho da janela-filha "edit", nosso novo código altera o valor original da altura para repassá-lo à função MoveWindow:
Código para alterar a altura da janela-filha "edit"
Observação: o desvio para nosso bloco de código vai ser obtido a partir de uma instrução jump que ocupará 5 bytes. Com isto, existem duas linhas do código original que serão "comidas" pelo jump e que precisam ser substituídas em algum ponto do nosso novo código. As duas linhas estão em 40122D e 401231. A primeira coloca no topo da pilha a altura (nHeight) da janela-filha com push [esp+C] e a segunda coloca no topo da pilha a largura (nWidth) da mesma janela-filha, também com um push [esp+C]. Como utilizamos o registrador EAX para calcular o novo valor da altura da janela-filha "edit", substituímos a linha 40122D push [esp+C] pela linha indicada em (1), colocando diretamente o valor de EAX no topo da pilha. A linha 401231 é substituída pela linha indicada em (2), mantendo a instrução push [esp+C].
Além disso, na linha indicada em (3), o valor calculado da nova altura é salvo no endereço 40DB70 porque vai ser necessário no módulo de código referente à impressão, citado logo adiante. A escolha do endereço das variáveis encontra-se logo abaixo na tabela de variáveis.
Após ter projetado o código para gerenciar o tamanho da janela-filha "edit", precisamos imprimir algum texto na janela principal (os números das linhas). Toda vez que uma janela precisa ser "repintada" (por exemplo, quando esteve encoberta por outra janela ou quando o usuário alterar seu tamanho), o Windows chama o procedimento WndProc com WM_PAINT. Isto significa que teremos que "caçar" a mensagem WM_PAINT e, novamente, forçar um salto para a nossa rotina que, desta vez, será um módulo que imprima texto.
Também precisamos imprimir texto toda vez que o cursor muda a sua posição vertical. A melhor maneira para conseguir isto é ignorar todas as mensagens possíveis que informem qualquer mudança na posição do cursor e passar a comparar a nova posição do cursor com a antiga.
Examinando o início do código do WndProc, nota-se que nada é feito com a mensagem WM_PAINT. Desta forma, no início do procedimento WndProc podemos inserir um salto (jump) para nosso módulo de código:
Novamente precisaremos de 5 bytes para forçar o salto. Temos espaço suficiente entre os endereços 401C8C e 401C90. O endereço para o retorno no nosso módulo de código será 401C93.
Código para imprimir números de linhas
Note que foram utilizadas algumas variáveis. Cada uma destas variáveis possui um endereço próprio. Alguns dos valores são alterados pelo próprio programa, outros precisam ser introduzidas através de um editor hexadecimal. Os endereços das variáveis foram escolhidos num bloco logo após o final do código:
Referência | Nome | Endereço | Valor |
(3) | novaAltura | 40DB70 | |
(5) | linhaAtual | 40DB78 | |
(7) | stringFormatação | 40DB80 | %#05d |
(12) | stringNroLinha | 40DB90 | linha : |
(8) | offsetNroAscii | 40DB9A | |
(11) | hContexto | 40DBA0 |
Foram usadas somente funções da API do Windows já importadas pelo notepad.exe (verifique a tabela de importações). O que fazer caso seja necessário lançar mão de funções da API que não constem da tabela de importações? No final do tutorial há uma explicação de como proceder nestes casos.
Algumas funções necessitam do handle da janela principal como parâmetro. Já foi descrito anteriormente como encontrar este valor.
- (4) Nesta linha e nas três seguintes a chamada para a função SendMessageA é preparada. Esta função obtém o valor da coordenada Y do cursor e o retorna no registrador EAX.
- (6) Se a posição do cursor for 0 (zero), estamos na linha 1. Se for 1, estamos na linha 2, etc. Portanto, precisamos ajustar o valor da coordenada Y do cursor somando 1.
- (7) Preparamos os parâmetros da função wsprintfA.
- (9) A função wsprintfA transforma o número enviado como parâmetro em caracteres ASCII de acordo com a string de formatação e coloca o resultado no endereço especificado. Por exemplo, transforma 1 em "00001".
- (10) Para utilizar a função TabbedTextOut, precisamos primeiro obter o handle do contexto. Para isto chamamos a função GetDC com o parâmtero da janela principal (onde o texto deve ser impresso).
- (11) A função GetDC devolve o handle do contexto no registrador EAX. Como precisaremos deste handle para liberar o contexto, armazenamos seu valor na variável hContexto.
- (13) A função TabbedTextOut exige muitos parâmetros para imprimir a string enviada no local especificado. As 9 linhas anteriores à chamada preparam os parâmetros necessários.
- (14) Finalmente, após a impressão da string na janela mãe, precisamos liberar o contexto que foi bloqueado pela função GetDC. Para isto utilizamos a função ReleaseDC.
Enxertando o código no executável
Já temos um projeto de código, agora é preciso incluí-lo no executável. Mais uma vez, vamos por partes para não nos perdermos.
Inserindo o código da altura da janela
É preciso encontrar espaço ocioso no executável que possa ser utilizado para incluir o código. Usando o PE Explorer (faça o download do trial na Heaven Tools), verificamos o seguinte nos cabeçalhos das seções:
Nome | Tam.Virtual | End.Virtual | Tam.Dados | Ponteiro | Características | Sobra |
.text | 00003EFC | 00401000 | 00004000 | 00001000 | 60000020 | 104 (260) |
.data | 0000083C | 00405000 | 00001000 | 00005000 | C0000040 | 7C4 (1988) |
.idata | 00000E22 | 00406000 | 00001000 | 00006000 | 40000040 | 1DE (478) |
.rsrc | 00006000 | 00407000 | 00006000 | 00007000 | 40000040 | -0- |
.reloc | 00000AAE | 0040D000 | 00001000 | 0000D000 | 42000040 | 552 (1362) |
- Procurando espaço livre
Vamos tomar a seção .data como exemplo. Esta seção foi dimensionada para ocupar um espaço de 1000 (tamanho dos dados) a partir do endereço 405000. Seu tamanho virtual é 83C e o espaço total disponível é de 1000, indicando que ainda há 7C4 bytes livres (1000 - 83C = 7C4). Tudo isto no sistema hexadecimal. Transformando os números para o sistema decimal, apenas por curiosidade, sabemos que o espaço reservado é de 4096, o espaço ocupado é de 2108, restando portanto 1988 bytes livres.
A segunda maior sobra de espaço se encontra na seção .reloc, onde há 1362 (552 em hexa) bytes disponíveis. Vamos escolher a seção .reloc para abrigar nosso código adicional. Ele cabe perfeitamente neste espaço livre, portanto, não vai haver a necessidade de ampliar o tamanho da seção escolhida.
- Calculando o tamanho do salto
Segue-se agora uma longa jornada de cálculos. O primeiro deles será determinar o endereço do primeiro salto. Conforme visto anteriormente, foi decidido fazer um desvio no ponto onde o programa põe na pilha o valor da altura e da largura da janela-filha "edit". Reveja o trecho de código obtido através do debugador OllyDbg (note as referências preciosas que o programa fornece!):
O OllyDbg está disponível na seção de downloads em Informática / Debuggers.
Portanto, faremos um salto incondicional - jmp do endereço 40122D para o início do espaço vago da seção .reloc. Pelo cabeçalho de seções verificamos que o endereço virtual inicial de .reloc é 40D000 e nós queremos colocar o código adicional AAEh bytes adiante. Basta somar ao endereço virtual da seção o tamanho do código para obter o endereço inicial do código adicional, ou seja, 40D000 + AAE = 40DAAE. Nosso salto, então, deverá ser de 40122D para 40DAAE, o que corresponde a um salto de C881 bytes. Como sabemos que nossa instrução, por ser um salto longo, conterá 5 bytes, precisamos descontar os mesmos para calcular o valor correto para o salto: 40DAAE - 40122D - 5 = C87C.
De posse deste valor, vamos compor a instrução lembrando que a distância do salto é indicada em 4 bytes na ordem inversa, ou seja, 00 00 C8 7C será indicado como 7C C8 00 00:
Instrução | Código hexadecimal | Observação |
JMP | E9 | Salto incondicional longo |
byte 1 | 7C | |
byte 2 | C8 | |
byte 3 | 00 | |
byte 4 | 00 | Resultado: E9 7CC80000 |
Para o salto de retorno é preciso pular alguns bytes "para trás". No exemplo anterior, pulamos 51324 bytes "para frente", no retorno teremos que saltar 51343 bytes "para trás", ou seja, -51343 (menos 51343). Em hexadecimal, 401235 - 40DABF - 5 = FFFF3771.
- Alterando o código para o salto
O programa original deve sofrer as seguintes alterações para a introdução do salto:
Endereço | Código original | Alterar para |
0040122D | FF7424 0C PUSH DWORD PTR SS:[ESP+C] | E9 7C C8 00 |
00401231 | FF7424 0C PUSH DWORD PTR SS:[ESP+C] | 00 90 90 90 |
00401235 | 6A 00 PUSH 0 |
Observe que ocupamos apenas o primeiro byte (FF) da instrução no endereço 401231 para poder completar nossa instrução de salto. Restaram os códigos 74, 24 e 0C que foram anulados com três instruções NOP (90) para manter íntegro o tamanho do código original e não desestruturar outros endereçamentos existentes. Com o salto devidamente inserido, este bloco de código passa a ser:
Endereço | Salto adicionado | |
0040122D | E9 7CC80000 | JMP NOTEPAD.0040DAAE |
00401232 | 90 | NOP |
00401233 | 90 | NOP |
00401234 | 90 | NOP |
00401235 | 6A 00 | PUSH 0 |
- Adicionando o código para alterar o tamanho da janela "edit"
Quando alcançar a linha 40122D, o salto é efetuado e a execução do código continua no endereço 40DAAE, exatamente o ponto onde vamos inserir o bloco 1 do código adicional. Nele, antes de refazermos as operações das linhas 40122D e 401231 que foram suprimidas para poder inserir o salto, a altura da janela "edit" é diminuída em 20 pixels. Para tanto, utilizamos o registrador EAX:
Endereço | Hexa | Operação | Observações |
0040DAAE | E9 7CC80000 | MOV EAX,DWORD PTR SS:[ESP+C] | ; Obtém o valor da altura |
0040DAB2 | 83E8 14 | SUB EAX,14 | ; Subtrai 14h = 20d da altura |
0040DAB5 | A3 70DB4000 | MOV DWORD PTR DS:[40DB70],EAX | ; Guarda nova altura em 40DB70 |
0040DABA | 50 | PUSH EAX | ; Nova altura na pilha |
0040DABB | FF7424 0C | SS:[ESP+C] | ; Largura no topo da pilha |
0040DABF | E9 7137FFFF | JMP NOTEPAD.00401235 | ; Retorna ao ponto de chamada |
Use um editor hexadecimal (Hackman ou Hacker's View, por exemplo) e faça as alterações propostas numa cópia do programa notepad.exe (o Hacker's View permite a entrada de código assembly e calcula os jumps, o que facilita muito o trabalho). Qualquer um dos programas sugeridos mostra o código a partir da posição 0 (zero) e NÃO pelo endereço virtual. Portanto, é preciso conhecer a posição dos endereços virtuais pelo seu deslocamento (offset) a partir da posição zero. Isto se consegue subtraindo a base da imagem (00400000) do endereço pretendido. Por exemplo, o deslocamento do endereço 40122D é 122D (40122D - 400000 = 122D). Caso você encontre dificuldades, o W32Dasm indica no rodapé o offset de cada linha de código
- Testando a primeira etapa
Após fazer as modificações sugeridas é aconselhável testar o programa modificado. Tudo que esperamos é que a janela-filha "edit" tenha uma altura menor do que a janela principal. Se tudo correu bem, a janela do novo bloco de notas tem um "rodapé" de 20 pixels (a diferença de altura entre as duas janelas), o qual será usado para imprimir o texto com a indicação dos números das linhas.
Inserindo o código de impressão
Já identificamos um espaço ocioso no executável onde é possível incluir o código, já sabemos como calcular a distância de saltos e onde inseri-los, então... vamos ao trabalho.
- Inserindo o segundo salto
Conforme já visto, decidimos fazer um desvio no início do procedimento WndProc. Reveja o trecho de código citado:
Portanto, faremos um salto incondicional - jmp do endereço 401C8D para um endereço após o final do nosso primeiro bloco de código. Deixamos alguns bytes zerados e iniciamos o segundo bloco de código em 40DACD (o primeiro bloco terminou em 40DABF). O salto, então, será de BE3B bytes (40DAC8 - 401C8D - 5 = BE3B).
De posse deste valor, vamos compor a instrução, lembrando novamente que a distância do salto é indicada em 4 bytes na ordem inversa, ou seja, 00 00 BE 3B será indicado como 3B BE 00 00
O programa original deve sofrer as seguintes alterações para a introdução do salto:
Endereço | Código original | Alterar para |
00401C8D | 8BEC MOV EBP,ESP | E9 3B |
00401C8F | 56 PUSH ESI | BE |
00401C90 | 8B75 0C MOV ESI,DWORD PTR SS:[EBP+C] | 00 00 90 |
00401C93 | 83FE CMP ESI,5 |
Observe que ocupamos apenas os dois primeiros bytes (8B e 75) da instrução no endereço 401C90 para poder completar a instrução de salto. Restou o código 0C, o qual foi anulado com uma instrução NOP (90) para manter íntegro o tamanho do código original e não desestruturar outros endereçamentos existentes. Com o salto devidamente inserido, este bloco de código passa a ser:
Endereço | Salto adicionado | |
00401C8D | E9 3EBE0000 | JMP NOTEPAD.0040DACD |
00401C92 | 90 | NOP |
00401C93 | 83FE | CMP ESI,5 |
- Adicionando o código da impressão dos números de linha
Quando alcançar a linha 401C8D, o salto é efetuado e a execução do código continua no endereço 40DACD, exatamente onde vamos inserir o bloco 2 do código adicional. Nele, antes de qualquer outra coisa, vamos refazer as operações suprimidas pela introdução do salto incondicional repetindo o código original:
Endereço | Hexa | Operação | Observações |
0040DACD | 8BEC | MOV EBP,ESP | |
0040DACF | 56 | PUSH ESI | |
0040DAD0 | 8B75 0C | MOV ESI,DWORD PTR SS:[EBP+C] | ; ESI contém a mensagem |
A seguir preparamos os parâmetros para a função SendMessageA da API do Windows e a chamamos. Lembre-se de que os parâmetros precisam ser colocados na pilha na ordem inversa (para que a função possa acessá-los na ordem correta). O endereço da função pode ser obtido através do PE Explorer, na janela "Imports", sob a rubrica USER32:
004064E4 function SendMessageA( hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; external 'user32.dll' name 'SendMessageA' index 537
Endereço | Hexa | Operação | Observações |
0040DAD3 | 6A 00 | PUSH 0 | ; lParam |
0040DAD5 | 6A FF | PUSH -1 | ; wParam |
0040DAD7 | 68 C9000000 | PUSH C9 | ; Mensagem |
0040DADC | FF35 04504000 | PUSH DWORD PTR DS:[405004] | ; Handle da janela filha "edit" |
0040DADE | FF15 E4644000 | CALL DWORD PTR DS:[<&USER32.SendMessage>] | ; Após esta chamada, eax contém a posição Y do cursor |
No retorno da função SendMessageA, o registrador EAX contém a posição Y do cursor na janela-filha "edit", ou seja, a linha atual da janela. Como já designamos o local de armazenamento do valor da variável dwLinha (40DB78), podemos comparar o valor de ambos. Se houver diferença, significa que o cursor mudou de linha e o rodapé precisa ser atualizado. Se ambos forem iguais, podemos voltar ao código original.
Endereço | Hexa | Operação | Observações |
0040DAE8 | 3B05 78DB4000 | CMP EAX,DWORD PTR DS:[40DB78] | ; Posição do cursor mudou? |
0040DAEE | 75 05 | JNZ SHORT 0040DAF5 | ; Sim, imprimir novo número de linha |
0040DAF0 | 83FE 0F | CMP ESI,0F | ; A mensagem atual é WM_PAINT? |
0040DAF3 | 75 64 | JNZ SHORT 0040DB59 | ; Não, vá para o fim da rotina e retorne para WndProc |
Caso o cursor tenha trocado de linha, algumas medidas precisam ser tomadas: atualizar a variável dwLinha em 40DB78 com o novo valor, somar 1 ao número da linha que deve ser impresso porque as linhas iniciam sua numeração em 0 (linha 0 - imprimimos linha 1, linha 1 - imprimimos linha 2, etc) e fazer uma chamada à função wsprintfA para "montar" a string que deve aparecer no rodapé:
0040641C function wsprintf( Output: PAnsiChar; Format: PAnsiChar): Integer; stdcall; external 'user32.dll' name 'wsprintfA' index 692;
Esta função recebe o valor que deve ser "stringado" (o número ajustado da linha), a string "%#05d" que corresponde ao formato de número precedido de zeros em 5 casas (1 será 00001) e o endereço onde deve ser armazenado o resultado (40DB90 + 0A = 40DB9A). O endereço da nossa "variável" para a string do rodapé é 40DB90. Os caracteres " Linha : " ocupam 10 posições, portanto, queremos que, a partir da 11a. posição seja armazenado o número transformado em ASCII no formato indicado pela string de formatação:
Endereço | Hexa | Operação | Observações |
0040DAF5 | A3 78DB4000 | MOV DWPRD PTR DS:[40DB78],EAX | ; Linha mudou, guarde o novo valor |
0040DAFA | 40 | INC EAX | ; Ajusta número de linha |
0040DAFB | 50 | PUSH EAX | ; Põe o novo número de linha na pilha |
0040DAFC | 8D05 80DB4000 | LEA EAX,DWORD PTR DS:[40DB80] | ; String de formatação |
0040DB02 | 50 | PUSH EAX | ; Põe string na pilha |
0040DB03 | 8D05 9ADB4000 | LEA EAX,DWROD PTR DS:[40DB9A] | ; Endereço para o resultado (ASCII) |
0040DB09 | 50 | PUSH EAX | ; Põe endereço na pilha |
0040DB0A | FF15 1C644000 | CALL DWORD PTR DS:[<&USER32.wsprintf>] | ; Transforma número em ASCII |
Agora precisamos obter o contexto do ambiente da janela principal para prepararmos a impressão da string que se encontra pronta no endereço 40DB90 na posição que definimos como rodapé. Para obter e bloquear o contexto utilizamos a função GetDC. Logo a seguir, armazenamos o handle do contexto no endereço 40DBA0 porque precisaremos do mesmo para imprimir a string e para liberar o contexto bloqueado depois de efetuarmos a impressão.
004064D8 function GetDC(hWnd: HWND): HDC; stdcall; external 'user32.dll' name 'GetDC' index 257;
Endereço | Hexa | Operação | Observações |
0040DB10 | FF35 00504000 | PUSH DWORD PTR DS:[405000] | ; Handle da janela principal |
0040DB16 | FF15 D8644000 | CALL DWORD PTR DS:[<&USER32.GetDC>] | ; Obtém contexto do ambiente |
0040DB1C | A3 A0DB4000 | MOV DWORD PTR DS:[40DBA0],EAX | ; Salva Handle do Contexto |
Tudo pronto para imprimir! A função utilizada é a TabbedTextOutA que se encarregará de transferir a string " Linha : 0000x" localizada no endereço 40DB90 para o rodapé da janela principal. Esta função exige uma pá de parâmetros:
00406468 function TabbedTextOut (hdc: HDC; X, Y: Integer; lpString: PAnsiChar; nCount, nTabPositions: Integer; var lpnTabStopPositions; nTabOrigin: Integer): Longint; stdcall; external 'user32.dll' name 'TabbedTextOutA' index 633;
Endereço | Hexa | Operação | Observações |
0040DB21 | 6A 01 | PUSH 1 | ; Origem do Tab |
0040DB23 | 6A 00 | PUSH 0 | ; |
0040DB25 | 6A 00 | PUSH 0 | ; |
0040DB27 | 6A 0F | PUSH F | ; Nro de caracteres (nCount) |
0040DB29 | 8D05 90DB4000 | LEA EAX,DWORD PTR DS:[40DB90] | ; String que deve ser impressa (lpString) |
0040DB2F | 50 | PUSH EAX | ; |
0040DB30 | FF35 70DB4000 | PUSH DWORD PTR DS:[40DB70] | ; Posição Y da string (= altura da janela edit) |
0040DB36 | 6A 00 | PUSH 0 | ; Posição X da string |
0040DB38 | FF35 A0DB4000 | PUSH DWORD PTR DS:[40DBA0] | ; Handle do Contexto |
0040DB3E | FF15 68644000 | CALL DWORD PTR DS:[<&USER32.TabbedTextOut>] | ; Imprime a string |
Ufa ! Está por pouco... só falta liberar o contexto para poder voltar ao código original. A função que utilizamos é a ReleaseDC:
004064DC function ReleaseDC(hWnd: HWND; hDc: HDC): Integer; stdcall; external 'user32.dll' name 'ReleaseDC' index 520;
Endereço | Hexa | Operação | Observações |
0040DB44 | FF35 A0DB4000 | PUSH DWORD PTR DS:[40DBA0] | ; Handle do Contexto |
0040DB4A | FF35 00504000 | PUSH DWORD PTR DS:[405000] | ; Handle da Janela Principal |
0040DB50 | FF15 DC644000 | CALL DWORD PTR DS:[<&USER32.ReleaseDC>] | ; Libera o Handle do Contexto |
Hora de voltar para o código principal. Porém, observando o Stack Pointer (ESP - Ponteiro da Pilha), verificamos que fizemos uma salada com tantos pushes. Avançamos 12 bytes que precisam ser "corrigidos" para evitar erros em referências posteriores. Como sabemos que a pilha "anda ao contrário", para DESCER 12 bytes precisamos SOMAR estas posições (12 dec = 0C hexa).
Endereço | Hexa | Operação | Observações |
0040DB56 | 83C4 0C | ADD ESP,C | ; Corrige o Stack Pointer |
0040DB59 | E9 3541FFFF | JMP 00401C93 | ; Volta para WndProc |
Fazendo os remendos (patches)
Para não perder tempo é preciso planejar. Planejamos o perfil do código, planejamos sua inserção no executável e agora vamos por a mão na massa. Para inserir os bytes referentes às instruções do primeiro bloco de código, podemos usar (se é que você já não usou) qualquer editor hexadecimal. Afinal, não é tanto código assim.
Para o segundo bloco de código sugiro o uso do Hacker's View (HIEW), pois permite a inserção direta do código assembly, além de calcular os deslocamentos dos jumps. Isto facilita muito o trabalho. Faça as alterações propostas numa cópia do programa notepad.exe.
Os editores mais conhecidos, como você já sabe, mostram o código a partir da posição 0 (zero) e NÃO pelo endereço virtual. Portanto, precisamos conhecer a posição dos endereços virtuais pelo seu deslocamento (offset) a partir da posição zero, o que se consegue subtraindo a base da imagem (00400000) do endereço pretendido. Por exemplo, o deslocamento do endereço 40DACD é DACD (40DACD - 400000 = DACD). Caso você encontre dificuldades, o W32Dasm indica no rodapé o offset de cada linha de código.
Após incluir os dois blocos de código, NÃO SE ESQUEÇA das variáveis. Duas delas precisam ser definidas: a string de formatação e a string do número de linha (veja em tabela de variáveis). Ainda com o editor hexadecimal da sua preferência vá até o offset DB80 e digite o valor da string de formatação. No offset DB90 digite a string do número de linha. Observe que há dois espaços (20 20) antes da palavra linha.
Endereço | Hexadecimal | ASCII |
0040DB70 | 00 00 00 00 00 00 00 00 | ........ |
0040DB78 | 00 00 00 00 00 00 00 00 | ........ |
0040DB80 | 25 23 30 35 64 00 00 00 | %#05d... |
0040DB88 | 00 00 00 00 00 00 00 00 | ........ |
0040DB90 | 20 20 6C 69 6E 68 61 20 | linha |
0040DB98 | 3A 20 00 00 00 00 00 00 | : ...... |
0040DBA0 | 00 00 00 00 00 00 00 00 | ........ |
Testando o novo bloco de notas
Finalmente podemos testar o programa modificado. Já sabemos que a janela-filha "edit" tem uma altura menor do que a janela principal, formando um "rodapé" de 20 pixels (a diferença de altura entre as duas janelas). Este rodapé é usado para imprimir o texto com a indicação dos números de linha. Se tudo estiver em ordem, o aspecto do novo notepad será este:
Recado da vó Vicki
Se você conseguiu chegar até o final deste artigo sem perder o fôlego, considere-se um herói da resistência. Agora, se além de acompanhar o texto ainda conseguiu reproduzir a experiência desta oficina de informática e criar seu bloco de notas com números de linha, seja bem vindo ao clube dos "malucos beleza", os escovadores de bits adeptos da co-engenharia que conseguiram se safar da camisa de força