ACE文件格式及改路径

最近winrar报了个洞,主要是解压ace文件的模块,年久失修。就研究了一把,记录下。

ace文件主要由MainHeader和多个FileHeader组成。
MainHeader里面主要包含信息:

header
hdr_crc 0xd3f1
hdr_size 44
hdr_type 0x00 MAIN
hdr_flags 0x8100 V20FORMAT|SOLID
magic b'**ACE**'
eversion 20 2.0
cversion 20 2.0
host 0x02 Win32
volume 0
datetime 0x4e556ccf 2019-02-21 13:38:30
reserved1 30 93 66 76 4e 20 00 00
advert b''
comment b''
reserved2 b'\x00\x00\x00H5U\x03]6 \x9e\x10\xfd\xc9\xfbErs'

其结构如下:

[0:2] hcrc MainHeader CRC == ace_crc16(file.read(4:hsize))
[2:4] hsize MainHeader Size
[4:5] htype header_type 0 ==> mainheader
[5:7] hflags
[7:14] == MainHeader.MAGIC == \*\*ACE**
[14:15] eversion 版本
[15:16] cversion 版本
[16:18] host
[18:20] volume
[20:22] datetime
[22:30] reserved1 保留位1
[30:31] avsz advert size 如果有的话
[31:31 + avsz] advert 内容 如果有的话
[31 + avsz: 33+avsz] cmsz comment size 如果有的话
[33 + avsz: 33 + avsz + cmsz] comment 内容如果有的话
[33 + avsz + cmsz: 4 + hsize] reserved2 保留位2

文件只能有一个MainHeader,可以有多个FIleHeader。FileHeader中,主要包括信息如下:

header
hdr_crc 0xd0f9
hdr_size 57
hdr_type 0x01 FILE32
hdr_flags 0x8001 ADDSIZE|SOLID
packsize 6
origsize 6
datetime 0x4e5554f4 2019-02-21 10:39:40
attribs 0x00000020 ARCHIVE
crc32 0xf68d2c9e
comptype 0x00 stored
compqual 0x03 normal
params 0x000a
reserved1 0x9e20
filename b'Users\\vs\\Desktop\\demo1.txt'
comment b''
ntsecurity b''
reserved2 b''

同样,分析结构,前8个字节含义相同:

[0:2] hcrc FileHeader CRC == ace_crc16(file.read(4:hsize))
[2:4] hsize FileHeader Size
[4:5] htype header_type 1 ==> fileheader
[5:7] hflags 0x8001 ADDSIZE|SOLID

此后,会根据文件是32位还是64位做不同的处理。hflags必须是 0x8001 。
64:

[7:15] packsize 压缩后大小
[15:23] origsize 源大小
[23:27] datetime 时间
[27:31] attribs 属性 一般0x00000020
[31:35] crc32
[35:36] comptype 压缩类型 0x00 stored
[36:37] compqual 0x03 normal
[37:39] params 参数
[39:41] reserved1 保留1
[41:43] fnsz 文件名大小
[43: 43+fnsz] filename 文件名
[43+fnsz: 45+fnsz] cmsz comment size 如果有的话
[45+fnsz: 45+fnsz+cmsz] comment 内容如果有的话
[45+fnsz+cmsz: 47+fnsz+cmsz] nssz ntsecurity size
[47+fnsz+cmsz: 47+fnsz+cmsz+nssz] ntsecurity 内容如果有的话
[47+fnsz+cmsz+nssz: 4+hsize] reserved2 保留位2
[4+hsize: 4+hsize+packsize] 文件内容

32:

[7:11] packsize 压缩后大小
[11:15] origsize 源大小
[15:19] datetime 时间
[19:23] attribs 属性 一般0x00000020
[23:27] crc32
[27:28] comptype 压缩类型 0x00 stored
[28:29] compqual 0x03 normal
[29:31] params 参数
[31:33] reserved1 保留1
[33:35] fnsz 文件名大小
[35: 35+fnsz] filename 文件名
[35+fnsz: 37+fnsz] cmsz comment size 如果有的话
[37+fnsz: 37+fnsz+cmsz] comment 内容如果有的话
[37+fnsz+cmsz: 39+fnsz+cmsz] nssz ntsecurity size
[39+fnsz+cmsz: 39+fnsz+cmsz+nssz] ntsecurity 内容如果有的话
[39+fnsz+cmsz+nssz:] reserved2 保留位2
[4+hsize: 4+hsize+packsize] 文件内容

