Baseado no curso da USNA e NSA (Stack Based Binary Exploits and Defenses)

Home

Aula 03: Desmontando(Disassembling) um Programa

Conteúdo

1 Um programa binário … o que é?

Analisamos muito como escrever programas em c, compilá-los em binários e, em seguida, executá-los. Agora vamos olhar diretamente para os próprios arquivos binários. Nós vamos examinar o que exatamente é um binário, como ele é formatado e como desmembra-lo ou fazer o dissambler do conteúdo dentro?

2 objdump e readelf básico

Para toda esta aula, vamos separar um programa helloworld simples:

#include <stdio.h>

int main(int argc, char *argv){

    char hello[15]="Hello, World!\n";
    char * p;

    for(p = hello; *p; p++){

        putchar(*p);         

    }

    return 0;
}

Vamos compilar o programa para criar um binário:

felipe@pc:~/binaryanalysis$ gcc helloworld.c -o helloworld

Agora, se usarmos o comando file, podemos ver que tipo de arquivo o binário é.

felipe@pc:~/binaryanalysis$ file helloworld
helloworld: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=2b27688b97f10f626f1ff62c232d7a2298d6afa1, not stripped

Vemos que na verdade é um arquivo ELF, que significa Executable and Linkable Format. Trabalharemos exclusivamente com binários em ELF.

2.1 ELF Files e ELF Headers

Todos os arquivos ELF têm um cabeçalho descrevendo as diferentes seções e Informações gerais. Podemos ler as informações do cabeçalho do nosso programa helloworld usando o readelf

felipe@pc:~/binaryanalysis$ readelf -h helloworld
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048370
  Start of program headers:          52 (bytes into file)
  Start of section headers:          4472 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         30
  Section header string table index: 27

A maioria dessas informações não é muito útil, mas deixe-me apontar algumas coisas importantes.

  1. Existe um número mágico! O número mágico é usado para dizer, ei, isso é ELF e qual versão
  2. A classe é ELF32, então é de 32 bits
  3. A máquina é Intel 80386, ou x386
  4. O ponto de entrada para o arquivo é o endereço 0x804870, essencialmente o que é a primeira instrução na função de seção _start que chama main.

Todo o resto não é super útil para nossos propósitos. Outra coisa interessante que podemos fazer com o readelf é olhar para todas as seções, que são regiões do binário para diferentes propósitos.

felipe@pc:~/binaryanalysis$ readelf -S helloworld
There are 30 section headers, starting at offset 0x1178:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048154 000154 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048168 000168 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE            08048188 000188 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        080481ac 0001ac 000020 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          080481cc 0001cc 000060 10   A  6   1  4
  [ 6] .dynstr           STRTAB          0804822c 00022c 000068 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          08048294 000294 00000c 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         080482a0 0002a0 000030 00   A  6   1  4
  [ 9] .rel.dyn          REL             080482d0 0002d0 000008 08   A  5   0  4
  [10] .rel.plt          REL             080482d8 0002d8 000020 08   A  5  12  4
  [11] .init             PROGBITS        080482f8 0002f8 000023 00  AX  0   0  4
  [12] .plt              PROGBITS        08048320 000320 000050 04  AX  0   0 16
  [13] .text             PROGBITS        08048370 000370 0001f2 00  AX  0   0 16
  [14] .fini             PROGBITS        08048564 000564 000014 00  AX  0   0  4
  [15] .rodata           PROGBITS        08048578 000578 000008 00   A  0   0  4
  [16] .eh_frame_hdr     PROGBITS        08048580 000580 00002c 00   A  0   0  4
  [17] .eh_frame         PROGBITS        080485ac 0005ac 0000b0 00   A  0   0  4
  [18] .init_array       INIT_ARRAY      08049f08 000f08 000004 00  WA  0   0  4
  [19] .fini_array       FINI_ARRAY      08049f0c 000f0c 000004 00  WA  0   0  4
  [20] .jcr              PROGBITS        08049f10 000f10 000004 00  WA  0   0  4
  [21] .dynamic          DYNAMIC         08049f14 000f14 0000e8 08  WA  6   0  4
  [22] .got              PROGBITS        08049ffc 000ffc 000004 04  WA  0   0  4
  [23] .got.plt          PROGBITS        0804a000 001000 00001c 04  WA  0   0  4
  [24] .data             PROGBITS        0804a01c 00101c 000008 00  WA  0   0  4
  [25] .bss              NOBITS          0804a024 001024 000004 00  WA  0   0  1
  [26] .comment          PROGBITS        00000000 001024 00004d 01  MS  0   0  1
  [27] .shstrtab         STRTAB          00000000 001071 000106 00      0   0  1
  [28] .symtab           SYMTAB          00000000 001628 000440 10     29  45  4
  [29] .strtab           STRTAB          00000000 001a68 000274 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

