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,255 @@
1
+ require 'json'
2
+ require 'digest'
3
+ require 'Open3'
4
+ require 'timeout'
5
+ require_relative 'resources'
6
+ require_relative 'logging'
7
+ require_relative 'utility'
8
+
9
+ class Driver
10
+ include Logging
11
+
12
+ UNESCAPES = {
13
+ 'a' => "\x07", 'b' => "\x08", 't' => "\x09",
14
+ 'n' => "\x0a", 'v' => "\x0b", 'f' => "\x0c",
15
+ 'r' => "\x0d", 'e' => "\x1b", '\\' => "\x5c",
16
+ '"' => "\x22", "'" => "\x27"
17
+ }
18
+ UNESCAPE_REGEX = /\\(?:([#{UNESCAPES.keys.join}])|u([\da-fA-F]{4}))|\\0?x([\da-fA-F]{2})/
19
+
20
+ OUTPUT_HEADER = '===ORACLE DRIVER OUTPUT==='
21
+ DRIVER_DIR = '/data/local'
22
+ DRIVER_CLASS = 'org.cf.oracle.Driver'
23
+
24
+ def initialize(device_id, timeout = 60)
25
+ @device_id = device_id
26
+ @timeout = timeout
27
+
28
+ device_str = device_id.empty? ? '' : "-s #{@device_id} "
29
+ @adb_base = "adb #{device_str}%s"
30
+ @cmd_stub = "export CLASSPATH=#{DRIVER_DIR}/od.zip; app_process /system/bin #{DRIVER_CLASS}"
31
+
32
+ @cache = {}
33
+ end
34
+
35
+ def install(dex)
36
+ fail 'Unable to find Java on the path.' unless Utility.which('java')
37
+
38
+ begin
39
+ # Merge driver and target dex file
40
+ # Congratulations. You're now one of the 5 people who've used this tool explicitly.
41
+ logger.debug("Merging #{dex.path} and driver dex ...")
42
+ fail "#{Resources.dx} does not exist and is required for DexMerger" unless File.exist?(Resources.dx)
43
+ fail "#{Resources.driver_dex} does not exist" unless File.exist?(Resources.driver_dex)
44
+ tf = Tempfile.new(['oracle-driver', '.dex'])
45
+ cmd = "java -cp #{Resources.dx} com.android.dx.merge.DexMerger #{tf.path} #{dex.path} #{Resources.driver_dex}"
46
+ exec("#{cmd}")
47
+
48
+ # Zip merged dex and push to device
49
+ logger.debug('Pushing merged driver to device ...')
50
+ tz = Tempfile.new(['oracle-driver', '.zip'])
51
+ Utility.create_zip(tz.path, { 'classes.dex' => tf })
52
+ adb("push #{tz.path} #{DRIVER_DIR}/od.zip")
53
+ rescue => e
54
+ puts "Error installing the driver: #{e}"
55
+ ensure
56
+ tf.close
57
+ tf.unlink
58
+ tz.close
59
+ tz.unlink
60
+ end
61
+ end
62
+
63
+ def run(class_name, signature, *args)
64
+ method = SmaliMethod.new(class_name, signature)
65
+ cmd = build_command(method.class, method.name, method.parameters, args)
66
+ output = nil
67
+ retries = 1
68
+ begin
69
+ output = drive(cmd)
70
+ rescue => e
71
+ # If you slam an emulator or device with too many app_process commands,
72
+ # it eventually gets angry and segmentation faults. No idea why.
73
+ # This took many frustrating hours to figure out.
74
+ if retries <= 3
75
+ logger.debug("Driver execution failed. Taking a quick nap and retrying, Zzzzz ##{retries} / 3 ...")
76
+ sleep 5
77
+ retries += 1
78
+ retry
79
+ else
80
+ raise e
81
+ end
82
+ end
83
+ output
84
+ end
85
+
86
+ def run_batch(batch)
87
+ push_batch_targets(batch)
88
+ retries = 1
89
+ begin
90
+ drive("#{@cmd_stub} @#{DRIVER_DIR}/od-targets.json", true)
91
+ rescue => e
92
+ if retries <= 3 && e.message.include?('Segmentation fault')
93
+ # Maybe we just need to retry
94
+ logger.debug("Driver execution segfaulted. Taking a quick nap and retrying, Zzzzz ##{retries} / 3 ...")
95
+ sleep 5
96
+ retries += 1
97
+ retry
98
+ else
99
+ raise e
100
+ end
101
+ end
102
+ pull_batch_outputs
103
+ end
104
+
105
+ def make_target(class_name, signature, *args)
106
+ method = SmaliMethod.new(class_name, signature)
107
+ target = {
108
+ className: method.class.tr('/', '.'),
109
+ methodName: method.name,
110
+ arguments: Driver.build_arguments(method.parameters, args)
111
+ }
112
+ # Identifiers are used to map individual inputs to outputs
113
+ target[:id] = Digest::SHA256.hexdigest(target.to_json)
114
+
115
+ target
116
+ end
117
+
118
+ private
119
+
120
+ def push_batch_targets(batch)
121
+ target_file = Tempfile.new(['oracle-targets', '.json'])
122
+ target_file << batch.to_json
123
+ target_file.flush
124
+ logger.info("Pushing #{batch.size} method targets to device ...")
125
+ adb("push #{target_file.path} #{DRIVER_DIR}/od-targets.json")
126
+ target_file.close
127
+ target_file.unlink
128
+ end
129
+
130
+ def pull_batch_outputs
131
+ output_file = Tempfile.new(['oracle-output', '.json'])
132
+ logger.debug('Pulling batch results from device ...')
133
+ adb("pull #{DRIVER_DIR}/od-output.json #{output_file.path}")
134
+ adb("shell rm #{DRIVER_DIR}/od-output.json")
135
+ outputs = JSON.parse(File.read(output_file.path))
136
+ outputs.each { |_, (_, v2)| v2.gsub!(/(?:^"|"$)/, '') if v2.start_with?('"') }
137
+ logger.debug("Pulled #{outputs.size} outputs.")
138
+ output_file.close
139
+ output_file.unlink
140
+ outputs
141
+ end
142
+
143
+ def exec(cmd, silent = true)
144
+ logger.debug("exec: #{cmd}")
145
+
146
+ retries = 1
147
+ begin
148
+ status = Timeout.timeout(@timeout) do
149
+ if !silent
150
+ `#{cmd}`
151
+ else
152
+ Open3.popen3(cmd) { |_, stdout, _, _| stdout.read }
153
+ end
154
+ end
155
+ rescue => e
156
+ if retries <= 3
157
+ logger.debug("ADB command execution timed out, retrying #{retries} ...")
158
+ sleep 5
159
+ retries += 1
160
+ retry
161
+ else
162
+ raise e
163
+ end
164
+ end
165
+ end
166
+
167
+ def validate_output(full_cmd, full_output)
168
+ output_lines = full_output.split(/\r?\n/)
169
+ exit_code = output_lines.last.to_i
170
+ if exit_code != 0
171
+ # Non zero exit code would only imply adb command itself was flawed
172
+ # app_process, dalvikvm, etc. don't propigate exit codes back
173
+ fail "Command failed with #{exit_code}: #{full_cmd}\nOutput: #{full_output}"
174
+ end
175
+
176
+ # Successful driver run should include driver header
177
+ # Otherwise it may be a Segmentation fault or Killed
178
+ logger.debug("Full output: #{full_output.inspect}")
179
+ header = output_lines[0]
180
+ fail "app_process execution failure, output: '#{full_output}'" if header != OUTPUT_HEADER
181
+
182
+ output_lines[1..-2].join("\n").rstrip
183
+ end
184
+
185
+ def drive(cmd, batch = false)
186
+ return @cache[cmd] if @cache.key?(cmd)
187
+
188
+ full_cmd = "shell \"#{cmd}\"; echo $?"
189
+ full_output = adb(full_cmd)
190
+ output = validate_output(full_cmd, full_output)
191
+
192
+ # The driver writes any actual exceptions to the filesystem
193
+ # Need to check to make sure the output value is legitimate
194
+ logger.debug('Checking if execution had any exceptions ...')
195
+ exception = adb("shell cat #{DRIVER_DIR}/od-exception.txt").strip
196
+ unless exception.end_with?('No such file or directory')
197
+ adb("shell rm #{DRIVER_DIR}/od-exception.txt")
198
+ fail exception
199
+ end
200
+ logger.debug('No exceptions found :)')
201
+
202
+ # Cache successful results for single method invocations for speed!
203
+ @cache[cmd] = output unless batch
204
+ logger.debug("output = #{output}")
205
+
206
+ output
207
+ end
208
+
209
+ def adb(cmd)
210
+ full_cmd = @adb_base % cmd
211
+ exec(full_cmd).rstrip
212
+ end
213
+
214
+ def self.unescape(str)
215
+ str.gsub(UNESCAPE_REGEX) do
216
+ if Regexp.last_match[1]
217
+ Regexp.last_match[1] == '\\' ? Regexp.last_match[1] : UNESCAPES[Regexp.last_match[1]]
218
+ elsif Regexp.last_match[2] # escape \u0000 unicode
219
+ [Regexp.last_match[2].hex].pack('U*')
220
+ elsif Regexp.last_match[3] # escape \0xff or \xff
221
+ [Regexp.last_match[3]].pack('H2')
222
+ end
223
+ end
224
+ end
225
+
226
+ def build_command(class_name, method_name, parameters, args)
227
+ class_name.tr!('/', '.') # Make valid Java class name
228
+ class_name.gsub!('$', '\$') # inner classes
229
+ method_name.gsub!('$', '\$') # synthetic method names
230
+ target = "'#{class_name}' '#{method_name}'"
231
+ target_args = Driver.build_arguments(parameters, args)
232
+ "#{@cmd_stub} #{target} #{target_args * ' '}"
233
+ end
234
+
235
+ def self.build_arguments(parameters, args)
236
+ parameters.map.with_index do |o, i|
237
+ build_argument(o, args[i])
238
+ end
239
+ end
240
+
241
+ def self.build_argument(parameter, argument)
242
+ if parameter[0] == 'L'
243
+ java_type = parameter[1..-2].tr('/', '.')
244
+ if java_type == 'java.lang.String'
245
+ # Need to unescape smali string to get the actual string
246
+ # Converting to bytes just avoids any weird non-printable characters nonsense
247
+ argument = "[#{Driver.unescape(argument).bytes.to_a.join(',')}]"
248
+ end
249
+ "#{java_type}:#{argument}"
250
+ else
251
+ argument = (argument == '1' ? 'true' : 'false') if parameter == 'Z'
252
+ "#{parameter}:#{argument}"
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,32 @@
1
+ require 'logger'
2
+
3
+ module Logging
4
+ class << self
5
+ def logger
6
+ unless @logger
7
+ @logger = Logger.new($stdout)
8
+ @logger.level = Logger::WARN
9
+ @logger.formatter = proc do |severity, datetime, progname, msg|
10
+ "[#{severity}] #{datetime.strftime('%Y-%m-%d %H:%M:%S')}: #{msg}\n"
11
+ end
12
+ end
13
+ @logger
14
+ end
15
+
16
+ def logger=(logger)
17
+ @logger = logger
18
+ end
19
+ end
20
+
21
+ def self.included(base)
22
+ class << base
23
+ def logger
24
+ Logging.logger
25
+ end
26
+ end
27
+ end
28
+
29
+ def logger
30
+ Logging.logger
31
+ end
32
+ end
@@ -0,0 +1,87 @@
1
+ class Plugin
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+)'
7
+ end
8
+
9
+ @plugins = []
10
+
11
+ def self.plugins
12
+ @plugins
13
+ end
14
+
15
+ def self.plugin_classes
16
+ Dir["#{File.dirname(__FILE__)}/plugins/*.rb"].each { |f| require f }
17
+ classes = []
18
+ Object.constants.each do |klass|
19
+ const = Kernel.const_get(klass)
20
+ next unless const.respond_to?(:superclass) && const.superclass == Plugin
21
+ classes << const
22
+ end
23
+
24
+ classes
25
+ end
26
+
27
+ def self.init_plugins(driver, smali_files, methods)
28
+ @plugins = plugin_classes.collect { |p| p.new(driver, smali_files, methods) }
29
+ end
30
+
31
+ def process
32
+ fail 'process not implemented'
33
+ end
34
+
35
+ def optimizations
36
+ fail 'optimizations not implemented'
37
+ end
38
+
39
+ # method_to_target_to_context -> { method: [target_to_context] }
40
+ # target_to_context -> [ [target, context] ]
41
+ # target = Driver.make_target, has :id key
42
+ # context = [ [original, out_reg] ]
43
+ def self.apply_batch(driver, method_to_target_to_contexts, modifier)
44
+ all_batches = method_to_target_to_contexts.values.collect(&:keys).flatten
45
+ return false if all_batches.empty?
46
+
47
+ target_id_to_output = driver.run_batch(all_batches)
48
+ apply_outputs(target_id_to_output, method_to_target_to_contexts, modifier)
49
+ end
50
+
51
+ # target_id_to_output -> { id: [status, output] }
52
+ # status = (success|failure)
53
+ def self.apply_outputs(target_id_to_output, method_to_target_to_contexts, modifier)
54
+ made_changes = false
55
+ method_to_target_to_contexts.each do |method, target_to_contexts|
56
+ target_to_contexts.each do |target, contexts|
57
+ status, output = target_id_to_output[target[:id]]
58
+ unless status == 'success'
59
+ logger.warn("Unsuccessful status: #{status} for #{output}")
60
+ next
61
+ end
62
+
63
+ contexts.each do |original, out_reg|
64
+ modification = modifier.call(original, output, out_reg)
65
+ #puts "modification #{original.inspect} = #{modification.inspect}"
66
+
67
+ # Go home Ruby. You're drunk.
68
+ # (gsub actually _modifies_ the replacement string)
69
+ #modification.gsub!('\\') { '\\\\' }
70
+ #method.body.gsub!(original) { modification }
71
+
72
+ dumb_replace(method.body, original, modification)
73
+ end
74
+
75
+ made_changes = true
76
+ method.modified = true
77
+ end
78
+ end
79
+
80
+ made_changes
81
+ end
82
+
83
+ def self.dumb_replace(string, find, replace)
84
+ string[find] = replace while string.include?(find)
85
+ string
86
+ end
87
+ end
@@ -0,0 +1,59 @@
1
+ require_relative '../logging'
2
+ require_relative '../utility'
3
+
4
+ class StringDecryptor < Plugin
5
+ include Logging
6
+ include CommonRegex
7
+
8
+ STRING_DECRYPT = Regexp.new(
9
+ '^[ \t]*(' << CONST_STRING << '\s+' \
10
+ 'invoke-static \{[vp]\d+\}, L([^;]+);->([^\(]+\(Ljava/lang/String;\))Ljava/lang/String;' \
11
+ '\s+' << MOVE_RESULT_OBJECT << ')'
12
+ )
13
+
14
+ MODIFIER = -> (_, output, out_reg) { "const-string #{out_reg}, \"#{output.split('').collect { |e| e.inspect[1..-2] }.join}\"" }
15
+
16
+ def initialize(driver, smali_files, methods)
17
+ @driver = driver
18
+ @smali_files = smali_files
19
+ @methods = methods
20
+ @optimizations = Hash.new(0)
21
+ end
22
+
23
+ def process
24
+ method_to_target_to_contexts = {}
25
+ @methods.each do |method|
26
+ logger.info("Decrypting strings #{method.descriptor}")
27
+ target_to_contexts = {}
28
+ target_to_contexts.merge!(decrypt_strings(method))
29
+ target_to_contexts.map { |_, v| v.uniq! }
30
+ method_to_target_to_contexts[method] = target_to_contexts unless target_to_contexts.empty?
31
+ end
32
+
33
+ made_changes = false
34
+ made_changes |= Plugin.apply_batch(@driver, method_to_target_to_contexts, MODIFIER)
35
+
36
+ made_changes
37
+ end
38
+
39
+ def optimizations
40
+ @optimizations
41
+ end
42
+
43
+ private
44
+
45
+ def decrypt_strings(method)
46
+ target_to_contexts = {}
47
+ matches = method.body.scan(STRING_DECRYPT)
48
+ @optimizations[:string_decrypts] += matches.size if matches
49
+ matches.each do |original, encrypted, class_name, method_signature, out_reg|
50
+ target = @driver.make_target(
51
+ class_name, method_signature, encrypted
52
+ )
53
+ target_to_contexts[target] = [] unless target_to_contexts.key?(target)
54
+ target_to_contexts[target] << [original, out_reg]
55
+ end
56
+
57
+ target_to_contexts
58
+ end
59
+ end
@@ -0,0 +1,155 @@
1
+ require_relative '../logging'
2
+ require_relative '../utility'
3
+
4
+ class Undexguard < Plugin
5
+ include Logging
6
+ include CommonRegex
7
+
8
+ STRING_LOOKUP_3INT = Regexp.new(
9
+ '^[ \t]*(' << ((CONST_NUMBER << '\s+') * 3) <<
10
+ 'invoke-static \{[vp]\d+, [vp]\d+, [vp]\d+\}, L([^;]+);->([^\(]+\(III\))Ljava/lang/String;' \
11
+ '\s+' << MOVE_RESULT_OBJECT << ')',
12
+ Regexp::MULTILINE
13
+ )
14
+
15
+ STRING_LOOKUP_1INT = Regexp.new(
16
+ '^[ \t]*(' << CONST_NUMBER << '\s+' \
17
+ 'invoke-static \{[vp]\d+\}, L([^;]+);->([^\(]+\(I\))Ljava/lang/String;' \
18
+ '\s+' << MOVE_RESULT_OBJECT << ')'
19
+ )
20
+
21
+ BYTES_DECRYPT = Regexp.new(
22
+ '^[ \t]*(' << CONST_STRING << '\s+' \
23
+ 'invoke-virtual \{[vp]\d+\}, Ljava\/lang\/String;->getBytes\(\)\[B\s+' \
24
+ 'move-result-object [vp]\d+\s+' \
25
+ 'invoke-static \{[vp]\d+\}, L([^;]+);->([^\(]+\(\[B\))Ljava/lang/String;' \
26
+ '\s+' << MOVE_RESULT_OBJECT << ')'
27
+ )
28
+
29
+ MULTI_BYTES_DECRYPT = Regexp.new(
30
+ '^[ \t]*(' << CONST_STRING << '\s+' \
31
+ 'new-instance ([vp]\d+), L[^;]+;\s+' \
32
+ 'invoke-static \{[vp]\d+\}, L([^;]+);->([^\(]+\(Ljava/lang/String;\))\[B\s+' \
33
+ 'move-result-object [vp]\d+\s+' <<
34
+ CONST_STRING << '\s+' \
35
+ 'invoke-static \{[vp]\d+, [vp]\d+\}, L([^;]+);->([^\(]+\(\[BLjava/lang/String;\))\[B\s+' \
36
+ 'move-result-object [vp]\d+\s+' \
37
+ 'invoke-static \{[vp]\d+\}, L([^;]+);->([^\(]+\(\[B\))\[B\s+' \
38
+ 'move-result-object [vp]\d+\s+' \
39
+ 'invoke-direct \{[vp]\d+, [vp]\d+\}, Ljava\/lang\/String;-><init>\(\[B\)V' \
40
+ ')'
41
+ )
42
+
43
+ MODIFIER = -> (_, output, out_reg) { "const-string #{out_reg}, \"#{output.split('').collect { |e| e.inspect[1..-2] }.join}\"" }
44
+
45
+ def initialize(driver, smali_files, methods)
46
+ @driver = driver
47
+ @smali_files = smali_files
48
+ @methods = methods
49
+ @optimizations = Hash.new(0)
50
+ end
51
+
52
+ def process
53
+ method_to_target_to_contexts = {}
54
+ @methods.each do |method|
55
+ logger.info("Undexguarding #{method.descriptor} - stage 1/2")
56
+ target_to_contexts = {}
57
+ target_to_contexts.merge!(lookup_strings_3int(method))
58
+ target_to_contexts.merge!(lookup_strings_1int(method))
59
+ target_to_contexts.merge!(decrypt_bytes(method))
60
+ target_to_contexts.map { |_, v| v.uniq! }
61
+ method_to_target_to_contexts[method] = target_to_contexts unless target_to_contexts.empty?
62
+ end
63
+
64
+ made_changes = Plugin.apply_batch(@driver, method_to_target_to_contexts, MODIFIER)
65
+
66
+ @methods.each do |method|
67
+ logger.info("Undexguarding #{method.descriptor} - stage 2/2")
68
+ made_changes |= decrypt_multi_bytes(method)
69
+ end
70
+
71
+ made_changes
72
+ end
73
+
74
+ def optimizations
75
+ @optimizations
76
+ end
77
+
78
+ private
79
+
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
+ def lookup_strings_3int(method)
90
+ target_to_contexts = {}
91
+ matches = method.body.scan(STRING_LOOKUP_3INT)
92
+ @optimizations[:string_lookups] += matches.size if matches
93
+ matches.each do |original, arg1, arg2, arg3, class_name, method_signature, out_reg|
94
+ target = @driver.make_target(
95
+ class_name, method_signature, arg1.to_i(16), arg2.to_i(16), arg3.to_i(16)
96
+ )
97
+ target_to_contexts[target] = [] unless target_to_contexts.key?(target)
98
+ target_to_contexts[target] << [original, out_reg]
99
+ end
100
+
101
+ target_to_contexts
102
+ end
103
+
104
+ def lookup_strings_1int(method)
105
+ target_to_contexts = {}
106
+ matches = method.body.scan(STRING_LOOKUP_1INT)
107
+ @optimizations[:string_lookups] += matches.size if matches
108
+ matches.each do |original, arg1, class_name, method_signature, out_reg|
109
+ target = @driver.make_target(
110
+ class_name, method_signature, arg1.to_i(16)
111
+ )
112
+ target_to_contexts[target] = [] unless target_to_contexts.key?(target)
113
+ target_to_contexts[target] << [original, out_reg]
114
+ end
115
+
116
+ target_to_contexts
117
+ end
118
+
119
+ def decrypt_bytes(method)
120
+ target_to_contexts = {}
121
+ matches = method.body.scan(BYTES_DECRYPT)
122
+ @optimizations[:string_decrypts] += matches.size if matches
123
+ matches.each do |original, encrypted, class_name, method_signature, out_reg|
124
+ target = @driver.make_target(
125
+ class_name, method_signature, encrypted.bytes.to_a
126
+ )
127
+ target_to_contexts[target] = [] unless target_to_contexts.key?(target)
128
+ target_to_contexts[target] << [original, out_reg]
129
+ end
130
+
131
+ target_to_contexts
132
+ end
133
+
134
+ def decrypt_multi_bytes(method)
135
+ target_to_contexts = {}
136
+ target_id_to_output = {}
137
+ matches = method.body.scan(MULTI_BYTES_DECRYPT)
138
+ @optimizations[:string_decrypts] += matches.size if matches
139
+ matches.each do |original, iv_str, out_reg, iv_class_name, iv_method_signature, iv2_str, iv2_class_name, iv2_method_signature, dec_class_name, dec_method_signature|
140
+ iv_bytes = @driver.run(iv_class_name, iv_method_signature, iv_str)
141
+ enc_bytes = @driver.run(iv2_class_name, iv2_method_signature, iv_bytes, iv2_str)
142
+ dec_bytes = @driver.run(dec_class_name, dec_method_signature, enc_bytes)
143
+ dec_array = Undexguard.array_string_to_array(dec_bytes)
144
+ dec_string = dec_array.pack('U*')
145
+
146
+ target = { id: Digest::SHA256.hexdigest(original) }
147
+ target_id_to_output[target[:id]] = ['success', dec_string]
148
+ target_to_contexts[target] = [] unless target_to_contexts.key?(target)
149
+ target_to_contexts[target] << [original, out_reg]
150
+ end
151
+
152
+ method_to_target_to_contexts = { method => target_to_contexts }
153
+ Plugin.apply_outputs(target_id_to_output, method_to_target_to_contexts, MODIFIER)
154
+ end
155
+ end