PK ZIP已知明文攻击

使用条件

前提:加密算法是 ZipCrypto

例:

alt text alt text

winrar、7z 默认使用 AES256 360压缩、2345好压 默认使用 ZipCrypto

压缩格式:zip、7z、xz、gzip、tar
压缩等级:0、1、3、5、7、9
压缩方法:Deflate、Deflate64、Bzip2、LZMA
加密算法:ZipCrypto、AES-256

题目中已知明文攻击使用

1、查看压缩包信息

zipinfo crypto.zip bkcrack -L crypto.zip

alt text

2、一般形式明文攻击

已知明文信息
bkcrack -C crypto.zip -c 123.txt -p 123
bkcrack -C crypto.zip -c 123.txt -p 123

“明文” 是指压缩后、加密前的内容,该压缩包恰好使用了 “仅存储”(Store)的压缩方式,所以明文就是文件的内容。

#提出压缩包内任意文件
bkcrack -C crypto.zip -c 123.pcap -k da3cda9d 4cbfcb95 cfaa7211  -d  456.pcap

#如果是压缩后的文件
bkcrack -C secrets.zip -c 123.pcap -k da3cda9d 4cbfcb95 cfaa7211 -d 456.pcap.deflate
python3 tools/inflate.py < 456.pcap.deflate > 456.pcap

#重置压缩包密码
 bkcrack -C crypto.zip -k da3cda9d 4cbfcb95 cfaa7211 -U secrets_with_new_password.zip ""

#尝试获取原密码
bkcrack -k da3cda9d 4cbfcb95 cfaa7211 --recover-password 10 ?p
bkcrack -k da3cda9d 4cbfcb95 cfaa7211 --bruteforce ?b --length 0..9

alt text

例题:
challenge1

alt text alt text bkcrack -C challenge_1.zip -c chromedriver_linux64.zip -p chromedriver_linux64.zip -U cha1_dec.zip "" 密码置空

challenge2

alt text

.pcapng 格式文件 pcapng 的开头部分是一个叫 Section Header Block 的数据结构: alt text

好的,到这一步我们已经能够肯定文件头一定是 0a0d0d0a____00004d3c2b1a01000000,有 14 个字节,其中 10 个字节连续。 bkcrack -C challenge_2.zip -c flag2.pcapng -p pcap_header.bin -o 6 -x 0 0a0d0d0a -U challenge_2_dec.zip "" (其中 pcap_header.bin 存储的内容如下:) alt text 如果大胆猜测了文件长度字段是 ffffffffffffffff,它会跑得更快一些。

实战中已知明文攻击使用

当有文件仅存储时,通过扩展名推测明文

.svg .xml 文件 开头为<?xml version="1.0"

echo -n '<?xml version="1.0" ' > plain.txt
bkcrack -C secrets.zip -c spiral.svg -p plain.txt

.pcapng文件

bkcrack -C challenge_2.zip -c flag2.pcapng -p pcap_header.bin -o 6 -x 0 0a0d0d0a -U challenge_2_dec.zip ""

#  pcap_header.bin  = 00004d3c2b1a01000000  or 00004d3c2b1a01000000ffffffffffffffff

.png文件

echo 89504E470D0A1A0A0000000D49484452 | xxd -r -ps > png_header
bkcrack -C png4.zip -c 2.png -p png_header -o 0

.zip 文件 ZIP文件会包含其中文件名,知道或可以猜测该压缩包内的文件名称 一般来说,单个文件打包成zip压缩文件时,压缩包名默认为文件名,且偏移值固定为30byte。

echo -n "flag.txt" > plain1.txt
bkcrack -C test5.zip -c flag.zip -p plain1.txt -o 30  -x 0 504B0304 >1.log


bkcrack  -C 20240103BBB.zip -c 20240103AAA.zip -p head.bin -o 30 -x 0 504B0304 >2.log

echo -n -e "\xf6<<< Oracle VM VirtualBox Disk Image >>>" > plain.txt
bkcrack -C Challange.zip -c oracle.vdi -p plain.txt -o -1

https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.9.TXT
压缩包在存储的文件数据之前添加了一个12字节的加密标头,加密标头的最后一个字节是文件CRC值的最高位字节 alt text

Signature标志):4个字节表示局部文件头的标志对于非加密文件这个值为 0x04034b50
Version Needed To Extract需要解压的版本):2个字节表示解压所需的最低ZIP规范版本
General Purpose Bit Flag通用位标志):2个字节包含一些通用标志其中第13位从右向左数表示是否使用了加密如果加密则这个位的值为1否则为0
Compression Method压缩方法):2个字节表示文件的压缩方法
Last Mod File Time最后修改文件时间):2个字节表示文件最后修改的时间
Last Mod File Date最后修改文件日期):2个字节表示文件最后修改的日期
CRC-32CRC校验值):4个字节表示文件内容的CRC-32校验值
Compressed Size压缩后的大小):4个字节表示文件压缩后的大小
Uncompressed Size未压缩的大小):4个字节表示文件未压缩时的大小
Filename Length文件名长度):2个字节表示文件名的长度
Extra Field Length额外字段长度):2个字节表示额外字段的长度
Encrypted Data加密数据):12个字节只有在通用位标志中的第13位为1时才存在加密数据这12个字节包含加密算法标识符和加密所需的一些信息