Novamente, um monte dessas informações não é muito útil para nós, mas pode ser mais tarde. Algumas coisas importantes a serem observadas:

  1. A seção .bss está listada, isso é o mesmo que bss no layout da memória do programa
  2. Há também a seção .data, igual ao layout da memória do programa
  3. Finalmente, há uma seção .text, igual a antes. E observe que é no endereço 0x08048370 que é o mesmo endereço no cabeçalho para o início(_start) das instruções do programa.

2.2 Chegando à montagem(assembly) com objdump

Agora que temos uma ideia de como o arquivo é formatado, seria bom entrar nos detalhes das instruções da máquina em si. Para isso, usaremos objdump ou "object dump". Simplesmente, nós podemos chamar o executável binário assim:

felipe@pc:~/binaryanalysis$ objdump -d helloworld 

helloworld:     file format elf32-i386


Disassembly of section .init:

080482f8 <_init>:
 80482f8:	53                   	push   %ebx
 80482f9:	83 ec 08             	sub    $0x8,%esp
 80482fc:	e8 9f 00 00 00       	call   80483a0 <__x86.get_pc_thunk.bx>
 8048301:	81 c3 ff 1c 00 00    	add    $0x1cff,%ebx
 8048307:	8b 83 fc ff ff ff    	mov    -0x4(%ebx),%eax
 804830d:	85 c0                	test   %eax,%eax
 804830f:	74 05                	je     8048316 <_init+0x1e>
 8048311:	e8 2a 00 00 00       	call   8048340 <__gmon_start__@plt>
 8048316:	83 c4 08             	add    $0x8,%esp
 8048319:	5b                   	pop    %ebx
 804831a:	c3                   	ret    

Disassembly of section .plt:

08048320 <__stack_chk_fail@plt-0x10>:
 8048320:	ff 35 04 a0 04 08    	pushl  0x804a004
 8048326:	ff 25 08 a0 04 08    	jmp    *0x804a008
 804832c:	00 00                	add    %al,(%eax)
	...

Vai despejar um monte de coisas, mas vamos olhar com mais cuidado para baixo, Veremos um cabeçalho que parece familiar, main:

0804841d <main>:
 804841d:	55                   	push   %ebp
 804841e:	89 e5                	mov    %esp,%ebp
 8048420:	83 e4 f0             	and    $0xfffffff0,%esp
 8048423:	83 ec 30             	sub    $0x30,%esp
 8048426:	c7 44 24 1d 48 65 6c 	movl   $0x6c6c6548,0x1d(%esp)
 804842d:	6c 
 804842e:	c7 44 24 21 6f 2c 20 	movl   $0x57202c6f,0x21(%esp)
 8048435:	57 
 8048436:	c7 44 24 25 6f 72 6c 	movl   $0x646c726f,0x25(%esp)
 804843d:	64 
 804843e:	66 c7 44 24 29 21 0a 	movw   $0xa21,0x29(%esp)
 8048445:	c6 44 24 2b 00       	movb   $0x0,0x2b(%esp)
 804844a:	8d 44 24 1d          	lea    0x1d(%esp),%eax
 804844e:	89 44 24 2c          	mov    %eax,0x2c(%esp)
 8048452:	eb 17                	jmp    804846b <main+0x4e>
 8048454:	8b 44 24 2c          	mov    0x2c(%esp),%eax
 8048458:	0f b6 00             	movzbl (%eax),%eax
 804845b:	0f be c0             	movsbl %al,%eax
 804845e:	89 04 24             	mov    %eax,(%esp)
 8048461:	e8 aa fe ff ff       	call   8048310 <putchar@plt>
 8048466:	83 44 24 2c 01       	addl   $0x1,0x2c(%esp)
 804846b:	8b 44 24 2c          	mov    0x2c(%esp),%eax
 804846f:	0f b6 00             	movzbl (%eax),%eax
 8048472:	84 c0                	test   %al,%al
 8048474:	75 de                	jne    8048454 <main+0x37>
 8048476:	b8 00 00 00 00       	mov    $0x0,%eax
 804847b:	c9                   	leave  
 804847c:	c3                   	ret    
 804847d:	66 90                	xchg   %ax,%ax
 804847f:	90                   	nop

