dex-oracle 1.0.2

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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +56 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +102 -0
  6. data/bin/dex-oracle +98 -0
  7. data/dex-oracle.gemspec +43 -0
  8. data/driver/build.gradle +52 -0
  9. data/driver/gradle/wrapper/gradle-wrapper.jar +0 -0
  10. data/driver/gradle/wrapper/gradle-wrapper.properties +6 -0
  11. data/driver/gradlew +160 -0
  12. data/driver/gradlew.bat +90 -0
  13. data/driver/src/main/java/org/cf/oracle/Driver.java +134 -0
  14. data/driver/src/main/java/org/cf/oracle/FileUtils.java +35 -0
  15. data/driver/src/main/java/org/cf/oracle/StackSpoofer.java +42 -0
  16. data/driver/src/main/java/org/cf/oracle/options/InvocationTarget.java +40 -0
  17. data/driver/src/main/java/org/cf/oracle/options/TargetParser.java +121 -0
  18. data/lib/dex-oracle/driver.rb +255 -0
  19. data/lib/dex-oracle/logging.rb +32 -0
  20. data/lib/dex-oracle/plugin.rb +87 -0
  21. data/lib/dex-oracle/plugins/string_decryptor.rb +59 -0
  22. data/lib/dex-oracle/plugins/undexguard.rb +155 -0
  23. data/lib/dex-oracle/plugins/unreflector.rb +85 -0
  24. data/lib/dex-oracle/resources.rb +13 -0
  25. data/lib/dex-oracle/smali_field.rb +21 -0
  26. data/lib/dex-oracle/smali_file.rb +64 -0
  27. data/lib/dex-oracle/smali_input.rb +81 -0
  28. data/lib/dex-oracle/smali_method.rb +33 -0
  29. data/lib/dex-oracle/utility.rb +37 -0
  30. data/lib/dex-oracle/version.rb +3 -0
  31. data/lib/oracle.rb +61 -0
  32. data/res/driver.dex +0 -0
  33. data/res/dx.jar +0 -0
  34. data/spec/data/helloworld.apk +0 -0
  35. data/spec/data/helloworld.dex +0 -0
  36. data/spec/data/plugins/bytes_decrypt.smali +18 -0
  37. data/spec/data/plugins/class_forname.smali +14 -0
  38. data/spec/data/plugins/multi_bytes_decrypt.smali +28 -0
  39. data/spec/data/plugins/string_decrypt.smali +14 -0
  40. data/spec/data/plugins/string_lookup_1int.smali +14 -0
  41. data/spec/data/plugins/string_lookup_3int.smali +18 -0
  42. data/spec/data/smali/helloworld.smali +17 -0
  43. data/spec/dex-oracle/driver_spec.rb +82 -0
  44. data/spec/dex-oracle/plugins/string_decryptor_spec.rb +25 -0
  45. data/spec/dex-oracle/plugins/undexguard_spec.rb +69 -0
  46. data/spec/dex-oracle/plugins/unreflector_spec.rb +29 -0
  47. data/spec/dex-oracle/smali_field_spec.rb +15 -0
  48. data/spec/dex-oracle/smali_file_spec.rb +41 -0
  49. data/spec/dex-oracle/smali_input_spec.rb +90 -0
  50. data/spec/dex-oracle/smali_method_spec.rb +19 -0
  51. data/spec/spec_helper.rb +9 -0
  52. data/update_driver +5 -0
  53. metadata +195 -0
