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,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,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
|
data/lib/oracle.rb
ADDED
@@ -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
|
data/res/driver.dex
ADDED
Binary file
|
data/res/dx.jar
ADDED
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
|