dex-oracle 1.0.4 → 1.0.5

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.
@@ -1,9 +1,9 @@
1
1
  class Plugin
2
2
  module CommonRegex
3
- CONST_NUMBER = 'const(?:\/\d+) [vp]\d+, (-?0x[a-f\d]+)'
4
- ESCAPE_STRING = '"(.*?)(?<!\\\\)"'
5
- CONST_STRING = 'const-string [vp]\d+, ' << ESCAPE_STRING << '.*'
6
- MOVE_RESULT_OBJECT = 'move-result-object ([vp]\d+)'
3
+ CONST_NUMBER = 'const(?:\/\d+) [vp]\d+, (-?0x[a-f\d]+)'.freeze
4
+ ESCAPE_STRING = '"(.*?)(?<!\\\\)"'.freeze
5
+ CONST_STRING = 'const-string(?:/jumbo)? [vp]\d+, ' << ESCAPE_STRING << '.*'.freeze
6
+ MOVE_RESULT_OBJECT = 'move-result-object ([vp]\d+)'.freeze
7
7
  end
8
8
 
9
9
  @plugins = []
@@ -16,7 +16,7 @@ class Plugin
16
16
  Dir["#{File.dirname(__FILE__)}/plugins/*.rb"].each { |f| require f }
17
17
  classes = []
18
18
  Object.constants.each do |klass|
19
- const = Kernel.const_get(klass)
19
+ const = Kernel.const_get(klass) unless klass == :TimeoutError
20
20
  next unless const.respond_to?(:superclass) && const.superclass == Plugin
21
21
  classes << const
22
22
  end
@@ -29,11 +29,11 @@ class Plugin
29
29
  end
30
30
 
31
31
  def process
32
- fail 'process not implemented'
32
+ raise 'process not implemented'
33
33
  end
34
34
 
35
35
  def optimizations
36
- fail 'optimizations not implemented'
36
+ raise 'optimizations not implemented'
37
37
  end
38
38
 
39
39
  # method_to_target_to_context -> { method: [target_to_context] }
@@ -0,0 +1,61 @@
1
+ require_relative '../logging'
2
+ require_relative '../utility'
3
+
4
+ # Sample: 0e18bbf2a3539e5669be76ed4c468257ecfd3d36
5
+ class BitwiseAntiSkid < Plugin
6
+ attr_reader :optimizations
7
+
8
+ include Logging
9
+ include CommonRegex
10
+
11
+ STRING_DECRYPT = Regexp.new(
12
+ '^[ \t]*(' +
13
+ CONST_STRING + '\s+' + \
14
+ CONST_NUMBER + '\s+' \
15
+ 'invoke-static \{[vp]\d+, [vp]\d+\}, L([^;]+);->(Go_Learn_Something\(Ljava/lang/String;I\))Ljava/lang/String;' \
16
+ '\s+' + \
17
+ MOVE_RESULT_OBJECT + ')'
18
+ )
19
+
20
+ MODIFIER = -> (_, output, out_reg) { "const-string #{out_reg}, \"#{output.split('').collect { |e| e.inspect[1..-2] }.join}\"" }
21
+
22
+ def initialize(driver, smali_files, methods)
23
+ @driver = driver
24
+ @smali_files = smali_files
25
+ @methods = methods
26
+ @optimizations = Hash.new(0)
27
+ end
28
+
29
+ def process
30
+ method_to_target_to_contexts = {}
31
+ @methods.each do |method|
32
+ logger.info("Decrypting Bitwise Anti-Skid #{method.descriptor}")
33
+ target_to_contexts = {}
34
+ target_to_contexts.merge!(decrypt_strings(method))
35
+ target_to_contexts.map { |_, v| v.uniq! }
36
+ method_to_target_to_contexts[method] = target_to_contexts unless target_to_contexts.empty?
37
+ end
38
+
39
+ made_changes = false
40
+ made_changes |= Plugin.apply_batch(@driver, method_to_target_to_contexts, MODIFIER)
41
+
42
+ made_changes
43
+ end
44
+
45
+ private
46
+
47
+ def decrypt_strings(method)
48
+ target_to_contexts = {}
49
+ matches = method.body.scan(STRING_DECRYPT)
50
+ @optimizations[:string_decrypts] += matches.size if matches
51
+ matches.each do |original, encrypted, number, class_name, method_signature, out_reg|
52
+ target = @driver.make_target(
53
+ class_name, method_signature, encrypted, number.to_i(16)
54
+ )
55
+ target_to_contexts[target] = [] unless target_to_contexts.key?(target)
56
+ target_to_contexts[target] << [original, out_reg]
57
+ end
58
+
59
+ target_to_contexts
60
+ end
61
+ end
@@ -2,13 +2,17 @@ require_relative '../logging'
2
2
  require_relative '../utility'
