Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,16 @@ sub-commands:
### SBOM sub-command

```shell
usage: blint sbom [-h] [-i SRC_DIR_IMAGE [SRC_DIR_IMAGE ...]] [--output-file SBOM_OUTPUT]
usage: blint sbom [-h] [-i SRC_DIR_IMAGE [SRC_DIR_IMAGE ...]] [-o SBOM_OUTPUT] [--deep]

options:
-h, --help show this help message and exit
-i SRC_DIR_IMAGE [SRC_DIR_IMAGE ...], --src SRC_DIR_IMAGE [SRC_DIR_IMAGE ...]
Source directories, container images or binary files. Defaults to current directory.
--output-file SBOM_OUTPUT
-o SBOM_OUTPUT, --output-file SBOM_OUTPUT
SBOM output file. Defaults to bom.json in current directory.
--deep Enable deep mode to collect more used symbols and modules aggressively. Slow
operation.
```

To test any binary including default commands
Expand Down Expand Up @@ -109,6 +111,12 @@ blint sbom -i /path/to/apk -o bom.json
blint sbom -i /directory/with/apk/aab -o bom.json
```

To parse all files including `.dex` files, pass `--deep` argument.

```shell
blint sbom -i /path/to/apk -o bom.json --deep
```

PowerShell example

![PowerShell](./docs/blint-powershell.jpg)
Expand Down
88 changes: 83 additions & 5 deletions blint/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import sys
import tempfile

from blint.binary import parse
from blint.binary import parse, parse_dex
from blint.cyclonedx.spec import (
Component,
ComponentEvidence,
Expand Down Expand Up @@ -64,7 +64,7 @@ def exec_tool(args, cwd=None, stdout=subprocess.PIPE):
return None


def collect_app_metadata(app_file):
def collect_app_metadata(app_file, deep_mode):
"""
Collect various metadata about an android app
"""
Expand All @@ -81,7 +81,7 @@ def collect_app_metadata(app_file):
parent_component.properties.append(
Property(name="internal.appPermissions", value=permissions)
)
components = collect_files_metadata(app_file)
components = collect_files_metadata(app_file, parent_component, deep_mode)
return parent_component, components


Expand Down Expand Up @@ -140,7 +140,6 @@ def collect_version_files_metadata(app_file, app_temp_dir):
rel_path = os.path.relpath(vf, app_temp_dir)
group = ""
name = ""
version_data = ""
if "_" in file_name:
parts = file_name.split("_")
name = file_name
Expand Down Expand Up @@ -257,7 +256,82 @@ def collect_so_files_metadata(app_file, app_temp_dir):
return file_components


def collect_files_metadata(app_file):
def collect_dex_files_metadata(app_file, parent_component, app_temp_dir):
file_components = []
# Parse all .dex files
dex_files = find_files(app_temp_dir, [".dex"])
for adex in dex_files:
dex_metadata = parse_dex(adex)
name = os.path.basename(adex).removesuffix(".dex")
rel_path = os.path.relpath(adex, app_temp_dir)
group = (
parent_component.group
if parent_component and parent_component.group
else ""
)
version = (
parent_component.version
if parent_component and parent_component.version
else "latest"
)
purl = f"pkg:generic/{name}@{version}"
component = Component(
type=Type.file,
group=group,
name=name,
version=version,
purl=purl,
scope=Scope.required,
evidence=ComponentEvidence(
identity=Identity(
field=FieldModel.purl,
confidence=0.2,
methods=[
Method(
technique=Technique.binary_analysis,
value=rel_path,
confidence=0.2,
)
],
)
),
properties=[
Property(name="internal:srcFile", value=rel_path),
Property(name="internal:appFile", value=app_file),
Property(
name="internal:functions",
value=", ".join(
set(
[
f"""{m.name}({','.join([_clean_type(p.underlying_array_type) for p in m.prototype.parameters_type])}):{_clean_type(m.prototype.return_type.underlying_array_type)}"""
for m in dex_metadata.get("methods")
]
)
),
),
Property(
name="internal:classes",
value=", ".join(
set(
[
_clean_type(c.fullname)
for c in dex_metadata.get("classes")
]
)
),
),
],
)
component.bom_ref = RefType(purl)
file_components.append(component)
return file_components


def _clean_type(t):
return str(t).replace("/", ".").removeprefix("L").removesuffix(";")


def collect_files_metadata(app_file, parent_component, deep_mode):
"""
Unzip the app and collect metadata
"""
Expand All @@ -266,6 +340,10 @@ def collect_files_metadata(app_file):
unzip_unsafe(app_file, app_temp_dir)
file_components += collect_version_files_metadata(app_file, app_temp_dir)
file_components += collect_so_files_metadata(app_file, app_temp_dir)
if deep_mode:
file_components += collect_dex_files_metadata(
app_file, parent_component, app_temp_dir
)
shutil.rmtree(app_temp_dir, ignore_errors=True)
return file_components

Expand Down
62 changes: 41 additions & 21 deletions blint/binary.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import sys

import lief
from lief import ELF, PE, MachO
from lief import DEX, ELF, PE, MachO

from blint.logger import LOG
from blint.logger import DEBUG, LOG
from blint.utils import calculate_entropy, check_secret, decode_base64

MIN_ENTROPY = 0.39
MIN_LENGTH = 80

lief.logging.disable()
# Enable lief logging in debug mode
if LOG.level != DEBUG:
lief.logging.disable()

ADDRESS_FMT = "0x{:<10x}"

Expand Down Expand Up @@ -54,7 +56,7 @@ def parse_notes(parsed_obj):
ndk_build_number = ""
abi = ""
version_str = ""
if isinstance(note_details, lief.ELF.AndroidNote):
if isinstance(note_details, lief.ELF.AndroidIdent):
sdk_version = note_details.sdk_version
ndk_version = note_details.ndk_version
ndk_build_number = note_details.ndk_build_number
Expand All @@ -71,7 +73,7 @@ def parse_notes(parsed_obj):
metadata["notes"].append(
{
"index": idx,
"description": str(description_str),
"description": description_str,
"type": type_str,
"details": note_details_str,
"sdk_version": sdk_version,
Expand All @@ -94,19 +96,19 @@ def parse_relro(parsed_obj):
now = False
try:
parsed_obj.get(lief.ELF.SEGMENT_TYPES.GNU_RELRO)
except lief.not_found:
except lief.lief_errors.not_found:
return "no"
try:
dynamic_tags = parsed_obj.get(lief.ELF.DYNAMIC_TAGS.FLAGS)
if dynamic_tags:
bind_now = lief.ELF.DYNAMIC_FLAGS.BIND_NOW in dynamic_tags
except lief.not_found:
except lief.lief_errors.not_found:
pass
try:
dynamic_tags = parsed_obj.get(lief.ELF.DYNAMIC_TAGS.FLAGS_1)
if dynamic_tags:
now = lief.ELF.DYNAMIC_FLAGS_1.NOW in dynamic_tags
except lief.not_found:
except lief.lief_errors.not_found:
pass
if bind_now or now:
return "full"
Expand Down Expand Up @@ -597,19 +599,19 @@ def parse(exe_file):
if parsed_obj.get_symbol(section):
metadata["has_canary"] = True
break
except lief.not_found:
except lief.lief_errors.not_found:
metadata["has_canary"] = False
# rpath check
try:
if parsed_obj.get(lief.ELF.DYNAMIC_TAGS.RPATH):
metadata["has_rpath"] = True
except lief.not_found:
except lief.lief_errors.not_found:
metadata["has_rpath"] = False
# runpath check
try:
if parsed_obj.get(lief.ELF.DYNAMIC_TAGS.RUNPATH):
metadata["has_runpath"] = True
except lief.not_found:
except lief.lief_errors.not_found:
metadata["has_runpath"] = False
static_symbols = parsed_obj.static_symbols
if len(static_symbols):
Expand Down Expand Up @@ -715,7 +717,7 @@ def parse(exe_file):
header = parsed_obj.header
optional_header = parsed_obj.optional_header
metadata["used_bytes_in_the_last_page"] = (
dos_header.used_bytes_in_the_last_page
dos_header.used_bytes_in_last_page
)
metadata["file_size_in_pages"] = dos_header.file_size_in_pages
metadata["num_relocation"] = dos_header.numberof_relocation
Expand Down Expand Up @@ -762,9 +764,7 @@ def parse(exe_file):
metadata["subsystem"] = str(optional_header.subsystem).rsplit(
".", maxsplit=1
)[-1]
metadata["is_gui"] = (
True if metadata["subsystem"] == "WINDOWS_GUI" else False
)
metadata["is_gui"] = metadata["subsystem"] == "WINDOWS_GUI"
metadata["exe_type"] = (
"PE32"
if optional_header.magic == PE.PE_TYPE.PE32
Expand Down Expand Up @@ -860,9 +860,10 @@ def parse(exe_file):
except Exception:
pass
try:
metadata["imports"], metadata["dynamic_entries"] = (
parse_pe_imports(parsed_obj.imports)
)
(
metadata["imports"],
metadata["dynamic_entries"],
) = parse_pe_imports(parsed_obj.imports)
except Exception:
pass
try:
Expand Down Expand Up @@ -1037,7 +1038,7 @@ def parse(exe_file):
metadata["has_main_command"] = True
if parsed_obj.thread_command:
metadata["has_thread_command"] = True
except lief.not_found:
except lief.lief_errors.not_found:
metadata["has_main"] = False
metadata["has_thread_command"] = False
try:
Expand Down Expand Up @@ -1074,15 +1075,15 @@ def parse(exe_file):
code_signature = parsed_obj.code_signature
metadata["code_signature"] = {
"available": True if code_signature.size else False,
"data": str(bytes(code_signature.data).hex()),
"data": str(code_signature.data.hex()),
"data_size": str(code_signature.data_size),
"size": str(code_signature.size),
}
if (
not parsed_obj.has_code_signature
and parsed_obj.has_code_signature_dir
):
code_signature = parsed_obj.has_code_signature_dir
code_signature = parsed_obj.code_signature_dir
metadata["code_signature"] = {
"available": True if code_signature.size else False,
"data": str(bytes(code_signature.data).hex()),
Expand All @@ -1106,3 +1107,22 @@ def parse(exe_file):
except Exception as e:
LOG.exception(e)
return metadata


def parse_dex(dex_file):
"""Parse dex files"""
metadata = {"file_path": dex_file}
try:
dexfile_obj = DEX.parse(dex_file)
metadata["version"] = dexfile_obj.version
metadata["header"] = dexfile_obj.header
metadata["classes"] = [cls for cls in dexfile_obj.classes]
metadata["fields"] = [f for f in dexfile_obj.fields]
metadata["methods"] = [m for m in dexfile_obj.methods]
metadata["strings"] = list(dexfile_obj.strings)
metadata["types"] = [t for t in dexfile_obj.types]
metadata["prototypes"] = [p for p in dexfile_obj.prototypes]
metadata["map"] = dexfile_obj.map
except Exception as e:
LOG.exception(e)
return metadata
9 changes: 8 additions & 1 deletion blint/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ def build_args():
dest="sbom_output",
help="SBOM output file. Defaults to bom.json in current directory.",
)
sbom_parser.add_argument(
"--deep",
action="store_true",
default=False,
dest="deep_mode",
help="Enable deep mode to collect more used symbols and modules aggressively. Slow operation.",
)
return parser.parse_args()


Expand Down Expand Up @@ -125,7 +132,7 @@ def main():
sbom_output = args.sbom_output
else:
sbom_output = os.path.join(os.getcwd(), "bom.json")
generate(src_dirs, sbom_output)
generate(src_dirs, sbom_output, args.deep_mode)
# Default case
else:
# Create reports directory
Expand Down
6 changes: 4 additions & 2 deletions blint/sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def default_metadata(src_dirs):
return metadata


def generate(src_dirs, output_file):
def generate(src_dirs, output_file, deep_mode):
android_files = []
components = []
dependencies = []
Expand Down Expand Up @@ -84,7 +84,9 @@ def generate(src_dirs, output_file):
for f in android_files:
dependencies_dict = {}
progress.update(task, description=f"Processing [bold]{f}[/bold]")
parent_component, app_components = collect_app_metadata(f)
parent_component, app_components = collect_app_metadata(
f, deep_mode
)
if parent_component:
if not sbom.metadata.component.components:
sbom.metadata.component.components = []
Expand Down
Loading