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