3
3
 
4
4
  class StringDecryptor < Plugin
5
+ attr_reader :optimizations
6
+
5
7
  include Logging
6
8
  include CommonRegex
7
9
 
8
10
  STRING_DECRYPT = Regexp.new(
9
- '^[ \t]*(' << CONST_STRING << '\s+' \
11
+ '^[ \t]*(' +
12
+ CONST_STRING + '\s+' \
10
13
  'invoke-static \{[vp]\d+\}, L([^;]+);->([^\(]+\(Ljava/lang/String;\))Ljava/lang/String;' \
11
- '\s+' << MOVE_RESULT_OBJECT << ')'
14
+ '\s+' +
15
+ MOVE_RESULT_OBJECT + ')'
12
16
  )
13
17
 
14
18
  MODIFIER = -> (_, output, out_reg) { "const-string #{out_reg}, \"#{output.split('').collect { |e| e.inspect[1..-2] }.join}\"" }
@@ -36,10 +40,6 @@ class StringDecryptor < Plugin
36
40
  made_changes
37
41
  end
38
42
 
39
- def optimizations
40
- @optimizations
41
- end
42
-
43
43
  private
44
44
 
45
45
  def decrypt_strings(method)
@@ -2,36 +2,45 @@ require_relative '../logging'
2
2
  require_relative '../utility'
3
3
 
4
4
  class Undexguard < Plugin
5
+ attr_reader :optimizations
6
+
5
7
  include Logging
6
8
  include CommonRegex
7
9
 
8
10
  STRING_LOOKUP_3INT = Regexp.new(
9
- '^[ \t]*(' << ((CONST_NUMBER << '\s+') * 3) <<
11
+ '^[ \t]*(' +
12
+ ((CONST_NUMBER + '\s+') * 3) +
10
13
  'invoke-static \{[vp]\d+, [vp]\d+, [vp]\d+\}, L([^;]+);->([^\(]+\(III\))Ljava/lang/String;' \
11
- '\s+' << MOVE_RESULT_OBJECT << ')',
14
+ '\s+' +
15
+ MOVE_RESULT_OBJECT + ')',
12
16
  Regexp::MULTILINE
13
17
  )
14
18
 
15
19
  STRING_LOOKUP_1INT = Regexp.new(
16
- '^[ \t]*(' << CONST_NUMBER << '\s+' \
20
+ '^[ \t]*(' +
21
+ CONST_NUMBER + '\s+' \
17
22
  'invoke-static \{[vp]\d+\}, L([^;]+);->([^\(]+\(I\))Ljava/lang/String;' \
18
- '\s+' << MOVE_RESULT_OBJECT << ')'
23
+ '\s+' +
24
+ MOVE_RESULT_OBJECT + ')'
19
25
  )
20
26
 
21
27
  BYTES_DECRYPT = Regexp.new(
22
- '^[ \t]*(' << CONST_STRING << '\s+' \
28
+ '^[ \t]*(' +
29
+ CONST_STRING + '\s+' \
23
30
  'invoke-virtual \{[vp]\d+\}, Ljava\/lang\/String;->getBytes\(\)\[B\s+' \
24
31
  'move-result-object [vp]\d+\s+' \
25
32
  'invoke-static \{[vp]\d+\}, L([^;]+);->([^\(]+\(\[B\))Ljava/lang/String;' \
26
- '\s+' << MOVE_RESULT_OBJECT << ')'
33
+ '\s+' +
34
+ MOVE_RESULT_OBJECT + ')'
27
35
  )
28
36
 
