Codando em Assembly (plus)
Entender como codar em Assembly pode parecer maluquice. Mas há ataques específicos, como o Buffer Overflow - Shellcode, que precisam que um código em assembly, mesmo que simples, seja escrito para que o ataque tenha sucesso.
Esta seção é um plus, então se quiser pular, fique à vontade, pois em Buffer Overflow - Shellcode também explico (mas apenas o básico) para codar um payload (carga útil maliciosa) em assembly. Utilize essa guia como um complemento.
Ferramentas
Para codar em assembly, primeiro precisamos baixar as seguintes ferramentas:
sudo apt install nasm gdb gcc build-essential
nasm --version # Assembler
ld --version # Linker
gdb --version # Debugger
Seções
As seções organizam e separam o código em variáveis inicializadas, não inicializadas, instruções e dados somente leitura.
Seção .data
Essa seção do programa contém dados inicializados que podem ser modificados (variáveis). Os valores iniciais são definidos no código fonte. Isso aumenta o tamanho do executável, e posteriormente isso também será carregado na memória RAM. É modificável, não são valores constantes.
section .data
; Variáveis com valores pré-definidos
idade db 25 ; byte (db = 1 byte) = 25
altura dw 180 ; word (dw = 2 bytes) = 180
salario dd 500000 ; dword (dd = 4 bytes) = 500000
preco dq 299900 ; qword (dq = 8 bytes) = 299900
pi dt 3.1415926535 ; tbyte (dt = 10 bytes)
; Strings (terminadas ou não)
nome db 'João Silva', 0 ; String terminada em null (\0)
msg db 'Olá', 0xA, 0 ; String com newline e null (\n\0)
buffer times 64 db 0 ; 64 bytes com valor 0 (cada 0 ocupa 1 byte por causa de db)
Quanto colocamos dado1, dado2, isso cria um "array": os dados ficam colados na memória. Assim, hello db "Hello", 0 cria uma string terminada em \0.
Seção .bss (block started by symbol)
Essa seção do programa contém dados não inicializados previamente, mas que podem ser modificados (variáveis). Reserva espaço sem definir valor inicial e não ocupa espaço no executável, só na memória RAM. Sempre inicia com zeros.
section .bss
input resb 100 ; Reserva 100 bytes
array resw 50 ; Reserva 50 words (100 bytes)
matriz resd 10*10 ; Reserva 100 dwords (400 bytes)
ponteiro resq 1 ; Reserva 1 qword (8 bytes)
; 'res' = reserve
; b = byte, w = word, d = dword, q = qword
Seção .text
Essa seção possui código executável, ou seja, as instruções do programa.
section .text
global _start ; Ponto de entrada (Entry Point), onde começa a execução do programa
_start:
; Instruções do programa
; ENDEREÇOS RELATIVOS aqui são calculados em tempo de linkagem
Endereços relativos aparecem em instruções como mov rsi, mensagem, sendo mensagem um endereço relativo declarado em section .data. A existência de endereços relativos existe por causa da organização de fases de compilação do Assembly.
Fase 1: Assembly (nasm)
Endereços relativos:
mensagem: 0x00000000 (relativo ao início da seção .data)
_start: 0x00000000 (relativo ao início da seção .text)
Código .o gerado contém:
"mov rsi, [0x00000000]" ← Placeholder!
Fase 2: Linkagem (ld)
O Linker decide onde cada seção vai na memória (.data, .text, etc) e corrige os endereços relativos.
.text: começa em 0x400000
.data: começa em 0x600000
mensagem = 0x600000
_start = 0x400000
Substitui "mov rsi, [0x00000000]" por
"mov rsi, [0x600000]"
Seção .rodata
Essa seção possui dados somente leitura (opcional). Uso para constantes que não devem ser modificadas
section .rodata
const_pi dq 3.141592653589793
mensagem_const db 'Erro fatal!', 0
Structs
O modo mais simples de declarar structs é na seção .data ou .bss.
section .bss
Pessoa:
.nome resb 32 ; 32 bytes para nome
.idade resw 1 ; 2 bytes para idade
.altura resd 1 ; 4 bytes para altura (em cm)
section .text
_start:
; 1. Obter endereço base
lea rbx, [Pessoa]
; 2. Acessar campos:
mov byte [rbx + 0], 'A' ; Pessoa.nome[0] = 'A'
mov word [rbx + 32], 25 ; Pessoa.idade = 25
mov dword [rbx + 34], 180 ; Pessoa.altura = 180
Arrays e Strings
Em conceito de baixo nível, arrays e strings são a mesma coisa: dados sucessivos na memória que estão unidos em uma variável só. Um array pode ser um conjunto de inteiros, decimais, etc, enquanto uma string sempre é um conjunto de inteiros que representam código ASCII.
Arrays estáticos:
section .data
; Array de bytes
letras db 'A', 'B', 'C', 'D', 'E'
letras2 db 'ABCDE' ; Mesmo resultado
letras3 db 65, 66, 67, 68, 69 ; Valores ASCII
; Array de words (2 bytes cada)
numeros dw 100, 200, 300, 400, 500
; Strings
str1 db 'Hello', 0 ; C-style string (termina com \0)
str2 db 'World', 0xA, 0 ; Com newline (\n\0)
; String multi-linha
texto db 'Esta é uma string', 0xA
db 'que ocupa múltiplas', 0xA
db 'linhas no código.', 0xA, 0
Arrays dinâmicos:
section .bss
; Array de 100 inteiros (4 bytes cada)
vetor resd 100 ; 400 bytes
Acesso a elementos do array:
section .data
array dw 10, 20, 30, 40, 50
section .text
_start:
; Acessar array[0] (primeiro elemento)
mov ax, [array] ; ax = 10
; Acessar array[2] (terceiro elemento)
; Cada elemento tem 2 bytes (word)
mov bx, [array + 2*2] ; bx = 30
Para percorrer array com loop:
section .data
array dw 10, 20, 30, 40, 50
section .text
_start:
; Percorrer array com loop
mov rcx, 5 ; 5 elementos
mov rsi, 0 ; índice
mov rbx, array ; endereço base
loop_array:
mov ax, [rbx + rsi*2] ; acessa array[i]
; ... fazer algo com ax ...
inc rsi
loop loop_array
OBS: Para fazer um loop simples:
mov rcx, 10 ; RCX = contador
loop_start:
; Código do loop aqui
loop loop_start ; RCX--, salta se RCX != 0
Strings
section .data
section .bss
; Buffer para input
input_buffer resb 1024
equ vs variável
A instrução equ do Assembly é uma substituição textual na compilação do nasm (uma constante), de modo que não é criada uma variável que ocupa memória, mas toda parte do código com tal rótulo terá seu valor substituído.
section .data
; equ: NÃO ocupa memória, só é substituição textual
TAMANHO equ 100 ; NASM substitui TAMANHO por 100
; variável: OCUPA memória
tamanho_var db 100 ; 1 byte na memória com valor 100
Medindo tamanhos de dados
Podemos usar o truque TAMANHO eq ($ - dado). Isso tem que ser utilizado obrigatoriamente na linha seguinte à declaração do dado. Isso pois $ obtém a posição atual do contador do assembler, e dado seria a posição do dado. Imagine que o contador do assembly fica, internamente, armazenando o espaço para cada dado conforme o tamanho que você definiu. Assim, atual - dado = tamanho_dado.
section .data
msg db 'Hello', 0xA
len equ $ - msg ; NASM calcula
Syscalls
Syscalls são interfaces de fácil uso que permitem que programas solicitem serviços do kernel do sistema operacional, como acesso a hardware, criação de processos, gerenciamento de arquivos, etc. Para fazer uma syscall damos o número da syscall (operação que queremos), os argumentos e em seguida usamos o comando syscall (x64) / int 0x80 (x86), como vemos abaixo.
;x86-64
mov rax, syscall_number ; Número da syscall
mov rdi, arg1 ; Primeiro argumento
mov rsi, arg2 ; Segundo argumento
mov rdx, arg3 ; Terceiro argumento
mov r10, rcx ; Quarto argumento (rcx não usado)
mov r8, r8 ; Quinto argumento
mov r9, r9 ; Sexto argumento
syscall ; Instrução para chamar o kernel
; x86
mov eax, syscall_number ; Número da syscall
mov ebx, arg1 ; Primeiro argumento
mov ecx, arg2 ; Segundo argumento
mov edx, arg3 ; Terceiro argumento
int 0x80 ; Interrupção para chamar o kernel
Achei mais conveniente abordar melhor syscalls em Buffer Overflow - Shellcode, para já conectar com o ataque a ser feito. Porém, syscalls são basicamente funções que o Sistema Operacional possui, facilitanto o uso de recursos do sistema (escrever, ler, conectar, sair, etc).
Para ver cada syscall, seu código, parâmetros e o que faz, recomendo olhar a Tabela de Syscalls para kernel Linux.
Outros materiais também estão disponíveis na internet:
- Tabela de Syscalls Linux x86 - IME USP
- Tabela de Syscalls Linux x86-64 - Fillipo
- Tabela de Syscalls Linux x86-64 - Rchapman
Executando código assembly
Para executar um código em assembly:
- Assemblar (cria objeto):
nasm -f elf64 programa.asm -o programa.o - Linkar (cria executável):
ld programa.o -o programa - Executar:
./programa - Opção All-in-One:
nasm -f elf64 programa.asm && ld programa.o -o programa && ./programa
Tópico Adicional: Diretivas Assembly
Para compilar um código em assembly em código de máquina, temos o assembler. Dado isto, temos as diretivas no código assembly, que não existem no executável final. Elas servem para guiar o Assembler na compilação do código:
Assembly Source (.s) → Assembler → Object File (.o) → Linker → Executable (.exe)
↓ ↓ ↓ ↓ ↓
[DIRETIVAS] [PROCESSADAS] [REMOVIDAS] [REMOVIDAS] [CÓDIGO PURO]
Abaixo temos uma lista de diretivas para NASM, e o que elas fazem:
; Diretivas de Dados/Strings
db 0x48, 0x31, 0xc0 ; 3 bytes: 48 31 c0
db 'A','B','C' ; Bytes ASCII: 41 42 43
dw 0x1234 ; 2 bytes: 34 12 (little-endian)
dw 255 ; 2 bytes: FF 00
dd 0x12345678 ; 4 bytes: 78 56 34 12
dd 0x41414141 ; 4 bytes: 41 41 41 41
dq 0x4141414141414141 ; 8 bytes de 'A'
dq 0x1234567890ABCDEF ; 8 bytes qualquer
db "Hello" ; 48 65 6c 6c 6f (sem \0)
db "flag.txt", 0 ; 66 6c 61 67 2e 74 78 74 00
db "Hello", 0 ; 48 65 6c 6c 6f 00
; Diretivas de Arquivos/Inclusão
%include "other.inc" ; Inclui código (NASM usa %)
incbin "file.bin" ; Insere bytes brutos
; Diretivas de Seções/Organização
section .text ; Seção de código
section .data ; Seção de dados
section .rodata ; Seção de dados somente leitura
section .bss ; Seção de dados não inicializados
global _start ; _start visível
global main ; main visível
; Diretivas de Alinhamento/Tamanho
align 4 ; Alinha para múltiplo de 4
align 8 ; Alinha para múltiplo de 8
times 100 db 0 ; 100 bytes de 0x00 (não .skip!)
times 50 db 0x41 ; 50 bytes de 'A' (não .space!)
times 64 db 0 ; 64 bytes de 0x00 (não .zero!)
; Diretivas de Repetição
times 10 db 0x90 ; 10 bytes de NOP (não .rept!)
%rep 10 ; %rep é NASM
db 0x90
%endrep
; Diretivas de labels/Símbolos
%define BUFFER_SIZE 100
%define SYS_EXIT 60
%macro exit 0-1 0
mov rax, 60
mov rdi, %1
syscall
%endmacro
; Condicionais
%if 1 == 1
db 0x41
%else
db 0x42
%endif
; Definir endereço atual
org 0x400000 ; org funciona em NASM binário
; Declarar variável comum
; Em NASM, na seção .bss:
; variavel resb 100
Tópico adicional: Analisando assembly compilado
Vamos compilar o seguinte código:
section .text
global _start
_start:
mov rax, 1 ; rax = 1
db 0x48, 0x31, 0xc0 ; Insere bytes que na verdade são uma instrução (xor rax, rax)
jmp .skip_string ; Não queremos que o ASCII abaixo seja interpretado como instrução
db "TEST" ; Insere string ("TEST" = 0x54 0x45 0x53 0x54)
.skip_string:
; Encerra o programa (exit(60))
mov rdi, 0
mov rax, 60
syscall
Após compilar e linkar o programa com nasm -f elf64 test.asm && ld test.o -o test && ./test. Agora vamos analisar o que foi gerado!
Executável completo
se utilizarmos o comando hexdump -C ./test veremos os bytes + ASCII do programa compilado. Agora vamos analisar o executável em bytes.
$ hexdump -C ./test
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 00 10 40 00 00 00 00 00 |..>.......@.....|
00000020 40 00 00 00 00 00 00 00 20 11 00 00 00 00 00 00 |@....... .......|
00000030 00 00 00 00 40 00 38 00 02 00 40 00 05 00 04 00 |....@.8...@.....|
00000040 01 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 |................|
00000050 00 00 40 00 00 00 00 00 00 00 40 00 00 00 00 00 |..@.......@.....|
00000060 b0 00 00 00 00 00 00 00 b0 00 00 00 00 00 00 00 |................|
00000070 00 10 00 00 00 00 00 00 01 00 00 00 05 00 00 00 |................|
00000080 00 10 00 00 00 00 00 00 00 10 40 00 00 00 00 00 |..........@.....|
00000090 00 10 40 00 00 00 00 00 1a 00 00 00 00 00 00 00 |..@.............|
000000a0 1a 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 |................|
000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00001000 b8 01 00 00 00 48 31 c0 eb 04 54 45 53 54 bf 00 |.....H1...TEST..|
00001010 00 00 00 b8 3c 00 00 00 0f 05 00 00 00 00 00 00 |....<...........|
00001020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00001030 00 00 00 00 00 00 00 00 01 00 00 00 04 00 f1 ff |................|
00001040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00001050 0a 00 00 00 00 00 01 00 0e 10 40 00 00 00 00 00 |..........@.....|
00001060 00 00 00 00 00 00 00 00 22 00 00 00 10 00 01 00 |........".......|
00001070 00 10 40 00 00 00 00 00 00 00 00 00 00 00 00 00 |..@.............|
00001080 1d 00 00 00 10 00 01 00 00 20 40 00 00 00 00 00 |......... @.....|
00001090 00 00 00 00 00 00 00 00 29 00 00 00 10 00 01 00 |........).......|
000010a0 00 20 40 00 00 00 00 00 00 00 00 00 00 00 00 00 |. @.............|
000010b0 30 00 00 00 10 00 01 00 00 20 40 00 00 00 00 00 |0........ @.....|
000010c0 00 00 00 00 00 00 00 00 00 74 65 73 74 2e 61 73 |.........test.as|
000010d0 6d 00 5f 73 74 61 72 74 2e 73 6b 69 70 5f 73 74 |m._start.skip_st|
000010e0 72 69 6e 67 00 5f 5f 62 73 73 5f 73 74 61 72 74 |ring.__bss_start|
000010f0 00 5f 65 64 61 74 61 00 5f 65 6e 64 00 00 2e 73 |._edata._end...s|
00001100 79 6d 74 61 62 00 2e 73 74 72 74 61 62 00 2e 73 |ymtab..strtab..s|
00001110 68 73 74 72 74 61 62 00 2e 74 65 78 74 00 00 00 |hstrtab..text...|
00001120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00001160 1b 00 00 00 01 00 00 00 06 00 00 00 00 00 00 00 |................|
00001170 00 10 40 00 00 00 00 00 00 10 00 00 00 00 00 00 |..@.............|
00001180 1a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00001190 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000011a0 01 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 |................|
000011b0 00 00 00 00 00 00 00 00 20 10 00 00 00 00 00 00 |........ .......|
000011c0 a8 00 00 00 00 00 00 00 03 00 00 00 03 00 00 00 |................|
000011d0 08 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00 |................|
000011e0 09 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 |................|
000011f0 00 00 00 00 00 00 00 00 c8 10 00 00 00 00 00 00 |................|
00001200 35 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |5...............|
00001210 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00001220 11 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 |................|
00001230 00 00 00 00 00 00 00 00 fd 10 00 00 00 00 00 00 |................|
00001240 21 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |!...............|
00001250 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00001260
Cabeçalho
Você viu o .ELF no começo? Isso representa que o tipo de arquivo é .ELF. os bytes que representam isso são chamados de magic numbers.
Temos um cabeçalho:
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 00 10 40 00 00 00 00 00 |..>.......@.....|
00000020 40 00 00 00 00 00 00 00 20 11 00 00 00 00 00 00 |@....... .......|
00000030 00 00 00 00 40 00 38 00 02 00 40 00 05 00 04 00 |....@.8...@.....|
7F 45 4C 46= Assinatura mágica "ELF" (0x7F 'E' 'L' 'F')02= 64-bit (1 seria 32-bit)01= Little-endian01= ELF version 100 10 40 00= Endereço de entrada: 0x401000 (onde seu código começa!)40 00= Tamanho do cabeçalho: 64 bytes20 11 00 00= Tamanho total do arquivo: 0x1120 bytes
Seção de código
Depois, temos a seção de código, que fica clara pois tem a string TEST que usamos no código original:
00001000 b8 01 00 00 00 48 31 c0 eb 04 54 45 53 54 bf 00 |.....H1...TEST..|
00001010 00 00 00 b8 3c 00 00 00 0f 05 00 00 00 00 00 00 |....<...........|
b8 01 00 00 00= mov eax, 1 (mov rax, 1)48 31 c0= xor rax, rax (db 0x48, 0x31, 0xc0 vira instrução!)eb 04= jmp +4 (pula 4 bytes)54= 'T' (parte de "TEST")45= 'E'53= 'S'54= 'T'bf 00 00 00 00= mov edi, 0 (mov rdi, 0)b8 3c 00 00 00= mov eax, 0x3c (mov rax, 60)0f 05= syscall
Symbol table
Em seguida, temos a tabela de símbolos, que são os nomes utilizados em diretivas, funções, etc, ao longo do arquivo.
000010c0 00 00 00 00 00 00 00 00 00 74 65 73 74 2e 61 73 |.........test.as|
000010d0 6d 00 5f 73 74 61 72 74 2e 73 6b 69 70 5f 73 74 |m._start.skip_st|
000010e0 72 69 6e 67 00 5f 5f 62 73 73 5f 73 74 61 72 74 |ring.__bss_start|
000010f0 00 5f 65 64 61 74 61 00 5f 65 6e 64 00 00 2e 73 |._edata._end...s|
test.asm- Nome do arquivo fonte_start- Seu ponto de entradaskip_string- Seu label__bss_start,_edata,_end- Símbolos padrão do linker
Seções do programa
Aqui são descritos ondee cada seção está localizada no arquivo.
00001160 1b 00 00 00 01 00 00 00 06 00 00 00 00 00 00 00 |................|
00001170 00 10 40 00 00 00 00 00 00 10 00 00 00 00 00 00 |..@.............|
Analisando melhor
Para ver apenas o código em assembly (section .text): objdump -D ./test.
objdump -D ./test
./test: file format elf64-x86-64
Disassembly of section .text:
0000000000401000 <_start>:
401000: b8 01 00 00 00 mov $0x1,%eax
401005: 48 31 c0 xor %rax,%rax
401008: eb 04 jmp 40100e <_start.skip_string>
40100a: 54 push %rsp
40100b: 45 53 rex.RB push %r11
40100d: 54 push %rsp
000000000040100e <_start.skip_string>:
40100e: bf 00 00 00 00 mov $0x0,%edi
401013: b8 3c 00 00 00 mov $0x3c,%eax
401018: 0f 05 syscall
Isso facilita analisar o código em assembly, identificando os bytes como instruções.
Também podemos usar o readelf para analisar o cabeçalho e a tabela de seções, assim como qualquer parte do código compilado:
# Ver o cabeçalho detalhado
readelf -h test
# Ver tabela de seções
readelf -S test
# Outros
readelf --help
Você pode utilizar o readelf tanto no objeto quanto no programa final.