Este é o assembly para a função principal. Olhando para o outro lado, da esquerda à direita, mais à esquerda está o endereço que esta instrução é carregado, então os bytes reais da instrução e, em seguida, finalmente o nome dos detalhes da instrução.

A primeira coisa que você pode notar sobre a instrução em si é que é muito, muito difícil de ler. Isso porque é a sintaxe da AT&T, o que, simplesmente, é uma merda! Usaremos um formato alternativo chamado Intel sintaxe, que é muito, muito melhor. Para isso, precisamos passar por um argumento para objdump:

felipe@pc:~/binaryanalysis$ objdump -M intel -d helloworld 

helloworld:     file format elf32-i386


Disassembly of section .init:

080482f8 <_init>:
 80482f8:	53                   	push   ebx
 80482f9:	83 ec 08             	sub    esp,0x8
(... snip ...)

0804841d <main>:
 804841d:	55                   	push   ebp
 804841e:	89 e5                	mov    ebp,esp
 8048420:	83 e4 f0             	and    esp,0xfffffff0
 8048423:	83 ec 30             	sub    esp,0x30
 8048426:	c7 44 24 1d 48 65 6c 	mov    DWORD PTR [esp+0x1d],0x6c6c6548
 804842d:	6c 
 804842e:	c7 44 24 21 6f 2c 20 	mov    DWORD PTR [esp+0x21],0x57202c6f
 8048435:	57 
 8048436:	c7 44 24 25 6f 72 6c 	mov    DWORD PTR [esp+0x25],0x646c726f
 804843d:	64 
 804843e:	66 c7 44 24 29 21 0a 	mov    WORD PTR [esp+0x29],0xa21
 8048445:	c6 44 24 2b 00       	mov    BYTE PTR [esp+0x2b],0x0
 804844a:	8d 44 24 1d          	lea    eax,[esp+0x1d]
 804844e:	89 44 24 2c          	mov    DWORD PTR [esp+0x2c],eax
 8048452:	eb 17                	jmp    804846b <main+0x4e>
 8048454:	8b 44 24 2c          	mov    eax,DWORD PTR [esp+0x2c]
 8048458:	0f b6 00             	movzx  eax,BYTE PTR [eax]
 804845b:	0f be c0             	movsx  eax,al
 804845e:	89 04 24             	mov    DWORD PTR [esp],eax
 8048461:	e8 aa fe ff ff       	call   8048310 <putchar@plt>
 8048466:	83 44 24 2c 01       	add    DWORD PTR [esp+0x2c],0x1
 804846b:	8b 44 24 2c          	mov    eax,DWORD PTR [esp+0x2c]
 804846f:	0f b6 00             	movzx  eax,BYTE PTR [eax]
 8048472:	84 c0                	test   al,al
 8048474:	75 de                	jne    8048454 <main+0x37>
 8048476:	b8 00 00 00 00       	mov    eax,0x0
 804847b:	c9                   	leave  
 804847c:	c3                   	ret    
 804847d:	66 90                	xchg   ax,ax
 804847f:	90                   	nop