29
37
  MULTI_BYTES_DECRYPT = Regexp.new(
30
- '^[ \t]*(' << CONST_STRING << '\s+' \
38
+ '^[ \t]*(' +
39
+ CONST_STRING + '\s+' \
31
40
  'new-instance ([vp]\d+), L[^;]+;\s+' \
32
41
  'invoke-static \{[vp]\d+\}, L([^;]+);->([^\(]+\(Ljava/lang/String;\))\[B\s+' \
33
- 'move-result-object [vp]\d+\s+' <<
34
- CONST_STRING << '\s+' \
42
+ 'move-result-object [vp]\d+\s+' +
43
+ CONST_STRING + '\s+' \
35
44
  'invoke-static \{[vp]\d+, [vp]\d+\}, L([^;]+);->([^\(]+\(\[BLjava/lang/String;\))\[B\s+' \
36
45
  'move-result-object [vp]\d+\s+' \
37
46
  'invoke-static \{[vp]\d+\}, L([^;]+);->([^\(]+\(\[B\))\[B\s+' \
@@ -71,21 +80,8 @@ class Undexguard < Plugin
71
80
  made_changes
72
81
  end
73
82
 
74
- def optimizations
75
- @optimizations
76
- end
77
-
78
83
  private
79
84
 
80
- def self.array_string_to_array(str)
81
- if str =~ /\A\[(?:\d+(?:,\d+)*)?\]\z/
82
- str = eval(str)
83
- else
84
- fail "Output is not in byte format; this frightens me: #{str}"
85
- end
86
- str
87
- end
88
-
89
85
  def lookup_strings_3int(method)
90
86
  target_to_contexts = {}
91
87
  matches = method.body.scan(STRING_LOOKUP_3INT)
@@ -140,7 +136,7 @@ class Undexguard < Plugin
140
136
  iv_bytes = @driver.run(iv_class_name, iv_method_signature, iv_str)
141
137
  enc_bytes = @driver.run(iv2_class_name, iv2_method_signature, iv_bytes, iv2_str)
142
138
  dec_bytes = @driver.run(dec_class_name, dec_method_signature, enc_bytes)
143
- dec_array = Undexguard.array_string_to_array(dec_bytes)
139
+ dec_array = array_string_to_array(dec_bytes)
144
140
  dec_string = dec_array.pack('U*')
145
141
 
146
142
  target = { id: Digest::SHA256.hexdigest(original) }
@@ -152,4 +148,13 @@ class Undexguard < Plugin
152
148
  method_to_target_to_contexts = { method => target_to_contexts }
153
149
  Plugin.apply_outputs(target_id_to_output, method_to_target_to_contexts, MODIFIER)
154
150
  end
151
+
152
+ def array_string_to_array(str)
153
+ if str =~ /\A\[(?:\d+(?:,\d+)*)?\]\z/
154
+ str = eval(str)
155
+ else
156
+ raise "Output is not in byte format; this frightens me: #{str}"
157
+ end
158
+ str
159
+ end
155
160
  end
@@ -2,42 +2,43 @@ require 'digest'
2
2
  require_relative '../logging'
3
3
 
4
4
  class Unreflector < Plugin
5
+ attr_reader :optimizations
6
+
5
7
  include Logging
6
8
  include CommonRegex
7
9
 
8
- attr_reader :optimizations
9
-
10
- CLASS_FOR_NAME = 'invoke-static \{[vp]\d+\}, Ljava\/lang\/Class;->forName\(Ljava\/lang\/String;\)Ljava\/lang\/Class;'
10
+ CLASS_FOR_NAME = 'invoke-static \{[vp]\d+\}, Ljava\/lang\/Class;->forName\(Ljava\/lang\/String;\)Ljava\/lang\/Class;'.freeze
11
11
 
12
12
  CONST_CLASS_REGEX = Regexp.new(
13
- '^[ \t]*(' << CONST_STRING << '\s+' <<
14
- CLASS_FOR_NAME << '\s+' <<
15
- MOVE_RESULT_OBJECT << ')'
13
+ '^[ \t]*(' +
14
+ CONST_STRING + '\s+' +
15
+ CLASS_FOR_NAME + '\s+' +
16
+ MOVE_RESULT_OBJECT + ')'
16
17
  )
17
18
 
18
19
  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 << ')'
20
+ '^[ \t]*(' +
21
+ CONST_STRING + '\s+' \
22
+ 'invoke-static \{[vp]\d+\}, Ljava\/lang\/Class;->forName\(Ljava\/lang\/String;\)Ljava\/lang\/Class;\s+' +
23
+ MOVE_RESULT_OBJECT + '\s+' +
24
+ CONST_STRING + '\s+' \
25
+ 'invoke-virtual \{[vp]\d+, [vp]\d+\}, Ljava\/lang\/Class;->getField\(Ljava\/lang\/String;\)Ljava\/lang\/reflect\/Field;\s+' +
26
+ MOVE_RESULT_OBJECT + '\s+' \
27
+ 'invoke-virtual \{[vp]\d+, ([vp]\d+)\}, Ljava\/lang\/reflect\/Field;->get\(Ljava\/lang\/Object;\)Ljava\/lang\/Object;\s+' +
28
+ MOVE_RESULT_OBJECT + ')'
28
29
  )
