Skip to content

Commit

Permalink
PE analyzer (#2448)
Browse files Browse the repository at this point in the history
* Use pecoff4j to read dll/exe version information metadata on windows

Dll and exe on windows that are not .NET assembly are only analyzed by the filename.
This is often not good enough because the filename can contain other numbers (x86, x64, ...) other than the version.
To improve the situation I've reduced the confidence of the filename parsed version and created a new analyzer
The FileVersionAnalyzer use the pecoff4j library to extract, if possible, the version from the file metadata

* updated PR #2446 to move this to a more full fledged PE Analyzer retrieving more than just the version number from the PE Headers

Co-authored-by: Amodio <[email protected]>
  • Loading branch information
jeremylong and pscamodio authored Feb 1, 2020
1 parent a30e04d commit 63c5489
Show file tree
Hide file tree
Showing 18 changed files with 423 additions and 10 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ velocity.log
/core/data/

# Apple macOS operating system Desktop Services Store files
.DS_Store
.DS_Store
/.vscode
/maven/.factorypath
1 change: 1 addition & 0 deletions cli/src/test/resources/sample.properties
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ analyzer.dependencybundling.enabled=true
analyzer.dependencymerging.enabled=true
analyzer.falsepositive.enabled=true
analyzer.filename.enabled=true
analyzer.pe.enabled=true
analyzer.hint.enabled=true
analyzer.nvdcve.enabled=true
analyzer.vulnerabilitysuppression.enabled=true
Expand Down
1 change: 1 addition & 0 deletions cli/src/test/resources/sample2.properties
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ analyzer.dependencybundling.enabled=false
analyzer.dependencymerging.enabled=false
analyzer.falsepositive.enabled=false
analyzer.filename.enabled=false
analyzer.pe.enabled=false
analyzer.hint.enabled=false
analyzer.nvdcve.enabled=false
analyzer.vulnerabilitysuppression.enabled=false
Expand Down
4 changes: 4 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ Copyright (c) 2012 Jeremy Long. All Rights Reserved.
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.whitesource</groupId>
<artifactId>pecoff4j</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jcs-core</artifactId>
Expand Down
1 change: 1 addition & 0 deletions core/src/main/java/org/owasp/dependencycheck/Engine.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public enum Mode {
INITIAL,
PRE_INFORMATION_COLLECTION,
INFORMATION_COLLECTION,
INFORMATION_COLLECTION2,
POST_INFORMATION_COLLECTION
),
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public enum AnalysisPhase {
* Information collection phase.
*/
INFORMATION_COLLECTION,
/**
* Information collection phase 2.
*/
INFORMATION_COLLECTION2,
/**
* Post information collection phase.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ public void analyzeDependency(Dependency dependency, Engine engine) throws Analy
dependency.addEvidence(EvidenceType.VERSION, "grokassembly", "ProductVersion", data.getProductVersion(), Confidence.HIGHEST);
}
if (!StringUtils.isEmpty(data.getFileVersion())) {
dependency.addEvidence(EvidenceType.VERSION, "grokassembly", "FileVersion", data.getFileVersion(), Confidence.HIGHEST);
dependency.addEvidence(EvidenceType.VERSION, "grokassembly", "FileVersion", data.getFileVersion(), Confidence.HIGH);
}

if (data.getFileVersion() != null && data.getProductVersion() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ protected void analyzeDependency(Dependency dependency, Engine engine) throws An
if (version.getVersionParts() == null || version.getVersionParts().size() < 2) {
dependency.addEvidence(EvidenceType.VERSION, "file", "version", version.toString(), Confidence.MEDIUM);
} else {
dependency.addEvidence(EvidenceType.VERSION, "file", "version", version.toString(), Confidence.HIGHEST);
dependency.addEvidence(EvidenceType.VERSION, "file", "version", version.toString(), Confidence.HIGH);
}
dependency.addEvidence(EvidenceType.VERSION, "file", "name", packageName, Confidence.MEDIUM);
}
Expand Down
282 changes: 282 additions & 0 deletions core/src/main/java/org/owasp/dependencycheck/analyzer/PEAnalyzer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
/*
* This file is part of dependency-check-core.
*
* 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.
*
* Copyright (c) 2020 Jeremy Long. All Rights Reserved.
*/
package org.owasp.dependencycheck.analyzer;

import com.github.packageurl.MalformedPackageURLException;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;

import org.boris.pecoff4j.PE;
import org.boris.pecoff4j.ResourceDirectory;
import org.boris.pecoff4j.ResourceEntry;
import org.boris.pecoff4j.constant.ResourceType;
import org.boris.pecoff4j.io.PEParser;
import org.boris.pecoff4j.io.ResourceParser;
import org.boris.pecoff4j.resources.StringFileInfo;
import org.boris.pecoff4j.resources.StringTable;
import org.boris.pecoff4j.resources.VersionInfo;
import org.boris.pecoff4j.util.ResourceHelper;

import javax.annotation.concurrent.ThreadSafe;
import org.apache.commons.lang3.StringUtils;

import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.dependency.Confidence;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.dependency.Evidence;
import org.owasp.dependencycheck.dependency.EvidenceType;
import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
import org.owasp.dependencycheck.exception.InitializationException;
import org.owasp.dependencycheck.utils.DependencyVersion;
import org.owasp.dependencycheck.utils.DependencyVersionUtil;
import org.owasp.dependencycheck.utils.FileFilterBuilder;
import org.owasp.dependencycheck.utils.FileUtils;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Takes a dependency and analyze the PE header for meta data that can be used
* to identify the library.
*
* @author Amodio Pesce
*/
@ThreadSafe
@Experimental
public class PEAnalyzer extends AbstractFileTypeAnalyzer {

//<editor-fold defaultstate="collapsed" desc="All standard implementation details of Analyzer">
/**
* Logger
*/
private static final Logger LOGGER = LoggerFactory.getLogger(AssemblyAnalyzer.class);
/**
* The name of the analyzer.
*/
private static final String ANALYZER_NAME = "PE Analyzer";

/**
* The phase that this analyzer is intended to run in.
*/
private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION2;
/**
* The set of file extensions supported by this analyzer.
*/
private static final String[] EXTENSIONS = {"exe", "dll"};

/**
* The file filter used to determine which files this analyzer supports.
*/
private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build();
/**
* A descriptor for the type of dependencies processed or added by this
* analyzer.
*/
public static final String DEPENDENCY_ECOSYSTEM = "native";

/**
* Returns the name of the analyzer.
*
* @return the name of the analyzer.
*/
@Override
public String getName() {
return ANALYZER_NAME;
}

/**
* Returns the phase that the analyzer is intended to run in.
*
* @return the phase that the analyzer is intended to run in.
*/
@Override
public AnalysisPhase getAnalysisPhase() {
return ANALYSIS_PHASE;
}

/**
* <p>
* Returns the setting key to determine if the analyzer is enabled.</p>
*
* @return the key for the analyzer's enabled property
*/
@Override
protected String getAnalyzerEnabledSettingKey() {
return Settings.KEYS.ANALYZER_PE_ENABLED;
}

/**
* Returns the FileFilter.
*
* @return the FileFilter
*/
@Override
protected FileFilter getFileFilter() {
return FILTER;
}

@Override
protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
//nothing to prepare
}

/**
* Collects information about the file name.
*
* @param dependency the dependency to analyze.
* @param engine the engine that is scanning the dependencies
* @throws AnalysisException is thrown if there is an error analyzing the PE
* file.
*/
@Override
protected void analyzeDependency(final Dependency dependency, final Engine engine) throws AnalysisException {
for (Evidence e : dependency.getEvidence()) {
if ("grokassembly".equals(e.getSource())) {
LOGGER.debug("Skipping {} because it was already analyzed by the Assembly Analyzer", dependency.getFileName());
return;
}
}
try {
final File fileToCheck = dependency.getActualFile();
final PE pe = PEParser.parse(fileToCheck.getPath());
final ResourceDirectory rd = pe.getImageData().getResourceTable();
final ResourceEntry[] entries = ResourceHelper.findResources(rd, ResourceType.VERSION_INFO);
for (ResourceEntry entrie : entries) {
final byte[] data = entrie.getData();
final VersionInfo version = ResourceParser.readVersionInfo(data);
final StringFileInfo strings = version.getStringFileInfo();
final StringTable table = strings.getTable(0);
String pVersion = null;
String fVersion = null;

for (int j = 0; j < table.getCount(); j++) {
final String key = table.getString(j).getKey();
final String value = table.getString(j).getValue();
switch (key) {
case "ProductVersion":
dependency.addEvidence(EvidenceType.VERSION, "PE Header", "ProductVersion", value, Confidence.HIGHEST);
pVersion = value;
break;
case "CompanyName":
dependency.addEvidence(EvidenceType.VENDOR, "PE Header", "CompanyName", value, Confidence.HIGHEST);
break;
case "FileVersion":
dependency.addEvidence(EvidenceType.VERSION, "PE Header", "FileVersion", value, Confidence.HIGH);
fVersion = value;
break;
case "InternalName":
dependency.addEvidence(EvidenceType.PRODUCT, "PE Header", "InternalName", value, Confidence.MEDIUM);
determineDependencyName(dependency, value);
break;
case "LegalCopyright":
dependency.addEvidence(EvidenceType.VENDOR, "PE Header", "LegalCopyright", value, Confidence.HIGHEST);
if (dependency.getLicense() != null && dependency.getLicense().length() > 0) {
dependency.setLicense(dependency.getLicense() + "/n/nLegal Copyright: " + value);
} else {
dependency.setLicense("Legal Copyright: " + value);
}
break;
case "OriginalFilename":
dependency.addEvidence(EvidenceType.VERSION, "PE Header", "OriginalFilename", value, Confidence.MEDIUM);
determineDependencyName(dependency, value);
break;
case "ProductName":
dependency.addEvidence(EvidenceType.PRODUCT, "PE Header", "ProductName", value, Confidence.HIGHEST);
determineDependencyName(dependency, value);
break;
}
if (fVersion != null && pVersion != null) {
final int max = fVersion.length() > pVersion.length() ? pVersion.length() : fVersion.length();
int pos;
for (pos = 0; pos < max; pos++) {
if (fVersion.charAt(pos) != pVersion.charAt(pos)) {
break;
}
}
final DependencyVersion fileVersion = DependencyVersionUtil.parseVersion(fVersion, true);
final DependencyVersion productVersion = DependencyVersionUtil.parseVersion(pVersion, true);
if (pos > 0) {
final DependencyVersion matchingVersion = DependencyVersionUtil.parseVersion(fVersion.substring(0, pos), true);
if (fileVersion != null && fileVersion.toString().length() == fVersion.length()) {
if (matchingVersion != null && matchingVersion.getVersionParts().size() > 2) {
dependency.addEvidence(EvidenceType.VERSION, "PE Header", "FilteredVersion",
matchingVersion.toString(), Confidence.HIGHEST);
dependency.setVersion(matchingVersion.toString());
}
}
}
if (dependency.getVersion() == null) {
if (fVersion.length() >= pVersion.length()) {
if (fileVersion != null && fileVersion.toString().length() == fVersion.length()) {
dependency.setVersion(fileVersion.toString());
} else if (productVersion != null && productVersion.toString().length() == pVersion.length()) {
dependency.setVersion(productVersion.toString());
}
} else {
if (productVersion != null && productVersion.toString().length() == pVersion.length()) {
dependency.setVersion(productVersion.toString());
} else if (fileVersion != null && fileVersion.toString().length() == fVersion.length()) {
dependency.setVersion(fileVersion.toString());
}
}
}
} else if (pVersion != null) {
final DependencyVersion productVersion = DependencyVersionUtil.parseVersion(pVersion, true);
if (productVersion != null && dependency.getActualFile().getName().contains(productVersion.toString())) {
dependency.setVersion(productVersion.toString());
}
} else if (fVersion != null) {
final DependencyVersion fileVersion = DependencyVersionUtil.parseVersion(fVersion, true);
if (fileVersion != null && dependency.getActualFile().getName().contains(fileVersion.toString())) {
dependency.setVersion(fileVersion.toString());
}
}
if (dependency.getName() != null && dependency.getVersion() != null) {
try {
dependency.addSoftwareIdentifier(new PurlIdentifier("generic", dependency.getName(), dependency.getVersion(), Confidence.MEDIUM));
} catch (MalformedPackageURLException ex) {
LOGGER.debug("Unable to create Package URL Identifier for " + dependency.getName(), ex);
dependency.addSoftwareIdentifier(new GenericIdentifier(
String.format("%s@%s", dependency.getName(), dependency.getVersion()),
Confidence.MEDIUM));
}
}
if (dependency.getEcosystem() == null) {//this could be an assembly
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
}
}
}
} catch (IOException ex) {
throw new AnalysisException(ex);
}
}

private void determineDependencyName(final Dependency dependency, final String value) {
if (dependency.getName() == null && StringUtils.containsIgnoreCase(dependency.getActualFile().getName(), value)) {
final String ext = FileUtils.getFileExtension(value);
if (ext != null) {
dependency.setName(value.substring(0, value.length() - ext.length() - 1));
} else {
dependency.setName(value);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,18 @@ public synchronized Set<Evidence> getEvidence(EvidenceType type) {
return null;
}

/**
* Returns the unmodifiable set of evidence.
*
* @return the unmodifiable set of evidence
*/
public synchronized Set<Evidence> getEvidence() {
Set e = new HashSet<>(vendors);
e.addAll(products);
e.addAll(versions);
return Collections.unmodifiableSet(e);
}

/**
* Tests if the evidence collection contains the given evidence.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
org.owasp.dependencycheck.analyzer.ArchiveAnalyzer
org.owasp.dependencycheck.analyzer.FileNameAnalyzer
org.owasp.dependencycheck.analyzer.PEAnalyzer
org.owasp.dependencycheck.analyzer.JarAnalyzer
org.owasp.dependencycheck.analyzer.HintAnalyzer
org.owasp.dependencycheck.analyzer.CPEAnalyzer
Expand Down
1 change: 1 addition & 0 deletions core/src/main/resources/dependencycheck.properties
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ analyzer.dependencybundling.enabled=true
analyzer.dependencymerging.enabled=true
analyzer.falsepositive.enabled=true
analyzer.filename.enabled=true
analyzer.pe.enabled=true
analyzer.hint.enabled=true
analyzer.nvdcve.enabled=true
analyzer.vulnerabilitysuppression.enabled=true
Expand Down
Loading

0 comments on commit 63c5489

Please sign in to comment.