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