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.rawdex; 18 19 import dexfuzz.Log; 20 21 import java.io.IOException; 22 import java.util.ArrayList; 23 import java.util.HashMap; 24 import java.util.List; 25 import java.util.Map; 26 27 /** 28 * This class allows the recording of both Offsettable items (that is, items that can be 29 * referred to by an offset somewhere else in the file - RawDexObjects) and Offsets. 30 * The idea in a nutshell is that for every Offsettable item we read, we remember 31 * its original position in the file using a map, and the order in which the Offsettables were 32 * written out. We also remember every Offset we read in, and its value. Then, after reading 33 * the whole file, we use the map to find the Offsettable it pointed at. 34 * Then, as we write out the file, for every Offsettable we write out, we record its new position, 35 * using the order we collected earlier. For every Offset we write out, we look at its Offsettable 36 * to see where it was written. If it hasn't been written yet, then we write out a blank value 37 * for the time being, remember where that blank value was written, and put the Offset into a 38 * table for patching once everything has been written out. 39 * There are some variables (index_after_map_list, restore_point) used for remembering certain 40 * points to jump forward and back to, because we cannot read and write the file out in exactly 41 * the same order. 42 * TODO: Perhaps it makes more sense to just reorder the offsettable_table once it's been read, 43 * in preparation for the order in which the file is written out? 44 * Finally, we provide methods for adding new Offsettable items into the right place in the order 45 * table. 46 */ 47 public class OffsetTracker { 48 /** 49 * A map from the original offset in the input DEX file to 50 * the Offsettable it points to. (That Offsettable will contain 51 * the actual item, and later on the new offset for the item when 52 * the item is written out. 53 */ 54 private Map<Integer, Offsettable> offsettableMap; 55 56 /** 57 * A table of all Offsettables. We need to ensure we write out 58 * all items in the same order we read them in, to make sure we update 59 * the Offsettable.new_position field with the correct value wrt to 60 * the original_position field. 61 */ 62 private List<Offsettable> offsettableTable; 63 64 /** 65 * A table of all offsets that is populated as we read in the DEX file. 66 * As the end, we find the correct Offsettable for the Offset in the above 67 * map, and associate them. 68 */ 69 private List<Offset> needsAssociationTable; 70 71 /** 72 * A table of all offsets that we could not write out an updated offset for 73 * as we write out a DEX file. Will be checked after writing is complete, 74 * to allow specific patching of each offset's location as at that point 75 * all Offsettables will have been updated with their new position. 76 */ 77 private List<Offset> needsUpdateTable; 78 79 /** 80 * Tracks how far we are through the offsettable_table as we write out the file. 81 */ 82 private int offsettableTableIdx; 83 84 /** 85 * Because we write in a slightly different order to how we read 86 * (why? to read, we read the header, then the map list, and then use the map 87 * list to read everything else. 88 * when we write, we write the header, and then we cannot write the map list 89 * because we don't know where it will go yet, so we write everything else first) 90 * so: we remember the position in the offsettable_table after we read the map list, 91 * so we can skip there after we write out the header. 92 */ 93 private int indexAfterMapList; 94 95 /** 96 * Related to index_after_map_list, this is the index we save when we're jumping back to 97 * write the map list. 98 */ 99 private int restorePoint; 100 101 /** 102 * Create a new OffsetTracker. Should persist between parsing a DEX file, and outputting 103 * the mutated DEX file. 104 */ OffsetTracker()105 public OffsetTracker() { 106 offsettableMap = new HashMap<Integer,Offsettable>(); 107 offsettableTable = new ArrayList<Offsettable>(); 108 needsAssociationTable = new ArrayList<Offset>(); 109 needsUpdateTable = new ArrayList<Offset>(); 110 } 111 112 /** 113 * Lookup an Item by the offset it had in the input DEX file. 114 * @param offset The offset in the input DEX file. 115 * @return The corresponding Item. 116 */ getItemByOffset(int offset)117 public RawDexObject getItemByOffset(int offset) { 118 return offsettableMap.get(offset).getItem(); 119 } 120 121 /** 122 * As Items are read in, they call this function once they have word-aligned the file pointer, 123 * to record their position and themselves into an Offsettable object, that will be tracked. 124 * @param file Used for recording position into the new Offsettable. 125 * @param item Used for recording the relevant Item into the new Offsettable. 126 */ getNewOffsettable(DexRandomAccessFile file, RawDexObject item)127 public void getNewOffsettable(DexRandomAccessFile file, RawDexObject item) throws IOException { 128 Offsettable offsettable = new Offsettable(item, false); 129 offsettable.setOriginalPosition((int) file.getFilePointer()); 130 offsettableMap.put(offsettable.getOriginalPosition(), offsettable); 131 offsettableTable.add(offsettable); 132 } 133 134 /** 135 * As Items read in Offsets, they call this function with the offset they originally 136 * read from the file, to allow later association with an Offsettable. 137 * @param originalOffset The original offset read from the input DEX file. 138 * @return An Offset that will later be associated with an Offsettable. 139 */ getNewOffset(int originalOffset)140 public Offset getNewOffset(int originalOffset) throws IOException { 141 Offset offset = new Offset(false); 142 offset.setOriginalOffset(originalOffset); 143 needsAssociationTable.add(offset); 144 return offset; 145 } 146 147 /** 148 * Only MapItem should call this method, when the MapItem that points to the header 149 * is read. 150 */ getNewHeaderOffset(int originalOffset)151 public Offset getNewHeaderOffset(int originalOffset) throws IOException { 152 Offset offset = new Offset(true); 153 offset.setOriginalOffset(originalOffset); 154 needsAssociationTable.add(offset); 155 return offset; 156 } 157 158 /** 159 * Call this after reading, to associate Offsets with Offsettables. 160 */ associateOffsets()161 public void associateOffsets() { 162 for (Offset offset : needsAssociationTable) { 163 if (offset.getOriginalOffset() == 0 && !(offset.pointsAtHeader())) { 164 offset.setPointsAtNull(); 165 } else { 166 offset.pointTo(offsettableMap.get(offset.getOriginalOffset())); 167 if (!offset.pointsToSomething()) { 168 Log.error(String.format("Couldn't find original offset 0x%x!", 169 offset.getOriginalOffset())); 170 } 171 } 172 } 173 needsAssociationTable.clear(); 174 } 175 176 /** 177 * As Items are written out into the output DEX file, this function is called 178 * to update the next Offsettable with the file pointer's current position. 179 * This should allow the tracking of new offset locations. 180 * This also requires that reading and writing of all items happens in the same order 181 * (with the exception of the map list, see above) 182 * @param file Used for recording the new position. 183 */ updatePositionOfNextOffsettable(DexRandomAccessFile file)184 public void updatePositionOfNextOffsettable(DexRandomAccessFile file) throws IOException { 185 if (offsettableTableIdx == offsettableTable.size()) { 186 Log.errorAndQuit("Not all created Offsettable items have been added to the " 187 + "Offsettable Table!"); 188 } 189 Offsettable offsettable = offsettableTable.get(offsettableTableIdx); 190 offsettable.setNewPosition((int) file.getFilePointer()); 191 offsettableTableIdx++; 192 } 193 194 /** 195 * As Items are written out, any writing out of an offset must call this function, passing 196 * in the relevant offset. This function will write out the offset, if the associated 197 * Offsettable has been updated with its new position, or else will write out a null value, and 198 * the Offset will be stored for writing after all Items have been written, and all 199 * Offsettables MUST have been updated. 200 * @param offset The offset received from getNewOffset(). 201 * @param file Used for writing out to the file. 202 * @param useUleb128 Whether or not the offset should be written in UINT or ULEB128 form. 203 */ tryToWriteOffset(Offset offset, DexRandomAccessFile file, boolean useUleb128)204 public void tryToWriteOffset(Offset offset, DexRandomAccessFile file, boolean useUleb128) 205 throws IOException { 206 if (!offset.isNewOffset() && (!offset.pointsToSomething())) { 207 if (useUleb128) { 208 file.writeUleb128(0); 209 } else { 210 file.writeUInt(0); 211 } 212 return; 213 } 214 215 if (offset.readyForWriting()) { 216 if (useUleb128) { 217 file.writeUleb128(offset.getNewPositionOfItem()); 218 } else { 219 file.writeUInt(offset.getNewPositionOfItem()); 220 } 221 } else { 222 offset.setOutputLocation((int) file.getFilePointer()); 223 if (useUleb128) { 224 file.writeLargestUleb128(offset.getOriginalOffset()); 225 offset.setUsesUleb128(); 226 } else { 227 file.writeUInt(offset.getOriginalOffset()); 228 } 229 needsUpdateTable.add(offset); 230 } 231 } 232 233 /** 234 * This is called after all writing has finished, to write out any Offsets 235 * that could not be written out during the original writing phase, because their 236 * associated Offsettables hadn't had their new positions calculated yet. 237 * @param file Used for writing out to the file. 238 */ updateOffsets(DexRandomAccessFile file)239 public void updateOffsets(DexRandomAccessFile file) throws IOException { 240 if (offsettableTableIdx != offsettableTable.size()) { 241 Log.errorAndQuit("Being asked to update dangling offsets but the " 242 + "correct number of offsettables has not been written out!"); 243 } 244 for (Offset offset : needsUpdateTable) { 245 file.seek(offset.getOutputLocation()); 246 if (offset.usesUleb128()) { 247 file.writeLargestUleb128(offset.getNewPositionOfItem()); 248 } else { 249 file.writeUInt(offset.getNewPositionOfItem()); 250 } 251 } 252 needsUpdateTable.clear(); 253 } 254 255 /** 256 * Called after writing out the header, to skip to after the map list. 257 */ skipToAfterMapList()258 public void skipToAfterMapList() { 259 offsettableTableIdx = indexAfterMapList; 260 } 261 262 /** 263 * Called once the map list needs to be written out, to set the 264 * offsettable table index back to the right place. 265 */ goBackToMapList()266 public void goBackToMapList() { 267 restorePoint = offsettableTableIdx; 268 offsettableTableIdx = (indexAfterMapList - 1); 269 } 270 271 /** 272 * Called once the map list has been written out, to set the 273 * offsettable table index back to where it was before. 274 */ goBackToPreviousPoint()275 public void goBackToPreviousPoint() { 276 if (offsettableTableIdx != indexAfterMapList) { 277 Log.errorAndQuit("Being asked to go to the point before the MapList was written out," 278 + " but we're not in the right place."); 279 } 280 offsettableTableIdx = restorePoint; 281 } 282 283 /** 284 * Called after reading in the map list, to remember the point to be skipped 285 * to later. 286 */ rememberPointAfterMapList()287 public void rememberPointAfterMapList() { 288 indexAfterMapList = offsettableTable.size(); 289 } 290 updateHeaderOffsetIfValid(Offset offset, Offsettable previousFirst, Offsettable newFirst, String offsetName)291 private void updateHeaderOffsetIfValid(Offset offset, Offsettable previousFirst, 292 Offsettable newFirst, String offsetName) { 293 if (offset.pointsToThisOffsettable(previousFirst)) { 294 offset.pointToNew(newFirst); 295 } else { 296 Log.errorAndQuit("Header " + offsetName + " offset not pointing at first element?"); 297 } 298 } 299 addTypeListsToMapFile(RawDexFile rawDexFile, Offsettable typeListOffsettable)300 private void addTypeListsToMapFile(RawDexFile rawDexFile, Offsettable typeListOffsettable) { 301 // Create a MapItem for the TypeLists 302 MapItem typeListMapItem = new MapItem(); 303 typeListMapItem.offset = new Offset(false); 304 typeListMapItem.offset.pointToNew(typeListOffsettable); 305 typeListMapItem.type = MapItem.TYPE_TYPE_LIST; 306 typeListMapItem.size = 1; 307 308 // Insert into the MapList. 309 // (first, find the MapItem that points to StringDataItems...) 310 int idx = 0; 311 for (MapItem mapItem : rawDexFile.mapList.mapItems) { 312 if (mapItem.type == MapItem.TYPE_STRING_DATA_ITEM) { 313 break; 314 } 315 idx++; 316 } 317 // (now insert the TypeList MapItem just before the StringDataItem one...) 318 rawDexFile.mapList.mapItems.add(idx, typeListMapItem); 319 } 320 addFieldIdsToHeaderAndMapFile(RawDexFile rawDexFile, Offsettable fieldOffsettable)321 private void addFieldIdsToHeaderAndMapFile(RawDexFile rawDexFile, 322 Offsettable fieldOffsettable) { 323 // Add the field IDs to the header. 324 rawDexFile.header.fieldIdsOff.unsetNullAndPointTo(fieldOffsettable); 325 rawDexFile.header.fieldIdsSize = 1; 326 327 // Create a MapItem for the field IDs. 328 MapItem fieldMapItem = new MapItem(); 329 fieldMapItem.offset = new Offset(false); 330 fieldMapItem.offset.pointToNew(fieldOffsettable); 331 fieldMapItem.type = MapItem.TYPE_FIELD_ID_ITEM; 332 fieldMapItem.size = 1; 333 334 // Insert into the MapList. 335 // (first, find the MapItem that points to MethodIdItems...) 336 int idx = 0; 337 for (MapItem mapItem : rawDexFile.mapList.mapItems) { 338 if (mapItem.type == MapItem.TYPE_METHOD_ID_ITEM) { 339 break; 340 } 341 idx++; 342 } 343 // (now insert the FieldIdItem MapItem just before the MethodIdItem one...) 344 rawDexFile.mapList.mapItems.add(idx, fieldMapItem); 345 } 346 347 updateOffsetsInHeaderAndMapFile(RawDexFile rawDexFile, Offsettable newFirstOffsettable)348 private void updateOffsetsInHeaderAndMapFile(RawDexFile rawDexFile, 349 Offsettable newFirstOffsettable) { 350 Offsettable prevFirstOffsettable = null; 351 for (int i = 0; i < offsettableTable.size(); i++) { 352 if (offsettableTable.get(i) == newFirstOffsettable) { 353 prevFirstOffsettable = offsettableTable.get(i + 1); 354 break; 355 } 356 } 357 if (prevFirstOffsettable == null) { 358 Log.errorAndQuit("When calling updateMapListOffsets, could not find new " 359 + "first offsettable?"); 360 } 361 362 // Based on the type of the item we just added, check the relevant Offset in the header 363 // and if it pointed at the prev_first_offsettable, make it point at the new one. 364 // NB: if it isn't pointing at the prev one, something is wrong. 365 HeaderItem header = rawDexFile.header; 366 if (newFirstOffsettable.getItem() instanceof StringIdItem) { 367 updateHeaderOffsetIfValid(header.stringIdsOff, prevFirstOffsettable, 368 newFirstOffsettable, "StringID"); 369 } else if (newFirstOffsettable.getItem() instanceof TypeIdItem) { 370 updateHeaderOffsetIfValid(header.typeIdsOff, prevFirstOffsettable, 371 newFirstOffsettable, "TypeID"); 372 } else if (newFirstOffsettable.getItem() instanceof ProtoIdItem) { 373 updateHeaderOffsetIfValid(header.protoIdsOff, prevFirstOffsettable, 374 newFirstOffsettable, "ProtoID"); 375 } else if (newFirstOffsettable.getItem() instanceof FieldIdItem) { 376 updateHeaderOffsetIfValid(header.fieldIdsOff, prevFirstOffsettable, 377 newFirstOffsettable, "FieldID"); 378 } else if (newFirstOffsettable.getItem() instanceof MethodIdItem) { 379 updateHeaderOffsetIfValid(header.methodIdsOff, prevFirstOffsettable, 380 newFirstOffsettable, "MethodID"); 381 } else if (newFirstOffsettable.getItem() instanceof ClassDefItem) { 382 updateHeaderOffsetIfValid(header.classDefsOff, prevFirstOffsettable, 383 newFirstOffsettable, "ClassDef"); 384 } 385 386 // Now iterate through the MapList's MapItems, and see if their Offsets pointed at the 387 // prev_first_offsettable, and if so, make them now point at the new_first_offsettable. 388 for (MapItem mapItem : rawDexFile.mapList.mapItems) { 389 if (mapItem.offset.pointsToThisOffsettable(prevFirstOffsettable)) { 390 Log.info("Updating offset in MapItem (type: " + mapItem.type + ") after " 391 + "we called insertNewOffsettableAsFirstOfType()"); 392 mapItem.offset.pointToNew(newFirstOffsettable); 393 } 394 } 395 } 396 insertOffsettableAt(int idx, Offsettable offsettable)397 private void insertOffsettableAt(int idx, Offsettable offsettable) { 398 offsettableTable.add(idx, offsettable); 399 if (indexAfterMapList > idx) { 400 indexAfterMapList++; 401 } 402 if (restorePoint > idx) { 403 restorePoint++; 404 } 405 } 406 407 /** 408 * If we're creating our first TypeList, then IdCreator has to call this method to 409 * ensure it gets put into the correct place in the offsettable table. 410 * This assumes TypeLists always come before StringDatas. 411 */ insertNewOffsettableAsFirstEverTypeList(RawDexObject item, RawDexFile rawDexFile)412 public Offsettable insertNewOffsettableAsFirstEverTypeList(RawDexObject item, 413 RawDexFile rawDexFile) { 414 // We find the first StringDataItem, the type lists will come before this. 415 Log.info("Calling insertNewOffsettableAsFirstEverTypeList()"); 416 for (int i = 0; i < offsettableTable.size(); i++) { 417 if (offsettableTable.get(i).getItem() instanceof StringDataItem) { 418 Offsettable offsettable = new Offsettable(item, true); 419 insertOffsettableAt(i, offsettable); 420 addTypeListsToMapFile(rawDexFile, offsettable); 421 return offsettable; 422 } 423 } 424 Log.errorAndQuit("Could not find any StringDataItems to insert the type list before."); 425 return null; 426 } 427 428 /** 429 * If we're creating our first FieldId, then IdCreator has to call this method to 430 * ensure it gets put into the correct place in the offsettable table. 431 * This assumes FieldIds always come before MethodIds. 432 */ insertNewOffsettableAsFirstEverField(RawDexObject item, RawDexFile rawDexFile)433 public Offsettable insertNewOffsettableAsFirstEverField(RawDexObject item, 434 RawDexFile rawDexFile) { 435 // We find the first MethodIdItem, the fields will come before this. 436 Log.info("Calling insertNewOffsettableAsFirstEverField()"); 437 for (int i = 0; i < offsettableTable.size(); i++) { 438 if (offsettableTable.get(i).getItem() instanceof MethodIdItem) { 439 Offsettable offsettable = new Offsettable(item, true); 440 insertOffsettableAt(i, offsettable); 441 addFieldIdsToHeaderAndMapFile(rawDexFile, offsettable); 442 return offsettable; 443 } 444 } 445 Log.errorAndQuit("Could not find any MethodIdItems to insert the field before."); 446 return null; 447 } 448 449 /** 450 * If we're creating a new Item (such as FieldId, MethodId) that is placed into the 451 * first position of the relevant ID table, then IdCreator has to call this method to 452 * ensure it gets put into the correct place in the offsettable table. 453 */ insertNewOffsettableAsFirstOfType(RawDexObject item, RawDexFile rawDexFile)454 public Offsettable insertNewOffsettableAsFirstOfType(RawDexObject item, 455 RawDexFile rawDexFile) { 456 Log.debug("Calling insertNewOffsettableAsFirstOfType()"); 457 int index = getOffsettableIndexForFirstItemType(item); 458 if (index == -1) { 459 Log.errorAndQuit("Could not find any object of class: " + item.getClass()); 460 } 461 Offsettable offsettable = new Offsettable(item, true); 462 insertOffsettableAt(index, offsettable); 463 updateOffsetsInHeaderAndMapFile(rawDexFile, offsettable); 464 return offsettable; 465 } 466 467 /** 468 * IdCreator has to call this method when it creates a new IdItem, to make sure it 469 * gets put into the correct place in the offsettable table. IdCreator should 470 * provide the IdItem that should come before this new IdItem. 471 */ insertNewOffsettableAfter(RawDexObject item, RawDexObject itemBefore)472 public Offsettable insertNewOffsettableAfter(RawDexObject item, RawDexObject itemBefore) { 473 Log.debug("Calling insertNewOffsettableAfter()"); 474 int index = getOffsettableIndexForItem(itemBefore); 475 if (index == -1) { 476 Log.errorAndQuit("Did not find specified 'after' object in offsettable table."); 477 } 478 Offsettable offsettable = new Offsettable(item, true); 479 insertOffsettableAt(index + 1, offsettable); 480 return offsettable; 481 } 482 getOffsettableIndexForFirstItemType(RawDexObject item)483 private int getOffsettableIndexForFirstItemType(RawDexObject item) { 484 Class<?> itemClass = item.getClass(); 485 for (int i = 0; i < offsettableTable.size(); i++) { 486 if (offsettableTable.get(i).getItem().getClass().equals(itemClass)) { 487 return i; 488 } 489 } 490 return -1; 491 } 492 getOffsettableIndexForItem(RawDexObject item)493 private int getOffsettableIndexForItem(RawDexObject item) { 494 for (int i = 0; i < offsettableTable.size(); i++) { 495 if (offsettableTable.get(i).getItem() == item) { 496 return i; 497 } 498 } 499 return -1; 500 } 501 502 /** 503 * Given a RawDexObject, get the Offsettable that contains it. 504 */ getOffsettableForItem(RawDexObject item)505 public Offsettable getOffsettableForItem(RawDexObject item) { 506 for (int i = 0; i < offsettableTable.size(); i++) { 507 if (offsettableTable.get(i).getItem() == item) { 508 return offsettableTable.get(i); 509 } 510 } 511 return null; 512 } 513 } 514