通过压缩过的文件进行明文攻击

条件: 适用于全部文件都已经压缩过,需要找到其中一个原始文件。 步骤:

先压缩文件 zip -5 deflate_jpg.zip origin.jpg 直接明文攻击可能失败 bkcrack -C secrets.zip -c secrets.jpg -P deflate_jpg.zip -p origin.jpg 寻找zip不同压缩等级 alt text
alt text alt text 测试不同的压缩格式

import os
import subprocess

def remove_old_zip(zip_filename):
    try:
        os.remove(zip_filename)
    except FileNotFoundError:
        pass  # Ignore if the file doesn't exist

def create_new_zip(zip_filename, level, source_file):
    subprocess.run(f"zip -{level} {zip_filename} {source_file} > /dev/null", shell=True)

def perform_known_plaintext_attack(plaintext, compressed_filename, target_zip):
    subprocess.run(f"bkcrack -C {target_zip} -c {compressed_filename} -P {compressed_filename} -p {plaintext}", shell=True)

if __name__ == "__main__":
    plaintext_file = "origin.jpg"
    compressed_filename = "deflate_jpg.zip"
    target_zip = "Desktop.zip"

    for level in range(10):
        print(f"\nLevel {level}:")

        # Remove old zip file
        remove_old_zip(compressed_filename)

        # Create new zip file
        create_new_zip(compressed_filename, level, plaintext_file)

        # Perform known plaintext attack
        perform_known_plaintext_attack(plaintext_file, compressed_filename, target_zip)

测试不同的压缩格式

import subprocess
import os

def remove_old_zip(zip_filename):
    try:
        os.remove(zip_filename)
    except FileNotFoundError:
        pass  # Ignore if the file doesn't exist

def create_new_zip(zip_filename, level, source_file):
    subprocess.run(f"7z a -tzip -mx{level} {zip_filename} {source_file} > /dev/null", shell=True)

def perform_known_plaintext_attack(plaintext, compressed_filename, target_zip):
    subprocess.run(f"bkcrack -C {target_zip} -c {compressed_filename} -P {compressed_filename} -p {plaintext}", shell=True)

if __name__ == "__main__":
    plaintext_file = "very_good_advice.jpg"
    compressed_filename = "very_good_advice.jpg.zip"
    target_zip = "secrets.zip"

    for level in range(1, 10):
        print(f"\nLevel {level}:")

        # Remove old zip file
        remove_old_zip(compressed_filename)

        # Create new zip file
        create_new_zip(compressed_filename, level, plaintext_file)

        # Perform known plaintext attack
        perform_known_plaintext_attack(plaintext_file, compressed_filename, target_zip)

ZipCrypto

ZIP 的传统加密,本质上也是异或加密。当然,不是用 password 异或,而是用一个伪随机数流来和明文进行异或。而产生这个伪随机数流,需要用到三个 keys,下文分别以 x,y,z 代指。这三个 keys 非常重要,加密解密过程实质上只需要这三个 keys,密码的作用其实是初始化这三个 keys。简要介绍一下加密流程:在加密前,首先会用密码作为种子初始化这个伪随机数流,然后每加密一个 byte,都会用这个 byte 作为输入产生下一个伪随机数(这个随机数称为 k )。解密过程也是差不多的,首先初始化伪随机数流,然后每解密一个 byte,都用解密后的 byte 作为输入产生下一个伪随机数。

/// 贯穿加密算法的三个 keys
/// 加密/解密每个数据块时都会初始化一次 keys
/// 拿到 keys 不一定能还原出密码,但已经足够进行加密/解密了
pub struct Keys {
    x: u32,
    y: u32,
    z: u32,
}

impl Keys {
    /// 使用密码初始化 keys
    pub fn new(password: &[u8]) -> Self {
        // 首先,使用三个魔术常量初始化 Keys
        // (这常量真随便
        let mut keys = Self {
            x: 0x1234_5678,
            y: 0x2345_6789,
            z: 0x3456_7890,
        };

        // 然后,获取密码的字节形式,并用它们更新 Keys
        for &c in password {
            keys.update_keys(c);
        }

        keys
        // 准备工作就绪了,接下来可以用 keys 来加密//解密文件了
    }

    /// 加密算法最核心的部分 (超短
    /// 在加密/解密过程中, 这个函数会被不断调用,以更新 keys
    fn update_keys(&mut self, c: u8) {
        self.x = crc32(self.x, c);
        // .wrapping_xxx(), 即允许溢出的运算
        // 虽然 release 模式下默认不检查溢出,但这样写显得严谨
        self.y = (self.y + u32::from(lsb(self.x))).wrapping_mul(0x0808_8405).wrapping_add(1);
        self.z = crc32(self.z, msb(self.y));
    }

