CTF | wargame

asisctf 2017 / Tatter (조각난 png조합, png 헤더 분석 , crc 연산)

nopdata 2017. 4. 10. 05:40

[ 문제 ]

문제는 위와 같다 옷이 갈기 갈기 찢겨 있다고 한다. 파일은 압축 파일이 주어지는데 압축을 풀면 해당 데이터의 sha1의 된 값이 파일 이름이 되어 있다.
총 159개의 파일이 존재하는데 어떤 파일인지 몰라 먼저 file 명령을 통해 각 파일의 시그니쳐를 알아보았으나 특이한 내용은 없었다. 그 중 MPEG관련이 하나 뜨길래 열어봤는데 
hex값으로 보니 IDAT이라는 문구를 발견할 수 있었다. IDAT청크는 PNG에서 사용되는 청크의 일부이다. 해서 png에서 사용되는 PNG, IEND를 조회해 보았으나 찾을 수 없었지만 IHDR은 찾을 수 있었다.
이를 가지고 조합을 하게 되면 사진이 복구될 것이라고 생각을 하여 진행을 하였다.

(1) 파일 정렬하기.
파이썬 콘솔을 주로 사용하기 때문에 바로확인할 수 있도록 만들었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os
 
files = os.listdir('.')
 
ihdr ""
idat = list()
els = list()
table = list()
 
for ch in files:
    tmp = open(ch,'rb').read()
    if len(tmp) == 63:
        table.append(tmp)
        if "IHDR" in tmp:
            ihdr=tmp
        elif "IDAT" in tmp:
            idat.append(tmp)
        else:
            els.append(tmp)
 
cs

(2) 시작 부분 잡기
먼저 찢어진 데이터에서 식별이 가능한 데이터부터 찾아야 한다. 시그니처는 날아갔으므로 청크 단위로 봐야 하는데 먼저 IHDR청크를 바로 찾을 수 있었기 때문에 그 값을 이용하였다.
IHDR청크는 '8528110b1d24644703a3ae98bd649ee1667f4983' 파일에 존재한다. 모든 파일의 크기는 63바이트로 동일하다.

[ IHDR이 들어있는 파일 ]
보면 빨간색으로 된 부분은 원래 존재하지 않는다. PNG파일 포맷상 png시그니쳐가 들어가야 하는 부분이다. 따라서 이 부분은 타 png나 파일 포맷을 보고 복구를 해주어야 한다.

다음으로 알 수 있는 것은 위 사진에서도 보이는 PLTE청크이다. 자주 사용되지는 않는 듯 하지만 이 역시 청크가 있기 때문에 유용한 정보로써 사용할 수 있다.
각 청크는 청크 시그니쳐 (IHDR, PLTE등)가 오기 전에 길이값이 주어진다. PLTE의 경우 PLTE가 오기전에 온 00 00 00 A2가 PLTE청크의 데이터 크기가 된다

[ 청크 crc 형태 ]

조건이 없기 때문에 전체를 대상으로 crc값을 확인하여 찾아낸다. 소스코드는 http://mwultong.blogspot.com/2007/04/python-crc32-file-crc.html를 참고하였다.
순서대로 짜맞추다 보면 PLTE - tRNS - IDAT청크로 이어지는 것을 알아낼 수 있다. 모두 CRC 청크 값을 기준으로 찾아내는 방법이다.

이제 여기서 문제가 생기는데 순서대로 짜맞추어도 순서를 알 수 없는 경우가 존재하게 된다. 63바이트로 파일크기가 동일하나 각 IDAT청크의 크기는 128로 고정이 되어 있다.
하지만 하나의 파일의 시작이 '\x00\x00\x80IDAT...'일 경우, crc값 이후로 잘렸기 때문에 이전의 idat청크 값을 알 수 없다. 반대의 경우도 동일하다.
이 경우를 해결하는 방법은 최대한 IDAT청크 덩어리를 붙일 수 있는 만큼 크게 붙여 덩어리화 시키고 각 기준에 맞게 맞추어 돌리는 수 밖에 없는 듯 하다.

먼저 각 idat청크를 찾아 각 청크의 크기를 확인해보았더니 하나의 idat청크의 크기가 3이고 나머지는 모두 128로 고정이 되어 있었다. 크기가 3인 idat는 제일 마지막에 붙는 idat청크이다.

(3) idat 관계 파악
각 파일에서 나타나는 "IDAT"의 인덱싱 번호는 다음과 같다.
index_list = [3, 10, 24, 59, 31, 52, 17, 45, 38]

idat 청크끼리는 붙어있기 때문에 순서상관을 유추해야 한다. 모든 파일이 63크기로 동일하게 잘려있기 때문에 순서생각을 해보면 다음과 같다.
for i in index_list:
     print i,(14+i)%63

이렇게 하면 맵핑 정보를 유추할 수 있다. 
3 17
10 24
24 38
59 10
31 45
52 3
17 31
45 59
38 52
3이 시작이라고 보면 3 - 17 - 31 - 45 - 59 - 10 - 24 - 38 - 52 - 3 으로 순환을 이루게 된다.

IDAT가 연결되는 형태는 (idat포함) + (일반파일) + (idat포함) 과 (idat포함) + (일반파일) + (일반파일) + (idat포함) 이 있다는 것을 알고 있어야 헤메지 않는다.
예를 들면 인덱스 번호가 59번과 10번이 이어지는 경우를 생각해보면 [~~IDAT] + [ 63바이트 ] + [ 63바이트 ] + [ ~ IDAT ~ ]의 형태를 가진다.
63바이트 두개를 뭉쳐도 128바이트가 나오지 않기 때문에 4개의 파일이 연결되어 있어야 하나의 idat청크를 조합할 수 있는 것이다.

