diff --git a/xlxunprotect-python3.py b/xlxunprotect-python3.py new file mode 100644 index 0000000..0070646 --- /dev/null +++ b/xlxunprotect-python3.py @@ -0,0 +1,770 @@ +""" Tool for removing IEEE-P1735 vhdl and verilog content protection """ +from __future__ import division, print_function, absolute_import # unicode_literals +import sys +import re +import struct +from binascii import b2a_hex, a2b_hex, a2b_uu +from Crypto.Cipher import AES, DES +from Crypto.Util import Counter +import zlib +import argparse +import platform + +if platform.python_version() < '3.0': + from StringIO import StringIO +else: + from io import StringIO + + +from base64 import b64decode + + +def uudecode(data): + dec = "" + for line in data.split("\n"): + dec += a2b_uu(line) + return dec + + +keys = { + "15": {"key":"93B5313FBB924FC9A57F40CBECE9EA3F41A6916E5BBC4AD8931F003C613BF934", "iv":"1C9C5CFB776F42EC8970FB3918D67B43"}, + # "16" + # "17" + # "1Y" + + # 32DB, 33EB, 34EB, 35EB, 36EB, 37EB, 37DB + + # 30 + # .. these keys are in the binary, but don't seem to be used. + # "32":{"key":"886592C2B46F3CD6", "iv":"E78437FD7C4503F3"}, + # "33":{"key":"7EB0C67E6B9B81E4", "iv":"FB744BDFFBAEE2B6"}, + # "34":{"key":"2990DC694EF16DC9", "iv":"567DFFD0706FE1AB"}, + # "35":{"key":"94FED660DE79DD61", "iv":"BFA101DDDECD319A"}, + # "36":{"key":"1CF07C2287EBE145", "iv":"7284567847970F9F"}, + "37":{"key":"278D7318A95839DF", "iv":"0D9B9E4FB62AF137"}, + # 9E5C257AF9954B05 F6D554BCDFF4BD32 + + # 38EB + "38": {"key": "F143CB05DA5FE26A", "iv": "0D9B9E4FB62AF137"}, + # 43 + "50": {"key": "B0E430FB8E024FB4AC0732270D9C3A88", "iv": "2C7F9EEBAE5D40BB8F6BC0B5EF240526"}, + "51": {"key": "8C700049CF544C24A7ABDBC0E896C4D1", "iv": "6D815F2526BAF48AB26519155E45AA49"}, + "60": {"key": "C145AF345F87526C233ADBC875051993A549C46663518B0859E35C1469DDA49D", "iv": "6F4A2505AD8367A4535ECE74D6438EA6"}, + "61": {"key": "2B3B4291844844DEA978A5175A7A8611BA8CFCF39DE54295B778CA7B29A0E537", "iv": "E9029FB591804136B858EFBE07A55729"}, + "62": {"key": "0DE4DDBC43D64F488DC9CE26D5161FA09E0EB0979DD34E4C8E8BB82B71BC9AEB", "iv": "D9A3D631B46BC3AA232219577DA16C48"}, + "64": {"key": "57D2680C0AD24BD38CFC2F0842DA67E5791207B8178042918DD7FC23B284D13A", "iv": "18C9FBCE90144BE997F910FA993FE28F"}, + "65": {"key": "581F4AA947971B02C9B76860BF153E027C0D9257937ABE03853FE928CADC6642", "iv": "4FF0F201813FBEAFA62AACDD88222910"}, + "80": {"key": "030E0F5C4123594F", "iv": "43A25FFF66954813"}, + + # "H1" + # "HY" -- keys are encoded in the header. + + # these formats i have not yet decoded: + # "HL" + # "c1" + + # "D0" - 'debug' +} + + +# decode hex values in key dictionary +for val in keys.values(): + val["key"] = a2b_hex(val["key"]) + val["iv"] = a2b_hex(val["iv"]) + + +class empty: pass + + +def parseheader(data): + if not data: + return + m = re.match(r'^XlxV(\d\d|H[LXY]|c1|D0)([ADE][ABM])([ 0-9a-f]{7}[0-9a-f])([ 0-9a-f]{7}[0-9a-f])$', data) + if not m: + return + + obj = empty() + obj.version = m.group(1) + obj.encoding = m.group(2) + obj.fullsize = int(m.group(3), 16) + obj.compsize = int(m.group(4), 16) + return obj + + +def strip_padding(data): + n = ord(data[-1]) + if 1 <= n <= 16 and data[-n:] == chr(n) * n: + return data[:-n] + return data + + +def cbcdecrypt(data, keyiv): + blksize = len(keyiv["iv"]) + keysize = len(keyiv["key"]) + datsize = len(data) + + if keysize == 8: + cipher = DES + else: + cipher = AES + + C = cipher.new(keyiv["key"], cipher.MODE_CBC, IV=keyiv["iv"]) + padding = datsize % blksize + + plain = "" + for o in xrange(0, datsize-padding, blksize): + plain += C.decrypt(data[o:o+blksize]) + return plain + + +# for HY +def gethword(data, o): + value, = struct.unpack_from(" + ix = b.find("\x00", 1) + return b[ix+1:] +# else: +# sys.stdout.write("`unwrap %s %s\n" % (key.name, b2a_hex(b))) + + +# decode HYbrid key section +def decrypt_hy_keys(data): + o = 0 + key2048, o = gethybytes(data, o) + count, o = gethword(data, o) + key3072, o = gethybytes(data, o) + wrapped_key, o = gethybytes(data, o) + wrapped_iv, o = gethybytes(data, o) + + key = unwrap(rsa2048, unwrap(rsa3072, wrapped_key)) + iv = unwrap(rsa2048, unwrap(rsa3072, wrapped_iv)) + + if struct.unpack_from(">L", key2048, 0)[0] != 0xdfe6242b: + print("HY: key2k= %08x %s" % b2a_hex(key2048), file=sys.stderr) + if struct.unpack_from(">L", key3072, 0)[0] != 0xe0e2d4d1: + print("HY: key3k= %s" % b2a_hex(key3072), file=sys.stderr) + if count != 1: + print("HY: count=%d" % count, file=sys.stderr) + + if o < len(data): + print("HY: left: %s" % b2a_hex(data[o:]), file=sys.stderr) + + print("hy: key=%s, iv=%s" % (key.encode('hex'), iv.encode('hex'))) + return {"key": key, "iv": iv} + +# decode c1 key section +def decrypt_c1_keys(data): + o = 0 + keyname, o = getc1bytes(data, o) + wrapped_key, o = getc1bytes(data, o) + wrapped_iv, o = getc1bytes(data, o) + + key = unwrap(rsa_2013_09, wrapped_key) + iv = unwrap(rsa_2013_09, wrapped_iv) + + if o < len(data): + print("c1: left: %s" % data[o:].encode("hex"), out=sys.stderr) + + print("c1: key=%s, iv=%s" % (key.encode('hex'), iv.encode('hex'))) + return {"key": key, "iv": iv} + + +# oldest IP encoding: only compression +def isStubFile(fh): + fh.seek(0) + hdr = fh.readline() + return hdr == "XILINX-XDB 0.1 STUB 0.1 ASCII\n" + + +# is old-style Xlx file +def isXlxFile(fh): + fh.seek(0) + hdr = fh.read(8) + return re.match(r'XlxV(\d\d|H[LXY]|c1|D0)([ADE][ABM])', hdr) != None + + +# not processing binary files ( xlx files start with ascii hdr ) +def isBinary(fh): + fh.seek(0) + hdr = fh.read(0x18) + return hdr.find("\x00") >= 0 + + +# decode Xlx file chunk +# for HY and c1 data returns a key/iv pair, for others returns the actual data +def decodechunk(hdr, data): + if hdr.version in keys: + print("enc = ", b2a_hex(data)) + print("key = ", b2a_hex(keys[hdr.version]['key']), 'iv=', b2a_hex(keys[hdr.version]['iv'])) + data = cbcdecrypt(data, keys[hdr.version]) + elif hdr.version == "HY": + return decrypt_hy_keys(data) + elif hdr.version == "c1": + return decrypt_c1_keys(data) + else: + print("unknown hdr: %s" % hdr.version) + + try: + if data: + if hdr.encoding == "DM": + data = b64decode(data) + C = zlib.decompressobj(15) + full = C.decompress(data) + + return full + except Exception as e: + print("ERROR %s in (%08x) %s" % (e, len(data), b2a_hex(data[0:16])), file=sys.stderr) + + +# read and decode Xlx chunk +def readchunk(fh): + hdrdata = fh.read(0x18) + if not hdrdata: + return + hdr = parseheader(hdrdata) + if not hdr: + return + data = fh.read(hdr.compsize) + + return decodechunk(hdr, data) + + +# process old style stub header +# 2 variants, one has XlxV32 data embedded, +# the other is 'rle' encoded -> rledecode +# involving ascii 0x20 - 0x7f chars +def readstubhdr(fh): + state = 0 + while True: + c = fh.read(1) + if c is None or len(c) == 0: + # eof + return False + if state < 3 and c == '#': + state += 1 + elif state < 3: + print("invalid stub hdr %d %02x" % (state, ord(c)), file=sys.stderr) + return False + elif state == 3 and '0' <= c <= '9': + pass + elif state == 3 and c == ':': + return True + else: + print("invalid stub hdr %d %02x" % (state, ord(c)), file=sys.stderr) + return False + + +def getxdmtype(line): + m = re.match(r'XILINX-XDM V1\.\d([a-z]*)', line) + if m: + return m.group(1) + + +def rledecode(x): + return "".join(chr(ord(x[i]) ^ (i & 15)) for i in range(len(x))) + + +# process Xlx file +# note: "HY" and "c1" keys are different for each file +def dumpxlx(fh): + if "HY" in keys: + keys.pop("HY") + if "c1" in keys: + keys.pop("c1") + hasstubs = False + + if isStubFile(fh): + secondline = fh.readline() + hasstubs = True + else: + fh.seek(0) + if hasstubs: + subver = getxdmtype(secondline) + # 'u': NOCOMPRESS + # 'e': RLE + # '': COMPRESS + if subver == "e": + yield rledecode(fh.read()) + return + + while True: + if hasstubs: + if not readstubhdr(fh): + break + chunk = readchunk(fh) + if not chunk: + break + if type(chunk) == dict: + # add HY or c1 key to keys table + keys["HY"] = chunk + keys["c1"] = chunk + else: + yield chunk + + +# code for decoding ieee 1364-2005 type IP protection +class ProtectParser: + # note: keyblocks can be decrypted using private key + # -> pkcs#1 v1.5 padded symmetric key + # datablock starts with IV block + def __init__(self, args): + self.verbose = args.verbose + self.clear() + + def clear(self): + self.props = {} + self.keys = [] + + @staticmethod + def pragma_protect(line): + # note: VHDL: `protect ... + # verilog: `pragma protect ... + # system verilog: //pragma protect + m = re.search(r'^\s*(?:`|//|--)(?:pragma\s+)?protect\s+(.*)', line) + if m: + return m.group(1) + + def moveprop(self, k, dst): + if k in self.props: + dst[k] = self.props[k] + del self.props[k] + + def addkey(self, keyblock): + kprop = {"key_block": keyblock} + + for k in ["key_keyname", "key_keyowner", "key_method", "encoding"]: + self.moveprop(k, kprop) + + self.keys.append(kprop) + + def adddata(self, datablock): + if "data_block" in self.props: + print("prot: multiple data blocks", file=sys.stderr) + self.props["data_block"] = datablock + + def parsetoken(self, text): + m = re.match(r'\s+', text) + if m: + return m.end(), None, None + m = re.match(r',', text) + if m: + return m.end(), None, None + + # double-quoted string + m = re.match(r'\s*(\w+)\s*=\s*"([^"]*)"', text) + if m: + return m.end(), m.group(1), m.group(2) + + # numeric string + m = re.match(r'\s*(\w+)\s*=\s*(\d+)', text) + if m: + return m.end(), m.group(1), int(m.group(2)) + + # bracketed string + m = re.match(r'\s*(\w+)\s*=\s*(\(.*?\))', text) + if m: + return m.end(), m.group(1), m.group(2) + + # unquoted string + m = re.match(r'\s*(\w+)\s*=\s*(\w+(?:\s\w+)*\.?)$', text) + if m: + return m.end(), m.group(1), m.group(2) + + # separate token + m = re.match(r'\s*(\w+)', text) + if m: + return m.end(), m.group(1), True + + print("protected: unknown syntax: '%s'" % text, file=sys.stderr) + return 1, None, None + + def parse_properties(self, line): + o = 0 + while o < len(line): + r, k, v = self.parsetoken(line[o:]) + o += r + if k is not None: + yield k, v + + def process_file(self, fh): + keyblock = None + datablock = None + for line in fh: + pp = self.pragma_protect(line) + if pp: + # when protect line found -> close {data|key}block + if keyblock is not None: + self.addkey(keyblock) + keyblock = None + elif datablock is not None: + self.adddata(datablock) + datablock = None + + for k, v in self.parse_properties(pp): + self.props[k.lower()] = v + + # handle the various tags + if "begin_protected" in self.props: + del self.props["begin_protected"] + self.keys = [] + elif "end_protected" in self.props: + del self.props["end_protected"] + self.decrypt() + self.clear() + elif "key_block" in self.props: + del self.props["key_block"] + keyblock = "" + elif "data_block" in self.props: + del self.props["data_block"] + datablock = "" + + # collect data + elif keyblock is not None: + keyblock += line + elif datablock is not None: + datablock += line + else: + # ignore plain text + sys.stdout.write(line) + + def findkey(self, keys, keylen): + res = None + for k in keys: + thiskey = None + wrapped = self.decode(k["encoding"], k["key_block"]) +# sys.stdout.write("`unwrap: wraped=%s\n" % b2a_hex(wrapped)) + for privkey in privkeys: + key = unwrap(privkey, wrapped) + if key and len(key) == keylen: + thiskey = (key, privkey.name, k["key_keyname"], k["key_keyowner"]) + if not self.verbose and thiskey: + return thiskey + if self.verbose: + if thiskey: + print("--- %s ; %s ; %s was decrypted using %s" % (k["key_keyname"], k["key_keyowner"], b2a_hex(thiskey[0]), thiskey[1])) + res = thiskey + else: + print("--- %s ; %s could not be decrypted" % (k["key_keyname"], k["key_keyowner"])) + print(k) + return res + + def decode(self, encoding, data): + if encoding.lower().find("uuencode") > 0: + return uudecode(data) + return b64decode(data) + + def decrypt(self): + key = None + if "data_method" not in self.props: + print("unknown data method") + return + + if self.props["data_method"].lower() == "aes256-cbc": + keylength = 32 + else: # if self.props["data_method"].lower() == "aes128-cbc": + keylength = 16 + + if self.keys: + key = self.findkey(self.keys, keylength) + else: + # todo: find CDS_DATA_KEY, from ncprotect + # or MTI static key + key = None + + if key: + key, privname, keyname, keyowner = key + data = self.decode(self.props["encoding"], self.props["data_block"]) + if len(data)<16: + print("ERROR: no iv") + return + plain = cbcdecrypt(data[16:], {"key": key, "iv": data[0:16]}) + plain = strip_padding(plain) + sys.stdout.write("`pragma begin_decoded privkey=\"%s\", key_keyname=\"%s\", key_keyowner=\"%s\"\n" % (privname, keyname, keyowner)) + if not args.nodata: + sys.stdout.write(plain) + elif len(plain)<=20: + sys.stdout.write("** %d bytes: %s" % (len(plain), b2a_hex(plain))) + else: + sys.stdout.write("** %d bytes: %s ... %s" % (len(plain), b2a_hex(plain[:8]), b2a_hex(plain[-8:]))) + sys.stdout.write("`pragma end_decoded\n") + else: + descriptions = ["%s:%s" % (k["key_keyowner"], k["key_keyname"]) for k in self.keys] + if "data_keyname" in self.props: + descriptions.append("DATAKEY/%s:%s" % (self.props["data_keyowner"], self.props["data_keyname"])) + sys.stdout.write("`pragma protect: no known key found: %s\n" % ",".join(descriptions)) + + +def process_protect(fh, args): + P = ProtectParser(args) + P.process_file(fh) + + + +parser = argparse.ArgumentParser(description='Tool for decoding xilinx protected files, or begin_protected sections from verlog code.') +parser.add_argument('--verbose', '-v', action='store_true', help='print info for each key') +parser.add_argument('--nodata', '-n', action='store_true', help='omit the decrypted data') +parser.add_argument('files', type=str, metavar='FILE', nargs='*', help='a protected data file') + + +args = parser.parse_args() + +if not args.files or (len(args.files)==1 and args.files[0]=='-'): + process_protect(sys.stdin, args) +else: + for fn in args.files: + if len(args.files) > 1: + print("==> %s <==" % (fn)) + isFirst = True + isRecursiveStub = False + alldata = "" + with open(fn, "rb") as fh: + if isBinary(fh): + pass + elif isXlxFile(fh) or isStubFile(fh): + fh.seek(0) + for chunk in dumpxlx(fh): + if isFirst: + isRecursiveStub = chunk[0:30] == "XILINX-XDB 0.1 STUB 0.1 ASCII\n" + isFirst = False + if isRecursiveStub: + alldata += chunk + else: + sys.stdout.write(chunk) + + # some files are doubly encoded + if isRecursiveStub: + for chunk in dumpxlx(StringIO.StringIO(alldata)): + sys.stdout.write(chunk) + else: + fh.seek(0) + process_protect(fh, args) + + if len(args.files) > 1: + print