Skip to content

Instantly share code, notes, and snippets.

@av-gantimurov
Created December 17, 2025 13:52
Show Gist options
  • Select an option

  • Save av-gantimurov/803fd62ae422cedd0786792c4b9e6916 to your computer and use it in GitHub Desktop.

Select an option

Save av-gantimurov/803fd62ae422cedd0786792c4b9e6916 to your computer and use it in GitHub Desktop.
Kaspersky Quarantine unpacking tool
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Kaspersky Quarantine unpacking tool
Based on dexray.pl
Date: 2025-11-26
"""
import argparse
import json
import struct
from datetime import datetime
EPOCH_AS_FILETIME = 116444736000000000
SHIFT = 0x46120CE758A60000
def decode_attr(value):
if isinstance(value, bytes):
value = int.from_bytes(value, byteorder="little")
masks = {"I": 0x2000, "A": 0x0020, "S": 0x0004, "H": 0x0002, "R": 0x0001}
attr = []
for v, m in masks.items():
if value & m:
attr.append(v)
return "".join(attr)
def convert_dt(value: int):
if isinstance(value, bytes):
value = int.from_bytes(value, byteorder="little")
if value < SHIFT:
raise ValueError(f"Incorrect FILETIME {value}")
filetime = (value - SHIFT) / 10
ts = (filetime - EPOCH_AS_FILETIME) / 10000000
return datetime.fromtimestamp(ts)
def prepare_argparse() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"files",
help="File or directories to parse",
metavar="FILE",
nargs="+",
)
return parser
def kav_unquarantine(file):
with open(file, "rb") as quarfile:
data = bytearray(quarfile.read())
# check for KLQB header
magic = struct.unpack("<I", data[0:4])[0]
if magic != 0x42514C4B:
return None, None
fsize = len(data)
headerlen = struct.unpack("<I", data[8:12])[0]
metaoffset = struct.unpack("<I", data[0x10:0x14])[0]
metalen = struct.unpack("<I", data[0x20:0x24])[0]
origlen = struct.unpack("<I", data[0x30:0x34])[0]
info = {}
if fsize < headerlen + origlen + metalen:
return None
if metaoffset < headerlen + origlen:
return None
key = [0xE2, 0x45, 0x48, 0xEC, 0x69, 0x0E, 0x5C, 0xAC]
curoffset = metaoffset
length = struct.unpack("<I", data[curoffset : curoffset + 4])[0]
while length:
for i in range(length):
data[curoffset + 4 + i] ^= key[i % len(key)]
idlen = struct.unpack("<I", data[curoffset + 4 : curoffset + 8])[0]
idname = (data[curoffset + 8 : curoffset + 8 + idlen]).decode().rstrip("\0")
value = bytes(data[curoffset + 8 + idlen : curoffset + 4 + length])
# print(idname, len(value))
if idname == "cNP_QB_FULLNAME":
# vallen = length - idlen
value = value.decode("utf-16").rstrip("\x00")
elif idname.endswith("_TIME"):
value = str(convert_dt(value))
elif idname == "cNP_QB_FILE_ATTRIBUTES":
value = decode_attr(value)
info[idname] = curoffset - metaoffset, value
curoffset += 4 + length
if curoffset >= metaoffset + metalen:
break
length = struct.unpack("<I", data[curoffset : curoffset + 4])[0]
for i in range(origlen):
data[headerlen + i] ^= key[i % len(key)]
return (data[headerlen : headerlen + origlen], info)
def dump_info(info, fname):
txt = {}
for m in info:
off, val = info[m]
if isinstance(val, str):
txt[m] = val
else:
with open(f"{fname}.prop{off:04X}.bin", "wb") as w:
print(f"Store {m} to {w.name}")
w.write(m.encode() + b"\x00")
w.write(val)
for m, val in txt.items():
print(f"{m}: {val}")
with open(f"{fname}.json", "w") as j:
json.dump(txt, j, indent=3, ensure_ascii=False)
print(f"Store meta in {j.name}")
def main() -> None:
parser = prepare_argparse()
args = parser.parse_args()
for f in args.files:
unp, info = kav_unquarantine(f)
if unp is None:
print(f"Unabled to unpack {f}")
continue
with open(f"{f}.unp", "wb") as fo:
print(f"Unpack quarantine to {fo.name}")
fo.write(unp)
dump_info(info, f"{f}.unp")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment