Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
blackbox-fuzzing — Fuzzing de Dispositivos IoT Usando o Roteador TL-WR902AC como Exemplo | Kitploit
Ferramentas/GitHubGitHub/otsmr/blackbox-fuzzing
Segurança IoTAnálise de VulnerabilidadesExploraçãoEngenharia ReversaFuzzingAnálise de BináriosPapers e PesquisaAprendizado e EducaçãoAnálise de Firmware
GitHubotsmr/blackbox-fuzzing

blackbox-fuzzing

Fuzzing de Dispositivos IoT Usando o Roteador TL-WR902AC como Exemplo

13217há 9 mesesRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar
Ver RepositórioSite

Fuzzing de Caixa Preta 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.

Introdução

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.

Estado da Arte

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.

Contexto

Harness

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.

Corpus

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.

Encontrando um alvo promissor

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.

```txt [+] STRCPY - top 10 results: 235 : libcmm.so : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking | 77 : wscd : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | Networking | [snip] 28 : httpd : common linux file: yes | RELRO | No Canary | NX enabled | No Symbols | Networking | 27 : cli : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking | ```

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

Reversing o binário

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.

```txt $ ps PID USER VSZ STAT COMMAND 962 admin 1096 S wscd -i ra0 -m 1 -w /var/tmp/wsc_upnp/ 1018 admin 1080 S wscd_5G -i rai0 -m 1 -w /var/tmp/wsc_upnp_5G/ ```

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.

```txt $ chroot root /qemu-mipsel-static /usr/bin/wscd Usage: wscd [-i infName] [-a ipaddress] [-p port] [-f descDoc] [-w webRootDir] -m UPnPOpMode -D [-d debugLevel] -h -i: Interface name this daemon will run wsc protocol(if not set, will use the default interface name - ra0) e.g.: ra0 -w: Filesystem path where descDoc and web files related to the device are stored e.g.: /etc/xml/ -m: UPnP system operation mode 1: Enable UPnP Device service(Support Enrolle or Proxy functions) 2: Enable UPnP Control Point service(Support Registratr function) 3: Enable both UPnP device service and Control Point services. [...] ```

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 main() if ((WscUPnPOpMode & 1) != 0) // Argument -m 1 WscUPnPDevStart() UpnpDownloadXmlDoc() -> my_http_Download() -> http_Download() if (http_MakeMessage()) http_RequestAndResponse() http_RecvMessage() ```

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.

Outras funções potencialmente vulneráveis

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

root@kitploit:~
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.

root@kitploit:~
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

root@kitploit:~
<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 .

root@kitploit:~
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

root@kitploit:~
### 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

root@kitploit:~
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 [...]

root@kitploit:~
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

root@kitploit:~
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.

```sh (gdb) si 0x004059b0 in http_parser_makeHeader () 1: x/4i \$pc => 0x4059b0 : jalr t9 0x4059b4 : addiu a1,a1,16248 0x4059b8 : li v0,200 0x4059bc : lw gp,16(sp) (gdb) ni 0x7f56a8ac in fprintf () from /share/root/lib/libc.so.0 1: x/4i \$pc => 0x7f56a8ac \: bal 0x7f56db80 0x7f56a8b0 \: nop 0x7f56a8b4 \: lw ra,36(sp) 0x7f56a8b8 \: jr ra 0x7f56a8bc \: addiu sp,sp,40 (gdb) n Single stepping until exit from function fprintf, which has no line number information.

Program received signal SIGSEGV, Segmentation fault.

root@kitploit:~
<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;

root@kitploit:~
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.

Gerar dados de corpus

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 }

root@kitploit:~
<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.

Fuzzar o alvo

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 @@

root@kitploit:~
<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.

Conclusão

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.

Baixar ferramenta