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.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +56 -0
- data/LICENSE.txt +21 -0
- data/README.md +102 -0
- data/bin/dex-oracle +98 -0
- data/dex-oracle.gemspec +43 -0
- data/driver/build.gradle +52 -0
- data/driver/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/driver/gradle/wrapper/gradle-wrapper.properties +6 -0
- data/driver/gradlew +160 -0
- data/driver/gradlew.bat +90 -0
- data/driver/src/main/java/org/cf/oracle/Driver.java +134 -0
- data/driver/src/main/java/org/cf/oracle/FileUtils.java +35 -0
- data/driver/src/main/java/org/cf/oracle/StackSpoofer.java +42 -0
- data/driver/src/main/java/org/cf/oracle/options/InvocationTarget.java +40 -0
- data/driver/src/main/java/org/cf/oracle/options/TargetParser.java +121 -0
- data/lib/dex-oracle/driver.rb +255 -0
- data/lib/dex-oracle/logging.rb +32 -0
- data/lib/dex-oracle/plugin.rb +87 -0
- data/lib/dex-oracle/plugins/string_decryptor.rb +59 -0
- data/lib/dex-oracle/plugins/undexguard.rb +155 -0
- data/lib/dex-oracle/plugins/unreflector.rb +85 -0
- data/lib/dex-oracle/resources.rb +13 -0
- data/lib/dex-oracle/smali_field.rb +21 -0
- data/lib/dex-oracle/smali_file.rb +64 -0
- data/lib/dex-oracle/smali_input.rb +81 -0
- data/lib/dex-oracle/smali_method.rb +33 -0
- data/lib/dex-oracle/utility.rb +37 -0
- data/lib/dex-oracle/version.rb +3 -0
- data/lib/oracle.rb +61 -0
- data/res/driver.dex +0 -0
- data/res/dx.jar +0 -0
- data/spec/data/helloworld.apk +0 -0
- data/spec/data/helloworld.dex +0 -0
- data/spec/data/plugins/bytes_decrypt.smali +18 -0
- data/spec/data/plugins/class_forname.smali +14 -0
- data/spec/data/plugins/multi_bytes_decrypt.smali +28 -0
- data/spec/data/plugins/string_decrypt.smali +14 -0
- data/spec/data/plugins/string_lookup_1int.smali +14 -0
- data/spec/data/plugins/string_lookup_3int.smali +18 -0
- data/spec/data/smali/helloworld.smali +17 -0
- data/spec/dex-oracle/driver_spec.rb +82 -0
- data/spec/dex-oracle/plugins/string_decryptor_spec.rb +25 -0
- data/spec/dex-oracle/plugins/undexguard_spec.rb +69 -0
- data/spec/dex-oracle/plugins/unreflector_spec.rb +29 -0
- data/spec/dex-oracle/smali_field_spec.rb +15 -0
- data/spec/dex-oracle/smali_file_spec.rb +41 -0
- data/spec/dex-oracle/smali_input_spec.rb +90 -0
- data/spec/dex-oracle/smali_method_spec.rb +19 -0
- data/spec/spec_helper.rb +9 -0
- data/update_driver +5 -0
- 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
|