0
|
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())
|