class Driver
Constants
- DRIVER_CLASS
- OUTPUT_HEADER
- UNESCAPES
- UNESCAPE_REGEX
Public Class Methods
new(device_id, timeout = 60)
click to toggle source
# File lib/dex-oracle/driver.rb, line 23 def initialize(device_id, timeout = 60) @device_id = device_id @timeout = timeout device_str = device_id.empty? ? '' : "-s #{@device_id} " @adb_base = "adb #{device_str}%s" @driver_dir = get_driver_dir unless @driver_dir logger.error 'Unable to find writable driver directory. Make sure /data/local or /data/local/tmp exists and is writable.' exit -1 end logger.debug "Using #{@driver_dir} as driver directory ..." @cmd_stub = "export CLASSPATH=#{@driver_dir}/od.zip; app_process /system/bin #{DRIVER_CLASS}" @cache = {} end
Public Instance Methods
install(dex)
click to toggle source
# File lib/dex-oracle/driver.rb, line 40 def install(dex) has_java = Utility.which('java') raise 'Unable to find Java on the path.' unless has_java begin # Merge driver and target dex file # Congratulations. You're now one of the 5 people who've used this tool explicitly. raise "#{Resources.dx} does not exist and is required for DexMerger" unless File.exist?(Resources.dx) raise "#{Resources.driver_dex} does not exist" unless File.exist?(Resources.driver_dex) logger.debug("Merging input DEX (#{dex.path}) and driver DEX (#{Resources.driver_dex}) ...") tf = Tempfile.new(%w(oracle-driver .dex)) cmd = "java -cp #{Resources.dx} com.android.dx.merge.DexMerger #{tf.path} #{dex.path} #{Resources.driver_dex}" stdout, stderr = exec(cmd) # Ideally, it says something like this: # Merged dex #1 (36 defs/87.9KiB) # Merged dex #2 (1225 defs/1092.7KiB) # Result is 1261 defs/1375.0KiB. Took 0.2s # But it might say this: # Exception in thread "main" com.android.dex.DexIndexOverflowException: Cannot merge new index 65776 into a non-jumbo instruction! if stderr.start_with?('Exception in thread "main"') logger.error("Failure to merge input DEX and driver DEX:\n#{stderr}") if stderr.include?('DexIndexOverflowException') logger.error("Your input DEX inexplicably contains const-string and const-string/jumbo. This probably means someone fucked with it. In any case, it means DexMerge is failing because there are too many strings.\nTry this: baksmali the DEX, replace all const-string instructions with const-string/jumbo, then recompile with smali and use that DEX as input. Sorry, I don't want to do this for you. It's too complicated.") end exit -1 end tf.close # Zip merged dex and push to device logger.debug('Pushing merged driver to device ...') tz = Tempfile.new(%w(oracle-driver .zip)) # Could pass tz to create_zip, but Windows doesn't let you rename if file open # And zip internally renames files when creating them tempzip_path = tz.path tz.close Utility.create_zip(tempzip_path, 'classes.dex' => tf) adb("push #{tz.path} #{@driver_dir}/od.zip") rescue => e puts "Error installing driver: #{e}\n#{e.backtrace.join("\n\t")}" ensure tf.close if tf tf.unlink if tf tz.close if tz tz.unlink if tz end end
make_target(class_name, signature, *args)
click to toggle source
# File lib/dex-oracle/driver.rb, line 126 def make_target(class_name, signature, *args) method = SmaliMethod.new(class_name, signature) target = { className: method.class.tr('/', '.'), methodName: method.name, arguments: build_arguments(method.parameters, args) } # Identifiers are used to map individual inputs to outputs target[:id] = Digest::SHA256.hexdigest(target.to_json) target end
run(class_name, signature, *args)
click to toggle source
# File lib/dex-oracle/driver.rb, line 87 def run(class_name, signature, *args) method = SmaliMethod.new(class_name, signature) cmd = build_command(method.class, method.name, method.parameters, args) output = nil retries = 1 begin output = drive(cmd) rescue => e # If you slam an emulator or device with too many app_process commands, # it eventually gets angry and segmentation faults. No idea why. # This took many frustrating hours to figure out. raise e if retries > 3 logger.debug("Driver execution failed. Taking a quick nap and retrying, Zzzzz ##{retries} / 3 ...") sleep 5 retries += 1 retry end output end
run_batch(batch)
click to toggle source
# File lib/dex-oracle/driver.rb, line 109 def run_batch(batch) push_batch_targets(batch) retries = 1 begin drive("#{@cmd_stub} @#{@driver_dir}/od-targets.json", true) rescue => e raise e if retries > 3 || !e.message.include?('Segmentation fault') # Maybe we just need to retry logger.debug("Driver execution segfaulted. Taking a quick nap and retrying, Zzzzz ##{retries} / 3 ...") sleep 5 retries += 1 retry end pull_batch_outputs end
Private Instance Methods
adb(cmd)
click to toggle source
# File lib/dex-oracle/driver.rb, line 241 def adb(cmd) adb_with_stderr(cmd)[0] end
adb_with_stderr(cmd)
click to toggle source
# File lib/dex-oracle/driver.rb, line 245 def adb_with_stderr(cmd) full_cmd = @adb_base % cmd stdout, stderr = exec(full_cmd, false) [stdout.rstrip, stderr.rstrip] end
build_argument(parameter, argument)
click to toggle source
# File lib/dex-oracle/driver.rb, line 282 def build_argument(parameter, argument) if parameter[0] == 'L' java_type = parameter[1..-2].tr('/', '.') if java_type == 'java.lang.String' # Need to unescape smali string to get the actual string # Converting to bytes just avoids any weird non-printable characters nonsense argument = "[#{unescape(argument).bytes.to_a.join(',')}]" end "#{java_type}:#{argument}" else argument = (argument == '1' ? 'true' : 'false') if parameter == 'Z' "#{parameter}:#{argument}" end end
build_arguments(parameters, args)
click to toggle source
# File lib/dex-oracle/driver.rb, line 278 def build_arguments(parameters, args) parameters.map.with_index { |o, i| build_argument(o, args[i]) } end
build_command(class_name, method_name, parameters, args)
click to toggle source
# File lib/dex-oracle/driver.rb, line 251 def build_command(class_name, method_name, parameters, args) class_name.tr!('/', '.') # Make valid Java class name class_name.gsub!('$', '\$') # inner classes method_name.gsub!('$', '\$') # synthetic method names target = "'#{class_name}' '#{method_name}'" target_args = build_arguments(parameters, args) "#{@cmd_stub} #{target} #{target_args * ' '}" end
drive(cmd, batch = false)
click to toggle source
# File lib/dex-oracle/driver.rb, line 217 def drive(cmd, batch = false) return @cache[cmd] if @cache.key?(cmd) full_cmd = "shell \"#{cmd}\"; echo $?" full_output = adb(full_cmd) output = validate_output(full_cmd, full_output) # The driver writes any actual exceptions to the filesystem # Need to check to make sure the output value is legitimate logger.debug('Checking if execution had any exceptions ...') exception = adb("shell cat #{@driver_dir}/od-exception.txt").strip unless exception.end_with?('No such file or directory') adb("shell rm #{@driver_dir}/od-exception.txt") raise exception end logger.debug('No exceptions found :)') # Cache successful results for single method invocations for speed! @cache[cmd] = output unless batch logger.debug("output = #{output}") output end
exec(cmd, silent = true)
click to toggle source
# File lib/dex-oracle/driver.rb, line 177 def exec(cmd, silent = true) logger.debug("exec: #{cmd}") retries = 1 begin status = Timeout.timeout(@timeout) do if silent Open3.popen3(cmd) { |_, stdout, stderr, _| [stdout.read, stderr.read] } else [`#{cmd}`, ''] end end rescue => e raise e if retries > 3 logger.debug("ADB command execution timed out, retrying #{retries} ...") sleep 5 retries += 1 retry end end
get_driver_dir()
click to toggle source
# File lib/dex-oracle/driver.rb, line 151 def get_driver_dir # On some older devices, /data/local is world writable # But on other devices, it's /data/local/tmp %w(/data/local /data/local/tmp).each do |dir| stdout = adb("shell -x ls #{dir}") next if stdout == "ls: #{dir}: Permission denied" next if stdout == "ls: #{dir}: No such file or directory" return dir end nil end
pull_batch_outputs()
click to toggle source
# File lib/dex-oracle/driver.rb, line 164 def pull_batch_outputs output_file = Tempfile.new(['oracle-output', '.json']) logger.debug('Pulling batch results from device ...') adb("pull #{@driver_dir}/od-output.json #{output_file.path}") adb("shell rm #{@driver_dir}/od-output.json") outputs = JSON.parse(File.read(output_file.path)) outputs.each { |_, (_, v2)| v2.gsub!(/(?:^"|"$)/, '') if v2.start_with?('"') } logger.debug("Pulled #{outputs.size} outputs.") output_file.close output_file.unlink outputs end
push_batch_targets(batch)
click to toggle source
# File lib/dex-oracle/driver.rb, line 141 def push_batch_targets(batch) target_file = Tempfile.new(%w(oracle-targets .json)) target_file << batch.to_json target_file.flush logger.info("Pushing #{batch.size} method targets to device ...") adb("push #{target_file.path} #{@driver_dir}/od-targets.json") target_file.close target_file.unlink end
unescape(str)
click to toggle source
# File lib/dex-oracle/driver.rb, line 262 def unescape(str) str.gsub(UNESCAPE_REGEX) do if Regexp.last_match[1] if Regexp.last_match[1] == '\\' Regexp.last_match[1] else UNESCAPES[Regexp.last_match[1]] end elsif Regexp.last_match[2] # escape \u0000 unicode [Regexp.last_match[2].hex].pack('U*') elsif Regexp.last_match[3] # escape \0xff or \xff [Regexp.last_match[3]].pack('H2') end end end
validate_output(full_cmd, full_output)
click to toggle source
# File lib/dex-oracle/driver.rb, line 199 def validate_output(full_cmd, full_output) output_lines = full_output.split(/\r?\n/) exit_code = output_lines.last.to_i if exit_code != 0 # Non zero exit code would only imply adb command itself was flawed # app_process, dalvikvm, etc. don't propigate exit codes back raise "Command failed with #{exit_code}: #{full_cmd}\nOutput: #{full_output}" end # Successful driver run should include driver header # Otherwise it may be a Segmentation fault or Killed logger.debug("Full output: #{full_output.inspect}") header = output_lines[0] raise "app_process execution failure, output: '#{full_output}'" if header != OUTPUT_HEADER output_lines[1..-2].join("\n").rstrip end