0804846d <main>:
 804846d:	55                   	push   ebp
 804846e:	89 e5                	mov    ebp,esp
 8048470:	83 e4 f0             	and    esp,0xfffffff0
 8048473:	83 ec 30             	sub    esp,0x30
 8048476:	65 a1 14 00 00 00    	mov    eax,gs:0x14
 804847c:	89 44 24 2c          	mov    DWORD PTR [esp+0x2c],eax
 8048480:	31 c0                	xor    eax,eax
 8048482:	c7 44 24 1d 48 65 6c 	mov    DWORD PTR [esp+0x1d],0x6c6c6548
 8048489:	6c 
 804848a:	c7 44 24 21 6f 2c 20 	mov    DWORD PTR [esp+0x21],0x57202c6f
 8048491:	57 
 8048492:	c7 44 24 25 6f 72 6c 	mov    DWORD PTR [esp+0x25],0x646c726f
 8048499:	64 
 804849a:	66 c7 44 24 29 21 0a 	mov    WORD PTR [esp+0x29],0xa21
 80484a1:	c6 44 24 2b 00       	mov    BYTE PTR [esp+0x2b],0x0
 80484a6:	8d 44 24 1d          	lea    eax,[esp+0x1d]
 80484aa:	89 44 24 18          	mov    DWORD PTR [esp+0x18],eax
 80484ae:	eb 17                	jmp    80484c7 <main+0x5a>
 80484b0:	8b 44 24 18          	mov    eax,DWORD PTR [esp+0x18]
 80484b4:	0f b6 00             	movzx  eax,BYTE PTR [eax]
 80484b7:	0f be c0             	movsx  eax,al
 80484ba:	89 04 24             	mov    DWORD PTR [esp],eax
 80484bd:	e8 9e fe ff ff       	call   8048360 <putchar@plt>
 80484c2:	83 44 24 18 01       	add    DWORD PTR [esp+0x18],0x1
 80484c7:	8b 44 24 18          	mov    eax,DWORD PTR [esp+0x18]
 80484cb:	0f b6 00             	movzx  eax,BYTE PTR [eax]
 80484ce:	84 c0                	test   al,al
 80484d0:	75 de                	jne    80484b0 <main+0x43>
 80484d2:	b8 00 00 00 00       	mov    eax,0x0
 80484d7:	8b 54 24 2c          	mov    edx,DWORD PTR [esp+0x2c]
 80484db:	65 33 15 14 00 00 00 	xor    edx,DWORD PTR gs:0x14
 80484e2:	74 05                	je     80484e9 <main+0x7c>
 80484e4:	e8 47 fe ff ff       	call   8048330 <__stack_chk_fail@plt>
 80484e9:	c9                   	leave  
 80484ea:	c3                   	ret    
 80484eb:	66 90                	xchg   ax,ax
 80484ed:	66 90                	xchg   ax,ax
 80484ef:	90                   	nop
 (... snip ...)

2.3 Dissasembling com gdb

Outra maneira de obter o código dissambly é usando gdb, o gnu debugger, que também faz uma série de outras tarefas que veremos posterior. Para começar, execute o programa no depurador:

felipe@pc:~/binaryanalysis$ gdb helloworld 
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from helloworld...(no debugging symbols found)...done.
(gdb)

Isso imprimirá um aviso de isenção de responsabilidade e deixará você em um terminal gdb. Agora Você pode digitar para desmontar a função principal disassemble main. Para facilitar você pode digitar "alias ds=disassemble".

