apklisparser 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apklisparser/__init__.py +0 -0
- apklisparser/apk.py +1228 -0
- apklisparser/axml.py +1818 -0
- apklisparser/build_icon.py +48 -0
- apklisparser/util.py +2006 -0
- apklisparser/vd2png.py +201 -0
- apklisparser-1.0.0.dist-info/METADATA +24 -0
- apklisparser-1.0.0.dist-info/RECORD +10 -0
- apklisparser-1.0.0.dist-info/WHEEL +5 -0
- apklisparser-1.0.0.dist-info/top_level.txt +1 -0
apklisparser/apk.py
ADDED
|
@@ -0,0 +1,1228 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
import zipfile
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from struct import unpack
|
|
7
|
+
from zlib import crc32
|
|
8
|
+
|
|
9
|
+
from cryptography import x509
|
|
10
|
+
from cryptography.hazmat.backends import default_backend
|
|
11
|
+
|
|
12
|
+
# Used for reading Certificates
|
|
13
|
+
from pyasn1.codec.der.decoder import decode
|
|
14
|
+
from pyasn1.codec.der.encoder import encode
|
|
15
|
+
|
|
16
|
+
from . import util
|
|
17
|
+
from .axml import ARSCParser, ARSCResTableConfig, AXMLPrinter
|
|
18
|
+
from lxml import objectify, etree
|
|
19
|
+
from .build_icon import build_icon
|
|
20
|
+
|
|
21
|
+
NS_ANDROID_URI = "http://schemas.android.com/apk/res/android"
|
|
22
|
+
NS_ANDROID = "{http://schemas.android.com/apk/res/android}"
|
|
23
|
+
|
|
24
|
+
log = logging.getLogger("ApklisParser")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Error(Exception):
|
|
28
|
+
"""Base class for exceptions in this module."""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FileNotPresent(Error):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BrokenAPKError(Error):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_xml_clean = etree.XSLT(
|
|
42
|
+
etree.fromstring(
|
|
43
|
+
"""
|
|
44
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
45
|
+
<xsl:output indent="yes"/>
|
|
46
|
+
<xsl:strip-space elements="*"/>
|
|
47
|
+
|
|
48
|
+
<xsl:template match="node()">
|
|
49
|
+
<xsl:copy>
|
|
50
|
+
<xsl:apply-templates select="@*|node()"/>
|
|
51
|
+
</xsl:copy>
|
|
52
|
+
</xsl:template>
|
|
53
|
+
|
|
54
|
+
<xsl:template match="*" priority="1">
|
|
55
|
+
<xsl:element name="{local-name()}" namespace="">
|
|
56
|
+
<xsl:apply-templates select="@*|node()"/>
|
|
57
|
+
</xsl:element>
|
|
58
|
+
</xsl:template>
|
|
59
|
+
|
|
60
|
+
<xsl:template match="@*">
|
|
61
|
+
<xsl:attribute name="{local-name()}" namespace="">
|
|
62
|
+
<xsl:value-of select="."/>
|
|
63
|
+
</xsl:attribute>
|
|
64
|
+
</xsl:template>
|
|
65
|
+
</xsl:stylesheet>
|
|
66
|
+
"""
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class APK(object):
|
|
72
|
+
# Constants in ZipFile
|
|
73
|
+
PK_END_OF_CENTRAL_DIR = b"\x50\x4b\x05\x06"
|
|
74
|
+
PK_CENTRAL_DIR = b"\x50\x4b\x01\x02"
|
|
75
|
+
|
|
76
|
+
# Constants in the APK Signature Block
|
|
77
|
+
APK_SIG_MAGIC = b"APK Sig Block 42"
|
|
78
|
+
APK_SIG_KEY_SIGNATURE = 0x7109871a
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self, filename, raw=False, magic_file=None, skip_analysis=False, testzip=False
|
|
82
|
+
):
|
|
83
|
+
"""
|
|
84
|
+
This class can access to all elements in an APK file
|
|
85
|
+
|
|
86
|
+
:param filename: specify the path of the file, or raw data
|
|
87
|
+
:param raw: specify if the filename is a path or raw data (optional)
|
|
88
|
+
:param magic_file: specify the magic file (optional)
|
|
89
|
+
:param skip_analysis: Skip the analysis, e.g. no manifest files are read. (default: False)
|
|
90
|
+
:param testzip: Test the APK for integrity, e.g. if the ZIP file is broken. Throw an exception on failure (default False)
|
|
91
|
+
|
|
92
|
+
:type filename: string
|
|
93
|
+
:type raw: boolean
|
|
94
|
+
:type magic_file: string
|
|
95
|
+
:type skip_analysis: boolean
|
|
96
|
+
:type testzip: boolean
|
|
97
|
+
|
|
98
|
+
:Example:
|
|
99
|
+
APK("myfile.apk")
|
|
100
|
+
APK(read("myfile.apk"), raw=True)
|
|
101
|
+
"""
|
|
102
|
+
self.filename = filename
|
|
103
|
+
|
|
104
|
+
self.xml = {}
|
|
105
|
+
self.axml = {}
|
|
106
|
+
self.arsc = {}
|
|
107
|
+
|
|
108
|
+
self.package = ""
|
|
109
|
+
self.androidversion = {}
|
|
110
|
+
self.permissions = []
|
|
111
|
+
self.uses_permissions = []
|
|
112
|
+
self.declared_permissions = {}
|
|
113
|
+
self.app_icon = ""
|
|
114
|
+
self.app_name = ""
|
|
115
|
+
self.valid_apk = False
|
|
116
|
+
self._is_signed_v2 = None
|
|
117
|
+
self._v2_blocks = {}
|
|
118
|
+
|
|
119
|
+
self._files = {}
|
|
120
|
+
self.files_crc32 = {}
|
|
121
|
+
|
|
122
|
+
self.magic_file = magic_file
|
|
123
|
+
|
|
124
|
+
if raw is True:
|
|
125
|
+
self.__raw = bytearray(filename)
|
|
126
|
+
else:
|
|
127
|
+
self.__raw = bytearray(util.read(filename))
|
|
128
|
+
|
|
129
|
+
self.size = len(self.__raw)
|
|
130
|
+
|
|
131
|
+
self.zip = zipfile.ZipFile(io.BytesIO(self.__raw), mode="r")
|
|
132
|
+
|
|
133
|
+
if testzip:
|
|
134
|
+
# Test the zipfile for integrity before continuing.
|
|
135
|
+
# This process might be slow, as the whole file is read.
|
|
136
|
+
# Therefore it is possible to enable it as a separate feature.
|
|
137
|
+
#
|
|
138
|
+
# A short benchmark showed, that testing the zip takes about 10 times longer!
|
|
139
|
+
# e.g. normal zip loading (skip_analysis=True) takes about 0.01s, where
|
|
140
|
+
# testzip takes 0.1s!
|
|
141
|
+
ret = self.zip.testzip()
|
|
142
|
+
if ret is not None:
|
|
143
|
+
# we could print the filename here, but there are zip which are so broken
|
|
144
|
+
# That the filename is either very very long or does not make any sense.
|
|
145
|
+
# Thus we do not do it, the user might find out by using other tools.
|
|
146
|
+
raise BrokenAPKError(
|
|
147
|
+
"The APK is probably broken: testzip returned an error."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if not skip_analysis:
|
|
151
|
+
self._apk_analysis()
|
|
152
|
+
|
|
153
|
+
def _apk_analysis(self):
|
|
154
|
+
"""
|
|
155
|
+
Run analysis on the APK file.
|
|
156
|
+
|
|
157
|
+
This method is usually called by __init__ except if skip_analysis is False.
|
|
158
|
+
It will then parse the AndroidManifest.xml and set all fields in the APK class which can be
|
|
159
|
+
extracted from the Manifest.
|
|
160
|
+
"""
|
|
161
|
+
for i in self.zip.namelist():
|
|
162
|
+
if i == "AndroidManifest.xml":
|
|
163
|
+
self.axml[i] = AXMLPrinter(self.zip.read(i))
|
|
164
|
+
self.xml[i] = None
|
|
165
|
+
raw_xml = self.axml[i].get_buff()
|
|
166
|
+
if len(raw_xml) == 0:
|
|
167
|
+
log.warning("AXML parsing failed, file is empty")
|
|
168
|
+
else:
|
|
169
|
+
try:
|
|
170
|
+
if self.axml[i].is_packed():
|
|
171
|
+
log.warning(
|
|
172
|
+
"XML Seems to be packed, parsing is very likely to fail."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
self.xml[i] = _xml_clean(self.axml[i].get_xml_obj()).getroot()
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
log.warning("reading AXML as XML failed: " + str(e))
|
|
179
|
+
|
|
180
|
+
if self.xml[i] is not None:
|
|
181
|
+
self.package = self.xml[i].get("package")
|
|
182
|
+
self.androidversion["Code"] = self.xml[i].get("versionCode")
|
|
183
|
+
self.androidversion["Name"] = self.xml[i].get("versionName")
|
|
184
|
+
|
|
185
|
+
for item in self.xml[i].findall("uses-permission"):
|
|
186
|
+
name = item.get("name")
|
|
187
|
+
self.permissions.append(name)
|
|
188
|
+
maxSdkVersion = None
|
|
189
|
+
try:
|
|
190
|
+
maxSdkVersion = int(item.get("maxSdkVersion"))
|
|
191
|
+
except ValueError:
|
|
192
|
+
log.warning(
|
|
193
|
+
item.get("maxSdkVersion")
|
|
194
|
+
+ "is not a valid value for <uses-permission> maxSdkVersion"
|
|
195
|
+
)
|
|
196
|
+
except TypeError:
|
|
197
|
+
pass
|
|
198
|
+
self.uses_permissions.append([name, maxSdkVersion])
|
|
199
|
+
|
|
200
|
+
# getting details of the declared permissions
|
|
201
|
+
for d_perm_item in self.xml[i].findall("permission"):
|
|
202
|
+
d_perm_name = self._get_res_string_value(
|
|
203
|
+
str(d_perm_item.get("name"))
|
|
204
|
+
)
|
|
205
|
+
d_perm_label = self._get_res_string_value(
|
|
206
|
+
str(d_perm_item.get("label"))
|
|
207
|
+
)
|
|
208
|
+
d_perm_description = self._get_res_string_value(
|
|
209
|
+
str(d_perm_item.get("description"))
|
|
210
|
+
)
|
|
211
|
+
d_perm_permissionGroup = self._get_res_string_value(
|
|
212
|
+
str(d_perm_item.get("permissionGroup"))
|
|
213
|
+
)
|
|
214
|
+
d_perm_protectionLevel = self._get_res_string_value(
|
|
215
|
+
str(d_perm_item.get("protectionLevel"))
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
d_perm_details = {
|
|
219
|
+
"label": d_perm_label,
|
|
220
|
+
"description": d_perm_description,
|
|
221
|
+
"permissionGroup": d_perm_permissionGroup,
|
|
222
|
+
"protectionLevel": d_perm_protectionLevel,
|
|
223
|
+
}
|
|
224
|
+
self.declared_permissions[d_perm_name] = d_perm_details
|
|
225
|
+
|
|
226
|
+
self.valid_apk = True
|
|
227
|
+
|
|
228
|
+
def __getstate__(self):
|
|
229
|
+
"""
|
|
230
|
+
Function for pickling APK Objects.
|
|
231
|
+
|
|
232
|
+
We remove the zip from the Object, as it is not pickable
|
|
233
|
+
And it does not make any sense to pickle it anyways.
|
|
234
|
+
|
|
235
|
+
:return: the picklable APK Object without zip.
|
|
236
|
+
"""
|
|
237
|
+
# Upon pickling, we need to remove the ZipFile
|
|
238
|
+
x = self.__dict__
|
|
239
|
+
x["axml"] = str(x["axml"])
|
|
240
|
+
x["xml"] = str(x["xml"])
|
|
241
|
+
del x["zip"]
|
|
242
|
+
|
|
243
|
+
return x
|
|
244
|
+
|
|
245
|
+
def get_resource_as_xml(self, id_, which=lambda x: x[0]):
|
|
246
|
+
res_parser = self.get_android_resources()
|
|
247
|
+
if isinstance(id_, str):
|
|
248
|
+
id_ = int(id_.replace('@', ''), 16)
|
|
249
|
+
candidates = res_parser.get_resolved_res_configs(id_)
|
|
250
|
+
conf, name = which(candidates)
|
|
251
|
+
if name not in self.xml:
|
|
252
|
+
self.xml[name] = AXMLPrinter(self.get_file(name)).get_xml_obj()
|
|
253
|
+
return self.xml[name]
|
|
254
|
+
|
|
255
|
+
def __setstate__(self, state):
|
|
256
|
+
"""
|
|
257
|
+
Load a pickled APK Object and restore the state
|
|
258
|
+
|
|
259
|
+
We load the zip file back by reading __raw from the Object.
|
|
260
|
+
|
|
261
|
+
:param state: pickled state
|
|
262
|
+
"""
|
|
263
|
+
self.__dict__ = state
|
|
264
|
+
|
|
265
|
+
self.zip = zipfile.ZipFile(io.BytesIO(self.__raw), mode="r")
|
|
266
|
+
|
|
267
|
+
def _get_res_string_value(self, string):
|
|
268
|
+
if not string.startswith("@string/"):
|
|
269
|
+
return string
|
|
270
|
+
string_key = string[9:]
|
|
271
|
+
|
|
272
|
+
res_parser = self.get_android_resources()
|
|
273
|
+
string_value = ""
|
|
274
|
+
for package_name in res_parser.get_packages_names():
|
|
275
|
+
extracted_values = res_parser.get_string(package_name, string_key)
|
|
276
|
+
if extracted_values:
|
|
277
|
+
string_value = extracted_values[1]
|
|
278
|
+
break
|
|
279
|
+
return string_value
|
|
280
|
+
|
|
281
|
+
def is_valid_APK(self):
|
|
282
|
+
"""
|
|
283
|
+
Return true if the APK is valid, false otherwise
|
|
284
|
+
|
|
285
|
+
:rtype: boolean
|
|
286
|
+
"""
|
|
287
|
+
return self.valid_apk
|
|
288
|
+
|
|
289
|
+
def get_size(self):
|
|
290
|
+
return self.size
|
|
291
|
+
|
|
292
|
+
def get_filename(self):
|
|
293
|
+
"""
|
|
294
|
+
Return the filename of the APK
|
|
295
|
+
|
|
296
|
+
:rtype: string
|
|
297
|
+
"""
|
|
298
|
+
return self.filename
|
|
299
|
+
|
|
300
|
+
def get_name(self):
|
|
301
|
+
"""
|
|
302
|
+
Return the appname of the APK
|
|
303
|
+
|
|
304
|
+
:rtype: string
|
|
305
|
+
"""
|
|
306
|
+
if not self.app_name:
|
|
307
|
+
self.app_name = self.get_element("application", "label")
|
|
308
|
+
|
|
309
|
+
if not self.app_name:
|
|
310
|
+
self.app_name = self.get_element(
|
|
311
|
+
"activity", "label", name=self.get_main_activity()
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if not self.app_name:
|
|
315
|
+
raise Exception("Error extracting application name.")
|
|
316
|
+
|
|
317
|
+
if self.app_name.startswith("@"):
|
|
318
|
+
res_id = int(self.app_name[1:], 16)
|
|
319
|
+
res_parser = self.get_android_resources()
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
self.app_name = res_parser.get_resolved_res_configs(
|
|
323
|
+
res_id, ARSCResTableConfig.default_config()
|
|
324
|
+
)[0][1]
|
|
325
|
+
except Exception as e:
|
|
326
|
+
raise Exception('Error extracting application name "%s".' % e)
|
|
327
|
+
|
|
328
|
+
return self.app_name
|
|
329
|
+
|
|
330
|
+
def get_icon(self, max_dpi=65536):
|
|
331
|
+
"""
|
|
332
|
+
Return the first non-greater density than max_dpi icon file name background and foreground,
|
|
333
|
+
unless exact icon resolution is set in the manifest, in which case
|
|
334
|
+
return the exact file
|
|
335
|
+
|
|
336
|
+
From https://developer.android.com/guide/practices/screens_support.html
|
|
337
|
+
ldpi (low) ~120dpi
|
|
338
|
+
mdpi (medium) ~160dpi
|
|
339
|
+
hdpi (high) ~240dpi
|
|
340
|
+
xhdpi (extra-high) ~320dpi
|
|
341
|
+
xxhdpi (extra-extra-high) ~480dpi
|
|
342
|
+
xxxhdpi (extra-extra-extra-high) ~640dpi
|
|
343
|
+
|
|
344
|
+
:rtype: string
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
if not self.app_icon:
|
|
348
|
+
self.app_icon = self.get_element("application", "icon") or self.get_element(
|
|
349
|
+
"activity", "icon", name=self.get_main_activity()
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if self.app_icon.startswith("@"):
|
|
353
|
+
self.app_icon = self._resolve_icon_resource(self.app_icon[1:], max_dpi)
|
|
354
|
+
|
|
355
|
+
if not self.app_icon:
|
|
356
|
+
raise Exception("Impossible to extract application icon.")
|
|
357
|
+
|
|
358
|
+
return self.app_icon
|
|
359
|
+
|
|
360
|
+
def _resolve_icon_resource(self, res, max_dpi):
|
|
361
|
+
res_id = int(res, 16)
|
|
362
|
+
res_parser = self.get_android_resources()
|
|
363
|
+
candidates = res_parser.get_resolved_res_configs(res_id)
|
|
364
|
+
|
|
365
|
+
res = None
|
|
366
|
+
current_dpi = -1
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
for config, file_name in candidates:
|
|
370
|
+
dpi = config.get_density()
|
|
371
|
+
if current_dpi < dpi <= max_dpi:
|
|
372
|
+
res = file_name
|
|
373
|
+
current_dpi = dpi
|
|
374
|
+
except Exception as e:
|
|
375
|
+
log.warning("Exception selecting application res: %s" % e)
|
|
376
|
+
return res
|
|
377
|
+
|
|
378
|
+
def get_part(self, tree, key):
|
|
379
|
+
element = tree.find(key)
|
|
380
|
+
if len(element.values()) == 0:
|
|
381
|
+
element = element.find('inset')
|
|
382
|
+
|
|
383
|
+
return element
|
|
384
|
+
|
|
385
|
+
def extract_icon(self, filename, max_dpi=65536):
|
|
386
|
+
"""
|
|
387
|
+
Extract application icon in `filename` location
|
|
388
|
+
:param filename:
|
|
389
|
+
:return:
|
|
390
|
+
"""
|
|
391
|
+
icon = self.get_icon()
|
|
392
|
+
if icon.endswith(".xml"):
|
|
393
|
+
icon_element = AXMLPrinter(self.get_file(icon)).get_xml_obj()
|
|
394
|
+
if icon_element.tag in ('adaptative-icon', 'adaptive-icon'):
|
|
395
|
+
parts = [
|
|
396
|
+
(self.get_part(icon_element, 'background').values())[0].replace(
|
|
397
|
+
"android:", ""
|
|
398
|
+
),
|
|
399
|
+
(self.get_part(icon_element, 'foreground').values())[0].replace(
|
|
400
|
+
"android:", ""
|
|
401
|
+
),
|
|
402
|
+
]
|
|
403
|
+
else:
|
|
404
|
+
# should be a bitmap
|
|
405
|
+
parts = [icon_element.attrib.values()[0]]
|
|
406
|
+
parts = [
|
|
407
|
+
self._resolve_icon_resource(p[1:], max_dpi) if p.startswith("@") else p
|
|
408
|
+
for p in parts
|
|
409
|
+
]
|
|
410
|
+
else:
|
|
411
|
+
parts = [icon]
|
|
412
|
+
parts = filter(None, parts) # some app have invalide parts :'(
|
|
413
|
+
parts = [(p, None if p.startswith("#") else self.get_file(p)) for p in parts]
|
|
414
|
+
build_icon(self, parts, filename)
|
|
415
|
+
|
|
416
|
+
def get_package_name(self):
|
|
417
|
+
"""
|
|
418
|
+
Return the name of the package
|
|
419
|
+
:rtype: string
|
|
420
|
+
"""
|
|
421
|
+
return self.package
|
|
422
|
+
|
|
423
|
+
def get_version_code(self):
|
|
424
|
+
"""
|
|
425
|
+
Return the application version code
|
|
426
|
+
|
|
427
|
+
:rtype: string
|
|
428
|
+
"""
|
|
429
|
+
return self.androidversion["Code"]
|
|
430
|
+
|
|
431
|
+
def get_version_name(self):
|
|
432
|
+
"""
|
|
433
|
+
Return the application version name
|
|
434
|
+
|
|
435
|
+
:rtype: string
|
|
436
|
+
"""
|
|
437
|
+
return self.androidversion["Name"]
|
|
438
|
+
|
|
439
|
+
def get_files(self):
|
|
440
|
+
"""
|
|
441
|
+
Return the files inside the APK
|
|
442
|
+
|
|
443
|
+
:rtype: a list of strings
|
|
444
|
+
"""
|
|
445
|
+
return self.zip.namelist()
|
|
446
|
+
|
|
447
|
+
def _get_file_magic_name(self, buffer):
|
|
448
|
+
"""
|
|
449
|
+
Return the filetype guessed for a buffer
|
|
450
|
+
:param buffer: bytes
|
|
451
|
+
:return: str of filetype
|
|
452
|
+
"""
|
|
453
|
+
# TODO this functions should be better in another package
|
|
454
|
+
default = "Unknown"
|
|
455
|
+
ftype = None
|
|
456
|
+
|
|
457
|
+
# There are several implementations of magic,
|
|
458
|
+
# unfortunately all called magic
|
|
459
|
+
try:
|
|
460
|
+
import magic
|
|
461
|
+
except ImportError:
|
|
462
|
+
# no lib magic at all, return unknown
|
|
463
|
+
return default
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
# We test for the python-magic package here
|
|
467
|
+
getattr(magic, "MagicException")
|
|
468
|
+
except AttributeError:
|
|
469
|
+
try:
|
|
470
|
+
# Check for filemagic package
|
|
471
|
+
getattr(magic.Magic, "id_buffer")
|
|
472
|
+
except AttributeError:
|
|
473
|
+
# Here, we load the file-magic package
|
|
474
|
+
ms = magic.open(magic.MAGIC_NONE)
|
|
475
|
+
ms.load()
|
|
476
|
+
ftype = ms.buffer(buffer)
|
|
477
|
+
else:
|
|
478
|
+
# This is now the filemagic package
|
|
479
|
+
if self.magic_file is not None:
|
|
480
|
+
m = magic.Magic(paths=[self.magic_file])
|
|
481
|
+
else:
|
|
482
|
+
m = magic.Magic()
|
|
483
|
+
ftype = m.id_buffer(buffer)
|
|
484
|
+
else:
|
|
485
|
+
# This is the code for python-magic
|
|
486
|
+
if self.magic_file is not None:
|
|
487
|
+
m = magic.Magic(magic_file=self.magic_file)
|
|
488
|
+
else:
|
|
489
|
+
m = magic.Magic()
|
|
490
|
+
ftype = m.from_buffer(buffer)
|
|
491
|
+
|
|
492
|
+
if ftype is None:
|
|
493
|
+
return default
|
|
494
|
+
else:
|
|
495
|
+
return self._patch_magic(buffer, ftype)
|
|
496
|
+
|
|
497
|
+
@property
|
|
498
|
+
def files(self):
|
|
499
|
+
"""
|
|
500
|
+
Wrapper for the files object
|
|
501
|
+
|
|
502
|
+
:return: dictionary of files and their mime type
|
|
503
|
+
"""
|
|
504
|
+
return self.get_files_types()
|
|
505
|
+
|
|
506
|
+
def get_files_types(self):
|
|
507
|
+
"""
|
|
508
|
+
Return the files inside the APK with their associated types (by using python-magic)
|
|
509
|
+
|
|
510
|
+
:rtype: a dictionnary
|
|
511
|
+
"""
|
|
512
|
+
if self._files == {}:
|
|
513
|
+
# Generate File Types / CRC List
|
|
514
|
+
for i in self.get_files():
|
|
515
|
+
buffer = self.zip.read(i)
|
|
516
|
+
self.files_crc32[i] = crc32(buffer)
|
|
517
|
+
# FIXME why not use the crc from the zipfile?
|
|
518
|
+
# crc = self.zip.getinfo(i).CRC
|
|
519
|
+
self._files[i] = self._get_file_magic_name(buffer)
|
|
520
|
+
|
|
521
|
+
return self._files
|
|
522
|
+
|
|
523
|
+
def _patch_magic(self, buffer, orig):
|
|
524
|
+
if ("Zip" in orig) or ("DBase" in orig):
|
|
525
|
+
val = util.is_android_raw(buffer)
|
|
526
|
+
if val == "APK":
|
|
527
|
+
return "Android application package file"
|
|
528
|
+
elif val == "AXML":
|
|
529
|
+
return "Android's binary XML"
|
|
530
|
+
|
|
531
|
+
return orig
|
|
532
|
+
|
|
533
|
+
def get_files_crc32(self):
|
|
534
|
+
"""
|
|
535
|
+
Calculates and returns a dictionary of filenames and CRC32
|
|
536
|
+
|
|
537
|
+
:return: dict of filename: CRC32
|
|
538
|
+
"""
|
|
539
|
+
if self.files_crc32 == {}:
|
|
540
|
+
for i in self.get_files():
|
|
541
|
+
buffer = self.zip.read(i)
|
|
542
|
+
self.files_crc32[i] = crc32(buffer)
|
|
543
|
+
|
|
544
|
+
return self.files_crc32
|
|
545
|
+
|
|
546
|
+
def get_files_information(self):
|
|
547
|
+
"""
|
|
548
|
+
Return the files inside the APK with their associated types and crc32
|
|
549
|
+
|
|
550
|
+
:rtype: string, string, int
|
|
551
|
+
"""
|
|
552
|
+
for k in self.get_files():
|
|
553
|
+
yield k, self.get_files_types()[k], self.get_files_crc32()[k]
|
|
554
|
+
|
|
555
|
+
def get_raw(self):
|
|
556
|
+
"""
|
|
557
|
+
Return raw bytes of the APK
|
|
558
|
+
|
|
559
|
+
:rtype: string
|
|
560
|
+
"""
|
|
561
|
+
return self.__raw
|
|
562
|
+
|
|
563
|
+
def get_file(self, filename):
|
|
564
|
+
"""
|
|
565
|
+
Return the raw data of the specified filename
|
|
566
|
+
inside the APK
|
|
567
|
+
|
|
568
|
+
:rtype: string
|
|
569
|
+
"""
|
|
570
|
+
try:
|
|
571
|
+
return self.zip.read(filename)
|
|
572
|
+
except KeyError:
|
|
573
|
+
raise FileNotPresent(filename)
|
|
574
|
+
|
|
575
|
+
def get_dex(self):
|
|
576
|
+
"""
|
|
577
|
+
Return the raw data of the classes dex file
|
|
578
|
+
|
|
579
|
+
:rtype: a string
|
|
580
|
+
"""
|
|
581
|
+
try:
|
|
582
|
+
return self.get_file("classes.dex")
|
|
583
|
+
except FileNotPresent:
|
|
584
|
+
return ""
|
|
585
|
+
|
|
586
|
+
def get_dex_names(self):
|
|
587
|
+
"""
|
|
588
|
+
Return the name of all classes dex files
|
|
589
|
+
|
|
590
|
+
:rtype: a list of string
|
|
591
|
+
"""
|
|
592
|
+
dexre = re.compile(r"classes(\d*).dex")
|
|
593
|
+
return filter(lambda x: dexre.match(x), self.get_files())
|
|
594
|
+
|
|
595
|
+
def get_all_dex(self):
|
|
596
|
+
"""
|
|
597
|
+
Return the raw data of all classes dex files
|
|
598
|
+
|
|
599
|
+
:rtype: a generator
|
|
600
|
+
"""
|
|
601
|
+
for dex_name in self.get_dex_names():
|
|
602
|
+
yield self.get_file(dex_name)
|
|
603
|
+
|
|
604
|
+
def is_multidex(self):
|
|
605
|
+
"""
|
|
606
|
+
Test if the APK has multiple DEX files
|
|
607
|
+
|
|
608
|
+
:return: True if multiple dex found, otherwise False
|
|
609
|
+
"""
|
|
610
|
+
dexre = re.compile(r"^classes(\d+)?.dex$")
|
|
611
|
+
return (
|
|
612
|
+
len([instance for instance in self.get_files() if dexre.search(instance)])
|
|
613
|
+
> 1
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
def get_elements(self, tag_name, attribute, with_namespace=True):
|
|
617
|
+
"""
|
|
618
|
+
Return elements in xml files which match with the tag name and the specific attribute
|
|
619
|
+
|
|
620
|
+
:param tag_name: a string which specify the tag name
|
|
621
|
+
:param attribute: a string which specify the attribute
|
|
622
|
+
"""
|
|
623
|
+
for i in self.xml:
|
|
624
|
+
for item in self.xml[i].findall(".//" + tag_name):
|
|
625
|
+
if with_namespace:
|
|
626
|
+
value = item.get(attribute)
|
|
627
|
+
else:
|
|
628
|
+
value = item.get(attribute)
|
|
629
|
+
# There might be an attribute without the namespace
|
|
630
|
+
if value:
|
|
631
|
+
yield self.format_value(value)
|
|
632
|
+
|
|
633
|
+
def format_value(self, value):
|
|
634
|
+
"""
|
|
635
|
+
Format a value with packagename, if not already set
|
|
636
|
+
|
|
637
|
+
:param value:
|
|
638
|
+
:return:
|
|
639
|
+
"""
|
|
640
|
+
if len(value) > 0:
|
|
641
|
+
if value[0] == ".":
|
|
642
|
+
value = self.package + value
|
|
643
|
+
else:
|
|
644
|
+
v_dot = value.find(".")
|
|
645
|
+
if v_dot == 0:
|
|
646
|
+
value = self.package + "." + value
|
|
647
|
+
elif v_dot == -1:
|
|
648
|
+
value = self.package + "." + value
|
|
649
|
+
return value
|
|
650
|
+
|
|
651
|
+
def get_element(self, tag_name, attribute, **attribute_filter):
|
|
652
|
+
"""
|
|
653
|
+
Return element in xml files which match with the tag name and the specific attribute
|
|
654
|
+
|
|
655
|
+
:param tag_name: specify the tag name
|
|
656
|
+
:type tag_name: string
|
|
657
|
+
:param attribute: specify the attribute
|
|
658
|
+
:type attribute: string
|
|
659
|
+
|
|
660
|
+
:rtype: string
|
|
661
|
+
"""
|
|
662
|
+
for i in self.xml:
|
|
663
|
+
if self.xml[i] is None:
|
|
664
|
+
continue
|
|
665
|
+
tag = self.xml[i].findall(".//" + tag_name)
|
|
666
|
+
if len(tag) == 0:
|
|
667
|
+
return None
|
|
668
|
+
for item in tag:
|
|
669
|
+
skip_this_item = False
|
|
670
|
+
for attr, val in list(attribute_filter.items()):
|
|
671
|
+
attr_val = item.get(attr)
|
|
672
|
+
if attr_val != val:
|
|
673
|
+
skip_this_item = True
|
|
674
|
+
break
|
|
675
|
+
|
|
676
|
+
if skip_this_item:
|
|
677
|
+
continue
|
|
678
|
+
|
|
679
|
+
value = item.get(attribute)
|
|
680
|
+
|
|
681
|
+
if value is not None:
|
|
682
|
+
return value
|
|
683
|
+
return None
|
|
684
|
+
|
|
685
|
+
def get_main_activity(self):
|
|
686
|
+
"""
|
|
687
|
+
Return the name of the main activity
|
|
688
|
+
|
|
689
|
+
:rtype: string
|
|
690
|
+
"""
|
|
691
|
+
x = set()
|
|
692
|
+
y = set()
|
|
693
|
+
|
|
694
|
+
for i in self.xml:
|
|
695
|
+
activities_and_aliases = self.xml[i].findall(".//activity") + self.xml[
|
|
696
|
+
i
|
|
697
|
+
].findall(".//activity-alias")
|
|
698
|
+
|
|
699
|
+
for item in activities_and_aliases:
|
|
700
|
+
# Some applications have more than one MAIN activity.
|
|
701
|
+
# For example: paid and free content
|
|
702
|
+
activityEnabled = item.get("enabled")
|
|
703
|
+
if (
|
|
704
|
+
activityEnabled is not None
|
|
705
|
+
and activityEnabled != ""
|
|
706
|
+
and activityEnabled == "false"
|
|
707
|
+
):
|
|
708
|
+
continue
|
|
709
|
+
|
|
710
|
+
for sitem in item.findall(".//action"):
|
|
711
|
+
val = sitem.get("name")
|
|
712
|
+
if val == "android.intent.action.MAIN":
|
|
713
|
+
x.add(item.get("name"))
|
|
714
|
+
|
|
715
|
+
for sitem in item.findall(".//category"):
|
|
716
|
+
val = sitem.get("name")
|
|
717
|
+
if val == "android.intent.category.LAUNCHER":
|
|
718
|
+
y.add(item.get("name"))
|
|
719
|
+
|
|
720
|
+
z = x.intersection(y)
|
|
721
|
+
if len(z) > 0:
|
|
722
|
+
return self.format_value(z.pop())
|
|
723
|
+
return None
|
|
724
|
+
|
|
725
|
+
def get_activities(self):
|
|
726
|
+
"""
|
|
727
|
+
Return the android:name attribute of all activities
|
|
728
|
+
|
|
729
|
+
:rtype: a list of string
|
|
730
|
+
"""
|
|
731
|
+
return list(self.get_elements("activity", "name"))
|
|
732
|
+
|
|
733
|
+
def get_services(self):
|
|
734
|
+
"""
|
|
735
|
+
Return the android:name attribute of all services
|
|
736
|
+
|
|
737
|
+
:rtype: a list of string
|
|
738
|
+
"""
|
|
739
|
+
return list(self.get_elements("service", "name"))
|
|
740
|
+
|
|
741
|
+
def get_receivers(self):
|
|
742
|
+
"""
|
|
743
|
+
Return the android:name attribute of all receivers
|
|
744
|
+
|
|
745
|
+
:rtype: a list of string
|
|
746
|
+
"""
|
|
747
|
+
return list(self.get_elements("receiver", "name"))
|
|
748
|
+
|
|
749
|
+
def get_providers(self):
|
|
750
|
+
"""
|
|
751
|
+
Return the android:name attribute of all providers
|
|
752
|
+
|
|
753
|
+
:rtype: a list of string
|
|
754
|
+
"""
|
|
755
|
+
return list(self.get_elements("provider", "name"))
|
|
756
|
+
|
|
757
|
+
def get_intent_filters(self, category, name):
|
|
758
|
+
d = {"action": [], "category": []}
|
|
759
|
+
|
|
760
|
+
for i in self.xml:
|
|
761
|
+
for item in self.xml[i].findall(".//" + category):
|
|
762
|
+
if self.format_value(item.get("name")) == name:
|
|
763
|
+
for sitem in item.findall(".//intent-filter"):
|
|
764
|
+
for ssitem in sitem.findall("action"):
|
|
765
|
+
if ssitem.get("name") not in d["action"]:
|
|
766
|
+
d["action"].append(ssitem.get("name"))
|
|
767
|
+
for ssitem in sitem.findall("category"):
|
|
768
|
+
if ssitem.get("name") not in d["category"]:
|
|
769
|
+
d["category"].append(ssitem.get("name"))
|
|
770
|
+
|
|
771
|
+
if not d["action"]:
|
|
772
|
+
del d["action"]
|
|
773
|
+
|
|
774
|
+
if not d["category"]:
|
|
775
|
+
del d["category"]
|
|
776
|
+
|
|
777
|
+
return d
|
|
778
|
+
|
|
779
|
+
def get_permissions(self):
|
|
780
|
+
"""
|
|
781
|
+
Return permissions
|
|
782
|
+
|
|
783
|
+
:rtype: list of string
|
|
784
|
+
"""
|
|
785
|
+
return self.permissions
|
|
786
|
+
|
|
787
|
+
def get_uses_implied_permission_list(self):
|
|
788
|
+
"""
|
|
789
|
+
Return all permissions implied by the target SDK or other permissions.
|
|
790
|
+
|
|
791
|
+
:rtype: list of string
|
|
792
|
+
"""
|
|
793
|
+
target_sdk_version = self.get_effective_target_sdk_version()
|
|
794
|
+
|
|
795
|
+
READ_CALL_LOG = "android.permission.READ_CALL_LOG"
|
|
796
|
+
READ_CONTACTS = "android.permission.READ_CONTACTS"
|
|
797
|
+
READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE"
|
|
798
|
+
READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"
|
|
799
|
+
WRITE_CALL_LOG = "android.permission.WRITE_CALL_LOG"
|
|
800
|
+
WRITE_CONTACTS = "android.permission.WRITE_CONTACTS"
|
|
801
|
+
WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE"
|
|
802
|
+
|
|
803
|
+
implied = []
|
|
804
|
+
|
|
805
|
+
implied_WRITE_EXTERNAL_STORAGE = False
|
|
806
|
+
if target_sdk_version < 4:
|
|
807
|
+
if WRITE_EXTERNAL_STORAGE not in self.permissions:
|
|
808
|
+
implied.append([WRITE_EXTERNAL_STORAGE, None])
|
|
809
|
+
implied_WRITE_EXTERNAL_STORAGE = True
|
|
810
|
+
if READ_PHONE_STATE not in self.permissions:
|
|
811
|
+
implied.append([READ_PHONE_STATE, None])
|
|
812
|
+
|
|
813
|
+
if (
|
|
814
|
+
WRITE_EXTERNAL_STORAGE in self.permissions or implied_WRITE_EXTERNAL_STORAGE
|
|
815
|
+
) and READ_EXTERNAL_STORAGE not in self.permissions:
|
|
816
|
+
maxSdkVersion = None
|
|
817
|
+
for name, version in self.uses_permissions:
|
|
818
|
+
if name == WRITE_EXTERNAL_STORAGE:
|
|
819
|
+
maxSdkVersion = version
|
|
820
|
+
break
|
|
821
|
+
implied.append([READ_EXTERNAL_STORAGE, maxSdkVersion])
|
|
822
|
+
|
|
823
|
+
if target_sdk_version < 16:
|
|
824
|
+
if (
|
|
825
|
+
READ_CONTACTS in self.permissions
|
|
826
|
+
and READ_CALL_LOG not in self.permissions
|
|
827
|
+
):
|
|
828
|
+
implied.append([READ_CALL_LOG, None])
|
|
829
|
+
if (
|
|
830
|
+
WRITE_CONTACTS in self.permissions
|
|
831
|
+
and WRITE_CALL_LOG not in self.permissions
|
|
832
|
+
):
|
|
833
|
+
implied.append([WRITE_CALL_LOG, None])
|
|
834
|
+
|
|
835
|
+
return implied
|
|
836
|
+
|
|
837
|
+
@DeprecationWarning
|
|
838
|
+
def get_requested_permissions(self):
|
|
839
|
+
"""
|
|
840
|
+
Returns all requested permissions.
|
|
841
|
+
|
|
842
|
+
:rtype: list of strings
|
|
843
|
+
"""
|
|
844
|
+
return self.get_permissions()
|
|
845
|
+
|
|
846
|
+
def get_declared_permissions(self):
|
|
847
|
+
"""
|
|
848
|
+
Returns list of the declared permissions.
|
|
849
|
+
|
|
850
|
+
:rtype: list of strings
|
|
851
|
+
"""
|
|
852
|
+
return list(self.declared_permissions.keys())
|
|
853
|
+
|
|
854
|
+
def get_declared_permissions_details(self):
|
|
855
|
+
"""
|
|
856
|
+
Returns declared permissions with the details.
|
|
857
|
+
|
|
858
|
+
:rtype: dict
|
|
859
|
+
"""
|
|
860
|
+
return self.declared_permissions
|
|
861
|
+
|
|
862
|
+
def get_max_sdk_version(self):
|
|
863
|
+
"""
|
|
864
|
+
Return the android:maxSdkVersion attribute
|
|
865
|
+
|
|
866
|
+
:rtype: string
|
|
867
|
+
"""
|
|
868
|
+
return self.get_element("uses-sdk", "maxSdkVersion")
|
|
869
|
+
|
|
870
|
+
def get_min_sdk_version(self):
|
|
871
|
+
"""
|
|
872
|
+
Return the android:minSdkVersion attribute
|
|
873
|
+
|
|
874
|
+
:rtype: string
|
|
875
|
+
"""
|
|
876
|
+
return self.get_element("uses-sdk", "minSdkVersion")
|
|
877
|
+
|
|
878
|
+
def get_target_sdk_version(self):
|
|
879
|
+
"""
|
|
880
|
+
Return the android:targetSdkVersion attribute
|
|
881
|
+
|
|
882
|
+
:rtype: string
|
|
883
|
+
"""
|
|
884
|
+
return self.get_element("uses-sdk", "targetSdkVersion")
|
|
885
|
+
|
|
886
|
+
def get_effective_target_sdk_version(self):
|
|
887
|
+
"""
|
|
888
|
+
Return the effective targetSdkVersion, always returns int > 0.
|
|
889
|
+
|
|
890
|
+
If the targetSdkVersion is not set, it defaults to 1. This is
|
|
891
|
+
set based on defaults as defined in:
|
|
892
|
+
https://developer.android.com/guide/topics/manifest/uses-sdk-element.html
|
|
893
|
+
|
|
894
|
+
:rtype: int
|
|
895
|
+
"""
|
|
896
|
+
target_sdk_version = self.get_target_sdk_version()
|
|
897
|
+
if not target_sdk_version:
|
|
898
|
+
target_sdk_version = self.get_min_sdk_version()
|
|
899
|
+
try:
|
|
900
|
+
return int(target_sdk_version)
|
|
901
|
+
except (ValueError, TypeError):
|
|
902
|
+
return 1
|
|
903
|
+
|
|
904
|
+
def get_libraries(self):
|
|
905
|
+
"""
|
|
906
|
+
Return the android:name attributes for libraries
|
|
907
|
+
|
|
908
|
+
:rtype: list
|
|
909
|
+
"""
|
|
910
|
+
return self.get_elements("uses-library", "name")
|
|
911
|
+
|
|
912
|
+
def get_certificate_der(self, filename):
|
|
913
|
+
"""
|
|
914
|
+
Return the DER coded X.509 certificate from the signature file.
|
|
915
|
+
|
|
916
|
+
:param filename: Signature filename in APK
|
|
917
|
+
:return: DER coded X.509 certificate as binary
|
|
918
|
+
"""
|
|
919
|
+
pkcs7message = self.get_file(filename)
|
|
920
|
+
|
|
921
|
+
# TODO for correct parsing, we would need to write our own ASN1Spec for the SignatureBlock format
|
|
922
|
+
message, _ = decode(pkcs7message)
|
|
923
|
+
cert = encode(message[1][3])
|
|
924
|
+
# Remove the first identifier
|
|
925
|
+
# byte 0 == identifier, skip
|
|
926
|
+
# byte 1 == length. If byte1 & 0x80 > 1, we have long format
|
|
927
|
+
# The length of to read bytes is then coded
|
|
928
|
+
# in byte1 & 0x7F
|
|
929
|
+
# Check if the first byte is 0xA0 (Sequence Tag)
|
|
930
|
+
tag = cert[0]
|
|
931
|
+
l = cert[1]
|
|
932
|
+
# Python2 compliance
|
|
933
|
+
if not isinstance(l, int):
|
|
934
|
+
l = ord(l)
|
|
935
|
+
tag = ord(tag)
|
|
936
|
+
if tag == 0xA0:
|
|
937
|
+
cert = cert[2 + (l & 0x7F) if l & 0x80 > 1 else 2:]
|
|
938
|
+
|
|
939
|
+
return cert
|
|
940
|
+
|
|
941
|
+
def get_certificate(self, filename):
|
|
942
|
+
"""
|
|
943
|
+
Return a X.509 certificate object by giving the name in the apk file
|
|
944
|
+
|
|
945
|
+
:param filename: filename of the signature file in the APK
|
|
946
|
+
:return: a `x509` certificate
|
|
947
|
+
"""
|
|
948
|
+
cert = self.get_certificate_der(filename)
|
|
949
|
+
certificate = x509.load_der_x509_certificate(cert, default_backend())
|
|
950
|
+
|
|
951
|
+
return certificate
|
|
952
|
+
|
|
953
|
+
def new_zip(self, filename, deleted_files=None, new_files={}):
|
|
954
|
+
"""
|
|
955
|
+
Create a new zip file
|
|
956
|
+
|
|
957
|
+
:param filename: the output filename of the zip
|
|
958
|
+
:param deleted_files: a regex pattern to remove specific file
|
|
959
|
+
:param new_files: a dictionnary of new files
|
|
960
|
+
|
|
961
|
+
:type filename: string
|
|
962
|
+
:type deleted_files: None or a string
|
|
963
|
+
:type new_files: a dictionnary (key:filename, value:content of the file)
|
|
964
|
+
"""
|
|
965
|
+
zout = zipfile.ZipFile(filename, "w")
|
|
966
|
+
|
|
967
|
+
for item in self.zip.infolist():
|
|
968
|
+
# Block one: deleted_files, or deleted_files and new_files
|
|
969
|
+
if deleted_files is not None:
|
|
970
|
+
if re.match(deleted_files, item.filename) is None:
|
|
971
|
+
# if the regex of deleted_files doesn't match the filename
|
|
972
|
+
if new_files is not False:
|
|
973
|
+
if item.filename in new_files:
|
|
974
|
+
# and if the filename is in new_files
|
|
975
|
+
zout.writestr(item, new_files[item.filename])
|
|
976
|
+
continue
|
|
977
|
+
# Otherwise, write the original file.
|
|
978
|
+
buffer = self.zip.read(item.filename)
|
|
979
|
+
zout.writestr(item, buffer)
|
|
980
|
+
# Block two: deleted_files is None, new_files is not empty
|
|
981
|
+
elif new_files is not False:
|
|
982
|
+
if item.filename in new_files:
|
|
983
|
+
zout.writestr(item, new_files[item.filename])
|
|
984
|
+
else:
|
|
985
|
+
buffer = self.zip.read(item.filename)
|
|
986
|
+
zout.writestr(item, buffer)
|
|
987
|
+
# Block three: deleted_files is None, new_files is empty.
|
|
988
|
+
# Just write out the default zip
|
|
989
|
+
else:
|
|
990
|
+
buffer = self.zip.read(item.filename)
|
|
991
|
+
zout.writestr(item, buffer)
|
|
992
|
+
zout.close()
|
|
993
|
+
|
|
994
|
+
def get_android_manifest_axml(self):
|
|
995
|
+
"""
|
|
996
|
+
Return the :class:`AXMLPrinter` object which corresponds to the AndroidManifest.xml file
|
|
997
|
+
|
|
998
|
+
:rtype: :class:`AXMLPrinter`
|
|
999
|
+
"""
|
|
1000
|
+
try:
|
|
1001
|
+
return self.axml["AndroidManifest.xml"]
|
|
1002
|
+
except KeyError:
|
|
1003
|
+
return None
|
|
1004
|
+
|
|
1005
|
+
def get_android_manifest_xml(self):
|
|
1006
|
+
"""
|
|
1007
|
+
Return the xml object which corresponds to the AndroidManifest.xml file
|
|
1008
|
+
|
|
1009
|
+
:rtype: object
|
|
1010
|
+
"""
|
|
1011
|
+
try:
|
|
1012
|
+
return self.xml["AndroidManifest.xml"]
|
|
1013
|
+
except KeyError:
|
|
1014
|
+
return None
|
|
1015
|
+
|
|
1016
|
+
def get_android_resources(self):
|
|
1017
|
+
"""
|
|
1018
|
+
Return the :class:`ARSCParser` object which corresponds to the resources.arsc file
|
|
1019
|
+
|
|
1020
|
+
:rtype: :class:`ARSCParser`
|
|
1021
|
+
"""
|
|
1022
|
+
try:
|
|
1023
|
+
return self.arsc["resources.arsc"]
|
|
1024
|
+
except KeyError:
|
|
1025
|
+
if "resources.arsc" not in self.zip.namelist():
|
|
1026
|
+
# There is a rare case, that no resource file is supplied.
|
|
1027
|
+
# Maybe it was added manually, thus we check here
|
|
1028
|
+
return None
|
|
1029
|
+
self.arsc["resources.arsc"] = ARSCParser(self.zip.read("resources.arsc"))
|
|
1030
|
+
return self.arsc["resources.arsc"]
|
|
1031
|
+
|
|
1032
|
+
def is_signed(self):
|
|
1033
|
+
"""
|
|
1034
|
+
Returns true if either a v1 or v2 (or both) signature was found.
|
|
1035
|
+
"""
|
|
1036
|
+
return self.is_signed_v1() or self.is_signed_v2()
|
|
1037
|
+
|
|
1038
|
+
def is_signed_v1(self):
|
|
1039
|
+
"""
|
|
1040
|
+
Returns true if a v1 / JAR signature was found.
|
|
1041
|
+
Returning `True` does not mean that the file is properly signed!
|
|
1042
|
+
It just says that there is a signature file which needs to be validated.
|
|
1043
|
+
"""
|
|
1044
|
+
return self.get_signature_name() is not None
|
|
1045
|
+
|
|
1046
|
+
def is_signed_v2(self):
|
|
1047
|
+
"""
|
|
1048
|
+
Returns true of a v2 / APK signature was found.
|
|
1049
|
+
Returning `True` does not mean that the file is properly signed!
|
|
1050
|
+
It just says that there is a signature file which needs to be validated.
|
|
1051
|
+
"""
|
|
1052
|
+
if not self._is_signed_v2:
|
|
1053
|
+
# Need to find an v2 Block in the APK.
|
|
1054
|
+
# The Google Docs gives you the following rule:
|
|
1055
|
+
# * go to the end of the ZIP File
|
|
1056
|
+
# * search for the End of Central directory
|
|
1057
|
+
# * then jump to the beginning of the central directory
|
|
1058
|
+
# * Read now the magic of the signing block
|
|
1059
|
+
# * before the magic there is the size_of_block, so we can jump to
|
|
1060
|
+
# the beginning.
|
|
1061
|
+
# * There should be again the size_of_block
|
|
1062
|
+
# * Now we can read the Key-Values
|
|
1063
|
+
# * IDs with an unknown value should be ignored.
|
|
1064
|
+
f = io.BytesIO(self.__raw)
|
|
1065
|
+
|
|
1066
|
+
size_central = None
|
|
1067
|
+
offset_central = None
|
|
1068
|
+
|
|
1069
|
+
# Go to the end
|
|
1070
|
+
f.seek(-1, io.SEEK_END)
|
|
1071
|
+
# we know the minimal length for the central dir is 16+4+2
|
|
1072
|
+
f.seek(-20, io.SEEK_CUR)
|
|
1073
|
+
while f.tell() > 0:
|
|
1074
|
+
f.seek(-1, io.SEEK_CUR)
|
|
1075
|
+
r, = unpack("<4s", f.read(4))
|
|
1076
|
+
if r == self.PK_END_OF_CENTRAL_DIR:
|
|
1077
|
+
# Read central dir
|
|
1078
|
+
this_disk, disk_central, this_entries, total_entries, size_central, offset_central = unpack(
|
|
1079
|
+
"<HHHHII", f.read(16)
|
|
1080
|
+
)
|
|
1081
|
+
# TODO according to the standard we need to check if the
|
|
1082
|
+
# end of central directory is the last item in the zip file
|
|
1083
|
+
# TODO We also need to check if the central dir is exactly
|
|
1084
|
+
# before the end of central dir...
|
|
1085
|
+
|
|
1086
|
+
# These things should not happen for APKs
|
|
1087
|
+
assert this_disk == 0, "Not sure what to do with multi disk ZIP!"
|
|
1088
|
+
assert disk_central == 0, "Not sure what to do with multi disk ZIP!"
|
|
1089
|
+
break
|
|
1090
|
+
f.seek(-4, io.SEEK_CUR)
|
|
1091
|
+
if offset_central:
|
|
1092
|
+
f.seek(offset_central)
|
|
1093
|
+
r, = unpack("<4s", f.read(4))
|
|
1094
|
+
f.seek(-4, io.SEEK_CUR)
|
|
1095
|
+
assert r == self.PK_CENTRAL_DIR, "No Central Dir at specified offset"
|
|
1096
|
+
|
|
1097
|
+
# Go back and check if we have a magic
|
|
1098
|
+
end_offset = f.tell()
|
|
1099
|
+
f.seek(-24, io.SEEK_CUR)
|
|
1100
|
+
size_of_block, magic = unpack("<Q16s", f.read(24))
|
|
1101
|
+
self._is_signed_v2 = False
|
|
1102
|
+
if magic == self.APK_SIG_MAGIC:
|
|
1103
|
+
# go back size_of_blocks + 8 and read size_of_block again
|
|
1104
|
+
f.seek(-(size_of_block + 8), io.SEEK_CUR)
|
|
1105
|
+
size_of_block_start, = unpack("<Q", f.read(8))
|
|
1106
|
+
assert (
|
|
1107
|
+
size_of_block_start == size_of_block
|
|
1108
|
+
), "Sizes at beginning and and does not match!"
|
|
1109
|
+
|
|
1110
|
+
# Store all blocks
|
|
1111
|
+
while f.tell() < end_offset - 24:
|
|
1112
|
+
size, key = unpack("<QI", f.read(12))
|
|
1113
|
+
value = f.read(size - 4)
|
|
1114
|
+
self._v2_blocks[key] = value
|
|
1115
|
+
|
|
1116
|
+
# Test if a signature is found
|
|
1117
|
+
if self.APK_SIG_KEY_SIGNATURE in self._v2_blocks:
|
|
1118
|
+
self._is_signed_v2 = True
|
|
1119
|
+
|
|
1120
|
+
return self._is_signed_v2
|
|
1121
|
+
|
|
1122
|
+
def get_certificates_der_v2(self):
|
|
1123
|
+
"""
|
|
1124
|
+
Return a list of DER coded X.509 certificates from the v2 signature
|
|
1125
|
+
"""
|
|
1126
|
+
# calling is_signed_v2 should also load the signature, if any
|
|
1127
|
+
if not self.is_signed_v2():
|
|
1128
|
+
return []
|
|
1129
|
+
|
|
1130
|
+
certificates = []
|
|
1131
|
+
block_bytes = self._v2_blocks[self.APK_SIG_KEY_SIGNATURE]
|
|
1132
|
+
block = io.BytesIO(block_bytes)
|
|
1133
|
+
|
|
1134
|
+
size_sequence, = unpack("<I", block.read(4))
|
|
1135
|
+
assert size_sequence + 4 == len(
|
|
1136
|
+
block_bytes
|
|
1137
|
+
), "size of sequence and blocksize does not match"
|
|
1138
|
+
while block.tell() < len(block_bytes):
|
|
1139
|
+
size_signer, = unpack("<I", block.read(4))
|
|
1140
|
+
|
|
1141
|
+
len_signed_data, = unpack("<I", block.read(4))
|
|
1142
|
+
len_digests, = unpack("<I", block.read(4))
|
|
1143
|
+
# Skip it for now
|
|
1144
|
+
block.seek(len_digests, io.SEEK_CUR)
|
|
1145
|
+
|
|
1146
|
+
len_certs, = unpack("<I", block.read(4))
|
|
1147
|
+
start_certs = block.tell()
|
|
1148
|
+
while block.tell() < start_certs + len_certs:
|
|
1149
|
+
len_cert, = unpack("<I", block.read(4))
|
|
1150
|
+
certificates.append(block.read(len_cert))
|
|
1151
|
+
|
|
1152
|
+
# Now we have the signatures and the public key...
|
|
1153
|
+
# we need to read it (or at least skip it)
|
|
1154
|
+
len_attr, = unpack("<I", block.read(4))
|
|
1155
|
+
block.seek(len_attr, io.SEEK_CUR)
|
|
1156
|
+
len_sigs, = unpack("<I", block.read(4))
|
|
1157
|
+
block.seek(len_sigs, io.SEEK_CUR)
|
|
1158
|
+
len_publickey, = unpack("<I", block.read(4))
|
|
1159
|
+
block.seek(len_publickey, io.SEEK_CUR)
|
|
1160
|
+
|
|
1161
|
+
return certificates
|
|
1162
|
+
|
|
1163
|
+
def get_certificates_v2(self):
|
|
1164
|
+
"""
|
|
1165
|
+
Return a list of :class:`cryptography.x509.Certificate` which are found
|
|
1166
|
+
in the v2 signing block.
|
|
1167
|
+
Note that we simply extract all certificates regardless of the signer.
|
|
1168
|
+
Therefore this is just a list of all certificates found in all signers.
|
|
1169
|
+
"""
|
|
1170
|
+
certs = []
|
|
1171
|
+
for cert in self.get_certificates_der_v2():
|
|
1172
|
+
certs.append(x509.load_der_x509_certificate(cert, default_backend()))
|
|
1173
|
+
|
|
1174
|
+
return certs
|
|
1175
|
+
|
|
1176
|
+
def get_signature_name(self):
|
|
1177
|
+
"""
|
|
1178
|
+
Return the name of the first signature file found.
|
|
1179
|
+
"""
|
|
1180
|
+
if self.get_signature_names():
|
|
1181
|
+
return self.get_signature_names()[0]
|
|
1182
|
+
else:
|
|
1183
|
+
# Unsigned APK
|
|
1184
|
+
return None
|
|
1185
|
+
|
|
1186
|
+
def get_signature_names(self):
|
|
1187
|
+
"""
|
|
1188
|
+
Return a list of the signature file names (v1 Signature / JAR
|
|
1189
|
+
Signature)
|
|
1190
|
+
|
|
1191
|
+
:rtype: List of filenames matching a Signature
|
|
1192
|
+
"""
|
|
1193
|
+
signature_expr = re.compile(r"^(META-INF/)(.*)(\.RSA|\.EC|\.DSA)$")
|
|
1194
|
+
signatures = []
|
|
1195
|
+
|
|
1196
|
+
for i in self.get_files():
|
|
1197
|
+
if signature_expr.search(i):
|
|
1198
|
+
signatures.append(i)
|
|
1199
|
+
|
|
1200
|
+
return signatures
|
|
1201
|
+
|
|
1202
|
+
def get_signature(self):
|
|
1203
|
+
"""
|
|
1204
|
+
Return the data of the first signature file found (v1 Signature / JAR
|
|
1205
|
+
Signature)
|
|
1206
|
+
|
|
1207
|
+
:rtype: First signature name or None if not signed
|
|
1208
|
+
"""
|
|
1209
|
+
if self.get_signatures():
|
|
1210
|
+
return self.get_signatures()[0]
|
|
1211
|
+
else:
|
|
1212
|
+
return None
|
|
1213
|
+
|
|
1214
|
+
def get_signatures(self):
|
|
1215
|
+
"""
|
|
1216
|
+
Return a list of the data of the signature files.
|
|
1217
|
+
Only v1 / JAR Signing.
|
|
1218
|
+
|
|
1219
|
+
:rtype: list of bytes
|
|
1220
|
+
"""
|
|
1221
|
+
signature_expr = re.compile(r"^(META-INF/)(.*)(\.RSA|\.EC|\.DSA)$")
|
|
1222
|
+
signature_datas = []
|
|
1223
|
+
|
|
1224
|
+
for i in self.get_files():
|
|
1225
|
+
if signature_expr.search(i):
|
|
1226
|
+
signature_datas.append(self.get_file(i))
|
|
1227
|
+
|
|
1228
|
+
return signature_datas
|