blob: 650501be12db9b583b54d49fbc40b533a2986715 [file] [log] [blame]
/*
* 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.program;
import dexfuzz.Log;
import dexfuzz.rawdex.CodeItem;
import dexfuzz.rawdex.EncodedCatchHandler;
import dexfuzz.rawdex.EncodedTypeAddrPair;
import dexfuzz.rawdex.Instruction;
import dexfuzz.rawdex.Opcode;
import dexfuzz.rawdex.TryItem;
import dexfuzz.rawdex.formats.ContainsTarget;
import dexfuzz.rawdex.formats.RawInsnHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Translates from a CodeItem (the raw list of Instructions) to MutatableCode
* (graph of Instructions, using MInsns and subclasses) and vice-versa.
*/
public class CodeTranslator {
/**
* Given a raw DEX file's CodeItem, produce a MutatableCode object, that CodeMutators
* are designed to operate on.
* @param codeItemIdx Used to make sure the correct CodeItem is updated later after mutation.
* @return A new MutatableCode object, which contains all relevant information
* obtained from the CodeItem.
*/
public MutatableCode codeItemToMutatableCode(Program program, CodeItem codeItem,
int codeItemIdx, int mutatableCodeIdx) {
Log.debug("Translating CodeItem " + codeItemIdx
+ " (" + codeItem.meta.methodName + ") to MutatableCode");
MutatableCode mutatableCode = new MutatableCode(program);
codeItem.registerMutatableCode(mutatableCode);
mutatableCode.name = codeItem.meta.methodName;
mutatableCode.shorty = codeItem.meta.shorty;
mutatableCode.isStatic = codeItem.meta.isStatic;
mutatableCode.codeItemIdx = codeItemIdx;
mutatableCode.mutatableCodeIdx = mutatableCodeIdx;
mutatableCode.registersSize = codeItem.registersSize;
mutatableCode.insSize = codeItem.insSize;
mutatableCode.outsSize = codeItem.outsSize;
mutatableCode.triesSize = codeItem.triesSize;
// Temporary map from bytecode offset -> instruction.
Map<Integer,MInsn> insnLocationMap = new HashMap<Integer,MInsn>();
List<Instruction> inputInsns = codeItem.insns;
// Create the MInsns.
int loc = 0;
for (Instruction insn : inputInsns) {
MInsn mInsn = null;
if (isInstructionSwitch(insn)) {
mInsn = new MSwitchInsn();
} else if (isInstructionBranch(insn)) {
mInsn = new MBranchInsn();
} else if (isInstructionFillArrayData(insn)) {
mInsn = new MInsnWithData();
} else {
mInsn = new MInsn();
}
mInsn.insn = insn;
// Populate the temporary map.
insnLocationMap.put(loc, mInsn);
// Populate the proper list of mutatable instructions.
mutatableCode.addInstructionToEnd(mInsn);
// Calculate the offsets for each instruction.
mInsn.location = loc;
mInsn.locationUpdated = false;
loc += mInsn.insn.getSize();
}
// Now make branch/switch instructions point at the right target instructions.
for (MInsn mInsn : mutatableCode.getInstructions()) {
if (mInsn instanceof MSwitchInsn) {
readSwitchInstruction((MSwitchInsn) mInsn, insnLocationMap);
} else if (mInsn instanceof MInsnWithData) {
ContainsTarget containsTarget = (ContainsTarget) mInsn.insn.info.format;
int targetLoc = mInsn.location + (int) containsTarget.getTarget(mInsn.insn);
((MInsnWithData)mInsn).dataTarget = insnLocationMap.get(targetLoc);
if (((MInsnWithData)mInsn).dataTarget == null) {
Log.errorAndQuit("Bad offset calculation in data-target insn");
}
} else if (mInsn instanceof MBranchInsn) {
ContainsTarget containsTarget = (ContainsTarget) mInsn.insn.info.format;
int targetLoc = mInsn.location + (int) containsTarget.getTarget(mInsn.insn);
((MBranchInsn)mInsn).target = insnLocationMap.get(targetLoc);
if (((MBranchInsn)mInsn).target == null) {
Log.errorAndQuit("Bad offset calculation in branch insn");
}
}
}
// Now create try blocks.
if (mutatableCode.triesSize > 0) {
readTryBlocks(codeItem, mutatableCode, insnLocationMap);
}
return mutatableCode;
}
/**
* Given a MutatableCode item that may have been mutated, update the original CodeItem
* correctly, to allow valid DEX to be written back to the output file.
*/
public void mutatableCodeToCodeItem(CodeItem codeItem, MutatableCode mutatableCode) {
Log.debug("Translating MutatableCode " + mutatableCode.name
+ " to CodeItem " + mutatableCode.codeItemIdx);
// We must first align any data instructions at the end of the code
// before we recalculate any offsets.
// This also updates their sizes...
alignDataInstructions(mutatableCode);
// Validate that the tracked locations for instructions are valid.
// Also mark locations as no longer being updated.
int loc = 0;
for (MInsn mInsn : mutatableCode.getInstructions()) {
if (mInsn.insn.justRaw) {
// All just_raw instructions need alignment!
if ((loc % 2) != 0) {
loc++;
}
}
if (mInsn.location != loc) {
Log.errorAndQuit(String.format("%s does not have expected location 0x%x",
mInsn, loc));
}
mInsn.locationUpdated = false;
loc += mInsn.insn.getSize();
}
// This new list will be attached to the CodeItem at the end...
List<Instruction> outputInsns = new LinkedList<Instruction>();
// Go through our new list of MInsns, adding them to the new
// list of instructions that will be attached to the CodeItem.
// Also recalculate offsets for branches.
for (MInsn mInsn : mutatableCode.getInstructions()) {
if (mInsn instanceof MSwitchInsn) {
updateSwitchInstruction((MSwitchInsn)mInsn, mutatableCode);
} else if (mInsn instanceof MInsnWithData) {
MInsn target = ((MInsnWithData) mInsn).dataTarget;
int dataOffset = target.location - mInsn.location;
ContainsTarget containsTarget = (ContainsTarget) mInsn.insn.info.format;
containsTarget.setTarget(mInsn.insn, dataOffset);
} else if (mInsn instanceof MBranchInsn) {
MInsn target = ((MBranchInsn) mInsn).target;
int branchOffset = target.location - mInsn.location;
ContainsTarget containsTarget = (ContainsTarget) mInsn.insn.info.format;
containsTarget.setTarget(mInsn.insn, branchOffset);
}
outputInsns.add(mInsn.insn);
}
// Calculate the new insns_size.
int newInsnsSize = 0;
for (Instruction insn : outputInsns) {
newInsnsSize += insn.getSize();
}
if (mutatableCode.triesSize > 0) {
updateTryBlocks(codeItem, mutatableCode);
}
codeItem.insnsSize = newInsnsSize;
codeItem.insns = outputInsns;
codeItem.registersSize = mutatableCode.registersSize;
codeItem.insSize = mutatableCode.insSize;
codeItem.outsSize = mutatableCode.outsSize;
codeItem.triesSize = mutatableCode.triesSize;
}
/**
* The TryItem specifies an offset into the EncodedCatchHandlerList for a given CodeItem,
* but we only have an array of the EncodedCatchHandlers that the List contains.
* This function produces a map that offers a way to find out the index into our array,
* from the try handler's offset.
*/
private Map<Short,Integer> createTryHandlerOffsetToIndexMap(CodeItem codeItem) {
// Create a sorted set of offsets.
List<Short> uniqueOffsets = new ArrayList<Short>();
for (TryItem tryItem : codeItem.tries) {
int index = 0;
while (true) {
if ((index == uniqueOffsets.size())
|| (uniqueOffsets.get(index) > tryItem.handlerOff)) {
// First condition means we're at the end of the set (or we're inserting
// into an empty set)
// Second condition means that the offset belongs here
// ...so insert it here, pushing the element currently in this position to the
// right, if it exists
uniqueOffsets.add(index, tryItem.handlerOff);
break;
} else if (uniqueOffsets.get(index) == tryItem.handlerOff) {
// We've already seen it, and we're making a set, not a list.
break;
} else {
// Keep searching.
index++;
}
}
}
// Now we have an (implicit) index -> offset mapping!
// Now create the reverse mapping.
Map<Short,Integer> offsetIndexMap = new HashMap<Short,Integer>();
for (int i = 0; i < uniqueOffsets.size(); i++) {
offsetIndexMap.put(uniqueOffsets.get(i), i);
}
return offsetIndexMap;
}
private void readTryBlocks(CodeItem codeItem, MutatableCode mutatableCode,
Map<Integer,MInsn> insnLocationMap) {
mutatableCode.mutatableTries = new LinkedList<MTryBlock>();
Map<Short,Integer> offsetIndexMap = createTryHandlerOffsetToIndexMap(codeItem);
// Read each TryItem into a MutatableTryBlock.
for (TryItem tryItem : codeItem.tries) {
MTryBlock mTryBlock = new MTryBlock();
// Get the MInsns that form the start and end of the try block.
int startLocation = tryItem.startAddr;
mTryBlock.startInsn = insnLocationMap.get(startLocation);
int endLocation = tryItem.startAddr + tryItem.insnCount;
mTryBlock.endInsn = insnLocationMap.get(endLocation);
// Sanity checks.
if (mTryBlock.startInsn == null) {
Log.errorAndQuit(String.format(
"Couldn't find a mutatable insn at start offset 0x%x",
startLocation));
}
if (mTryBlock.endInsn == null) {
Log.errorAndQuit(String.format(
"Couldn't find a mutatable insn at end offset 0x%x",
endLocation));
}
// Get the EncodedCatchHandler.
int handlerIdx = offsetIndexMap.get(tryItem.handlerOff);
EncodedCatchHandler encodedCatchHandler = codeItem.handlers.list[handlerIdx];
// Do we have a catch all? If so, associate the MInsn that's there.
if (encodedCatchHandler.size <= 0) {
mTryBlock.catchAllHandler =
insnLocationMap.get(encodedCatchHandler.catchAllAddr);
// Sanity check.
if (mTryBlock.catchAllHandler == null) {
Log.errorAndQuit(
String.format("Couldn't find a mutatable insn at catch-all offset 0x%x",
encodedCatchHandler.catchAllAddr));
}
}
// Do we have explicitly-typed handlers? This will remain empty if not.
mTryBlock.handlers = new LinkedList<MInsn>();
// Associate all the explicitly-typed handlers.
for (int i = 0; i < Math.abs(encodedCatchHandler.size); i++) {
EncodedTypeAddrPair handler = encodedCatchHandler.handlers[i];
MInsn handlerInsn = insnLocationMap.get(handler.addr);
// Sanity check.
if (handlerInsn == null) {
Log.errorAndQuit(String.format(
"Couldn't find a mutatable instruction at handler offset 0x%x",
handler.addr));
}
mTryBlock.handlers.add(handlerInsn);
}
// Now finally add the new MutatableTryBlock into this MutatableCode's list!
mutatableCode.mutatableTries.add(mTryBlock);
}
}
private void updateTryBlocks(CodeItem codeItem, MutatableCode mutatableCode) {
// TODO: Support ability to add extra try blocks/handlers?
for (MTryBlock mTryBlock : mutatableCode.mutatableTries) {
if (mTryBlock.startInsn.location > mTryBlock.endInsn.location) {
// Mutation has put this try block's end insn before its start insn. Fix this.
MInsn tempInsn = mTryBlock.startInsn;
mTryBlock.startInsn = mTryBlock.endInsn;
mTryBlock.endInsn = tempInsn;
}
}
// First, manipulate the try blocks if they overlap.
for (int i = 0; i < mutatableCode.mutatableTries.size() - 1; i++) {
MTryBlock first = mutatableCode.mutatableTries.get(i);
MTryBlock second = mutatableCode.mutatableTries.get(i + 1);
// Do they overlap?
if (first.endInsn.location > second.startInsn.location) {
Log.debug("Found overlap in TryBlocks, moving 2nd TryBlock...");
Log.debug("1st TryBlock goes from " + first.startInsn + " to " + first.endInsn);
Log.debug("2nd TryBlock goes from " + second.startInsn + " to " + second.endInsn);
// Find the first instruction that comes after that does not overlap
// with the first try block.
MInsn newInsn = second.startInsn;
int ptr = mutatableCode.getInstructionIndex(newInsn);
while (first.endInsn.location > newInsn.location) {
ptr++;
newInsn = mutatableCode.getInstructionAt(ptr);
}
second.startInsn = newInsn;
Log.debug("Now 2nd TryBlock goes from " + second.startInsn + " to " + second.endInsn);
}
}
Map<Short,Integer> offsetIndexMap = createTryHandlerOffsetToIndexMap(codeItem);
int tryItemIdx = 0;
for (MTryBlock mTryBlock : mutatableCode.mutatableTries) {
TryItem tryItem = codeItem.tries[tryItemIdx];
tryItem.startAddr = mTryBlock.startInsn.location;
tryItem.insnCount =
(short) (mTryBlock.endInsn.location - mTryBlock.startInsn.location);
// Get the EncodedCatchHandler.
EncodedCatchHandler encodedCatchHandler =
codeItem.handlers.list[offsetIndexMap.get(tryItem.handlerOff)];
if (encodedCatchHandler.size <= 0) {
encodedCatchHandler.catchAllAddr = mTryBlock.catchAllHandler.location;
}
for (int i = 0; i < Math.abs(encodedCatchHandler.size); i++) {
MInsn handlerInsn = mTryBlock.handlers.get(i);
EncodedTypeAddrPair handler = encodedCatchHandler.handlers[i];
handler.addr = handlerInsn.location;
}
tryItemIdx++;
}
}
/**
* Given a switch instruction, find the associated data's raw[] form, and update
* the targets of the switch instruction to point to the correct instructions.
*/
private void readSwitchInstruction(MSwitchInsn switchInsn,
Map<Integer,MInsn> insnLocationMap) {
// Find the data.
ContainsTarget containsTarget = (ContainsTarget) switchInsn.insn.info.format;
int dataLocation = switchInsn.location + (int) containsTarget.getTarget(switchInsn.insn);
switchInsn.dataTarget = insnLocationMap.get(dataLocation);
if (switchInsn.dataTarget == null) {
Log.errorAndQuit("Bad offset calculation for data target in switch insn");
}
// Now read the data.
Instruction dataInsn = switchInsn.dataTarget.insn;
int rawPtr = 2;
int targetsSize = (int) RawInsnHelper.getUnsignedShortFromTwoBytes(dataInsn.rawBytes, rawPtr);
rawPtr += 2;
int[] keys = new int[targetsSize];
int[] targets = new int[targetsSize];
if (dataInsn.rawType == 1) {
switchInsn.packed = true;
// Dealing with a packed-switch.
// Read the first key.
keys[0] = (int) RawInsnHelper.getUnsignedIntFromFourBytes(dataInsn.rawBytes, rawPtr);
rawPtr += 4;
// Calculate the rest of the keys.
for (int i = 1; i < targetsSize; i++) {
keys[i] = keys[i - 1] + 1;
}
} else if (dataInsn.rawType == 2) {
// Dealing with a sparse-switch.
// Read all of the keys.
for (int i = 0; i < targetsSize; i++) {
keys[i] = (int) RawInsnHelper.getUnsignedIntFromFourBytes(dataInsn.rawBytes,
rawPtr);
rawPtr += 4;
}
}
// Now read the targets.
for (int i = 0; i < targetsSize; i++) {
targets[i] = (int) RawInsnHelper.getUnsignedIntFromFourBytes(dataInsn.rawBytes,
rawPtr);
rawPtr += 4;
}
// Store the keys.
switchInsn.keys = keys;
// Convert our targets[] offsets into pointers to MInsns.
for (int target : targets) {
int targetLocation = switchInsn.location + target;
MInsn targetInsn = insnLocationMap.get(targetLocation);
switchInsn.targets.add(targetInsn);
if (targetInsn == null) {
Log.errorAndQuit("Bad offset calculation for target in switch insn");
}
}
}
/**
* Given a mutatable switch instruction, which may have had some of its branch
* targets moved, update all the target offsets in the raw[] form of the instruction.
*/
private void updateSwitchInstruction(MSwitchInsn switchInsn, MutatableCode mutatableCode) {
// Update the offset to the data instruction
MInsn dataTarget = switchInsn.dataTarget;
int dataOffset = dataTarget.location - switchInsn.location;
ContainsTarget containsTarget = (ContainsTarget) switchInsn.insn.info.format;
containsTarget.setTarget(switchInsn.insn, dataOffset);
int targetsSize = switchInsn.targets.size();
int[] keys = switchInsn.keys;
int[] targets = new int[targetsSize];
// Calculate the new offsets.
int targetIdx = 0;
for (MInsn target : switchInsn.targets) {
targets[targetIdx] = target.location - switchInsn.location;
targetIdx++;
}
// Now write the data back to the raw bytes.
Instruction dataInsn = switchInsn.dataTarget.insn;
int rawPtr = 2;
// Write out the size.
RawInsnHelper.writeUnsignedShortToTwoBytes(dataInsn.rawBytes, rawPtr, targetsSize);
rawPtr += 2;
// Write out the keys.
if (switchInsn.packed) {
// Only write out one key - the first.
RawInsnHelper.writeUnsignedIntToFourBytes(dataInsn.rawBytes, rawPtr, keys[0]);
rawPtr += 4;
} else {
// Write out all the keys.
for (int i = 0; i < targetsSize; i++) {
RawInsnHelper.writeUnsignedIntToFourBytes(dataInsn.rawBytes, rawPtr, keys[i]);
rawPtr += 4;
}
}
// Write out all the targets.
for (int i = 0; i < targetsSize; i++) {
RawInsnHelper.writeUnsignedIntToFourBytes(dataInsn.rawBytes, rawPtr, targets[i]);
rawPtr += 4;
}
}
/**
* After mutation, data instructions may no longer be 4-byte aligned.
* If this is the case, insert nops to align them all.
* This makes a number of assumptions about data currently:
* - data is always at the end of method insns
* - all data instructions are stored contiguously
*/
private void alignDataInstructions(MutatableCode mutatableCode) {
// Find all the switch data instructions.
List<MInsn> dataInsns = new ArrayList<MInsn>();
// Update raw sizes of the data instructions as well.
for (MInsn mInsn : mutatableCode.getInstructions()) {
if (mInsn instanceof MSwitchInsn) {
// Update the raw size of the instruction.
MSwitchInsn switchInsn = (MSwitchInsn) mInsn;
int targetsSize = switchInsn.targets.size();
Instruction dataInsn = switchInsn.dataTarget.insn;
if (switchInsn.packed) {
dataInsn.rawSize = (targetsSize * 2) + 4;
} else {
dataInsn.rawSize = (targetsSize * 4) + 2;
}
dataInsns.add(switchInsn.dataTarget);
} else if (mInsn instanceof MInsnWithData) {
MInsnWithData insnWithData =
(MInsnWithData) mInsn;
dataInsns.add(insnWithData.dataTarget);
}
}
// Only need to align switch data instructions if there are any!
if (!dataInsns.isEmpty()) {
Log.debug("Found data instructions, checking alignment...");
// Sort data_insns by location.
Collections.sort(dataInsns, new Comparator<MInsn>() {
@Override
public int compare(MInsn first, MInsn second) {
if (first.location < second.location) {
return -1;
} else if (first.location > second.location) {
return 1;
}
return 0;
}
});
boolean performedAlignment = false;
// Go through all the data insns, and insert an alignment nop if they're unaligned.
for (MInsn dataInsn : dataInsns) {
if (dataInsn.location % 2 != 0) {
Log.debug("Aligning data instruction with a nop.");
int alignmentNopIdx = mutatableCode.getInstructionIndex(dataInsn);
MInsn nop = new MInsn();
nop.insn = new Instruction();
nop.insn.info = Instruction.getOpcodeInfo(Opcode.NOP);
mutatableCode.insertInstructionAt(nop, alignmentNopIdx);
performedAlignment = true;
}
}
if (!performedAlignment) {
Log.debug("Alignment okay.");
}
}
}
/**
* Determine if a particular instruction is a branch instruction, based on opcode.
*/
private boolean isInstructionBranch(Instruction insn) {
Opcode opcode = insn.info.opcode;
if (Opcode.isBetween(opcode, Opcode.IF_EQ, Opcode.IF_LEZ)
|| Opcode.isBetween(opcode, Opcode.GOTO, Opcode.GOTO_32)) {
return true;
}
return false;
}
/**
* Determine if a particular instruction is a switch instruction, based on opcode.
*/
private boolean isInstructionSwitch(Instruction insn) {
Opcode opcode = insn.info.opcode;
if (Opcode.isBetween(opcode, Opcode.PACKED_SWITCH, Opcode.SPARSE_SWITCH)) {
return true;
}
return false;
}
private boolean isInstructionFillArrayData(Instruction insn) {
return (insn.info.opcode == Opcode.FILL_ARRAY_DATA);
}
}