
Fuzzing de Dispositivos IoT Usando o Roteador TL-WR902AC como Exemplo
Esta é a versão em HTML do meu trabalho acadêmico, que pode ser baixado como PDF aqui.
O fuzzing tornou-se "uma das maneiras mais eficazes" de encontrar bugs em software. Com esta ou alegações semelhantes, muitos artigos atuais relacionados a fuzzing começam [google-scholar]. O principal objetivo do nosso último trabalho acadêmico sobre o tópico "Internet of Vulnerable Things" era encontrar um bug relacionado a memória e então escrever um exploit para essa vulnerabilidade. Conseguimos encontrar uma vulnerabilidade ao fazer engenharia reversa do firmware, mas nenhum bug relacionado a memória foi encontrado. Encontrar um buffer overflow ao fazer engenharia reversa de um binário manualmente não é apenas demorado, mas também exige muita experiência. Ao mesmo tempo, o fuzzing visa ser a "maneira mais eficaz" de encontrar tais vulnerabilidades relacionadas a memória. O Google, por exemplo, introduziu o OSS-Fuzz, que faz fuzzing contínuo de software de código aberto e já encontrou mais de 10.000 vulnerabilidades em 1.000 projetos [oss-fuzz].
O objetivo deste trabalho acadêmico é novamente encontrar uma vulnerabilidade relacionada a memória, mas desta vez usando fuzzing. A vulnerabilidade alvo deve ser explorável pela rede sem conhecimento das credenciais de administrador. Este trabalho descreve a maneira de alcançar esse objetivo. Para isso, o trabalho está separado em duas partes. A primeira parte foca em como encontrar um alvo promissor, quais ferramentas podem ser usadas e do que um bom alvo de fuzzing deve consistir. A segunda parte descreve então como desenvolver e depurar um harness capaz de fazer fuzzing em uma função específica de um binário. Em seguida, o harness desenvolvido é usado pelo AFL++ para fazer fuzzing na função alvo. A seguir, uma breve contextualização é apresentada e qual é o estado da arte atual quando se trata de fuzzing de dispositivos IoT.
Todos os arquivos criados no contexto deste trabalho acadêmico também são publicados integralmente no GitHub e podem ser acessados usando a seguinte URL: otsmr/blackbox-fuzzing.
Fazer fuzzing em dispositivos IoT não é tão fácil quanto fazer fuzzing em um projeto de código aberto. Frequentemente, o código-fonte é proprietário, o que torna o fuzzing de caixa cinza, que instrumenta o código-fonte para o melhor desempenho de fuzzing, impossível [afl-persistent]. Além disso, a arquitetura de CPU muitas vezes não é suportada nativamente pelos fuzzers, o que exige um emulador como QEMU [qemu], que também diminui a velocidade do fuzzing [afl-persistent]. Outro problema são os periféricos de hardware, que dificultam o desenvolvimento de uma abordagem geral. O artigo "Embedded Fuzzing: A Review of Challenges, Tools, and Solutions" [embedded-fuzzing] fornece uma visão geral de diferentes estratégias de fuzzing, como o fuzzing embarcado baseado em hardware. A maioria dessas estratégias precisa do código-fonte do programa alvo, como quando se porta o código-fonte do fuzzer, como o AFL, para dispositivos IoT baseados em ARM, a fim de executar o fuzzer no hardware IoT. Executar o fuzzer no hardware do dispositivo também apresenta problemas de desempenho, pois eles geralmente têm CPUs de baixo nível, que são mais lentas do que CPUs normais de desktop. Outra abordagem apresentada neste artigo é o fuzzing embarcado baseado em emulação. Em que um único programa alvo é executado em um emulador para realizar fuzzing guiado por cobertura ou o sistema completo.
Um harness descreve uma sequência de chamadas de API que processam as entradas fornecidas pelo fuzzer. Ao contrário de uma aplicação normal, que muitas vezes não precisa de um harness, uma biblioteca que implementa funções reutilizáveis deve ser chamada com os parâmetros corretos e também na sequência certa, para que o estado entre múltiplas chamadas de funções compartilhadas possa ser chamado. Submeter a biblioteca a fuzzing aleatório sem construir a máquina de estados provavelmente não terá sucesso e, em contraste, criará muitos crashes falso-positivos quando as dependências da biblioteca não forem aplicadas. Isso pode acontecer quando, por exemplo, uma verificação de tamanho de buffer é ignorada pelo fuzzer, resultando em um buffer overflow espúrio.
Neste trabalho, aplicações normais serão submetidas a fuzzing, mas por causa das dependências de hardware do uso de sockets e multi-threading, precisamos criar um harness para elas também. O harness é carregado no contexto do binário e pode chamar funções internas do programa alvo, como mostrado no Código 10.
O termo "corpus" descreve amostras de entrada válidas ou casos de teste e serve como referência fundamental para gerar novos dados de entrada durante o processo de fuzzing. No Código 10, isso seria, por exemplo, uma requisição HTTP. Os fuzzers então utilizam esse corpus para criar casos de teste mutados ou diversificados, auxiliando na detecção de vulnerabilidades de software por meio da exploração de vários cenários de entrada.
A parte que mais consome tempo do fuzzing de caixa preta é encontrar uma potencial
função vulnerável no firmware. O primeiro passo é encontrar
binários interessantes que, por exemplo, são acessíveis pela rede,
usam funções inseguras ou não possuem recursos de segurança como stack
canary habilitado, que é uma proteção contra buffer overflow. Nosso último trabalho
([iovt]) já descreveu como extrair o firmware
do roteador alvo e como encontrar um binário potencialmente perigoso.
Para isso, foi usada a ferramenta EMBA [emba]. O EMBA classifica todos
os binários encontrados no firmware pela quantidade de funções inseguras, como
strcpy, acesso à rede e proteção de segurança como stack canary ou
o NX-Bit, que se tornam interessantes ao explorar um buffer overflow,
que pode ser encontrado no Código 1.
Código 1: Resultado do EMBAs para usos inseguros da função strcpy.
Como o objetivo deste artigo é encontrar uma vulnerabilidade de memória que possa ser explorada através da rede sem o conhecimento das credenciais de administrador, a função vulnerável deve ser chamável através da rede e interagir diretamente com a entrada fornecida pelo usuário. Mas ter interação pela rede não significa que o binário também seja diretamente acessível através da rede. Para descobrir quais binários estão em escuta, podemos usar o shell root da UART, que já foi estabelecido em [iovt].
```txt ~ # netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.1:20002 0.0.0.0:* LISTEN 1045/tmpd tcp 0 0 0.0.0.0:1900 0.0.0.0:* LISTEN 1034/upnpd tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1027/httpd tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1224/dropbear udp 0 0 0.0.0.0:20002 0.0.0.0:* 1048/tdpd [...] ```Código 2: Usando o shell root via UART para executar netstat
O primeiro binário que parece promissor é wscd. O binário tem a maioria das chamadas inseguras de strcpy
(exceto pela biblioteca libcmm.so) e interação de rede, o que no caso do wscd significa que ele
se conecta a um dispositivo UPnP e não escuta em uma porta específica. Ele tem, como mostrado mais adiante, uma função
fácil de fuzzing, motivo pelo qual este binário foi selecionado como exemplo neste artigo para explicar o
procedimento geral. Antes de fazer reversing, podemos usar o shell root via UART para descobrir se o binário está
em execução e como ele foi iniciado.
Código 3: Usando o comando ps para exibir todos os programas em execução.
Com o ps, não apenas vemos que o binário está em execução, mas também quais são os argumentos, que são
importantes para verificar se uma função potencial é realmente chamada. O significado desses argumentos pode ser
obtido na ajuda da CLI, que é exibida ao chamar o binário sem nenhum argumento.
Código 4: Opções do binário wscd.
Como mostrado no Código 4, o wscd é iniciado com "Enabled UPnP Device service", o que
parece promissor. Após verificar que o binário está realmente em execução no roteador, o binário pode
então ser analisado usando Ghidra para procurar funções suspeitas. Para
fuzzing, funções de parsing são especialmente interessantes porque geralmente são complexas e, muitas
vezes, a entrada analisada possui campos de tamanho para os dados contidos, assim como o pacote TCP
contém o comprimento do payload.

