알집에서 EGG 파일에 암호를 설정할 때, 난수로 채워져야 하는 부분이 잘못된 타입 캐스팅 때문에 항상 0 또는 1로 결정되는 취약점으로, 이로 인해 난수로 채워지는 부분에 대해 256^10의 경우의 수가 발생해야 하는데, 실제로는 2^10의 경우의 수만 발생하게 되어, Known Plaintext Attack에 대한 공격 복잡도가 크게 감소합니다. 또한 기본옵션인 “최적압축” 옵션으로 특정 확장자(docx, pptx, xlsx, png, 7z, gz 등.) 의 파일이 같이 압축될 시에는 공격의 복잡도가 추가로 감소합니다.
EGG 파일은 압축파일에 비밀번호를 설정했을 때 아래 3가지 옵션을 지원합니다.
이 중 Zip 2.0 Compatible
옵션은 ZipCrypto 혹은 PKZip StreamCipher 라고 불리는 암호를 의미하며, Zip 파일에서도 사용됩니다.
해당 암호의 간단한 Python 구현은 아래와 같습니다.
class PKZIPStreamCipher:
def __init__(self, password):
self.keys = [0x12345678, 0x23456789, 0x34567890]
for char in password:
self.update_keys(char)
def update_keys(self, char):
self.keys[0] = self.crc32(self.keys[0], char)
self.keys[1] = (self.keys[1] + (self.keys[0] & 0xFF)) & 0xFFFFFFFF
self.keys[1] = (self.keys[1] * 0x08088405 + 1) & 0xFFFFFFFF
self.keys[2] = self.crc32(self.keys[2], self.keys[1] >> 24)
def crc32(self, old_crc, char):
POLY = 0xedb88320
crc = old_crc ^ char
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ POLY
else:
crc >>= 1
return crc & 0xFFFFFFFF
def decrypt_byte(self):
temp = (self.keys[2] & 0xFFFF) | 2
return ((temp * (temp ^ 1)) >> 8) & 0xFF
def encrypt(self, plaintext):
ciphertext = []
for char in plaintext:
k = char ^ self.decrypt_byte()
self.update_keys(char)
ciphertext.append(k)
return bytes(ciphertext)
def decrypt(self, ciphertext):
plaintext = []
for char in ciphertext:
k = char ^ self.decrypt_byte()
self.update_keys(k)
plaintext.append(k)
return bytes(plaintext)
password = b"452345gedH"
cipher = PKZIPStreamCipher(password)
encrypted_data = cipher.encrypt(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7B\x74")
print(encrypted_data)
구현에서 보는 바와 같이 태생이 스트림 암호이기 때문에, 압축파일에서 사용할 때는 실제 데이터 암호화에 앞서 무작위한 값 12바이트(혹은 무작위 값 10바이트 + 2바이트 CRC로 구성될 수 있음.) 를 암호화 한 후 사용합니다. (참고 : 7-zip의 해당 부분 구현)
이 암호는 1994년 Biham 과 Kocher에 의해 Known plaintext attack에 취약함이 알려졌습니다. 하지만 해당 공격에서 이야기하는 평문은 압축된 데이터이기 때문에, 데이터를 압축(Deflate 등) 해서 저장하였다면, 특정 파일 전체를 아는것이 아닐 경우, 파일 헤더등 파일 내용의 일부를 알아도 공격이 쉽지 않습니다.
한편, 알집으로 압축할 때, EGG 포맷에서는 "최적압축"이 기본옵션인데, 이 기능은 확장자 별로 압축 알고리즘을 다르게 적용하는 기능입니다.
(개인적으로 이 기능은 확장자가 아니라 파일의 엔트로피값을 이용하여 구현하여야 한다고 생각합니다.)
알고리즘 | 확장자 |
---|---|
LZMA | .ppt, .xls, .doc, .ani, .ico, .cur, .pcx, .emf, |
AZO | .sys, .com, |
Store | .ace, .alz, .arc, .arj, .bz, .bz2, .cab, .egg, .ice, .ear, .war, .gz, .ha, .jar, .lha, .lzh, .pak, .rar, .tbz, .tbz2, .tgz, .7z, .z, .zip, .zoo, .docx, .xlsx, .pptx, .jpg, .jpeg, .png, .gif, .ape, .mp4, .mov, .flac, .flv, .mp3, .ogg, .wma, .wmv |
Deflate | 나머지 |
이 중 Store는 압축 없이 저장하는 것을 의미합니다.
Store로 압축하는 확장자중에서는 고정된 헤더를 쓰는 파일이 많으며, 정리하면 아래와 같습니다.
확장자 | 고정된헤더 |
---|---|
ace | - |
alz | 41 4C 5A 01 0A 00 00 00 42 4C 5A 01 (12바이트) |
arc | 1A 02 (2바이트) 또는 1A 03 (2바이트) 또는 1A 04 (2바이트) 또는 1A 08 (2바이트) 또는 1A 09 (2바이트) |
arj | - |
bz | - |
bz2 | 42 5A 68 39 31 41 59 26 53 59 (10바이트) |
cab | - |
egg | 45 47 47 41 00 01 (6바이트) |
ice | - |
ear | - |
war | - |
gz | 1F 8B 08 00 00 00 00 00 00 03 (10바이트) |
ha | - |
jar | 50 4B 03 04 (4바이트) |
lha | - |
lzh | - |
pak | - |
rar | 52 61 72 21 1A 07 00 (7바이트) |
tbz | - |
tbz2 | - |
tgz | 1F 8B 08 00 27 A5 4F 5A 00 03 (10바이트) |
7z | 37 7A BC AF 27 1C 00 03 (8바이트) 또는 37 7A BC AF 27 1C 00 04 (8바이트) |
z | 1F 9D 90 (3바이트) |
zip | 50 4B 03 04 (4바이트) |
zoo | 5A 4F 4F 20 (4바이트) |
pptx, docx, xlsx | 50 4B 03 04 14 00 06 00 08 00 00 00 21 00 (14바이트) |
jpg | FF D8 FF DB (4바이트) 또는 FF D8 FF E0 00 10 4A 46 49 46 00 01(12바이트) 또는 FF D8 FF E1 (4바이트) |
jpeg | jpg와 동일 |
png | 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 (16바이트) |
gif | 47 49 46 38 39 61 (6바이트) 또는 47 49 46 38 39 61 (6바이트) |
ape | - |
mp4 | 00 00 00 18 66 74 79 70 (8바이트) 또는 33 67 70 35 (4바이트) |
mov | 6D 64 61 74 (4바이트) |
flac | 66 4C 61 43 00 00 00 22 (4바이트) |
flv | 46 4C 56 01 (4바이트) |
mp3 | 49 44 33 (3바이트) |
ogg | 4F 67 67 53 (4바이트) |
wma, wmv | 30 26 B2 75 8E 66 CF (7바이트) 또는 30 26 B2 75 8E 66 CF 11 A6 D9 00 AA 00 62 CE 6C (16바이트) |
한편, 무작위한 데이터를 넣어야 할 부분을 분석하여 보면, rand() 함수가 사용되는 것을 알 수 있습니다.
이것 자체로도 문제가 있지만, 잘못된 type-casting 으로 인해, (rand() % 0x7fff)
의 MSB 비트만 남게 되어, 경우의수가 크게 감소합니다.
따라서 ZIP Plaintext Attack 의 전제조건인 파일의 일부 내용을 알 필요 없이도 Known Plaintext attack이 가능합니다.
또한 위에서 설명한 "최적압축" 기능 때문에 일부 확장자의 파일이 같이 압축된 경우, 공격에 소요되는 시간을 크게 단축시킬 수 있습니다.
이 취약점은 제가 처음으로 버그바운티를 통해 보상금을 받은 취약점입니다.
보상금으로는 270만원을 받았는데, 당시 고등학교 2학년이였던 저에겐 꽤 큰 돈이였습니다.
보상금은 몇달 뒤에 모스크바에서 열린 한 해킹대회의 본선에 참여할 때 여행경비로 잘 썼습니다.
슬프게도 당시의 보고서와 POC코드를 분실했습니다.ㅜㅜ
남아있는 영상과 프로그램을 바탕으로 글을 작성했습니다.