(gdb) ds main
Dump of assembler code for function main:
   0x0804841d <+0>:	push   ebp
   0x0804841e <+1>:	mov    ebp,esp
   0x08048420 <+3>:	and    esp,0xfffffff0
   0x08048423 <+6>:	sub    esp,0x30
   0x08048426 <+9>:	mov    DWORD PTR [esp+0x1d],0x6c6c6548
   0x0804842e <+17>:	mov    DWORD PTR [esp+0x21],0x57202c6f
   0x08048436 <+25>:	mov    DWORD PTR [esp+0x25],0x646c726f
   0x0804843e <+33>:	mov    WORD PTR [esp+0x29],0xa21
   0x08048445 <+40>:	mov    BYTE PTR [esp+0x2b],0x0
   0x0804844a <+45>:	lea    eax,[esp+0x1d]
   0x0804844e <+49>:	mov    DWORD PTR [esp+0x2c],eax
   0x08048452 <+53>:	jmp    0x804846b <main+78>
   0x08048454 <+55>:	mov    eax,DWORD PTR [esp+0x2c]
   0x08048458 <+59>:	movzx  eax,BYTE PTR [eax]
   0x0804845b <+62>:	movsx  eax,al
   0x0804845e <+65>:	mov    DWORD PTR [esp],eax
   0x08048461 <+68>:	call   0x8048310 <putchar@plt>
   0x08048466 <+73>:	add    DWORD PTR [esp+0x2c],0x1
   0x0804846b <+78>:	mov    eax,DWORD PTR [esp+0x2c]
   0x0804846f <+82>:	movzx  eax,BYTE PTR [eax]
   0x08048472 <+85>:	test   al,al
   0x08048474 <+87>:	jne    0x8048454 <main+55>
   0x08048476 <+89>:	mov    eax,0x0
   0x0804847b <+94>:	leave  
   0x0804847c <+95>:	ret    
End of assembler dump.

Se a saída estiver na sintaxe AT&T, então execute esse comando:

(gdb) set disassembly-flavor intel

Para ter a saída gdb na sintaxe Intel.

Vou trabalhar principalmente com a saída gdb dissambled porque é mais bem formatado. Nossa próxima tarefa é entender o que diabos está acontecendo?!?!

3 x86 o estado do Registro do Processador

3.1 x86 Conjunto de instruções

Vamos começar com um item simples, o que é x86? É um conjunto de instruções de montagem(assembly) – uma linguagem de programação. Podemos reprimir termos x86 de seu byte (como visto na saída objdump) ou em um formato legível humano (como visto na sintaxe Intel ou AT&T).

Você pode ter trabalhado com um conjunto de instruções anteriormente, como MIPS. MIPS tem a propriedade de ser um conjunto de instrumentos RISC, ou Reduce Instruction Set Computing, que tem a vantagem de que todos instruções e argumentos são sempre do mesmo tamanho, 32 bits.

x86 é um conjunto de instruções CISC, ou Complex Instruction Set Computing, e tem a propriedade de que os tamanhos das instruções não são contestados. Eles pode variar 8 bits e 64 bits e mais, dependendo do instrução. Você pode se perguntar, por que diabos qualquer coisa seria projetado desta forma? A resposta é a inércia do mercado e a capacidade de retrocesso. À medida que os chips Intel dominavam o mercado, mais e mais binários era x86.

Hoje, outro conjunto de instruções tornou-se muito relevante: ARM ou Acron Risc Machine. E, como o nome indica, é um conjunto de instruções RISC e, portanto, está trazendo de volta um pouco de sanidade ao mundo do conjunto de instruções. O ARM também é a arquitetura preferida em muitos dispositivos móveis, Portanto, será relevante por algum tempo.

No entanto, não trabalharemos com o ARM nesta classe, apenas x86 e usaremos apenas um conjunto muito pequeno de instruções x86.

3.2 Anatomia de uma Instrução

Uma instrução, no formato legível por humanos, tem o seguinte formato:

operation <dst>, <src>

O nome da operação é o tipo de operação que será realizada. Por exemplo, pode ser add ou mov ou and. O <dst> é onde o resultado será armazenado, o que normalmente é um Registro. O <src> é de onde os dados são lidos para serem operados sobre o qual também pode incluir dados referenciados no <dst>. O <src> é opcional e depende do comando.

Se pegarmos algumas operações do nosso código de exemplo:

0x0804841d <+0>:	push   ebp
0x0804841e <+1>:	mov    ebp,esp
0x08048420 <+3>:	and    esp,0xfffffff0

O primeiro comando push pega um argumento e coloca esse argumento em a pilha, ajustando o ponteiro da pilha. Nesse caso, ele envia o valor do ponteiro base armazenado no registro no pilha ebp. O segundo comando recebe dois argumentos e mov um valor de um local para outro, muito parecido com a atribuição. O segundo move o valor no ponteiro da pilha esp e o salva no comando ponteiro base ebp. Finalmente, o último comando é um bit a bit e operação tem dois argumentos. Ele executa um bitwise no <dst> com o <src> e armazena o resultado em <dst>. Nesse contexto, o comando and alinha o ponteiro da pilha com o menor Valor de 4 bits. O alinhamento de 4 bits é devido a um bug antigo na divisão unidade do processador x86, e então você verá muito essa sequência em assembly.