可能解析代码有问题,其中还有文件内容部分,并没有在文件结构中展示出来。
其次,文件头部size似乎不影响解析。

重点在两个CRC校验。
第一个是对头部的校验:用的AceCRC16。

class AceCRC16(AceCRC32):
"""
Calculate an ACE CRC-16 checksum, which is actually just the lower 16 bits
of an ACE CRC-32.
>>> crc = AceCRC16()
>>> crc += b"12345"
>>> crc += b"6789"
>>> crc.sum
50905
>>> crc == 50905
True
"""
def __str__(self):
"""
String representation of object is hex value of checksum.
"""
return "0x%04x" % self.sum
@property
def sum(self):
"""
The checksum.
"""
return super().sum & 0xFFFF

另一个是文件部分的CRC校验,用的是CRC32,而且,内容部分的CRC校验主要是验证有密码的情况。

所以,要更改生成指定的路径的ace文件,可以改完算CRC16,和改完的长度即可。
提供python脚本如下:

#!/usr/bin/env python3
# vim: set list et ts=8 sts=4 sw=4 ft=python:
import sys
import acefile
import builtins
import struct
def read_main_header(filename):
buf = builtins.open(filename, 'rb').read(4)
hcrc, hsize = struct.unpack('<HH', buf)
mainheader = buf = builtins.open(filename, 'rb').read(4 + hsize)
return mainheader
def read_header(filename, seek, datasize):
_fin = builtins.open(filename, 'rb')
_fin.seek(seek)
buf = _fin.read(4)
hcrc, hsize = struct.unpack('<HH', buf)
_fin.seek(seek)
fileheader = _fin.read(4 + hsize + datasize)
return fileheader
def replace_filename(header_content, filename_offset, oldfile, newfile):
tmp1 = header_content[0:filename_offset - 2]
tmp2 = struct.pack('<H', len(newfile))
tmp3 = newfile
tmp4 = header_content[filename_offset+len(oldfile):]
result = tmp1+tmp2+tmp3+tmp4
if len(result) < 30:
print('FileName too Short!')
sys.exit()
return result
def construct_file(main_header, file_headers_content, file_headers):
tmp = main_header
header_num = 0
for file_header_content in file_headers_content:
header_content = file_header_content[4:-len(file_headers[header_num].dataoffile)]
crc16 = acefile.ace_crc16(header_content)
header_num += 1
length = len(header_content)
tmp += struct.pack('<H',crc16)
tmp += struct.pack('<H',length)
tmp += file_header_content[4:]
# print(tmp)
with open('tmp.rar', 'wb') as _fout:
_fout.write(tmp)
if __name__ == '__main__':
filename = sys.argv[1]
main_header = read_main_header(filename)
ace = acefile.AceVolume(filename, 'r')
file_headers = ace.get_file_headers()
file_headers_content = []
file_num = 0
seek = len(main_header)
print("="*30)
for header in file_headers:
print("[%s] %s" % (file_num, str(header.filename, encoding='utf-8')))
file_num += 1
header.dataoffile = ace.file_segment_for(header).read()
header_content = read_header(filename, seek, len(header.dataoffile))
file_headers_content.append(header_content)
seek += len(header_content)
try:
fn = int(input("Which filename you want to change? > "))
if fn > len(file_headers):
print("Wrong Input!")
sys.exit()
except Exception as e:
sys.exit()
header = file_headers[fn]
newfile = str.encode(input("Plz input a new filename > "))
file_offset = file_headers_content[fn].find(header.filename)
file_headers_content[fn] = replace_filename(file_headers_content[fn], file_offset, header.filename, newfile)
construct_file(main_header, file_headers_content, file_headers)
print("Done! new rar file is: tmp.rar")

——Tracy_梓朋
2019年2月22日13:04:48