-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpermissionsTracer.py
executable file
·1293 lines (1041 loc) · 48.8 KB
/
permissionsTracer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
from enum import Enum
import os
import re
import sys
import datetime
import ntpath
import json
import configparser
import tempfile
import hashlib
import pickle
config = configparser.ConfigParser()
config.read('config.ini')
if len(config.sections()) == 0:
try:
import androguard
except:
print("{ERROR} YOU MUST HAVE ANDROGUARD INSTALLED")
sys.exit(1)
from androguard.core.analysis.analysis import Analysis, ClassAnalysis, MethodAnalysis
from androguard.misc import AnalyzeAPK
from androguard.misc import AnalyzeDex
# HARDCODED PATH TO DEXTRIPADOR
sys.path = ["dextripador"] + sys.path
from Dextripador import Extractor
else:
androguard_path = ""
dextripador_path = ""
try:
androguard_path = str(config['PATHS']['ANDROGUARD_PATH'])
dextripador_path = str(config['PATHS']['DEXTRIPADOR_PATH'])
except KeyError as ke:
print("{ERROR} %s" % str(ke))
sys.exit(1)
sys.path = [androguard_path] + sys.path
sys.path = [dextripador_path] + sys.path
from androguard.core.analysis.analysis import Analysis, ClassAnalysis, MethodAnalysis
from androguard.misc import AnalyzeAPK
from androguard.misc import AnalyzeDex
from androguard.core.analysis.analysis import ExternalClass
from androguard.core.analysis.analysis import ExternalMethod
from androguard.core.bytecodes.apk import APK
from androguard.core.bytecodes.dvm_types import Kind
from Dextripador import Extractor
DEBUG_FLAG = False
WARNING_FLAG = False
ERROR_FLAG = False
ANALYST_FLAG = False
FILE_OUTPUT = False
FILE_NAME = ""
class Debug:
def __init__(self):
''' Constructor of Debug class '''
@staticmethod
def log(msg):
''' print debug messages '''
if DEBUG_FLAG:
print("{DEBUG} %s - %s" % (datetime.datetime.now(), msg))
@staticmethod
def analyst(msg):
''' print analyst messages '''
if ANALYST_FLAG:
print("{ANALYST} %s - %s" % (datetime.datetime.now(), msg))
@staticmethod
def warning(msg, error):
''' print warning messages '''
if WARNING_FLAG:
print("{WARNING} %s - %s: %s" %
(datetime.datetime.now(), msg, str(error)))
@staticmethod
def error(msg, error, exception):
''' print error messages '''
if ERROR_FLAG:
print("{ERROR} %s - %s: %s" %
(datetime.datetime.now(), msg, str(error)))
raise exception(msg)
class IntentFilterAnalyzer:
# Supported component types for analysis
class ComponentTypes(Enum):
"""
Components that we support (or going to support)
this will be written as an Enum value starting
from 1, and ending in 99 that means is something
we do not support.
"""
ACTIVITY = 1
SERVICE = 2
PROVIDER = 3
RECEIVER = 4
NOT_SUPPORTED = 99
def __str__(self) -> str:
if self.value == 1:
return "Activity"
elif self.value == 2:
return "Service"
elif self.value == 3:
return "Content provider"
elif self.value == 4:
return "Broadcast receiver"
else:
return "Not Supported"
def __init__(self, apk_analysis: Analysis, apk: APK):
"""
Constructor method for IntentFilterAnalyzer, initialization
of variables is done here.
:param apk_analysis: object Analysis returned by Androguard.
:param apk: object APK returned by Androguard.
:return: None
"""
self.apk_analysis = apk_analysis
self.apk = apk
self.exposed_methods = {}
self.data_provided = {}
self.component_type = {}
'''
Generic methods, use them in your analysis implementation
also feel free to include whatever generic method you want,
following always the programming convention followed, parameters
include the type of the parameter, and the comment of each
method should describe what the method does, the parameters and
the return type.
Names for private methods will be written starting with '_'.
'''
def _extract_component_type(self, class_name: str):
"""
Method to extract the type of the class name given,
depending on the analysis that we support, we will
return a given value.
:param class_name: str with the class name to categorize.
:return: ComponentTypes
"""
# clean the name and rewrite it properly to search
# it in the manifest file
if class_name[0] == 'L':
class_name = class_name[1:]
if class_name[-1] == ';':
class_name = class_name[:-1]
class_name = class_name.replace('/', '.')
if class_name in self.apk.get_activities():
return self.ComponentTypes.ACTIVITY
if class_name in self.apk.get_services():
return self.ComponentTypes.SERVICE
if class_name in self.apk.get_providers():
return self.ComponentTypes.PROVIDER
if class_name in self.apk.get_receivers():
return self.ComponentTypes.RECEIVER
return self.ComponentTypes.NOT_SUPPORTED
def _get_class_from_analysis(self, class_name: str):
"""
Return a ClassAnalysis object from a given class name inside
of the analysis object.
:param class_name: str with class name to obtain object.
:return: ClassAnalysis
"""
for class_ in self.apk_analysis.get_classes():
if str(class_.name) == class_name:
Debug.log("Found class '%s'" % (class_name))
return class_
return None
def _get_method_from_class_object(self, class_object: ClassAnalysis, method_name: str):
"""
Return a Method object from a given method name from
a specific class.
:param class_object: ClassAnalysis object where to search the method.
:param method_name: method to look for.
:return: MethodAnalysis
"""
for method in class_object.get_methods():
if str(method.name) == method_name:
Debug.log("Found method '%s'" % (method_name))
return method
return None
def _get_types_as_list(self, types_str: str):
"""
Get the smali types of a string as a list of
types, we will parse it as a list of types.
:parameter types_str: string with a list of types (it can be just one).
:return: list
"""
types_list = []
types_str = types_str.replace('(', '').replace(')', '')
len_types_str = len(types_str)
is_object = False
is_array = False
start_index = 0
for i in range(len_types_str):
if is_object:
if types_str[i] == ';':
is_object = False
if not is_array:
types_list.append(str(types_str[start_index:i+1]))
else:
types_list.append(
str(types_str[start_index:i+1]) + "[]")
is_array = False
else:
continue
if types_str[i] == 'Z':
if not is_array:
types_list.append('boolean')
else:
types_list.append('boolean[]')
is_array = False
elif types_str[i] == 'B':
if not is_array:
types_list.append('byte')
else:
types_list.append('byte[]')
is_array = False
elif types_str[i] == 'C':
if not is_array:
types_list.append('char')
else:
types_list.append('char[]')
is_array = False
elif types_str[i] == 'D':
if not is_array:
types_list.append('double')
else:
types_list.append('double[]')
is_array = False
elif types_str[i] == 'F':
if not is_array:
types_list.append('float')
else:
types_list.append('float[]')
is_array = False
elif types_str[i] == 'I':
if not is_array:
types_list.append('integer')
else:
types_list.append('integer[]')
is_array = False
elif types_str[i] == 'J':
if not is_array:
types_list.append('long')
else:
types_list.append('long[]')
is_array = False
elif types_str[i] == 'S':
if not is_array:
types_list.append('short')
else:
types_list.append('short[]')
is_array = False
elif types_str[i] == 'V':
if not is_array:
types_list.append('void')
else:
types_list.append('void[]')
is_array = False
elif types_str[i] == 'L':
is_object = True
start_index = i
elif types_str[i] == '[':
is_array = True
return types_list
def _get_references_to_method(self, method_object: MethodAnalysis, class_name: str):
"""
Method to get all the cross references from a
method object, the class name must appear in
the cross reference in order to be included.
:param method_object: method of which we will extract the cross references.
:param class_name: only the cross references done from this class will be retrieved.
:return: list of cross references.
"""
xrefs = []
all_xrefs = method_object.get_xref_from()
for _, call, _ in all_xrefs:
if class_name in str(call.class_name):
Debug.log("Found xrefs to %s from %s->%s" %
(str(method_object.name), str(call.class_name), str(call.name)))
xrefs.append(call)
return xrefs
def _get_intent_actions(self, class_name: str, component_type: str):
'''
Method to retrieve from the AndroidManifest all the action
names that the class will respond to. We will focus on those
actions from AOSP. These together with the return types
could be useful to see what data is retrieved from a component.
:param class_name: class to search in the manifest to retrieve the intent actions.
:param component_type: string with type of the component.
:return: list[str]
'''
element = self.apk.get_android_manifest_xml()
application = element.find('application')
actions = set()
component_to_analyze = None
class_name = class_name.replace('/', '.')[1:-1]
for component in application.findall(component_type):
if component.attrib['{http://schemas.android.com/apk/res/android}name'] == class_name:
component_to_analyze = component
break
if component_to_analyze is None:
return []
for intent_filter in component_to_analyze.findall('intent-filter'):
for action in intent_filter.findall('action'):
actions.add(
action.attrib['{http://schemas.android.com/apk/res/android}name'])
return list(actions)
'''
Methods to analyze methods exported by a service
in order to get what a Service "could" leak through
an intent.
'''
def _get_method_returned_classes(self, method_object: MethodAnalysis):
'''
Methods such as onBind are used by Services to return a Bind object
that other Applications or Activities can use in order to call Service
methods and retrieve information. Another example is the method
onReceive in broadcasts receivers.
This method will return all the returned classes from these methods.
:param method_object: MethodAnalysis of the onBind method.
:return: list[ClassAnalysis]
'''
method_instructions = []
classes_returned_by_method = []
for block in method_object.get_basic_blocks():
for inst in block.get_instructions():
method_instructions.append(inst)
for i in range(len(method_instructions)):
# look for all the return instructions in order
# to retrieve all the returned types!
inst = method_instructions[i]
if inst.get_op_value() == 0x11:
Debug.log("Found instruction return-object: '%s'" %
(inst.disasm()))
# retrieve the operand
if len(inst.get_operands()) > 1:
# operands for return object should be just one
continue
if inst.get_operands()[0][0].name != 'REGISTER':
# we look for registers
continue
reg_number = inst.get_operands()[0][1]
Debug.log("Register number used: %d" % (reg_number))
class_object = None
# start backward analysis
for j in range(i, -1, -1):
# check for iget-object
if method_instructions[j].get_op_value() != 0x54:
continue
# check that where object is stored is a register
if method_instructions[j].get_operands()[0][0].name != 'REGISTER':
continue
# check is the one we want to find
if method_instructions[j].get_operands()[0][1] != reg_number:
continue
class_return_str = method_instructions[j].get_operands()[
2][2]
# "where_is_stored->var_name var_type"
# retrieve only the var_type
class_return_str = class_return_str.split(' ')[1]
class_object = self._get_class_from_analysis(
class_return_str)
if class_object is None or class_object.is_android_api() or class_object.is_external():
class_object = None
else:
Debug.log("Found class instance returned by method '%s'" % (
str(class_object.name)))
break
# Once finished, check if we can add it
if class_object is not None:
classes_returned_by_method.append(class_object)
return classes_returned_by_method
def _get_types_from_set_result(self, method_object: MethodAnalysis, class_name: str):
'''
The method setResult from the Intent is used to return data from an
Activity or BroadcastReceiver called (also through an intent) through
the intent extras, this method does a backward analysis of the calls
to setResult to extract all the extras. The output of the method will
return a list of dictionaries being each key the name of the extra
and the value is the type the extra returns.
:param method_object: MethodAnalysis of the setResult method.
:param class_name: class where to find for references to the method.
:return: list[dict]
'''
method_xrefs = self._get_references_to_method(
method_object=method_object, class_name=class_name)
return_values = []
if len(method_xrefs) == 0:
return []
for method in method_xrefs:
# analyze the method where 'setResult' is called.
method_instructions = []
for block in method.get_basic_blocks():
for inst in block.get_instructions():
method_instructions.append(inst)
for i in range(len(method_instructions)):
# look for an invoke-virtual instruction
# of the setResult method.
inst = method_instructions[i]
# get those invoke-virtual
# with operands
# where the called function is setResult
# and finally it contains and intent (return results)
if inst.get_op_value() == 0x6e and \
len(inst.get_operands()) > 0 and \
'setResult' in str(inst.get_operands()[-1][-1]) and \
'(I Landroid/content/Intent;)' in str(inst.get_operands()[-1][-1]):
Debug.log(
"Found instruction invoke-virtual instruction: '%s'" % (inst.disasm()))
intent_register = inst.get_operands()[-2][1]
Debug.log("Register with intent: '%d'" % (intent_register))
# start backward analysis
for j in range(i, -1, -1):
if method_instructions[j].get_op_value() == 0x6e and \
len(method_instructions[j].get_operands()) > 0 and \
'putExtra' in str(method_instructions[j].get_operands()[-1][-1]):
# if the intent is not the one we want...
# leave
if method_instructions[j].get_operands()[0][1] != intent_register:
break
put_extra_type = self._get_types_as_list(
method_instructions[j].get_operands()[-1][-1].split('(')[1].split(')')[0])[-1]
key = ""
# search for the key in previous instructions
# for the moment support const-string need to
# add support for other ways to get strings
if method_instructions[j-1].get_op_value() == 0x1a:
key = str(
method_instructions[j-1].get_operands()[-1][-1])
elif method_instructions[j-2].get_op_value() == 0x1a:
key = str(
method_instructions[j-2].get_operands()[-1][-1])
elif method_instructions[j-3].get_op_value() == 0x1a:
key = str(
method_instructions[j-3].get_operands()[-1][-1])
Debug.log(
"Found 'putExtra' call with key-type: '%s'-'%s'" % (key, put_extra_type))
return_values.append({key: put_extra_type})
return return_values
def _analyze_service(self, class_name: str):
"""
Method to analyze a Service, we will analyze Remote
Bound Services in order to discover onBind method,
retrieve the returned object and finally retrieve all
the method prototypes.
:param class_name: str with class name to extract information.
:return: List
"""
class_object = self._get_class_from_analysis(class_name)
method_prototypes = []
if class_object is None:
return []
# Search for onBind method
method_object = self._get_method_from_class_object(
class_object=class_object, method_name="onBind")
if method_object is None:
return []
returned_classes = self._get_method_returned_classes(
method_object=method_object)
# we have in returned classes a list of classes that
# are returned by onBind method
for class_ in returned_classes:
for method in class_.get_methods():
if method.is_external():
# as the returned classes are interfaces
# return the external methods
descriptor = method.descriptor
# get the return type
ret_type = self._get_types_as_list(
descriptor.split(')')[1])[0]
parameters = self._get_types_as_list(
descriptor.split(')')[0])
method_prototypes.append(
{str(method.name): {
"return-type": ret_type,
"parameters": parameters
}}
)
return method_prototypes
def _analyze_content_provider(self, class_name: str):
"""
Method to analyze a Content Provider.
It analyzes the `getType()` method which allows other apps to access
whatever data is provided, and returns the type of data available.
:param class_name: str with class name to extract information.
:return: List
"""
class_object = self._get_class_from_analysis(class_name)
returned_data_types = []
if class_object is None:
return []
# Search for getType method
get_type_method_object = self._get_method_from_class_object(
class_object=class_object, method_name="getType")
if get_type_method_object is None:
return []
method_instructions = []
for block in get_type_method_object.get_basic_blocks():
for inst in block.get_instructions():
method_instructions.append(inst)
registers = dict()
for i in range(len(method_instructions)):
inst = method_instructions[i]
if inst.get_op_value() == 0x1a:
Debug.log("Found instruction const-string: '%s'" %
(inst.disasm()))
register_index = inst.get_operands()[0][1]
register_string = inst.get_operands()[1][2]
registers[register_index] = register_string
if inst.get_op_value() == 0x11:
Debug.log("Found instruction return-object: '%s'" %
(inst.disasm()))
register_index = inst.get_operands()[0][1]
try:
Debug.log("Returned string at register v%d" %
register_index)
returned_data_types.append(registers[register_index])
except KeyError:
Debug.log("Error: no string at register v%d" %
register_index)
return returned_data_types
def _analyze_broadcast_receiver(self, class_name: str):
"""
Method to analyze a broadcast receiver.
In the same way than with Activities,
:param class_name: str with class name to extract information.
:return: List
"""
class_object = self._get_class_from_analysis(class_name)
return_vaues = []
if class_object is None:
return []
# Search for onReceive method
method_object = self._get_method_from_class_object(
class_object=class_object, method_name="setResult")
if method_object is None:
return []
return_vaues = self._get_types_from_set_result(
method_object=method_object, class_name=class_name)
return return_vaues
def _analyze_activity(self, class_name: str):
"""
Method to analyze Activities exported by
an application, in the case of the Activities
this must be called with 'startActivityForResult'
and then in the called activity the method
'setResult' must be called with an intent as second
parameter, containing this intent data extra with
the variables.
:param class_name: str with class name to extract information.
:return: List
"""
class_object = self._get_class_from_analysis(class_name)
return_values = []
if class_object is None:
return []
method_object = self._get_method_from_class_object(
class_object=class_object, method_name="setResult")
if method_object is None:
return []
return_values = self._get_types_from_set_result(
method_object=method_object, class_name=class_name)
return return_values
def analyze_class(self, class_name: str):
"""
Main method of the class, in this method we will analyze
the class, and we will extract all the method prototypes.
:param class_name: str with the class name to categorize.
:return: List
"""
Debug.log("Starting the analysis of the class '%s'" % (class_name))
component_type = self._extract_component_type(class_name)
Debug.log("Component type to analyze is '%s'" % (str(component_type)))
if component_type == self.ComponentTypes.ACTIVITY:
self.exposed_methods = {'interface': {},
'intent-filter_actions': self._get_intent_actions(class_name, 'activity')}
self.data_provided = {
'data_provided': self._analyze_activity(class_name)}
elif component_type == self.ComponentTypes.SERVICE:
self.exposed_methods = {
'interface': self._analyze_service(class_name),
'intent-filter_actions': self._get_intent_actions(class_name, 'service')}
self.data_provided = {'data_provided': {}}
elif component_type == self.ComponentTypes.PROVIDER:
self.exposed_methods = {'interface': {},
'intent-filter_actions': self._get_intent_actions(class_name, 'provider')}
cp_data_type = self._analyze_content_provider(class_name)
length = len(cp_data_type)
self.data_provided = {'data_provided': {
'cp_data_{}'.format(i): cp_data_type[i] for i in range(0, length)}}
elif component_type == self.ComponentTypes.RECEIVER:
self.exposed_methods = {
'interface': {},
'intent-filter_actions': self._get_intent_actions(class_name, 'receiver')}
self.data_provided = {
'data_provided': self._analyze_broadcast_receiver(class_name)}
else:
self.exposed_methods = {'interface': {}}
self.data_provided = {'data_provided': {}}
return self.exposed_methods, self.data_provided, {'component_type': str(component_type)}
class PermissionTracer:
def __init__(self, APK, md5):
"""
Constructor method for PermissionTracer class, it will store and
initialize variables, also it will call some starting methods.
:param APK: path to the apk to analyze
:param class_name_to_analyze: class where is the method to analyze
:return: None
"""
self.MAX_DEPTH = 7 # constant value
self.apk = None # value that must be set only once by __androguard_open_apk
self.analysis = None # value that must be set only once by __androguard_open_apk
self.analysis_ok = False # value that must be set only once by __androguard_open_apk
self._md5 = md5
self.intent_filter_analyzer = None
self.__androguard_open_apk(APK)
if self.analysis_ok:
self.intent_filter_analyzer = IntentFilterAnalyzer(
apk_analysis=self.analysis, apk=self.apk)
def __read_md5_odex_file(self):
return pickle.load(open(config['PATHS']['MD5-ODEX-PICKLE'], 'rb'))
def __calculate_md5(self, fname):
hash_md5 = hashlib.md5()
with open(fname, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
def __set_variables(self):
"""
Function to set variables to empty values once
per execution of new class
:return:
"""
self.external_classes = list()
self.analyzed_methods = []
self.permission_analysis = {}
self.method_object_start = []
self.class_name = ""
def __androguard_analyze_apk(self, path_to_file):
"""
Wrapper of androguard AnalyzeAPK, it will also use the library
extractor from dextripador if there's no class, we will try to
get the odex from same path, if it doesn't exist... No dex analysis
:param path_to_file: file to analyze
:return: An APK object, array of DalvikVMFormat, and Analysis object
"""
Debug.log("Analyzing file \"%s\"" % (path_to_file))
apk, classes_dex, analysis = AnalyzeAPK(path_to_file)
if len(classes_dex) == 0:
Debug.warning("Warning, class.dex file not present, looking for odex to extract it",
FileNotFoundError("File classes.dex not found in %s" % path_to_file))
path_to_odex = path_to_file.replace('.apk', '.odex')
odex_file_name = path_to_odex.split('/')[-1]
extractor = None
if not os.path.isfile(path_to_odex):
path_to_odex = '/'.join(path_to_file.split('/')
[:-1]) + '/arm/' + odex_file_name
if not os.path.isfile(path_to_odex):
path_to_odex = '/'.join(path_to_file.split('/')
[:-1]) + '/arm64/' + odex_file_name
if not os.path.isfile(path_to_odex):
path_to_odex = '/'.join(path_to_file.split('/')
[:-1]) + '/oat/arm/' + odex_file_name
if not os.path.isfile(path_to_odex):
path_to_odex = '/'.join(path_to_file.split('/')
[:-1]) + '/oat/arm64/' + odex_file_name
if not os.path.isfile(path_to_odex):
Debug.warning("odex file not found in the apk directory, computing md5 and looking in the list",
FileNotFoundError("odex file not found in directory"))
# Lookup the list of MD5-ODEX
# If no md5 is provided by command line, calculate it
if self._md5 == '':
self._md5 = self.__calculate_md5(path_to_file)
self.odex_md5_dict = self.__read_md5_odex_file()
if (self._md5 in self.odex_md5_dict):
path_to_odex = self.odex_md5_dict[self._md5]
Debug.log("Found odex for md5=%s in the list. Path:%s" %
(self._md5, path_to_odex))
else:
Debug.warning("odex file for hash %s not present in the list. Looking in the apk directory" %
self._md5, FileNotFoundError("odex file for %s not found in list" % self._md5))
raise FileNotFoundError(
"odex file not found neither in directory nor by md5")
extractor = Extractor(path_to_odex)
extractor.load()
fo = tempfile.NamedTemporaryFile()
fo.close() # we're just interested in the name
if len(extractor.get_dex_files()) > 0:
Debug.log("Extracting dex from odex to %s" %
(fo.name + '.dex'))
if extractor.extract_dex(0, fo.name + ".dex", True):
# now try to analyze with androguard
_, classes_dex, analysis = AnalyzeDex(fo.name + '.dex')
return apk, classes_dex, analysis
def __androguard_open_apk(self, APK):
"""
Method used to analyze an apk at the beginning of the execution
it will also call load_specific_api.
This method should be called only once per APK, as AnalyzeAPK
from Androguard can take long time for the execution.
:param APK: path of the apk to analyze
:param class_name_to_analyze: class where to obtain the methods
:return: None
"""
Debug.log("analyzing %s file with androguard" % (APK))
try:
self.apk, _, self.analysis = self.__androguard_analyze_apk(APK)
except Exception as e:
Debug.warning("Error analyzing apk with androguard", e)
self.permission_analysis = {
"ERROR_TAG": "EXCEPTION",
"FILE": APK,
"ERROR_MESSAGE": str(e),
"EXCEPTION_TYPE": str(type(e))
}
self.analysis_ok = False
return
api_level = self.apk.get_effective_target_sdk_version()
Debug.log("Loading api-permission map for api level %d" % (api_level))
# Change this after extracting new api levels
if api_level > 29:
Debug.log(
"Api level greater than maximum supported (29), using 29 by default")
api_level = 29
elif api_level < 16:
Debug.log(
"Api level lower than minimum supported (16), using 16 by default")
api_level = 16
self.analysis.load_specific_api(api_level)
self.analysis_ok = True
return
def __is_external_class(self, class_name):
"""
Method to test if a class_name is one of the external
classes or not (considered internal).
:param class_name: name of the method to check if is external
:return: boolean based on given class
"""
for external_class in self.external_classes:
if class_name == external_class.name:
Debug.log("%s class is external" % (class_name))
return True
Debug.log("%s class is not external" % (class_name))
return False
def __get_method_objects(self, class_=""):
"""
Given a class, get from the analysis object all the methods,
this method changed in order to analyze all the methods from that
class
:param class_: name of the class where to search the method
:return: List of MethodAnalysis object
"""
# in other case, search manually the first method by class and name
classes = list(self.analysis.get_classes())
methods = []
Debug.log("Checking for the presence of class %s" % (class_))
for c in classes:
if class_ == str(c.name):
Debug.log("Class %s found in apk classes" % class_)
methods = list(c.get_methods())
break
return methods
def __get_method_object(self, class_="", method_="", descriptor=""):
"""
Given a class and a method, get from analysis object the method
object given as name. It is necessary to search by classes and
method names.
:param class_: name of the class where to search the method
:param method_: name of the method to search
:param descriptor: descriptor of the method, useful when we have
more than one method with the same name.
:return: MethodAnalysis object
"""
method_analysis = None
# if descriptor is given, use it to search through androguard methods
if descriptor is not None and descriptor != "":
method_analysis = self.analysis.get_method_analysis_by_name(
class_, method_, descriptor)
if method_analysis is not None:
return method_analysis
# in other case, search manually the first method by class and name
classes = list(self.analysis.get_classes())
methods = []
Debug.log("Checking for the presence of class %s and method %s" %
(class_, method_))
for c in classes:
if class_ == str(c.name):
Debug.log("Class %s found in apk classes" % class_)
methods = list(c.get_methods())
break
for method in methods:
if method_ == str(method.name):
if descriptor is None or len(descriptor) > 0:
Debug.log("Method %s found in the class" % method_)
return method
elif descriptor == str(method.descriptor):
Debug.log("Method %s found in the class" % method_)
return method
Debug.log("Method not found returning NULL")
return None
def __extract_class_and_method_names(self, complete_name):
"""
Extract the class name, and the method name from a complete
name given as the parameter of an "invoke" instruction.
...example:
Ljava/lang/Object;-><init>()V
We should return from here two strings:
Ljava/lang/Object <--- class name
<init> <--- method name
:param complete_name: name where to extract information from
:return: class and method names
"""
class_regex = re.compile("(L.+)(?=->)")
method_regex = re.compile("(?=(->)).+(?=\()")
class_name = ""
method_name = ""