给你压缩包却不给你密码的人到底在想什么

这该死的出题人又只丢给我一个带密码的压缩包,他**的到底想干嘛?

CTF比赛中经常出现这样的问题,如果不能顺利解压真的是件很抓狂的事情。

这次就聊一聊加密的压缩包(内容真的很杂,很乱,很伤眼睛)。


开始

在这里,我建议大家装两个解压软件,一个随意(我用的7z),一个是winrar

嫌右键菜单挤你就装虚拟机里呗。

因为这两个压缩软件压缩出来的zip总是有点不同,最明显的一点,就是在已知明文攻击(后面会说)的时候,两种软件压缩出来的压缩包在构造明文压缩包的时候不能互用。


注释

拿到压缩包上来一定要看有没有注释,一定要看有没有注释,一定要看有没有注释,重要的话说三遍。

有的时候他真的是想送你分,把密码或是hint写在注释里,但你就是不去看他一眼。

用hex方式打开的打一般在末尾

中文注释的话这样可能会乱码。建议压缩软件打开

弱密码

这个其实真的没什么好说的,上来应该先试一试的,因为也不用动脑子,直接放后台跑就行了。

首先先跑纯数字密码,1到9位直接跑一遍,也就1分钟左右的时间吧。

不对的话可以上字典,或是短密码穷举一下,直接丢后台就行,人脑可以再做其他的方向的分析。

另外,如果跑字典都跑不到的话,可以试试此次CTF的名字,或是这个题目的名字。

软件用archpr,网上直接下就行,这里就不分享了。


压缩包伪加密

一个伪加密的压缩包冒充加密压缩包,你要知道压缩软件是如何识别一个压缩包是否被加密的。

软件主要是围绕frFlagsdeFlags来判断的。

我们用winrar创建一个加密的压缩包,可以看到加密的压缩包的frFlags和deFlags都为9。

其中,deFlags是针对单个文件的,压缩包中的每个文件都有。

而未加密的都为0。

用7z创建一个加密的压缩包,frFlags和deFlags都为1。这里就不多放图了。

而未加密的依然都为0。

综上,大家应该已经知道怎么改标志位来构造伪加密以及如何搞定伪加密了。


已知明文攻击

一种比较巧妙的攻击方法,首先你需要一个压缩包中已知的文件(文件大小大于12bytes),比如readme.txt

1
2
3
4
5
├─enc.zip
│ ├─flag.txt *
│ └─readme.txt *

└─readme.txt

这样我们就可以构造明文zip

1
2
├─plaintext.zip
│ └─readme.txt

原理大概是压缩包里的所有文件都是使用同一个加密密钥来加密的,所以可以用已知文件反推加密密钥,利用密钥来解密其他加密文件。

划重点:构造明文压缩包时要选用与加密压缩包相同的压缩软件,如果他用winrar压的,你用7z构造出的压缩包来做明文压缩包,软件是会报错的。

这样就是还原出密钥了,点OK后软件会叫你保存解密后的压缩包。


CRC碰撞

CRC32碰撞用于非常小的文件(6字节以上基本就别试了),就是通过CRC来反推文件内容。

而且CRC32是很容易碰撞的,所以就6字节而言,同一个CRC32可能对应着十几个字符串(纯可视字符)。

当文件刚好是6字节时,使用下面的crc32.py脚本

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
#!/usr/bin/env python
# CRC32 tools by Victor

#usage: python crc32.py reverse 0xffffffff(the crc)

import argparse
import os
import sys