29
30
 
30
31
  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+' \
32
+ '^[ \t]*(' +
33
+ CONST_STRING + '\s+' +
34
+ CLASS_FOR_NAME + '\s+' +
35
+ MOVE_RESULT_OBJECT + '\s+' +
36
+ CONST_STRING +
37
+ 'invoke-virtual \{[vp]\d+, [vp]\d+\}, Ljava\/lang\/Class;->getField\(Ljava\/lang\/String;\)Ljava\/lang\/reflect\/Field;\s+' +
38
+ MOVE_RESULT_OBJECT + '\s+' \
38
39
  '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 <<
40
+ 'invoke-virtual \{[vp]\d+, ([vp]\d+)\}, Ljava\/lang\/reflect\/Field;->get\(Ljava\/lang\/Object;\)Ljava\/lang\/Object;\s+' +
41
+ MOVE_RESULT_OBJECT +
41
42
  ')'
42
43
  )
43
44
 
@@ -60,10 +61,6 @@ class Unreflector < Plugin
60
61
  made_changes
61
62
  end
62
63
 
63
- def optimizations
64
- @optimizations
65
- end
66
-
67
64
  private
68
65
 
69
66
  def lookup_classes(method)
@@ -1,13 +1,15 @@
1
+ require_relative 'logging'
2
+
1
3
  class Resources
2
4
  include Logging
3
5
 
4
6
  PATH = File.join(File.dirname(File.expand_path(__FILE__)), '../../res')
5
7
 
6
8
  def self.dx
7
- return "#{PATH}/dx.jar"
9
+ "#{PATH}/dx.jar"
8
10
  end
9
11
 
10
12
  def self.driver_dex
11
- return "#{PATH}/driver.dex"
13
+ "#{PATH}/driver.dex"
12
14
  end
13
15
  end
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  require_relative 'smali_field'
2
3
  require_relative 'smali_method'
3
4
  require_relative 'logging'
@@ -7,13 +8,13 @@ class SmaliFile
7
8
 
8
9
  include Logging
9
10
 
10
- ACCESSOR = /(?:interface|public|protected|private|abstract|static|final|synchronized|transient|volatile|native|strictfp|synthetic|enum|annotation)/
11
+ ACCESSOR = /(?:abstract|annotation|constructor|enum|final|interface|native|private|protected|public|static|strictfp|synchronized|synthetic|transient|volatile)/
11
12
  TYPE = /(?:[IJFDZBCV]|L[^;]+;)/
