Skip to content

Commit

Permalink
PE dotnet sbom
Browse files Browse the repository at this point in the history
Signed-off-by: Prabhu Subramanian <[email protected]>
  • Loading branch information
prabhu committed Feb 27, 2024
1 parent e8883d4 commit b7763fb
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 165 deletions.
8 changes: 3 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM almalinux:9.3-minimal
FROM ghcr.io/appthreat/base-lang:main

LABEL maintainer="appthreat" \
org.opencontainers.image.authors="Team AppThreat <[email protected]>" \
Expand All @@ -11,14 +11,12 @@ LABEL maintainer="appthreat" \
org.opencontainers.image.description="BLint is a Binary Linter and SBOM generator." \
org.opencontainers.docker.cmd="docker run --rm -it -v /tmp:/tmp -v $(pwd):/app:rw -w /app -t ghcr.io/owasp-dep-scan/blint"

ENV COMPOSER_ALLOW_SUPERUSER=1 \
ANDROID_HOME=/opt/android-sdk-linux \
ENV ANDROID_HOME=/opt/android-sdk-linux \
PYTHONUNBUFFERED=1 \
PYTHONIOENCODING="utf-8"
ENV PATH=${PATH}:/usr/local/bin/:/root/.local/bin:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools:

RUN microdnf install -y python3.11 python3.11-devel python3.11-pip java-21-openjdk-headless make gcc \
which tar gzip zip unzip sudo ncurses \
RUN microdnf install -y make gcc ncurses \
&& alternatives --install /usr/bin/python3 python /usr/bin/python3.11 1 \
&& python3 --version \
&& python3 -m pip install --upgrade pip \
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,9 @@ sbom command generates CycloneDX json.
## Discord support
The developers could be reached via the [discord](https://discord.gg/DCNxzaeUpd) channel.
## Sponsorship wishlist
If you love blint, you should consider [donating](https://owasp.org/donate?reponame=www-project-dep-scan&title=OWASP+dep-scan) to our project. In addition, consider donating to the below projects which make blint possible.
- [LIEF](https://github.com/sponsors/lief-project/)
54 changes: 54 additions & 0 deletions blint/binary.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# pylint: disable=too-many-lines,consider-using-f-string
import codecs
import contextlib
import json
import sys

import lief
Expand Down Expand Up @@ -380,6 +382,13 @@ def process_pe_resources(parsed_obj):
if not rm or isinstance(rm, lief.lief_errors):
return {}
resources = {}
version_metadata = {}
version_info: lief.PE.ResourceVersion = rm.version if rm.has_version else None
if version_info and version_info.has_string_file_info:
string_file_info: lief.PE.ResourceStringFileInfo = version_info.string_file_info
for lc_item in string_file_info.langcode_items:
if lc_item.items:
version_metadata.update(lc_item.items)
try:
resources = {
"has_accelerator": rm.has_accelerator,
Expand All @@ -393,6 +402,8 @@ def process_pe_resources(parsed_obj):
"version_info": str(rm.version) if rm.has_version else None,
"html": rm.html if rm.has_html else None,
}
if version_metadata:
resources["version_metadata"] = version_metadata
except (AttributeError, UnicodeError):
return resources
return resources
Expand Down Expand Up @@ -875,6 +886,35 @@ def determine_elf_flags(header):
return eflags_str


def parse_pe_overlay(parsed_obj: lief.PE.Binary) -> dict[str, dict]:
"""
Parse the PE overlay section to extract dependencies
Args:
parsed_obj (lief.PE.Binary): The parsed object representing the PE binary.
Returns:
dict: Dict representing the deps.json if available.
"""
deps = {}
if hasattr(parsed_obj, "overlay"):
overlay = parsed_obj.overlay
overlay_str = codecs.decode(overlay.tobytes(), encoding="utf-8", errors="backslashreplace").replace("\0",
"").replace(
"\n", "").replace(" ", "")
if overlay_str.find('{"runtimeTarget') > -1:
start_index = overlay_str.find('{"runtimeTarget')
end_index = overlay_str.rfind('}}}')
if end_index > -1:
overlay_str = overlay_str[start_index:end_index + 3]
try:
# If all is good, deps should have runtimeTarget, compilationOptions, targets, and libraries
# Use libraries to construct BOM components and targets for the dependency tree
deps = json.loads(overlay_str)
except json.DecodeError:
pass
return deps


def add_pe_metadata(exe_file, metadata, parsed_obj):
"""Adds PE metadata to the given metadata dictionary.
Expand Down Expand Up @@ -911,10 +951,21 @@ def add_pe_metadata(exe_file, metadata, parsed_obj):
metadata["imports"],
metadata["dynamic_entries"],
) = parse_pe_imports(parsed_obj.imports)
# Attempt to detect if this PE is a driver
if metadata["dynamic_entries"]:
for e in metadata["dynamic_entries"]:
if e["name"] == "ntoskrnl.exe":
metadata["is_driver"] = True
break
metadata["exports"] = parse_pe_exports(parsed_obj.get_export())
metadata["functions"] = parse_functions(parsed_obj.functions)
metadata["ctor_functions"] = parse_functions(parsed_obj.ctor_functions)
metadata["exception_functions"] = parse_functions(parsed_obj.exception_functions)
# Detect if this PE might be dotnet
for i, dd in enumerate(parsed_obj.data_directories):
if i == 14 and type(dd) == "CLR_RUNTIME_HEADER":
metadata["is_dotnet"] = True
metadata["pe_dependencies"] = parse_pe_overlay(parsed_obj)
tls = parsed_obj.tls
if tls and tls.sizeof_zero_fill:
metadata["tls_address_index"] = tls.addressof_index
Expand Down Expand Up @@ -997,6 +1048,9 @@ def add_pe_optional_headers(metadata, optional_header):
for chara in optional_header.dll_characteristics_lists
]
)
# Detect if this binary is a driver
if "WDM_DRIVER" in metadata["dll_characteristics"]:
metadata["is_driver"] = True
metadata["subsystem"] = str(optional_header.subsystem).rsplit(".", maxsplit=1)[-1]
metadata["is_gui"] = metadata["subsystem"] == "WINDOWS_GUI"
metadata["exe_type"] = "PE32" if optional_header.magic == lief.PE.PE_TYPE.PE32 else "PE64"
Expand Down
77 changes: 74 additions & 3 deletions blint/sbom.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import base64
import os
import uuid
from datetime import datetime
Expand All @@ -12,6 +13,8 @@
BomFormat,
Component,
CycloneDX,
Hash,
HashAlg,
Lifecycles,
Metadata,
Phase,
Expand All @@ -22,7 +25,7 @@
Type,
)
from blint.logger import LOG
from blint.utils import create_component_evidence, find_android_files, gen_file_list, get_version
from blint.utils import camel_to_snake, create_component_evidence, find_android_files, gen_file_list, get_version


def default_parent(src_dirs: list[str]) -> Component:
Expand Down Expand Up @@ -246,9 +249,9 @@ def process_exe_file(
list[Component]: The updated list of components.
"""
dependencies_dict = {}
parent_component: Component = default_parent([exe])
dependencies_dict: dict[str, set] = {}
metadata: Dict[str, Any] = parse(exe)
parent_component: Component = default_parent([exe])
parent_component.properties = []
lib_components: list[Component] = []
for prop in (
Expand All @@ -266,12 +269,15 @@ def process_exe_file(
"flags",
"relro",
"is_pie",
"is_reproducible_build",
"has_nx",
"static",
"characteristics",
"dll_characteristics",
"subsystem",
"is_gui",
"is_driver",
"is_dotnet",
"major_linker_version",
"minor_linker_version",
"major_operating_system_version",
Expand All @@ -288,6 +294,13 @@ def process_exe_file(
if note.get("version"):
parent_component.properties.append(
Property(name=f"internal:{note.get('type')}", value=note.get('version')))
# For PE binaries, resources could have a dict called version_metadata with interesting properties
if metadata.get("resources"):
version_metadata = metadata.get("resources").get("version_metadata")
if version_metadata and isinstance(version_metadata, dict):
for vk, vv in version_metadata.items():
parent_component.properties.append(
Property(name=f"internal:{camel_to_snake(vk)}", value=vv))
if deep_mode:
symbols_version: list[dict] = metadata.get("symbols_version", [])
# Attempt to detect library components from the symbols version block
Expand Down Expand Up @@ -343,6 +356,10 @@ def process_exe_file(
for entry in metadata["dynamic_entries"]:
comp = create_dynamic_component(entry, exe)
lib_components.append(comp)
# Convert libraries and targets from PE binaries
if metadata.get("pe_dependencies"):
pe_components = process_pe_dependencies(metadata.get("pe_dependencies"), dependencies_dict)
lib_components += pe_components
if lib_components:
components += lib_components
track_dependency(dependencies_dict, parent_component, lib_components)
Expand Down Expand Up @@ -441,6 +458,60 @@ def process_android_file(
return components


def process_pe_dependencies(pe_deps: dict[str, dict], dependencies_dict: dict[str, set]) -> list[Component]:
"""
Process the dependencies metadata extracted for PE binaries
Args:
pe_deps (dict[str, dict]): PE dependencies metadata
dependencies_dict (dict[str, set]): Existing dependencies dictionary
Returns:
list: New component list
"""
components = []
libraries = pe_deps.get("libraries", {})
# k: 'Microsoft.CodeAnalysis.Analyzers/3.3.4'
# v: {'type': 'package', 'serviceable': True, 'sha512': 'sha512-AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==', 'path': 'microsoft.codeanalysis.analyzers/3.3.4', 'hashPath': 'microsoft.codeanalysis.analyzers.3.3.4.nupkg.sha512'}
for k, v in libraries.items():
tmp_a = k.split("/")
purl = f"pkg:nuget/{tmp_a[0]}@{tmp_a[1]}"
hash_content = ""
try:
hash_content = str(base64.b64decode(v.get("sha512", "").removeprefix("sha512-"), validate=True), "utf-8")
except Exception:
pass
comp = Component(
type=Type.application if v.get("type") == "project" else Type.library,
name=tmp_a[0],
version=tmp_a[1],
purl=purl,
scope=Scope.required,
properties=[
Property(name="internal:serviceable", value=str(v.get("serviceable")).lower()),
Property(name="internal:hash_path", value=v.get("hashPath")),
],
)
if v.get("path"):
comp.evidence = create_component_evidence(v.get("path"), 1.0),
if hash_content:
comp.hashes = [Hash(alg=HashAlg.SHA_512, content=hash_content)],
comp.bom_ref = RefType(purl)
components.append(comp)
targets: dict[str, dict[str, dict]] = pe_deps.get("targets", {})
for tk, tv in targets.items():
for k, v in tv.items():
tmp_a = k.split("/")
purl = f"pkg:nuget/{tmp_a[0]}@{tmp_a[1]}"
depends_on = []
for adep_name, adep_version in v.get("dependencies", {}).items():
depends_on.append(f"pkg:nuget/{adep_name}@{adep_version}")
if not dependencies_dict.get(purl):
dependencies_dict[purl] = set()
dependencies_dict[purl].update(depends_on)
return components


def track_dependency(
dependencies_dict: dict, parent_component: Component, app_components: list[Component]
) -> None:
Expand Down
7 changes: 6 additions & 1 deletion blint/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from defusedxml.ElementTree import fromstring
from rich import box
from rich.table import Table

from blint.config import (
ignore_directories,
ignore_files,
Expand All @@ -21,7 +22,6 @@
secrets_regex
)
from blint.cyclonedx.spec import ComponentEvidence, FieldModel, Identity, Method, Technique

from blint.logger import console, LOG

CHARSET = string.digits + string.ascii_letters + r"""!&@"""
Expand Down Expand Up @@ -502,3 +502,8 @@ def create_component_evidence(method_value: str, confidence: float) -> Component
],
)
)


def camel_to_snake(name: str) -> str:
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
Loading

0 comments on commit b7763fb

Please sign in to comment.