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

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

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

├─enc.zip
│  ├─flag.txt *
│  └─readme.txt *
│
└─readme.txt

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

├─plaintext.zip
│  └─readme.txt

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

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

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


CRC碰撞

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

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

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

#!/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):

#!/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

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