├── ctk ├── __init__.py ├── crc.py ├── solver.py └── datatypes.py ├── .gitignore ├── test.py └── README.md /ctk/__init__.py: -------------------------------------------------------------------------------- 1 | from crc import CRC 2 | from solver import * 3 | from datatypes import * 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from ctk import * 4 | 5 | if __name__ == "__main__": 6 | s = Solver() 7 | s += Data("41") + ~Data("a4 1f 10") + TargetCRC("0f") 8 | s += ~Data("41 a4 1f 10") + Permute( Data("20"), Data("40"), ~Data("00")) + Data("00") + TargetCRC("d1") 9 | s += ~Data("41 a4 1f 10") + Data("3b 40 00 00") + TargetCRC("a2") 10 | 11 | s.search_post = [0] 12 | s.solve() 13 | -------------------------------------------------------------------------------- /ctk/crc.py: -------------------------------------------------------------------------------- 1 | class CRC(object): 2 | "A generic CRC generator" 3 | 4 | def __init__(self, order, polynom, inverse = False, init_value = 0, post_xor = 0): 5 | """Initialize a new CRC generator object. 6 | order is an integer specifying the width of the CRC register in bits, e.g. 8 for an 8-bit CRC. 7 | polynom is the CRC polynom as an integer. 8 | If inverse is true the shift direction on state update is reversed (shift left instead of shift right). 9 | init_value is the value the state register is set to at clear() time. 10 | post_xor is XORed onto the result before being returned by finish().""" 11 | self._order = order 12 | self._mask = (1<>i) & 1) 27 | if self._inverse: 28 | self._state = (self._state << 1) & self._mask 29 | else: 30 | self._state = (self._state >> 1) & self._mask 31 | if b ^ d: 32 | self._state = self._state ^ self._poly 33 | 34 | def finish(self): 35 | return self._state ^ self._post 36 | 37 | def map(self, expression): 38 | data_width = expression.get_data_width() 39 | 40 | for data in expression.expand(): 41 | self.clear() 42 | for word in data: 43 | self.update(word, data_width) 44 | yield self.finish() 45 | 46 | def calculate(self, data): 47 | for result in self.map(data): 48 | return result # Only return the first result from the iterator 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ctk 2 | 3 | CRC Tool Kit - Python tools for working with CRCs. 4 | 5 | The main functionality is a brute-force search (the search space is pruned 6 | as soon as possible), that, when given a specification of data sequences 7 | and the CRC values they should evaluate to, will try to find the CRC 8 | parameters. 9 | 10 | ## CRC Solver 11 | 12 | For the case when it is not exactly known over which sequences the CRC 13 | is calculated it is possible to symbolically specify multiple alternate 14 | possibilities using an embedded domain specific language based on Python 15 | operator overloading. 16 | 17 | Usage example: 18 | 19 | from ctk import * 20 | 21 | s = Solver() 22 | s += Data("41 8B 35 10") + TargetCRC("9e") 23 | s.solve() 24 | 25 | In general one example will not be enough to uniquely restrict the 26 | parameter space. Two examples will usually fix the polynom and inverse 27 | parameters. If both are of the same length, the initial value and post 28 | XOR value will still be free, since it's possible to give one for an 29 | arbitrary value of the other. To circumvent that you may limit the 30 | search space if you know or suspect a parameter (e.g. fix post XOR to 0): 31 | 32 | s = Solver() 33 | s += Data("41 8B 35 10") + TargetCRC("9e") 34 | s += Data("57 81 82 da") + TargetCRC("6d") 35 | s.search_post = [0] 36 | s.solve() 37 | 38 | ## Data variations 39 | 40 | To specify potential variations a set of combining operators is provided 41 | that allow examples to be built up from expressions and combinations of 42 | expressions (`Data()` itself is an expression): 43 | * `Concat()` (Operator `+` ) 44 | * `Optional()` (Operator `~` ) 45 | * `Repeat()` (Operator `* integer` or `* (integer, integer)` ) 46 | * `Permute()` 47 | * `Combine()` 48 | 49 | 50 | ### Concat(a, b, …) 51 | simply concatenates the variations of the source expressions. 52 | If applicable, the Cartesian product is generated. 53 | 54 | ### Optional(a) 55 | will either include `a` or not (yielding two variations) 56 | 57 | ### Repeat(a, min, max=1) 58 | repeats `a` for a fixed or variable number of repetitions. 59 | `Repeat(a, 2)` is equivalent to `Concat(a, a)`. 60 | Note that if `a` yields multiple variations the Cartesian product is 61 | generated (same as with `Concat()`). 62 | 63 | ### Permute(a, b, …, min=?, max=?) 64 | generates all permutations of the given elements. If `min` and/or 65 | `max` are specified it can additionally generate permutations that 66 | include only the given number of elements. 67 | 68 | ### Combine(a, b, …, min=?, max=?) 69 | generates combinations, similar to `Permute()` but only with 70 | elements in the given order. `Combine(a, b, c)` is equivalent to 71 | `Concat(a, b, c)`. Note that `Combine(a, b, min=1, max=1)` is 72 | equivalent to the alternation operator in regular expressions. 73 | 74 | 75 | Example: 76 | 77 | s = Solver() 78 | s += Data("41 8B 35 10") + TargetCRC("9e") 79 | s += Data("57 81 82 da") + TargetCRC("6d") 80 | s += ~Data("57 81 82 da") + Data("20 40 00 00") + TargetCRC("7a") 81 | s.solve() 82 | -------------------------------------------------------------------------------- /ctk/solver.py: -------------------------------------------------------------------------------- 1 | from crc import CRC 2 | 3 | def _solve_internal(self, poly, inv, same_length, output): 4 | c = self.crc(self.order, poly, inv, 0, 0) 5 | 6 | ## This part of the algorithm fixes poly and inv and searches for 7 | ## overall successful pairs of init_value and post_xor 8 | ## To be overall successful a pair must yield correct results for 9 | ## at least one alternative in each dataset. 10 | ## That means: a) If no init_value/post_xor pair is 11 | ## successful for one entire dataset, there can not be 12 | ## an overall successful pair for this poly/inv combination 13 | ## and we can abort this search branch 14 | ## b) The set of overall successful pairs is a subset of 15 | ## per dataset successful pairs. Therefore, on datasets 16 | ## after the first, we only need to check pairs that were 17 | ## successful on all previous datasets. 18 | ## Further optimization: post_xor need not be attempted, but can 19 | ## be looked up: crc.finish() ^ potential_post_xor == given_crc 20 | ## (for potential_post_xor in search_post_xor) is equivalent to 21 | ## crc.finish() ^ given_crc in search_post_xor. This is faster 22 | ## than a manual comparison if search_post_xor is xrange(), and 23 | ## should fall back otherwise. 24 | 25 | pairs = {} 26 | for init in self.search_init: 27 | c._init = init 28 | 29 | ## Step 1: Go through the first dataset and generate 30 | ## candidate init_value/post_xor pairs. Abort this poly 31 | ## if none are found 32 | 33 | 34 | dataset = self.data[0] 35 | data_width = dataset.get_data_width() 36 | given_crc = dataset._get_crc() 37 | for alternative in dataset.expand(): 38 | c.clear() 39 | 40 | for word in alternative: 41 | c.update(word, data_width) 42 | 43 | post = c.finish()^given_crc 44 | 45 | if post in self.search_post: 46 | ## Hit! Record this combination 47 | 48 | if not pairs.has_key( (init, post) ): 49 | pairs[ (init, post) ] = [] 50 | 51 | pairs[ (init, post) ].append( alternative ) 52 | 53 | if len(pairs) == 0: 54 | return 55 | 56 | ## Step 2: Go through the rest of the datasets, and 57 | ## thin out the list generated in step 1, skip to next 58 | ## init_value if it becomes empty. 59 | 60 | for dataset in self.data[1:]: 61 | data_width = dataset.get_data_width() 62 | given_crc = dataset._get_crc() 63 | 64 | for init, post in pairs.keys(): 65 | c._init = init 66 | 67 | hitone = False 68 | 69 | for alternative in dataset.expand(): 70 | c.clear() 71 | 72 | for word in alternative: 73 | c.update(word, data_width) 74 | 75 | if c.finish() ^ post == given_crc: 76 | hitone = True 77 | ## Hit! Record this alternative as matching 78 | 79 | pairs[ (init, post) ].append(alternative) 80 | 81 | if not hitone: 82 | ## Miss for all alternatives! Discard this pair, and go to next pair 83 | del pairs[ (init, post) ] 84 | 85 | if len(pairs) == 0: 86 | return 87 | 88 | ## Emit all found combinations 89 | 90 | for (init, post), alternatives in pairs.items(): 91 | skip_this = False 92 | 93 | if same_length: 94 | l = len(alternatives[0]) 95 | for alternative in alternatives: 96 | if l != len(alternative): 97 | skip_this = True 98 | break 99 | 100 | if not skip_this: 101 | lines = [] 102 | lines.append("poly=%02X, inv=%r, init=%02X, post=%02X, success on:" % (poly, inv, init, post)) 103 | for alternative in alternatives: 104 | lines.append("%3i: %s" % (len(alternative), " ".join(["%02X" % e for e in alternative]))) 105 | output.put("\n".join(lines)) 106 | 107 | 108 | class _cacher: 109 | """"Evaluate Data or one of the combination operators' attributes 110 | on object creation, store the results and then just return the 111 | stored results""" 112 | 113 | __slots__ = ["_o", "_r", "_e", "_c", "_w"] 114 | 115 | def __init__(self, o): 116 | self._o = o 117 | self._r = self._o.__repr__() 118 | self._e = tuple(self._o.expand()) 119 | self._c = self._o._get_crc() 120 | self._w = self._o.get_data_width() 121 | 122 | def __repr__(self): 123 | return self._r 124 | 125 | def expand(self): 126 | return self._e 127 | 128 | def _get_crc(self): 129 | return self._c 130 | 131 | def get_data_width(self): 132 | return self._w 133 | 134 | class Solver(object): 135 | """An object that keeps a list of given data value/CRC value examples and can be 136 | instructed to search for matching CRC parameters. 137 | 138 | Publically modifiable attributes of instances are: 139 | search_poly: a sequence of polynom values to search, defaults to xrange(1< self.max: 39 | self.min = self.max 40 | 41 | def __repr__(self): 42 | return "Permute(%s,min=%i,max=%i)" % (", ".join(map(repr, self.values)), self.min, self.max) 43 | 44 | def expand(self): 45 | import itertools 46 | 47 | def recursive_unroll(head, tail): 48 | if len(tail) == 0: 49 | for a in head.expand(): 50 | yield a 51 | else: 52 | for a in head.expand(): 53 | for b in recursive_unroll(tail[0], tail[1:]): 54 | yield a + b 55 | 56 | for r in range(self.min, self.max+1): 57 | for order in itertools.permutations(self.values, r): 58 | for result in recursive_unroll(order[0], order[1:]): 59 | yield result 60 | 61 | def _get_crc(self): 62 | for v in self.values: 63 | r = v._get_crc() 64 | if r is not None: 65 | return r 66 | 67 | def get_data_width(self): 68 | return self.values[0].get_data_width() 69 | 70 | class Combine(Permute): 71 | """Investigates a subset of all possible combinations of the input data items, possibly 72 | restricted by minimum and maximum of data items that must be used. 73 | 74 | Combine(Data("1"), Data("2"), Data("3")) will yield 75 | (1,2,3). 76 | 77 | Comine(Data("1"), Data("2"), Data("3"), max=2) will yield 78 | (1,2), (1,3), (2,3).""" 79 | 80 | def __repr__(self): 81 | return "Combine(%s,min=%i,max=%i)" % (", ".join(map(repr, self.values)), self.min, self.max) 82 | 83 | def expand(self): 84 | import itertools 85 | 86 | def recursive_unroll(head, tail): 87 | if len(tail) == 0: 88 | for a in head.expand(): 89 | yield a 90 | else: 91 | for a in head.expand(): 92 | for b in recursive_unroll(tail[0], tail[1:]): 93 | yield a + b 94 | 95 | for r in range(self.min, self.max+1): 96 | for order in itertools.combinations(self.values, r): 97 | for result in recursive_unroll(order[0], order[1:]): 98 | yield result 99 | 100 | class Concat(object, _operations): 101 | """Concatenates expressions. 102 | 103 | Concat(Data("1"), Data("2")) will yield 104 | (1,2).""" 105 | 106 | def __init__(self, *values): 107 | self.a = values[0] 108 | b = values[1:] 109 | 110 | if len(b) > 1: 111 | self.b = Concat(*b) 112 | else: 113 | self.b = b[0] 114 | 115 | def __repr__(self): 116 | return "%r + %r" % (self.a, self.b) 117 | 118 | def expand(self): 119 | for a in self.a.expand(): 120 | for b in self.b.expand(): 121 | yield a + b 122 | 123 | def _get_crc(self): 124 | return self.a._get_crc() or self.b._get_crc() 125 | 126 | def get_data_width(self): 127 | return self.a.get_data_width() 128 | 129 | 130 | class Optional(object, _operations): 131 | """Makes an expression optional. 132 | 133 | Optional(Data("1")) will yield 134 | (), (1).""" 135 | 136 | def __init__(self, a): 137 | self.a = a 138 | 139 | def __repr__(self): 140 | return "Optional(%r)" % self.a 141 | 142 | def expand(self): 143 | yield tuple() 144 | for a in self.a.expand(): 145 | yield a 146 | 147 | def _get_crc(self): 148 | return self.a._get_crc() 149 | 150 | def get_data_width(self): 151 | return self.a.get_data_width() 152 | 153 | class Repeat(object, _operations): 154 | """Repeats an expression, a fixed number of times or over a range of repetitions. 155 | 156 | Repeat(Data("1"), min=3) will yield 157 | (1,1,1). 158 | 159 | Repeat(Data("1", min=1, max=3)) will yield 160 | (1), (1,1), (1,1,1).""" 161 | 162 | def __init__(self, a, min, max=1): 163 | self.a = a 164 | self.min = min 165 | self.max = max 166 | 167 | if self.max < self.min: 168 | self.max = self.min 169 | 170 | def __repr__(self): 171 | return "Repeat(%r, min=%i, max=%i)" % (self.a, self.min, self.max) 172 | 173 | def expand(self): 174 | def recursive_unroll(remainder): 175 | for a in self.a.expand(): 176 | if remainder > 1: 177 | for b in recursive_unroll(remainder - 1): 178 | yield a + b 179 | else: 180 | yield a 181 | 182 | for i in range(self.min, self.max+1): 183 | for result in recursive_unroll(i): 184 | yield result 185 | 186 | def _get_crc(self): 187 | return self.a._get_crc() 188 | 189 | def get_data_width(self): 190 | return self.a.get_data_width() 191 | 192 | class Data(object, _operations): 193 | """A basic data item, a sequence of words of a fixed word length. 194 | 195 | These can be combined with one of the expression combinators in this 196 | module. For convenience, operator overloading is defined with the 197 | following operators: 198 | 199 | Data(a) + Data(b) is Concat(Data(a), Data(b)) 200 | Data(a) * b is Repeat(Data(a), min=b, max=b) 201 | Data(a) * (b,c) is Repeat(Data(a), min=b, max=c) 202 | ~Data(a) is Optional(Data(a)). 203 | 204 | Combinations are valid and will be solved by Pythons normal operator 205 | precedence rules: 206 | Data(a) + ~Data(b) * c is Concat(Data(a), Repeat(Optional(Data(b)), min=c, max=c). 207 | 208 | Note that this example is inefficient: 209 | Repeat(Optional(Data(b)), min=c, max=c) will yield the same set of combinations as 210 | Repeat(Data(b), min=0, max=c), but will repeat many of the same outputs.""" 211 | 212 | def __init__(self, value, format=FORMAT_HEX_SPACE, data_width=8): 213 | r"""Initializes a new Data item. The value can be given in multiple formats 214 | and will internally be converted to a list of words (as integers). 215 | 216 | FORMAT_HEX_SPACE: "3 14 0A" -> (3, 20, 10) 217 | FORMAT_INTEGER: (1, 2, 3) -> (1, 2, 3) 218 | FORMAT_BINARY: "3\x3 " -> (51, 3, 32).""" 219 | 220 | self.data_width = data_width 221 | 222 | if format == FORMAT_HEX_SPACE: 223 | self.value = tuple([int(e, 16) for e in value.split()]) 224 | elif format == FORMAT_INTEGER: 225 | self.value = tuple(value) 226 | else: 227 | self.value = tuple(map(ord, value)) 228 | 229 | def __repr__(self): 230 | return "%s(value='%s')" % (self.__class__.__name__, " ".join(["%02X" % e for e in self.value])) 231 | 232 | def expand(self): 233 | yield self.value 234 | 235 | def _get_crc(self): 236 | return None 237 | 238 | def get_data_width(self): 239 | # FIXME Is not properly implemented for mixed values 240 | return self.data_width 241 | 242 | class TargetCRC(Data): 243 | """The target CRC that this expression should evaluate to. 244 | For convenience it is handled just as Data objects, though you should not 245 | add more than one (concatenation etc. will not work). 246 | 247 | Data("3A") + TargetCRC("F0") specifies the single byte 0x3A as data that 248 | should result in the CRC 0xF0. 249 | Note: TargetCRC.data_width should be equal to CRC.order, which may be 250 | distinct from Data.data_width (e.g. 16-bit CRC over 8-bit 'words', that is, 251 | bytes).""" 252 | 253 | def expand(self): 254 | yield tuple() 255 | 256 | def _get_crc(self): 257 | return self.value[0] 258 | --------------------------------------------------------------------------------