Figura 1: Usando o Ghidra para procurar funções de parsing.
Outra vantagem das funções de parsing é que elas frequentemente não interagem com outras partes do código nem têm interação com o usuário pela rede. Assim, a função de parsing pode ser chamada diretamente com a entrada, sem modificar o binário ou sobrescrever outras funções, de modo que a função pode ser fuzzeada.
Antes de começar a fuzzar a função, deve-se verificar se a função é acionada de fato, pois a função só
é interessante quando é chamada com uma entrada controlada pelo usuário. Para isso, o Ghidra pode ser
usado para procurar referências à função alvo. No caso da função parser_parse, existem vários
caminhos. Como sabemos como o programa é iniciado, as chamadas podem ser reduzidas a uma única árvore
de chamadas de função, mostrada no Código 5.
Código 5: Árvore de chamadas da função parser_parse
Depois que uma função alvo é encontrada, podemos agora criar uma configuração de fuzzing para aplicar fuzzing à função, o que é descrito na próxima parte. Mas primeiro, outras funções promissoras são apresentadas.
Para este artigo, vários binários potenciais foram analisados manualmente em busca de funções suspeitas. A seguir, um breve resumo de outros possíveis alvos encontrados.
O binário httpd é o backend da interface web de administração. O binário é acessível pela rede na porta 80. Uma função interessante em httpd é a função httpd_parser_main. Ao examinar a implementação do parser usando o Ghidra, várias partes suspeitas do código puderam ser identificadas. Uma das partes suspeitas é o parsing do Content-Type. A seguir, uma requisição HTTP básica pode ser encontrada.```txt
POST / HTTP/1.1\r\n
Content-Type: multipart/form-data; boundary=X;\r\n
Host: example.com\r\n
\r\n
\r\n
DATA\r\n
Abaixo está um trecho da função `httpd_parser_main` que analisa o `Content-Type` da requisição http fornecida pelo usuário.
<div id="c6"></div>```c
// user_input_ptr points to
// "Content-Type: multipart/form-data; boundary=X;\r\nHost: example.com\r\n..."
cursor = strstr(user_input_ptr,"multipart/form-data");
if (user_input_ptr == cursor) {
cursor = strstr(user_input_ptr,"boundary=");
user_input_ptr = cursor + 9;
// user_input_ptr points now to "X;\r\nHost: example.com\r\n..."
if (cursor != (char *)0x0) {
do {
while (cursor = user_input_ptr, *cursor == " ") {
user_input_ptr = cursor + 1;
}
user_input_ptr = cursor + 1;
} while (*cursor == "\t");
// cursor points now to "X;\r\nHost: example.com\r\n..."
// strchr returns a pointer to the first occurrence of ";" in the user request.
// If ";" is not found, the function returns a null pointer.
user_input_ptr = strchr(cursor, ";");
if (user_input_ptr != (char *)0x0) {
// The character ";" is replaced by an null byte to terminate the string
*user_input_ptr = "\0";
// cursor points now to "X\0\r\nHost: example.com\r\n..."
}
// DAT_00444050 global array from 0x00444050 to 0x0044414f (255 Bytes)
strcpy(&DAT_00444050, cursor);
// DAT_00444050 contains now "X"
}
}
Code 6: Call tree of the function parser_parse
A vulnerabilidade neste código é a chamada da função strcpy e a suposição de que o
Content-Type termina com um ponto e vírgula. Como strcpy copia o buffer até o próximo byte nulo,
e conforme mostrado no Code 6, o byte nulo só é adicionado quando um ponto e vírgula é encontrado. Ao remover o
ponto e vírgula, o próximo byte nulo está no final do buffer de entrada, por exemplo, no final da solicitação HTTP.
Assim, a variável global DAT_00444050 pode ser estourada, o que então sobrescreve dados além do endereço
0x0044414f. A parte desafiadora não é apenas encontrar uma variável global interessante além deste
endereço que possa ser sobrescrita, mas também que nenhum byte nulo pode ser usado por causa do strcpy. Mas
quando existe um erro como esse, provavelmente há mais para encontrar.
O binário tdpd é usado pelo aplicativo móvel e é acessível via UDP na rede local.
tdpd tem quase as mesmas funções que o tmpd, que na maioria das vezes nunca são chamadas. A função
principal apenas escuta mensagens pela porta UDP e sempre responde com informações básicas
sobre o roteador, como o nome ou o modelo. Há pouquíssima interação com a entrada fornecida pelo usuário,
que portanto não é interessante para fuzzing.
Outro par interessante de binários são upnpd e ushare. Ambos os binários lidam com mensagens UPnP,
que portanto precisam analisar XML. Como uma string de copyright pode ser encontrada no binário,
pode-se supor que esses programas não foram desenvolvidos pela TP-Link.```sh
$ strings usr/bin/ushare | grep "(C)"
Benjamin Zores (C) 2005-2007, for GeeXboX Team.
Ambos os binários estão carregando as bibliotecas compartilhadas `libupnp.so` e `libixml.so` que possuem as mesmas funções que o projeto de código aberto `pupnp` [\[pupnp\]](https://github.com/pupnp/pupnp/). Como o foco deste artigo é fuzzing de caixa preta, esses binários são ignorados. Mas fuzzing de caixa cinza desta biblioteca poderia ter potencial, pois em 2021 foi encontrado um vazamento de memória em `libixml.so`
[\[pupnp-mem-leak\]](https://github.com/pupnp/pupnp/issues/249).
O binário **tmpd** é o backend do aplicativo móvel. A parte interessante é que o roteador e o aplicativo móvel estão se comunicando por meio de um protocolo binário personalizado. A seguir, uma mensagem do cliente para o servidor é mostrada.
<div id="c7"></div>```txt
00000000 01 00 05 00 00 08 00 00 00 00 00 17 50 7b 6e fe |............P{n.|
00000010 01 01 02 00 00 00 00 00 |........ |
Código 7: Mensagem do aplicativo móvel para o roteador.
Para entender o protocolo binário, o binário tmpd foi revertido usando Ghidra. Com essa
informação, a mensagem em Código 7 pode ser dividida da seguinte forma:```txt
01 00 05 00 : Version
00 08 00 00 : Size (8 Bytes)
00 00 00 17 : Datatype
50 7b 6e fe : Checksum (CRC32)
01 01 : Options
02 00 : Function id
00 00 00 00 : Function parameters
<p class="text-align: center">Code 8: Protocolo binário personalizado decomposto.</p>
Isso parece promissor porque esses protocolos binários precisam ser analisados. Mas a parte mais suspeita do
protocolo binário não é o campo de comprimento, mas o uso do ID da função e dos parâmetros da função.
<figure id="f2">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/43cb089d7f85d30ba036234ff0fdb29b6a54dd4a3382b2bd68e065f9f6c75369.png" style="width:90.0%" /></p>
<figcaption>
<p style="text-align: center">Figura 2: Função revertida do tmpd que analisa o ID da função e seus parâmetros.</p>
</figcaption>
</figure>
A [Figura 2](#f2) mostra uma parte da função de análise do protocolo personalizado decompilada. Na linha 16,
o ID da função é extraído, e a função correspondente é então chamada na linha 29. O comportamento
suspeito é que a função é chamada com parâmetros extraídos sem qualquer verificação do
buffer de entrada controlado pelo usuário. Poderíamos agora tentar encontrar uma função na tabela de saltos mostrada na
[Figura 3](#f3) onde isso poderia ser perigoso, como quando o parâmetro é usado para indexar um buffer
ou interpretado como uma string. Em vez de reverter e pesquisar manualmente as mais de 100 funções,
o que consumiria tempo, podemos usar um fuzzer que faria isso automaticamente.
<figure id="f3">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/708cf59aee459b840ca6dbcac32948d2b80fb426c3d4cc745157eac4f9b7c28e.png"
style="width:90.0%" /></p>
<figcaption><p style="text-align: center">Figura 3: Função revertida do tmpd que analisa o ID da função
e seus parâmetros.</p></figcaption>
</figure>
Infelizmente, o binário `tmpd` só é acessível localmente pela rede, como mostrado no [Code
2](#c2). Para conectar a esse binário, o aplicativo primeiro se conecta ao roteador via SSH no modo
`direct-tcpip`, que apenas encaminha os pacotes para o processo local. E a conexão SSH é
protegida pelas credenciais de administrador. Mas, como descrito em
[\[iovt\]](https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/main/Internet_of_Vulnerable_Things.pdf)
a conexão SSH pode ser facilmente comprometida porque a chave de host do servidor nunca é verificada pelo
aplicativo. Ao descartar todos os pacotes roteados para a internet, o administrador pode ser enganado para fazer login no
roteador enquanto um ataque man-in-the-middle é realizado para roubar as credenciais.
## Fuzzing com AFL++ e QEMU
Nesta seção, um harness é desenvolvido visando uma das funções encontradas anteriormente. Após o
harness ser desenvolvido, o fuzzer de última geração AFL++
[\[aflpp\]](https://github.com/AFLplusplus/AFLplusplus) é usado para executar fuzzing na função alvo. Como
os binários são compilados para a arquitetura `mipsel`, o emulador QEMU é usado para executar o
binário. A configuração básica de fuzzing usada neste artigo é inspirada principalmente na postagem do blog "Firmware
Fuzzing 101" de Adam Van Prooyen [\[b101\]](https://www.mayhem.security/blog/firmware-fuzzing-101).
### Ambiente de fuzzing
Para criar facilmente um ambiente de fuzzing reproduzível, o Docker é a melhor escolha. Criamos um
Dockerfile que instala todas as ferramentas necessárias, como um compilador cruzado para a arquitetura de CPU `mipsel`
ou `gdb-multiarch`, que pode ser usado para depurar o harness.
Além disso, o AFLplusplus é baixado e compilado juntamente com o QEMU, que é construído em uma versão
com pequenos ajustes para permitir que binários não instrumentados sejam executados sob o afl-fuzz.```docker
FROM debian:latest
RUN apt update && apt install -y \
curl \
vim \
gcc-mipsel-linux-gnu \
openssh-server \
qemu-user-static \
gdb-multiarch
# Qemu statics are installed at /usr/bin/qemu-mipsel-static
# Compiling AFL++
RUN apt install -y git make build-essential clang ninja-build pkg-config libglib2.0-dev libpixman-1-dev
RUN git clone https://github.com/AFLplusplus/AFLplusplus /AFLplusplus
WORKDIR /AFLplusplus
RUN make all
WORKDIR /AFLplusplus/qemu_mode
RUN CPU_TARGET=mipsel ./build_qemu_support.sh
RUN echo "#!/bin/bash\n\nsleep infinity" >> /entry.sh
RUN chmod +x /entry.sh
WORKDIR /share
ENTRYPOINT [ "/entry.sh" ]
Dockerfile que instala as ferramentas necessárias.
A imagem pode então ser construída usando docker build.```sh
docker build -t fuzz .
Quando a imagem é construída, ela pode ser facilmente usada com `docker run`, que
inicia o contêiner.```sh
docker run -d --rm -v $PWD/:/share --name fuzz fuzz
Using a opção -d iniciará o contêiner em segundo plano. Com docker exec, vários shells podem ser iniciados dentro do contêiner, o que é útil para iniciar o executável em uma sessão usando QEMU e na outra sessão gdb-multiarch.```sh
docker exec -it fuzz /bin/bash
### Sobrescrever a função main
Na seção anterior, um alvo de fuzz poderoso foi identificado. O problema é que, ao executar o
binário, nunca chegaremos à chamada da função porque a função `parser_parse` só é chamada se
um pacote TCP for recebido por meio de um socket. Isso seria não apenas ruim para o desempenho, mas também
difícil de configurar. É por isso que a entrada do fuzzer deve estar em um local diferente da função main
normal. Para isso, a variável de ambiente `LD_PRELOAD`, que permite injetar um harness com
acesso às funções internas, pode ser usada. Como a página de manual do `ld.so`, que é responsável por
vincular as bibliotecas compartilhadas necessárias para um executável em tempo de execução, descreve, `LD_PRELOAD` pode ser usada
"para substituir seletivamente funções em outros objetos compartilhados
[\[man-pages\]](https://www.man7.org/linux/man-pages/man8/ld.so.8.html)."
A função `__uClibc_main` é a mais adequada para esse propósito. Para sobrescrever essa função, um arquivo C
deve ser criado contendo uma função com o mesmo nome.```c
void __uClibc_main(void *main, int argc, char** argv) {
// Harness code, e.g. call the function parser_append
printf("My custom __uClibc_main was called!");
}
O arquivo C pode então ser cross-compilado para um objeto compartilhado na arquitetura mipsel usando
mipsel-linux-gnu-gcc. A opção -fPIC habilita "Position Independent Code", o que significa que o
código de máquina não depende de estar localizado em um endereço específico, usando endereçamento relativo
em vez de absoluto.```txt
$ mipsel-linux-gnu-gcc parser_parse_hook.c -o parser_parse_hook.o -shared -fPIC
A biblioteca compartilhada recém-criada pode então ser carregada adicionando a variável de ambiente `LD_PRELOAD`
ao comando QEMU.```txt
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o /usr/bin/wscd
My custom __uClibc_main was called!
Com o comando chroot, os diretórios atual e raiz podem ser alterados para o comando fornecido.
Isso é útil porque o executável wscd abre outros arquivos, como bibliotecas compartilhadas do
firmware. Podemos observar esse comportamento adicionando o argumento -strace ao QEMU.```txt
chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o -strace /usr/bin/wscd /corpus/notify.txt
38180 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|0x4000000,-1,0) = 0x7f7e7000
38180 stat("/etc/ld.so.cache",0x7ffffa48) = -1 errno=2 (No such file or directory)
38180 open("/parser_parse_hook.o",O_RDONLY) = 3
38180 fstat(3,0x7ffff920) = 0
38180 close(3) = 0
38180 munmap(0x7f7e6000,4096) = 0
38180 open("/lib/libpthread.so.0",O_RDONLY) = 3
38180 open("/lib/libc.so.0",O_RDONLY) = 3
[...]
Como podemos ver, o executável abre múltiplas bibliotecas na pasta `/lib/` no firmware e não
no host.
### Desenvolvendo e depurando o harness
Após a criação do setup, podemos agora começar a desenvolver um harness. Conforme descrito na seção Background,
o harness é o driver entre o fuzzer e a função alvo. O harness carrega a
entrada de fuzzing, que é armazenada pelo AFL++ em um arquivo. Com o caminho do arquivo como parâmetro, o harness então
chama o alvo de fuzzing; neste caso, seria `parser_append`. As funções podem ser chamadas
usando o endereço.
<div id="c10"></div>```c
void __uClibc_main(void *main, int argc, char** argv)
{
// Verify that a filename is provided
if (argc != 2) exit(1);
// Create function pointer to the fuzz target
int (*parser_request_init)(void *, int) = (void *) 0x00412564;
int (*parser_append)(void *, void *, int) = (void *) 0x00412e98;
// Open the fuzz input file
int fd = open(argv[1], O_RDONLY);
char fuzz_buf[2048 + 1];
int fuzz_buf_len = read(fd, fuzz_buf, sizeof(fuzz_buf) - 1);
if (fuzz_buf_len < 0) exit(1);
fuzz_buf[fuzz_buf_len] = 0;
// Call the target functions
uint8_t parsed_data[220];
parser_request_init(parsed_data, 8);
int status = parser_append(parsed_data, fuzz_buf, fuzz_buf_len);
printf("Response is %d\n", status);
exit(0);
}
Código 10: Código de harness com o alvo de fuzzing `parser_append` no binário wscd.
Como mostrado no Código 10, a função parser_parse não é chamada diretamente, mas sim através da
função parser_append. Antes que esta função seja chamada, a função de inicialização
parser_request_init deve ser chamada, a qual inicializa a struct de saída da
função parser_parse.
Embora no caso do parser_parse o harness seja bastante fácil de configurar, outros alvos exigem
harnesses mais sofisticados, como a função httpd_parser_main. Por exemplo, antes de chamar a
função alvo, a função http_init_main deve ser chamada, o que termina em um SIGSEGV. Para descobrir onde
essa falha de segmentação é causada, é útil depurar o código com um depurador como gdb. Para fazer
isso, o QEMU pode ser iniciado com a opção -g, que cria um gdb-server na porta fornecida.```sh
chroot root /qemu-mipsel-static -strace -g 1234 -E LD_PRELOAD="/httpd_parser_main.o" /usr/bin/httpd
corpus/httpd/simple.txt
Como o binário está na arquitetura `mipsel`, o `gdb-multiarch` deve ser usado. Depois que o gdb for
iniciado, o seguinte script de inicialização pode ser carregado com o gdb usando `sources <path to script>`.```sh
set solib-absolute-prefix /share/root/
file /share/root/usr/bin/httpd
target remote :1234
# break bevor fuzz target is called
# break __uClibc_main
break http_parser_main
display/4i $pc
Por causa do chroot, o script primeiro alterou o caminho absoluto do prefixo para que, quando o binário carregar um objeto compartilhado, o gdb encontre o arquivo. Em seguida, o arquivo alvo é definido, porque o gdb-server do QEMU não suporta transferência de arquivos, então o gdb tenta carregar os arquivos do disco. Depois que o gdb é configurado, o script conecta-se ao gdb-server com target remote e cria um breakpoint no início da função alvo. Com display, a saída é apenas melhorada, então, ao percorrer passo a passo, as próximas quatro linhas de assembly serão mostradas. Usando si, podemos avançar uma instrução, o que é útil quando o harness tem uma falha de segmentação usando o corpus padrão, que deve sempre funcionar. Como mostrado no Código 11, o binário tem uma falha de segmentação na função fprintf.
Program received signal SIGSEGV, Segmentation fault.
<p style="text-align: center">Código 11: Falha de segmentação em printf.</p>
Para investigar o erro, o Ghidra pode ser usado para descobrir com quais parâmetros a função é chamada.```c
fprintf(
*(FILE **)(iVar1 + 0x101c),
"HTTP/1.1 %d %s\r\n",
*(undefined4 *)(&DAT_0042ee68 + (uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 8),
(&PTR_DAT_0042ee6c)[(uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 2]
);
O SIGSEGV provavelmente é causado pelo fato de que o primeiro parâmetro não é um descritor de arquivo, mas um ponteiro nulo. Onde iVar1 é apenas uma referência à entrada da função httpd_parser_main. Isso significa que a entrada de fuzzing deve ter um descritor de arquivo na posição 0x101c. Portanto, a entrada deve ser ajustada para a seguinte struct.```c
typedef struct {
int _a; // 4 Bytes
int _b; // 4 Bytes
int socket; // 4 Bytes
int ip; // 4 Bytes
int mac; // 4 Bytes
unsigned char body[0x1008]; // 0x101c - 4*5 = 0x1008 Bytes
FILE * fd_out; // expected to be a valid file descriptor
} HttpMainT;
Como `fd_out` apenas precisa ser um ponteiro de descritor de arquivo válido, ele pode ser facilmente definido como `stdout`. Executar o `httpd_parser_main` novamente agora produzirá uma saída HTTP válida.```c
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/httpd_parser_main.o \
/usr/bin/httpd /httpd_corpus.txt
bind: No such file or directory
[ dm_shmInit ] 086: shmget to exitst shared memory failed. Could not create shared memory.
rdp_getObj is called with: 4274932gdpr_getSystemGDPREntry Error
gdpr_getNewSystemGDPREntry OK
#Msg: getsockname error
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 24257
Set-Cookie: JSESSIONID=deleted; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Path=/; HttpOnly
Connection: close
<!DOCTYPE html>
[...]
O harness funciona agora e pode ser usado para fuzzar a função usando AFL++, o que será explicado na próxima seção.
Como mencionado no contexto, um seed corpus descreve amostras de entrada válidas, que serve como uma referência fundamental para gerar novos dados de entrada durante o processo de fuzzing.
Essas entradas são normalmente escolhidas para representar diferentes aspectos dos programas-alvo. O seed corpus é usado por um fuzzer para gerar casos de teste mutados ou evoluídos que são então executados contra o software-alvo para descobrir bugs, crashes ou outros problemas. Esse corpus desempenha um papel importante em direcionar o fuzzer para áreas relevantes do programa e aumentar a probabilidade de detectar vulnerabilidades ou comportamentos inesperados. Ao fornecer um conjunto diverso e representativo de entradas iniciais, o seed corpus ajuda o fuzzer a explorar diferentes caminhos no alvo mais rapidamente e, assim, aumenta a cobertura.
Quando se trata de funções que analisam dados de rede, essas entradas podem ser criadas usando o Wireshark para registrar diferentes pacotes.
Para a função httpd_parse_main, quatro corpora diferentes foram criados. Cada um visando diferentes
caminhos no binário. Um exemplo é a requisição de login, que contém o nome de usuário e a senha. Para
este corpus, o harness teve que ser modificado porque a TP-Link usa criptografia (fraca) para "proteger"
a senha. Para isso, a senha é criptografada no navegador usando AES e depois descriptografada no
backend. Sendo que a senha é gerada no navegador e depois criptografada usando RSA. Em seguida, os
dados criptografados são assinados. Como um fuzzer não pode criar uma assinatura ou criptografar dados, algumas
funções foram sobrescritas e agora apenas decodificam os dados de base64. Para isso, os dados foram primeiro
extraídos em texto puro do navegador usando o depurador mostrado na Figura 4.

Figura 4: Extraindo os dados antes da criptografia.
No alvo, a função rsa_tmp_decrypt_bypart foi então sobrescrita para substituir a lógica de
descriptografar os dados por apenas decodificar a partir de base64.```c
// Replacing the logic with b64_decode
int rsa_tmp_decrypt_bypart(uint8_t *input, int input_len, uint8_t *output) { // other params just key data
int (*b64_decode)(uint8_t *, int, uint8_t *, int) = (void *) 0x0040bf00;
b64_decode(output, 0x1000, input, input_len);
int * seqnumber = (int *) 0x00444db0;
*seqnumber = 0x3ac28e29-input_len+12;
return 0; // says it was okay
}
<p style="text-align: center">Código 12: A função rsa_tmp_decrypt_bypart agora apenas decodifica base64 em vez de descriptografar os dados.</p>
Ao executar o corpus, a função alvo sempre retorna um documento HTML com o erro "408 Request Timeout". Usando Ghidra e GDB, o problema pôde ser identificado. O erro sempre ocorre após a chamada da função `http_stream_fgets`. A linha problemática era a verificação do caractere de quebra de linha `\n`.```c
if (((cVar1 == '\n') && (param_3 < pcVar4)) && (pcVar4[-1] == '\r')) {
Esta condição garante que, após cada quebra de linha, um retorno de carro deve seguir. Depois de adicionar o retorno de carro, todos os corpora criados funcionaram.
Na última seção, desenvolvemos vários harnesses e os executamos usando QEMU. Nesta seção,
o QEMU é substituído pelo AFL++, que recebe os corpora gerados como entrada de semente para fuzzar a função
alvo. Na seção "Ambiente de fuzzing", uma imagem docker foi criada que já baixa o AFL++
do GitHub e, em seguida, usa um script fornecido pelo AFL++ para construir uma versão corrigida do QEMU. Portanto, o AFL++ pode
agora ser iniciado com o seguinte comando, que recebe diferentes parâmetros, como -Q, que informa
ao AFL++ para usar a versão corrigida do QEMU.```sh
QEMU_LD_PREFIX=/share/root AFL_PRELOAD=/share/root/httpd_parser_main.o
/AFLplusplus/afl-fuzz -Q
-i /share/root/corpus/httpd/ -o /share/afl-out/httpd/
-- /share/root/usr/bin/httpd @@
<p style="text-align: center">Código 13: Fuzzing do binário <code>httpd</code> usando o harness
e <code>afl-fuzz</code>.</p>
Ao contrário de antes, o comando `chroot` não é mais necessário e é substituído pela variável
`QEMU_LD_PREFIX`. Ela informa ao QEMU onde procurar objetos compartilhados. Além disso, a variável `LD_PRELOAD`
é substituída pela versão específica do AFL, `AFL_PRELOAD`. O último argumento do comando são
os dois caracteres `@`. Eles serão substituídos pelo AFL++ por um caminho de arquivo que contém a entrada de fuzzing.
Quando iniciado, o AFL++ mostra o progresso usando a interface de terminal exibida na [Figura 5](#f5).
<div id="f5"></div>
<figure>
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/ac4b12fcf84c2041c9edc2a0871c5bb89b576d976b2eba0a2d0f843e7e708795.png" style="width:95.0%" /></p>
<figcaption><p style="text-align: center">Figura 5: A tela de status do AFL++.</p></figcaption>
</figure>
A tela de status do `AFL++` fornece informações essenciais sobre o processo de fuzzing atual. A documentação do
`AFL++` tem uma boa visão geral dos termos usados na tela de status
[\[afl-screen\]](https://aflplus.plus/docs/status_screen/). Ao depurar o corpus com as seguintes
variáveis de ambiente, a interface pode ser desabilitada e, com `AFL_DEBUG`, um log detalhado é
habilitado, que mostra a entrada atual do fuzzer e o `stdout` do programa alvo.```sh
export AFL_DEBUG=1 && export AFL_NO_UI=1
unset AFL_DEBUG && unset AFL_NO_UI
Como mostrado na Figura 5, fuzzing de um binário pode levar bastante tempo. De acordo com a documentação,
"deve-se esperar que seja executado por dias ou semanas" e "alguns trabalhos poderão ser executados por meses." Para
melhorar o tempo necessário, a velocidade de execução deve ser acima de 100 execuções/seg. Quando, por exemplo, o alvo
httpd_main_parser foi submetido a fuzzing, a velocidade de execução estava no início em torno de 30/seg. Para melhorar a
velocidade, o binário alvo foi buscado por funções suspeitas, que provavelmente são a causa da
lentidão. Uma das funções suspeitas era rsa_gdpr_generate_key porque gerar uma chave RSA é
conhecidamente lento. Após sobrescrever a função, a velocidade melhorou para 600 execuções por segundo.
Um indicador que ajuda a saber quando parar o fuzzing é o contador de ciclos. O AFL++ destacará o número em verde quando "o fuzzer não tem visto nenhuma ação por um bom tempo," o que ajuda a tomar a decisão de parar o fuzzer.
Mas o número mais interessante é provavelmente "total crashes". Isso mostra quando o programa falha
devido à entrada de fuzzing atual e provavelmente é um bug relacionado à memória. Para verificar se este é um
bug real, gdb pode ser usado novamente para encontrar a posição do bug.
Fuzzing pode ser a maneira mais eficaz de encontrar vulnerabilidades de segurança. Neste trabalho acadêmico, três funções diferentes foram submetidas a fuzzing, mas nenhuma foi encontrada. Embora a configuração de fuzzing de caixa preta em si não seja tão complexa e demorada, encontrar um alvo potente e desenvolver um harness funcional são. Na maioria das vezes, o harness precisa ser depurado e, em seguida, a lógica subjacente no binário deve ser revertida, o que novamente consome muito tempo.