/* 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.application;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import org.apache.log4j.Logger;
import de.rub.syssec.saaf.analysis.steps.obfuscation.Entropy;
import de.rub.syssec.saaf.application.instructions.Instruction;
import de.rub.syssec.saaf.application.methods.Method;
import de.rub.syssec.saaf.misc.ByteUtils;
import de.rub.syssec.saaf.model.application.ApplicationInterface;
import de.rub.syssec.saaf.model.application.ClassInterface;
import de.rub.syssec.saaf.model.application.CodeLineInterface;
import de.rub.syssec.saaf.model.application.DetectionLogicError;
import de.rub.syssec.saaf.model.application.FieldInterface;
import de.rub.syssec.saaf.model.application.MethodInterface;
import de.rub.syssec.saaf.model.application.PackageInterface;
import de.rub.syssec.saaf.model.application.SmaliClassError;
import de.rub.syssec.saaf.model.application.instruction.InstructionType;
/**
* This class represents a parsed SMALI file on the disk with all methods, fields and so on.
* This class always represents one SMALI file with its parsed bytecode.
*/
public class SmaliClass implements ClassInterface {
private final File smaliFile;
private final ApplicationInterface app;
private static final boolean DEBUG=Boolean.parseBoolean(System.getProperty("debug.slicing","false"));
private PackageInterface javaPackage;
private LinkedList<CodeLineInterface> codeLineList = new LinkedList<CodeLineInterface>();
private LinkedList<MethodInterface> methodList = new LinkedList<MethodInterface>();
private LinkedList<MethodInterface> emptyMethodList = new LinkedList<MethodInterface>();
private LinkedList<FieldInterface> fieldList = new LinkedList<FieldInterface>();
private String fuzzyHash = null; //TODO: Implement
private int id = -1; //ID from the table in db
private static final byte[] IMPLEMENTS = ".implements ".getBytes();
private static final byte[] SUPER = ".super ".getBytes();
private static final byte[] CLASS = ".class ".getBytes();
private static final byte[] SOURCE = ".source ".getBytes();
private static final Logger LOGGER = Logger.getLogger(SmaliClass.class);
private String superClass = null;
private String sourceFile = null;
private final HashSet<String> implementedInterfaces = new HashSet<String>();
private static final int MAXIMAL_SMALI_FILE_SIZE = 1024 * 1024 * 100; // 100mb
private int size = 0;
private final int label;
private boolean inAdFramework=false;
private boolean changed;
private boolean obfuscated;
private Entropy entropy;
/**
* The parsed SMALI file.
*
* @param smaliFile the corresponding file
* @param app the app
* @param label the unique label of the SMALI file within an application
* @throws IOException is some IO error occurred
* @throws DetectionLogicError if the BBs could not be correctly labeled
* @throws SmaliClassError
*/
public SmaliClass(File smaliFile, ApplicationInterface app, int label) throws IOException, DetectionLogicError, SmaliClassError {
this.smaliFile = smaliFile;
this.app = app;
this.label = label;
this.javaPackage = new JavaPackage(app);
if (DEBUG) LOGGER.debug("Parsing SMALI code for file "+smaliFile.getName());
parse();
this.changed = true;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getFile()
*/
@Override
public File getFile() {
return smaliFile;
}
/**
* Parse the codelines.
* @throws IOException
* @throws DetectionLogicError if the BBs could not be correctly labeled
* @throws SmaliClassError
*/
private void parse() throws IOException, DetectionLogicError, SmaliClassError {
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
int lineNr = 1;
fis = new FileInputStream(smaliFile);
bis = new BufferedInputStream(fis);
byte[] line;
while ((line = ByteUtils.parseLine(bis, 256000)) != null) { // 250k
codeLineList.addLast(new CodeLine(line, lineNr++, this));
size += line.length;
if (size > MAXIMAL_SMALI_FILE_SIZE) throw new IOException("Maximum SMALI file size of "+MAXIMAL_SMALI_FILE_SIZE+" bytes exceeded!");
}
}
// catch (Exception e) {
// LOGGER.logError(SmaliClass.class, "Could not read file: "+smaliFile.getAbsolutePath()+": "+e.getMessage());
// e.printStackTrace();
// }
finally {
try { if (bis != null) bis.close(); } catch (Exception e) { /*ignore*/ }
try { if (fis != null) fis.close(); } catch (Exception e) { /*ignore*/ }
}
boolean insideMethod = false;
LinkedList<CodeLineInterface> blockedCodeLines = new LinkedList<CodeLineInterface>();
/**
* All codelines not belonging to a method.
* Fields, enums, Annotations etc
*/
LinkedList<CodeLineInterface> otherCL = new LinkedList<CodeLineInterface>();
final byte[] START_METHOD = ".method ".getBytes();
final byte[] END_METHOD = ".end method".getBytes();
/*
* TODO:
* :array_0
* .array-data 0x1
* 0x78t <- is wrongly parsed b/c it is assumed to be an instruction, should also not be put into the BB
* 0x79t <- see above
* 0x7at <- see above
* .end array-data
*/
int methodLabel = 0;
for (CodeLineInterface cl : codeLineList) {
if (cl.isEmpty()) continue; // skip empty lines
if (insideMethod) {
if (ByteUtils.startsWith(cl.getLine(), END_METHOD)) {
// append, store method
blockedCodeLines.addLast(cl);
Method m = new Method(blockedCodeLines, this, methodLabel++); // save
if (DEBUG) LOGGER.debug("> Parsing instructions/opcodes for method '"+m.getName()+"'");
for (CodeLineInterface mcl : blockedCodeLines) {
mcl.setMethod(m); // set a reference to the method for later and faster access
mcl.getInstruction().parseOpCode();
}
//TODO: do better, added generateBBs Method to Method.java, which will now generate the BBs instead of directly
//generating the blocks at construction time, this should be the only place where this call is currently necessary
m.generateBBs();
/*
* This is a "fix" for empty methods. Otherwise, this happens:
* Method.getFirstBasicBlock w/ this content
* .method public abstract PpNzwq9T()Ljava/util/List;
* .end method
* produces a java.util.NoSuchElementException.
*/
if (!m.getBasicBlocks().isEmpty()) methodList.addLast(m);
else emptyMethodList.addLast(m);
blockedCodeLines = new LinkedList<CodeLineInterface>(); // reset
insideMethod = false;
}
else { // do not append .line to the method
if(!ByteUtils.startsWith(cl.getLine(), ".line".getBytes()))
blockedCodeLines.addLast(cl);
}
}
else {
if (ByteUtils.startsWith(cl.getLine(), START_METHOD)) {
// new block and append
blockedCodeLines.addLast(cl); // either still empty or reseted in END
insideMethod = true;
}
else {
// append to otherCL
otherCL.addLast(cl);
}
}
}
fieldList = Field.parseAllFields(otherCL);
// parse implements, class and super lines
for (CodeLineInterface cl : otherCL) {
if (cl.startsWith(SUPER)) { // .super Landroid/app/Activity;
byte[] tmp = Instruction.split(cl.getLine()).getLast();
superClass = new String(ByteUtils.subbytes(tmp, 1, tmp.length-1)).replace("/", ".");
}
else if (cl.startsWith(IMPLEMENTS)) { // .implements Ljava/io/Serializable;
byte[] tmp = Instruction.split(cl.getLine()).getLast();
implementedInterfaces.add(new String(ByteUtils.subbytes(tmp, 1, tmp.length-1)).replace("/", "."));
}
else if (cl.startsWith(CLASS)) { // .class public Ltest/android/AndroidTestActivity;
byte[] tmp = Instruction.split(cl.getLine()).getLast();
List<String> packageNames = new ArrayList<String>();
String x[] = new String(ByteUtils.subbytes(tmp, 1, tmp.length-1)).split("/");
for (int i = 0; i<x.length-1; i++) { // do not include the class name
packageNames.add(x[i]);
}
this.javaPackage.setName(packageNames);
} else if (cl.startsWith(SOURCE)) {// .source "MagicSMSActivity.java"
byte[] tmp = Instruction.split(cl.getLine()).getLast();
sourceFile = new String(ByteUtils.subbytes(tmp, 1, tmp.length-1));
}
}
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getMethods()
*/
@Override
public LinkedList<MethodInterface> getMethods() {
return methodList;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getEmptyMethods()
*/
@Override
public LinkedList<MethodInterface> getEmptyMethods() {
return emptyMethodList;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getAllCodeLines()
*/
@Override
public LinkedList<CodeLineInterface> getAllCodeLines() {
return codeLineList;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getAllCodeLine(de.rub.syssec.saaf.application.instructions.InstructionMap.InstructionType)
*/
@Override
public LinkedList<CodeLineInterface> getAllCodeLine(InstructionType ... types) {
LinkedList<CodeLineInterface> ret = new LinkedList<CodeLineInterface>();
for (CodeLineInterface cl : getAllCodeLines()) {
for (InstructionType type : types) {
if (cl.getInstruction().getType() == type) ret.addLast(cl);
}
}
return ret;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getAllFields()
*/
@Override
public Collection<FieldInterface> getAllFields() {
return Collections.unmodifiableCollection(fieldList);
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getLinesOfCode()
*/
@Override
public int getLinesOfCode() {
return codeLineList.size();
}
// /* (non-Javadoc)
// * @see de.rub.syssec.saaf.application.ClassInterface#getSha1()
// */
// @Override
// public String getSha1() throws NoSuchAlgorithmException, IOException {
// if (sha1Hash == null) sha1Hash = Hash.calculateHash(Digest.SHA1, smaliFile);
// return sha1Hash;
// }
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#searchPattern(byte[], de.rub.syssec.saaf.application.SmaliClass.SearchType)
*/
@Override
public LinkedList<CodeLineInterface> searchPattern(byte[] pattern, SearchType searchType) {
LinkedList<CodeLineInterface> results = new LinkedList<CodeLineInterface>();
for (CodeLineInterface cl : codeLineList) {
switch (searchType) {
case INSTRUCTIONS_ONLY:
if (!cl.isCode()) continue;
break;
case NON_INSTRUCTIONS_ONLY:
if (cl.isCode()) continue;
break;
case INSTRUCTIONS_AND_NON_INSTRUCTIONS:
if (cl.isEmpty()) continue;
break;
default:
break;
}
if (cl.contains(pattern)) results.addLast(cl);
}
return results;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#searchPattern(byte[], de.rub.syssec.saaf.application.instructions.InstructionMap.InstructionType)
*/
@Override
public LinkedList<CodeLineInterface> searchPattern(byte[] pattern, InstructionType ... types) {
LinkedList<CodeLineInterface> results = new LinkedList<CodeLineInterface>();
for (CodeLineInterface cl : codeLineList) {
for (InstructionType type : types) {
if (type == cl.getInstruction().getType()) {
results.add(cl);
}
}
}
return results;
}
//################# getter and setter ##################################
@Override
public int getId() {
return id;
}
@Override
public void setId(int id) {
this.id = id;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getPackageId()
*/
@Override
public int getPackageId() {
return this.javaPackage.getId();
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#setPackageId(int)
*/
@Override
public void setPackageId(int packageId) {
this.javaPackage.setId(packageId);
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getImplementedInterfaces()
*/
@Override
public Collection<String> getImplementedInterfaces() {
return Collections.unmodifiableCollection(implementedInterfaces);
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getSuperClass()
*/
@Override
public String getSuperClass() {
return superClass;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getPackageName(boolean)
*/
@Override
public String getPackageName(boolean useDots) {
return this.javaPackage.getName(useDots);
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getClassName()
*/
@Override
public String getClassName() {
return smaliFile.getName().replace(".smali", "");
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getFullClassName(boolean)
*/
@Override
public String getFullClassName(boolean useDots) {
String separator;
if (useDots) separator = ".";
else separator = "/";
return getPackageName(useDots) + separator + getClassName();
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#setHash_ssDeep(java.lang.String)
*/
@Override
public void setSsdeepHash(String hash) {
fuzzyHash=hash;
setChanged(true);
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getHash_ssDeep()
*/
@Override
public String getSsdeepHash() {
return fuzzyHash;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getSourceFile()
*/
@Override
public String getSourceFile() {
return sourceFile;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getApplication()
*/
@Override
public ApplicationInterface getApplication() {
return app;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getSize()
*/
@Override
public int getSize() {
return size;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#getUniqueId()
*/
@Override
public String getUniqueId() {
return String.valueOf(label);
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#isInAdFrameworkPackage()
*/
@Override
public boolean isInAdFrameworkPackage() {
// LOGGER.debug("Stub: Ad Check not yet implemented!");
return inAdFramework;
}
/* (non-Javadoc)
* @see de.rub.syssec.saaf.application.ClassInterface#setInAdFramework(boolean)
*/
@Override
public void setInAdFramework(boolean hasAd) {
inAdFramework = hasAd;
setChanged(true);
}
@Override
public PackageInterface getPackage() {
return this.javaPackage;
}
@Override
public void setPackage(PackageInterface javaPackage) {
this.javaPackage=javaPackage;
setChanged(true);
}
@Override
public void setChanged(boolean changed) {
this.changed = changed;
}
@Override
public boolean isChanged() {
return this.changed;
}
@Override
public String getRelativeFile() {
String separator = File.separator;//this is too fix issues with File.separator under windows being "\", which makes the replaceFirst fail, due to \ being a special char in regex
if(separator.equals("\\"))
separator = separator + separator;
return this.smaliFile.getAbsolutePath().replaceFirst(app.getDecompiledContentDir().getAbsolutePath()+separator+"smali"+separator, "");
}
public String toString(){
return getClassName();
}
@Override
public void setObfuscated(boolean b) {
this.obfuscated = b;
}
@Override
public boolean isObfuscated() {
return this.obfuscated;
}
@Override
public void setEntropy(Entropy entropy) {
this.entropy=entropy;
}
@Override
public Entropy getEntropy() {
return this.entropy;
}
}