Skip to content

Commit 2bcaacb

Browse files
authored
Merge pull request #73 from owasp-dep-scan/feature/go-binary
go binary sbom
2 parents da0718f + fe782c8 commit 2bcaacb

File tree

5 files changed

+175
-44
lines changed

5 files changed

+175
-44
lines changed

.github/workflows/bintests.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,21 @@ jobs:
2727
poetry install
2828
- name: Test binaries
2929
run: |
30-
mkdir -p bintests
30+
mkdir -p bintests gobintests
3131
cd bintests
3232
wget -q https://github.com/owasp-dep-scan/dosai/releases/download/v0.1.1/Dosai.exe
3333
wget -q https://github.com/owasp-dep-scan/dosai/releases/download/v0.1.1/Dosai
3434
wget -q https://github.com/owasp-dep-scan/dosai/releases/download/v0.1.1/Dosai-osx-arm64
3535
cd ..
36+
cd gobintests
37+
wget -q https://github.com/containerd/containerd/releases/download/v1.7.14/containerd-1.7.14-linux-amd64.tar.gz
38+
wget -q https://github.com/containerd/nerdctl/releases/download/v1.7.4/nerdctl-1.7.4-windows-amd64.tar.gz
39+
tar -xvf containerd-1.7.14-linux-amd64.tar.gz
40+
tar -xvf nerdctl-1.7.4-windows-amd64.tar.gz
41+
rm containerd-1.7.14-linux-amd64.tar.gz
42+
rm nerdctl-1.7.4-windows-amd64.tar.gz
43+
cd ..
3644
poetry run blint sbom -i bintests -o reports/bom.json --deep
45+
poetry run blint sbom -i gobintests -o reports/bom.json --deep
3746
env:
3847
SCAN_DEBUG_MODE: "debug"

README.md

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# BLint
22

