/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dexfuzz.fuzzers; import dexfuzz.Log; import dexfuzz.Options; import dexfuzz.Timer; import dexfuzz.executors.Architecture; import dexfuzz.executors.Arm64InterpreterExecutor; import dexfuzz.executors.Arm64OptimizingBackendExecutor; import dexfuzz.executors.ArmInterpreterExecutor; import dexfuzz.executors.ArmOptimizingBackendExecutor; import dexfuzz.executors.Device; import dexfuzz.executors.Executor; import dexfuzz.executors.X86InterpreterExecutor; import dexfuzz.executors.X86OptimizingBackendExecutor; import dexfuzz.executors.X86_64InterpreterExecutor; import dexfuzz.executors.X86_64OptimizingBackendExecutor; import dexfuzz.listeners.BaseListener; import dexfuzz.program.Mutation; import dexfuzz.program.Program; import dexfuzz.rawdex.DexRandomAccessFile; import dexfuzz.rawdex.OffsetTracker; import dexfuzz.rawdex.RawDexFile; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A particular fuzzing strategy, this class provides the common methods * most fuzzing will involve, and subclasses override the run() method, to * employ a particular strategy. */ public abstract class Fuzzer { private List executors; private OffsetTracker offsetTracker; /** * This is the executor that we use to test for self-divergent programs. */ private Executor goldenExecutor; /* * These two flags are set during fuzz(), and then cleared at the end of execute(). */ private boolean mutatedSuccessfully; private boolean savedSuccessfully; private Timer totalTimer = new Timer("Total Time"); private Timer timerDexInput = new Timer("DEX Input"); private Timer timerProgGen = new Timer("Program Generation"); private Timer timerMutation = new Timer("Mutation Time"); private Timer timerDexOutput = new Timer("DEX Output"); private Timer timerChecksumCalc = new Timer("Checksum Calculation"); protected BaseListener listener; protected Fuzzer(BaseListener listener) { totalTimer.start(); executors = new ArrayList(); this.listener = listener; } public abstract void run(); protected abstract String getNextInputFilename(); protected abstract String getNextOutputFilename(); /** * Call this after fuzzer execution to print out timing results. */ public void printTimingInfo() { totalTimer.stop(); timerDexInput.printTime(listener); timerProgGen.printTime(listener); timerMutation.printTime(listener); timerDexOutput.printTime(listener); timerChecksumCalc.printTime(listener); totalTimer.printTime(listener); } /** * Make sure this is called to correctly shutdown each Executor's StreamConsumers. */ public void shutdown() { if (executors != null) { for (Executor executor : executors) { executor.shutdown(); } } } private void addExecutorsForArchitecture(Device device, Class optimizing, Class interpreter) { // NB: Currently OptimizingBackend MUST come immediately before same arch's Interpreter. // This is because intepreter execution relies on there being an OAT file already // created to produce correct debug information. Otherwise we will see // false-positive divergences. try { if (Options.useOptimizing) { Constructor constructor = optimizing.getConstructor(BaseListener.class, Device.class); executors.add(constructor.newInstance(listener, device)); } if (Options.useInterpreter) { Constructor constructor = interpreter.getConstructor(BaseListener.class, Device.class); executors.add(constructor.newInstance(listener, device)); } } catch (NoSuchMethodException e) { Log.errorAndQuit("Executor doesn't have correct constructor."); } catch (InstantiationException e) { Log.errorAndQuit("Executor couldn't be instantiated."); } catch (IllegalAccessException e) { Log.errorAndQuit("Executor couldn't be accessed."); } catch (IllegalArgumentException e) { Log.errorAndQuit("Invalid arguments to instantiation of Executor."); } catch (InvocationTargetException e) { Log.errorAndQuit("Instantiation of Executor threw an Exception!"); } } protected void addExecutors() { Device device = null; if (Options.executeOnHost) { device = new Device(); } else { device = new Device(Options.deviceName, Options.noBootImage); } if (Options.useArchArm64) { addExecutorsForArchitecture(device, Arm64OptimizingBackendExecutor.class, Arm64InterpreterExecutor.class); } if (Options.useArchArm) { addExecutorsForArchitecture(device, ArmOptimizingBackendExecutor.class, ArmInterpreterExecutor.class); } if (Options.useArchX86_64) { addExecutorsForArchitecture(device, X86_64OptimizingBackendExecutor.class, X86_64InterpreterExecutor.class); } if (Options.useArchX86) { addExecutorsForArchitecture(device, X86OptimizingBackendExecutor.class, X86InterpreterExecutor.class); } // Add the first backend as the golden executor for self-divergence tests. goldenExecutor = executors.get(0); } /** * Called from each Fuzzer subclass that we can instantiate. Parses the program, fuzzes it, * and then saves it, if mutation was successful. We can use --skip-mutation to bypass * the mutation phase, if we wanted to verify that a test program itself works. */ protected Program fuzz() { String inputFile = getNextInputFilename(); Program program = loadProgram(inputFile, null); if (program == null) { Log.errorAndQuit("Problem loading seed file."); } // Mutate the program. if (!Options.skipMutation) { timerMutation.start(); program.mutateTheProgram(); mutatedSuccessfully = program.updateRawDexFile(); timerMutation.stop(); if (!mutatedSuccessfully) { listener.handleMutationFail(); } } else { Log.info("Skipping mutation stage as requested."); mutatedSuccessfully = true; } if (mutatedSuccessfully) { savedSuccessfully = saveProgram(program, getNextOutputFilename()); } return program; } protected boolean safeToExecute() { return mutatedSuccessfully && savedSuccessfully; } protected void execute(Program program) { if (!safeToExecute()) { Log.errorAndQuit("Your Fuzzer subclass called execute() " + "without checking safeToExecute()!"); } String programName = getNextOutputFilename(); boolean verified = true; if (!Options.skipHostVerify && !Options.executeOnHost) { verified = goldenExecutor.verifyOnHost(programName); if (verified) { listener.handleSuccessfulHostVerification(); } } if (verified) { boolean skipAnalysis = false; for (Executor executor : executors) { executor.reset(); executor.prepareProgramForExecution(programName); executor.execute(programName); if (!executor.didTargetVerify()) { listener.handleFailedTargetVerification(); skipAnalysis = true; break; } // Results are saved in the executors until they reset, usually at the // next iteration. } if (!skipAnalysis) { listener.handleSuccessfullyFuzzedFile(programName); analyseResults(program, programName); } } goldenExecutor.finishedWithProgramOnDevice(); mutatedSuccessfully = false; savedSuccessfully = false; } /** * Checks if the different outputs we observed align with different architectures. */ private boolean checkForArchitectureSplit(Map> outputMap) { if (outputMap.size() != 2) { // Cannot have a two-way split if we don't have 2 kinds of output. return false; } Architecture[] architectures = new Architecture[2]; int archIdx = 0; // For each kind of output we saw, make sure they all // came from the same architecture. for (List executorList : outputMap.values()) { architectures[archIdx] = executorList.get(0).getArchitecture(); for (int execIdx = 1; execIdx < executorList.size(); execIdx++) { if (executorList.get(execIdx).getArchitecture() != architectures[archIdx]) { // Not every executor with this output shared the same architecture. return false; } } archIdx++; } // Now make sure that the two outputs we saw were different architectures. if (architectures[0] == architectures[1]) { return false; } return true; } private boolean checkGoldenExecutorForSelfDivergence(String programName) { // Run golden executor multiple times, make sure it always produces // the same output, otherwise report that it is self-divergent. // TODO: Instead, produce a list of acceptable outputs, and see if the divergent // outputs of the backends fall within this set of outputs. String seenOutput = null; for (int i = 0; i < Options.divergenceRetry + 1; i++) { goldenExecutor.reset(); goldenExecutor.execute(programName); String output = goldenExecutor.getResult().getFlattenedOutput(); if (seenOutput == null) { seenOutput = output; } else if (!seenOutput.equals(output)) { return true; } } return false; } private void analyseResults(Program program, String programName) { // Check timeouts. // Construct two lists of executors, those who timed out, and those who did not. // Report if we had some timeouts. List timedOut = new ArrayList(); List didNotTimeOut = new ArrayList(); for (Executor executor : executors) { if (executor.getResult().isTimeout()) { timedOut.add(executor); } else { didNotTimeOut.add(executor); } } if (!timedOut.isEmpty()) { listener.handleTimeouts(timedOut, didNotTimeOut); // Do not bother reporting divergence information. return; } // Check divergences. // Construct a map {output1: [executor that produced output1, ...], output2: [...]} // If the map has more than one output, we had divergence, report it. Map> outputMap = new HashMap>(); for (Executor executor : executors) { String output = executor.getResult().getFlattenedOutput(); if (Options.dumpOutput) { listener.handleDumpOutput( executor.getResult().getFlattenedOutputWithNewlines(), executor); } if (outputMap.containsKey(output)) { outputMap.get(output).add(executor); } else { List newList = new ArrayList(); newList.add(executor); outputMap.put(output, newList); } } if (outputMap.size() > 1) { // Report that we had divergence. listener.handleDivergences(outputMap); listener.handleMutations(program.getMutations()); // If we found divergences, try running the "golden executor" // a few times in succession, to see if the output it produces is different // from run to run. If so, then we're probably executing something with either: // a) randomness // b) timing-dependent code // c) threads // So we will not consider it a "true" divergence, but still useful? if (checkGoldenExecutorForSelfDivergence(programName)) { listener.handleSelfDivergence(); return; } // If we found divergences, try checking if the differences // in outputs align with differences in architectures. // For example, if we have: {Output1: [ARM, ARM], Output2: [ARM64, ARM64]} if (checkForArchitectureSplit(outputMap)) { listener.handleArchitectureSplit(); } } else { // No problems with execution. listener.handleSuccess(outputMap); } } private Program loadProgram(String inputName, List mutations) { Program program = null; try { DexRandomAccessFile input = new DexRandomAccessFile(inputName, "r"); offsetTracker = new OffsetTracker(); input.setOffsetTracker(offsetTracker); // Read the raw DexFile RawDexFile rawDexFile = new RawDexFile(); timerDexInput.start(); rawDexFile.read(input); timerDexInput.stop(); input.close(); // Create the program view. timerProgGen.start(); program = new Program(rawDexFile, mutations, listener); timerProgGen.stop(); } catch (FileNotFoundException e) { Log.errorAndQuit("Couldn't open a file called " + inputName); } catch (IOException e) { Log.errorAndQuit("IOException when trying to load a DEX test file!"); } return program; } private boolean saveProgram(Program program, String outputName) { boolean success = false; try { // Write out the results of mutation. DexRandomAccessFile output = new DexRandomAccessFile(outputName, "rw"); output.setOffsetTracker(offsetTracker); // Delete the contents of the file, in case it already existed. output.setLength(0); // Write out the file. timerDexOutput.start(); program.writeRawDexFile(output); timerDexOutput.stop(); // Recalculate the header info. timerChecksumCalc.start(); program.updateRawDexFileHeader(output); timerChecksumCalc.stop(); output.close(); success = true; } catch (FileNotFoundException e) { Log.errorAndQuit("Couldn't open a file called " + outputName); } catch (IOException e) { Log.errorAndQuit("IOException when trying to save a DEX test file!"); } return success; } }