12
13
  CLASS = /^\.class (?:#{ACCESSOR} )+(L[^;]+;)/
13
14
  SUPER = /^\.super (L[^;]+;)/
14
15
  INTERFACE = /^\.implements (L[^;]+;)/
15
16
  FIELD = /^\.field (?:#{ACCESSOR} )+([^\s]+)$/
16
- METHOD = /^.method (?:#{ACCESSOR} )+([^\s]+)$/
17
+ METHOD = /^\.method (?:#{ACCESSOR} )+([^\s]+)$/
17
18
 
18
19
  def initialize(file_path)
19
20
  @file_path = file_path
@@ -38,23 +39,44 @@ class SmaliFile
38
39
  private
39
40
 
40
41
  def parse(file_path)
41
- @content = IO.read(file_path)
42
+ logger.debug("Parsing: #{file_path} ...")
43
+ @content = File.open(file_path, 'r:UTF-8', &:read)
42
44
  @class = @content[CLASS, 1]
43
45
  @super = @content[SUPER, 1]
44
46
  @interfaces = []
45
47
  @content.scan(INTERFACE).each { |m| @interfaces << m.first }
46
48
  @fields = []
47
49
  @content.scan(FIELD).each { |m| @fields << SmaliField.new(@class, m.first) }
50
+ parse_methods
51
+ end
52
+
53
+ def parse_methods
48
54
  @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)
55
+ method_signature = nil
56
+ in_method = false
57
+ body = nil
58
+ @content.each_line do |line|
59
+ if in_method
60
+ if /^\.end method$/ =~ line
61
+ in_method = false
62
+ @methods << SmaliMethod.new(@class, method_signature, body)
63
+ next
64
+ end
65
+ body << line
66
+ else
67
+ next unless line.include?('.method ')
68
+ m = METHOD.match(line)
69
+ next unless m
70
+
71
+ in_method = true
72
+ method_signature = m.captures.first
73
+ body = "\n"
74
+ end
53
75
  end
54
76
  end
55
77
 
56
78
  def build_method_regex(method_signature)
57
- /\.method (?:#{ACCESSOR} )+#{Regexp.escape(method_signature)}(.*)^\.end method/m
79
+ /^\.method (?:#{ACCESSOR} )+#{Regexp.escape(method_signature)}(.+?)^\.end method$/m
58
80
  end
59
81
 
60
82
  def update_method(method)
@@ -5,8 +5,10 @@ require_relative 'utility'
5
5
  class SmaliInput
6
6
  attr_reader :dir, :out_apk, :out_dex, :temp_dir, :temp_dex
7
7
 
8
- DEX_MAGIC = [0x64, 0x65, 0x78]
9
- PK_ZIP_MAGIC = [0x50, 0x4b, 0x3]
8
+ include Logging
9
+
10
+ DEX_MAGIC = [0x64, 0x65, 0x78].freeze
11
+ PK_ZIP_MAGIC = [0x50, 0x4b, 0x3].freeze
10
12
 
11
13
  def initialize(input)
12
14
  prepare(input)
@@ -14,19 +16,18 @@ class SmaliInput
14
16
 
15
17
  def finish
16
18
  SmaliInput.update_apk(dir, @out_apk) if @out_apk
17
- SmaliInput.compile(dir, @out_dex) if @out_dex
19
+ SmaliInput.compile(dir, @out_dex) if @out_dex && !@out_apk
18
20
  FileUtils.rm_rf(@dir) if @temp_dir
19
21
  FileUtils.rm_rf(@out_dex) if @temp_dex
20
22
  end
21
23
 
22
- private
23
-
24
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?
25
+ raise 'Smali could not be found on the path.' if Utility.which('smali').nil?
26
+ out_dex = Tempfile.new(%w(oracle .dex)) if out_dex.nil?
27
+ logger.info("Compiling DEX #{out_dex.path} ...")
27
28
  exit_code = SmaliInput.exec("smali #{dir} -o #{out_dex.path}")
28
29
  # 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
+ raise 'Crap, smali compilation failed.' if $CHILD_STATUS.exitstatus != 0
30
31
  out_dex
31
32
  end
32
33
 
@@ -39,6 +40,20 @@ class SmaliInput
39
40
  Utility.extract_file(apk, 'classes.dex', out_dex)
40
41
  end
41
42
 
43
+ def self.exec(cmd)
44
+ `#{cmd}`
45
+ end
46
+
47
+ private
48
+
49
+ def baksmali(input)
50
+ logger.debug("Disassembling #{input} ...")
51
+ raise 'Baksmali could not be found on the path.' if Utility.which('baksmali').nil?
52
+ @dir = Dir.mktmpdir
53
+ cmd = "baksmali #{input} -o #{@dir}"
54
+ SmaliInput.exec(cmd)
55
+ end
56
+
42
57
  def prepare(input)
43
58
  if File.directory?(input)
44
59
  @temp_dir = false
@@ -54,7 +69,7 @@ class SmaliInput
54
69
  @temp_dex = true
55
70
  @temp_dir = true
56
71
  @out_apk = "#{File.basename(input, '.*')}_oracle#{File.extname(input)}"
57
- @out_dex = Tempfile.new(['oracle', '.dex'])
72
+ @out_dex = Tempfile.new(%w(oracle .dex))
58
73
  FileUtils.cp(input, @out_apk)
59
74
  SmaliInput.extract_dex(@out_apk, @out_dex)
60
75
  baksmali(input)
@@ -62,20 +77,11 @@ class SmaliInput
62
77
  @temp_dex = false
63
78
  @temp_dir = true
64
79
  @out_dex = "#{File.basename(input, '.*')}_oracle#{File.extname(input)}"
80
+ FileUtils.cp(input, @out_dex)
81
+ @out_dex = File.new(@out_dex)
65
82
  baksmali(input)
66
83
  else
67
- fail "Unrecognized file type for: #{input}, magic=#{magic.inspect}"
84
+ raise "Unrecognized file type for: #{input}, magic=#{magic.inspect}"
68
85
  end
69
86
  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
87
  end