|
1 #!/usr/bin/env python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 ############################################################################## |
|
5 # decodesisx.py - Decodes a Symbian OS v9.x SISX file |
|
6 # Copyright 2006, 2007 Jussi Ylänen |
|
7 # |
|
8 # This program is based on a whitepaper by Symbian's Security team: |
|
9 # Symbian OS v9.X SIS File Format Specification, Version 1.1, June 2006 |
|
10 # http://developer.symbian.com/main/downloads/papers/SymbianOSv91/softwareinstallsis.pdf |
|
11 # |
|
12 # This program is part of Ensymble developer utilities for Symbian OS(TM). |
|
13 # |
|
14 # Ensymble is free software; you can redistribute it and/or modify |
|
15 # it under the terms of the GNU General Public License as published by |
|
16 # the Free Software Foundation; either version 2 of the License, or |
|
17 # (at your option) any later version. |
|
18 # |
|
19 # Ensymble is distributed in the hope that it will be useful, |
|
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
22 # GNU General Public License for more details. |
|
23 # |
|
24 # You should have received a copy of the GNU General Public License |
|
25 # along with Ensymble; if not, write to the Free Software |
|
26 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
|
27 # |
|
28 # |
|
29 # Version history |
|
30 # --------------- |
|
31 # |
|
32 # v0.09 2006-09-22 |
|
33 # Replaced every possible range(...) with xrange(...) for efficiency |
|
34 # |
|
35 # v0.08 2006-09-12 |
|
36 # Implemented a SISController dump option (-c, for use with -d and/or -f) |
|
37 # |
|
38 # v0.07 2006-09-05 |
|
39 # Implemented a header hex dump option (-e) |
|
40 # |
|
41 # v0.06 2006-08-29 |
|
42 # Fixed errors in uncompressed SISCompressed field handling |
|
43 # |
|
44 # v0.05 2006-08-10 |
|
45 # Option --dumptofile (-f) now uses a directory for dumped files |
|
46 # A temporary directory is generated if none given by the user |
|
47 # Other small corrections and polishing |
|
48 # |
|
49 # v0.04 2006-08-09 |
|
50 # Added command line options using getopt |
|
51 # Added support for reading the SISX file from stdin |
|
52 # Made it possible to extract files from SISX |
|
53 # Improved hexdump ASCII support |
|
54 # |
|
55 # v0.03 2006-08-07 |
|
56 # Added some crude debug features: dumping to files, decompressed data dumping |
|
57 # |
|
58 # v0.02 2006-08-06 |
|
59 # Changed field type flags to callbacks for flexibility |
|
60 # Added an UID checking and printing |
|
61 # |
|
62 # v0.01 2006-08-04 |
|
63 # Initial version |
|
64 # |
|
65 # v0.00 2006-08-03 |
|
66 # Work started |
|
67 ############################################################################## |
|
68 |
|
69 VERSION = "v0.09 2006-09-22" |
|
70 |
|
71 import sys |
|
72 import os |
|
73 import zlib |
|
74 import struct |
|
75 import getopt |
|
76 import random |
|
77 import tempfile |
|
78 |
|
79 # Parameters |
|
80 MAXSISFILESIZE = 8 * 1024 * 1024 # Arbitrary maximum size of SISX file |
|
81 |
|
82 sisfilename = None |
|
83 tempdir = None |
|
84 dumpcounter = 0 |
|
85 norecursecompressed = False |
|
86 |
|
87 class options: |
|
88 '''Command line options''' |
|
89 |
|
90 hexdump = False |
|
91 headerdump = False |
|
92 dumpcontroller = False |
|
93 dumptofile = False |
|
94 |
|
95 def mkdtemp(template): |
|
96 ''' |
|
97 Create a unique temporary directory. |
|
98 |
|
99 tempfile.mkdtemp() was introduced in Python v2.3. This is for |
|
100 backward compatibility. |
|
101 ''' |
|
102 |
|
103 # Cross-platform way to determine a suitable location for temporary files. |
|
104 systemp = tempfile.gettempdir() |
|
105 |
|
106 if not template.endswith("XXXXXX"): |
|
107 raise ValueError("invalid template for mkdtemp(): %s" % template) |
|
108 |
|
109 for n in xrange(10000): |
|
110 randchars = [] |
|
111 for m in xrange(6): |
|
112 randchars.append(random.choice("abcdefghijklmnopqrstuvwxyz")) |
|
113 |
|
114 tempdir = os.path.join(systemp, template[: -6]) + "".join(randchars) |
|
115 |
|
116 try: |
|
117 os.mkdir(tempdir, 0700) |
|
118 return tempdir |
|
119 except OSError: |
|
120 pass |
|
121 |
|
122 def hexdump(data, datalen = None): |
|
123 '''Print binary data as a human readable hex dump.''' |
|
124 |
|
125 if datalen == None or datalen > len(data): |
|
126 datalen = len(data) |
|
127 |
|
128 offset = 0 |
|
129 while offset < datalen: |
|
130 line = [] |
|
131 line.append("%06x:" % offset) |
|
132 for n in xrange(16): |
|
133 if n & 3 == 0: |
|
134 line.append(" ") |
|
135 if (offset + n) < datalen: |
|
136 c = data[offset + n] |
|
137 line.append("%02x " % ord(c)) |
|
138 else: |
|
139 line.append(" ") |
|
140 line.append(' "') |
|
141 for n in xrange(16): |
|
142 if (offset + n) < datalen: |
|
143 c = data[offset + n] |
|
144 if ord(c) >= 32 and ord(c) < 127: |
|
145 line.append(c) |
|
146 else: |
|
147 line.append(".") |
|
148 else: |
|
149 break |
|
150 line.append('"') |
|
151 |
|
152 print "".join(line) |
|
153 offset += 16 |
|
154 |
|
155 def handlearray(data, datalen, reclevel): |
|
156 '''Handle SISArray.''' |
|
157 |
|
158 arraytype = data[:4] |
|
159 data = data[4:] |
|
160 |
|
161 arraypos = 0 |
|
162 arraylen = datalen - 4 |
|
163 while arraypos < arraylen: |
|
164 # Construct virtual SISFields for each array element. |
|
165 arraydata = arraytype + data[arraypos:] |
|
166 arraypos += parsesisfield(arraydata, |
|
167 arraylen - arraypos + 4, reclevel + 1) - 4 |
|
168 |
|
169 if arraypos != arraylen: |
|
170 raise ValueError("SISArray data length mismatch") |
|
171 |
|
172 def handlecompressed(data, datalen, reclevel): |
|
173 '''Handle SISCompressed.''' |
|
174 |
|
175 if datalen < 12: |
|
176 raise ValueError("SISCompressed contents too short") |
|
177 |
|
178 compalgo = struct.unpack("<L", data[:4])[0] |
|
179 uncomplen = struct.unpack("<Q", data[4:12])[0] |
|
180 |
|
181 print "%s%s %d bytes uncompressed, algorithm %d" % (" " * reclevel, |
|
182 " " * 13, uncomplen, |
|
183 compalgo) |
|
184 |
|
185 if compalgo == 0: |
|
186 # No compression, strip SISField and SISCompressed headers. |
|
187 data = data[12:datalen] |
|
188 elif compalgo == 1: |
|
189 # RFC1950 (zlib header and checksum) compression, decompress. |
|
190 data = zlib.decompress(data[12:datalen]) |
|
191 else: |
|
192 raise ValueError("invalid SISCompressed algorithm %d" % compalgo) |
|
193 |
|
194 if uncomplen != len(data): |
|
195 raise ValueError("SISCompressed uncompressed data length mismatch") |
|
196 |
|
197 if norecursecompressed: |
|
198 # Recursive parsing disabled temporarily from handlefiledata(). |
|
199 # Dump data instead. |
|
200 dumpdata(data, uncomplen, reclevel + 1) |
|
201 else: |
|
202 # Normal recursive parsing, delegate to handlerecursive(). |
|
203 handlerecursive(data, uncomplen, reclevel) |
|
204 |
|
205 def handlerecursive(data, datalen, reclevel): |
|
206 '''Handle recursive SISFields, i.e. SISFields only containing |
|
207 other SISFields.''' |
|
208 |
|
209 parselen = parsebuffer(data, datalen, reclevel + 1) |
|
210 if datalen != parselen: |
|
211 raise ValueError("recursive SISField data length mismatch %d %d" % |
|
212 (datalen, parselen)) |
|
213 |
|
214 def handlefiledata(data, datalen, reclevel): |
|
215 '''Handle SISFileData.''' |
|
216 |
|
217 global norecursecompressed |
|
218 |
|
219 # Temporarily disable recursion for handlecompressed(). |
|
220 oldnrc = norecursecompressed |
|
221 norecursecompressed = True |
|
222 handlerecursive(data, datalen, reclevel) |
|
223 norecursecompressed = oldnrc |
|
224 |
|
225 def handlecontroller(data, datalen, reclevel): |
|
226 '''Handle SISController SISField. Dump data if required.''' |
|
227 |
|
228 if options.dumpcontroller: |
|
229 dumpdata(data, datalen, reclevel) |
|
230 |
|
231 # Handle contained fields as usual. |
|
232 handlerecursive(data, datalen, reclevel) |
|
233 |
|
234 def dumpdata(data, datalen, reclevel): |
|
235 '''Dumps data to a file in a temporary directory.''' |
|
236 |
|
237 global tempdir, dumpcounter |
|
238 |
|
239 if options.hexdump: |
|
240 hexdump(data, datalen) |
|
241 print |
|
242 if options.dumptofile: |
|
243 if tempdir == None: |
|
244 # Create temporary directory for dumped files. |
|
245 tempdir = mkdtemp("decodesisx-XXXXXX") |
|
246 dumpcounter = 0 |
|
247 |
|
248 filename = os.path.join(tempdir, "dump%04d" % dumpcounter) |
|
249 dumpcounter += 1 |
|
250 f = file(filename, "wb") |
|
251 f.write(data[:datalen]) |
|
252 f.close() |
|
253 print "%sContents written to %s" % (" " * reclevel, filename) |
|
254 |
|
255 # SISField types and callbacks |
|
256 sisfieldtypes = [ |
|
257 ("Invalid SISField", None), |
|
258 ("SISString", dumpdata), |
|
259 ("SISArray", handlearray), |
|
260 ("SISCompressed", handlecompressed), |
|
261 ("SISVersion", dumpdata), |
|
262 ("SISVersionRange", handlerecursive), |
|
263 ("SISDate", dumpdata), |
|
264 ("SISTime", dumpdata), |
|
265 ("SISDateTime", handlerecursive), |
|
266 ("SISUid", dumpdata), |
|
267 ("Unused", None), |
|
268 ("SISLanguage", dumpdata), |
|
269 ("SISContents", handlerecursive), |
|
270 ("SISController", handlecontroller), |
|
271 ("SISInfo", dumpdata), # TODO: SISInfo |
|
272 ("SISSupportedLanguages", handlerecursive), |
|
273 ("SISSupportedOptions", handlerecursive), |
|
274 ("SISPrerequisites", handlerecursive), |
|
275 ("SISDependency", handlerecursive), |
|
276 ("SISProperties", handlerecursive), |
|
277 ("SISProperty", dumpdata), |
|
278 ("SISSignatures", handlerecursive), |
|
279 ("SISCertificateChain", handlerecursive), |
|
280 ("SISLogo", handlerecursive), |
|
281 ("SISFileDescription", dumpdata), # TODO: SISFileDescription |
|
282 ("SISHash", dumpdata), # TODO: SISHash |
|
283 ("SISIf", handlerecursive), |
|
284 ("SISElseIf", handlerecursive), |
|
285 ("SISInstallBlock", handlerecursive), |
|
286 ("SISExpression", dumpdata), # TODO: SISExpression |
|
287 ("SISData", handlerecursive), |
|
288 ("SISDataUnit", handlerecursive), |
|
289 ("SISFileData", handlefiledata), |
|
290 ("SISSupportedOption", handlerecursive), |
|
291 ("SISControllerChecksum", dumpdata), |
|
292 ("SISDataChecksum", dumpdata), |
|
293 ("SISSignature", handlerecursive), |
|
294 ("SISBlob", dumpdata), |
|
295 ("SISSignatureAlgorithm", handlerecursive), |
|
296 ("SISSignatureCertificateChain", handlerecursive), |
|
297 ("SISDataIndex", dumpdata), |
|
298 ("SISCapabilities", dumpdata) # TODO: SISCapabilities |
|
299 ] |
|
300 |
|
301 def parsesisfieldheader(data): |
|
302 datalen = len(data) |
|
303 |
|
304 headerlen = 8 |
|
305 if datalen < headerlen: |
|
306 raise ValueError("not enough data for a complete SISField header") |
|
307 |
|
308 # Get SISField type. |
|
309 fieldtype = struct.unpack("<L", data[:4])[0] |
|
310 |
|
311 # Get SISField length, 31-bit or 63-bit. |
|
312 fieldlen = struct.unpack("<L", data[4:8])[0] |
|
313 fieldlen2 = None |
|
314 if fieldlen & 0x8000000L: |
|
315 # 63-bit length, read rest of length. |
|
316 headerlen = 12 |
|
317 if datalen < headerlen: |
|
318 raise ValueError("not enough data for a complete SISField header") |
|
319 fieldlen2 = struct.unpack("<L", data[8:12])[0] |
|
320 fieldlen = (fieldlen & 0x7ffffffL) | (fieldlen2 << 31) |
|
321 |
|
322 return fieldtype, headerlen, fieldlen |
|
323 |
|
324 def parsesisfield(data, datalen, reclevel): |
|
325 '''Parse one SISField. Call an appropriate callback |
|
326 from sisfieldtypes[].''' |
|
327 |
|
328 fieldtype, headerlen, fieldlen = parsesisfieldheader(data) |
|
329 |
|
330 # Check SISField type. |
|
331 fieldcallback = None |
|
332 if fieldtype < len(sisfieldtypes): |
|
333 fieldname, fieldcallback = sisfieldtypes[fieldtype] |
|
334 |
|
335 if fieldcallback == None: |
|
336 # Invalid field type, terminate. |
|
337 raise ValueError("invalid SISField type %d" % fieldtype) |
|
338 |
|
339 # Calculate padding to 32-bit boundary. |
|
340 padlen = ((fieldlen + 3) & ~0x3L) - fieldlen |
|
341 |
|
342 # Verify length. |
|
343 if (headerlen + fieldlen + padlen) > datalen: |
|
344 raise ValueError("SISField contents too short") |
|
345 |
|
346 print "%s%s: %d bytes" % (" " * reclevel, fieldname, fieldlen) |
|
347 |
|
348 if options.headerdump: |
|
349 hexdump(data[:headerlen]) |
|
350 print |
|
351 |
|
352 # Call field callback. |
|
353 sisfieldtypes[fieldtype][1](data[headerlen:], fieldlen, reclevel) |
|
354 |
|
355 return headerlen + fieldlen + padlen |
|
356 |
|
357 def parsebuffer(data, datalen, reclevel): |
|
358 '''Parse all successive SISFields.''' |
|
359 |
|
360 datapos = 0 |
|
361 while datapos < datalen: |
|
362 fieldlen = parsesisfield(data[datapos:], datalen - datapos, reclevel) |
|
363 datapos += fieldlen |
|
364 |
|
365 return datapos |
|
366 |
|
367 def main(): |
|
368 global sisfilename, tempdir, dumpcounter, options |
|
369 |
|
370 pgmname = os.path.basename(sys.argv[0]) |
|
371 pgmversion = VERSION |
|
372 |
|
373 try: |
|
374 try: |
|
375 gopt = getopt.gnu_getopt |
|
376 except: |
|
377 # Python <v2.3, GNU-style parameter ordering not supported. |
|
378 gopt = getopt.getopt |
|
379 |
|
380 # Parse command line using getopt. |
|
381 short_opts = "decft:h" |
|
382 long_opts = [ |
|
383 "hexdump", "headerdump", "dumpcontroller", |
|
384 "dumptofile", "dumpdir", "help" |
|
385 ] |
|
386 args = gopt(sys.argv[1:], short_opts, long_opts) |
|
387 |
|
388 opts = dict(args[0]) |
|
389 pargs = args[1] |
|
390 |
|
391 if len(pargs) > 1 or "--help" in opts.keys() or "-h" in opts.keys(): |
|
392 # Help requested. |
|
393 print ( |
|
394 ''' |
|
395 DecodeSISX - Symbian OS v9.x SISX file decoder %(pgmversion)s |
|
396 |
|
397 usage: %(pgmname)s [--dumptofile] [--hexdump] [--dumpdir=DIR] [sisfile] |
|
398 |
|
399 -d, --hexdump - Show interesting SISFields as hex dumps |
|
400 -e, --headerdump - Show SISField headers as hex dumps |
|
401 -c, --dumpcontroller - Dump each SISController SISField separately |
|
402 -f, --dumptofile - Save interesting SISFields to files |
|
403 -t, --dumpdir - Directory to use for dumped files (automatic) |
|
404 sisfile - SIS file to decode (stdin if not given or -) |
|
405 |
|
406 ''' % locals()) |
|
407 return 0 |
|
408 |
|
409 if "--hexdump" in opts.keys() or "-d" in opts.keys(): |
|
410 options.hexdump = True |
|
411 |
|
412 if "--headerdump" in opts.keys() or "-e" in opts.keys(): |
|
413 options.headerdump = True |
|
414 |
|
415 if "--dumpcontroller" in opts.keys() or "-c" in opts.keys(): |
|
416 options.dumpcontroller = True |
|
417 |
|
418 if "--dumptofile" in opts.keys() or "-f" in opts.keys(): |
|
419 options.dumptofile = True |
|
420 |
|
421 # A temporary directory is generated by default. |
|
422 tempdir = opts.get("--dumpdir", opts.get("-t", None)) |
|
423 |
|
424 if len(pargs) == 0 or pargs[0] == '-': |
|
425 sisfilename = "stdin" |
|
426 sisfile = sys.stdin |
|
427 else: |
|
428 sisfilename = pargs[0] |
|
429 sisfile = file(sisfilename, "rb") |
|
430 |
|
431 try: |
|
432 # Load the whole SIS file as a string. |
|
433 sisdata = sisfile.read(MAXSISFILESIZE) |
|
434 if len(sisdata) == MAXSISFILESIZE: |
|
435 raise IOError("%s: file too large" % sisfilename) |
|
436 finally: |
|
437 if sisfile != sys.stdin: |
|
438 sisfile.close() |
|
439 |
|
440 if len(sisdata) < 16: |
|
441 raise ValueError("%s: file too short" % sisfilename) |
|
442 |
|
443 # Check UIDs. |
|
444 uid1, uid2, uid3, uidcrc = struct.unpack("<LLLL", sisdata[:16]) |
|
445 if uid1 != 0x10201a7a: |
|
446 if (uid2 in (0x1000006D, 0x10003A12)) and uid3 == 0x10000419: |
|
447 raise ValueError("%s: pre-9.1 SIS file" % sisfilename) |
|
448 else: |
|
449 raise ValueError("%s: not a SIS file" % sisfilename) |
|
450 |
|
451 print "UID1: 0x%08x, UID2: 0x%08x, UID3: 0x%08x, UIDCRC: 0x%08x\n" % ( |
|
452 uid1, uid2, uid3, uidcrc) |
|
453 |
|
454 # Recursively parse the SIS file. |
|
455 parsebuffer(sisdata[16:], len(sisdata) - 16, 0) |
|
456 except (TypeError, ValueError, IOError, OSError), e: |
|
457 return "%s: %s" % (pgmname, str(e)) |
|
458 except KeyboardInterrupt: |
|
459 return "" |
|
460 |
|
461 # Call main if run as stand-alone executable. |
|
462 if __name__ == '__main__': |
|
463 sys.exit(main()) |