@@ -0,0 +1,85 @@
1
+ require 'digest'
2
+ require_relative '../logging'
3
+
4
+ class Unreflector < Plugin
5
+ include Logging
6
+ include CommonRegex
7
+
8
+ attr_reader :optimizations
9
+
10
+ CLASS_FOR_NAME = 'invoke-static \{[vp]\d+\}, Ljava\/lang\/Class;->forName\(Ljava\/lang\/String;\)Ljava\/lang\/Class;'
11
+
12
+ CONST_CLASS_REGEX = Regexp.new(
13
+ '^[ \t]*(' << CONST_STRING << '\s+' <<
14
+ CLASS_FOR_NAME << '\s+' <<
15
+ MOVE_RESULT_OBJECT << ')'
16
+ )
17
+
18
+ VIRTUAL_FIELD_LOOKUP = Regexp.new(
19
+ '^[ \t]*(' <<
20
+ CONST_STRING << '\s+' \
21
+ 'invoke-static \{[vp]\d+\}, Ljava\/lang\/Class;->forName\(Ljava\/lang\/String;\)Ljava\/lang\/Class;\s+' <<
22
+ MOVE_RESULT_OBJECT << '\s+' <<
23
+ CONST_STRING << '\s+' \
24
+ 'invoke-virtual \{[vp]\d+, [vp]\d+\}, Ljava\/lang\/Class;->getField\(Ljava\/lang\/String;\)Ljava\/lang\/reflect\/Field;\s+' <<
25
+ MOVE_RESULT_OBJECT << '\s+' \
26
+ 'invoke-virtual \{[vp]\d+, ([vp]\d+)\}, Ljava\/lang\/reflect\/Field;->get\(Ljava\/lang\/Object;\)Ljava\/lang\/Object;\s+' <<
27
+ MOVE_RESULT_OBJECT << ')'
28
+ )
29
+
30
+ STATIC_FIELD_LOOKUP = Regexp.new(
31
+ '^[ \t]*(' <<
32
+ CONST_STRING << '\s+' <<
33
+ CLASS_FOR_NAME << '\s+' <<
34
+ MOVE_RESULT_OBJECT << '\s+' <<
35
+ CONST_STRING <<
36
+ 'invoke-virtual \{[vp]\d+, [vp]\d+\}, Ljava\/lang\/Class;->getField\(Ljava\/lang\/String;\)Ljava\/lang\/reflect\/Field;\s+' <<
37
+ MOVE_RESULT_OBJECT << '\s+' \
38
+ 'const/4 [vp]\d+, 0x0\s+' \
39
+ 'invoke-virtual \{[vp]\d+, ([vp]\d+)\}, Ljava\/lang\/reflect\/Field;->get\(Ljava\/lang\/Object;\)Ljava\/lang\/Object;\s+' <<
40
+ MOVE_RESULT_OBJECT <<
41
+ ')'
42
+ )
43
+
44
+ CLASS_LOOKUP_MODIFIER = -> (_, output, out_reg) { "const-class #{out_reg}, #{output}" }
45
+
46
+ def initialize(driver, smali_files, methods)
47
+ @driver = driver
48
+ @smali_files = smali_files
49
+ @methods = methods
50
+ @optimizations = Hash.new(0)
51
+ end
52
+
53
+ def process
54
+ made_changes = false
55
+ @methods.each do |method|
56
+ logger.info("Unreflecting #{method.descriptor}")
57
+ made_changes |= lookup_classes(method)
58
+ end
59
+
60
+ made_changes
61
+ end
62
+
63
+ def optimizations
64
+ @optimizations
65
+ end
66
+
67
+ private
68
+
69
+ def lookup_classes(method)
70
+ target_to_contexts = {}
71
+ target_id_to_output = {}
72
+ matches = method.body.scan(CONST_CLASS_REGEX)
73
+ @optimizations[:class_lookups] += matches.size
74
+ matches.each do |original, class_name, out_reg|
75
+ target = { id: Digest::SHA256.hexdigest(original) }
76
+ smali_class = "L#{class_name.tr('.', '/')};"
77
+ target_id_to_output[target[:id]] = ['success', smali_class]
78
+ target_to_contexts[target] = [] unless target_to_contexts.key?(target)
79
+ target_to_contexts[target] << [original, out_reg]
80
+ end
81
+
82
+ method_to_target_to_contexts = { method => target_to_contexts }
83
+ Plugin.apply_outputs(target_id_to_output, method_to_target_to_contexts, CLASS_LOOKUP_MODIFIER)
84
+ end
85
+ end
@@ -0,0 +1,13 @@
1
+ class Resources
2
+ include Logging
3
+
4
+ PATH = File.join(File.dirname(File.expand_path(__FILE__)), '../../res')
5
+
6
+ def self.dx
7
+ return "#{PATH}/dx.jar"
8
+ end
9
+
10
+ def self.driver_dex
11
+ return "#{PATH}/driver.dex"
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ class SmaliField
2
+ attr_reader :name, :class, :type, :descriptor
3
+
4
+ def initialize(class_name, field_signature)
5
+ @class = class_name
6
+ @descriptor = "#{class_name}->#{field_signature}"
7
+ @name, @type = field_signature.split(':')
8
+ end
9
+
10
+ def to_s
11
+ @descriptor
12
+ end
13
+
14
+ def ==(other)
15
+ other.class == self.class && other.state == state
16
+ end
17
+
18
+ def state
19
+ [@name, @class, @type, @descriptor]
20
+ end
21
+ end
@@ -0,0 +1,64 @@
1
+ require_relative 'smali_field'
2
+ require_relative 'smali_method'
3
+ require_relative 'logging'
4
+
5
+ class SmaliFile
6
+ attr_reader :class, :super, :interfaces, :methods, :fields, :file_path, :content
7
+
8
+ include Logging
9
+
10
+ ACCESSOR = /(?:interface|public|protected|private|abstract|static|final|synchronized|transient|volatile|native|strictfp|synthetic|enum|annotation)/
11
+ TYPE = /(?:[IJFDZBCV]|L[^;]+;)/
12
+ CLASS = /^\.class (?:#{ACCESSOR} )+(L[^;]+;)/
13
+ SUPER = /^\.super (L[^;]+;)/
14
+ INTERFACE = /^\.implements (L[^;]+;)/
15
+ FIELD = /^\.field (?:#{ACCESSOR} )+([^\s]+)$/
16
+ METHOD = /^.method (?:#{ACCESSOR} )+([^\s]+)$/
17
+
18
+ def initialize(file_path)
19
+ @file_path = file_path
20
+ @modified = false
21
+ parse(file_path)
22
+ end
23
+
24
+ def update
25
+ @methods.each do |m|
26
+ next unless m.modified
27
+ logger.debug("Updating method: #{m}")
28
+ update_method(m)
29
+ m.modified = false
30
+ end
31
+ File.open(@file_path, 'w') { |f| f.write(@content) }
32
+ end
33
+
34
+ def to_s
35
+ @class
36
+ end
37
+
38
+ private
39
+
40
+ def parse(file_path)
41
+ @content = IO.read(file_path)
42
+ @class = @content[CLASS, 1]
43
+ @super = @content[SUPER, 1]
44
+ @interfaces = []
45
+ @content.scan(INTERFACE).each { |m| @interfaces << m.first }
46
+ @fields = []
47
+ @content.scan(FIELD).each { |m| @fields << SmaliField.new(@class, m.first) }
48
+ @methods = []
49
+ @content.scan(METHOD).each do |m|
50
+ body_regex = build_method_regex(m.first)
51
+ body = @content[body_regex, 1]
52
+ @methods << SmaliMethod.new(@class, m.first, body)
53
+ end
54
+ end
55
+
56
+ def build_method_regex(method_signature)
57
+ /\.method (?:#{ACCESSOR} )+#{Regexp.escape(method_signature)}(.*)^\.end method/m
58
+ end
59
+
60
+ def update_method(method)
61
+ body_regex = build_method_regex(method.signature)
62
+ @content[body_regex, 1] = method.body
63
+ end
64
+ end
@@ -0,0 +1,81 @@
1
+ require 'zip'
2
+ require 'english'
3
+ require_relative 'utility'
4
+
5
+ class SmaliInput
6
+ attr_reader :dir, :out_apk, :out_dex, :temp_dir, :temp_dex
7
+
8
+ DEX_MAGIC = [0x64, 0x65, 0x78]
9
+ PK_ZIP_MAGIC = [0x50, 0x4b, 0x3]
10
+
11
+ def initialize(input)
12
+ prepare(input)
13
+ end
14
+
15
+ def finish
16
+ SmaliInput.update_apk(dir, @out_apk) if @out_apk
17
+ SmaliInput.compile(dir, @out_dex) if @out_dex
18
+ FileUtils.rm_rf(@dir) if @temp_dir
19
+ FileUtils.rm_rf(@out_dex) if @temp_dex
20
+ end
21
+
22
+ private
23
+
24
+ def self.compile(dir, out_dex = nil)
25
+ fail 'Smali could not be found on the path.' if Utility.which('smali').nil?
26
+ out_dex = Tempfile.new(['oracle', '.dex']) if out_dex.nil?
27
+ exit_code = SmaliInput.exec("smali #{dir} -o #{out_dex.path}")
28
+ # Remember kids, if you make a CLI, exit with non-zero status for failures
29
+ fail 'Crap, smali compilation failed.' if $CHILD_STATUS.exitstatus != 0
30
+ out_dex
31
+ end
32
+
33
+ def self.update_apk(dir, out_apk)
34
+ out_dex = compile(dir)
35
+ Utility.update_zip(out_apk, 'classes.dex', out_dex)
36
+ end
37
+
38
+ def self.extract_dex(apk, out_dex)
39
+ Utility.extract_file(apk, 'classes.dex', out_dex)
40
+ end
41
+
42
+ def prepare(input)
43
+ if File.directory?(input)
44
+ @temp_dir = false
45
+ @temp_dex = true
46
+ @dir = input
47
+ @out_dex = SmaliInput.compile(dir)
48
+ return
49
+ end
50
+
51
+ magic = File.open(input) { |f| f.read(3) }.bytes.to_a
52
+ case magic
53
+ when PK_ZIP_MAGIC
54
+ @temp_dex = true
55
+ @temp_dir = true
56
+ @out_apk = "#{File.basename(input, '.*')}_oracle#{File.extname(input)}"
57
+ @out_dex = Tempfile.new(['oracle', '.dex'])
58
+ FileUtils.cp(input, @out_apk)
59
+ SmaliInput.extract_dex(@out_apk, @out_dex)
60
+ baksmali(input)
61
+ when DEX_MAGIC
62
+ @temp_dex = false
63
+ @temp_dir = true
64
+ @out_dex = "#{File.basename(input, '.*')}_oracle#{File.extname(input)}"
65
+ baksmali(input)
66
+ else
67
+ fail "Unrecognized file type for: #{input}, magic=#{magic.inspect}"
68
+ end
69
+ end
70
+
71
+ def baksmali(input)
72
+ fail 'Baksmali could not be found on the path.' if Utility.which('baksmali').nil?
73
+ @dir = Dir.mktmpdir
74
+ cmd = "baksmali #{input} -o #{@dir}"
75
+ SmaliInput.exec(cmd)
76
+ end
77
+
78
+ def self.exec(cmd)
79
+ `#{cmd}`
80
+ end
81
+ end
@@ -0,0 +1,33 @@
1
+ class SmaliMethod
2
+ attr_accessor :modified, :body
3
+ attr_reader :name, :class, :descriptor, :signature, :parameters, :return_type
4
+
5
+ PARAMETER_ISOLATOR = /\([^\)]+\)/
6
+ PARAMETER_INDIVIDUATOR = /(\[*(?:[BCDFIJSZ]|L[^;]+;))/
7
+
8
+ def initialize(class_name, signature, body = nil)
9
+ @modified = false
10
+ @class = class_name
11
+ @name = signature[/[^\(]+/]
12
+ @body = body
13
+ @return_type = signature[/[^\)$]+$/]
14
+ @descriptor = "#{class_name}->#{signature}"
15
+ @signature = signature
16
+ @parameters = []
17
+ parameter_string = signature[PARAMETER_ISOLATOR]
18
+ return if parameter_string.nil?
19
+ parameter_string.scan(PARAMETER_INDIVIDUATOR).each { |m| @parameters << m.first }
20
+ end
21
+
22
+ def to_s
23
+ @descriptor
24
+ end
25
+
26
+ def ==(other)
27
+ other.class == self.class && other.state == state
28
+ end
29
+
30
+ def state
31
+ [@name, @class, @descriptor, @parameters, @return_type, @modified, @body]
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+ require 'zip'
2
+
3
+ class Utility
4
+ def self.which(cmd)
5
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
6
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
7
+ exts.each do |ext|
8
+ exe = File.join(path, "#{cmd}#{ext}")
9
+ return exe if File.executable?(exe) && !File.directory?(exe)
10
+ end
11
+ end
12
+ nil
13
+ end
14
+
15
+ def self.create_zip(zip, name_to_file)
16
+ Zip::File.open(zip, Zip::File::CREATE) do |zf|
17
+ name_to_file.each { |n, f| zf.add(n, f) }
18
+ end
19
+ end
20
+
21
+ def self.extract_file(zip, name, dest)
22
+ Zip::File.open(zip) do |zf|
23
+ zf.each do |e|
24
+ next unless e.name == name
25
+ e.extract(dest) { true } # overwrite
26
+ break
27
+ end
28
+ end
29
+ end
30
+
31
+ def self.update_zip(zip, name, target)
32
+ Zip::File.open(zip) do |zf|
33
+ zf.remove(name)
34
+ zf.add(name, target.path)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ module DexOracle
2
+ VERSION = '1.0.2'
3
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'dex-oracle/logging'
2
+ require_relative 'dex-oracle/plugin'
3
+ require_relative 'dex-oracle/smali_file'
4
+
5
+ class Oracle
6
+ include Logging
7
+
8
+ def initialize(smali_dir, driver, include_types, exclude_types, disable_plugins)
9
+ @smali_files = Oracle.parse_smali(smali_dir)
10
+ @methods = Oracle.filter_methods(@smali_files, include_types, exclude_types)
11
+ Plugin.init_plugins(driver, @smali_files, @methods)
12
+ @disable_plugins = disable_plugins
13
+ logger.info("Disabled plugins: #{@disable_plugins * ','}") unless @disable_plugins.empty?
14
+ end
15
+
16
+ def divine
17
+ puts "Optimizing #{@methods.size} methods over #{@smali_files.size} Smali files."
18
+ made_changes = process_plugins
19
+ @smali_files.each(&:update) if made_changes
20
+ optimizations = {}
21
+ Plugin.plugins.each { |p| optimizations.merge!(p.optimizations) }
22
+ opt_str = optimizations.collect { |k, v| "#{k}=#{v}" } * ', '
23
+ puts "Optimizations: #{opt_str}"
24
+ end
25
+
26
+ def process_plugins
27
+ made_changes = false
28
+ loop do
29
+ sweep_changes = false
30
+ Plugin.plugins.each do |p|
31
+ next if @disable_plugins.include?(p.class.name.downcase)
32
+ sweep_changes |= p.process
33
+ end
34
+ made_changes |= sweep_changes
35
+ break unless sweep_changes
36
+ end
37
+ made_changes
38
+ end
39
+
40
+ def self.filter_methods(smali_files, include_types, exclude_types)
41
+ methods = []
42
+ smali_files.each do |smali_file|
43
+ smali_file.methods.each do |method|
44
+ if include_types
45
+ next if method.descriptor !~ include_types
46
+ elsif exclude_types && !(method.descriptor !~ exclude_types)
47
+ next
48
+ end
49
+ methods << method
50
+ end
51
+ end
52
+
53
+ methods
54
+ end
55
+
56
+ def self.parse_smali(smali_dir)
57
+ smali_files = []
58
+ Dir["#{smali_dir}/**/*.smali"].each { |f| smali_files << SmaliFile.new(f) }
59
+ smali_files
60
+ end
61
+ end
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,18 @@
1
+ .class public Lorg/cf/BytesDecrypt;
2
+ .super Ljava/lang/Object;
3
+
4
+ .method public static doStuff()V
5
+ .locals 1
6
+
7
+ const-string v0, "asdf"
8
+
9
+ invoke-virtual {v0}, Ljava/lang/String;->getBytes()[B
10
+
11
+ move-result-object v0
12
+
13
+ invoke-static {v0}, Lorg/cf/BytesDecrypt;->decrypt([B)Ljava/lang/String;
14
+
15
+ move-result-object v0
16
+
17
+ return-void
18
+ .end method
@@ -0,0 +1,14 @@
1
+ .class public Lorg/cf/ClassForName;
2
+ .super Ljava/lang/Object;
3
+
4
+ .method public static doStuff()V
5
+ .locals 1
6
+
7
+ const-string v0, "android.content.Intent"
8
+
9
+ invoke-static {v0}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class;
10
+
11
+ move-result-object v0
12
+
13
+ return-void
14
+ .end method