""" Tool for removing IEEE-P1735 vhdl and verilog content protection """ import sys import re import struct from Crypto.Cipher import AES, DES from Crypto.Util import Counter import zlib import argparse import StringIO from base64 import b64decode import binascii def uudecode(data): dec = "" for line in data.split("\n"): dec += binascii.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.itervalues(): val["key"] = val["key"].decode("hex") val["iv"] = val["iv"].decode("hex") 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, b.encode("hex"))) # 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 >> sys.stderr, "HY: key2k= %08x %s" % key2048.encode("hex") if struct.unpack_from(">L", key3072, 0)[0] != 0xe0e2d4d1: print >> sys.stderr, "HY: key3k= %s" % key3072.encode("hex") if count != 1: print >> sys.stderr, "HY: count=%d" % count if o < len(data): print >> sys.stderr, "HY: left: %s" % data[o:].encode("hex") 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 >> sys.stderr, "c1: left: %s" % data[o:].encode("hex") 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 = ", data.encode('hex') print "key = ", keys[hdr.version]['key'].encode('hex'), 'iv=', keys[hdr.version]['iv'].encode('hex') 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) print "comp = ", data.encode('hex') full = C.decompress(data) return full except Exception, e: print >> sys.stderr, "ERROR %s in (%08x) %s" % (e, len(data), data[0:16].encode("hex")) # 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 >> sys.stderr, "invalid stub hdr %d %02x" % (state, ord(c)) return False elif state == 3 and '0' <= c <= '9': pass elif state == 3 and c == ':': return True else: print >> sys.stderr, "invalid stub hdr %d %02x" % (state, ord(c)) 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 >> sys.stderr, "prot: multiple data blocks" 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 >> sys.stderr, "protected: unknown syntax: '%s'" % text 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.xreadlines(): 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" % wrapped.encode("hex")) 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"], thiskey[0].encode("hex"), 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)) sys.stdout.write(plain) 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('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