소스코드를 풀이 흐름에 따라 작성했기 때문에 따라가면 금방 이해할 수 있을 듯
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# -*-coding:utf-8 -*-
import os
import zlib
 
ihdr = ""
idat = list()
els = list()
table = list()
 
 
# 의식의 흐름에 따라 흘러가는 코드
# 인덱스 번호를 조합하여 리턴 ex) get_tbl([1,2,3]) == table[1]+table[2]+table[3]
def get_tbl(s):
    result = ""
    for ch in s:
        result += table[ch]
    return result
 
# crc연산에 사용
def unsigned32(n):
  return n & 0xFFFFFFFFL
 
 
files = os.listdir('.')
 
for ch in files:
    tmp = open(ch,'rb').read()
    if len(tmp) == 63:
        table.append(tmp)
        if "IHDR" in tmp:
            ihdr=tmp
        elif "IDAT" in tmp:
            idat.append(tmp)
        else:
            els.append(tmp)
 
header = "\x89\x50\x4E\x47\x0D\x0A\x1A" # png 시그니쳐
header += ihdr  # IHDR 청크 헤더
header += table[6]*2 # PLTE 청크 헤더
header += table[37]  # tRNS 청크 헤더
 
index_list = [59453117352382410]
 
 
idat_list = [[],[],[],[],[],[],[],[],[]]
for ch in idat:
    indx = ch.index("IDAT")
    idat_list[index_list.index(indx)].append(idat.index(ch))
 
 
 
mapping_list = list()
for i in range(8,-1,-1):
    tmp_mapping = list()
    for pre in idat_list[i]:
        for pos in idat_list[i-1]:
            for mid in els:
                tmp = idat[pre]+mid+idat[pos]
                indx = tmp.index("IDAT")
                if "%08x"%unsigned32(zlib.crc32(tmp[indx:indx+132])) == tmp[indx+132:indx+132+4].encode('hex'):
                    tmp_mapping.append([table.index(idat[pre]),table.index(mid),table.index(idat[pos])])
    mapping_list.append(tmp_mapping)
 
# 인덱스 59 - 10, 52 - 3 관계는 4개의 파일이 필요하기 때문에 아직 맵핑되지 않음.
 
# 52 - 3 연결
tmp_mapping = list()
for pre in idat_list[5]:
    for mid in els:
        for pos in els:
            tmp = idat[pre] + mid + pos
            indx = tmp.index("IDAT")
            if "%08x"%unsigned32(zlib.crc32(tmp[indx:indx+132])) == tmp[indx+132:indx+132+4].encode('hex'):
                tmp_mapping.append([table.index(idat[pre]),table.index(mid),table.index(pos)])
 
 
 
 
mapping_list[3= tmp_mapping
 
 
tmp_mapping = list()
for pre in els:
    for mid in els:
        for pos in idat_list[8]:
            tmp = "IDAT" + pre + mid + idat[pos]
            indx = tmp.index("IDAT")
            if "%08x"%unsigned32(zlib.crc32(tmp[indx:indx+132])) == tmp[indx+132:indx+132+4].encode('hex'):
                tmp_mapping.append([table.index(pre),table.index(mid),table.index(idat[pos])])
 
 
mapping_list[8= tmp_mapping
 
 
 
# 인덱스 번호 59로 시작하는 맵핑 연결
for ch in mapping_list[8]:
    tmp = ch
    chk = 0
    while True:
        for i in range(0,8):
            for j in mapping_list[i]:
                if tmp[-1== j[0]:
                    tmp += j[1:]
                    chk = 1
                    del mapping_list[i][mapping_list[i].index(j)]
                    break
            if chk == 1:
                break
        if chk == 0 :
            break
        chk = 0
 
 
 
# 인덱스 번호 3으로 시작하는 맵핑 연결
for ch in mapping_list[4]:
    tmp = ch
    chk = 0
    while True:
        for i in [0,1,2,3,5,6,7,8]:
            for j in mapping_list[i]:
                if tmp[-1== j[0]:
                    tmp += j[1:]
                    chk = 1
                    del mapping_list[i][mapping_list[i].index(j)]
                    break
            if chk == 1:
                break
        if chk == 0 :
            break
        chk = 0
 
# 여기까지 하면 3~ 59~는 다 연결이 되는데 107로 시작하는 링크 하나가 연결되지 않음.
# 107번 인덱스는 IHDR - PLTE-  tRNS로 이어지는 첫 부분이기 때문에 따로 연결이 필요함.
# [107, 85, 35, 147, 140, 32, 118]
# mapping_list 8번 : 7-1-3-2-5-6-0-4
# mapping_list 4번 : 2-6-0-5-4-1-3
result = get_tbl([107853514714032118])
result += get_tbl(mapping_list[8][7])
conn = [[2,1],[6,3],[0,2],[5,5],[4,6],[1,0],[3,4]]
for i,j in conn:
    result += get_tbl(mapping_list[4][i])
    result += get_tbl(mapping_list[8][j])
 
 
 
 
# png footer 없어도 무방
footer = "\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82"
 
f=open('result.png','wb')
f.write(header+result+footer)
f.close()
os.system('mspaint result.png')
cs

풀게 되면 flag가 들어있는 사진을 얻을 수 있다.

flag : ASIS{gj_You_could_Rec0ver_Split3d_PNG_aga1n!!!}