/** * Find Security Bugs * Copyright (c) Philippe Arteau, All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3.0 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. */ package com.h3xstream.findsecbugs.password; import edu.umd.cs.findbugs.BugInstance; import edu.umd.cs.findbugs.BugReporter; import edu.umd.cs.findbugs.OpcodeStack; import edu.umd.cs.findbugs.Priorities; import edu.umd.cs.findbugs.ba.XField; import edu.umd.cs.findbugs.bcel.OpcodeStackDetector; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.classfile.Method; /** * General detector for hard coded passwords and cryptographic keys * * @author David Formanek (Y Soft Corporation, a.s.) */ @OpcodeStack.CustomUserValue public class ConstantPasswordDetector extends OpcodeStackDetector { private static final String HARD_CODE_PASSWORD_TYPE = "HARD_CODE_PASSWORD"; private static final String HARD_CODE_KEY_TYPE = "HARD_CODE_KEY"; private final BugReporter bugReporter; private boolean staticInitializerSeen = false; // configuration file with password methods private static final String CONFIG_DIR = "password-methods"; private static final String METHODS_FILENAME = "password-methods-all.txt"; // full method names private static final String GET_BYTES_STRING = "java/lang/String.getBytes(Ljava/lang/String;)[B"; private static final String GET_BYTES = "java/lang/String.getBytes()[B"; private static final String TO_CHAR_ARRAY = "java/lang/String.toCharArray()[C"; private static final String BIGINTEGER_CONSTRUCTOR_STRING = "java/math/BigInteger.<init>(Ljava/lang/String;)V"; private static final String BIGINTEGER_CONSTRUCTOR_STRING_RADIX = "java/math/BigInteger.<init>(Ljava/lang/String;I)V"; private static final String BIGINTEGER_CONSTRUCTOR_BYTE = "java/math/BigInteger.<init>([B)V"; private static final String BIGINTEGER_BYTE_SIGNUM = "java/math/BigInteger.<init>(I[B)V"; // suspicious variable names with password or keys private static final String PASSWORD_NAMES = ".*(pass|pwd|psw|secret|key|cipher|crypt|des|aes|mac|private|sign|cert).*"; private static final Pattern PASSWORD_PATTERN = Pattern.compile(PASSWORD_NAMES, Pattern.CASE_INSENSITIVE); private final Map<String, Collection<Integer>> sinkMethods = new HashMap<String, Collection<Integer>>(); private boolean isFirstArrayStore = false; private boolean wasToConstArrayConversion = false; private static final Set<String> hardCodedFields = new HashSet<String>(); private static final Set<String> reportedFields = new HashSet<String>(); private String calledMethod = null; public ConstantPasswordDetector(BugReporter bugReporter) { this.bugReporter = bugReporter; try { loadMap(METHODS_FILENAME, sinkMethods, "#"); } catch (IOException ex) { throw new RuntimeException("cannot load resources", ex); } } @Override public void visit(JavaClass javaClass) { staticInitializerSeen = false; Method[] methods = javaClass.getMethods(); for (Method method : methods) { if (method.getName().equals(STATIC_INITIALIZER_NAME)) { // check field initialization before visiting methods doVisitMethod(method); staticInitializerSeen = true; break; } } isFirstArrayStore = false; wasToConstArrayConversion = false; } @Override public void visitAfter(JavaClass obj) { Collection<String> fieldsToReport = new ArrayList<String>(); for (String field : hardCodedFields) { if (isSuspiciousName(field, obj) && !reportedFields.contains(field)) { fieldsToReport.add(field); } } reportBugSource(fieldsToReport, Priorities.NORMAL_PRIORITY); // TODO global analysis hardCodedFields.clear(); reportedFields.clear(); super.visitAfter(obj); } private static boolean isSuspiciousName(String fullFieldName, JavaClass obj) { int classNameLength = obj.getClassName().length(); // do not search pattern in class name (signature not important) String fieldName = fullFieldName.substring(classNameLength); return PASSWORD_PATTERN.matcher(fieldName).matches(); } @Override public void visit(Method method) { isFirstArrayStore = false; wasToConstArrayConversion = false; } @Override public void sawOpcode(int seen) { if (isAlreadyAnalyzed()) { return; } markHardCodedItemsFromFlow(); if (seen == NEWARRAY) { isFirstArrayStore = true; } if (isStoringToArray(seen)) { markArraysHardCodedOrNot(); isFirstArrayStore = false; } if (wasToConstArrayConversion) { markTopItemHardCoded(); wasToConstArrayConversion = false; } if (seen == PUTFIELD || seen == PUTSTATIC) { saveArrayFieldIfHardCoded(); } if (isInvokeInstruction(seen)) { calledMethod = getCalledMethodName(); wasToConstArrayConversion = isToConstArrayConversion(); markBigIntegerHardCodedOrNot(); reportBadSink(); } } private boolean isAlreadyAnalyzed() { return getMethodName().equals(STATIC_INITIALIZER_NAME) && staticInitializerSeen; } private void markHardCodedItemsFromFlow() { for (int i = 0; i < stack.getStackDepth(); i++) { OpcodeStack.Item stackItem = stack.getStackItem(i); if ((stackItem.getConstant() != null || stackItem.isNull()) && !stackItem.getSignature().startsWith("[")) { setHardCodedItem(stackItem); } if (hasHardCodedFieldSource(stackItem)) { setHardCodedItem(stackItem); } } } private boolean hasHardCodedFieldSource(OpcodeStack.Item stackItem) { XField xField = stackItem.getXField(); if (xField == null) { return false; } String[] split = xField.toString().split(" "); int length = split.length; if (length < 2) { return false; } String fieldSignature = split[length - 1]; if (!isSupportedSignature(fieldSignature)) { return false; } String fieldName = split[length - 2] + fieldSignature; return hardCodedFields.contains(fieldName); } private static boolean isStoringToArray(int seen) { return seen == CASTORE || seen == BASTORE || seen == SASTORE || seen == IASTORE; } private void markArraysHardCodedOrNot() { if (hasHardCodedStackItem(0) && hasHardCodedStackItem(1)) { if (isFirstArrayStore) { setHardCodedItem(2); } } else { // then array not hard coded stack.getStackItem(2).setUserValue(null); } } private void markTopItemHardCoded() { assert stack.getStackDepth() > 0; setHardCodedItem(0); } private void saveArrayFieldIfHardCoded() { String fieldSignature = getSigConstantOperand(); if (isSupportedSignature(fieldSignature) && hasHardCodedStackItem(0) && !stack.getStackItem(0).isNull()) { String fieldName = getFullFieldName(); hardCodedFields.add(fieldName); } } private static boolean isInvokeInstruction(int seen) { return seen >= INVOKEVIRTUAL && seen <= INVOKEINTERFACE; } private boolean isToConstArrayConversion() { return isInMethodWithConst(TO_CHAR_ARRAY, 0) || isInMethodWithConst(GET_BYTES, 0) || isInMethodWithConst(GET_BYTES_STRING, 1); } private void markBigIntegerHardCodedOrNot() { if (isInMethodWithConst(BIGINTEGER_CONSTRUCTOR_STRING, 0) || isInMethodWithConst(BIGINTEGER_CONSTRUCTOR_BYTE, 0)) { setHardCodedItem(1); } else if (isInMethodWithConst(BIGINTEGER_CONSTRUCTOR_STRING_RADIX, 1) || isInMethodWithConst(BIGINTEGER_BYTE_SIGNUM, 0)) { setHardCodedItem(2); } } private void reportBadSink() { if (!sinkMethods.containsKey(calledMethod)) { return; } Collection<Integer> offsets = sinkMethods.get(calledMethod); Collection<Integer> offsetsToReport = new ArrayList<Integer>(); for (Integer offset : offsets) { if (hasHardCodedStackItem(offset) && !stack.getStackItem(offset).isNull()) { offsetsToReport.add(offset); String sourceField = getStackFieldName(offset); if (sourceField != null) { reportedFields.add(sourceField); } } } if (!offsetsToReport.isEmpty()) { reportBugSink(Priorities.HIGH_PRIORITY, offsets); } } private String getStackFieldName(int offset) { XField xField = stack.getStackItem(offset).getXField(); if (xField == null) { return null; } String[] split = xField.toString().split(" "); if (split.length < 2) { return null; } return split[split.length - 2] + split[split.length - 1]; } private void reportBugSink(int priority, Collection<Integer> offsets) { String bugType = HARD_CODE_KEY_TYPE; for (Integer paramIndex : offsets) { OpcodeStack.Item stackItem = stack.getStackItem(paramIndex); String signature = stackItem.getSignature(); if ("Ljava/lang/String;".equals(signature) || "[C".equals(signature)) { bugType = HARD_CODE_PASSWORD_TYPE; break; } } BugInstance bugInstance = new BugInstance(this, bugType, priority) .addClass(this).addMethod(this) .addSourceLine(this).addCalledMethod(this); for (Integer paramIndex : offsets) { OpcodeStack.Item stackItem = stack.getStackItem(paramIndex); bugInstance.addParameterAnnotation(paramIndex, "Hard coded parameter number (in reverse order) is") .addFieldOrMethodValueSource(stackItem); Object constant = stackItem.getConstant(); if (constant != null) { bugInstance.addString(constant.toString()); } } bugReporter.reportBug(bugInstance); } private void reportBugSource(Collection<String> fields, int priority) { if (fields.isEmpty()) { return; } String bugType = HARD_CODE_KEY_TYPE; for (String field : fields) { if (field.endsWith("[C")) { bugType = HARD_CODE_PASSWORD_TYPE; break; } } BugInstance bug = new BugInstance(this, bugType, priority).addClass(this); for (String field : fields) { bug.addString("is hard coded in field " + field + " with suspicious name"); } bugReporter.reportBug(bug); } private void setHardCodedItem(int stackOffset) { setHardCodedItem(stack.getStackItem(stackOffset)); } private void setHardCodedItem(OpcodeStack.Item stackItem) { stackItem.setUserValue(Boolean.TRUE); } private boolean hasHardCodedStackItem(int stackOffset) { return stack.getStackItem(stackOffset).getUserValue() != null; } private boolean isInMethodWithConst(String method, int stackOffset) { return method.equals(calledMethod) && hasHardCodedStackItem(stackOffset); } private String getFullFieldName() { String fieldName = getDottedClassConstantOperand() + "." + getNameConstantOperand() + getSigConstantOperand(); return fieldName; } private static boolean isSupportedSignature(String signature) { return "[C".equals(signature) || "[B".equals(signature) || "Ljava/math/BigInteger;".equals(signature); } private String getCalledMethodName() { String methodNameWithSignature = getNameConstantOperand() + getSigConstantOperand(); return getClassConstantOperand() + "." + methodNameWithSignature; } private void loadMap(String filename, Map<String, Collection<Integer>> map, String separator) throws IOException { BufferedReader reader = null; try { reader = getReader(filename); for (;;) { String line = reader.readLine(); if (line == null) { break; } line = line.trim(); if (line.isEmpty()) { continue; } String[] tuple = line.split(separator); int count = tuple.length - 1; Collection<Integer> parameters = new ArrayList<Integer>(count); for (int i = 0; i < count; i++) { parameters.add(Integer.parseInt(tuple[i + 1])); } map.put(tuple[0], parameters); } } finally { if (reader != null) { reader.close(); } } } private BufferedReader getReader(String filename) { String path = CONFIG_DIR + "/" + filename; return new BufferedReader(new InputStreamReader( getClass().getClassLoader().getResourceAsStream(path) )); } }