flare-floss/floss/render/default.py
Ana Maria Martinez Gomez ab07022ff3
[copyright + license] Fix headers
Replace the header from source code files using the following script:
```Python
for dir_path, dir_names, file_names in os.walk("flare-floss"):
    for file_name in file_names:
        try:
            file_path = f"{dir_path}/{file_name}"
            f = open(file_path, "rb+")
            content = f.read()
            m = re.search(OLD_HEADER, content)
            if not m:
                continue
            print(f"{file_path}: {m.group('year')}")
            content = content.replace(m.group(0), NEW_HEADER % m.group("year"))
            f.seek(0)
            f.write(content)
        except:
            continue
```

Some files had the copyright headers inside a `"""` comment and needed
manual changes before applying the script.

The old header had the confusing sentence `All rights reserved`, which
does not make sense for an open source license. Replace the header by
the default Google header that corrects this issue and keep floss
consistent with other Google projects.
2025-01-14 17:52:22 +01:00

379 lines
14 KiB
Python

# Copyright 2022 Google LLC
#
# 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.
import io
import sys
import textwrap
import collections
from typing import Dict, List, Tuple, Union
from rich import box
from rich.table import Table
from rich.markup import escape
from rich.console import Console
import floss.utils as util
import floss.logging_
import floss.language.identify
from floss.render import Verbosity
from floss.results import AddressType, StackString, TightString, DecodedString, ResultDocument, StringEncoding
from floss.render.sanitize import sanitize
MIN_WIDTH_LEFT_COL = 22
MIN_WIDTH_RIGHT_COL = 82
DISABLED = "Disabled"
logger = floss.logging_.getLogger(__name__)
def heading_style(s: str):
colored_string = "[cyan]" + escape(s) + "[/cyan]"
return colored_string
def string_style(s: str):
colored_string = "[green]" + escape(s) + " [/green]"
return colored_string
def width(s: str, character_count: int) -> str:
"""pad the given string to at least `character_count`"""
if len(s) < character_count:
return s + " " * (character_count - len(s))
else:
return s
def render_meta(results: ResultDocument, console, verbose):
rows: List[Tuple[str, str]] = list()
lang = f"{results.metadata.language}" if results.metadata.language else ""
lang_v = (
f" ({results.metadata.language_version})"
if results.metadata.language != "unknown" and results.metadata.language_version
else ""
)
lang_s = f" - selected: {results.metadata.language_selected}" if results.metadata.language_selected else ""
language_value = f"{lang}{lang_v}{lang_s}"
if verbose == Verbosity.DEFAULT:
rows.append((width("file path", MIN_WIDTH_LEFT_COL), width(results.metadata.file_path, MIN_WIDTH_RIGHT_COL)))
rows.append(("identified language", language_value))
else:
rows.extend(
[
(width("file path", MIN_WIDTH_LEFT_COL), width(results.metadata.file_path, MIN_WIDTH_RIGHT_COL)),
("start date", results.metadata.runtime.start_date.strftime("%Y-%m-%d %H:%M:%S")),
("runtime", strtime(results.metadata.runtime.total)),
("version", results.metadata.version),
("identified language", language_value),
("imagebase", f"0x{results.metadata.imagebase:x}"),
("min string length", f"{results.metadata.min_length}"),
]
)
rows.append(("extracted strings", ""))
rows.extend(render_string_type_rows(results))
if verbose > Verbosity.DEFAULT:
rows.extend(render_function_analysis_rows(results))
table = Table(box=box.ASCII2, show_header=False)
for row in rows:
table.add_row(str(row[0]), str(row[1]))
console.print(table)
def render_string_type_rows(results: ResultDocument) -> List[Tuple[str, str]]:
len_ss = len(results.strings.static_strings)
len_ls = len(results.strings.language_strings)
len_chars_ss = sum([len(s.string) for s in results.strings.static_strings])
len_chars_ls = sum([len(s.string) for s in results.strings.language_strings])
return [
(
" static strings",
(
f"{len_ss:>{len(str(len_ss))}} ({len_chars_ss:>{len(str(len_chars_ss))}d} characters)"
if results.analysis.enable_static_strings
else DISABLED
),
),
(
" language strings",
(
f"{len_ls:>{len(str(len_ss))}} ({len_chars_ls:>{len(str(len_chars_ss))}d} characters)"
if results.metadata.language
else DISABLED
),
),
(
" stack strings",
str(len(results.strings.stack_strings)) if results.analysis.enable_stack_strings else DISABLED,
),
(
" tight strings",
str(len(results.strings.tight_strings)) if results.analysis.enable_tight_strings else DISABLED,
),
(
" decoded strings",
str(len(results.strings.decoded_strings)) if results.analysis.enable_decoded_strings else DISABLED,
),
]
def render_function_analysis_rows(results) -> List[Tuple[str, str]]:
if results.metadata.runtime.vivisect == 0:
return [("analyzed functions", DISABLED)]
rows = [
("analyzed functions", ""),
(" discovered", results.analysis.functions.discovered),
(" library", results.analysis.functions.library),
]
if results.analysis.enable_stack_strings:
rows.append((" stack strings", str(results.analysis.functions.analyzed_stack_strings)))
if results.analysis.enable_tight_strings:
rows.append((" tight strings", str(results.analysis.functions.analyzed_tight_strings)))
if results.analysis.enable_decoded_strings:
rows.append((" decoded strings", str(results.analysis.functions.analyzed_decoded_strings)))
if results.analysis.functions.decoding_function_scores:
rows.append(
(
" identified decoding functions\n (offset, score, and number of xrefs to)",
textwrap.fill(
", ".join(
[
f"0x{fva:x} ({d['score']:.3f}, xrefs_to: {d['xrefs_to']})"
for fva, d in results.analysis.functions.decoding_function_scores.items()
]
),
max(len(results.metadata.file_path), MIN_WIDTH_RIGHT_COL),
),
)
)
return rows
def strtime(seconds):
m, s = divmod(seconds, 60)
return f"{m:02.0f}:{s:02.0f}"
def render_language_strings(language, language_strings, language_strings_missed, console, verbose, disable_headers):
strings = sorted(language_strings + language_strings_missed, key=lambda s: s.offset)
render_heading(f"FLOSS {language.upper()} STRINGS ({len(strings)})", console, verbose, disable_headers)
offset_len = len(f"{strings[-1].offset}")
for s in strings:
if verbose == Verbosity.DEFAULT:
console.print(sanitize(s.string, is_ascii_only=False), markup=False)
else:
colored_string = string_style(sanitize(s.string, is_ascii_only=False))
console.print(f"0x{s.offset:>0{offset_len}x} {colored_string}")
def render_static_substrings(strings, encoding, offset_len, console, verbose, disable_headers):
if verbose != Verbosity.DEFAULT:
encoding = heading_style(encoding)
render_sub_heading(f"FLOSS STATIC STRINGS: {encoding}", len(strings), console, disable_headers)
for s in strings:
if verbose == Verbosity.DEFAULT:
console.print(sanitize(s.string), markup=False)
else:
colored_string = string_style(sanitize(s.string))
console.print(f"0x{s.offset:>0{offset_len}x} {colored_string}")
def render_staticstrings(strings, console, verbose, disable_headers):
render_heading(f"FLOSS STATIC STRINGS ({len(strings)})", console, verbose, disable_headers)
ascii_strings = list(filter(lambda s: s.encoding == StringEncoding.ASCII, strings))
unicode_strings = list(filter(lambda s: s.encoding == StringEncoding.UTF16LE, strings))
ascii_offset_len = 0
unicode_offset_len = 0
if ascii_strings:
ascii_offset_len = len(f"{ascii_strings[-1].offset}")
if unicode_strings:
unicode_offset_len = len(f"{unicode_strings[-1].offset}")
offset_len = max(ascii_offset_len, unicode_offset_len)
render_static_substrings(ascii_strings, "ASCII", offset_len, console, verbose, disable_headers)
console.print("\n")
render_static_substrings(unicode_strings, "UTF-16LE", offset_len, console, verbose, disable_headers)
def render_stackstrings(
strings: Union[List[StackString], List[TightString]], console, verbose: bool, disable_headers: bool
):
if verbose == Verbosity.DEFAULT:
for s in strings:
console.print(sanitize(s.string), markup=False)
else:
if strings:
table = Table(
"Function",
"Function Offset",
"Frame Offset",
"String",
show_header=not (disable_headers),
box=box.ASCII2,
show_edge=False,
)
for s in strings:
table.add_row(
util.hex(s.function),
util.hex(s.program_counter),
util.hex(s.frame_offset),
string_style(sanitize(s.string)),
)
console.print(table)
def render_decoded_strings(decoded_strings: List[DecodedString], console, verbose, disable_headers):
"""
Render results of string decoding phase.
"""
if verbose == Verbosity.DEFAULT:
for ds in decoded_strings:
console.print(sanitize(ds.string), markup=False)
else:
strings_by_functions: Dict[int, list] = collections.defaultdict(list)
for ds in decoded_strings:
strings_by_functions[ds.decoding_routine].append(ds)
for fva, data in strings_by_functions.items():
render_sub_heading(" FUNCTION at " + heading_style(f"0x{fva:x}"), len(data), console, disable_headers)
rows = []
for ds in data:
if ds.address_type == AddressType.STACK:
offset_string = escape("[stack]")
elif ds.address_type == AddressType.HEAP:
offset_string = escape("[heap]")
else:
offset_string = hex(ds.address or 0)
rows.append((offset_string, hex(ds.decoded_at), string_style(sanitize(ds.string))))
if rows:
table = Table(
"Offset", "Called At", "String", show_header=not (disable_headers), box=box.ASCII2, show_edge=False
)
for row in rows:
table.add_row(row[0], row[1], row[2])
console.print(table)
console.print("\n")
def render_heading(heading, console, verbose, disable_headers):
"""
example::
─────────────────────────
FLOSS TIGHT STRINGS (0)
─────────────────────────
"""
if disable_headers:
return
style = ""
if verbose != Verbosity.DEFAULT:
style = "cyan"
table = Table(box=box.HORIZONTALS, style=style, show_header=False)
table.add_row(heading, style=style)
if verbose == Verbosity.DEFAULT:
console.print(table)
else:
console.print(table)
console.print()
def render_sub_heading(heading, n, console, disable_headers):
"""
example::
+-----------------------------------+
| FLOSS STATIC STRINGS: ASCII (862) |
+-----------------------------------+
"""
if disable_headers:
return
table = Table(box=box.ASCII2, show_header=False)
table.add_row(heading + f" ({n})")
console.print(table)
console.print()
def get_color(color):
if color == "always":
color_system = "256"
elif color == "auto":
color_system = "windows"
elif color == "never":
color_system = None
else:
raise RuntimeError("unexpected --color value: " + color)
return color_system
def render(results: floss.results.ResultDocument, verbose, disable_headers, color):
sys.__stdout__.reconfigure(encoding="utf-8") # type: ignore [union-attr]
console = Console(file=io.StringIO(), color_system=get_color(color), highlight=False, soft_wrap=True)
if not disable_headers:
console.print("\n")
if verbose == Verbosity.DEFAULT:
console.print(f"FLARE FLOSS RESULTS (version {results.metadata.version})\n")
else:
colored_str = heading_style(f"FLARE FLOSS RESULTS (version {results.metadata.version})\n")
console.print(colored_str)
render_meta(results, console, verbose)
console.print("\n")
if results.analysis.enable_static_strings:
render_staticstrings(results.strings.static_strings, console, verbose, disable_headers)
console.print("\n")
if results.metadata.language in (
floss.language.identify.Language.GO.value,
floss.language.identify.Language.RUST.value,
):
render_language_strings(
results.metadata.language,
results.strings.language_strings,
results.strings.language_strings_missed,
console,
verbose,
disable_headers,
)
console.print("\n")
if results.analysis.enable_stack_strings:
render_heading(f"FLOSS STACK STRINGS ({len(results.strings.stack_strings)})", console, verbose, disable_headers)
render_stackstrings(results.strings.stack_strings, console, verbose, disable_headers)
console.print("\n")
if results.analysis.enable_tight_strings:
render_heading(f"FLOSS TIGHT STRINGS ({len(results.strings.tight_strings)})", console, verbose, disable_headers)
render_stackstrings(results.strings.tight_strings, console, verbose, disable_headers)
console.print("\n")
if results.analysis.enable_decoded_strings:
render_heading(
f"FLOSS DECODED STRINGS ({len(results.strings.decoded_strings)})", console, verbose, disable_headers
)
render_decoded_strings(results.strings.decoded_strings, console, verbose, disable_headers)
console.file.seek(0)
return console.file.read()