Simple Shellcode Tale!

Posted on Fri, Jun 4, 2021 osce binary-exploitation shellcoding

Ambiente no qual o artigo foi baseado!

diego> uname -a
Linux kali 5.10.0-kali7-amd64 #1 SMP Debian 5.10.28-1kali1 (2021-04-12) x86_64 GNU/Linux

Hoje me peguei em um completo desespero 😂 ao tentar simplesmente executar um shellcode simples ( apenas executar um /bin/sh usando a syscall execve() ) através de um código em C "simples".

A idéia era usar o shellcode disponível em http://shell-storm.org/shellcode/files/shellcode-827.php, transcrito abaixo:

/*
    *****************************************************
    *    Linux/x86 execve /bin/sh shellcode 23 bytes    *
    *****************************************************
    *         Author: Hamza Megahed             *
    *****************************************************
    *             Twitter: @Hamza_Mega                  *
    *****************************************************
    *     blog: hamza-mega[dot]blogspot[dot]com         *
    *****************************************************
    *   E-mail: hamza[dot]megahed[at]gmail[dot]com      *
    *****************************************************

xor    %eax,%eax
push   %eax
push   $0x68732f2f
push   $0x6e69622f
mov    %esp,%ebx
push   %eax
push   %ebx
mov    %esp,%ecx
mov    $0xb,%al
int    $0x80
*/

#include <stdio.h>
#include <string.h>

char *shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
          "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";

int main(void)
{
	fprintf(stdout,"Length: %d\n",strlen(shellcode));
	(*(void(*)()) shellcode)();
	return 0;
}

O primeiro problema que encontramos foi que o shellcode não era executado de forma alguma, gerando um segmentation fault. Ao analisar no gdb notávamos que este segmentation fault surgia logo após a primeira instrução ser executada.

Segmentation Fault ao executar o shellcode

Compilando o nosso código, desabilitando todas as proteções (gcc -m32 -fno-stack-protector -zexecstack -Wl,-z,norelro -no-pie shellcode-test.c -o shellcode-test ) e executando obtemos o Segmentation Fault

O que acontece é que a região de memória onde o shellcode é colocado é marcada só para leitura e escrita e não para execução. Olhando atentamente o que observamos é que declaramos uma constante global e não local à função main(), logo, ela será armazenada, de acordo com o compilador, em uma região específica. Como trata-se de ponteiro global inicializado o gcc, pelo que vemos, coloca ele na seção .rodata. Esta seção não tem permissão de execução.

Vamos verificar isso direto no gdb, com o plugin gef já instalado, para entendermos melhor.

Executando o comando gef> disas main, observamos onde as nossas funções são chamadas.

Colocando um breakpoint em 0x080491c0 <+78>: call eax , usando o comando b *0x080491c0, e executando o programa gef> r, paramos exatamente na instrução que transferirá o fluxo de execução para o nosso shellcode.

Colocando um breakpoint no endereço que está apontado por eax

e dando continuidade a execução do programa, pararemos na primeira instrução do nosso shellcode.

Vamos observar então, como estão as permissões da região de memória onde nosso shellcode foi colocado, ou seja, qa região de memória que contém o endereço 0x0804a008, utlizando o comando vmmap.

Observamos então que nosso shellcode está em uma região de memória onde só é permitida a leitura e não a execução de código.

Se utlizarmos o comando info files podemos observar em que seção do binário ELF nosso shellcode foi colocado pelo compilador:

Neste caso, na seção .rodata.

Do wiki https://wiki.osdev.org/ELF , temos:

.rodata -> that’s where your strings go, usually the things you forgot when linking and that cause your kernel not to work. objdump -s -j .rodata .process.o will hexdump it. Note that depending on the compiler, you may have more sections like this.

Usando o objdump, conforme sugerido pelo artigo, podemos confirmar a presença do nosso shellcode:

Logo, estamos realmente desabilitando as proteções permitindo que códigos na pílha sejam executados, mas esta ação não surte efeito pois o nosso código não está na pilha.

SOLUÇÃO 1

Para resolver esta situação podemos declarar a nosso variável como local à função main()** e ser do tipo array e não um ponteiro**. Se deixarmos a declaração como ponteiro, nosso variável ainda será colocada em outra seção e consequentemente não estará na pilha.

#include <stdio.h>
#include <string.h>

int main(void)
{

    char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
                       "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";


    fprintf(stdout,"Length: %d\n",strlen(shellcode));
    (*(void(*)()) shellcode)();
    return 0;
    
}

Uma explicação do motivo de ser necessário declarar um array e não um ponteiro.

