dex-oracle 1.0.4 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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