3-
![blint logo](blint.png)
4-
5-
BLint is a Binary Linter to check the security properties, and capabilities in your executables. It is powered by [lief](https://github.com/lief-project/LIEF). Since version 2, blint can also generate Software Bill-of-Materials (SBOM) for supported binaries.
3+
![blint logo]
4+
BLint is a Binary Linter that checks the security properties and capabilities of your executables. It is powered by [lief](https://github.com/lief-project/LIEF). Since version 2, blint can also generate Software Bill-of-Materials (SBOM) for supported binaries.
65

76
[![BLint Demo](https://asciinema.org/a/438138.png)](https://asciinema.org/a/438138)
87

@@ -13,15 +12,13 @@ Supported binary formats:
1312
- PE (exe, dll)
1413
- Mach-O (x64, arm64)
1514

16-
You can run blint on Linux, Windows and Mac against any of these binary formats.
15+
You can run blint on Linux, Windows, and Mac against any of these binary formats.
1716

1817
## Motivation
1918

20-
Nowadays, vendors distribute statically linked binaries produced by golang or rust or dotnet tooling. Users are used to running antivirus and anti-malware scans while using these binaries in their local devices. Blint augments these scans by listing the technical capabilities of a binary. For example, whether the binary could use network connections, or can perform file system operations and so on.
21-
22-
The binary is first parsed using lief framework to identify the various properties such as functions, static, and dynamic symbols present. Thanks to YAML based [annotations](./blint/data/annotations) data, this information could be matched against capabilities and presented visually using a rich table.
23-
24-
NOTE: The presence of capabilities doesn't imply that the operations are always performed by the binary. Use the output of this tool to get an idea about a binary. Also, this tool is not suitable to review malware and other heavily obfuscated binaries for obvious reasons.
19+
Nowadays, vendors distribute statically linked binaries produced by Golang, Rust, or Dotnet tooling. Users are used to running antivirus and anti-malware scans while using these binaries in their local devices. Blint augments these scans by listing the technical capabilities of a binary. For example, whether the binary could use network connections or can perform file system operations and so on.
20+
The binary is first parsed using the lief framework to identify the various properties, such as functions and the presence of symtab and dynamic symbols. Thanks to YAML-based annotation data, this information can be matched against capabilities and presented visually using a rich table.
21+
NOTE: The presence of capabilities doesn't imply that the binary always performs the operations. Use the output of this tool to get an idea about a binary. Also, this tool is not suitable for reviewing malware and other heavily obfuscated binaries for obvious reasons.
2522

2623
## Use cases
2724

@@ -39,7 +36,7 @@ pip install blint
3936

4037
### Single binary releases
4138

42-
You can download single binary builds from the [blint-bin releases](https://github.com/OWASP-dep-scan/blint/releases). These executables should work with requiring python to be installed. The macOS .pkg file is signed with a valid developer account.
39+
You can download single binary builds from the [blint-bin releases](https://github.com/OWASP-dep-scan/blint/releases). These executables should work without requiring python to be installed. The macOS .pkg file is signed with a valid developer account.
4340

4441
## Usage
4542

@@ -83,7 +80,7 @@ options:
8380
operation.
8481
```
8582
86-
To test any binary including default commands
83+
To test any binary, including default commands
8784
8885
```bash
8986
blint -i /bin/netstat -o /tmp/blint
@@ -111,12 +108,24 @@ blint sbom -i /path/to/apk -o bom.json
111108
blint sbom -i /directory/with/apk/aab -o bom.json
112109
```
113110
114-
To parse all files including `.dex` files, pass `--deep` argument.
111+
To parse all files, including `.dex` files, pass `--deep` argument.
115112
116113
```shell
117114
blint sbom -i /path/to/apk -o bom.json --deep
118115
```
119116
117+
The following binaries are supported:
118+
119+
- Android (apk/aab)
120+
- Dotnet executable binaries
121+
- Go binaries
122+
123+
```shell
124+
blint sbom -i /path/to/go-binaries -o bom.json --deep
125+
```
126+
127+
For all other binaries, the symbols will be collected and represented as properties with `internal` prefixes for the parent component. Child components and dependencies would be missing.
128+
120129
PowerShell example
121130
122131
![PowerShell](./docs/blint-powershell.jpg)
@@ -127,7 +136,7 @@ Blint produces the following json artifacts in the reports directory:
127136
128137
- blint-output.html - HTML output from the console logs
129138
- exename-metadata.json - Raw metadata about the parsed binary. Includes symbols, functions, and signature information
130-
- findings.json - Contains information from the security properties audit. Useful for CI/CD based integration
139+
- findings.json - Contains information from the security properties audit. Useful for CI/CD integrations
131140
- reviews.json - Contains information from the capability reviews. Useful for further analysis
132141
- fuzzables.json - Contains a suggested list of methods for fuzzing
133142
@@ -140,10 +149,10 @@ sbom command generates CycloneDX json.
140149
141150
## Discord support
142151
143-
The developers could be reached via the [discord](https://discord.gg/DCNxzaeUpd) channel.
152+
The developers can be reached via the [Discord](https://discord.gg/DCNxzaeUpd) channel.
144153
145154
## Sponsorship wishlist
146155
147-
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.
156+
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.
148157
149158
- [LIEF](https://github.com/sponsors/lief-project/)

blint/binary.py

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def ignorable_symbol(symbol_name: str | None) -> bool:
229229
"""
230230
if not symbol_name:
231231
return True
232-
for pref in ("$f64.", "__"):
232+
for pref in ("_$f", "$f64.", "__"):
233233
if symbol_name.startswith(pref):
234234
return True
235235
return False
@@ -656,18 +656,19 @@ def parse_macho_symbols(symbols):
656656
if not symbol_name or isinstance(symbol_name, lief.lief_errors):
657657
symbol_name = symbol.name
658658
symbol_name = symbol_name.replace("..", "::")
659-
if not exe_type:
660-
exe_type = guess_exe_type(symbol_name)
661-
symbols_list.append(
662-
{
663-
"name": (f"{libname}::{symbol_name}" if libname else symbol_name),
664-
"short_name": symbol_name,
665-
"type": symbol.type,
666-
"num_sections": symbol.numberof_sections,
667-
"description": symbol.description,
668-
"value": symbol_value,
669-
}
670-
)
659+
if not ignorable_symbol(symbol_name):
660+
if not exe_type:
661+
exe_type = guess_exe_type(symbol_name)
662+
symbols_list.append(
663+
{
664+
"name": (f"{libname}::{symbol_name}" if libname else symbol_name),
665+
"short_name": symbol_name,
666+
"type": symbol.type,
667+
"num_sections": symbol.numberof_sections,
668+
"description": symbol.description,
669+
"value": symbol_value,
670+
}
671+
)
671672
except (AttributeError, TypeError):
672673
continue
673674
return symbols_list, exe_type
@@ -759,6 +760,8 @@ def add_elf_metadata(exe_file, metadata, parsed_obj):
759760
metadata["functions"] = parse_functions(parsed_obj.functions)
760761
metadata["ctor_functions"] = parse_functions(parsed_obj.ctor_functions)
761762
metadata["dotnet_dependencies"] = parse_overlay(parsed_obj)
763+
metadata["go_dependencies"], metadata["go_formulation"] = parse_go_buildinfo(parsed_obj)
764+
762765
return metadata
763766

764767

@@ -927,6 +930,59 @@ def parse_overlay(parsed_obj: lief.Binary) -> dict[str, dict]:
927930
return deps
928931

929932

933+
def parse_go_buildinfo(parsed_obj: lief.Binary) -> (dict[str, dict[str, str]], dict[str, str]):
934+
"""
935+
Parse the go build info section to extract go dependencies
936+
Args:
937+
parsed_obj (lief.Binary): The parsed object representing the binary.
938+
939+
Returns:
940+
tuple(dict[str, str], dict[str, str]): Tuple representing the dependencies and formulation.
941+
"""
942+
formulation = {}
943+
deps = {}
944+
build_info_str: str = ""
945+
# Look for specific buildinfo sections for ELF and MachO binaries
946+
build_info: lief.Section = None
947+
if isinstance(parsed_obj, lief.ELF.Binary):
948+
build_info = parsed_obj.get_section(".go.buildinfo")
949+
elif isinstance(parsed_obj, lief.MachO.Binary):
950+
build_info = parsed_obj.get_section("__go_buildinfo")
951+
if build_info and build_info.size:
952+
build_info_str = (
953+
codecs.decode(build_info.content.tobytes(), encoding="utf-8", errors="replace")
954+
.replace("\0", "")
955+
.replace("\uFFFD", "")
956+
.replace("\t", " ")
957+
).strip()
958+
build_info_str = build_info_str.encode('ascii', 'ignore').decode('ascii')
959+
elif isinstance(parsed_obj, lief.PE.Binary):
960+
# For PE binaries look for .data section
961+
s: lief.PE.Section = parsed_obj.get_section(".data")
962+
build_info_str = codecs.decode(s.content.tobytes()[:int(s.size / 32)], encoding="ascii",
963+
errors="replace").replace("\0", "").replace("\uFFFD", "").replace("\t", " ")
964+
lines = build_info_str.split("\n")
965+
for line in lines:
966+
if line.startswith("Go buildinf:"):
967+
tmp_a = line.split("Go buildinf:")
968+
formulation["go_version"] = tmp_a[-1].split("\x19")[0].split(" ")[-1]
969+
if "path " in line:
970+
tmp_a = line.split("path ")
971+
formulation["path"] = tmp_a[-1]
972+
if line.startswith("mod "):
973+
tmp_a = line.split("mod ")
974+
formulation["module"] = tmp_a[-1]
975+
if line.startswith("dep "):
976+
tmp_a = line.removeprefix("dep ").split(" ")
977+
deps[tmp_a[0]] = {"version": tmp_a[1],
978+
"hash": tmp_a[2] if len(tmp_a) == 3 and tmp_a[2].startswith("h1:") else None}
979+
if line.startswith("build "):
980+
tmp_a = line.removeprefix("build ").split("=")
981+
formulation[tmp_a[0].replace("-", "")] = tmp_a[1]
982+
983+
return deps, formulation
984+
985+
930986
def add_pe_metadata(exe_file: str, metadata: dict, parsed_obj: lief.PE.Binary):
931987
"""Adds PE metadata to the given metadata dictionary.
932988
@@ -978,6 +1034,7 @@ def add_pe_metadata(exe_file: str, metadata: dict, parsed_obj: lief.PE.Binary):
9781034
if i == 14 and dd.type.value == lief.PE.DataDirectory.TYPES.CLR_RUNTIME_HEADER.value:
9791035
metadata["is_dotnet"] = True
9801036
metadata["dotnet_dependencies"] = parse_overlay(parsed_obj)
1037+
metadata["go_dependencies"], metadata["go_formulation"] = parse_go_buildinfo(parsed_obj)
9811038
tls = parsed_obj.tls
9821039
if tls and tls.sizeof_zero_fill:
9831040
metadata["tls_address_index"] = tls.addressof_index
@@ -1138,6 +1195,7 @@ def add_mach0_metadata(exe_file, metadata, parsed_obj):
11381195
metadata = add_mach0_commands(metadata, parsed_obj)
11391196
metadata = add_mach0_functions(metadata, parsed_obj)
11401197
metadata = add_mach0_signature(exe_file, metadata, parsed_obj)
1198+
metadata["go_dependencies"], metadata["go_formulation"] = parse_go_buildinfo(parsed_obj)
11411199
return metadata
11421200

11431201

blint/sbom.py

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import base64
2+
import binascii
3+
import codecs
24
import os
5+
import urllib.parse
36
import uuid
47
from datetime import datetime
58
from typing import Any, Dict
@@ -317,44 +320,44 @@ def process_exe_file(
317320
# If this is unsuccessful then store the information as a property
318321
lib_components += components_from_symbols_version(symbols_version)
319322
if not lib_components and symbols_version:
320-
parent_component.properties += [
323+
parent_component.properties.append(
321324
Property(
322325
name="internal:symbols_version",
323326
value=", ".join([f["name"] for f in symbols_version]),
324327
)
325-
]
328+
)
326329
internal_functions = [f["name"] for f in metadata.get("functions", []) if not f["name"].startswith("__")]
327330
if internal_functions:
328-
parent_component.properties += [
331+
parent_component.properties.append(
329332
Property(
330333
name="internal:functions",
331334
value=SYMBOL_DELIMITER.join(internal_functions),
332335
)
333-
]
336+
)
334337
symtab_symbols = [f["name"] for f in metadata.get("symtab_symbols", [])]
335338
if symtab_symbols:
336-
parent_component.properties += [
339+
parent_component.properties.append(
337340
Property(
338341
name="internal:symtab_symbols",
339342
value=SYMBOL_DELIMITER.join(symtab_symbols),
340343
)
341-
]
344+
)
342345
all_imports = [f["name"] for f in metadata.get("imports", [])]
343346
if all_imports:
344-
parent_component.properties += [
347+
parent_component.properties.append(
345348
Property(
346349
name="internal:imports",
347350
value=SYMBOL_DELIMITER.join(all_imports),
348351
)
349-
]
352+
)
350353
dynamic_symbols = [f["name"] for f in metadata.get("dynamic_symbols", [])]
351354
if dynamic_symbols:
352-
parent_component.properties += [
355+
parent_component.properties.append(
353356
Property(
354357
name="internal:dynamic_symbols",
355358
value=SYMBOL_DELIMITER.join(dynamic_symbols),
356359
)
357-
]
360+
)
358361
if not sbom.metadata.component.components:
359362
sbom.metadata.component.components = []
360363
_add_to_parent_component(sbom.metadata.component.components, parent_component)
@@ -370,6 +373,18 @@ def process_exe_file(
370373
if metadata.get("dotnet_dependencies"):
371374
pe_components = process_dotnet_dependencies(metadata.get("dotnet_dependencies"), dependencies_dict)
372375
lib_components += pe_components
376+
# Convert go dependencies
377+
if metadata.get("go_dependencies"):
378+
go_components = process_go_dependencies(metadata.get("go_dependencies"))
379+
lib_components += go_components
380+
# Convert go formulation section
381+
for k, v in metadata.get("go_formulation", {}).items():
382+
parent_component.properties.append(
383+
Property(
384+
name=f"internal:{camel_to_snake(k)}",
385+
value=str(v).strip(),
386+
)
387+
)
373388
if lib_components:
374389
components += lib_components
375390
track_dependency(dependencies_dict, parent_component, lib_components)
@@ -485,9 +500,10 @@ def process_dotnet_dependencies(dotnet_deps: dict[str, dict], dependencies_dict:
485500
purl = f"pkg:nuget/{tmp_a[0]}@{tmp_a[1]}"
486501
hash_content = ""
487502
try:
488-
hash_content = str(base64.b64decode(v.get("sha512", "").removeprefix("sha512-"), validate=True), "utf-8")
489-
except Exception:
490-
pass
503+
hash_content = codecs.encode(base64.b64decode(v.get("sha512").removeprefix("sha512-"), validate=True),
504+
encoding="hex")
505+
except binascii.Error:
506+
hash_content = str(v.get("hash").removeprefix("sha512-"))
491507
comp = Component(
492508
type=Type.application if v.get("type") == "project" else Type.library,
493509
name=tmp_a[0],
@@ -505,7 +521,7 @@ def process_dotnet_dependencies(dotnet_deps: dict[str, dict], dependencies_dict:
505521
comp.bom_ref = RefType(purl)
506522
components.append(comp)
507523
targets: dict[str, dict[str, dict]] = dotnet_deps.get("targets", {})
508-
for tk, tv in targets.items():
524+
for _, tv in targets.items():
509525
for k, v in tv.items():
510526
tmp_a = k.split("/")
511527
purl = f"pkg:nuget/{tmp_a[0]}@{tmp_a[1]}"
@@ -518,6 +534,45 @@ def process_dotnet_dependencies(dotnet_deps: dict[str, dict], dependencies_dict:
518534
return components
519535

520536

537+
def process_go_dependencies(go_deps: dict[str, str]) -> list[Component]:
538+
"""
539+
Process the go dependencies metadata extracted for binary overlays
540+
541+
Args:
542+
go_deps (dict[str, str]): dependencies metadata
543+
544+
Returns:
545+
list: New component list
546+
"""
547+
components = []
548+
# Key is the name and value is the version
549+
# We need to construct a purl by pretending the module name is the name with no namespace
550+
# This would make this compatible with cdxgen and depscan
551+
# See https://github.com/CycloneDX/cdxgen/issues/897
552+
for k, v in go_deps.items():
553+
purl = f"""pkg:golang/{urllib.parse.quote_plus(k)}@{v.get("version")}"""
554+
comp = Component(
555+
type=Type.library,
556+
name=k,
557+
version=v.get("version"),
558+
purl=purl,
559+
scope=Scope.required,
560+
evidence=create_component_evidence(k, 1.0)
561+
)
562+
hash_content = ""
563+
if v.get("hash"):
564+
try:
565+
hash_content = codecs.encode(base64.b64decode(v.get("hash").removeprefix("h1:"), validate=True),
566+
encoding="hex")
567+
except binascii.Error:
568+
hash_content = str(v.get("hash").removeprefix("h1:"))
569+
if hash_content:
570+
comp.hashes = [Hash(alg=HashAlg.SHA_256, content=hash_content)]
571+
comp.bom_ref = RefType(f"""pkg:golang/{k}@{v.get("version")}""")
572+
components.append(comp)
573+
return components
574+
575+
521576
def track_dependency(
522577
dependencies_dict: dict[str, set], parent_component: Component, app_components: list[Component]
523578
) -> None:

0 commit comments

Comments
 (0)