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

Home

Aula 01: C Revisão

Conteúdo

1 Hello World

Vamos começar do começo: Hello World!

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char * argv[]){

  printf("Hello World!\n");

  return 0;
}

Este programa imprime "Hello World!" fazendo uma chamada para a função da biblioteca printf(). Além disso, observe que main() em programas C levam dois argumentos:

  • int argc : o número de argumentos de linha de comando (sempre pelo menos 1)
  • char * argv[] : uma matriz de strings(array) terminada em NULL para os argumentos de linha de comando.

Voltaremos aos argumentos da função main()mais tarde, quando discutirmos matrizes e strings. O último item a ser observado da função main() é que ele tem um valor de retorno, ou seja, 0. Esse valor retornado também é o valor de saída do programa. É comum que os programas retornem o valor 0 quando são bem sucedidos enquanto aqueles que não o são retornem outros valores, tipicamente 1 ou 2 dependendo do erro.

Finalmente, observe que os dois incluem: stdio.h stdlib.h. Estes são os arquivos de cabeçalho (header files) para partes da lib padrão c (muitas vezes referido como como clib). stdio.h refere-se à entrada e saída padrão c, e stdlib refere-se às funções da biblioteca padrão c.

Como veremos abaixo, por padrão para todos os programas c, libc está incluído, mas os cabeçalhos descrevem quais partes da biblioteca de funções estára em uso. Os arquivos de cabeçalho (header files) contêm as definições das funções que estarão em uso, por exemplo printf(), para que o compilador saiba quais tipos de cheques a função chamará.

1.1 Processo de compilação simples

Usaremos o gcc (compilador GNU C) exclusivamente para fazer compilações para programas c. A maneira mais direta de usar gcc é só chamar com o source do programa como argumento.

felipe@pc:~/binaryanalysis$ gcc helloworld.c 
felipe@pc:~/binaryanalysis$ ls
a.out  helloworld.c
felipe@pc:~/binaryanalysis$ ./a.out 
Hello World!

Isso produz um arquivo binário de saída chamado a.out que podemos executar para receber a mensagem "Hello World!". Se quisermos compilar o programa para um nome de arquivo específico, digamos helloworld, então usamos a opção -o para especificar o nome do arquivo de saída.

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

1.2 Processo de compilação em várias etapas

Na verdade, existe uma grande parte obscura do processo de compilação que realmente envolve várias etapas. Um programa de origem passa por dois estágios antes de se tornar um executável binário.

Primeiro, o código-fonte deve ser compilado em código objeto, que é uma representação intermediária do arquivo de origem. Isso é chamado de compilação porque há uma transformação literal de um código-fonte para outro código-fonte. O arquivo de objeto contém o arquivo compilado em instruções de nível de máquina (por exemplo, assembly x86), mas não é executável ainda porque deve ser montado (assembled) e vinculado (linked) corretamente com algumas outras fontes de código (por exemplo, código da biblioteca padrão C) para que ele possa realmente ser executado na máquina de destino específica.

Para ver como isso funciona, vamos dar uma olhada no programa hello world de vários arquivos. Em um arquivo (abaixo) temos a função main() que chama duas outros funções hello() e world(), mas apenas hello() é fornecido na source do programa. Ambas as funções têm definições, ou seja, o tipos de sua entrada é conhecido, mas não o código para ambas as funções.

#include <stdio.h>
#include <stdlib.h>

void world(void);
void hello(void);

void hello(){
  printf("Hello ");
}

int main(int argc, char *argv[]){
  hello();
  world();
}

Se tentarmos compilar este programa, obteremos um erro.

felipe@pc:~/binaryanalysis$ gcc -o hello hello.c
/usr/bin/ld: /tmp/ccmeqVzy.o: in function `main':
hello.c:(.text+0x38): undefined reference to `world'
collect2: error: ld returned 1 exit status

Olhando atentamente para o erro, vemos que na verdade não é o gcc que está imprimindo um erro, mas sim, ld. Isso ocorre porque o programa está realmente compilado, mas não montado (assembled) corretamente. ld o linker GNU não foi capaz de encontrar a referência (ou código) world() e falhou ao vincular o código à source executável e então nada foi montado (assembled).

Você pode ver, que sim, este programa realmente foi compilado usando a opção -c com gcc, que diz para compilar a source para um arquivo objeto:

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

Isso é bem-sucedido, e agora temos um arquivo de objeto para hello e precisamos para fornecer mais código compilado para concluir o processo de montagem. Especificamente, precisamos fornecer um código que preencha a função world().

#include <stdio.h>
#include <stdlib.h>

void world( ){

  printf("World!\n");
}

Uma vez que tenhamos isso, podemos compilar world.c em world.o e podemos montar os dois arquivos .o em um único executável.

felipe@pc:~/binaryanalysis$ gcc -c -o world.o world.c
felipe@pc:~/binaryanalysis$ gcc -o hello hello.o world.o
felipe@pc:~/binaryanalysis$ ./hello 
Hello World!

No entanto, como você verá muitas vezes nesta aula. Ainda há mais acontecendo abaixo da superfície. Ainda há mais código que está sendo usado no processo de montagem. E podemos usar o ld diretamente para seja feita vinculação final e expor todas essas partes.

ld -o hello hello.o world.o --dynamic-linker /lib/ld-linux.so.2 /usr/lib/i386-linux-gnu/crt1.o /usr/lib/i386-linux-gnu/crti.o -lc /usr/lib/i386-linux-gnu/crtn.o

