diff --git a/.gitignore b/.gitignore index ff6cdb1..e0cd274 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,12 @@ data/ *.til *.nam .coverage -*.log \ No newline at end of file +*.log +*.run +*.hexlic +*.reg +*.key +*.i64 +*.BinExport +*.quokka +.gitlab-ci-local* \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3b31504..5f15855 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,25 +1,140 @@ -image: python:latest +#========================== STEPS USED IN WORKFLOWS ==================================== +.step_python_setup_install: &step_python_setup_install + - echo -e "\e[95m===== Setup Python" + - python --version ; pip --version + - python3 -m venv venv + - source venv/bin/activate +.step_install_pyrrha_test: &step_install_pyrrha_test + - echo -e "\e[95m===== Install Pyrrha with test extension" + - pip install '.[test]' -before_script: - - python --version ; pip --version # For debugging - - pip install virtualenv - - virtualenv venv - - source venv/bin/activate +.step_configure_disassembler: &step_configure_disassembler + - if [[ ${DISASSEMBLER} == "ida" ]]; then + echo -e "\e[95m===== Configure IDA" && + mkdir -p ~/.idapro/ && + echo $KEY | base64 -d > ~/.idapro/$KEY_NAME && + echo $REG | base64 -d > ~/.idapro/ida.reg && + export IDA_LICENSE=keyfile=$KEY_NAME && + idapyswitch -a ; fi; + +.step_gen_artifacts: &step_gen_artifacts + - echo -e "\e[95m===== Generate artifacts" + - mkdir -p ${ARTIFACTS} + - cp -r tests ${ARTIFACTS} + - (cd ${ARTIFACTS} && pyrrha $MAPPER --db ${DB} -j $(nproc) tests/test_fw ${MAPPER_OPTIONS}) + +.step_run_tests: &step_run_tests + - echo -e "\e[95m===== Tests" + - coverage run --source=${TEST_COVERAGE_SOURCE} -m pytest --junitxml=report.xml -vvv -x ${TEST_SUP_OPTIONS} ${TEST_PATH} + - coverage xml + - coverage report + +#========================== OBJECTS TESTS ==================================== + +test_data_structures: + stage: test + before_script: + - *step_python_setup_install + - *step_install_pyrrha_test + script: + - *step_run_tests + image: python:latest + variables: + TEST_COVERAGE_SOURCE: pyrrha_mapper.common.objects + TEST_PATH: tests/test_filesystem_objects.py + coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' + artifacts: + reports: + junit: report.xml + coverage_report: + coverage_format: cobertura + path: coverage.xml -test: +#========================== MAPPERS TESTS ==================================== +.run_pyrrha_test_artifacts: + stage: test + before_script: + - *step_python_setup_install + - *step_install_pyrrha_test script: - - pip install ".[test]" - - pytest --junitxml=report.xml --cov="pyrrha_mapper" --cov-report xml -v tests/ - - coverage report + - *step_gen_artifacts + - *step_run_tests coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' artifacts: + name: db_$CI_JOB_NAME_SLUG when: always + paths: + - ${ARTIFACTS}/${DB}.srctrldb + - ${ARTIFACTS}/${DB}.srctrlprj reports: junit: report.xml coverage_report: coverage_format: cobertura path: coverage.xml + variables: + ARTIFACTS: tmp/artifacts + +test_fs: + extends: + - .run_pyrrha_test_artifacts + image: python:latest + variables: + DB: fs + MAPPER: fs + TEST_COVERAGE_SOURCE: pyrrha_mapper.common.filesystem_mapper,pyrrha_mapper.fs + TEST_PATH: tests/test_cli.py::TestFSMapper + +.test_fs-cg: + extends: + - .run_pyrrha_test_artifacts + before_script: + - !reference [.run_pyrrha_test_artifacts, before_script] + - *step_configure_disassembler + image: + name: $CONTAINER_PATH/${DISASSEMBLER}:${VERSION} + docker: + user: user + variables: + DB: ${DISASSEMBLER}_${VERSION}_${EXPORTER} + MAPPER: fs-cg + MAPPER_OPTIONS: '--disassembler ${DISASSEMBLER} --exporter ${EXPORTER}' + TEST_COVERAGE_SOURCE: pyrrha_mapper.common.filesystem_mapper,pyrrha_mapper.intercg + TEST_PATH: tests/test_cli.py::TestFsCgMapper + TEST_SUP_OPTIONS: ${MAPPER_OPTIONS} +test_fs-cg_ghidra: + extends: + - .test_fs-cg + variables: + DISASSEMBLER: ghidra + parallel: + matrix: + - VERSION: 11.1.2 + EXPORTER: binexport +test_fs-cg_ida: + extends: + - .test_fs-cg + variables: + DISASSEMBLER: ida + parallel: + matrix: + - VERSION: 11.1.2 + EXPORTER: binexport + - VERSION: 84 + EXPORTER: [quokka, binexport] + - VERSION: 91 + EXPORTER: quokka + rules: + - if: $VERSION == "84" + variables: + KEY: $IDA_KEY + KEY_NAME: ida.key + REG: $IDA84_REG + - if: $VERSION == "91" + variables: + KEY: $LICENSE + KEY_NAME: ida_license.hexlic + REG: $IDA_REG diff --git a/ci/ghidra/Dockerfile b/ci/ghidra/Dockerfile new file mode 100644 index 0000000..69b5785 --- /dev/null +++ b/ci/ghidra/Dockerfile @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Copyright 2023-2025 Quarkslab +# +# 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 +# +# https://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. + +FROM openjdk:21-jdk-slim +SHELL ["/bin/bash", "-c"] + +# ======================== Ghidra Installation ================================= + +ARG GHIDRA_VERSION=11.1.2 +ARG GHIDRA_RELEASE_DATE=20240709 +ARG GHIDRA_URL=https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_RELEASE_DATE}.zip +ENV GHIDRA_INSTALL_DIR=/opt/ghidra_${GHIDRA_VERSION}_PUBLIC + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \ + ca-certificates \ + libfreetype6 \ + libmagic1 \ + libpython3-dev \ + python3-minimal \ + python3-pip \ + python3-venv \ + python-is-python3 \ + unzip \ + wget \ + && rm -rf /var/lib/apt/lists/* + +RUN wget $GHIDRA_URL -O ghidra.zip && unzip ghidra.zip -d /opt/ && rm ghidra.zip + +ENV PATH=${GHIDRA_INSTALL_DIR}:${PATH} + +# ======================== Plugin Installation ============================== + +ARG BINEXPORT_URL=https://github.com/google/binexport/archive/refs/heads/main.zip +ARG GRADLE_VERSION=8.14.3 +ARG GRADLE_URL=https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip +ARG GHIDRA_PLUGIN_DIR=/root/.config/ghidra/ghidra_${GHIDRA_VERSION}_PUBLIC/Extensions + +RUN wget ${GRADLE_URL} -O gradle.zip \ + && unzip gradle.zip -d gradle \ + && wget ${BINEXPORT_URL} -O binexport.zip \ + && mkdir -p ${GHIDRA_PLUGIN_DIR} \ + && unzip binexport.zip binexport-main/java/* -d binexport \ + && (cd binexport/binexport-main/java/ && /gradle/gradle-${GRADLE_VERSION}/bin/gradle buildExtension -PGHIDRA_INSTALL_DIR=${GHIDRA_INSTALL_DIR} && unzip dist/ghidra_${GHIDRA_VERSION}_PUBLIC_$( date +%Y%m%d)_BinExport.zip -d ${GHIDRA_PLUGIN_DIR}) \ + && rm -rf gradle.zip gradle binexport.zip binexport \ + && apt-get purge --yes wget unzip && apt --yes autoremove + +# ======================== USER CREATION ============================== +ARG USER_GHIDRA_PLUGIN_DIR=/home/user/.config/ghidra/ghidra_${GHIDRA_VERSION}_PUBLIC/Extensions + +RUN useradd --create-home -u 1000 -m user && chown -R user:user $GHIDRA_INSTALL_DIR +RUN mkdir -p ${USER_GHIDRA_PLUGIN_DIR} && mv ${GHIDRA_PLUGIN_DIR}/* ${USER_GHIDRA_PLUGIN_DIR} && chown -R user:user /home/user/.config +USER user +WORKDIR /home/user + +CMD ["/bin/bash"] \ No newline at end of file diff --git a/ci/ida/Dockerfile b/ci/ida/Dockerfile new file mode 100644 index 0000000..02087cb --- /dev/null +++ b/ci/ida/Dockerfile @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +# Copyright 2023-2025 Quarkslab +# +# 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 +# +# https://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. + +FROM docker.io/library/debian:testing-slim +SHELL ["/bin/bash", "-c"] + +# ============= How to generate the required data =========================== +# paths can be changed from commandline if needed +# idapro.hexlic: license file, downloaded from your account on hex-rays website +# ida-pro_91.run: executable file, downloaded from your account on hex-rays website +# ida.reg: history file, to be generated manually. Keep in memory that the licence has alredy been accepted. +# 1. Build this docker with an empty ida.reg and launch it. +# 2. Launch idat and accept the license. +# 3. In another terminal, get the id of the current ida docker with `docker ps` +# 4. Run `docker cp ID:/root/.idapro/ida.reg ./` where ID is the id get at the +# previous step, you know have a correct ida.reg. +# 5. Rebuild your image with the correct ida.reg +# =========================================================================== + +# ======================== IDA Installation ================================= + +ARG IDA_VERSION=91 +ARG IDA_INSTALLER=ida-pro_${IDA_VERSION}.run +ENV IDA_INSTALL_DIR=/opt/ida_${IDA_VERSION} + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \ + ca-certificates \ + ccache \ + cmake \ + g++ \ + gcc \ + git \ + libpython3-dev \ + libqt5gui5 \ + libfontconfig1 \ + libmagic1 \ + libsecret-1-0 \ + make \ + ninja-build \ + python3-minimal \ + python3-pip \ + python3-venv \ + python-is-python3 \ + unzip \ + xcb-proto \ + wget \ + zlib1g-dev \ + && mkdir -p $IDA_INSTALL_DIR ~/.local/share/applications/ + +RUN --mount=type=bind,src=${IDA_INSTALLER},target=${IDA_INSTALLER} DEBIAN_FRONTEND=noninteractive apt-get install --yes --reinstall libxcb-xinerama0 && \ + ./${IDA_INSTALLER} --mode unattended --prefix ${IDA_INSTALL_DIR} + +ENV PATH=${IDA_INSTALL_DIR}:${PATH} + +# ======================== Plugin Installation ============================== + +ARG QUOKKA_VERSION=v0.6.1 +ARG QUOKKA_URL=https://github.com/quarkslab/quokka/releases/download/${QUOKKA_VERSION}/${IDA_VERSION}-quokka_plugin0064.so +ARG BINEXPORT_URL=https://github.com/google/binexport/releases/download/v12-20240417-ghidra_11.0.3/BinExport-Linux.zip +RUN if [[ ${IDA_VERSION} -eq 84 ]]; then \ + wget ${QUOKKA_URL} -O ${IDA_INSTALL_DIR}/plugins/quokka64.so \ + && wget ${BINEXPORT_URL} -O binexport.zip \ + && unzip -j binexport.zip ida/binexport12_ida.so ida/binexport12_ida64.so -d ${IDA_INSTALL_DIR}/plugins/ \ + && rm -f binexport.zip ; \ + else wget ${QUOKKA_URL} -O ${IDA_INSTALL_DIR}/plugins/quokka.so ; fi \ + && apt-get purge --yes wget \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd --create-home -u 1000 -m user && chown -R user:user $IDA_INSTALL_DIR +USER user +RUN $IDA_INSTALL_DIR/idapyswitch -a +WORKDIR /home/user diff --git a/docs/disassembler.md b/docs/disassembler.md new file mode 100644 index 0000000..6b0046d --- /dev/null +++ b/docs/disassembler.md @@ -0,0 +1,143 @@ +# Disassembler Integration + +Some pyrrha mappers and especially the `exe-decomp` enables jumping in a disassembler from the UI +by right-clicking on a function and selecting "Open in disassembler". Executing arbitrary command +is made available by the [Numbat feature](https://quarkslab.github.io/numbat/customization/) and +requires opening a Sourcetrail DB with ``NumbatUI``. + +The link between Numbat and a disassembler is made by implementing custom URL protocol handlers. As such, +clicking "Open in disassembler" will trigger a command like: + +```bash +xdg-open 'disas://e62f747cf47383858bd563febb813e20?idb=inadyn.i64&offset=0x0124c8' +``` + +On Linux `xdg-open` will open the URL with the default application associated with the `disas` protocol. +For windows and MacOS, application opened are respectively `start` and `open`. For it to work, +we need to register a custom URL handler for the `disas` protocol. This is done by using [heimdallr](https://github.com/interruptlabs/heimdallr-client) developped by [Interrupt Labs](https://interruptlabs.com/). + + +## Heimdallr + +Heimdallr is a custom URL handler that allows you to open a disassembler from the UI. Developpres provides +an [IDA plugin](https://github.com/interruptlabs/heimdallr-ida) to support it and some folks added a [Ghidra +support](https://github.com/foundryzero/ghidra-deep-links). It works by running a gRPC server in the disassembler +that will listen for incoming requests. The image below summarizes the workflow on Linux: + + + +As shown on the image the Linux system handles URL handlers with `.desktop` files that needs to be registered. +The handler will call `heimdallr_client` utility that is in charge of identifying running gRPC servers to send +the query to a running disassembler or to start it. + +## Installation + +Heimdallr is fairly unmaintained and undocumented. Still, it works rather well. In order to get it working +one need to perform the following steps: + +1. Install `heimdallr-ida` plugin in IDA +2. Install `heimdallr-client` "globally" so that it is reachable by the URL handler dispatcher +3. Configure a `settings.json` file to specify disassembler path etc. +4. Create and register a `.desktop` file to handle the `disas://` protocol. + +**1-heimdallr-ida**: The plugin is available on the [Github page](https://github.com/interruptlabs/heimdallr-ida). +The README.md provides installation steps. The ``install()`` command will automatically copy files in the IDA Pro +directory and creates a default `settings.json` file in `$HOME/.config/heimdallr/settings.json`. + +!!! tip + The install command might be a bit buggy, so it is recommended to install the plugin manually by copying the + files in IDA. + +**2-heimdallr-client**: The client is available on the [Github page](https://github.com/interruptlabs/heimdallr-client). +It can be installed with `pip`: + +```bash +pip3 install git+https://git@github.com/interruptlabs/heimdallr-client.git#egg=heimdallr_client +``` + +!!! note + It should be installed globally so that it is reachable by the URL handler dispatcher. Thus it is recommended + to install it with `--user`. + +**3-Configuring settings**: The `$HOME/.config/heimdallr` will contain all files used by `heimdallr` to locate +running RPC server instances in order to send them requests. The file `settings.json` is used to configure +the disassembler path and paths where to look for binaries. Thus configure carefully your IDA path inside. + +```json +{ + "ida_location": "/my/path/to/ida", + "idb_path": [ + ], + "heimdallr_client": "heimdallr_client" +} +``` + +!!! note + The IDA location binary provided should be a non-blocking IDA or bash script, as `heimdallr-client` + will run it with `subprocess.run` and wait for it before sending the request. + + +**4-Creating protocol handler**: The `.desktop` file is used to register the `disas://` protocol handler. +On Linux, it is usually located in `~/.local/share/applications/`. Creates a file in this directory with +the following content: + +???+ "`heimdallr.desktop`" + ```ini + [Desktop Entry] + Name=Heimdallr-handler + Comment=Disas URL handler + GenericName=heimdallr-handler-generic + Exec=heimdallr_client %u + Type=Application + StartupNotify=true + Categories=GNOME;GTK;Utility; + MimeType=x-scheme-handler/disas; + ``` + +Then you need to update the associated `mimeinfo.cache` file with: + +```bash +update-desktop-database ~/.local/share/applications +``` +This will allow you to handle URLs with the `disas://` scheme. +*It shall add the line: `x-scheme-handler/disas=heimdallr.desktop` in the file.* + + +## Testing + +You can test that URL are properly resolved by running: + +```bash +xdg-mime query default x-scheme-handler/disas +``` +This should return `heimdallr.desktop`. Then you can try opening a binary with: + +```bash +xdg-open 'disas://e62f747cf47383858bd563febb813e20?idb=inadyn.i64&offset=0x0124c8' +``` + +Where you provide the MD5 hash of the binary, its DB name and the offset to jump to. +By default, heimdallr look in the IDA Pro history to locate the idb. Otherwise, it search +for directories referenced in the "idb_path" field of the `settings.json` file. + + + +## Usage in Pyrrha mappers + +Pyrrha uses `heimdallr` to resolve binaries location and offsets. Thus when working +on a specific firmware you might need to specify its root directory in the `ida_path` +of the `settings.json` file. Pyrrha provides an utility command to list, add and remove +entries in this file. + +```bash +pyrrha workspace-utils --list # list all entries +``` + +```bash +pyrrha workspace-utils --add /path/to/firmware/rootfs # add directory in search path +``` + +```bash +pyrrha workspace-utils --delete /path/to/firmware/rootfs # remove directory from search path +``` + diff --git a/docs/img/heimdallr.svg b/docs/img/heimdallr.svg new file mode 100644 index 0000000..c608b02 --- /dev/null +++ b/docs/img/heimdallr.svg @@ -0,0 +1,3 @@ + + +disas://URLxdg-opendisas://URL NumbatUI call .desktop(define handler)start orsend RPCheimdallr_client(search listening gRPC server)IDA Pro(with heimdallr-ida plugin)Or another disassember \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 1f3a9dd..289cbbd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,6 +42,7 @@ nav: - Exe-Decomp: mappers/exe-decomp.md - Contributing: - Mapper Development: contributing/dev_mapper.md + - Disassembler Integration: disassembler.md - Changelog: changelog.md - License: license.md diff --git a/pyproject.toml b/pyproject.toml index 26cb753..4301a80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,16 +39,14 @@ classifiers = [ 'Environment :: Console', ] dependencies = [ - 'click', + 'click>=8.2.0', 'coloredlogs', 'lief>=0.15.0', 'numbat>=0.2.6', 'pydantic', 'rich', # InterCG mapper - 'quokka-project', - # Exedecomp mapper - 'idascript<=0.3.1', + "qbinary>=0.0.3", # will also install idascript ] dynamic = ['version'] diff --git a/src/pyrrha_mapper/__main__.py b/src/pyrrha_mapper/__main__.py index 3e33180..d30407f 100644 --- a/src/pyrrha_mapper/__main__.py +++ b/src/pyrrha_mapper/__main__.py @@ -15,17 +15,22 @@ # limitations under the License. """CLI Module.""" +import json import logging import multiprocessing +import os +import shutil +import sys from pathlib import Path import click import coloredlogs # type: ignore # no typing used in this library from numbat import SourcetrailDB +from qbinary.types import Disassembler, ExportFormat from pyrrha_mapper import exedecomp, fs, intercg from pyrrha_mapper.common import FileSystem -from pyrrha_mapper.types import Disassembler, Exporters, ResolveDuplicateOption +from pyrrha_mapper.types import ResolveDuplicateOption # ------------------------------------------------------------------------------- # Common stuff for mappers @@ -44,9 +49,9 @@ def __init__(self, *args, **kwargs): 0, click.core.Option( ("--db",), - help="NumbatUI DB file path (.srctrldb).", + help=f"NumbatUI DB file path ({SourcetrailDB.SOURCETRAIL_DB_EXT}).", type=click.Path(file_okay=True, dir_okay=True, path_type=Path), - default=Path() / f"{self.name}.srctrldb", + default=Path() / f"{self.name}{SourcetrailDB.SOURCETRAIL_DB_EXT}", show_default=True, ), ) @@ -63,9 +68,7 @@ def setup_logs(is_debug_level: bool, db_path: Path | None = None) -> None: :param is_debug_level: if True set the log level as DEBUG else INFO :param db_path: if provided, save a collocated log file. """ - log_format = dict( - fmt="[%(asctime)s][%(levelname)s]: %(message)s", datefmt="%Y-%m-%d %H:%M:%S" - ) + log_format = dict(fmt="[%(asctime)s][%(levelname)s]: %(message)s", datefmt="%Y-%m-%d %H:%M:%S") level = logging.DEBUG if is_debug_level else logging.INFO coloredlogs.install( level=level, @@ -101,7 +104,10 @@ def setup_db(db_path, overwrite_db: bool = True) -> SourcetrailDB: if SourcetrailDB.exists(db_path): db = SourcetrailDB.open(db_path, clear=overwrite_db) else: - db = SourcetrailDB.create(db_path) + path = Path(db_path) + if path.suffix != SourcetrailDB.SOURCETRAIL_DB_EXT: + path = path.with_suffix(f"{path.suffix}{SourcetrailDB.SOURCETRAIL_DB_EXT}") + db = SourcetrailDB.create(path) return db @@ -176,8 +182,8 @@ def pyrrha(): # noqa: D103 # help='Path of the directory containing the filesystem to map.', type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), ) -def fs_mapper(# noqa: D103 - debug: bool, +def fs_mapper( # noqa: D103 + debug: bool, db: Path, export: bool, jobs: int, @@ -240,18 +246,18 @@ def fs_mapper(# noqa: D103 @click.option( "--disassembler", required=False, - type=Disassembler, + type=click.Choice(Disassembler, case_sensitive=False), default=Disassembler.AUTO, show_default=True, - help="Disassembler to use for disassembly.", + help="Disassembler to use", ) @click.option( "--exporter", required=False, - type=Exporters, - default=Exporters.AUTO, + type=click.Choice(ExportFormat, case_sensitive=False), + default=ExportFormat.AUTO, show_default=True, - help="Binary exporter to use for binary analysis.", + help="Binary exporter", ) @click.argument( "root_directory", @@ -264,21 +270,27 @@ def fs_call_graph_mapper( # noqa: D103 jobs: int, resolve_duplicates: ResolveDuplicateOption, disassembler: Disassembler, - exporter: Exporters, - root_directory, + exporter: ExportFormat, + root_directory: Path, ): setup_logs(debug, db) db_instance = setup_db(db) - if disassembler not in [Disassembler.AUTO, Disassembler.IDA]: + if disassembler not in [Disassembler.AUTO, Disassembler.IDA, Disassembler.GHIDRA]: click.echo("disassembler not yet supported") # TODO: add support for other disassembler return 1 - if exporter not in [Exporters.AUTO, Exporters.QUOKKA]: - click.echo(f"binary exporter: {exporter.name} not yet supported") - # TODO: add support for other disassembler - return 1 + if disassembler is Disassembler.GHIDRA: + ghidra_env_var = "GHIDRA_PATH" + ghidra_dir = os.environ.get(ghidra_env_var) + if not ghidra_dir: + for ghidra_name in ["ghidra", "ghidraRun"]: + if ghidra_path := shutil.which(ghidra_name): + os.environ[ghidra_env_var] = str(Path(ghidra_path).resolve().parent) + + intercg.InterImageCGMapper.DISASS = disassembler + intercg.InterImageCGMapper.EXPORT = exporter root_directory = root_directory.absolute() @@ -308,34 +320,96 @@ def fs_call_graph_mapper( # noqa: D103 @click.option( "--disassembler", required=False, - type=Disassembler, + type=click.Choice(Disassembler, case_sensitive=False), default=Disassembler.AUTO, show_default=True, - help="Disassembler to use for disassembly.", + help="Disassembler to use for disassembly and decompilation.", +) +@click.option( + "--exporter", + required=False, + type=click.Choice(ExportFormat, case_sensitive=False), + default=ExportFormat.AUTO, + show_default=True, + help="Binary export format to use for binary analysis.", ) @click.argument( "executable", - type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path), + type=click.Path(exists=False, file_okay=True, dir_okay=False, path_type=Path), ) def fs_exe_decompiled_mapper( # noqa: D103 - debug: bool, db: Path, disassembler: Disassembler, executable: Path + debug: bool, db: Path, disassembler: Disassembler, exporter: ExportFormat, executable: Path ): + # Change default db name. By default will be .srctrldb + if db.name == "exe-decomp.srctrldb": + db = Path(str(executable) + ".srctrldb") + setup_logs(debug, db) db_instance = setup_db(db) if disassembler not in [Disassembler.AUTO, Disassembler.IDA]: - click.echo("disassembler not yet supported") + click.echo(f"disassembler {disassembler.name} not yet supported") # TODO: add support for other disassembler (forward parameter to mapper) return 1 - if exedecomp.map_binary(db_instance, executable): + if exedecomp.map_binary(db_instance, executable, disassembler, exporter): logging.info("success.") else: logging.error("failure.") + logging.info(f"write db into: {db_instance.path}") db_instance.commit() db_instance.close() +@pyrrha.command( + "workspace-utils", short_help="Help managing workspaces (for cross-binary referencing)." +) +@click.option("-l", "--list", is_flag=True, default=False, help="List all workspaces.") +@click.option("-a", "--add", is_flag=True, default=False, help="Add a rootfs as workspace.") +@click.option("-d", "--delete", is_flag=True, default=False, help="Remove a rootfs as workspace.") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=True, dir_okay=True, path_type=Path), + required=False, +) +def workspace_utils(list: bool, add: bool, delete: bool, path: Path): + """Manage workspaces for cross-binary referencing.""" + # Configure logs (there is not debug ones) + setup_logs(False) + + # Get the base config directory + if sys.platform == "win32": + heimdallr_settings = Path(os.path.expandvars("%APPDATA%/heimdallr/settings.json")) + else: + heimdallr_settings = Path(os.path.expandvars("$HOME/.config/heimdallr/settings.json")) + if not heimdallr_settings.exists(): + click.echo(f"heimdallr config directory {heimdallr_settings} does not exists") + return -1 + + # Load settings + settings = json.loads(heimdallr_settings.read_text()) + idb_path = settings.get("idb_path") + if idb_path is None: + click.echo(f"heimdallr settings file {heimdallr_settings} does not contain idb_path") + return -1 + + if list: + for path in idb_path: + logging.info(f"- {path}") + + if add: + settings["idb_path"].append(str(Path(path).absolute())) + heimdallr_settings.write_text(json.dumps(settings, indent=4)) # Write it back + + if delete: + try: + settings["idb_path"].remove(str(path)) + heimdallr_settings.write_text(json.dumps(settings, indent=4)) # Write it back + except ValueError: + click.echo(f"Path {path} not in idb_path of settings.") + return -1 + + if __name__ == "__main__": pyrrha() diff --git a/src/pyrrha_mapper/common/__init__.py b/src/pyrrha_mapper/common/__init__.py index bdb4265..b4be036 100644 --- a/src/pyrrha_mapper/common/__init__.py +++ b/src/pyrrha_mapper/common/__init__.py @@ -13,7 +13,7 @@ # 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. -"""Common objects that can be used for any mapper.""" +"""Common objects and functions that can be used for any mapper.""" from .filesystem_mapper import FileSystemMapper, hide_progress from .objects import Binary, FileSystem, Symbol, Symlink diff --git a/src/pyrrha_mapper/common/filesystem_mapper.py b/src/pyrrha_mapper/common/filesystem_mapper.py index 8c444c5..3225fb7 100755 --- a/src/pyrrha_mapper/common/filesystem_mapper.py +++ b/src/pyrrha_mapper/common/filesystem_mapper.py @@ -31,7 +31,7 @@ TimeElapsedColumn, ) -from pyrrha_mapper.common.objects import Binary, FileSystem, Symlink +from pyrrha_mapper.common.objects import Binary, FileSystem, Symlink, Symbol from pyrrha_mapper.exceptions import PyrrhaError from pyrrha_mapper.types import ResolveDuplicateOption @@ -152,10 +152,12 @@ def record_binary_in_db(self, binary: Binary, log_prefix: str = "") -> Binary: parent_id=binary.id, prefix=hex(symbol.addr) if symbol.addr is not None else "None", ) + if symbol.id is None: logging.error(f"{log_prefix}: Record of symbol '{symbol.demangled_name}' failed.") else: try: + self.symbol_recorded(binary, symbol) self.db_interface.record_public_access(symbol.id) recorded_symb[symbol.demangled_name] = symbol.id except DBException as e: @@ -163,6 +165,7 @@ def record_binary_in_db(self, binary: Binary, log_prefix: str = "") -> Binary: f"{log_prefix}: Cannot register access to symbol {symbol.demangled_name}: " f"{e}" ) from e + for symbol in set(binary.iter_not_exported_functions()): symbol.id = self.db_interface.record_method( symbol.demangled_name, @@ -173,6 +176,7 @@ def record_binary_in_db(self, binary: Binary, log_prefix: str = "") -> Binary: logging.error(f"{log_prefix}: Record of symbol '{symbol.demangled_name}' failed.") else: try: + self.symbol_recorded(binary, symbol) self.db_interface.record_private_access(symbol.id) except DBException as e: raise PyrrhaError( @@ -182,6 +186,16 @@ def record_binary_in_db(self, binary: Binary, log_prefix: str = "") -> Binary: return binary + def symbol_recorded(self, binary: Binary, symbol: Symbol) -> None: + """Hook called when a symbol is recorded in the DB. + + This method can be overridden to add custom behavior. + + :param binary: the Binary object containing the method + :param symbol: the Symbol object representing the method + """ + pass # Default implementation does nothing + def record_symlink_in_db(self, sym: Symlink, log_prefix: str = "") -> Symlink: """Record into DB the symlink and its link to its target. diff --git a/src/pyrrha_mapper/exedecomp/binmapper.py b/src/pyrrha_mapper/exedecomp/binmapper.py index 47d1d66..7544279 100644 --- a/src/pyrrha_mapper/exedecomp/binmapper.py +++ b/src/pyrrha_mapper/exedecomp/binmapper.py @@ -13,20 +13,26 @@ # 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. -"""Filesystem mapper based on Lief, which computes imports and exports.""" +"""Decompilation code binary mapper.""" -import json import logging -from collections import defaultdict, namedtuple -from dataclasses import dataclass +import json from pathlib import Path +from collections import defaultdict +from dataclasses import dataclass +import sys +from typing import NamedTuple from tempfile import NamedTemporaryFile +import hashlib # third-party imports +from qbinary import Program, Function, FunctionType +from qbinary.types import Disassembler, ExportFormat, DisassExportNotImplemented, ExportException + + +from numbat import SourcetrailDB from idascript import IDA from numbat import SourcetrailDB -from quokka import Function, Program -from quokka.types import FunctionType from rich.progress import ( BarColumn, MofNCompleteColumn, @@ -35,11 +41,32 @@ TimeElapsedColumn, ) +# local imports +from pyrrha_mapper.exceptions import FsMapperError + DECOMPILE_SCRIPT = Path(__file__).parent / "decompile.py" +# Determine the command to open URLs based on the platform +try: + URL_OPEN_CMD = { + "linux": "xdg-open", + "win32": "start", + "darwin": "open" + }[sys.platform] +except KeyError: + logging.warning(f"Unsupported platform: {sys.platform} (will not add URL handler)") + URL_OPEN_CMD = "" # type: ignore + + once_check = True -Location = namedtuple("Location", "start_line start_col end_line end_col") + + +class Location(NamedTuple): + start_line: int + start_col: int + end_line: int + end_col: int @dataclass @@ -56,37 +83,23 @@ class DecompiledFunction: numbat_id: int = -1 -def is_new_numbat(db: SourcetrailDB) -> bool: - """:return: True heck if a DB is generated by NumbatUI, False if it is by Sourcetrail.""" - global once_check - if hasattr(db, "change_node_color"): # check that attribute but could have been another one - return True - else: - if once_check: - logging.warning("Numbat does not support advanced features") - once_check = False - return False - - def normalize_name(name: str) -> str: """Transform function name.""" return name.strip("_").strip(".") -def find_all_call_references( - f: Function, source: str, log_prefix: str = "" -) -> tuple[Location, dict[int, list[Location]]]: +def find_all_call_references(p:Program, f: Function, source: str, + log_prefix: str = "") -> tuple[Location, dict[int, list[Location]]]: decl_loc = None - refs = defaultdict(list) # dict: call_addr -> list[Location] - # ppname = lambda name: name.strip("_").strip(".") - - call_name_to_addr = { - normalize_name(c.name): c.start for c in f.calls if c.name - } # NOTE: we exclude by design calls that - call_addr_to_name = { - c.start: normalize_name(c.name) for c in f.calls if c.name - } # don't have a name, usually these are calls + refs: dict[int, list[Location]] = defaultdict(list) # dict: call_addr -> list[Location] + #ppname = lambda name: name.strip("_").strip(".") + + # NOTE: we exclude by design calls that don't have a name, usually these are calls # to unrecognized function e.g: loc_185CC + call_name_to_addr = {normalize_name(p[c].name): c for c in f.children if p[c].name} + call_addr_to_name = {c: normalize_name(p[c].name) for c in f.children if p[c].name} + + for idx, line in enumerate(source.splitlines()): # try to find function declaration if decl_loc is None: @@ -133,43 +146,47 @@ def find_all_call_references( if decl_loc is None: logging.error(f"{log_prefix}: function declaration not found in source code") - for ref in (x for x in call_addr_to_name if x not in refs): - logging.error( - f"{log_prefix}: call to {ref:#08x}: '{call_addr_to_name[ref]}' not found in source code" - ) + + if not is_thunk_to_import(p, f): # it is normal no to find the call in thunks to imports + for ref in (x for x in call_addr_to_name if x not in refs): + logging.warning(f"{log_prefix}: call to {ref:#08x}: '{call_addr_to_name[ref]}' not found in source code") return decl_loc, refs -def decompile_program(program: Program) -> Path: +def decompile_program(program: Program) -> None: """Generate a PROGRAM_NAME.decompiled file which contained the binary decompilee obtained with IDA. :param program: Program object of the file to decompiled :return: path of the created decompiled file. """ - bin_path = program.executable.exec_file + bin_path: str = program.exec_path + assert bin_path, "program.exec_path is not set, can't decompile" ida = IDA(bin_path, str(DECOMPILE_SCRIPT), [], timeout=600, exit_virtualenv=True) ida.start() ida.wait() - return Path(str(bin_path) + ".decompiled") -def load_decompiled(program: Program, progress: Progress) -> dict[int, DecompiledFunction]: - decompile_file = program.executable.exec_file.with_suffix( - f"{program.executable.exec_file.suffix}.decompiled" - ) +def load_decompiled(program: Program, progress: Progress, + log_prefix: str = "") -> dict[int, DecompiledFunction]: + decompile_file = Path(f"{program.exec_path}.decompiled") + if decompile_file.exists(): - logging.info(f"load decompilation file: {decompile_file}") + logging.info(f"{log_prefix}: load file: {decompile_file}") data = {int(k): v for k, v in json.loads(decompile_file.read_text()).items()} - final_data = {} + final_data: dict[int, DecompiledFunction] = {} # Iterate the decompiled data to try make references inside decomp_load = progress.add_task("[deep_pink2]Decompiled binary loading", total=len(data)) for f_addr, source_text in data.items(): - f = program[f_addr] + f: Function = program.get(f_addr) + if f is None: + logging.warning(f"{log_prefix}: function at {f_addr:#08x} referenced " + "in decompiled code not found in exported program") + continue - decl, refs = find_all_call_references( - f, source_text, log_prefix=f"[Decompiled binary loading] {f.name}" - ) + decl, refs = find_all_call_references(program, f, source_text, f"{log_prefix} {f.name}") + + assert decl is not None, f"function {f.name} declaration not found in source code" final_data[f_addr] = DecompiledFunction( address=f_addr, name=f.name, text=source_text, location=decl, references=refs @@ -178,39 +195,42 @@ def load_decompiled(program: Program, progress: Progress) -> dict[int, Decompile return final_data else: - logging.info("extracting decompilation file (with idascript)") - decompile_file = decompile_program(program) + logging.info(f"{log_prefix}: extracting decompilation file {decompile_file} (with idascript)") + decompile_program(program) if decompile_file.exists(): - return load_decompiled(program, progress) # call ourselves again + return load_decompiled(program, progress, log_prefix) # call ourselves again else: - logging.warning("can't find decompilation file and idascript failed") - return {} + raise FileNotFoundError("can't find decompilation file (idascript failed)") -def load_program(bin_path: Path) -> Program: - quokka_file = Path(f"{bin_path}.quokka") - if quokka_file.exists(): - logging.info("loading existing Quokka file") - return Program(quokka_file, bin_path) - else: # Quokka file does not exists - return Program.from_binary(bin_path, quokka_file) +def load_program(bin_path: Path, disass: Disassembler, format: ExportFormat) -> Program | None: + # First try to find pre-existing exported files if format is AUTO + try: + return Program.from_binary(bin_path, + export_format=format, + disassembler=disass, + timeout= 600, # TODO: Receive through command line ? + override=False, # if export exists use it + ) + except DisassExportNotImplemented as e: + logging.error(f"Disassembler {disass} does not support export format {format}: {e}") + except ExportException as e: + logging.error(f"Error while loading binary {bin_path}: {e}") + return None def set_function_color(db: SourcetrailDB, p: Program, fun: Function, f_id: int) -> None: - if is_new_numbat(db): # Check that we have the capability - # Change node color based on its type - if is_thunk_to_import(p, fun): - db.change_node_color(f_id, fill_color="#bee0af", border_color="#395f33") - elif fun.type == FunctionType.THUNK: - db.change_node_color(f_id, fill_color="gray") - # elif fun.type == FunctionType.EXTERN: - # db.change_node_color(f_id, fill_color="magenta") - # elif fun.type == FunctionType.IMPORTED: - # db.change_node_color(f_id, fill_color="mediumvioletred") - else: - pass # Normal function let default color + # Change node color based on its type + if is_thunk_to_import(p, fun): + db.change_node_color(f_id, fill_color="#bee0af", border_color="#395f33") + elif fun.type == FunctionType.thunk: + db.change_node_color(f_id, fill_color="gray") + # elif fun.type == FunctionType.EXTERN: + # db.change_node_color(f_id, fill_color="magenta") + # elif fun.type == FunctionType.IMPORTED: + # db.change_node_color(f_id, fill_color="mediumvioletred") else: - return + pass # Normal function let default color def add_source_file( @@ -220,17 +240,19 @@ def add_source_file( info: DecompiledFunction, log_prefix: str = "", ) -> bool: - with NamedTemporaryFile(mode="wt", delete_on_close=False) as tmp: + """:return: True if successfully added source info.text as a source file in DB.""" + with NamedTemporaryFile(mode="wt", delete_on_close=True) as tmp: tmp.write(info.text) - tmp.close() + tmp.flush() # Ensure the file is written before we try to record it # Record file file_id = db.record_file(Path(tmp.name), name=mangled_name) if file_id is None: return False db.record_file_language(file_id, "cpp") + tmp.close() # Add the function to the file - logging.debug(f"{log_prefix}: add function to file {file_id}") + logging.debug(f"{log_prefix}: add function {mangled_name} to file {file_id}") info.numbat_id = file_id # record de symbol declaration if info.location: @@ -243,100 +265,114 @@ def add_source_file( def is_thunk_to_import(p: Program, f: Function) -> bool: - if f.type == FunctionType.THUNK: - if len(f.calls) == 1: - c = f.calls[0] - callee = p.get_function_by_chunk(c)[0] - if callee.type in [FunctionType.EXTERN, FunctionType.IMPORTED]: + if f.type == FunctionType.thunk: + if len(f.children) == 1: + c = list(f.children)[0] + callee: Function = p[c] + if callee.type == FunctionType.imported: return True return False else: return False -def map_binary(db: SourcetrailDB, program_path: Path) -> bool: - # Load the Quokka file - program = load_program(program_path) - if program is None: - logging.error("can't generate exported binary") - return False +def add_url_handler(db: SourcetrailDB, program: Program, hash: str, function: Function, f_id: int) -> None: + """ Open the function using a dedicated URL handler. (Use Heimdallr) """ + if URL_OPEN_CMD and program.exec_path: + url = f"disas://{hash}?idb={Path(program.exec_path).name+'.i64'}&offset={function.addr:#08x}" + cmd: list[str] = [URL_OPEN_CMD, url] + db.set_custom_command(f_id, cmd, "Open in Disassembler") # type: ignore + else: + pass # Can't add URL unsuported platform + +def map_binary(db: SourcetrailDB, program_path: Path, disass: Disassembler, format: ExportFormat) -> bool: + # Load the Quokka file with Progress( TextColumn("[progress.description]{task.description}"), BarColumn(), MofNCompleteColumn(), TimeElapsedColumn(), ) as progress: - # Load the decompilation file - decompiled = load_decompiled(program, progress) - if not decompiled: # empty - logging.error("failed to obtain decompiled code") + # Load the decompilation and quokka files + log_prefix = "[binary loading]" + try: + program = load_program(program_path, disass, format) + if program is None: + logging.error(f"{log_prefix} can't generate exported binary") + return False + except FileNotFoundError as e: + logging.error(f"{log_prefix}: Cannot found {program_path}: {e}") + return False + except FsMapperError as e: + logging.error(f"{log_prefix}: Error during Quokka export generation/loading: {e}") + return False + + # Try loading the decompiled file + try: + decompiled = load_decompiled(program, progress, log_prefix) + except FileNotFoundError as e: + logging.error(f"{log_prefix}: failed to obtain decompiled code: {e}") return False + # Compute MD5 hash for URL handler + p_hash = hashlib.md5(Path(program.exec_path).read_bytes()).hexdigest() + # Index all the functions f_mapping = {} # f_addr -> numbat_id func_map = progress.add_task("[orange_red1]Functions analysis", total=len(program)) for f_addr, f in program.items(): - log_prefix = f"[Func analysis] {f.name} ({f.type})" - if f.type in [FunctionType.EXTERN, FunctionType.IMPORTED]: + log_prefix = f"[func analysis] {f.name} ({f.type})" + if f.type == FunctionType.imported: logging.debug(f"{log_prefix}: extern function, skip") progress.update(func_map, advance=1) continue # do not add EXTERN functions is_imp = is_thunk_to_import(program, f) - f_id = db.record_function( - f.name, - parent_id=None, - is_indexed=not is_imp, - ) + f_id = db.record_function(f.name, parent_id=None, is_indexed=not is_imp) if f_id is None: logging.error(f"{log_prefix}: error while recording function in db") progress.update(func_map, advance=1) continue f_mapping[f_addr] = f_id - if not is_imp: - # Change node color based on its type - set_function_color(db, program, f, f_id) + # Change node color based on its type + set_function_color(db, program, f, f_id) - # Add custom command to open that function in IDA - abs_path = program.executable.exec_file.absolute() - cmd = ["ida64", f"-ONumbatJump:{f_addr:#08x}", str(abs_path)] - db.set_custom_command(f_id, cmd, "Open in IDA Pro") + # Add custom command to open that function in IDA + add_url_handler(db, program, p_hash, f, f_id) - # Add source code if any - if f_addr in decompiled: - info = decompiled[f_addr] - if not add_source_file(db, f.mangled_name, f_id, info): - logging.warning(f"{log_prefix}: failed to add decompiled code") + # Add source code if any + if f_addr in decompiled and not is_imp: + info = decompiled[f_addr] + if not add_source_file(db, f.mangled_name, f_id, info): + logging.warning(f"{log_prefix}: failed to add decompiled code") + elif f_addr not in decompiled and not is_imp: + logging.warning(f"{log_prefix}: function not in decompiled dict") + else: + pass # do not add decompiled code for thunks to imports - else: - logging.warning(f"{log_prefix}: function not in decompiled dict") progress.update(func_map, advance=1) + # Index the call graph cg_map = progress.add_task("[orange1]Call Graph Indexing", total=len(program)) for f_addr, f in program.items(): - log_prefix = f"[Call Graph Indexing] {f.name}" + log_prefix = f"[callgraph indexing] {f.name}" decomp_fun = decompiled.get(f_addr, None) - for callee in f.calls: + for callee in f.children: try: - callee_id = f_mapping[callee.start] - ref_id = db.record_ref_call(f_mapping[f_addr], callee_id) # record the call + callee_id = f_mapping[callee] + db.record_ref_call(f_mapping[f_addr], callee_id) # record the call if decomp_fun: # if we have info about the decompiled function - if refs := decomp_fun.references.get( - callee.start - ): # get the refs associated with callee + if refs := decomp_fun.references.get(callee): # get the refs associated with callee for li, coli, le, cole in refs: # iterate them and add them - db.record_reference_location( - ref_id, decomp_fun.numbat_id, li, coli, le, cole - ) + db.record_reference_location(callee_id, decomp_fun.numbat_id, li, coli, le, cole) else: - logging.debug( # correspond to the previous error - f"{log_prefix}: calls {callee.name} but not references in DecompiledFunction" - ) + logging.warning(f"{log_prefix} calls {program[callee].name} " + "but not references in DecompiledFunction") except KeyError: pass # ignore call to non recognized functions diff --git a/src/pyrrha_mapper/exedecomp/decompile.py b/src/pyrrha_mapper/exedecomp/decompile.py index 9719555..4fb8078 100644 --- a/src/pyrrha_mapper/exedecomp/decompile.py +++ b/src/pyrrha_mapper/exedecomp/decompile.py @@ -21,9 +21,16 @@ def main_ida(): input_file = ida_nalt.get_input_file_path() output_file = input_file+".decompiled" + raw_file = input_file+".c" + + # First decompile the whole program + ida_hexrays.clear_cached_cfuncs() + ida_hexrays.decompile_many(raw_file, None, + ida_hexrays.VDRUN_NEWFILE | ida_hexrays.VDRUN_MAYSTOP | ida_hexrays.VDRUN_SILENT) funs = {} + # Then reiterate all functions to get them individually for fun_ea in idautils.Functions(): decomp = ida_hexrays.decompile(fun_ea) if decomp is not None: diff --git a/src/pyrrha_mapper/fs/imports_mapper.py b/src/pyrrha_mapper/fs/imports_mapper.py index 80f93bd..b92092d 100644 --- a/src/pyrrha_mapper/fs/imports_mapper.py +++ b/src/pyrrha_mapper/fs/imports_mapper.py @@ -18,7 +18,9 @@ import logging import queue from abc import ABC +from collections.abc import Callable from dataclasses import dataclass +from functools import partial from multiprocessing import Queue, get_context from pathlib import Path from typing import Any @@ -55,6 +57,14 @@ def is_binary_supported(p: Path) -> bool: :return: True is the path point on a file """ return p.is_file() and not p.is_symlink() and (lief.is_elf(str(p)) or lief.is_pe(str(p))) + + def load_binary_args(self) -> dict[str, Any]: + """Return dict of args for load_binary that are always the same for the wholde firmware. + + Use to optimize multiprocessing. Set here there real values. + """ + return {"root_directory": self.root_directory} + @staticmethod def load_binary(root_directory: Path, file_path: Path) -> tuple[Binary, Any] | str: @@ -143,20 +153,20 @@ def load_binary(root_directory: Path, file_path: Path) -> tuple[Binary, Any] | s return (bin_obj, None) @classmethod - def parse_binary_job(cls, ingress: Queue, egress: Queue, root_directory: Path) -> None: + def parse_binary_job(cls, ingress: Queue, egress: Queue, parse_func: Callable) -> None: """Parse an executable file and create the associated Binary object. It is used for multiprocessing. :param ingress: input Queue, contain a Path :param egress: output Queue, send back (file path, Binary result or logging string if an issue happen) - :param root_directory: path of the virtual root of the firmware + :param parse_func: func which take a path as argument (called file_path) and parse it """ while True: try: path = ingress.get(timeout=0.5) try: - egress.put((path, cls.load_binary(root_directory, path))) + egress.put((path, parse_func(file_path = path))) except Exception as e: egress.put((path, e)) except queue.Empty: @@ -430,16 +440,18 @@ def map_binaries_main(self, threads: int, progress: Progress) -> None: logging.debug(f"[main] Start Binaries parsing: {len(binary_paths)} binaries to parse") binaries_map = progress.add_task("[deep_pink2]Binaries mapping", total=len(binary_paths)) + load_bin_func = partial(self.load_binary, **self.load_binary_args()) if threads > 1: # multiprocessed case ctx = get_context("spawn") # fork usage deprecated starting 3.12 manager = ctx.Manager() ingress = manager.Queue() egress = manager.Queue() pool = ctx.Pool(threads) + parse_job = partial(self.parse_binary_job, parse_func=load_bin_func) # Launch all workers and fill input queue for _ in range(threads - 1): - pool.apply_async(self.parse_binary_job, (ingress, egress, self.root_directory)) + pool.apply_async(parse_job, (ingress, egress)) for path in binary_paths: ingress.put(path) logging.debug(f"[main] {threads - 1} threads created") @@ -456,7 +468,7 @@ def map_binaries_main(self, threads: int, progress: Progress) -> None: else: logging.debug("[main] One thread mode") for path in binary_paths: - res = self.load_binary(self.root_directory, path) + res = load_bin_func(file_path=path) self._treat_bin_parsing_result(path, res) progress.update(binaries_map, advance=1) self.commit() diff --git a/src/pyrrha_mapper/intercg/fwmapper.py b/src/pyrrha_mapper/intercg/fwmapper.py index 24f069b..270a674 100644 --- a/src/pyrrha_mapper/intercg/fwmapper.py +++ b/src/pyrrha_mapper/intercg/fwmapper.py @@ -19,6 +19,8 @@ from collections import defaultdict from pathlib import Path from typing import Any +from hashlib import md5 +import sys # third-party imports from numbat import SourcetrailDB @@ -36,12 +38,25 @@ from pyrrha_mapper.fs import FileSystemImportsMapper from pyrrha_mapper.intercg.loader import load_program from pyrrha_mapper.types import ResolveDuplicateOption +from qbinary.types import Disassembler, ExportFormat IGNORE_LIST = ["__gmon_start__"] QUOKKA_EXT = ".quokka" -NUMBAT_UI_BIN = "numbat-ui" +NUMBAT_UI_BIN = "NumbatUi" + +# Determine the command to open URLs based on the platform +try: + URL_OPEN_CMD = { + "linux": "xdg-open", + "win32": "start", + "darwin": "open" + }[sys.platform] +except KeyError: + logging.warning(f"Unsupported platform: {sys.platform} (will not add URL handler)") + URL_OPEN_CMD = "" # type: ignore + class InterImageCGMapper(FileSystemImportsMapper): @@ -49,6 +64,9 @@ class InterImageCGMapper(FileSystemImportsMapper): FS_EXT = ".fs.json" + DISASS = Disassembler.AUTO + EXPORT = ExportFormat.AUTO + def __init__(self, root_directory: Path | str, db: SourcetrailDB | None): super(InterImageCGMapper, self).__init__(root_directory, db) # super initialize root_directory, db_interface, fs and _dry_run variables @@ -66,6 +84,7 @@ def __init__(self, root_directory: Path | str, db: SourcetrailDB | None): self.exports_to_bins: dict[str, list[Binary]] = {} self.progress: Progress | None = None self.unresolved_callgraph: dict[Path, dict[Symbol, list[str]]] = dict() + self._current_binary_hash = "" def _correct_map_result(self, res: Any) -> bool: return ( @@ -79,11 +98,23 @@ def _correct_map_result(self, res: Any) -> bool: ) ) ) + + def load_binary_args(self) -> dict[str, Any]: + """Return dict of args for load_binary that are always the same for the wholde firmware. + + Use to optimize multiprocessing. Set here there real values. + """ + res = super().load_binary_args() + res["disass"] = self.DISASS + res["exporter"] = self.EXPORT + return res @staticmethod def load_binary( root_directory: Path, file_path: Path, + disass: Disassembler = DISASS, + exporter: ExportFormat = EXPORT, ) -> tuple[Binary, dict[Symbol, list[str]] | None] | str: """Load all the binaries located in the filesystem as Binary objects. @@ -109,18 +140,25 @@ def load_binary( f"{binary.real_path.name} (skip)" ) - quokka_file = binary.auxiliary_file(append=QUOKKA_EXT) try: - unresolved_cg = load_program(binary, f"[binary mapping] {binary.name}") - except SyntaxError as e: - logging.error( - f"[binary mapping] {binary.name}: cannot load Quokka files {quokka_file}: {e}" - ) - return (binary, None) - except (FileNotFoundError, FsMapperError) as e: - logging.error(f"[binary mapping] {binary.name}: error during file analysis: {e}") - return (binary, None) - return (binary, unresolved_cg) + prefix = f"[binary mapping] {binary.name}" + unresolved_cg = load_program(binary, disass, exporter, prefix) + return binary, unresolved_cg + except (FileNotFoundError, FsMapperError, SyntaxError) as e: + logging.error(f"ERROR: Loading error: {binary.name}: {e}") + return binary, None + + + def add_url_handler(self, hash: str, binary: Binary, symbol: Symbol) -> None: + """ Open the function using a dedicated URL handler. (Use Heimdallr) """ + if not hash: + return # no hash, no URL handler + if URL_OPEN_CMD: + url = f"disas://{hash}?idb={binary.name+'.i64'}&offset={symbol.addr:#08x}" + cmd: list[str] = ["xdg-open", url] + self.db_interface.set_custom_command(symbol.id, cmd, "Open in Disassembler") # type: ignore + else: + pass # Can't add URL unsuported platform def map_binary( self, @@ -132,6 +170,8 @@ def map_binary( This function updates the filesystem representation stored as `self.fs`. :param bin_object: Binary object """ + self._current_binary_hash = md5(Path(bin_object.real_path).read_bytes()).hexdigest() + super().map_binary(bin_object) if additional_res is not None: self.unresolved_callgraph[bin_object.path] = additional_res @@ -140,6 +180,12 @@ def map_binary( if additional_res is not None: self._record_custom_command(bin_object, f"[bin mapping] {bin_object.name}") + def symbol_recorded(self, binary: Binary, symbol: Symbol) -> None: + """ + Register a symbol recorded handler to add a custom command. + """ + self.add_url_handler(self._current_binary_hash, binary, symbol) + def _treat_bin_parsing_result(self, path: Path, res: Any): """Handle load_binary res, map it or display error.""" log_prefix = f"[binary mapping] {path.name}" @@ -269,11 +315,11 @@ def _record_custom_command(self, binary: Binary, log_prefix: str = "") -> None: if self.dry_run_mode: return None assert self.db_interface is not None - cmd = ["NumbatUi", str(binary.real_path) + ".srctrlprj"] + cmd = [NUMBAT_UI_BIN, str(binary.real_path) + ".srctrlprj"] if binary.id is None: logging.warning(f"{log_prefix}: cannot record command as binary has no id") else: - self.db_interface.set_custom_command(binary.id, cmd, "Open in NumbatUI") + self.db_interface.set_custom_command(binary.id, cmd, f"Open in {NUMBAT_UI_BIN}") def _record_call_ref(self, src: Symbol, dst: Symbol, log_prefix: str = "") -> bool: """Add call reference between two symbols in DB. diff --git a/src/pyrrha_mapper/intercg/loader.py b/src/pyrrha_mapper/intercg/loader.py index 7152747..ff53778 100644 --- a/src/pyrrha_mapper/intercg/loader.py +++ b/src/pyrrha_mapper/intercg/loader.py @@ -19,31 +19,18 @@ from typing import NamedTuple # third-party imports -from quokka import Program -from quokka.exc import ChunkMissingError -from quokka.types import FunctionType +from qbinary import Program, FunctionType, DisassExportNotImplemented, ExportException, \ + Disassembler, ExportFormat # local imports from pyrrha_mapper.common import Binary, Symbol from pyrrha_mapper.exceptions import FsMapperError -QUOKKA_EXT = ".quokka" -logger = logging.getLogger("quokka") -logger.setLevel(logging.WARNING) -""" -[, - , - , - , - , - ] -""" - - -def load_program(binary: Binary, log_prefix: str = "") -> dict[Symbol, list[str]]: - """Create a Binary object from a given file using lief and quokka. +def load_program(binary: Binary, disass: Disassembler, + export: ExportFormat, log_prefix: str = "") -> dict[Symbol, list[str]]: + """Create a Binary object from a given file using lief and qbinary. It modifies the provided binary object in place. @@ -57,6 +44,8 @@ def load_program(binary: Binary, log_prefix: str = "") -> dict[Symbol, list[str] raise: FsMapperError if cannot load it :param binary: a Binary object that will be completed + :param disass: Disassembler enum to use for program loading + :param export: Export format to use for program loading :return: a dict of called done by each symbol of the binary """ @@ -64,19 +53,21 @@ def load_program(binary: Binary, log_prefix: str = "") -> dict[Symbol, list[str] if file_path is None: raise FileNotFoundError(file_path) - quokka_file = binary.auxiliary_file(append=QUOKKA_EXT) try: - if quokka_file.exists(): - program: Program | None = Program(quokka_file, file_path) - else: - program = Program.from_binary(file_path, quokka_file, timeout=3600) - except ChunkMissingError as e: - raise FsMapperError(e) from e - if program is None: - raise FsMapperError("Quokka does not produce a Program object") - - # Load the call graph - return compute_call_graph(binary, program, log_prefix) + program = Program.from_binary(file_path, + export_format=export, + disassembler=disass, + timeout=600, # TODO: Receive through command line ? + override=False) # if export exists use it + # Load the call graph + return compute_call_graph(binary, program, log_prefix) # type: ignore + except DisassExportNotImplemented as e: + logging.error(f"Disassembler {disass} does not support export format {export}: {e}") + raise FsMapperError(f"{e}") from e + except ExportException as e: + logging.error(f"Error while loading binary {file_path}: {e}") + raise FsMapperError(f"{e}") from e + return None class _FuncData(NamedTuple): @@ -99,9 +90,7 @@ def addr(self) -> int: return self.symbol.addr -def _generate_calls_list( - func: _FuncData, call_graph: dict[int, _FuncData], log_prefix: str -) -> list[str]: +def _generate_calls_list(func: _FuncData, call_graph: dict[int, _FuncData], log_prefix: str) -> list[str]: """Given a function return its call list. It only contains functions that are contained in the call graph and have a name. @@ -117,9 +106,7 @@ def _generate_calls_list( return res -def combine_program_analysis_binary( - binary: Binary, program: Program, log_prefix: str -) -> dict[int, _FuncData]: +def combine_program_analysis_binary(binary: Binary, program: Program, log_prefix: str) -> dict[int, _FuncData]: """Combine program and binary objects by computing useful data. It updates binary object if new functions are determined. @@ -147,15 +134,13 @@ def combine_program_analysis_binary( program_data[f_addr] = _FuncData( symbol=f_symb, type=f.type, - calls=list(set(x.start for x in f.calls)), - callers=list(set(x.start for x in f.callers)), + calls=list(f.children), + callers=list(f.parents), ) return program_data -def compute_call_graph( - binary: Binary, program: Program, log_prefix: str = "" -) -> dict[Symbol, list[str]]: +def compute_call_graph(binary: Binary, program: Program, log_prefix: str = "") -> dict[Symbol, list[str]]: """Compute the call graph of the program using Quokka/Binexport. It fill the call attribute of binary. @@ -165,7 +150,7 @@ def compute_call_graph( """ def _nb_initial_underscore(x: str) -> int: - return len(x) - len(x.strip("_")) + return len(x) - len(x.strip("_.")) # Call graph fun_name -> [callee_name1, callee_name2] call_graph: dict[Symbol, list[str]] = {} @@ -184,9 +169,7 @@ def _nb_initial_underscore(x: str) -> int: # Check that we have a match on names continue # else case - logging.debug( - f"{log_prefix}: export {canon.name}: {hex(exp_addr)} address not found in program(add)." - ) + logging.debug(f"{log_prefix}: export {canon.name}: {hex(exp_addr)} address not found in program.") call_graph[canon] = [] if len(all_symbs) > 1: # all the symbols will point on the chosen one map(lambda x: binary.replace_function(canon, x, True), all_symbs) @@ -197,10 +180,10 @@ def _nb_initial_underscore(x: str) -> int: removed_trampoline: dict[str, str] = dict() for f in program_data.values(): if ( - f.type in [FunctionType.NORMAL, FunctionType.LIBRARY] + f.type in [FunctionType.normal, FunctionType.library] # If thunk AND exported or thunk AND call several func, keep it (for later resolution) or ( - f.type == FunctionType.THUNK + f.type == FunctionType.thunk and ((f.addr in exports) or (f.addr + 1 in exports) or len(f.calls) > 1) ) ): @@ -208,9 +191,9 @@ def _nb_initial_underscore(x: str) -> int: continue # Replace thunk calling only one function (and only one) - elif f.type == FunctionType.THUNK and len(f.calls) == 1 and f.calls[0] in program_data: + elif f.type == FunctionType.thunk and len(f.calls) == 1 and f.calls[0] in program_data: sub_callee = program_data[f.calls[0]] - if sub_callee.type in [FunctionType.IMPORTED, FunctionType.EXTERN]: + if sub_callee.type == FunctionType.imported: # Keep the name of the thunk "strcpy, sprintf" name, target = sub_callee.name, f.name # in case of nested functions (starting with _, keep the less nested one) @@ -219,7 +202,7 @@ def _nb_initial_underscore(x: str) -> int: else: # Forward the call to the underlying function name name, target = f.name, sub_callee[0].name # resolve trampoline and update associated dict - while target in removed_trampoline: + while target in removed_trampoline and removed_trampoline[target] != target: target = removed_trampoline[target] removed_trampoline[name] = target for key, val in removed_trampoline.items(): @@ -227,7 +210,7 @@ def _nb_initial_underscore(x: str) -> int: removed_trampoline[key] = target # If terminal thunk keep it in binary - elif f.type == FunctionType.THUNK and len(f.calls) == 0 and len(f.callers) > 0: + elif f.type == FunctionType.thunk and len(f.calls) == 0 and len(f.callers) > 0: continue # remove any function not explicitely kept (THUNK, IMPORTED, EXTERN) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b354e3c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +import pytest +from qbinary.types import ExportFormat, Disassembler + +def pytest_addoption(parser): + parser.addoption( + "--disassembler", + action="store", + help="disassembler", + choices={x.name.lower() for x in Disassembler}, + ) + parser.addoption( + "--exporter", + action="store", + help="exporter", + choices={x.name.lower() for x in ExportFormat}, + ) \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 51eb2b2..58c7161 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -28,6 +28,17 @@ from pyrrha_mapper.intercg.fwmapper import InterImageCGMapper +def check_click_result(res: Result) -> None: + """Raise Assertion error if issue.""" + assert res.exit_code == 0 + assert not res.exception, res.exception + for log in res.stderr.splitlines(): + assert ( + "ERROR" not in log + and "WARNING" not in log + and "CRITICAL" not in log + ), f"Error log: {log}" + class TestCLI: """Tests to check that the CLI works and display correct messages.""" @@ -40,8 +51,8 @@ def test_usage(self): res_short = runner.invoke(self.COMMAND, ["-h"]) res_long = runner.invoke(self.COMMAND, ["--help"]) for res in [res_long, res_short]: - assert res.exit_code == 0 assert res.output.startswith(f"Usage: {self.COMMAND.name}") + check_click_result(res) assert res_short.output == res_long.output, "Usage different with -h/--help" @pytest.mark.parametrize("subcommand", SUBCOMMANDS) @@ -51,8 +62,8 @@ def test_subcommand_usage(self, subcommand: str): res_short = runner.invoke(self.COMMAND, [subcommand, "-h"]) res_long = runner.invoke(self.COMMAND, [subcommand, "--help"]) for res in [res_long, res_short]: - assert res.exit_code == 0 assert res.output.startswith(f"Usage: {self.COMMAND.name} {subcommand}") + check_click_result(res) assert res_short.output == res_long.output, "Usage different with -h/--help" @@ -80,7 +91,6 @@ def SUBCOMMAND(self) -> str: } FW_TEST_SYMLINKS_PATHS = {Path("/lib/libssl.so")} - # =============================== INTERNAL STUFFS ================================== class ExecResults(NamedTuple): # noqa: D106 @@ -133,15 +143,14 @@ def export_dump(self, export_res: ExecResults) -> FileSystem: @pytest.mark.parametrize("pyrrha_exec", [1, 16], indirect=True) def test_numbat_project_creation(self, pyrrha_exec: ExecResults): """Two files are generated with correct extensions.""" - assert pyrrha_exec.res.exit_code == 0 - assert pyrrha_exec.db_path.exists() + check_click_result(pyrrha_exec.res) assert pyrrha_exec.db_path.with_suffix(".srctrldb").exists(), "Missing DB file" assert pyrrha_exec.db_path.with_suffix(".srctrlprj").exists(), "Missing project file" @pytest.mark.parametrize("export_res", [1, 16], indirect=True) def test_export_creation(self, export_res: ExecResults) -> None: """Export file exist.""" - assert export_res.res.exit_code == 0 + check_click_result(export_res.res) assert export_res.export_path.exists(), "Export file does not exist" @pytest.mark.parametrize("export_res", [1, 16], indirect=True) @@ -232,7 +241,7 @@ def export_res(self, tmp_path_factory, request) -> BaseTestFsMapper.ExecResults: f"{self.FW_TEST_PATH}", ] return self.ExecResults(res=runner.invoke(self.COMMAND, args), db_path=tmp_path) - + # =================================== TESTS ======================================== @pytest.mark.parametrize("export_res", [1, 16], indirect=True) @@ -262,6 +271,28 @@ def export_path(self) -> Path: # noqa: D102 # =============================== FIXTURES ========================================= + @pytest.fixture(scope="class") + def pyrrha_exec(self, request, tmp_path_factory) -> BaseTestFsMapper.ExecResults: + """Run pyrrha whith the given thread number and the given db path.""" + runner = CliRunner() + tmp_path = ( + tmp_path_factory.mktemp("db", numbered=True) + / f"{self.SUBCOMMAND}-{request.param}.srctrlprj" + ) + args = [ + self.SUBCOMMAND, + "--disassembler", + f"{request.config.getoption('--disassembler')}", + "--exporter", + f"{request.config.getoption('--exporter')}", + "--db", + f"{tmp_path}", + "-j", + request.param, + f"{self.FW_TEST_PATH}", + ] + return self.ExecResults(res=runner.invoke(self.COMMAND, args), db_path=tmp_path) + @pytest.fixture(scope="class") def export_res(self, tmp_path_factory, request) -> BaseTestFsMapper.ExecResults: """Run Pyrrha with export activated.""" @@ -272,6 +303,10 @@ def export_res(self, tmp_path_factory, request) -> BaseTestFsMapper.ExecResults: ) args = [ self.SUBCOMMAND, + "--disassembler", + f"{request.config.getoption('--disassembler')}", + "--exporter", + f"{request.config.getoption('--exporter')}", "--db", f"{tmp_path}", "-j", @@ -279,7 +314,7 @@ def export_res(self, tmp_path_factory, request) -> BaseTestFsMapper.ExecResults: f"{self.FW_TEST_PATH}", ] return self.ExecResults(res=runner.invoke(self.COMMAND, args), db_path=tmp_path) - + # =================================== TESTS ======================================== @pytest.mark.parametrize("export_res", [1, 16], indirect=True) diff --git a/tests/test_fw/bin/openssl.quokka b/tests/test_fw/bin/openssl.quokka deleted file mode 100644 index a990d91..0000000 --- a/tests/test_fw/bin/openssl.quokka +++ /dev/null @@ -1,55881 +0,0 @@ - -< -openssl *$ cca8c47bf723c2c46fc3910ae9897e0a8@H 0.6.0 - - - - - -Ԁ - - H - - 0 - - - - - - - - - - - - , - - - - $ - - - - | - - H - - - L - - - - - - -Ƞ -$ - - - - -d - - -8 - - - - - - - -( -а - - - - - - - - - - - - - -2 - -` - - - - - - -̊@ - - -Б@ - -@ - -В -Ԓ - - - - - - - - - -p - - - -̚ - - -# - -l - - -` - - -$ - - -$ - - - - - -@ - - -` - - -Ԓt - -ȓT - - - - -< -ؖ - - - -L -ȧ - -< - -ܯT - - - - -Բ0 - - -ػp -ȼ - -ȾP - - -@ - - -H - - - - - - - - - - -| - - -Ȅ - - - - -܇ - - - - - - - - -Ԑ 4 - - - X - - - ( - - - 0 - - - - - - - - - - - - - - - - x - - - - 0 - -ة - -ܩ \ - - - - - - - - 0 -в - -̺ 0 - - - - - - - - - - - - - ( - -x - -8 - - - - - - - -̎0 - -X - -ԏ - -X - - -Đ - - - - -H - - -ܻ@ - - -$ - - -0 - -l - - - - - - - - - - - - -D - - - - -$ - - - - - - - -` - - -0 - - - - - -X - - -D - - -L -؎ - -, - - -ؗ$ - - - - - -Ԟl - - -| - - - - - -< - - -и - - - - - - - - - -, - - - - - -l - - - - - - - - - - - - - - - - - - -0 - - -$ - - - -؇ -̊" - -Ь - - -X - - -ع@ - - -Ի\ - - -8 - - - - - - - -x - - - - - - - - - - -8 - - - - - - - - - - - - -D - - - - - - -` - - - - - - - -$ - -܋ -( - -< - - - - -, - - -, - - -0 - - -X - - - - -@ - - -4 -Р - - - - - -Ф - -4 - - -ܪ - - - - -@ - - - - - - - - - - -$ - -< - -Ȳ -̲ - - - - - -ܿ, - - -( - - - - - - - -" - - - - - - - -P - - -D - - -, - - -4 - -ȴ - -ص - - - - - - - - -P - - - -` - - - -x - - - - - - - - - - - - -X - - - -p - - - -p - - - -p - - - -p - - - -p - - - -p - - - - - - -p - - - -X - - - -x - - - -\ - - - -\ - - - -\ - - - -\ - - - - - - -\ - - - -x - - - - - - -8 - - - - - - - - - - - - - - - - - - - -̀ - - - - - - - -d - - - -` - -܄ - - - - - - - - - - - -0 - -Ȏ8 - - - - -= - -. - - - -ܝ< - - -8 - - - - - - - -t - - - - - - - -Ԯ@ - - - -P - - - - - - - -, - - -H - - -T - - - - - - - - - - - -p - - -܂p -̃ - - - - -0 - - -4 - - -< - -H - -0 - - -< - - - - - - - -̾ - - - -p - - - - - - -H - - - - - - - - - -0 - - - - -$ - - - - - - - - - - - - - -p - - - -( - - - - - - - - - - - - - -p - - - - - - - -( - - - - - -X - - - -( - - - -| - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Ж - - -$ - - -0 - - -8 - -ء@ - - - - -( -Ԩ - - - - - - - - - - -$ - -а - -ܰ - - - - -P - -IJ - -̲ - - - -` - -̳ -Գ - -X -Թ - - - - -P - - - - - - - - - - - - - - - - - - - - - - -$ - - - - - -( - - - - - - - - -( - - - - - - - - - - - - - - - - - -0 - - - - - -P - -P - - - - - - - - - - - - - - -( - -\ - - - -\ - - - - - - - -l - -܌ - - - - -ؓ0 - - - - - - - - - - - -Ȕ -̔ - -, - - -$ - - -ğ$ - -d - -̠ -Р - -P - -Ĩd - - - - -0 -ح - - - - -8 - - - - - -ܹ - - - - - - - - -, - - - - - - - - -, - - -| - - - - - -0 - -x - - - -8 - - - -h - - - - - - - - - - -( - - -H - - - - - - - - - - - - - - -0 - - -( - - - - -< -ȟ -̟ - -7 - - - - - -% - - - - -Ĭ| - - - - - - - - - - - - - - - -H -< - - - -ȇ - - - -h -L -̎ - - -̹ - - - -7 - - - - - - - - - - -< - - - - -N - - - - - - - - - - -l - - - - - - - - - - - - -K - - - - - - - -ı - -ж - -мY -Ȗ -ɖ - -̖ -Ж - - -t - - - - - - - -# - - -7 - - - -Ё - - - -' -ع - - - - - - - - - - - -ؽ -, - - - - - - - - - - - - - - - - - - - - - - -/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -A - - - - - - - -# -# -# -# -# -# -# -# -# -# -#+ -$ - -ĩ$ -ȩ$ - -$ -$ - -$ -$ - -$ -$ - -$ -Ȫ$ -Ⱥ$ -$ -$ -$ -ʽ$ -˽$ -$ -$ -$ -$ - -$ -$ - -$ -$ -$ -$ -$ -$ -$ -$ -$ -$C -Ȏ% -Ɏ%" -հ% -ְ% -% -% -% -% -% -% -% -% - -%@ -% - -%8 -%D - -% -% - -% -% - -% -% -% - -%% -%( - -% -%~ - -% -% -% - -%4 -% -% -% -% -% -% -& -& -& -& -& -& - -& -& -& -& -& - -& -&d - -ؑ& -&$ - -& -& - -& -& - -& -& - -Ȗ&( -& -&' - -&$ -& - -̗& - -ܗ& - -& -&( -&" -! 8" " " -! 8" " -! 8"ļ " -! 8" - 0""8" -! 8" -! 8" -! 8"" - 0" -̚ 0""" - 0" -Ж" 8" - 0" -! 8"" "! 08" - 0" -! 8" - 0" - - 0 " " -! 8"ž" " " -ȴ 0 -"! 08"Ȋ " " -" 8" -! 8" " - 0" -! 8";" " - 0 "ľ " -Ԟ 0" " -" - 0" -! 8" -" 8" " - 0"! " -Ի 0" "! " " " - - 0" -! 8" " "Թ " " -! 8" " -Ⱦ 0" -" 8" - 0" - 0" " -" 8" - 0" "" " - 0"""" -̖" 8"" - 0"" -! 8" "!8" - 0"" " -ܻ - 0" " " - 0" " " " -̊ 0"" -ܝ 0" " " " " - 0"" " " "" - ""܋ " - - 0"!8" " " " " - " " -! 8" " -Ԯ 0 "" - 0!" -! 8"" -Ԑ 0"" " - 0#"̺ "" - 0$"!" - 0%" " -! 8" - - 0&"" -Բ 0'"" -! 8" "" -" " -! 8" - 0("ʡ" " "ӄ"" - " " " -ؗ" 8" " - 0)""$"ݪ" - 0*""" -" 8" - 0+"۱"̱" "" -ؗ 0," -! 8"" - 0-"" - 0." " -ܖ" 8""Љ "Ț " "*"ܺ"! " - 0/""" -! 8" -! 8"")" " " -" 8" " -! 8" -! 8"" -! 8" " -܇ 00"" " -! 8" " -! 8"" " -" 8" " -! 8"" -и 01""." " " -! 8" " " " " -ؑ" 8" - 02"" "ܹ "" - 03"" - " " ""7""" "Ԏ "" "" - 04"%" "ȼ " "̟"""" """̚"ܦ " -! 8" " -ܿ 05" " -! 8" "" -! 8" " "Ӯ""" " " -""""" -! 8" "" -Ȗ" 8" "Ⱥ " - 06""#""" -" 8"" -! 8"Ū" -! 8"" "" -! 8" " - 07"" -" 8"" -! 8" "" "#" " " -"̺ "к " " -! 8" " """" " ""ܻ " -! 8"" " " - " -" 8" "" " -! 8"" "" " - " - - 08" "ܓ " ""%" " " "";"ԛ """"/" " """1" -! 8""" -! 8"" " "! " """ "ϡ" "ĭ" ".""" -"Ż"Қ" " -! 8" -! 8"""%" " "" "" "Ѽ" - 09" -""""" " -" "ʹ""/""" "ؤ" "'""̹" "" " " " "" "," "" "0"!"8"Ϡ"$" - "+" ""'" """ð" " " "ȼ "&" - " ""ϡ" """'" "" "! " "Ё" ""*"!"5"""%"0" " " """" - " " "" ""ص."" -! 8" """ " -" 8"" " - " " """*" -! 8"""ݼ'" """9" "+"" " -! 8" " -"ߡ";"""" "" "" """"""""̖ """! " " " "ة "" ""Ź*" "©" "."%" "" "&"C" "" " -" 8"""ԑ "Ĥ "!""" "" """ " ""ȫ " "" " """"" "" -! 8" " " " ""'""" " "ݬ#"" "<" ""$"!"" """%" """ -"Ƚ " " -! 8"2" ""! "" -! 8" "$"" -! 8""" ")"Ŵ" "" -! 8""ܮ " """" " "" " -! 8"" " " "G"%" "" "" " " " - "ε#" """"" -! 8"("Ǯ"""̉ """"8"ֲ-" "" -" " " "F"" """" " " -" 8""" " -" ""Ь "&"" """̛ "ʳ "" """ ""'" "" -" " ""&"" "'" " -""("" """ "̯" " """ ""ԉ "-""! "ϻ"" " "" -! 8" " """ """ ""9" "ܑ " " "" ""!" " " " """" "" " """ "(""""ٰ"" "Ȗ" """"""(" ""߫""ݯ" " -" 8" "#" " " " "ؖ """ "" - " -"ڶ"" " "")""":"""" "ظ ""8"" "!""""" "" "" "")"ɭ"%"я"2"Ժ "ԯ """" " -! 8""""%" " " " """"" " "ڷ" -"0"" " " "ć "ȉ """"" "" """""""" - " "")"" - """ ")"" " """ " " " -! 8" " " "˷ -"" """ " ""8" " "&"!""," -"""! " "'" -! 8" " ","" " " """"'" "."" " " """ "խ "Ȼ"" " " """ " -""ǿ#"Ԝ""ի -"%" " ""2" " "" " "ƶ"""'" """"" ""̘ "M" "E""؛ " """"%"""ȩ0"$"ˍ " " " -! 8" """ "܂ "Ι2""ݏ" "؇ ""ݭ ")" "݅""ͺ -"" -" 8" " " -" 8"""0:" "" " -! 8"" " ",""+"ϓ" """Օ"""""%"<"̤ """ -! 8"&" " " "$"Ǽ!"-" """ "$" ""Ă"."*"""ز -" "3"""h"%"""%" """ "L" -" " " """" -" """!" ""#"'""" """*" ""#""-"܋ """." ""#" "ع """ -" "&""""""+" """" -")"" " " -! 8" -Ԗ" 8"""#""" """("" "K" -! 8"/""""܇ " "0""$":""("""""" """ "$" "ؐ%"" " ")","#" "@"؛ """""շ-""" " "" """""Б """ " "," " " "" " """ "ƣ " """"" -""" " " "Г"˖!""" - "ɖ5" """," ""ɾ"X" "","""ܾ7" "+""" " """ʦ ""("" " "$"""7"" " " "ǜ"""/"""&"" " " ".")" " "#" ""F"" " ",""ך""ݵ""""" " -" " "" -! 8" " "#"Ք "$" -! 8""Ǟ""!"" "" "" " "ȍ ">"̞"̼;""" " """ ""%""" " " - "@" " ""ߗ" " - " "" """*""""ۜ" "*"" "" "" "a"1" - " -" 8" "ӟ;"""¢%"ԩ """$""" "*"$"о(" "" - " " " "?"" """""("""""Ȳ" "C" -! 8"θ"ɗ$""+"ؘ ""#" ""3"؍"5" " " "ɒ"""#""""" "*""" -"""""""%"ތ -"4" " - " """"""""""""8"$"""""!"" " "" "#" "" " " "A"ݻ"" ",""ʇ""."1""%""1""I""6""" ""Ɲ""2"ȗ """ """ " " -! 8""!""" "K"""@" "м"+"ޜ" ""6" "#""""̎ - "(""""/"""" "" " """ð"" "" "(" " -" """"" "" " "ܒ "#""J"̼ ")"Ԙ " """2""D" "" "ٓ"#""$"";"[" "!"2",""F" "ܿ "<"#" ":"" "-"0;"X""'" " "+""."L""!" "" - ""C""!"5"'""" """""""$" "*"ъ$"Ʈ"" "*"3"""" -" 8""""" "%"" """Я.""" " "" " """*"" " ""2" "" " -"ݥ"Ͻ" " -"W" "ڽ"""*" "ɳ"2" - ""("""0"č " """""$"" "" "<"" "" " " """ - " "K")"":"" "2" "#""" "N"֜""""""""""њ"""" "" ""$"" "н)"<" "ؽ " "2" " ""߬R"#" """%""" "X""ĩ "" "" "ÿ2""֞""("" - "O"ݯ+"" -! 8" " " """"2"")"ۘ'"ý""+"ԙ " "" """ " " "؉ - "(""" "" "" "g"Ļ"'""" " "0" " """қ"ӣ"#""!" ""м" "#"!""" ""̘ " -""ڹ"" "/"*""""",""ъ""" ""&""#""I"""""Ґ"" " " -"#"" "Ƚ" "1" " """""4" "ڔ" "" "" ")"D" "6" "(" - """" """ "*"Ƙ"" "$" "'"" """6"ԗ"Ӫ"")"ў" " " """9"'""B""X","&"E" "Ɛ"" """" "" "#" -" "5""""" " " "4"5"" "E"Dž""ʻ" """0" "" "" " " "" "$" "ԝ"" ".""""˭" " " ""(" " """ "҅";"Π" "ݚ"" " "л""." " - """ "˹'"" """""" "ώ"";" """ ""'" """""" " " "ִ" " """" "'""""½""" " """""N""%""۶""" "'""""<"4"'" "ܺ "" "!" " " " """" -"@""""վ" """ "'"" " """"$" " "&" - "$"A"&""" " -" 8" " ""ۃ" """" "!"2""9"Ī "ڞ!"2"" "(""" " "Ô"" -" 8"""%""1""'""̂"3"/" "ڴ""ѹ"̠ "" """ "ث2"#""T"ڱ5"!"#"" ""0<""ж" "Ȫ""" """" " """$")""ı"")" "%""""" " - """)""" " " "*" "&"""6""" - """"" " """"" ";"$" ""%")"˻""" " """ "-"ب """ """" ";"" """("" " "2"a"Ͽ"" "."" - " "ё"ԍ " "!" "" " " "Ɣ!" " " -""""""ù."ƽ" "۲")" " -""""K""0=""$"'""-""̎Q"""ܩ "C"(""2":"<"ô -"3" -" 8"" """)"#""!"L" """ ""ļ "" ""(" -" 8""$"" -"ػ """""""F""" "Æ#"" """ "ܘ " ""ؗ " " " "" "Ǔ""" "]""ܰ " " "չ""""!" """"" " ""!" "Ԡ" "%" """ " - "0"" "*""#"" "" """)" "2" - "" " "" """ -""ϱ!"" """ ""į"Ǩ("!".""߭$" "ȍ """!" """ "'"" "("M"8" "%""Ú """" "õ" "" "%" " "Ѱ"""1"" """̈ " "" ""#"""" " ""'"" """""ȫ"""" "="""܍ """Ю#" "ג""""" "6"U"""*""Ę " "տ!"""*"8"" " " "ޯ)""˽"շ1"" "6"""Ȝ" """#"""" - " " "","""1""Ԏ - """("˥!"3"" "" ""͛"""""!"""" " ""%""З ""ė "!8""""ԝ"" - "ӣ.""."@""0"" - ""ؖ."̶ """"ĉ " "1"""1"" """"ȫ " " " " " "" " " " " " "Ј " " "ا " "" "" " "̫ " " " " " """"" "" " "" " " "" " " " " "" "" " "" " " " " " " " "ǃ" " "̚ " "" " "Ȕ "" " "" "" "Ȇ "" " " " " " - " " "" ""Ċ " " " " " " "" "" " "Ȉ "" "<" " " " " "" "ܘ " " " """ " " "" " "Ǿ" " " "܉ - " - " "ܢ " "" " " "" " "ĕ " " "Ȧ "!" " " " " " "" " " "" " - "" " " " " " -"Ȩ """ " " - " " " """د " " " """" " " " " "Ԃ " " "Ч " " " "Ԩ " " " " ""؈ " " "" "" " " "" " " " " " " " ""$"Ա " " " " "" " " " "" "" " " "" "؟ " """"" "ؓ "ع " " " "" " "" " " "ȃ " " " "" "" " " " " " "ă " " " " " " -"Ј " " "" " " "" " " " " " -! 8" " " " " """ " " " "" """ " """ "ȡ "Ģ " "Ȕ"" " """Ĉ " "" " " " " " " -"" " " " " "" " " " " " - " "" " " " " ""Ȃ "Ȅ " "Є "̜ " " " ""Ѐ " " " " "" "" " " - "Ԫ " "" " " " " " " """ ""ϋ" " "" " " ""ܦ " " " " "܋ " "" "н " " """"" " "Ȏ " " " " " "ؼ " "̗ " -" """ "" """ "ğ " " "" " " " "" " " -" "" " """""" " " "" ""0"Ћ "" " "ȱ " " "ܗ "! "" "ؙ " "ԣ " " " " " """ܟ "&" "" "" " "ԙ " "Ĺ " "" " " " " " " " "" " " ""%"" "" " " " "ˮ" " ""، " " " " " " "" " " "݆"̙ " " " " "" " """ " "" "̩ " " "й " " " " """""" "ȗ " "" " " " "" "" " " " "" " " "Ԟ " " "" """ " "" " "ޢ" "" """Ġ "" " "" """ " " " " "Ԗ -" " " " " " " "" " "" """ "" "Љ - " "" " "" "" " " "" " "ԏ - " "М "" " " "" " " " """ - """ " "IJ "" "܄ " " " " " "%""" " "" " " -" "͓" " " " - "Զ " " " " "Ԗ " " " ""Ӥ " " " " "" " " " "" "" "" "" " " " " " "" - " " "" " " " " " ""Ԉ " " " " "!"" "ؤ "Ą " - " " " "" " " " " "" " " " " " " " "" " " " "" ""Ԗ " "ȟ " " " " " " - " " " "" " "ę """ "Ȱ " " " " " " "" " "ġ " " "" - " " "̈ ""̅ " " " " ""Ђ"" " "Ȳ " "" " " """ "ԧ " " " " " " " "" " " "ܝ "" " - " " " " " "غ " " " - " " "" " " " " " - " " "Ϲ" " " " " " " " " " "" "" " "ȃ " "ܬ " " "" " " "܆ " " " "" - " " " """" "ī "܈ "ܡ"" " " "Č " " """ "đ " " "ԓ """ - " " "" " " " " "Ġ " """" " " " "Ќ " "" " " " " " "" " " "#" " " "ă "" " " " "ě " " " " "ګ" "" "" "! "" " " " " " - "" " "" "́" "" " " " " "Ё "" "" - " " " " " " " " " " """ " " """ " """ " "Ȩ "Ī " " """ "؇ " " " " " " "" "ƭ" "Ȳ " " " " " "" " " " " " " "ܗ " " " " " "ȇ " " "" "" " "Ľ "҅"̑ " ""и " "Р" " " - " " " " "ԟ " " "з" " " " - " " " " "" " " " " " - " " "܈ " " " "Ď " ""ܤ " " "ئ " "̃ "" " "ڞ " " " "" "Ъ " "Ѝ "" " " " -" " " " " " "" " " "؋ " " " " " " " " """ " " " " " " " "" " ""Ȋ " - " """ - "" " " " " "ؐ "" " " " " " " " " - " " " " " " " " "" " ""Ђ " " " " " """ " "" " "" " "ԏ "" ""ܺ " " " " "؊ ""̽ " " "ĩ "" "Џ " "ܶ " " " """܂ "Ԑ " " "" "К " "" " " "" " " " " " " " " "" "" " "Ĩ " " "؏ - " "" ""۳" " " " " " "ߗ" """ "ت "Ԏ " " "" "Ě " " " "ȕ "Ը " "Ш """ " "܈ "ܼ " "к " " " " " " " "" " " " "̘ "Ĩ " " " " " " " "Ҋ" "Ԧ "" """ "܉ " " " "ܤ " " " "" -"" " "̨ " " " " "ȩ " " "̍ """ " "" "" "ԑ " "" ""ܹ ""ܨ "ܥ " "ԇ "ܡ "" """""Ȁ " " " " " " "" " " " "" " " "" "" "" "" "Đ " " " "! " " " " "" "" " " " " " " ""ԣ" " " "" " "ȁ " "؛ " """" " "̄ "&" " " "ز" " " "ș " " "$""̓ " " "/"" "ܖ " " " " " "͟""Ь" "" " ""̣ " """ - " " " " " "" """" " " "" " """" "ě "؆ " "" "¦""" " " " " "" "" "Ѓ " " " " " "ȓ "" " "" " " "" "ԇ" "ٮ"" " " """"й " " " " "Ж " " "" " " " " " " " "" " "" " " "" " " " "ȹ " "" " " " "" " " "ә" " " " " " " " " " "Լ "ԝ ""ܛ " " " - " "" - "" " "" " " "ܚ " " " " " " " """ " "" """ " " "" " "" " " " - "" " " """ "Ę " " "Ƞ " " "" " " " ""̛ ":" "؍ "!8" " "ؑ " " "" "̤ " ""̚ " " " " "" " """ " "Լ " " """ " "Ԓ " """ ""ج " " " " " " " " "" " " " "ܛ " " "" " "Љ " " " " " " " " " " "" "Њ " " " " " " " " " """ " "" " ""ȅ " " " " "֎"" " " ""̹ " " " " " " "" ""#" "ͪ" "" """ " " " " " " " " " " " " "" " " " " " " "" " " """ " " " " """" "" " " " " " " " " " " " " - """ܸ " " " " """ " " "ħ " " "̨ " " "Ȳ" " " "̙ " - """ " " " " " " " " " " " " "̥ " " " " " "Ţ" "ȥ " " " "" " - "" " " "܍ " " " "ܫ ""$""Ĕ "ؒ " " " " "ޖ"" " " " """" -" "ؒ " "" " " -̗" 8"Հ"" "̉ "Ԋ " "" " " " "" "" " " " " """ " " "Ԅ "" "̤ """ " "̃ "" " - "Ė " "" " """ " " " " " " " " " " " " "" ""Ԁ " """ " "" " " " " "" " " "Ш " " "" " " -" " " " " """ " "Д " " "" " " "" " - " " "" " "ؘ " "Ħ " "ԯ "" " " " " " " "̌ " " "" " ""ܪ " " "܁ " " "̝ "" "̞ " "ā """С " "܄ """ " " " " " " "" "" "" " " " " " "ܖ " "" " " " " " " " " " " "ջ" " " "ސ" " " "ț "" " " "" " - " " " " "Š" " " " " "" " -ؖ" 8" " "݁" " " " " " " " - " " "Ԇ "" " " """" " ""؈ """ " "ؓ " "ԃ " "" " "ď " " " " ""̭ " "ж " "Ȓ " -" "" " """ " " " " " " " """ " " - " -" "" " " " -" " " " " " " "" "" " " " "У "؏ " "" " " """ " """ "" " " " """ٵ!" " " "؝ " " "Ԩ " " " - " " """" " "ı " "ܱ " "" " "ȉ " " " " "" " "ܪ " "" " """" " " " """ " " " " "ܨ " " "Ĥ " " "" - " - " "" " " "" """)""" " " " " " " " " " " " " - " "إ "" " " " " " " "؎ "Ķ " " "ؠ " " "" " "" " " " " " " " "" " "" " " """" "#" " " "" " " " " " " "&" " " " "" " " " " " " " " " " " "" " - "" " " " "Й "" " " """ " " - "" - " " " " "Ȫ " " " " " """"" " "ą "ș """Ф " " " " """؟ " " "" "" " " " - " "ؗ " " " " " " -ԗ" 8" " """ " """ "ו" " " "" " " "" " " " " " " " "" " """̼ "" " "" " "ċ " " " " " " " " " " " "ԩ " " " " "Т """ - "" " " " " " "Ԡ "Ԛ " " - """ض " "" " " """́ " " " " " " " " "" " " " " " " " " " " " " " " " "ǜ" " " "" " " """ȃ "، " "" " " " " "ԗ "" " "" """ " " " " "" "" " "" "" """"Ԉ " "Ԍ " " " " """ " ""̳ " " "ԋ "Ț "ܛ " """" " " " " """ " " " - " " " "" " " " " " "ث " "" "" " "" " "" " "&" " "!"̆ " " "̌ " "" " - " """ """ " " "" " " "" " " "" " "" " " "Ʃ"" "" " " " " "ț " " " " " " " " " "B""" "" " " " " " " " ""̐" " "Ğ " " "̀"" "" " ""ԡ " " " " "ȝ "" " " " " "Ī " - " " ""Ѐ " " " " "А " " " " """ "̰ " " " -"ؽ " "غ" ""ȋ " " " " "ܒ " "К " " " " " " "" ""ܩ "ܔ " " "Ԥ "!" " " " " " " "" " "̎ " " "" " " " " " " "" " " " " "" - " " " " " " " ")" "" " - " " - " " " " "" " "ؕ " " " """ȧ " " """ "Ď "" ""Ȉ " "؈ """ " - "ȩ " " " " " "Ԙ " " " - " " " - "" " " " """ " "" " "Ȝ ""ܓ "ф" " " " " - " ""В " "ē " ""̔ " " "Ԕ " " " " " "$" " " " " " " "" "" " -" " - " " " " "И" "" " " " "" " " " "ȡ """ " "" "̋ "" """ """ "Ș " " "" "'" " " " "" " " " " "܌ " "Ў ""ǯ%" " - " " " " "̪ " " "" " "܌ " "" " " "" " " - " " " " "" " " " """ " " " "" "" " " """ " " -" "" " " " " "" "ĭ "Ċ """Ⱦ " " - "" " " " " " "+" " " "" """ "" " " " " " "6" - ""Ƞ " " " "" " "" - " " - " " "" " " " "߉"" " " "ʸ" " " "Ԝ " " " " "٨""̠ "ة " " "" "ܑ " """ " "" " " " " "ݦ" " " " " " "ĥ "б " " " ""8" " "" " " "" " " " " " " " ""Ĝ """ "ġ " " "" ""ܠ ""؞ " ""̒ " - " " " " " " " "" " ""ä" "" """ " "̀ " " " " "Ӳ" " " ""Ќ " " """" " "Ў - " """Ќ " " " """"" " " "0"" " " " " " " "" "'" " " " " " " """ "" """ͩ"Р " "ȉ - "" - " " " " " "Ā " " "" " " " " " " " " " " "" " " "" " " " " " " " "" " "" "" " "" -" " " " "ĉ - " "" " -З" 8" " " " - " " "" " "" "ȼ " " " "" """ "" " "ڸ" " "" "ٱ " " "ȋ " " "" " " " " " """ " "" "" " " " "" "" " " " "Ȩ " " " " " " " "" " " """""̰ " "߾" " " " " " " "" " " """"Ĩ " "" " " "" " " " " ""С " " " "" " " "" " " " " " " """̱ " " " " ""̦ "ԗ "" " ""ؤ " "" " " "ԉ " " " " "ܟ " " " " "" "" "" " " " " " """ - " "ȏ " " " " " " " " "Ў """ " " " " " " " " " "" " - " " " - """ "" " " " "" """ " ""Ȟ """Л " " " "̌ "" "" ""Թ " " "" """""" " " "ԭ " -" " " " " " " " "̡ ""Ƚ "Ї " " "ԫ " " " "П " "" " " - " " "ě "Щ " " " " "" " " " " " "ӳ"" "̛ " "" "" "Г "̨ "" """؍ " " " -"" " " " " " " " " " ""ȑ " " " " "أ " "Ć "ģ " - " "Ж " " "" " " "Ւ" " -"" " " - " " " " " " " "Ȥ " " """ " " " " " " "܍ "" " "" -""#" " """ " " " " " " """ "Ϟ"""ۧ" " "ܙ "" "̍ " " "" " " " "" " "" " " " " " "̋ " "" " """ԛ " "Ԫ "Č " " " " " "" "" - "ȹ " " "" " "ܙ " " " "" " - " "֛" " " "ʍ" """" " "" " " " " "" " " " " " " " " "܀ " ""Ώ"̍ "ț " " " " " "" " " " " " " " " "" " " "" " " "Υ" "" " - " " " " " """ " ""ь"Ș " " " "ċ"Ȏ " "ة " " " "" " "" "ǡ"̸ " " "" " "" " "" " " -" " " " " "Й " " " " " " " " "" " " "Ē " "! "" "" "Ľ """ "" " "" "" "$"" " "ؚ " " " " " "̂ " " " ""Ȳ " "" " """" " "" " " " "Ò"" "ܪ "ԃ """ " " " " "" " "ԟ " " "" "" " "İ " " "" " "̲ "ԍ " "ܤ""Ѓ " "" " " " " """ " " "" " " " "" "" "" " "" " "" " " - " "܇ " " " " " " " " "О " " " "܅ " "!"" " " "ȑ" " " "Ԣ " " " """Ԍ " " " " " ""܃ " " " " " " " " "Ȕ " " "" "̚ " """ "؍ "" " " "" " " " " " """" " " " "" " " "" """" " " " "" "" "" " """ " " " ""Ļ"""" " "ؔ "И " ""IJ " " "" " "2" ""Н " " "Ԁ" " " ""Ш "Ǻ" " " " " " "" "" " "" """ """ " " " "" " " " """ " " " " "Ժ " " " " "Ѕ "؋ " " " " "" " " " " " -! 8" "" - "" " " " - " "" "ĸ " " " " "ܐ "" " "ܠ " " " " "؉ "" "˶" "" " " " " " " - " " "" " " - "̕ " " "" "" "ؑ " "" " " " "ԗ"ܼ " " " """ "Ԡ " "" " " " " " ""Ј" " " "" " " " " """ " "Ĺ " " "ϱ -" "Խ "ؙ " " " " "" " " " " " " " " " " " "" " " " " " " " "" " "" " " " " " " " "" """ "" " "" " "ܞ " " " "İ " " " " "Ў " -" "" " " """ " "4 " "Ă " " " " - " " " " " " " " " "" " "" " " """ļ " " "" - " " " - " " " " ""ܹ "Б " " " "̢ " " - " " "ȍ " "" " " "ԥ " " "Э " " "" " " " " "" " " " " " " " " "" " " """ " " "" " " "" " " " - "ٜ"Ԛ " "" " " " " "ؚ "" " " "Ě "Ȍ " " "Ъ "ă " " " " " "" " " " "" "" " "״" "" " "ԅ " " " " "" "̟ " "" " " "ފ"" " "" " "В "ė " " "Ц " "Ĥ "" " " " " "" " " -" - " " " " " " " ""̳#"č """ " - "Ԥ " " " " "Ԁ "̿"̐ " - "" " "" " " " "" " """" " " " ""Л " " ""!" " "" " "" " " " " " " "ȭ "Ĉ " " " " "" "̠ "Ы ""Ȍ " " """ "" "" """" " "ؖ " " " " " "ت " """" ""Ȥ " " "ԕ " " "" " " " " " "" """ " "Ѝ " - ""ȗ"̹ " " " "" " "Ȫ "Н"Я " " " "" " " " "" " "" "2"" " "ȟ " ""̇ " " " "Ȱ "" " " " " "" -""؎ " " " " " " - "ܧ " " "̖ " "" " " " " " " " ""ԉ - " "č " """ " ""Ȑ " " " "ċ " " " " " " " "" " """ "" """" " " " " " " " " " " "Е " " "" " "" "" " "" " "" " " " " "" " "" - " "м "Ȉ " " " "" " " " " """" " """ " " " "Ȥ " "̉ - " " " " " " " " " "ر " " - " " "" " "И " " " " " " " "Ӕ"" "ܕ " " " ""ҧ " " " " "ē " " " ""Ԉ " " " "" " " " " "̧ "Ԓ " " " " " " "" " "Խ" " " - " "̊ " " " " " "ȶ " "ę """ " " " " " "ܜ " " "ʽ " " "" - " " " " "ܚ " " " " - " " " " "ܣ " "" "" " " " " " " - " " " "" " " "Ą " " " " " " " " " "" " " ""ܽ " """ "" " " "" -"" " " " "" " " "" "Ѝ " " " " " "̗ " " " "" "З " " " " ""̪ " " " " " "%" " - " " "" "І "" " " " " "" " " " " " " "" " " " " " - "" " "" "" - " " " " " " "" "" " " " " " " "" " " "̎ "ȣ " "" "" " - "" "ԛ "Л "" " " " " " "Ȍ " " - " " " " " " "ĩ " " " "" "" " """ "" " " " " " "" - " "" " " - " " "" " " " " "г ""݀ " - "" " " - " "" " "" "ԋ " "Č " " " " " " "" " "" " " " "" " " " "Ћ " " " " "" " " "","܃ "̈ " - "$" " "، " " "ȯ " " "̏ " ""П " " "" " "" " "ć "ԡ " " " "Ц """̡ "" " " " " "" """ " "" " "" "" " " " " " "" " " " " " " " "ĝ " "ؠ " "ۑ" "" " " " " " " " " " " " " "" " - " " " "" " "Р " " " "" " " " """ " " " "Ȗ "" "" " "ȸ " """ " " " "ĉ " " " " " " "" "ģ"Ĉ "" "!" "ğ " " "" " " " " " " " "Х " " " " " " " " " " " "" " "" "" " " " "ԁ " " "ء " " - " " " " " " " " "į "؎ - " ":" " " "" "ب " "̀ "" " " " " -"ߍ" "܉ " " " " " """ԍ " " " " " -" " """؉ " "" "Ј " " "" "̠ " " ""Щ " " " "" "Ԍ " "" " " "Ԭ " "̩ "" " " " "Ȣ "" - " " " " - "" """ " - " " "Ф " "" " " """ "" " " " " "Ƞ "" " " """ "̯ " "" " - " " "" " " " " " ""آ "" "܊ "" " "" " " " " " " "ؼ " "" " " " " "" "̟ " ""!" " " ""ˣ%"ܑ" " " "Ԡ "" "ƕ " " " " "G")"7" " ""І "ȕ ""҉ " " " "" "̎ "ȍ """"" " " " " " "г " " " " " "P "؇ " " " " -" " " "" "" """""۳$"" " " " "" " " " "" " "ҕ "؋ "Ը """ -" " " " ""ȃ " """ "ʔ "!" "Ծ " " "Δ "" "&" " "݂&" " " " " " " " " " " "#" " " " "" " -" " " " ""٭" """"ҹ -"" " " " "" " " "օ" ""S""." " "" " " " ""IJ " " " " "," " """ " " " "" -" " " "" " "ԥ " " "-"Ҕ " " " "" " " " "Ч " "ܣ " " " "" " " " ""̆ " "" " " "ġ " " " " " "Ķ " "" " -" " "+"" "܆ " " " "ċ " " " " " " "У ""ԧ "̣ " "в" " " " " " " " " " "8""2"" """)""'""о " " " -Ֆ" -8" " "Ǒ"ء" " " "" ""ѥ" " " " " " "" "Ƒ "" "̪ "1"ó" "ڇ " " """ؓ " " "" " ""̬ " "Ę "" " "" " -"" " " "ȁ " ";"Џ"ҍ " " " " ""("" " " "":"" " "̺ "Έ " " " " " " "ʉ "#" "" " " "ڌ " " " "Ģ " " " -" " "̖" "A" "1" "܁ "" " "՞ -" " "К " "! " " " " " " " " "" " " " " " " " " " " "З " " " " " " " "ӫ" " "" ""߁" " " " "ܾ "" "1" " "̲ " -"$" " " " " " " " ""փ&" " " -"" " " -" " " " " " " " ""/" " "" " " " "Ю " "" " " "Ѽ""Ҋ "Ī " "$"=" " " " " "" " " " " -"Ȏ!" " " "" "ܒ " "Ԛ "܂" "н "" " "ćY"" " "ʐ "ƃ " " """ " " " " " " "" "̅"˞>" " " " """" " " " " " " ""!"" " ""۰ -" "*" " " " " """ " "" "؊ " " " " " "߹ -" " " " """" " " " " " " """̄ "" " " "" " " " " " "Ђ " " "п " " " " " " " " "ڑ " " "ڐ " -" "з " "" "!"" " " " " "" "" " " "ܝ " "" "Ի ""Ɓ " "#" " " ""߱" " "%" "ކ " "Њ "" ""٫," " " " " "ƒ " "̒ " " " " " " " ""Е " "/" " " "Ԣ " " "" "ց "0" " " " -"Д " " " " " "Ј " "" " " " " " " " " " " "5"R" " " " " " "΅ " " " " " " " " " " " "" " " "֑ " " " "ҋ "" "̸ "Ҁ%"Ĵ " ""," "" " " "" "$" -"" " "̶ "ܠ " " "/" " "Ԩ"Х " " " " " " "3" " " " "" "0" "܉ " "ړ "" "!""+"" "į " " "" "!" " " "" """ "" " " " " " " "Ь "" "" " " " " " " " " " " "" " " "" " "" " " " " " " " " "" " "ͯ "h " """ " -" "$"" -" "+"" -"ܫ " "&" -" " "" """""ܮ """ " "" "'" " -" " "Ъ " " "Խ " " -"̦ " " "" " " " "ԯ ""! " " "" -"" -" " " "ܘ "" """" "">" -" "в """""$"*" " " " " " " " " " "о "" " "6" "@ " " "" " "" " " " "0 " "9" "߁" ""Џ " " "" "'" " " "| " "̟ " " " " " " " " " "ܷ "Ԧ "Ժ "" """1" "" "" ";"! "F" "Ą ""Ĝ " " " "" " -" -8" "Ĩ " " "м " " "ԇ " " " "ݳ" "" " " " "̳ " " " " "(" " " " """Т """ "#" """ " " " " "ě " " " " L 0>"" " " " " " " "" " " "."Ғ " "" " " " " ""'" " " " " " -" "" """#" " "ލ " " "ԃ " "" ""D" "Ԩ " " " " " " " "݃" -" " " "̐ " ""$" " " "" " " " "Č " "ܔ " " " " "! " " "Ē " "" " " " """"" "" " "!" " " "" " " " " " "̝ " " "ܪ "" " " " " "" "" "غ " " " " " " " " " " " -"8" "ރ "͜#" " " " " " " " """8"ґ " "ƌ ")"" " "Ԑ"ʸ" " " " "Ɠ " " "" " "" " "զ!" " " " " " " -" " " " " " " "̩ "ԭ " " " "ĉ "" "" ""Г " " " " " " " "ؒ" "Щ " "Ί " " " -" "&" "ܶ " "" " " " " """ " " " " " "#" " " "" ""ڏ " "" -" " """*" " " " ""å%" " " " " " "" " " " " " " "" " "+"в " " " " " " "" -"" "" "9"(""Ж "" ""! " " " ">" "" ""!"!" " " " """ "Э "#" " "" "ԅ " " " "֓ " " " "" "أ"" " " " "҄ "Ɖ " " " """:" " " "Ш "" " "̍ " " -";" " " " " "5" " " " " "%")" -" -8" "" " " "ܓ "Ɛ " -" "1" """ " ""4""ñ" t 0?"#" " " "́" " """ " ""X " "2" """" "" -" " " -" " " " " " " -" " " """ "" " " " " "ڕ " " "#" "" " "Ȇ " ""ʏ " "" " " " " " """2" " " " " "" "Ǯ " " " " "ܽ -""" "" " " "" " " " " " " " " " " "м " " " " " ""Й " " " "" " ""4" "֍ " " " " " " " " " -"ā " " " "" " " " """ " "Љ "Ǝ " " " ""̥ "" """ " -"(" " " " " " " " "" " " " " ")"У " "Ѝ "" " " " ""Χ-" "ԝ " " ""3" "" "Ý" " " " " " " " " "В " " "/" """"" " " " " " "! " " " " " " "" "Ö ""Ȏ " " " " "6"" " " "" " " " " " " "" "!" "چ """ " "" "Ч " "" " "("" " " ""Я ""-" "а" " "Ԋ " " " " "Ԕ """ " " " " " "̕ "" " " " " " " " "ڔ " " " "/"," " "Х "Ό "" " "А" -""ġ""1"у" "*" " "ܛ " " "ܨ " " " ""'"" " " "Ŀ "ē "Ԫ " "ĭ "" "Ы " -" "Ď"" " " " "0" " " "Ԍ " "֒ " "" "Ά " " " " "ܰ " " "ģ " "< " "" " " "4""O" " -" "" " """ " " " ""! "" "ɂ" " " " "H " " """ " "D " " " ""&""ԗ " "̫ ""Ё" "҂ " " "! "Ώ " " "<" " "%" " "" " " "" "."" -" "Ѓ " " " ""*"0"#" " "ж " " " " " " " "" "" " " " " " """ " ""Ȅ ""9" " " " " " "5" " """!""" " " " " " " " "ĩ ""Է " " " " ","ԓ""" " " " "" " "" " " " "ʈ " ""(" " " "" " " " -! -8"1" "" " " " " " """ "͋" " "" " "" " " "С " " " " " " " "" " " " " " "" " " "Р " "ʁ -"܊0" " " """ " "҆ " "" " " " """ -" "Ђ " "ڄ "" " " "" " "ʥ" " " " " "&" " " "Ԝ " "Ԭ " ""Б " " " " "" " " " " " " ""&" " " " " " " " " " " " "" "ȋ " "Ϭ " " " -"" " "Ģ " "/"$""" " ""ԏ " " " " " "1" " " " "l " ""#"¾" " " " " " " "Ԯ " " " " "ć "̔ "" " """ƅ " "ͤ " "1" "" "Ğ " "Ю " " " """ "ʋ ""Е "ʄ "ܩ " ""x "ܙ "! " " """"Ĉ " "" " " "" " "ʊ " "" " " " " " " " " " " " """ "#"ݶ "" ">" " " "" " """ -")"" "" " "ܞ " "*" " "" "ޔ " " "· " " " " """ " " "̉ ". "5" """" " L 0@" ""/" "+" "#" " " " """" "Ѕ " "Љ " " " " "ԙ " " ""ʂ "" " " "ʍ "3" " "-"," """" " " ""$"" " " " " " ""8" " " " " "" " " " ".""ʆ " "Ĕ " " " " " " """ " " " ""ɭ" " " "'""" " "" " "д "@" " " " " " " " " " """ " " "Ф " " " " " " " "" " " " " " " " " " " " " "̾ "" "" "ڶ" "" " " "" " " "'""Β " " " " " "" " " "" "ԛ " "8""ƹ"" " "?"" "܊ -"" "Ї " " " "Ѕ "" " " "" """ "ڊ " "̙ " " " " "Б " """ " " " " "ؑ "ѐ$" " " " " """ "2 "(""̇ " " "&" " "л " " " L 0A" "" "" ""ɓ"" " " " " " " " " " " " "(" " "" " " " " " " " "ށ ""ȋ'" " "" "ܖ " "ޒ ""̤ " " " " " " " " "Ы " "Ҏ " " " " " " "˴ """"" "ǰ -" " "ط"6"" " "" " " " "" """ " "֏ ""܅ " " " "ܯ "" "3" " " """ " -""ɋ" " -" """ "Գ " " " "1""0"" "R" -"5" -" "ۿ" " " 0B" " " " " " "Я " " " "" " "!" " " " " ""Ƅ " "2" "" " " "!" "" " " "" " " "ԡ " " " "" "Ԟ " " "! " "ܑ " " " " ")"ĝ"" " " " " " " "2" " " " " "" "$")"̮ " "" "%" " " " " " "1" " "̃ " " " " " " "ܦ "݀" "." " " " " " " "" " " "/";" " "9" """-"ݧ" ""̭ " "" ""΄ " " "" " " " " " ""1","""" " " "گ " " "Ў " " " "܋ " "̖ " " " " " " "։ ""з " " "" " ""І "" "ԓ " " "Њ " " " " " " " "ډ ""'"+" "":" " "" " "$" " " ""գ" "@" "ߌ&" "ԍ "ԟ " " "ވ " " "?"̛ " " " ""̽ " """#" " " "¼" "" " " "" " " "+" "2"" " """" " " "ނ " " "ܸ "%""" "ę " " " " " "" -"" " " " " " "" " " " "̼ "" " " "4"-" " " " " "Ѱ -" "" "̯ "."" " " " " " "":"7" " " """ " " " " " " " "" " " " ""܄ " " -"" "܈ "̋ "" " """ڂ " " "ސ " " " "" " " " " "" " "" "О " " " " " "̗ "ȇ "Ծ"""н " "" " " "Ȑ " " " "М " " " "" " "" ""и "ߞ" " " " ""ɒ " " "" ","" " " "̱ "Ѓ "މ " """ " "ƕ"ޑ "'""7"̡ "И "б " " " " " "2" " " " " "ԣ " "6" ""Ċ " "څ " "Ƣ*"""" "" " " "ʒ " " " """ " " "+" " " " " " " " " " "҈ " " " " " "ك " " " " " " -" " " " "ʕ "N" " "ɽ" " "" " " " " """" "" "" "" " "" " " " " "Р "@" " " " " "" "" " " -" " " """ " ""̷ " ""Ѐ " "#" " " "҅ " -" " "Ɣ4" "" "ܗ " "" "#" " " "" " "#"(" "֔ " "." " " " ")"߽ -" ""̰ " " " "Ҩ" " " "" -"Ə "Ԙ " ""ԉ " "Դ "" " "д "ޛ """! " "Թ " "" "#"" " " "8 " " "!"ԁ "&"" """ "Ɔ " " "ۑ/" " " " " " " " " " """ -" " "" "Լ "" " " " " "" " " "̵ " " " " " " " "'" "" "Ա " " " " " " " "" " "ҁ " "2"7""5""" "" " "" -("!" "'"Ҍ " ""/" " " "Ĉ" " " " -" " " " " "ķ " ""Ҍ " " " "" " " "" "," " " " " ","" " " "ܴ " "" "" " "" " " " " " " "Ή " " " " " " ""ɬ%" " " " " " "C" "ފ " " "!" " -" " " " """ " " " "8"" " " " ""к " " " " " " " "'" " " -" " " " "" "" " " " " " " " ""! "Ԑ "" " "` "ԍ " " " " " " " """" ""Ɗ " " " " " " "" " " " " "" " "̊ " "" "" " " "ݨ"" "ք " "0" "ش "" """ " " " " " -" """ " " "Ќ " " " "&","؉ "0""">"Ƃ " " " " " "" " " "ʇ " " " " " " "̜" "" " " "" " " " "Զ " -" " " " " "Ȍ" """ " " " "T "$" "=""֎ " ""dž"Ӳ"" "ı "(" " " " "ޠ!" " " " " " " " " "ؔ "! " " " " "ܜ " " " "T"" " "" " " """ "Ȕ " " -" " " """" " "" " "" " " " " " " "Є " "3", "" " " " " """ "А "9" """ " " " " " " " " -""" "" "Ƭ "" " " " " " "" " " " "!"ދ " " " " " " " " " """""Բ "" "" " " " "Ґ " "̿ " " "հ!" " "" " """ʅ " " "" " " " " " " " " ""҇ " " ">"̘ ""." " " " " " " ""ʎ " " "!"" " "'" "̅ " " " " " """ " ">"ܹ " "" ""ޔ" "" "ľ " -"*" "С " "ī " " "%"!" " " "" " " " " " " " "" " "" "" "ƙ" ""ׇ " " " " " -" " -" " """ " "&" "." " " "а " " " " "ħ " " "6"" "ܡ " " "%" " " " " "" " " " ""܅" " " " " "" " "ܧ " " " " " " " "! "Н " " """ " " " " " "" " """ " " "$" " " " " "Ћ " "-" "ڋ " -"" " " " "" " " " "" " """ "и " " "ˆ"ȓ " "" " -" "" ""ˏ7" ""'" "4"""" " " " " " " " "В "%"" "!"" "ʑ " " "" " " "" -" "փ " """ " " " "."! " " " " " " " " "" " "" " " " """"ԇ"" " " "̈ " " " " " " "ʁ "d "" " " " " "ͪ""""" " " " " """ " "+" " " " " " " "܃ "" " "ɽ -"ĵ " ""! " " " " "Г "Ě " "" "ă "" " " " "Ԓ " " " "'"ܼ "ѕ" """ " " " "к """ """&" " "( " " "Ύ " " "ĸ " ""͈!" " " " " " "K" "" -"ڍ "" "؋&" ""Ј " " " "٣"" " " " "" " "" " "! " "б " "" "ڎ "1"̌ "Ĭ " " "" " "" " " " "ֈ "ܐ " "й " " " " "t " ""#"ж """چ" " " "" "ؐ " -" " " " " " " " "/" " " "֤+" " " " " " " " " " "$"" """" " " " " " "" " " " " "" "" " " """ȿ " " " " " " "";" " " " "܌ " ";" " " " " ""'" " " " " " " "$" " " -" -8" "֍ " " " """)" " " " " " " " "/""" "" " " " "" "" -" " "" " "&"%" " " "Ё " "" " " """ " """" " " " " " " -" "Л " " "G" " " "" " " " " " "ޏ " " """ " " "" -"" " " " " " " "%" " " " " " " " " " " " " " ""ɢ2" "" -" "" " " "օ " " " " " -"" "ؕ "'" " "Ղ"ʌ "" " " " " " " -" "" " " " " " " "" " ";" T 0C"""-" "Ї " " " " " " " " " """ " " " "ܻ " " " " " " " "Ε " "ğ "9" "؎ " "л " " -"" " " " "" " " ""ã""ij " "ڒ " "" " "" -" "#" " " "" "" "" "","" "ʲ(" " " " "*" ":" "ّ" "L " " " " " " -"Џ "п " "" " " " " " " "ֆ "" ""Ŀ" "Т " """ " " " " " " "Ԏ "ԩ " "":" " " " " " " " ")" "/"" "* " " "" " "+"""."" " " """ "Đ " " " " "A" " " " " "ܒ"" "" " " ""ގ " " " " " " " " " " "Ե " "! " " " " " "" " "Ƈ " "<" -"ƍ " " " "Ĝ"p ""1" " " """ " " "ܚ " " " " " " " " ""ޓ " " " " 0D" " " " " "" " " "#" " " "" "" "*" "ԋ " " " " "6" "0""Ă ""ތ " " " " " "г " "" " " -" " " -" "" " " "̂ " " " " " " " " """" ""Д " " " " " "(""""/" " "2"" "! " "Ȃ " "ȑ " " " "ׇ" " " " "ĥ " "؈ " "ܕ "%" " -ܑ" -8" " " " " " " " " "ď "" " "" " " " "T"Ѝ " " " # 0E" " " " -" " "" "" " " " " " " " "" " "̠ " " " "" """" " " " " ","Ќ " "C" ""Ԥ "Ԇ"" " " " " "̨ " "ܽ " ""ǃ!"2"" " "" " " "" " """ " " " "!" " " "Э "" -" " " "" "" L 0F" " " " "+""İ ""̓ "և " " " " " " " " ""ԫ "("" " """ " " "Ɩ -" " " """" " " " "" " " "Ё " """ " "ބ " "܊ "ĝ " " " " " " " " "" " " """ " "2" " -"""ѩ"" """ "ځ " " " "! " " " " " " "" " "æ" "" " " " " "1" " """ " " "+" " ""<" -" " ""ܭ ""Ɣ " " "3" "" " " "'"ӓ" " " "0" " ""č " "̢"" " " " """ " " " " " " " -" "1" " " " " " " " " " " " " " " " "Ъ "2""@" "-" """ "̜ "" " " " "ݍ"؆ "" " " "" " " "Ħ "Ћ """ "" "9" " " " "ĺ " " "" "đ "̞ " " " "ޑ -""" " " " " "Б" " " "" -" "" " "ɟ ""!"! " "" " "Ԅ "" " " " " " " " " " " " " "! " "\ "܇ " " "ҏ " " " " ""'" "ܲ """ " " " " "Ԉ " " ""9" " " -"" " " "О " " " " "" " " " " " "" " " "" "1" "'">" " "Ď "̹ " "" " " " " " " " " " " " "" " "̑ "" "ܕ" " " "/" " """ " 0G"" "т2""ʓ " " " "ą " " "" " -" " "" " " " " " " " "ܤ "П " " " " " "̏ " """" " " "+" " " -" " ""%" -" " " " -" " " " " " " " " " " "Ƌ "ԕ " "" " " " "ة""̢ " " " " " " " "ԁ*")" "އ " " "("" " " " " " "%" " " " " "ė " " " " " " " " " " "" "" ""*" "ޕ " " "" " " " " " " " " "Ĺ " """ "Ç -"" "Ģ " " "" "Ȍ " -"" " " " " " " " " ""("ʃ " " " " "%" " " """ǃ -" " "$ " " " " "Պ "" " " " " " " "̚ " " " " " -""̧ "" " " " " " "" """ " "" " " " " -" " " " " "ڮ -"" " " " "H" " " "" "!"м " " " " " " " " " "%""ަ"(""" " " " ""ǀ"Α ""]"$" " "Ĥ " " " " "" " ""%" " "&"ܵ " " " " "Ш " " """ """ " "" "" ""1" " " "ܥ " " "?"7"е " " ""(" " " " " "ΐ "ޙ -"" " "֕ " " -"" " " " " " "Ԃ "й "̴ "" " " " " "" "" " " "" "Ȋ " ""Γ " " " "ޅ " " """ " "ܟ " -""3""0"ϊ " -! -8" "" " " " "Ԇ " "Ц " " " " " "ڈ " " " " " " " """ " "Ϧ" -" "" "، "" "ۦ -" " " """ "" " " "" " "" " "͍" " "̊ -" " " " " "ܿ ""Ԉ""Ȓ "Ձ -"֒-" "а " "ܢ " " " " "ļ " "Ġ " " "5" " " " " " " "" "Н "ٮ " " -" -8"Į "! " "Ь " ""&"-""ڄ "ƕ""܍ " "" " " "" " ""ȏ " " "Ć "" "J" " ""!" " " "" """ܱ "4"" "ܬ " " "," -! -8"܂ " " " " " " " "" "!""""ւ "" "" " "" " " "! -" "" "Щ " " " " " "ȅ " " "́ " " " "٭"ʼn1" " " " " """ " "А "!" " "Є "" "" " -""6" " " "Կ " -" " " " "" " " " " "ƈ "̻ " ""е "*""ĕ "П " " "" " " " " " " """ȉ " " """ " "ڃ " """ "" "" " " "Ļ " "" " " "ؒ " " " " " ";" " " "؍ " " " "҉"0"" " " ""҃ " " " "" " " " " """ " " "! ""ԑ " -"" " "" "$" "" -" "" -! -8" " " " "H"" "" " "" " " " " " " " " " " " """ "" "Ľ " " -" "" L 0H" " """ "" " " "" " " " " "("" " " " "؏ " -""" """ " "*"" " " "ܳ " ""Ф " "ғ " """""ԍ" " "" " " " " " "" "(" " " """" -" ""%" " " " " " "Ȉ "ܺ "" " " "" " "֊ " " " """"ج " " ""ѱ"" "Ю """ *L -,Elf32_Dyn::$A263394DDF3EC2D4B1B8448EDD30E249*d_val *d_ptr 2 -^ (20t (2 .% (2 r% (2 E% (2 B% (2 -Ɍ% (2 =% (2 /% (2 j% (2 j% (2 >% (2 F% (2 Z% (2 M% (2 -% (2 ;% (2 k% (2 X% (2 h% (2 4% (2 C% (2 v% (2 .% (2 ^% (2 q% (2 _% (2 N% (2 a% (2 ]% (2 u% (2 X% (2 B% (2 C% (2 ~% (2 z% (2 X% (2 |% (2 g% (2 6% (2 E% (2 o% (2 @% (2 k% (2 k% (2 F% (2 -% (2 i% (2 0% (2 G% (2 j% (2 Q% (2 4% (2 [% (2 p% (2 E% (2 Y% (2 -% (2 O% (2 =% (2 I% (2 N% (2 -% (2 x% (2 I% (2 @% (2 z% (2 G% (2 k% (2 +% (2 K% (2 -% (2 p% (2 A% (2 S% (2 -% (2 /% (2 {% (2 l% (2 Y% (2 8% (2 n% (2 T% (2 |% (2 q% (2 P% (2 )% (2 8% (2 -܌% (2 q% (2 n% (2 y% (2 -% (2 4% (2 z% (2 :% (2 >% (2 % (2 H% (2 k% (2 Z% (2 g% (2 Y% (2 I% (2 ]% (2 b% (2 T% (2 g% (2 F% (2 P% (2 A% (2 \% (2 F% (2 G% (2 -% (2 ]% (2 v% (2 -% (2 d% (2 F% (2 <% (2 }% (2 ?% (2 Y% (2 -% (2 }% (2 )% (2 W% (2 -֊% (2 n% (2 X% (2 -% (2 U% (2 -% (2 i% (2 @% (2 =% (2 J% (2 K% (2 g% (2 c% (2 -Ȃ% (2 Y% (2 {% (2 >% (2 /% (2 S% (2 :% (2 ~% (2 m% (2 :% (2 % (2 q% (2 5% (2. (2f (2? (2U (2a (2z (2+ (2k (2g (2i (2d (2i (2e (2[ (2h (2a (2 (2I (2. (2 (23 (2_ (2h (2z (2^ (2B (2A (2E (2 (2[ (2E (2} (2/ (2 (2F (2t (2o (2B (23 (26 (2i (2N (2a (2? (2v (2i (2) (2n (2p (2: (2Z (2a (2D (2t (2 (2l (2T (2j (2) (2 (2Y (2o (2 (2> (2; (2w (2< (2d (2T (2P (2 І (2. (2m (2m (2i (2* (2O (25 (23 (2j (2D (2S (2Y (2q (2~ (2[ (2D (2Ȁ{ (2Ԁf (2Y (2w (2 ъ (2g (2{ (2E (2f (2w (2I (2́ (2U (2d (2g (2@ (2W (2m (2j (2X (2S (2ĂN (2Ђc (2܂; (2 ܈ (2 ǀ (2- (2< (2v (2m (2X (2^ (2ȃ5 (2ԃI (2; (2@ (2z (2; (2y (2 (2? (2f (2* (2̄Y (2s (2N( (2E (2i (2j' (2) (2B (2o (2 & (2ąT! (2Ѕ (2܅d (2o (2{ (2h (2S (2_ (2{! (2< (28 (2ȆU# (2ԆU( (2 Ċ# (2N" (2r& (2U (2U (2Y" (2j (2 (2= (2̇J' (2؇- (2j% (2u% (2l$ (2 Њ" (2: (2Y& (2T (2 & (2ĈU (2Ј}# (2܈S (2P$ (2?" (2 (2v (2S (2]( (2 ӄ (2u$ (2ȉ3 (2ԉ ˊ (2 (2P$ (2] (2N (2h% (2/! (2 (2` (2u (2̊,# (2؊a# (2; (2i( (2=( (2 {)! (2 a)$ (2 -) (2 H) (2 *)( (2ċ ?) (2Ћ q)! (2܋ p) (2 4) (2 e) (2 5)# (2 K) (2 K)) (2 g)& (2 Y)! (2 B)# (2Ȍ h) (2Ԍ p)% (2 p)! (2 X)# (2 H)) (2 *)" (2 Q)( (2 w) (2 ^) (2 r)$ (2 4) (2̍ @)$ (2؍ /)% (2 `) (2 \)' (2 6)) (2 x)' (2 t)( (2 4)! (2 {) (2 X) (2Ď G)' (2Ў |) (2 V) (2 2)' (2 l) (2 ;)$ (2 I) (2 w) (2 h) (2 f) (2 j)' (2ȏ @) (2ԏ -ۈ)! (2 +) (2 {) (2 b) (2 O)( (2 H) (2 -) (2 -) (2 4) (2 y)" (2̐ h) (2ؐ ,)( (2 G) (2 X) (2 ~)" (2 -ǂ) (2 /)% (2 k) (2 b) (2 4) (2đ C) (2Б 8) (2ܑ I)' (2 ^) (2 ;) (2 L)) (2 z) (2 2) (2 <)& (2 K)$ (2 A)! (2Ȓ *) (2Ԓ r)' (2 ,)& (2 B)% (2 -)& (2 ^) (2 q)" (2 Z)! (2 o)& (2 h) (2 {)& (2̓ -)% (2ؓ A)' (2 -) (2 L) (2 =) (2 T) (2 1)' (2 3) (2 e)" (2 B)! (2Ĕ 1) (2Д >) (2ܔ S) (2 ;)" (2 w) (2 -݄)$ (2 J)# (2 I) (2 3)$ (2 d) (2 T) (2ȕ C) (2ԕ g) (2 -)# (2 n) (2 Y)# (2 M) (2 =) (2 M) (2 b) (2 Q) (2 -) (2̖ 6) (2ؖ F) (2 g)$ (2 A)# (2 h) (2 t) (2 i)# (2 |)' (2 w)! (2 J) (2ė /) (2З -)& (2ܗ q) (2 -)& (2 -) (2 c) (2 -) (2 t) (2 0) (2t( (2; (2Șp (2ԘH# (2Y! (2=& (2 ̀ (2: (25 (2; (2n (2> (2t (2̙ (2ؙz (2 (27& (2.! (2; (2/ (2+ (2 Ʌ (2g (2Ě- (2КM (2ܚC (2n (2@ (2: (2b (2Y (2v (2_ (2M (2țC (2ԛ (2] (2] (2 ۉ (2e (2x (2 ً (2L (2D (2[ (2̜e (2R (2> (2? (2V (2 (2L (2c (2~ (2u (2ĝ= (2НP (2ܝ6 (2 ۆ (2b (25 (2N (2e (2y (2 (2 (2Ȟ8 (2Ԟf (2K (2x (2h (2T (2+ (2* (2U (2j (2G (2̟| (2؟T (2C (29 (2F (2D (2@ (26 (2R (2+ (2Ġe (2Р (2ܠJ (2P (2G (2? (2Y (2l (2 (2{ (2E (2ȡg (2ԡa (2e (2- (2> (2= (2C (2: (2 ԃ (2 ؆ (2g (2̢m (2آ3 (2I (26 (2| (2 (2L (2P (23 (2a (2ģx (2УC (2ܣh (2b (2 (2f (2 (2X (27 (21 (2 (2Ȥ= (2ԤT (2. (22 (2. (2V (2E (23 (20 (2{ (2, (2̥{ (2إ? (2? (2c (2U (2; (2N (2 ӂ (2 (26 (2ĦM (2Цe (2ܦw (2s (2` (2o (2[ (2a (2k (26 (2\ (2ȧ. (2ԧ` (2W (2L (2 (2i (2m (2A (2o (2b (2W (2̨ (2ب@ (2V (2D (24 (26 (2~ (2S (2f (2A (2ĩJ (2Щ> (2ܩ^ (2 (2K (2K (2E (2X (2 (2O (2f (2Ȫ| (2Ԫ4 (2@ (2@ (2S (25 (2_ (2 (2 (2Z (2L (2̫} (2ث[ (2Y (2= (2} (21 (2~ (2E (2` (2S (2Ĭ9 (2Ь (2ܬ+ (2T (26 (2 (2> (2q (2M (2y (2 (2ȭ (2ԭ+ (2 (2, (2b (2V (2+ (2N (2F (2u (2t (2̮6 (2خu (2e (2 (2C (2y (2* (2e (2d (2M (2į (2Я5 (2ܯf (2 ݈ (2| (2 Ƈ (23 (2n (2t (2} (2, (2Ȱ5 (2] (2k (2| (2G (2T (2 (2O (2c (2q (2y (2̱k (2ر[ (2| (2B (2? (2) (2J (2= (2I (2 (2IJ (2в. (2ܲP (2b (24 (2t (2S (2D (2} (2l (2H (2ȳb (2Գ (21 (2Y (25 (2 (2g (2 (2> (2B (2d (2̴l (2شy (2P( (27 (2E (2B' (2@ (2V (2w (2@& (2ĵn! (2е (2ܵ (2a (2Z (22 (2Q (2x (2[! (21 (2O (2ȶ # (2Զv( (2,# (2u" (2L& (26 (2 (2<" (25 (2/ (2_ (2̷=' (2ط5 (2s% (2A% (21$ (2s" (2n (2 & (2I (2r& (2ĸj (2и9# (2ܸs (2T$ (2Q" (2 (2. (2T (2 ( (2 (2 $ (2ȹG (2Թ5 (2o (2}$ (2E (2V (2x% (2)! (2 (2n (2O (2̺ # (2غX# (2} (2 ( (2.( (2k! (2H$ (2] (2M (2z( (2ĻN (2л8! (2ܻD (2z (22 (2m# (2} (2 ͅ) (2;& (2^! (2z# (2ȼ2 (2Լ6% (2V! (2+# (2`) (2{" (2,( (2 ϋ (2 (2M$ (2l (2̽1$ (2ؽ/% (2] (2P' (2}) (2f' (2 Ό( (2 ޅ! (2 (2: (2ľ ' (2оM (2ܾ@ (2n' (2? (2x$ (2 (2[ (2q (2T (2k' (2ȿf (2Կ+! (2V (2\ (2= (2L( (2 (2w (2 (2w (2S" (2X (2D( (2l (2K (2+" (22 (2W% (2 (2R (2I (2~ (2k (2 ' (24 (2< (23) (2e (2i (21& (2W$ (2 ڃ! (2; (2j' (2y& (2G% (2 & (2p (20" (2 ! (2:& (2 (2?& (2X% (2I' (2N (2F (2> (2E (2d' (2@ (2`" (2D! (2c (2. (2F (2\" (2K (2w$ (2h# (2_ (2@$ (2y (21 (2* (2 (2r# (2G (2^# (2` (2 (2r (2D (2q (2E (2~ (2^ (2-$ (2J# (2 (2j (2[# (2h' (2V! (2/ (2O (2i& (2L (25& (2` (2 (28 (21 (2I (2D( (2< (2 (2K# (27! (2z& (2 Ƌ (2o (2S (2 ̓ (2 (2a (2I (2p (2 (2{ (2@& (2! (2l (2\ (2E (2 (2= (2f (2L (2C (2A (2e (20 (2z (2] (2: (2t (26 (2y (2r (2 (2e (2b (2 Ճ (2] (2- (2= (2} (2 (2K (2\ (2N (2v (2p (2@ (2V (2 (2 (2 (2E (24 (2{ (2s (2v (2 (2< (2= (2, (2P (29 (2p (22 (2E (2F (2@ (2V (2] (2| (2| (2^ (2C (2V (2F (2g (2Y (2H (2o (2 (2~ (2N (2n (2S (2e (2Q (2n (2r (2@ (2; (2B (2 (2+ (2? (2[ (29 (2) (2V (2j (2} (28 (2R (2 (2\ (2+ (25 (2B (2 (2r (2X (2 (2| (2] (2` (2{ (2} (2` (2` (2w (2E (21 (2K (2G (2/ (2L (23 (2e (2V (2Z (2z (2u (2 ρ (2t (2, (2Z (2` (2- (26 (23 (2\ (2_ (21 (2 (2\ (2R (2I (29 (2= (2S (2Z (2 Ç (2q (2 (2 (2L (2 (2 (2* (2` (25 (2` (2 (22 (2O (21 (2x (25 (2^ (2[ (2j (2? (2o (2] (2 (2R (24 (2> (2f (2h (2~ (2) (2\ (2 (2R (2k (2m (27 (2C (2M (27 (2 (21 (2U (2f (2 (27 (2J (2_ (24 (22 (2i (2` (2f (2\ (2p (2 (2L (2S (2| (2c (2L (2| (2 (2p (2c (2 ۇ (28 (2, (2 (2P (26 (2~ (2w (2n (2J (2| (2r (2s (2[ (2 (2. (2{ (2i (2H (2c (2l (2} (2Y (2{ (2_ (2g (2y (2e (2 ߄ (2q (2x (2e (2R (2 (2S (2| (2} (2R (2L (2q (2p (2 (2H (2] (2I (25 (22 (26 (2o (2O (2v (2H (2< (2B (2] (2 (2 (2n (2w (2w (2y (2@ (2q (21 (2w (2X (2 (2d (2B (2C (2F (2V (2= (2y (25 (2\ (2_ (2 (23 (2t (2] (2x (2} (2 Q"#mm&`,D"<oш_J# -x|o#'L -'=I-Y 5J_t-B -wN w(!*& }@CisewC j^6l[)+$Shb#[H19s 4w -{9w{9 /=HvS" -Z3"^ Il C4I -,o7w ABw '!. - - - - -(2$@7T[T