permitted_characters = set(
map(ord, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890_')) # \w

testing = False

args = None


def get_poly():
poly = parse_dword(args.poly)
if args.msb:
poly = reverseBits(poly)
check32(poly)
return poly


def get_input():
if args.instr:
return tuple(map(ord, args.instr))
with args.infile as f: # pragma: no cover
return tuple(map(ord, f.read()))


def out(msg):
if not testing: # pragma: no cover
args.outfile.write(msg)
args.outfile.write(os.linesep)

table = []
table_reverse = []


def init_tables(poly, reverse=True):
global table, table_reverse
table = []
# build CRC32 table
for i in range(256):
for j in range(8):
if i & 1:
i >>= 1
i ^= poly
else:
i >>= 1
table.append(i)
assert len(table) == 256, "table is wrong size"
# build reverse table
if reverse:
table_reverse = []
found_none = set()
found_multiple = set()
for i in range(256):
found = []
for j in range(256):
if table[j] >> 24 == i:
found.append(j)
table_reverse.append(tuple(found))
if not found:
found_none.add(i)
elif len(found) > 1:
found_multiple.add(i)
assert len(table_reverse) == 256, "reverse table is wrong size"
if found_multiple:
out('WARNING: Multiple table entries have an MSB in {0}'.format(
rangess(found_multiple)))
if found_none:
out('ERROR: no MSB in the table equals bytes in {0}'.format(
rangess(found_none)))


def calc(data, accum=0):
accum = ~accum
for b in data:
accum = table[(accum ^ b) & 0xFF] ^ ((accum >> 8) & 0x00FFFFFF)
accum = ~accum
return accum & 0xFFFFFFFF


def rewind(accum, data):
if not data:
return (accum,)
stack = [(len(data), ~accum)]
solutions = set()
while stack:
node = stack.pop()
prev_offset = node[0] - 1
for i in table_reverse[(node[1] >> 24) & 0xFF]:
prevCRC = (((node[1] ^ table[i]) << 8) |
(i ^ data[prev_offset])) & 0xFFFFFFFF
if prev_offset:
stack.append((prev_offset, prevCRC))
else:
solutions.add((~prevCRC) & 0xFFFFFFFF)
return solutions


def findReverse(desired, accum):
solutions = set()
accum = ~accum
stack = [(~desired,)]
while stack:
node = stack.pop()
for j in table_reverse[(node[0] >> 24) & 0xFF]:
if len(node) == 4:
a = accum
data = []
node = node[1:] + (j,)
for i in range(3, -1, -1):
data.append((a ^ node[i]) & 0xFF)
a >>= 8
a ^= table[node[i]]
solutions.add(tuple(data))
else:
stack.append(((node[0] ^ table[j]) << 8,) + node[1:] + (j,))
return solutions

# Tools


def parse_dword(x):
return int(x, 0) & 0xFFFFFFFF


def reverseBits(x):
# http://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel
# http://stackoverflow.com/a/20918545
x = ((x & 0x55555555) << 1) | ((x & 0xAAAAAAAA) >> 1)
x = ((x & 0x33333333) << 2) | ((x & 0xCCCCCCCC) >> 2)
x = ((x & 0x0F0F0F0F) << 4) | ((x & 0xF0F0F0F0) >> 4)
x = ((x & 0x00FF00FF) << 8) | ((x & 0xFF00FF00) >> 8)
x = ((x & 0x0000FFFF) << 16) | ((x & 0xFFFF0000) >> 16)
return x & 0xFFFFFFFF

# Compatibility with Python 2.6 and earlier.
if hasattr(int, "bit_length"):
def bit_length(num):
return num.bit_length()
else:
def bit_length(n):
if n == 0:
return 0
bits = -32
m = 0
while n:
m = n
n >>= 32
bits += 32
while m:
m >>= 1
bits += 1
return bits


def check32(poly):
if poly & 0x80000000 == 0:
out('WARNING: polynomial degree ({0}) != 32'.format(bit_length(poly)))
out(' instead, try')
out(' 0x{0:08x} (reversed/lsbit-first)'.format(poly | 0x80000000))
out(' 0x{0:08x} (normal/msbit-first)'.format(reverseBits(poly | 0x80000000)))


def reciprocal(poly):
''' Return the reversed reciprocal (Koopman notatation) polynomial of a
reversed (lsbit-first) polynomial '''
return reverseBits((poly << 1) | 1)


def print_num(num):
''' Write a numeric result in various forms '''
out('hex: 0x{0:08x}'.format(num))
out('dec: {0:d}'.format(num))
out('oct: 0o{0:011o}'.format(num))
out('bin: 0b{0:032b}'.format(num))

import itertools


def ranges(i):
for kg in itertools.groupby(enumerate(i), lambda x: x[1] - x[0]):
g = list(kg[1])
yield g[0][1], g[-1][1]


def rangess(i):
return ', '.join(map(lambda x: '[{0},{1}]'.format(*x), ranges(i)))

# Parsers


def get_parser():
''' Return the command-line parser '''
parser = argparse.ArgumentParser(
description="Reverse, undo, and calculate CRC32 checksums")
subparsers = parser.add_subparsers(metavar='action')

poly_flip_parser = argparse.ArgumentParser(add_help=False)
subparser_group = poly_flip_parser.add_mutually_exclusive_group()
subparser_group.add_argument(
'-m', '--msbit', dest="msb", action='store_true',
help='treat the polynomial as normal (msbit-first)')
subparser_group.add_argument('-l', '--lsbit', action='store_false',
help='treat the polynomial as reversed (lsbit-first) [default]')

desired_poly_parser = argparse.ArgumentParser(add_help=False)
desired_poly_parser.add_argument(
'desired', type=str, help='[int] desired checksum')

default_poly_parser = argparse.ArgumentParser(add_help=False)
default_poly_parser.add_argument(
'poly', default='0xEDB88320', type=str, nargs='?',
help='[int] polynomial [default: 0xEDB88320]')

accum_parser = argparse.ArgumentParser(add_help=False)
accum_parser.add_argument(
'accum', type=str, help='[int] accumulator (final checksum)')

default_accum_parser = argparse.ArgumentParser(add_help=False)
default_accum_parser.add_argument(
'accum', default='0', type=str, nargs='?',
help='[int] starting accumulator [default: 0]')

outfile_parser = argparse.ArgumentParser(add_help=False)
outfile_parser.add_argument('-o', '--outfile',
metavar="f",
type=argparse.FileType('w'),
default=sys.stdout,
help="Output to a file instead of stdout")

infile_parser = argparse.ArgumentParser(add_help=False)
subparser_group = infile_parser.add_mutually_exclusive_group()
subparser_group.add_argument('-i', '--infile',
metavar="f",
type=argparse.FileType('rb'),
default=sys.stdin,
help="Input from a file instead of stdin")
subparser_group.add_argument('-s', '--str',
metavar="s",
type=str,
default='',
dest='instr',
help="Use a string as input")

subparser = subparsers.add_parser('flip', parents=[outfile_parser],
help="flip the bits to convert normal(msbit-first) polynomials to reversed (lsbit-first) and vice versa")
subparser.add_argument('poly', type=str, help='[int] polynomial')
subparser.set_defaults(
func=lambda: print_num(reverseBits(parse_dword(args.poly))))

subparser = subparsers.add_parser('reciprocal', parents=[outfile_parser],
help="find the reciprocal (Koopman notation) of a reversed (lsbit-first) polynomial and vice versa")
subparser.add_argument('poly', type=str, help='[int] polynomial')
subparser.set_defaults(func=reciprocal_callback)

subparser = subparsers.add_parser('table', parents=[outfile_parser,
poly_flip_parser,
default_poly_parser],
help="generate a lookup table for a polynomial")
subparser.set_defaults(func=table_callback)

subparser = subparsers.add_parser('reverse', parents=[
outfile_parser,
poly_flip_parser,
desired_poly_parser,
default_accum_parser,
default_poly_parser],
help="find a patch that causes the CRC32 checksum to become a desired value")
subparser.set_defaults(func=reverse_callback)

subparser = subparsers.add_parser('undo', parents=[
outfile_parser,
poly_flip_parser,
accum_parser,
default_poly_parser,
infile_parser],
help="rewind a CRC32 checksum")
subparser.add_argument('-n', '--len', metavar='l', type=str,
default='0', help='[int] number of bytes to rewind [default: 0]')
subparser.set_defaults(func=undo_callback)

subparser = subparsers.add_parser('calc', parents=[
outfile_parser,
poly_flip_parser,
default_accum_parser,
default_poly_parser,
infile_parser],
help="calculate the CRC32 checksum")
subparser.set_defaults(func=calc_callback)

return parser


def reciprocal_callback():
poly = parse_dword(args.poly)
check32(poly)
print_num(reciprocal(poly))


def table_callback():
# initialize tables
init_tables(get_poly(), False)
# print table
out('[{0}]'.format(', '.join(map('0x{0:08x}'.format, table))))


def reverse_callback():
# initialize tables
init_tables(get_poly())
# find reverse bytes
desired = parse_dword(args.desired)
accum = parse_dword(args.accum)
# 4-byte patch
patches = findReverse(desired, accum)
for patch in patches:
out('4 bytes: {{0x{0:02x}, 0x{1:02x}, 0x{2:02x}, 0x{3:02x}}}'.format(*patch))
checksum = calc(patch, accum)
out('verification checksum: 0x{0:08x} ({1})'.format(
checksum, 'OK' if checksum == desired else 'ERROR'))
# 6-byte alphanumeric patches
for i in permitted_characters:
for j in permitted_characters:
patch = [i, j]
patches = findReverse(desired, calc(patch, accum))
for last_4_bytes in patches:
if all(p in permitted_characters for p in last_4_bytes):
patch.extend(last_4_bytes)
out('alternative: {1}{2}{3}{4}{5}{6} ({0})'.format(
'OK' if calc(patch, accum) == desired else 'ERROR', *map(chr, patch)))


def undo_callback():
# initialize tables
init_tables(get_poly())
# calculate checksum
accum = parse_dword(args.accum)
maxlen = int(args.len, 0)
data = get_input()
if not 0 < maxlen <= len(data):
maxlen = len(data)
out('rewinded {0}/{1} ({2:.2f}%)'.format(maxlen, len(data),
maxlen * 100.0 / len(data) if len(data) else 100))
for solution in rewind(accum, data[-maxlen:]):
out('')
print_num(solution)


def calc_callback():
# initialize tables
init_tables(get_poly(), False)
# calculate checksum
accum = parse_dword(args.accum)
data = get_input()
out('data len: {0}'.format(len(data)))
out('')
print_num(calc(data, accum))


def main(argv=None):
''' Runs the program and handles command line options '''
parser = get_parser()

# Parse arguments and run the function
global args
args = parser.parse_args(argv)
args.func()

if __name__ == '__main__':
main() # pragma: no cover

示例:

当字节数小于6时,用下面的crack.py脚本(用python3):

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
#!/usr/bin/env python3
import sys
import os
import string
import collections

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('file', nargs='*')
parser.add_argument('--hex', action='append')
parser.add_argument('--dec', action='append')
parser.add_argument('--limit', type=int)
parser.add_argument('--compiler', default='g++')
parser.add_argument('--alphabet', type=os.fsencode, default=string.printable.encode())
args = parser.parse_args()

targets = collections.OrderedDict()
limit = 0
crcs = []

if args.limit:
limit = max(limit, args.limit)
if args.hex or args.dec:
if not args.limit:
parser.error('Limit of length not specified')

if args.hex:
for s in args.hex:
crc = int(s, 16)
targets[s] = crc
for l in range(args.limit + 1):
crcs += [( crc, l )]
if args.dec:
for s in args.dec:
crc = int(s)
targets[s] = crc
for l in range(args.limit + 1):
crcs += [( crc, l )]

if args.file:
print('reading zip files...', file=sys.stderr)
import zipfile
for zipname in args.file:
fh = zipfile.ZipFile(zipname)
for info in fh.infolist():
targets['%s / %s' % ( zipname, info.filename )] = ( info.CRC, info.file_size )
crcs += [( info.CRC, info.file_size )]
limit = max(limit, info.file_size)
print('file found: %s / %s: crc = 0x%08x, size = %d' % (zipname, info.filename, info.CRC, info.file_size), file=sys.stderr)

if not crcs:
parser.error('No CRCs given')

# compiling c++ in python script is the easy way to have the both a good interface and better speed
code = ''
code += r'''
#include <cstdio>
#include <vector>
#include <array>
#include <string>
#include <set>
#include <cstdint>
#include <cctype>
#define repeat(i,n) for (int i = 0; (i) < (n); ++(i))
using namespace std;

uint32_t crc_table[256];
void make_crc_table() {
repeat (i, 256) {
uint32_t c = i;
repeat (j, 8) {
c = (c & 1) ? (0xedb88320 ^ (c >> 1)) : (c >> 1);
}
crc_table[i] = c;
}
}
const uint32_t initial_crc32 = 0xffffffff;
uint32_t next_crc32(uint32_t c, char b) {
return crc_table[(c ^ b) & 0xff] ^ (c >> 8);
}
const uint32_t mask_crc32 = 0xffffffff;

const char alphabet[] = { ''' + ', '.join(map(str, args.alphabet)) + r''' };
const int limit = ''' + str(limit) + r''';

array<set<uint32_t>, limit+1> crcs;
string stk;
void dfs(uint32_t crc) {
if (crcs[stk.length()].count(crc ^ mask_crc32)) {
fprintf(stderr, "crc found: 0x%08x: \"", crc ^ mask_crc32);
for (char c : stk) fprintf(stderr, isprint(c) && (c != '\\') ? "%c" : "\\x%02x", c);
fprintf(stderr, "\"\n");
printf("%08x ", crc ^ mask_crc32);
for (char c : stk) printf(" %02x", c);
printf("\n");
}
if (stk.length() < limit) {
for (char c : alphabet) {
stk.push_back(c);
dfs(next_crc32(crc, c));
stk.pop_back();
}
}
}

int main() {
'''
for crc, size in crcs:
code += ' crcs[' + str(size) + '].insert(' + hex(crc) + ');\n'
code += r'''
make_crc_table();
dfs(initial_crc32);
return 0;
}
'''

import tempfile
import subprocess
with tempfile.TemporaryDirectory() as tmpdir:
cppname = os.path.join(tmpdir, 'a.cpp')
with open(cppname, 'w') as fh:
fh.write(code)
binname = os.path.join(tmpdir, 'a.out')
print('compiling...', file=sys.stderr)
p = subprocess.check_call([args.compiler, '-std=c++11', '-O3', '-o', binname, cppname])
print('searching...', file=sys.stderr)
p = subprocess.Popen([binname], stdout=subprocess.PIPE)
output, _ = p.communicate()

print('done', file=sys.stderr)
print(file=sys.stderr)
result = collections.defaultdict(list)
for line in output.decode().strip().split('\n'):
crc, *val = map(lambda x: int(x, 16), line.split())
result[( crc, len(val) )] += [ bytes(val) ]
for key, crc in targets.items():
for s in result[crc]:
print('%s : %s' % (key, repr(s)[1:]))

示例:


MORE

还有很多压缩包的密码需要联系题中的其他文件来解,这个就不在本篇的讨论范围内了,故不再讨论。