1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package dexfuzz.fuzzers; 18 19 import dexfuzz.Log; 20 import dexfuzz.Options; 21 import dexfuzz.Timer; 22 import dexfuzz.executors.Architecture; 23 import dexfuzz.executors.Arm64InterpreterExecutor; 24 import dexfuzz.executors.Arm64OptimizingBackendExecutor; 25 import dexfuzz.executors.ArmInterpreterExecutor; 26 import dexfuzz.executors.ArmOptimizingBackendExecutor; 27 import dexfuzz.executors.Device; 28 import dexfuzz.executors.Executor; 29 import dexfuzz.executors.X86InterpreterExecutor; 30 import dexfuzz.executors.X86OptimizingBackendExecutor; 31 import dexfuzz.executors.X86_64InterpreterExecutor; 32 import dexfuzz.executors.X86_64OptimizingBackendExecutor; 33 import dexfuzz.listeners.BaseListener; 34 import dexfuzz.program.Mutation; 35 import dexfuzz.program.Program; 36 import dexfuzz.rawdex.DexRandomAccessFile; 37 import dexfuzz.rawdex.OffsetTracker; 38 import dexfuzz.rawdex.RawDexFile; 39 40 import java.io.FileNotFoundException; 41 import java.io.IOException; 42 import java.lang.reflect.Constructor; 43 import java.lang.reflect.InvocationTargetException; 44 import java.util.ArrayList; 45 import java.util.HashMap; 46 import java.util.List; 47 import java.util.Map; 48 49 /** 50 * A particular fuzzing strategy, this class provides the common methods 51 * most fuzzing will involve, and subclasses override the run() method, to 52 * employ a particular strategy. 53 */ 54 public abstract class Fuzzer { 55 private List<Executor> executors; 56 private OffsetTracker offsetTracker; 57 58 /** 59 * This is the executor that we use to test for self-divergent programs. 60 */ 61 private Executor goldenExecutor; 62 63 /* 64 * These two flags are set during fuzz(), and then cleared at the end of execute(). 65 */ 66 private boolean mutatedSuccessfully; 67 private boolean savedSuccessfully; 68 69 private Timer totalTimer = new Timer("Total Time"); 70 private Timer timerDexInput = new Timer("DEX Input"); 71 private Timer timerProgGen = new Timer("Program Generation"); 72 private Timer timerMutation = new Timer("Mutation Time"); 73 private Timer timerDexOutput = new Timer("DEX Output"); 74 private Timer timerChecksumCalc = new Timer("Checksum Calculation"); 75 76 protected BaseListener listener; 77 Fuzzer(BaseListener listener)78 protected Fuzzer(BaseListener listener) { 79 totalTimer.start(); 80 executors = new ArrayList<Executor>(); 81 this.listener = listener; 82 } 83 run()84 public abstract void run(); 85 getNextInputFilename()86 protected abstract String getNextInputFilename(); 87 getNextOutputFilename()88 protected abstract String getNextOutputFilename(); 89 90 /** 91 * Call this after fuzzer execution to print out timing results. 92 */ printTimingInfo()93 public void printTimingInfo() { 94 totalTimer.stop(); 95 timerDexInput.printTime(listener); 96 timerProgGen.printTime(listener); 97 timerMutation.printTime(listener); 98 timerDexOutput.printTime(listener); 99 timerChecksumCalc.printTime(listener); 100 totalTimer.printTime(listener); 101 } 102 103 /** 104 * Make sure this is called to correctly shutdown each Executor's StreamConsumers. 105 */ shutdown()106 public void shutdown() { 107 if (executors != null) { 108 for (Executor executor : executors) { 109 executor.shutdown(); 110 } 111 } 112 } 113 addExecutorsForArchitecture(Device device, Class<? extends Executor> optimizing, Class<? extends Executor> interpreter)114 private void addExecutorsForArchitecture(Device device, Class<? extends Executor> optimizing, 115 Class<? extends Executor> interpreter) { 116 // NB: Currently OptimizingBackend MUST come immediately before same arch's Interpreter. 117 // This is because intepreter execution relies on there being an OAT file already 118 // created to produce correct debug information. Otherwise we will see 119 // false-positive divergences. 120 try { 121 if (Options.useOptimizing) { 122 Constructor<? extends Executor> constructor = 123 optimizing.getConstructor(BaseListener.class, Device.class); 124 executors.add(constructor.newInstance(listener, device)); 125 } 126 if (Options.useInterpreter) { 127 Constructor<? extends Executor> constructor = 128 interpreter.getConstructor(BaseListener.class, Device.class); 129 executors.add(constructor.newInstance(listener, device)); 130 } 131 } catch (NoSuchMethodException e) { 132 Log.errorAndQuit("Executor doesn't have correct constructor."); 133 } catch (InstantiationException e) { 134 Log.errorAndQuit("Executor couldn't be instantiated."); 135 } catch (IllegalAccessException e) { 136 Log.errorAndQuit("Executor couldn't be accessed."); 137 } catch (IllegalArgumentException e) { 138 Log.errorAndQuit("Invalid arguments to instantiation of Executor."); 139 } catch (InvocationTargetException e) { 140 Log.errorAndQuit("Instantiation of Executor threw an Exception!"); 141 } 142 } 143 addExecutors()144 protected void addExecutors() { 145 Device device = null; 146 if (Options.executeOnHost) { 147 device = new Device(); 148 } else { 149 device = new Device(Options.deviceName, Options.noBootImage); 150 } 151 152 if (Options.useArchArm64) { 153 addExecutorsForArchitecture(device, Arm64OptimizingBackendExecutor.class, 154 Arm64InterpreterExecutor.class); 155 } 156 157 if (Options.useArchArm) { 158 addExecutorsForArchitecture(device, ArmOptimizingBackendExecutor.class, 159 ArmInterpreterExecutor.class); 160 } 161 162 if (Options.useArchX86_64) { 163 addExecutorsForArchitecture(device, X86_64OptimizingBackendExecutor.class, 164 X86_64InterpreterExecutor.class); 165 } 166 167 if (Options.useArchX86) { 168 addExecutorsForArchitecture(device, X86OptimizingBackendExecutor.class, 169 X86InterpreterExecutor.class); 170 } 171 172 // Add the first backend as the golden executor for self-divergence tests. 173 goldenExecutor = executors.get(0); 174 } 175 176 /** 177 * Called from each Fuzzer subclass that we can instantiate. Parses the program, fuzzes it, 178 * and then saves it, if mutation was successful. We can use --skip-mutation to bypass 179 * the mutation phase, if we wanted to verify that a test program itself works. 180 */ fuzz()181 protected Program fuzz() { 182 String inputFile = getNextInputFilename(); 183 Program program = loadProgram(inputFile, null); 184 if (program == null) { 185 Log.errorAndQuit("Problem loading seed file."); 186 } 187 // Mutate the program. 188 if (!Options.skipMutation) { 189 timerMutation.start(); 190 program.mutateTheProgram(); 191 192 mutatedSuccessfully = program.updateRawDexFile(); 193 timerMutation.stop(); 194 if (!mutatedSuccessfully) { 195 listener.handleMutationFail(); 196 } 197 } else { 198 Log.info("Skipping mutation stage as requested."); 199 mutatedSuccessfully = true; 200 } 201 if (mutatedSuccessfully) { 202 savedSuccessfully = saveProgram(program, getNextOutputFilename()); 203 } 204 return program; 205 } 206 safeToExecute()207 protected boolean safeToExecute() { 208 return mutatedSuccessfully && savedSuccessfully; 209 } 210 execute(Program program)211 protected void execute(Program program) { 212 if (!safeToExecute()) { 213 Log.errorAndQuit("Your Fuzzer subclass called execute() " 214 + "without checking safeToExecute()!"); 215 } 216 217 String programName = getNextOutputFilename(); 218 boolean verified = true; 219 220 if (!Options.skipHostVerify && !Options.executeOnHost) { 221 verified = goldenExecutor.verifyOnHost(programName); 222 if (verified) { 223 listener.handleSuccessfulHostVerification(); 224 } 225 } 226 227 if (verified) { 228 boolean skipAnalysis = false; 229 230 for (Executor executor : executors) { 231 executor.reset(); 232 executor.prepareProgramForExecution(programName); 233 executor.execute(programName); 234 if (!executor.didTargetVerify()) { 235 listener.handleFailedTargetVerification(); 236 skipAnalysis = true; 237 break; 238 } 239 // Results are saved in the executors until they reset, usually at the 240 // next iteration. 241 } 242 243 if (!skipAnalysis) { 244 listener.handleSuccessfullyFuzzedFile(programName); 245 analyseResults(program, programName); 246 } 247 } 248 249 goldenExecutor.finishedWithProgramOnDevice(); 250 mutatedSuccessfully = false; 251 savedSuccessfully = false; 252 } 253 254 /** 255 * Checks if the different outputs we observed align with different architectures. 256 */ checkForArchitectureSplit(Map<String, List<Executor>> outputMap)257 private boolean checkForArchitectureSplit(Map<String, List<Executor>> outputMap) { 258 if (outputMap.size() != 2) { 259 // Cannot have a two-way split if we don't have 2 kinds of output. 260 return false; 261 } 262 263 Architecture[] architectures = new Architecture[2]; 264 int archIdx = 0; 265 266 // For each kind of output we saw, make sure they all 267 // came from the same architecture. 268 for (List<Executor> executorList : outputMap.values()) { 269 architectures[archIdx] = executorList.get(0).getArchitecture(); 270 for (int execIdx = 1; execIdx < executorList.size(); execIdx++) { 271 if (executorList.get(execIdx).getArchitecture() != architectures[archIdx]) { 272 // Not every executor with this output shared the same architecture. 273 return false; 274 } 275 } 276 archIdx++; 277 } 278 279 // Now make sure that the two outputs we saw were different architectures. 280 if (architectures[0] == architectures[1]) { 281 return false; 282 } 283 return true; 284 } 285 checkGoldenExecutorForSelfDivergence(String programName)286 private boolean checkGoldenExecutorForSelfDivergence(String programName) { 287 // Run golden executor multiple times, make sure it always produces 288 // the same output, otherwise report that it is self-divergent. 289 290 // TODO: Instead, produce a list of acceptable outputs, and see if the divergent 291 // outputs of the backends fall within this set of outputs. 292 String seenOutput = null; 293 for (int i = 0; i < Options.divergenceRetry + 1; i++) { 294 goldenExecutor.reset(); 295 goldenExecutor.execute(programName); 296 String output = goldenExecutor.getResult().getFlattenedOutput(); 297 if (seenOutput == null) { 298 seenOutput = output; 299 } else if (!seenOutput.equals(output)) { 300 return true; 301 } 302 } 303 return false; 304 } 305 analyseResults(Program program, String programName)306 private void analyseResults(Program program, String programName) { 307 // Check timeouts. 308 // Construct two lists of executors, those who timed out, and those who did not. 309 // Report if we had some timeouts. 310 List<Executor> timedOut = new ArrayList<Executor>(); 311 List<Executor> didNotTimeOut = new ArrayList<Executor>(); 312 for (Executor executor : executors) { 313 if (executor.getResult().isTimeout()) { 314 timedOut.add(executor); 315 } else { 316 didNotTimeOut.add(executor); 317 } 318 } 319 if (!timedOut.isEmpty()) { 320 listener.handleTimeouts(timedOut, didNotTimeOut); 321 // Do not bother reporting divergence information. 322 return; 323 } 324 325 // Check divergences. 326 // Construct a map {output1: [executor that produced output1, ...], output2: [...]} 327 // If the map has more than one output, we had divergence, report it. 328 Map<String, List<Executor>> outputMap = new HashMap<String, List<Executor>>(); 329 for (Executor executor : executors) { 330 String output = executor.getResult().getFlattenedOutput(); 331 if (Options.dumpOutput) { 332 listener.handleDumpOutput( 333 executor.getResult().getFlattenedOutputWithNewlines(), executor); 334 } 335 if (outputMap.containsKey(output)) { 336 outputMap.get(output).add(executor); 337 } else { 338 List<Executor> newList = new ArrayList<Executor>(); 339 newList.add(executor); 340 outputMap.put(output, newList); 341 } 342 } 343 344 if (outputMap.size() > 1) { 345 // Report that we had divergence. 346 listener.handleDivergences(outputMap); 347 listener.handleMutations(program.getMutations()); 348 // If we found divergences, try running the "golden executor" 349 // a few times in succession, to see if the output it produces is different 350 // from run to run. If so, then we're probably executing something with either: 351 // a) randomness 352 // b) timing-dependent code 353 // c) threads 354 // So we will not consider it a "true" divergence, but still useful? 355 if (checkGoldenExecutorForSelfDivergence(programName)) { 356 listener.handleSelfDivergence(); 357 return; 358 } 359 // If we found divergences, try checking if the differences 360 // in outputs align with differences in architectures. 361 // For example, if we have: {Output1: [ARM, ARM], Output2: [ARM64, ARM64]} 362 if (checkForArchitectureSplit(outputMap)) { 363 listener.handleArchitectureSplit(); 364 } 365 } else { 366 // No problems with execution. 367 listener.handleSuccess(outputMap); 368 } 369 } 370 loadProgram(String inputName, List<Mutation> mutations)371 private Program loadProgram(String inputName, List<Mutation> mutations) { 372 Program program = null; 373 try { 374 DexRandomAccessFile input = new DexRandomAccessFile(inputName, "r"); 375 offsetTracker = new OffsetTracker(); 376 input.setOffsetTracker(offsetTracker); 377 // Read the raw DexFile 378 RawDexFile rawDexFile = new RawDexFile(); 379 timerDexInput.start(); 380 rawDexFile.read(input); 381 timerDexInput.stop(); 382 input.close(); 383 // Create the program view. 384 timerProgGen.start(); 385 program = new Program(rawDexFile, mutations, listener); 386 timerProgGen.stop(); 387 } catch (FileNotFoundException e) { 388 Log.errorAndQuit("Couldn't open a file called " + inputName); 389 } catch (IOException e) { 390 Log.errorAndQuit("IOException when trying to load a DEX test file!"); 391 } 392 return program; 393 } 394 saveProgram(Program program, String outputName)395 private boolean saveProgram(Program program, String outputName) { 396 boolean success = false; 397 398 try { 399 // Write out the results of mutation. 400 DexRandomAccessFile output = new DexRandomAccessFile(outputName, "rw"); 401 output.setOffsetTracker(offsetTracker); 402 // Delete the contents of the file, in case it already existed. 403 output.setLength(0); 404 // Write out the file. 405 timerDexOutput.start(); 406 program.writeRawDexFile(output); 407 timerDexOutput.stop(); 408 // Recalculate the header info. 409 timerChecksumCalc.start(); 410 program.updateRawDexFileHeader(output); 411 timerChecksumCalc.stop(); 412 output.close(); 413 success = true; 414 } catch (FileNotFoundException e) { 415 Log.errorAndQuit("Couldn't open a file called " + outputName); 416 } catch (IOException e) { 417 Log.errorAndQuit("IOException when trying to save a DEX test file!"); 418 } 419 return success; 420 } 421 } 422