    /// 对外提供的解密函数
    pub fn decrypt(&mut self, data: &mut [u8]) {
        for c in data.iter_mut() {
            let p = *c ^ self.get_k();
            self.update_keys(p);
            *c = p;
        }
    }

    /// 不知道这函数该叫啥。。。功能是从 keys 中计算出一个用来加密/解密的 byte
    /// 是个实际操作中可以打表的函数
    fn get_k(&mut self) -> u8 {
        // 标准中这里是 `| 2`, 其实效果都一样,毕竟 `2 ^ 1 == 3`, `3 ^ 1 == 2`
        // 不过写成 `| 3` 有助于后续结论的推导
        let temp = (self.z | 3) as u16;
        lsb(((temp * (temp ^ 1)) >> 8).into())
    }

    /// 对外提供的加密函数
    pub fn encrypt(&mut self, data: &mut [u8]) {
        // 和解密过程基本一样(毕竟异或
        for p in data.iter_mut() {
            let c = *p ^ self.get_k();
            self.update_keys(*p);
            *p = c;
        }
    }
}

/// 朴实的 CRC32 函数,实际操作中一般都会打表
/// 其中 0xEDB8_8320 是 ZIP 标准规定魔术常量
fn crc32(old_crc: u32, c: u8) -> u32 {
    let mut crc = old_crc ^ c as u32;
    for _ in 0..8 {
        if crc % 2 != 0 {
            crc = crc >> 1 ^ 0xEDB8l_8320;    
        } else {
            crc = crc >> 1;
        }
    }
    crc
}

/// 朴实的 lsb 函数,注意是最低有效字节,不是最低有效位
fn lsb(x: u32) -> u8 {
    x as u8
}

/// 朴实的 msb 函数,注意是最高有效字节,不是最高有效位
fn msb(x: u32) -> u8 {
    (x >> 24) as u8
}

// 展示一下用法
fn main() {
    let mut keys = Keys::new("123456");
    let mut data = b"Illyasviel von Einzbern".bytes().collect::<Vec<_>>();
    println!("加密前:\n{:?}\n{}", data, String::from_utf8_lossy(&data));

    keys.encrypt(&mut data);
    println!("加密后:\n{:?}\n{}", data, String::from_utf8_lossy(&data));

    // 注意要重新初始化 Keys
    let mut keys = Keys::new("123456");
    keys.decrypt(&mut data);
    println!("解密后:\n{:?}\n{}", data, String::from_utf8_lossy(&data));
}

https://www.aloxaf.com/2019/04/zip_plaintext_attack/#%E6%98%8E%E6%96%87%E6%94%BB%E5%87%BB https://link.springer.com/content/pdf/10.1007/3-540-60590-8_12.pdf

要想攻击首先需要知道 ZipCrypto 的加密方式。这个加密算法是面向字节流的(而现代对称加密算法是对一个分组进行加密),它内部使用了 3 个 32 比特的整数来表示密钥,可以将它们称为 key0,key1 和 key2。通常我们在打包文件时输入的密码会被转换成这 3 个 dword。算法基本就是用一个循环来加密数据,加密函数记为 UpdateKeys,cpp 代码如下

// p 是用来更新内部 key 的明文
// lsb、msb 分别表示取 dword 的最低和最高字节(不是位)
void PKCipher::UpdateKeys(uint8_t p) {
    k0 = crc::Crc32(k0, p);
    k1 = (k1 + crc::lsb(k0)) * 134775813 + 1;    // 134775813 是 magic number
    k2 = crc::Crc32(k2, crc::msb(k1));
}
uint8_t PKCipher::GetK3() {
    uint16_t tmp = k2 | 3;
    return crc::lsb((tmp * (tmp ^ 1)) >> 8);
}
void PKCipher::Encrypt(std::vector<uint8_t>& data) {
    for (uint8_t& p : data) {
        uint8_t c = p ^ GetK3();
        UpdateKeys(p);
        p = c;
    }
}

UpdateKeys 仅仅是更新了内部 key,真正对明文进行加密用的是从 key2 派生出来的 16 比特的 key3。可以看到,不仅 key3 只有 16 比特长,它的低两位因为和 3 做了位与操作所以必然是 1,使得 key3 的取值范围缩小到 214,这就有了反推的基础。 https://flandre-scarlet.moe/blog/1685/

结论

知道其中一个文件的明文(很常见,尤其是当压缩一些程序的时候,经常会混入一些已知的 DLL 或者 LICENSE.txt),或者知道一个以 “仅存储” 方式压缩的文件的 12 个字节(也很常见,因为 ZIP、JPEG、PNG 这种格式经常以 “仅存储” 方式压缩,而且在文件头和文件尾也很容易凑出 12 字节的已知明文,还有一种情况就是数据量大的压缩包经过两次压缩之后,就不再压缩文件,仅进行存储,所以才有机可乘。