Vamos dar uma olhada mais de perto nessas instruções novamente em um segundo, Mas antes de fazermos isso, precisamos entender esses registros e como eles são usados com mais detalhes.

3.3 Registradores do Processador

Os registros são espaços de armazenamento especiais no processador que armazenam o estado do programa. Alguns registros são usados para uso geral de armazenamento para armazenar um armazenamento intermediário, enquanto outros são usados para manter a noção do estado de execução, por exemplo, como qual é a próxima instrução.

Aqui estão os registros padrão que você encontrará. Existem alguns outros, mas vamos explicá-los quando nos depararmos com eles:

  • esp: Registro de 32 bits para armazenar o ponteiro da pilha
  • ebp: Registro de 32 bits para armazenar o ponteiro base
  • eax: Registrador de uso geral de 32 bits, às vezes chamado de "acumulador"
  • ecx: Registro de uso geral de 32 bits
  • ebx: Registro de uso geral de 32 bits
  • edx: Registro de uso geral de 32 bits
  • esi: Registradores de uso geral de 32 bits usados principalmente para carregar e armazenar
  • edi: Registradores de uso geral de 32 bits usados principalmente para carregar e armazenar

Cada um dos registros de uso geral pode ser referenciado por um valor completo de 32 bits ou por algum subconjunto disso, como os primeiros 8 bits ou segundo 8 bits. Por exemplo, eax refere-se ao general de 32 bits registradores, mas ax refere-se aos últimos 16 bits do registrador eax e al são os primeiros 8 bits. Dependendo do tipo de dados, o registro está armazenando, podemos fazer referência a diferentes partes.

3.4 O ponteiro base e o ponteiro de pilha

Dois registros serão referenciados mais do que qualquer outro: o de base e o ponteiro de pilha. Esses registros mantêm o estado de referência de memória para a execução atual, com referência ao quadro de função atual. Um quadro de função é parte da memória na pilha que armazena o informações para uma execução de funções atuais, incluindo dados locais e endereços de retorno. O ponteiro base define a parte superior e inferior do quadro de função.

A estrutura de um quadro de função é assim

           <- 4 bytes ->
          .-------------.    
          |    ...      |    higher address
ebp+0x8 ->| func args   |
ebp+0x4 ->| return addr |       
    ebp ->| saved ebp   |
ebp-0x4 ->|             |
   :      :             :              
   '      '             '
            local args
   .      .             .
   :      :             :
esp+0x4 ->|             |
    esp ->|             |    lower addreses
          '-------------'

Movendo-se de endereços mais altos para endereços mais baixos, a parte superior do quadro armazena os argumentos da função. Estes são normalmente referenciados em compensações positivas de registro ebp. Por exemplo, o primeiro argumento é ebp+0x8 movendo para cima dessa posição.

O segundo item no quadro de função é o endereço de retorno em ebp+0x4. O valor armazenado nesta memória é onde a próxima instrução esta após a instrução return, ou qual instrução ocorre após a conclusão da chamada para esta instrução. Vamos gastar MUITO TEMPO falando sobre isso mais tarde.

Finalmente, há o salvo ebp, este é o endereço onde o último ponteiro base para a função de chamada. Precisamos salvar esse valor para que o quadro de pilha da função de chamada pode ser restaurado uma vez que este função seja concluída.

O ponteiro da pilha faz referência à parte inferior da pilha, o mais baixo endereço alocado. Endereços após este ponto são considerados não alocado. No entanto, é muito fácil alocar mais espaço, vamos apenas subtrair do ponteiro da pilha.

3.5 Gerenciando o quadro de pilha e a pilha

Agora que temos algum contexto para os registros, vamos dar uma olhada no primeiro conjunto de instruções em nosso código:

0x0804841d <+0>:	push   ebp
0x0804841e <+1>:	mov    ebp,esp
0x08048420 <+3>:	and    esp,0xfffffff0
0x08048423 <+6>:	sub    esp,0x30

Vamos primeiro analisar as quatro primeiras instruções. A instrução push enviará um valor para a pilha e, neste caso, é o anterior ponteiro base, ou seja, o ponteiro base salvo. Em seguida, o ponteiro base é definido como o ponteiro de pilha () (mov), e, em seguida, alinhado a 4 bits (and). Em seguida, subtrair do ponteiro da pilha aloca o restante do quadro de pilha, que tem 0x30 bytes ou 48 bytes (não se esqueça sobre o hexadecimal).

3.6 Referenciando, desreferenciando e configurando a memória

O próximo conjunto de instruções autoriza a memória da pilha. Vamos voltar para o código C para ver isso em c antes de olharmos para ele em assembly.

char hello[15]="Hello, World!\n";

A cadeia de caracteres "Hello World!\n" é definida na pilha em um array de caracteres de 15 bytes. Na montagem, isso se parece assim.

0x08048426 <+9>:	mov    DWORD PTR [esp+0x1d],0x6c6c6548
0x0804842e <+17>:	mov    DWORD PTR [esp+0x21],0x57202c6f
0x08048436 <+25>:	mov    DWORD PTR [esp+0x25],0x646c726f
0x0804843e <+33>:	mov    WORD PTR [esp+0x29],0xa21
0x08048445 <+40>:	mov    BYTE PTR [esp+0x2b],0x0

Se você apertar os olhos em <src> para os operadores, reconhecerá que isso é ASCII. Se você não acredita, confira a tabela ASCII. O DWORD ou WORD ou BYTE PTR são comandos de deferência.

  • BYTE PTR[addr] : ponteiro de byte: desreferenciar um byte no endereço
  • WORD PTR[addr] : ponteiro de palavra: desreferenciar os dois bytes no endereço
  • DWORD PTR[addr] : ponteiro de palavra duplo : desreferenciar os quatro bytes no endereço

Outra maneira de ver essas instruções em C seria assim (Não programe assim, no entanto):

char hello[15];
//                      l l e H  
* ((int *) (hello)) = 0x6c6c6548;      // set hello[0]->hello[3]
//                          W   , o
* ((int *) (hello + 4)) = 0x57202c6f; // set hello[4]->hello[7]
//                          d l r o      
* ((int *) (hello + 8)) = 0x646c726f; // set hello[8]->hello[11]
//                             \n !
* ((short *) (hello + 12)) = 0x0a21;  // set hello[12]->hello[13]
//                         \0
* ((char *) (hello+14)) = 0x00;  // set hello[14]

As próximas duas instruções são um pouco diferentes:

0x0804844a <+45>:	lea    eax,[esp+0x1d]
0x0804844e <+49>:	mov    DWORD PTR [esp+0x2c],eax

lea significa load effective address e é um atalho para fazer um pouco de matemática e calcular um deslocamento do ponteiro e armazená-lo. Se olharmos no que vem a seguir no programa C, vemos que ele está configurando o for-loop.