A compilação, na verdade, requer três outros arquivos de objeto crt1.o crti.o e crtn.o e também uma biblioteca vinculada dinamicamente ld-linkux.so.2 para realmente montar o código. Esses arquivos de objeto fornecem blocos de código iniciais e finais importantes e outras funções que se tornarão relevantes quando começarmos a fazer engenharia reversa de alguns software.

2 Library Functions vs. System Calls

Se você olhar mais de perto o ld a linha de comando acima , você também verá a flag -lc que diz para incluir clib na compilação. A biblioteca padrão c fornece muitas funcionalidades para o programador, mas sua principal tarefa é fornecer uma interface por que o programador possa acessar facilmente a interace subjacente do sistema operacional.

Lembre-se de que uma chamada de sistema(system call) é um mecanismo para o programador ganhar acesso a um recurso do sistema operacional. O sistema operacional fornece alguns recursos importantes relevantes para esta classe:

  • Input/Output : leitura e gravação de dispositivos, como o terminal, rede e outros periféricos.
  • Memory Management : manter a memória para programas e garantir que os programas não acessem memória inválida, isso inclui manter o layout de memória virtual para um programa.
  • Program Execution: carregar e descarregar programas e executar eles na CPU.

2.1 Função de rastreamento e chamadas do sistema(syscall)

Uma função de biblioteca, por outro lado, fornece um ambiente com uma interface mais amigável para as chamadas do sistema (syscall). Para ver essa dinâmica, podemos dar um trace na execução de programas e ver onde estão as funções das biblioteca do sistema. Existem dois rastreadores que usaremos muito nesta classe:

  • ltrace rastrear funções das bibliotecas.
  • strace rastrear chamadas do sistema..

E podemos olhar para a saída desses rastreadores para ter uma noção de como programas executam.

felipe@pc:~/binaryanalysis$ ltrace ./hello > /dev/null 
__libc_start_main(0x8048304, 1, 0xbffa7744, 0x8048360 <unfinished ...>
printf("Hello ")                                                                  = 6
puts("World!")                                                                    = 7
+++ exited (status 7) +++

No ltrace acima, vemos que para hello o programa da seção anterior, há duas chamadas de biblioteca: printf() e puts(). Se você olhar no manual, puts() printf() ambos pegam uma string e a gravam em stdout. No entanto, sabemos que que o método real para stdout é usar write() uma system call, e podemos ver isso com o strace.

felipe@pc:~/binaryanalysis$ strace ./hello > /dev/null 
execve("./hello", ["./hello"], [/* 20 vars */]) = 0
brk(0)                                  = 0x87b7000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7760000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=70286, ...}) = 0
mmap2(NULL, 70286, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb774e000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\340\233\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1754876, ...}) = 0
mmap2(NULL, 1759868, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb75a0000
mmap2(0xb7748000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a8000) = 0xb7748000
mmap2(0xb774b000, 10876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb774b000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb759f000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb759f940, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xb7748000, 8192, PROT_READ)   = 0
mprotect(0xb7783000, 4096, PROT_READ)   = 0
munmap(0xb774e000, 70286)               = 0
fstat64(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0xbf8187d8) = -1 ENOTTY (Inappropriate ioctl for device)
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb775f000
write(1, "Hello World!\n", 13)          = 13
exit_group(7)                           = ?
+++ exited with 7 +++

Na verdade, há muitas chamadas de sistema envolvidas aqui. Começando no topo, temos o sistema chamando execve() isso executa o programa, mas depois disso, há muito carregamento e leituras bibliotecas na memória. E, finalmente, subindo duas linhas de baixo para cima, podemos ver, write() system call para stdout (file descriptor 1).

2.2 Hello System Call

Podemos, é claro, escrever um programa hello-world sem nenhuma função de biblioteca. Mas, precisaremos de algumas funções auxiliares, como escrever nossas próprias função de comprimento de string.

#include <unistd.h>


int mystrlen(char * str){

  int i;
  for(i=0; str[i]; i++);

  return i;

}


int main(int argc, char *argv[]){

  char str[] = "Hello World!\n";

  write(1,str,mystrlen(str));

}

Compilando e executando este programa e analisando ltrace, nós ainda vemos uma chamada para write() porém nenhuma para puts() ou printf().

felipe@pc:~/binaryanalysis$ ltrace ./hellosystem > /dev/null 
__libc_start_main(0x8048494, 1, 0xbf8c5034, 0x8048510 <unfinished ...>
write(1, "Hello World!\n", 13)                                                                                                    = 13
+++ exited (status 13) +++

A razão para quewrite() ainda aparece é que write() não é a syscal real de write(), mas é um wrapper de biblioteca para ela … Mas isso é uma história para outro dia. O que é mais interessante é o strace, que se você observar de perto, verá que é o mesmo como a outra versão do programa.

felipe@pc:~/binaryanalysis$ strace ./hellosystem > /dev/null 
execve("./hellosystem", ["./hellosystem"], [/* 20 vars */]) = 0
brk(0)                                  = 0x8c9b000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77c3000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=70286, ...}) = 0
mmap2(NULL, 70286, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb77b1000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\340\233\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1754876, ...}) = 0
mmap2(NULL, 1759868, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7603000
mmap2(0xb77ab000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a8000) = 0xb77ab000
mmap2(0xb77ae000, 10876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb77ae000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7602000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb7602940, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xb77ab000, 8192, PROT_READ)   = 0
mprotect(0x8049000, 4096, PROT_READ)    = 0
mprotect(0xb77e6000, 4096, PROT_READ)   = 0
munmap(0xb77b1000, 70286)               = 0
write(1, "Hello World!\n", 13)          = 13
exit_group(13)                          = ?
+++ exited with 13 +++