For an example, if two declarations are like char s[20], and char *s respectively, then by using sizeof() we will get 20, and 4. The first one will be 20 as it is showing that there are 20 bytes of data. But second one is showing only 4 as this is the size of one pointer variable. For the array, the total string is stored in the stack section, but for the pointer, the pointer variable is stored into stack section, and content is stored at code section. And the most important difference is that, we cannot edit the pointer type string. So this is read-only. But we can edit array representation of the string ( https://www.tutorialspoint.com/difference-between-char-s-and-char-s-in-c )

Desta forma, podemos notar, através mais uma vez do gdb, que desta vez o EAX apontará para um endereço na stack da função, que agora tem permissão de execução.

Colocando um breakpoint antes da instrução call eax, podemos observar que agora, o registrador EAX aponta para uma região da stack da função atual ( main() ).

Confirmamos com o vmmap que nosso shellcode está na stack e que ela tem permissão de execução.

SOLUÇÃO 2

Uma outra forma de contornar essa restrição, sem precisar se preocupar em declarar a vairável como um array local e remover as restrições é mapear uma região de memória com as devidas permissões e nesta região colocar nosso shellcode. O código abaixo faz isso.

#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <errno.h>


const char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
                       "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";

int main(void)
{
    printf("Shellcoding lenght: %d\n", strlen(shellcode));
    
    char *buf;
    int prot = PROT_READ | PROT_WRITE | PROT_EXEC;
    int flags = MAP_PRIVATE | MAP_ANONYMOUS;

    buf = mmap(0, sizeof(shellcode), prot, flags, -1, 0);
    memcpy(buf, shellcode, sizeof(shellcode));

    ((void (*)(void))buf)();
}

Shellcode com problema!

Outro problema surgiu foi que, mesmo após dadas as soluções acima, ao executar o programa este retornava com segmentation fault. Depois de debugar e consultar algumas fontes na internet observamos que o shellcode não zera o registrador EDX antes de chamar a syscall execve e nem tão pouco faz a saída do shellcode de forma correta.

Observamos que a execve() requer 3 parâmetros

E quando estamos trabalhando com syscall linux 32bits devemos passar os parâmetros através dos registradores, da seguinte forma: EAX -> syscall number (11 = 0x0a), EBX -> Param 1 , ECX -> Param 2, EDX -> param3.

O que acontece é que sem tratar o EDX (terceiro parâmetro) estamos passando “lixo” para a função execve. Como o terceiro parâmetro se refere a variáveis de ambiente podemos simplesmente zerá-lo, como se passássemos nulo para a função.

Logo outro shellcode (http://shell-storm.org/shellcode/files/shellcode-811.php), foi utilizado, onde este zera o registrador EDX antes da syscall e também executa o exit() após o execve.

/*
Title:  Linux x86 execve("/bin/sh") - 28 bytes
Author: Jean Pascal Pereira <pereira@secbiz.de>
Web:    http://0xffe4.org


Disassembly of section .text:

08048060 <_start>:
 8048060: 31 c0                 xor    %eax,%eax
 8048062: 50                    push   %eax
 8048063: 68 2f 2f 73 68        push   $0x68732f2f
 8048068: 68 2f 62 69 6e        push   $0x6e69622f
 804806d: 89 e3                 mov    %esp,%ebx
 804806f: 89 c1                 mov    %eax,%ecx
 8048071: 89 c2                 mov    %eax,%edx
 8048073: b0 0b                 mov    $0xb,%al
 8048075: cd 80                 int    $0x80
 8048077: 31 c0                 xor    %eax,%eax
 8048079: 40                    inc    %eax
 804807a: cd 80                 int    $0x80



*/

#include <stdio.h>

int main()
{

  char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x73"
                   "\x68\x68\x2f\x62\x69\x6e\x89"
                   "\xe3\x89\xc1\x89\xc2\xb0\x0b"
                   "\xcd\x80\x31\xc0\x40\xcd\x80";

  fprintf(stdout,"Lenght: %d\n",strlen(shellcode));
  (*(void  (*)()) shellcode)();
}

Usando o nosso código, agora com variável local:

#include <stdio.h>
#include <string.h>

int main(void)
{

    char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x73"
                   "\x68\x68\x2f\x62\x69\x6e\x89"
                   "\xe3\x89\xc1\x89\xc2\xb0\x0b"
                   "\xcd\x80\x31\xc0\x40\xcd\x80";

    fprintf(stdout,"Length: %d\n",strlen(shellcode));
    (*(void(*)()) shellcode)();
    return 0;
    
}

ou, usando o mapeamento de memória:

#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <errno.h>

const char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x73"
                         "\x68\x68\x2f\x62\x69\x6e\x89"
                         "\xe3\x89\xc1\x89\xc2\xb0\x0b"
                         "\xcd\x80";

int main(void)
{
    printf("Shellcoding lenght: %d\n", strlen(shellcode));
    
    char *buf;
    int prot = PROT_READ | PROT_WRITE | PROT_EXEC;
    int flags = MAP_PRIVATE | MAP_ANONYMOUS;

    buf = mmap(0, sizeof(shellcode), prot, flags, -1, 0);
    memcpy(buf, shellcode, sizeof(shellcode));

    ((void (*)(void))buf)();
}

Conseguimos executar nosso shellcode, como esperado:

Referências Utilizadas para entender o problema

Agradecimentos

Gostaria de agradecer ao Hélvio (https://sec4us.com.br/) e ao Fabiano Furtado (https://www.linkedin.com/in/fabianofurtado/) que me ajudaram no entendimento do problema.