/* SAAF: A static analyzer for APK files. * Copyright (C) 2013 syssec.rub.de * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.rub.syssec.saaf.analysis.steps.slicing; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; import org.apache.log4j.Logger; import de.rub.syssec.saaf.application.methods.BasicBlock; import de.rub.syssec.saaf.misc.ByteUtils; import de.rub.syssec.saaf.model.application.BasicBlockInterface; import de.rub.syssec.saaf.model.application.CodeLineInterface; import de.rub.syssec.saaf.model.application.DetectionLogicError; import de.rub.syssec.saaf.model.application.SyntaxException; /** * * This class is a helper class for the DetectionLogic and holds "jobs" which need to be processed * at a later time in the program slicing process. * * @author Johannes Hoffmann <johannes.hoffmann@rub.de> * */ public class TodoList { private static final boolean DEBUG=Boolean.parseBoolean(System.getProperty("debug.slicing","false")); private HashSet<ClassContentTracker> returnMap = new HashSet<ClassContentTracker>(); private HashSet<ClassContentTracker> returnMapDone = new HashSet<ClassContentTracker>(); private static final Logger LOGGER = Logger.getLogger(TodoList.class); // TODO: Make it configurable protected static final int MAX_FUZZY_LEVEL = 10; protected static final int MAX_RS_COUNT = 100000; /** * Add new method to search. * @param cm * @param fuzzyLevel * @param fuzzyLevelOffset * @param path * @return true if this was not yet searched through, create a new object, you'll get inconsistencies if you'll reuse the same object */ public boolean addReturnValuesFromMethod(byte[][] cm, int fuzzyLevel, int fuzzyLevelOffset, LinkedList<BasicBlockInterface> path) { if (DEBUG) LOGGER.debug(" -> Add RETURN VALUE: "+new String(cm[0])+"."+new String(cm[1])+"\tfuzzy="+fuzzyLevel+"/"+fuzzyLevelOffset); if (fuzzyLevel+fuzzyLevelOffset > MAX_FUZZY_LEVEL) { if (DEBUG) LOGGER.debug(" Maximum fuzzy level reached ("+MAX_FUZZY_LEVEL+"): aborting."); return false; } ClassContentTracker ctt = new ClassContentTracker(cm, fuzzyLevel, fuzzyLevelOffset, path); if (returnMap.contains(ctt)) return false; if (returnMapDone.contains(ctt)) return false; returnMap.add(ctt); return true; } public ClassContentTracker getNextReturnValuesFromMethod() { ClassContentTracker ctt = null; for (ClassContentTracker ctt2 : returnMap) { // get one item "at random" ctt = ctt2; break; } returnMap.remove(ctt); returnMapDone.add(ctt); if (DEBUG) LOGGER.debug("\n\n-> TRACKING RETURN VALUE: "+new String(ctt.getCi()[0])+"."+new String(ctt.getCi()[1])+"\tfuzzy="+ctt.getFuzzyLevel()+"/"+ctt.getFuzzyOffset()); return ctt; } /** * FIXME name */ public int getRemainingReturnValuesFromMethods() { return returnMap.size(); } // // private HashSet<ClassContentTracker> fieldMap = new HashSet<ClassContentTracker>(); private HashSet<ClassContentTracker> fieldMapDone = new HashSet<ClassContentTracker>(); /** * Add new field to search. * @param cf class, field * @param fuzzyLevel * @param fuzzyLevelOffset * @param linkedList * @return true if this was not yet searched through, create a new object, you'll get inconsistencies if you'll reuse the same object */ public boolean addField(byte[][] cf, int fuzzyLevel, int fuzzyLevelOffset, LinkedList<BasicBlockInterface> linkedList) { if (DEBUG) LOGGER.debug(" -> Add FIELD: "+new String(cf[0])+"."+new String(cf[1])+"\tfuzzy="+fuzzyLevel+"/"+fuzzyLevelOffset); if (fuzzyLevel+fuzzyLevelOffset > MAX_FUZZY_LEVEL) { if (DEBUG) LOGGER.debug(" Maximum fuzzy level reached ("+MAX_FUZZY_LEVEL+"): aborting."); return false; } if (fuzzyLevelOffset < MAX_FUZZY_LEVEL-2) { if (DEBUG) LOGGER.debug(" Setting fuzzy offset to "+(MAX_FUZZY_LEVEL-2)); fuzzyLevelOffset = MAX_FUZZY_LEVEL-2; } ClassContentTracker ctt = new ClassContentTracker(cf, fuzzyLevel, fuzzyLevelOffset, linkedList); if (fieldMap.contains(ctt)) return false; if (fieldMapDone.contains(ctt)) return false; fieldMap.add(ctt); return true; } public ClassContentTracker getNextField() { ClassContentTracker ctt = null; for (ClassContentTracker ctt2 : fieldMap) { // get one item "at random" ctt = ctt2; break; } fieldMap.remove(ctt); fieldMapDone.add(ctt); if (DEBUG) LOGGER.debug("\n\n-> TRACKING FIELD: "+new String(ctt.getCi()[0])+"."+new String(ctt.getCi()[1])+"\tfuzzy="+ctt.getFuzzyLevel()+"/"+ctt.getFuzzyOffset()); return ctt; } public int getRemainingFieldsToTrack() { return fieldMap.size(); } // // public static class RegisterSearch { private final byte[] register; private BasicBlockInterface bb; private final int index; private final int fuzzyLevel; private LinkedList<BasicBlockInterface> path; private int fuzzyOffset; /** * A helper class to backtrack a register. * @param register the registername to backtrack, eg, v0. * @param bb2 the {@linkplain BasicBlock} to backtrack * @param index the index where to start backtracking inside the {@linkplain BasicBlock} * @param fuzzyLevel set this to >1 if the search gets noisy, eg, if you are backtracking into the blue for unknown method calls and are interested in the parameters from such a call * @param fuzzyOffset * @param path the path through the BBs of this search, create a new object, you'll get inconsistencies if you'll reuse the same object */ public RegisterSearch(byte[] register, BasicBlockInterface bb, int index, int fuzzyLevel, int fuzzyOffset, LinkedList<BasicBlockInterface> path) { this.register = register; this.bb = bb; this.index = index; this.fuzzyLevel = fuzzyLevel; this.fuzzyOffset = fuzzyOffset; this.path = path; } /** * The register to track backwards. * @return */ public byte[] getRegister() { return register; } /** * The BB which holds all instructions */ public BasicBlockInterface getBB() { return bb; } /** * The index of the opcode from which the register is tracked backwards. * The index points to the previously found codeline where the search * 'stopped' and this RS was created. * @return */ public int getIndex() { return index; } /** * Was this a fuzzy search. If so, the Results may be (very) inaccurate. * @return 0 for a non-fuzzy search. Higher values means more fuzziness */ public int getFuzzyLevel() { return fuzzyLevel; } /** * The path through the program for this search. * @return the path */ public LinkedList<BasicBlockInterface> getPath() { return path; } /** * This method sets an offset to the fuzzy value. If the offset * plus the fuzzy is too high, the search will be aborted. * @param offset */ public void setFuzzyOffset(int offset) { fuzzyOffset = offset; } public int getFuzzyOffset() { return fuzzyOffset; } } private static boolean checkRsForEquality(RegisterSearch rs1, RegisterSearch rs2) { if (!Arrays.equals(rs1.getRegister(), rs2.getRegister())) return false; if (rs1.getBB() != rs2.getBB()) return false; if (rs1.getIndex() != rs2.getIndex()) return false; return true; } LinkedList<RegisterSearch> regList = new LinkedList<RegisterSearch>(); LinkedList<RegisterSearch> regListDone = new LinkedList<RegisterSearch>(); /** * @param rs * @return * @throws DetectionLogicError */ public boolean addRegisterToTrack(RegisterSearch rs) { if (DEBUG) LOGGER.debug(" -> Add REGISTER: "+new String(rs.getRegister())+", "+rs.getBB().getMethod().getName()+":"+rs.getIndex()+"\tfuzzy="+rs.getFuzzyLevel()+"/"+rs.getFuzzyOffset()+", bb="+rs.getBB().getUniqueId()); if (rs.getFuzzyLevel()+rs.getFuzzyOffset() > MAX_FUZZY_LEVEL) { if (DEBUG) LOGGER.debug(" Maximum fuzzy level reached ("+MAX_FUZZY_LEVEL+"): aborting."); return false; } for (RegisterSearch rs2 : regListDone) { if (checkRsForEquality(rs, rs2)) { if (DEBUG) LOGGER.debug(" Already searched this RS. It will be ignored! (This is ok)"); return false; } } boolean found = false; for (RegisterSearch rs2 : regList) { if (checkRsForEquality(rs, rs2)) { found = true; break; } } if (!found) regList.addFirst(rs); // this way we first work up actual stuff else if (DEBUG) LOGGER.debug(" Duplicate RS added. It will be ignored! (This is ok)"); return true; } public RegisterSearch getNextRegisterToTrack() { if (regList.size() == 0) return null; else { RegisterSearch rs = regList.getFirst(); regList.removeFirst(); regListDone.addLast(rs); // debug only: FIXME int i = rs.getIndex(); if (i<0) i = 0; else if (i>=rs.getBB().getMethod().getCodeLines().size()) { i = rs.getBB().getMethod().getCodeLines().size()-1; } CodeLineInterface cl = rs.getBB().getMethod().getCodeLines().get(i); if (DEBUG) LOGGER.debug("\n\n-> TRACKING REGISTER: "+new String(rs.getRegister())+", "+rs.getBB().getMethod().getSmaliClass().getFullClassName(true)+"."+rs.getBB().getMethod().getName()+":"+cl.getLineNr()+"\tfuzzy="+rs.getFuzzyLevel()+"/"+rs.getFuzzyOffset()); // /debug only return rs; } } public int getRemainingRegistersToTrack() { return regList.size(); } // // private HashSet<ClassContentTracker> arrayMap = new HashSet<ClassContentTracker>(); private HashSet<ClassContentTracker> arrayMapDone = new HashSet<ClassContentTracker>(); /** * A helper class to identify content of a given class. This may be * an array name, a method or a field name. */ public static class ClassContentTracker { private final byte[][] ci; private final int fuzzyLevel; private final int fuzzyLevelOffset; private LinkedList<BasicBlockInterface> path; /** * This is a helper class to wrap some content. It can contain arbitrary data, * but the first entry of the first parameter must always be the full class * name and the second one the identifier of a method, array or field. * Further entries are optional and can contain, eg, the parameters for a * method and it's return value. * @param ci [class, identifier, more optional] * @param fuzzyLevel * @param fuzzyLevelOffset the offset to the fuzzyLevel * @param create a new object, you'll get inconsistencies if you'll reuse the same object */ public ClassContentTracker(byte[][] ci, int fuzzyLevel, int fuzzyLevelOffset, LinkedList<BasicBlockInterface> path2) { this.ci = ci; this.fuzzyLevel = fuzzyLevel; this.fuzzyLevelOffset = fuzzyLevelOffset; this.path = path2; } /** * Get the class and identifier of the class content (array, method or field name) * and other optional data. * @return ci, ci[0]=full class name, ci[1]=identifier (ci[2+] are optional and can contain more data) */ public byte[][] getCi() { return ci; } /** * Is the search fuzzy, that means inaccurate? * @return 0 if it is not fuzzy, or a number > 0 for a fuzzy search */ public int getFuzzyLevel() { return fuzzyLevel; } public int getFuzzyOffset() { return fuzzyLevelOffset; } /** * The path through the program for this search, the list is always a new copy of the original list. * Changes to the returned list will not effect the internal list! * @return the path */ public LinkedList<BasicBlockInterface> getPath() { return new LinkedList<BasicBlockInterface>(path); } @Override public boolean equals(Object other) { if (!(other instanceof ClassContentTracker)) { return false; } // check the arrays for equality if (ci.length != ((ClassContentTracker)other).getCi().length) return false; for (int i=0; i<ci.length; i++) { if (!(Arrays.equals(ci[i], ((ClassContentTracker)other).getCi()[i]))) return false; } return true; } private Integer hashCode = null; @Override public int hashCode() { if (hashCode == null) { byte[] tmp = new byte[0]; for (int i=0; i<ci.length; i++) { tmp = ByteUtils.concatAll(tmp, ci[i]); } hashCode = Arrays.hashCode(tmp); } return hashCode; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("ci="); for (byte[] b : ci) { sb.append(new String(b)); sb.append(" "); } sb.append(", fuzzy="); sb.append(fuzzyLevel); sb.append(", pathLen="); sb.append(path.size()); return sb.toString(); } } /** * Add a new array field instance where accesses to are searched later on. * @param ci: class, array-field-name * @param fuzzyLevel * @param path, create a new object, you'll get inconsistencies if you'll reuse the same object * @throws SyntaxException */ public boolean addArrayFieldToTrack(byte[][] ca, int fuzzyLevel, int fuzzyLevelOffset, LinkedList<BasicBlockInterface> path) throws SyntaxException { if (DEBUG) LOGGER.debug(" -> Add ARRAY FIELD: "+new String(ca[0])+"."+new String(ca[1])+"\tfuzzy="+fuzzyLevel+"/"+fuzzyLevelOffset); if (fuzzyLevel+fuzzyLevelOffset > MAX_FUZZY_LEVEL) { if (DEBUG) LOGGER.debug(" Maximum fuzzy level reached ("+MAX_FUZZY_LEVEL+"): aborting."); return false; } if (fuzzyLevelOffset < MAX_FUZZY_LEVEL-2) { if (DEBUG) LOGGER.debug(" Setting fuzzy offset to "+(MAX_FUZZY_LEVEL-2)); fuzzyLevelOffset = MAX_FUZZY_LEVEL-2; } ClassContentTracker ctt = new ClassContentTracker(ca, fuzzyLevel, fuzzyLevelOffset, path); if (arrayMap.contains(ctt)) return false; if (arrayMapDone.contains(ctt)) return false; arrayMap.add(ctt); return true; } public ClassContentTracker getNextCaToTrack() { ClassContentTracker ctt = null; for (ClassContentTracker ctt2 : arrayMap) { // get one item "at random" ctt = ctt2; break; } arrayMap.remove(ctt); arrayMapDone.add(ctt); if (DEBUG) LOGGER.debug("\n\n-> TRACKING ARRAY FIELD: "+new String(ctt.getCi()[0])+"."+new String(ctt.getCi()[1])+"\tfuzzy="+ctt.getFuzzyLevel()+"/"+ctt.getFuzzyOffset()); return ctt; } public int getRemainingArraysToTrack() { return arrayMap.size(); } public boolean isFinished() { if ( getRemainingRegistersToTrack() > 0 || getRemainingFieldsToTrack() > 0 || getRemainingReturnValuesFromMethods() > 0 || getRemainingArraysToTrack() > 0) return false; else return true; } /** * Returns the amount of finished RS searches. * @return */ public int getFinishedRsCount() { return regListDone.size(); } }