for(p = hello; *p; p++){

A primeira parte do loop for está inicializando o ponteiro p para referência ao início da string hello. A partir do código anterior, o início da string hello está no deslocamento de endereço esp+0x1d e queremos para definir esse endereço com o valor de p. Este é um processo de duas etapas:

  1. O endereço real deve ser calculado usando adição de esp e armazenado. lea eax,[esp+0x1d] calculará o endereço e armazenará emeax.
  2. O valor em eax deve ser armazenado na memória reservada para p, que está no endereço esp+0x2c, o comando move faz isso.

Neste ponto, tudo está configurado. E para referência, lembre-se de que o endereço de p está em esp+0x2c.

3.7 Loops, Saltos e Testes de condição

Agora, chegamos ao cerne do programa: o loop interno. Nós podemos seguir a execução neste ponto seguindo os saltos.

0x08048452 <+53>: jmp    0x804846b <main+78>      # -----------.
0x08048454 <+55>: mov    eax,DWORD PTR [esp+0x2c] # <-------.  |
0x08048458 <+59>: movzx  eax,BYTE PTR [eax]       #         |  |
0x0804845b <+62>: movsx  eax,al                   #         |  |
0x0804845e <+65>: mov    DWORD PTR [esp],eax      #         |  |  //loop body
0x08048461 <+68>: call   0x8048310 <putchar@plt>  #         |  |
0x08048466 <+73>: add    DWORD PTR [esp+0x2c],0x1 #         |  |
0x0804846b <+78>: mov    eax,DWORD PTR [esp+0x2c] # <-------+--'
0x0804846f <+82>: movzx  eax,BYTE PTR [eax]       #         |    //exit condition
0x08048472 <+85>: test   al,al                    #         |
0x08048474 <+87>: jne    0x8048454 <main+55>      #  -------'

A instrução jmp altera o ponteiro de instrução para o destino especificado. Não é condicionado, é um salto forçado explícito. Seguindo esse salto no código, encontramos as três instruções a seguir:

0x0804846f <+82>: movzx  eax,BYTE PTR [eax]       
0x08048472 <+85>: test   al,al                    
0x08048474 <+87>: jne    0x8048454 <main+55>

Mais fácil começar com a instrução movzx. Lembre-se de que neste ponto no código, eax tem o valor que é o mesmo valor dep. E você pode ver que é o caso na instrução anterior mov eax,DWORD PTR [esp+0x2c] onde esp+0x2c é o endereço de memória para p.

A instrução movzx irá deferenciar o endereço armazenado em eax que é o que referencia p, ler um byte nesse endereço e escrever nos 8 bits inferiores de eax. Esta é essencialmente *p a operação que é algum caractere em hello, e então o que queremos testar é se p referência ao NULL no final de hello.

Esse teste ocorre test al,al no qual compara ao registrador em diferentes modos. Aqui estamos testando o registrador al que é o mais baixo dos 8 bits de eax, aonde nos armazenamos a deferencia de p. Os resultados de teste, maior que, menor que, igual, não zero, etc. são armazenado em sets de bit flag. Aquele com o qual nos preocupamos é a flag ZF flag ou a zero flag. Se al é zero então ZF é setado para 1 que seria o caso quando preferência o final da string hello.

O comando jne diz pular quando não for igual a zero.Se for o caso que al é zero, não pule, caso contrário, continue para o endereço e continue o loop.

3.8 Chamadas de Função

Se investigarmos o corpo do loop, encontraremos as seguintes instruções:

0x08048454 <+55>: mov    eax,DWORD PTR [esp+0x2c] 
0x08048458 <+59>: movzx  eax,BYTE PTR [eax]       
0x0804845b <+62>: movsx  eax,al                   
0x0804845e <+65>: mov    DWORD PTR [esp],eax      
0x08048461 <+68>: call   0x8048310 <putchar@plt>

O primeiro conjunto de instruções, muito parecido com o teste anterior, é deferência do ponteiro p.

  1. Carregar o valor p, um endereço de memória, em eax
  2. Leia o byte referenciado em p nos 8 bits inferiores de eax
  3. zerar os bits restantes de eax deixar apenas 8 bits inferiores

Neste ponto, eax armazena um valor como 0x0000048 (i.e, 'H') onde o byte mais baixo é o caractere de interesse e os bytes restantes são 0.

Esse valor é então gravado na parte superior da pilha, conforme referenciado por, esp porque estamos prestes a fazer uma chamada de função. Os argumentos para pushed as funções são enviadas para a pilha antes de uma chamada. Neste caso, alocou esse espaço de pilha com antecedência para que não precisemos empurrar, mas o argumento está no lugar certo, no topo da pilha.

A próxima operação é uma call que executará a função putchar, convenientemente nos disse pelo gdb. Uma vez que a função seja concluída, a execução continuará até o ponto logo após o call, que é a instrução add.

0x08048466 <+73>: add    DWORD PTR [esp+0x2c],0x1

Olhando atentamente para esta instrução, você vê que isso aumentará o ponteiro p, e as instruções após o teste de tempo p agora referência zero. E o loop continua … à medida que o mundo gira.