diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..55e54a0 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,13 @@ + +[run] +branch = True +source = + zelos + +[paths] +source = + src + .tox/*/site-packages + +[report] +show_missing = True diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..8434b4b --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = E203, W503 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9db3cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,139 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +/.Python +/build/ +/develop-eggs/ +/dist/ +/downloads/ +/eggs/ +/.eggs/ +/lib/ +/lib64/ +/parts/ +/sdist/ +/var/ +/wheels/ +/pip-wheel-metadata/ +/share/python-wheels/ +/*.egg-info/ +/.installed.cfg +/*.egg +/MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +/*.manifest +/*.spec + +# Installer logs +/pip-log.txt +/pip-delete-this-directory.txt + +# Unit test / coverage reports +/htmlcov/ +/.tox/ +/.nox/ +/.coverage +/.coverage.* +/.cache +/nosetests.xml +/coverage.xml +/*.cover +/*.py,cover +/.hypothesis/ +/.pytest_cache/ + +# Translations +/*.mo +/*.pot + +# Scrapy stuff: +/.scrapy + +# Sphinx documentation +/docs/_build/ +/docs/api/ +/docs/log.txt + +# PyBuilder +/target/ + +# Jupyter Notebook +/.ipynb_checkpoints + +# IPython +/profile_default/ +/ipython_config.py + +# pyenv +/.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +/celerybeat-schedule +/celerybeat.pid + +# SageMath parsed files +/*.sage.py + +# Environments +/.env +/.venv +/env +/venv + +# Spyder project settings +/.spyderproject +/.spyproject + +# Rope project settings +/.ropeproject + +# mkdocs documentation +/site + +# mypy +/.mypy_cache/ +/.dmypy.json +/dmypy.json + +# Pyre type checker +/.pyre/ + +# Zelos +/.vscode/* +/settings.json +**/sandbox/* + +# Mypy type checking +/*.mypy_cache/* + +# pyenv local settings +/.python-version + +# pyinstaller +/build/* +/dist/* + +# ida files +*.i64 +*.idb +*.id0 +*.id1 +*.id2 +*.nam +*.til diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0b1fa2d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +repos: + - repo: https://github.com/psf/black + rev: 19.3b0 + hooks: + - id: black + language_version: python3.6 + # override until resolved: https://github.com/ambv/black/issues/402 + files: \.pyi?$ + types: [] + + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.8 + hooks: + - id: flake8 + language_version: python3.6 + + - repo: https://github.com/asottile/seed-isort-config + rev: v1.9.3 + hooks: + - id: seed-isort-config + args: [--exclude=examples/.*\.py] + + - repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort + additional_dependencies: [toml] + language_version: python3.6 + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: debug-statements diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..e6a8043 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,10 @@ +--- +version: 2 +python: + version: 3.7 + + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..68c86ce --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,13 @@ +# The Core Zelos Team + +* [Kevin Valakuzhy](//www.linkedin.com/in/kevin-valakuzhy-319a5447/) - Research Engineer, Developer +* [Ryan C. Court](//www.linkedin.com/in/rccourt) - Research Engineer, Developer +* [Kevin Z. Snow](//www.linkedin.com/in/kevinsnow/) - Co-Founder, Developer + +### Special Thanks To + +* Fabian Monrose - Co-Founder +* Ann Cox - DHS Program Manager +* Angelos Keromytis - DARPA Program Manager (Former) +* Dustin Fraze - DARPA Program Manager +* Suyup Kim - Intern diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7808933 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + + +## [0.0.1] - 2019-12-25 + +### Added + +- Initial open source commit. + +### Changed + +- Added repository boilerplate. + +### Removed + +- N/A + +[unreleased]: https://github.com/zeropointdynamics/zelos/compare/v1.1.0...HEAD +[0.0.1]: https://github.com/zeropointdynamics/zelos/releases/tag/v0.0.1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..11dd990 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM ubuntu:bionic +MAINTAINER "Kevin Z. Snow " + +RUN apt-get update && \ + apt-get -y upgrade && \ + apt-get install -y python3 python3-pip python3-venv git cmake + +RUN useradd -s /bin/bash -m zelos +RUN su - zelos -c "python3 -m venv /home/zelos/.venv/zelos" +RUN su - zelos -c "source /home/zelos/.venv/zelos/bin/activate && git clone https://github.com/zeropointdynamics/zelos && cd zelos && pip install -e '.[dev]'" +RUN su - zelos -c "echo 'source /home/zelos/.venv/zelos/bin/activate' >> /home/zelos/.bashrc" +CMD su - zelos diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..d9b4f8a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,28 @@ +graft src/zelos + +include LICENSE *.md *.toml *.yml *.yaml *.ini .flake8 +graft .github + +# Tests +include tox.ini .coveragerc conftest.py +recursive-include tests *.py + +# Documentation +include docs/Makefile docs/make.bat requirements.txt +recursive-include docs *.png +recursive-include docs *.svg +recursive-include docs *.ico +recursive-include docs *.py +recursive-include docs *.rst +recursive-include docs *.md +prune docs/_build +prune docs/api + +# added by check_manifest.py +include Dockerfile +recursive-include tests *.c +recursive-include tests *.so + +# Ignore +global-exclude *.py[co] +global-exclude __pycache__ diff --git a/README.md b/README.md index 0ad3646..7c3e112 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,117 @@ -# zelos -A comprehensive binary emulation platform. +![PyPI](https://img.shields.io/pypi/v/zelos) +[![Build Status](https://dev.azure.com/kevin0853/zelos/_apis/build/status/zeropointdynamics.zelos?branchName=master)](https://dev.azure.com/kevin0853/zelos/_build/latest?definitionId=1&branchName=master) +[![codecov](https://codecov.io/gh/zeropointdynamics/zelos/branch/master/graph/badge.svg)](https://codecov.io/gh/zeropointdynamics/zelos) +[![Documentation Status](https://readthedocs.org/projects/zelos/badge/?version=latest)](https://zelos.readthedocs.io/en/latest/?badge=latest) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/zelos) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) +Code style: black + +# Zelos +Zelos is a Python-based binary emulation platform. Linux x86, x86_64, ARMv7 and MIPS binaries are supported. + +## Installation + +Use the package manager [pip](https://pip.pypa.io/en/stable/) to install zelos. + +```bash +pip install zelos +``` + +## Basic Usage + +### Command-line +To emulate a binary with default options: + +```console +$ zelos my_binary +``` + +To view the instructions that are being executed, add the `-v` flag: +```console +$ zelos -v my_binary +``` + +You can print only the first time each instruction is executed, rather than *every* execution, using `--fasttrace`: +```console +$ zelos -v --fasttrace my_binary +``` + +By default, syscalls are emitted on stdout. To write syscalls to a file instead, use the `--strace` flag: +```console +$ zelos --strace path/to/file my_binary +``` + +Specify any command line arguments after the binary name: +```console +$ zelos my_binary arg1 arg2 +``` + +### Programmatic +```python +import zelos + +z = zelos.Zelos("my_binary") +z.start(timeout=3) +``` + +## Contributing +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. + +Please make sure to update tests as appropriate. + +### Local Development Environment + +First, create a new python virtual environment. This will ensure no package version conflicts arise: + +```console +$ python3 -m venv ~/.venv/zelos +$ source ~/.venv/zelos/bin/activate +``` + +Now clone the repository and change into the `zelos` directory: + +```console +(zelos) $ git clone git@github.com:zeropointdynamics/zelos.git +(zelos) $ cd zelos +``` + +Install an *editable* version of zelos into the virtual environment. This makes `import zelos` available, and any local changes to zelos will be effective immediately: + +```console +(zelos) $ pip install -e '.[dev]' +``` + +At this point, tests should pass and documentation should build: + +```console +(zelos) $ pytest +(zelos) $ cd docs +(zelos) $ make html +``` + +Built documentation is found in ``docs/_build/html/``. + +Install zelos pre-commit hooks to ensure code style compliance: + +```console +(zelos) $ pre-commit install +``` + +In addition to automatically running every commit, you can run them anytime with: + +```console +(zelos) $ pre-commit run --all-files +``` + +#### Windows Development: + +Commands vary slightly on Windows: + +```console +C:\> python3 -m venv zelos_venv +C:\> zelos_venv\Scripts\activate.bat +(zelos) C:\> pip install -e .[dev] +``` + +## License +[AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..46cbd19 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,117 @@ +--- +trigger: + - master + +jobs: + - job: 'Test' + pool: + vmImage: 'ubuntu-latest' + strategy: + matrix: + Lint: + python.version: '3.6' + tox.env: lint + + py36: + python.version: '3.6' + tox.env: py36 + py37: + python.version: '3.7' + tox.env: py37 + py38: + python.version: '3.8' + tox.env: py38 + + # pypy3: + # python.version: 'pypy3' + # tox.env: pypy3 + + Docs: + python.version: '3.6' + tox.env: docs + PyPI-Description: + python.version: '3.7' + tox.env: pypi-description + + steps: + - task: UsePythonVersion@0 + displayName: Get Python for Python tools. + inputs: + versionSpec: '3.7' + addToPath: false + name: pyTools + + - script: $(pyTools.pythonLocation)/bin/pip install --upgrade tox + displayName: Install Python-based tools. + + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + architecture: 'x64' + # condition: not(in(variables['python.version'], '3.8')) + displayName: Use cached Python $(python.version) for tests. + + # - script: | + # sudo add-apt-repository ppa:deadsnakes + # sudo apt-get update + # sudo apt-get install -y --no-install-recommends python$(python.version)-dev python$(python.version)-distutils + # condition: in(variables['python.version'], '3.8') + # displayName: Install Python $(python.version) from the deadsnakes PPA for tests. + + - script: $(pyTools.pythonLocation)/bin/tox -e $(tox.env) + env: + TOX_AP_TEST_EXTRAS: azure-pipelines + displayName: run tox -e $(tox.env) + + - script: | + if [ ! -f .coverage.* ]; then + echo No coverage data found. + exit 0 + fi + + # codecov shells out to "coverage" and avoiding 'sudo pip' allows for + # package caching. + PATH=$HOME/.local/bin:$PATH + + case "$(python.version)" in + "pypy2") PY=pypy ;; + "pypy3") PY=pypy3 ;; + *) PY=python$(python.version) ;; + esac + + # Python 3.8 needs an up-to-date pip. + if [ "$(python.version)" = "3.8" ]; then + curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py + $PY get-pip.py --user + fi + + $PY -m pip install --user coverage codecov + + coverage combine + codecov + env: + CODECOV_TOKEN: $(codecov.token) + displayName: Report Coverage + condition: succeeded() + + + - job: 'Windows' + pool: + vmImage: 'windows-latest' + strategy: + matrix: + py36: + python.version: '3.6' + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + architecture: 'x64' + displayName: Use cached Python $(python.version) for tests. + + - script: python -m pip install -e .[dev] + displayName: Install package in dev mode. + + - script: python -m pytest + displayName: Run tests. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..60a1e5c --- /dev/null +++ b/codecov.yml @@ -0,0 +1,10 @@ +--- +comment: false +coverage: + status: + patch: + default: + target: "100" + project: + default: + target: "100" diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..50deb3b --- /dev/null +++ b/conftest.py @@ -0,0 +1,9 @@ +from hypothesis import HealthCheck, settings + + +def pytest_configure(config): + # HealthCheck.too_slow causes more trouble than good -- especially in CIs. + settings.register_profile( + "patience", settings(suppress_health_check=[HealthCheck.too_slow]) + ) + settings.load_profile("patience") diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..4e51f06 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= -w log.txt -W -n -T +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..7c3e112 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,117 @@ +![PyPI](https://img.shields.io/pypi/v/zelos) +[![Build Status](https://dev.azure.com/kevin0853/zelos/_apis/build/status/zeropointdynamics.zelos?branchName=master)](https://dev.azure.com/kevin0853/zelos/_build/latest?definitionId=1&branchName=master) +[![codecov](https://codecov.io/gh/zeropointdynamics/zelos/branch/master/graph/badge.svg)](https://codecov.io/gh/zeropointdynamics/zelos) +[![Documentation Status](https://readthedocs.org/projects/zelos/badge/?version=latest)](https://zelos.readthedocs.io/en/latest/?badge=latest) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/zelos) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) +Code style: black + +# Zelos +Zelos is a Python-based binary emulation platform. Linux x86, x86_64, ARMv7 and MIPS binaries are supported. + +## Installation + +Use the package manager [pip](https://pip.pypa.io/en/stable/) to install zelos. + +```bash +pip install zelos +``` + +## Basic Usage + +### Command-line +To emulate a binary with default options: + +```console +$ zelos my_binary +``` + +To view the instructions that are being executed, add the `-v` flag: +```console +$ zelos -v my_binary +``` + +You can print only the first time each instruction is executed, rather than *every* execution, using `--fasttrace`: +```console +$ zelos -v --fasttrace my_binary +``` + +By default, syscalls are emitted on stdout. To write syscalls to a file instead, use the `--strace` flag: +```console +$ zelos --strace path/to/file my_binary +``` + +Specify any command line arguments after the binary name: +```console +$ zelos my_binary arg1 arg2 +``` + +### Programmatic +```python +import zelos + +z = zelos.Zelos("my_binary") +z.start(timeout=3) +``` + +## Contributing +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. + +Please make sure to update tests as appropriate. + +### Local Development Environment + +First, create a new python virtual environment. This will ensure no package version conflicts arise: + +```console +$ python3 -m venv ~/.venv/zelos +$ source ~/.venv/zelos/bin/activate +``` + +Now clone the repository and change into the `zelos` directory: + +```console +(zelos) $ git clone git@github.com:zeropointdynamics/zelos.git +(zelos) $ cd zelos +``` + +Install an *editable* version of zelos into the virtual environment. This makes `import zelos` available, and any local changes to zelos will be effective immediately: + +```console +(zelos) $ pip install -e '.[dev]' +``` + +At this point, tests should pass and documentation should build: + +```console +(zelos) $ pytest +(zelos) $ cd docs +(zelos) $ make html +``` + +Built documentation is found in ``docs/_build/html/``. + +Install zelos pre-commit hooks to ensure code style compliance: + +```console +(zelos) $ pre-commit install +``` + +In addition to automatically running every commit, you can run them anytime with: + +```console +(zelos) $ pre-commit run --all-files +``` + +#### Windows Development: + +Commands vary slightly on Windows: + +```console +C:\> python3 -m venv zelos_venv +C:\> zelos_venv\Scripts\activate.bat +(zelos) C:\> pip install -e .[dev] +``` + +## License +[AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) diff --git a/docs/_static/zelos/favicon.ico b/docs/_static/zelos/favicon.ico new file mode 100644 index 0000000..d19759a Binary files /dev/null and b/docs/_static/zelos/favicon.ico differ diff --git a/docs/_static/zelos/logo.png b/docs/_static/zelos/logo.png new file mode 100644 index 0000000..83bc015 Binary files /dev/null and b/docs/_static/zelos/logo.png differ diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..f279390 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,110 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import shutil +import sys + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +import sphinx_rtd_theme + +from recommonmark.transform import AutoStructify + + +sys.path.insert(0, os.path.abspath("../")) + + +shutil.copyfile(os.path.join("..", "README.md"), "README.md") + + +# -- Project information ----------------------------------------------------- + +project = "Zelos" +copyright = "2020, Zeropoint Dynamics" +author = "Zeropoint Dynamics" + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.mathjax", + "sphinx.ext.autodoc", + "sphinx.ext.todo", + # 'sphinx.ext.viewcode', + "sphinx.ext.napoleon", + "recommonmark", + "sphinxcontrib.apidoc", + "sphinx.ext.doctest", + "sphinx.ext.todo", + "sphinx.ext.intersphinx", +] + +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [ + "_build", + "Thumbs.db", + ".DS_Store", + "api/zelos.lib.*", + "api/zelos.regipy.*", + "api/zelos.unicorn.rst", + "api/zelos.lief.rst", + "api/modules.rst", +] + +apidoc_module_dir = "../src/zelos" +apidoc_output_dir = "api" +apidoc_excluded_paths = ["lib", "regipy", "unicorn", "lief"] +apidoc_separate_modules = True + +nitpick_ignore = [ + ("py:class", "Any value"), + ("py:class", "callable"), + ("py:class", "callables"), + ("py:class", "tuple of types"), + ("py:class", "object"), +] + +# -- Options for HTML output ------------------------------------------------- + + +html_theme = "sphinx_rtd_theme" +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +html_logo = "_static/zelos/logo.png" +html_favicon = "_static/zelos/favicon.ico" +autodoc_member_order = "bysource" + + +# Setup AutoStructify +def setup(app): + app.add_config_value( + "recommonmark_config", {"auto_toc_tree_section": "Contents"}, True + ) + app.add_transform(AutoStructify) diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..45a39e5 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,31 @@ +Zelos Documentation +================================= + +.. toctree:: + :maxdepth: 2 + + ./README.md + + +.. toctree:: + :caption: Tutorials + :maxdepth: 1 + + tutorials/01_cmdline + tutorials/02_scripting + tutorials/03_using_hooks + tutorials/04_writing_plugins + tutorials/05_syscall_limit_plugin + + +.. toctree:: + :caption: Script API + :maxdepth: 1 + + api/zelos.api + +.. toctree:: + :caption: Internal Package Docs + :maxdepth: 1 + + api/zelos diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..2119f51 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/tutorials/01_cmdline.md b/docs/tutorials/01_cmdline.md new file mode 100644 index 0000000..413521e --- /dev/null +++ b/docs/tutorials/01_cmdline.md @@ -0,0 +1,72 @@ +# 01 - Command Line Use + +To emulate a binary with default options: + +```console +$ zelos my_binary +``` + +To emulate a binary and view the instructions being executed, add the `-v` flag: +```console +$ zelos -v my_binary +``` + +To print only the *first* time an instruction is executed, rather than *every* instruction, using the `--fasttrace` flag: +```console +$ zelos -v --fasttrace my_binary +``` + +To write output to a file, instead of stdout, use the `--strace` flag: +```console +$ zelos --strace /path/to/file my_binary +``` + +To provide command line arguments to the emulated binary, specify them after the binary name: +```console +$ zelos my_binary arg1 arg2 +``` + +To log various Zelos-related debug information, you can specify log level with flag `--log` and specify one of the options from 'info', 'verbose', 'debug', 'spam', 'notice', 'warning', 'success', 'error', or 'fatal'. The default options is 'info'. +```console +$ zelos --log debug my_binary +``` + +To specify a timeout in seconds, after which emulation will stop, use the flag `-t`: +```console +$ zelos -t 10 my_binary +``` + +To specify a memory limit in mb, after which an exception is thrown an emulation will stop, use the flag `m`: +```console +$ zelos -m 1024 my_binary +``` + +To specify a virtual filename, the name that will be used for the binary during emulation, use the `--virtual-filename` flag: +```console +$ zelos --virtual-filename virtualname my_binary +``` + +To specify a virtual file path, the path that will be used for the binary during emulation, use the `--virtual-path` flag: +```console +$ zelos --virtual-path /home/admin/ my_binary +``` + +To specify environment variables to use during emulation, use the `--env-vars` flag: +```console +$ zelos --env-vars FOO:bar my_binary +``` + +To specify the date in YYYY-MM-DD format, use the `--date` flag. This is primarily used when emulating date-related system calls such as __time__ and __gettimeofday__. +```console +$ zelos --date 2020-03-04 my_binary +``` + +To mount a specified file or path into the emulated filesystem, use the `--mount` flag. The format is `--mount ARCH,DEST,SRC`. `ARCH` is one of `x86`, `x86-64`, `arm`, or `mips`. `DEST` is the emulated path to mount the specified `SRC`. `SRC` is the absolute host path to the file or path to mount. +``` +$ zelos --mount x86,/path/to/dest,/path/to/src my_binary +``` + +To specify a directory to use as the rootfs directory during emulation of a linux system, use `--linux-rootfs` flag. The format is `--linux-rootfs ARCH,PATH`. `ARCH` is one of `x86`, `x86-64`, `arm`, or `mips`. `PATH` is the absolute host path to the directory to be used as rootfs. For example, if you were running Zelos on a linux host machine, and you wanted to use your own root filesystem as the emulated rootfs, you would do the following: +```console +$ zelos --linux-rootfs x86,/ my_binary +``` diff --git a/docs/tutorials/02_scripting.md b/docs/tutorials/02_scripting.md new file mode 100644 index 0000000..baa4cc1 --- /dev/null +++ b/docs/tutorials/02_scripting.md @@ -0,0 +1,426 @@ +# 02 - Scripting with Zelos + +This tutorial demonstrates how Zelos can be used as a library in scripts to +dynamically change behavior at runtime. + + +## Hello Zelos + +Files and scripts from this example are available in the [examples/hello](https://github.com/zeropointdynamics/zelos/tree/master/examples/hello) directory. + + +Consider the following example binary: + +``` +$ ./hello.bin +Hello, Zelos! +``` + +To emulate this binary with Zelos: + +```python +from zelos import Zelos + +z = Zelos("hello.bin") +z.start() +``` + +Which will produce the following output: + +``` +[main] [SYSCALL] brk ( addr=0x0 ) -> 90000038 +[main] [SYSCALL] brk ( addr=0x90001238 ) -> 90001238 +[main] [SYSCALL] arch_prctl ( option=0x1002 (ARCH_SET_FS), addr=0x90000900 ) -> 0 +[main] [SYSCALL] uname ( buf=0xff08eae0 ) -> 0 +[main] [SYSCALL] readlink ( pathname=0x57ee83 ("/proc/self/exe"), buf=0xff08dc10, bufsiz=0x1000 ) -> 31 +[main] [SYSCALL] brk ( addr=0x90022238 ) -> 90022238 +[main] [SYSCALL] brk ( addr=0x90023000 ) -> 90023000 +[main] [SYSCALL] access ( pathname=0x57ea5a ("/etc/ld.so.nohwcap"), mode=0x0 ) -> -1 +[main] [SYSCALL] fstat ( fd=0x1 (stdout), statbuf=0xff08ea50 ) -> 0 +IOCTL: 0 +[main] [SYSCALL] ioctl ( fd=0x1 (stdout), request=0x5401, data=0xff08e9b0 ) -> -1 +[StdOut]: 'bytearray(b'Hello, Zelos!\n')' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x900132d0 ("Hello, Zelos!\n"), count=0xe ) -> e +16:36:17:threads___:SUCCES:Done executing thread main +[main] [SYSCALL] exit_group ( status=0x0 ) -> void +``` + +## Scripting Tutorial - Bypass + +The source code and test program for this tutorial can be found in the [examples/script_bypass](https://github.com/zeropointdynamics/zelos/tree/master/examples/script_bypass) directory. + +Consider the following example binary: + +```sh +$ ./password_check.bin +What's the password? +password +Incorrect + +$ ./password_check.bin +What's the password +0point +Correct! +``` + +The above binary prompts the user for a password from stdin. Upon +entry of the correct password, the program will output "Correct!" to +stdout and exit. Upon entry of an incorrect password, however, the +program will output "Incorrect" to stdout. + +Our objective is to bypass the password check, such that +any password can be entered and the program will always print "Correct!" +to stdout. For this tutorial we will accomplish this in three different ways, +by dynamically writing directly to memory, setting registers, and patching code. + +For each of these, we start with a boilerplate script that loads the binary +and emulates normal behavior: + +```python +from zelos import Zelos + +def main(): + z = Zelos("password_check.bin", verbosity=1) + z.start() + +if __name__ == "__main__": + main() +``` + +We can examine the output of the above script to locate where the string +comparison and subsequent check for equality actually occurs: + + +``` +... +[main] [INS] [004017c0] <_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE10_S_compareEmm> +[main] [INS] [004017c0] push rbp ; push(0xff08ebf0) -> ff08ec70 +[main] [INS] [004017c1] mov rbp, rsp ; rbp = 0xff08eb80 -> ff08ebf0 +[main] [INS] [004017c4] mov qword ptr [rbp - 0x10], rdi ; store(0xff08eb70,0x0) +[main] [INS] [004017c8] mov qword ptr [rbp - 0x18], rsi ; store(0xff08eb68,0x6) +[main] [INS] [004017cc] mov rsi, qword ptr [rbp - 0x10] ; rsi = 0x0 +[main] [INS] [004017d0] sub rsi, qword ptr [rbp - 0x18] ; rsi = 0xfffffffffffffffa +[main] [INS] [004017d4] mov qword ptr [rbp - 0x20], rsi ; store(0xff08eb60,0xfffffffffffffffa) +[main] [INS] [004017d8] cmp qword ptr [rbp - 0x20], 0x7fffffff ; 0xfffffffffffffffa vs 0x7fffffff +[main] [INS] [004017e0] jle 0x4017f2 +[main] [INS] [004017f2] cmp qword ptr [rbp - 0x20], -0x80000000 ; 0xfffffffffffffffa vs 0x-80000000 +[main] [INS] [004017fa] jge 0x40180c +[main] [INS] [0040180c] mov rax, qword ptr [rbp - 0x20] ; rax = 0xfffffffffffffffa +[main] [INS] [00401810] mov ecx, eax ; ecx = 0xfffffffa +[main] [INS] [00401812] mov dword ptr [rbp - 4], ecx ; store(0xff08eb7c,0xfffffffa) +[main] [INS] [00401815] mov eax, dword ptr [rbp - 4] ; eax = 0xfffffffa +[main] [INS] [00401818] pop rbp ; rbp = 0xff08ebf0 -> ff08ec70 +[main] [INS] [00401819] ret +[main] [INS] [004012a7] mov dword ptr [rbp - 0x2c], eax ; store(0xff08ebc4,0xfffffffa) +[main] [INS] [004012aa] mov eax, dword ptr [rbp - 0x2c] ; eax = 0xfffffffa +[main] [INS] [004012ad] add rsp, 0x60 ; rsp = 0xff08ebf0 -> ff08ec70 +[main] [INS] [004012b1] pop rbp ; rbp = 0xff08ec70 -> 49cfa0 +[main] [INS] [004012b2] ret +[main] [INS] [00401079] mov dword ptr [rbp - 0x38], eax ; store(0xff08ec38,0xfffffffa) +[main] [INS] [0040107c] cmp dword ptr [rbp - 0x38], 0 +[main] [INS] [00401080] jne 0x4010d7 +... + +``` + +### Method 1 - Writing Memory + +We can see from the above output that the result of comparison is +initially contained in `eax` before being moved to the memory location +at `[rbp - 0x38]` after the last `ret`. This value in memory is then +used in the subequent `cmp` instruction to determine equality. In the +above output, the `jne` instruction is what determines whether the +program will execute code that prints "Correct!" vs "Incorrect". If the +jump is taken, the program will print "Incorrect". + +To bypass this, we can ensure that this jump is never taken by writing +`0x0` to the memory location that is used ub the `cmp` instruction. + +```python +def patch_mem(): + z = Zelos("password_check.bin", verbosity=1) + # The address cmp instr observed above + target_address = 0x0040107C + # run to the target address and stop + z.plugins.runner.run_to_addr(target_address) + + # Execution is now STOPPED at address 0x0040107C + + # Write 0x0 to address [rbp - 0x38] + z.memory.write_int(z.regs.rbp - 0x38, 0x0) + # resume execution + z.start() + +if __name__ == "__main__": + patch_mem() +``` + +To check our script, we can see that the last four lines of the output are: + +``` +... +[StdOut]: 'bytearray(b'Correct!\n')' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x900132d0 ("Correct!\n"), count=0x9 ) -> 9 +11:32:11:threads___:SUCCES:Done executing thread main +[main] [SYSCALL] exit_group ( status=0x0 ) -> void +``` + +### Method 2 - Setting Registers + +We noted in method 1 that the result of comparison is initially contained in `eax` before being moved to the memory location at `[rbp - 0x38]` after the last `ret`. Therefore, +we can accomplish the same behavior as method 1 by setting `eax` to `0x0` before +it is used. + +```python +def patch_reg(): + z = Zelos("password_check.bin", verbosity=1) + # The address of the first time eax is used above + target_address = 0x00401810 + # run to the target address and stop + z.plugins.runner.run_to_addr(target_address) + # Execution is now STOPPED at address 0x00401810 + + # Set eax to 0x0 + z.eax = 0x0 + # Resume execution + z.start() + +if __name__ == "__main__": + patch_reg() +``` + +Again, to check our script, we can see that the last four lines of the output are: + +``` +... +[StdOut]: 'bytearray(b'Correct!\n')' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x900132d0 ("Correct!\n"), count=0x9 ) -> 9 +12:08:38:threads___:SUCCES:Done executing thread main +[main] [SYSCALL] exit_group ( status=0x0 ) -> void +``` + +### Method 3 - Patching Code + +An alternative approach to methods 1 & 2 is to ensure that the final +jump is never taken by replacing the `cmp` that immediately precedes the +final `jne`. In the following script, this is accomplished by replacing +`cmp dword ptr [rbp - 0x38], 0` with `cmp eax, eax`, which ensures that +the compared values never differ and the jump is never taken. + +We make use of the keystone assembler to encode our replacement code, which +also includes two NOP instructions since we are replacing a 4 byte instruction. + +```python +def patch_code(): + z = Zelos("password_check.bin", verbosity=1) + # The address of the cmp instr + target_address = 0x0040107C + # run to the address of cmp and stop + z.plugins.runner.run_to_addr(target_address) + + # Execution is now STOPPED at address 0x0040107C + + + # Code we want to insert + code = b"NOP; NOP; CMP eax, eax" + # Assemble with keystone + ks = Ks(KS_ARCH_X86, KS_MODE_64) + encoding, count = ks.asm(code) + + # replace the four bytes at this location with our code + for i in range(len(encoding)): + z.memory.write_uint8(target_address + i, encoding[i]) + + # resume execution + z.start() + +if __name__ == "__main__": + patch_code() +``` + +Yet again, to check our script, we can see that the last four lines of the output are: + +``` +... +[StdOut]: 'bytearray(b'Correct!\n')' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x900132d0 ("Correct!\n"), count=0x9 ) -> 9 +12:12:26:threads___:SUCCES:Done executing thread main +[main] [SYSCALL] exit_group ( status=0x0 ) -> void +``` + +## Scripting Tutorial - Brute + +The source code and test program for this tutorial can be found at +https://github.com/zeropointdynamics/zelos/tree/master/examples/simple_brute + +This example demonstrates some more of the dynamic capabilities of zelos. Consider the following example binary: + +```sh +$ ./password.bin +What's the password? +password +Incorrect + +$ ./password.bin +What's the password +0point +Correct! +``` + +The above binary prompts the user for a password from stdin. Upon +entry of the correct password, the program will output "Correct!" to +stdout and exit. Upon entry of an incorrect password, however, the +program will output "Incorrect" to stdout and then sleep for 10 seconds +before exiting. + +Let's say that our objective is to dynamically brute force this password, +but we don't have time to wait 10 seconds between every failure. Our +goal is to focus only on the part of the program that checks user input, + namely the `strcmp` function. + +We start with a script that loads the binary and emulates normal behavior: + +```python +from zelos import Zelos + +def brute(): + z = Zelos("password.bin", verbosity=1) + # Start execution + z.start() + +if __name__ == "__main__": + brute() +``` + +We can examine the output of the above to locate where the `strcmp` +function is invoked. Here we can see the `call` to `strcmp` is invoked at +address `0x00400bb6`. Additionally, the `rsi` and `rdi` registers appear to point to the strings being compared. + + +``` +... +[main] [INS] [00400bac] lea rsi, [rip + 0xab349] ; rsi = 0x4abefc -> "0point" +[main] [INS] [00400bb3] mov rdi, rax ; rdi = 0xff08ec00 -> 0 +[main] [INS] [00400bb6] call 0x4004b0 ; call(0x4004b0) +[main] [INS] [004004b0] jmp qword ptr [rip + 0x2d3be2] ; jmp(0x425df0) +[main] [INS] [00425df0] <__strcmp_ssse3> +[main] [INS] [00425df0] mov ecx, esi ; ecx = 0x4abefc -> "0point" +[main] [INS] [00425df2] mov eax, edi ; eax = 0xff08ec00 -> 0 +... + +``` + +Ignoring for a moment the fact that Zelos annotates pointers with the data at their location, let's modify our script to stop at the address of the call to `strcmp` and save the contents of the `rsi` & `rdi` registers. Let's also take the opportunity +to guess the password by writing a string to the address in `rdi`. + +```python +from zelos import Zelos + + +def brute(): + z = Zelos("password.bin", verbosity=1) + # The address of strcmp observed above + strcmp_address = 0x00400BB6 + # run to the address of call to strcmp and stop + z.plugins.runner.run_to_addr(strcmp_address) + # Execution is now STOPPED at address 0x00400BB6 + + # get initial reg values of rdi & rsi before strcmp is called + rdi = z.regs.rdi # user input + rsi = z.regs.rsi # 'real' password + + # Write the string "our best guess" to address in rdi + z.memory.write_string(rdi, "our best guess") + + # Resume execution + z.start() + +if __name__ == "__main__": + brute() +``` + +At this point, we can inspect the output of the above modified script to +see that we successfully wrote the string "_our best guess_" to memory, +but unfortunately (and unsurprisingly) it was not correct. + +We can see that zelos has annotated register `edi` with the first 8 +characters ("our best") of the string at the address pointed to. We can +also see the stdout output indicating that our guess was incorrect. + +``` +... +[main] [INS] [00425df0] <__strcmp_ssse3> +[main] [INS] [00425df0] mov ecx, esi ; ecx = 0x4abefc -> "0point" +[main] [INS] [00425df2] mov eax, edi ; eax = 0xff08ec00 -> "our best" +... + +[StdOut]: 'bytearray(b"What\'s the password?\nIncorrect\n")' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x90001690 ("What's the password?\nIncorrect\n"), count=0x1f ) -> 1f +... +``` + +Now we are prepared to add the actual 'brute-force' to this script. +For this, we will need to know where the check occurs that causes +behavior to diverge when inputting a correct vs incorrect password. +This appears to occur in a `test` instruction immediately after the +`strcmp` function returns, at address `0x400bbb`. + +``` +... +[main] [INS] [0042702c] sub eax, ecx +[main] [INS] [0042702e] ret +[main] [INS] [00400bbb] test eax, eax +[main] [INS] [00400bbd] jne 0x400bcd +... +``` + +We will 'brute-force' by repeatedly writing our guess to memory, letting execution +run until we reach the above `test` instruction, inspect the flag `zf` set as a result +of the `test`, and reset `IP` & `rsi` & `rdi` back to the call to `strcmp` if `zf` indicates that strings differ. + +```python +from zelos import Zelos + + +def brute(): + z = Zelos("password.bin", verbosity=1) + # The address of strcmp observed above + strcmp_address = 0x00400BB6 + # run to the address of call to strcmp and stop + z.plugins.runner.run_to_addr(strcmp_address) + # Execution is now STOPPED at address 0x00400BB6 + + # get initial reg values of rdi & rsi before strcmp is called + rdi = z.regs.rdi # user input + rsi = z.regs.rsi # 'real' password + + # 'brute force' the correct string + for i in range(9, -1, -1): + + # write our bruteforced guess to memory + z.memory.write_string(rdi, str(i) + "point") + + # Address of the test instr + test_address = 0x00400BBB + # run to the address of test instr and stop + z.plugins.runner.run_to_addr(test_address) + # execute one step, in this case the test instr + z.step() + + # check the zf bit for result of test + flags = z.regs.flags + zf = (flags & 0x40) >> 6 + if zf == 1: + # if correct, run to completion + z.start() + return + + # otherwise, reset ip to strcmp func & set regs + z.regs.setIP(strcmp_address) + z.regs.rdi = rdi + z.regs.rsi = rsi + + +if __name__ == "__main__": + brute() +``` diff --git a/docs/tutorials/03_using_hooks.md b/docs/tutorials/03_using_hooks.md new file mode 100644 index 0000000..531df86 --- /dev/null +++ b/docs/tutorials/03_using_hooks.md @@ -0,0 +1,310 @@ +# 03 - Using Hooks + +This tutorial demonstrates how hooks in the Zelos API can be used to identify strings that are copied during runtime without symbol information. + +Files and scripts from this tutorial are available in the [examples/inmemory_strings](https://github.com/zeropointdynamics/zelos/blob/master/examples/inmemory_strings) directory. + +## Hook Overview + +Hooks are a way to invoke your code whenever certain events occur during execution. To hook on: +```eval_rst +* Memory reads and writes use :py:meth:`~zelos.Zelos.hook_memory` +* Invocations of syscalls use :py:meth:`~zelos.Zelos.hook_syscalls` +* Execution of an instruction :py:meth:`~zelos.Zelos.hook_execution` + +Each hook offers different configuration options and requires a different type of callback. For more details, as well as examples, for each type of hook, look at the :py:class:`~zelos.Zelos`. +``` + +## Pwnable.kr Challenge + +The [flag challenge on pwnable.kr](http://pwnable.kr/play.php) provides a binary that we need to extract a flag from. We can start off by just running the binary in zelos using `zelos pwnablekr_flag_binary`. +This will produce the output +``` +[main] [SYSCALL] mmap ( addr=0x800000, length=0x2d295e, prot=0x7, flags=0x32, fd=0x0 (stdin), offset=0x0 ) -> 800000 +[main] [SYSCALL] readlink ( pathname=0x84a78d ("/proc/self/exe"), buf=0xff08deb4, bufsiz=0x1000 ) -> 12 +[main] [SYSCALL] mmap ( addr=0x400000, length=0x2c7000, prot=0x0, flags=0x32, fd=0xffffffff (unknown), offset=0x0 ) -> 400000 +[main] [SYSCALL] mmap ( addr=0x400000, length=0xc115e, prot=0x7, flags=0x32, fd=0xffffffff (unknown), offset=0x0 ) -> 400000 +[main] [SYSCALL] mprotect ( addr=0x400000, len=0xc115e, prot=0x5 ) -> 0 +[main] [SYSCALL] mmap ( addr=0x6c1000, length=0x26f0, prot=0x3, flags=0x32, fd=0xffffffff (unknown), offset=0xc1000 ) -> 6c1000 +[main] [SYSCALL] mprotect ( addr=0x6c1000, len=0x26f0, prot=0x3 ) -> 0 +[main] [SYSCALL] mmap ( addr=0x6c4000, length=0x22d8, prot=0x3, flags=0x32, fd=0xffffffff (unknown), offset=0x0 ) -> 6c4000 +[main] [SYSCALL] munmap ( addr=0x801000, length=0x2d195e ) -> 0 +[main] [SYSCALL] uname ( buf=0xff08dab0 ) -> 0 +[main] [SYSCALL] brk ( addr=0x0 ) -> 90000048 +[main] [SYSCALL] brk ( addr=0x90001208 ) -> 90001208 +[main] [SYSCALL] arch_prctl ( option=0x1002 (ARCH_SET_FS), addr=0x90000900 ) -> 0 +[main] [SYSCALL] brk ( addr=0x90022208 ) -> 90022208 +[main] [SYSCALL] brk ( addr=0x90023000 ) -> 90023000 +[main] [SYSCALL] fstat ( fd=0x1 (stdout), statbuf=0xff08db40 ) -> 0 +[main] [SYSCALL] ioctl ( fd=0x1 (stdout), request=0x5401, data=0xff08dab8 ) -> -1 +[main] [SYSCALL] mmap ( addr=0x0, length=0x1000, prot=0x3, flags=0x22, fd=0xffffffff (unknown), offset=0x0 ) -> 10000 +[StdOut]: 'bytearray(b'I will malloc() and strcpy the flag there. take it.\n')' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x10000 ("I will malloc() and strcpy the flag there. take it.\n"), count=0x34 ) -> 34 +00:45:32:threads___:SUCCES:Done executing thread main +[main] [SYSCALL] exit_group ( status=0x0 ) -> void +``` + +Immediately, we see the line: + +``` +[StdOut]: 'bytearray(b'I will malloc() and strcpy the flag there. take it.\n')' +``` + +An initial approach may be to dump all of the strings that are present in the binary using the `strings` utility, unfortunately the is packed with [UPX](https://en.wikipedia.org/wiki/UPX). Seems like we'll have to run the binary and find strings while the binary is running... + +## Script to Print In-Memory String Writes +To identify the flag, we will create a script that will print all the times strings are written. + +To begin with, let's create a script that will run the target binary similar to how we ran it using the Zelos command line tool. + +```python +from zelos import Zelos + +z = Zelos("pwnablekr_flag_binary") +z.start() +``` + +```eval_rst +Next, let's print out every write to memory that occurs. Use the :py:meth:`~zelos.Zelos.hook_memory` to register the hook and specify the :py:const:`zelos.HookType.MEMORY.WRITE` hook type. +``` + +``` python +from zelos import Zelos, HookType + +z = Zelos("pwnablekr_flag_binary") + +def mem_hook_callback(zelos: Zelos, access: int, address: int, size: int, value: int): + "Prints the destination and contents of every memory write." + print(f"Address: {address:x}, Value: {value:x}") + +z.hook_memory(HookType.MEMORY.WRITE, mem_hook_callback) + +z.start() +``` +```eval_rst +The function signature used by :code:`mem_hook_callback` is required by :py:meth:`~zelos.Zelos.hook_memory`. You can find the required callback function signature in the documentation for the hook registration functions in :py:class:`~zelos.Zelos`. +Unfortunately this script will print out a lot of garbage. What we want is a very specific subset of these writes, and to print them in a way that we can easily understand. We'll make some basic assumptions on how strings are written to memory via strcpy. +``` + + 1. A single string is written from beginning to end with no memory writes to other locations inbetween. + 2. The bytes that are written make up a valid utf-8 string. + +Let's write a class that can keep track of subsequent writes and decodes strings as they are written. + +```python +class StringCollector: + def __init__(self): + self._current_string = "" + self._next_addr = 0 + + def collect_writes(self, zelos: zelos, access: int, address: int, size: int, value: int): + # Pack converts the value into its representation in bytes. + data = zelos.memory.pack(value) + try: + decoded_data = data.decode("utf-8") + except UnicodeDecodeError: + self._next_addr = 0 + self._end_current_string() + return + decoded_data = decoded_data[:size] + + if address != self._next_addr: + self._end_current_string() + + self._next_addr = address + size + self._current_string += decoded_data + return + + def _end_current_string(self): + print(f'Found string: "{self._current_string}"') + self._current_string = "" +``` +```eval_rst +Let's put this class to use. Note that we kept the method signature for :code:`collect_writes` similar to :code:`mem_hook_callback` from before. This allows us to use it as the callback for :py:meth:`~zelos.Zelos.hook_memory` +``` + +```python +from zelos import Zelos, HookType + +class StringCollector: + ... + +z = Zelos("example_binary") + +sc = StringCollector() +z.hook_memory(HookType.MEMORY.WRITE, sc.collect_writes) + +z.start() +``` +Running this script, we see the following input +``` +Found string: "4" +Found string: "" +Found string: "" +Found string: "4" +Found string: "" +Found string: "" +Found string: "" +[StdOut]: 'bytearray(b'I will malloc() and strcpy the flag there. take it.\n')' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x10000 ("I will malloc() and strcpy the flag there. take it.\n"), count=0x34 ) -> 34 +Found string: "" +Found string: "" +Found string: "" +Found string: "" +Found string: "" +Found string: "" + +``` + +There is still a lot of random looking data being printed. Let's clean up the results a bit by making two more assumptions. + +1. A string can only contain a null byte at the end. +2. We're only interested in strings with 4 or more characters (similar to the `strings` utility) + +Our new and improved `StringCollector` looks like this now + +```python +class StringCollector: + def __init__(self): + self._min_len = 4 + self._current_string = "" + self._next_addr = 0 + + def collect_writes(self, zelos, access, address, size, value): + data = zelos.memory.pack(value) + try: + decoded_data = data.decode("utf-8") + except UnicodeDecodeError: + self._next_addr = 0 + self._end_current_string() + return + decoded_data = decoded_data[:size] + + first_null_byte = decoded_data.find("\x00") + if first_null_byte != -1: + decoded_data = decoded_data[:first_null_byte] + self._current_string += decoded_data + self._next_addr = 0 + self._end_current_string() + return + + if address != self._next_addr: + self._end_current_string() + + self._next_addr = address + size + self._current_string += decoded_data + return + + def _end_current_string(self) -> None: + if len(self._current_string) >= self._min_len: + print(f'Found string: "{self._current_string}"') + self._current_string = "" + +``` + +Running this script still prints out quite a bit due to the aforementioned obfuscation, however near the end you should see the target string printed out! + +``` +[main] [SYSCALL] ioctl ( fd=0x1 (stdout), request=0x5401, data=0xff08dab8 ) -> -1 +[main] [SYSCALL] mmap ( addr=0x0, length=0x1000, prot=0x3, flags=0x22, fd=0xffffffff (unknown), offset=0x0 ) -> 10000 +Found string: "I will malloc() and strcpy the flag there. take it.3" +Found string: "UPX...? sounds like a delivery service :)" +[StdOut]: 'bytearray(b'I will malloc() and strcpy the flag there. take it.\n')' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x10000 ("I will malloc() and strcpy the flag there. take it.\n"), count=0x34 ) -> 34 +01:36:32:threads___:SUCCES:Done executing thread main +[main] [SYSCALL] exit_group ( status=0x0 ) -> void +``` + +Our script still needs some work, since there are many nonsensical characters printed out and we accidentally added a byte onto the string that got printed to stdout. However, we didn't have to worry about UPX! (We'll deal with it in a later tutorial.) + +The following example script showing how to collect in-memory strings can be found at [examples/inmemory_strings/strings_script.py](https://github.com/zeropointdynamics/zelos/blob/master/examples/inmemory_strings/strings_script.py). + +```python +import argparse + +from zelos import Zelos, HookType + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--len", + type=int, + default=4, + help="The minimum size of string to identify", + ) + parser.add_argument("filename", type=str, help="The file to analyze") + + args = parser.parse_args() + + z = Zelos(args.filename) + sc = StringCollector(args.len) + + z.hook_memory( + HookType.MEMORY.WRITE, sc.collect_writes, name="strings_syscall_hook" + ) + z.start() + + +class StringCollector: + """ + Identifies strings that are written in-memory. We identify strings by the + observation that when they are written to memory + * The string is written in sequential chunks. + * They are comprised of valid utf-8 bytes + + This runs into some false positives with data that happens to be + valid utf-8. To reduce false positives we observe that + * Strings often end at the first null byte. + * False positives are often short strings. There is a higher + chance that 2 consecutive characters are valid utf-8 than + 4 consecutive characters. + + """ + + def __init__(self, min_len): + self._min_len = min_len + self._current_string = "" + self._next_addr = 0 + + def collect_writes(self, zelos, access, address, size, value): + """ + Collects strings that are written to memory. Intended to be used + as a callback in a Zelos HookType.MEMORY hook. + """ + data = zelos.memory.pack(value) + try: + decoded_data = data.decode() + except UnicodeDecodeError: + self._next_addr = 0 + self._end_current_string() + return + decoded_data = decoded_data[:size] + + first_null_byte = decoded_data.find("\x00") + if first_null_byte != -1: + decoded_data = decoded_data[:first_null_byte] + self._current_string += decoded_data + self._next_addr = 0 + self._end_current_string() + return + + if address != self._next_addr: + self._end_current_string() + + self._next_addr = address + size + self._current_string += decoded_data + return + + def _end_current_string(self) -> None: + """ + Ends the currently identified string. May save the string if it + looks legit enough. + """ + if len(self._current_string) >= self._min_len: + print(f'Found string: "{self._current_string}"') + self._current_string = "" + +if __name__ == "__main__": + main() + +``` diff --git a/docs/tutorials/04_writing_plugins.md b/docs/tutorials/04_writing_plugins.md new file mode 100644 index 0000000..7650767 --- /dev/null +++ b/docs/tutorials/04_writing_plugins.md @@ -0,0 +1,145 @@ +# 04 - Creating Plugins + +This tutorial demonstrates how the in-memory string finding script from the previous tutorial can be adapted to a plugin. + +Files and scripts from this tutorial are available in the [examples/inmemory_strings](https://github.com/zeropointdynamics/zelos/blob/master/examples/inmemory_strings) directory. + +## Plugin Overview + +Plugins are ways that make sharing additional functionalities for Zelos even easier. Plugins can be used to + * Modify how Zelos executes + * Provide additional output from zelos + * Extend Zelos's capabilities + +In order for Zelos to find plugins, the python module containing the plugin must be located in a path specified by the `ZELOS_PLUGIN_DIR` environment variable. + +## Building a Minimal Plugin +```eval_rst +Zelos identifies plugins as objects that subclass the :py:class:`zelos.IPlugin` class. +``` +```python +from zelos import IPlugin + +class MinimalPlugin(IPlugin): + pass +``` + +If we include this in a file `/home/kevin/zelos_plugins/MinimalPlugin.py`, let's just set our environment up appropriately before running zelos with our plugin! + +``` +$ ZELOS_PLUGIN_DIR=$ZELOS_PLUGIN_DIR,`/home/kevin/zelos_plugins` +$ zelos target_binary +Plugins: runner, minimalplugin +... +``` +```eval_rst +Unfortunately, our plugin doesn't do much at the moment. We can add some functionality, but first we should have a way to turn our plugin on and off from the command line. This prevents plugins from running costly operations or printing extraneous output when they aren't being used. The easiest way to do this is by specifying a :py:class:`zelos.CommandLineOption` to add flags to the zelos command line tool. The arguments for creating a :py:class:`zelos.CommandLineOption` are identical to the python :code:`argparse` library's `add_argument() `_ function. + +The ideal time to activate the plugin is when the plugin is initialized by Zelos through the :code:`__init__` function. You can add your own initialization code by creating an :code:`__init__` which takes :py:class:`zelos.Zelos` as an input. Remember to begin with a call to the parent :code:`__init__` function. +``` + +```python +from zelos import IPlugin, Zelos + +class MinimalPlugin(IPlugin): + def __init__(self, z:Zelos): + super.__init__(z) + print("Minimal plugin is created.") +``` +```eval_rst +Now, we add the :py:class:`zelos.CommandLineOption` to change behavior at run time. The option can then be accessed using :py:class`zelos.Zelos`'s :code:`config` field. +``` + +```python +from zelos import IPlugin, CommandLineOption + +CommandLineOption('activate_minimal_plugin', action='store_true') + +class MinimalPlugin(IPlugin): + def __init__(self, z): + super.__init__(z) + print("Minimal plugin is created.") + if z.config.activate_minimal_plugin: + print("Minimal plugin has been activated!") +``` + +Now we can change the behavior of zelos using our `MinimalPlugin`! + +``` +$ zelos target_binary +Minimal plugin is created. +... +$ zelos --activate_minimal_plugin target_binary +Minimal plugin is created. +Minimal plugin has been activated! +... +``` +Now to do something a bit more complicated. + +## Creating the In-Memory Strings Plugin. +The script from [the previous tutorial](03_using_hooks.md) can be converted into a plugin so that we can easily use it in the future. + +The following plugin showing how to collect in-memory strings can be found at [examples/inmemory_strings/strings_plugin.py](https://github.com/zeropointdynamics/zelos/blob/master/examples/inmemory_strings/strings_plugin.py). To invoke the plugin, run `zelos --print_strings 4 target_binary`. + +```python +from zelos import CommandLineOption, Zelos, HookType, IPlugin + +CommandLineOption( + "print_strings", + type=int, + default=None, + help="The minimum size of string to identify", +) + +class StringCollectorPlugin(IPlugin): + def __init__(self, z: Zelos): + super().__init__(z) + if z.config.print_strings: + z.hook_memory( + HookType.MEMORY.WRITE, + self.collect_writes, + name="strings_syscall_hook", + ) + self._min_len = z.config.print_strings + self._current_string = "" + self._next_addr = 0 + + def collect_writes(self, zelos, access, address, size, value): + """ + Collects strings that are written to memory. Intended to be used + as a callback in a Zelos HookType.MEMORY hook. + """ + data = zelos.memory.pack(value) + try: + decoded_data = data.decode() + except UnicodeDecodeError: + self._next_addr = 0 + self._end_current_string() + return + decoded_data = decoded_data[:size] + + first_null_byte = decoded_data.find("\x00") + if first_null_byte != -1: + decoded_data = decoded_data[:first_null_byte] + self._current_string += decoded_data + self._next_addr = 0 + self._end_current_string() + return + + if address != self._next_addr: + self._end_current_string() + + self._next_addr = address + size + self._current_string += decoded_data + return + + def _end_current_string(self) -> None: + """ + Ends the currently identified string. May save the string if it + looks legit enough. + """ + if len(self._current_string) >= self._min_len: + print(f'Found string: "{self._current_string}"') + self._current_string = "" + +``` diff --git a/docs/tutorials/05_syscall_limit_plugin.md b/docs/tutorials/05_syscall_limit_plugin.md new file mode 100644 index 0000000..254bdce --- /dev/null +++ b/docs/tutorials/05_syscall_limit_plugin.md @@ -0,0 +1,236 @@ +# 05 - Syscall Limiter Plugin + +This tutorial explains how the syscall-limiter plugin was written and how it works. + +The source code for this plugin can be fond in [src/zelos/ext/plugins/syscall_limiter.py](https://github.com/zeropointdynamics/zelos/blob/master/src/zelos/ext/plugins/syscall_limiter.py). + +## Overview + +The Syscall Limiter Plugin provides the following additional functionalities for Zelos: + * Stop Zelos emulation after a specified number of syscalls have been executed across all threads. + * Stop a thread after specified number of syscalls have been executed on a thread. + * Swap threads after a specified number of syscalls have been executed on a thread. + +## Create the Command Line Options + +```eval_rst +As mentioned in the previous tutorial, we create three :py:class:`zelos.CommandLineOption` to be able to specify the number of syscalls we want to limit overall, the number of syscalls per thread we want to limit, and the number of syscalls before swapping at run time. +``` + +```python +from zelos import CommandLineOption + +CommandLineOption( + "syscall_limit", + type=int, + default=0, +) + +CommandLineOption( + "syscall_thread_limit", + type=int, + default=0, +) + +CommandLineOption( + "syscall_thread_swap", + type=int, + default=100, +) +``` + +## Initializing the Plugin + +```eval_rst +We create the plugin by creating a class that subclasses :py:class:`zelos.IPlugin`. We initialize by invoking the superclass init function through :code:`super()__init__(z)` in the SyscallLimiter's :code:`__init__` function. +``` + +```python +from zelos import CommandLineOption, IPlugin + +CommandLineOption( + "syscall_limit", + type=int, + default=0, +) + +CommandLineOption( + "syscall_thread_limit", + type=int, + default=0, +) + +CommandLineOption( + "syscall_thread_swap", + type=int, + default=100, +) + +class SyscallLimiter(IPlugin): + + def __init__(self, z): + super().__init__(z) + pass + +``` + +## Implementing the Syscall Hook + +```eval_rst +In order to implement the desired behavior of SyscallLimiter, we create a syscall hook using the :py:meth:`~zelos.Zelos.hook_syscalls` function. As noted in the previous tutorial, we can access our command line options through :py:class`zelos.Zelos`'s :code:`config` field. Additionally, we create a callback function that keeps track of the number of syscalls executed overall and per thread. +``` + +```python +from collections import defaultdict +from zelos import CommandLineOption, IPlugin, HookType + +CommandLineOption( + "syscall_limit", + type=int, + default=0, +) + +CommandLineOption( + "syscall_thread_limit", + type=int, + default=0, +) + +CommandLineOption( + "syscall_thread_swap", + type=int, + default=100, +) + +class SyscallLimiter(IPlugin): + + def __init__(self, z): + super().__init__(z) + # If we specify any of the above commandline options, + # then create a syscall hook + if ( + z.config.syscall_limit > 0 + or z.config.syscall_thread_limit > 0 + or z.config.syscall_thread_swap > 0 + ): + self.zelos.hook_syscalls( + HookType.SYSCALL.AFTER, self._syscall_callback + ) + # Fields to keep track of syscalls executed + self.syscall_cnt = 0 + self.syscall_thread_cnt = defaultdict(int) + + def _syscall_callback(self, p, sysname, args, retval): + if self.zelos.thread is None: + return + # Get the name of the current thread + thread_name = self.zelos.thread.name + + self.syscall_cnt += 1 + self.syscall_thread_cnt[thread_name] += 1 + +``` + +## Limiting Syscalls Overall + +```eval_rst +To stop after a specified number of syscalls have been executed, we use the :py:meth:`~zelos.Zelos.hook_syscalls` function. +``` + +```python + def _syscall_callback(self, p, sysname, args, retval): + if self.zelos.thread is None: + return + # Get the name of the current thread + thread_name = self.zelos.thread.name + + self.syscall_cnt += 1 + self.syscall_thread_cnt[thread_name] += 1 + + # End execution if syscall limit reached + if ( + self.zelos.config.syscall_limit > 0 + and self.syscall_cnt >= self.zelos.config.syscall_limit + ): + self.zelos.stop("syscall limit") + return + +``` + +## Limiting Syscalls Per Thread + +```eval_rst +To stop & complete a thread after specified number of syscalls have been executed on it, we use the :py:meth:`~zelos.Zelos.end_thread` function. +``` + +```python + def _syscall_callback(self, p, sysname, args, retval): + if self.zelos.thread is None: + return + # Get the name of the current thread + thread_name = self.zelos.thread.name + + self.syscall_cnt += 1 + self.syscall_thread_cnt[thread_name] += 1 + + # End execution if syscall limit reached + if ( + self.zelos.config.syscall_limit > 0 + and self.syscall_cnt >= self.zelos.config.syscall_limit + ): + self.zelos.stop("syscall limit") + return + + # End thread if syscall thread limit reached + if ( + self.zelos.config.syscall_thread_limit != 0 + and self.syscall_thread_cnt[thread_name] + % self.zelos.config.syscall_thread_limit + == 0 + ): + self.zelos.end_thread() + return +``` + +## Swapping Threads + +```eval_rst +To force a thread swap to occur after specified number of syscalls have been executed on it, we use the :py:meth:`~zelos.Zelos.swap_thread` function. +``` + +```python + def _syscall_callback(self, p, sysname, args, retval): + if self.zelos.thread is None: + return + # Get the name of the current thread + thread_name = self.zelos.thread.name + + self.syscall_cnt += 1 + self.syscall_thread_cnt[thread_name] += 1 + + # End execution if syscall limit reached + if ( + self.zelos.config.syscall_limit > 0 + and self.syscall_cnt >= self.zelos.config.syscall_limit + ): + self.zelos.stop("syscall limit") + return + + # End thread if syscall thread limit reached + if ( + self.zelos.config.syscall_thread_limit != 0 + and self.syscall_thread_cnt[thread_name] + % self.zelos.config.syscall_thread_limit + == 0 + ): + self.zelos.end_thread() + return + + # Swap threads if syscall thread swap limit reached + if ( + self.zelos.config.syscall_thread_swap > 0 + and self.syscall_cnt % self.zelos.config.syscall_thread_swap == 0 + ): + self.zelos.swap_thread("syscall limit thread swap") + return +``` diff --git a/examples/hello/README.md b/examples/hello/README.md new file mode 100644 index 0000000..a011d64 --- /dev/null +++ b/examples/hello/README.md @@ -0,0 +1,33 @@ +## Hello Zelos + +The sources for this example can be found at +https://github.com/zeropointdynamics/zelos/tree/master/examples/hello + +To emulate a binary with Zelos: + +```python +from zelos import Zelos + +z = Zelos("hello.bin") +z.start() +``` + +Which produces the following output + +``` +[main] [SYSCALL] brk ( addr=0x0 ) -> 90000038 +[main] [SYSCALL] brk ( addr=0x90001238 ) -> 90001238 +[main] [SYSCALL] arch_prctl ( option=0x1002 (ARCH_SET_FS), addr=0x90000900 ) -> 0 +[main] [SYSCALL] uname ( buf=0xff08eae0 ) -> 0 +[main] [SYSCALL] readlink ( pathname=0x57ee83 ("/proc/self/exe"), buf=0xff08dc10, bufsiz=0x1000 ) -> 31 +[main] [SYSCALL] brk ( addr=0x90022238 ) -> 90022238 +[main] [SYSCALL] brk ( addr=0x90023000 ) -> 90023000 +[main] [SYSCALL] access ( pathname=0x57ea5a ("/etc/ld.so.nohwcap"), mode=0x0 ) -> -1 +[main] [SYSCALL] fstat ( fd=0x1 (stdout), statbuf=0xff08ea50 ) -> 0 +IOCTL: 0 +[main] [SYSCALL] ioctl ( fd=0x1 (stdout), request=0x5401, data=0xff08e9b0 ) -> -1 +[StdOut]: 'bytearray(b'Hello, Zelos!\n')' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x900132d0 ("Hello, Zelos!\n"), count=0xe ) -> e +16:36:17:threads___:SUCCES:Done executing thread main +[main] [SYSCALL] exit_group ( status=0x0 ) -> void +``` diff --git a/examples/hello/hello.bin b/examples/hello/hello.bin new file mode 100644 index 0000000..eaca4eb Binary files /dev/null and b/examples/hello/hello.bin differ diff --git a/examples/hello/hello.py b/examples/hello/hello.py new file mode 100644 index 0000000..5b6e0d3 --- /dev/null +++ b/examples/hello/hello.py @@ -0,0 +1,29 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +from os import path + +from zelos import Zelos + + +DATA_DIR = path.dirname(path.abspath(__file__)) + + +# Initialize Zelos +z = Zelos(path.join(DATA_DIR, "hello.bin")) +# Start Execution +z.start() diff --git a/examples/inmemory_strings/pwnablekr_flag b/examples/inmemory_strings/pwnablekr_flag new file mode 100644 index 0000000..bfe60ff Binary files /dev/null and b/examples/inmemory_strings/pwnablekr_flag differ diff --git a/examples/inmemory_strings/strings_plugin.py b/examples/inmemory_strings/strings_plugin.py new file mode 100644 index 0000000..8a10d65 --- /dev/null +++ b/examples/inmemory_strings/strings_plugin.py @@ -0,0 +1,90 @@ +from zelos import CommandLineOption, HookType, IPlugin, Zelos + + +""" +# tl;dr + +This is a copy of the strings_script.py file, except written as a Zelos +plugin. In order to include this plugin, you must either + + * copy this file into the zelos/ext/plugins folder + * specify the containing folder in the ZELOS_PLUGIN_DIR environment + variable + + +""" + +CommandLineOption( + "print_strings", + type=int, + default=None, + help="The minimum size of string to identify", +) + + +class StringCollectorPlugin(IPlugin): + NAME = "strings" + """ + Identifies strings that are written in-memory. We identify strings by the + observation that when they are written to memory + * They are comprised of valid utf-8 bytes + * The string is written in sequential chunks. + + This runs into some false positives with data that happens to be + valid utf-8. To reduce false positives we observe that + * Strings often end at the first null byte. + * False positives are often short strings. There is a higher + chance that 2 consecutive characters are valid utf-8 than + 4 consecutive characters. + + """ + + def __init__(self, z: Zelos): + super().__init__(z) + if z.config.print_strings: + z.hook_memory( + HookType.MEMORY.WRITE, + self.collect_writes, + name="strings_syscall_hook", + ) + self._min_len = z.config.print_strings + self._current_string = "" + self._next_addr = 0 + + def collect_writes(self, zelos, access, address, size, value): + """ + Collects strings that are written to memory. Intended to be used + as a callback in a Zelos HookType.MEMORY hook. + """ + data = zelos.memory.pack(value) + try: + decoded_data = data.decode() + except UnicodeDecodeError: + self._next_addr = 0 + self._end_current_string() + return + decoded_data = decoded_data[:size] + + first_null_byte = decoded_data.find("\x00") + if first_null_byte != -1: + decoded_data = decoded_data[:first_null_byte] + self._current_string += decoded_data + self._next_addr = 0 + self._end_current_string() + return + + if address != self._next_addr: + self._end_current_string() + + self._next_addr = address + size + self._current_string += decoded_data + return + + def _end_current_string(self) -> None: + """ + Ends the currently identified string. May save the string if it + looks legit enough. + """ + if len(self._current_string) >= self._min_len: + print(f'Found string: "{self._current_string}"') + self._current_string = "" diff --git a/examples/inmemory_strings/strings_script.py b/examples/inmemory_strings/strings_script.py new file mode 100644 index 0000000..1d958ea --- /dev/null +++ b/examples/inmemory_strings/strings_script.py @@ -0,0 +1,116 @@ +import argparse + +from typing import List + +from zelos import HookType, Zelos + + +""" +# tl;dr +# http://pwnable.kr has a binary that contains a hidden string that is +# only written in memory. To identify the flag, run +# `python inmemory_strings.py ` + +This example is intended as a potential solution for the +pwnable.kr challenge "flag". This challenge contains a binary that +"strcpy"s the flag into "malloc"ed memory, and our goal is to find that +flag. + +The original binary is packed using UPX (which can be identified through +the "strings" utility), but the unpacked binary contains symbols which +indicate the location of the flag. This script identifies the target +string without requiring the use of symbols. In addition, this script +can be used to identify in-memory strings of other binaries as well. + +When this script is run on the packed binary, you will notice a lot of +strings being printed during unpacking. These could be filtered out, +however, we know the valid string by when the string in "strcpy"ed. +""" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--len", + type=int, + default=4, + help="The minimum size of string to identify", + ) + parser.add_argument("filename", type=str, help="The file to analyze") + + args = parser.parse_args() + + z = Zelos(args.filename) + sc = StringCollector(args.len) + + z.hook_memory( + HookType.MEMORY.WRITE, sc.collect_writes, name="strings_syscall_hook" + ) + z.start() + + +class StringCollector: + """ + Identifies strings that are written in-memory. We identify strings + by the observation that when they are written to memory + * The string is written in sequential chunks. + * They are comprised of valid utf-8 bytes + + This runs into some false positives with data that happens to be + valid utf-8. To reduce false positives we observe that + * Strings often end at the first null byte. + * False positives are often short strings. There is a higher + chance that 2 consecutive characters are valid utf-8 than + 4 consecutive characters. + + """ + + def __init__(self, min_len): + self.strings_found: List[str] = [] + + self._min_len = min_len + self._current_string = "" + self._next_addr = 0 + + def collect_writes(self, zelos, access, address, size, value): + """ + Collects strings that are written to memory. Intended to be used + as a callback in a Zelos HookType.MEMORY hook. + """ + data = zelos.memory.pack(value) + try: + decoded_data = data.decode() + except UnicodeDecodeError: + self._next_addr = 0 + self._end_current_string() + return + decoded_data = decoded_data[:size] + + first_null_byte = decoded_data.find("\x00") + if first_null_byte != -1: + decoded_data = decoded_data[:first_null_byte] + self._current_string += decoded_data + self._next_addr = 0 + self._end_current_string() + return + + if address != self._next_addr: + self._end_current_string() + + self._next_addr = address + size + self._current_string += decoded_data + return + + def _end_current_string(self) -> None: + """ + Ends the currently identified string. May save the string if it + looks legit enough. + """ + if len(self._current_string) >= self._min_len: + print(f'Found string: "{self._current_string}"') + self.strings_found.append(self._current_string) + self._current_string = "" + + +if __name__ == "__main__": + main() diff --git a/examples/script_brute/README.md b/examples/script_brute/README.md new file mode 100644 index 0000000..0a195cd --- /dev/null +++ b/examples/script_brute/README.md @@ -0,0 +1,187 @@ +## Simple Brute Tutorial + +The source code and test program for this tutorial can be found at +https://github.com/zeropointdynamics/zelos/tree/master/examples/script_brute + +This example demonstrates some of the dynamic capabilities of zelos. Consider the following example binary: + +```sh +$ ./password.bin +What's the password? +password +Incorrect + +$ ./password.bin +What's the password +0point +Correct! +``` + +The above binary prompts the user for a password from stdin. Upon +entry of the correct password, the program will output "Correct!" to +stdout and exit. Upon entry of an incorrect password, however, the +program will output "Incorrect" to stdout and then sleep for 10 seconds +before exiting. + +Let's say that our objective is to dynamically brute force this password, +but we don't have time to wait 10 seconds between every failure. Our +goal is to focus only on the part of the program that checks user input, + namely the `strcmp` function. + +We start with a script that loads the binary and emulates normal behavior: + +```python +from zelos import Zelos + + +def brute(): + z = Zelos("password.bin") + z.set_verbose(True) + # Start execution + z.start() + + +if __name__ == "__main__": + brute() +``` + +We can examine the output of the above to locate where the `strcmp` +function is invoked. Here we can see the `call` to `strcmp` is invoked at +address `0x00400bb6`. Additionally, the `rsi` and `rdi` registers appear to point to the strings being compared. + + +``` +... +[main] [INS] [00400bac] lea rsi, [rip + 0xab349] ; rsi = 0x4abefc -> "0point" +[main] [INS] [00400bb3] mov rdi, rax ; rdi = 0xff08ec00 -> 0 +[main] [INS] [00400bb6] call 0x4004b0 ; call(0x4004b0) +[main] [INS] [004004b0] jmp qword ptr [rip + 0x2d3be2] ; jmp(0x425df0) +[main] [INS] [00425df0] <__strcmp_ssse3> +[main] [INS] [00425df0] mov ecx, esi ; ecx = 0x4abefc -> "0point" +[main] [INS] [00425df2] mov eax, edi ; eax = 0xff08ec00 -> 0 +... + +``` + +Ignoring for a moment the fact that Zelos annotates pointers with the data at their location, let's modify our script to stop at the address of the call to `strcmp` and save the contents of the `rsi` & `rdi` registers. Let's also take the opportunity +to guess the password by writing a string to the address in `rdi`. + +```python +from zelos import Zelos + + +def brute(): + z = Zelos("password.bin") + z.set_verbose(True) + # The address of strcmp observed above + strcmp_address = 0x00400BB6 + # run to the address of call to strcmp and stop + z.plugins.runner.run_to_addr(strcmp_address) + + # Execution is now STOPPED at address 0x00400BB6 + + # Get the current process + p = z.current_process + # get initial reg values of rdi & rsi before strcmp is called + rdi = p.emu.get_reg("rdi") # user input + rsi = p.emu.get_reg("rsi") # 'real' password + + # Write the string "our best guess" to address in rdi + p.memory.write_string(rdi, "our best guess") + + # Resume execution + z.start() + +if __name__ == "__main__": + brute() +``` + +At this point, we can inspect the output of the above modified script to +see that we successfully wrote the string "_our best guess_" to memory, +but unfortunately (and unsurprisingly) it was not correct. + +We can see that zelos has annotated register `edi` with the first 8 +characters ("our best") of the string at the address pointed to. We can +also see the stdout output indicating that our guess was incorrect. + +``` +... +[main] [INS] [00425df0] <__strcmp_ssse3> +[main] [INS] [00425df0] mov ecx, esi ; ecx = 0x4abefc -> "0point" +[main] [INS] [00425df2] mov eax, edi ; eax = 0xff08ec00 -> "our best" +... + +[StdOut]: 'bytearray(b"What\'s the password?\nIncorrect\n")' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x90001690 ("What's the password?\nIncorrect\n"), count=0x1f ) -> 1f +... +``` + +Now we are prepared to add the actual 'brute-force' to this script. +For this, we will need to know where the check occurs that causes +behavior to diverge when inputting a correct vs incorrect password. +This appears to occur in a `test` instruction immediately after the +`strcmp` function returns, at address `0x400bbb`. + +``` +... +[main] [INS] [0042702c] sub eax, ecx +[main] [INS] [0042702e] ret +[main] [INS] [00400bbb] test eax, eax +[main] [INS] [00400bbd] jne 0x400bcd +... +``` + +We will 'brute-force' by repeatedly writing our guess to memory, letting execution +run until we reach the above `test` instruction, inspect the flag `zf` set as a result +of the `test`, and reset `IP` & `rsi` & `rdi` back to the call to `strcmp` if `zf` indicates that strings differ. + +```python +from zelos import Zelos + + +def brute(): + z = Zelos("password.bin") + z.set_verbose(True) + # The address of strcmp observed above + strcmp_address = 0x00400BB6 + # run to the address of call to strcmp and stop + z.plugins.runner.run_to_addr(strcmp_address) + + # Execution is now STOPPED at address 0x00400BB6 + + # Get the current process + p = z.current_process + # get initial reg values of rdi & rsi before strcmp is called + rdi = p.current_thread.get_reg("rdi") # user input + rsi = p.current_thread.get_reg("rsi") # 'real' password + + # 'brute force' the correct string + for i in range(9, -1, -1): + + # write our bruteforced guess to memory + p.memory.write_string(rdi, str(i) + "point") + + # Address of the test instr + test_address = 0x00400BBB + # run to the address of test instr and stop + z.plugins.runner.run_to_addr(test_address) + # execute one step, in this case the test instr + z.step() + + # check the zf bit for result of test + flags = p.current_thread.get_reg("flags") + zf = (flags & 0x40) >> 6 + if zf == 1: + # if correct, run to completion + z.start() + return + + # otherwise, reset ip to strcmp func & set regs + p.current_thread.setIP(strcmp_address) + p.current_thread.set_reg("rdi", rdi) + p.current_thread.set_reg("rsi", rsi) + + +if __name__ == "__main__": + brute() +``` diff --git a/examples/script_brute/brute.py b/examples/script_brute/brute.py new file mode 100644 index 0000000..e94b876 --- /dev/null +++ b/examples/script_brute/brute.py @@ -0,0 +1,67 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +from os import path + +from zelos import Zelos + + +DATA_DIR = path.dirname(path.abspath(__file__)) + + +def brute(): + z = Zelos(path.join(DATA_DIR, "password.bin"), verbosity=1) + # The address of strcmp observed above + strcmp_address = 0x00400BB6 + # run to the address of call to strcmp and stop + z.plugins.runner.run_to_addr(strcmp_address) + + # Execution is now STOPPED at address 0x00400BB6 + + # get initial reg values of rdi & rsi before strcmp is called + rdi = z.regs.rdi # user input + rsi = z.regs.rsi # 'real' password + + # 'brute force' the correct string + for i in range(9, -1, -1): + + # write our bruteforced guess to memory + z.memory.write_string(rdi, str(i) + "point") + + # Address of the test instr + test_address = 0x00400BBB + # run to the address of test instr and stop + z.plugins.runner.run_to_addr(test_address) + # execute one step, in this case the test instr + z.step() + + # check the zf bit for result of test + flags = z.regs.flags + zf = (flags & 0x40) >> 6 + if zf == 1: + # if correct, run to completion + z.start() + return + + # otherwise, reset ip to strcmp func & set regs + z.regs.setIP(strcmp_address) + z.regs.rdi = rdi + z.regs.rsi = rsi + + +if __name__ == "__main__": + brute() diff --git a/examples/script_brute/password.bin b/examples/script_brute/password.bin new file mode 100644 index 0000000..cc05884 Binary files /dev/null and b/examples/script_brute/password.bin differ diff --git a/examples/script_bypass/README.md b/examples/script_bypass/README.md new file mode 100644 index 0000000..b2c4c40 --- /dev/null +++ b/examples/script_bypass/README.md @@ -0,0 +1,219 @@ +## Scripting Tutorial - Bypass + +The source code for this tutorial can be found at +https://github.com/zeropointdynamics/zelos/tree/master/examples/script_bypass + +Consider the following example binary: + +```sh +$ ./password_check.bin +What's the password? +password +Incorrect + +$ ./password_check.bin +What's the password +0point +Correct! +``` + +The above binary prompts the user for a password from stdin. Upon +entry of the correct password, the program will output "Correct!" to +stdout and exit. Upon entry of an incorrect password, however, the +program will output "Incorrect" to stdout. + +Let's say that our objective is to bypass the password check, such that +any password can be entered and the program will always print "Correct!" +to stdout. For this tutorial we will accomplish this in three different ways, +by dynamically writing directly to memory, setting registers, and patching code. + +For each of these, we start with a script that loads the binary +and emulates normal behavior: + +```python +from zelos import Zelos + + +def main(): + z = Zelos("password_check.bin") + z.set_verbose(True) + # Start execution + z.start() + + +if __name__ == "__main__": + main() +``` + +We can examine the output of the above script to locate where the string +comparison and subsequent check for equality actually occurs: + + +``` +... +[main] [INS] [004017c0] <_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE10_S_compareEmm> +[main] [INS] [004017c0] push rbp ; push(0xff08ebf0) -> ff08ec70 +[main] [INS] [004017c1] mov rbp, rsp ; rbp = 0xff08eb80 -> ff08ebf0 +[main] [INS] [004017c4] mov qword ptr [rbp - 0x10], rdi ; store(0xff08eb70,0x0) +[main] [INS] [004017c8] mov qword ptr [rbp - 0x18], rsi ; store(0xff08eb68,0x6) +[main] [INS] [004017cc] mov rsi, qword ptr [rbp - 0x10] ; rsi = 0x0 +[main] [INS] [004017d0] sub rsi, qword ptr [rbp - 0x18] ; rsi = 0xfffffffffffffffa +[main] [INS] [004017d4] mov qword ptr [rbp - 0x20], rsi ; store(0xff08eb60,0xfffffffffffffffa) +[main] [INS] [004017d8] cmp qword ptr [rbp - 0x20], 0x7fffffff ; 0xfffffffffffffffa vs 0x7fffffff +[main] [INS] [004017e0] jle 0x4017f2 +[main] [INS] [004017f2] cmp qword ptr [rbp - 0x20], -0x80000000 ; 0xfffffffffffffffa vs 0x-80000000 +[main] [INS] [004017fa] jge 0x40180c +[main] [INS] [0040180c] mov rax, qword ptr [rbp - 0x20] ; rax = 0xfffffffffffffffa +[main] [INS] [00401810] mov ecx, eax ; ecx = 0xfffffffa +[main] [INS] [00401812] mov dword ptr [rbp - 4], ecx ; store(0xff08eb7c,0xfffffffa) +[main] [INS] [00401815] mov eax, dword ptr [rbp - 4] ; eax = 0xfffffffa +[main] [INS] [00401818] pop rbp ; rbp = 0xff08ebf0 -> ff08ec70 +[main] [INS] [00401819] ret +[main] [INS] [004012a7] mov dword ptr [rbp - 0x2c], eax ; store(0xff08ebc4,0xfffffffa) +[main] [INS] [004012aa] mov eax, dword ptr [rbp - 0x2c] ; eax = 0xfffffffa +[main] [INS] [004012ad] add rsp, 0x60 ; rsp = 0xff08ebf0 -> ff08ec70 +[main] [INS] [004012b1] pop rbp ; rbp = 0xff08ec70 -> 49cfa0 +[main] [INS] [004012b2] ret +[main] [INS] [00401079] mov dword ptr [rbp - 0x38], eax ; store(0xff08ec38,0xfffffffa) +[main] [INS] [0040107c] cmp dword ptr [rbp - 0x38], 0 +[main] [INS] [00401080] jne 0x4010d7 +... + +``` + +### Method 1 - Writing Memory + +We can see from the above output that the result of comparison is +initially contained in `eax` before being moved to the memory location +at `[rbp - 0x38]` after the last `ret`. This value in memory is then +used in the subequent `cmp` instruction to determine equality. In the +above output, the `jne` instruction is what determines whether the +program will execute code that prints "Correct!" vs "Incorrect". If the +jump is taken, the program will print "Incorrect". + +To bypass this, we can ensure that this jump is never taken by writing +`0x0` to the memory location that is used ub the `cmp` instruction. + +```python +def patch_mem(): + z = Zelos("password_check.bin") + # z.set_verbose(True) + # The address cmp instr observed above + target_address = 0x0040107C + # run to the target address and stop + z.plugins.runner.run_to_addr(target_address) + + # Execution is now STOPPED at address 0x0040107C + + # Get the current process + p = z.current_process + # Get the value of rbp + rbp = p.emu.get_reg("rbp") + # Write 0x0 to address [rbp - 0x38] + p.memory.write_int(rbp - 0x38, 0x0) + # resume execution + z.start() + +if __name__ == "__main__": + patch_mem() +``` + +To check our script, we can see that the last four lines of the output are: + +``` +... +[StdOut]: 'bytearray(b'Correct!\n')' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x900132d0 ("Correct!\n"), count=0x9 ) -> 9 +11:32:11:threads___:SUCCES:Done executing thread main +[main] [SYSCALL] exit_group ( status=0x0 ) -> void +``` + +### Method 2 - Setting Registers + +We noted in method 1 that the result of comparison is initially contained in `eax` before being moved to the memory location at `[rbp - 0x38]` after the last `ret`. Therefore, +we can accomplish the same behavior as method 1 by setting `eax` to `0x0` before +it is used. + +```python +def patch_reg(): + z = Zelos("password_check.bin") + # z.set_verbose(True) + # The address of the first time eax is used above + target_address = 0x00401810 + # run to the target address and stop + z.plugins.runner.run_to_addr(target_address) + + # Execution is now STOPPED at address 0x00401810 + + # Get the current process + p = z.current_process + # Set eax to 0x0 + p.current_thread.set_reg("eax", 0x0) + # Resume execution + z.start() + +if __name__ == "__main__": + patch_reg() +``` + +Again, to check our script, we can see that the last four lines of the output are: + +``` +... +[StdOut]: 'bytearray(b'Correct!\n')' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x900132d0 ("Correct!\n"), count=0x9 ) -> 9 +12:08:38:threads___:SUCCES:Done executing thread main +[main] [SYSCALL] exit_group ( status=0x0 ) -> void +``` + +### Method 3 - Patching Code + +An alternative approach to methods 1 & 2 is to ensure that the final +jump is never taken by replacing the `cmp` that immediately precedes the +final `jne`. In the following script, this is accomplished by replacing +`cmp dword ptr [rbp - 0x38], 0` with `cmp eax, eax`, which ensures that +the compared values never differ and the jump is never taken. + +We make use of the keystone assembler to encode our replacement code, which +also includes two NOP instructions since we are replacing a 4 byte instruction. + +```python +def patch_code(): + z = Zelos("password_check.bin") + # z.set_verbose(True) + # The address of the cmp instr + target_address = 0x0040107C + # run to the address of cmp and stop + z.plugins.runner.run_to_addr(target_address) + + # Execution is now STOPPED at address 0x0040107C + + # Get the current process + p = z.current_process + + # Code we want to insert + code = b"NOP; NOP; CMP eax, eax" + # Assemble with keystone + ks = Ks(KS_ARCH_X86, KS_MODE_64) + encoding, count = ks.asm(code) + + # replace the four bytes at this location with our code + for i in range(len(encoding)): + p.memory.write_uint8(target_address + i, encoding[i]) + + # resume execution + z.start() + +if __name__ == "__main__": + patch_code() +``` + +Yet again, to check our script, we can see that the last four lines of the output are: + +``` +... +[StdOut]: 'bytearray(b'Correct!\n')' +[main] [SYSCALL] write ( fd=0x1 (stdout), buf=0x900132d0 ("Correct!\n"), count=0x9 ) -> 9 +12:12:26:threads___:SUCCES:Done executing thread main +[main] [SYSCALL] exit_group ( status=0x0 ) -> void +``` diff --git a/examples/script_bypass/bypass.py b/examples/script_bypass/bypass.py new file mode 100644 index 0000000..5b51828 --- /dev/null +++ b/examples/script_bypass/bypass.py @@ -0,0 +1,93 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import sys + +from os import path + +from zelos import Zelos + + +DATA_DIR = path.dirname(path.abspath(__file__)) + + +def patch_mem(): + z = Zelos(path.join(DATA_DIR, "password_check.bin")) + # The address of the cmp instr + target_address = 0x0040107C + # run to the address of cmp and stop + z.internal_engine.plugins.runner.run_to_addr(target_address) + + # Execution is now STOPPED at address 0x0040107C + + # Write 0x0 to address [rbp - 0x38] + z.memory.write_int(z.regs.rbp - 0x38, 0x0) + # resume execution + z.start() + + +def patch_reg(): + z = Zelos(path.join(DATA_DIR, "password_check.bin")) + # The address of the first time eax is used above + target_address = 0x00401810 + # run to the target address and stop + z.internal_engine.plugins.runner.run_to_addr(target_address) + + # Execution is now STOPPED at address 0x00401810 + + # Set eax to 0x0 + z.regs.eax = 0x0 + # Resume execution + z.start() + + +def patch_code(): + from keystone import KS_ARCH_X86, KS_MODE_64, Ks + + z = Zelos(path.join(DATA_DIR, "password_check.bin")) + # The address of the cmp instr + target_address = 0x0040107C + # run to the address of cmp and stop + z.internal_engine.plugins.runner.run_to_addr(target_address) + + # Execution is now STOPPED at address 0x0040107C + + # Code we want to insert + code = b"NOP; NOP; CMP eax, eax" + # Assemble with keystone + ks = Ks(KS_ARCH_X86, KS_MODE_64) + encoding, count = ks.asm(code) + + # replace the four bytes at this location with our code + for i in range(len(encoding)): + z.memory.write_uint8(target_address + i, encoding[i]) + + # resume execution + z.start() + + +if __name__ == "__main__": + fn = "mem" + if len(sys.argv) > 1: + if sys.argv[1] in ["mem", "reg", "code"]: + fn = sys.argv[1] + if fn == "mem": + patch_mem() + elif fn == "reg": + patch_reg() + else: + patch_code() diff --git a/examples/script_bypass/password_check.bin b/examples/script_bypass/password_check.bin new file mode 100644 index 0000000..5c1cf2b Binary files /dev/null and b/examples/script_bypass/password_check.bin differ diff --git a/examples/test_examples.py b/examples/test_examples.py new file mode 100644 index 0000000..805d599 --- /dev/null +++ b/examples/test_examples.py @@ -0,0 +1,105 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import subprocess +import unittest + +from os import path + + +# from zelos.api.zelos_api import ZelosCmdline + + +DATA_DIR = path.dirname(path.abspath(__file__)) + + +class ExamplesTest(unittest.TestCase): + # def test_inmemory_strings_script(self): + # stdout = subprocess.check_output( + # [ + # "python", + # path.join(DATA_DIR, "inmemory_strings", "strings_script.py"), + # path.join(DATA_DIR, "inmemory_strings", "pwnablekr_flag"), + # ] + # ) + + # self.assertIn(b"UPX...? sounds like a delivery service :)", stdout) + + # def test_inmemory_strings_plugin(self): + # os.environ["ZELOS_PLUGIN_DIR"] = path.join( + # DATA_DIR, "inmemory_strings" + # ) + # filepath = path.join(DATA_DIR, "inmemory_strings", "pwnablekr_flag") + + # # sys.stdout = printed_output + # z = ZelosCmdline(f"--print_strings 4 {filepath}") + # z.start() + + # # This test doesn't work on windows. + # # self.assertIn( + # # "UPX...? sounds like a delivery service :)", + # # printed_output.getvalue(), + # # ) + + def test_hello(self): + output = subprocess.check_output( + ["python", path.join(DATA_DIR, "hello", "hello.py")] + ) + self.assertTrue("Hello, Zelos!" in str(output)) + + def test_brute(self): + output = subprocess.check_output( + ["python", path.join(DATA_DIR, "script_brute", "brute.py")] + ) + self.assertTrue("Correct!" in str(output)) + + def test_bypass_mem(self): + output = subprocess.check_output( + [ + "python", + path.join(DATA_DIR, "script_bypass", "bypass.py"), + "mem", + ] + ) + self.assertTrue("Correct!" in str(output)) + + def test_bypass_reg(self): + output = subprocess.check_output( + [ + "python", + path.join(DATA_DIR, "script_bypass", "bypass.py"), + "reg", + ] + ) + self.assertTrue("Correct!" in str(output)) + + # def test_bypass_code(self): + # output = subprocess.check_output( + # [ + # "python", + # path.join(DATA_DIR, "script_bypass", "bypass.py"), + # "code", + # ] + # ) + # self.assertTrue("Correct!" in str(output)) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9cd0d75 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=40.6.0", "wheel"] +build-backend = "setuptools.build_meta" + + +[tool.black] +line-length = 79 +target-version = ['py36', 'py37', 'py38'] + +[tool.isort] +atomic=true +force_grid_wrap=0 +include_trailing_comma=true +lines_after_imports=2 +lines_between_types=1 +multi_line_output=3 +not_skip="__init__.py" +use_parentheses=true + +known_first_party="zelos" +known_third_party=["capstone", "colorama", "configargparse", "dnslib", "hypothesis", "lief", "pypacker", "recommonmark", "setuptools", "sortedcontainers", "sphinx_rtd_theme", "termcolor", "unicorn", "verboselogs", "zelos"] + +[tool.pytest] +junit_family = "xunit2" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9cd7a1a --- /dev/null +++ b/setup.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Note: To use the 'upload' functionality of this file, you must: +# $ pipenv install twine --dev + +import codecs +import os +import re +import sys + +from shutil import rmtree + +from setuptools import Command, find_packages, setup + + +NAME = "zelos" +PACKAGES = find_packages(where="src") +META_PATH = os.path.join("src", "zelos", "__init__.py") +KEYWORDS = ["emulation", "dynamic analysis", "binary analysis"] +PROJECT_URLS = { + "Documentation": "https://zelos.zeropointdynamics.com/", + "Bug Tracker": "https://github.com/zeropointdynamics/zelos/issues", + "Source Code": "https://github.com/zeropointdynamics/zelos", +} +CLASSIFIERS = [ + "Development Status :: 1 - Planning", + "Natural Language :: English", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", +] +INSTALL_REQUIRES = [ + "wheel", + "colorama==0.4.1", + "termcolor==1.1.0", + "capstone==4.0.1", + "sortedcontainers==2.1.0", + "verboselogs==1.7", + "dnslib==0.9.10", + "hexdump==3.3", + "dpkt==1.9.2", + "coloredlogs==10.0", + "configargparse==0.15.1", + "pypacker==4.9", + "lief>=0.9.0", + "unicorn==1.0.2rc1", +] +EXTRAS_REQUIRE = { + "docs": [ + "sphinx", + "sphinx_rtd_theme", + "sphinxcontrib-apidoc", + "recommonmark", + ], + "tests": [ + "coverage", + "hypothesis", + "pympler", + "pytest>=4.3.0", + "pytest-xdist", + ], +} +EXTRAS_REQUIRE["dev"] = ( + EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] + ["pre-commit", "tox"] +) +EXTRAS_REQUIRE["azure-pipelines"] = EXTRAS_REQUIRE["tests"] + [ + "pytest-azurepipelines" +] + +######################################################################## + +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def read(*parts): + """ + Build an absolute path from *parts* and and return the contents of + the resulting file. Assume UTF-8 encoding. + """ + with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: + return f.read() + + +META_FILE = read(META_PATH) + + +def find_meta(meta): + """ + Extract __*meta*__ from META_FILE. + """ + meta_match = re.search( + r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), META_FILE, re.M + ) + if meta_match: + return meta_match.group(1) + raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) + + +VERSION = find_meta("version") +URL = find_meta("url") +LONG = ( + read("README.md") + + "\n\n" + + read("CHANGELOG.md") + + "\n\n" + + read("AUTHORS.md") +) + + +class UploadCommand(Command): + """Support setup.py upload.""" + + description = "Build and publish the package." + user_options = [] + + @staticmethod + def status(s): + """Prints things in bold.""" + print("\033[1m{0}\033[0m".format(s)) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + try: + self.status("Removing previous builds…") + rmtree(os.path.join(HERE, "dist")) + except OSError: + pass + + self.status("Building Source and Wheel (universal) distribution…") + os.system( + "{0} setup.py sdist bdist_wheel --universal".format(sys.executable) + ) + + self.status("Uploading the package to PyPI via Twine…") + os.system("twine upload dist/*") + + self.status("Pushing git tags…") + os.system("git tag v{0}".format(VERSION)) + os.system("git push --tags") + + sys.exit() + + +if __name__ == "__main__": + setup( + name=NAME, + description=find_meta("description"), + license=find_meta("license"), + url=URL, + project_urls=PROJECT_URLS, + version=VERSION, + author=find_meta("author"), + author_email=find_meta("email"), + maintainer=find_meta("author"), + maintainer_email=find_meta("email"), + keywords=KEYWORDS, + long_description=LONG, + long_description_content_type="text/markdown", + packages=PACKAGES, + package_dir={"": "src"}, + python_requires=">=3.6.0", + zip_safe=False, + classifiers=CLASSIFIERS, + install_requires=INSTALL_REQUIRES, + extras_require=EXTRAS_REQUIRE, + include_package_data=True, + options={"bdist_wheel": {"universal": "1"}}, + setup_requires=["wheel"], + cmdclass={"upload": UploadCommand}, + entry_points={"console_scripts": [f"{NAME} = {NAME}.__main__:main"]}, + ) diff --git a/src/zelos/__init__.py b/src/zelos/__init__.py new file mode 100644 index 0000000..b1f3172 --- /dev/null +++ b/src/zelos/__init__.py @@ -0,0 +1,81 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +__version__ = "0.0.0.dev0" + +__title__ = "zelos" +__description__ = "A comprehensive binary emulation platform." +__url__ = "https://github.com/zeropointdynamics/zelos" +__uri__ = __url__ +__doc__ = __description__ + " <" + __uri__ + ">" + +__author__ = "Zeropoint Dynamics" +__email__ = "zelos@zeropointdynamics.com" + +__license__ = "AGPLv3" +__copyright__ = "Copyright (c) 2019 Zeropoint Dynamics" + +import os +import sys + +import colorama + +from .api.zelos_api import Zelos +from .engine import Engine +from .exceptions import ( + InvalidHookTypeException, + InvalidRegException, + OutOfMemoryException, + UnsupportedBinaryError, + ZelosException, + ZelosLoadException, + ZelosRuntimeException, +) +from .hooks import HookType +from .memory import ProtType +from .plugin import CommandLineOption, IPlugin, ISubcommand + + +__all__ = [ + "Zelos", + "Engine", + "ZelosException", + "ZelosLoadException", + "ZelosRuntimeException", + "InvalidRegException", + "InvalidHookTypeException", + "UnsupportedBinaryError", + "OutOfMemoryException", + "IPlugin", + "ISubcommand", + "CommandLineOption", + "HookType", + "ProtType", +] + +""" Initialize colorama only once """ +colorama.init() + +# FIXME for OSS release +private_path = os.path.abspath( + os.path.join( + os.path.dirname(os.path.abspath(__file__)), + os.pardir, + os.pardir, + os.pardir, + ) +) +sys.path.insert(0, private_path) diff --git a/src/zelos/__main__.py b/src/zelos/__main__.py new file mode 100644 index 0000000..5c9b6cf --- /dev/null +++ b/src/zelos/__main__.py @@ -0,0 +1,32 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + + +def main(): + import sys + from zelos.api.zelos_api import ZelosCmdline + + z = ZelosCmdline(sys.argv[1:]) + + try: + z.start(z.config.timeout) + finally: + z.internal_engine.close() + + +if __name__ == "__main__": + main() diff --git a/src/zelos/api/__init__.py b/src/zelos/api/__init__.py new file mode 100644 index 0000000..170e319 --- /dev/null +++ b/src/zelos/api/__init__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +from .zelos_api import Zelos + + +__all__ = ["Zelos"] diff --git a/src/zelos/api/memory_api.py b/src/zelos/api/memory_api.py new file mode 100644 index 0000000..b5458bd --- /dev/null +++ b/src/zelos/api/memory_api.py @@ -0,0 +1,381 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import ctypes + +from typing import Optional + +from zelos.enums import ProtType + + +class MemoryApi: + def __init__(self, zelos): + self._zelos = zelos + + @property + def _memory(self): + return self._zelos.internal_engine.memory + + def read(self, addr: int, size: int) -> bytearray: + """ + Copies specified region of memory. Requires that the specified + address is mapped. + + Args: + addr: Address to start reading from. + size: Number of bytes to read. + + Returns: + Bytes corresponding to data held in memory. + """ + return self._memory.read(addr, size) + + def write(self, addr: int, data: bytearray) -> None: + """ + Writes specified bytes to memory. Requires that the specified + address is mapped. + + Args: + addr: Address to start writing data to. + data: Bytes to write in memory. + """ + return self._memory.write(addr, bytes(data)) + + def read_int( + self, addr: int, size: int = None, signed: bool = False + ) -> int: + """ + Reads an integer value from the specified address. Can handle + multiple sizes and representations of integers. + + Args: + addr: Address to begin reading int from. + size: Size (# of bytes) of integer representation. If None, + uses the architecture to determine size (32 bit -> 4, + 64bit -> 8). Default is None. + signed: If true, interpret bytes as signed integer. Default + false. + + Returns: + Integer represntation of bytes read. + """ + return self._memory.read_int(addr, size, signed) + + def write_int( + self, addr: int, value: int, size: int = None, signed: bool = False + ) -> int: + """ + Writes an integer value to the specified address. Can handle + multiple sizes and representations of integers. + + Args: + addr: Address in memory to write integer to. + value: Integer to write into memory. + size: Size (# of bytes) to write into memory. If None, + uses the architecture to determine size (32 bit -> 4, + 64bit -> 8). Default is None. + signed: If true, write number as signed integer. Default + false. + + Returns: + Number of bytes written to memory. + """ + return self._memory.write_int(addr, value, size, signed) + + def read_string(self, addr: int, size: int = 1024) -> str: + """ + Reads a utf-8 string from memory. Stops at null terminator. + Fails if a byte is uninterpretable. + + Args: + addr: Address in memory to start reading from. + size: Maximum size of string to read from memory. + + Returns: + String read from memory. + """ + return self._memory.read_string(addr, size) + + def read_wstring(self, addr: int, size: int = 1024) -> str: + """ + Reads a utf-16 string from memory. Stops at null terminator. + Fails if a byte is uninterpretable. + + Args: + addr: Address in memory to start reading from. + size: Maximum size of string to read from memory. + + Returns: + String read from memory. + """ + return self._memory.write_wstring(addr, size) + + def read_punicode_string(self, addr: int) -> str: + """ + Given the address of a pointer to a unicode string, returns the + string that was pointed to as a python string. + + Args: + addr: Address of the unicode string pointer + + Returns: + String read from memory + """ + return self._memory.get_punicode_string(addr) + + def read_pansi_string(self, addr: int) -> int: + """ + Given the address of a pointer to a ANSI string, returns the + string that was pointed to as a python string. + + Args: + addr: Address of the ANSI string pointer + + Returns: + String read from memory + """ + return self._memory.get_pansi_string(addr) + + def write_string( + self, addr: int, value: str, terminal_null_byte: bool = True + ) -> int: + """ + Writes a string to a specified address as utf-8. By default, + adds a terminal null byte. + + Args: + addr: Address in memory to begin writing string to. + value: String to write to memory. + terminal_null_byte: If True, adds terminal null byte. + Default True. + + Returns: + Number of bytes written. + """ + return self._memory.write_string(addr, value, terminal_null_byte) + + def write_wstring( + self, addr: int, value: int, terminal_null_byte: bool = True + ) -> int: + """ + Writes a string to a specified address as utf-16-le. By default, + adds a terminal null byte. + + Args: + addr: Address in memory to begin writing string to. + value: String to write to memory. + terminal_null_byte: If True, adds terminal null byte. + Default True. + + Returns: + Number of bytes written. + """ + return self._memory.write_wstring(addr, value, terminal_null_byte) + + def readstruct(self, addr: int, obj: ctypes.Structure) -> ctypes.Structure: + """ + Reads a ctypes structure from memory. + + Args: + addr: Address in memory to begin reading structure from. + obj: An instance of the structure to create from memory. + + Returns: + Instance of structure read from memory. + + Example: + .. code-block:: python + + class SIGACTION(ctypes.Structure): + _fields_ = [ + ("sa_handler", ctypes.c_uint64), + ("sa_flags", ctypes.c_uint64), + ("sa_restorer", ctypes.c_uint64), + ("sa_mask", ctypes.c_uint64), + ] + + # Pointer to sigaction struct + pointer = 0xdeadbeef + + sigaction = api.memory.readstruct(pointer, SIGACTION()) + print(sigaction.sa_handler) + """ + return self._memory.readstruct(addr, obj) + + def writestruct(self, address: int, structure: ctypes.Structure) -> int: + """ + Write a ctypes Structure to memory. + + Args: + addr: Address in memory to begin writing to. + structure: An instance of the structure to write to memory. + + Returns: + Number of bytes written to memory. + + Example: + .. code-block:: python + + class SIGACTION(ctypes.Structure): + _fields_ = [ + ("sa_handler", ctypes.c_uint64), + ("sa_flags", ctypes.c_uint64), + ("sa_restorer", ctypes.c_uint64), + ("sa_mask", ctypes.c_uint64), + ] + + sigaction = SIGACTION() + sigaction.handler = 0xdeadbeef + + # Memory address to write struct to + destination = 0xb0bad00d + + bytes_written = api.memory.writestruct(destination, sigaction) + """ + return self._memory.writestruct(address, structure) + + def map( + self, + address: int, + size: int, + name: str = "", + kind: str = "", + module_name: str = "", + prot: int = ProtType.RWX, + ptr: Optional[ctypes.POINTER] = None, + reserve: bool = False, + ) -> None: + """ + Maps a region of memory at the specified address. + + Args: + address: Address to map. + size: # of bytes to map. This will be rounded up to the + nearest 0x1000. + name: String used to identify mapped region. Used for + debugging. + kind: String used to identify the purpose of the mapped + region. Used for debugging. + module_name: String used to identify the module that mapped + this region. + prot: An integer representing the RWX protection to be set + on the mapped region. + ptr: If specified, creates a memory map from the pointer. + reserve: Reserves memory to prepare for mapping. An option + used in Windows. + + """ + return self._memory.map( + address, size, name, kind, module_name, prot, ptr, reserve + ) + + def read_ptr(self, addr: int) -> int: + """ + Reads a pointer at `addr`. The number of bytes read + is dependent on the architecture of the binary. + """ + return self._memory.read_ptr(addr) + + def read_size_t(self, addr: int) -> int: + """ + Reads a value of type `size_t` at `addr`. + """ + return self._memory.read_size_t(addr) + + def pack( + self, + x: int, + bytes: int = None, + little_endian: bool = None, + signed: bool = False, + ) -> bytes: + """ + Unpacks an integer from a byte format. Defaults to the + current architecture bytes and endianness. + """ + return self._memory.pack( + x, bytes=bytes, little_endian=little_endian, signed=signed + ) + + def unpack( + self, + x: bytes, + bytes: int = None, + little_endian: bool = None, + signed: bool = False, + ) -> int: + """ + Unpacks an integer from a byte format. Defaults to the + current architecture bytes and endianness. + """ + return self._memory.unpack( + x, bytes=bytes, little_endian=little_endian, signed=signed + ) + + def read_int64(self, addr: int) -> int: + return self._memory.read_int64(addr) + + def read_uint64(self, addr: int) -> int: + return self._memory.read_uint64(addr) + + def read_int32(self, addr: int) -> int: + return self._memory.read_int32(addr) + + def read_uint32(self, addr: int) -> int: + return self._memory.read_uint32(addr) + + def read_int16(self, addr: int) -> int: + return self._memory.read_int16(addr) + + def read_uint16(self, addr: int) -> int: + return self._memory.read_uint16(addr) + + def read_int8(self, addr: int) -> int: + return self._memory.read_int8(addr) + + def read_uint8(self, addr: int) -> int: + return self._memory.read_uint8(addr) + + def write_ptr(self, addr: int, value: int) -> int: + return self._memory.write_ptr(addr, value) + + def write_size_t(self, addr: int, value: int) -> int: + return self._memory.write_size_t(addr, value) + + def write_int64(self, addr: int, value: int) -> int: + return self._memory.write_int64(addr, value) + + def write_uint64(self, addr: int, value: int) -> int: + return self._memory.write_uint64(addr, value) + + def write_int32(self, addr: int, value: int) -> int: + return self._memory.write_int32(addr, value) + + def write_uint32(self, addr: int, value: int) -> int: + return self._memory.write_uint32(addr, value) + + def write_int16(self, addr: int, value: int) -> int: + return self._memory.write_int16(addr, value) + + def write_uint16(self, addr: int, value: int) -> int: + return self._memory.write_uint16(addr, value) + + def write_int8(self, addr: int, value: int) -> int: + return self._memory.write_int8(addr, value) + + def write_uint8(self, addr: int, value: int) -> int: + return self._memory.write_uint8(addr, value) diff --git a/src/zelos/api/regs_api.py b/src/zelos/api/regs_api.py new file mode 100644 index 0000000..2053e73 --- /dev/null +++ b/src/zelos/api/regs_api.py @@ -0,0 +1,93 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + + +class RegsApi: + """ + Allows accessing registers directly by their name. + + Methods also exist for accessing registers that hold the + instruction, stack, and frame pointers in a platform agnostic way, + as well as functions for manipulating the stack. + + .. code-block:: python + + from zelos import Zelos, HookType + + # 32 bit x86 binary + z = Zelos("binary_to_emulate") + + # Increment the starting address by 2 + z.regs.eip = z.regs.eip + 2 + + # A platform agnostic way of adjusting the Instruction Pointer + z.regs.setIP(z.regs.getIP() + 2) + + """ + + def __init__(self, zelos): + # Called on super() object to avoid triggering the __setattr__ + # on the RegsApi class. + super().__setattr__("_zelos", zelos) + + @property + def _current_thread(self): + return self._zelos.internal_engine.current_thread + + def __getattr__(self, attr): + return self._current_thread.get_reg(attr) + + def __setattr__(self, attr, value): + self._current_thread.set_reg(attr, value) + + def getIP(self) -> int: + return self._current_thread.getIP() + + def setIP(self, new_ip: int) -> None: + self._current_thread.setIP(new_ip) + + def getSP(self) -> int: + return self._current_thread.getSP() + + def setSP(self, new_sp: int) -> None: + self._current_thread.setSP(new_sp) + + def getFP(self) -> int: + return self._current_thread.getFP() + + def setFP(self, new_fp: int) -> None: + return self._current_thread.setFP(new_fp) + + def getstack(self, offset: int) -> int: + """ + Returns data that is `offset * word_size` bytes from the top of + the stack. + """ + return self._current_thread.getstack(offset) + + def setstack(self, offset: int, val: int) -> None: + """ + Sets data that is `offset * word_size` bytes from the top of + the stack. + """ + self._current_thread.setstack(offset, val) + + def popstack(self) -> int: + return self._current_thread.popstack() + + def pushstack(self, data: int) -> None: + return self._current_thread.pushstack(data) diff --git a/src/zelos/api/zelos_api.py b/src/zelos/api/zelos_api.py new file mode 100644 index 0000000..fd1a0be --- /dev/null +++ b/src/zelos/api/zelos_api.py @@ -0,0 +1,569 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +from collections import defaultdict +from typing import Any, Callable, Optional + +from zelos.api.memory_api import MemoryApi +from zelos.api.regs_api import RegsApi +from zelos.config_gen import generate_config, generate_config_from_cmdline +from zelos.engine import Engine +from zelos.hooks import HookInfo, HookType +from zelos.plugin import Plugins + + +class Zelos: + """ API class that provides access to interal api wrappers. """ + + def __init__(self, filename, *cmdline_args, **flags): + config = generate_config(filename, *cmdline_args, **flags) + self._setup(config) + + def _setup(self, config): + self.config = config + self.regs = RegsApi(self) + self.memory = MemoryApi(self) + + self._breakpoints = {} + self._watchpoints = defaultdict(dict) + + # If you need to access data that is not exposed through the api + # yet, access the internal_engine representation at your own + # risk. + Engine(config=config, api=self) + self.plugins = Plugins(self, ["plugins"]) + self.internal_engine.plugins = self.plugins + + # **** Begin Hook API **** + def hook_memory( + self, + hook_type: HookType.MEMORY, + callback: Callable[["Zelos", int, int, int, int], Any], + mem_low: Optional[int] = None, + mem_high: Optional[int] = None, + name: Optional[str] = None, + end_condition: Optional[Callable[[], bool]] = None, + ) -> HookInfo: + """ + Registers a hook on memory. Executes callback every time the + specified event happens in memory. + + The hook will only trigger when the event occurs at an address + between mem_low and mem_high, if either of them are specified. + + The hook will continue to trigger until the end_condition + specified evaluates to True. + + Args: + hook_type: Specifies the event in memory that should trigger + the callback to be executed. Options can be found in + :py:class:`zelos.HookType.MEMORY` + callback: The code that should be executed when the + specified event occurs. The function should accept the + following inputs: (zelos, access, address, size, value). + The return value of "callback" is ignored. + mem_low: If specified, only executes callback if the + event occurs at an address greater than or equal to + this. + mem_high: If specified, only executes callback if the + event occurs at an address less than or equal to this. + name: An identifier for this hook. Used for debugging. + end_condition: If specified, executes after the callback. If + the function returns True, this hook is deleted. + + Returns: + Information regarding the hook. + + Example: + .. code-block:: python + + from zelos import Zelos, HookType + + # Print every write to memory + def memory_hook(zelos, access, address, size, value): + print(value) + + z = Zelos("binary_to_emulate") + z.hook_memory( + HookType.MEMORY.WRITE, + memory_hook + ) + z.start() + """ + + return self.internal_engine.hook_manager.register_mem_hook( + hook_type, + callback, + mem_low=mem_low, + mem_high=mem_high, + name=name, + end_condition=end_condition, + ) + + def hook_execution( + self, + hook_type: HookType.EXEC, + callback: Callable[["Zelos", int, int], Any], + ip_low: Optional[int] = None, + ip_high: Optional[int] = None, + name: Optional[str] = None, + end_condition: Optional[Callable[[], bool]] = None, + ) -> HookInfo: + """ + Registers a hook that executes when code is executed. This is + either for every instruction that is executed, or every block. + + The hook will only trigger when the event occurs at an address + between ip_low and ip_high, if either of them are specified. + + The hook will continue to trigger until the end_condition + specified evaluates to True. + + Args: + hook_type: Specifies whether the callback should be + triggered every instruction, or every block. Options can + be found in :py:class:`zelos.HookType.EXEC` + callback: The code that should be executed when the + specified event occurs. The function should accept the + following inputs: (zelos, address, size). + The return value of "callback" is ignored. + mem_low: If specified, only executes callback if the + event occurs at an address greater than or equal to + this. + mem_high: If specified, only executes callback if the + event occurs at an address less than or equal to this. + name: An identifier for this hook. Used for debugging. + end_condition: If specified, executes after the callback. If + the function returns True, this hook is deleted. + + Returns: + Information regarding the hook. + + Example: + .. code-block:: python + + from zelos import Zelos, HookType + + # Print the first address of every block + def exec_hook(zelos, address, size): + print(address) + + z = ("binary_to_emulate") + z.hook_execution( + HookType.EXEC.BLOCK, exec_hook + ) + z.start() + """ + return self.internal_engine.hook_manager.register_exec_hook( + hook_type, + callback, + ip_low=ip_low, + ip_high=ip_high, + name=name, + end_condition=end_condition, + ) + + def hook_close(self, closure: Callable[[], Any]) -> HookInfo: + """ + Registers a closure that is called when + :py:meth:`zelos.Engine.close()` is called. + + Args: + closure: Called when zelos is closed. Does not take any + arguments. The return value of `closure` is ignored + + Example: + .. code-block:: python + + from zelos import Zelos + + # Close a file you are using with zelos + file = open("testfile", "r") + def close_cleanup(): + file.close() + + z = ("binary_to_emulate") + z.hook_close(close_cleanup) + z.start() + + # Hooks are run at this point + z.close() + """ + return self.internal_engine.hook_manager.register_close_hook(closure) + + def hook_syscalls( + self, + syscall_hook_type: HookType.SYSCALL, + callback: Callable[["Zelos", str, "Args", int], Any], + name: str = None, + ) -> HookInfo: + """ + Registers a closure that is called when a syscall is invoked. + + Args: + syscall_hook_type: Decides when the hook should be triggered + in relation to the execution of the syscall. Options can + be found in :py:class:`zelos.HookType.SYSCALL` + callback: The code that should be executed when the + specified event occurs. The function should accept the + following inputs: + (zelos, syscall_name, args, return_value) + The return value of "callback" is ignored. + name: An identifier for this hook. Used for debugging. + + Example: + .. code-block:: python + + from zelos import Zelos, HookType + + # Keep track of the syscall return values + syscall_return_values = [] + def syscall_hook(zelos, sys_name, args, ret_val): + syscall_return_values.append((sys_name, ret_val)) + + z = ("binary_to_emulate") + z.hook_syscalls( + HookType.SYSCALL.AFTER, syscall_hook + ) + z.start() + + """ + return self.internal_engine.hook_manager.register_syscall_hook( + syscall_hook_type, callback, name + ) + + def delete_hook(self, hook_info: HookInfo) -> None: + self.internal_engine.hook_manager.delete_hook(hook_info) + + # **** Begin Debugging API **** + + def start(self, timeout: float = 0) -> None: + """ + Begin emulation, starting execution at the IP. + + Args: + timeout: Stops execution after `timeout` seconds. + + Example: + .. code-block:: python + + from zelos import Zelos + + z = ("binary_to_emulate") + + z.start() + + """ + self.internal_engine.start(timeout=timeout) + + def step(self, count=1) -> None: + """ + Begin emulation, stopping after executing `count` instructions. + + Args: + count: Maximum number of instructions to execute before + stopping + """ + self.internal_engine.step(count=count) + + def stop(self, reason: str = "plugin"): + """ + Stop the Zelos run loop and end execution. + + Args: + reason: An optional identifier that specifies a reason for + stopping execution. Upon calling stop, the reason will + be printed to stdout after Zelos exits the run loop. + This is useful for debugging when log level is set to + 'debug' or 'spam'. + """ + self.internal_engine.scheduler.stop(reason) + + def close(self): + """ + Closes Zelos and runs cleanup functions. + """ + self.internal_engine.close() + + def end_thread(self): + """ Stop the current thread. Mark as successfully completed.""" + self.internal_engine.thread_manager.complete_current_thread() + + def swap_thread(self, reason: str = "thread swap"): + """ + Swap the running thread with the next scheduled thread. + + Args: + reason: An optional identifier that specifies a reason for + swapping threads. Upon calling swap_thread, the reason + will be printed to stdout after Zelos has successfully + swapped to the next scheduled thread. This is useful + for debugging when log level is set to 'spam'. + """ + self.internal_engine.scheduler.stop_and_exec( + reason, self.internal_engine.processes.schedule_next + ) + + def set_breakpoint(self, address: int, temporary: bool = False): + """ + Set a breakpoint at a particular address. + + Args: + address: Target address of breakpoint. + temporary: Determines whether or not the breakpoint is + temporary. A temporary breakpoint will be automatically + removed after use. + + Example: + .. code-block:: python + + from zelos import Zelos + + z = ("binary_to_emulate") + + z.set_breakpoint(0xdeadbeef) + + z.start() + + """ + + def hook(zelos, access, size): + zelos.stop("breakpoint") + + hook_info = self.internal_engine.hook_manager.register_exec_hook( + HookType.EXEC.INST, + hook, + ip_low=address, + ip_high=address, + name=f"breakpoint_{address:x}", + end_condition=lambda: temporary, + ) + + self._breakpoints[address] = hook_info + + return True + + def remove_breakpoint(self, address: int): + """ + Remove a previously set breakpoint. + + Args: + address: Target address of breakpoint to remove. + + Example: + .. code-block:: python + + from zelos import Zelos + + z = ("binary_to_emulate") + + z.set_breakpoint(0xdeadbeef) + + z.remove_breakpoint(0xdeadbeef) + + z.start() + + """ + hook_info = self._breakpoints[address] + self.internal_engine.hook_manager.delete_hook(hook_info) + + def set_syscall_breakpoint(self, syscall_name: str): + """ + Set a breakpoint at all syscalls of a specified name. + + Args: + syscall_name: Target syscall set breakpoint at. + + Example: + .. code-block:: python + + from zelos import Zelos + + z = ("binary_to_emulate") + + z.set_syscall_breakpoint("write") + + z.start() + + """ + self.internal_engine.zos.syscall_manager.set_breakpoint(syscall_name) + + def remove_syscall_breakpoint(self, syscall_name: str): + """ + Remove a previously set syscall breakpoint specified by name. + + Args: + syscall_name: Target syscall to remove breakpoints from. + + Example: + .. code-block:: python + + from zelos import Zelos + + z = ("binary_to_emulate") + + z.set_syscall_breakpoint("write") + + z.remove_syscall_breakpoint("write") + + z.start() + + """ + self.internal_engine.zos.syscall_manager.remove_breakpoint( + syscall_name + ) + + def set_watchpoint( + self, address: int, read: bool, write: bool, temporary: bool = False + ): + """ + Set a watchpoint on a particular memory address. + + Args: + address: Target address of watchpoint. + read: Determines whether to watch for reads to the target + memory address. + write: Determines whether to watch for writes to the target + memory address. + + Example: + .. code-block:: python + + from zelos import Zelos + + z = ("binary_to_emulate") + + # Break at any read or write to memory address 0xdeadbeef + z.set_watchpoint(0xdeadbeef, True, True) + + # Break only at writes to memory address 0xfeedf00d + z.set_watchpoint(0xfeedf00d, False, True) + + # Break only at reads to memory address 0xb0bad00d + z.set_watchpoint(0xb0bad00d, True, False) + + z.start() + + """ + + def hook(zelos, access, address, size, value): + zelos.stop("watchpoint") + + if read: + read_hook_info = self.hook_memory( + HookType.MEMORY.READ, + hook, + name=f"read_watchpoint_{address:x}", + mem_low=address, + mem_high=address, + end_condition=lambda: temporary, + ) + self._watchpoints[address]["read"] = read_hook_info + if write: + write_hook_info = self.hook_memory( + HookType.MEMORY.WRITE, + hook, + name=f"write_watchpoint_{address:x}", + mem_low=address, + mem_high=address, + end_condition=lambda: temporary, + ) + self._watchpoints[address]["write"] = write_hook_info + + return True + + def remove_watchpoint(self, address: int): + """ + Remove a previously set watchpoint. + + Args: + address: Target address of watchpoint to remove. + + Example: + .. code-block:: python + + from zelos import Zelos + + z = ("binary_to_emulate") + + z.set_watchpoint(0xdeadbeef, True, True) + + z.remove_watchpoint(0xdeadbeef) + + z.start() + + """ + for hook_info in self._watchpoints[address].values(): + self.internal_engine.hook_manager.delete_hook(hook_info) + del self._watchpoints[address] + + return True + + # **** Begin Context API **** + + @property + def date(self): + """ Returns the date used internally during emulation. """ + return self.internal_engine.date + + @date.setter + def date(self, date_str: str): + """ + Set the date to be used for emulation. This affects the linux + system calls sys_time, sys_gettimeofday, and sys_clock_gettime. + + Args: + date_str: A string of the form `YYYY-MM-DD` specifying the + target date. + + Example: + .. code-block:: python + + from zelos import Zelos + + z = ("binary_to_emulate") + + z.date = "2020-03-04" + + z.start() + + """ + self.internal_engine.date = date_str + + @property + def process(self): + """ Returns the active process""" + return self.internal_engine.current_process + + @property + def thread(self): + """ Returns the active thread""" + return self.internal_engine.current_process.current_thread + + +class ZelosCmdline(Zelos): + """ + A workaround for allowing `Zelos` to be initialized with a + commandline string while also allowing the main `Zelos` to maintain + its simple/intuitive constructor based on commandline arguments and + flags. + """ + + def __init__(self, cmdline_args): + config = generate_config_from_cmdline(cmdline_args) + self._setup(config) diff --git a/src/zelos/config_gen.py b/src/zelos/config_gen.py new file mode 100644 index 0000000..ffa586b --- /dev/null +++ b/src/zelos/config_gen.py @@ -0,0 +1,225 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import os + +from typing import Optional + +import configargparse + +from zelos.plugin import PluginCommands + + +def generate_config( + binary_path: Optional[str], *cmdline_args: str, **kwargs: str +): + """ + Generates a config used to modify the analysis run by Zelos. The + config uses the same options present in the command line flags. + + Args: + binary_path: Relative or absolute filepath to binary + cmdline_args: Command line arguments that will be passed to the + binary. + kwargs: Additional options to specify corresponding to command + line flags + + Returns: + Config that can be used to initialize Zelos. + """ + + if binary_path is None: + return _generate_without_binary(**kwargs) + flags = [] + for k, v in kwargs.items(): + if v in [False, True]: + flags.append(f"--{k}") + else: + flags.append(f"--{k}={v}") + flag_string = " ".join(flags) + + cmdline_arg_string = " ".join(cmdline_args) + return generate_config_from_cmdline( + f"{flag_string} {binary_path} {cmdline_arg_string}" + ) + + +def _generate_without_binary(**kwargs): + # Generating a config without a binary should only be done when + # testing zelos. A file is required when using zelos, pass in a fake + # file and then overwrite that field immediately after. We can + # consider removing this requirement and doing manual checking after + # getting the config + config = generate_config("NOFILE", **kwargs) + config.filename = "" + return config + + +def generate_config_from_cmdline(cmdline_string): + parser = configargparse.ArgumentParser() + group_logging = parser.add_argument_group("logging") + group_reporting = parser.add_argument_group("reporting") + group_limits = parser.add_argument_group("limits") + group_networking = parser.add_argument_group("networking") + group_fs = parser.add_argument_group("filesystem") + group_clock = parser.add_argument_group("clock") + parser.add("-c", "--config", is_config_file=True, help="config file path") + group_fs.add_argument( + "--virtual-filename", + type=str, + default=None, + help="Emulated filename (if different from real filename).", + ) + group_fs.add_argument( + "--virtual-path", + type=str, + default=None, + help="Emulated file path (optional). " + "(default: '/home/admin/zelos_dir/').", + ) + group_logging.add_argument( + "-v", + "--verbosity", + action="count", + default=0, + help="Increase output verbosity. Enables instruction-level tracing.", + ) + group_logging.add_argument( + "--log", + type=str, + default="info", + help="Decide what level of logging should be used. LOG is " + "'info', 'verbose', 'debug', 'spam', 'notice', 'warning', 'success', " + "'error', or 'fatal'. Note that this does not affect " + "verbosity. (default: 'info')", + ) + group_networking.add_argument( + "--dns", + action="count", + default=0, + help="Simulate DNS response for all domains (resolve to 127.0.0.1)", + ) + group_logging.add_argument( + "--fasttrace", + action="count", + default=0, + help="Enable instruction-level tracing only the first time a memory " + "address is reached.", + ) + group_limits.add_argument( + "-t", + "--timeout", + type=int, + default=0, + help="If specified, execution will end after TIMEOUT seconds have " + "passed.", + ) + group_limits.add_argument( + "-m", + "--memlimit", + type=int, + default=0, + help="Limits memory allocation to MEMLIMIT total mb.", + ) + group_logging.add_argument( + "--traceon", + type=str, + default="", + help="[Experimental] Enable verbose tracing after specified address " + "or API name.", + ) + group_logging.add_argument( + "--traceoff", + type=str, + default="", + help="[Experimental] Disable verbose tracing after " + "specified address or API name.", + ) + group_logging.add_argument( + "--tracethread", + type=str, + default="", + help="[Experimental] Enable verbose tracing on a single thread.", + ) + group_logging.add_argument( + "--writetrace", + type=str, + default="", + help="Print a message every time a value at the given memory " + "location is written.", + ) + group_clock.add_argument( + "--date", + type=str, + default="2019-02-02", + help="Emulated system date. Format: YYYY-MM-DD. " + "(default: '2019-02-02')", + ) + parser.add_argument( + "--startat", + type=str, + default=None, + help="[Experimental] Start execution at the given hex address.", + ) + parser.add_argument( + "--disableNX", + action="store_true", + help="Disable the no-execute bit. All memory becomes executable.", + ) + group_reporting.add_argument( + "--strace", + type=str, + default=None, + help="Writes the system call trace to the specified output file.", + ) + group_logging.add_argument( + "--log_exports", + action="store_true", + help="Enable logging of calls to exported functions. (default: off)", + ) + group_fs.add_argument( + "--mount", + action="append", + default=[], + help="[Experimental] Mount the specified file or path into the " + "emulated root filesystem. Format: '--mount ARCH,DEST," + "SRC'. ARCH is 'x86', 'x86-64', 'arm', or 'mips'. " + "DEST is the emulated path to mount. SRC is the absolute host path to " + "the file or directory to mount. Can be specified multiple times to " + "mount multiple files.", + ) + group_fs.add_argument( + "--env_vars", + action="append", + default=[], + help="Emulated environment variables. ENV_VARS is a comma separated " + "key value pair. Can be specified multiple times to set multiple " + "environment variables. Format: '--env_vars FOO:bar --env_vars " + "ZERO:point'.", + ) + + path = os.environ.get("ZELOS_PLUGIN_DIR", None) + paths = path.split(",") if path is not None else [] + _ = PluginCommands(paths, parser) + + parser.add_argument("filename", type=str, help="Executable to emulate") + parser.add_argument( + "cmdline_args", type=str, nargs="*", help="Arguments to the executable" + ) + config = parser.parse_args(cmdline_string) + + return config diff --git a/src/zelos/emulator/__init__.py b/src/zelos/emulator/__init__.py new file mode 100644 index 0000000..6ea7f84 --- /dev/null +++ b/src/zelos/emulator/__init__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +from .base import create_emulator + + +__all__ = ["create_emulator"] diff --git a/src/zelos/emulator/arm.py b/src/zelos/emulator/arm.py new file mode 100644 index 0000000..1069351 --- /dev/null +++ b/src/zelos/emulator/arm.py @@ -0,0 +1,188 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import unicorn.arm_const as uc + +from .base import IEmuHelper + + +class ArmEmuHelper(IEmuHelper): + ip_reg = "pc" + sp_reg = "sp" + fp_reg = "fp" + regmap = { + "apsr": uc.UC_ARM_REG_APSR, + "apsr_nzcv": uc.UC_ARM_REG_APSR_NZCV, + "cpsr": uc.UC_ARM_REG_CPSR, + "fpexc": uc.UC_ARM_REG_FPEXC, + "fpinst": uc.UC_ARM_REG_FPINST, + "fpscr": uc.UC_ARM_REG_FPSCR, + "fpscr_nzcv": uc.UC_ARM_REG_FPSCR_NZCV, + "fpsid": uc.UC_ARM_REG_FPSID, + "itstate": uc.UC_ARM_REG_ITSTATE, + "lr": uc.UC_ARM_REG_LR, + "pc": uc.UC_ARM_REG_PC, + "sp": uc.UC_ARM_REG_SP, + "spsr": uc.UC_ARM_REG_SPSR, + "d0": uc.UC_ARM_REG_D0, + "d1": uc.UC_ARM_REG_D1, + "d2": uc.UC_ARM_REG_D2, + "d3": uc.UC_ARM_REG_D3, + "d4": uc.UC_ARM_REG_D4, + "d5": uc.UC_ARM_REG_D5, + "d6": uc.UC_ARM_REG_D6, + "d7": uc.UC_ARM_REG_D7, + "d8": uc.UC_ARM_REG_D8, + "d9": uc.UC_ARM_REG_D9, + "d10": uc.UC_ARM_REG_D10, + "d11": uc.UC_ARM_REG_D11, + "d12": uc.UC_ARM_REG_D12, + "d13": uc.UC_ARM_REG_D13, + "d14": uc.UC_ARM_REG_D14, + "d15": uc.UC_ARM_REG_D15, + "d16": uc.UC_ARM_REG_D16, + "d17": uc.UC_ARM_REG_D17, + "d18": uc.UC_ARM_REG_D18, + "d19": uc.UC_ARM_REG_D19, + "d20": uc.UC_ARM_REG_D20, + "d21": uc.UC_ARM_REG_D21, + "d22": uc.UC_ARM_REG_D22, + "d23": uc.UC_ARM_REG_D23, + "d24": uc.UC_ARM_REG_D24, + "d25": uc.UC_ARM_REG_D25, + "d26": uc.UC_ARM_REG_D26, + "d27": uc.UC_ARM_REG_D27, + "d28": uc.UC_ARM_REG_D28, + "d29": uc.UC_ARM_REG_D29, + "d30": uc.UC_ARM_REG_D30, + "d31": uc.UC_ARM_REG_D31, + "fpinst2": uc.UC_ARM_REG_FPINST2, + "mvfr0": uc.UC_ARM_REG_MVFR0, + "mvfr1": uc.UC_ARM_REG_MVFR1, + "mvfr2": uc.UC_ARM_REG_MVFR2, + "q0": uc.UC_ARM_REG_Q0, + "q1": uc.UC_ARM_REG_Q1, + "q2": uc.UC_ARM_REG_Q2, + "q3": uc.UC_ARM_REG_Q3, + "q4": uc.UC_ARM_REG_Q4, + "q5": uc.UC_ARM_REG_Q5, + "q6": uc.UC_ARM_REG_Q6, + "q7": uc.UC_ARM_REG_Q7, + "q8": uc.UC_ARM_REG_Q8, + "q9": uc.UC_ARM_REG_Q9, + "q10": uc.UC_ARM_REG_Q10, + "q11": uc.UC_ARM_REG_Q11, + "q12": uc.UC_ARM_REG_Q12, + "q13": uc.UC_ARM_REG_Q13, + "q14": uc.UC_ARM_REG_Q14, + "q15": uc.UC_ARM_REG_Q15, + "r0": uc.UC_ARM_REG_R0, + "r1": uc.UC_ARM_REG_R1, + "r2": uc.UC_ARM_REG_R2, + "r3": uc.UC_ARM_REG_R3, + "r4": uc.UC_ARM_REG_R4, + "r5": uc.UC_ARM_REG_R5, + "r6": uc.UC_ARM_REG_R6, + "r7": uc.UC_ARM_REG_R7, + "r8": uc.UC_ARM_REG_R8, + "r9": uc.UC_ARM_REG_R9, + "r10": uc.UC_ARM_REG_R10, + "r11": uc.UC_ARM_REG_R11, + "r12": uc.UC_ARM_REG_R12, + "s0": uc.UC_ARM_REG_S0, + "s1": uc.UC_ARM_REG_S1, + "s2": uc.UC_ARM_REG_S2, + "s3": uc.UC_ARM_REG_S3, + "s4": uc.UC_ARM_REG_S4, + "s5": uc.UC_ARM_REG_S5, + "s6": uc.UC_ARM_REG_S6, + "s7": uc.UC_ARM_REG_S7, + "s8": uc.UC_ARM_REG_S8, + "s9": uc.UC_ARM_REG_S9, + "s10": uc.UC_ARM_REG_S10, + "s11": uc.UC_ARM_REG_S11, + "s12": uc.UC_ARM_REG_S12, + "s13": uc.UC_ARM_REG_S13, + "s14": uc.UC_ARM_REG_S14, + "s15": uc.UC_ARM_REG_S15, + "s16": uc.UC_ARM_REG_S16, + "s17": uc.UC_ARM_REG_S17, + "s18": uc.UC_ARM_REG_S18, + "s19": uc.UC_ARM_REG_S19, + "s20": uc.UC_ARM_REG_S20, + "s21": uc.UC_ARM_REG_S21, + "s22": uc.UC_ARM_REG_S22, + "s23": uc.UC_ARM_REG_S23, + "s24": uc.UC_ARM_REG_S24, + "s25": uc.UC_ARM_REG_S25, + "s26": uc.UC_ARM_REG_S26, + "s27": uc.UC_ARM_REG_S27, + "s28": uc.UC_ARM_REG_S28, + "s29": uc.UC_ARM_REG_S29, + "s30": uc.UC_ARM_REG_S30, + "s31": uc.UC_ARM_REG_S31, + "c1_c0_2": uc.UC_ARM_REG_C1_C0_2, + "c13_c0_2": uc.UC_ARM_REG_C13_C0_2, + "c13_c0_3": uc.UC_ARM_REG_C13_C0_3, + "ipsr": uc.UC_ARM_REG_IPSR, + "msp": uc.UC_ARM_REG_MSP, + "psp": uc.UC_ARM_REG_PSP, + "control": uc.UC_ARM_REG_CONTROL, + "ending": uc.UC_ARM_REG_ENDING, + # alias registers + "r13": uc.UC_ARM_REG_R13, + "r14": uc.UC_ARM_REG_R14, + "r15": uc.UC_ARM_REG_R15, + "sb": uc.UC_ARM_REG_SB, + "sl": uc.UC_ARM_REG_SL, + "fp": uc.UC_ARM_REG_FP, + "ip": uc.UC_ARM_REG_IP, + } + + # These are the default registers that should be printed + # when debugging + imp_regs = [ + "r0", + "r1", + "r2", + "r3", + "r4", + "r5", + "r6", + "r7", + "r8", + "r9", + "r10", + "r11", + "r12", + "sp", + "lr", + "pc", + "cpsr", + "fp", + "fpscr", + "fpsid", + "fpexc", + ] + + def __init__(self, unicorn, state): + super().__init__(unicorn, state) + # Enables arm VFP: + # https://github.com/unicorn-engine/unicorn/pull/684 + tmp_val = self.get_reg("c1_c0_2") + tmp_val |= 0xF << 20 + self.set_reg("c1_c0_2", tmp_val) + self.set_reg("fpexc", 0x40000000) diff --git a/src/zelos/emulator/base.py b/src/zelos/emulator/base.py new file mode 100644 index 0000000..2d72f2c --- /dev/null +++ b/src/zelos/emulator/base.py @@ -0,0 +1,248 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import logging + +from typing import Dict, Iterable, List + +from unicorn import UC_ARCH_ARM, UC_ARCH_MIPS, UC_ARCH_X86 + +from zelos.exceptions import InvalidRegException, ZelosLoadException +from zelos.util import columnate, struct + + +class IEmuHelper: + """ + This is a class that serves as a wrapper around Unicorn, providing some + additional functionality surrounding stacks and commonly used registers. + Each architecture will need to implement their own subclass to provide the + additional register information that is needed. + + Because there may be multiple threads sharing a single emu instance, prefer + to access these methods through the Thread class. + + We chose to use string names for registers rather than an enum for quality + of life reasons. + """ + + def __init__(self, unicorn_engine, state): + self.unicorn_engine = unicorn_engine + self.logger = logging.getLogger(__name__) + self.is_running = False + self.state = state + + # This allows the emu helper to have all of the functionality of the + # unicorn engine + + def __getattr__(self, attr): + return getattr(self.unicorn_engine, attr) + + @property + def regmap(self): + raise NotImplementedError() + + @property + def bytes(self) -> int: + return self.state.bytes + + def getstack(self, idx: int) -> int: + sp = self.getSP() + data = self.mem_read(sp + (idx * self.bytes), self.bytes) + return self.unpack(data) + + def setstack(self, idx: int, val: int) -> None: + sp = self.getSP() + self.mem_write(sp + (idx * self.bytes), self.pack(val)) + + def popstack(self) -> int: + sp = self.getSP() + data = self.unpack(self.mem_read(sp, self.bytes)) + self.setSP(sp + self.bytes) + return data + + def pushstack(self, data: int) -> None: + sp = self.getSP() + self.mem_write(sp - self.bytes, self.pack(data)) + self.setSP(sp - self.bytes) + + def setSP(self, val: int) -> None: + self.set_reg(self.sp_reg, val) + + def getSP(self) -> int: + return self.get_reg(self.sp_reg) + + def setFP(self, val: int): + self.set_reg(self.fp_reg, val) + + def getFP(self) -> int: + return self.get_reg(self.fp_reg) + + def get_reg(self, reg_name: str) -> int: + try: + return self.reg_read(self.regmap[reg_name]) + except KeyError: + raise InvalidRegException(reg_name) + + def set_reg(self, reg_name: str, val: int) -> None: + try: + self.reg_write(self.regmap[reg_name], val) + except KeyError: + raise InvalidRegException(reg_name) + + def setIP(self, val: int) -> None: + self.set_reg(self.ip_reg, val) + + def getIP(self) -> int: + return self.get_reg(self.ip_reg) + + def get_all_regs(self) -> List[str]: + """ + Gets all registers for this architecture. + Order of returned values is consistent between calls. + """ + # Regmap may store aliases of registers, ensure that the emitted + # list is unique + reg_names = sorted(self.regmap.keys()) + # We sorted the reg_names so that the last repeat for a given + # index is always taken. + index_to_name = {self.regmap[name]: name for name in reg_names} + unique_reg_names = index_to_name.values() + # Make sure the order of registers returned is consistent + return sorted(unique_reg_names) + + def get_all_reg_vals(self) -> Dict[str, int]: + """ + Returns a dict of {reg_name:reg_val} for all regs for the + current architecture. + """ + return {name: self.get_reg(name) for name in self.get_all_regs()} + + def get_regs(self, regs: Iterable[str] = None) -> Dict[str, int]: + """ + Returns a dictionary of registers and their values. Defaults + to important regs for the current architecture + """ + if regs is None: + regs = self.imp_regs + return {r: self.get_reg(r) for r in regs} + + def dumpregs(self, regs: Iterable[str] = None) -> str: + if regs is None: + regs = self.imp_regs + + def get_reg_string(reg): + val = self.get_reg(reg) + fmt = "{0}: 0x{1:0" + str(self.state.bytes * 2) + "x}" + return (fmt).format(reg, val) + + reg_strings = [get_reg_string(r) for r in regs] + + return columnate(reg_strings, 4) + + ############### + # FOR UTILITY # + ############### + + def to_signed(self, x, bytes=None): + return self.unpack(self.pack(x, bytes=bytes), bytes=bytes, signed=True) + + def pack( + self, + x: int, + bytes: int = None, + little_endian: bool = None, + signed: bool = False, + ) -> bytes: + """ + Unpacks an integer from a byte format. Defaults to the + current architecture bytes and endianness. + """ + endian_char, bit_char, bit_mask = self._pack_format( + bytes, little_endian, signed + ) + return struct.pack(endian_char + bit_char, x & bit_mask) + + def unpack( + self, + x: bytes, + bytes: int = None, + little_endian: bool = None, + signed: bool = False, + ) -> int: + """ + Unpacks an integer from a byte format. Defaults to the + current architecture bytes and endianness. + """ + endian_char, bit_char, bit_mask = self._pack_format( + bytes, little_endian, signed + ) + return struct.unpack(endian_char + bit_char, x)[0] + + def _pack_format(self, bytes: int, little_endian: bool, signed: bool): + """ + Generates the format for the struct.pack and unpack functions. + Defaults to the current architecture bytes and endianness + """ + if bytes is None: + bytes = self.bytes + if little_endian is None: + little_endian = self.state.endianness == "little" + bits = bytes * 8 + + bit_char = {8: "B", 16: "H", 32: "I", 64: "Q"}[bits] + bit_mask = 2 ** bits - 1 + + if signed: + bit_char = bit_char.lower() + endian_char = "<" if little_endian else ">" + return endian_char, bit_char, bit_mask + + +def create_emulator(arch, mode, state) -> IEmuHelper: + """ + Factory method for constructing the appropriate IEmuHelper + """ + from unicorn.unicorn import UcError + from unicorn import Uc + + try: + uc = Uc(arch, mode) + arch = uc._arch + if arch == UC_ARCH_X86 and state.bits == 32: + from .x86 import x86EmuHelper + + return x86EmuHelper(uc, state) + if arch == UC_ARCH_X86 and state.bits == 64: + from .x86 import x86_64EmuHelper + + return x86_64EmuHelper(uc, state) + elif arch == UC_ARCH_ARM: + from .arm import ArmEmuHelper + + return ArmEmuHelper(uc, state) + elif arch == UC_ARCH_MIPS: + from .mips import MipsEmuHelper + + return MipsEmuHelper(uc, state) + else: + raise ZelosLoadException( + f"Unsupported architecture {arch} {state.bits}" + ) + except UcError: + raise ZelosLoadException( + f"Custom unicorn does not support the arch/mode/bits" + + f" {arch}/{mode}/{state.bits}" + ) diff --git a/src/zelos/emulator/mips.py b/src/zelos/emulator/mips.py new file mode 100644 index 0000000..9178df7 --- /dev/null +++ b/src/zelos/emulator/mips.py @@ -0,0 +1,252 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import unicorn.mips_const as uc + +from .base import IEmuHelper + + +class MipsEmuHelper(IEmuHelper): + ip_reg = "pc" + sp_reg = "sp" + fp_reg = "fp" + regmap = { + "pc": uc.UC_MIPS_REG_PC, + "0": uc.UC_MIPS_REG_0, + "1": uc.UC_MIPS_REG_1, + "2": uc.UC_MIPS_REG_2, + "3": uc.UC_MIPS_REG_3, + "4": uc.UC_MIPS_REG_4, + "5": uc.UC_MIPS_REG_5, + "6": uc.UC_MIPS_REG_6, + "7": uc.UC_MIPS_REG_7, + "8": uc.UC_MIPS_REG_8, + "9": uc.UC_MIPS_REG_9, + "10": uc.UC_MIPS_REG_10, + "11": uc.UC_MIPS_REG_11, + "12": uc.UC_MIPS_REG_12, + "13": uc.UC_MIPS_REG_13, + "14": uc.UC_MIPS_REG_14, + "15": uc.UC_MIPS_REG_15, + "16": uc.UC_MIPS_REG_16, + "17": uc.UC_MIPS_REG_17, + "18": uc.UC_MIPS_REG_18, + "19": uc.UC_MIPS_REG_19, + "20": uc.UC_MIPS_REG_20, + "21": uc.UC_MIPS_REG_21, + "22": uc.UC_MIPS_REG_22, + "23": uc.UC_MIPS_REG_23, + "24": uc.UC_MIPS_REG_24, + "25": uc.UC_MIPS_REG_25, + "26": uc.UC_MIPS_REG_26, + "27": uc.UC_MIPS_REG_27, + "28": uc.UC_MIPS_REG_28, + "29": uc.UC_MIPS_REG_29, + "30": uc.UC_MIPS_REG_30, + "31": uc.UC_MIPS_REG_31, + # DSP registers + "dspccond": uc.UC_MIPS_REG_DSPCCOND, + "dspcarry": uc.UC_MIPS_REG_DSPCARRY, + "dspefi": uc.UC_MIPS_REG_DSPEFI, + "dspoutflag": uc.UC_MIPS_REG_DSPOUTFLAG, + "dspoutflag16_19": uc.UC_MIPS_REG_DSPOUTFLAG16_19, + "dspoutflag20": uc.UC_MIPS_REG_DSPOUTFLAG20, + "dspoutflag21": uc.UC_MIPS_REG_DSPOUTFLAG21, + "dspoutflag22": uc.UC_MIPS_REG_DSPOUTFLAG22, + "dspoutflag23": uc.UC_MIPS_REG_DSPOUTFLAG23, + "dsppos": uc.UC_MIPS_REG_DSPPOS, + "dspscount": uc.UC_MIPS_REG_DSPSCOUNT, + # ACC registers + "ac0": uc.UC_MIPS_REG_AC0, + "ac1": uc.UC_MIPS_REG_AC1, + "ac2": uc.UC_MIPS_REG_AC2, + "ac3": uc.UC_MIPS_REG_AC3, + # COP registers + "cc0": uc.UC_MIPS_REG_CC0, + "cc1": uc.UC_MIPS_REG_CC1, + "cc2": uc.UC_MIPS_REG_CC2, + "cc3": uc.UC_MIPS_REG_CC3, + "cc4": uc.UC_MIPS_REG_CC4, + "cc5": uc.UC_MIPS_REG_CC5, + "cc6": uc.UC_MIPS_REG_CC6, + "cc7": uc.UC_MIPS_REG_CC7, + # FPU registers + "f0": uc.UC_MIPS_REG_F0, + "f1": uc.UC_MIPS_REG_F1, + "f2": uc.UC_MIPS_REG_F2, + "f3": uc.UC_MIPS_REG_F3, + "f4": uc.UC_MIPS_REG_F4, + "f5": uc.UC_MIPS_REG_F5, + "f6": uc.UC_MIPS_REG_F6, + "f7": uc.UC_MIPS_REG_F7, + "f8": uc.UC_MIPS_REG_F8, + "f9": uc.UC_MIPS_REG_F9, + "f10": uc.UC_MIPS_REG_F10, + "f11": uc.UC_MIPS_REG_F11, + "f12": uc.UC_MIPS_REG_F12, + "f13": uc.UC_MIPS_REG_F13, + "f14": uc.UC_MIPS_REG_F14, + "f15": uc.UC_MIPS_REG_F15, + "f16": uc.UC_MIPS_REG_F16, + "f17": uc.UC_MIPS_REG_F17, + "f18": uc.UC_MIPS_REG_F18, + "f19": uc.UC_MIPS_REG_F19, + "f20": uc.UC_MIPS_REG_F20, + "f21": uc.UC_MIPS_REG_F21, + "f22": uc.UC_MIPS_REG_F22, + "f23": uc.UC_MIPS_REG_F23, + "f24": uc.UC_MIPS_REG_F24, + "f25": uc.UC_MIPS_REG_F25, + "f26": uc.UC_MIPS_REG_F26, + "f27": uc.UC_MIPS_REG_F27, + "f28": uc.UC_MIPS_REG_F28, + "f29": uc.UC_MIPS_REG_F29, + "f30": uc.UC_MIPS_REG_F30, + "f31": uc.UC_MIPS_REG_F31, + "fcc0": uc.UC_MIPS_REG_FCC0, + "fcc1": uc.UC_MIPS_REG_FCC1, + "fcc2": uc.UC_MIPS_REG_FCC2, + "fcc3": uc.UC_MIPS_REG_FCC3, + "fcc4": uc.UC_MIPS_REG_FCC4, + "fcc5": uc.UC_MIPS_REG_FCC5, + "fcc6": uc.UC_MIPS_REG_FCC6, + "fcc7": uc.UC_MIPS_REG_FCC7, + # AFPR128 + "w0": uc.UC_MIPS_REG_W0, + "w1": uc.UC_MIPS_REG_W1, + "w2": uc.UC_MIPS_REG_W2, + "w3": uc.UC_MIPS_REG_W3, + "w4": uc.UC_MIPS_REG_W4, + "w5": uc.UC_MIPS_REG_W5, + "w6": uc.UC_MIPS_REG_W6, + "w7": uc.UC_MIPS_REG_W7, + "w8": uc.UC_MIPS_REG_W8, + "w9": uc.UC_MIPS_REG_W9, + "w10": uc.UC_MIPS_REG_W10, + "w11": uc.UC_MIPS_REG_W11, + "w12": uc.UC_MIPS_REG_W12, + "w13": uc.UC_MIPS_REG_W13, + "w14": uc.UC_MIPS_REG_W14, + "w15": uc.UC_MIPS_REG_W15, + "w16": uc.UC_MIPS_REG_W16, + "w17": uc.UC_MIPS_REG_W17, + "w18": uc.UC_MIPS_REG_W18, + "w19": uc.UC_MIPS_REG_W19, + "w20": uc.UC_MIPS_REG_W20, + "w21": uc.UC_MIPS_REG_W21, + "w22": uc.UC_MIPS_REG_W22, + "w23": uc.UC_MIPS_REG_W23, + "w24": uc.UC_MIPS_REG_W24, + "w25": uc.UC_MIPS_REG_W25, + "w26": uc.UC_MIPS_REG_W26, + "w27": uc.UC_MIPS_REG_W27, + "w28": uc.UC_MIPS_REG_W28, + "w29": uc.UC_MIPS_REG_W29, + "w30": uc.UC_MIPS_REG_W30, + "w31": uc.UC_MIPS_REG_W31, + "hi": uc.UC_MIPS_REG_HI, + "lo": uc.UC_MIPS_REG_LO, + "p0": uc.UC_MIPS_REG_P0, + "p1": uc.UC_MIPS_REG_P1, + "p2": uc.UC_MIPS_REG_P2, + "mpl0": uc.UC_MIPS_REG_MPL0, + "mpl1": uc.UC_MIPS_REG_MPL1, + "mpl2": uc.UC_MIPS_REG_MPL2, + "ending": uc.UC_MIPS_REG_ENDING, + "zero": uc.UC_MIPS_REG_ZERO, + "at": uc.UC_MIPS_REG_AT, + "v0": uc.UC_MIPS_REG_V0, + "v1": uc.UC_MIPS_REG_V1, + "a0": uc.UC_MIPS_REG_A0, + "a1": uc.UC_MIPS_REG_A1, + "a2": uc.UC_MIPS_REG_A2, + "a3": uc.UC_MIPS_REG_A3, + "t0": uc.UC_MIPS_REG_T0, + "t1": uc.UC_MIPS_REG_T1, + "t2": uc.UC_MIPS_REG_T2, + "t3": uc.UC_MIPS_REG_T3, + "t4": uc.UC_MIPS_REG_T4, + "t5": uc.UC_MIPS_REG_T5, + "t6": uc.UC_MIPS_REG_T6, + "t7": uc.UC_MIPS_REG_T7, + "s0": uc.UC_MIPS_REG_S0, + "s1": uc.UC_MIPS_REG_S1, + "s2": uc.UC_MIPS_REG_S2, + "s3": uc.UC_MIPS_REG_S3, + "s4": uc.UC_MIPS_REG_S4, + "s5": uc.UC_MIPS_REG_S5, + "s6": uc.UC_MIPS_REG_S6, + "s7": uc.UC_MIPS_REG_S7, + "t8": uc.UC_MIPS_REG_T8, + "t9": uc.UC_MIPS_REG_T9, + "k0": uc.UC_MIPS_REG_K0, + "k1": uc.UC_MIPS_REG_K1, + "gp": uc.UC_MIPS_REG_GP, + "sp": uc.UC_MIPS_REG_SP, + "fp": uc.UC_MIPS_REG_FP, + "s8": uc.UC_MIPS_REG_S8, + "ra": uc.UC_MIPS_REG_RA, + "hi0": uc.UC_MIPS_REG_HI0, + "hi1": uc.UC_MIPS_REG_HI1, + "hi2": uc.UC_MIPS_REG_HI2, + "hi3": uc.UC_MIPS_REG_HI3, + "lo0": uc.UC_MIPS_REG_LO0, + "lo1": uc.UC_MIPS_REG_LO1, + "lo2": uc.UC_MIPS_REG_LO2, + "lo3": uc.UC_MIPS_REG_LO3, + "cp0_userlocal": uc.UC_MIPS_REG_CP0_USERLOCAL, + } + + imp_regs = [ + "zero", + "at", + "v0", + "v1", + "a0", + "a1", + "a2", + "a3", + "t0", + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "t7", + "s0", + "s1", + "s2", + "s3", + "s4", + "s5", + "s6", + "s7", + "t8", + "t9", + "k0", + "k1", + "gp", + "sp", + "s8", + "ra", + "lo", + "hi", + "pc", + ] + # 'sr', #missing + # 'bad', #missing + # 'cause', #missing diff --git a/src/zelos/emulator/x86.py b/src/zelos/emulator/x86.py new file mode 100644 index 0000000..b517d65 --- /dev/null +++ b/src/zelos/emulator/x86.py @@ -0,0 +1,305 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import unicorn.x86_const as uc + +from zelos.emulator.base import IEmuHelper + + +REGMAP = { + "ah": uc.UC_X86_REG_AH, + "al": uc.UC_X86_REG_AL, + "ax": uc.UC_X86_REG_AX, + "bh": uc.UC_X86_REG_BH, + "bl": uc.UC_X86_REG_BL, + "bp": uc.UC_X86_REG_BP, + "bpl": uc.UC_X86_REG_BPL, + "bx": uc.UC_X86_REG_BX, + "ch": uc.UC_X86_REG_CH, + "cl": uc.UC_X86_REG_CL, + "cs": uc.UC_X86_REG_CS, + "cx": uc.UC_X86_REG_CX, + "dh": uc.UC_X86_REG_DH, + "di": uc.UC_X86_REG_DI, + "dil": uc.UC_X86_REG_DIL, + "dl": uc.UC_X86_REG_DL, + "ds": uc.UC_X86_REG_DS, + "dx": uc.UC_X86_REG_DX, + "eax": uc.UC_X86_REG_EAX, + "ebp": uc.UC_X86_REG_EBP, + "ebx": uc.UC_X86_REG_EBX, + "ecx": uc.UC_X86_REG_ECX, + "edi": uc.UC_X86_REG_EDI, + "edx": uc.UC_X86_REG_EDX, + "flags": uc.UC_X86_REG_EFLAGS, + "eip": uc.UC_X86_REG_EIP, + "eiz": uc.UC_X86_REG_EIZ, + "es": uc.UC_X86_REG_ES, + "esi": uc.UC_X86_REG_ESI, + "esp": uc.UC_X86_REG_ESP, + "fpsw": uc.UC_X86_REG_FPSW, + "fs": uc.UC_X86_REG_FS, + "gs": uc.UC_X86_REG_GS, + "ip": uc.UC_X86_REG_IP, + "rax": uc.UC_X86_REG_RAX, + "rbp": uc.UC_X86_REG_RBP, + "rbx": uc.UC_X86_REG_RBX, + "rcx": uc.UC_X86_REG_RCX, + "rdi": uc.UC_X86_REG_RDI, + "rdx": uc.UC_X86_REG_RDX, + "rip": uc.UC_X86_REG_RIP, + "riz": uc.UC_X86_REG_RIZ, + "rsi": uc.UC_X86_REG_RSI, + "rsp": uc.UC_X86_REG_RSP, + "si": uc.UC_X86_REG_SI, + "sil": uc.UC_X86_REG_SIL, + "sp": uc.UC_X86_REG_SP, + "spl": uc.UC_X86_REG_SPL, + "ss": uc.UC_X86_REG_SS, + "cr0": uc.UC_X86_REG_CR0, + "cr1": uc.UC_X86_REG_CR1, + "cr2": uc.UC_X86_REG_CR2, + "cr3": uc.UC_X86_REG_CR3, + "cr4": uc.UC_X86_REG_CR4, + "cr5": uc.UC_X86_REG_CR5, + "cr6": uc.UC_X86_REG_CR6, + "cr7": uc.UC_X86_REG_CR7, + "cr8": uc.UC_X86_REG_CR8, + "cr9": uc.UC_X86_REG_CR9, + "cr10": uc.UC_X86_REG_CR10, + "cr11": uc.UC_X86_REG_CR11, + "cr12": uc.UC_X86_REG_CR12, + "cr13": uc.UC_X86_REG_CR13, + "cr14": uc.UC_X86_REG_CR14, + "cr15": uc.UC_X86_REG_CR15, + "dr0": uc.UC_X86_REG_DR0, + "dr1": uc.UC_X86_REG_DR1, + "dr2": uc.UC_X86_REG_DR2, + "dr3": uc.UC_X86_REG_DR3, + "dr4": uc.UC_X86_REG_DR4, + "dr5": uc.UC_X86_REG_DR5, + "dr6": uc.UC_X86_REG_DR6, + "dr7": uc.UC_X86_REG_DR7, + "dr8": uc.UC_X86_REG_DR8, + "dr9": uc.UC_X86_REG_DR9, + "dr10": uc.UC_X86_REG_DR10, + "dr11": uc.UC_X86_REG_DR11, + "dr12": uc.UC_X86_REG_DR12, + "dr13": uc.UC_X86_REG_DR13, + "dr14": uc.UC_X86_REG_DR14, + "dr15": uc.UC_X86_REG_DR15, + "fp0": uc.UC_X86_REG_FP0, + "fp1": uc.UC_X86_REG_FP1, + "fp2": uc.UC_X86_REG_FP2, + "fp3": uc.UC_X86_REG_FP3, + "fp4": uc.UC_X86_REG_FP4, + "fp5": uc.UC_X86_REG_FP5, + "fp6": uc.UC_X86_REG_FP6, + "fp7": uc.UC_X86_REG_FP7, + "k0": uc.UC_X86_REG_K0, + "k1": uc.UC_X86_REG_K1, + "k2": uc.UC_X86_REG_K2, + "k3": uc.UC_X86_REG_K3, + "k4": uc.UC_X86_REG_K4, + "k5": uc.UC_X86_REG_K5, + "k6": uc.UC_X86_REG_K6, + "k7": uc.UC_X86_REG_K7, + "mm0": uc.UC_X86_REG_MM0, + "mm1": uc.UC_X86_REG_MM1, + "mm2": uc.UC_X86_REG_MM2, + "mm3": uc.UC_X86_REG_MM3, + "mm4": uc.UC_X86_REG_MM4, + "mm5": uc.UC_X86_REG_MM5, + "mm6": uc.UC_X86_REG_MM6, + "mm7": uc.UC_X86_REG_MM7, + "r8": uc.UC_X86_REG_R8, + "r9": uc.UC_X86_REG_R9, + "r10": uc.UC_X86_REG_R10, + "r11": uc.UC_X86_REG_R11, + "r12": uc.UC_X86_REG_R12, + "r13": uc.UC_X86_REG_R13, + "r14": uc.UC_X86_REG_R14, + "r15": uc.UC_X86_REG_R15, + "st(0)": uc.UC_X86_REG_ST0, + "st(1)": uc.UC_X86_REG_ST1, + "st(2)": uc.UC_X86_REG_ST2, + "st(3)": uc.UC_X86_REG_ST3, + "st(4)": uc.UC_X86_REG_ST4, + "st(5)": uc.UC_X86_REG_ST5, + "st(6)": uc.UC_X86_REG_ST6, + "st(7)": uc.UC_X86_REG_ST7, + "xmm0": uc.UC_X86_REG_XMM0, + "xmm1": uc.UC_X86_REG_XMM1, + "xmm2": uc.UC_X86_REG_XMM2, + "xmm3": uc.UC_X86_REG_XMM3, + "xmm4": uc.UC_X86_REG_XMM4, + "xmm5": uc.UC_X86_REG_XMM5, + "xmm6": uc.UC_X86_REG_XMM6, + "xmm7": uc.UC_X86_REG_XMM7, + "xmm8": uc.UC_X86_REG_XMM8, + "xmm9": uc.UC_X86_REG_XMM9, + "xmm10": uc.UC_X86_REG_XMM10, + "xmm11": uc.UC_X86_REG_XMM11, + "xmm12": uc.UC_X86_REG_XMM12, + "xmm13": uc.UC_X86_REG_XMM13, + "xmm14": uc.UC_X86_REG_XMM14, + "xmm15": uc.UC_X86_REG_XMM15, + "xmm16": uc.UC_X86_REG_XMM16, + "xmm17": uc.UC_X86_REG_XMM17, + "xmm18": uc.UC_X86_REG_XMM18, + "xmm19": uc.UC_X86_REG_XMM19, + "xmm20": uc.UC_X86_REG_XMM20, + "xmm21": uc.UC_X86_REG_XMM21, + "xmm22": uc.UC_X86_REG_XMM22, + "xmm23": uc.UC_X86_REG_XMM23, + "xmm24": uc.UC_X86_REG_XMM24, + "xmm25": uc.UC_X86_REG_XMM25, + "xmm26": uc.UC_X86_REG_XMM26, + "xmm27": uc.UC_X86_REG_XMM27, + "xmm28": uc.UC_X86_REG_XMM28, + "xmm29": uc.UC_X86_REG_XMM29, + "xmm30": uc.UC_X86_REG_XMM30, + "xmm31": uc.UC_X86_REG_XMM31, + "ymm0": uc.UC_X86_REG_YMM0, + "ymm1": uc.UC_X86_REG_YMM1, + "ymm2": uc.UC_X86_REG_YMM2, + "ymm3": uc.UC_X86_REG_YMM3, + "ymm4": uc.UC_X86_REG_YMM4, + "ymm5": uc.UC_X86_REG_YMM5, + "ymm6": uc.UC_X86_REG_YMM6, + "ymm7": uc.UC_X86_REG_YMM7, + "ymm8": uc.UC_X86_REG_YMM8, + "ymm9": uc.UC_X86_REG_YMM9, + "ymm10": uc.UC_X86_REG_YMM10, + "ymm11": uc.UC_X86_REG_YMM11, + "ymm12": uc.UC_X86_REG_YMM12, + "ymm13": uc.UC_X86_REG_YMM13, + "ymm14": uc.UC_X86_REG_YMM14, + "ymm15": uc.UC_X86_REG_YMM15, + "ymm16": uc.UC_X86_REG_YMM16, + "ymm17": uc.UC_X86_REG_YMM17, + "ymm18": uc.UC_X86_REG_YMM18, + "ymm19": uc.UC_X86_REG_YMM19, + "ymm20": uc.UC_X86_REG_YMM20, + "ymm21": uc.UC_X86_REG_YMM21, + "ymm22": uc.UC_X86_REG_YMM22, + "ymm23": uc.UC_X86_REG_YMM23, + "ymm24": uc.UC_X86_REG_YMM24, + "ymm25": uc.UC_X86_REG_YMM25, + "ymm26": uc.UC_X86_REG_YMM26, + "ymm27": uc.UC_X86_REG_YMM27, + "ymm28": uc.UC_X86_REG_YMM28, + "ymm29": uc.UC_X86_REG_YMM29, + "ymm30": uc.UC_X86_REG_YMM30, + "ymm31": uc.UC_X86_REG_YMM31, + "zmm0": uc.UC_X86_REG_ZMM0, + "zmm1": uc.UC_X86_REG_ZMM1, + "zmm2": uc.UC_X86_REG_ZMM2, + "zmm3": uc.UC_X86_REG_ZMM3, + "zmm4": uc.UC_X86_REG_ZMM4, + "zmm5": uc.UC_X86_REG_ZMM5, + "zmm6": uc.UC_X86_REG_ZMM6, + "zmm7": uc.UC_X86_REG_ZMM7, + "zmm8": uc.UC_X86_REG_ZMM8, + "zmm9": uc.UC_X86_REG_ZMM9, + "zmm10": uc.UC_X86_REG_ZMM10, + "zmm11": uc.UC_X86_REG_ZMM11, + "zmm12": uc.UC_X86_REG_ZMM12, + "zmm13": uc.UC_X86_REG_ZMM13, + "zmm14": uc.UC_X86_REG_ZMM14, + "zmm15": uc.UC_X86_REG_ZMM15, + "zmm16": uc.UC_X86_REG_ZMM16, + "zmm17": uc.UC_X86_REG_ZMM17, + "zmm18": uc.UC_X86_REG_ZMM18, + "zmm19": uc.UC_X86_REG_ZMM19, + "zmm20": uc.UC_X86_REG_ZMM20, + "zmm21": uc.UC_X86_REG_ZMM21, + "zmm22": uc.UC_X86_REG_ZMM22, + "zmm23": uc.UC_X86_REG_ZMM23, + "zmm24": uc.UC_X86_REG_ZMM24, + "zmm25": uc.UC_X86_REG_ZMM25, + "zmm26": uc.UC_X86_REG_ZMM26, + "zmm27": uc.UC_X86_REG_ZMM27, + "zmm28": uc.UC_X86_REG_ZMM28, + "zmm29": uc.UC_X86_REG_ZMM29, + "zmm30": uc.UC_X86_REG_ZMM30, + "zmm31": uc.UC_X86_REG_ZMM31, + "r8b": uc.UC_X86_REG_R8B, + "r9b": uc.UC_X86_REG_R9B, + "r10b": uc.UC_X86_REG_R10B, + "r11b": uc.UC_X86_REG_R11B, + "r12b": uc.UC_X86_REG_R12B, + "r13b": uc.UC_X86_REG_R13B, + "r14b": uc.UC_X86_REG_R14B, + "r15b": uc.UC_X86_REG_R15B, + "r8d": uc.UC_X86_REG_R8D, + "r9d": uc.UC_X86_REG_R9D, + "r10d": uc.UC_X86_REG_R10D, + "r11d": uc.UC_X86_REG_R11D, + "r12d": uc.UC_X86_REG_R12D, + "r13d": uc.UC_X86_REG_R13D, + "r14d": uc.UC_X86_REG_R14D, + "r15d": uc.UC_X86_REG_R15D, + "r8w": uc.UC_X86_REG_R8W, + "r9w": uc.UC_X86_REG_R9W, + "r10w": uc.UC_X86_REG_R10W, + "r11w": uc.UC_X86_REG_R11W, + "r12w": uc.UC_X86_REG_R12W, + "r13w": uc.UC_X86_REG_R13W, + "r14w": uc.UC_X86_REG_R14W, + "r15w": uc.UC_X86_REG_R15W, + "gdtr": uc.UC_X86_REG_GDTR, +} + + +class x86EmuHelper(IEmuHelper): + ip_reg = "eip" + sp_reg = "esp" + fp_reg = "ebp" + regmap = REGMAP + + imp_regs = [ + "eax", + "ebx", + "ecx", + "edx", + "esi", + "edi", + "ebp", + "esp", + "eip", + "flags", + ] + + +class x86_64EmuHelper(IEmuHelper): + ip_reg = "rip" + sp_reg = "rsp" + fp_reg = "rbp" + regmap = REGMAP + + imp_regs = [ + "rax", + "rbx", + "rcx", + "rdx", + "rsi", + "rdi", + "rbp", + "rsp", + "rip", + "flags", + ] diff --git a/src/zelos/emulator/x86_gdt.py b/src/zelos/emulator/x86_gdt.py new file mode 100644 index 0000000..a0a1428 --- /dev/null +++ b/src/zelos/emulator/x86_gdt.py @@ -0,0 +1,124 @@ +# MIT License + +# Copyright (c) 2017 Ryo ICHIKAWA + +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# ====================================================================== + +# Code in this file derived from: +# https://github.com/icchy/tracecorn/blob/master/unitracer/lib/segment.py +import struct + +from zelos.util import align + + +class GDT_32(object): + def __init__(self, memory, gdt_base=0x80000000, size=0x1000): + self.emu = memory.emu + + memory.map(gdt_base, align(size), prot=0x3) + self.emu.set_reg("gdtr", (0, gdt_base, size, 0x0)) + self.gdt_base = gdt_base + self._init_gdt() + + @staticmethod + def _gdt_entry(base, limit, flags): + # 0:15 -> limit 0:15 + # 16:31 -> base 0:15 + # 32:39 -> base 16:23 + # 40:47 -> access + # 48:51 -> limit 16:19 + # 52:55 -> flags + # 56:63 -> base 24:31 + + entry = limit & 0xFFFF + entry |= (base & 0xFFFF) << 16 + entry |= ((base >> 16) & 0xFF) << 32 + entry |= (flags & 0xFF) << 40 + entry |= ((limit >> 16) & 0xF) << 48 + entry |= ((flags >> 8) & 0xF) << 52 + entry |= ((base >> 24) & 0xFF) << 56 + return struct.pack(" rpl + # 2: 2 -> ti + # 3:15 -> index + + sel = rpl + sel |= ti << 2 + sel |= index << 3 + return sel + + def set_entry(self, index, base, limit, flags, ti=0, rpl=3): + emu = self.emu + gdt_base = self.gdt_base + + emu.mem_write( + gdt_base + index * 8, self._gdt_entry(base, limit, flags) + ) + return self._seg_selector(index, ti, rpl) + + # TODO: This probably has different incarnations on different + # architectures (this is x86/x64 specific), also for different OSes + def _init_gdt(self, teb_address=0x7FFDF000): + # cs : 0x0023 (index:4) + flags = self.gdt_entry_flags( + gr=1, sz=1, pr=1, privl=3, ex=1, dc=0, rw=1, ac=1 + ) + selector = self.set_entry(4, 0x0, 0xFFFFFFFF, flags) + self.emu.set_reg("cs", selector) + + # ds, es, gs : 0x002b (index:5) + flags = self.gdt_entry_flags( + gr=1, sz=1, pr=1, privl=3, ex=0, dc=0, rw=1, ac=1 + ) + selector = self.set_entry(5, 0x0, 0xFFFFFFFF, flags) + self.emu.set_reg("ds", selector) + self.emu.set_reg("es", selector) + self.emu.set_reg("gs", selector) + + # ss + flags = self.gdt_entry_flags( + gr=1, sz=1, pr=1, privl=0, ex=0, dc=1, rw=1, ac=1 + ) + selector = self.set_entry(6, 0x0, 0xFFFFFFFF, flags, rpl=0) + self.emu.set_reg("ss", selector) + + # fs : 0x0053 (index:10) + flags = self.gdt_entry_flags( + gr=0, sz=1, pr=1, privl=3, ex=0, dc=0, rw=1, ac=1 + ) # 0x4f3 + selector = self.set_entry(10, teb_address, 0xFFF, flags) + self.emu.set_reg("fs", selector) diff --git a/src/zelos/engine.py b/src/zelos/engine.py new file mode 100644 index 0000000..c19fe58 --- /dev/null +++ b/src/zelos/engine.py @@ -0,0 +1,744 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import functools +import logging +import ntpath +import os + +from collections import namedtuple +from shutil import copyfile +from tempfile import mkstemp + +import unicorn +import verboselogs + +from capstone import ( + CS_ARCH_ARM, + CS_ARCH_MIPS, + CS_ARCH_X86, + CS_GRP_CALL, + CS_MODE_32, + CS_MODE_64, + CS_MODE_ARM, + CS_MODE_BIG_ENDIAN, + CS_MODE_LITTLE_ENDIAN, + CS_MODE_MIPS32, + Cs, +) +from unicorn import UcError + +from zelos import util +from zelos.config_gen import _generate_without_binary, generate_config +from zelos.exceptions import UnsupportedBinaryError, ZelosLoadException +from zelos.file_system import FileSystem +from zelos.hooks import ExceptionHooks, HookManager, HookType, InterruptHooks +from zelos.network import Network +from zelos.plugin import OSPlugins +from zelos.processes import Processes +from zelos.state import State +from zelos.tracer import Tracer +from zelos.triggers import Triggers + + +class Engine: + # Stack size + # we need at least 0x80000 for some malware: + # (ff07b93686aca11c9b3484f43b8b910306f30b52cc1c01638bfc16960038dd75) + STACK_SIZE = 0x90000 + + def __init__(self, config=None, api=None): + self.api = api + self.api.internal_engine = self + if config is None: + config = _generate_without_binary() + if isinstance(config, str): + config = generate_config(config) + self.config = config + # OS plugins place OS-specific, system-wide, functionality + # in engine.zos + + class ZOS(object): + def __init__(self): + pass + + self.zos = ZOS() + + binary = config.filename + + # Set provided arguments + self.original_binary = binary + self.cmdline_args = ( + [] if config.cmdline_args is None else config.cmdline_args + ) + + self.random_file_name = getattr(config, "random_file_name", False) + + self.log_level = getattr(logging, config.log.upper(), None) + if not isinstance(self.log_level, int): + raise ValueError("Invalid log level: %s" % config.log) + + # To get different logs based on the level, read this: + # https://stackoverflow.com/questions/14844970 + # /modifying-logging-message-format-based-on-message- + # logging-level-in-python3 + self._init_logging(self.log_level) + + # If verbose is true, print lots of info, including every + # instruction + self.original_file_name = "" + self.main_module_name = "" + self.main_module = None + + self.date = "2019-02-02" + self.traceon = "" + self.traceoff = "" + + # Handling of the logging + self.verbose = False + self.verbosity = 0 + self.fasttrace_on = False + self.timer = util.Timer() + + self.hook_manager = HookManager(self, self.api) + self.interrupt_handler = InterruptHooks(self.hook_manager, self) + self.exception_handler = ExceptionHooks(self) + self.processes = Processes( + self.hook_manager, + self.interrupt_handler, + self.original_binary, + self.STACK_SIZE, + disableNX=self.config.disableNX, + ) + + self.files = FileSystem(self, self.processes, self.hook_manager) + + self.os_plugins = OSPlugins(self) + + if binary is not None and binary != "": + self.load_executable(binary, entrypoint_override=config.startat) + else: + self._initialize_zelos() # For testing purposes. + + head, tail = ntpath.split(config.filename) + original_filename = tail or ntpath.basename(head) + self.original_file_name = original_filename + self.date = config.date + + if config.fasttrace > 0: + self.fasttrace_on = True + if config.dns > 0: + self.flags_dns = True + + self.set_trace_on(config.traceon) + self.traceoff = config.traceoff + if config.tracethread != "": + self.trace.threads_to_print.add(config.tracethread) + if config.writetrace != "": + target_addr = int(config.writetrace, 16) + self.set_writetrace(target_addr) + if config.memlimit > 0: + self.set_mem_limit(config.memlimit) + for m in config.mount: + try: + arch, dest, src = m.split(",") + # TODO: Use arch, dest to determine where to mount + # For now, always mounts src at default location + if os.path.isdir(src): + self.files.mount_folder(src) + else: + self.files.add_file(src) + except ValueError: + self.logger.error( + f"Incorrectly formatted input to '--mount': {m}" + ) + continue + if config.strace is not None: + self.zos.syscall_manager.set_strace_file(config.strace) + + self.verbosity = config.verbosity + self.set_verbose(config.verbosity > 0) + + def __del__(self): + try: + self.processes.handles.close_all() + except Exception as e: + print("Engine: could not close handles:", e) + + def _init_logging(self, initial_log_level): + if initial_log_level is None: + initial_log_level = verboselogs.logging.INFO + verboselogs.install() + # This will be the parent to all loggers in this project. + logger = verboselogs.logging.getLogger("zelos") + fmt = "{asctime}:{module:_<10.10s}:{levelname:_<6.6s}:{message}" + datefmt = "%H:%M:%S" + try: + import coloredlogs + + coloredlogs.install( + logger=logger, + level=initial_log_level, + fmt=fmt, + datefmt=datefmt, + style="{", + ) + except ModuleNotFoundError: + logger.error("You do not have the required coloredlogs dependency") + console = verboselogs.logging.StreamHandler() + # set a format which is simpler for console use + formatter = verboselogs.logging.Formatter(fmt, datefmt, style="{") + console.setFormatter(formatter) + logger.addHandler(console) + + self.logger = logger + + def set_log_level(self, log_level): + fmt = "{asctime}:{module:_<10.10s}:{levelname:_<6.6s}:{message}" + datefmt = "%H:%M:%S" + try: + import coloredlogs + + coloredlogs.install( + logger=self.logger, + reconfigure=True, + level=log_level, + fmt=fmt, + datefmt=datefmt, + style="{", + ) + except ModuleNotFoundError: + self.logger.setLevel(log_level) + + def log_api(self, args, isNative=False): + self.trace.api(args, isNative) + + def log_api_dbg(self, args): + self.trace.api_dbg(args) + + def hexdump(self, address: int, size: int) -> None: + import hexdump + + try: + data = self.memory.read(address, size) + hexdump.hexdump(data) + except Exception: + self.logger.exception("Invalid address range.") + + @property + def current_process(self): + return self.processes.current_process + + @property + def emu(self): + return self.current_process.emu + + @property + def memory(self): + return self.current_process.memory + + @property + def scheduler(self): + return self.current_process.scheduler + + @property + def thread_manager(self): + return self.current_process.threads + + @property + def current_thread(self): + return self.current_process.current_thread + + @property + def loader(self): + return self.current_process.loader + + @loader.setter + def loader(self, loader): + self.current_process.loader = loader + + @property + def modules(self): + return self.current_process.modules + + @property + def handles(self): + return self.processes.handles + + def set_mem_limit(self, limit_in_mb: int) -> None: + limit = limit_in_mb * 1024 * 1024 + """ Sets the memory limit for the python process""" + try: + import resource + + soft, hard = resource.getrlimit(resource.RLIMIT_AS) + resource.setrlimit(resource.RLIMIT_AS, (limit, hard)) + except ModuleNotFoundError: + self.logger.error("Unable to set memory limit in Windows") + + def set_writetrace(self, target): + def hook(zelos, access, address, size, value): + if address == target: + self.logger.error( + "[WRITE 0x%x] EIP: 0x%x: value 0x%x" + % (address, self.current_thread.getIP(), value) + ) + + self.hook_manager.register_mem_hook( + HookType.MEMORY.WRITE, hook, name="write_trace" + ) + + def _first_parse(self, module_path, random_file_name=False): + """ Function to parse an executable """ + + if random_file_name: + self.original_file_name = module_path + original_file_name = module_path + # To ensure we don't get any issues with the size of the + # file name, we copy the file and rename it 'target' + fd, temp_path = mkstemp(dir=".", suffix=".xex") + os.close(fd) + temp_filename = os.path.basename(temp_path) + copyfile(module_path, temp_filename) + module_path = temp_filename + self.hook_manager.register_close_hook( + functools.partial(os.remove, temp_filename) + ) + self.logger.debug( + f"Setting random file name for " + f"{original_file_name} : {module_path}" + ) + + self.logger.verbose("Parse Main Module") + + with open(module_path, "rb") as f: + file_data = bytearray(f.read()) + if file_data.startswith(b"ZENC"): + file_data = util.in_mem_decrypt(file_data) + + return self._parse_file_data(module_path, file_data) + + def parse_file(self, filename): + with open(filename, "rb") as f: + file_data = bytearray(f.read()) + return self._parse_file_data(filename, file_data) + + def _parse_file_data(self, filename, filedata): + parsed_file = self.os_plugins.parse(filename, filedata) + + if parsed_file is not None: + assert len(parsed_file.Data) > 0, "File has no data" + return parsed_file + raise UnsupportedBinaryError(f"{filename} is unsupported file format") + + def load_executable(self, module_path, entrypoint_override=None): + """ + This method simply loads the executable, without starting the + emulation + """ + + original_file_name = os.path.basename(module_path) + self.original_file_name = original_file_name + + file = self._first_parse( + module_path, random_file_name=self.random_file_name + ) + + module_path = file.Filepath + self.main_module = file + self._initialize_zelos(file) + + self.os_plugins.load( + file, self.current_process, entrypoint_override=entrypoint_override + ) + + # TODO: don't let this be in loader and zelos + self.main_module_name = self.loader.main_module_name + + # We need to create this file in the file system, so that other + # files can access it. + self.files.create_file(self.files.zelos_file_prefix + module_path) + + # If you remove one of the hooks on _hook_code, be careful that + # you don't break the ability to stop a running emulation + if self.verbose: + self.set_hook_granularity(HookType.EXEC.INST) + + def _initialize_zelos(self, binary=None): + self.state = State(self, binary, self.date) + + cs_arch_mode_sm_dict = { + "x86": (CS_ARCH_X86, CS_MODE_32), + "x86_64": (CS_ARCH_X86, CS_MODE_64), + "arm": (CS_ARCH_ARM, CS_MODE_ARM), + "mips": (CS_ARCH_MIPS, CS_MODE_MIPS32), + } + + arch = self.state.arch + (cs_arch, cs_mode) = cs_arch_mode_sm_dict[arch] + + endianness = self.state.endianness + if endianness == "little": + cs_mode |= CS_MODE_LITTLE_ENDIAN + elif endianness == "big": + cs_mode |= CS_MODE_BIG_ENDIAN + else: + raise ZelosLoadException(f"Unsupported endianness {endianness}") + self.cs = Cs(cs_arch, cs_mode) + self.cs.detail = True + + self.logger.debug( + f"Initialized {arch} {self.state.bits} emulator/disassembler" + ) + + self.last_instruction = None + self.last_instruction_size = None + self.should_print_last_instruction = True + + self.triggers = Triggers(self) + self.processes.set_architecture(self.state) + + self.network = Network(self.helpers, self.files, None) + + self.processes._create_first_process(self.main_module_name) + p = self.current_process + p.cmdline_args = self.cmdline_args + p.environment_variables = self.config.env_vars + p.virtual_filename = self.config.virtual_filename + p.virtual_path = self.config.virtual_path + + if hasattr(unicorn.unicorn, "WITH_ZEROPOINT_PATCH"): + + def process_switch_wrapper(*args, **kwargs): + # Block count interrupt. Fires every 2^N blocks executed + # Use this as an opportunity to swap threads. + self.logger.info(">>> Tracing Thread Swap Opportunity") + self.processes.schedule_next() + + self.interrupt_handler.register_interrupt_handler( + 0xF8F8F8F8, process_switch_wrapper + ) + + if self.config.filename is not None and self.config.filename != "": + if ( + self.config.virtual_filename is not None + and self.config.virtual_filename != "" + ): + self.files.add_file( + self.config.filename, self.config.virtual_filename + ) + else: + self.files.add_file(self.config.filename) + self.trace = Tracer(self.helpers, self, self.cs, self.modules) + + # TODO: SharedSection needs to be removed + self.processes.handles.new("section", "\\Windows\\SharedSection") + + @property + def helpers(self): + """ + Helpers are the first layer in the components hierarchy, which + mainly deal with providing help to developers. + """ + helpers_class = namedtuple( + "Helpers", ["handles", "triggers", "state", "processes"] + ) + return helpers_class( + self.handles, self.triggers, self.state, self.processes + ) + + def load_library(self, module_name): + binary, _ = self.loader._load_module(module_name, depth=1) + return binary + + def disas(self, address: int, size: int): + """ + Disassemble code at the given address, for up to size bytes + """ + code = self.memory.read(address, size) + return [insn for insn in self.cs.disasm(bytes(code), address)] + + def step(self, count: int = 1) -> None: + """ Steps one assembly level instruction """ + self.start(count=count, swap_threads=False) + if self.last_instruction is not None: + self.trace.bb(self.last_instruction, self.last_instruction_size) + else: + self.trace.bb() + + def step_over(self, count: int = 1) -> None: + """ + Steps on assembly level instruction up to count instructions + """ + for i in range(count): + if not self._step_over(): + return + + def _step_over(self): + """Returns True if it successfully stepped.""" + max_inst_size = 15 + insts = self.disas(self.emu.getIP(), max_inst_size) + if len(insts) == 0: + self.logger.notice(f"Unable to disassemble 0x{self.emu.getIP():x}") + return False + i = insts[0] + if insts[0].group(CS_GRP_CALL): + self.plugins.runner.run_to_addr(i.address + i.size) + else: + self.step() + return True + + def start(self, count=0, timeout=0, swap_threads=True) -> None: + """ + Starts execution of the program at the given offset or entry + point. + """ + if timeout > 0: + self.timer.begin(timeout) + + def timeout_hook(zelos, addr, size): + self._check_timeout() + + # TODO: Delete timeout hook after timeout is triggered. + self.hook_manager.register_exec_hook( + HookType.EXEC.BLOCK, timeout_hook, name="timeout_hook" + ) + + if self.processes.num_active_processes() == 0: + self.processes.logger.info( + "No more processes or threads to execute." + ) + return + + self.ehCount = 0 + + # Main emulated execution loop + while self._should_continue(): + if self.current_thread is None: + self.processes.swap_with_next_thread() + + self.last_instruction = self.emu.getIP() + self.last_instruction_size = 1 + try: + if self.processes.num_active_processes() == 0: + self.processes.logger.info( + "No more processes or threads to execute." + ) + else: + # Execute until emulator exception + self._run(self.current_process, count) + except UcError as e: + # TODO: This is a special case for forcing a stop. + # Sometimes setting a stop reason doesn't stop + # execution (especially when changingEIP). + # This is a hack. Fix me plz + if self.current_thread is not None and not ( + self.emu.getIP() == 0x30 + and "kill thread" in self.scheduler.end_reasons + ): + self.exception_handler.handle_exception(e) + + # If we get here and there are no end_reasons this is + # because emu ended early. If we have swap thread set, this + # is because this is a signal to zelos to swap threads. + # Otherwise, this is a signal that execution is over + # (for example, stepping) + if not self.scheduler._has_end_reasons(): + if not swap_threads: + break + self.processes.swap_with_next_thread() + + return + + def _run(self, p, count): + t = p.current_thread + assert ( + t is not None + ), "Current thread is None. Something has gone horribly wrong." + + t.emu.is_running = True + try: + t.emu.emu_start(t.getIP(), 0, count=count) + finally: + stop_addr = p.threads.scheduler._pop_stop_addr(t.id) + t.emu.is_running = False + + # Only set the stop addr if you stopped benignly + if stop_addr is not None: + t.setIP(stop_addr) + + def _should_continue(self): + """ + Takes the reasons for ending unicorn execution, and decides + whether to continue or end execution + """ + + if self.current_thread is None: + self.processes.swap_with_next_thread() + + if self.scheduler._resolve_end_reasons() is False: + return False + + if self.processes.num_active_processes() == 0: + return False + + # Keep running unless told otherwise. + return True + + def close(self) -> None: + """ Handles the end of the run command """ + for closure in self.hook_manager._get_hooks(HookType._OTHER.CLOSE): + try: + closure() + except Exception: + self.logger.exception("Exception while trying to close Zelos") + + def _dbgprint(self, address): + service = self.emu.get_reg("eax") + if service == 1: # DbgPrint functionality + length = self.emu.get_reg("edx") + buffer = self.emu.get_reg("ecx") + buffer_s = self.memory.read_string(buffer, length) + print("[DBGPRINT SYSCALL] {0}".format(buffer_s)) + else: + self.logger.info( + ">>> Tracing DebugService at 0x%x Routine 0x%x" + % (address, service) + ) + + def set_trace_on(self, val): + try: + i = int(val, 0) + + def f(zelos, address, size): + self.verbosity = 2 # Allow logging inside modules + self.set_verbose(True) + + self.hook_manager.register_exec_hook( + HookType.EXEC.INST, f, name="traceon", ip_low=i, ip_high=i + ) + except ValueError: + pass + self.traceon = val + + def _check_timeout(self): + if self.timer.is_timed_out(): + self.scheduler.stop("timeout") + + # Hook invoked for each instruction or block. + def _hook_code(self, zelos, address, size): + try: + self._hook_code_impl(zelos, address, size) + self._check_timeout() + except Exception: + if self.current_thread is not None: + self.current_process.threads.kill_thread( + self.current_thread.id + ) + self.logger.exception("Stopping execution due to exception") + + def _hook_code_impl(self, zelos, address, size): + current_process = self.current_process + current_thread = self.current_thread + # TCG Dump example usage: + # self.emu.get_tcg(0, 0) + if current_thread is None: + self.emu.emu_stop() + return + + # Log the total number of blocks executed per thread. Swap + # threads if the specified number of blocks is exceeded and + # other threads exist + current_thread.total_blocks_executed += 1 + if ( + current_thread.total_blocks_executed % 1000 == 0 + and address not in self.modules.reverse_module_functions + ): + self.current_process.scheduler.stop_and_exec( + "process swap", self.processes.schedule_next + ) + return + + if self.verbose: + if self.should_print_last_instruction: # Print block + # Turn on full trace to do trace comparison + self.trace.bb( + self.last_instruction, + self.last_instruction_size, + full_trace=False, + ) + self.should_print_last_instruction = True + if ( + self.fasttrace_on + and current_process.threads.block_seen_before(address) + ): + self.should_print_last_instruction = False + + current_process.threads.record_block(address) + + self.last_instruction = address + self.last_instruction_size = size + + def set_verbose(self, should_set_verbose: bool) -> None: + """ + Used to set the verbosity level, and change the hooks. + This prevents two types of issues: + + 1) Running block hooks when printing individual instructions + This will cause the annotations that are printed to be + the values at the end of the block's execution + 2) Running instruction hooks when not printing instructions + This will slow down the emulation (sometimes + considerably) + """ + if self.verbose == should_set_verbose: + return + self.verbose = should_set_verbose + + if should_set_verbose: + self.set_hook_granularity(HookType.EXEC.INST) + else: + self.set_hook_granularity(HookType.EXEC.BLOCK) + + def set_hook_granularity(self, granularity: HookType.EXEC): + try: + self.hook_manager.delete_hook(self._code_hook_info) + except AttributeError: + pass # first time setting _code_hook_info + + self._code_hook_info = self.hook_manager.register_exec_hook( + granularity, self._hook_code, name="code_hook" + ) + + # Estimates the number of function arguments with the assumption + # that the callee is responsible for cleaning up the stack. + # Disassembles insts linearly until a RETN instruction is + # encountered. The RETN operand indicates the number of stack bytes + # the caller had pushed as arguments. + + def _estimate_function_stack_adjustment(self, function_start_address): + address = function_start_address + while True: + code = self.emu.mem_read(address, 1000) + for insn in self.cs.disasm(str(code), address): + if insn.mnemonic != "ret": + address += insn.size + continue + if len(insn.operands) == 0: + return 0 # no stack adjustment + # imm bytes popped by this function + return insn.operands[0].imm diff --git a/src/zelos/enums.py b/src/zelos/enums.py new file mode 100644 index 0000000..21edfc2 --- /dev/null +++ b/src/zelos/enums.py @@ -0,0 +1,110 @@ +from enum import Enum, IntEnum, auto + +from unicorn import ( + UC_PROT_ALL, + UC_PROT_EXEC, + UC_PROT_NONE, + UC_PROT_READ, + UC_PROT_WRITE, +) + + +class ProtType(IntEnum): + NONE = UC_PROT_NONE + READ = UC_PROT_READ + WRITE = UC_PROT_WRITE + EXEC = UC_PROT_EXEC + RWX = UC_PROT_ALL + RX = UC_PROT_READ | UC_PROT_EXEC + RW = UC_PROT_READ | UC_PROT_WRITE + + +class HookType: + class MEMORY(Enum): + """ + Used by :py:meth:`zelos.Zelos.hook_memory` to specify the + memory event to hook on. View the registration function for more + details. + """ + + READ = auto() + WRITE = auto() + READ_UNMAPPED = auto() + WRITE_UNMAPPED = auto() + READ_PROT = auto() + WRITE_PROT = auto() + READ_AFTER = auto() + UNMAPPED = auto() + PROT = auto() + READ_INVALID = auto() + WRITE_INVALID = auto() + INVALID = auto() + VALID = auto() + + class EXEC(Enum): + """ + Used by :py:meth:`zelos.Zelos.hook_execution`. + If INST is chosen, the registered hook will be executed every + time a single instruction is executed. + + If BLOCK is chosen, the registered hook will be executed after + every block of instructions is executed. A block is interpreted + as a contiguous sequence of code where only the last instruction + can modify control flow, typically a branch or return + instruction. + + View the registration function for more details. + """ + + INST = auto() + BLOCK = auto() + + class THREAD(Enum): + """ + Not usable yet through Zelos API + """ + + CREATE = auto() + SWAP = auto() + DESTROY = auto() + + class PROCESS(Enum): + """ + Not usable yet through Zelos API + """ + + CREATE = auto() + SWAP = auto() + DESTROY = auto() + + class SYSCALL(Enum): + """ + Used by :py:meth:`zelos.Zelos.hook_syscalls`. + + If AFTER is chosen, the hook will be triggered after the syscall + hass been executed. + + View the registration function for more details. + """ + + AFTER = auto() + # TODO: support BEFORE to allow conditionally executing syscall. + # BEFORE = auto() + + class _INST(Enum): + """ + HookTypes used for triggering on specific instructions. These + are intended for internal use only. + """ + + X86_SYSCALL = auto() + + class _OTHER(Enum): + """ + HookTypes that do not need to be specified since they have no + options. Only used internally. + """ + + CLOSE = auto() + INTERRUPT = auto() + EXCEPTION = auto() diff --git a/src/zelos/exceptions.py b/src/zelos/exceptions.py new file mode 100644 index 0000000..4e7dc48 --- /dev/null +++ b/src/zelos/exceptions.py @@ -0,0 +1,44 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + + +class ZelosException(Exception): + pass + + +class InvalidRegException(ZelosException): + pass + + +class ZelosLoadException(ZelosException): + pass + + +class ZelosRuntimeException(ZelosException): + pass + + +class InvalidHookTypeException(ZelosException): + pass + + +class UnsupportedBinaryError(ZelosException): + pass + + +class OutOfMemoryException(Exception): + pass diff --git a/src/zelos/ext/env/linux-armv7/etc/hosts b/src/zelos/ext/env/linux-armv7/etc/hosts new file mode 100644 index 0000000..ba712fe --- /dev/null +++ b/src/zelos/ext/env/linux-armv7/etc/hosts @@ -0,0 +1 @@ +127.0.0.1 localhost diff --git a/src/zelos/ext/env/linux-armv7/etc/resolv.conf b/src/zelos/ext/env/linux-armv7/etc/resolv.conf new file mode 100644 index 0000000..54f0edd --- /dev/null +++ b/src/zelos/ext/env/linux-armv7/etc/resolv.conf @@ -0,0 +1,2 @@ +nameserver 8.8.8.8 +options edns0 diff --git a/src/zelos/ext/env/linux-armv7/lib/ld-linux-armhf.so.3 b/src/zelos/ext/env/linux-armv7/lib/ld-linux-armhf.so.3 new file mode 100644 index 0000000..f359021 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/ld-linux-armhf.so.3 differ diff --git a/src/zelos/ext/env/linux-armv7/lib/ld-linux.so.3 b/src/zelos/ext/env/linux-armv7/lib/ld-linux.so.3 new file mode 100644 index 0000000..3749f81 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/ld-linux.so.3 differ diff --git a/src/zelos/ext/env/linux-armv7/lib/ld-uClibc-0.9.33.2.so b/src/zelos/ext/env/linux-armv7/lib/ld-uClibc-0.9.33.2.so new file mode 100644 index 0000000..05eed01 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/ld-uClibc-0.9.33.2.so differ diff --git a/src/zelos/ext/env/linux-armv7/lib/ld-uClibc-1.0.31.so b/src/zelos/ext/env/linux-armv7/lib/ld-uClibc-1.0.31.so new file mode 100644 index 0000000..66c3bbd Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/ld-uClibc-1.0.31.so differ diff --git a/src/zelos/ext/env/linux-armv7/lib/ld-uClibc.so b/src/zelos/ext/env/linux-armv7/lib/ld-uClibc.so new file mode 100644 index 0000000..d576efd Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/ld-uClibc.so differ diff --git a/src/zelos/ext/env/linux-armv7/lib/ld-uClibc.so.0 b/src/zelos/ext/env/linux-armv7/lib/ld-uClibc.so.0 new file mode 100644 index 0000000..66c3bbd Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/ld-uClibc.so.0 differ diff --git a/src/zelos/ext/env/linux-armv7/lib/ld-uClibc.so.1 b/src/zelos/ext/env/linux-armv7/lib/ld-uClibc.so.1 new file mode 100644 index 0000000..66c3bbd Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/ld-uClibc.so.1 differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libc++.so b/src/zelos/ext/env/linux-armv7/lib/libc++.so new file mode 100644 index 0000000..93b2851 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libc++.so differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libc.so b/src/zelos/ext/env/linux-armv7/lib/libc.so new file mode 100644 index 0000000..3ec5224 --- /dev/null +++ b/src/zelos/ext/env/linux-armv7/lib/libc.so @@ -0,0 +1,6 @@ +/* GNU ld script + * Use the shared library, but some functions are only in + * the static library, so try that secondarily. */ +OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", + "elf32-littlearm") +GROUP ( libc.so.1 uclibc_nonshared.a libpthread_nonshared.a AS_NEEDED ( ld-uClibc.so.1 ) ) diff --git a/src/zelos/ext/env/linux-armv7/lib/libc.so.0 b/src/zelos/ext/env/linux-armv7/lib/libc.so.0 new file mode 100644 index 0000000..074adf4 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libc.so.0 differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libc.so.1 b/src/zelos/ext/env/linux-armv7/lib/libc.so.1 new file mode 100644 index 0000000..074adf4 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libc.so.1 differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libc.so.6 b/src/zelos/ext/env/linux-armv7/lib/libc.so.6 new file mode 100644 index 0000000..a590680 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libc.so.6 differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libcrypt.so.0 b/src/zelos/ext/env/linux-armv7/lib/libcrypt.so.0 new file mode 100644 index 0000000..28a0952 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libcrypt.so.0 differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libdl.so b/src/zelos/ext/env/linux-armv7/lib/libdl.so new file mode 100644 index 0000000..4ff2512 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libdl.so differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libgcc_s.so.1 b/src/zelos/ext/env/linux-armv7/lib/libgcc_s.so.1 new file mode 100644 index 0000000..3a33d93 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libgcc_s.so.1 differ diff --git a/src/zelos/ext/env/linux-armv7/lib/liblog.so b/src/zelos/ext/env/linux-armv7/lib/liblog.so new file mode 100644 index 0000000..2c0c8af Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/liblog.so differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libm.so b/src/zelos/ext/env/linux-armv7/lib/libm.so new file mode 100644 index 0000000..170342e Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libm.so differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libm.so.0 b/src/zelos/ext/env/linux-armv7/lib/libm.so.0 new file mode 100644 index 0000000..302e097 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libm.so.0 differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libstdc++.so b/src/zelos/ext/env/linux-armv7/lib/libstdc++.so new file mode 100644 index 0000000..950b94f Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libstdc++.so differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libuClibc-1.0.31.so b/src/zelos/ext/env/linux-armv7/lib/libuClibc-1.0.31.so new file mode 100644 index 0000000..074adf4 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libuClibc-1.0.31.so differ diff --git a/src/zelos/ext/env/linux-armv7/lib/libz.so b/src/zelos/ext/env/linux-armv7/lib/libz.so new file mode 100644 index 0000000..7b2d603 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/lib/libz.so differ diff --git a/src/zelos/ext/env/linux-armv7/usr/lib/README.md b/src/zelos/ext/env/linux-armv7/usr/lib/README.md new file mode 100644 index 0000000..03acc30 --- /dev/null +++ b/src/zelos/ext/env/linux-armv7/usr/lib/README.md @@ -0,0 +1 @@ +These files were retrieved from the netgear-r9000-arm firmware given to V by Kevin over slack on Aug 21 diff --git a/src/zelos/ext/env/linux-armv7/usr/lib/libconfig.so b/src/zelos/ext/env/linux-armv7/usr/lib/libconfig.so new file mode 100644 index 0000000..c514f75 Binary files /dev/null and b/src/zelos/ext/env/linux-armv7/usr/lib/libconfig.so differ diff --git a/src/zelos/ext/env/linux-mips/etc/hosts b/src/zelos/ext/env/linux-mips/etc/hosts new file mode 100644 index 0000000..ba712fe --- /dev/null +++ b/src/zelos/ext/env/linux-mips/etc/hosts @@ -0,0 +1 @@ +127.0.0.1 localhost diff --git a/src/zelos/ext/env/linux-mips/etc/resolv.conf b/src/zelos/ext/env/linux-mips/etc/resolv.conf new file mode 100644 index 0000000..54f0edd --- /dev/null +++ b/src/zelos/ext/env/linux-mips/etc/resolv.conf @@ -0,0 +1,2 @@ +nameserver 8.8.8.8 +options edns0 diff --git a/src/zelos/ext/env/linux-x86-64/etc/hosts b/src/zelos/ext/env/linux-x86-64/etc/hosts new file mode 100644 index 0000000..ba712fe --- /dev/null +++ b/src/zelos/ext/env/linux-x86-64/etc/hosts @@ -0,0 +1 @@ +127.0.0.1 localhost diff --git a/src/zelos/ext/env/linux-x86-64/etc/resolv.conf b/src/zelos/ext/env/linux-x86-64/etc/resolv.conf new file mode 100644 index 0000000..54f0edd --- /dev/null +++ b/src/zelos/ext/env/linux-x86-64/etc/resolv.conf @@ -0,0 +1,2 @@ +nameserver 8.8.8.8 +options edns0 diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/ld-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/ld-2.27.so new file mode 100644 index 0000000..56b3340 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/ld-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/ld-linux.so.2 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/ld-linux.so.2 new file mode 100644 index 0000000..56b3340 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/ld-linux.so.2 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libBrokenLocale-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libBrokenLocale-2.27.so new file mode 100644 index 0000000..9013219 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libBrokenLocale-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libBrokenLocale.so.1 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libBrokenLocale.so.1 new file mode 100644 index 0000000..9013219 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libBrokenLocale.so.1 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libSegFault.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libSegFault.so new file mode 100644 index 0000000..5b73ba3 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libSegFault.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libanl-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libanl-2.27.so new file mode 100644 index 0000000..29094aa Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libanl-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libanl.so.1 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libanl.so.1 new file mode 100644 index 0000000..29094aa Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libanl.so.1 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libc-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libc-2.27.so new file mode 100644 index 0000000..7813976 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libc-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libc.so.6 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libc.so.6 new file mode 100644 index 0000000..7813976 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libc.so.6 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcidn-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcidn-2.27.so new file mode 100644 index 0000000..33e6e17 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcidn-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcidn.so.1 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcidn.so.1 new file mode 100644 index 0000000..33e6e17 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcidn.so.1 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcrypt-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcrypt-2.27.so new file mode 100644 index 0000000..1cd4114 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcrypt-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcrypt.so.1 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcrypt.so.1 new file mode 100644 index 0000000..1cd4114 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libcrypt.so.1 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libdl-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libdl-2.27.so new file mode 100644 index 0000000..8c3b881 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libdl-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libdl.so.2 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libdl.so.2 new file mode 100644 index 0000000..8c3b881 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libdl.so.2 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libgcc_s.so.1 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libgcc_s.so.1 new file mode 100644 index 0000000..27b0548 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libgcc_s.so.1 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libm-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libm-2.27.so new file mode 100644 index 0000000..80f220a Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libm-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libm.so.6 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libm.so.6 new file mode 100644 index 0000000..80f220a Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libm.so.6 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libmemusage.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libmemusage.so new file mode 100644 index 0000000..fe93817 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libmemusage.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnsl-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnsl-2.27.so new file mode 100644 index 0000000..1df973d Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnsl-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnsl.so.1 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnsl.so.1 new file mode 100644 index 0000000..1df973d Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnsl.so.1 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_compat-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_compat-2.27.so new file mode 100644 index 0000000..021497d Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_compat-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_compat.so.2 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_compat.so.2 new file mode 100644 index 0000000..021497d Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_compat.so.2 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_dns-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_dns-2.27.so new file mode 100644 index 0000000..87efa07 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_dns-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_dns.so.2 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_dns.so.2 new file mode 100644 index 0000000..87efa07 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_dns.so.2 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_files-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_files-2.27.so new file mode 100644 index 0000000..03673ba Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_files-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_files.so.2 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_files.so.2 new file mode 100644 index 0000000..03673ba Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_files.so.2 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_hesiod-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_hesiod-2.27.so new file mode 100644 index 0000000..bb8396a Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_hesiod-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_hesiod.so.2 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_hesiod.so.2 new file mode 100644 index 0000000..bb8396a Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_hesiod.so.2 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nis-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nis-2.27.so new file mode 100644 index 0000000..c3e89f9 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nis-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nis.so.2 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nis.so.2 new file mode 100644 index 0000000..c3e89f9 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nis.so.2 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nisplus-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nisplus-2.27.so new file mode 100644 index 0000000..c77b5a5 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nisplus-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nisplus.so.2 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nisplus.so.2 new file mode 100644 index 0000000..c77b5a5 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libnss_nisplus.so.2 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpcprofile.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpcprofile.so new file mode 100644 index 0000000..b5eb2a4 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpcprofile.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpcre.so.3 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpcre.so.3 new file mode 100644 index 0000000..fb8b142 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpcre.so.3 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpthread-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpthread-2.27.so new file mode 100644 index 0000000..82921ae Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpthread-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpthread.so.0 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpthread.so.0 new file mode 100644 index 0000000..82921ae Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libpthread.so.0 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libresolv-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libresolv-2.27.so new file mode 100644 index 0000000..7099bbd Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libresolv-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libresolv.so.2 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libresolv.so.2 new file mode 100644 index 0000000..7099bbd Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libresolv.so.2 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/librt-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/librt-2.27.so new file mode 100644 index 0000000..4fdafe3 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/librt-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/librt.so.1 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/librt.so.1 new file mode 100644 index 0000000..4fdafe3 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/librt.so.1 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libselinux.so.1 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libselinux.so.1 new file mode 100644 index 0000000..258111f Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libselinux.so.1 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libstdc++.so.6 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libstdc++.so.6 new file mode 100644 index 0000000..f26626c Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libstdc++.so.6 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libthread_db-1.0.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libthread_db-1.0.so new file mode 100644 index 0000000..5acffb1 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libthread_db-1.0.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libthread_db.so.1 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libthread_db.so.1 new file mode 100644 index 0000000..5acffb1 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libthread_db.so.1 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libutil-2.27.so b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libutil-2.27.so new file mode 100644 index 0000000..88b82f7 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libutil-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libutil.so.1 b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libutil.so.1 new file mode 100644 index 0000000..88b82f7 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/i386-linux-gnu/libutil.so.1 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib/x86_64-linux-gnu/libc.so.6 b/src/zelos/ext/env/linux-x86-64/lib/x86_64-linux-gnu/libc.so.6 new file mode 100644 index 0000000..f5b26e3 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib/x86_64-linux-gnu/libc.so.6 differ diff --git a/src/zelos/ext/env/linux-x86-64/lib64/ld-linux-x86-64.so.2 b/src/zelos/ext/env/linux-x86-64/lib64/ld-linux-x86-64.so.2 new file mode 100644 index 0000000..a8cb029 Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/lib64/ld-linux-x86-64.so.2 differ diff --git a/src/zelos/ext/env/linux-x86-64/usr/lib/i386-linux-gnu/libstdc++.so.6 b/src/zelos/ext/env/linux-x86-64/usr/lib/i386-linux-gnu/libstdc++.so.6 new file mode 100644 index 0000000..f26626c Binary files /dev/null and b/src/zelos/ext/env/linux-x86-64/usr/lib/i386-linux-gnu/libstdc++.so.6 differ diff --git a/src/zelos/ext/env/linux-x86/etc/hosts b/src/zelos/ext/env/linux-x86/etc/hosts new file mode 100644 index 0000000..ba712fe --- /dev/null +++ b/src/zelos/ext/env/linux-x86/etc/hosts @@ -0,0 +1 @@ +127.0.0.1 localhost diff --git a/src/zelos/ext/env/linux-x86/etc/ld.so.cache b/src/zelos/ext/env/linux-x86/etc/ld.so.cache new file mode 100644 index 0000000..1f65c4b Binary files /dev/null and b/src/zelos/ext/env/linux-x86/etc/ld.so.cache differ diff --git a/src/zelos/ext/env/linux-x86/etc/resolv.conf b/src/zelos/ext/env/linux-x86/etc/resolv.conf new file mode 100644 index 0000000..54f0edd --- /dev/null +++ b/src/zelos/ext/env/linux-x86/etc/resolv.conf @@ -0,0 +1,2 @@ +nameserver 8.8.8.8 +options edns0 diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/ld-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/ld-2.27.so new file mode 100644 index 0000000..56b3340 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/ld-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/ld-linux.so.2 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/ld-linux.so.2 new file mode 100644 index 0000000..56b3340 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/ld-linux.so.2 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libBrokenLocale-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libBrokenLocale-2.27.so new file mode 100644 index 0000000..9013219 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libBrokenLocale-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libBrokenLocale.so.1 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libBrokenLocale.so.1 new file mode 100644 index 0000000..9013219 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libBrokenLocale.so.1 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libSegFault.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libSegFault.so new file mode 100644 index 0000000..5b73ba3 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libSegFault.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libanl-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libanl-2.27.so new file mode 100644 index 0000000..29094aa Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libanl-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libanl.so.1 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libanl.so.1 new file mode 100644 index 0000000..29094aa Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libanl.so.1 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libc-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libc-2.27.so new file mode 100644 index 0000000..7813976 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libc-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libc.so.6 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libc.so.6 new file mode 100644 index 0000000..7813976 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libc.so.6 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcidn-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcidn-2.27.so new file mode 100644 index 0000000..33e6e17 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcidn-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcidn.so.1 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcidn.so.1 new file mode 100644 index 0000000..33e6e17 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcidn.so.1 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcrypt-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcrypt-2.27.so new file mode 100644 index 0000000..1cd4114 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcrypt-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcrypt.so.1 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcrypt.so.1 new file mode 100644 index 0000000..1cd4114 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libcrypt.so.1 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libdl-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libdl-2.27.so new file mode 100644 index 0000000..8c3b881 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libdl-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libdl.so.2 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libdl.so.2 new file mode 100644 index 0000000..8c3b881 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libdl.so.2 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libgcc_s.so.1 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libgcc_s.so.1 new file mode 100644 index 0000000..27b0548 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libgcc_s.so.1 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libm-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libm-2.27.so new file mode 100644 index 0000000..80f220a Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libm-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libm.so.6 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libm.so.6 new file mode 100644 index 0000000..80f220a Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libm.so.6 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libmemusage.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libmemusage.so new file mode 100644 index 0000000..fe93817 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libmemusage.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnsl-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnsl-2.27.so new file mode 100644 index 0000000..1df973d Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnsl-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnsl.so.1 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnsl.so.1 new file mode 100644 index 0000000..1df973d Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnsl.so.1 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_compat-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_compat-2.27.so new file mode 100644 index 0000000..021497d Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_compat-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_compat.so.2 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_compat.so.2 new file mode 100644 index 0000000..021497d Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_compat.so.2 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_dns-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_dns-2.27.so new file mode 100644 index 0000000..87efa07 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_dns-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_dns.so.2 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_dns.so.2 new file mode 100644 index 0000000..87efa07 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_dns.so.2 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_files-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_files-2.27.so new file mode 100644 index 0000000..03673ba Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_files-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_files.so.2 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_files.so.2 new file mode 100644 index 0000000..03673ba Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_files.so.2 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_hesiod-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_hesiod-2.27.so new file mode 100644 index 0000000..bb8396a Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_hesiod-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_hesiod.so.2 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_hesiod.so.2 new file mode 100644 index 0000000..bb8396a Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_hesiod.so.2 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nis-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nis-2.27.so new file mode 100644 index 0000000..c3e89f9 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nis-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nis.so.2 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nis.so.2 new file mode 100644 index 0000000..c3e89f9 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nis.so.2 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nisplus-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nisplus-2.27.so new file mode 100644 index 0000000..c77b5a5 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nisplus-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nisplus.so.2 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nisplus.so.2 new file mode 100644 index 0000000..c77b5a5 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libnss_nisplus.so.2 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpcprofile.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpcprofile.so new file mode 100644 index 0000000..b5eb2a4 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpcprofile.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpcre.so.3 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpcre.so.3 new file mode 100644 index 0000000..fb8b142 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpcre.so.3 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpthread-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpthread-2.27.so new file mode 100644 index 0000000..82921ae Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpthread-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpthread.so.0 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpthread.so.0 new file mode 100644 index 0000000..82921ae Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libpthread.so.0 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libresolv-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libresolv-2.27.so new file mode 100644 index 0000000..7099bbd Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libresolv-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libresolv.so.2 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libresolv.so.2 new file mode 100644 index 0000000..7099bbd Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libresolv.so.2 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/librt-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/librt-2.27.so new file mode 100644 index 0000000..4fdafe3 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/librt-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/librt.so.1 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/librt.so.1 new file mode 100644 index 0000000..4fdafe3 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/librt.so.1 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libselinux.so.1 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libselinux.so.1 new file mode 100644 index 0000000..258111f Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libselinux.so.1 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libstdc++.so.6 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libstdc++.so.6 new file mode 100644 index 0000000..f26626c Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libstdc++.so.6 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libthread_db-1.0.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libthread_db-1.0.so new file mode 100644 index 0000000..5acffb1 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libthread_db-1.0.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libthread_db.so.1 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libthread_db.so.1 new file mode 100644 index 0000000..5acffb1 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libthread_db.so.1 differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libutil-2.27.so b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libutil-2.27.so new file mode 100644 index 0000000..88b82f7 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libutil-2.27.so differ diff --git a/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libutil.so.1 b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libutil.so.1 new file mode 100644 index 0000000..88b82f7 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/i386-linux-gnu/libutil.so.1 differ diff --git a/src/zelos/ext/env/linux-x86/lib/ld-linux.so.2 b/src/zelos/ext/env/linux-x86/lib/ld-linux.so.2 new file mode 100644 index 0000000..56b3340 Binary files /dev/null and b/src/zelos/ext/env/linux-x86/lib/ld-linux.so.2 differ diff --git a/src/zelos/ext/platforms/linux/__init__.py b/src/zelos/ext/platforms/linux/__init__.py new file mode 100644 index 0000000..1d64132 --- /dev/null +++ b/src/zelos/ext/platforms/linux/__init__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +from .linux import Linux + + +__all__ = ["Linux"] diff --git a/src/zelos/ext/platforms/linux/linux.py b/src/zelos/ext/platforms/linux/linux.py new file mode 100644 index 0000000..9131258 --- /dev/null +++ b/src/zelos/ext/platforms/linux/linux.py @@ -0,0 +1,224 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import functools +import os +import posixpath + +from inspect import getfile + +import lief + +from zelos import CommandLineOption +from zelos.hooks import HookType +from zelos.plugin import OSPlugin + +from .loader import ElfLoader, LinuxMode +from .parse import LiefELF +from .signals import Signals +from .syscall_manager import construct_syscall_manager + + +CommandLineOption( + "linux_rootfs", + action="append", + default=[], + help="Specify the rootfs directory for an emulated architecture. Can " + "be specified multiple times to set the rootfs for different " + "architectures, and the appropriate rootfs will be used during " + "emulation. Format: '--linux_rootfs ARCH,PATH'. ARCH is 'x86', " + "'x86-64', 'arm', or 'mips'. PATH is the absolute host path to the " + "directory to be used as rootfs.", +) + + +class Linux(OSPlugin): + NAME = "Linux" + + def __init__(self, z): + super().__init__(z) + self.initial_parse = False + + def parse(self, path, binary_data): + binary = lief.parse(binary_data) + if binary.format != lief.EXE_FORMATS.ELF: + return None + + arch = { + lief.ELF.ARCH.i386: "x86", + lief.ELF.ARCH.x86_64: "x86_64", + lief.ELF.ARCH.ARM: "arm", + lief.ELF.ARCH.MIPS: "mips", + }[binary.header.machine_type] + if not self.initial_parse: + self._first_parse_setup(arch) + emulated_path = self._get_emulated_path(self.z.config, path) + if self.z is not None: + self.z.cmdline_args.insert(0, emulated_path) + + # TODO: /proc/self/exe needs to be a symbolic link to be read + # properly with readlink/readlinkat + self.z.files.add_file(path, emulated_path="/proc/self/exe") + + # TODO: synchronize this path with the main zelos rootfs + parsed_file = LiefELF(self.z.files, path, binary) + if parsed_file is None: + return None + + return parsed_file + + def _get_emulated_path(self, config, path: str) -> str: + if config.virtual_path is not None: + dir_path = config.virtual_path + else: + dir_path = os.path.dirname(path) + + if config.virtual_filename is not None: + filename = config.virtual_filename + else: + filename = os.path.basename(path) + return posixpath.join(dir_path, filename) + + def load(self, file, process, entrypoint_override=None): + process.loader = ElfLoader( + self.z, + self.z.state, + self.z.files, + process, + self.z.triggers, + os.path.basename(self.z.main_module.Filepath), + ) + + process.loader.load( + self.z.main_module.Filepath, + self.z.main_module, + entrypoint_override=entrypoint_override, + ) + + def _first_parse_setup(self, arch): + self.z.zos.syscall_manager = construct_syscall_manager(arch, self.z) + + # On first parse, register process & thread creation hooks + LinuxMode(arch, self.z) + self.z.hook_manager.register_thread_hook( + HookType.THREAD.CREATE, self._init_thread + ) + init_process = functools.partial(self._init_process, arch) + self.z.hook_manager.register_process_hook( + HookType.PROCESS.CREATE, init_process + ) + self.initial_parse = True + + if self.z.config.virtual_path is None: + self.z.config.virtual_path = "/home/admin/zelos_dir/" + + self.z.files.setup(self.z.config.virtual_path) + + rootfs = {} + for s in self.z.config.linux_rootfs: + try: + k, v = s.split(",") + rootfs[k] = v + except ValueError: + self.z.logger.error( + f"Incorrectly formatted input to '--linux_rootfs': {s}" + ) + self.z.logger.warn( + "Falling back to default rootfs for this architecture" + ) + continue + + if arch in rootfs: + if self.z.files.mount_folder(rootfs[arch]): + self.z.logger.verbose(f"Rootfs set to {rootfs[arch]}") + return + self.z.logger.warn("Falling back to default rootfs") + self.z.files.mount_folder(self._get_arch_subfolder(arch)) + + def _init_process(self, arch, p): + p.zos.signals = Signals(arch, p) + + def _init_thread(self, thread, stack_setup): + # if self.z.state.arch != "mips": + # self.z.current_process.threads.stack_min = 0xff000000 + # z.current_process.threads.stack_max = 0xffff0000 + self.populate_thread_stack(thread) + if stack_setup is not None: + stack_setup(thread) + + thread.save_context() + + def populate_thread_stack(self, thread): + """ This populates the stack of a new thread""" + thread.setSP(thread.stack_base) + thread.setFP(thread.stack_base) + + # This was _populate_process_stack. Right now we are doign + # this for every thread, we need to investigate whether this is + # truly the case, or only for the "main thread" + + # Mimic the stack of a newly initialized process + entryStack = [ + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + ] + for value in reversed(entryStack): + thread.pushstack(value) + + def _get_arch_subfolder(self, arch): + subfolder = { + "x86": "linux-x86", + "x86_64": "linux-x86-64", + "arm": "linux-armv7", + "mips": "linux-mips", + }[arch] + + zelos_dir = os.path.dirname( + os.path.realpath(getfile(self.z.__class__)) + ) + env_dir = os.path.join(zelos_dir, "ext", "env") + return os.path.join(env_dir, subfolder) diff --git a/src/zelos/ext/platforms/linux/loader.py b/src/zelos/ext/platforms/linux/loader.py new file mode 100644 index 0000000..862d9e2 --- /dev/null +++ b/src/zelos/ext/platforms/linux/loader.py @@ -0,0 +1,351 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +from os.path import basename + +from unicorn import UC_ERR_EXCEPTION + +from zelos.exceptions import ZelosLoadException +from zelos.hooks import HookType +from zelos.plugin import Loader +from zelos.util import align + + +class LinuxMode: + def __init__(self, arch, z): + self.z = z + self.logger = z.logger + self.z.hook_manager.register_exception_hook(self.handle_exception) + + # TODO: removing this fails + # test.test_linux_arm.ZelosTest.test_dynamic_elf + # due to the stack being allocated at the same address as + # the binary. Stacks in linux should be allocated from the + # top-down to avoid this collision. + # Set the stack address range + if arch != "mips": + + def set_stack_region(current_process): + current_process.threads.stack_min = 0xFF000000 + current_process.threads.stack_max = 0xFFFF0000 + + self.z.hook_manager.register_process_hook( + HookType.PROCESS.CREATE, set_stack_region + ) + + self.z = z + + def handle_exception(self, p, e): + # TODO(kzsnow): This goes in core? + try: + self.z.trace.bb( + self.z.last_instruction, self.z.last_instruction_size + ) + except Exception: + self.z.logger.exception("Couldn't print basic block") + + if p.current_thread.getIP() == p.current_thread.end_address: + # TODO: this only removes the mapping, but does not change + # where the next allocation will occur. + # p.current_thread.cleanup(self) + p.threads.complete_current_thread() + return + + if self.z.state.arch == "arm": + arm_private_syscall = { + 0xFFFF0F60: self.z.zos.syscall_manager._kuser_cmpxchg64, + 0xFFFF0FA0: self.z.zos.syscall_manager._kuser_memory_barrier, + 0xFFFF0FC0: self.z.zos.syscall_manager._kuser_cmpxchg, + 0xFFFF0FE0: self.z.zos.syscall_manager._kuser_get_tls, + }.get(p.current_thread.getIP(), None) + if arm_private_syscall is not None: + arm_private_syscall() + return + if e.errno == UC_ERR_EXCEPTION: + if self._attempt_to_handle_syscall(): + return # linear execution after syscall (interrupt style) + + self.z.trace.bb() + p.threads.fail_current_thread(fail_reason=f"Exception {e}") + self.z.processes.handles.close_all(self.z.current_process.pid) + + def _attempt_to_handle_syscall(self): + if self.z.verbose: + self.z.trace.bb( + self.z.last_instruction, + self.z.last_instruction_size, + full_trace=False, + ) + syscall_action = self.z.zos.syscall_manager.handle_syscall( + self.z.current_process + ) + was_handled = syscall_action is not None + return was_handled + + def create_tls(self, thread): + if thread.local_data_address is not None: + flags = thread.memory.gdt.gdt_entry_flags( + gr=0, sz=1, pr=1, privl=3, ex=0, dc=0, rw=1, ac=1 + ) + thread.memory.gdt.set_entry( + 10, thread.thread_local_data, 0xFFF, flags + ) + + +class ElfLoader(Loader): + + TLS_ADDR = 0x7FFDF000 + + def load( + self, module_path, file, thread_name="main", entrypoint_override=None + ): + self.main_module_name = module_path + self.main_module = file + if self.main_module.ExtraCmdlineArg is not None: + filename = self.process.cmdline_args[0] + self.process.cmdline_args[0] = f"./{basename(filename)}" + self.process.cmdline_args.insert( + 0, self.main_module.ExtraCmdlineArg + ) + + self._create_process_address_space(file) + + self.base = self._load_module(file, module_path) + + # Need to load the thread local storage before the gs register: + # https://wiki.osdev.org/Thread_Local_Storage#i386 + # tdata section is the initial state of this data: + # https://stackoverflow.com/questions/4126184/elf-file-tls-and-load-program-sections + tdata = self._z.main_module.Tls + self.process.memory.write(self.TLS_ADDR - len(tdata), bytes(tdata)) + + self.EntryPoint = self._get_entrypoint(file, entrypoint_override) + self._create_thread(self.EntryPoint, module_path, thread_name) + + self.logger.verbose( + f'Map Module "{module_path}" | ' + f"ImageBase: 0x{file.ImageBase:08x} " + f"MapBase: 0x{self.base:08x}" + ) + + return self.base, None + + def _create_thread( + self, + entry_point, + module_path, + thread_name=None, + priority=0, + benign_code=False, + ): + self.process.new_thread( + entry_point, + name=thread_name, + priority=priority, + stack_setup=self._stack_setup, + module_path=module_path, + benign_code=benign_code, + ) + + def _load_module(self, elf, module_name): + module_path, normalized_module_name = self._get_module_name( + module_name + ) + data = bytearray(elf.Data) + base = self.memory._alloc_at( + "", + "main", + basename(normalized_module_name), + elf.ImageBase, + elf.VirtualSize, + ) + self.memory.write(base, bytes(data)) + # Set proper permissions for each section of the module + for s in elf.Sections: + try: + self.memory.protect( + s.Address, + align(s.VirtualSize, s.Alignment), + s.Permissions, + # s.Name, + # "main", + # module_name=basename(module_path), + ) + except Exception: + raise ZelosLoadException( + f"Bad section {hex(s.Address)} {hex(s.VirtualSize)}" + ) + return base + + def _create_process_address_space(self, binary): + self.ADDRESS = binary.ImageBase + self._z.STACK_SIZE = max(binary.StackSize + 0x1000, self._z.STACK_SIZE) + self.size = binary.VirtualSize + self.entry = binary.EntryPoint + + # Discusses the setup of TLS https://akkadia.org/drepper/tls.pdf + # I have seen an access behind the start address in linux x86 + # hello world + self.memory.map(self.TLS_ADDR - 0x1000, 0x2000, "TLS", "system") + # Linux puts the syscall function at gs 10 + self.memory.write_int(self.TLS_ADDR + 0x10, 0xB0BABABE) + + def _stack_setup(self, thread): + # A good overview of the stack format: + # http://articles.manugarg.com/aboutelfauxiliaryvectors.html + + # This needs to be changed for dynamically linked binaries a la + # http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html + # regs = ['eax', 'ebx', 'ecx', 'edx', 'edi', 'ebp', 'esi'] + # for r in regs: + # thread.set_reg(r, 0) + + # aux vector codes: + # https://github.com/torvalds/linux/blob/v3.19/include/uapi/linux/auxvec.h + + module_path_ptr, str_len = self.memory.heap.allocstr( + f"/home/admin/{self.main_module_name}", alloc_name="Module Path" + ) + cpu_string_ptr, _ = self.memory.heap.allocstr( + "some computer", alloc_name="cpu_string" + ) + + # Begin by mapping strings that exist at stack bottom. + + env_vars = {"SHELL": "bin/bash"} + + env_strings = [f"{k}={v}\x00" for k, v in env_vars.items()] + env_strings.extend( + [x + "\x00" for x in self.process.environment_variables] + ) + arg_strings = [s + "\x00" for s in self.process.cmdline_args] + + self.logger.debug(f"Command line args are {arg_strings}") + self.logger.debug(f"Env vars are {env_strings}") + + # Setup the stack bottom + thread.pushstack(0) + + def push_data(data): + sp = thread.getSP() + string_addr = sp - len(data) + self.memory.write(string_addr, data) + thread.setSP(string_addr) + return string_addr + + env_string_ptrs = [push_data(s.encode()) for s in env_strings] + # ptrs must be in the same order as the arg strings + arg_string_ptrs = [push_data(s.encode()) for s in arg_strings] + + # Padding would come next. To figure out how much padding is + # needed to align the stack pointer, we collect the data that + # comes after the padding + stack_top = self._get_stack_top_bytes( + thread, arg_string_ptrs, env_string_ptrs + ) + + padding = self._get_padding_bytes(thread, stack_top) + push_data(stack_top + padding) + self.logger.debug(f"SP is set to {thread.getSP():x}") + + # Functions used in the setup of the stack # + + def _get_stack_top_bytes(self, thread, arg_string_ptrs, env_string_ptrs): + def get_ptr_bytes(thread, ptrs): + return b"".join([thread.pack(p) for p in ptrs]) + + argc = thread.pack(len(arg_string_ptrs)) + argv = get_ptr_bytes(thread, arg_string_ptrs) + args_bytes = argc + argv + thread.pack(0) + + env_bytes = get_ptr_bytes(thread, env_string_ptrs) + thread.pack(0) + + aux_vector_bytes = self._get_aux_vector_bytes() + + return args_bytes + env_bytes + aux_vector_bytes + + def _get_padding_bytes(self, thread, stack_top): + # The padding needs to make sure that + # (current_sp - (padding_len + initial_stack_data_len)) + # % alignment == 0 + # restricting padding_len < alignment,using basic maths implies + # (current_sp - initial_stack_data_len) + # % alignment == padding_len + ALIGNMENT = 16 + padding_size = (thread.getSP() - len(stack_top)) % ALIGNMENT + self.logger.debug( + f"stack_top: {len(stack_top):x}, sp: {thread.getSP():x}" + f" padding: {padding_size:x}" + ) + return b"\x00" * padding_size + + def _get_aux_vector_bytes(self): + random_bytes_ptr, str_len = self.memory.heap.allocstr( + "RANDOMBYTESRAND", alloc_name="random bytes" + ) + + aux_vector = [ + # legal values + # (0x01, 0), #AT_IGNORE: entry should be ignored + # (0x02, val), #AT_EXECFD: file descriptor of program + # AT_PHDR: program headers for program + (0x03, self.main_module.HeaderAddress), + # AT_PHENT: size of program header entry + (0x04, self.main_module.HeaderSize), + # AT_PHNUM: number of program headers + (0x05, self.main_module.NumberOfProgramHeaders), + (0x06, 0x1000), # AT_PAGESZ: system page size + # AT_BASE: base address of interpreter + (0x07, self.main_module.ImageBase), + (0x08, 0), # AT_FLAGS: flags + # AT_ENTRY: program entry point + (0x09, self.main_module.EntryPoint), + # (0x0a, 0), #AT_NOTELF: program is not ELF + (0x0B, 0x3E8), # AT_UID: real uid + (0x0C, 0x3E8), # AT_EUID: effective uid + (0x0D, 0x3E8), # AT_GID: real gid + (0x0E, 0x3E8), # AT_EGID: effective gid + (0x11, 0x64), # AT_CLKTCK: frequency of times() + # hardware related + # (0x0f, 'x86_64'), #AT_PLATFORM: string ident platform + ( + 0x10, + 0x001FB897, + ), # AT_HWCAP: machine dependent processor capabilities + # (0x18, 0) #AT_BASE_PLATFORM: string for real platforms + (0x1A, 0), # AT_HWCAP2: extension of processor capabilities + # FPU related (kernel use) + # (0x12, 0), #AT_FPUCW: used FPU control word + # cache block sizes + # (0x13, 0), #AT_DCACHEBSIZE: data cache block size + # (0x14, 0), #AT_ICACHEBSIZE: instruction cache block size + # (0x15, 0), #AT_UCACHEBSIZE: unified cache block size + # PPC related (kernel use) + # (0x16, 0), #AT_IGNOREPPC: entry should be ignored + # (0x17, 0), #AT_SECURE: exec is setuid-like + (0x19, random_bytes_ptr), # AT_RANDOM: addr of 16 rand byte + # global system pages + # (0x20, 0), #AT_SYSINFO: entry point to syscall in vDSO + # (0x21, 0), #AT_SYSINFO_EHDR: page address of vDSO + (0x00, 0), # AT_NULL: end of vector + ] + + data = b"" + for (key, val) in aux_vector: + data += self.emu.pack(key) + data += self.emu.pack(val) + return data diff --git a/src/zelos/ext/platforms/linux/network.py b/src/zelos/ext/platforms/linux/network.py new file mode 100644 index 0000000..dd8e4bd --- /dev/null +++ b/src/zelos/ext/platforms/linux/network.py @@ -0,0 +1,66 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import ipaddress +import socket + +import zelos.util as zelos_util + +from .syscalls.syscalls_const import SOCKADDR_IN, SOCKADDR_IN6, SocketFamily + + +def _bytes_to_host(b, domain): + """ + Converts an integer containing the domain to a string represnting + the ip of the host. + """ + if domain == SocketFamily.AF_INET: + b_htonl = socket.htonl(b) + return str(ipaddress.IPv4Address(b_htonl)) + return None + + +def _host_to_bytes(host, domain): + if domain == SocketFamily.AF_INET: + return int.from_bytes(ipaddress.IPv4Address(host).packed, "little") + return -1 + + +def _bytes_to_port(b): + return socket.htons(b) + + +def _port_to_bytes(port): + return socket.ntohs(port) + + +def get_host_and_port(domain, struct_bytes): + host = "255.255.255.255" + port = 65536 + if len(struct_bytes) == 0: + return (None, None) + if domain == SocketFamily.AF_INET: + s_in = SOCKADDR_IN() + zelos_util.str2struct(s_in, bytes(struct_bytes)) + host = _bytes_to_host(s_in.sin_addr, domain) + port = _bytes_to_port(s_in.sin_port) + elif domain == SocketFamily.AF_INET6: + s_in6 = SOCKADDR_IN6() + zelos_util.str2struct(s_in6, bytes(struct_bytes)) + host = _bytes_to_host(s_in6.sin6_addr, domain) + port = _bytes_to_port(s_in6.sin6_port) + return (host, port) diff --git a/src/zelos/ext/platforms/linux/parse.py b/src/zelos/ext/platforms/linux/parse.py new file mode 100644 index 0000000..6b3a1d9 --- /dev/null +++ b/src/zelos/ext/platforms/linux/parse.py @@ -0,0 +1,278 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +from os.path import basename, exists, splitext + +import lief + +import zelos.util as util + +from zelos.exceptions import UnsupportedBinaryError +from zelos.file_system import FileSystem +from zelos.plugin import Parser, Section + + +class LiefELF(Parser): + def __init__(self, file_system: FileSystem, path: str, binary): + super().__init__() + self._files = file_system + self.ExtraCmdlineArg = None + self.parse(path, binary) + + def file_format(self): + return "ELF" + + def _get_interpreter(self, binary): + try: + return binary.interpreter + except Exception: + return None + + def _find_interpreter(self, requested_interpreter, binary): + if requested_interpreter == "": + self.logger.notice("Requested interpreter is blank") + linker_path = self._files.emulated_path_to_host_path( + requested_interpreter + ) + if linker_path is not None and exists(linker_path): + return linker_path + + machine = binary.header.machine_type + if machine == lief.ELF.ARCH.i386: + path = "/lib/ld-linux.so.2" + elif machine == lief.ELF.ARCH.ARM: + path = "/lib/ld-linux.so.3" + else: + self.logger.notice("No default interpreter for this arch") + return "" + self.logger.verbose(f"Attempting with default linker {path}") + default_linker_path = self._files.emulated_path_to_host_path(path) + if default_linker_path is not None and exists(default_linker_path): + return default_linker_path + raise UnsupportedBinaryError( + f"Couldn't find linker {requested_interpreter}" + ) + + def _setup_dynamic_binary(self, requested_interpreter, binary): + """ + Dynamic binaries need to run the appropriate linker, and pass + the executable as a commandline argument + """ + self.logger.verbose( + f"Requested {requested_interpreter} to load dynamic binary" + ) + + linker_path = self._find_interpreter(requested_interpreter, binary) + + self.ExtraCmdlineArg = requested_interpreter + linker_binary = lief.parse(linker_path) + return (linker_path, linker_binary) + + def parse(self, path, binary): + interpreter = self._get_interpreter(binary) + if interpreter is not None: + # TODO: automatically do setup to run dynamic linux binaries + (path, binary) = self._setup_dynamic_binary(interpreter, binary) + self.Filepath = path + self.binary = binary + + # Refer parsed binary and symbols for better logging + # @@NOTE binary.get_function_address on binary.symbols invokes + # a _lot_ of brk() + functions = {} + for symbol in binary.static_symbols: + if symbol.is_function: + text_sections = binary.get_section(".text") + text_va = text_sections.virtual_address + text_offset = text_sections.offset + text_base = text_va - text_offset + symbol_offset = symbol.value - text_base + if symbol_offset > 0: + functions[symbol.value] = symbol.name + self.exported_functions = functions + + # Parse Architecture + machine = binary.header.machine_type + if machine == lief.ELF.ARCH.i386: + self.Architecture = "x86" + self.Mode = "32" + self.Bits = 32 + elif machine == lief.ELF.ARCH.x86_64: + self.Architecture = "x86_64" + self.Mode = "64" + self.Bits = 64 + elif machine == lief.ELF.ARCH.ARM: + self.Architecture = "arm" + self.Mode = "32" + self.Bits = 32 + # When looking at other archs, this gives information about + # stack for arm: + # https://stackoverflow.com/questions/1802783/initial-state-of-program-registers-and-stack-on-linux-arm/6002815#6002815 + elif machine == lief.ELF.ARCH.MIPS: + self.Architecture = "mips" + self.mode = "32" + self.bits = 32 + else: + raise UnsupportedBinaryError(f"Unsupported arch {machine} for ELF") + + if binary.is_pie: + raise UnsupportedBinaryError("Can't handle PIE binaries") + + self.Data = [0] * binary.virtual_size + + # TODO: More time should be invested here to figure out whether + # this is legit. + # lets arbitrarily load things at 0x0b000000 + self.logger.debug(f"Binary's imagebase is {binary.imagebase:x}") + relocated_base = 0 if binary.imagebase != 0 else 0xB000000 + base = relocated_base + binary.imagebase + self.base = base + + # Only load segments that are the LOAD type. + segments_to_load = [] + for s in binary.segments: + if s.type == lief.ELF.SEGMENT_TYPES.LOAD: + segments_to_load.append(s) + if len(segments_to_load) == 0: + raise UnsupportedBinaryError("No loadable segment") + + for segment in segments_to_load: + + virtual_offset = segment.virtual_address - binary.imagebase + self.Data[ + virtual_offset : virtual_offset + len(segment.content) + ] = segment.content + self.logger.debug( + f"Load segment from {binary.imagebase + virtual_offset:x} to" + f" {binary.imagebase+virtual_offset+len(segment.content):x}" + ) + for s in segment.sections: + section = Section() + section.Name = s.name + alignment = s.alignment + section.Size = util.align(s.size, alignment) + section.VirtualSize = util.align(s.size, alignment) + section.Address = relocated_base + s.virtual_address + section.Permissions = 7 + section.Alignment = 0 if s.alignment < 2 else s.alignment + # print(s) + # print(dir(s)) + self.Sections.append(section) + offset = section.Address - self.base + self.logger.verbose( + "Adding data for section %s at offset %x of size %x", + s.name, + offset, + len(s.content), + ) + + # Load the ELF header and the program/section headers. + ph_offset = binary.header.program_header_offset + ph_data_size = ( + binary.header.program_header_size * binary.header.numberof_segments + ) + self.Data[ + : ph_offset + ph_data_size + ] = binary.get_content_from_virtual_address( + binary.imagebase, ph_offset + ph_data_size + ) + + self.set_tls_data(binary) + + # Set Misc. Binary Attributes + self.Filename = basename(self.Filepath) + self.Shortname = splitext(self.Filename)[0] + self.ImageBase = base + self.EntryPoint = relocated_base + binary.entrypoint + self.VirtualSize = binary.virtual_size + self.HeaderAddress = base + binary.header.program_header_offset + self.HeaderSize = binary.header.program_header_size + self.NumberOfProgramHeaders = binary.header.numberof_segments + return + + def set_tls_data(self, binary): + """ Used to init the tls data of an elf file""" + self.Tls = bytearray() + + tdata_sections = self._get_tdata_like_sections(binary) + if len(tdata_sections) > 0: + s = tdata_sections[0] + self.Tls.extend(bytearray(s.content)) + + tbss_sections = self._get_tbss_like_sections(binary) + if len(tbss_sections) > 0: + s = tbss_sections[0] + self.Tls.extend(bytearray(s.size)) + + def _get_tdata_like_sections(self, binary): + tdata_sections = [ + s + for s in binary.sections + if lief.ELF.SECTION_FLAGS.TLS in s + and s.type == lief.ELF.SECTION_TYPES.PROGBITS + ] + if len(tdata_sections) > 1: + self.logger.notice( + f"Unexpected number of tdata-like sections: " + f"{len(tdata_sections)}" + ) + return tdata_sections + + def _get_tbss_like_sections(self, binary): + tbss_sections = [ + s + for s in binary.sections + if lief.ELF.SECTION_FLAGS.TLS in s + and s.type == lief.ELF.SECTION_TYPES.NOBITS + ] + if len(tbss_sections) > 1: + self.logger.notice( + f"Unexpected number of tbss-like sections: " + f"{len(tbss_sections)}" + ) + return tbss_sections + + +def print_tls(binary): + format_str = "{:<33} {:<30}" + format_hex = "{:<33} 0x{:<28x}" + + print("== TLS ==") + tls = binary.tls + callbacks = tls.callbacks + print(format_hex.format("Address of callbacks:", tls.addressof_callbacks)) + if len(callbacks) > 0: + print("Callbacks:") + for callback in callbacks: + print(" " + hex(callback)) + + print(format_hex.format("Address of index:", tls.addressof_index)) + print(format_hex.format("Size of zero fill:", tls.sizeof_zero_fill)) + print( + "{:<33} 0x{:<10x} 0x{:<10x}".format( + "Address of raw data:", + tls.addressof_raw_data[0], + tls.addressof_raw_data[1], + ) + ) + print(format_hex.format("Size of raw data:", len(tls.data_template))) + print(format_hex.format("Characteristics:", tls.characteristics)) + print(format_str.format("Section:", tls.section.name)) + print(format_str.format("Data directory:", str(tls.directory.type))) + print(("Callbacks:", tls.callbacks)) + for cb in tls.callbacks: + print(format_hex.format(" Callback:", cb)) + print("") diff --git a/src/zelos/ext/platforms/linux/signals.py b/src/zelos/ext/platforms/linux/signals.py new file mode 100644 index 0000000..a9b77a6 --- /dev/null +++ b/src/zelos/ext/platforms/linux/signals.py @@ -0,0 +1,217 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import enum +import logging + +from zelos.util import columnate + + +def is_signal_blocked(signum: int, sigmask: int) -> bool: + return 0 != (2 ** (signum - 1) & sigmask) + + +def sigmask_string(sigmask: int) -> str: + s = "|".join( + [sig.name for sig in Signal if is_signal_blocked(sig, sigmask)] + ) + return f"[{s}]" + + +class Signals: + """ + Signals register an action that should be taken for a certain + process. They are another form of coordination across processes. + """ + + def __init__(self, arch, p): + self.arch = arch + self.logger = logging.getLogger(__name__) + self.p = p + # 32 linux signals + self.signal_actions = {i: 0 for i in range(32)} + self.signal_mask = 0 + self.signal_queue = [] + + def __str__(self): + signal_strings = [ + f"{i:<2}: 0x{addr:10. +# ====================================================================== +import functools +import inspect + +from collections import defaultdict +from typing import Callable, Dict, Optional + +from termcolor import colored + +from zelos import HookType +from zelos.exceptions import ZelosException +from zelos.plugin import ArgFactory, SyscallManager + +from .syscalls.arg_strings import get_arg_string + + +def construct_syscall_manager(arch, z): + sm_class = { + "x86": X86SyscallManager, + "x86_64": X86_64SyscallManager, + "arm": ARMSyscallManager, + "mips": MIPSSyscallManager, + }.get(arch, None) + + if sm_class is None: + return None + return sm_class(z) + + +class LinuxSyscallManager(SyscallManager): + def __init__(self, arch, engine): + super(LinuxSyscallManager, self).__init__(engine) + self.arch = arch + self.call_map = self.__load_linux_syscall_maps(arch) + self._name2syscall_func = self._load_linux_syscall_funcs() + self.rev_map = {v: k for k, v in self.call_map.items()} + + self.offset_dict = defaultdict(int) + + self.arg_factory = ArgFactory( + functools.partial(get_arg_string, self.z) + ) + + self.socketcall_dict = { + 1: "socket", + 2: "bind", + 3: "connect", + 4: "listen", + 5: "accept", + 6: "getsockname", + 7: "getpeername", + 8: "socketpair", + 9: "send", + 10: "recv", + 11: "sendto", + 12: "recvfrom", + 13: "shutdown", + 14: "setsockopt", + 15: "getsockopt", + 16: "sendmsg", + 17: "recvmsg", + 18: "accept4", + 19: "recvmmsg", + 20: "sendmmsg", + } + + # These are processes that are exited, and so a parent process + # can wait on them. + # parent_pid -> child_pid + self.child_state_changes = defaultdict(list) + + def _load_linux_syscall_funcs( + self, + ) -> Dict[str, Callable[[any], Optional[int]]]: + """ + Returns map of name -> syscall implementation. + """ + from .syscalls import syscalls as linux_syscall_module + + linux_syscalls = inspect.getmembers( + linux_syscall_module, inspect.isfunction + ) + + return { + name.partition("sys_")[-1]: func + for name, func in linux_syscalls + if name.startswith("sys_") + } + + def __load_linux_syscall_maps(self, arch): + """ + Loads the list of supported syscalls from the syscalls table. + """ + from .syscalls.syscalls_table import cols, table + + try: + i = cols.index(arch) + except ValueError: + raise ZelosException(f"Invalid architecture '{arch}'") + + return {k: v[i] for (k, v) in table.items() if v[i] != -1} + + def handle_syscall(self, process): + """ + Additionally translate `socketcall` syscalls to their target + socket system call, e.g. `recv`, for the purpose of syscall + breaks. This ensures that breakpoints for `recv` will be + triggered both for `recv` and `socketcall` syscalls that invoke + `recv`, etc. + """ + sys_num = self.get_syscall_number() + sys_name = self.find_syscall_name_by_number(sys_num) + if sys_name == "socketcall": + socketcall_args = self.get_last_syscall_args() + args = self.get_args( + [("int", "call"), ("unsigned long *", "callargs")], + sys_num=sys_num, + ) + status = super(LinuxSyscallManager, self).handle_syscall(process) + if sys_name == "socketcall": + socketcall = self.socketcall_dict.get(args.call, None) + if socketcall is not None: + self.last_syscall_args = socketcall_args + self._handle_syscall_break(socketcall) + return status + + def _get_socketcall_args( + self, process, func_name, args_addr, arg_list, arg_string_overrides={} + ): + # If calling these syscalls directly, get_args the old + # fashioned way. + if args_addr < 0: + return self.get_args(arg_list, arg_string_overrides) + + arg_vals = [ + process.memory.read_int(args_addr + i * 4) + for i in range(len(arg_list)) + ] + args = self.arg_factory.gen_args( + arg_list, arg_vals, arg_string_overrides=arg_string_overrides + ) + self.last_syscall_args = args + self._print_socket_syscall(func_name, args) + return args + + def _print_socket_syscall(self, func_name, args): + s = colored(f"{func_name}", "white", attrs=["bold"]) + f" ( {args} )" + self.z.trace.print("SOCKET SYSCALL", s) + + #################### + # HELPER FUNCTIONS # + #################### + + def get_args(self, arg_list, arg_string_overrides={}, sys_num=None): + """ + Gets arguments according to linux syscall calling convention + """ + z = self.z + if sys_num is None: + sys_num = self.get_syscall_number() + reg_list = self._REG_ARGS + + arg_regs = reg_list[: len(arg_list)] + + arg_vals = [z.current_thread.get_reg(arg) for arg in arg_regs] + + # Get the rest of the arguments off of the stack + i = len(arg_vals) + while len(arg_vals) < len(arg_list): + arg_vals.append(self.emu.getstack(i)) + i += 1 + args = self.arg_factory.gen_args( + arg_list, arg_vals, arg_string_overrides=arg_string_overrides + ) + self.last_syscall_args = args + return args + + +class X86SyscallManager(LinuxSyscallManager): + def __init__(self, engine): + super(X86SyscallManager, self).__init__("x86", engine) + + def syscall_handler_wrapper(current_process, *args, **kwargs): + self.handle_syscall(current_process) + + engine.interrupt_handler.register_interrupt_handler( + 0x80, syscall_handler_wrapper + ) + + _REG_NUMBER = "eax" + _REG_ARGS = ["ebx", "ecx", "edx", "esi", "edi", "ebp"] + _REG_RETURN = "eax" + _REG_RETURN_2 = "edx" + + def get_syscall_number(self): + return self.emu.get_reg("eax") + + def set_return_value(self, value): + self.emu.set_reg("eax", value) + + def return_addr(self): + return self.emu.getIP() + 2 + + def handle_syscall(self, *args, **kwargs): + """ + Calls the corresponding syscall with given name or number. + """ + t = self.z.current_process.current_thread + addr = t.getIP() + + super(X86SyscallManager, self).handle_syscall(*args, **kwargs) + + if self.syscall_break_name is None: + + def set_ip(): + t.setIP(addr + 2) + + self.z.scheduler.stop_and_exec("handle_syscall", set_ip) + else: + self.pending_ip_change = addr + 2 + + return True + + +class X86_64SyscallManager(LinuxSyscallManager): + def __init__(self, engine): + super(X86_64SyscallManager, self).__init__("x86_64", engine) + + def handle_syscall_callback(zelos): + current_process = engine.current_process + """ + We need to execute a syscall at this point, however, + certain syscalls may not be runnable within a hook (they + cause unicorn to execute code which is not allowed in a + hook) + """ + handle_syscall_closure = functools.partial( + self.handle_syscall, current_process + ) + current_process.scheduler.stop_and_exec( + "handle syscall", handle_syscall_closure + ) + return True + + # Syscalls are made using the syscall instruction. Unicorn does + # not catch these by default though. + engine.hook_manager.register_inst_type_hook( + HookType._INST.X86_SYSCALL, + handle_syscall_callback, + name="x64_syscall_hook", + ) + + _REG_NUMBER = "rax" + _REG_ARGS = ["rdi", "rsi", "rdx", "r10", "r8", "r9"] + _REG_RETURN = "rax" + _REG_RETURN_2 = "rdx" + + def get_syscall_number(self): + return self.emu.get_reg("rax") + + def handle_syscall(self, *args, **kwargs): + t = self.z.current_thread + addr = t.getIP() + super().handle_syscall(*args, **kwargs) + + if addr == t.getIP(): + + def set_ip(): + t.setIP(addr + 2) + + self.z.scheduler.stop_and_exec("handle_syscall", set_ip) + + def set_return_value(self, value): + self.emu.set_reg("rax", value) + + def return_addr(self): + return self.emu.getIP() + 2 + + def pause_syscall(self, process, condition=None): + """ + Defines what happens when the pause syscall exception is + received + """ + super().pause_syscall(process, condition=condition) + return + + +class ARMSyscallManager(LinuxSyscallManager): + def __init__(self, engine): + super(ARMSyscallManager, self).__init__("arm", engine) + + def syscall_handler_wrapper(current_process, *args, **kwargs): + self.handle_syscall(current_process) + + engine.hook_manager.register_interrupt_hook( + syscall_handler_wrapper, intno=0x2 + ) + + _REG_NUMBER = "r7" + _REG_ARGS = ["r0", "r1", "r2", "r3", "r4", "r5", "r6"] + _REG_RETURN = "r0" + _REG_RETURN_2 = "r1" + + def get_syscall_number(self): + # Some syscalls are passed through svc, and these seem to be the + # "old abi" (https://w3challs.com/syscalls/?arch=arm_strong) + # Discussion on difference between oldabi and eabi: + # https://lists.gnu.org/archive/html/qemu-devel/2009-01/msg01512.html + # More discussion: + # https://www.raspberrypi.org/forums/viewtopic.php?t=158915 + svc_inst = self.z.disas(self.z.current_thread.getIP() - 4, 4)[0] + if svc_inst.insn_name() == "svc": + svc_val = svc_inst.operands[0].imm + if svc_val != 0: + return svc_val - 0x900000 + else: + self.logger.notice( + f"Why was there an exception here if the last instruction " + f"wasn't a syscall {self.z.current_thread.getIP():x}" + ) + val = self.emu.get_reg("r7") + return val + + def set_return_value(self, value): + self.emu.set_reg("r0", value) + + def return_addr(self): + # (V) we had this as ip+4 initially, but found that to be wrong. + # Double check that this is consistently correct. + return self.emu.getIP() + + # arch/arm/kernel/entry-armv.S: + # __kuser_get_tls: @ 0xffff0fe0 + # #if !defined(CONFIG_HAS_TLS_REG) && !defined(CONFIG_TLS_REG_EMUL) + # ldr r0, [pc, #(16 - 8)] @ TLS stored at 0xffff0ff0 + # #else + # mrc p15, 0, r0, c13, c0, 3 @ read TLS register + # #endif + # usr_ret lr + + def _kuser_get_tls(self): + self.logger.info("Called kuser_get_tls") + tls = self.emu.get_reg("c13_c0_3") + self.set_return_value(tls) + self.emu.setIP(self.emu.get_reg("lr")) + + def _kuser_cmpxchg(self): + self.logger.info("Called kuser_cmpxchg") + oldval = self.emu.get_reg("r0") + newval = self.emu.get_reg("r1") + ptr = self.emu.get_reg("r2") + + d_ptr = self.z.memory.read_int(ptr) + + if d_ptr != oldval: + CPSR_CF_CLEAR = self.emu.get_reg("cpsr") & 0xDFFFFFFF # CPSR[29] + self.emu.set_reg("cpsr", CPSR_CF_CLEAR) + self.emu.setIP(self.emu.get_reg("lr")) + self.set_return_value(1) + else: + self.z.memory.write_int(ptr, newval) + CPSR_CF_SET = self.emu.get_reg("cpsr") | 0x20000000 # CPSR[29] + self.emu.set_reg("cpsr", CPSR_CF_SET) + self.emu.setIP(self.emu.get_reg("lr")) + self.set_return_value(0) + + def _kuser_memory_barrier(self): + self.logger.info("Called kuser_memory_barrier") + self.emu.setIP(self.emu.get_reg("lr")) + + def _kuser_cmpxchg64(self): + self.logger.notice("Reached kuser_cmpxchg64, needs to be implemented") + self.emu.setIP(self.emu.get_reg("lr")) + + +class MIPSSyscallManager(LinuxSyscallManager): + def __init__(self, engine): + super(MIPSSyscallManager, self).__init__("mips", engine) + + def syscall_handler_wrapper(current_process, *args, **kwargs): + self.handle_syscall(current_process) + + engine.hook_manager.register_interrupt_hook( + syscall_handler_wrapper, intno=0x11 + ) + + _REG_NUMBER = "v0" + _REG_ARGS = ["a0", "a1", "a2", "a3"] + _REG_RETURN = "v0" + _REG_RETURN_2 = "v1" + + def handle_syscall(self, *args, **kwargs): + super(MIPSSyscallManager, self).handle_syscall(*args, **kwargs) + + return_address = self.emu.getIP() + 4 + + if self.syscall_break_name is None: + + def set_ip(): + self.emu.setIP(return_address) + + self.z.scheduler.stop_and_exec("handle_syscall", set_ip) + else: + self.pending_ip_change = return_address + + return True + + def get_syscall_number(self): + return self.emu.get_reg("v0") + + def set_return_value(self, value): + self.emu.set_reg("v0", value) + + def return_addr(self): + return self.emu.getIP() diff --git a/src/zelos/ext/platforms/linux/syscalls/__init__.py b/src/zelos/ext/platforms/linux/syscalls/__init__.py new file mode 100644 index 0000000..43b266c --- /dev/null +++ b/src/zelos/ext/platforms/linux/syscalls/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== diff --git a/src/zelos/ext/platforms/linux/syscalls/arg_strings.py b/src/zelos/ext/platforms/linux/syscalls/arg_strings.py new file mode 100644 index 0000000..71b713f --- /dev/null +++ b/src/zelos/ext/platforms/linux/syscalls/arg_strings.py @@ -0,0 +1,132 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +from ..signals import Signal, sigmask_string +from .syscall_utils import twos_comp +from .syscalls_const import ( + ARCH_PRCTL_OPTIONS, + FCNTL_COMMANDS, + protocol_families, +) + + +def get_arg_string(z, arg): + type_str = arg.type + name = arg.name + val = arg.value + val_s = "" + if type_str in [ + "LPCSTR", + "const char*", + "LPCCH", + "PCHAR", + "LPTSTR", + "LPSTR", + "PCSTR", + "const void*", + ]: + val_s = z.memory.read_string(val) + val_s = repr(bytes(val_s, "utf8"))[2:-1] + arg_string = '{0}=0x{1:x} ("{2}")'.format(name, val, val_s) + elif type_str in ["LPCWSTR", "PCWSTR", "PCWCH", "LPCTSTR"]: + val_s = z.memory.read_wstring(val) + val_s = repr(bytes(val_s, "utf8"))[2:-1] + arg_string = '{0}=0x{1:x} ("{2}")'.format(name, val, val_s) + elif type_str in ["DWORD*"]: + val_pointed_to = z.memory.read_int(val) if val != 0 else 0 + arg_string = "*{0}=0x{1:x} ({2})".format(name, val, val_pointed_to) + elif name == "sockfd": + socket_handle = z.handles.get(val) + socket_name = "" + if socket_handle is not None: + if hasattr(socket_handle, "socket"): + sock = socket_handle.socket + domain = str(sock.domain).split(".")[1] + sock_type = str(sock.type).split(".")[1] + host = sock.host_and_port[0] + if host is None: + host = "?" + port = sock.host_and_port[1] + if port is None: + port = "?" + socket_name = f" ({domain}:{sock_type}:{host}:{str(port)})" + else: + socket_name = ( + " (" + socket_handle.data.get("dst_name", "?") + ")" + ) + arg_string = "{0}=0x{1:x}{2}".format(name, val, socket_name) + elif type_str in ["int_DOMAIN"]: + family = protocol_families[val] + arg_string = f"{name}=0x{val:x} ({family})" + elif type_str in ["int_FCNTL"]: + cmd_name = FCNTL_COMMANDS.get(val, "unknown") + arg_string = f"{name}=0x{val:x} ({cmd_name})" + elif type_str in ["off_t"]: + val = twos_comp(val, z.state.bits) + arg_string = f"{name}=0x{val:x}" + elif type_str in ["pid_t"]: + if val > 0xFFFFF: + val = twos_comp(val, 32) + arg_string = f"{name}=0x{val:x}" + elif type_str in ["int_ARCH_PRCTL"]: + cmd_name = ARCH_PRCTL_OPTIONS.get(val, "unknown") + arg_string = f"{name}=0x{val:x} ({cmd_name})" + elif name in ["signum"]: + try: + signal_name = Signal(val).name + except Exception: + signal_name = "unknown" + arg_string = f"{name}={signal_name}" + elif type_str in ["const kernel_sigset_t*"] and val != 0: + sigmask = z.memory.read_uint32(val) + signals_blocked = sigmask_string(sigmask) + arg_string = f"{name}=0x{val:x} ({signals_blocked})" + elif type_str in ["int"] and name in ["fd"]: + handle = z.processes.handles.get(val) + handle_category = ( + handle.category() if handle is not None else "unknown" + ) + arg_string = f"{name}=0x{val:x} ({handle_category})" + elif type_str in ["fd_set*"]: + if val == 0: + arg_string = f"{name}=0x{val:x} ()" + else: + fds = _parse_fdset(z, val) + arg_string = ( + f"{name}=0x{val:x} ({','.join([hex(x) for x in fds])})" + ) + else: + arg_string = "{0}=0x{1:x}".format(name, val) + if val_s != "": + z.triggers.api_strings.add(val_s) + return arg_string + + +def _parse_fdset(z, addr): + """ + Parse the individual fd's that are 'ready' from an fd_set. An fd + is 'ready' when its corresponding bit is set in the fd_set. The + fd_set is an array of bitmasks, where the ith bit of the + jth element corresponds to the fd value (j * 32) + i. + """ + fds = [] + for i in range(0, 1024 // 8, 32 // 8): + val = z.memory.read_uint32(addr + i) + for bit in range(32): + if val & 2 ** bit != 0: + fds.append((i // 4) * 32 + bit) + return fds diff --git a/src/zelos/ext/platforms/linux/syscalls/syscall_structs.py b/src/zelos/ext/platforms/linux/syscalls/syscall_structs.py new file mode 100644 index 0000000..8722d38 --- /dev/null +++ b/src/zelos/ext/platforms/linux/syscalls/syscall_structs.py @@ -0,0 +1,143 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import ctypes + + +# Validated on x64 +class SIGACTION(ctypes.Structure): + _fields_ = [ + ("sa_handler", ctypes.c_uint64), + ("sa_flags", ctypes.c_uint64), + ("sa_restorer", ctypes.c_uint64), + ("sa_mask", ctypes.c_uint64), + ] + + +class IOVEC(ctypes.Structure): + _fields_ = [("iov_base", ctypes.c_uint64), ("iov_len", ctypes.c_uint64)] + + +class MSGHDR(ctypes.Structure): + _fields_ = [ + ("msg_name", ctypes.c_uint64), + ("msg_namelen", ctypes.c_uint64), + ("msg_iov", ctypes.c_uint64), + ("msg_iovlen", ctypes.c_uint64), + ("msg_control", ctypes.c_uint64), + ("msg_controllen", ctypes.c_uint64), + ("msg_flags", ctypes.c_uint64), + ] + + +class MMSGHDR(ctypes.Structure): + _fields_ = [("msg_hdr", MSGHDR), ("msg_len", ctypes.c_uint64)] + + +def get_stat_struct(arch): + if arch == "arm": + return ARMSTAT() + else: + return STAT() + + +class ARMSTAT(ctypes.Structure): + # This is intended for the arm architecture. + # Retrieved from arm-linux-gnueabi/include/asm/stat + _fields_ = [ + ("st_dev", ctypes.c_uint32), # integer vector describing variable */ + ("st_ino", ctypes.c_uint32), # length of this vector */ + ( + "st_mode", + ctypes.c_uint16, + ), # 0 or address where to store old value */ + # Note nlink and mode are switched for arm... + ("st_nlink", ctypes.c_uint16), + ("st_uid", ctypes.c_uint16), + ("st_gid", ctypes.c_uint16), + ("st_rdev", ctypes.c_uint32), + ("st_size", ctypes.c_uint32), + ("st_blksize", ctypes.c_int32), + ("st_blocks", ctypes.c_int32), + ("st_atime", ctypes.c_int32), + ("st_atime_nsec", ctypes.c_uint32), + ("st_mtime", ctypes.c_int32), + ("st_mtime_nsec", ctypes.c_uint32), + ("st_ctime", ctypes.c_int32), + ("st_ctime_nsec", ctypes.c_uint32), + # ('__unused4', ctypes.c_uint32), + # ('__unused5', ctypes.c_uint32), + ] + + +class STAT(ctypes.Structure): + # This is intended for 64 bit architectures. + # https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/stat.h + _fields_ = [ + ("st_dev", ctypes.c_uint64), # integer vector describing variable */ + ("st_ino", ctypes.c_uint64), # length of this vector */ + ("st_nlink", ctypes.c_uint64), + ( + "st_mode", + ctypes.c_uint32, + ), # 0 or address where to store old value */ + ("st_uid", ctypes.c_uint32), + ("st_gid", ctypes.c_uint32), + ("__pad0", ctypes.c_uint32), + ("st_rdev", ctypes.c_uint64), + ("st_size", ctypes.c_uint64), + ("st_blksize", ctypes.c_int32), + ("st_blocks", ctypes.c_int64), + ("st_atime", ctypes.c_int64), + ("st_atime_nsec", ctypes.c_uint64), + ("st_mtime", ctypes.c_int64), + ("st_mtime_nsec", ctypes.c_uint64), + ("st_ctime", ctypes.c_int64), + ("st_ctime_nsec", ctypes.c_uint64), + # ('__unused4', ctypes.c_uint32), + # ('__unused5', ctypes.c_uint32), + ] + + +class STAT64(ctypes.Structure): + # Used on 32 bit systems + # https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/stat.h + _fields_ = [ + ("st_dev", ctypes.c_uint64), # integer vector describing variable */ + ("__pad0", ctypes.c_uint32), + ("st_ino", ctypes.c_uint32), # length of this vector */ + ( + "st_mode", + ctypes.c_uint32, + ), # 0 or address where to store old value */ + ("st_nlink", ctypes.c_uint32), + ("st_uid", ctypes.c_uint32), + ("st_gid", ctypes.c_uint32), + ("st_rdev", ctypes.c_uint64), + ("__pad1", ctypes.c_uint32), + ("st_size", ctypes.c_int32), + ("st_blksize", ctypes.c_int32), + ("__pad2", ctypes.c_int32), # 0 or address of new value */ + ("st_blocks", ctypes.c_int64), + ("st_atime", ctypes.c_int32), + ("st_atime_nsec", ctypes.c_uint32), + ("st_mtime", ctypes.c_int32), + ("st_mtime_nsec", ctypes.c_uint32), + ("st_ctime", ctypes.c_int32), + ("st_ctime_nsec", ctypes.c_uint32), + # ('__unused4', ctypes.c_uint32), + # ('__unused5', ctypes.c_uint32), + ] diff --git a/src/zelos/ext/platforms/linux/syscalls/syscall_utils.py b/src/zelos/ext/platforms/linux/syscalls/syscall_utils.py new file mode 100644 index 0000000..9347ac4 --- /dev/null +++ b/src/zelos/ext/platforms/linux/syscalls/syscall_utils.py @@ -0,0 +1,118 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + + +def twos_comp(val, bits): + """compute the 2's complement of int value val""" + if ( + val & (1 << (bits - 1)) + ) != 0: # if sign bit is set e.g., 8bit: 128-255 + val = val - (1 << bits) # compute negative value + return val # return positive value as is + + +_FSMSR = 0xC0000100 +_GSMSR = 0xC0000101 + + +def _set_msr(p, msr, value): + """ + set the given model-specific register (MSR) to the given value. + this will clobber some memory at the given scratch address, as it + emits some code. + """ + emu = p.emu + memory = p.memory + # save clobbered registers + orax = emu.get_reg("rax") + ordx = emu.get_reg("rdx") + orcx = emu.get_reg("rcx") + orip = emu.get_reg("rip") + + # In addition, special handling needs to be done for setting and + # getting the fs and gs registers + # x86: wrmsr + buf = b"\x0f\x30" + buf_ptr = memory.map_anywhere(2, "wrmsr inst") + memory.write(buf_ptr, buf) + # x86: wrmsr + emu.set_reg("rax", value & 0xFFFFFFFF) + emu.set_reg("rdx", (value >> 32) & 0xFFFFFFFF) + emu.set_reg("rcx", msr & 0xFFFFFFFF) + emu.emu_start(buf_ptr, buf_ptr + len(buf), count=1) + + # stop for all syscalls + + # restore clobbered registers + emu.set_reg("rax", orax) + emu.set_reg("rdx", ordx) + emu.set_reg("rcx", orcx) + emu.set_reg("rip", orip) + + +def _get_msr(p, msr): + """ + fetch the contents of the given model-specific register (MSR). + this will clobber some memory at the given scratch address, as it + emits some code. + """ + + emu = p.emu + memory = p.memory + # save clobbered registers + orax = emu.get_reg("rax") + ordx = emu.get_reg("rdx") + orcx = emu.get_reg("rcx") + orip = emu.get_reg("rip") + + # x86: rdmsr + buf = "\x0f\x32" + buf_ptr = memory.heap.alloc(2, "wrmsr inst") + memory.write(buf_ptr, buf) + + emu.set_reg("rcx", msr & 0xFFFFFFFF) + emu.emu_start(buf_ptr, buf_ptr + len(buf), count=1) + eax = emu.get_reg("eax") + edx = emu.get_reg("edx") + + # restore clobbered registers + emu.set_reg("rax", orax) + emu.set_reg("rdx", ordx) + emu.set_reg("rcx", orcx) + emu.set_reg("rip", orip) + + return (edx << 32) | (eax & 0xFFFFFFFF) + + +def set_gs(p, addr): + GSMSR = 0xC0000101 + return _set_msr(p, GSMSR, addr) + + +def get_gs(p): + GSMSR = 0xC0000101 + return _get_msr(p, GSMSR) + + +def set_fs(p, addr): + FSMSR = 0xC0000100 + return _set_msr(p, FSMSR, addr) + + +def get_fs(p): + FSMSR = 0xC0000100 + return _get_msr(p, FSMSR) diff --git a/src/zelos/ext/platforms/linux/syscalls/syscalls.py b/src/zelos/ext/platforms/linux/syscalls/syscalls.py new file mode 100644 index 0000000..71cd810 --- /dev/null +++ b/src/zelos/ext/platforms/linux/syscalls/syscalls.py @@ -0,0 +1,1885 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import ctypes +import datetime +import enum +import os +import time + +from os import path + +from zelos import handles +from zelos.exceptions import ZelosLoadException +from zelos.threads import ThreadState +from zelos.util import align, dumpstruct, str2struct, struct2str + +from ..signals import Signal +from . import syscall_structs as structs +from . import syscall_utils as sys_utils +from . import syscalls_socket as socketcall +from .syscalls_const import FCNTL_COMMANDS, PRCTL, SysError + + +def ptr2struct(z, addr, struct_class): + """ + Returns an instance of struct_class read starting from addr + """ + data = z.memory.read(addr, ctypes.sizeof(struct_class)) + instance = struct_class() + str2struct(instance, bytes(data)) + return instance + + +def get_pchar_array(z, addr, size=-1): + """ + Reads a set of string pointers starting at addr up to the first + null pointer (with a max of size, if specified) + Returns a list of null-terminated strings read from those pointers. + """ + if addr == 0: + return [] + result = [] + i = 0 + while i != size: + pstr = z.memory.read_int(addr + i * z.state.bytes) + if pstr == 0: + break + result.append(z.memory.read_string(pstr)) + i += 1 + return result + + +def sys_brk(sm, p): + # Returns the location of the system break. + args = sm.get_args([("void*", "addr")]) + if args.addr == 0: + return p.memory.heap.current_offset + + # asking for more memory + memory_to_alloc = args.addr - p.memory.heap.current_offset + if memory_to_alloc > 0: + sm.logger.debug( + f"sys_brk heap manager allocs " + f"{memory_to_alloc:x}, {memory_to_alloc}" + ) + p.memory.heap.alloc(memory_to_alloc, name="sys_brk", align=0x1) + elif memory_to_alloc < 0: + p.memory.heap.dealloc(-memory_to_alloc) + # Always return the new location of the break. If failure, this is + # the same as the old location. + return p.memory.heap.current_offset + + +def sys_syscall(sm, p): + _ = sm.get_args([("long", "number")]) + return 0 + + +def sys_close(sm, p): + args = sm.get_args([("int", "fd")]) + if sm.z.handles.get(args.fd) is None: + return SysError.EBADF + sm.z.handles.close(args.fd) + return 0 + + +def sys_cacheflush(sm, p): + sm.get_args([]) + return 0 + + +def sys_unlink(sm, p): + sm.get_args([("const char*", "pathname")]) + return 0 + + +def sys_uname(sm, p): + args = sm.get_args([("struct utsname*", "buf")]) + uname_data = ( + "Linux", # sysname + "zelos-tower", # nodename + "4.18.0-25-generic", # release + "#26~18.04.1-Ubuntu SMP Thu Jun 27 07:28:31 UTC 2019", + "armv7l", + "(none)", + ) + for i, data in enumerate(uname_data): + padded_data = data + "\x00" * (64 - len(data)) + p.memory.write_string(args.buf + 65 * i, padded_data) + return 0 + + +def sys_creat(sm, p): + args = sm.get_args([("const char*", "pathname"), ("mode_t", "mode")]) + O_CREAT = 0x40 + O_WRONLY = 0x1 + O_TRUNC = 0x200 + args.flags = O_CREAT | O_WRONLY | O_TRUNC + return xopen(sm, p, args) + + +def sys_open(sm, p): + args = sm.get_args([("const char*", "pathname"), ("int", "flags")]) + return xopen(sm, p, args) + + +def sys_openat(sm, p): + args = sm.get_args( + [("int", "dirfd"), ("const char*", "pathname"), ("int", "flags")] + ) + return xopen(sm, p, args) + + +def xopen(sm, p, args): + # TODO: impl. the different modes + # O_ACCMODE = 0x3 + # O_RDONLY = 0x0 + # O_WRONLY = 0x1 + # O_RDWR = 0x2 + # O_CREAT = 0x40 + # O_EXCL = 0x80 + # O_NOCTTY = 0x100 + # O_TRUNC = 0x200 + # O_APPEND = 0x400 + # O_NONBLOCK = 0x800 + + pathname_s = p.memory.read_string(args.pathname) + path = sm.z.files.find_library(pathname_s) + sm.z.triggers.tr_file_open(pathname_s) + if path is not None: + handle_num = sm.z.handles.new_file(pathname_s) + retval = handle_num + elif args.flags & 0x200 != 0 or args.flags & 0x40 != 0: + sm.z.files.write_to_sandbox(pathname_s, b"") + handle_num = sm.z.handles.new_file(pathname_s) + retval = handle_num + else: + retval = -1 + return retval + + +def sys_readv(sm, p): + args = sm.get_args( + [("int", "fd"), ("const struct iovec*", "iov"), ("int", "iovcnt")] + ) + + handle = sm.z.handles.get(args.fd) + if handle is None: + return 0 + + bytes_read = 0 + for i in range(args.iovcnt): + iovec = p.memory.read_ptr(args.iov + 0x8 * i) + iov_len = p.memory.read_uint32(args.iov + 0x8 * i + 0x4) + if iov_len == 0: + continue + if isinstance(handle, handles.SocketHandle): + bread = socketcall._recv(sm, p, args.fd, iovec, iov_len) + elif isinstance(handle, handles.FileHandle): + with sm.z.files.open_library(handle.Name) as f: + f.seek(handle.Offset) + data = f.read(iov_len) + handle.seek(iov_len, os.SEEK_CUR) + p.memory.write(iovec, data) + bread = len(data) + else: + continue + bytes_read += bread + + return bytes_read + + +def sys_writev(sm, p): + def print_iov(args): + s = "" + for i in range(0, args.iovcnt): + iov_addr = args.iov + i * 2 * p.memory.state.bytes + base_addr = p.memory.read_ptr(iov_addr) + bytes_to_read = p.memory.read_int(iov_addr + p.memory.state.bytes) + try: + s += repr(bytes(p.memory.read(base_addr, size=bytes_to_read)))[ + 2:-1 + ] + except Exception: + pass + if len(s) == 0: + return f"*iov=0x{args.iov:x}" + return f'*iov=0x{args.iov:x} ("{s}")' + + args = sm.get_args( + [("int", "fd"), ("const struct iovec*", "iov"), ("int", "iovcnt")], + arg_string_overrides={"iov": print_iov}, + ) + + bytes_written = 0 + # TODO this should just be a struct with the correct sizes in there. + word_size = 8 if sm.arch == "x86_64" else 4 + for i in range(args.iovcnt): + iovec = p.memory.read_ptr(args.iov + 2 * word_size * i) + iov_len = p.memory.read_uint32( + args.iov + 2 * word_size * i + word_size + ) + iov_base_content = p.memory.read(iovec, iov_len) + + handle = sm.z.handles.get(args.fd) + if handle is not None and hasattr(handle, "write"): + handle.write(iov_base_content) + else: + sm.print(f"[writev:{args.fd}]: '{iov_base_content}'") + bytes_written += iov_len + return bytes_written + + +def sys_madvise(sm, p): + sm.get_args([("void*", "addr"), ("size_t", "length"), ("int", "advice")]) + return 0 + + +def sys_msync(sm, p): + sm.get_args([("void*", "addr"), ("size_t", "length"), ("int", "flags")]) + return 0 + + +def sys_mmap(sm, p): + args = sm.get_args( + [ + ("void*", "addr"), + ("size_t", "length"), + ("int", "prot"), + ("int", "flags"), + ("int", "fd"), + ("off_t", "offset"), + ] + ) + try: + return mmapx(sm, p, "mmap", args, args.offset) + except Exception as e: + sm.print("mmap exception: " + str(e)) + return -1 + + +def sys_mmap2(sm, p): + args = sm.get_args( + [ + ("void*", "addr"), + ("size_t", "length"), + ("int", "prot"), + ("int", "flags"), + ("int", "fd"), + ("off_t", "pgoffset"), + ] + ) + try: + return mmapx(sm, p, "mmap2", args, args.pgoffset * 0x1000) + except Exception as e: + sm.print("mmap2 exception: " + str(e)) + return -1 + + +def mmapx(sm, p, syscall_name, args, offset): + MAP_SHARED = 0x1 + memory_region_name = syscall_name + handle = sm.z.handles.get(args.fd) + if handle is not None: + memory_region_name = f"{syscall_name} -> {handle.Name}" + + addr = args.addr + if addr == 0: + addr = p.memory._find_free_space(args.length) + + data = b"" + if handle is not None: + f = sm.z.files.open_library(handle.Name) + if f is not None: + f.seek(offset) + data = f.read(args.length) + f.close() + + data += b"\0" * (args.length - len(data)) + + # If this is shared, map it with the pointer + if args.flags & MAP_SHARED != 0: + print(len(data)) + ptr = ctypes.POINTER(ctypes.c_void_p)( + ctypes.c_void_p.from_buffer(bytearray(data)) + ) + p.memory.map( + addr, + align(args.length), + name=memory_region_name, + kind=syscall_name, + ptr=ptr, + ) + return addr + + try: + p.memory.map( + addr, + align(args.length), + name=memory_region_name, + kind=syscall_name, + ) + except Exception: + if args.flags & 0x10 > 0: + # This must be mapped to this region, we should be able to + # just write over the existing data. + # This should crash if we are unable to write to the desired + # region + pass + else: + sm.logger.notice(f"Address {addr:x} already mapped") + addr = p.memory.map_anywhere( + args.length, name=memory_region_name, kind=syscall_name + ) + + p.memory.write(addr, data) + + return addr + + +def sys_munmap(sm, p): + sm.get_args([("void*", "addr"), ("size_t", "length")]) + return 0 + + +def sys_mprotect(sm, p): + sm.get_args([("void*", "addr"), ("size_t", "len"), ("int", "prot")]) + return 0 + + +class USERDESC(ctypes.Structure): + _fields_ = [ + ("entry_number", ctypes.c_uint32), + ("base_address", ctypes.c_uint32), + ("limit", ctypes.c_uint32), + ("seg_32bit", ctypes.c_ubyte), + ("contents", ctypes.c_uint16), + ("read_exec_only", ctypes.c_ubyte), + ("limit_in_pages", ctypes.c_ubyte), + ("seg_not_present", ctypes.c_ubyte), + ("useable", ctypes.c_ubyte), + # ('lm', ctypes.c_ubyte), # only for x86_64 + ] + + +# arch/arm/kernel/traps.c: +# case NR(set_tls): +# thread->tp_value = regs->ARM_r0; +# #if defined(CONFIG_HAS_TLS_REG) +# asm ("mcr p15, 0, %0, c13, c0, 3" : : "r" (regs->ARM_r0) ); +# #elif !defined(CONFIG_TLS_REG_EMUL) +# *((unsigned int *)0xffff0ff0) = regs->ARM_r0; +# #endif +# return 0; + + +def sys_set_tls(sm, p): + args = sm.get_args([("CPUARMState*", "env")]) + sm.emu.set_reg("c13_c0_3", args.env) + sm.set_return_value(0) + + +def sys_set_thread_area(sm, p): + if sm.arch == "mips": + return mips_set_thread_area(sm, p) + + from zelos.emulator.x86_gdt import GDT_32 + + args = sm.get_args([("struct user_desc*", "u_info")]) + userdesc = ptr2struct(sm.z, args.u_info, USERDESC) + p.memory.write_int(args.u_info, 0xC) + + flags = GDT_32.gdt_entry_flags( + gr=0, sz=1, pr=1, privl=3, ex=0, dc=0, rw=1, ac=1 + ) # 0x4f3 + p.gdt.set_entry(0xC, userdesc.base_address, 0xFFF, flags) + + tdata = sm.z.main_module.Tls + p.memory.write(userdesc.base_address - len(tdata), bytes(tdata)) + return 0 + + +def mips_set_thread_area(sm, p): + args = sm.get_args([("unsigned long", "addr")]) + p.emu.set_reg("cp0_userlocal", args.addr) + p.emu.set_reg("a3", 0) + return 0 + + +def sys_read(sm, p): + args = sm.get_args([("int", "fd"), ("void*", "buf"), ("size_t", "count")]) + handle = sm.z.handles.get(args.fd) + + if handle is None: + return 0 + data = "" + if isinstance(handle, handles.SocketHandle): + return socketcall._recv(sm, p, args.fd, args.buf, args.count) + if isinstance(handle, handles.PipeOutHandle): + if handle.pipe.is_empty(): + if handle.pipe.write_end_closed: + return 0 # End-of-file + + def unpause_when(): + return (not handle.pipe.is_empty()) or ( + handle.pipe.write_end_closed + ) + + sm.pause_syscall(p, condition=unpause_when) + return + data = handle.read(args.count) + p.memory.write(args.buf, data) + return len(data) + if isinstance(handle, handles.PipeInHandle): + return SysError.EBADF + + try: + f = sm.z.files.open_library(handle.Name) + except PermissionError: + return SysError.EACCES + if f is not None: + f.seek(sm.offset_dict.get(args.fd, 0)) + data = f.read(args.count) + p.memory.write(args.buf, data) + sm.offset_dict[args.fd] += len(data) + return len(data) + + +def sys_geteuid32(sm, p): + sm.get_args([]) + return 1000 + + +def sys_geteuid(sm, p): + sm.get_args([]) + return 0 + + +def sys_getuid32(sm, p): + sm.get_args([]) + return 1000 + + +def sys_getegid32(sm, p): + sm.get_args([]) + return 0xD11B + + +def sys_getgid32(sm, p): + sm.get_args([]) + return 0xD11B + + +def sys_getuid(sm, p): + sm.get_args([]) + return 0 + + +def sys_getegid(sm, p): + sm.get_args([]) + return 0xD11B + + +def sys_getgid(sm, p): + sm.get_args([]) + return 0xD11B + + +def sys_setpgid(sm, p): + sm.get_args([("pid_t", "pid"), ("pid_t", "pgid")]) + return 0 + + +def sys_setgid32(sm, p): + sm.get_args([("gid_t", "gid")]) + return 0 + + +def sys_setuid32(sm, p): + sm.get_args([("uid_t", "uid")]) + return 0 + + +def sys_setsid(sm, p): + sm.get_args([]) + return 0xD00B + + +def sys_getgroups(sm, p): + sm.get_args([("int", "size"), ("gid_t[]", "list")]) + return 0 + + +def sys__llseek( + sm, p +): # There may be a bug in gcc_coreutils_32_o0_tail with this function + args = sm.get_args( + [ + ("unsigned int", "fd"), + ("unsigned long", "offset_high"), + ("unsigned long", "offset_low"), + ("loff_t *", "result"), + ("unsigned int", "whence"), + ] + ) + offset = sys_utils.twos_comp( + (args.offset_high << 32) | args.offset_low, 64 + ) + xlseek(sm, args.fd, offset, args.whence) + sm.offset_dict[args.fd] &= 0xFFFFFFFF + handle = sm.z.handles.get(args.fd) + sm.z.logger.debug( + f"File {handle.Name} ({args.fd:x}) to {sm.offset_dict[args.fd]}" + ) + p.memory.write_int(args.result, sm.offset_dict[args.fd]) + return 0 + + +def sys_lseek(sm, p): + args = sm.get_args( + [("unsigned int", "fd"), ("off_t", "offset"), ("int", "whence")] + ) + offset = p.emu.to_signed(args.offset) + xlseek(sm, args.fd, offset, args.whence) + return sm.offset_dict[args.fd] + + +def xlseek(sm, fd, offset, whence): + if whence == 0: # SEEK_SET + sm.offset_dict[fd] = offset + elif whence == 1: # SEEK_CUR + sm.offset_dict[fd] += offset + elif whence == 2: # SEEK_END + handle = sm.z.handles.get(fd) + library_path = sm.z.files.find_library(handle.Name) + sm.offset_dict[fd] = path.getsize(library_path) + offset + + +def sys_readlink(sm, p): + args = sm.get_args( + [("const char*", "pathname"), ("char*", "buf"), ("size_t", "bufsiz")] + ) + # TODO: This is bypassing the filesystem protections, this should not be + # allowed without doing a validation in the filesystem first + try: + pathname = p.memory.read_string(args.pathname) + linked_path = os.readlink(pathname) + s_len = p.memory.write_string( + args.buf, linked_path, terminal_null_byte=False + ) + return s_len + except OSError: + return -1 + + +def sys_readlinkat(sm, p): + args = sm.get_args( + [ + ("int", "dirfd"), + ("const char*", "pathname"), + ("char*", "buf"), + ("size_t", "bufsiz"), + ] + ) + try: + pathname = p.memory.read_string(args.pathname) + linked_path = os.readlink(pathname) + s_len = p.memory.write_string( + args.buf, linked_path, terminal_null_byte=False + ) + return s_len + except OSError: + return -1 + + +def sys_getcwd(sm, p): + args = sm.get_args([("char*", "buf"), ("size_t", "size")]) + size = p.memory.write_string(args.buf, sm.z.files.zelos_file_prefix) + + return size + + +def sys_faccessat(sm, p): + args = sm.get_args( + [ + ("int", "dirfd"), + ("const char*", "pathname"), + ("int", "mode"), + ("int", "flags"), + ] + ) + pathname_s = p.memory.read_string(args.pathname) + sm.z.triggers.tr_file_check(pathname_s) + retval = -1 + if sm.z.files.find_library(pathname_s) is not None: + retval = 0 + return retval + + +def sys_access(sm, p): + args = sm.get_args([("const char*", "pathname"), ("int", "mode")]) + pathname_s = p.memory.read_string(args.pathname) + sm.z.triggers.tr_file_check(pathname_s) + retval = -1 + if sm.z.files.find_library(pathname_s) is not None: + retval = 0 + + return retval + + +class FCNTL(enum.IntEnum): + """ + FCNTL Flags + https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/fcntl.h + """ + + O_ACCMODE = 0o00000003 + O_RDONLY = 0o00000000 + O_WRONLY = 0o00000001 + O_RDWR = 0o00000002 + O_CREAT = 0o00000100 # /* not fcntl */ + O_EXCL = 0o00000200 # /* not fcntl */ + O_NOCTTY = 0o00000400 # /* not fcntl */ + O_TRUNC = 0o00001000 # /* not fcntl */ + O_APPEND = 0o00002000 + O_NONBLOCK = 0o00004000 + O_DSYNC = 0o00010000 # /* used to be O_SYNC, see below */ + FASYNC = 0o00020000 # /* fcntl, for BSD compatibility */ + O_DIRECT = 0o00040000 # /* direct disk access hint */ + O_LARGEFILE = 0o00100000 + O_DIRECTORY = 0o00200000 # /* must be a directory */ + O_NOFOLLOW = 0o00400000 # /* don't follow links */ + O_NOATIME = 0o01000000 + O_CLOEXEC = 0o02000000 # /* set close_on_exec */ + __O_SYNC = 0o04000000 + O_SYNC = 0o04010000 + O_PATH = 0o10000000 + __O_TMPFILE = 0o20000000 + O_TMPFILE = 0o20200000 + O_TMPFILE_MASK = 0o20200100 + O_NDELAY = 0o00004000 + + +def sys_fcntl64(sm, p): + return sys_fcntl(sm, p) + + +def sys_fcntl(sm, p): + args = sm.get_args([("int", "fd"), ("int_FCNTL", "cmd"), ("int", "arg")]) + cmd_name = FCNTL_COMMANDS.get(args.cmd, "unknown") + + if cmd_name == "F_GETFL": + # return fd flags + pass + elif cmd_name == "F_SETFL": + handle = sm.z.handles.get(args.fd) + if isinstance(handle, handles.SocketHandle): + handle.socket.set_nonblock(args.arg & FCNTL.O_NONBLOCK != 0) + + return 0 + + +def sys_lstat(sm, p): + return sys_stat(sm, p) + + +def sys_lstat64(sm, p): + return sys_stat64(sm, p) + + +def sys_stat(sm, p): + stat_struct = structs.get_stat_struct(sm.arch) + return _statx(sm, p, stat_struct) + + +def sys_stat64(sm, p): + return _statx(sm, p, structs.STAT64()) + + +def _statx(sm, p, struct): + args = sm.get_args( + [("const char*", "pathname"), ("struct stat*", "statbuf")] + ) + pathname_s = p.memory.read_string(args.pathname) + + library_path = sm.z.files.find_library(pathname_s) + if library_path is None or not path.exists(library_path): + return -1 + + statinfo = os.stat(library_path) + + retval = _handle_xstat64(sm, p, args.statbuf, statinfo, struct) + return retval + + +def sys_fstat(sm, p): + stat_struct = structs.get_stat_struct(sm.arch) + return _fstatx(sm, p, stat_struct) + + +def sys_fstat64(sm, p): + return _fstatx(sm, p, structs.STAT64()) + + +def _fstatx(sm, p, struct): + args = sm.get_args([("int", "fd"), ("struct stat*", "statbuf")]) + if args.fd in [0, 1, 2]: + statinfo = os.fstat(args.fd) + else: + handle = sm.z.handles.get(args.fd) + if handle is None: + sm.logger.notice("Invalid handle") + return -1 + + library_path = sm.z.files.find_library(handle.Name) + if library_path is None or not path.exists(library_path): + return -1 + + statinfo = os.stat(library_path) + retval = _handle_xstat64(sm, p, args.statbuf, statinfo, struct) + return retval + + +def _handle_xstat64(sm, p, buf_addr, statinfo, stat_struct): + + stat_struct.st_dev = statinfo.st_dev + stat_struct.st_ino = statinfo.st_ino + stat_struct.st_mode = statinfo.st_mode + stat_struct.st_nlink = 1 + stat_struct.st_uid = statinfo.st_uid + stat_struct.st_gid = statinfo.st_gid + stat_struct.st_rdev = 0 + stat_struct.st_size = statinfo.st_size + stat_struct.st_blksize = 4096 + stat_struct.st_blocks = statinfo.st_size // 512 + 1 + stat_struct.st_atime = 0x100 + stat_struct.st_atime_nsec = 0x200 + stat_struct.st_mtime = int(statinfo.st_mtime) + stat_struct.st_mtime_nsec = 0x400 + stat_struct.st_ctime = 0x500 + stat_struct.st_ctime_nsec = 0x600 + + p.memory.writestruct(buf_addr, stat_struct) + + return 0 + + +run_once = None + +# class DIRENT64(ctypes.Structure): +# _fields_ = [ +# ('d_ino', ctypes.c_uint64), +# ('d_off', ctypes.c_uint64), +# ('d_reclen', ctypes.c_uint16), +# ('d_type', ctypes.c_ubyte), +# ('d_name', ctypes.c_char_p), # Unsure how to handle this since +# # its actually a char array, not a pointer to a char array +# ] + + +def sys_getdents(sm, p): + args = sm.get_args( + [ + ("unsigned int", "fd"), + ("struct linux_dirent *", "dirp"), + ("unsigned int", "count"), + ] + ) + + handle = sm.z.handles.get(args.fd) + if handle is None: + return -1 + + folder_contents = handle.data.get("dents", None) + if folder_contents is None: + # Get the dents and run this function + folder_contents = sm.z.files.list_dir(handle.Name) + if len(folder_contents) == 0: + handle.data["dents"] = folder_contents + return 0 + + prev_struct_start = None + struct_start = args.dirp + total_bytes_written = 0 + while len(folder_contents) > 0: + full_name = os.path.join(handle.Name, folder_contents[-1]) + bytes_written = _write_dirent_x86_64( + sm, + p, + full_name, + folder_contents[-1], + struct_start, + prev_struct_start, + args.dirp + args.count, + ) + if bytes_written == 0: + break + else: + folder_contents.pop() + total_bytes_written += bytes_written + prev_struct_start = struct_start + struct_start = align(struct_start + bytes_written, alignment=0x4) + + handle.data["dents"] = folder_contents + return total_bytes_written + + +def _write_dirent_x86_64( + sm, p, full_name, basename, struct_start, prev_struct_start, max_addr +): + struct_len = align(len(basename) + 2 + 0x12, 4) + if struct_start + struct_len > max_addr: + return 0 + + library_path = sm.z.files.find_library(full_name) + if library_path is None or not path.exists(library_path): + return -1 + + statinfo = os.stat(library_path) + p.memory.write_uint64(struct_start, statinfo.st_ino) + # This will be overridden in the next call to this func + p.memory.write_uint64(struct_start + 0x8, 0) # next struct_start + p.memory.write_uint16(struct_start + 0x10, struct_len) + p.memory.write_string( + struct_start + 0x12, basename, terminal_null_byte=True + ) + p.memory.write_uint8(struct_start + struct_len - 1, 8) # regular + + if prev_struct_start is not None: + p.memory.write_uint64(prev_struct_start + 0x8, struct_start) + + return struct_len + + +def sys_getdents64(sm, p): + global run_once + if run_once is not None: + return 0 + + args = sm.get_args( + [ + ("unsigned int", "fd"), + ("struct linux_dirent64 *", "dirp"), + ("unsigned int", "count"), + ] + ) + # struct linux_dirent64 { + # ino64_t d_ino; /* 64-bit inode number */ + # off64_t d_off; /* 64-bit offset to next struct */ + # unsigned short d_reclen; /* Size of this dirent */ + # unsigned char d_type; /* File type */ + # char d_name[]; /* Filename (null-terminated) */ + # }; + + p.memory.write_int(args.dirp + 0x0, 56, sz=8) + p.memory.write_int(args.dirp + 0x8, 0x0, sz=8) + + p.memory.write_int(args.dirp + 0x12, 6, sz=1) + s_len = p.memory.write_string(args.dirp + 0x13, "FolderContents") + struct_size = align(0x13 + s_len, 4) + + # val = bytes([0xb + s_len, 0xb + s_len]) + # from zelos.util import p16 + # val2 = p16(0x13+s_len +0x100 ) + + # p.memory.write(args.dirp + 0x10, bytes([0x30])) + + p.memory.write_int(args.dirp + 0x10, struct_size, sz=2) + run_once = 1 + return struct_size + + +def sys_ftruncate(sm, p): + sm.get_args([("int", "fd"), ("off_t", "length")]) + # TODO + # handle = sm.z.handles.get(args.fd) + + return 0 + + +def sys_write(sm, p): + def print_buf(args): + s = repr(bytes(p.memory.read(args.buf, size=args.count)))[2:-1] + return f'buf=0x{args.buf:x} ("{s}")' + + args = sm.get_args( + [("int", "fd"), ("const void*", "buf"), ("size_t", "count")], + arg_string_overrides={"buf": print_buf}, + ) + + s = p.memory.read(args.buf, args.count) + + handle = sm.z.handles.get(args.fd) + # Just fake the write if we don't have the handle + if handle is None: + return len(s) + elif isinstance(handle, handles.PipeOutHandle): + return SysError.EBADF + + if hasattr(handle, "write"): + handle.write(s) + else: + sm.z.triggers.tr_file_write( + f"{type(handle).__name__}, {handle.Name}", s + ) + if isinstance(handle, handles.SocketHandle): + payload = p.memory.read(args.buf, args.count) + sent_bytes = socketcall._send(sm, p, args.fd, payload) + return sent_bytes + sm.print(s) + return len(s) + + +def sys_dup2(sm, p): + args = sm.get_args([("int", "oldfd"), ("int", "newfd")]) + handle = sm.z.handles.get(args.oldfd) + if handle is not None: + sm.z.handles.add_handle(handle, args.newfd) + return args.newfd + + +def sys_dup3(sm, p): + args = sm.get_args([("int", "oldfd"), ("int", "newfd"), ("int", "flags")]) + handle = sm.z.handles.get(args.oldfd) + if handle is not None: + sm.z.handles.add_handle(handle, args.newfd) + return args.newfd + + +def sys_pipe2(sm, p): + args = sm.get_args([("int[2]", "pipefd"), ("int", "flags")]) + return _pipe(sm, p, args.pipefd, args.flags) + + +def sys_pipe(sm, p): + args = sm.get_args([("int[2]", "pipefd")]) + return _pipe(sm, p, args.pipefd, None) + + +def _pipe(sm, p, pipefd, flags): + (out_handle_num, in_handle_num) = sm.z.handles.new_pipe("") + p.memory.write_int(pipefd, out_handle_num) + p.memory.write_int(pipefd + 4, in_handle_num) # valid in x64 + sm.logger.info( + f"Pipe handles are out:{out_handle_num:x} in:{in_handle_num:x}" + ) + return 0 + + +def sys_ipc(sm, p): + sm.get_args( + [ + ("unsigned int", "call"), + ("int", "first"), + ("int", "second"), + ("int", "third"), + ("void *", "ptr"), + ("long", "fifth"), + ] + ) + return -1 + + +def sys_socketcall(sm, p): + args = sm.get_args([("int", "call"), ("unsigned long *", "callargs")]) + socket_dict = { + 1: socketcall.socket, + 2: socketcall.bind, + 3: socketcall.connect, + 4: socketcall.listen, + 5: socketcall.accept, + 6: socketcall.getsockname, + 7: socketcall.getpeername, + 8: socketcall.socketpair, + 9: socketcall.send, + 10: socketcall.recv, + 11: socketcall.sendto, + 12: socketcall.recvfrom, + 13: socketcall.shutdown, + 14: socketcall.setsockopt, + 15: socketcall.getsockopt, + 16: socketcall.sendmsg, + 17: socketcall.recvmsg, + 18: socketcall.accept4, + 19: socketcall.recvmmsg, + 20: socketcall.sendmmsg, + } + retval = socket_dict[args.call](sm, p, args.callargs) + return retval + + +def sys_socket(sm, p): + return socketcall.socket(sm, p, -1) + + +def sys_bind(sm, p): + return socketcall.bind(sm, p, -1) + + +def sys_connect(sm, p): + return socketcall.connect(sm, p, -1) + + +def sys_listen(sm, p): + return socketcall.listen(sm, p, -1) + + +def sys_accept(sm, p): + return socketcall.accept(sm, p, -1) + + +def sys_getsockname(sm, p): + return socketcall.getsockname(sm, p, -1) + + +def sys_getpeername(sm, p): + return socketcall.getpeername(sm, p, -1) + + +def sys_socketpair(sm, p): + return socketcall.socketpair(sm, p, -1) + + +def sys_send(sm, p): + return socketcall.send(sm, p, -1) + + +def sys_recv(sm, p): + return socketcall.recv(sm, p, -1) + + +def sys_sendto(sm, p): + return socketcall.sendto(sm, p, -1) + + +def sys_recvfrom(sm, p): + return socketcall.recvfrom(sm, p, -1) + + +def sys_shutdown(sm, p): + return socketcall.shutdown(sm, p, -1) + + +def sys_setsockopt(sm, p): + return socketcall.setsockopt(sm, p, -1) + + +def sys_getsockopt(sm, p): + return socketcall.getsockopt(sm, p, -1) + + +def sys_sendmsg(sm, p): + return socketcall.sendmsg(sm, p, -1) + + +def sys_recvmsg(sm, p): + return socketcall.recvmsg(sm, p, -1) + + +def sys_accept4(sm, p): + return socketcall.accept4(sm, p, -1) + + +def sys_recvmmsg(sm, p): + return socketcall.recvmmsg(sm, p, -1) + + +def sys_sendmmsg(sm, p): + return socketcall.sendmmsg(sm, p, -1) + + +def sys_clone(sm, p): + if sm.arch == "x86_64": + args = sm.get_args( + [ + ("unsigned long", "flags"), + ("void*", "child_stack"), + ("int*", "ptid"), + ("int*", "ctid"), + ("unsigned long", "newtls"), + ] + ) + else: + args = sm.get_args( + [ + ("unsigned long", "flags"), + ("void*", "child_stack"), + ("int*", "ptid"), + ("unsigned long", "newtls"), + ("int*", "ctid"), + ] + ) + + child_process = _new_process(sm, p) + parent_handles = sm.z.handles._all_handles(p.pid) + for num, h in parent_handles: + sm.z.handles.add_handle(h, handle_num=num, pid=child_process.pid) + try: + child_process.memory.write_uint32(args.ctid, child_process.pid) + except Exception: + pass + + # if args.newtls != 0: + # userdesc = ptr2struct(sm.z, args.newtls, USERDESC) + # # dumpstruct(userdesc) + + # # sm.z._add_tdata_before(userdesc.base_address) + # # sm.logger.error(f'ADDRESS: {userdesc.base_address}') + # # child_process.current_thread.local_data_address = + # # userdesc.base_address + + return child_process.pid + + +def sys_fork(sm, p): + sm.get_args([]) + + child_process = _new_process(sm, p) + return child_process.pid + + +def _new_process(sm, p): + processes = sm.z.processes + child_pid = processes.new_process() + child = processes.get_process(child_pid) + + # duplicate the state of the target process. + child.memory.copy(p.memory) + + # Create this same thread inside the process + current_thread = p.current_thread + child.new_thread( + sm.return_addr(), + priority=current_thread.priority, + module_path=current_thread.module_path, + ) + child.threads.swap_with_next_thread() + p.current_thread.save_context() + child.emu.context_restore(p.current_thread.context) + child.emu.setIP(sm.return_addr()) + processes._as_current_process(child, lambda: sm.set_return_value(0)) + child.current_thread.save_context() + + return child + + +# temporary implementation +# @@TODO: handle correctly (parent has to be suspended until +# child finishes) + + +def sys_vfork(sm, p): + sm.get_args([]) + current_thread_priority = p.current_thread.priority + t = sm.z.processes.new_thread_for_current_process( + sm.return_addr(), + module_path=p.current_thread.module_path, + priority=current_thread_priority + 1, + ) + + sm.z.thread_manager.swap_with_thread(tid=t.id) + return t.id + + +def sys_pause(sm, p): + sm.get_args([]) + return SysError.EINTR + + +def sys_wait4(sm, p): + args = sm.get_args( + [ + ("pid_t", "pid"), + ("int*", "wstatus"), + ("int", "options"), + ("struct rusage*", "rusage"), + ] + ) + state_changes = sm.child_state_changes[p.pid] + if len(state_changes) > 0: + if args.pid in [0, 0xFFFFFFFF]: + return state_changes.pop(0) + if args.pid in state_changes: + return state_changes.pop(state_changes.index(args.pid)) + + # Wait for any children. + if args.pid in [0, 0xFFFFFFFF]: + children = p.get_child_processes() + if len(children) == 0: + sm.logger.notice( + f"Can't wait on id {args.pid}, " + f"couldn't find corresponding thread" + ) + return SysError.ECHILD + + active_children = [c for c in children if c.is_active] + if len(active_children) == 0: + return SysError.ECHILD + + def unpause_when(): + for child in active_children: + if not child.is_active: + return True + return False + + sm.pause_syscall(p, condition=unpause_when) + # TODO: Should be returning the newly paused thread's pid + return 0 + + target_thread = sm.z.processes.get_thread(args.pid) + if target_thread is None: + sm.logger.notice( + f"Can't wait on id {args.pid}, " + f"couldn't find corresponding thread" + ) + return 0 + # (V) If this thread waits on itself... uh... unsure what to do. + # Reduce its priority so other threads finish before coming back. + if target_thread.id == p.current_thread.id: + target_thread.priority -= 1 + p.scheduler.stop_and_exec( + "process swap", sm.z.processes.swap_with_next_thread + ) + else: + + def unpause_when(): + return target_thread.state != ThreadState.RUNNING + + p.threads.pause_current_thread(condition=unpause_when) + return args.pid + + +def sys_sched_getscheduler(sm, p): + sm.get_args([("pid_t", "pid")]) + return 1 + + +def sys_sched_getaffinity(sm, p): + sm.get_args( + [("pid_t", "pid"), ("size_t", "cpusetsize"), ("cpu_set_t *", "mask")] + ) + return -1 + + +def sys_execve(sm, p): + def print_argv(args): + vals = get_pchar_array(sm.z, args.argv) + s = " ".join(vals) + return f'*argv=0x{args.argv:x} ("{s}")' + + def print_envp(args): + vals = get_pchar_array(sm.z, args.envp) + s = " ".join(vals) + return f'*envp=0x{args.envp:x} ("{s}")' + + args = sm.get_args( + [ + ("const char*", "pathname"), + ("char *const", "argv"), + ("char *const", "envp"), + ], + arg_string_overrides={"argv": print_argv, "envp": print_envp}, + ) + + argv = get_pchar_array(sm.z, args.argv) + envp = get_pchar_array(sm.z, args.envp) + pathname = p.memory.read_string(args.pathname) + + sm.logger.debug("Replacing first argument with pathname") + if len(argv) > 0: + argv[0] = pathname + + # FIXME: execve is not working. It executes the main binary's + # entrypoint again. + return + + p.memory.clear() + + p.cmdline_args = argv + p.environment_variables = envp + + # You can also exec shell files + try: + with open(pathname, "rb") as f: + string = f.readline() + if string.startswith(b"#! /bin/sh"): + p.cmdline_args.insert(0, "/bin/sh") + pathname = "/bin/sh" + except FileNotFoundError: + pass + except PermissionError: + return SysError.EACCES + + try: + file = sm.z.parse_file(pathname) + sm.z.files.add_file(pathname) + except ZelosLoadException: + return SysError.ENOENT + + sm.z.os_plugins.load(file, sm.z.current_process) + + # If this is successful, this thread essentially ends. + # TODO: we should have a list of things that can be execve'd, to + # make this configurable + p.threads.complete_current_thread() + return + + +def sys_exit(sm, p): + return sys_exit_group(sm, p) + + +def sys_exit_group(sm, p): + args = sm.get_args([("int", "status")]) + + sm.z.processes.handles.close_all(p.pid) + if args.status == 0: + p.threads.complete_current_thread() + else: + p.threads.fail_current_thread( + fail_reason=f"syscall Exit_Group status {args.status}" + ) + + if p.parent_pid is not None: + parent = sm.z.processes.get_process(p.parent_pid) + sm.child_state_changes[parent.pid].append(p.pid) + # parent.signals.handle_signal(17) + + +def sys_time(sm, p): + args = sm.get_args([("time_t*", "tloc")]) + current_time = time.mktime( + datetime.datetime.strptime(sm.z.date, "%Y-%m-%d").timetuple() + ) + current_time = round(current_time) + if args.tloc != 0: + p.memory.write_int(args.tloc, current_time) + return current_time + + +def sys_gettimeofday(sm, p): + args = sm.get_args([("struct timeval*", "tv"), ("struct timezone*", "tz")]) + if not args.tv: + return 0 + current_time = time.mktime( + datetime.datetime.strptime(sm.z.date, "%Y-%m-%d").timetuple() + ) + second, microsecond = str(current_time).split(".") + try: + p.memory.write_uint32(args.tv + 0x0, int(second)) + p.memory.write_uint32(args.tv + 0x4, int(microsecond)) + return 0 + except Exception: + pass + + return -1 + + +def sys_clock_gettime(sm, p): + args = sm.get_args([("clockid_t", "clk_id"), ("struct timespec *", "res")]) + current_time = time.mktime( + datetime.datetime.strptime(sm.z.date, "%Y-%m-%d").timetuple() + ) + second, microsecond = str(current_time).split(".") + try: + p.memory.write_uint32(args.res + 0x0, int(second)) + p.memory.write_uint32(args.res + 0x4, int(microsecond)) + return 0 + except Exception: + pass + return -1 + + +def sys_set_robust_list(sm, p): + sm.get_args([("struct robust_list_head *", "head"), ("size_t", "len")]) + return 0 + + +def sys_set_tid_address(sm, p): + sm.get_args([("int*", "tidptr")]) + return p.current_thread.id + + +def sys_getpid(sm, p): + sm.get_args([]) + return p.pid + + +def sys_getppid(sm, p): + sm.get_args([]) + return p.parent_pid + + +def sys_times(sm, p): + sm.get_args([("struct tms*", "buf")]) + # struct tms { + # clock_t tms_utime; /* user time */ + # clock_t tms_stime; /* system time */ + # clock_t tms_cutime; /* user time of children */ + # clock_t tms_cstime; /* system time of children */ + # }; + return 0xDEED + + +class __SYSCTL_ARGS(ctypes.Structure): + _fields_ = [ + ("name", ctypes.c_uint32), # integer vector describing variable + ("nlen", ctypes.c_uint32), # length of this vector + ("oldval", ctypes.c_uint32), # 0 or address where to store old value + # available room for old value, overwritten by size of old value + ("oldlenp", ctypes.c_uint32), + ("newval", ctypes.c_uint32), # 0 or address of new value */ + ("newlen", ctypes.c_uint32), # size of new value */) + ] + + +def sys__sysctl(sm, p): + args = sm.get_args([("struct __sysctl_args*", "sys_args")]) + __sysctl_args = ptr2struct(sm.z, args.sys_args, __SYSCTL_ARGS) + dumpstruct(__sysctl_args) + return 0 + + +class IOCTLS(enum.IntEnum): + """ + IOCTL INTERNAL (PARTIAL) + https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/ioctls.h + """ + + FIONREAD = 0x541B + + +def sys_ioctl(sm, p): + args = sm.get_args( + [("int", "fd"), ("unsigned long", "request"), ("void *", "data")] + ) + + sm.z.handles.get(args.fd) + if args.data == 0: + return -1 + + data = p.memory.read_uint32(args.data) + sm.print(f"IOCTL: {data}") + + handle = sm.z.handles.get(args.fd) + if isinstance(handle, handles.SocketHandle): + FIONBIO = 0x5421 + FIONREAD = 0x541B + if args.request == FIONBIO: + handle.socket.set_nonblock(data) + return 0 + elif args.request == FIONREAD: + sock_data = handle.socket.peek() + len_avail = len(sock_data) + p.memory.write_uint32(args.data, len_avail) + return 1 + + return -1 + + +def sys_arch_prctl(sm, p): + args = sm.get_args( + [("int_ARCH_PRCTL", "option"), ("unsigned long", "addr")] + ) + if args.option == 0x1001: + sys_utils.set_gs(p, args.addr) + elif args.option == 0x1002: + sys_utils.set_fs(p, args.addr) + + return 0 + + +def sys_prctl(sm, p): + args = sm.get_args( + [ + ("int", "option"), + ("unsigned long", "arg2"), + ("unsigned long", "arg3"), + ("unsigned long", "arg4"), + ("unsigned long", "arg5"), + ] + ) + + if args.option == PRCTL.PR_SET_NAME: + proc_name = p.memory.read_string(args.arg2) + sm.print(f"PRCTL[PR_SET_NAME]: setting process name to [{proc_name}]") + + return 0 + + +def sys_umask(sm, p): + sm.get_args([("mode_t", "mask")]) + return 0o777 + + +def sys_statfs(sm, p): + sm.get_args([("const char*", "path"), ("struct statfs *", "buf")]) + return -1 + + +def sys_alarm(sm, p): + args = sm.get_args([("unsigned int", "seconds")]) + return args.seconds + + +def sys_rt_sigaction(sm, p): + args = sm.get_args( + [ + ("int", "signum"), + ("const sigaction*", "act"), + ("struct sigaction *", "oldact"), + ] + ) + if args.act != 0: + new_sigaction = p.memory.readstruct(args.act, structs.SIGACTION()) + p.zos.signals.set_signal_action(args.signum, new_sigaction.sa_handler) + return 0 + + +def sys_rt_sigprocmask(sm, p): + args = sm.get_args( + [ + ("int", "how"), + ("const kernel_sigset_t*", "set"), + ("kernel_sigset_t *", "oldset"), + ("size_t", "sigsetsize"), + ] + ) + old_signal_mask = p.zos.signals.get_signal_mask() + if args.oldset != 0: + p.memory.write_uint32(args.oldset, old_signal_mask) + if args.set != 0: + sigset = p.memory.read_uint32(args.set) + + if args.how == 0: # SIG_BLOCK + new_signal_mask = old_signal_mask | sigset + elif args.how == 1: # SIG_UNBLOCK + new_signal_mask = old_signal_mask & ~sigset + elif args.how == 2: # SIG_SETMASK + new_signal_mask = sigset + + p.zos.signals.set_signal_mask(new_signal_mask) + + # TODO: Attempt to handle any signals that are no longer blocked. + # p.zos.signals.handle_signal_queue() + return 0 + + +class RLIMIT(ctypes.Structure): + _fields_ = [("rlim_cur", ctypes.c_uint32), ("rlim_max", ctypes.c_uint32)] + + +def sys_ugetrlimit(sm, p): + args = sm.get_args([("int", "resource"), ("struct rlimit*", "rlim")]) + rlimit = RLIMIT() + RLIM_INFINITY = 0xFFFFFFFF + rlimit.rlim_cur = RLIM_INFINITY + rlimit.rlim_max = RLIM_INFINITY + data = struct2str(rlimit) + p.memory.write(args.rlim, bytes(data)) + return 0 + + +def sys_setrlimit(sm, p): + sm.get_args([("int", "resource"), ("const struct rlimit *", "rlim")]) + return 0 + + +def sys_prlimit64(sm, p): + sm.get_args( + [ + ("pid_t", "pid"), + ("int", "resource"), + ("const struct rlimit *", "rlim"), + ("struct rlimit*", "old_limit"), + ] + ) + return 0 + + +def _read_fd_set(sm, p, fd_set_ptr): + if fd_set_ptr == 0: + return [] + fds = [] + for i in range(0, 1024 // 8, 32 // 8): + val = p.memory.read_uint32(fd_set_ptr + i) + for bit in range(32): + if val & 2 ** bit != 0: + fds.append((i // 4) * 32 + bit) + return fds + + +def _write_fd_set(sm, p, fd_set_ptr, fds): + if fd_set_ptr == 0: + return + for i in range(0, 1024 // 8, 32 // 8): + val = int(0) + for bit in range(32): + fd = (i // 4) * 32 + bit + if fd in fds: + val |= 1 << bit + p.memory.write_uint32(fd_set_ptr + i, val) + + +def sys_select(sm, p): + return sys__newselect(sm, p) + + +def sys__newselect(sm, p): + args = sm.get_args( + [ + ("int", "nfds"), + ("fd_set*", "readfds"), + ("fd_set*", "writefds"), + ("fd_set*", "exceptfds"), + ("struct timeval*", "timeout"), + ] + ) + # Get the set(s) of FDs requested + readfds = _read_fd_set(sm, p, args.readfds) + writefds = _read_fd_set(sm, p, args.writefds) + exceptfds = _read_fd_set(sm, p, args.exceptfds) + + # Dump FD sets + if len(readfds) > 0: + sm.print(f"readfds: {', '.join([hex(x) for x in readfds])}") + if len(writefds) > 0: + sm.print(f"writefds: {', '.join([hex(x) for x in writefds])}") + if len(exceptfds) > 0: + sm.print(f"exceptfds: {', '.join([hex(x) for x in exceptfds])}") + + # Select is only supported on sockets right now. Always + # return 'ready' for all other types of FDs + sockets = sm.z.network.handles.get_by_type(handles.SocketHandle) + if len(sockets) == 0: + return len(readfds) + len(writefds) + len(exceptfds) + + # Perform the select implemented by socket + (in_ready, out_ready, ex_ready) = sm.z.network.select.select( + readfds, writefds, exceptfds, timeout=0.1 + ) + + # Dump FD sets that were signalled + if len(in_ready) > 0: + sm.print(f"signaled readfds: {', '.join([hex(x) for x in in_ready])}") + if len(out_ready) > 0: + sm.print( + f"signaled writefds: {', '.join([hex(x) for x in out_ready])}" + ) + if len(ex_ready) > 0: + sm.print( + f"signaled exceptfds: {', '.join([hex(x) for x in ex_ready])}" + ) + + # Selectively set only the FDs that were signalled + _write_fd_set(sm, p, args.readfds, in_ready) + _write_fd_set(sm, p, args.writefds, out_ready) + _write_fd_set(sm, p, args.exceptfds, ex_ready) + + count = len(in_ready) + len(out_ready) + len(ex_ready) + + return count + + +def sys_futex(sm, p): + args = sm.get_args( + [ + ("int*", "uaddr"), + ("int", "futex_op"), + ("int", "val"), + # or: uint32_t val2 + ("const struct timespec*", "timeout"), + ("int*", "uaddr2"), + ("int", "val3"), + ] + ) + operation = args.futex_op & 0xF + print(operation) + if operation == 1: + # Futex wake + return 0 # Number of waiters woken + if operation == 9: + mem_val = p.memory.read_uint32(args.uaddr) + print(mem_val, args.val) + if mem_val == args.val: + return 0 + return -1 # They need to be the same when this operation starts + return 0 + + +def sys_nanosleep(sm, p): + sm.get_args( + [("const struct timespec*", "req"), ("struct timespec *", "rem")] + ) + return 0 + + +def sys_chmod(sm, p): + sm.get_args([("const char*", "pathname"), ("mode_t", "mode")]) + return 0 + + +def sys_chown(sm, p): + sm.get_args( + [("const char*", "pathname"), ("uid_t", "owner"), ("gid_t", "group")] + ) + return 0 + + +def sys_chdir(sm, p): + sm.get_args([("const char*", "pathname")]) + return 0 + + +def sys_mkdir(sm, p): + sm.get_args([("const char*", "pathname"), ("mode_t", "mode")]) + return 0 + + +def sys_rmdir(sm, p): + sm.get_args([("const char*", "pathname")]) + return 0 + + +# technically, single-threaded process should return pid + + +def sys_gettid(sm, p): + sm.get_args([]) + return p.current_thread.id + + +def sys_mincore(sm, p): + sm.get_args( + [("void*", "addr"), ("size_t", "length"), ("unsigned char*", "vec")] + ) + return -1 + + +def sys_fadvise64(sm, p): + sm.get_args( + [ + ("int", "fd"), + ("off_t", "offset"), + ("off_t", "len"), + ("int", "advice"), + ] + ) + return 0 + + +def sys_sigaltstack(sm, p): + sm.get_args([("stack_t*", "ss"), ("stack_t*", "oldss")]) + return 0 + + +def sys_kill(sm, p): + args = sm.get_args([("pid_t", "pid"), ("int", "sig")]) + if args.pid in [-1, 0xFFFFFFFF, 0, 1]: + # TODO handle these cases. + return -1 + process = sm.z.processes.get_process(args.pid) + if process is None: + return SysError.ESRCH + + try: + Signal(args.sig) + except ValueError: + return SysError.EINVAL + + process.zos.signals.handle_signal(args.sig) + + # if args.pid == 0: + # current_tid = p.current_thread.id + # for child in p.threads.get_child_threads(current_tid): + # p.threads.kill_thread(child.id) + # if 0 < args.pid and args.pid <= 0xffff: + # sm.z.processes.kill_process(args.pid) + return 0 + + +def sys_tgkill(sm, p): + sm.get_args([("int", "tgid"), ("int", "tid"), ("int", "sig")]) + return 0 + + +class POLL(enum.IntFlag): + """ + POLL INTERNAL + https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/poll.h + """ + + POLLIN = 0x0001 + POLLPRI = 0x0002 + POLLOUT = 0x0004 + POLLERR = 0x0008 + POLLHUP = 0x0010 + POLLNVAL = 0x0020 + POLLRDNORM = 0x0040 + POLLRDBAND = 0x0080 + POLLWRNORM = 0x0100 + POLLWRBAND = 0x0200 + POLLMSG = 0x0400 + POLLREMOVE = 0x1000 + POLLRDHUP = 0x2000 + + +class POLLFD(ctypes.Structure): + _fields_ = [ + ("fd", ctypes.c_int32), + ("events", ctypes.c_short), + ("revents", ctypes.c_short), + ] + + +def sys_poll(sm, p): + args = sm.get_args( + [("struct pollfd *", "fds"), ("nfds_t", "nfds"), ("int", "timeout")] + ) + # parse the file descriptors of interest + sz = ctypes.sizeof(POLLFD()) + fds = {} + for i in range(args.nfds): + pollfd = POLLFD() + fd_addr = args.fds + i * sz + pollfd_data = p.memory.read(fd_addr, sz) + str2struct(pollfd, bytes(pollfd_data)) + fds[fd_addr] = pollfd + + fds_poll = [(v.fd, v.events) for k, v in fds.items()] + + e = ", ".join([f"fd={x[0]:x} events={repr(POLL(x[1]))}" for x in fds_poll]) + sm.print("polled_fds: " + e) + + revents = sm.z.network.select.poll(fds_poll, timeout=0.1) + + e = ", ".join([f"fd={x[0]:x} events={repr(POLL(x[1]))}" for x in revents]) + sm.print("signaled_fds: " + e) + + # commit pollfd struct changes + ready_fds = 0 + for i in range(len(fds_poll)): + revent = revents[i][1] + if revent >= 0: + fd_addr = args.fds + i * sz + v = fds[fd_addr] + v.revents = revent + pollfd_data = struct2str(v) + p.memory.write(fd_addr, struct2str(v)) + ready_fds += 1 + + return ready_fds diff --git a/src/zelos/ext/platforms/linux/syscalls/syscalls_const.py b/src/zelos/ext/platforms/linux/syscalls/syscalls_const.py new file mode 100644 index 0000000..83c1590 --- /dev/null +++ b/src/zelos/ext/platforms/linux/syscalls/syscalls_const.py @@ -0,0 +1,819 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import ctypes +import enum + + +class SysError(enum.IntEnum): + EPERM = -1 # Operation not permitted */ + ENOENT = -2 # No such file or directory */ + ESRCH = -3 # No such process */ + EINTR = -4 # Interrupted system call */ + EIO = -5 # I/O error */ + ENXIO = -6 # No such device or address */ + E2BIG = -7 # Arg list too long */ + ENOEXEC = -8 # Exec format error */ + EBADF = -9 # Bad file number */ + ECHILD = -10 # No child processes */ + EAGAIN = -11 # Try again */ + ENOMEM = -12 # Out of memory */ + EACCES = -13 # Permission denied */ + EFAULT = -14 # Bad address */ + ENOTBLK = -15 # Block device required */ + EBUSY = -16 # Device or resource busy */ + EEXIST = -17 # File exists */ + EXDEV = -18 # Cross-device link */ + ENODEV = -19 # No such device */ + ENOTDIR = -20 # Not a directory */ + EISDIR = -21 # Is a directory */ + EINVAL = -22 # Invalid argument */ + ENFILE = -23 # File table overflow */ + EMFILE = -24 # Too many open files */ + ENOTTY = -25 # Not a typewriter */ + ETXTBSY = -26 # Text file busy */ + EFBIG = -27 # File too large */ + ENOSPC = -28 # No space left on device */ + ESPIPE = -29 # Illegal seek */ + EROFS = -30 # Read-only file system */ + EMLINK = -31 # Too many links */ + EPIPE = -32 # Broken pipe */ + EDOM = -33 # Math argument out of domain of func */ + ERANGE = -34 # Math result not representable */ + EDEADLK = -35 # Resource deadlock would occur */ + ENAMETOOLONG = -36 # File name too long */ + ENOLCK = -37 # No record locks available */ + ENOSYS = -38 # Function not implemented */ + ENOTEMPTY = -39 # Directory not empty */ + ELOOP = -40 # Too many symbolic links encountered */ + EWOULDBLOCK = EAGAIN # Operation would block */ + ENOMSG = -42 # No message of desired type */ + EIDRM = -43 # Identifier removed */ + ECHRNG = -44 # Channel number out of range */ + EL2NSYNC = -45 # Level 2 not synchronized */ + EL3HLT = -46 # Level 3 halted */ + EL3RST = -47 # Level 3 reset */ + ELNRNG = -48 # Link number out of range */ + EUNATCH = -49 # Protocol driver not attached */ + ENOCSI = -50 # No CSI structure available */ + EL2HLT = -51 # Level 2 halted */ + EBADE = -52 # Invalid exchange */ + EBADR = -53 # Invalid request descriptor */ + EXFULL = -54 # Exchange full */ + ENOANO = -55 # No anode */ + EBADRQC = -56 # Invalid request code */ + EBADSLT = -57 # Invalid slot */ + + EDEADLOCK = EDEADLK + EBFONT = -59 # Bad font file format */ + ENOSTR = -60 # Device not a stream */ + ENODATA = -61 # No data available */ + ETIME = -62 # Timer expired */ + ENOSR = -63 # Out of streams resources */ + ENONET = -64 # Machine is not on the network */ + ENOPKG = -65 # Package not installed */ + EREMOTE = -66 # Object is remote */ + ENOLINK = -67 # Link has been severed */ + EADV = -68 # Advertise error */ + ESRMNT = -69 # Srmount error */ + ECOMM = -70 # Communication error on send */ + EPROTO = -71 # Protocol error */ + EMULTIHOP = -72 # Multihop attempted */ + EDOTDOT = -73 # RFS specific error */ + EBADMSG = -74 # Not a data message */ + EOVERFLOW = -75 # Value too large for defined data type */ + ENOTUNIQ = -76 # Name not unique on network */ + EBADFD = -77 # File descriptor in bad state */ + EREMCHG = -78 # Remote address changed */ + ELIBACC = -79 # Can not access a needed shared library */ + ELIBBAD = -80 # Accessing a corrupted shared library */ + ELIBSCN = -81 # .lib section in a.out corrupted */ + ELIBMAX = -82 # Attempting to link in too many shared libraries */ + ELIBEXEC = -83 # Cannot exec a shared library directly */ + EILSEQ = -84 # Illegal byte sequence */ + ERESTART = -85 # Interrupted system call should be restarted */ + ESTRPIPE = -86 # Streams pipe error */ + EUSERS = -87 # Too many users */ + ENOTSOCK = -88 # Socket operation on non-socket */ + EDESTADDRREQ = -89 # Destination address required */ + EMSGSIZE = -90 # Message too long */ + EPROTOTYPE = -91 # Protocol wrong type for socket */ + ENOPROTOOPT = -92 # Protocol not available */ + EPROTONOSUPPORT = -93 # Protocol not supported */ + ESOCKTNOSUPPORT = -94 # Socket type not supported */ + EOPNOTSUPP = -95 # Operation not supported on transport endpoint */ + EPFNOSUPPORT = -96 # Protocol family not supported */ + EAFNOSUPPORT = -97 # Address family not supported by protocol */ + EADDRINUSE = -98 # Address already in use */ + EADDRNOTAVAIL = -99 # Cannot assign requested address */ + ENETDOWN = -100 # Network is down */ + ENETUNREACH = -101 # Network is unreachable */ + ENETRESET = -102 # Network dropped connection because of reset */ + ECONNABORTED = -103 # Software caused connection abort */ + ECONNRESET = -104 # Connection reset by peer */ + ENOBUFS = -105 # No buffer space available */ + EISCONN = -106 # Transport endpoint is already connected */ + ENOTCONN = -107 # Transport endpoint is not connected */ + ESHUTDOWN = -108 # Cannot send after transport endpoint shutdown */ + ETOOMANYREFS = -109 # Too many references = splice */ + ETIMEDOUT = -110 # Connection timed out */ + ECONNREFUSED = -111 # Connection refused */ + EHOSTDOWN = -112 # Host is down */ + EHOSTUNREACH = -113 # No route to host */ + EALREADY = -114 # Operation already in progress */ + EINPROGRESS = -115 # Operation now in progress */ + ESTALE = -116 # Stale NFS file handle */ + EUCLEAN = -117 # Structure needs cleaning */ + ENOTNAM = -118 # Not a XENIX named type file */ + ENAVAIL = -119 # No XENIX semaphores available */ + EISNAM = -120 # Is a named type file */ + EREMOTEIO = -121 # Remote I/O error */ + EDQUOT = -122 # Quota exceeded */ + + ENOMEDIUM = -123 # No medium found */ + EMEDIUMTYPE = -124 # Wrong medium type */ + + +ARCH_PRCTL_OPTIONS = { + 0x1001: "ARCH_SET_GS", + 0x1002: "ARCH_SET_FS", + 0x1003: "ARCH_GET_FS", + 0x1004: "ARCH_GET_GS", +} + + +FCNTL_COMMANDS = { + 0: "F_DUPFD", # dup + 1: "F_GETFD", # get close_on_exec + 2: "F_SETFD", # set/clear close_on_exec + 3: "F_GETFL", # get file->f_flags + 4: "F_SETFL", # set file->f_flags + 5: "F_GETLK", + 6: "F_SETLK", + 7: "F_SETLKW", + 8: "F_SETOWN", # for sockets. + 9: "F_GETOWN", # for sockets. + 10: "F_SETSIG", # for sockets. + 11: "F_GETSIG", # for sockets. + 12: "F_GETLK64", # using 'struct flock64' + 13: "F_SETLK64", + 14: "F_SETLKW64", + 15: "F_SETOWN_EX", + 16: "F_GETOWN_EX", + 17: "F_GETOWNER_UIDS", +} + + +# Used to determine what kind of socket is being used. +protocol_families = { + 1: "UNIX", # /* Unix domain sockets */ + 2: "INET", # /* Internet IP Protocol */ + 3: "AX25", # /* Amateur Radio AX.25 */ + 4: "IPX", # /* Novell IPX */ + 5: "APPLETALK", # /* AppleTalk DDP */ + 6: "NETROM", # /* Amateur Radio NET/ROM */ + 7: "BRIDGE", # /* Multiprotocol bridge */ + 8: "ATMPVC", # /* ATM PVCs */ + 9: "X25", # /* Reserved for X.25 project */ + 10: "INET6", # /* IP version 6 */ + 11: "ROSE", # /* Amateur Radio X.25 PLP */ + 12: "DECnet", # /* Reserved for DECnet project */ + 13: "NETBEUI", # /* Reserved for 802.2LLC project*/ + 14: "SECURITY", # /* Security callback pseudo AF */ + 15: "KEY", # /* PF_KEY key management API */ + 16: "NETLINK", # /* Alias to emulate 4.4BSD */ + 17: "PACKET", # /* Packet family */ + 18: "ASH", # * Ash */ + 19: "ECONET", # /* Acorn Econet */ + 20: "ATMSVC", # /* ATM SVCs */ + 21: "RDS", # /* RDS sockets */ + 22: "SNA", # /* Linux SNA Project (nutters!) */ + 23: "IRDA", # /* IRDA sockets */ + 24: "PPPOX", # /* PPPoX sockets */ + 25: "WANPIPE", # /* Wanpipe API Sockets */ + 26: "LLC", # /* Linux LLC */ + 27: "IB", # /* Native InfiniBand address */ + 28: "MPLS", # * MPLS */ + 29: "CAN", # /* Controller Area Network */ + 30: "TIPC", # /* TIPC sockets */ + 31: "BLUETOOTH", # /* Bluetooth sockets */ + 32: "IUCV", # /* IUCV sockets */ + 33: "RXRPC", # /* RxRPC sockets */ + 34: "ISDN", # /* mISDN sockets */ + 35: "PHONET", # /* Phonet sockets */ + 36: "IEEE802154", # /* IEEE802154 sockets */ + 37: "CAIF", # /* CAIF sockets */ + 38: "ALG", # /* Algorithm sockets */ + 39: "NFC", # /* NFC sockets */ + 40: "VSOCK", # * vSockets */ + 41: "KCM", # /* Kernel Connection Multiplexor*/ + 42: "QIPCRTR", # /* Qualcomm IPC Router */ + 43: "SMC", # /* smc sockets: reserve number for + 44: "XDP", # /* XDP sockets */ + 45: "MAX", # /* For now.. */ +} + + +class SOCKADDR(ctypes.Structure): + _fields_ = [ + ("sin_family", ctypes.c_ushort), + ("sa_data", ctypes.c_char * 8), + ] + + +class SOCKADDR_UN(ctypes.Structure): + _fields_ = [ + ("sun_family", ctypes.c_ushort), + ("sun_path", ctypes.c_char * 108), + ] + + +class SOCKADDR_IN(ctypes.Structure): + _fields_ = [ + ("sin_family", ctypes.c_ushort), + ("sin_port", ctypes.c_ushort), + ("sin_addr", ctypes.c_uint32), + ("sin_zero", ctypes.c_char * 8), + ] + + +class SOCKADDR_IN6(ctypes.Structure): + _fields_ = [ + ("sin6_family", ctypes.c_ushort), + ("sin6_port", ctypes.c_ushort), + ("sin6_flowinfo", ctypes.c_uint32), + ("sin6_addr", ctypes.c_uint64), + ("sin6_scope_id", ctypes.c_uint32), + ] + + +class SocketFamily(enum.IntEnum): + """ + Socket Family: + https://github.com/torvalds/linux/blob/master/include/linux/socket.h + """ + + AF_UNSPEC = 0 + AF_UNIX = 1 # /* Unix domain sockets */ + AF_LOCAL = 1 # /* POSIX name for AF_UNIX */ + AF_INET = 2 # /* Internet IP Protocol */ + AF_AX25 = 3 # /* Amateur Radio AX.25 */ + AF_IPX = 4 # /* Novell IPX */ + AF_APPLETALK = 5 # /* AppleTalk DDP */ + AF_NETROM = 6 # /* Amateur Radio NET/ROM */ + AF_BRIDGE = 7 # /* Multiprotocol bridge */ + AF_ATMPVC = 8 # /* ATM PVCs */ + AF_X25 = 9 # /* Reserved for X.25 project */ + AF_INET6 = 10 # /* IP version 6 */ + AF_ROSE = 11 # /* Amateur Radio X.25 PLP */ + AF_DECnet = 12 # /* Reserved for DECnet project */ + AF_NETBEUI = 13 # /* Reserved for 802.2LLC project*/ + AF_SECURITY = 14 # /* Security callback pseudo AF */ + AF_KEY = 15 # /* PF_KEY key management API */ + AF_NETLINK = 16 + AF_ROUTE = 16 # /* AF_NETLINK: Alias to emulate 4.4BSD */ + AF_PACKET = 17 # /* Packet family */ + AF_ASH = 18 # /* Ash */ + AF_ECONET = 19 # /* Acorn Econet */ + AF_ATMSVC = 20 # /* ATM SVCs */ + AF_RDS = 21 # /* RDS sockets */ + AF_SNA = 22 # /* Linux SNA Project (nutters!) */ + AF_IRDA = 23 # /* IRDA sockets */ + AF_PPPOX = 24 # /* PPPoX sockets */ + AF_WANPIPE = 25 # /* Wanpipe API Sockets */ + AF_LLC = 26 # /* Linux LLC */ + AF_IB = 27 # /* Native InfiniBand address */ + AF_MPLS = 28 # /* MPLS */ + AF_CAN = 29 # /* Controller Area Network */ + AF_TIPC = 30 # /* TIPC sockets */ + AF_BLUETOOTH = 31 # /* Bluetooth sockets */ + AF_IUCV = 32 # /* IUCV sockets */ + AF_RXRPC = 33 # /* RxRPC sockets */ + AF_ISDN = 34 # /* mISDN sockets */ + AF_PHONET = 35 # /* Phonet sockets */ + AF_IEEE802154 = 36 # /* IEEE802154 sockets */ + AF_CAIF = 37 # /* CAIF sockets */ + AF_ALG = 38 # /* Algorithm sockets */ + AF_NFC = 39 # /* NFC sockets */ + AF_VSOCK = 40 # /* vSockets */ + AF_KCM = 41 # /* Kernel Connection Multiplexor*/ + AF_QIPCRTR = 42 # /* Qualcomm IPC Router */ + AF_SMC = 43 # /* smc sockets: reserve for PF_SMC protocol family */ + AF_XDP = 44 # /* XDP sockets */ + AF_MAX = 45 # /* For now.. */ + + +class SocketType(enum.IntEnum): + """ + Socket Types: + https://github.com/torvalds/linux/blob/master/include/linux/net.h + """ + + SOCK_STREAM = 1 + SOCK_DGRAM = 2 + SOCK_RAW = 3 + SOCK_RDM = 4 + SOCK_SEQPACKET = 5 + SOCK_DCCP = 6 + SOCK_PACKET = 10 + SOCK_MAX = 11 + + SOCK_CLOEXEC = 0o2000000 + SOCK_NONBLOCK = 0o4000 + + +class SocketProtocol(enum.IntEnum): + IPPROTO_IP = 0 # Dummy protocol for TCP. + IPPROTO_ICMP = 1 # Internet Control Message Protocol. + IPPROTO_IGMP = 2 # Internet Group Management Protocol. + IPPROTO_IPIP = 4 # IPIP tunnels (older KA9Q tunnels use 94). + IPPROTO_TCP = 6 # Transmission Control Protocol. + IPPROTO_EGP = 8 # Exterior Gateway Protocol. + IPPROTO_PUP = 12 # PUP protocol. + IPPROTO_UDP = 17 # User Datagram Protocol. + IPPROTO_IDP = 22 # XNS IDP protocol. + IPPROTO_TP = 29 # SO Transport Protocol Class 4. + IPPROTO_DCCP = 33 # Datagram Congestion Control Protocol. + IPPROTO_IPV6 = 41 # IPv6 header. + IPPROTO_RSVP = 46 # Reservation Protocol. + IPPROTO_GRE = 47 # General Routing Encapsulation. + IPPROTO_ESP = 50 # encapsulating security payload. + IPPROTO_AH = 51 # authentication header. + IPPROTO_MTP = 92 # Multicast Transport Protocol. + IPPROTO_BEETPH = 94 # IP option pseudo header for BEET. + IPPROTO_ENCAP = 98 # Encapsulation Header. + IPPROTO_PIM = 103 # Protocol Independent Multicast. + IPPROTO_COMP = 108 # Compression Header Protocol. + IPPROTO_SCTP = 132 # Stream Control Transmission Protocol. + IPPROTO_UDPLITE = 136 # UDP-Lite protocol. + IPPROTO_MPLS = 137 # MPLS in IP. + IPPROTO_RAW = 255 # Raw IP packets. + + +class SocketOptionsLevels(enum.IntEnum): + IPPROTO_IP = 0 + SOL_SOCKET = 1 + IPPROTO_TCP = 6 + IPPROTO_IPV6 = 41 + + +class SocketOptionsSocket(enum.IntEnum): + SO_DEBUG = 1 + SO_REUSEADDR = 2 + SO_TYPE = 3 + SO_ERROR = 4 + SO_DONTROUTE = 5 + SO_BROADCAST = 6 + SO_SNDBUF = 7 + SO_RCVBUF = 8 + SO_SNDBUFFORCE = 32 + SO_RCVBUFFORCE = 33 + SO_KEEPALIVE = 9 + SO_OOBINLINE = 10 + SO_NO_CHECK = 11 + SO_PRIORITY = 12 + SO_LINGER = 13 + SO_BSDCOMPAT = 14 + SO_REUSEPORT = 15 + SO_PASSCRED = 16 + SO_PEERCRED = 17 + SO_RCVLOWAT = 18 + SO_SNDLOWAT = 19 + SO_RCVTIMEO = 20 + SO_SNDTIMEO = 21 + + # Security levels - as per NRL IPv6 - don't actually do anything + SO_SECURITY_AUTHENTICATION = 22 + SO_SECURITY_ENCRYPTION_TRANSPORT = 23 + SO_SECURITY_ENCRYPTION_NETWORK = 24 + SO_BINDTODEVICE = 25 + + # Socket filtering + SO_ATTACH_FILTER = 26 + SO_DETACH_FILTER = 27 + SO_PEERNAME = 28 + SO_TIMESTAMP = 29 + SO_ACCEPTCONN = 30 + SO_PEERSEC = 31 + SO_PASSSEC = 34 + SO_TIMESTAMPNS = 35 + SO_MARK = 36 + SO_TIMESTAMPING = 37 + SO_PROTOCOL = 38 + SO_DOMAIN = 39 + SO_RXQ_OVFL = 40 + SO_WIFI_STATUS = 41 + SO_PEEK_OFF = 42 + + # Instruct lower device to use last 4-bytes of skb data as FCS + SO_NOFCS = 43 + SO_LOCK_FILTER = 44 + SO_SELECT_ERR_QUEUE = 45 + SO_BUSY_POLL = 46 + SO_MAX_PACING_RATE = 47 + SO_BPF_EXTENSIONS = 48 + SO_INCOMING_CPU = 49 + SO_ATTACH_BPF = 50 + SO_ATTACH_REUSEPORT_CBPF = 51 + SO_ATTACH_REUSEPORT_EBPF = 52 + SO_CNX_ADVICE = 53 + SCM_TIMESTAMPING_OPT_STATS = 54 + SO_MEMINFO = 55 + SO_INCOMING_NAPI_ID = 56 + SO_COOKIE = 57 + SCM_TIMESTAMPING_PKTINFO = 58 + SO_PEERGROUPS = 59 + SO_ZEROCOPY = 60 + + +class SocketOptionsIp(enum.IntEnum): + IP_TOS = 1 + IP_TTL = 2 + IP_HDRINCL = 3 + IP_OPTIONS = 4 + IP_ROUTER_ALERT = 5 + IP_RECVOPTS = 6 + IP_RETOPTS = 7 + IP_PKTINFO = 8 + IP_PKTOPTIONS = 9 + IP_MTU_DISCOVER = 10 + IP_RECVERR = 11 + IP_RECVTTL = 12 + IP_RECVTOS = 13 + IP_MTU = 14 + IP_FREEBIND = 15 + IP_IPSEC_POLICY = 16 + IP_XFRM_POLICY = 17 + IP_PASSSEC = 18 + IP_TRANSPARENT = 19 + + # TProxy original addresses + IP_ORIGDSTADDR = 20 + + IP_MINTTL = 21 + IP_NODEFRAG = 22 + IP_CHECKSUM = 23 + IP_BIND_ADDRESS_NO_PORT = 24 + IP_RECVFRAGSIZE = 25 + + IP_MULTICAST_IF = 32 + IP_MULTICAST_TTL = 33 + IP_MULTICAST_LOOP = 34 + IP_ADD_MEMBERSHIP = 35 + IP_DROP_MEMBERSHIP = 36 + IP_UNBLOCK_SOURCE = 37 + IP_BLOCK_SOURCE = 38 + IP_ADD_SOURCE_MEMBERSHIP = 39 + IP_DROP_SOURCE_MEMBERSHIP = 40 + IP_MSFILTER = 41 + MCAST_JOIN_GROUP = 42 + MCAST_BLOCK_SOURCE = 43 + MCAST_UNBLOCK_SOURCE = 44 + MCAST_LEAVE_GROUP = 45 + MCAST_JOIN_SOURCE_GROUP = 46 + MCAST_LEAVE_SOURCE_GROUP = 47 + MCAST_MSFILTER = 48 + IP_MULTICAST_ALL = 49 + IP_UNICAST_IF = 50 + + +class SocketOptionsTcp(enum.IntEnum): + TCP_NODELAY = 1 # Turn off Nagle's algorithm. + TCP_MAXSEG = 2 # Limit MSS + TCP_CORK = 3 # Never send partially complete segments + TCP_KEEPIDLE = 4 # Start keeplives after this period + TCP_KEEPINTVL = 5 # Interval between keepalives + TCP_KEEPCNT = 6 # Number of keepalives before death + TCP_SYNCNT = 7 # Number of SYN retransmits + TCP_LINGER2 = 8 # Life time of orphaned FIN-WAIT-2 state + TCP_DEFER_ACCEPT = 9 # Wake up listener only when data arrive + TCP_WINDOW_CLAMP = 10 # Bound advertised window + TCP_INFO = 11 # Information about this connection. + TCP_QUICKACK = 12 # Block/reenable quick acks + TCP_CONGESTION = 13 # Congestion control algorithm + TCP_MD5SIG = 14 # TCP MD5 Signature (RFC2385) + TCP_THIN_LINEAR_TIMEOUTS = 16 # Use linear timeouts for thin streams*/ + TCP_THIN_DUPACK = 17 # Fast retrans. after = 1 dupack + TCP_USER_TIMEOUT = 18 # How long for loss retry before timeout + TCP_REPAIR = 19 # TCP sock is under repair right now + TCP_REPAIR_QUEUE = 20 + TCP_QUEUE_SEQ = 21 + TCP_REPAIR_OPTIONS = 22 + TCP_FASTOPEN = 23 # Enable FastOpen on listeners + TCP_TIMESTAMP = 24 + TCP_NOTSENT_LOWAT = 25 # limit number of unsent bytes in write queue + TCP_CC_INFO = 26 # Get Congestion Control (optional) info + TCP_SAVE_SYN = 27 # Record SYN headers for new connections + TCP_SAVED_SYN = 28 # Get SYN headers recorded for connection + TCP_REPAIR_WINDOW = 29 # Get/set window parameters + TCP_FASTOPEN_CONNECT = 30 # Attempt FastOpen with connect + TCP_ULP = 31 # Attach a ULP to a TCP connection + TCP_MD5SIG_EXT = 32 # TCP MD5 Signature with extensions + TCP_FASTOPEN_KEY = 33 # Set the key for Fast Open (cookie) + TCP_FASTOPEN_NO_COOKIE = 34 # Enable TFO without a TFO cookie + + +class SocketOptionsIPV6(enum.IntEnum): + IPV6_ADDRFORM = 1 + IPV6_2292PKTINFO = 2 + IPV6_2292HOPOPTS = 3 + IPV6_2292DSTOPTS = 4 + IPV6_2292RTHDR = 5 + IPV6_2292PKTOPTIONS = 6 + IPV6_CHECKSUM = 7 + IPV6_2292HOPLIMIT = 8 + IPV6_NEXTHOP = 9 + IPV6_AUTHHDR = 10 + IPV6_FLOWINFO = 11 + + IPV6_UNICAST_HOPS = 16 + IPV6_MULTICAST_IF = 17 + IPV6_MULTICAST_HOPS = 18 + IPV6_MULTICAST_LOOP = 19 + IPV6_ADD_MEMBERSHIP = 20 + IPV6_DROP_MEMBERSHIP = 21 + IPV6_ROUTER_ALERT = 22 + IPV6_MTU_DISCOVER = 23 + IPV6_MTU = 24 + IPV6_RECVERR = 25 + IPV6_V6ONLY = 26 + IPV6_JOIN_ANYCAST = 27 + IPV6_LEAVE_ANYCAST = 28 + + # Flowlabel + IPV6_FLOWLABEL_MGR = 32 + IPV6_FLOWINFO_SEND = 33 + + IPV6_IPSEC_POLICY = 34 + IPV6_XFRM_POLICY = 35 + IPV6_HDRINCL = 36 + + IPV6_RECVPKTINFO = 49 + IPV6_PKTINFO = 50 + IPV6_RECVHOPLIMIT = 51 + IPV6_HOPLIMIT = 52 + IPV6_RECVHOPOPTS = 53 + IPV6_HOPOPTS = 54 + IPV6_RTHDRDSTOPTS = 55 + IPV6_RECVRTHDR = 56 + IPV6_RTHDR = 57 + IPV6_RECVDSTOPTS = 58 + IPV6_DSTOPTS = 59 + IPV6_RECVPATHMTU = 60 + IPV6_PATHMTU = 61 + IPV6_DONTFRAG = 62 + + IPV6_RECVTCLASS = 66 + IPV6_TCLASS = 67 + + IPV6_AUTOFLOWLABEL = 70 + # RFC5014: Source address selection + IPV6_ADDR_PREFERENCES = 72 + + # RFC5082: Generalized Ttl Security Mechanism + IPV6_MINHOPCOUNT = 73 + + IPV6_ORIGDSTADDR = 74 + IPV6_TRANSPARENT = 75 + IPV6_UNICAST_IF = 76 + IPV6_RECVFRAGSIZE = 77 + IPV6_FREEBIND = 78 + + +class PRCTL(enum.IntEnum): + """ + Process Control Types: + https://github.com/torvalds/linux/blob/master/include/uapi/linux/prctl.h + """ + + PR_SET_PDEATHSIG = 1 + PR_GET_PDEATHSIG = 2 + PR_GET_DUMPABLE = 3 + PR_SET_DUMPABLE = 4 + PR_GET_UNALIGN = 5 + PR_SET_UNALIGN = 6 + PR_UNALIGN_NOPRINT = 1 + PR_UNALIGN_SIGBUS = 2 + PR_GET_KEEPCAPS = 7 + PR_SET_KEEPCAPS = 8 + PR_GET_FPEMU = 9 + PR_SET_FPEMU = 10 + PR_FPEMU_NOPRINT = 1 + PR_FPEMU_SIGFPE = 2 + PR_GET_FPEXC = 11 + PR_SET_FPEXC = 12 + PR_FP_EXC_SW_ENABLE = 0x80 + PR_FP_EXC_DIV = 0x010000 + PR_FP_EXC_OVF = 0x020000 + PR_FP_EXC_UND = 0x040000 + PR_FP_EXC_RES = 0x080000 + PR_FP_EXC_INV = 0x100000 + PR_FP_EXC_DISABLED = 0 + PR_FP_EXC_NONRECOV = 1 + PR_FP_EXC_ASYNC = 2 + PR_FP_EXC_PRECISE = 3 + PR_GET_TIMING = 13 + PR_SET_TIMING = 14 + PR_TIMING_STATISTICAL = 0 + PR_TIMING_TIMESTAMP = 1 + PR_SET_NAME = 15 + PR_GET_NAME = 16 + PR_GET_ENDIAN = 19 + PR_SET_ENDIAN = 20 + PR_ENDIAN_BIG = 0 + PR_ENDIAN_LITTLE = 1 + PR_ENDIAN_PPC_LITTLE = 2 + PR_GET_SECCOMP = 21 + PR_SET_SECCOMP = 22 + PR_CAPBSET_READ = 23 + PR_CAPBSET_DROP = 24 + PR_GET_TSC = 25 + PR_SET_TSC = 26 + PR_TSC_ENABLE = 1 + PR_TSC_SIGSEGV = 2 + PR_GET_SECUREBITS = 27 + PR_SET_SECUREBITS = 28 + PR_SET_TIMERSLACK = 29 + PR_GET_TIMERSLACK = 30 + PR_TASK_PERF_EVENTS_DISABLE = 31 + PR_TASK_PERF_EVENTS_ENABLE = 32 + PR_MCE_KILL = 33 + PR_MCE_KILL_CLEAR = 0 + PR_MCE_KILL_SET = 1 + PR_MCE_KILL_LATE = 0 + PR_MCE_KILL_EARLY = 1 + PR_MCE_KILL_DEFAULT = 2 + PR_MCE_KILL_GET = 34 + PR_SET_MM = 35 + PR_SET_MM_START_CODE = 1 + PR_SET_MM_END_CODE = 2 + PR_SET_MM_START_DATA = 3 + PR_SET_MM_END_DATA = 4 + PR_SET_MM_START_STACK = 5 + PR_SET_MM_START_BRK = 6 + PR_SET_MM_BRK = 7 + PR_SET_MM_ARG_START = 8 + PR_SET_MM_ARG_END = 9 + PR_SET_MM_ENV_START = 10 + PR_SET_MM_ENV_END = 11 + PR_SET_MM_AUXV = 12 + PR_SET_MM_EXE_FILE = 13 + PR_SET_MM_MAP = 14 + PR_SET_MM_MAP_SIZE = 15 + PR_SET_PTRACER = 0x59616D61 + # PR_SET_PTRACER_ANY = ((unsigned long)-1) + PR_SET_CHILD_SUBREAPER = 36 + PR_GET_CHILD_SUBREAPER = 37 + PR_SET_NO_NEW_PRIVS = 38 + PR_GET_NO_NEW_PRIVS = 39 + PR_GET_TID_ADDRESS = 40 + PR_SET_THP_DISABLE = 41 + PR_GET_THP_DISABLE = 42 + PR_MPX_ENABLE_MANAGEMENT = 43 + PR_MPX_DISABLE_MANAGEMENT = 44 + PR_SET_FP_MODE = 45 + PR_GET_FP_MODE = 46 + PR_FP_MODE_FR = 1 + PR_FP_MODE_FRE = 2 + PR_CAP_AMBIENT = 47 + PR_CAP_AMBIENT_IS_SET = 1 + PR_CAP_AMBIENT_RAISE = 2 + PR_CAP_AMBIENT_LOWER = 3 + PR_CAP_AMBIENT_CLEAR_ALL = 4 + PR_SVE_SET_VL = 50 + PR_SVE_SET_VL_ONEXEC = 0x40000 + PR_SVE_GET_VL = 51 + PR_SVE_VL_LEN_MASK = 0xFFFF + PR_SVE_VL_INHERIT = 0x20000 + PR_GET_SPECULATION_CTRL = 52 + PR_SET_SPECULATION_CTRL = 53 + PR_SPEC_STORE_BYPASS = 0 + PR_SPEC_INDIRECT_BRANCH = 1 + PR_SPEC_NOT_AFFECTED = 0 + PR_SPEC_PRCTL = 0x00000001 + PR_SPEC_ENABLE = 0x00000002 + PR_SPEC_DISABLE = 0x00000004 + PR_SPEC_FORCE_DISABLE = 0x00000008 + PR_SPEC_DISABLE_NOEXEC = 0x00000010 + PR_PAC_RESET_KEYS = 54 + PR_PAC_APIAKEY = 0x00000001 + PR_PAC_APIBKEY = 0x00000002 + PR_PAC_APDAKEY = 0x00000004 + PR_PAC_APDBKEY = 0x00000008 + PR_PAC_APGAKEY = 0x00000010 + + +class SIGINFO(ctypes.Structure): + _fields_ = [ + ("si_signo", ctypes.c_int32), + ("si_errno", ctypes.c_int32), + ("si_code", ctypes.c_int32), + ("si_trapno", ctypes.c_int32), + ("si_pid", ctypes.c_int32), # pid_t + ] + + +""" +('sin6_family', ctypes.c_ushort), +('sin6_port', ctypes.c_ushort), +('sin6_flowinfo', ctypes.c_uint32), +('sin6_addr', ctypes.c_uint64), +('sin6_scope_id', ctypes.c_uint32), +""" + + +class SIGAction(enum.IntEnum): + SI_USER = 0 + SI_KERNEL = 0x80 + SI_QUEUE = -1 + SI_TIMER = -2 + SI_MESGQ = -3 + SI_ASYNCIO = -4 + SI_SIGIO = -5 + SI_TKILL = -6 + SI_DETHREAD = -7 + SI_ASYNCNL = -60 + ILL_ILLOPC = 1 + ILL_ILLOPN = 2 + ILL_ILLADR = 3 + ILL_ILLTRP = 4 + ILL_PRVOPC = 5 + ILL_PRVREG = 6 + ILL_COPROC = 7 + ILL_BADSTK = 8 + ILL_BADIADDR = 9 + __ILL_BREAK = 10 + __ILL_BNDMOD = 11 + NSIGILL = 11 + FPE_INTDIV = 1 + FPE_INTOVF = 2 + FPE_FLTDIV = 3 + FPE_FLTOVF = 4 + FPE_FLTUND = 5 + FPE_FLTRES = 6 + FPE_FLTINV = 7 + FPE_FLTSUB = 8 + __FPE_DECOVF = 9 + __FPE_DECDIV = 10 + __FPE_DECERR = 11 + __FPE_INVASC = 12 + __FPE_INVDEC = 13 + FPE_FLTUNK = 14 + FPE_CONDTRAP = 15 + NSIGFPE = 15 + SEGV_MAPERR = 1 + SEGV_ACCERR = 2 + SEGV_BNDERR = 3 + SEGV_PKUERR = 4 + SEGV_ACCADI = 5 + SEGV_ADIDERR = 6 + SEGV_ADIPERR = 7 + NSIGSEGV = 7 + BUS_ADRALN = 1 + BUS_ADRERR = 2 + BUS_OBJERR = 3 + BUS_MCEERR_AR = 4 + BUS_MCEERR_AO = 5 + NSIGBUS = 5 + TRAP_BRKPT = 1 + TRAP_TRACE = 2 + TRAP_BRANCH = 3 + TRAP_HWBKPT = 4 + TRAP_UNK = 5 + NSIGTRAP = 5 + CLD_EXITED = 1 + CLD_KILLED = 2 + CLD_DUMPED = 3 + CLD_TRAPPED = 4 + CLD_STOPPED = 5 + CLD_CONTINUED = 6 + NSIGCHLD = 6 + POLL_IN = 1 + POLL_OUT = 2 + POLL_MSG = 3 + POLL_ERR = 4 + POLL_PRI = 5 + POLL_HUP = 6 + NSIGPOLL = 6 + SYS_SECCOMP = 1 + NSIGSYS = 1 + EMT_TAGOVF = 1 + NSIGEMT = 1 + SIGEV_SIGNAL = 0 + SIGEV_NONE = 1 + SIGEV_THREAD = 2 + SIGEV_THREAD_ID = 4 diff --git a/src/zelos/ext/platforms/linux/syscalls/syscalls_socket.py b/src/zelos/ext/platforms/linux/syscalls/syscalls_socket.py new file mode 100644 index 0000000..2f57b92 --- /dev/null +++ b/src/zelos/ext/platforms/linux/syscalls/syscalls_socket.py @@ -0,0 +1,606 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import ctypes + +import zelos.network.dns as dns + +from zelos.ext.platforms.linux.network import ( + _host_to_bytes, + _port_to_bytes, + get_host_and_port, +) +from zelos.ext.platforms.linux.syscalls.syscalls_const import ( + SOCKADDR_IN, + SocketFamily, + SocketOptionsIp, + SocketOptionsIPV6, + SocketOptionsLevels, + SocketOptionsSocket, + SocketOptionsTcp, + SocketProtocol, + SocketType, +) +from zelos.util import dumpstruct, str2struct, struct2str + +from . import syscall_structs as structs + + +def _parse_sockaddr_in(p, addr, size): + class SOCKADDR_IN(ctypes.Structure): + _fields_ = [ + ("sin_family", ctypes.c_ushort), + ("sin_port", ctypes.c_ushort), + ("sin_addr", ctypes.c_uint32), + ("sin_zero", ctypes.c_char * 8), + ] + + sockaddr_in = SOCKADDR_IN() + str2struct(sockaddr_in, bytes(p.memory.read(addr, size))) + + +def _parse_sockaddr(p, addr, size): + class SOCKADDR(ctypes.Structure): + _fields_ = [ + ("sa_family", ctypes.c_ushort), + ("sa_addr", ctypes.c_char * 14), + ] + + sockaddr = SOCKADDR() + str2struct(sockaddr, bytes(p.memory.read(addr, size))) + + +def _create_sockaddr_in(domain, host, port): + import socket + + struct_bytes = b"" + if domain == socket.AF_INET: + domain = SocketFamily.AF_INET + else: + domain = SocketFamily.AF_INET6 + s_in = SOCKADDR_IN() + s_in.sin_family = domain + s_in.sin_addr = _host_to_bytes(host, domain) + s_in.sin_port = _port_to_bytes(port) + struct_bytes = struct2str(s_in) + return struct_bytes + + +def _socket_linux_to_python(domain, type, protocol): + """ + Convert Linux socket domain, type and protocol constants into their + equivalent python constants. + """ + import socket + + domain_map = { + SocketFamily.AF_INET: socket.AF_INET, + SocketFamily.AF_INET6: socket.AF_INET6, + } + type_map = { + SocketType.SOCK_STREAM: socket.SOCK_STREAM, + SocketType.SOCK_DGRAM: socket.SOCK_DGRAM, + SocketType.SOCK_RAW: socket.SOCK_RAW, + } + proto_map = { + SocketProtocol.IPPROTO_TCP: socket.IPPROTO_TCP, + SocketProtocol.IPPROTO_UDP: socket.IPPROTO_UDP, + SocketProtocol.IPPROTO_RAW: socket.IPPROTO_RAW, + SocketProtocol.IPPROTO_ICMP: socket.IPPROTO_ICMP, + SocketProtocol.IPPROTO_IP: socket.IPPROTO_IP, + } + + if not hasattr(socket, "SOCK_CLOEXEC"): + # Windows support + socket.SOCK_CLOEXEC = 0x80000 + socket.SOCK_NONBLOCK = 0x800 + socket.AF_UNIX = 0x1 + + cloexec = bool(type & SocketType.SOCK_CLOEXEC) + nonblock = bool(type & SocketType.SOCK_NONBLOCK) + type &= ~(SocketType.SOCK_CLOEXEC | SocketType.SOCK_NONBLOCK) + + try: + domain = domain_map[domain] + type = type_map[type] + protocol = proto_map[protocol] + except Exception: + raise Exception(f"Unsupported socket({domain}, {type}, {protocol}") + + if cloexec: + type |= socket.SOCK_NONBLOCK + if nonblock: + type |= socket.SOCK_CLOEXEC + + return (domain, type, protocol) + + +def _socktopt_linux_to_python(level, name): + """ + Convert Linux socktop level and name constants into their + equivalent python constants. + """ + import socket + + level_map = { + SocketOptionsLevels.SOL_SOCKET: socket.SOL_SOCKET, + SocketOptionsLevels.IPPROTO_TCP: socket.IPPROTO_TCP, + SocketOptionsLevels.IPPROTO_IPV6: socket.IPPROTO_IPV6, + SocketOptionsLevels.IPPROTO_IP: socket.IPPROTO_IP, + } + opt_map = { + socket.SOL_SOCKET: { + SocketOptionsSocket.SO_REUSEADDR: socket.SO_REUSEADDR, + SocketOptionsSocket.SO_KEEPALIVE: socket.SO_KEEPALIVE, + }, + socket.IPPROTO_TCP: {SocketOptionsTcp.TCP_NODELAY: socket.TCP_NODELAY}, + socket.IPPROTO_IPV6: { + SocketOptionsIPV6.IPV6_V6ONLY: socket.IPV6_V6ONLY + }, + socket.IPPROTO_IP: { + SocketOptionsIp.IP_HDRINCL: socket.IP_HDRINCL, + SocketOptionsIp.IP_OPTIONS: socket.IP_OPTIONS, + }, + } + try: + level = level_map[level] + name = opt_map[level][name] + except Exception: + raise (f"unsupported sockopt option:" f"level: {level} name: {name}") + return (level, name) + + +def socket(sm, p, args_addr): + args = sm._get_socketcall_args( + p, + "socket", + args_addr, + [("int_DOMAIN", "domain"), ("int", "type"), ("int", "protocol")], + ) + + try: + (domain, type, protocol) = _socket_linux_to_python( + args.domain, args.type, args.protocol + ) + socket_handle_num = sm.z.network.create_socket(domain, type, protocol) + except Exception as e: + print("socket error :", e) + return -1 + return socket_handle_num + + +def bind(sm, p, args_addr): + args = sm._get_socketcall_args( + p, + "bind", + args_addr, + [ + ("int", "sockfd"), + ("const struct sockaddr*", "addr"), + ("socklen_t", "addrlen"), + ], + ) + _parse_sockaddr(p, args.addr, args.addrlen) + socket_handle = sm.z.handles.get(args.sockfd) + sock = socket_handle.socket + addr = bytes(p.memory.read(args.addr, args.addrlen)) + (host, port) = get_host_and_port(sock.domain, addr) + sm.print(f"binding socket 0x{args.sockfd:x} to ({host}, {port})") + return sock.bind((host, port)) + + +def connect(sm, p, args_addr): + def print_addr(args): + socket_handle = sm.z.handles.get(args.sockfd) + if socket_handle is None: + return "{0}=0x{1:x}".format("addr", args.addr) + sock = socket_handle.socket + sockaddr = bytes(p.memory.read(args.addr, args.addrlen)) + (host, port) = get_host_and_port(sock.domain, sockaddr) + return f"dest_addr=0x{args.addr:x} ({host}:{port})" + + args = sm._get_socketcall_args( + p, + "connect", + args_addr, + [ + ("int", "sockfd"), + ("const struct sockaddr*", "addr"), + ("socklen_t", "addrlen"), + ], + arg_string_overrides={"addr": print_addr}, + ) + # _parse_sockaddr(p, args.addr, args.addrlen) + socket_handle = sm.z.handles.get(args.sockfd) + if socket_handle is None: + sm.logger.error("Invalid socket handle") + return -1 + socket = socket_handle.socket + addr = p.memory.read(args.addr, args.addrlen) + + host, port = get_host_and_port(socket.domain, bytes(addr)) + socket_handle.data["dst_name"] = f"{host}:{port}" + socket_handle.data["host"] = host + socket_handle.data["port"] = port + + status = socket.connect((host, port)) + return status + + +def listen(sm, p, args_addr): + args = sm._get_socketcall_args( + p, "listen", args_addr, [("int", "sockfd"), ("int", "backlog")] + ) + + socket_handle = sm.z.handles.get(args.sockfd) + if socket_handle is None: + sm.logger.error("Invalid socket handle") + return -1 + socket = socket_handle.socket + + socket.listen(args.backlog) + + return 0 + + +def accept(sm, p, args_addr): + args = sm._get_socketcall_args( + p, + "accept", + args_addr, + [ + ("int", "sockfd"), + ("struct sockaddr *", "addr"), + ("socklen_t *", "addrlen"), + ], + ) + + socket_handle = sm.z.handles.get(args.sockfd) + if socket_handle is None: + sm.logger.error("Invalid socket handle") + return -1 + socket = socket_handle.socket + + socket.accept() + + return args.sockfd + 1 + + +def getsockname(sm, p, args_addr): + sm._get_socketcall_args( + p, + "getsockname", + args_addr, + [ + ("int", "sockfd"), + ("struct sockaddr *", "addr"), + ("socklen_t *", "addrlen"), + ], + ) + return 0 + + +def getpeername(sm, p, args_addr): + sm._get_socketcall_args( + p, + "getpeername", + args_addr, + [ + ("int", "sockfd"), + ("struct sockaddr *", "addr"), + ("socklen_t *", "addrlen"), + ], + ) + return 0 + + +def socketpair(sm, p, args_addr): + sm._get_socketcall_args( + p, + "socketpair", + args_addr, + [ + ("int", "domain"), + ("int", "type"), + ("int", "protocol"), + ("int *", "sv"), + ], + ) + return 0 + + +def send(sm, p, args_addr): + def print_buf(args): + s = repr(bytes(p.memory.read(args.buf, size=args.len)))[2:-1] + return f'buf=0x{args.buf:x} ("{s}")' + + args = sm._get_socketcall_args( + p, + "send", + args_addr, + [ + ("int", "sockfd"), + ("const void*", "buf"), + ("size_t", "len"), + ("int", "flags"), + ], + arg_string_overrides={"buf": print_buf}, + ) + payload = p.memory.read(args.buf, args.len) + return _send(sm, p, args.sockfd, payload, args.flags) + + +def sendto(sm, p, args_addr): + def print_buf(args): + s = repr(bytes(p.memory.read(args.buf, size=args.len)))[2:-1] + return f'buf=0x{args.buf:x} ("{s}")' + + def print_dst(args): + socket_handle = sm.z.handles.get(args.sockfd) + if socket_handle is None: + return "{0}=0x{1:x}".format("dest_addr", args.dest_addr) + sock = socket_handle.socket + sockaddr = bytes(p.memory.read(args.dest_addr, args.addrlen)) + (host, port) = get_host_and_port(sock.domain, sockaddr) + return f"dest_addr=0x{args.dest_addr:x} ({host}:{port})" + + args = sm._get_socketcall_args( + p, + "sendto", + args_addr, + [ + ("int", "sockfd"), + ("const void*", "buf"), + ("size_t", "len"), + ("int", "flags"), + ("const struct sockaddr*", "dest_addr"), + ("socklen_t", "addrlen"), + ], + arg_string_overrides={"buf": print_buf, "dest_addr": print_dst}, + ) + socket_handle = sm.z.handles.get(args.sockfd) + if socket_handle is None: + sm.logger.notice(f"Could not find socket {args.sockfd}") + return -1 + sock = socket_handle.socket + sockaddr = bytes(p.memory.read(args.dest_addr, args.addrlen)) + (host, port) = get_host_and_port(sock.domain, sockaddr) + payload = p.memory.read(args.buf, args.len) + + if socket_handle.data.get("port", 0) == 53: + target = dns.parse_dns_request(payload) + if target is not None: + sm.print_info(f"DNS Request: {target}") + sm.z.network.add_attempted_connection(target, "sendto") + + return sock.sendto(payload, (host, port), args.flags) + + +def _send(sm, p, sockfd, payload, flags=0): + socket_handle = sm.z.handles.get(sockfd) + if socket_handle is None: + sm.logger.notice(f"Invalid socket fd 0x{sockfd:x}") + return -1 + sock = socket_handle.socket + sent_len = sock.send(payload, flags) + + if socket_handle.data.get("port", 0) == 53: + target = dns.parse_dns_request(payload) + if target is not None: + sm.print_info(f"DNS Request: {target}") + sm.z.network.add_attempted_connection(target, "sendto") + + return sent_len + + +def recv(sm, p, args_addr): + args = sm._get_socketcall_args( + p, + "recv", + args_addr, + [ + ("int", "sockfd"), + ("void *", "buf"), + ("size_t", "len"), + ("int", "flags"), + ], + ) + return _recv(sm, p, args.sockfd, args.buf, args.len, args.flags) + + +def recvfrom(sm, p, args_addr): + args = sm._get_socketcall_args( + p, + "recvfrom", + args_addr, + [ + ("int", "sockfd"), + ("void *", "buf"), + ("size_t", "len"), + ("int", "flags"), + ("struct sockaddr *", "src_addr"), + ("socklen_t *", "addrlen"), + ], + ) + socket_handle = sm.z.handles.get(args.sockfd) + sock = socket_handle.socket + + try: + (data, domain, host, port) = sock.recvfrom(args.len, args.flags) + if args.src_addr != 0 and args.addrlen != 0: + sockaddr = _create_sockaddr_in(domain, host, port) + p.memory.write(args.src_addr, sockaddr) + p.memory.write_uint32(args.addrlen, len(sockaddr)) + if len(data) > 0: + p.memory.write(args.buf, data) + return len(data) + except Exception as e: + print("[recvfrom] error: " + str(e)) + return -1 + + +def _recv(sm, p, sockfd, buf, _len, flags=0): + socket_handle = sm.z.handles.get(sockfd) + if socket_handle is None: + return -1 + sock = socket_handle.socket + has_data = sock.peek() + if has_data: + data = sock.recv(_len, flags) + sm.print(f"received: '{data}'") + p.memory.write(buf, data) + return len(data) + return 0 + + +def shutdown(sm, p, args_addr): + sm._get_socketcall_args( + p, "shutdown", args_addr, [("int", "sockfd"), ("int", "how")] + ) + return 0 + + +def setsockopt(sm, p, args_addr): + arg_list = [ + ("int", "sockfd"), + ("int", "level"), + ("int", "optname"), + ("const void*", "optval"), + ("socklen_t", "optlen"), + ] + args = sm._get_socketcall_args(p, "setsockopt", args_addr, arg_list) + + socket_handle = sm.z.handles.get(args.sockfd) + if socket_handle is None: + sm.logger.error("Invalid socket handle") + return -1 + socket = socket_handle.socket + + optval = p.memory.read(args.optval, args.optlen) + + try: + (level, name) = _socktopt_linux_to_python(args.level, args.optname) + return socket.setsockopt(args.level, args.optname, optval) + except Exception as e: + print("[setsockopt] failed:", e) + + return 0 + + +def getsockopt(sm, p, args_addr): + sm._get_socketcall_args( + p, + "getsockopt", + args_addr, + [ + ("int", "sockfd"), + ("int", "level"), + ("int", "optname"), + ("void *", "optval"), + ("socklen_t *", "optlen"), + ], + ) + return 0 + + +def sendmsg(sm, p, args_addr): + args = sm._get_socketcall_args( + p, + "sendmsg", + args_addr, + [("int", "sockfd"), ("const struct msghdr*", "msg"), ("int", "flags")], + ) + msghdr = p.memory.readstruct(args.msg, structs.MSGHDR()) + return _sendmsg(sm, p, args.sockfd, msghdr, args.flags) + + +def sendmmsg(sm, p, args_addr): + args = sm._get_socketcall_args( + p, + "sendmmsg", + args_addr, + [ + ("int", "sockfd"), + ("struct mmsghdr*", "msgvec"), + ("unsigned int", "vlen"), + ("int", "flags"), + ], + ) + mmsg_addr = args.msgvec + for i in range(args.vlen): + msghdr = p.memory.readstruct(mmsg_addr, structs.MSGHDR()) + bytes_sent = _sendmsg(sm, p, args.sockfd, msghdr, args.flags) + msg_len_addr = mmsg_addr + ctypes.sizeof(msghdr) + int_size = p.memory.write_int(msg_len_addr, bytes_sent) + mmsg_addr += ctypes.sizeof(msghdr) + int_size + return args.vlen + + +def _sendmsg(sm, p, sockfd, msghdr, flags): + dumpstruct(msghdr) + + iovec_array = p.memory.readstructarray( + msghdr.msg_iov, msghdr.msg_iovlen, structs.IOVEC() + ) + + gathered_results = b"" + for iovec in iovec_array: + gathered_results += p.memory.read(iovec.iov_base, iovec.iov_len) + + sent_len = _send(sm, p, sockfd, gathered_results, flags) + + return sent_len + + +def recvmsg(sm, p, args_addr): + sm._get_socketcall_args( + p, + "recvmsg", + args_addr, + [("int", "sockfd"), ("struct msghdr *", "msg"), ("int", "flags")], + ) + return 0 + + +def accept4(sm, p, args_addr): + sm._get_socketcall_args( + p, + "accept4", + args_addr, + [ + ("int", "sockfd"), + ("struct sockaddr *", "addr"), + ("socklen_t *", "addrlen"), + ("int", "flags"), + ], + ) + return 0 + + +def recvmmsg(sm, p, args_addr): + sm._get_socketcall_args( + p, + "recvmmsg", + args_addr, + [ + ("int", "sockfd"), + ("struct mmsghdr *", "msgvec"), + ("unsigned int", "vlen"), + ("int", "flags"), + ("struct timespec *", "timeout"), + ], + ) + return 0 diff --git a/src/zelos/ext/platforms/linux/syscalls/syscalls_table.py b/src/zelos/ext/platforms/linux/syscalls/syscalls_table.py new file mode 100644 index 0000000..1094b53 --- /dev/null +++ b/src/zelos/ext/platforms/linux/syscalls/syscalls_table.py @@ -0,0 +1,508 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +# If you modify this table, tag the line with "MODIFIED" and specify +# what you changed. + +# Table recieved from https://fedora.juszkiewicz.com.pl/syscalls.html +# Renamed mipso32 -> mips +# Renamed i386 -> x86 +cols = ("arm64", "arm", "x86_64", "x32", "x86", "mips", "powerpc", "ia64") + +table = { + "_llseek": (-1, 140, -1, -1, 140, 4140, 140, -1), + "_newselect": (-1, 142, -1, -1, 142, 4142, 142, -1), + "_sysctl": (-1, 149, 156, -1, 149, 4153, 149, 1150), + "accept": (202, 285, 43, 1073741867, -1, 4168, 330, 1194), + "accept4": (242, 366, 288, 1073742112, 364, 4334, 344, 1334), + "access": (-1, 33, 21, 1073741845, 33, 4033, 33, 1049), + "acct": (89, 51, 163, 1073741987, 51, 4051, 51, 1064), + "add_key": (217, 309, 248, 1073742072, 286, 4280, 269, 1271), + "adjtimex": (171, 124, 159, 1073741983, 124, 4124, 124, 1131), + "afs_syscall": (-1, -1, 183, 1073742007, 137, 4137, 137, 1141), + "alarm": (-1, -1, 37, 1073741861, 27, 4027, 27, -1), + "arch_prctl": (-1, -1, 158, 1073741982, 384, -1, -1, -1), + "arm_fadvise64_64": (-1, 270, -1, -1, -1, -1, -1, -1), + "arm_sync_file_range": (-1, 341, -1, -1, -1, -1, -1, -1), + "bdflush": (-1, 134, -1, -1, 134, 4134, 134, 1138), + "bind": (200, 282, 49, 1073741873, 361, 4169, 327, 1191), + "bpf": (280, 386, 321, 1073742145, 357, 4355, 361, 1341), + "break": (-1, -1, -1, -1, 17, 4017, 17, -1), + "brk": (214, 45, 12, 1073741836, 45, 4045, 45, 1060), + "cachectl": (-1, -1, -1, -1, -1, 4148, -1, -1), + # Modified cacheflush arm -1 -> 983042 + "cacheflush": (-1, 983042, -1, -1, -1, 4147, -1, -1), + "capget": (90, 184, 125, 1073741949, 184, 4204, 183, 1185), + "capset": (91, 185, 126, 1073741950, 185, 4205, 184, 1186), + "chdir": (49, 12, 80, 1073741904, 12, 4012, 12, 1034), + "chmod": (-1, 15, 90, 1073741914, 15, 4015, 15, 1038), + "chown": (-1, 182, 92, 1073741916, 182, 4202, 181, 1039), + "chown32": (-1, 212, -1, -1, 212, -1, -1, -1), + "chroot": (51, 61, 161, 1073741985, 61, 4061, 61, 1068), + "clock_adjtime": (266, 372, 305, 1073742129, 343, 4341, 347, 1328), + "clock_adjtime64": (-1, 405, -1, -1, 405, 4405, 405, -1), + "clock_getres": (114, 264, 229, 1073742053, 266, 4264, 247, 1255), + "clock_getres_time64": (-1, 406, -1, -1, 406, 4406, 406, -1), + "clock_gettime": (113, 263, 228, 1073742052, 265, 4263, 246, 1254), + "clock_gettime64": (-1, 403, -1, -1, 403, 4403, 403, -1), + "clock_nanosleep": (115, 265, 230, 1073742054, 267, 4265, 248, 1256), + "clock_nanosleep_time64": (-1, 407, -1, -1, 407, 4407, 407, -1), + "clock_settime": (112, 262, 227, 1073742051, 264, 4262, 245, 1253), + "clock_settime64": (-1, 404, -1, -1, 404, 4404, 404, -1), + "clone": (220, 120, 56, 1073741880, 120, 4120, 120, 1128), + "clone2": (-1, -1, -1, -1, -1, -1, -1, 1213), + "close": (57, 6, 3, 1073741827, 6, 4006, 6, 1029), + "connect": (203, 283, 42, 1073741866, 362, 4170, 328, 1192), + "copy_file_range": (285, 391, 326, 1073742150, 377, 4360, 379, 1347), + "creat": (-1, 8, 85, 1073741909, 8, 4008, 8, 1030), + "create_module": (-1, -1, 174, -1, 127, 4127, 127, -1), + "delete_module": (106, 129, 176, 1073742000, 129, 4129, 129, 1134), + "dup": (23, 41, 32, 1073741856, 41, 4041, 41, 1057), + "dup2": (-1, 63, 33, 1073741857, 63, 4063, 63, 1070), + "dup3": (24, 358, 292, 1073742116, 330, 4327, 316, 1316), + "epoll_create": (-1, 250, 213, 1073742037, 254, 4248, 236, 1243), + "epoll_create1": (20, 357, 291, 1073742115, 329, 4326, 315, 1315), + "epoll_ctl": (21, 251, 233, 1073742057, 255, 4249, 237, 1244), + "epoll_ctl_old": (-1, -1, 214, -1, -1, -1, -1, -1), + "epoll_pwait": (22, 346, 281, 1073742105, 319, 4313, 303, 1305), + "epoll_wait": (-1, 252, 232, 1073742056, 256, 4250, 238, 1245), + "epoll_wait_old": (-1, -1, 215, -1, -1, -1, -1, -1), + "eventfd": (-1, 351, 284, 1073742108, 323, 4319, 307, 1309), + "eventfd2": (19, 356, 290, 1073742114, 328, 4325, 314, 1314), + "execve": (221, 11, 59, 1073742344, 11, 4011, 11, 1033), + "execveat": (281, 387, 322, 1073742369, 358, 4356, 362, 1342), + "exit": (93, 1, 60, 1073741884, 1, 4001, 1, 1025), + "exit_group": (94, 248, 231, 1073742055, 252, 4246, 234, 1236), + "faccessat": (48, 334, 269, 1073742093, 307, 4300, 298, 1293), + "fadvise64": (223, -1, 221, 1073742045, 250, 4254, 233, 1234), + "fadvise64_64": (-1, -1, -1, -1, 272, -1, 254, -1), + "fallocate": (47, 352, 285, 1073742109, 324, 4320, 309, 1303), + "fanotify_init": (262, 367, 300, 1073742124, 338, 4336, 323, 1323), + "fanotify_mark": (263, 368, 301, 1073742125, 339, 4337, 324, 1324), + "fchdir": (50, 133, 81, 1073741905, 133, 4133, 133, 1035), + "fchmod": (52, 94, 91, 1073741915, 94, 4094, 94, 1099), + "fchmodat": (53, 333, 268, 1073742092, 306, 4299, 297, 1292), + "fchown": (55, 95, 93, 1073741917, 95, 4095, 95, 1100), + "fchown32": (-1, 207, -1, -1, 207, -1, -1, -1), + "fchownat": (54, 325, 260, 1073742084, 298, 4291, 289, 1284), + "fcntl": (25, 55, 72, 1073741896, 55, 4055, 55, 1066), + "fcntl64": (-1, 221, -1, -1, 221, 4220, 204, -1), + "fdatasync": (83, 148, 75, 1073741899, 148, 4152, 148, 1052), + "fgetxattr": (10, 231, 193, 1073742017, 231, 4229, 214, 1222), + "finit_module": (273, 379, 313, 1073742137, 350, 4348, 353, 1335), + "flistxattr": (13, 234, 196, 1073742020, 234, 4232, 217, 1225), + "flock": (32, 143, 73, 1073741897, 143, 4143, 143, 1145), + "fork": (-1, 2, 57, 1073741881, 2, 4002, 2, -1), + "fremovexattr": (16, 237, 199, 1073742023, 237, 4235, 220, 1228), + "fsconfig": (431, 431, 431, 1073742255, 431, 4431, 431, 1455), + "fsetxattr": (7, 228, 190, 1073742014, 228, 4226, 211, 1219), + "fsmount": (432, 432, 432, 1073742256, 432, 4432, 432, 1456), + "fsopen": (430, 430, 430, 1073742254, 430, 4430, 430, 1454), + "fspick": (433, 433, 433, 1073742257, 433, 4433, 433, 1457), + "fstat": (80, 108, 5, 1073741829, 108, 4108, 108, 1212), + "fstat64": (-1, 197, -1, -1, 197, 4215, 197, -1), + "fstatat64": (-1, 327, -1, -1, 300, 4293, 291, -1), + "fstatfs": (44, 100, 138, 1073741962, 100, 4100, 100, 1104), + "fstatfs64": (-1, 267, -1, -1, 269, 4256, 253, 1257), + "fsync": (82, 118, 74, 1073741898, 118, 4118, 118, 1051), + "ftime": (-1, -1, -1, -1, 35, 4035, 35, -1), + "ftruncate": (46, 93, 77, 1073741901, 93, 4093, 93, 1098), + "ftruncate64": (-1, 194, -1, -1, 194, 4212, 194, -1), + "futex": (98, 240, 202, 1073742026, 240, 4238, 221, 1230), + "futex_time64": (-1, 422, -1, -1, 422, 4422, 422, -1), + "futimesat": (-1, 326, 261, 1073742085, 299, 4292, 290, 1285), + "get_kernel_syms": (-1, -1, 177, -1, 130, 4130, 130, -1), + "get_mempolicy": (236, 320, 239, 1073742063, 275, 4269, 260, 1260), + "get_robust_list": (100, 339, 274, 1073742355, 312, 4310, 299, 1299), + "get_thread_area": (-1, -1, 211, -1, 244, -1, -1, -1), + "getcpu": (168, 345, 309, 1073742133, 318, 4312, 302, 1304), + "getcwd": (17, 183, 79, 1073741903, 183, 4203, 182, 1184), + "getdents": (-1, 141, 78, 1073741902, 141, 4141, 141, 1144), + "getdents64": (61, 217, 217, 1073742041, 220, 4219, 202, 1214), + "getegid": (177, 50, 108, 1073741932, 50, 4050, 50, 1063), + "getegid32": (-1, 202, -1, -1, 202, -1, -1, -1), + "geteuid": (175, 49, 107, 1073741931, 49, 4049, 49, 1047), + "geteuid32": (-1, 201, -1, -1, 201, -1, -1, -1), + "getgid": (176, 47, 104, 1073741928, 47, 4047, 47, 1062), + "getgid32": (-1, 200, -1, -1, 200, -1, -1, -1), + "getgroups": (158, 80, 115, 1073741939, 80, 4080, 80, 1077), + "getgroups32": (-1, 205, -1, -1, 205, -1, -1, -1), + "getitimer": (102, 105, 36, 1073741860, 105, 4105, 105, 1119), + "getpeername": (205, 287, 52, 1073741876, 368, 4171, 332, 1196), + "getpgid": (155, 132, 121, 1073741945, 132, 4132, 132, 1079), + "getpgrp": (-1, 65, 111, 1073741935, 65, 4065, 65, -1), + "getpid": (172, 20, 39, 1073741863, 20, 4020, 20, 1041), + "getpmsg": (-1, -1, 181, 1073742005, 188, 4208, 187, 1188), + "getppid": (173, 64, 110, 1073741934, 64, 4064, 64, 1042), + "getpriority": (141, 96, 140, 1073741964, 96, 4096, 96, 1101), + "getrandom": (278, 384, 318, 1073742142, 355, 4353, 359, 1339), + "getresgid": (150, 171, 120, 1073741944, 171, 4191, 170, 1075), + "getresgid32": (-1, 211, -1, -1, 211, -1, -1, -1), + "getresuid": (148, 165, 118, 1073741942, 165, 4186, 165, 1073), + "getresuid32": (-1, 209, -1, -1, 209, -1, -1, -1), + "getrlimit": (163, -1, 97, 1073741921, 76, 4076, 76, 1085), + "getrusage": (165, 77, 98, 1073741922, 77, 4077, 77, 1086), + "getsid": (156, 147, 124, 1073741948, 147, 4151, 147, 1082), + "getsockname": (204, 286, 51, 1073741875, 367, 4172, 331, 1195), + "getsockopt": (209, 295, 55, 1073742366, 365, 4173, 340, 1204), + "gettid": (178, 224, 186, 1073742010, 224, 4222, 207, 1105), + "gettimeofday": (169, 78, 96, 1073741920, 78, 4078, 78, 1087), + "getuid": (174, 24, 102, 1073741926, 24, 4024, 24, 1046), + "getuid32": (-1, 199, -1, -1, 199, -1, -1, -1), + "getunwind": (-1, -1, -1, -1, -1, -1, -1, 1215), + "getxattr": (8, 229, 191, 1073742015, 229, 4227, 212, 1220), + "gtty": (-1, -1, -1, -1, 32, 4032, 32, -1), + "idle": (-1, -1, -1, -1, 112, 4112, 112, -1), + "init_module": (105, 128, 175, 1073741999, 128, 4128, 128, 1133), + "inotify_add_watch": (27, 317, 254, 1073742078, 292, 4285, 276, 1278), + "inotify_init": (-1, 316, 253, 1073742077, 291, 4284, 275, 1277), + "inotify_init1": (26, 360, 294, 1073742118, 332, 4329, 318, 1318), + "inotify_rm_watch": (28, 318, 255, 1073742079, 293, 4286, 277, 1279), + "io_cancel": (3, 247, 210, 1073742034, 249, 4245, 231, 1242), + "io_destroy": (1, 244, 207, 1073742031, 246, 4242, 228, 1239), + "io_getevents": (4, 245, 208, 1073742032, 247, 4243, 229, 1240), + "io_pgetevents": (292, 399, 333, 1073742157, 385, 4368, 388, 1351), + "io_pgetevents_time64": (-1, 416, -1, -1, 416, 4416, 416, -1), + "io_setup": (0, 243, 206, 1073742367, 245, 4241, 227, 1238), + "io_submit": (2, 246, 209, 1073742368, 248, 4244, 230, 1241), + "io_uring_enter": (426, 426, 426, 1073742250, 426, 4426, 426, 1450), + "io_uring_register": (427, 427, 427, 1073742251, 427, 4427, 427, 1451), + "io_uring_setup": (425, 425, 425, 1073742249, 425, 4425, 425, 1449), + "ioctl": (29, 54, 16, 1073742338, 54, 4054, 54, 1065), + "ioperm": (-1, -1, 173, 1073741997, 101, 4101, 101, -1), + "iopl": (-1, -1, 172, 1073741996, 110, 4110, 110, -1), + "ioprio_get": (31, 315, 252, 1073742076, 290, 4315, 274, 1275), + "ioprio_set": (30, 314, 251, 1073742075, 289, 4314, 273, 1274), + "ipc": (-1, -1, -1, -1, 117, 4117, 117, -1), + "kcmp": (272, 378, 312, 1073742136, 349, 4347, 354, 1345), + "kexec_file_load": (294, 401, 320, 1073742144, -1, -1, 382, -1), + "kexec_load": (104, 347, 246, 1073742352, 283, 4311, 268, 1268), + "keyctl": (219, 311, 250, 1073742074, 288, 4282, 271, 1273), + "kill": (129, 37, 62, 1073741886, 37, 4037, 37, 1053), + "lchown": (-1, 16, 94, 1073741918, 16, 4016, 16, 1124), + "lchown32": (-1, 198, -1, -1, 198, -1, -1, -1), + "lgetxattr": (9, 230, 192, 1073742016, 230, 4228, 213, 1221), + "link": (-1, 9, 86, 1073741910, 9, 4009, 9, 1031), + "linkat": (37, 330, 265, 1073742089, 303, 4296, 294, 1289), + "listen": (201, 284, 50, 1073741874, 363, 4174, 329, 1193), + "listxattr": (11, 232, 194, 1073742018, 232, 4230, 215, 1223), + "llistxattr": (12, 233, 195, 1073742019, 233, 4231, 216, 1224), + "lock": (-1, -1, -1, -1, 53, 4053, 53, -1), + "lookup_dcookie": (18, 249, 212, 1073742036, 253, 4247, 235, 1237), + "lremovexattr": (15, 236, 198, 1073742022, 236, 4234, 219, 1227), + "lseek": (62, 19, 8, 1073741832, 19, 4019, 19, 1040), + "lsetxattr": (6, 227, 189, 1073742013, 227, 4225, 210, 1218), + "lstat": (-1, 107, 6, 1073741830, 107, 4107, 107, 1211), + "lstat64": (-1, 196, -1, -1, 196, 4214, 196, -1), + "madvise": (233, 220, 28, 1073741852, 219, 4218, 205, 1209), + "mbind": (235, 319, 237, 1073742061, 274, 4268, 259, 1259), + "membarrier": (283, 389, 324, 1073742148, 375, 4358, 365, 1344), + "memfd_create": (279, 385, 319, 1073742143, 356, 4354, 360, 1340), + "migrate_pages": (238, 400, 256, 1073742080, 294, 4287, 258, 1280), + "mincore": (232, 219, 27, 1073741851, 218, 4217, 206, 1208), + "mkdir": (-1, 39, 83, 1073741907, 39, 4039, 39, 1055), + "mkdirat": (34, 323, 258, 1073742082, 296, 4289, 287, 1282), + "mknod": (-1, 14, 133, 1073741957, 14, 4014, 14, 1037), + "mknodat": (33, 324, 259, 1073742083, 297, 4290, 288, 1283), + "mlock": (228, 150, 149, 1073741973, 150, 4154, 150, 1153), + "mlock2": (284, 390, 325, 1073742149, 376, 4359, 378, 1346), + "mlockall": (230, 152, 151, 1073741975, 152, 4156, 152, 1154), + "mmap": (222, -1, 9, 1073741833, 90, 4090, 90, 1151), + "mmap2": (-1, 192, -1, -1, 192, 4210, 192, 1172), + "modify_ldt": (-1, -1, 154, 1073741978, 123, 4123, 123, -1), + "mount": (40, 21, 165, 1073741989, 21, 4021, 21, 1043), + "move_mount": (429, 429, 429, 1073742253, 429, 4429, 429, 1453), + "move_pages": (239, 344, 279, 1073742357, 317, 4308, 301, 1276), + "mprotect": (226, 125, 10, 1073741834, 125, 4125, 125, 1155), + "mpx": (-1, -1, -1, -1, 56, 4056, 56, -1), + "mq_getsetattr": (185, 279, 245, 1073742069, 282, 4276, 267, 1267), + "mq_notify": (184, 278, 244, 1073742351, 281, 4275, 266, 1266), + "mq_open": (180, 274, 240, 1073742064, 277, 4271, 262, 1262), + "mq_timedreceive": (183, 277, 243, 1073742067, 280, 4274, 265, 1265), + "mq_timedreceive_time64": (-1, 419, -1, -1, 419, 4419, 419, -1), + "mq_timedsend": (182, 276, 242, 1073742066, 279, 4273, 264, 1264), + "mq_timedsend_time64": (-1, 418, -1, -1, 418, 4418, 418, -1), + "mq_unlink": (181, 275, 241, 1073742065, 278, 4272, 263, 1263), + "mremap": (216, 163, 25, 1073741849, 163, 4167, 163, 1156), + "msgctl": (187, 304, 71, 1073741895, 402, 4402, 402, 1112), + "msgget": (186, 303, 68, 1073741892, 399, 4399, 399, 1109), + "msgrcv": (188, 302, 70, 1073741894, 401, 4401, 401, 1111), + "msgsnd": (189, 301, 69, 1073741893, 400, 4400, 400, 1110), + "msync": (227, 144, 26, 1073741850, 144, 4144, 144, 1157), + "multiplexer": (-1, -1, -1, -1, -1, -1, 201, -1), + "munlock": (229, 151, 150, 1073741974, 151, 4155, 151, 1158), + "munlockall": (231, 153, 152, 1073741976, 153, 4157, 153, 1159), + "munmap": (215, 91, 11, 1073741835, 91, 4091, 91, 1152), + "name_to_handle_at": (264, 370, 303, 1073742127, 341, 4339, 345, 1326), + "nanosleep": (101, 162, 35, 1073741859, 162, 4166, 162, 1168), + "newfstatat": (79, -1, 262, 1073742086, -1, -1, -1, 1286), + "nfsservctl": (42, 169, 180, -1, 169, 4189, 168, 1169), + "ni_syscall": (-1, -1, -1, -1, -1, -1, -1, 1024), + "nice": (-1, 34, -1, -1, 34, 4034, 34, -1), + "oldfstat": (-1, -1, -1, -1, 28, -1, 28, -1), + "oldlstat": (-1, -1, -1, -1, 84, -1, 84, -1), + "oldolduname": (-1, -1, -1, -1, 59, -1, 59, -1), + "oldstat": (-1, -1, -1, -1, 18, -1, 18, -1), + "olduname": (-1, -1, -1, -1, 109, -1, 109, -1), + "open": (-1, 5, 2, 1073741826, 5, 4005, 5, 1028), + "open_by_handle_at": (265, 371, 304, 1073742128, 342, 4340, 346, 1327), + "open_tree": (428, 428, 428, 1073742252, 428, 4428, 428, 1452), + "openat": (56, 322, 257, 1073742081, 295, 4288, 286, 1281), + "pause": (-1, 29, 34, 1073741858, 29, 4029, 29, -1), + "pciconfig_iobase": (-1, 271, -1, -1, -1, -1, 200, -1), + "pciconfig_read": (-1, 272, -1, -1, -1, -1, 198, 1173), + "pciconfig_write": (-1, 273, -1, -1, -1, -1, 199, 1174), + "perf_event_open": (241, 364, 298, 1073742122, 336, 4333, 319, 1352), + "perfmonctl": (-1, -1, -1, -1, -1, -1, -1, 1175), + "personality": (92, 136, 135, 1073741959, 136, 4136, 136, 1140), + "pidfd_send_signal": (424, 424, 424, 1073742248, 424, 4424, 424, 1448), + "pipe": (-1, 42, 22, 1073741846, 42, 4042, 42, 1058), + "pipe2": (59, 359, 293, 1073742117, 331, 4328, 317, 1317), + "pivot_root": (41, 218, 155, 1073741979, 217, 4216, 203, 1207), + "pkey_alloc": (289, 395, 330, 1073742154, 381, 4364, 384, 1355), + "pkey_free": (290, 396, 331, 1073742155, 382, 4365, 385, 1356), + "pkey_mprotect": (288, 394, 329, 1073742153, 380, 4363, 386, 1354), + "poll": (-1, 168, 7, 1073741831, 168, 4188, 167, 1090), + "ppoll": (73, 336, 271, 1073742095, 309, 4302, 281, 1295), + "ppoll_time64": (-1, 414, -1, -1, 414, 4414, 414, -1), + "prctl": (167, 172, 157, 1073741981, 172, 4192, 171, 1170), + "pread64": (67, 180, 17, 1073741841, 180, 4200, 179, 1148), + "preadv": (69, 361, 295, 1073742358, 333, 4330, 320, 1319), + "preadv2": (286, 392, 327, 1073742370, 378, 4361, 380, 1348), + "prlimit64": (261, 369, 302, 1073742126, 340, 4338, 325, 1325), + "process_vm_readv": (270, 376, 310, 1073742363, 347, 4345, 351, 1332), + "process_vm_writev": (271, 377, 311, 1073742364, 348, 4346, 352, 1333), + "prof": (-1, -1, -1, -1, 44, 4044, 44, -1), + "profil": (-1, -1, -1, -1, 98, 4098, 98, -1), + "pselect6": (72, 335, 270, 1073742094, 308, 4301, 280, 1294), + "pselect6_time64": (-1, 413, -1, -1, 413, 4413, 413, -1), + "ptrace": (117, 26, 101, 1073742345, 26, 4026, 26, 1048), + "putpmsg": (-1, -1, 182, 1073742006, 189, 4209, 188, 1189), + "pwrite64": (68, 181, 18, 1073741842, 181, 4201, 180, 1149), + "pwritev": (70, 362, 296, 1073742359, 334, 4331, 321, 1320), + "pwritev2": (287, 393, 328, 1073742371, 379, 4362, 381, 1349), + "query_module": (-1, -1, 178, -1, 167, 4187, 166, -1), + "quotactl": (60, 131, 179, 1073742003, 131, 4131, 131, 1137), + "read": (63, 3, 0, 1073741824, 3, 4003, 3, 1026), + "readahead": (213, 225, 187, 1073742011, 225, 4223, 191, 1216), + "readdir": (-1, -1, -1, -1, 89, 4089, 89, -1), + "readlink": (-1, 85, 89, 1073741913, 85, 4085, 85, 1092), + "readlinkat": (78, 332, 267, 1073742091, 305, 4298, 296, 1291), + "readv": (65, 145, 19, 1073742339, 145, 4145, 145, 1146), + "reboot": (142, 88, 169, 1073741993, 88, 4088, 88, 1096), + "recv": (-1, 291, -1, -1, -1, 4175, 336, 1200), + "recvfrom": (207, 292, 45, 1073742341, 371, 4176, 337, 1201), + "recvmmsg": (243, 365, 299, 1073742361, 337, 4335, 343, 1322), + "recvmmsg_time64": (-1, 417, -1, -1, 417, 4417, 417, -1), + "recvmsg": (212, 297, 47, 1073742343, 372, 4177, 342, 1206), + "remap_file_pages": (234, 253, 216, 1073742040, 257, 4251, 239, 1125), + "removexattr": (14, 235, 197, 1073742021, 235, 4233, 218, 1226), + "rename": (-1, 38, 82, 1073741906, 38, 4038, 38, 1054), + "renameat": (38, 329, 264, 1073742088, 302, 4295, 293, 1288), + "renameat2": (276, 382, 316, 1073742140, 353, 4351, 357, 1338), + "request_key": (218, 310, 249, 1073742073, 287, 4281, 270, 1272), + "restart_syscall": (128, 0, 219, 1073742043, 0, 4253, 0, 1246), + "rmdir": (-1, 40, 84, 1073741908, 40, 4040, 40, 1056), + "rseq": (293, 398, 334, 1073742158, 386, 4367, 387, 1357), + "rt_sigaction": (134, 174, 13, 1073742336, 174, 4194, 173, 1177), + "rt_sigpending": (136, 176, 127, 1073742346, 176, 4196, 175, 1178), + "rt_sigprocmask": (135, 175, 14, 1073741838, 175, 4195, 174, 1179), + "rt_sigqueueinfo": (138, 178, 129, 1073742348, 178, 4198, 177, 1180), + "rt_sigreturn": (139, 173, 15, 1073742337, 173, 4193, 172, 1181), + "rt_sigsuspend": (133, 179, 130, 1073741954, 179, 4199, 178, 1182), + "rt_sigtimedwait": (137, 177, 128, 1073742347, 177, 4197, 176, 1183), + "rt_sigtimedwait_time64": (-1, 421, -1, -1, 421, 4421, 421, -1), + "rt_tgsigqueueinfo": (240, 363, 297, 1073742360, 335, 4332, 322, 1321), + "rtas": (-1, -1, -1, -1, -1, -1, 255, -1), + "sched_get_priority_max": ( + 125, + 159, + 146, + 1073741970, + 159, + 4163, + 159, + 1165, + ), + "sched_get_priority_min": ( + 126, + 160, + 147, + 1073741971, + 160, + 4164, + 160, + 1166, + ), + "sched_getaffinity": (123, 242, 204, 1073742028, 242, 4240, 223, 1232), + "sched_getattr": (275, 381, 315, 1073742139, 352, 4350, 356, 1337), + "sched_getparam": (121, 155, 143, 1073741967, 155, 4159, 155, 1160), + "sched_getscheduler": (120, 157, 145, 1073741969, 157, 4161, 157, 1162), + "sched_rr_get_interval": (127, 161, 148, 1073741972, 161, 4165, 161, 1167), + "sched_rr_get_interval_time64": (-1, 423, -1, -1, 423, 4423, 423, -1), + "sched_setaffinity": (122, 241, 203, 1073742027, 241, 4239, 222, 1231), + "sched_setattr": (274, 380, 314, 1073742138, 351, 4349, 355, 1336), + "sched_setparam": (118, 154, 142, 1073741966, 154, 4158, 154, 1161), + "sched_setscheduler": (119, 156, 144, 1073741968, 156, 4160, 156, 1163), + "sched_yield": (124, 158, 24, 1073741848, 158, 4162, 158, 1164), + "seccomp": (277, 383, 317, 1073742141, 354, 4352, 358, 1353), + "security": (-1, -1, 185, 1073742009, -1, -1, -1, -1), + "select": (-1, -1, 23, 1073741847, 82, -1, 82, 1089), + "semctl": (191, 300, 66, 1073741890, 394, 4394, 394, 1108), + "semget": (190, 299, 64, 1073741888, 393, 4393, 393, 1106), + "semop": (193, 298, 65, 1073741889, -1, -1, -1, 1107), + "semtimedop": (192, 312, 220, 1073742044, -1, -1, -1, 1247), + "semtimedop_time64": (-1, 420, -1, -1, 420, 4420, 420, -1), + "send": (-1, 289, -1, -1, -1, 4178, 334, 1198), + "sendfile": (71, 187, 40, 1073741864, 187, 4207, 186, 1187), + "sendfile64": (-1, 239, -1, -1, 239, 4237, 226, -1), + "sendmmsg": (269, 374, 307, 1073742362, 345, 4343, 349, 1331), + "sendmsg": (211, 296, 46, 1073742342, 370, 4179, 341, 1205), + "sendto": (206, 290, 44, 1073741868, 369, 4180, 335, 1199), + "set_mempolicy": (237, 321, 238, 1073742062, 276, 4270, 261, 1261), + "set_robust_list": (99, 338, 273, 1073742354, 311, 4309, 300, 1298), + "set_thread_area": (-1, -1, 205, -1, 243, 4283, -1, -1), + "set_tid_address": (96, 256, 218, 1073742042, 258, 4252, 232, 1233), + # Custom arm syscall set_tls. + "set_tls": (-1, 0xF0005, -1, -1, -1, -1, -1, -1), + "setdomainname": (162, 121, 171, 1073741995, 121, 4121, 121, 1129), + "setfsgid": (152, 139, 123, 1073741947, 139, 4139, 139, 1143), + "setfsgid32": (-1, 216, -1, -1, 216, -1, -1, -1), + "setfsuid": (151, 138, 122, 1073741946, 138, 4138, 138, 1142), + "setfsuid32": (-1, 215, -1, -1, 215, -1, -1, -1), + "setgid": (144, 46, 106, 1073741930, 46, 4046, 46, 1061), + "setgid32": (-1, 214, -1, -1, 214, -1, -1, -1), + "setgroups": (159, 81, 116, 1073741940, 81, 4081, 81, 1078), + "setgroups32": (-1, 206, -1, -1, 206, -1, -1, -1), + "sethostname": (161, 74, 170, 1073741994, 74, 4074, 74, 1083), + "setitimer": (103, 104, 38, 1073741862, 104, 4104, 104, 1118), + "setns": (268, 375, 308, 1073742132, 346, 4344, 350, 1330), + "setpgid": (154, 57, 109, 1073741933, 57, 4057, 57, 1080), + "setpriority": (140, 97, 141, 1073741965, 97, 4097, 97, 1102), + "setregid": (143, 71, 114, 1073741938, 71, 4071, 71, 1072), + "setregid32": (-1, 204, -1, -1, 204, -1, -1, -1), + "setresgid": (149, 170, 119, 1073741943, 170, 4190, 169, 1076), + "setresgid32": (-1, 210, -1, -1, 210, -1, -1, -1), + "setresuid": (147, 164, 117, 1073741941, 164, 4185, 164, 1074), + "setresuid32": (-1, 208, -1, -1, 208, -1, -1, -1), + "setreuid": (145, 70, 113, 1073741937, 70, 4070, 70, 1071), + "setreuid32": (-1, 203, -1, -1, 203, -1, -1, -1), + "setrlimit": (164, 75, 160, 1073741984, 75, 4075, 75, 1084), + "setsid": (157, 66, 112, 1073741936, 66, 4066, 66, 1081), + "setsockopt": (208, 294, 54, 1073742365, 366, 4181, 339, 1203), + "settimeofday": (170, 79, 164, 1073741988, 79, 4079, 79, 1088), + "setuid": (146, 23, 105, 1073741929, 23, 4023, 23, 1045), + "setuid32": (-1, 213, -1, -1, 213, -1, -1, -1), + "setxattr": (5, 226, 188, 1073742012, 226, 4224, 209, 1217), + "sgetmask": (-1, -1, -1, -1, 68, 4068, 68, -1), + "shmat": (196, 305, 30, 1073741854, 397, 4397, 397, 1114), + "shmctl": (195, 308, 31, 1073741855, 396, 4396, 396, 1116), + "shmdt": (197, 306, 67, 1073741891, 398, 4398, 398, 1115), + "shmget": (194, 307, 29, 1073741853, 395, 4395, 395, 1113), + "shutdown": (210, 293, 48, 1073741872, 373, 4182, 338, 1202), + "sigaction": (-1, 67, -1, -1, 67, 4067, 67, -1), + "sigaltstack": (132, 186, 131, 1073742349, 186, 4206, 185, 1176), + "signal": (-1, -1, -1, -1, 48, 4048, 48, -1), + "signalfd": (-1, 349, 282, 1073742106, 321, 4317, 305, 1307), + "signalfd4": (74, 355, 289, 1073742113, 327, 4324, 313, 1313), + "sigpending": (-1, 73, -1, -1, 73, 4073, 73, -1), + "sigprocmask": (-1, 126, -1, -1, 126, 4126, 126, -1), + "sigreturn": (-1, 119, -1, -1, 119, 4119, 119, -1), + "sigsuspend": (-1, 72, -1, -1, 72, 4072, 72, -1), + "socket": (198, 281, 41, 1073741865, 359, 4183, 326, 1190), + # modified socketcall arm -1 -> 102 + "socketcall": (-1, 102, -1, -1, 102, 4102, 102, -1), + "socketpair": (199, 288, 53, 1073741877, 360, 4184, 333, 1197), + "splice": (76, 340, 275, 1073742099, 313, 4304, 283, 1297), + "spu_create": (-1, -1, -1, -1, -1, -1, 279, -1), + "spu_run": (-1, -1, -1, -1, -1, -1, 278, -1), + "ssetmask": (-1, -1, -1, -1, 69, 4069, 69, -1), + "stat": (-1, 106, 4, 1073741828, 106, 4106, 106, 1210), + "stat64": (-1, 195, -1, -1, 195, 4213, 195, -1), + "statfs": (43, 99, 137, 1073741961, 99, 4099, 99, 1103), + "statfs64": (-1, 266, -1, -1, 268, 4255, 252, 1258), + "statx": (291, 397, 332, 1073742156, 383, 4366, 383, 1350), + "stime": (-1, -1, -1, -1, 25, 4025, 25, -1), + "stty": (-1, -1, -1, -1, 31, 4031, 31, -1), + "subpage_prot": (-1, -1, -1, -1, -1, -1, 310, -1), + "swapcontext": (-1, -1, -1, -1, -1, -1, 249, -1), + "swapoff": (225, 115, 168, 1073741992, 115, 4115, 115, 1095), + "swapon": (224, 87, 167, 1073741991, 87, 4087, 87, 1094), + "switch_endian": (-1, -1, -1, -1, -1, -1, 363, -1), + "symlink": (-1, 83, 88, 1073741912, 83, 4083, 83, 1091), + "symlinkat": (36, 331, 266, 1073742090, 304, 4297, 295, 1290), + "sync": (81, 36, 162, 1073741986, 36, 4036, 36, 1050), + "sync_file_range": (84, -1, 277, 1073742101, 314, 4305, -1, 1300), + "sync_file_range2": (-1, 341, -1, -1, -1, -1, 308, -1), + "syncfs": (267, 373, 306, 1073742130, 344, 4342, 348, 1329), + "sys_debug_setcontext": (-1, -1, -1, -1, -1, -1, 256, -1), + # https://chromium.googlesource.com/native_client/nacl-newlib/+/master/libgloss/arm/linux-syscall.h + # Found that sys_syscall is a thing + "syscall": (-1, 113, -1, -1, -1, -1, -1, -1), + "sysfs": (-1, 135, 139, 1073741963, 135, 4135, 135, 1139), + "sysinfo": (179, 116, 99, 1073741923, 116, 4116, 116, 1127), + "syslog": (116, 103, 103, 1073741927, 103, 4103, 103, 1117), + "sysmips": (-1, -1, -1, -1, -1, 4149, -1, -1), + "tee": (77, 342, 276, 1073742100, 315, 4306, 284, 1301), + "tgkill": (131, 268, 234, 1073742058, 270, 4266, 250, 1235), + # MODIFIED arm: 13 + "time": (-1, 13, 201, 1073742025, 13, 4013, 13, -1), + "timer_create": (107, 257, 222, 1073742350, 259, 4257, 240, 1248), + "timer_delete": (111, 261, 226, 1073742050, 263, 4261, 244, 1252), + "timer_getoverrun": (109, 260, 225, 1073742049, 262, 4260, 243, 1251), + "timer_gettime": (108, 259, 224, 1073742048, 261, 4259, 242, 1250), + "timer_gettime64": (-1, 408, -1, -1, 408, 4408, 408, -1), + "timer_settime": (110, 258, 223, 1073742047, 260, 4258, 241, 1249), + "timer_settime64": (-1, 409, -1, -1, 409, 4409, 409, -1), + "timerfd": (-1, -1, -1, -1, -1, 4318, -1, 1308), + "timerfd_create": (85, 350, 283, 1073742107, 322, 4321, 306, 1310), + "timerfd_gettime": (87, 354, 287, 1073742111, 326, 4322, 312, 1312), + "timerfd_gettime64": (-1, 410, -1, -1, 410, 4410, 410, -1), + "timerfd_settime": (86, 353, 286, 1073742110, 325, 4323, 311, 1311), + "timerfd_settime64": (-1, 411, -1, -1, 411, 4411, 411, -1), + "times": (153, 43, 100, 1073741924, 43, 4043, 43, 1059), + "tkill": (130, 238, 200, 1073742024, 238, 4236, 208, 1229), + "truncate": (45, 92, 76, 1073741900, 92, 4092, 92, 1097), + "truncate64": (-1, 193, -1, -1, 193, 4211, 193, -1), + "tuxcall": (-1, -1, 184, 1073742008, -1, -1, 225, -1), + "ugetrlimit": (-1, 191, -1, -1, 191, -1, 190, -1), + "ulimit": (-1, -1, -1, -1, 58, 4058, 58, -1), + "umask": (166, 60, 95, 1073741919, 60, 4060, 60, 1067), + "umount": (-1, -1, -1, -1, 22, 4022, 22, 1044), + "umount2": (39, 52, 166, 1073741990, 52, 4052, 52, 1044), + "uname": (160, 122, 63, 1073741887, 122, 4122, 122, 1130), + "unlink": (-1, 10, 87, 1073741911, 10, 4010, 10, 1032), + "unlinkat": (35, 328, 263, 1073742087, 301, 4294, 292, 1287), + "unshare": (97, 337, 272, 1073742096, 310, 4303, 282, 1296), + "uselib": (-1, 86, 134, -1, 86, 4086, 86, 1093), + "userfaultfd": (282, 388, 323, 1073742147, 374, 4357, 364, 1343), + "ustat": (-1, 62, 136, 1073741960, 62, 4062, 62, 1069), + "utime": (-1, -1, 132, 1073741956, 30, 4030, 30, -1), + "utimensat": (88, 348, 280, 1073742104, 320, 4316, 304, 1306), + "utimensat_time64": (-1, 412, -1, -1, 412, 4412, 412, -1), + "utimes": (-1, 269, 235, 1073742059, 271, 4267, 251, 1036), + "vfork": (-1, 190, 58, 1073741882, 190, -1, 189, -1), + "vhangup": (58, 111, 153, 1073741977, 111, 4111, 111, 1123), + "vm86": (-1, -1, -1, -1, 166, 4113, 113, -1), + "vm86old": (-1, -1, -1, -1, 113, -1, -1, -1), + "vmsplice": (75, 343, 278, 1073742356, 316, 4307, 285, 1302), + "vserver": (-1, 313, 236, -1, 273, 4277, -1, 1269), + "wait4": (260, 114, 61, 1073741885, 114, 4114, 114, 1126), + "waitid": (95, 280, 247, 1073742353, 284, 4278, 272, 1270), + "waitpid": (-1, -1, -1, -1, 7, 4007, 7, -1), + "write": (64, 4, 1, 1073741825, 4, 4004, 4, 1027), + "writev": (66, 146, 20, 1073742340, 146, 4146, 146, 1147), +} diff --git a/src/zelos/ext/platforms/linux/test_network.py b/src/zelos/ext/platforms/linux/test_network.py new file mode 100644 index 0000000..224a7a3 --- /dev/null +++ b/src/zelos/ext/platforms/linux/test_network.py @@ -0,0 +1,36 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import unittest + +from zelos.ext.platforms.linux.network import _bytes_to_host +from zelos.ext.platforms.linux.syscalls.syscalls_const import SocketFamily + + +class NetworkTest(unittest.TestCase): + def test_helper_funcs(self): + self.assertEqual( + _bytes_to_host(0x80706050, SocketFamily.AF_INET), "80.96.112.128" + ) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/src/zelos/ext/plugins/runner.py b/src/zelos/ext/plugins/runner.py new file mode 100644 index 0000000..b40cb23 --- /dev/null +++ b/src/zelos/ext/plugins/runner.py @@ -0,0 +1,99 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +from zelos import HookType, IPlugin + + +class Runner(IPlugin): + """ + Useful for getting the emulator to run until a desired condition + """ + + def run_to_addr(self, address): + """ Stops emulator the next time this address is executed""" + self.zelos.step() + self.stop_at(address) + self.zelos.start() + + def stop_at(self, target_addr): + """ Causes execution to stop at the target_addr """ + + def stop_with_interrupt(zelos, address, size): + current_process = zelos.process + process_name = current_process.name + self.logger.debug( + f"Got to {target_addr:x} in process {process_name}" + ) + + current_process.scheduler.stop("stop_at") + + self.zelos.hook_execution( + HookType.EXEC.INST, + stop_with_interrupt, + name="stop_at_hook", + ip_low=target_addr, + ip_high=target_addr, + end_condition=lambda: True, + ) + + # TODO consider allowing tunability, by giving option to adjust how + # often a hook can be checked + # TODO Work on allowing this to delete itself. + def stop_when(self, condition): + """ + Stops execution when the condition is found to be true. This + will only be checked as frequently as the hook type.For example, + UC_HOOK_BLOCK will only check the condition at the beginning of + each block""" + + def stop_with_interrupt(zelos, address, size): + if condition(): + zelos.stop("stop_when") + + self.zelos.hook_execution( + HookType.EXEC.BLOCK, stop_with_interrupt, name="stop_when_hook" + ) + + def next_ret(self): + """ Stops emulator after the next ret instruction """ + zelos = self.zelos + while True: + zelos.step() + byte = zelos.memory.read(zelos.regs.getIP(), 1) + if byte[0] == 0xC3: + zelos.step() + return + + def next_write(self, target_addr): + """ + Stops emulator after the next time the target address is + written to + """ + + def hook(zelos, access, address, size, value): + print("Writing %x (%d bytes) to %x" % (value, size, address)) + zelos.stop("next_write") + + self.zelos.hook_memory( + HookType.MEMORY.WRITE, + hook, + name="temp_memwrite_hook", + mem_low=target_addr, + mem_high=target_addr, + end_condition=lambda: True, + ) + self.zelos.start() diff --git a/src/zelos/ext/plugins/syscall_limiter.py b/src/zelos/ext/plugins/syscall_limiter.py new file mode 100644 index 0000000..efd1840 --- /dev/null +++ b/src/zelos/ext/plugins/syscall_limiter.py @@ -0,0 +1,94 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +from collections import defaultdict + +from zelos import CommandLineOption, HookType, IPlugin + + +CommandLineOption( + "syscall_limit", + type=int, + default=0, + help="Stop execution after SYSCALL_LIMIT syscalls are executed.", +) + +CommandLineOption( + "syscall_thread_limit", + type=int, + default=0, + help="End THREAD after SYSCALL_THREAD_LIMIT syscalls are executed" + " in that thread", +) + +CommandLineOption( + "syscall_thread_swap", + type=int, + default=100, + help="Swap threads after every SYSCALL_THREAD_SWAP syscalls are executed", +) + + +class SyscallLimiter(IPlugin): + """ Limit execution by the number of syscalls overall or by thread. """ + + def __init__(self, z): + super().__init__(z) + if ( + z.config.syscall_limit > 0 + or z.config.syscall_thread_limit > 0 + or z.config.syscall_thread_swap > 0 + ): + self.zelos.hook_syscalls( + HookType.SYSCALL.AFTER, self._syscall_callback + ) + self.syscall_cnt = 0 + self.syscall_thread_cnt = defaultdict(int) + + def _syscall_callback(self, zelos, sysname, args, retval): + if zelos.thread is None: + return + + thread_name = zelos.internal_engine.current_thread.name + + self.syscall_cnt += 1 + self.syscall_thread_cnt[thread_name] += 1 + + # End execution if syscall limit reached + if ( + zelos.config.syscall_limit > 0 + and self.syscall_cnt >= zelos.config.syscall_limit + ): + zelos.stop("syscall limit") + return + + # End thread if syscall thread limit reached + if ( + zelos.config.syscall_thread_limit != 0 + and self.syscall_thread_cnt[thread_name] + % zelos.config.syscall_thread_limit + == 0 + ): + zelos.end_thread() + return + + # Swap threads if syscall thread swap limit reached + if ( + zelos.config.syscall_thread_swap > 0 + and self.syscall_cnt % zelos.config.syscall_thread_swap == 0 + ): + zelos.swap_thread("syscall limit thread swap") + return diff --git a/src/zelos/file_system.py b/src/zelos/file_system.py new file mode 100644 index 0000000..4108ef7 --- /dev/null +++ b/src/zelos/file_system.py @@ -0,0 +1,332 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import logging +import ntpath +import os +import posixpath + +from collections import defaultdict +from typing import Optional + +from zelos.exceptions import ZelosException + + +class PathTranslator: + """ + PathTranslator manages the relationship between paths in the + emulated environment. It will mimic whatever path it is initialized + with. + """ + + def __init__(self, file_prefix): + # Determine what type of path it is + # Ntpath includes posixpaths, so be sure to test + # posix first :P + if posixpath.isabs(file_prefix): + self._is_absolute_path = posixpath.isabs + self.emulated_join = posixpath.join + self.working_directory = "/" + elif ntpath.isabs(file_prefix): + self._is_absolute_path = ntpath.isabs + self.emulated_join = ntpath.join + self.working_directory, _ = ntpath.splitdrive(file_prefix) + else: + raise ZelosException( + ( + f"Path {file_prefix} is not an absolute " + "filepath of any system I know of." + ) + ) + + self.logger = logging.getLogger(__name__) + self.added_files = {} + self.mounted_folders = defaultdict(list) + + def is_absolute_path(self, emulated_path): + return self._is_absolute_path(emulated_path) + + def change_working_directory(self, emulated_path): + emulated_path = self._normalize_emulated_path(emulated_path) + self.working_directory = emulated_path + + def add_file(self, real_path, emulated_path=None): + if not os.path.isfile(real_path): + self.logger.error( + f"Unable to locate file {real_path} for inclusion in " + f"emulated filesystem" + ) + return False + if emulated_path is None: + emulated_path = self.emulated_join( + self.working_directory, os.path.basename(real_path) + ) + emulated_path = self._normalize_emulated_path(emulated_path) + self.added_files[emulated_path] = real_path + return True + + def mount_folder(self, real_path, emulated_path=None): + if not os.path.isdir(real_path): + self.logger.error( + f"Unable to locate folder {real_path} for mounting" + ) + return False + if emulated_path is None: + emulated_path = self.working_directory + emulated_path = self._normalize_emulated_path(emulated_path) + self.mounted_folders[emulated_path].append(real_path) + return True + + def _normalize_emulated_path(self, emulated_path): + """ + If emulated path is None, defaults to the working directory + """ + if emulated_path is None: + emulated_path = self.working_directory + if emulated_path.startswith("./"): + emulated_path = emulated_path[2:] + if not self.is_absolute_path(emulated_path): + emulated_path = self.emulated_join( + self.working_directory, emulated_path + ) + return emulated_path + + def emulated_path_to_host_path(self, emulated_path: str) -> Optional[str]: + # Order for checking files + # Sandbox (since these files could be modified versions of + # files elsewhere) + # added files (individual ones) + # mounted folders + emulated_path = self._normalize_emulated_path(emulated_path) + + path = self.get_sandbox_path(emulated_path) + if path is not None: + self.logger.debug(f"From sandbox path: {emulated_path} -> {path}") + return path + + path = self.added_files.get(emulated_path, None) + if path is not None: + self.logger.debug(f"From added file: {emulated_path} -> {path}") + return path + + for emu_mount, real_mounts in self.mounted_folders.items(): + for real_mount in real_mounts: + self.logger.debug( + f"Checking {emu_mount}->{real_mount} for {emulated_path} " + ) + if emulated_path.startswith(emu_mount): + real_path = os.path.join( + real_mount, emulated_path[len(emu_mount) :] + ) + if os.path.lexists(real_path): + self.logger.debug( + f"From mounted folder: " + f"{emulated_path} -> {real_path}" + ) + return real_path + + self.logger.debug(f"No real path for '{emulated_path}'") + return None + + def get_sandbox_path(self, emulated_path): + return None + + +class FileSystem(PathTranslator): + # TODO: We need to allow /tmp directory to be accessed, otherwise + # cloud stuff probably won't work. + def __init__(self, z, processes, hook_manager): + self.directories = [] + self.z = z + self.handles = z.handles + self._processes = processes + self._hook_manager = hook_manager + self.logger = logging.getLogger(__name__) + + # Written files go into an isolated virtual file system + self.sandbox_path = "sandbox" + self.sandboxed_files = dict() + + self.fds = [] + + def __del__(self): + for fd in self.fds: + try: + fd.close() + except Exception: + pass + + def setup(self, file_prefix): + PathTranslator.__init__(self, file_prefix) + self.zelos_file_prefix = file_prefix + + def create_file(self, emulated_path): + """ + Creates file with the given name. Returns the handle used to + access it + """ + handle_num = self.handles.new_file(emulated_path) + handle = self.handles.get(handle_num) + handle.data["offset"] = 0 + return handle_num + + def get_file_by_name(self, filename): + handle_num = self.handles.get_by_name(filename) + return handle_num + + def get_filename(self, handle): + handle_data = self.handles.get(handle) + return "" if handle_data is None else handle_data.Name + + def get_file_offset(self, handle): + handle_data = self.handles.get(handle) + return 0 if handle_data is None else handle_data.data["offset"] + + def set_file_offset(self, handle, new_offset): + handle_data = self.handles.get(handle) + if handle_data is not None: + handle_data.data["offset"] = new_offset + + def create_file_mapping(self, handle): + new_handle_num = self.handles.new("file_mapping", "0x%x" % handle) + new_handle = self.handles.get(new_handle_num) + new_handle.data["file"] = handle + return new_handle_num + + def get_file_mapping(self, handle): + handle_data = self.handles.get(handle) + return 0 if handle_data is None else handle_data.data["file"] + + def write_to_sandbox(self, orig_filename, data, offset=0): + if orig_filename == "": + return + # TODO: There should be a generalized way to map between the + # windows vision of the files and the internal zelos vision. + if orig_filename.startswith(self.zelos_file_prefix): + orig_filename = orig_filename[len(self.zelos_file_prefix) :] + + self.z.triggers.tr_file_write(orig_filename, data) + + orig_filename = str(orig_filename).lower() + filename = self.sandboxed_files.get(orig_filename, "") + if len(filename) == 0: + filename = ( + orig_filename.replace("\\", "_") + .replace("/", "_") + .replace(":", "_") + ) + while filename != filename.replace("..", "."): + filename = filename.replace("..", ".") + filename = os.path.join(self.sandbox_path, filename) + self.sandboxed_files[orig_filename] = filename + print(os.path.dirname(os.path.abspath(filename))) + print(os.path.abspath(self.sandbox_path)) + if os.path.dirname(os.path.abspath(filename)) != os.path.abspath( + self.sandbox_path + ): + print( + "[Sandbox] Filename attempts to escape sandbox, " + "ignoring this file write..." + ) + return + print("[Sandbox] Created file {0}".format(filename)) + if not os.path.exists(self.sandbox_path): + os.makedirs(self.sandbox_path) + if os.path.exists(filename): + f = self.unsafe_open(filename, "r+b") + else: + f = self.unsafe_open(filename, "wb") + f.seek(offset) + f.write(data) + f.close() + + def list_dir(self, orig_filename): + path = self.find_library(orig_filename) + if path is None: + return None + return os.listdir(path) + + def open_library(self, orig_filename): + path = self.find_library(orig_filename) + if path is None: + return None + fd = open(path, "rb") + self.fds.append(fd) + return fd + + def find_library(self, orig_filename): + """ + Second return value is True if this was a library found in the + library path. + """ + if orig_filename == "": + return None + + if orig_filename.startswith(self.zelos_file_prefix): + orig_filename = orig_filename[len(self.zelos_file_prefix) :] + + # Handle the /proc virtual subsystem # linux specific + # if orig_filename.startswith("/proc"): + # return self.handle_proc_virtual_filesystem(orig_filename) + + # bytearrays cannot be hashed, ensure this is a string when + # checking dicts + path = self._find_path(orig_filename) + if path is None: + path = self._find_path(orig_filename.lower()) + return path + + # def handle_proc_virtual_filesystem(self, filename): + # # TODO: insert the current processes filepath here. Or some + # # other way of getting the filepath of the current process. + # # if filename == '/proc/self/exe': + # # return self._processes.current_process.module_path + # return None + + def unsafe_open(self, *args, **kwargs): + """ + Ensures that the file opened by this call is closed upon call to + `Engine.close`. This function does not validate that the + filepath is restricted appropriately, and thus should not be + used in syscalls, or anywhere else that executing code has + control over the inputs to the binary. + """ + f = open(*args, **kwargs) + self._hook_manager.register_close_hook(f.close) + return f + + def _find_path(self, filename): + """ + TODO: consolidate the filepath checking code to be more + sensical, probably using the idea of mount points + """ + if filename in self.sandboxed_files: + return self.sandboxed_files[filename] + + # For absolute linux paths, remove the first slash + + return self.emulated_path_to_host_path(filename) + + # for d in self.library_path: + # p = os.path.join(d, filename) + # self.logger.debug(f'Looking for path {p}') + # if not p.startswith(d): + # continue + # if os.path.exists(p): + # return p + + # return None diff --git a/src/zelos/handles/__init__.py b/src/zelos/handles/__init__.py new file mode 100644 index 0000000..2a1bb88 --- /dev/null +++ b/src/zelos/handles/__init__.py @@ -0,0 +1,56 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +from .base_handles import ( + FileHandle, + Handle, + Handles, + KeyedEventHandle, + ObjectHandle, + PipeInHandle, + PipeOutHandle, + ProcessHandle, + RegistryKeyHandle, + SectionHandle, + SocketHandle, + StdErr, + StdIn, + StdOut, + SymbolicLinkObjectHandle, + ThreadHandle, + WorkerFactoryHandle, +) + + +__all__ = [ + "Handle", + "FileHandle", + "SocketHandle", + "RegistryKeyHandle", + "SectionHandle", + "SymbolicLinkObjectHandle", + "WorkerFactoryHandle", + "ObjectHandle", + "KeyedEventHandle", + "ProcessHandle", + "ThreadHandle", + "PipeInHandle", + "PipeOutHandle", + "StdIn", + "StdOut", + "StdErr", + "Handles", +] diff --git a/src/zelos/handles/base_handles.py b/src/zelos/handles/base_handles.py new file mode 100644 index 0000000..138b5f2 --- /dev/null +++ b/src/zelos/handles/base_handles.py @@ -0,0 +1,502 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +from __future__ import print_function + +import logging + +from collections import defaultdict +from typing import List, Optional, Tuple + +from termcolor import colored + +from zelos.hooks import HookType + +from .pipe import Pipe + + +class Handle(object): + def __init__(self, name, parent_thread, access=0): + self.Refs = 0 + self.Access = 0 + self.Name = name + self.data = {} + + self.parent_thread = parent_thread + + def __str__(self): + return f"{self.category()}\tRefs {self.Refs}\tAccess" + f" {self.Access:08x}\t\t{self.Name}" + + def close(self) -> None: + """ + Closes an instance of this handle, maintaining the number of + references to the underlying object. + """ + if self.Refs == 0: + logging.getLogger(__name__).notice( + "Tried to close handle, but there are no more refs." + ) + return + self.Refs -= 1 + return + + def category(self) -> str: + """ + Returns: + The type of object this handle represents. + """ + s = type(self).__name__ + return s.replace("Handle", "").lower() + + +class FileHandle(Handle): + def __init__(self, name, parent_thread, access=0, is_dir=False): + super().__init__(name, parent_thread, access) + self.Offset = 0 + self.Size = 0 + self.is_dir = is_dir + + def seek(self, offset: int, whence: int = 0) -> None: + if whence == 0: # SEEK_SET + self.Offset = offset + elif whence == 1: # SEEK_CUR + self.Offset += offset + elif whence == 2: # SEEK_END + pass + # self.Offset = self.Size - offset + + +class SocketHandle(Handle): + def __init__(self, name, parent_thread, socket, access=0): + super().__init__(name, parent_thread, access) + self.socket = socket + + def close(self) -> None: + super().close() + self.socket.close() + + +class RegistryKeyHandle(Handle): + def __init__(self, name, parent_thread, access=0, attributes=None): + super().__init__(name, parent_thread, access) + self.object_attributes = attributes + + +class SectionHandle(Handle): + def __init__(self, name, parent_thread, access=0, attributes=None): + super().__init__(name, parent_thread, access) + self.object_attributes = attributes + + +class SymbolicLinkObjectHandle(Handle): + def __init__(self, name, parent_thread, access=0, attributes=None): + super().__init__(name, parent_thread, access) + self.object_attributes = attributes + + +class WorkerFactoryHandle(Handle): + def __init__(self, name, parent_thread, access=0, attributes=None): + super().__init__(name, parent_thread, access) + self.object_attributes = attributes + + +class ObjectHandle(Handle): + def __init__(self, name, parent_thread, access=0, attributes=None): + super().__init__(name, parent_thread, access) + self.object_attributes = attributes + + +class KeyedEventHandle(Handle): + def __init__(self, name, parent_thread, access=0, attributes=None): + super().__init__(name, parent_thread, access) + self.object_attributes = attributes + + +class ProcessHandle(Handle): + def __init__( + self, name, parent_thread, pid, access=0, attributes=None, flags=None + ): + super().__init__(name, parent_thread, access) + self.object_attributes = attributes + self.pid = pid + self.flags = flags + + +class ThreadHandle(Handle): + def __init__( + self, + name, + parent_thread, + pid, + tid, + access=0, + attributes=None, + flags=None, + ): + super().__init__(name, parent_thread, access) + self.object_attributes = attributes + self.pid = pid + self.tid = tid + self.flags = flags + + +class PipeInHandle(Handle): + def __init__(self, name, pipe, parent_thread=None, access=0): + super().__init__(name, parent_thread, access) + self.pipe = pipe + + def write(self, data: bytes) -> int: + bytes_written = self.pipe.write(data) + return bytes_written + + def close(self) -> None: + super().close() + if self.Refs == 0: + self.pipe.write_end_closed = True + + +class PipeOutHandle(Handle): + def __init__(self, name, pipe, parent_thread=None, access=0): + super().__init__(name, parent_thread, access) + self.pipe = pipe + + def read(self, size: int) -> bytes: + return self.pipe.read(size) + + def close(self) -> None: + super().close() + if self.Refs == 0: + self.pipe.read_end_closed = True + + +class StdIn(Handle): + def __init__(self, parent_thread="unknown"): + super().__init__("StdIn", parent_thread) + + +class StdOut(Handle): + def __init__(self, parent_thread="unknown"): + super().__init__("StdOut", parent_thread) + + def write(self, data): + print(f'{colored("[StdOut]:", "green")} \'{data}\'') + + +class StdErr(Handle): + def __init__(self, parent_thread="unknown"): + super().__init__("StdErr", parent_thread) + + def write(self, data): + print(f'{colored("[StdErr]:", "red")} \'{data}\'') + + +class Handles: + def __init__(self, processes, hook_manager): + self.processes = processes + self.logger = logging.getLogger(__name__) + self.handle_dict = defaultdict(dict) + self.closed_handles = dict() + # TODO Add function for managing handle indices, so anybody who + # modifies handle-creation code in the future does not + # accidentally break internal index rules (being a multiple of + # 4, for example) + + # Handles must be a multiple of 4. + # Lower two bits used by usermode. + # see devblogs.microsoft.com/oldnewthing/20050121-00/?p=36633 + self.handle_index = 4 + + def init_handles(p): + # Add some default system handles + self.add_handle(StdIn(), handle_num=0, pid=p.pid) + self.add_handle(StdOut(), handle_num=1, pid=p.pid) + self.add_handle(StdErr(), handle_num=2, pid=p.pid) + + hook_manager.register_process_hook( + HookType.PROCESS.CREATE, init_handles + ) + + def add_handle(self, handle, handle_num=None, pid=None): + """ Returns the handle id for the handle""" + if pid is None: + pid = self.processes.current_process.pid + if handle_num is None: + handle_num = self._get_handle_num() + if self.exists(handle_num, pid): + self.close(handle_num, pid) + self.logger.notice(f"Closed existing handle at '{handle_num}'") + handle.Refs += 1 + return self._add_handle(handle_num, handle, pid) + + def get( + self, handle_num: int, pid: Optional[int] = None + ) -> Optional[Handle]: + """ + Return the handle object with the given index, or None if it + does not exist + """ + if pid is None: + pid = self.processes.current_process.pid + return self._get_handle(handle_num, pid) + + def exists(self, handle_num: int, pid: Optional[int] = None) -> bool: + """ + Returns true if the given handle_num already exists for the pid + """ + handle = self.get(handle_num, pid=pid) + return handle is not None + + # Return a new {File,Section,Event,Key,Mutant,Directory,Desktop, + # ALPC Port,Semaphore,WindowStation,etc.} handle + + def _current_thread_name(self) -> str: + curr_thread = self.processes.current_process.current_thread + return "none" if curr_thread is None else curr_thread.name + + def new(self, T, name, access=0, handle_num=None): + """ + Used to create handles that are not one we already support + """ + parent_thread = self._current_thread_name() + handle = Handle(name, parent_thread, access) + handle.Type = T + handle_num = self.add_handle(handle, handle_num=handle_num) + return handle_num + + def new_file(self, name, access=0, handle_num=None, is_dir=False): + parent_thread = self._current_thread_name() + handle = FileHandle(name, parent_thread, access, is_dir) + handle_num = self.add_handle(handle, handle_num=handle_num) + return handle_num + + def new_socket(self, name, socket, access=0, handle_num=None): + parent_thread = self._current_thread_name() + handle = SocketHandle(name, parent_thread, socket, access) + handle_num = self.add_handle(handle, handle_num=handle_num) + return handle_num + + def new_regkey(self, name, access=0, attributes=None, handle_num=None): + parent_thread = self._current_thread_name() + handle = RegistryKeyHandle(name, parent_thread, access, attributes) + handle_num = self.add_handle(handle, handle_num=handle_num) + return handle_num + + def new_process( + self, + name, + pid, + attributes, + parent_thread="unknown", + access=0, + handle_num=None, + flags=None, + ): + handle = ProcessHandle( + name, parent_thread, pid, access, attributes, flags + ) + handle_num = self.add_handle(handle, handle_num=handle_num) + return handle_num + + def new_thread( + self, + name, + pid, + tid, + attributes, + parent_thread="unknown", + access=0, + handle_num=None, + flags=None, + ): + handle = ThreadHandle( + name, parent_thread, pid, tid, attributes, access, flags + ) + handle_num = self.add_handle(handle, handle_num=handle_num) + return handle_num + + def new_pipe(self, name, access=0): + parent_thread = self._current_thread_name() + pipe = Pipe() + + out_handle = PipeOutHandle( + name + "_out", pipe, parent_thread=parent_thread, access=access + ) + in_handle = PipeInHandle( + name + "_in", pipe, parent_thread=parent_thread, access=access + ) + out_handle_num = self.add_handle(out_handle) + in_handle_num = self.add_handle(in_handle) + return (out_handle_num, in_handle_num) + + def get_by_name(self, name: str) -> Optional[int]: + """ + Gets the numeric identifier for the first handle that has the + specified name. + + Args: + name: The name of the handle to retrieve. + + Returns: + The handle number corresponding to the specified name if one + exists. If no such handle exists, returns None. + """ + for handle_num, h in self._all_handles(None): + if h.Name == name: + return handle_num + return None + + def get_by_type(self, class_type: type) -> List[Handle]: + """ + Returns all handles of the given type. + + Args: + class_type: Specifies the type that all returned handles + should be an instance of. + + Returns: + All handles that are an instance of the specified type. + """ + return [ + h for _, h in self._all_handles(None) if isinstance(h, class_type) + ] + + def get_by_parent_thread( + self, parent_thread_name: str + ) -> List[Tuple[int, Handle]]: + """ + Gets all handles created by the specified thread + + Args: + parent_thread_name: Restricts the handles given back to + those created by the thread with this name. + + Returns: + A list of tuples containing the handle num and handle of + all the handles created by the parent thread. + """ + return [ + (num, h) + for (num, h) in self._all_handles(None) + if h.parent_thread == parent_thread_name + ] + + def close(self, handle_num: int, pid: Optional[int] = None) -> None: + """ + Close this handle. If there are more references to the + underlying object, it will remain open and only decrement the + reference count. + + Args: + handle_num: The handle_id of the handle you want to close + pid: The process you want to edit the handles of. Defaults + to the current process. + + """ + if pid is None: + pid = self.processes.current_process.pid + + h = self.get(handle_num, pid) + if h is None: + self.logger.notice( + f"Unable to close 0x{handle_num:x} in pid 0x{pid:x}" + ) + return + + h.close() + self._del_handle(handle_num, pid) + + def close_all(self, pid: Optional[int] = None) -> None: + """ + Closes all handles present in the specified process. + + Args: + pid: The pid of the process to close all handles of. + Defaults to all handles in all processes. + """ + for num, _ in self._all_handles(pid=pid): + self.close(num, pid=pid) + + def _add_handle(self, handle_num, handle, pid): + self.handle_dict[pid][handle_num] = handle + return handle_num + + def _del_handle(self, handle_num, pid): + process_handle_dict = self.handle_dict[pid] + del process_handle_dict[handle_num] + + def _clear(self): + self.handle_dict.clear() + + def _get_handle(self, handle_num: int, pid: int) -> Optional[Handle]: + try: + return self.handle_dict[pid][handle_num] + except KeyError: # this handle doesn't exist. + return None + + def _all_handles(self, pid=None): + handles = [] + if pid is not None: + return [ + (num, h) for num, h in self.handle_dict.get(pid, {}).items() + ] + + # Return all handles across processes + for process_handle_dict in self.handle_dict.values(): + handles.extend( + [(num, h) for num, h in process_handle_dict.items()] + ) + return handles + + def _get_handle_num(self, requested_num=None): + """ + Allocates a handle number if not provided, and checks if the + handle is valid. + """ + handle_num = requested_num + if handle_num is None: + self.handle_index += 4 + handle_num = self.handle_index + + if self.exists(handle_num): + self.logger.error(f"Handle {handle_num} has already been taken") + return None + return handle_num + + def _save_state(self): + context = { + "handles": self._all_handles(None), + "closed_handles": self.closed_handles.copy(), + "handle_index": self.handle_index, + } + return context + + def _load_state(self, data): + self._clear() + for (num, h) in data["handles"]: + self._add_handle(num, h, self.processes.current_process) + self.handles = data["handles"] + self.closed_handles = data["closed_handles"] + self.handle_index = data["handle_index"] + + def __str__(self): + s = "Handles" + for k, h in sorted(self._all_handles(None)): + s += f"0x{k:x}: {h}\n" + return s + + def __repr__(self): + return self.__str__() diff --git a/src/zelos/handles/pipe.py b/src/zelos/handles/pipe.py new file mode 100644 index 0000000..26b438b --- /dev/null +++ b/src/zelos/handles/pipe.py @@ -0,0 +1,49 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + + +class Pipe: + """ + Class used to communicate information between processes similar + to a linux pipe. + """ + + def __init__(self): + self.buffer = b"" + self.write_end_closed = False + self.read_end_closed = False + + def write(self, data: bytes) -> int: + """ + Write data to the pipe's buffer. Returns the number of bytes + written to buffer + """ + self.buffer += data + return len(data) + + def read(self, size=None) -> bytes: + """ + Read data from the pipe's buffer up to the requested size. + Defaults to reading the entire buffer + """ + if size is None: + size = len(self.buffer) + data, self.buffer = self.buffer[:size], self.buffer[size:] + return data + + def is_empty(self) -> bool: + return len(self.buffer) == 0 diff --git a/src/zelos/hooks.py b/src/zelos/hooks.py new file mode 100644 index 0000000..2075b4c --- /dev/null +++ b/src/zelos/hooks.py @@ -0,0 +1,565 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import logging + +from collections import defaultdict +from typing import Any, Callable, Optional + +import unicorn as uc + +from zelos.enums import HookType +from zelos.exceptions import InvalidHookTypeException, ZelosRuntimeException + + +class HookInfo: + def __init__( + self, + hook_type, + callback, + handle, + name: str = "", + start=None, + end=None, + end_condition=None, + ): + self.type = hook_type + self.callback = callback + self.handle = handle + self.name = name + self.start = start + self.end = end + self.end_condition = end_condition + + def __str__(self): + hook_string = f"{self.name}, Type {self.type}" + if self.start is not None or self.end is not None: + start = "None" if self.start is None else hex(self.start) + end = "None" if self.end is None else hex(self.end) + + hook_string += f", Effective from {start} - {end}" + return hook_string + + +def _zelos_hook_to_unicorn(hook_type): + return { + HookType.MEMORY.READ: uc.UC_HOOK_MEM_READ, + HookType.MEMORY.WRITE: uc.UC_HOOK_MEM_WRITE, + HookType.MEMORY.READ_UNMAPPED: uc.UC_HOOK_MEM_READ_UNMAPPED, + HookType.MEMORY.WRITE_UNMAPPED: uc.UC_HOOK_MEM_WRITE_UNMAPPED, + HookType.MEMORY.READ_PROT: uc.UC_HOOK_MEM_READ_PROT, + HookType.MEMORY.WRITE_PROT: uc.UC_HOOK_MEM_WRITE_PROT, + HookType.MEMORY.READ_AFTER: uc.UC_HOOK_MEM_READ_AFTER, + HookType.MEMORY.UNMAPPED: uc.UC_HOOK_MEM_UNMAPPED, + HookType.MEMORY.PROT: uc.UC_HOOK_MEM_PROT, + HookType.MEMORY.READ_INVALID: uc.UC_HOOK_MEM_READ_INVALID, + HookType.MEMORY.WRITE_INVALID: uc.UC_HOOK_MEM_WRITE_INVALID, + HookType.MEMORY.INVALID: uc.UC_HOOK_MEM_INVALID, + HookType.MEMORY.VALID: uc.UC_HOOK_MEM_VALID, + HookType.EXEC.INST: uc.UC_HOOK_CODE, + HookType.EXEC.BLOCK: uc.UC_HOOK_BLOCK, + HookType._OTHER.INTERRUPT: uc.UC_HOOK_INTR, + HookType._INST.X86_SYSCALL: uc.x86_const.UC_X86_INS_SYSCALL, + }[hook_type] + + +class HookManager: + """ + Manages hooks that allow user code to execute at certain predefined + events, such as the creation of threads/process, or the execution of + a block of instructions. + """ + + def __init__(self, z, api) -> None: + self.logger = logging.getLogger(__name__) + self.z = z + self.api = api + self.exception_handle_hook = None + + self._hook_index = 0 + self._hooks = defaultdict(dict) + + self._cross_process_hooks = {} + + def register_mem_hook( + self, + hook_type: HookType.MEMORY, + callback: Callable[["Zelos", int, int, int, int], Any], + mem_low: Optional[int] = None, + mem_high: Optional[int] = None, + name: Optional[str] = None, + end_condition: Optional[Callable[[], bool]] = None, + ) -> HookInfo: + """ + Registers a hook on memory. Executes callback every time the + specified event happens in memory. + + The hook will only trigger when the event occurs at an address + between mem_low and mem_high, if either of them are specified. + + The hook will continue to trigger until the end_condition + specified evaluates to True. + + Args: + hook_type: Specifies the event in memory that should trigger + the callback to be executed. + callback: The code that should be executed when the + specified event occurs. The function should accept the + following inputs: (zelos, access, address, size, value). + The return value of "callback" is ignored. + mem_low: If specified, only executes callback if the + event occurs at an address greater than or equal to + this. + mem_high: If specified, only executes callback if the + event occurs at an address less than or equal to this. + name: An identifier for this hook. Used for debugging. + end_condition: If specified, executes after the callback. If + the function returns True, this hook is deleted. + + Returns + Information regarding the hook. Can be used for deletion. + + """ + + def memhook_wrapper(uc, access, address, size, value, user_data): + return callback(self.api, access, address, size, value) + + return self._add_unicorn_hook( + hook_type, + memhook_wrapper, + name, + mem_low, + mem_high, + end_condition=end_condition, + ) + + def register_exec_hook( + self, + hook_type: HookType.EXEC, + callback: Callable[["Zelos", int, int], Any], + ip_low: Optional[int] = None, + ip_high: Optional[int] = None, + name: Optional[str] = None, + end_condition: Optional[Callable[[], bool]] = None, + ) -> HookInfo: + """ + Registers a hook that executes when code is executed. This is + either for every instruction that is executed, or every block. + + The hook will only trigger when the event occurs at an address + between ip_low and ip_high, if either of them are specified. + + The hook will continue to trigger until the end_condition + specified evaluates to True. + + Args: + hook_type: Specifies whether the callback should be + triggered every instruction, or every block. + callback: The code that should be executed when the + specified event occurs. The function should accept the + following inputs: (zelos, address, size). + The return value of "callback" is ignored. + mem_low: If specified, only executes callback if the + event occurs at an address greater than or equal to + this. + mem_high: If specified, only executes callback if the + event occurs at an address less than or equal to this. + name: An identifier for this hook. Used for debugging. + end_condition: If specified, executes after the callback. If + the function returns True, this hook is deleted. + + Returns + Information regarding the hook. Can be used for deletion. + """ + + def exechook_wrapper(uc, address, size, user_data): + return callback(self.api, address, size) + + return self._add_unicorn_hook( + hook_type, + exechook_wrapper, + name, + ip_low, + ip_high, + end_condition=end_condition, + ) + + def register_interrupt_hook( + self, callback, intno=None, name=None, end_condition=None + ): + if intno is None: + return self.z.interrupt_handler.register_generic_interrupt_handler( + callback + ) + else: + return self.z.interrupt_handler.register_interrupt_handler( + intno, callback + ) + + def register_thread_hook(self, hook_type, callback, name=None): + if not isinstance(hook_type, HookType.THREAD): + raise InvalidHookTypeException() + return self._add_zelos_hook(hook_type, callback, name) + + def register_process_hook(self, hook_type, callback, name=None): + if not isinstance(hook_type, HookType.PROCESS): + raise InvalidHookTypeException() + return self._add_zelos_hook(hook_type, callback, name) + + def register_inst_type_hook( + self, inst_type, callback, name="", start_addr=None, end_addr=None + ) -> HookInfo: + def insttype_hook_wrapper(uc, user_data): + return callback(self.api) + + return self._add_unicorn_hook( + inst_type, + insttype_hook_wrapper, + name=name, + start_addr=start_addr, + end_addr=end_addr, + ) + + def register_syscall_hook( + self, syscall_hook_type, callback, name=None + ) -> HookInfo: + return self._add_zelos_hook(syscall_hook_type, callback, name) + + def register_exception_hook(self, callback, name=None) -> HookInfo: + self.z.exception_handler.register_exception_handler(callback) + return HookInfo(HookType._OTHER.EXCEPTION, callback, None, name) + + def register_close_hook( + self, closure: Callable[[], Any], name=None + ) -> HookInfo: + """ + Registers a closure that is called before Zelos benignly exits. + If Zelos does not exist cleanly, there is no guarantee that + hooks registered here will be called. + + Args: + closure: Called before Zelos exits. + """ + return self._add_zelos_hook(HookType._OTHER.CLOSE, closure, name) + + def delete_hook(self, hook_info: HookInfo) -> None: + """ + Deletes a hook + + Args: + hook_info: + """ + if self._is_unicorn_hook(hook_info.type): + self._delete_unicorn_hook(hook_info.handle) + else: + try: + del self._hooks[hook_info.type][hook_info.handle] + except KeyError: + self.logger.warning( + f"Hook handle {hook_info.handle} does not exist for" + f"hook type {hook_info.type}" + ) + + def _delete_unicorn_hook(self, handle): + for p in self.z.processes.process_list: + p.hooks._delete_unicorn_hook(handle) + + def _is_unicorn_hook(self, hook_type): + if isinstance( + hook_type, (HookType.MEMORY, HookType.EXEC) + ) or hook_type in [HookType._OTHER.INTERRUPT]: + return True + elif isinstance( + hook_type, (HookType.PROCESS, HookType.THREAD, HookType.SYSCALL) + ) or hook_type in [HookType._OTHER.CLOSE]: + return False + raise Exception( + f"Unsure whether {hook_type} is a type of unicorn hook." + ) + return False + + def _add_zelos_hook(self, hook_type, callback, name=None) -> HookInfo: + hook_info = HookInfo(hook_type, callback, self._hook_index, name=name) + self._hooks[hook_type][self._hook_index] = callback + self._hook_index += 1 + return hook_info + + def _add_unicorn_hook( + self, + hook_type, + callback, + name=None, + start_addr=None, + end_addr=None, + end_condition=None, + ) -> HookInfo: + """ + A cross process hook must accept a process as the first + argument, followed by the arguments expected by a unicorn hook + of the given hook_type. + """ + handle = self._hook_index + self._hook_index += 1 + if end_condition is None: + wrapped_callback = callback + else: + name = f"{name}_{start_addr}" + + def hook_end_wrapper(*args): + try: + callback(*args) + if end_condition(): + self._delete_unicorn_hook(handle) + except Exception: + self.logger.exception( + "Hook %s failed to execute. Deleting now", name + ) + self._delete_unicorn_hook(handle) + + wrapped_callback = hook_end_wrapper + + if hasattr(self.z, "processes"): + for p in self.z.processes.process_list: + p.hooks.add_hook( + hook_type, + wrapped_callback, + handle, + name, + start_addr=start_addr, + end_addr=end_addr, + ) + + self._cross_process_hooks[name] = HookInfo( + hook_type, + wrapped_callback, + handle, + name, + start_addr, + end_addr, + end_condition, + ) + + return self._cross_process_hooks[name] + + def _get_hooks(self, hook_type): + return self._hooks[hook_type].values() + + +class Hooks: + """ Keeps track of the hooks that are in action.""" + + def __init__(self, emu, threads): + self.emu = emu + self.threads = threads + self.logger = logging.getLogger(__name__) + + # Used for hooks that will be active until a user deactivates. + self._hook_dict = {} + self.unnamed_hook_index = 1 + self.emu.hook_add( + uc.UC_HOOK_MEM_READ_UNMAPPED | uc.UC_HOOK_MEM_WRITE_UNMAPPED, + self.hook_mem_invalid, + ) + + # List of closures to run to delete hooks + self._cleanup_closures = [] + + # callback for tracing invalid memory access (READ or WRITE) + + def hook_mem_invalid(self, uc, access, address, size, value, user_data): + eip = self.emu.getIP() + print( + "Missing memory at 0x%x, IP = 0x%x data size = %u, " + "data value = 0x%x" % (address, eip, size, value) + ) + return True # Stop executing + + def add_hook( + self, + zelos_hook_type, + callback, + handle, + name=None, + start_addr=None, + end_addr=None, + ) -> None: + """ + Adds a hook to unicorn. Depending on the hook type, the callback + is triggered at different moments, such as on ever instruction + or every basic block. In addition, if you specify an address + region, the hook will only run on those addresses. Restricting + the addresses that a hook can trigger can result in considerable + speedups. + """ + if isinstance(zelos_hook_type, HookType._INST): + unicorn_hook_type = uc.UC_HOOK_INSN + arg1 = _zelos_hook_to_unicorn(zelos_hook_type) + else: + unicorn_hook_type = _zelos_hook_to_unicorn(zelos_hook_type) + arg1 = 0 + + try: + if start_addr is not None: + unicorn_handle = self.emu.hook_add( + unicorn_hook_type, + callback, + begin=start_addr, + end=end_addr, + arg1=arg1, + ) + else: + unicorn_handle = self.emu.hook_add( + unicorn_hook_type, callback, arg1=arg1 + ) + + except uc.UcError: + raise ZelosRuntimeException( + f"Issue adding hook {name}, " + f"type {zelos_hook_type}, arg1 {arg1}" + ) + + self._hook_dict[handle] = unicorn_handle + + def _delete_unicorn_hook(self, zelos_handle): + unicorn_handle = self._hook_dict[zelos_handle] + if self.emu.is_running: + + def cleanup(): + self.emu.hook_del(unicorn_handle) + + self.threads.scheduler.stop_and_exec("cleanup hooks", cleanup) + else: + self.emu.hook_del(unicorn_handle) + + def del_hook(self, name): + if name not in self._hook_dict: + self.logger.notice("No hook with name %s" % name) + return + handle = self._hook_dict.pop(name) + self._delete_unicorn_hook(handle) + + def print_active_hooks(self): + print("Permanent Hooks:") + for name, handle in self._hook_dict.items(): + print(" {0}: {1}".format(name, handle)) + + def _save_state(self): + return self._hook_dict + + def _load_state(self, data): + self._hook_dict = data + + +class InterruptHooks: + """ + Manages hooks that handle interrupts emitted by the cpu emulator + """ + + def __init__(self, hook_manager, z): + self.logger = logging.getLogger(__name__) + self.hook_manager = hook_manager + self._z = z + + # CPUID interrupt is 0xf0f0f0f0 (TODO) + self.interrupt_handlers = {} + self.generic_interrupt_handlers = [] + self.unhandled_interrupt_handlers = [] + + self._interrupt_handler_hook_info = None + self.enable() + + def __str__(self): + s = "Registered Interrupt Handlers:\n" + s += "\n".join( + [f" 0x{k:x}: {v}" for k, v in self.interrupt_handlers.items()] + ) + return s + + def enable(self) -> None: + """Enables hooks for cpu interrupts across all processes.""" + + def interrupt_hook_wrapper(uc, intno, userdata): + self._hook_interrupt(self._z.api, intno) + + hook_info = self.hook_manager._add_unicorn_hook( + HookType._OTHER.INTERRUPT, + interrupt_hook_wrapper, + name="interrupt_hook", + ) + self._interrupt_handler_hook_info = hook_info + + def disable(self) -> None: + """Disable hooks for cpu interrupts across all processes.""" + if self._interrupt_handler_hook_info is not None: + self.hook_manager.delete_hook(self._interrupt_handler_hook_info) + self._interrupt_handler_hook_info = None + + def register_interrupt_handler(self, interrupt_number, handler): + self.interrupt_handlers[interrupt_number] = handler + + def register_generic_interrupt_handler(self, handler): + self.generic_interrupt_handlers.append(handler) + + def register_unhandled_interrupt_handler(self, handler): + self.unhandled_interrupt_handlers.append(handler) + + def _hook_interrupt(self, zelos, intno): + if zelos.thread is None: + zelos.internal_engine.scheduler.stop_and_exec( + "interrupt_null_thread", lambda: True + ) + return + + address = zelos.regs.getIP() + + self.logger.spam( + f"Got interrupt {intno:x} at 0x{address:x} " + f"on thread {zelos.thread.name}" + ) + + handler = self.interrupt_handlers.get(intno, None) + + interrupt_handled = False + if handler is not None: + handler(zelos.process) + interrupt_handled = True + for handler in self.generic_interrupt_handlers: + if handler(intno, zelos.process): + interrupt_handled = True + if not interrupt_handled: + for handler in self.unhandled_interrupt_handlers: + handler(intno, zelos.process) + + +class ExceptionHooks: + def __init__(self, z): + self.z = z + self.logger = logging.getLogger(__name__) + self.handler = None + + def handle_exception(self, e): + if self.handler is None: + self.logger.notice("No exception handler registered") + self.z.scheduler.stop(f"Unhandled exception") + return + self.logger.debug( + f"Invoking Exception Handler: {e} " + f"EIP = 0x{self.z.current_thread.getIP():x}" + ) + self.handler(self.z.current_process, e) + + def register_exception_handler(self, callback): + self.handler = callback diff --git a/src/zelos/manager.py b/src/zelos/manager.py new file mode 100644 index 0000000..eec890a --- /dev/null +++ b/src/zelos/manager.py @@ -0,0 +1,45 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import logging + + +class IManager: + def __init__(self, helpers): + self._processes = helpers.processes + self.triggers = helpers.triggers + self.handles = helpers.handles + self.state = helpers.state + self.logger = logging.getLogger(__name__) + + def get_current_thread(self): + return self._processes.current_thread + + @property + def emu(self): + return self._processes.current_process.emu + + @property + def scheduler(self): + return self._processes.current_process.scheduler + + @property + def hooks(self): + return self._processes.current_process.hooks + + @property + def memory(self): + return self._processes.current_process.memory diff --git a/src/zelos/memory.py b/src/zelos/memory.py new file mode 100644 index 0000000..90ebcab --- /dev/null +++ b/src/zelos/memory.py @@ -0,0 +1,1335 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +from __future__ import absolute_import, print_function + +import ctypes +import logging + +from collections import defaultdict +from string import printable +from typing import List, Optional + +from sortedcontainers import SortedDict, SortedListWithKey + +import zelos.util as util + +from zelos.enums import ProtType +from zelos.exceptions import OutOfMemoryException + + +class Section: + """ + Represents a region of memory that has been mapped. + """ + + def __init__( + self, + emu, + address, + size, + name, + kind, + module_name, + reserve=False, + ptr=None, + ): + self.emu = emu + self.address = address + self.size = size + self.name = name + self.kind = kind + self.module_name = module_name + self.reserved = reserve + + # If the ptr is set, this means the section is ptr mapped. + # These sections should be shared across processes + self.ptr = ptr + + def __str__(self): + s = f"0x{self.address:08x}-0x{self.address+self.size:08x}: " + s += f"{self.module_name} {self.name}, {self.kind}" + if self.ptr is not None: + s += " (p)" + return s + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def get_data(self) -> bytearray: + """ + Returns all data in the region. + + Returns: + Data from the region. + """ + return self.emu.mem_read(self.address, self.size) + + def entropy(self) -> float: + """ + Calculates the entropy of data contained within the section. + + Returns: + Entropy of this section. + """ + data = self.get_data() + import numpy as np + from scipy.stats import entropy + + value, counts = np.unique(data, return_counts=True) + return entropy(counts) + + def get_strings(self, min_len: int = 5) -> List[str]: + """ + Returns all strings found in the region's memory. + + Args: + min_len: The minimum length a string must be to be included + in the output. + + Returns: + List of strings found within this section's data. + + """ + strings = [] + string_so_far = "" + data = self.get_data() + for c in data: + if chr(c) in printable: + string_so_far += chr(c) + else: + if len(string_so_far) >= min_len: + strings.append(string_so_far) + string_so_far = "" + + # Also look for wide strings + string_so_far = "" + i = 0 + while i + 1 < len(data): + c = chr(data[i]) + if c in printable and data[i + 1] == 0: + string_so_far += c + i += 2 + else: + if len(string_so_far) >= min_len: + strings.append(string_so_far) + string_so_far = "" + i += 1 + + return strings + + +class Memory: + """ + Responsbile for interactions with emulated memory. + """ + + # Users of memory class should use memory's protection definitions + # defined here instead of unicorn constants directly. + + HEAP_BASE = 0x90000000 + HEAP_MAX_SIZE = 100 * 1024 * 1024 + + # A separate heap base for virtual allocations + VALLOC_BASE = 0x00C50000 + + MAX_UINT64 = 0xFFFFFFFFFFFFFFFF + + def __init__( + self, + emu, + state, + lowest_addr: int = 0, + max_addr: int = MAX_UINT64, + disableNX: bool = False, + ) -> None: + self.emu = emu + self.state = state + self.logger = logging.getLogger(__name__) + + self.max_addr = max_addr + self.disableNX = disableNX + + from unicorn import UC_HOOK_MEM_READ_PROT + + self.emu.hook_add(UC_HOOK_MEM_READ_PROT, self._hook_read_prot) + self.mem_hooks = dict() + + # Prevent runaway allocation + self.MEM_LIMIT = 3 * 1024 * 1024 * 1024 + + self._setup() + + self.heap = Heap(self, self.emu, self.HEAP_BASE, self.HEAP_MAX_SIZE) + + def _setup(self): + """Sets up variables after init or after memory is cleared.""" + self.memory_info = SortedDict() + self.memory_info[0] = Section( + self.emu, 0x0, 0x1000, "reserved", "zelos", "", False + ) + + self.num_bytes_mapped = 0 + + # Keep track of memory changes that have been made. + self.initial_memory_state = {} + + # Current virual allocation location + self.VALLOC_CUR = self.VALLOC_BASE + + def __str__(self): + s = "Memory Manager:\n" + for info in self.memory_info.values(): + s += " {0}\n".format(info) + return s + + def copy(self, other_memory: "Memory") -> None: + """ + Creates the same state in other_memory. + + Args: + other_memory: Memory that will contain a copy of self. + """ + for (start, end, prot) in self.emu.mem_regions(): + self.logger.spam(f"Clearing {start:x}-{end:x}") + size = end + 1 - start + self.emu.mem_unmap(start, size) + + self.heap._clear() + + self._setup() + + # Copy the Memory metadata + for section in other_memory.memory_info.values(): + self.copy_section(section, other_memory) + + # other tidbits + self.max_addr = other_memory.max_addr + self.disableNX = other_memory.disableNX + self.num_bytes_mapped = other_memory.num_bytes_mapped + self.heap = other_memory.heap + + def clear(self) -> None: + """ + Clears all of memory + """ + for (start, end, prot) in self.emu.mem_regions(): + self.logger.spam(f"Clearing {start:x}-{end:x}") + size = end + 1 - start + self.emu.mem_unmap(start, size) + + self.heap._clear() + + self._setup() + + self.heap = Heap(self, self.emu, self.HEAP_BASE, self.HEAP_MAX_SIZE) + + def get_sections(self) -> List[Section]: + """ + Gets meaningful sections from Zelos in memory. Reserved sections + are not guaranteed to be written into Zelos, so memory + operations on these addresses may not be meaningful. + + Returns: + Sections present in memory. + """ + return [ + meminfo + for meminfo in self.memory_info.values() + if meminfo.name not in ["reserved"] + ] + + def record_initial_memory_state(self) -> None: + """ + We record the initial memory state in order to use it to tell + what memory has changed. + """ + if len(self.initial_memory_state) > 0: + return + for section in self.get_sections(): + self.initial_memory_state[section.address] = ( + section, + section.get_data(), + ) + + # Helper functions for reading and writing memory. All generic + # helpers for memory should go here moving forward + + def read(self, addr: int, size: int) -> bytearray: + """ + Copies specified region of memory. Requires that the specified + address is mapped. + + Args: + addr: Address to start reading from. + size: Number of bytes to read. + + Returns: + Bytes corresponding to data held in memory. + """ + return self.emu.mem_read(addr, size) + + def write(self, addr: int, data: bytes) -> None: + """ + Writes specified bytes to memory. Requires that the specified + address is mapped. + + Args: + addr: Address to start writing data to. + data: Bytes to write in memory. + """ + self.emu.mem_write(addr, data) + + def read_int(self, addr: int, sz: int = None, signed: bool = False) -> int: + """ + Reads an integer value from the specified address. Can handle + multiple sizes and representations of integers. + + Args: + addr: Address to begin reading int from. + sz: Size (# of bytes) of integer representation. + signed: If true, interpret bytes as signed integer. Default + false. + + Returns: + Integer represntation of bytes read. + """ + sz = self.state.bytes if sz is None else sz + value = self.emu.mem_read(addr, sz) + return self.emu.unpack(value, bytes=sz, signed=signed) + + def write_int( + self, addr: int, value: int, sz: int = None, signed: bool = False + ) -> int: + """ + Writes an integer value to the specified address. Can handle + multiple sizes and representations of integers. + + Args: + addr: Address in memory to write integer to. + value: Integer to write into memory. + sz: Size (# of bytes) to write into memory. + signed: If true, write number as signed integer. Default + false. + + Returns: + Number of bytes written to memory. + """ + packed = self.emu.pack(value, bytes=sz, signed=signed) + self.emu.mem_write(addr, packed) + return len(packed) + + def read_string(self, addr: int, size: int = 1024) -> str: + """ + Reads a utf-8 string from memory. Stops at null terminator. + Fails if a byte is uninterpretable. + + Args: + addr: Address in memory to start reading from. + size: Maximum size of string to read from memory. + + Returns: + String read from memory. + """ + if addr == 0: + return "" + data = b"" + try: + for i in range(size): + byte = bytes(self.emu.mem_read(addr + i, 1)) + if byte == b"\x00": + break + data += byte + # TODO: Allow for different decodings. + return data.decode() + except Exception as e: + # TODO: We need to differentiate between attempts to check + # if a string exists or if expecting a string to exist. We + # shouldn't log an error if we aren't expecting the string + # to exist. + self.logger.debug( + "Couldn't read str at 0x{0:x}: {1}".format(addr + i, e) + ) + return "" + + def read_wstring(self, addr: int, size: int = 1024) -> str: + """ + Reads a utf-16 string from memory. Stops at null terminator. + Fails if a byte is uninterpretable. + + Args: + addr: Address in memory to start reading from. + size: Maximum size of string to read from memory. + + Returns: + String read from memory. + """ + if addr == 0: + return "" + data = b"" + try: + for i in range(0, size, 2): + chars = self.emu.mem_read(addr + i, 2) + if chars == b"\x00\x00": + break + data += chars + return data.decode("utf-16") + except Exception: + print("Couldn't read wstr at 0x{0:x}".format(addr + i)) + return "" + + def get_punicode_string(self, addr: int) -> str: + # punicode + # length ushort + # maxlength ushort + # wstring pvoid + try: + length = self.read_uint16(addr) + string_pointer = self.read_ptr(addr + 4) + if length == 0: + value = self.read_wstring(string_pointer) + else: + value = self.read_wstring(string_pointer, length) + return value + except Exception as e: + print("Could not read string @", hex(addr), ":", e) + return "" + + def get_pansi_string(self, addr: int) -> int: + # punicode + # length ushort + # maxlength ushort + # string pvoid + try: + length = self.read_uint16(addr) + string_pointer = self.read_ptr(addr + 4) + if length == 0: + value = self.read_string(string_pointer) + else: + value = self.read_string(string_pointer, length) + return value + except Exception as e: + print("Could not read string @", hex(addr), ":", e) + return "" + + def write_string( + self, addr: int, value: str, terminal_null_byte: bool = True + ) -> int: + """ + Writes a string to a specified address as utf-8. By default, + adds a terminal null byte. + + Args: + addr: Address in memory to begin writing string to. + value: String to write to memory. + terminal_null_byte: If True, adds terminal null byte. + Default True. + + Returns: + Number of bytes written. + """ + byte_value = value.encode() + if terminal_null_byte: + byte_value += b"\x00" + if addr != 0: + self.emu.mem_write(addr, byte_value) + return len(byte_value) + + def write_wstring( + self, addr: int, value: int, terminal_null_byte: bool = True + ) -> int: + """ + Writes a string to a specified address as utf-16-le. By default, + adds a terminal null byte. + + Args: + addr: Address in memory to begin writing string to. + value: String to write to memory. + terminal_null_byte: If True, adds terminal null byte. + Default True. + + Returns: + Number of bytes written. + """ + byte_value = value.encode("utf-16-le") + if terminal_null_byte: + byte_value += b"\x00\x00" + self.emu.mem_write(addr, byte_value) + return len(byte_value) + + # @@TODO handle MBCS / WCHAR nonsense + # for more information this is a good read: + # https://utf8everywhere.org/ + + def readstruct(self, addr: int, obj: ctypes.Structure) -> ctypes.Structure: + """ + Reads a ctypes structure from memory. + + Args: + addr: Address in memory to begin reading structure from. + obj: An instance of the structure to create from memory. + + Returns: + Instance of structure read from memory. + """ + data = self.emu.mem_read(addr, ctypes.sizeof(obj)) + util.str2struct(obj, data) + return obj + + def readstructarray( + self, addr: int, count: int, obj: ctypes.Structure + ) -> List[ctypes.Structure]: + """ + Read an array of ctypes structure from memory. + + Args: + addr: Address in memory to begin reading structure from. + count: number of instances of the object to read. + obj: An instance of the structure to create from memory. + + Returns: + List of structures read from memory. + """ + results = [] + for i in range(count): + struct = self.readstruct(addr, obj) + results.append(struct) + addr += ctypes.sizeof(struct) + return results + + def writestruct(self, address: int, structure: ctypes.Structure) -> int: + """ + Write a ctypes Structure to memory. + + Args: + addr: Address in memory to begin writing to. + structure: An instance of the structure to write to memory. + + Returns: + Number of bytes written to memory. + """ + data = util.struct2str(structure) + self.emu.mem_write(address, data) + return len(data) + + def dumpstruct( + self, structure: ctypes.Structure, indent_level: int = 0 + ) -> None: + """ + Prints a string representing the data held in a struct. + + Args: + structure: The structure to print out. + indent_level: Number of indents when printing output. Makes + for easier reading. Defaults to no indentation. + """ + util.dumpstruct(structure, indent_level=indent_level) + + def map_anywhere( + self, + size: int, + name: str = "", + kind: str = "", + min_addr: int = 0, + max_addr: int = 0xFFFFFFFFFFFFFFFF, + alignment: int = 0x1000, + prot: int = ProtType.RWX, + ) -> int: + """ + Maps a region of memory with requested size, within the + addresses specified. The size and start address will respect the + alignment. + + Args: + size: # of bytes to map. This will be rounded up to match + the alignment. + name: String used to identify mapped region. Used for + debugging. + kind: String used to identify the purpose of the mapped + region. Used for debugging. + min_addr: The lowest address that could be mapped. + max_addr: The highest address that could be mapped. + alignment: Ensures the size and start address are multiples + of this. Must be a multiple of 0x1000. Default 0x1000. + prot: RWX permissions of the mapped region. Defaults to + granting all permissions. + Returns: + Start address of mapped region. + """ + address = self._find_free_space( + size, min_addr=min_addr, max_addr=max_addr, alignment=alignment + ) + self.map(address, util.align(size), name, kind) + return address + + def map( + self, + address: int, + size: int, + name: str = "", + kind: str = "", + module_name: str = "", + prot: int = ProtType.RWX, + ptr: Optional[ctypes.POINTER] = None, + reserve: bool = False, + ) -> None: + """ + Maps a region of memory at the specified address. + + Args: + address: Address to map. + size: # of bytes to map. This will be rounded up to the + nearest 0x1000. + name: String used to identify mapped region. Used for + debugging. + kind: String used to identify the purpose of the mapped + region. Used for debugging. + module_name: String used to identify the module that mapped + this region. + prot: An integer representing the RWX protection to be set + on the mapped region. + ptr: If specified, creates a memory map from the pointer. + reserve: Reserves memory to prepare for mapping. An option + used in Windows. + + """ + if self.disableNX: + prot = prot | ProtType.EXEC + self.logger.debug( + f"Mapping region " + f"0x{address:x} of size 0x{size:x} ({name}, {kind})" + ) + self.num_bytes_mapped += size + if self.num_bytes_mapped > self.MEM_LIMIT: + self.logger.critical("OUT OF MEMORY") + raise OutOfMemoryException + + if ptr is None: + self.emu.mem_map(address, size) + if prot != ProtType.RWX: + self.protect(address, size, prot) + else: + self.logger.debug( + f"mapping " + f"{address:x}, size: {size:x}, prot {prot:x}, ptr: {ptr}" + ) + self.emu.mem_map_ptr(address, size, prot, ptr) + self._new_section( + address, size, name, kind, module_name, reserve=reserve, ptr=ptr + ) + + def copy_section(self, section: Section, other_memory: "Memory") -> None: + """ + Copies a section from this instance of memory into another + instance of memory. + + Args: + section: The section to copy. Must correspond to a section + within this memory object. + other_memory: An instance of memory to copy the specified + section to. + """ + start = section.address + size = section.size + end = start + size + + # We have the beginning mapped for special addresses + if start == 0: + return + + # Some sections are added to differentiate different sections in + # the binary. These are typically not aligned. If they are, + # should only be an extra copy. + if start != util.align(start) or end != util.align(end): + return + + self.logger.spam(f"Copying {start:x}-{end:x}") + + if section.ptr is None: + data = other_memory.read(start, size) + self.map(start, size) + self.write(start, bytes(data)) + else: + self.map(start, size, ptr=section.ptr) + self._new_section( + section.address, + section.size, + name=section.name, + kind=section.kind, + module_name=section.module_name, + ptr=section.ptr, + ) + + def protect(self, address: int, size: int, prot: int) -> None: + """ + Sets memory permissions on the specified memory region. Respects + alignment of 0x1000. + + Args: + address: Address of memory region to modify permissions. + Rounds down to nearest 0x1000. + size: Size of region to protect. Rounds up to nearest + 0x1000. + prot: Desired RWX permissions. + + TODO: + This does not correspond to Sections at the moment. + + """ + if self.disableNX: + prot = prot | ProtType.EXEC + aligned_address = address & 0xFFFFF000 # Address needs to align with + aligned_size = util.align((address & 0xFFF) + size) + try: + self.emu.mem_protect(aligned_address, aligned_size, prot) + self.logger.debug( + "Protected region 0x%x + 0x%x, Prot: %x", + aligned_address, + aligned_size, + prot, + ) + except Exception as e: + self.logger.error( + f"Error trying to protect region " + f"0x{aligned_address:x} + 0x{aligned_size:x}, " + f"Prot: {prot:x}: {e}" + ) + + def unmap(self, address, size) -> None: + """ + Unmaps a memory region, allowing it to be mapped again. + + Args: + address: Address of section to be unmapped. + size: Number of bytes to unmap. + + TODO: + This currently only unmaps the section at the specified + address. If size encompasses multiple sections, only the + first will be unmapped. + Also, unmaps that split up sections need work to maintain + a correct representation in the Sections. + + """ + if address in self.memory_info.keys(): + section_size = self.memory_info[address].size + if size != section_size: + self.logger.info( + "Deleting section, though size is not the same " + "(was %x, requested %x)", + section_size, + size, + ) + del self.memory_info[address] + + self.emu.mem_unmap(address, size) + else: + self.logger.info("Attempting to unmap part of alloc section") + self.emu.mem_unmap(address, size) + + def get_base(self, address: int) -> Optional[int]: + """ + Returns the base address of the memory region that contains this + address. + + Returns: + The base address of the containing region, or None if + address is not contained within any region. + + TODO: + This function should operate on the Section object. + Also, clarity regions split by unmap is needed. + """ + regions = self.emu.mem_regions() + for region in regions: + addr = region[0] + size = region[1] - addr + 1 + if address >= addr and address < addr + size: + return addr + return None + + def get_perms(self, address: int) -> int: + """ + Returns the permissions of the section containg the given + address. + + Args: + address: Used to pick the containing section. + + Returns: + Permissions of the containing section. + """ + regions = self.emu.mem_regions() + for region in regions: + addr = region[0] + size = region[1] - addr + 1 + perm = region[2] + if address >= addr and address < addr + size: + return perm + return None + + def get_size(self, address: int) -> int: + """ + Returns the size of the section containg the given address. + + Args: + address: Used to pick the containing section. + + Returns: + Size of the containing section. + """ + regions = self.emu.mem_regions() + for region in regions: + addr = region[0] + size = region[1] - addr + 1 + if address >= addr and address < addr + size: + return size + return None + + def hook_first_read(self, region_addr, hook): + region = self.get_region(region_addr) + size = self.get_size(region_addr) + perms = self.get_perms(region_addr) + if region is None or size is None or perms is None: + return False + addr = region.address + + try: + self.emu.mem_protect(addr, util.align(size), ProtType.NONE) + except Exception: + self.logger.exception( + "Error trying to protect portion 0x%x + 0x%x, Prot: %x", + addr, + util.align(size), + ProtType.NONE, + ) + self.mem_hooks[addr] = _MemHook(addr, size, perms, hook) + return True + + def _hook_read_prot(self, uc, access, address, size, value, user_data): + region = self.get_region(address) + addr = region.address if region is not None else None + if addr not in self.mem_hooks: + return False + mem_hook = self.mem_hooks[addr] + self.emu.mem_protect( + mem_hook.addr, util.align(mem_hook.size), mem_hook.orig_perms + ) + del self.mem_hooks[addr] + return mem_hook.hook(uc, access, address, size, value, user_data) + + def get_region(self, address): + """ Gets the region that this address belongs to.""" + for section in self.memory_info.values(): + if section.address <= address < section.address + section.size: + return section + return None + + def get_initial_region(self, address): + """ + Returns the initial region that contained this address, along + with the memory at that time. + """ + for (section, mem) in self.initial_memory_state.values(): + if section.address <= address < section.address + section.size: + return (section, mem) + return (None, None) + + def get_region_hash(self): + """ Used for determining whether a memory region has changed""" + hashes = {} + for region in self.memory_info.values(): + try: + hashes[region.address] = region.get_data() + except Exception: + pass + return hashes + + def read_ptr(self, addr: int) -> int: + return self.read_int(addr) + + def read_size_t(self, addr: int) -> int: + return self.read_int(addr) + + def read_int64(self, addr: int) -> int: + return self.read_int(addr, sz=8, signed=True) + + def read_uint64(self, addr: int) -> int: + return self.read_int(addr, sz=8, signed=False) + + def read_int32(self, addr: int) -> int: + return self.read_int(addr, sz=4, signed=True) + + def read_uint32(self, addr: int) -> int: + return self.read_int(addr, sz=4, signed=False) + + def read_int16(self, addr: int) -> int: + return self.read_int(addr, sz=2, signed=True) + + def read_uint16(self, addr: int) -> int: + return self.read_int(addr, sz=2, signed=False) + + def read_int8(self, addr: int) -> int: + return self.read_int(addr, sz=1, signed=True) + + def read_uint8(self, addr: int) -> int: + return self.read_int(addr, sz=1, signed=False) + + def write_ptr(self, addr: int, value: int) -> int: + return self.write_int(addr, value) + + def write_size_t(self, addr: int, value: int) -> int: + return self.write_int(addr, value) + + def write_int64(self, addr: int, value: int) -> int: + return self.write_int(addr, value, sz=8, signed=True) + + def write_uint64(self, addr: int, value: int) -> int: + return self.write_int(addr, value, sz=8, signed=False) + + def write_int32(self, addr: int, value: int) -> int: + return self.write_int(addr, value, sz=4, signed=True) + + def write_uint32(self, addr: int, value: int) -> int: + return self.write_int(addr, value, sz=4, signed=False) + + def write_int16(self, addr: int, value: int) -> int: + return self.write_int(addr, value, sz=2, signed=True) + + def write_uint16(self, addr: int, value: int) -> int: + return self.write_int(addr, value, sz=2, signed=False) + + def write_int8(self, addr: int, value: int) -> int: + return self.write_int(addr, value, sz=1, signed=True) + + def write_uint8(self, addr: int, value: int) -> int: + return self.write_int(addr, value, sz=1, signed=False) + + def pack( + self, + x: int, + bytes: int = None, + little_endian: bool = None, + signed: bool = False, + ) -> bytes: + """ + Unpacks an integer from a byte format. Defaults to the + current architecture bytes and endianness. + """ + return self.emu.pack( + x, bytes=bytes, little_endian=little_endian, signed=signed + ) + + def unpack( + self, + x: bytes, + bytes: int = None, + little_endian: bool = None, + signed: bool = False, + ) -> int: + """ + Unpacks an integer from a byte format. Defaults to the + current architecture bytes and endianness. + """ + return self.emu.unpack( + x, bytes=bytes, little_endian=little_endian, signed=signed + ) + + def _new_section( + self, + address: int, + size: int, + name: str = "", + kind: str = "", + module_name: str = "", + ptr: Optional[ctypes.POINTER] = None, + reserve: bool = False, + ): + if size == 0: + self.logger.notice("Will not insert region of size 0") + return + self.memory_info[address] = Section( + self.emu, + address, + size, + name, + kind, + module_name, + ptr=ptr, + reserve=reserve, + ) + + def _has_overlap(self, requested_addr, size): + """ + Checks to see whether a region at the requested address of the + given size would overlap with an existing mapped region. + """ + requested_end = requested_addr + size + for region in self.emu.mem_regions(): + region_begin = region[0] + region_end = region[1] + if requested_addr <= region_end and requested_end >= region_begin: + return True + return False + + def _get_next_gap(self, size, start, end): + """ + Returns the start address of the next region between start and + end that allows for memory of the given size to be mapped. + """ + min_addr_so_far = start + for region in sorted(list(self.emu.mem_regions()), key=lambda x: x[0]): + region_begin = region[0] + region_end = region[1] + if region_begin >= end or region_end < start: + continue + + gap = region_begin - min_addr_so_far + if gap < size: + min_addr_so_far = util.align(region_end) + continue + return min_addr_so_far + # Check to see there is a gap after the last region + gap = end - min_addr_so_far + if gap < size: + self.logger.error( + "No gap of size %x between %x and %x" % (size, start, end) + ) + return min_addr_so_far + + # Allocate a chunk of memory at the requested address. If the + # requested address is not available, find the first available chunk + # of memory greater or equal to min_base. Returns the address of the + # allocated chunk, or zero on failure. + # This method was added after map_anywhere, due to legacy reasons. + # Look into combining the two functionalities + + def _alloc_at( + self, + name, + kind, + module_name, + requested_addr, + size, + min_addr=0x60000000, + max_addr=0x90000000, + prot=ProtType.RWX, + ptr=None, + ): + # if requested_addr < min_addr: + # requested_addr = min_addr + if requested_addr > max_addr: + requested_addr = min_addr + size = util.align(size) + relocated_addr = 0 + if self._has_overlap(requested_addr, size): + relocated_addr = self._get_next_gap(size, min_addr, max_addr) + + self.logger.debug( + "[Loader] Relocating Overlapping Region from " + "0x{0:08x} to 0x{1:08x}".format(requested_addr, relocated_addr) + ) + try: + self.map( + relocated_addr, + size, + name, + kind, + module_name=module_name, + prot=prot, + ptr=ptr, + ) + return relocated_addr + except Exception: + self.logger.exception("Couldn't relocate properly") + exit() + else: + self.map( + requested_addr, + size, + name, + kind, + module_name=module_name, + prot=prot, + ptr=ptr, + ) + return requested_addr + + def _is_free(self, address): + """ + Returns whether a specified addrss is free or already part of an + allocated region. + """ + for section in self.memory_info.values(): + if ( + address >= section.address + and address < section.address + section.size + ): + return False + for region in list(self.emu.mem_regions()): + if address >= region[0] and address < region[1]: + return False + return True + + def _find_free_space( + self, size, min_addr=0, max_addr=MAX_UINT64, alignment=0x10000 + ): + """ + Finds a region of memory that is free, larger than 'size' arg, + and aligned. + """ + sections = list(self.memory_info.values()) + for i in range(0, len(sections)): + addr = util.align( + sections[i].address + sections[i].size, alignment=alignment + ) + # Enable allocating memory in the middle of a gap when the + # min requested address falls in the middle of a gap + if addr < min_addr: + addr = min_addr + # Cap the gap's max address by accounting for the next + # section's start address, requested max address, and the + # max possible address + max_gap_addr = ( + self.max_addr + if i == len(sections) - 1 + else sections[i + 1].address + ) + max_gap_addr = min(max_gap_addr, max_addr) + # Ensure the end address is less than the max and the start + # address is free + if addr + size < max_gap_addr and self._is_free(addr): + return addr + raise OutOfMemoryException() + + def _save_state(self): + data = [] + for address, meminfo in self.memory_info.items(): + if meminfo.kind not in ["main", "mmap", "stack", "section"]: + continue + mem = self.emu.mem_read(address, meminfo.size) + data.append((meminfo, mem)) + return data + + def _load_state(self, data): + for meminfo, mem in data: + self.logger.debug( + "Loading: ", hex(meminfo.address), hex(meminfo.size), meminfo + ) + mem = bytes(mem) + try: + self.emu.mem_write(meminfo.address, mem) + except Exception: + self.map( + meminfo.address, + util.align(meminfo.size), + meminfo.name, + meminfo.kind, + ) + self.emu.mem_write(meminfo.address, mem) + + +class _HeapObjInfo: + """ Information on a heap object. """ + + def __init__( + self, + address, + size, + current_thread_name="", + call_stack=None, + name="unnamed", + ): + self.address = address + self.size = size + self.name = name + # TODO(kvalakuzhy): More work needs to be done if we want to + # have a robust call_stack implementation. + self.call_stack = [] if call_stack is None else list(call_stack) + self.accesses = defaultdict(dict) + self.parent_thread = current_thread_name + + def call_stack_string(self): + s = "" + if len(self.call_stack) == 0: + return s + + def func_name(call): + max_name_size = 25 + if len(call.name) <= max_name_size: + return "(" + call.name + ")" + return "(" + call.name[:max_name_size] + "...)" + + s += " <- ".join( + [func_name(call) for call in reversed(self.call_stack[-5:])] + ) + if len(self.call_stack) > 5: + s += " <- ..." + return s + + def created_in_func(self): + if len(self.call_stack) == 0: + return None + return self.call_stack[-1] + + def add_access(self, eip, access, address, size): + self.accesses[address - self.address][eip] = (access, size) + + def is_accessed_at(self, eip): + for accesses in self.accesses.values(): + if eip in accesses: + return True + return False + + def has_overflow(self): + for offset, accesses in self.accesses.items(): + for access, size in accesses.values(): + if offset + size > self.size: + return True + return False + + +class Heap: + """ Helper class to manage heap allocation.""" + + def __init__(self, memory, emu, heap_start, heap_max_size): + self.memory = memory + self.emu = emu + self.logger = logging.getLogger(__name__) + + self.heap_start = heap_start + self.current_offset = heap_start + self.heap_max_size = heap_max_size + self.heap_objects = SortedListWithKey(key=lambda x: x.address) + + self._setup() + + def _setup(self): + # Initialize default process heap + self.memory.map( + self.heap_start, + self.heap_max_size, + "main_heap", + "heap", + prot=ProtType.READ | ProtType.WRITE, + ) + + def _clear(self): + # This function does not need to clear its memory, since memory + # is in charge of that. It does need to clear it's tracking + # though. + self.current_offset = self.heap_start + self.heap_objects.clear() + + def dealloc(self, size: int) -> int: + """ + Returns memory from the heap. + + Args: + size: # of bytes to return from the heap. + + Returns: + Address of the new heap boundary. + + TODO: + Deallocs should be aligned as well. + """ + # TODO: Notify objects if they have been deallocated. + if self.current_offset - size < self.heap_start: + self.logger.notice( + ( + f"Failed to dealloc {size:x} from heap, " + "which would go beyond the heap start" + ) + ) + return self.current_offset + + self.logger.debug(f"Deallocating {size:x} from heap") + self.current_offset -= size + return self.current_offset + + def alloc(self, size: int, name: str = None, align: int = 0x4) -> int: + """ + Allocates memory to the heap. These are rounded up to the size + of the alignment. + + Args: + size: Number of bytes to allocate. + name: Used to keep track of what information was allocated. + Used for debugging + align: Ensures that the memory allocated is a multiple of + this value. Defaults to 4. + + Returns: + Address of the new heap boundary + """ + self.logger.debug(f"Allocating {size:x} bytes named {name}") + ret = self.current_offset + requested_size = util.align(size, alignment=align) + if ( + self.current_offset + requested_size + >= self.heap_start + self.heap_max_size + ): + self.logger.error( + "Ran out of heap memory . Try increasing max heap size." + ) + return ret + self.current_offset += requested_size + # TODO(kvalakuzhy): It would be nice if this could be moved into + # a heap tracking class. + self.heap_objects.add(_HeapObjInfo(ret, size, name=name)) + return ret + + def allocstr( + self, s, terminal_null_byte=True, is_wide=False, alloc_name="allocstr" + ): + """ + Allocates a string to the heap. These are rounded up to the size + of the alignment. + + Args: + size: Number of bytes to allocate. + name: Used to keep track of what information was allocated. + Used for debugging + align: Ensures that the memory allocated is a multiple of + this value. Defaults to 4. + + Returns: + Address of the new heap boundary + """ + out_string = "" + if is_wide: + for c in s: + out_string += c + "\x00" + if terminal_null_byte: + out_string += "\x00\x00" + else: + out_string = s + if terminal_null_byte: + out_string += "\x00" + + p_str = self.alloc(len(out_string), name=alloc_name) + self.emu.mem_write(p_str, out_string.encode()) + return p_str, len(out_string) + + +class _MemHook(object): + def __init__(self, addr, size, perms, hook): + self.addr = addr + self.size = size + self.orig_perms = perms + self.hook = hook diff --git a/src/zelos/modules.py b/src/zelos/modules.py new file mode 100644 index 0000000..2ae37c5 --- /dev/null +++ b/src/zelos/modules.py @@ -0,0 +1,85 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import logging + +from os.path import basename + + +# This class does not yet depend on any external Helpers. The Manager +# Superclass was intentionally not included here just to keep it clear +# that no dependency existed. If this this class ends up needing these +# things, feel free to put them in. + + +class Modules: + def __init__(self): + self.logger = logging.getLogger(__name__) + + # Map of name -> function implementation. + self.function_hooks = {} + # The set of currently loaded modules + self.modules = [] + # The set of currently base-hooked module functions + self.module_functions = {} + # Map of address -> import name + self.reverse_module_functions = {} + + def get_function_name(self, address): + return self.reverse_module_functions.get(address, None) + + def get_function_impl(self, function_name, use_function_hooks=True): + """ + Returns the function name and the hook if a corresponding one + exists. + """ + if use_function_hooks: + hook_struct = self.function_hooks.get(function_name, None) + if hook_struct is not None: + return hook_struct.hook + return None + + def get_module_base(self, module_name): + module_name = self._normalize_name(module_name) + for module in self.modules: + if module_name == module[0]: + return module[1] + return 0 + + def get_module_name_at_address(self, imagebase): + for module in self.modules: + if module[1] == imagebase: + return module[0] + return "" + + def is_loaded(self, modulename): + modulename = self._normalize_name(modulename) + for module in self.modules: + if modulename == module[0]: + return True + return False + + # Returns the normalized module name with path stripped/lowercased. + def _normalize_name(self, module_name): + module_name = basename(module_name) + module_name = module_name.lower() + return module_name + + def _save_state(self): + return "" + + def _load_state(self, data): + pass diff --git a/src/zelos/network/__init__.py b/src/zelos/network/__init__.py new file mode 100644 index 0000000..0662dc7 --- /dev/null +++ b/src/zelos/network/__init__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +from .network import Network + + +__all__ = ["Network"] diff --git a/src/zelos/network/base_socket.py b/src/zelos/network/base_socket.py new file mode 100644 index 0000000..532b81f --- /dev/null +++ b/src/zelos/network/base_socket.py @@ -0,0 +1,562 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import queue +import socket + +from collections import defaultdict + +import dnslib + +from pypacker.layer3 import ip +from pypacker.layer4 import tcp + +from zelos.handles import SocketHandle + + +class BaseSocket: + """ + This socket pretends that every connection succeeds. Every return + should succeed and return all zeros. + """ + + def __init__(self, network_manager, domain, sock_type, protocol): + """ + Initialize a socket of the specified type. Raises an exception + if the socket type is not supported. + + Args: + network_manager: A reference to the `Network` object. + domain: socket domain (as defined in python `socket`). + type: socket type (as defined in python `socket`). + protocol: socket protocol (as defined in python `socket`). + """ + if not hasattr(socket, "SOCK_CLOEXEC"): + # Windows support + socket.SOCK_CLOEXEC = 0x80000 + socket.SOCK_NONBLOCK = 0x800 + socket.AF_UNIX = 0x1 + + sock_type &= ~(socket.SOCK_CLOEXEC | socket.SOCK_NONBLOCK) + sock_type = socket.SocketKind(sock_type) + + self._errno = 0 + self.network = network_manager + self.domain = domain + self.type = sock_type + self.protocol = protocol + self.host_and_port = (None, None) + self._is_nonblock = True + + self.history = defaultdict(list) + self.sock = None + + if domain == socket.AF_UNIX: + raise Exception("[BaseSocket] AF_UNIX domain not supported.") + + if sock_type == socket.SOCK_RAW: + self.sock = RawSocketSimulator(self.domain) + + @property + def errno(self): + """ + The last error that occurred (see python `errno`) for any method + of this class that returns -1 or raises an exception. + """ + return self._errno + + def setsockopt(self, level, name, value): + """ + Sets socket options. + + Args: + level: option level (as defined in python `socket`) + name: option name (as defined in python `socket`) + value: option value (type depends on option name) + + Returns: + 0 on success, -1 on failure. + """ + return 0 + + def set_nonblock(self, is_nonblock: bool): + """ + Sets the socket blocking option. + + Args: + is_nonblock: if True, makes the socket non-blocking. + """ + self._is_nonblock = is_nonblock + + def is_nonblock(self): + """ + Gets the socket blocking status. + + Returns: + True if the socket is non_blocking, False otherwise. + """ + return self._is_nonblock + + def connect(self, host_and_port): + """ + Connects the socket. + + Args: + host_and_port: A tuple of the form (host: string, port: int) + + Returns: + 0 on success, -1 on failure. + """ + self.host_and_port = host_and_port + if self.sock is None and host_and_port[1] == 53: + self.sock = DnsSocketSimulator( + self.domain, host_and_port[0], host_and_port[1] + ) + self.history["connect"].append(host_and_port) + return 0 + + def close(self): + """ + Closes the socket. + """ + pass + + def bind(self, host_and_port): + """ + Binds the socket to a port. + + Args: + host_and_port: A tuple of the form (host: string, port: int) + + Returns: + 0 on success, -1 on failure. + """ + self.host_and_port = host_and_port + self.history["bind"].append(host_and_port) + return 0 + + def listen(self, backlog: int = 0): + """ + Socket listen. + + Returns: + 0 on success, -1 on failure. + """ + return 0 + + def accept(self): + """ + Accepts a new connection on the listening socket. + + Returns: + 0 on success, -1 on failure. + """ + return 0 + + def peek(self): + """ + Peek at readable data on the socket. + + Returns: + A byte array containing the observed data. + """ + return b"0" * 1 + + def send(self, data: bytes, flags: int = 0): + """ + Sends data over the socket. + + Args: + data: The byte array to send. + flags: socket send flags + + Returns: + The length of the data sent. + """ + host = self.host_and_port[0] + port = self.host_and_port[1] + + if self.sock is None and port == 53: + self.sock = DnsSocketSimulator(self.domain, host, port) + + if self.sock is not None: + return self.sock.send(data, flags) + + self.history["send"].append(data) + return len(data) + + def recv(self, bufsize: int, flags: int): + """ + Receive data from the socket. + + Args: + bufsize: maximum size of data to receive. + flags: socket recv flags + + Returns: + A byte array of the data received, or None. + """ + # If sock exists, use it's simulated receiver + if self.sock is not None: + return self.sock.recv(bufsize, flags) + return b"0" * bufsize + + def recvfrom(self, bufsize: int, flags: int = 0): + """ + Receive data from a non-streaming (i.e. non-TCP) protocol. + + Args: + bufsize: maximum size of data to receive. + flags: socket recv flags + + Returns: + The tuple (data received, domain, host, port). + """ + # If sock exists, use it's simulated receiver + if self.sock is not None: + return self.sock.recvfrom(bufsize, flags) + return ( + b"0" * bufsize, + self.domain, + self.host_and_port[0], + self.host_and_port[1], + ) + + def sendto(self, data: bytes, host_and_port, flags: int = 0): + """ + Send data over non-streaming (i.e. non-TCP) protocol. + If host and port are not None, send to that address. Otherwise, + send to the address given during socket connect. + + Args: + host_and_port: A tuple of the form (host: string, port: int) + flags: socket recv flags + + Returns: + The length sent. + """ + if self.host_and_port is not None and host_and_port[0] is not None: + self.host_and_port = host_and_port + port = self.host_and_port[1] + + if self.sock is None and port == 53: + self.sock = DnsSocketSimulator( + self.domain, host_and_port[0], host_and_port[1] + ) + + if self.sock is not None: + return self.sock.sendto(data, self.host_and_port, flags) + + self.history["sendto"].append([data, host_and_port]) + return len(data) + + +class DnsSocketSimulator: + """ + Simulate DNS requests and responses. + """ + + def __init__(self, domain, host=None, port=None): + self.hostname = "" + self.domain = domain + self.host_and_port = (host, port) + self.query = None + self.dns_id = None + + def send(self, payload, flags=0): + try: + domain = self._parse_dns_request(payload) + if domain is None: + raise Exception( + "[DnsSocketSimulator] Parse Failed: " + str(payload) + ) + else: + self.hostname = domain + print(f"[DnsSocketSimulator] DNS Query {self.hostname}") + except Exception as e: + print("DNS_INVALID:", e) + + return len(payload) + + def recv(self, bufsize, flags=0): + if self.dns_id is None: + return b"0" * bufsize + + id = self.dns_id + self.dns_id = None + reply = self._create_dns_response( + hostname=self.hostname, ip="127.0.0.1", id=id + ) + return reply + + def sendto(self, payload, host_and_port, flags=0): + self.host_and_port = host_and_port + return self.send(payload) + + def recvfrom(self, bufsize, flags=0): + host = self.host_and_port[0] + port = self.host_and_port[1] + result = (self.recv(bufsize), socket.AF_INET, host, port) + return result + + def is_readable(self): + return True + + def _parse_dns_request(self, payload): + """ + Parses a DNS packet. Additionally handles some bugs in Mirai's + DNS request packet generation. + """ + dns_id = int.from_bytes(payload[:2], byteorder="big") + self.dns_id = dns_id + domain = None + original_payload = payload + chop_count = 0 + while True: + parts = [] + if len(payload) <= chop_count: + break + payload = original_payload[chop_count:] + self.dns_id = dns_id + payload = payload[12:] + chop_count += 1 + while True: + cnt = payload[0] + if cnt == 0 or len(payload) < cnt: + break + part = payload[1 : cnt + 1] + + try: + decoded = str(part.decode("utf8")) + if not self._is_valid_domain(decoded): + break + parts.append(decoded) + except Exception: + break + + payload = payload[cnt + 1 :] + if len(payload) <= 6: + break + if len(parts) >= 2: + domain = ".".join(parts) + break + return domain + + def _is_valid_domain(self, domain): + valid = True + for c in domain: + if c.isalnum() or c == "-" or c == ".": + continue + valid = False + return valid + + def _create_dns_response(self, hostname="google.com", ip=None, id=1): + """ + Create a DNS response packet for the specified hostname. If `ip` + is specified (as a string, e.g., '127.0.0.1'), it will be used + for the response. Otherwise, a not found (NXDOMAIN) response is + returned. + """ + try: + if ip is None: + d = dnslib.DNSRecord( + dnslib.DNSHeader(qr=1, aa=1, ra=1, rcode=3, id=id), + q=dnslib.DNSQuestion(hostname), + ) + else: + d = dnslib.DNSRecord( + dnslib.DNSHeader(qr=1, aa=1, ra=1, id=id), + q=dnslib.DNSQuestion(hostname), + a=dnslib.RR(hostname, rdata=dnslib.A(ip)), + ) + payload = bytes(d.pack()) + return payload + except Exception as e: + print("DNS_CREATE failed:", e) + return None + + +class RawSocketSimulator: + """ + Simulates scans that make use of raw sockets. For instance, raw + SYN scan packets will be replied to with appropriate SYN+ACK + packets. + """ + + def __init__(self, domain, host=None, port=None): + self._raw_syn_queue = queue.Queue() + self.domain = domain + self.host_and_port = (host, port) + + def send(self, payload): + packet = ip.IP(payload) + if packet[ip.IP, tcp.TCP] is not None: + print( + "[RawSocketSimulator] RAW TCP %s:%s -> %s:%s" + % ( + packet[ip.IP].src_s, + packet[tcp.TCP].sport, + packet[ip.IP].dst_s, + packet[tcp.TCP].dport, + ) + ) + self.host_and_port = (packet[ip.IP].dst_s, packet[tcp.TCP].dport) + # Handle TCP SYN Scan + if packet[tcp.TCP].flags == tcp.TH_SYN: + if self._raw_syn_queue.qsize() < 16: + self._raw_syn_queue.put( + ( + packet[ip.IP].src_s, + packet[ip.IP].dst_s, + packet[tcp.TCP].sport, + packet[tcp.TCP].dport, + packet[tcp.TCP].seq, + ) + ) + else: + print("[RawSocketSimulator] Unsupported RAW scan packet:", packet) + + def recv(self, bufsize): + if not self._raw_syn_queue.empty(): + req = self._raw_syn_queue.get() + response = self._make_syn_ack_packet( + req[1], req[0], req[3], req[2], req[4] + ) + response = response[: min(len(response), bufsize)] + return response + + # TODO: handle other raw queue types here, e.g. + # xmas, fin, null, etc. + + raise Exception("[RawSocketSimulator] No handler for this raw socket.") + + def sendto(self, payload, host_and_port, flags=0): + return self.send(payload) + + def recvfrom(self, bufsize, flags=0): + host = self.host_and_port[0] + port = self.host_and_port[1] + return (self.recv(bufsize), socket.AF_INET, host, port) + + def is_readable(self): + return True + + def _make_syn_ack_packet(self, saddr, daddr, sport, dport, seq): + packet = ip.IP(src_s=saddr, dst_s=daddr) + tcp.TCP( + dport=dport, + sport=sport, + seq=seq, + ack=seq + 1, + flags=tcp.TH_SYN | tcp.TH_ACK, + ) + return packet.bin() + + +class BaseSelect: + """ + Implements `select` and `poll` for zelos `SocketHandles` that make + use of the BaseSocket. + """ + + POLLIN = 0x0001 + POLLPRI = 0x0002 + POLLOUT = 0x0004 + POLLERR = 0x0008 + POLLHUP = 0x0010 + POLLNVAL = 0x0020 + POLLRDNORM = 0x0040 + POLLRDBAND = 0x0080 + POLLWRNORM = 0x0100 + POLLWRBAND = 0x0200 + POLLMSG = 0x0400 + POLLREMOVE = 0x1000 + POLLRDHUP = 0x2000 + + def __init__(self, network_manager): + self.network = network_manager + + def select(self, in_handles, out_handles, ex_handles, timeout=0.1): + """ + Select file descriptors. + + Given 3 File Descriptor lists, `select` the first one + that is ready within the timeout window. For BaseSockets, always + return `ready` for `write` events, and `not ready` for read + events, as BaseSockets will never reply with data. + + Args: + in_handles: fds to select for `read`. + out_handles: fds to select for `write`. + ex_handles: fds to select for `exceptional` events. + + Returns: + 3 lists that indicate which handles ids are ready. + """ + + # If the handle refers to a simulated socket (e.g. DNS or + # RAW socket simulator), check if there is data to read. + # Otherwise, nothing else is readable, and everything is + # writable. + readable_socks = [] + in_socks = self._handles_to_sockets(in_handles) + for sock in in_socks: + if sock is not None and sock.is_readable(): + readable_socks.append(sock) + in_handles_ready = self._sockets_to_handles(in_handles, readable_socks) + + return (in_handles_ready, out_handles, []) + + def poll(self, fds, timeout=0.1): + """ + Poll file descriptors. + + Args: + fds: a list of tuples [(fd, events),..]. + timeout: maximum time to wait for the file descriptors. + + Returns: + the list of tuples with fired events [(fd, revents),..]. + """ + + return fds + + def _get_sockfd_from_handle(self, fd): + handle = self.network.handles.get(fd) + if ( + handle is None + or not isinstance(handle, SocketHandle) + or not isinstance(handle.socket, BaseSocket) + ): + return None + return handle.socket + + def _handles_to_sockets(self, fds): + socks = [] + for fd in fds: + sockfd = self._get_sockfd_from_handle(fd) + if sockfd is not None: + socks.append(sockfd.sock) + return socks + + def _sockets_to_handles(self, fds, socks_ready): + ready = [] + for fd in fds: + sockfd = self._get_sockfd_from_handle(fd) + if sockfd is not None: + if sockfd.sock in socks_ready: + ready.append(fd) + return ready diff --git a/src/zelos/network/dns.py b/src/zelos/network/dns.py new file mode 100644 index 0000000..60718c2 --- /dev/null +++ b/src/zelos/network/dns.py @@ -0,0 +1,69 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import dnslib + + +# Code that is used to aid dns parsing. + + +def parse_dns_request(raw_packet_data): + try: + d = str( + dnslib.DNSRecord.parse(raw_packet_data).get_q().get_qname() + ).rstrip(".") + return str(d) + except Exception as e: + print("DNS_INVALID:", e) + return None + + +def parse_dns_response(raw_packet_data): + try: + d = str( + dnslib.DNSRecord.parse(raw_packet_data).get_a().get_rname() + ).rstrip(".") + print("DNS_RESPONSE:", str(d)) + return str(d) + except Exception as e: + print("DNS_INVALID:", e) + if len(str(d)) == 0: + return None + return None + + +def create_dns_response(hostname="google.com", ip=None): + """ + Create a DNS response packet for the specified hostname. If `ip` is + specified (as a string, e.g., '127.0.0.1'), it will be used for the + response. Otherwise, a not found (NXDOMAIN) response is returned. + """ + try: + if ip is None: + d = dnslib.DNSRecord( + dnslib.DNSHeader(qr=1, aa=1, ra=1, rcode=3), + q=dnslib.DNSQuestion(hostname), + ) + else: + d = dnslib.DNSRecord( + dnslib.DNSHeader(qr=1, aa=1, ra=1), + q=dnslib.DNSQuestion(hostname), + a=dnslib.RR(hostname, rdata=dnslib.A(ip)), + ) + return d.pack() + except Exception as e: + print("DNS_CREATE failed:", e) + return None diff --git a/src/zelos/network/network.py b/src/zelos/network/network.py new file mode 100644 index 0000000..c4ff432 --- /dev/null +++ b/src/zelos/network/network.py @@ -0,0 +1,112 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import logging + +from zelos.handles import SocketHandle +from zelos.manager import IManager + +from .base_socket import BaseSelect, BaseSocket + + +class Network(IManager): + def __init__(self, helpers, file_manager, tracer): + super().__init__(helpers) + self.file_manager = file_manager + self.trace = tracer + self.logger = logging.getLogger(__name__) + self.attempted_connections = set() + self.num_sockets = 0 + + self.ignore_whitelist = False + self.whitelist = { + "127.0.0.53": [53] + # '8.8.8.8': [53] + } + + # Entries in the DNS whitelist will always result in a + # successful DNS response. If real networking is used, and the + # domain is non-existant, the response will be replaced with a + # default IP. This is useful for ensuring network checks for + # common domains succeed. + self.dns_whitelist = {"google.com", "pastebin.com", "virustotal.com"} + # Entries in the DNS blacklist will always result in an NXDOMAIN + # result, whether using real networking or Base sockets. This + # is useful making it appear as if hardcoded C2 hosts are + # unavailable, which often invokes DGA functionality. + self.dns_blacklist = {} + + # When returning a fake DNS response, use this IP address in the + # answer. This could be pointed to a network simulator. + self.dns_default_ip = "45.45.45.45" + + # By default, return non-existant (NX) responses for all + # domains, except those listed in the whitelist. + self.dns_default_to_nx = True + + self.socket_class = BaseSocket + self.select = BaseSelect(self) + + @property + def sockets(self): + return self.handles.get_by_type(SocketHandle) + + def set_socket_class(self, socket_class): + """ + Allows network activity to be handled by a different socket + class. + """ + self.socket_class = socket_class + + def set_select_class(self, select_class): + """ + Allows select/poll to be handled by a different class. + """ + self.select = select_class(self) + + def add_attempted_connection(self, string, method): + self.attempted_connections.add(string) + self.triggers.tr_contacts_domain(string, method) + + if len(self.attempted_connections) > 10: + self.triggers.tr_contacts_many_domains(self.attempted_connections) + + def create_socket(self, domain, type, protocol=0): + sock = self.socket_class(self, domain, type, protocol) + + sock_handle_num = self.handles.new_socket( + "sock#{0:03d}".format(self.num_sockets), sock + ) + self.num_sockets += 1 + + return sock_handle_num + + def enable_whitelist(self): + self.ignore_whitelist = False + + def disable_whitelist(self): + self.ignore_whitelist = True + + def is_whitelisted(self, host, port): + """ + Returns if the host and port to connect is whitelisted + """ + if host in self.whitelist: + if self.whitelist[host] == -1: + return True + if port in self.whitelist[host]: + return True + return self.ignore_whitelist diff --git a/src/zelos/plugin/__init__.py b/src/zelos/plugin/__init__.py new file mode 100644 index 0000000..e75f01b --- /dev/null +++ b/src/zelos/plugin/__init__.py @@ -0,0 +1,46 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +from .arg_base import ArgFactory +from .loader_base import Loader +from .parser_base import Parser, Section +from .plugin import ( + CommandLineOption, + IPlugin, + ISubcommand, + OSPlugin, + OSPlugins, + PluginCommands, + Plugins, +) +from .syscall_manager_base import SyscallManager + + +__all__ = [ + "IPlugin", + "Plugins", + "CommandLineOption", + "OSPlugin", + "OSPlugins", + "ISubcommand", + "PluginCommands", + "SyscallManager", + "Loader", + "Parser", + "Section", + "ArgFactory", +] diff --git a/src/zelos/plugin/arg_base.py b/src/zelos/plugin/arg_base.py new file mode 100644 index 0000000..388c771 --- /dev/null +++ b/src/zelos/plugin/arg_base.py @@ -0,0 +1,61 @@ +from typing import Any, Callable, Dict, List, Tuple + + +class Arg: + def __init__(self, type_str, name, value, string): + self.type = type_str + self.name = name + self.value = value + self.string = string + + +class Args: + def __init__(self, args: List[Arg]) -> None: + self._args = args + for a in args: + setattr(self, a.name, a.value) + + def __str__(self) -> str: + return ", ".join(self._arg_str_list()) + + def _arg_str_list(self) -> List[str]: + return [a.string for a in self._args] + + def to_dict_list(self) -> List[Dict[str, Any]]: + """ + Serialize arguments to dictionary list, e.g.: + args = [ { 'type': 'PCHAR', 'name': 'buf', 'value': 0x12345 } ] + """ + return [ + {"type": arg.type, "name": arg.name, "value": arg.val} + for arg in self._args + ] + + +class ArgFactory: + def __init__(self, str_func: Callable[[Arg], str]): + self._str_func = str_func + + def gen_args( + self, + arg_spec: List[Tuple[str, str]], + values: List[int], + arg_string_overrides: Dict[str, Callable[[Args], str]] = {}, + ) -> Args: + arg_list = [] + + # We collect the args first since some overrides require all of + # the arg values. For example, when passed a buffer and a count + # of bytes to write, we may want to restrict the size of the + # buffer to print by the count. + for (type_str, name), val in zip(arg_spec, values): + arg_list.append(Arg(type_str, name, val, "")) + args = Args(arg_list) + + for a in args._args: + if a.name in arg_string_overrides: + a.string = arg_string_overrides[a.name](args) + else: + a.string = self._str_func(a) + + return args diff --git a/src/zelos/plugin/loader_base.py b/src/zelos/plugin/loader_base.py new file mode 100644 index 0000000..325dbfb --- /dev/null +++ b/src/zelos/plugin/loader_base.py @@ -0,0 +1,81 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import logging + + +class Loader: + + STACK_BASE = 0x001A0000 + + def __init__(self, z, state, files, process, triggers, original_file_name): + self._z = z + self.state = state + self.modules = process.modules + self.files = files + self.process = process + self.triggers = triggers + self.original_file_name = original_file_name + self.logger = logging.getLogger(__name__) + + @property + def emu(self): + return self.process.emu + + @property + def memory(self): + return self.process.memory + + def _get_module_name(self, module_name): + normalized_module_name = self.modules._normalize_name(module_name) + + module_path = self.files.find_library(normalized_module_name) + if module_path is None: + module_path = module_name # support exe's w/out .exe extensions + normalized_module_name = module_path + # Try to find the file in the VFS + if module_path is None: + module_path = self.files.find_library(module_name) + return module_path, normalized_module_name + + def _get_entrypoint(self, pe, entrypoint_override): + if entrypoint_override is None: + return pe.EntryPoint + # If the input is the name of an export or an address, start + # execution of the main thread at that point + try: + return int(entrypoint_override, 16) + except Exception: + pass + try: + return pe.get_export(entrypoint_override).Address + except Exception: + pass + print( + "entrypoint_override (%s) was neither an export nor an address." + % entrypoint_override + ) + return pe.EntryPoint + + """ + Load a new process with specified module path, environment, + arguments and options + """ + + def load( + self, module_path, file, thread_name="main", entrypoint_override=None + ): + raise NotImplementedError() diff --git a/src/zelos/plugin/parser_base.py b/src/zelos/plugin/parser_base.py new file mode 100644 index 0000000..9f0976f --- /dev/null +++ b/src/zelos/plugin/parser_base.py @@ -0,0 +1,159 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import logging + +from zelos.enums import ProtType + + +PERM_NONE = ProtType.NONE +PERM_READ = ProtType.READ +PERM_WRITE = ProtType.WRITE +PERM_EXEC = ProtType.EXEC +PERM_RWX = ProtType.RWX +PERM_RX = ProtType.RX +PERM_RW = ProtType.RW +PERM_GUARD = 8 + + +class Section(object): + def __init__(self): + self.Name = "" + self.Address = 0x0 + self.Size = 0x0 + self.VirtualSize = 0x0 + self.Permissions = 0x0 + self.Alignment = 0x0 + + def string(self): + return "Section(Name={0.Name}, Perms=0x{0.Permissions:02x}," + "Address=0x{0.Address:08x}, VirtualSize=0x{0.VirtualSize:08x}," + "Alignment=0x{0.Alignment:04x})".format(self) + + def __str__(self): + return self.string() + + def __repr__(self): + return self.string() + + +class Imports(object): + def __init__(self): + self.ModuleName = "" + self.Entries = [] + + def string(self): + return "Imports(Name={0.ModuleName}, Count={1})".format( + self, len(self.Entries) + ) + + def __str__(self): + return self.string() + + def __repr__(self): + return self.string() + + +class ImportEntry(object): + def __init__(self, base, import_data): + self.Entries = [] + self.Address = base + import_data.iat_address + self.Name = import_data.name + self.Ordinal = 0 + self.IsOrdinal = False + if import_data.is_ordinal: + self.IsOrdinal = True + self.Ordinal = import_data.ordinal + + def string(self): + return "Import(Name={0.Name}, Address=0x{0.Address:08x}".format(self) + + def __str__(self): + return self.string() + + def __repr__(self): + return self.string() + + +class Export(object): + def __init__(self): + self.Name = "" + self.Address = 0x0 + self.Ordinal = 0x0 + self.IsExtern = False + self.ExternModule = "" + self.ExternFunction = "" + + def string(self): + extern = ", [EXTERN]" if self.IsExtern else "" + return "Export(Name={0}, Address=0x{1:08x}{2})".format( + self.Name[:20], self.Address, extern + ) + + def __str__(self): + return self.string() + + def __repr__(self): + return self.string() + + +class Symbol(object): + def __init__(self): + self.Name = "" + self.Address = 0x0 + self.Size = 0x0 + self.Type = 0x0 + + +class Parser(object): + def __init__(self): + self.logger = logging.getLogger(__name__) + self.Filepath = "" + self.Magic = "" + self.Architecture = "x86" + self.Mode = "32" + self.Bits = 32 + self.Filename = "" # e.g. ntdll.dll, libc.so + self.ShortName = "" # e.g. ntdll, libc + self.ImageBase = 0x0 + self.EntryPoint = 0x0 + self.Metadata = {} # e.g. PIE, ASLR, etc. (dict of misc key values) + self.Imports = [] # [module: [(addr, fnName),], module2: ] + self.Exports = [] + self.Sections = [] + self.Data = "" # blob of all section data, including virtual data + self.Size = 0x0 + self.VirtualSize = 0x0 + self.HeaderSize = 0x0 + self.StackSize = 0x0 + self.HeapSize = 0x0 + self.RelocDwords = None + self.Symbols = None + + # Parse the binary. Returns False if the format is not supported. + def parse(self, filename, filedata="", options={}): + raise NotImplementedError() + + def string(self): + return "Binary(Name={0.ShortName}, Address=0x{0.ImageBase:08x}," + "VirtualSize=0x{0.VirtualSize:08x}, Arch={0.Architecture}," + "Mode={0.Bits})".format(self) + + def __str__(self): + return self.string() + + def __repr__(self): + return self.string() diff --git a/src/zelos/plugin/plugin.py b/src/zelos/plugin/plugin.py new file mode 100644 index 0000000..31e93bc --- /dev/null +++ b/src/zelos/plugin/plugin.py @@ -0,0 +1,209 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import inspect +import logging +import pkgutil + +from collections import defaultdict +from os.path import isabs +from typing import Callable + +import zelos.ext.platforms +import zelos.ext.plugins + +from zelos.exceptions import UnsupportedBinaryError +from zelos.manager import IManager + + +class IPlugin(IManager): + """ + Base class for Plugins that provides an api for interacting with + zelos objects. + """ + + def __init__(self, zelos): + super().__init__(zelos.internal_engine.helpers) + self.zelos = zelos + + def __init_subclass__(cls, **kwargs): + Plugins.loaded_plugins.append(cls) + super().__init_subclass__(**kwargs) + + +plugins_loaded = set() + + +def load(paths): + """Loads the plugins that are located in the plugins directory.""" + global plugins_loaded + + # Load plugins that come with zelos + paths += zelos.ext.plugins.__path__._path + paths += zelos.ext.platforms.__path__._path + + paths = {p for p in paths if isabs(p) and p not in plugins_loaded} + if len(paths) == 0: + return + + for finder, name, _ in pkgutil.iter_modules(paths): + try: + _ = finder.find_module(name).load_module(name) + except Exception as e: + logging.getLogger(__name__).exception( + f"Could not load plugin at '{name}': {e}" + ) + plugins_loaded.update(paths) + + +class Plugins: + """ + Plugins are set as attributes of this class for convenience. + """ + + loaded_plugins = [] + + def __init__(self, zelos, paths): + self.registered_plugins = {} + self.logger = logging.getLogger(__name__) + load(paths) + self._zelos = zelos + + for p in self.loaded_plugins: + self.register_plugin(p) + print(f"Plugins: {', '.join(self.registered_plugins.keys())}") + + def register_plugin( + self, plugin_class: Callable[["Zelos"], IPlugin] + ) -> None: + name = getattr(plugin_class, "NAME", plugin_class.__name__.lower()) + plugin = plugin_class(self._zelos) + self.registered_plugins[name] = plugin + setattr(self, name, plugin) + self.logger.debug(f"Successfully registered plugin '{name}'") + + def get(self, plugin_name): + return self.registered_plugins.get(plugin_name, None) + + def has(self, plugin_name): + return hasattr(self, plugin_name) + + +class OSPlugin: + def __init__(self, z): + self.z = z + self.logger = self.z.logger + + def __init_subclass__(cls, **kwargs): + OSPlugins.unregistered_os_plugins.append(cls) + + def parse(self, *args, **kwargs): + raise NotImplementedError + + def load(self, *args, **kwargs): + raise NotImplementedError + + +class OSPlugins: + unregistered_os_plugins = [] + + def __init__(self, z): + self.logger = z.logger + self._registered_os_plugins = [] + self._register_plugins(z) + self.chosen_os = None + + def _register_plugins(self, z): + for p in self.unregistered_os_plugins: + self._registered_os_plugins.append(p(z)) + name = p.__name__.lower() + if hasattr(p, "NAME"): + name = p.NAME + self.logger.debug( + f"Successfully registered platform plugin '{name}'" + ) + + def parse(self, path, binary): + if self.chosen_os is not None: + return self.chosen_os.parse(path, binary) + + for os_plugin in self._registered_os_plugins: + parsed_file = os_plugin.parse(path, binary) + if parsed_file is not None: + self.chosen_os = os_plugin + return parsed_file + raise UnsupportedBinaryError( + f"File {path} does not have a supported parser" + ) + + def load(self, file, process, entrypoint_override=None): + if self.chosen_os is None: + raise UnsupportedBinaryError( + f"No supported parser was identified during parsing" + ) + self.chosen_os.load( + file, process, entrypoint_override=entrypoint_override + ) + + +class ISubcommand: + # TODO: Subcommands need to be moved to scripts, then the ISubcommand + # class can be deleted + + def __init__(self, argparser): + self.logger = logging.getLogger(__name__) + + +class PluginCommands: + registered_flags = defaultdict(dict) + + flags_to_resolve = [] + + def __init__(self, paths, argparser): + self.logger = logging.getLogger(__name__) + load(paths) + + self._added_flags = {} + for source_file, flags in self.registered_flags.items(): + arg_group = argparser.add_argument_group(source_file) + self.add_flags(source_file, flags, arg_group) + + def add_flags(self, source_file_name, flags_dict, argparser): + for name, args in flags_dict.items(): + if name in self._added_flags: + self.logger.warning( + ( + f"Skipped flag {name} from {source_file_name}, " + f"already defined in {self._added_flags[name]}" + ) + ) + continue + argparser.add_argument(f"--{name}", **args) + self._added_flags[name] = source_file_name + + +class CommandLineOption: + """ + Registers a command line option for Zelos. The kwargs are those + recognized by the argparse library + """ + + def __init__(self, name, **kwargs): + stack = inspect.stack() + frame = stack[1] + + PluginCommands.registered_flags[frame.filename][name] = kwargs diff --git a/src/zelos/plugin/syscall_manager_base.py b/src/zelos/plugin/syscall_manager_base.py new file mode 100644 index 0000000..ba65b01 --- /dev/null +++ b/src/zelos/plugin/syscall_manager_base.py @@ -0,0 +1,328 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see + +# . +# ====================================================================== +import ctypes +import logging +import sys + +from typing import Dict, List + +from termcolor import colored + +from zelos.hooks import HookType + + +def ptr2struct(z, addr, struct_class): + """ + Returns an instance of struct_class read starting from addr + """ + data = z.memory.read(addr, ctypes.sizeof(struct_class)) + instance = struct_class() + str2struct(instance, bytes(data)) + return instance + + +def get_pchar_array(z, addr, size=-1): + """ + Reads a set of string pointers starting at addr up to the first null + pointer (with a max of size, if specified) + Returns a list of null-terminated strings read from those pointers. + """ + result = [] + i = 0 + while i != size: + pstr = z.memory.read_int(addr + i * 4) + if pstr == 0: + break + result.append(z.memory.read_string(pstr)) + i += 1 + return result + + +def str2struct(struct_obj, data): + fit = min(len(data), ctypes.sizeof(struct_obj)) + ctypes.memmove(ctypes.addressof(struct_obj), data, fit) + + +class SyscallManager(object): + def __init__(self, engine): + self.logger = logging.getLogger(__name__) + self.z = engine + + self.strace_file = sys.stdout + + self.breakpoints = set() + + # Reference to the last Args() created from get_args(). Used to + # provide argument information for syscall breakpoints. + self.last_syscall_args = None + self.last_retval = 0 + + # If this is set, engine will set the IP value after breaking + # execution. This is needed to avoid an issue in Unicorn wherein + # emu_stop() fails to stop if IP is changed from within a hook. + self.pending_ip_change = None + + self.syscall_break_name = None + + @property + def emu(self): + return self.z.current_process.emu + + def set_breakpoint(self, syscall_name): + self.breakpoints.add(syscall_name) + + def remove_breakpoint(self, syscall_name): + self.breakpoints.remove(syscall_name) + + def get_last_syscall_args(self): + """ Gets the last set of Args() parsed by get_args """ + return self.last_syscall_args + + def get_last_retval(self): + """ Gets the last retval return by a syscall """ + return self.last_retval + + def get_retval_register(self): + """ Gets the register name used for syscall return values """ + return self._REG_RETURN + + def _handle_syscall_break(self, syscall_name): + # Check if a breakpoint was requested for this syscall name. If + # so, use the `break_exception` to exit the run-loop + # post-syscall. Save a reference to the syscall name that caused + # the break. Note that syscall breaks stop execution *after* + # zemu has already run the simulated syscall, cleaned up the + # stack (if needed), and set PC to the return address. + self.syscall_break_name = None + if syscall_name in self.breakpoints: + self.logger.warning(f"BREAKPOINT ON SYSCALL '{syscall_name}'") + self.syscall_break_name = syscall_name + self.z.scheduler.stop("syscall breakpoint") + + def generate_break_state(self): + if self.syscall_break_name is None: + syscall = None + else: + if self.pending_ip_change is not None: + self.z.current_thread.setIP(self.pending_ip_change) + self.pending_ip_change = None + syscall = { + "name": self.syscall_break_name, + "args": self.get_last_syscall_args().to_dict_list(), + "retval": self.get_last_retval(), + "retval_register": self.get_retval_register(), + } + self.syscall_break_name = None + + return { + "pc": self.z.current_thread.getIP(), + "syscall": syscall, + "bits": self.z.state.bits, + } + + def set_strace_file(self, filename): + self.strace_file = self.z.files.unsafe_open(filename, "w") + + def print(self, string, max_len=1000): + """ + Used to print additional debug information within a syscall. + Will not appear in the strace. + """ + if not self.z.trace.should_print_thread(): + return + if len(string) > max_len: + string = str(string[:max_len]) + "..." + + print(string) + + def print_info(self, string): + """Used to print auxiliary information to the strace file""" + if not self.z.trace.should_print_thread(): + return + if self.strace_file is sys.stdout: + s = ( + colored(f"[{self.z.current_thread.name}]", "magenta") + + " " + + colored(f"[INFO]", "white") + + f" {string}" + ) + else: + s = f"[{self.z.current_thread.name}] " + f"[INFO] {string}" + print(s, file=self.strace_file, flush=True) + + def print_syscall(self, thread, syscall_name, args, retval): + """ + Prints information regarding a syscall for the strace. + Note, this may not immediately print the syscall (may need to + wait for return value + """ + self.z.triggers.tr_syscall(thread, syscall_name, args, "Unknown") + + if not self.z.trace.should_print_thread(thread): + return + + retstr = "void" if retval is None else f"{retval:x}" + + if args is None: + self.z.logger.warning("Syscall did not call get_args") + + if self.strace_file is sys.stdout: + s = ( + colored(f"[{thread.name}]", "magenta") + + " " + + colored(f"[SYSCALL]", "red") + + " " + + colored(f"{syscall_name}", "white", attrs=["bold"]) + + f" ( {args} ) -> {retstr}" + ) + else: + ip = thread.getIP() + s = ( + f"[{thread.name}] " + f"[0x{ip:x}] {syscall_name} ( {args} ) -> {retstr}" + ) + + print(s, file=self.strace_file, flush=True) + + def handle_syscall(self, process): + """ + Calls the corresponding syscall with given name or number in the + context of the given process + """ + sys_num = self.get_syscall_number() + sys_name = self.find_syscall_name_by_number(sys_num) + self.z.triggers.tr_call_syscall(sys_name) + self.logger.spam(f"Executing syscall {sys_name}") + sys_fn = self.find_syscall(sys_name) + try: + # The current thread might get modified by the syscall. + thread = self.z.current_thread + self.last_syscall_args = None + retval = sys_fn(self, process) + if retval is not None: + self.set_return_value(retval) + self.print_syscall( + thread, sys_name, self.last_syscall_args, retval + ) + except Exception as e: + self.logger.error(f"Error happened inside syscall {sys_name}") + self.print_syscall(thread, sys_name, self.last_syscall_args, None) + raise e + + hooks = self.z.hook_manager._get_hooks(HookType.SYSCALL.AFTER) + for hook in hooks: + hook(self.z.api, sys_name, self.last_syscall_args, retval) + + self.last_retval = retval + self._handle_syscall_break(sys_name) + return True + + def pause_syscall(self, process, condition=None): + """ + Defines what happens when the pause syscall exception is + received. + """ + process.threads.pause_current_thread(condition=condition) + return + + def register_overrides(self, override_dict: Dict[str, List[int]]): + """ + Overrides return value behavior in the syscall manager. + """ + for sys_name, overrides in override_dict.items(): + sys_func = self._name2syscall_func[sys_name] + + def sys_func_wrapper(sm, p): + retval = sys_func(sm, p) + if len(overrides) > 0: + self.logger.info("Invoking sysfunc return override") + return overrides.pop(0) + return retval + + self._name2syscall_func[sys_name] = sys_func_wrapper + + def find_syscall_name_by_number(self, n): + """ + Finds and returns syscall name by syscall number. + """ + if n in self.rev_map: + sys_name = self.rev_map[n] + return sys_name + else: + self.logger.error( + f"[!] [0x{self.z.current_thread.getIP():x}] " + f"Could not find syscall name by number: [{n} 0x{n:x}]" + ) + return "Unknown" + + def find_syscall(self, sys_name): + """ + Finds and returns syscall implementation by syscall number. + """ + sys_fn = self._name2syscall_func.get(sys_name, self.nullsub) + + if sys_fn == self.nullsub: + self.logger.warning( + "[*] Using nullsub for syscall [{0}]...".format(sys_name) + ) + return sys_fn + + def add_custom_syscall(self, sys_num, sys_name, sys_func): + if sys_name in self.call_map: + self.logger.warning( + "[!] syscall number [{0}] already exists. " + "overwriting...".format(sys_num) + ) + if sys_num in self.rev_map: + self.logger.warning( + "[!] syscall name [{0}] already exists. " + "overwriting...".format(sys_name) + ) + self.call_map[sys_name] = sys_num + self.rev_map[sys_num] = sys_name + self._name2syscall_func[sys_name] = sys_func + + def return_addr(self): + raise NotImplementedError() + + def nullsub(self, sm, p): + return + + def fixme(self, msg): + self.print(f"[FIXME] {msg}") + + ########################################## + # ARCHITECTURE SPECIFIC MEMBER VARIABLES # + ########################################## + + # @@TODO handle cases where we use 2 return registers + # @@TODO handle argument loading from the stack + + _REG_NUMBER = "" + _REG_ARGS = [] + _REG_RETURN = "" + _REG_RETURN_2 = "" # pipe(2) + _REG_IP = "" + _REG_SP = "" + + def get_syscall_number(self): + raise NotImplementedError() + + def set_return_value(self, value): + raise NotImplementedError() diff --git a/src/zelos/processes.py b/src/zelos/processes.py new file mode 100644 index 0000000..940b6d7 --- /dev/null +++ b/src/zelos/processes.py @@ -0,0 +1,520 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import logging + +from typing import Callable, Dict, List + +import unicorn as uc + +from zelos.emulator import create_emulator +from zelos.emulator.base import IEmuHelper +from zelos.emulator.x86_gdt import GDT_32 +from zelos.exceptions import ZelosLoadException +from zelos.handles import Handles +from zelos.hooks import HookManager, Hooks, HookType, InterruptHooks +from zelos.memory import Memory +from zelos.modules import Modules +from zelos.scheduler import Scheduler +from zelos.state import State +from zelos.threads import Thread, Threads + + +# This has no dependencies. Purposefully not subclassing with IManager. +# If those dependencies are needed, go ahead and subclass it. +# Just kept it out for cleanliness + + +class Process: + def __init__( + self, + processes: str, + hook_manager: HookManager, + pid: int, + name: str, + emu: IEmuHelper, + parent_pid: int, + main_module: str = None, + orig_file_name: str = "", + cmdline_args: List = None, + environment_variables: List = None, + virtual_filename: str = None, + virtual_path: str = None, + last_instruction: str = None, + last_instruction_size: int = 0, + disableNX: bool = False, + ): + # OS plugins place OS-specific, process-level, functionality + class ZOS(object): + def __init__(self): + pass + + self.zos = ZOS() + + self.processes = processes + self._hook_manager = hook_manager + self.emu = emu + self.name = name + self.pid = pid + self.parent_pid = parent_pid + self.main_module = main_module + self.main_module_name = ( + "" if main_module is None else main_module.Filepath + ) + self.cmdline_args = [] if cmdline_args is None else cmdline_args + self.environment_variables = ( + [] if environment_variables is None else environment_variables + ) + self.virtual_filename = virtual_filename + self.virtual_path = virtual_path + self.original_file_name = orig_file_name + self.last_instruction = last_instruction + self.last_instruction_size = last_instruction_size + + self.modules = Modules() + + self.memory = Memory(self.emu, processes.state, disableNX=disableNX) + + self.threads = Threads( + self.emu, self.memory, self.processes.stack_size + ) + self.hooks = Hooks(self.emu, self.threads) + + def __str__(self) -> str: + return f"Name: '{self.name}', pid: {self.pid:x}, " + f"Active threads: {self.threads.num_active_threads()}" + + @property + def is_active(self) -> bool: + """ + Returns true if this process can be scheduled. + """ + return self.threads.num_active_threads() > 0 + + @property + def scheduler(self) -> Scheduler: + return self.threads.scheduler + + @property + def current_thread(self) -> Thread: + return self.threads.current_thread + + def new_thread( + self, + start_addr: int, + name: str = None, + priority: int = 0, + stack_setup: Callable = None, + module_path: str = "????", + benign_code: bool = False, + ) -> Thread: + """ + Creates a new thread for the current process. + + Args: + start_addr: The starting address of the new thread + name: Name of the new thread + priority: Scheduling priority of the new thread + stack_setup: Callback that populates stack of the new thread + module_path: Name of module of new thread + benign_code: Logging parameter + + Returns: + Thread object + """ + if len(self.threads.get_all_threads()) == 0: + tid = self.pid + else: + tid = self.processes.gen_tid() + if name is None: + name = f"{self.pid:x}_thread_{len(self.threads.get_all_threads())}" + t = self.threads.new_thread( + start_addr, + tid, + name=name, + priority=priority, + stack_setup=stack_setup, + module_path=module_path, + benign_code=benign_code, + ) + + current_thread = self.current_thread + self.threads.swap_with_thread(tid=t.id) + for hook in self._hook_manager._get_hooks(HookType.THREAD.CREATE): + hook(t, stack_setup) + if current_thread is not None: + self.threads.swap_with_thread(tid=current_thread.id) + return t + + def get_thread(self, tid: int) -> Thread: + """ + Gets the thread in this process with the specified tid. + + Args: + tid: Thread id + + Returns: + Thread object + """ + return self.threads.get_thread(tid) + + def get_child_processes(self) -> List: + """ + Get a list of all child processes created by this process. + + Returns: + List of Process Objects + """ + return [ + p for p in self.processes.process_list if p.parent_pid == self.pid + ] + + def priority(self) -> int: + """ + Returns the scheduling priority of this process. The scheduling + priority of a Process is that of its highest priority Thread. + + Returns: + Number denoting priority + """ + thread_priority_list = [ + t.priority for t in self.threads.get_active_threads() + ] + if len(thread_priority_list) == 0: + return -100 + return max(thread_priority_list) + + def blocks_executed(self) -> int: + """ + Calculates # of unique blocks executed across all threads + of this process. + + Returns: + Number of blocks executed + """ + unique_blocks = set() + for t in self.threads.get_all_threads(): + unique_blocks.update(t.blocks_executed) + return len(unique_blocks) + + def __lt__(self, other) -> bool: # Python 3 + return other.priority() < self.priority() # Sorts high to low + + +class Processes: + """ Exposes the processes that are on the virtual machine.""" + + def __init__( + self, + hook_manager: HookManager, + interrupt_handler: InterruptHooks, + main_module_name: str, + thread_stack_size: int, + disableNX: bool = False, + ): + self._hook_manager = hook_manager + self._interrupt_handler = interrupt_handler + self.process_list = [] + self.state = None + self.stack_size = thread_stack_size + self.next_tid = 0x7400 + self.logger = logging.getLogger(__name__) + self.disableNX = disableNX + self.main_module_name = main_module_name + + self.current_process = None + + # Counter to keep track of which process we are at. + self.process_counter = 0 + + self.handles = Handles(self, hook_manager) + + def apply_cross_process_hooks(p): + for hook in self._hook_manager._cross_process_hooks.values(): + p.hooks.add_hook( + hook.type, + hook.callback, + hook.handle, + name=hook.name, + start_addr=hook.start, + end_addr=hook.end, + ) + + self._hook_manager.register_process_hook( + HookType.PROCESS.CREATE, apply_cross_process_hooks + ) + + def set_architecture(self, state: State) -> None: + self.state = state + + def _create_first_process(self, main_module_name: str) -> None: + self.new_process(main_module_name + "_main", None) + self.current_process = self.process_list[0] + + def __str__(self) -> str: + s = "Process Manager's Processes:\n" + for p in self.process_list: + s += p.__str__() + "\n" + s += p.threads.__str__() + "\n" + return s + + @property + def current_thread(self) -> Thread: + return self.current_process.current_thread + + @property + def thread_manager(self) -> Threads: + return self.current_process.threads + + def gen_tid(self) -> int: + """ + Generates a tid that is guaranteed not to have been used before. + """ + tid = self.next_tid + self.next_tid += 1 + return tid + + def new_process( + self, + name: str = None, + parent_pid: int = None, + main_module=None, + cmdline_args: List = [], + ) -> int: + """ + Creates a new process. + + Args: + name: Name of the new thread. + parent_pid: ID of the parent process. + main_module: Module that is used to start the new process. + cmdline_args: Arguments to pass to the new process. + + Returns: + ID of the newly created process. + """ + pid = self.gen_tid() + + if name is None: + name = f"proc_{self.process_counter}" + if self.current_process is not None: + if parent_pid is None: + parent_pid = self.current_process.pid + if main_module is None: + main_module = self.current_process.main_module + + process = Process( + self, + self._hook_manager, + pid, + name, + self._create_emulator(), + parent_pid, + main_module=main_module, + cmdline_args=cmdline_args, + disableNX=self.disableNX, + ) + + for hook in self._hook_manager._get_hooks(HookType.PROCESS.CREATE): + hook(process) + + self.process_list.insert(0, process) + self.process_counter += 1 + + if self.state.arch in ["x86", "x86_64"]: + process.gdt = GDT_32(process.memory) + + return pid + + def _create_emulator(self) -> IEmuHelper: + arch = self.state.arch + + uc_arch_mode_dict = { + "x86": (uc.UC_ARCH_X86, uc.UC_MODE_32), + "x86_64": (uc.UC_ARCH_X86, uc.UC_MODE_64), + "arm": (uc.UC_ARCH_ARM, uc.UC_MODE_ARM), + "mips": (uc.UC_ARCH_MIPS, uc.UC_MODE_MIPS32), + } + + (uc_arch, uc_mode) = uc_arch_mode_dict[arch] + + endianness = self.state.endianness + if endianness == "little": + uc_mode |= uc.UC_MODE_LITTLE_ENDIAN + elif endianness == "big": + uc_mode |= uc.UC_MODE_BIG_ENDIAN + else: + raise ZelosLoadException(f"Unsupported endianness {endianness}") + + return create_emulator(uc_arch, uc_mode, self.state) + + def _as_current_process(self, p: Process, closure: Callable) -> None: + temp = self.current_process + self.current_process = p + closure() + self.current_process = temp + + def kill_process(self, pid: int) -> None: + """ + Stops a running process and all its threads. + + Args: + pid: ID of process to kill + """ + p = self.get_process(pid) + if p is not None: + for t in p.threads.get_active_threads(): + p.threads.kill_thread(t.id) + + def new_thread_for_current_process( + self, + start_addr: int, + name: str = None, + priority: int = 0, + stack_setup: Callable = None, + module_path: str = "????", + benign_code: bool = False, + ) -> Thread: + """ + Creates a new thread for the currently running process. + + Args: + start_addr: The starting address of the new thread + name: Name of the new thread + priority: Scheduling priority of the new thread + stack_setup: Callback that populates stack of the new thread + module_path: Name of module of new thread + benign_code: Logging parameter + + Returns: + Thread object + """ + return self.current_process.new_thread( + start_addr, + name, + priority=priority, + stack_setup=stack_setup, + module_path=module_path, + benign_code=benign_code, + ) + + def num_active_processes(self) -> int: + return len([1 for p in self.process_list if p.is_active]) + + def get_process(self, pid: int) -> Process: + for p in self.process_list: + if p.pid == pid: + return p + self.logger.notice(f"No process for pid {pid:x}") + return None + + def get_thread(self, tid: int) -> Thread: + """ + Gets the thread for the given tid. + + Args: + tid: ID of thread. + """ + for p in self.process_list: + t = p.get_thread(tid) + if t is not None: + return t + return None + + def get_all_threads(self) -> List[Thread]: + """Returns a list of threads across all processes""" + return [ + t for p in self.process_list for t in p.threads.get_all_threads() + ] + + def load_next_process(self) -> None: + """ + Loads the next process. Will skip processes that are not active. + """ + self.process_list.sort() + p = self.process_list.pop(0) + self.process_list.append(p) + self._load(p) + + def schedule_next(self) -> None: + """ + Swaps processes and threads in order to ensure that all + eventually get executed. + """ + # TODO: consider process following a process priority based on + # thread priority? + self.load_next_process() + self.swap_with_next_thread() + + def swap_with_next_thread(self) -> None: + """ + Tries to swap with the next thread in the current process. + If that is not possible, attempts to swap processes. + """ + if self.current_process.is_active: + self.current_process.threads.swap_with_next_thread() + for hook in self._hook_manager._get_hooks(HookType.THREAD.SWAP): + hook(self.current_thread) + else: + self.load_next_process() + + def load_process(self, pid) -> None: + """ + This attempts to load the designated process. This is a no-op + if the process to be loaded is the same as the current process. + + Args: + pid: ID of Process to load. + """ + p = self.get_process(pid) + self._load(p) + + def _load(self, p) -> None: + if self.current_process is not None: + if self.current_process.pid == p.pid: + return + assert not self.current_process.emu.is_running + self.logger.verbose(f"Loading process 0x{p.pid:x}") + self.current_process = p + if self.current_process.current_thread is None: + p.threads.swap_with_next_thread() + + def serialize_process(self, p): + raise NotImplementedError() + + def deserialize_process(self, data): + raise NotImplementedError() + + def _save_state(self) -> Dict: + def serialize_process(self, p): + return {} + + context = { + "deprecated_next_pid": self.deprecated_next_pid, + "process_list": [ + self.serialize_process(p) for p in self.process_list + ], + } + return context + + def _load_state(self, data) -> None: + def deserialize_process(self, process_data): + pass + + self.deprecated_next_pid = data["deprecated_next_pid"] + self.process_list = [ + self.deserialize_process(pdata) for pdata in data["process_list"] + ] diff --git a/src/zelos/scheduler.py b/src/zelos/scheduler.py new file mode 100644 index 0000000..4818af9 --- /dev/null +++ b/src/zelos/scheduler.py @@ -0,0 +1,115 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import logging + +from multiprocessing import Lock +from typing import Callable + + +class Scheduler: + """ + Handles the pausing and stopping of execution of threads in Zelos. + + There are subtlties here due to how changing EIP prevents unicorn + from stopping appropriately. Specifically, changing EIP while also + calling stop may invalidate the stop. + """ + + # Each class has its own emu, since each process has its own + # instance of unicorn. However, we want the stop reasons to be + # universal, since there should only be one thread running. + # This was to handle weird thread/process swapping errors. This is + # why end_reasons was a class variable. + # TODO: This breaks simultaneous instances of zelos + _end_reasons = [] + # Used to protect from another python thread initiating user input. + _user_input_mutex = Lock() + + def __init__(self, threads, emu): + self.logger = logging.getLogger(__name__) + self._threads = threads + self._emu = emu + # Map of tid to stop address + self._stop_addr = {} + + def stop(self, stop_reason: str) -> None: + """ + Stops execution of the running processes, exiting the run loop. + If there is no process running, this will prevent the next run. + + Args: + stop_reason: A string passed in for debugging purposes to + indicate what caused Zelos to stop. + + """ + self.stop_and_exec(stop_reason, lambda: False) + + def stop_and_exec( + self, stop_reason: str, should_continue: Callable[[], bool] + ) -> None: + """ + Stops execution of the running proesses in order to run the + provided closure. If the `should_continue` closure returns True, + execution will continue, otherwise the run loop will be exited. + + Args: + stop_reason: A string passed in for debugging purposes to + indicate what caused Zelos to stop. + should_continue: A closure that is run after the running + process is stopped. We should + + """ + self._end_reasons.append((stop_reason, should_continue)) + self._emu.emu_stop() + if self._threads.current_thread is not None: + t = self._threads.current_thread + self._stop_addr[t.id] = t.getIP() + + # This function is needed specifically for a bug in unicorn for ARM, + # where stopping in the middle of a block during a code hook results + # in the ip address being reset to the beginning of the block. + + def _pop_stop_addr(self, tid) -> int: + return self._stop_addr.pop(tid, None) + + def _resolve_end_reasons(self) -> bool: + """Returns True if execution should restart.""" + stop_reasons = self._pop_end_reasons() + if len(stop_reasons) > 0: + self.logger.debug(f"End reasons are {stop_reasons}") + + should_continue = True + while len(stop_reasons) > 0: + (reason, action) = stop_reasons.pop(0) + if action() is False: + should_continue = False + stop_reasons += self._pop_end_reasons() + + return should_continue + + def _pop_end_reasons(self): + # This copies the list, rather than taking a reference. + with Scheduler._user_input_mutex: + temp = Scheduler._end_reasons[:] + # You can't assign to self.end_reasons, as this will create + # an instance variable. + Scheduler._end_reasons.clear() + return temp + + def _has_end_reasons(self): + return len(Scheduler._end_reasons) > 0 diff --git a/src/zelos/state.py b/src/zelos/state.py new file mode 100644 index 0000000..a93ca1d --- /dev/null +++ b/src/zelos/state.py @@ -0,0 +1,63 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import datetime + + +class State: + """ + This maintains all state that is useful internally to other + Components, but does not belong in any specific one. + """ + + def __init__(self, z, binary, date): + if binary is not None: + self.bits = binary.Bits + self.arch = binary.Architecture + else: + self.bits = 32 + self.arch = "x86" + + self.date = date + self.datetime = datetime.datetime.now() + + # Whether or not to implement our modification to Unicorn's TCG + # generation. Extra speed, but hooking behavior is different. + self.patched_unicorn_enabled = False + + self.endianness = self.__get_endianness(binary) + + @property + def is64(self): + return self.bits == 64 + + @property + def bytes(self): + assert self.bits % 8 == 0, "Bits is not a multiple of 8" + return self.bits // 8 + + def __get_endianness(self, binary): + try: + id = binary.binary.header.identity_data + assert id != id.NONE, "currently only 32 bit is supported" + if id == id.MSB: + return "big" + elif id == id.LSB: + return "little" + else: + return "unknown" + except Exception: + return "little" diff --git a/src/zelos/symbol_manager.py b/src/zelos/symbol_manager.py new file mode 100644 index 0000000..6a1317e --- /dev/null +++ b/src/zelos/symbol_manager.py @@ -0,0 +1,54 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import logging + +from zelos.hooks import HookType + + +class SymbolManager: + def __init__(self, z): + self.z = z + self.logger = logging.getLogger(__name__) + # list of exports currently hooked in Unicorn + self.hooked_exports = {} + + def should_auto_simulate(self, module_name, func_name): + """ + Returns true if the autohooks should be used to simulate apis. + Modify this function in order to modify autohook behavior + """ + return False + + def should_setup_permanent_export_hook(self, address): + # Block translation interrupt, use this to add permanent hooks + # to blocks that represent the start of exported API functions + return False + + def setup_permanent_export_hook(self, address): + funcName = self.z.modules.reverse_module_functions[address] + self.hooked_exports[funcName] = True + self.z.hook_manager.register_exec_hook( + HookType.EXEC.BLOCK, + self.hook_export, + name=f"export_{funcName}_{address:x}", + ip_low=address, + ip_high=address, + ) + + def hook_export(self, zelos, address, size): + pass diff --git a/src/zelos/threads.py b/src/zelos/threads.py new file mode 100644 index 0000000..2eaf4ee --- /dev/null +++ b/src/zelos/threads.py @@ -0,0 +1,826 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import logging + +from collections import defaultdict +from enum import Enum +from typing import Any, Callable, List, Optional + +from unicorn import UC_PROT_READ, UC_PROT_WRITE + +import zelos.util as util + +from zelos.exceptions import ZelosException +from zelos.scheduler import Scheduler +from zelos.util import struct + + +class ThreadState(Enum): + UNKNOWN = 0 + RUNNING = 1 + SUCCESS = 2 + FAILURE = 3 + PAUSED = 4 + KILLED = 5 + + +class ThreadException(ZelosException): + pass + + +class InvalidTidException(ZelosException): + def __init__(self, tid): + if tid is not None: + super().__init__(f"{tid:x} is not a valid tid") + else: + super().__init__(f"'None' is not a valid tid") + + +class Thread(object): + """ + Represents information regarding a thread of execution. + + Remember, a thread is mostly its register state. This is contained + in the Emu class, and as such, if you are interacting with a thread + that is not "the current thread" you may not be changing the correct + state. Remember to save_context and load_context where appropriate + to ensure that your changes only effect the thread that you care + about. + """ + + def __init__( + self, + thread_manager, + context, + stack_base, + stack_size, + id, + name=None, + priority=0, + parent=None, + module_path=None, + benign_code=False, + ): + # OS plugins place OS-specific, thread-level, functionality + class ZOS(object): + def __init__(self): + pass + + self.zos = ZOS() + + self.threads = thread_manager + self.total_blocks_executed = 0 + self.blocks_executed = defaultdict(int) + + self.id = id + self.name = "unnamed_thread" if name is None else name + self.priority = priority + self.parent = "None" if parent is None else parent.name + self.parent_id = 0 if parent is None else parent.id + self.stack_base = stack_base + self.stack_size = stack_size + self.context = context + self.module_path = module_path + self.state = ThreadState.RUNNING + + self._callstack_indent_count = -1 + self.api_count = 0 + + # If the thread fails, keeps track of why. + self.fail_reason = None + + # If the thread is paused, this condition must evaluate to true + # to unpause + self.pause_condition = None + + # We know threads created from DLLs in our windows set are + # benign, we shouldn't ignore these threads. + self.benign_code = benign_code + + # TODO: + # This can point to address of thread local storage + self.local_data_address = None + + # Address expected to be reached if the thread ends successfully + self.end_address = 0xDEADBEEF + + @property + def memory(self): + return self.threads.memory + + @property + def emu(self): + return self.threads.emu + + @property + def is_active(self): + return self.state == ThreadState.RUNNING + + def get_reg(self, reg_name: str) -> int: + """ + Gets the value of the specified register for this thread. + + Args: + reg_name: The name of the register to get the value of. + + Returns: + An unsigned integer containing the value of the register. + """ + return self.threads.as_current_thread( + self, lambda: self.emu.get_reg(reg_name) + ) + + def set_reg(self, reg_name: str, val: int) -> None: + """ + Gets the value of the specified register for this thread. + + Args: + reg_name: The name of the register to set. + val: The value to set the register to. + + Returns: + An unsigned integer containing the value of the register. + """ + self.threads.as_current_thread( + self, lambda: self.emu.set_reg(reg_name, val) + ) + + def getIP(self) -> int: + return self.threads.as_current_thread(self, lambda: self.emu.getIP()) + + def setIP(self, new_ip: int) -> None: + self.threads.as_current_thread(self, lambda: self.emu.setIP(new_ip)) + + def getSP(self) -> int: + return self.threads.as_current_thread(self, lambda: self.emu.getSP()) + + def setSP(self, new_sp: int) -> None: + self.threads.as_current_thread(self, lambda: self.emu.setSP(new_sp)) + + def getFP(self) -> int: + return self.threads.as_current_thread(self, lambda: self.emu.getFP()) + + def setFP(self, new_fp: int) -> None: + return self.threads.as_current_thread( + self, lambda: self.emu.setFP(new_fp) + ) + + def getstack(self, idx: int) -> int: + return self.threads.as_current_thread( + self, lambda: self.emu.getstack(idx) + ) + + def setstack(self, idx: int, val: int) -> None: + self.threads.as_current_thread( + self, lambda: self.emu.setstack(idx, val) + ) + + def popstack(self) -> int: + return self.threads.as_current_thread( + self, lambda: self.emu.popstack() + ) + + def pushstack(self, data: int) -> int: + return self.threads.as_current_thread( + self, lambda: self.emu.pushstack(data) + ) + + def get_all_regs(self): + return self.threads.as_current_thread( + self, lambda: self.emu.get_all_regs() + ) + + def get_all_reg_vals(self): + return self.threads.as_current_thread( + self, lambda: self.emu.get_all_reg_vals() + ) + + def get_regs(self, regs=None): + return self.threads.as_current_thread( + self, lambda: self.emu.get_regs(regs) + ) + + def dumpregs(self, regs=None): + return self.threads.as_current_thread( + self, lambda: self.emu.dumpregs(regs) + ) + + def pack(self, x, bytes=None, little_endian=None, signed=False): + return self.threads.emu.pack(x, bytes, little_endian, signed) + + def unpack(self, x, bytes=None, little_endian=None, signed=False): + return self.threads.emu.unpack(x, bytes, little_endian, signed) + + def __str__(self): + return f"{self.name} (0x{self.id:x}), PRI: {self.priority}, " + f"parent: {self.parent}, IP: 0x{self.getIP():x}, " + f"blocks_exec'd: 0x{self.total_blocks_executed:x}, {self.state}" + + def __lt__(self, other): # Python 3 + return other.priority < self.priority # Sorts high to low + + def cleanup(self, z): + z.emu.mem_unmap(self.stack_base, self.stack_size) + + def save_context(self): + self.context = self.threads.emu.context_save() + + def load_context(self): + self.threads.emu.context_restore(self.context) + + def print_stack(self, sp=0, fp=0, top_count=5, bottom_count=10): + self.threads.current_thread.save_context() + self.load_context() + max_lines = 100 + # Start at stack top (sp) and print every 32-bit value between + # sp and fp, incrementing 4 bytes at a time. Print + # top{bottom}_count additional stack values from the top and + # bottom addresses surrounding sp and bp + result = "" + if sp == 0: + sp = self.emu.getSP() + if fp == 0: + fp = self.emu.getFP() + if fp == 0: + fp = sp + ptr_size = 4 + address = sp - top_count * ptr_size + end = fp + bottom_count * ptr_size + if end < address: + end = address + 0x4 * 15 + line_count = 0 + while address <= end: + line_count += 1 + if line_count >= max_lines: + break + prefix = " " + if address == fp: + prefix = " fp --> " + elif address == sp: + prefix = " sp --> " + try: + val = self.emu.mem_read(address, ptr_size) + val = struct.unpack(" List[Thread]: + """Returns all active threads""" + self._check_paused_threads() + return [t for t in self.get_all_threads() if t.is_active] + + def is_current_thread(self, t: Thread) -> bool: + """ + Returns True if "t" is the currently running thread. + + Args: + t: The thread to check. + + Returns: + True if "t" is currently running. + """ + return ( + self.current_thread is not None and self.current_thread.id == t.id + ) + + def kill_thread(self, tid: int) -> None: + """ + Changes the state of the specified thread to KILLED + + Args: + tid: The thread id of desired thread to kill + """ + t = self.get_thread(tid) + if t is None: + self.logger.notice(f"No thread {tid:x} to kill") + return + if t.state != ThreadState.RUNNING: + self.logger.info( + f"Thread {tid:x}, is already in state {t.state}. " + f"Refusing to kill" + ) + return + self.logger.info(f"Killing {tid:x}") + if self.is_current_thread(t): + self._inactivate_with_state(ThreadState.FAILURE) + else: + t.state = ThreadState.KILLED + + def as_current_thread(self, t: Thread, closure: Callable[[], Any]) -> Any: + """ + Executes the closure as if "t" was the current thread. + + Args: + t: The thread to set active while executing the closure + closure: The function to execute + + Returns: + The result of the closure. + """ + if self.is_current_thread(t): + return closure() + + current_thread_tid = None + if self.current_thread is not None: + current_thread_tid = self.current_thread.id + self.swap_with_thread(tid=t.id) + + ret_val = closure() + + if current_thread_tid is not None: + self.swap_with_thread(tid=current_thread_tid) + return ret_val + + # TODO(V): Remove this function, put in timeleap + def record_block(self, block_address): + if self.current_thread is not None: + self.current_thread.blocks_executed[block_address] += 1 + + def block_seen_before(self, block_address): + """ + Returns true if the block address has been seen in the current + thread before + """ + if self.current_thread is None: + return None + return block_address in self.current_thread.blocks_executed + + def num_unique_blocks(self, thread_name=None): + """ + Returns the number of unique blocks for the given thread. + Returns unique blocks across threads if no thread name is given + """ + if thread_name is not None: + t = self.get_thread_by_name(thread_name) + return len(t.blocks_executed) if t is not None else 0 + + threads = self.get_active_threads() + return sum(len(t.blocks_executed) for t in threads) + + def executed_within_region(self, begin_addr, end_addr, thread_names=None): + """ + Returns all block starts within the specified region, executed + by the specified threads. If no thread_names are specified, + checks all threads + """ + threads = self.get_threads(thread_names) + all_block_starts = [] + for t in threads: + block_starts = [ + addr + for addr in t.blocks_executed.keys() + if begin_addr <= addr < end_addr + ] + all_block_starts.extend(block_starts) + return all_block_starts + + def num_active_threads(self) -> int: + """ + Returns the number of threads that are still executing. + + Returns: + Number of threads that are still executing + """ + return len(self.get_active_threads()) + + # Ways a thread can fail + # * Inside an API, we can just say what the api is + # * If it was a sys call, it may be useful to indicate this + # * Outside an api, this has to be an exception + + def fail_current_thread(self, fail_reason: Optional[str] = None) -> None: + """ + Records the current thread as a failure and removes it from + execution + + Args: + fail_reason: Keeps track of why the thread failed. Used in + debugging + """ + self.current_thread.fail_reason = ( + "Unknown" if fail_reason is None else fail_reason + ) + self.logger.error( + "Thread %s failed: %s", + self.current_thread.name, + self.current_thread.fail_reason, + ) + self._inactivate_with_state(ThreadState.FAILURE) + + def complete_current_thread(self) -> None: + """ + Records the current thread as having completed successfully and + removes it from execution + """ + self.logger.success( + f"Done executing thread {self.current_thread.name}" + ) + self._inactivate_with_state(ThreadState.SUCCESS) + + def pause_current_thread( + self, condition: Optional[Callable[[], bool]] = None + ) -> None: + """ + Pauses the thread until the condition closure is checked and it + evaluates to true. If no condition is supplied, the thread is + paused indefinitely. + + Args: + condition: Evaluated periodically, if it ever returns True, + unpauses the thread + + """ + if condition is None: + + def condition(): + return False + + if condition(): + self.logger.notice( + "Pause condition is already true. " + "This is probably unintended." + ) + return + + self.current_thread.pause_condition = condition + + self.logger.info(f"Pausing thread {self.current_thread.name}") + self._inactivate_with_state(ThreadState.PAUSED) + + def _inactivate_with_state(self, thread_state): + self.current_thread.state = thread_state + self._swap(None) + self.emu.setIP(0x30) + self.scheduler.stop_and_exec("inactivate thread", lambda: True) + + def _check_paused_threads(self): + """Checks whether any paused threads are ready to run""" + for t in self.get_all_threads(): + if t.state != ThreadState.PAUSED: + continue + if t.pause_condition(): + self.logger.info(f"Thread {t.name} has been unpaused!") + t.pause_condition = None + t.state = ThreadState.RUNNING + + def new_thread( + self, + start_addr: int, + tid: int, + name: Optional[str] = None, + priority: int = 0, + stack_setup=None, + module_path: str = "????", + benign_code: bool = False, + ) -> Thread: + """ + Adds a thread which will run the thread_setup before starting. + """ + # We want to ensure that we initialize the stack for this thread + if name is None: + name = f"child_thread_{self.thread_count}" + self.thread_count += 1 + + stack_bottom = self.memory.map_anywhere( + self.stack_size, + min_addr=self.stack_min, + max_addr=self.stack_max, + name=name, + kind="stack", + prot=UC_PROT_READ | UC_PROT_WRITE, + ) + + stack_base = util.align_down( + stack_bottom + self.stack_size - 1, alignment=0x1000 + ) + + new_thread = self.create_thread( + start_addr, + tid, + stack_base, + name, + priority, + stack_setup, + module_path, + benign_code, + ) + + self.thread_list.append(new_thread) + + return new_thread + + def create_thread( + self, + start_addr, + tid, + stack_base, + name=None, + priority=0, + stack_setup=None, + module_path="????", + benign_code=False, + ) -> Thread: + temp_context = self.emu.context_save() + self.emu.setIP(start_addr) + new_thread_context = self.emu.context_save() + + self.logger.debug( + f" Adding thread {name} (priority {priority}) " + f"stack base at {stack_base:x}" + ) + new_thread = Thread( + self, + new_thread_context, + stack_base, + self.stack_size, + tid, + name, + priority, + parent=self.current_thread, + module_path=module_path, + benign_code=benign_code, + ) + + # TODO: Not a fan of the fact that we set the current thread to + # be the new thread for the hooks. Need to find a way to allow + # for the thread_create hooks to run, without having to + # continually switch between the active thread and this one. + + self.emu.context_restore(temp_context) + return new_thread + + def change_thread_priority(self, thread_name, new_priority): + """ Change the priority of a thread""" + t = self.get_thread_by_name(thread_name) + if t is None: + print("Unable to find thread %s" % thread_name) + return + t.priority = new_priority + + def get_all_threads(self) -> List[Thread]: + """ Returns all threads, whether active or stopped""" + return self.thread_list[:] + + def get_thread_by_name(self, name: str) -> Optional[Thread]: + """ Returns the first thread with the given name """ + threads = self.get_threads([name]) + return threads[0] if len(threads) > 0 else None + + def get_thread(self, tid): + for t in self.get_all_threads(): + if t.id == tid: + return t + return None + + def get_threads(self, names): + """ + Returns threads that have a name within the given list. If names + is None, returns all threads + """ + if names is None: + return self.get_all_threads() + threads = [] + for t in self.get_all_threads(): + if t.name in names: + threads.append(t) + return threads + + def get_child_threads(self, tid: int) -> List[Thread]: + """Returns all threads with the given parent name""" + return [t for t in self.get_all_threads() if t.parent_id == tid] + + def swap_with_thread( + self, name: Optional[str] = None, tid: Optional[int] = None + ) -> None: + """ + Swaps the current thread with the first thread with the given + name or thread id. Keep in mind, this will override the priority + given to threads. + You can only specify one of name or tid. + + Args: + name: If specified, finds a thread by the name + tid: If specified, finds a thread with that thread id + """ + if name is None and tid is None: + raise ThreadException("Must specify at least one of name/tid") + if name is not None and tid is not None: + raise ThreadException("May only specify one of name/tid") + if name is not None: + t = self.get_thread_by_name(name) + if t is None: + raise ThreadException(f"No thread named {name} exists.") + if tid is not None: + t = self.get_thread(tid) + if t is None: + raise InvalidTidException(tid) + + self._swap(t) + + def swap_with_next_thread(self) -> None: + """ + Swaps the current thread with the next thread to execute. This + respects priority, and will not swap if there is no thread of + equal or greater priority + """ + self._check_paused_threads() + t = self._next() + if t is None: + self.logger.spam("Can't swap with thread, no other threads") + + self._swap(t) + + def _swap(self, thread): + """ + Swaps the currently executing thread with the specified thread + in the emulator + """ + if self.current_thread is not None: + self.current_thread.save_context() + self._load(thread) + + def _load(self, thread): + """ Loads the specified thread into the emulator """ + if thread is None: + self.current_thread = None + return + if not thread.is_active: + self.logger.error( + f"Loading a thread with inactive state {thread.state}" + ) + self.emu.context_restore(thread.context) + self.current_thread = thread + self.logger.verbose( + "Loaded thread {0}, starting at {1:x}, stack at {2:x}".format( + thread.name, self.emu.getIP(), self.emu.getSP() + ) + ) + self.emu.setIP(self.emu.getIP()) + + def _next(self, tid=None): + """Returns the next thread to be scheduled.""" + active_threads = sorted(self.get_active_threads()) + if len(active_threads) == 0: + return None + next_thread = active_threads[0] + self._send_to_back(next_thread.id) + return next_thread + + def _send_to_back(self, tid): + """ + Sends this tid back to the end of the list. Used for scheduling. + """ + for i, t in enumerate(self.thread_list): + if t.id == tid: + self.thread_list.append(self.thread_list.pop(i)) + return + raise InvalidTidException(tid) + + regs_to_save = ( + "eax", + "ebp", + "ebx", + "ecx", + "edi", + "edx", + "flags", + "eip", + "esi", + "esp", + ) + + def _save_state(self): + def _serialize_thread(thread): + if thread is None: + return None + d = thread.__dict__.copy() + del d["context"] # Can't pickle, must be removed from dict + self.emu.context_restore(thread.context) + return (d, [self.emu.get_reg(reg) for reg in self.regs_to_save]) + + if self.current_thread is not None: + self.current_thread.save_context() + + context = { + # Must be done first, since it is current loaded + "current_thread_tid": self.current_thread.id + if self.current_thread is not None + else None, + "thread_list": [_serialize_thread(t) for t in self.thread_list], + "thread_count": self.thread_count, + } + + # Restore the current thread's context + if self.current_thread is not None: + self.emu.context_restore(self.current_thread.context) + return context + + def _load_state(self, data): + self._reset() + + def _deserialize_thread(data): + if data is None: + return None + (thread_dict, reg_vals) = data + # Unsure if you need deepcopy, but you definitely need to + # make sure that you are not linking the state data and the + # thread_manager. + thread_dict = thread_dict.copy() + for val, reg in zip(reg_vals, self.regs_to_save): + self.emu.set_reg(reg, val) + thread_dict["context"] = self.emu.context_save() + # Get a thread, we will set attributes manually. + t = Thread(None, None, None, None, None) + t.__dict__ = thread_dict + return t + + self.thread_list = [ + _deserialize_thread(d) for d in data["thread_list"] + ] + # End with loading the current_thread, so that execution state + # is ready to go. + current_thread = self.get_thread(data["current_thread_tid"]) + self._load(current_thread) + self.thread_count = data["thread_count"] diff --git a/src/zelos/tracer.py b/src/zelos/tracer.py new file mode 100644 index 0000000..611a079 --- /dev/null +++ b/src/zelos/tracer.py @@ -0,0 +1,504 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import logging + +import capstone.arm_const as cs_arm +import capstone.x86_const as cs_x86 + +from termcolor import colored +from unicorn import UC_ERR_READ_UNMAPPED, UcError + +from .manager import IManager + + +class Comment: + def __init__(self, address, thread_id, text): + self.address = address + self.thread_id = thread_id + self.text = text + + +class Tracer(IManager): + """ + Tracer prints information to the user to help understand the state + of execution. + """ + + def __init__(self, helpers, z, cs, modules): + super().__init__(helpers) + self._z = z + self._cs = cs + self._modules = modules + self.logger = logging.getLogger(__name__) + + self.current_return_address = 0 + self.current_function_name = "???" + self.current_api_module = "???" + + # Comments (to be dumped) + # We should save a lot of space by moving this to a grouping by + # thread, rather than keeping the thread with each comment. + # This will break compatibility with systems like Doppler. + self.comments = [] + + self.MAX_INDENTS = 40 + self.threads_to_print = set() + + if self.state.arch in ["x86", "x86_64"]: + self.comment_generator = x86CommentGenerator(self, modules) + elif self.state.arch == "arm": + self.comment_generator = ArmCommentGenerator(z, self, modules) + else: + self.comment_generator = EmptyCommentGenerator() + + @property + def functions_called(self): + return self.comment_generator.functions_called + + def bb(self, address=None, size=20, full_trace=False): + if not self.should_print_thread(): + return + """ Prints instructions starting at the given address up to + 'size' bytes away. If no address is given, prints starting at + the current address""" + if address is None: + address = self.emu.getIP() + try: + code = self.emu.mem_read(address, size) + insns = [insn for insn in self._cs.disasm(code, address)] + if len(insns) == 0: + return + # For full trace, we'll just print the first instruction, + # and then all the registers + if full_trace: + self.ins(insns[0]) + self.regs() + else: + for insn in insns: + self.ins(insn) + except UcError as e: + if e.errno == UC_ERR_READ_UNMAPPED: + print("Unable to read instruction at address %x" % address) + else: + raise e + + def regs(self): + """ Prints registers at the current address""" + s = "" + reg_list = self.emu.imp_regs + for reg in reg_list: + s += " ".join([f"{reg}={self.emu.get_reg(reg):x}"]) + s += "\n" + print(s) + + # There are issues when this is used with autohooks. Need to see how + # we can include this in the future. without endless indents + def indent(self): + self.get_current_thread()._callstack_indent_count += 1 + + def unindent(self): + self.get_current_thread()._callstack_indent_count -= 1 + + def set_current_return_address(self, addr): + self.current_return_address = addr + + def set_current_function_name(self, name): + self.current_function_name = name + + def set_current_api_module(self, name): + self.current_api_module = name + + def print(self, category, s, thread=None, addr_str=None): + if thread is None: + thread = self.get_current_thread().name + if addr_str is None: + addr_str = f"{self.emu.getIP():08x}" + thread_str = colored(f"[{thread}]", "magenta") + category_str = colored(f"[{category}]", "red") + addr_str_str = colored(f"[{addr_str}]", "white", attrs=["bold"]) + print(f"{thread_str} {category_str} {addr_str_str} {s}") + + # Prints the thread, return address and api string + def api(self, args, isNative=False): + """ Prints an API that was called""" + if not self.should_print_thread(): + return + return_address = self.current_return_address + indent_count = self.get_current_thread()._callstack_indent_count + try: + caller_module = ( + self.memory.get_region(return_address).module_name.split(".")[ + 0 + ] + + "_____" + )[:8] + except Exception: + caller_module = "________" + native_s = "" + if isNative: + native_s = "[Native] " + if indent_count == -1: + indent_count = 0 + args = "".join([i if ord(i) < 128 else "." for i in args]) + s = ( + " " * min(indent_count, self.MAX_INDENTS) + + f"{native_s}{self.current_api_module}!{args}" + ) + self.print("API", s, addr_str=f"{caller_module}:{return_address:08x}") + + # Prints the thread, return address and api string + def api_dbg(self, args, isNative=False): + """ Prints an API that was called""" + if not self.should_print_thread(): + return + return_address = self.current_return_address + indent_count = self.get_current_thread()._callstack_indent_count + caller_module = ( + self.memory.get_region(return_address).module_name.split(".")[0] + + "_____" + )[:8] + if indent_count == -1: + indent_count = 0 + args = "".join([i if ord(i) < 128 else "." for i in args]) + s = " " * min(indent_count, self.MAX_INDENTS) + colored( + args, "white", attrs=["bold"] + ) + self.print("API", s, addr_str=f"{caller_module}:{return_address:08x}") + + def ins(self, insn): + """ Prints the thread, address and instruction string """ + if not self.should_print_thread(): + return + sep = "" + if insn.address == self.emu.getIP(): + sep = "*" + address = insn.address + ins_string = self._get_insn_string(insn) + if address in self._z.main_module.exported_functions: + function_name = self._z.main_module.exported_functions[address] + s = colored(f"<{function_name}>", "white", attrs=["bold"]) + self.print("INS", s, addr_str=f"{sep}{address:08x}") + self.print("INS", ins_string, addr_str=f"{sep}{address:08x}") + + def should_print_thread(self, t=None): + """ + Decides whether log statements should be printed for the given + thread + """ + # If the thread is known to be benign, let's ignore it. We can + # add a config to print these if we need it. + if t is None: + t = self.get_current_thread() + if t is None: + return False + + if t.benign_code is True: + return False + # The user can choose to print threads or to focus only on + # specific threads. + if len(self.threads_to_print) == 0: + return True + return t.name in self.threads_to_print + + def _get_insn_string(self, insn): + """ Gets the string to be printed for an instruction.""" + cmt = "" + try: + cmt = self.comment_generator.get_comment(insn) + except Exception as e: + self.logger.notice( + f"Issue printing {insn.mnemonic} instruction comment: {e}" + ) + + result = "" + insn_str = "{0}\t{1}".format(insn.mnemonic, insn.op_str) + if len(cmt) > 0: + padding = "" + padSize = 60 - len(insn_str) + if padSize < 0: + padSize = 1 + for y in range(0, padSize): + padding += " " + result += ( + insn_str + + " " + + padding + + colored(" ; " + cmt, "grey", attrs=["bold"]) + ) + # Log comment for the dump + self.comments.append( + Comment(insn.address, self.get_current_thread().id, cmt) + ) + else: + result += insn_str + + return result + + +class EmptyCommentGenerator: + def get_comment(self, insn): + return "" + + +class ArmCommentGenerator: + def __init__(self, z, tracer, modules): + self.functions_called = {} + self._z = z + self.tracer = tracer + self._modules = modules + + def get_comment(self, insn): + if insn.mnemonic[:3] in [ + "add", + "sub", + "mov", + "mvn", + "mul", + "and", + "orr", + ]: + return self._dst_comment(insn) + if insn.mnemonic[:3] in ["cmp", "cmn", "tst", "teq"]: + return self._cmp_comment(insn) + if insn.mnemonic[:3] == "ldr": + return self._ldr_comment(insn) + if insn.mnemonic[:3] == "str": + return self._str_comment(insn) + if insn.mnemonic in ["b", "bl"]: + return self._branch_comment(insn) + if insn.mnemonic in ["push", "pop"]: + return self._push_pop(insn) + if insn.mnemonic == "svc": + return self._svc_comment(insn) + + return "." + + def _push_pop(self, insn): + """ + Returns all instructions that are pushed or popped + """ + reg_vals = [ + f"{self._get_reg_or_mem_val(insn, i): x}" for i in insn.operands + ] + return f"[{','.join(reg_vals)}]" + + def _svc_comment(self, insn): + """ + Returns the syscall name + """ + if insn.insn_name() == "svc" and insn.operands[0].imm != 0: + syscall_num = insn.operands[0].imm - 0x900000 + else: + syscall_num = self._z.zos.syscall_manager.get_syscall_number() + syscall_name = self._z.zos.syscall_manager.find_syscall_name_by_number( + syscall_num + ) + return f"{syscall_name}" + + def _dst_comment(self, insn): + """ + Returns the destination register + """ + dst_val = self._get_reg_or_mem_val(insn, insn.operands[0]) + return f"{insn.reg_name(insn.operands[0].value.reg)} = 0x{dst_val:x}" + + def _cmp_comment(self, insn): + dst_val = self._get_reg_or_mem_val(insn, insn.operands[0]) + src_val = self._get_reg_or_mem_val(insn, insn.operands[1]) + return f"0x{dst_val:x} vs 0x{src_val:x}" + + def _ldr_comment(self, insn): + """ + Returns a comment on loading a register from memory. + """ + dst_val = self._get_reg_or_mem_val(insn, insn.operands[0]) + src_val = self._get_reg_or_mem_val(insn, insn.operands[1], is_dst=True) + return f"{insn.reg_name(insn.operands[0].value.reg)} = " + f"load(0x{src_val:x}) = 0x{dst_val:x}" + + def _str_comment(self, insn): + """ + Returns a comment on storing a register in memory. + """ + src_val = self._get_reg_or_mem_val(insn, insn.operands[0]) + dst_val = self._get_reg_or_mem_val(insn, insn.operands[1], is_dst=True) + return f"store(0x{src_val:x}, 0x{dst_val:x})" + + def _branch_comment(self, insn): + """ + Returns a comment on branch to label. + """ + src_val = self._get_reg_or_mem_val(insn, insn.operands[0]) + if src_val in self._z.main_module.exported_functions: + func_name = self._z.main_module.exported_functions[src_val] + return f"<{func_name:s}> (0x{src_val:x})" + return f"<0x{src_val:x}>" + + def _get_reg_or_mem_val(self, insn, x, is_dst=False): + """ + Gets the value of the operand, for memory addresses, gets + the memory value at the location specified + """ + if x.type == cs_arm.ARM_OP_REG: + return self.tracer.emu.get_reg(insn.reg_name(x.value.reg)) + elif x.type == cs_arm.ARM_OP_IMM: + return x.imm + else: + base_val = ( + 0 + if x.mem.base == 0 + else self.tracer.emu.get_reg(insn.reg_name(x.mem.base)) + ) + shift_val = ( + 0 + if x.mem.index == 0 + else self.tracer.emu.get_reg(insn.reg_name(x.mem.index)) + * x.mem.scale + ) + if is_dst: + return base_val + shift_val + x.value.mem.disp + else: + return self.tracer.memory.read_int( + base_val + shift_val + x.value.mem.disp + ) + + +class x86CommentGenerator: + def __init__(self, tracer, modules): + self.tracer = tracer + self._modules = modules + self.functions_called = {} + + def _get_ptr_val_string(self, ptr: int) -> str: + """Returns a string representing the data pointed to by 'ptr' if 'ptr' + is a valid pointer. Otherwise, reutrns an empty string.""" + try: + pointer_data = self.tracer.memory.read_int(ptr) + except UcError as e: + if e.errno == UC_ERR_READ_UNMAPPED: + return "" + raise e + + s = "" + try: + s = self.tracer.memory.read_string(ptr, 8) + except UcError as e: + if e.errno != UC_ERR_READ_UNMAPPED: + raise e + + # Require a certain amount of valid characters to reduce false + # positives for string identification. + if len(s) > 2: + return f' -> "{s}"' + + return f" -> {pointer_data:x}" + + def get_comment(self, insn): + cmt = "" + if insn.mnemonic == "call" or insn.mnemonic == "jmp": + cmt = self._call_string(insn) + elif insn.mnemonic == "push": + cmt = self._push_string(insn) + elif len(insn.operands) == 1: + cmt = self._single_operand(insn) + elif insn.mnemonic == "test" or insn.mnemonic == "cmp": + cmt = self._test_or_cmp_string(insn) + elif len(insn.operands) == 2: + cmt = self._double_operand(insn) + return cmt + + def _call_string(self, insn): + # Only used when looking at the current instruction. + # op = operands[0] + # target = op.value.imm + target = self.tracer.emu.getIP() + self.functions_called[target] = True + cmt = insn.mnemonic + "(0x{0:x}) ".format(target) + if target in self._modules.reverse_module_functions: + cmt += " " + self._modules.reverse_module_functions[target] + return cmt + + def _push_string(self, insn): + op = insn.operands[0] + value = self._get_reg_or_mem_val(insn, op) + ptr_val_str = self._get_ptr_val_string(value) + return f"push(0x{value:x}){ptr_val_str}" + + def _single_operand(self, insn): + op = insn.operands[0] + # Just resolve any non-immediate values + value = self._get_reg_or_mem_val(insn, op) + ptr_val_string = self._get_ptr_val_string(value) + if op.type == cs_x86.X86_OP_REG: + reg_name = insn.reg_name(op.value.reg) + s = f"{reg_name} = 0x{value:x}{ptr_val_string}" + elif op.type == cs_x86.X86_OP_MEM: + s = f"mem is (0x{value:x}){ptr_val_string}" + else: + return "" + + return s + + def _double_operand(self, insn): + dst = insn.operands[0] + dst_val = self._get_reg_or_mem_val(insn, dst, is_dst=True) + if dst.type == cs_x86.X86_OP_REG: + dst_name = insn.reg_name(dst.value.reg) + ptr_val_string = self._get_ptr_val_string(dst_val) + return f"{dst_name} = 0x{dst_val:x}{ptr_val_string}" + + src = insn.operands[1] + src_val = self._get_reg_or_mem_val(insn, src) + + if dst.type == cs_x86.X86_OP_MEM: + dst_target = dst_val # dst.value.mem.disp + ptr_val_string = self._get_ptr_val_string(src_val) + return f"store(0x{dst_target:x},0x{src_val:x}){ptr_val_string}" + return "" + + def _test_or_cmp_string(self, insn): + dst_val = self._get_reg_or_mem_val(insn, insn.operands[0]) + src_val = self._get_reg_or_mem_val(insn, insn.operands[1]) + return "0x{0:x} vs 0x{1:x}".format(dst_val, src_val) + + def _get_reg_or_mem_val(self, insn, x, is_dst=False): + """ + Gets the value of the operand, for memory addresses, gets the + memory value at the location specified + """ + if x.type == cs_x86.X86_OP_REG: + return self.tracer.emu.get_reg(insn.reg_name(x.value.reg)) + elif x.type == cs_x86.X86_OP_IMM: + return x.imm + else: + base_val = ( + 0 + if x.mem.base == 0 + else self.tracer.emu.get_reg(insn.reg_name(x.mem.base)) + ) + shift_val = ( + 0 + if x.mem.index == 0 + else self.tracer.emu.get_reg(insn.reg_name(x.mem.index)) + * x.mem.scale + ) + if is_dst: + return base_val + shift_val + x.value.mem.disp + else: + return self.tracer.memory.read_int( + base_val + shift_val + x.value.mem.disp, sz=x.size + ) diff --git a/src/zelos/triggers.py b/src/zelos/triggers.py new file mode 100644 index 0000000..f3611ef --- /dev/null +++ b/src/zelos/triggers.py @@ -0,0 +1,452 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import time + +from collections import defaultdict +from enum import Enum + + +class RuleType(Enum): + NORMAL = 1 + TABLE = 2 + + +class Trigger: + """ + Triggers represent an action taken by a binary that is worth + recording. + """ + + def __init__(self, name, details, tags): + self.name = name + self.tags = tags + if len(self.tags) == 0: + self.tags.append("Misc") + + self.details = defaultdict(int) + if details is not None: + self.details[details] = 1 + + def add_occurrence(self, details): + if details is not None: + self.details[details] += 1 + + def clear_details(self): + self.details = defaultdict(int) + + +class TableTrigger(Trigger): + def __init__(self, name, details, column_names, tags): + Trigger.__init__(self, name, details, tags) + self.column_names = column_names + + +class Triggers: + """ + Manages triggers that are given to the Reporter for the purpose of + Report Generation + """ + + def __init__(self, z): + self.z = z + self.rules = {} + self.groupings = [ + # 'Lineage Analysis', + "Yara Rules", + "Network Activity", + "Process Manipulation", + "Registry Key Manipulation", + "File System", + "Misc", + "Thread Report", + ] + self.reached_entrypoint = False + self.seen_rdtsc = False + + self.total_time_slept_in_ms = 0 + self.update_time = time.time() + self.num_syscalls_called = 0 + self.unique_domains = set() + self.process_write_message = "Process Write: " + self.registry_create_key_message = "Registry Create: " + self.registry_key_write_message = "Registry Write: " + self.load_library_message = "Load library: " + self.rdtsc_message = ( + "Evasion: Anti-debug technique detected (RDTSC timing method)" + ) + self.exec_unpacked = False + self.exec_unpacked_message = "Evasion: Detected unpacked code exection" + self.rpc_message = "Remote Procedure Call: " + # thread_name -> [Api()] + self.apis_called = defaultdict(list) + self.api_strings = set() + self.syscalls_called = defaultdict(list) + + def _update_msg(self): + blocks = self.z.emu.bb_count() + if blocks % 10000 != 0 or blocks == 0: + return + + curr_time = time.time() + if curr_time - self.update_time >= 1: + self.update_time = curr_time + unique_blocks = self.z.current_process.blocks_executed() + self._custom_print( + f"Blocks: {blocks}, Unique_blocks: {unique_blocks}, " + f"Syscalls {self.num_syscalls_called}, Time Slept (s): " + f"{round(self.total_time_slept_in_ms/1000)}" + ) + + def _custom_print(self, msg): + # Used just for the hackathon + self.update_time = time.time() + if self.z.current_thread is not None: + name = self.z.current_thread.name + if "|" in name: + name = name.split("|")[0] + print(f"[HIGHLIGHTS] [{name}]: {msg}") + + def update_trigger(self, name, details, grouping="Misc", tags=[]): + if name not in self.rules: + self.trigger(name, details, tags, grouping) + return + self.rules[name].clear_details() + self.rules[name].add_occurrence(details) + + def trigger( + self, + name, + details=None, + tags=None, + rule_type=RuleType.NORMAL, + type_info=None, + grouping="Misc", + ): + """ Register a rule which has been triggered.""" + # Keep track of what groupings have been used + if grouping not in self.groupings: + self.groupings.append(grouping) + tags = [] if tags is None else tags + tags.append(grouping) + + if name not in self.rules: + if rule_type == RuleType.NORMAL: + self.rules[name] = Trigger(name, details, tags) + elif rule_type == RuleType.TABLE: + self.rules[name] = TableTrigger(name, details, type_info, tags) + else: + self.rules[name].add_occurrence(details) + + # Update the file if the report directory is specified + # if len(self.report_filepath) > 0: + # self.gen_report(filename=self.report_filepath) + + # TODO + # Tree overall for all threads + # Report for individual thread + # Report for all threads. + + # Collect names from mutex, semaphore, events, atoms (others...) + # Setting memory to writable / executable + # in virtual protect / virtualalloc + # Reading the stack values to get the address of kernel32 + # (stack values prior to execution) + + # Manual parsing of peb loader list + + def tr_read_peb(self, eip): + pass # This seems to be done in most binaries actually. + # self.trigger('Reads Process Environment Block (PEB)', + # 'Read PEB from eip = 0x{0:x}'.format(eip), + # grouping='PEB Access', tags=['evasive']) + + def tr_read_peb_ldr(self, eip): + self.trigger( + "Implements custom Import/GetProcAddress API", + "Read PEB_LDR_LIST from eip = 0x{0:x}".format(eip), + tags=["evasive"], + ) + + # Any kind of network activity, using sockets + def tr_contacts_domain(self, domain_name, method_name): + # Contacting domains isn't suspicious by itself, + # but certain patterns are + # - Contacting random looking domains + # - Contacting known malicious domains + max_printed_domains = 10 + self.trigger( + "Contacts domain", + "Connects to %s using %s" % (domain_name, method_name), + grouping="Network Activity", + ) + if ( + len(self.unique_domains) < max_printed_domains + and domain_name not in self.unique_domains + ): + self._custom_print(f'DNS QUERY: "{domain_name}"') + if len(self.unique_domains) == max_printed_domains: + self._custom_print( + f"DNS QUERY: Suppressing additional query output" + ) + self.unique_domains.add(domain_name) + if len(self.unique_domains) % 100 == 0: + self._custom_print( + f"DNS QUERY: Contacted {len(self.unique_domains)}" + "unique domains" + ) + + def tr_contacts_many_domains(self, domains): + self.update_trigger( + "Contacts many domains", + "Connects to %s..." % ",".join(domains), + grouping="Network Activity", + ) + + def tr_contacts_malicious_domain(self, domain_name, method_name): + self.trigger( + "Contacts known malicious domain", + "Connects to %s using %s" % (domain_name, method_name), + grouping="Network Activity", + tags=["malicious"], + ) + + # Anything that creates another process + # readprocessmem + # writeprocessmem + # CreateFileMapping (mapping in a remote process) + + def tr_create_process(self, name_of_remote_process, address): + self.trigger( + "Creates new process", + "Executes %s" % name_of_remote_process, + grouping="Process Manipulation", + tags=["evasive"], + ) + self._custom_print(f"Process Created: {name_of_remote_process}") + + def tr_create_thread(self, thread_address, thread_name): + msg = f'Thread "{thread_name}" address is 0x{thread_address:x}' + self.trigger( + "Creates new thread", msg, grouping="Process Manipulation", tags=[] + ) + self._custom_print(f"Thread Created: {msg}") + + def tr_gets_processes(self, details): + self.trigger( + "Gets list of processes", details, grouping="Process Manipulation" + ) + + def tr_process_injection(self, details): + self.trigger( + "Injects into another process", + details, + grouping="Process Manipulation", + ) + + def tr_process_write( + self, base_address, data_len, process_name, dll_region_name=None + ): + # Check if this writes into a known dll + if dll_region_name is not None: + self.trigger( + "Writes into separate process", + "Inserted into dll region: %s" % dll_region_name, + grouping="Process Manipulation", + ) + else: + self.trigger( + "Writes into separate process", grouping="Process Manipulation" + ) + msg = f"Process Name: {process_name} Address: 0x{base_address:x} " + "Bytes Written: 0x{data_len:x}" + self._custom_print(self.process_write_message + msg) + + def tr_registry_key_open(self, key_name, sub_key_name, perm): + self.trigger( + "Registry Key opened", + "Key: %s" % sub_key_name, + grouping="Registry Key Manipulation", + ) + + def tr_registry_key_read(self, key_name, perm): + self.trigger( + "Registry Key read", + "Key: %s" % key_name, + grouping="Registry Key Manipulation", + ) + + def tr_registry_create_key(self, key_name): + self.trigger( + "New Registry key", + f"Key: {key_name}", + grouping="Registry Key Manipulation", + ) + + def tr_registry_key_value_write(self, key_name, value_name, value_data): + max_data = 100 + if len(value_data) > max_data: + value_data = value_data[:max_data] + value_data = "".join([i if ord(i) < 128 else "." for i in value_data]) + msg = "%s\\%s: %s" % (key_name, value_name, value_data) + self.trigger( + "Value added to registry key", + msg, + grouping="Registry Key Manipulation", + ) + self._custom_print(self.registry_key_write_message + msg) + + def tr_registry_key_value_read(self, key_name, value_name): + self.trigger( + "Value read from registry key", + (key_name, value_name), + rule_type=RuleType.TABLE, + type_info=("Key", "Value Name"), + grouping="Registry Key Manipulation", + ) + + # File System + def tr_file_check(self, filename): + self.trigger( + "File details checked", + "Name: %s" % filename, + grouping="File System", + ) + + def tr_file_open(self, filename): + self.trigger( + "Files opened", "Name: %s" % filename, grouping="File System" + ) + + def tr_file_read(self): + pass + + def tr_file_write(self, file_name, data): + msg = f"File name: {file_name}, Wrote {len(data)} bytes" + self.trigger("File written", msg, grouping="File System") + self._custom_print(f"File written: {msg}") + + # Misc + def tr_reached_entrypoint(self, address): + if self.reached_entrypoint: + return + self.reached_entrypoint = True + self.trigger("Reached EntryPoint", f"0x{address:x}") + self._custom_print(f"Reached EntryPoint: 0x{address:x}") + + def tr_load_library(self, module_name): + self.trigger("Runtime DLLs", module_name) + self._custom_print(f"{self.load_library_message} {module_name}") + + def tr_mutex_open(self, mutex_name): + self.trigger("Opens Mutex", 'Name: "%s"' % mutex_name) + self._custom_print(f"Open Mutex: '{mutex_name}'") + + def tr_mutex_create(self, mutex_name): + self.trigger("Creates Mutex", 'Name: "%s"' % mutex_name) + self._custom_print(f"Create Mutex: '{mutex_name}'") + + def tr_call_crypto_func(self, func_name): + self.trigger("Calls Crypto function", "%s" % func_name) + + def tr_sleep(self, time_slept_in_ms, address): + if time_slept_in_ms > 2 * 60 * 1000: + self.trigger( + "Long Sleep", + "Sleep for %.2f seconds" % time_slept_in_ms, + tags=["evasive"], + ) + self.total_time_slept_in_ms += time_slept_in_ms + + def tr_rdtsc(self, address): + if self.seen_rdtsc: + return + self.seen_rdtsc = True + self.trigger("Measures performance", "rdtsc called") + self._custom_print(self.rdtsc_message) + + def tr_call_syscall(self, syscall_name): + self.num_syscalls_called += 1 + + def tr_syscall(self, thread, name, args, retval): + if thread is None: + thread_name = "NULLTHREAD" + bb_count = 0 + else: + thread_name = thread.name + bb_count = thread.total_blocks_executed + self.syscalls_called[thread_name].append( + Syscall(name, args, retval, bb_count) + ) + + def tr_api(self, thread, name, args, retval, simulated): + if thread is None: + thread_name = "NULLTHREAD" + bb_count = 0 + else: + thread_name = thread.name + bb_count = thread.total_blocks_executed + + self.apis_called[thread_name].append( + Api(name, args, retval, bb_count, simulated) + ) + + def tr_unpacked_code_execution(self, region): + if not self.exec_unpacked: + self.exec_unpacked = True + self._custom_print(self.exec_unpacked_message) + self.trigger("Execute unpacked code") + + def tr_rpc(self, interface, server_name): + self.trigger("Uses RPC", "NdrClientCall2 called") + if server_name is not None: + self._custom_print( + f"{self.rpc_message}{interface} -> {server_name}" + ) + else: + self._custom_print(f"{self.rpc_message}{interface}") + + +class Syscall: + """ A record of a syscall """ + + def __init__(self, api_string, args, ret_val, bb_count): + self.args = args + self.ret_val = ret_val + self.bb_count = bb_count + self.name = api_string + + +class Api: + """ A record of an api call.""" + + def __init__(self, api_string, args, ret_val, bb_count, is_simulated): + self.is_simulated = is_simulated + self.args = args + self.ret_val = ret_val + self.bb_count = bb_count + if "!" not in api_string: + self.module = "UnknownModule" + self.name = api_string + else: + self.module, self.name = api_string.split("!") + + def arg_html_string(self): + if self.args is None: + return "Unknown" + arg_strings = self.args.arg_str_list() + return "
".join(arg_strings) diff --git a/src/zelos/util.py b/src/zelos/util.py new file mode 100644 index 0000000..86b11b9 --- /dev/null +++ b/src/zelos/util.py @@ -0,0 +1,149 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import ctypes +import struct +import time + +from io import BytesIO + + +def p8(x): + return struct.pack(". +# ====================================================================== diff --git a/tests/data/dynamic_elf_arm_helloworld b/tests/data/dynamic_elf_arm_helloworld new file mode 100644 index 0000000..e387f50 Binary files /dev/null and b/tests/data/dynamic_elf_arm_helloworld differ diff --git a/tests/data/dynamic_elf_helloworld b/tests/data/dynamic_elf_helloworld new file mode 100644 index 0000000..b5d8492 Binary files /dev/null and b/tests/data/dynamic_elf_helloworld differ diff --git a/tests/data/dynamic_elf_x64_helloworld b/tests/data/dynamic_elf_x64_helloworld new file mode 100644 index 0000000..bed999d Binary files /dev/null and b/tests/data/dynamic_elf_x64_helloworld differ diff --git a/tests/data/ld-linux.so b/tests/data/ld-linux.so new file mode 100644 index 0000000..56b3340 Binary files /dev/null and b/tests/data/ld-linux.so differ diff --git a/tests/data/ltp_x64/syscalls/brk01 b/tests/data/ltp_x64/syscalls/brk01 new file mode 100644 index 0000000..112d75d Binary files /dev/null and b/tests/data/ltp_x64/syscalls/brk01 differ diff --git a/tests/data/ltp_x64/syscalls/chdir01 b/tests/data/ltp_x64/syscalls/chdir01 new file mode 100644 index 0000000..d4d04c5 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/chdir01 differ diff --git a/tests/data/ltp_x64/syscalls/chdir02 b/tests/data/ltp_x64/syscalls/chdir02 new file mode 100644 index 0000000..20a664f Binary files /dev/null and b/tests/data/ltp_x64/syscalls/chdir02 differ diff --git a/tests/data/ltp_x64/syscalls/chdir03 b/tests/data/ltp_x64/syscalls/chdir03 new file mode 100644 index 0000000..5ee4152 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/chdir03 differ diff --git a/tests/data/ltp_x64/syscalls/chdir04 b/tests/data/ltp_x64/syscalls/chdir04 new file mode 100644 index 0000000..2f6a5d3 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/chdir04 differ diff --git a/tests/data/ltp_x64/syscalls/fork01 b/tests/data/ltp_x64/syscalls/fork01 new file mode 100644 index 0000000..df85450 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork01 differ diff --git a/tests/data/ltp_x64/syscalls/fork02 b/tests/data/ltp_x64/syscalls/fork02 new file mode 100644 index 0000000..d235770 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork02 differ diff --git a/tests/data/ltp_x64/syscalls/fork03 b/tests/data/ltp_x64/syscalls/fork03 new file mode 100644 index 0000000..959c9c7 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork03 differ diff --git a/tests/data/ltp_x64/syscalls/fork04 b/tests/data/ltp_x64/syscalls/fork04 new file mode 100644 index 0000000..728155c Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork04 differ diff --git a/tests/data/ltp_x64/syscalls/fork05 b/tests/data/ltp_x64/syscalls/fork05 new file mode 100644 index 0000000..e2811d4 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork05 differ diff --git a/tests/data/ltp_x64/syscalls/fork06 b/tests/data/ltp_x64/syscalls/fork06 new file mode 100644 index 0000000..c3facd1 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork06 differ diff --git a/tests/data/ltp_x64/syscalls/fork07 b/tests/data/ltp_x64/syscalls/fork07 new file mode 100644 index 0000000..96c5e0e Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork07 differ diff --git a/tests/data/ltp_x64/syscalls/fork08 b/tests/data/ltp_x64/syscalls/fork08 new file mode 100644 index 0000000..977902d Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork08 differ diff --git a/tests/data/ltp_x64/syscalls/fork09 b/tests/data/ltp_x64/syscalls/fork09 new file mode 100644 index 0000000..2cc8f84 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork09 differ diff --git a/tests/data/ltp_x64/syscalls/fork10 b/tests/data/ltp_x64/syscalls/fork10 new file mode 100644 index 0000000..b355317 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork10 differ diff --git a/tests/data/ltp_x64/syscalls/fork11 b/tests/data/ltp_x64/syscalls/fork11 new file mode 100644 index 0000000..9c2a1c2 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork11 differ diff --git a/tests/data/ltp_x64/syscalls/fork12 b/tests/data/ltp_x64/syscalls/fork12 new file mode 100644 index 0000000..ce1a4cb Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork12 differ diff --git a/tests/data/ltp_x64/syscalls/fork13 b/tests/data/ltp_x64/syscalls/fork13 new file mode 100644 index 0000000..1cdd275 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork13 differ diff --git a/tests/data/ltp_x64/syscalls/fork14 b/tests/data/ltp_x64/syscalls/fork14 new file mode 100644 index 0000000..1bc6508 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/fork14 differ diff --git a/tests/data/ltp_x64/syscalls/getpid01 b/tests/data/ltp_x64/syscalls/getpid01 new file mode 100644 index 0000000..8eb7262 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/getpid01 differ diff --git a/tests/data/ltp_x64/syscalls/getpid02 b/tests/data/ltp_x64/syscalls/getpid02 new file mode 100644 index 0000000..1877f92 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/getpid02 differ diff --git a/tests/data/ltp_x64/syscalls/getppid01 b/tests/data/ltp_x64/syscalls/getppid01 new file mode 100644 index 0000000..4bf2027 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/getppid01 differ diff --git a/tests/data/ltp_x64/syscalls/getppid02 b/tests/data/ltp_x64/syscalls/getppid02 new file mode 100644 index 0000000..5257e08 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/getppid02 differ diff --git a/tests/data/ltp_x64/syscalls/kill01 b/tests/data/ltp_x64/syscalls/kill01 new file mode 100644 index 0000000..a831871 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill01 differ diff --git a/tests/data/ltp_x64/syscalls/kill02 b/tests/data/ltp_x64/syscalls/kill02 new file mode 100644 index 0000000..33f05c1 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill02 differ diff --git a/tests/data/ltp_x64/syscalls/kill03 b/tests/data/ltp_x64/syscalls/kill03 new file mode 100644 index 0000000..c452732 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill03 differ diff --git a/tests/data/ltp_x64/syscalls/kill04 b/tests/data/ltp_x64/syscalls/kill04 new file mode 100644 index 0000000..c992ca0 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill04 differ diff --git a/tests/data/ltp_x64/syscalls/kill05 b/tests/data/ltp_x64/syscalls/kill05 new file mode 100644 index 0000000..b3bdd42 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill05 differ diff --git a/tests/data/ltp_x64/syscalls/kill06 b/tests/data/ltp_x64/syscalls/kill06 new file mode 100644 index 0000000..bff2600 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill06 differ diff --git a/tests/data/ltp_x64/syscalls/kill07 b/tests/data/ltp_x64/syscalls/kill07 new file mode 100644 index 0000000..d050403 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill07 differ diff --git a/tests/data/ltp_x64/syscalls/kill08 b/tests/data/ltp_x64/syscalls/kill08 new file mode 100644 index 0000000..5b9921b Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill08 differ diff --git a/tests/data/ltp_x64/syscalls/kill09 b/tests/data/ltp_x64/syscalls/kill09 new file mode 100644 index 0000000..b1f7dbb Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill09 differ diff --git a/tests/data/ltp_x64/syscalls/kill10 b/tests/data/ltp_x64/syscalls/kill10 new file mode 100644 index 0000000..4a15227 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill10 differ diff --git a/tests/data/ltp_x64/syscalls/kill11 b/tests/data/ltp_x64/syscalls/kill11 new file mode 100644 index 0000000..6845ecb Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill11 differ diff --git a/tests/data/ltp_x64/syscalls/kill12 b/tests/data/ltp_x64/syscalls/kill12 new file mode 100644 index 0000000..837cefc Binary files /dev/null and b/tests/data/ltp_x64/syscalls/kill12 differ diff --git a/tests/data/ltp_x64/syscalls/open01 b/tests/data/ltp_x64/syscalls/open01 new file mode 100644 index 0000000..fa9f996 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open01 differ diff --git a/tests/data/ltp_x64/syscalls/open02 b/tests/data/ltp_x64/syscalls/open02 new file mode 100644 index 0000000..1c60132 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open02 differ diff --git a/tests/data/ltp_x64/syscalls/open03 b/tests/data/ltp_x64/syscalls/open03 new file mode 100644 index 0000000..f21196d Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open03 differ diff --git a/tests/data/ltp_x64/syscalls/open04 b/tests/data/ltp_x64/syscalls/open04 new file mode 100644 index 0000000..a058f26 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open04 differ diff --git a/tests/data/ltp_x64/syscalls/open05 b/tests/data/ltp_x64/syscalls/open05 new file mode 100644 index 0000000..ce5db28 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open05 differ diff --git a/tests/data/ltp_x64/syscalls/open06 b/tests/data/ltp_x64/syscalls/open06 new file mode 100644 index 0000000..1025a75 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open06 differ diff --git a/tests/data/ltp_x64/syscalls/open07 b/tests/data/ltp_x64/syscalls/open07 new file mode 100644 index 0000000..f4e89b0 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open07 differ diff --git a/tests/data/ltp_x64/syscalls/open08 b/tests/data/ltp_x64/syscalls/open08 new file mode 100644 index 0000000..bb85d34 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open08 differ diff --git a/tests/data/ltp_x64/syscalls/open09 b/tests/data/ltp_x64/syscalls/open09 new file mode 100644 index 0000000..e364c8b Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open09 differ diff --git a/tests/data/ltp_x64/syscalls/open10 b/tests/data/ltp_x64/syscalls/open10 new file mode 100644 index 0000000..e52715b Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open10 differ diff --git a/tests/data/ltp_x64/syscalls/open11 b/tests/data/ltp_x64/syscalls/open11 new file mode 100644 index 0000000..df1b792 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open11 differ diff --git a/tests/data/ltp_x64/syscalls/open12 b/tests/data/ltp_x64/syscalls/open12 new file mode 100644 index 0000000..9ee3828 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open12 differ diff --git a/tests/data/ltp_x64/syscalls/open12_child b/tests/data/ltp_x64/syscalls/open12_child new file mode 100644 index 0000000..2f21306 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open12_child differ diff --git a/tests/data/ltp_x64/syscalls/open13 b/tests/data/ltp_x64/syscalls/open13 new file mode 100644 index 0000000..9b6a477 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open13 differ diff --git a/tests/data/ltp_x64/syscalls/open14 b/tests/data/ltp_x64/syscalls/open14 new file mode 100644 index 0000000..e9483a0 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/open14 differ diff --git a/tests/data/ltp_x64/syscalls/openat01 b/tests/data/ltp_x64/syscalls/openat01 new file mode 100644 index 0000000..907f379 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/openat01 differ diff --git a/tests/data/ltp_x64/syscalls/openat02 b/tests/data/ltp_x64/syscalls/openat02 new file mode 100644 index 0000000..66b36a8 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/openat02 differ diff --git a/tests/data/ltp_x64/syscalls/openat02_child b/tests/data/ltp_x64/syscalls/openat02_child new file mode 100644 index 0000000..92186d8 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/openat02_child differ diff --git a/tests/data/ltp_x64/syscalls/openat03 b/tests/data/ltp_x64/syscalls/openat03 new file mode 100644 index 0000000..e2332d5 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/openat03 differ diff --git a/tests/data/ltp_x64/syscalls/pipe01 b/tests/data/ltp_x64/syscalls/pipe01 new file mode 100644 index 0000000..32cca87 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/pipe01 differ diff --git a/tests/data/ltp_x64/syscalls/pipe02 b/tests/data/ltp_x64/syscalls/pipe02 new file mode 100644 index 0000000..7b63882 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/pipe02 differ diff --git a/tests/data/ltp_x64/syscalls/pipe03 b/tests/data/ltp_x64/syscalls/pipe03 new file mode 100644 index 0000000..69a4089 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/pipe03 differ diff --git a/tests/data/ltp_x64/syscalls/pipe04 b/tests/data/ltp_x64/syscalls/pipe04 new file mode 100644 index 0000000..277a588 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/pipe04 differ diff --git a/tests/data/ltp_x64/syscalls/pipe05 b/tests/data/ltp_x64/syscalls/pipe05 new file mode 100644 index 0000000..f0d80b2 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/pipe05 differ diff --git a/tests/data/ltp_x64/syscalls/pipe06 b/tests/data/ltp_x64/syscalls/pipe06 new file mode 100644 index 0000000..a050da4 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/pipe06 differ diff --git a/tests/data/ltp_x64/syscalls/pipe07 b/tests/data/ltp_x64/syscalls/pipe07 new file mode 100644 index 0000000..bacd5b3 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/pipe07 differ diff --git a/tests/data/ltp_x64/syscalls/pipe08 b/tests/data/ltp_x64/syscalls/pipe08 new file mode 100644 index 0000000..78d0ffc Binary files /dev/null and b/tests/data/ltp_x64/syscalls/pipe08 differ diff --git a/tests/data/ltp_x64/syscalls/pipe09 b/tests/data/ltp_x64/syscalls/pipe09 new file mode 100644 index 0000000..456117d Binary files /dev/null and b/tests/data/ltp_x64/syscalls/pipe09 differ diff --git a/tests/data/ltp_x64/syscalls/read01 b/tests/data/ltp_x64/syscalls/read01 new file mode 100644 index 0000000..fa73669 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/read01 differ diff --git a/tests/data/ltp_x64/syscalls/read02 b/tests/data/ltp_x64/syscalls/read02 new file mode 100644 index 0000000..9a19c0e Binary files /dev/null and b/tests/data/ltp_x64/syscalls/read02 differ diff --git a/tests/data/ltp_x64/syscalls/read03 b/tests/data/ltp_x64/syscalls/read03 new file mode 100644 index 0000000..1097105 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/read03 differ diff --git a/tests/data/ltp_x64/syscalls/read04 b/tests/data/ltp_x64/syscalls/read04 new file mode 100644 index 0000000..9a3279a Binary files /dev/null and b/tests/data/ltp_x64/syscalls/read04 differ diff --git a/tests/data/ltp_x64/syscalls/rmdir01 b/tests/data/ltp_x64/syscalls/rmdir01 new file mode 100644 index 0000000..cb516ff Binary files /dev/null and b/tests/data/ltp_x64/syscalls/rmdir01 differ diff --git a/tests/data/ltp_x64/syscalls/sbrk01 b/tests/data/ltp_x64/syscalls/sbrk01 new file mode 100644 index 0000000..78db809 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/sbrk01 differ diff --git a/tests/data/ltp_x64/syscalls/sbrk02 b/tests/data/ltp_x64/syscalls/sbrk02 new file mode 100644 index 0000000..4af4afc Binary files /dev/null and b/tests/data/ltp_x64/syscalls/sbrk02 differ diff --git a/tests/data/ltp_x64/syscalls/sbrk03 b/tests/data/ltp_x64/syscalls/sbrk03 new file mode 100644 index 0000000..da2231f Binary files /dev/null and b/tests/data/ltp_x64/syscalls/sbrk03 differ diff --git a/tests/data/ltp_x64/syscalls/vfork01 b/tests/data/ltp_x64/syscalls/vfork01 new file mode 100644 index 0000000..709c1e6 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/vfork01 differ diff --git a/tests/data/ltp_x64/syscalls/vfork02 b/tests/data/ltp_x64/syscalls/vfork02 new file mode 100644 index 0000000..c53dfdf Binary files /dev/null and b/tests/data/ltp_x64/syscalls/vfork02 differ diff --git a/tests/data/ltp_x64/syscalls/write01 b/tests/data/ltp_x64/syscalls/write01 new file mode 100644 index 0000000..6b8fd53 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/write01 differ diff --git a/tests/data/ltp_x64/syscalls/write02 b/tests/data/ltp_x64/syscalls/write02 new file mode 100644 index 0000000..a9d4799 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/write02 differ diff --git a/tests/data/ltp_x64/syscalls/write03 b/tests/data/ltp_x64/syscalls/write03 new file mode 100644 index 0000000..65b6868 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/write03 differ diff --git a/tests/data/ltp_x64/syscalls/write04 b/tests/data/ltp_x64/syscalls/write04 new file mode 100644 index 0000000..710a46d Binary files /dev/null and b/tests/data/ltp_x64/syscalls/write04 differ diff --git a/tests/data/ltp_x64/syscalls/write05 b/tests/data/ltp_x64/syscalls/write05 new file mode 100644 index 0000000..ac1b046 Binary files /dev/null and b/tests/data/ltp_x64/syscalls/write05 differ diff --git a/tests/data/src/loaddll.c b/tests/data/src/loaddll.c new file mode 100644 index 0000000..285455a --- /dev/null +++ b/tests/data/src/loaddll.c @@ -0,0 +1,32 @@ +// +// Loads the DLL specified on the command line. Used for testing Zemu's ability +// to load various DLLs and successfully execute their DLLMain. +// +// All DLLs in a directory can be tested as follows: +// find zemu/lib/windows/filesystems/win7x86/Windows/System32/ -type f -iname *.dll -exec sh -c 'DLL=$(basename {}); echo Trying to reach NtTerminateProcess with load of $DLL; python3 zemu-exec.py --patched --winnative --disable_export_hooks --timeout=30 --cmdline_args="$DLL" demo/loaddll.exe' _ {} \; 2>&1 | tee loaddll_test.txt +// cat loaddll_test.txt | grep NtTerminateProcess +// +// Originally built in the VC++ 2008 32-bit command prompt with command: +// cl loaddll.c + +#include +#include + +int main(int argc, char *argv[], char *envp[]) { + HINSTANCE hinstLib; + + if (argc < 2) { + return 1; + } + + hinstLib = LoadLibrary(TEXT(argv[1])); + + if (hinstLib == NULL) { + return 1; + } + + printf("OK\n"); + + FreeLibrary(hinstLib); + return 0; +} diff --git a/tests/data/src/multithread.c b/tests/data/src/multithread.c new file mode 100644 index 0000000..79c2bf6 --- /dev/null +++ b/tests/data/src/multithread.c @@ -0,0 +1,27 @@ +#include +#include + +void *inc_x(void *x){ + int *x_ptr = (int *)x; + ++(*x_ptr); + printf("x increment finished\n"); + return NULL; +} + +int main(){ + int x = 0, y = 0; + printf("x: %d, y: %d\n", x, y); + pthread_t inc_x_thread; + if(pthread_create(&inc_x_thread, NULL, inc_x, &x)) { + fprintf(stderr, "Error creating thread\n"); + return 1; + } + ++y; + printf("y increment finished\n"); + if(pthread_join(inc_x_thread, NULL)) { + fprintf(stderr, "Error joining thread\n"); + return 2; + } + printf("x: %d, y: %d\n", x, y); + return 0; +} diff --git a/tests/data/static-socket-x86-musl b/tests/data/static-socket-x86-musl new file mode 100644 index 0000000..d4bbc72 Binary files /dev/null and b/tests/data/static-socket-x86-musl differ diff --git a/tests/data/static_elf_arm_helloworld b/tests/data/static_elf_arm_helloworld new file mode 100644 index 0000000..8d679fb Binary files /dev/null and b/tests/data/static_elf_arm_helloworld differ diff --git a/tests/data/static_elf_helloworld b/tests/data/static_elf_helloworld new file mode 100644 index 0000000..8c7d046 Binary files /dev/null and b/tests/data/static_elf_helloworld differ diff --git a/tests/data/static_elf_mips_lsb_helloworld_mti b/tests/data/static_elf_mips_lsb_helloworld_mti new file mode 100644 index 0000000..8abe83f Binary files /dev/null and b/tests/data/static_elf_mips_lsb_helloworld_mti differ diff --git a/tests/data/static_elf_mips_msb_helloworld_img b/tests/data/static_elf_mips_msb_helloworld_img new file mode 100644 index 0000000..c637854 Binary files /dev/null and b/tests/data/static_elf_mips_msb_helloworld_img differ diff --git a/tests/data/static_elf_mips_msb_helloworld_mti b/tests/data/static_elf_mips_msb_helloworld_mti new file mode 100644 index 0000000..2dc19f0 Binary files /dev/null and b/tests/data/static_elf_mips_msb_helloworld_mti differ diff --git a/tests/data/static_elf_mipseb_mti_helloworld b/tests/data/static_elf_mipseb_mti_helloworld new file mode 100644 index 0000000..3c80127 Binary files /dev/null and b/tests/data/static_elf_mipseb_mti_helloworld differ diff --git a/tests/data/static_elf_mipsel_mti_helloworld b/tests/data/static_elf_mipsel_mti_helloworld new file mode 100644 index 0000000..6b78eb1 Binary files /dev/null and b/tests/data/static_elf_mipsel_mti_helloworld differ diff --git a/tests/data/static_elf_x64_helloworld b/tests/data/static_elf_x64_helloworld new file mode 100644 index 0000000..fc022d2 Binary files /dev/null and b/tests/data/static_elf_x64_helloworld differ diff --git a/tests/data/x86_multithread b/tests/data/x86_multithread new file mode 100644 index 0000000..f7a5997 Binary files /dev/null and b/tests/data/x86_multithread differ diff --git a/tests/encrypt_test_file.py b/tests/encrypt_test_file.py new file mode 100644 index 0000000..a7a569b --- /dev/null +++ b/tests/encrypt_test_file.py @@ -0,0 +1,29 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import sys + +from zelos.util import file_encrypt + + +if __name__ == "__main__": + if len(sys.argv) == 1: + print("Usage: python {0} ".format(sys.argv[0])) + exit() + + files = sys.argv[1:] + for filename in files: + file_encrypt(filename) diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..6a57c1d --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,252 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import unittest + +from collections import defaultdict +from os import path + +from zelos import HookType, Zelos + + +DATA_DIR = path.join(path.dirname(path.abspath(__file__)), "data") + + +class ZelosTest(unittest.TestCase): + def test_regs(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + self.assertEqual(z.regs.eip, 0x8048B70) + z.regs.eip = 0x1 # should fail. + z.start() + self.assertEqual( + 1, len(z.internal_engine.thread_manager.failed_threads) + ) + + # This test failed on windows for some reason. + # def test_start_timeout(self): + # z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + # # This should be enough such that this binary couldn't finish in + # # this time. If for some reason, Zelos is fast enough to run + # # this binary in 1 microsecond: + # # 1) congratulations + # # 2) just reduce the timeout even lower. + # z.start(timeout=0.000001) + + # self.assertEqual( + # 1, z.internal_engine.thread_manager.num_active_threads() + # ) + + def test_memory_hook(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + + read_addresses = [] + + def mem_read_hook(zelos, access, address, size, value): + read_addresses.append(address) + + write_addresses = [] + + def mem_write_hook(zelos, access, address, size, value): + write_addresses.append(address) + + z.hook_memory(HookType.MEMORY.READ, mem_read_hook) + + z.hook_memory(HookType.MEMORY.WRITE, mem_write_hook) + z.start() + + self.assertGreater(len(read_addresses), 0) + self.assertGreater(len(write_addresses), 0) + + def test_exec_hook(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + + blocks = [] + + def block_hook(zelos, address, size): + blocks.append(address) + + instr_addr = [] + + def single_instr_hook(zelos, address, size): + instr_addr.append(address) + + z.hook_execution(HookType.EXEC.BLOCK, block_hook) + + target_addr = 0x08109A7E + z.hook_execution( + HookType.EXEC.INST, + single_instr_hook, + ip_low=target_addr, + ip_high=target_addr, + end_condition=lambda: True, + ) + + z.start() + + self.assertGreater(len(blocks), 15000) + self.assertEqual(target_addr, instr_addr[0]) + + def test_close_hook(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + + end_addr = [] + + def close_hook(): + end_addr.append( + z.internal_engine.thread_manager.completed_threads[0].getIP() + ) + + z.hook_close(close_hook) + + z.start() + + z.internal_engine.close() + + completed_addr = z.internal_engine.thread_manager.completed_threads[ + 0 + ].getIP() + + self.assertEqual(end_addr[0], completed_addr) + + def test_syscall_hook(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + + syscall_cnt = defaultdict(int) + + def syscall_hook(zelos, syscall_name, args, return_value): + syscall_cnt[syscall_name] += 1 + + z.hook_syscalls(HookType.SYSCALL.AFTER, syscall_hook) + + z.start() + + self.assertEqual(syscall_cnt["write"], 1) + self.assertEqual(syscall_cnt["brk"], 4) + self.assertEqual(syscall_cnt["set_thread_area"], 1) + self.assertEqual(syscall_cnt["uname"], 1) + self.assertEqual(syscall_cnt["readlink"], 1) + self.assertEqual(syscall_cnt["access"], 1) + self.assertEqual(syscall_cnt["fstat64"], 1) + self.assertEqual(syscall_cnt["exit_group"], 1) + + def test_step(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + addr = 0x0816348F + z.plugins.runner.run_to_addr(addr) + self.assertEqual(z.thread.getIP(), addr) + z.step() + self.assertEqual(z.thread.getIP(), 0x08163492) + + def test_stop(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + + def instr_hook(zelos, address, size): + zelos.stop() + + addr = 0x08109A7E + z.hook_execution( + HookType.EXEC.INST, + instr_hook, + ip_low=addr, + ip_high=addr, + end_condition=lambda: True, + ) + + z.start() + + tm = z.internal_engine.thread_manager + self.assertEqual(tm.num_active_threads(), 1) + self.assertEqual(z.thread.getIP(), addr) + + def test_end_thread(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + addr = 0x0816348F + z.plugins.runner.run_to_addr(addr) + z.end_thread() + tm = z.internal_engine.thread_manager + self.assertEqual(tm.num_active_threads(), 0) + + def test_breakpoint(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + addr = 0x0816348F + z.set_breakpoint(addr) + z.start() + + tm = z.internal_engine.thread_manager + self.assertEqual(tm.num_active_threads(), 1) + self.assertEqual(z.regs.getIP(), addr) + + z.remove_breakpoint(addr) + z.start() + + self.assertEqual(1, len(tm.completed_threads)) + + def test_syscall_breakpoint(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + + z.set_syscall_breakpoint("brk") + + z.start() + + brk = 0x0815B575 + + self.assertEqual(z.thread.getIP(), brk) + + z.remove_syscall_breakpoint("brk") + + z.start() + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_watchpoint(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + + z.set_watchpoint(0x081E9934, True, True) + + z.start() + + self.assertEqual(z.thread.getIP(), 0x081096F3) + + z.remove_watchpoint(0x081E9934) + + z.start() + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_date(self): + z = Zelos(None) + d = z.date + + self.assertEqual(d, "2019-02-02") + + z.date = "2019-03-03" + d = z.date + + self.assertEqual(d, "2019-03-03") + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_args.py b/tests/test_args.py new file mode 100644 index 0000000..964f366 --- /dev/null +++ b/tests/test_args.py @@ -0,0 +1,42 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# . +# ====================================================================== + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +import unittest + +from zelos.plugin import ArgFactory + + +class ArgFactoryTest(unittest.TestCase): + def test_create_args(self): + arg_factory = ArgFactory(lambda arg: "") + + args = arg_factory.gen_args( + [("int", "fd"), ("void*", "buf"), ("size_t", "count")], + [0x4, 0xDEADBEEF, 0x10], + ) + self.assertEqual(args.fd, 0x4) + self.assertEqual(args.buf, 0xDEADBEEF) + self.assertEqual(args.count, 0x10) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_emu_helper.py b/tests/test_emu_helper.py new file mode 100644 index 0000000..1db6592 --- /dev/null +++ b/tests/test_emu_helper.py @@ -0,0 +1,38 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# . +# ====================================================================== +from __future__ import absolute_import + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +import unittest + +from zelos import Zelos + + +class EmuHelperTest(unittest.TestCase): + def test_pack(self): + z = Zelos(None) + emu = z.internal_engine.emu + self.assertEqual(123, emu.unpack(emu.pack(123))) + self.assertEqual(-1, emu.unpack(emu.pack(-1), signed=True)) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_file_system.py b/tests/test_file_system.py new file mode 100644 index 0000000..cb59ba8 --- /dev/null +++ b/tests/test_file_system.py @@ -0,0 +1,139 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import os +import tempfile +import unittest + +from zelos import Zelos +from zelos.file_system import PathTranslator + + +def path_leaf(path): + import ntpath + + head, tail = ntpath.split(path) + return tail or ntpath.basename(head) + + +class TestPathTranslator(unittest.TestCase): + def test_add_file(self): + path_translator = PathTranslator("/") + + file = tempfile.NamedTemporaryFile() + path_translator.add_file(file.name, "/root/testfile") + + file_name = path_translator.emulated_path_to_host_path( + "/root/testfile" + ) + self.assertEqual(file_name, file.name) + + file_name = path_translator.emulated_path_to_host_path("/testfile") + self.assertIsNone(file_name) + + def test_order(self): + path_translator = PathTranslator("/") + + file = tempfile.NamedTemporaryFile() + folder = tempfile.TemporaryDirectory() + f1 = open( + path_translator.emulated_join(folder.name, path_leaf(file.name)), + "wb", + ) + f2 = open( + path_translator.emulated_join(folder.name, "testfile2"), "wb" + ) + path_translator.add_file(file.name, "/testfolder/testfile2") + path_translator.mount_folder(folder.name, "/testfolder") + + file_name = path_translator.emulated_path_to_host_path("/testfolder") + self.assertEqual(file_name, folder.name + os.path.sep) + + file_name = path_translator.emulated_path_to_host_path( + "/testfolder/testfile2" + ) + self.assertEqual(file_name, file.name) + + file_name = path_translator.emulated_path_to_host_path("/testfile2") + self.assertIsNone(file_name) + + f1.close() + f2.close() + folder.cleanup() + + def test_change_directory(self): + path_translator = PathTranslator("/") + + file = tempfile.NamedTemporaryFile() + path_translator.add_file(file.name, "/root/testfile") + + file_name = path_translator.emulated_path_to_host_path( + "/root/testfile" + ) + self.assertEqual(file_name, file.name) + + path_translator.change_working_directory("/") + file_name = path_translator.emulated_path_to_host_path("root/testfile") + + file_name = path_translator.emulated_path_to_host_path("testfile") + self.assertIsNone(file_name) + + path_translator.change_working_directory("/root") + file_name = path_translator.emulated_path_to_host_path("testfile") + self.assertEqual(file_name, file.name) + + # def test_convert_to_host_path(self): + # import platform + # if platform.system() == 'Linux': + # path_translator = PathTranslator("/") + + # host_path = path_translator._convert_to_host_path( + # 'this/that/whatever.txt') + # self.assertEqual('this/that/whatever.txt', host_path) + + # path_translator = PathTranslator("C:\\") + + # host_path = path_translator._convert_to_host_path( + # 'this\\that\\whatever.txt') + # self.assertEqual('this/that/whatever.txt', host_path) + + +class FileSystemTest(unittest.TestCase): + def test_get_file(self): + z = Zelos(None) + file_system = z.internal_engine.files + handle = file_system.create_file("test_file1") + self.assertEqual(file_system.get_filename(handle), "test_file1") + + def test_offsets(self): + z = Zelos(None) + file_system = z.internal_engine.files + handle_num = file_system.create_file("test_file1") + h = z.internal_engine.handles.get(handle_num) + + self.assertEqual(0, h.Offset) + h.Offset = 100 + h = z.internal_engine.handles.get(handle_num) + + self.assertEqual(100, h.Offset) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_handles.py b/tests/test_handles.py new file mode 100644 index 0000000..061a858 --- /dev/null +++ b/tests/test_handles.py @@ -0,0 +1,86 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# . +# ====================================================================== +from __future__ import absolute_import + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +import unittest + +from zelos import Zelos +from zelos.handles import FileHandle + + +class HandleTest(unittest.TestCase): + def test_add_handle(self): + z = Zelos(None) + handles = z.internal_engine.handles + handle_num = handles.new_file("test") + + file_handle = handles.get(handle_num) + self.assertEqual(file_handle.category(), "file") + self.assertEqual(file_handle.Refs, 1) + self.assertTrue(handles.exists(handle_num)) + + handles.close(handle_num) + self.assertEqual(file_handle.Refs, 0) + self.assertFalse(handles.exists(handle_num)) + + def test_overwrite_handle(self): + z = Zelos(None) + handles = z.internal_engine.handles + + num1 = handles.new_file("test1") + file_handle1 = handles.get(num1) + self.assertEqual(file_handle1.Name, "test1") + + num2 = handles.new_file("test2") + file_handle2 = handles.get(num2) + self.assertEqual(file_handle2.Name, "test2") + + handles.add_handle(file_handle2, num1) + # Now both num1 and num2 should refer to the second handle + self.assertIs(handles.get(num1), file_handle2) + self.assertIs(handles.get(num2), file_handle2) + + self.assertEqual(file_handle2.Refs, 2) + self.assertEqual(file_handle1.Refs, 0) + + def test_get_by(self): + z = Zelos(None) + handles = z.internal_engine.handles + + handles.new_pipe("pipe1") + file_num1 = handles.new_file("file1") + file_num2 = handles.new_file("file2") + file1 = handles.get(file_num1) + file2 = handles.get(file_num2) + self.assertIs(handles.get_by_name("file1"), file_num1) + + file_handles = handles.get_by_type(FileHandle) + self.assertSetEqual(set(file_handles), set([file1, file2])) + + self.assertIsNone(handles.get(file_num1, pid=0x1000)) + + handles.add_handle(file1, handle_num=file_num1, pid=0x1000) + self.assertIs(handles.get(file_num1, pid=0x1000), file1) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_heap_manager.py b/tests/test_heap_manager.py new file mode 100644 index 0000000..a549360 --- /dev/null +++ b/tests/test_heap_manager.py @@ -0,0 +1,93 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import unittest + +from zelos import Zelos + + +class HeapManagerTest(unittest.TestCase): + def test_alloc(self): + z = Zelos(None) + heap = z.internal_engine.memory.heap + + addr1 = heap.alloc(0x10, name="obj1") + addr2 = heap.alloc(0x10, name="obj2") + self.assertLessEqual(addr1 + 0x10, addr2) + self.assertEqual(2, len(heap.heap_objects)) + + def test_dealloc(self): + z = Zelos(None) + heap = z.internal_engine.memory.heap + starting_offset = heap.current_offset + + # Don't dealloc past the beginning + new_heap_start = heap.dealloc(0x10) + self.assertEqual(starting_offset, new_heap_start) + + # dealloc when appropriate + heap.alloc(0x100) + new_current_offset = heap.dealloc(0xF0) + self.assertEqual(starting_offset + 0x10, new_current_offset) + + # Dealloc when asking to go back to the beginning. + new_current_offset = heap.dealloc( + heap.current_offset - heap.heap_start + ) + self.assertEqual(starting_offset, new_current_offset) + + def test_bug_alloc_is_aligned(self): + # We should ensure that allocs are aligned, as some binaries + # (helloVB6-native.exe) do not work with unaligned memory + # allocs. + z = Zelos(None) + heap = z.internal_engine.memory.heap + + addr1 = heap.alloc(0x11, name="obj1") + addr2 = heap.alloc(0x3, name="obj2") + + self.assertEqual(0, addr1 % 4) + self.assertEqual(0, addr2 % 4) + self.assertEqual(2, len(heap.heap_objects)) + + def test_allocstr(self): + z = Zelos(None) + heap = z.internal_engine.memory.heap + s1 = "We are the future" + p_str, size = heap.allocstr(s1) + self.assertEqual(size, len(s1) + 1) + expected_s1 = z.internal_engine.memory.read_string(p_str) + self.assertEqual(s1, expected_s1) + + s2 = "you best believe it" + p_str, size = heap.allocstr(s2, is_wide=True) + self.assertEqual(size, len(s2) * 2 + 2) + expected_s2 = z.internal_engine.memory.read_wstring(p_str) + self.assertEqual(expected_s2, s2) + + s3 = "this is it" + size = z.internal_engine.memory.write_string(p_str, s3) + self.assertEqual(size, len(s3) + 1) + expected_s3 = z.internal_engine.memory.read_string(p_str) + self.assertEqual(expected_s3, s3) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_hook_manager.py b/tests/test_hook_manager.py new file mode 100644 index 0000000..6a9e58d --- /dev/null +++ b/tests/test_hook_manager.py @@ -0,0 +1,256 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import unittest + +from os import path +from unittest.mock import ANY, Mock + +from zelos import Zelos +from zelos.hooks import HookType + + +DATA_DIR = path.join(path.dirname(path.abspath(__file__)), "data") + + +class HookManagerTest(unittest.TestCase): + def test_hook_at(self): + z = Zelos(path.join(DATA_DIR, "static_elf_arm_helloworld")) + hm = z.internal_engine.current_process.hooks + action = Mock() + handle = "handle_val" + hm.add_hook(HookType.EXEC.INST, action, handle, name="test_hook") + z.internal_engine.plugins.runner.stop_at(0x2B3C8) + z.internal_engine.start() + action.assert_called() + + action.reset_mock() + # It is worth noting here that the hooks seem to run once for + # the last instruction, even if this instruction is not + # executed. In this test, we see that the mock is called + # 2 times, even though only 1 instruction is executed. + action.assert_not_called() + z.step() + action.assert_called() + + hm._delete_unicorn_hook(handle) + action.reset_mock() + action.assert_not_called() + z.step() + action.assert_not_called() + + def test_hook_syscall(self): + z = Zelos(path.join(DATA_DIR, "static_elf_arm_helloworld")) + + syscall_hook_data = [] + + def syscall_hook(p, sys_name, args, retval): + syscall_hook_data.append((sys_name, args, retval)) + + z.internal_engine.hook_manager.register_syscall_hook( + HookType.SYSCALL.AFTER, syscall_hook, "test_hook" + ) + + z.internal_engine.start() + + self.assertGreaterEqual(len(syscall_hook_data), 12) + self.assertEqual(syscall_hook_data[0][0], "brk") + self.assertEqual(syscall_hook_data[0][1].addr, 0) + self.assertGreater(syscall_hook_data[0][2], 0) + self.assertEqual(syscall_hook_data[-1][0], "exit_group") + + def test_temp_hook_at(self): + z = Zelos(path.join(DATA_DIR, "static_elf_arm_helloworld")) + action = Mock() + z.hook_execution( + HookType.EXEC.INST, + action, + ip_low=0x2B3C4, + ip_high=0x2B3C4, + end_condition=lambda: True, + ) + z.internal_engine.plugins.runner.stop_at(0x2B3C8) + z.internal_engine.start() + action.assert_called_once() + + action.reset_mock() + z.internal_engine.plugins.runner.run_to_addr(0x2B3C8) + action.assert_not_called() + + def test_temp_hook_at_with_end_condition(self): + z = Zelos( + path.join(DATA_DIR, "static_elf_arm_helloworld"), log="debug" + ) + action = Mock() + end_condition = Mock(side_effect=[False, True]) + z.hook_execution( + HookType.EXEC.INST, + action, + ip_low=0x2B3C4, + ip_high=0x2B3C4, + end_condition=end_condition, + ) + z.internal_engine.plugins.runner.stop_at(0x2B3C8) + z.internal_engine.start() + action.assert_called_once() + + action.reset_mock() + z.internal_engine.plugins.runner.run_to_addr(0x2B3C8) + action.assert_called_once() + + action.reset_mock() + z.internal_engine.plugins.runner.run_to_addr(0x2B3C8) + action.assert_not_called() + + def test_add_multiple_hooks(self): + z = Zelos(path.join(DATA_DIR, "static_elf_arm_helloworld")) + action1 = Mock(name="action1") + z.hook_execution(HookType.EXEC.INST, action1, name="test_hook") + action2 = Mock(name="action2") + z.hook_execution(HookType.EXEC.INST, action2, name="test_hook") + + z.step() + + action1.assert_called() + action2.assert_called() + + def test_add_multiple_temp_hooks(self): + z = Zelos(path.join(DATA_DIR, "static_elf_arm_helloworld")) + action1 = Mock() + z.hook_execution( + HookType.EXEC.INST, + action1, + ip_low=0x2B3C4, + ip_high=0x2B3C4, + end_condition=lambda: True, + ) + action2 = Mock() + z.hook_execution( + HookType.EXEC.INST, + action2, + ip_low=0x2B3C4, + ip_high=0x2B3C4, + end_condition=lambda: True, + ) + action3 = Mock() + z.hook_execution( + HookType.EXEC.INST, + action3, + ip_low=0x2B3C8, + ip_high=0x2B3C8, + end_condition=lambda: True, + ) + + z.internal_engine.plugins.runner.stop_at(0x2B3CC) + z.internal_engine.start() + action1.assert_called() + action2.assert_called() + action3.assert_called() + + def test_cross_process_hooks_new_process(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + + mock_hook = Mock() + + start_process_name = z.internal_engine.current_process.name + start_addr = 0x8048B70 + + hook_info = z.hook_execution( + HookType.EXEC.INST, mock_hook, name="test_hook" + ) + + mock_hook.assert_not_called() + z.step() + mock_hook.assert_called_once() + mock_hook.reset_mock() + + pid = z.internal_engine.processes.new_process("test_process") + p = z.internal_engine.processes.get_process(pid) + p.new_thread(start_addr) + p.memory.copy(z.internal_engine.memory) + + z.internal_engine.processes.load_next_process() + self.assertEqual( + z.internal_engine.current_process.name, "test_process" + ) + mock_hook.assert_not_called() + z.step() + mock_hook.assert_called_once_with(z, ANY, ANY) + mock_hook.reset_mock() + + z.delete_hook(hook_info) + mock_hook.assert_not_called() + z.step() + mock_hook.assert_not_called() + + z.internal_engine.processes.load_next_process() + self.assertEqual( + z.internal_engine.current_process.name, start_process_name + ) + mock_hook.assert_not_called() + z.step() + mock_hook.assert_not_called() + + def test_cross_process_hooks_existing_process(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + + mock_hook = Mock() + + start_process_name = z.internal_engine.current_process.name + start_addr = 0x8048B70 + + pid = z.internal_engine.processes.new_process("test_process") + p = z.internal_engine.processes.get_process(pid) + p.new_thread(start_addr) + p.memory.copy(z.internal_engine.memory) + + hook_info = z.hook_execution( + HookType.EXEC.INST, mock_hook, name="test_hook" + ) + mock_hook.assert_not_called() + z.step() + mock_hook.assert_called_once_with(z, ANY, ANY) + mock_hook.reset_mock() + + z.internal_engine.processes.load_next_process() + self.assertEqual( + z.internal_engine.current_process.name, "test_process" + ) + mock_hook.assert_not_called() + z.step() + mock_hook.assert_called_once_with(z, ANY, ANY) + mock_hook.reset_mock() + + z.delete_hook(hook_info) + mock_hook.assert_not_called() + z.step() + mock_hook.assert_not_called() + + z.internal_engine.processes.load_next_process() + self.assertEqual( + z.internal_engine.current_process.name, start_process_name + ) + mock_hook.assert_not_called() + z.step() + mock_hook.assert_not_called() + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_libutils.py b/tests/test_libutils.py new file mode 100644 index 0000000..493a45f --- /dev/null +++ b/tests/test_libutils.py @@ -0,0 +1,33 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# . +# ====================================================================== +from __future__ import absolute_import + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +import unittest + +import zelos.util as util + + +class UtilTest(unittest.TestCase): + def test_align(self): + self.assertEqual(0x1000, util.align(0x1000)) + self.assertEqual(0x2000, util.align(0x1001)) + self.assertEqual(0x1000, util.align(1)) + self.assertEqual(0x12000, util.align(0x11002)) + + self.assertEqual(0x14, util.align(0x11, alignment=0x4)) + self.assertEqual(0x10, util.align(0xF, alignment=0x4)) diff --git a/tests/test_linux_arm.py b/tests/test_linux_arm.py new file mode 100644 index 0000000..db5e13f --- /dev/null +++ b/tests/test_linux_arm.py @@ -0,0 +1,68 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import os +import unittest + +from os import path + +from zelos import Zelos + + +DATA_DIR = path.join(path.dirname(path.abspath(__file__)), "data") + + +class ZelosTest(unittest.TestCase): + def test_static_elf_verbose(self): + z = Zelos(path.join(DATA_DIR, "static_elf_arm_helloworld")) + z.internal_engine.set_verbose(True) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.start(timeout=3) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_static_elf(self): + z = Zelos(path.join(DATA_DIR, "static_elf_arm_helloworld")) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.start(timeout=10) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_dynamic_elf(self): + if os.name == "nt": + raise unittest.SkipTest( + "Skipping `test_dynamic_elf`: " + "Windows fatal exception: access violation" + ) + z = Zelos(path.join(DATA_DIR, "dynamic_elf_arm_helloworld")) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.start(timeout=10) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_linux_mips.py b/tests/test_linux_mips.py new file mode 100644 index 0000000..693b230 --- /dev/null +++ b/tests/test_linux_mips.py @@ -0,0 +1,57 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import os +import unittest + +from os import path + +from zelos import HookType, Zelos + + +DATA_DIR = path.join(path.dirname(path.abspath(__file__)), "data") + + +class ZelosTest(unittest.TestCase): + def test_static_elf_el(self): + z = Zelos(path.join(DATA_DIR, "static_elf_mipsel_mti_helloworld")) + z.internal_engine.set_hook_granularity(HookType.EXEC.BLOCK) + z.internal_engine.start(timeout=10) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_static_elf_eb(self): + if os.name == "nt": + raise unittest.SkipTest( + "Skipping `test_static_elf_eb`: Windows lief fails to parse" + ) + z = Zelos(path.join(DATA_DIR, "static_elf_mipseb_mti_helloworld")) + z.internal_engine.set_hook_granularity(HookType.EXEC.BLOCK) + z.internal_engine.start(timeout=10) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_linux_x64.py b/tests/test_linux_x64.py new file mode 100644 index 0000000..12e7f58 --- /dev/null +++ b/tests/test_linux_x64.py @@ -0,0 +1,70 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import unittest + +from os import path + +from zelos import Zelos + + +DATA_DIR = path.join(path.dirname(path.abspath(__file__)), "data") + + +class ZelosTest(unittest.TestCase): + def test_static_elf_unpatched(self): + z = Zelos(path.join(DATA_DIR, "static_elf_x64_helloworld")) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.start(timeout=3) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_static_elf(self): + z = Zelos(path.join(DATA_DIR, "static_elf_x64_helloworld")) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.start(timeout=3) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_dynamic_elf_unpatched(self): + z = Zelos(path.join(DATA_DIR, "dynamic_elf_x64_helloworld")) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.start(timeout=3) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_dynamic_elf(self): + z = Zelos(path.join(DATA_DIR, "dynamic_elf_x64_helloworld")) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.start(timeout=3) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_linux_x86.py b/tests/test_linux_x86.py new file mode 100644 index 0000000..49760cd --- /dev/null +++ b/tests/test_linux_x86.py @@ -0,0 +1,87 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import unittest + +from os import path + +from zelos import HookType, Zelos + + +DATA_DIR = path.join(path.dirname(path.abspath(__file__)), "data") + + +class ZelosTest(unittest.TestCase): + def test_static_elf_unpatched(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.start(timeout=3) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_static_elf(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.start(timeout=3) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_dynamic_elf(self): + z = Zelos( + path.join(DATA_DIR, "ld-linux.so"), "./dynamic_elf_helloworld" + ) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.files.add_file( + path.join(DATA_DIR, "dynamic_elf_helloworld") + ) + + z.internal_engine.start(timeout=3) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_dynamic_elf_directly(self): + z = Zelos(path.join(DATA_DIR, "dynamic_elf_helloworld")) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.start(timeout=3) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + def test_socket_elf(self): + z = Zelos(path.join(DATA_DIR, "static-socket-x86-musl")) + z.internal_engine.trace.threads_to_print.add("none") + z.internal_engine.set_hook_granularity(HookType.EXEC.BLOCK) + z.internal_engine.network.disable_whitelist() + z.internal_engine.start(timeout=5) + + self.assertEqual( + 1, len(z.internal_engine.thread_manager.completed_threads) + ) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_ltp_syscalls.py b/tests/test_ltp_syscalls.py new file mode 100644 index 0000000..8040827 --- /dev/null +++ b/tests/test_ltp_syscalls.py @@ -0,0 +1,157 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# . +# ====================================================================== +from __future__ import absolute_import + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +import unittest + +from os import path + +from zelos import Zelos +from zelos.threads import ThreadState + + +DATA_DIR = path.join(path.dirname(path.abspath(__file__)), "data") + + +class ZelosTest(unittest.TestCase): + def _ltp_run(self, bin_path, timeout=3): + z = Zelos(path.join(DATA_DIR, bin_path), log="ERROR") + z.internal_engine.set_verbose(False) + z.internal_engine.trace.threads_to_print.add("none") + + stdout = z.internal_engine.handles.get(1) + buffer = bytearray() + + def write_override(data): + buffer.extend(data) + + stdout.write = write_override + + z.internal_engine.start(timeout=timeout) + + # All threads should exit successfully + self.assertTrue( + all( + [ + t.state == ThreadState.SUCCESS + for t in z.internal_engine.processes.get_all_threads() + ] + ), + msg=z.internal_engine.processes.__str__(), + ) + + return buffer + + def test_brk(self): + buffer = self._ltp_run("ltp_x64/syscalls/brk01") + self.assertIn("passed 1", str(buffer)) + + def test_chdir(self): + # self._ltp_run('ltp_x64/syscalls/chdir01') + self._ltp_run("ltp_x64/syscalls/chdir02") + # self._ltp_run('ltp_x64/syscalls/chdir03') + # self._ltp_run('ltp_x64/syscalls/chdir04') + + def test_fork(self): + # self._ltp_run('ltp_x64/syscalls/fork01') + self._ltp_run("ltp_x64/syscalls/fork02") + self._ltp_run("ltp_x64/syscalls/fork03") + # self._ltp_run('ltp_x64/syscalls/fork04') + # test fork05 only for x32 + # self._ltp_run('ltp_x64/syscalls/fork06') + # self._ltp_run('ltp_x64/syscalls/fork07') + # self._ltp_run('ltp_x64/syscalls/fork08') + # self._ltp_run('ltp_x64/syscalls/fork09') + + def test_getpid(self): + buffer = self._ltp_run("ltp_x64/syscalls/getpid01") + self.assertEqual(1, str(buffer).count("TPASS")) + buffer = self._ltp_run("ltp_x64/syscalls/getpid02") + self.assertEqual(1, str(buffer).count("TPASS")) + + def test_getppid(self): + buffer = self._ltp_run("ltp_x64/syscalls/getppid01") + self.assertEqual(1, str(buffer).count("TPASS")) + self._ltp_run("ltp_x64/syscalls/getppid02") + + def test_kill(self): + pass + # self._ltp_run('ltp_x64/syscalls/kill01') + # self._ltp_run('ltp_x64/syscalls/kill02') + # Needs to kill child process instead of letting it exit. + # self._ltp_run('ltp_x64/syscalls/kill03') + # Needs proc/sys/kernel/pid_max + # self._ltp_run('ltp_x64/syscalls/kill04') + # self._ltp_run('ltp_x64/syscalls/kill05') + # self._ltp_run('ltp_x64/syscalls/kill06') + # self._ltp_run('ltp_x64/syscalls/kill07') + # self._ltp_run('ltp_x64/syscalls/kill08') + # Needs to kill child process instead of letting it exit. + # self._ltp_run('ltp_x64/syscalls/kill09') + + def test_open(self): + # self._ltp_run('ltp_x64/syscalls/open01') + # self._ltp_run('ltp_x64/syscalls/open02') + self._ltp_run("ltp_x64/syscalls/open03") + # self._ltp_run('ltp_x64/syscalls/open04') + # self._ltp_run('ltp_x64/syscalls/open05') + # self._ltp_run('ltp_x64/syscalls/open06') + # self._ltp_run('ltp_x64/syscalls/open07') + # self._ltp_run('ltp_x64/syscalls/open08') + # self._ltp_run('ltp_x64/syscalls/open09') + + def test_pipe(self): + self._ltp_run("ltp_x64/syscalls/pipe01") + # self._ltp_run('ltp_x64/syscalls/pipe02') + self._ltp_run("ltp_x64/syscalls/pipe03") + # self._ltp_run('ltp_x64/syscalls/pipe04') + # self._ltp_run('ltp_x64/syscalls/pipe05') + # self._ltp_run('ltp_x64/syscalls/pipe06') + # self._ltp_run('ltp_x64/syscalls/pipe07') + # self._ltp_run('ltp_x64/syscalls/pipe08') + self._ltp_run("ltp_x64/syscalls/pipe09") + + def test_read(self): + buffer = self._ltp_run("ltp_x64/syscalls/read01") + self.assertIn("passed 1", str(buffer)) + # buffer = self._ltp_run('ltp_x64/syscalls/read02') + # buffer = self._ltp_run('ltp_x64/syscalls/read03') + # buffer = self._ltp_run('ltp_x64/syscalls/read04') + + def test_rmdir(self): + buffer = self._ltp_run("ltp_x64/syscalls/rmdir01") + self.assertIn("passed 1", str(buffer)) + # buffer = self._ltp_run('ltp_x64/syscalls/rmdir02') + # buffer = self._ltp_run('ltp_x64/syscalls/rmdir03') + + def test_sbrk(self): + buffer = self._ltp_run("ltp_x64/syscalls/sbrk01") + self.assertEqual(2, str(buffer).count("TPASS")) + buffer = self._ltp_run("ltp_x64/syscalls/sbrk02") + # sbrk03 only works on 32bit + + def test_vfork(self): + self._ltp_run("ltp_x64/syscalls/vfork01") + # self._ltp_run('ltp_x64/syscalls/vfork02') + + def test_write(self): + # self._ltp_run('ltp_x64/syscalls/write01') + self._ltp_run("ltp_x64/syscalls/write02") + # self._ltp_run('ltp_x64/syscalls/write03') + # self._ltp_run('ltp_x64/syscalls/write04') + # self._ltp_run('ltp_x64/syscalls/write05') diff --git a/tests/test_memory_manager.py b/tests/test_memory_manager.py new file mode 100644 index 0000000..9b236fe --- /dev/null +++ b/tests/test_memory_manager.py @@ -0,0 +1,224 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# . +# ====================================================================== +from __future__ import absolute_import + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +import unittest + +from unicorn import UC_ARCH_X86, UC_MODE_32, Uc + +from zelos import Zelos +from zelos.memory import Memory, OutOfMemoryException, Section + + +class MemoryTest(unittest.TestCase): + def test_memory_manager_map_anywhere(self): + mm = Memory(Uc(UC_ARCH_X86, UC_MODE_32), None, 32) + address1 = mm.map_anywhere(0x1000, "name1", "size1") + + self.assertEqual( + mm.memory_info[address1], + Section(mm.emu, address1, 0x1000, "name1", "size1", ""), + ) + + address2 = mm.map_anywhere(0x2000, "name2", "size2") + + self.assertEqual( + mm.memory_info[address1], + Section(mm.emu, address1, 0x1000, "name1", "size1", ""), + ) + self.assertEqual( + mm.memory_info[address2], + Section(mm.emu, address2, 0x2000, "name2", "size2", ""), + ) + + mm.unmap(address1, 0x1000) + self.assertNotIn(address1, mm.memory_info) + self.assertEqual( + mm.memory_info[address2], + Section(mm.emu, address2, 0x2000, "name2", "size2", ""), + ) + + def test_map_anywhere_bounded(self): + # Check mapping when given bounds + mm = Memory(Uc(UC_ARCH_X86, UC_MODE_32), None, 32) + min_addr = 0x10000 + max_addr = 0x12000 + address1 = mm.map_anywhere( + 0x1000, + min_addr=min_addr, + max_addr=max_addr, + name="name1", + kind="size1", + ) + + self.assertEqual( + mm.memory_info[address1], + Section(mm.emu, address1, 0x1000, "name1", "size1", ""), + ) + self.assertGreaterEqual(address1, min_addr) + self.assertLessEqual(address1, max_addr) + + def test_map_anywhere_bounded_preexisting_sections(self): + mm = Memory(Uc(UC_ARCH_X86, UC_MODE_32), None, 32) + mm.map(0x10000, 0x1000) + mm.map(0x15000, 0x1000) + min_addr = 0x12000 + max_addr = 0x14000 + address1 = mm.map_anywhere( + 0x1000, + min_addr=min_addr, + max_addr=max_addr, + name="name1", + kind="size1", + ) + + self.assertEqual( + mm.memory_info[address1], + Section(mm.emu, address1, 0x1000, "name1", "size1", ""), + ) + self.assertGreaterEqual(address1, min_addr) + self.assertLessEqual(address1, max_addr) + + def test_alloc_at(self): + z = Zelos(None) + mm = z.internal_engine.memory + mm._alloc_at( + "Test1", + "test_mem", + "main_module", + 0x400000, + 0x10000, + min_addr=0x400000, + max_addr=0x500000, + ) + mm._alloc_at( + "Test2", + "test_mem", + "main_module", + 0x400000, + 0x10000, + min_addr=0x400000, + max_addr=0x500000, + ) + mm._alloc_at( + "Test3", + "test_mem", + "main_module", + 0x410000, + 0x10000, + min_addr=0x400000, + max_addr=0x500000, + ) + self.assertEqual(mm.memory_info[0x400000].name, "Test1") + self.assertEqual(mm.memory_info[0x410000].name, "Test2") + self.assertEqual(mm.memory_info[0x420000].name, "Test3") + + def test_read_int(self): + z = Zelos(None) + mm = z.internal_engine.memory + mm.map(0x88880000, 0x1000) + mm.write(0x88880000, b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a") + + self.assertEqual(mm.read_int(0x88880000, 1), 0x01) + self.assertEqual(mm.read_int(0x88880000, 2), 0x0201) + self.assertEqual(mm.read_int(0x88880000, 4), 0x04030201) + self.assertEqual(mm.read_int(0x88880000, 8), 0x0807060504030201) + self.assertRaises(Exception, mm.read_int, 0x88880000, 3) + + def test_write_int(self): + z = Zelos(None) + mm = z.internal_engine.memory + mm.map(0x88880000, 0x1000) + mm.write(0x88880004, b"\x00\x00\x00\x00") + + mm.write_int(0x88880000, 0x0807060504030201) + self.assertEqual(mm.read(0x88880000, 5), b"\x01\x02\x03\x04\x00") + + mm.write_int(0x88880000, 0x121110, 2) + self.assertEqual(mm.read(0x88880000, 4), b"\x10\x11\x03\x04") + + mm.write_int(0x88880000, 0x0101, 1) + self.assertEqual(mm.read(0x88880000, 2), b"\x01\x11") + + def test_read_writeint(self): + z = Zelos(None) + mm = z.internal_engine.memory + + address1 = mm.map_anywhere(0x1000, "name1", "size1") + mm.write_int(address1, 10) + self.assertEqual(10, z.internal_engine.memory.read_int(address1)) + + self.assertEqual(10, mm.read_int(address1)) + + def test_str_methods(self): + z = Zelos(None) + mm = z.internal_engine.memory + mm.map(0x10000, 0x1000) + mm.write(0x10000, b"\xff" * 0x100) + mm.write_string(0x10004, "TestString") + self.assertEqual(mm.read_string(0x10004), "TestString") + self.assertEqual( + mm.read(0x10004, len("TestString") + 2), + "TestString\x00".encode() + b"\xff", + ) + + mm.write_string(0x10024, "TestString", terminal_null_byte=False) + self.assertEqual( + mm.read(0x10024, len("TestString") + 1), + "TestString".encode() + b"\xff", + ) + self.assertEqual( + mm.read_string(0x10024, size=len("TestString")), "TestString" + ) + + def test_wstr_methods(self): + z = Zelos(None) + mm = z.internal_engine.memory + mm.map(0x10000, 0x1000) + mm.write(0x10000, b"\xff" * 0x100) + mm.write_wstring(0x10004, "Test") + self.assertEqual( + mm.read(0x10004, len("Test") * 2 + 3), + b"T\x00e\x00s\x00t\x00\x00\x00" + b"\xff", + ) + self.assertEqual(mm.read_wstring(0x10004), "Test") + + mm.write_wstring(0x10024, "Test", terminal_null_byte=False) + self.assertEqual( + mm.read(0x10024, len("Test") * 2 + 1), + "T\x00e\x00s\x00t\x00".encode() + b"\xff", + ) + self.assertEqual( + mm.read_wstring(0x10024, size=len("Test") * 2), "Test" + ) + + def test_memory_failure(self): + z = Zelos(None) + mm = z.internal_engine.memory + z.internal_engine.thread_manager.logger.warning("Testing logging") + + with self.assertRaises(OutOfMemoryException): + mm.map(0x1000, 1024 * 1024 * 1024 * 10) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_processes.py b/tests/test_processes.py new file mode 100644 index 0000000..891081e --- /dev/null +++ b/tests/test_processes.py @@ -0,0 +1,105 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +from __future__ import absolute_import + +import unittest + +from os import path + +from zelos import Zelos + + +DATA_DIR = path.join(path.dirname(path.abspath(__file__)), "data") + + +class ProcessesTest(unittest.TestCase): + def test_emu_swap(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + z.internal_engine.current_process.threads.swap_with_next_thread() + + self.assertEqual(z.internal_engine.processes.num_active_processes(), 1) + pid1 = z.internal_engine.current_process.pid + self.assertEqual( + z.internal_engine.thread_manager.num_active_threads(), 1 + ) + z.internal_engine.current_thread.setIP(0x2000) + + pid2 = z.internal_engine.processes.new_process("Process_2", pid1) + p2 = z.internal_engine.processes.get_process(pid2) + p2.new_thread(0x1000, "thread2") + p2.threads.swap_with_next_thread() + + self.assertEqual(z.internal_engine.current_thread.getIP(), 0x2000) + z.internal_engine.processes.load_process(pid2) + self.assertEqual(z.internal_engine.current_thread.getIP(), 0x1000) + + def test_memory_swap(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + + self.assertEqual(z.internal_engine.processes.num_active_processes(), 1) + pid1 = z.internal_engine.current_process.pid + z.internal_engine.current_process.memory.map(0xDEADB000, 0x1000) + self.assertEqual( + z.internal_engine.memory.read(0xDEADB000, 0x4), b"\x00" * 4 + ) + z.internal_engine.memory.write(0xDEADB000, b"\x01\x02\x03\x04") + self.assertEqual( + z.internal_engine.memory.read(0xDEADB000, 0x4), b"\x01\x02\x03\x04" + ) + + pid2 = z.internal_engine.processes.new_process("Process_2", pid1) + z.internal_engine.processes.load_process(pid2) + z.internal_engine.current_process.memory.map(0xDEADB000, 0x1000) + self.assertEqual( + z.internal_engine.memory.read(0xDEADB000, 0x4), b"\x00" * 4 + ) + z.internal_engine.memory.write(0xDEADB000, b"\x05\x05\x05\x05") + self.assertEqual( + z.internal_engine.memory.read(0xDEADB000, 0x4), b"\x05\x05\x05\x05" + ) + + def test_sys_fork(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + z.internal_engine.processes.swap_with_next_thread() + self.assertEqual(z.internal_engine.processes.num_active_processes(), 1) + p1 = z.internal_engine.current_process + + # Make handle_syscall call fork + z.internal_engine.zos.syscall_manager.find_syscall_name_by_number = ( + lambda x: "fork" + ) + z.internal_engine.zos.syscall_manager.handle_syscall(p1) + + new_pid = p1.emu.get_reg("eax") + p2 = z.internal_engine.processes.get_process(new_pid) + # We add 2 to the IP of P1 because, since Zelos is not actually + # running, the callback of stop_and_exec that is defined inside + # handle_syscall is never invoked and the parent process IP is + # never incremented. + # This is just for this test, as during normal execution this + # would not be the case. + self.assertEqual(p1.emu.getIP() + 2, p2.emu.getIP()) + self.assertEqual(p2.emu.get_reg("eax"), 0) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 0000000..7c6d180 --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,83 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +from __future__ import absolute_import + +import unittest + +from os import path + +from zelos import Zelos + + +DATA_DIR = path.join(path.dirname(path.abspath(__file__)), "data") + + +class RunnerTest(unittest.TestCase): + def test_run_to_addr(self): + z = Zelos(path.join(DATA_DIR, "dynamic_elf_helloworld")) + z.internal_engine.thread_manager.swap_with_thread("main") + + z.internal_engine.plugins.runner.run_to_addr(0x0B01B3CA) + self.assertEqual(z.internal_engine.current_thread.getIP(), 0x0B01B3CA) + + def test_stop_when(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + z.internal_engine.thread_manager.swap_with_thread("main") + + def stop(): + triggers = z.internal_engine.triggers + return any( + [ + syscall.name == "fstat64" + for syscall_list in triggers.syscalls_called.values() + for syscall in syscall_list + ] + ) + + z.internal_engine.plugins.runner.stop_when(stop) + z.internal_engine.start() + self.assertEqual(z.internal_engine.current_thread.getIP(), 0x081356E2) + + def test_run_to_ret(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + z.internal_engine.thread_manager.swap_with_thread("main") + + z.internal_engine.step() + z.internal_engine.plugins.runner.next_ret() + self.assertEqual( + str(z.internal_engine.current_thread.getIP()), str(0x08048B80) + ) + + def test_run_to_next_write(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + z.internal_engine.thread_manager.swap_with_thread("main") + + z.internal_engine.plugins.runner.next_write(0xFF08EDD0) + t = z.internal_engine.current_thread + self.assertEqual( + t.getIP(), 0x8135778, f"IP is 0x{t.getIP():x} vs. 0x8135778" + ) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_script_interface.py b/tests/test_script_interface.py new file mode 100644 index 0000000..a8f8e0c --- /dev/null +++ b/tests/test_script_interface.py @@ -0,0 +1,39 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import unittest + +from zelos import Zelos + + +class ZelosTest(unittest.TestCase): + def test_invalid_args(self): + self.assertRaises( + SystemExit, + Zelos, + "tests/data/static_elf_helloworld", + invalid_arg="testval", + ) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_syscall_limiter.py b/tests/test_syscall_limiter.py new file mode 100644 index 0000000..b8de2c3 --- /dev/null +++ b/tests/test_syscall_limiter.py @@ -0,0 +1,67 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + + +# . +# ====================================================================== + +# import io +import unittest + +from os import path + +from zelos import Zelos + + +# from zelos.api.zelos_api import ZelosCmdline + +DATA_DIR = path.dirname(path.abspath(__file__)) + + +class SyscallLimiterTest(unittest.TestCase): + def test_syscall_limit(self): + z = Zelos( + path.join(DATA_DIR, "data", "static_elf_helloworld"), + syscall_limit=5, + ) + z.start() + self.assertEqual( + z.internal_engine.plugins.syscalllimiter.syscall_cnt, 5 + ) + + def test_thread_limit(self): + z = Zelos( + path.join(DATA_DIR, "data", "static_elf_helloworld"), + syscall_thread_limit=5, + ) + z.start() + self.assertEqual( + z.internal_engine.plugins.syscalllimiter.syscall_cnt, 5 + ) + + # def test_syscall_limit_plugin(self): + + # filepath = path.join(DATA_DIR, "data", "static_elf_helloworld") + # z = ZelosCmdline(f"--syscall_limit 5 {filepath}") + # z.start() + + # self.assertEqual( + # z.internal_engine.plugins.syscalllimiter.syscall_cnt, 5 + # ) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_syscall_manager.py b/tests/test_syscall_manager.py new file mode 100644 index 0000000..9e3a827 --- /dev/null +++ b/tests/test_syscall_manager.py @@ -0,0 +1,42 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import unittest + +from unittest.mock import Mock + +from zelos import Zelos +from zelos.ext.platforms.linux.syscall_manager import X86SyscallManager + + +class SyscallManagerTest(unittest.TestCase): + def test_syscall_override(self): + z = Zelos(None) + sm = X86SyscallManager(z.internal_engine) + sm.register_overrides({"getuid": [1, 2]}) + sys_func = sm._name2syscall_func["getuid"] + p = Mock() + self.assertEqual(1, sys_func(sm, p)) + self.assertEqual(2, sys_func(sm, p)) + sys_func(sm, p) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_thread_manager.py b/tests/test_thread_manager.py new file mode 100644 index 0000000..f55c8b4 --- /dev/null +++ b/tests/test_thread_manager.py @@ -0,0 +1,219 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import unittest + +from os import path + +from zelos import Zelos +from zelos.threads import ThreadState + + +DATA_DIR = path.join(path.dirname(path.abspath(__file__)), "data") + + +class ThreadManagerTest(unittest.TestCase): + def test_swapWithNextThread_oneThread(self): + z = Zelos(None) + + tman = z.internal_engine.thread_manager + self.assertIsNone(tman.current_thread) + + tman.new_thread(0x1000, 0x1234, name="thread_1", priority=1) + self.assertIsNone(tman.current_thread) + self.assertEqual(1, tman.num_active_threads()) + + tman.swap_with_next_thread() + self.assertIsNotNone(tman.current_thread) + self.assertEqual(1, tman.num_active_threads()) + + def test_swapWithThread_oneThread(self): + z = Zelos(None) + + tman = z.internal_engine.thread_manager + self.assertIsNone(tman.current_thread) + + thread_1 = tman.new_thread(0x1000, 0x1234, name="thread_1", priority=1) + self.assertIsNone(tman.current_thread) + self.assertEqual(1, tman.num_active_threads()) + + tman.swap_with_thread(tid=thread_1.id) + self.assertIsNotNone(tman.current_thread) + self.assertEqual(1, tman.num_active_threads()) + + def test_swapWithNextThread_TwoThreads(self): + z = Zelos(None) + + tman = z.internal_engine.thread_manager + self.assertIsNone(tman.current_thread) + + tman.new_thread(0x1000, 0x1234, name="thread_1", priority=1) + tman.new_thread(0x2000, 0x1235, name="thread_2", priority=1) + self.assertIsNone(tman.current_thread) + self.assertEqual(2, tman.num_active_threads()) + + tman.swap_with_next_thread() + self.assertIsNotNone(tman.current_thread) + self.assertEqual(2, tman.num_active_threads()) + self.assertEqual("thread_1", tman.current_thread.name) + + tman.swap_with_next_thread() + self.assertIsNotNone(tman.current_thread) + self.assertEqual("thread_2", tman.current_thread.name) + self.assertEqual(2, tman.num_active_threads()) + + def test_changeThreadPriority(self): + z = Zelos(None) + + tman = z.internal_engine.thread_manager + self.assertIsNone(tman.current_thread) + + tman.new_thread(0x1000, 0x1234, name="thread_1", priority=1) + tman.new_thread(0x2000, 0x1235, name="thread_2", priority=2) + tman.new_thread(0x2000, 0x1236, name="thread_3", priority=3) + + tman.swap_with_next_thread() + self.assertIsNotNone(tman.current_thread) + self.assertEqual(3, tman.num_active_threads()) + self.assertEqual("thread_3", tman.current_thread.name) + + tman._reset() + self.assertIsNone(tman.current_thread) + + tman.new_thread(0x1000, 0x1234, name="thread_1", priority=1) + tman.new_thread(0x2000, 0x1235, name="thread_2", priority=2) + tman.new_thread(0x2000, 0x1236, name="thread_3", priority=3) + + tman.change_thread_priority("thread_3", 1) + tman.change_thread_priority("thread_1", 3) + + tman.swap_with_next_thread() + self.assertIsNotNone(tman.current_thread) + self.assertEqual(3, tman.num_active_threads()) + self.assertEqual("thread_1", tman.current_thread.name) + + tman.swap_with_next_thread() + self.assertEqual("thread_1", tman.current_thread.name) + + tman.change_thread_priority("thread_1", 1) + tman.swap_with_next_thread() + self.assertEqual("thread_2", tman.current_thread.name) + + def test_save_after_edit(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + tman = z.internal_engine.thread_manager + tman.swap_with_next_thread() + + self.assertIsNotNone(tman.current_thread) + self.assertEqual(1, tman.num_active_threads()) + self.assertEqual(tman.current_thread.state, ThreadState.RUNNING) + data = tman._save_state() + + tman.pause_current_thread() + self.assertEqual(0, tman.num_active_threads()) + + tman._load_state(data) + self.assertIsNotNone(tman.current_thread) + self.assertEqual(1, tman.num_active_threads()) + self.assertEqual(tman.current_thread.state, ThreadState.RUNNING) + + tman.pause_current_thread() + self.assertEqual(0, tman.num_active_threads()) + + tman._load_state(data) + self.assertIsNotNone(tman.current_thread) + self.assertEqual(1, tman.num_active_threads()) + self.assertEqual(tman.current_thread.state, ThreadState.RUNNING) + + def test_saveload(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + tman = z.internal_engine.thread_manager + tman._reset() + + tman.new_thread(0x1000, 0x1234, name="thread_1", priority=1) + tman.new_thread(0x2000, 0x1235, name="thread_2", priority=2) + tman.new_thread(0x2000, 0x1236, name="thread_3", priority=3) + tman.swap_with_next_thread() + + data = tman._save_state() + self.assertEqual(3, tman.num_active_threads()) + + tman._reset() + tman._load_state(data) + self.assertIsNotNone(tman.current_thread) + self.assertEqual(3, tman.num_active_threads()) + self.assertEqual("thread_3", tman.current_thread.name) + + def test_saveload_current_thread_none(self): + z = Zelos(path.join(DATA_DIR, "static_elf_helloworld")) + tman = z.internal_engine.thread_manager + tman._reset() + + tman.new_thread(0x1000, 0x1234, name="thread_1", priority=1) + tman.new_thread(0x2000, 0x1235, name="thread_2", priority=2) + tman.new_thread(0x2000, 0x1236, name="thread_3", priority=3) + self.assertIsNone(tman.current_thread) + data = tman._save_state() + self.assertEqual(3, tman.num_active_threads()) + tman.swap_with_next_thread() + + tman._load_state(data) + self.assertIsNone(tman.current_thread) + self.assertEqual(3, tman.num_active_threads()) + + def test_get_thread_by_name(self): + z = Zelos(None) + tman = z.internal_engine.thread_manager + tman.new_thread(0x1000, 0x1234, name="thread_1", priority=1) + tman.new_thread(0x2000, 0x1235, name="thread_2", priority=2) + t = tman.get_thread_by_name("thread_1") + self.assertIsNotNone(t) + self.assertEqual(t.priority, 1) + + t = tman.get_thread_by_name("thread_3") + self.assertIsNone(t) + + def test_block_counts(self): + z = Zelos(None) + tman = z.internal_engine.thread_manager + main_thread = tman.new_thread(0x1000, 0x1234, name="main", priority=1) + child_thread = tman.new_thread( + 0x1000, 0x1235, name="child", priority=1 + ) + tman.swap_with_thread(tid=main_thread.id) + tman.record_block(0x1000) + tman.record_block(0x2000) + tman.record_block(0x1000) + tman.record_block(0x3000) + + self.assertTrue(tman.block_seen_before(0x2000)) + self.assertFalse(tman.block_seen_before(0x2010)) + + tman.swap_with_thread(tid=child_thread.id) + tman.record_block(0x1000) + tman.record_block(0x4000) + + self.assertEqual(tman.num_unique_blocks(thread_name="main"), 3) + self.assertEqual(tman.num_unique_blocks(thread_name="child"), 2) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_unicorn.py b/tests/test_unicorn.py new file mode 100644 index 0000000..1130829 --- /dev/null +++ b/tests/test_unicorn.py @@ -0,0 +1,262 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== +import unittest + +from unicorn import ( + UC_ARCH_X86, + UC_HOOK_CODE, + UC_HOOK_INSN, + UC_MODE_32, + UC_MODE_64, + Uc, + UcError, +) +from unicorn.x86_const import ( + UC_X86_INS_SYSCALL, + UC_X86_REG_EAX, + UC_X86_REG_EIP, + UC_X86_REG_RAX, +) + +from zelos import Zelos + + +class UnicornTest(unittest.TestCase): + def test_fail_end_partial_addr(self): + w = Zelos(None) + w.internal_engine.interrupt_handler.disable() + uc = w.internal_engine.emu + uc.mem_map(0, 0x2000) + self.assertRaises( + UcError, uc.emu_start, 0x1000, 0x1007 + ) # ends inbetween instruction + + def test_hooks(self): + record = [] + + def hook(uc, address, size, userdata): + record.append(address) + + uc = Uc(UC_ARCH_X86, UC_MODE_32) + uc.mem_map(0, 0x2000) + uc.hook_add(UC_HOOK_CODE, hook) + uc.emu_start(0x1000, 0x1006) + self.assertListEqual(record, [0x1000, 0x1002, 0x1004]) + + def test_setip_before_emustop(self): + record = [] + uc = Uc(UC_ARCH_X86, UC_MODE_32) + + def hook(uc, address, size, userdata): + record.append(address) + + def hook_stop(uc, address, size, userdata): + if address == 0x1002: + uc.reg_write(UC_X86_REG_EIP, 0x1006) + uc.emu_stop() + + uc.mem_map(0, 0x2000) + uc.hook_add(UC_HOOK_CODE, hook) + uc.hook_add(UC_HOOK_CODE, hook_stop) + uc.emu_start(0x1000, 0x1008) + self.assertListEqual(record, [0x1000, 0x1002, 0x1006]) + + def test_setip_after_emustop(self): + record = [] + uc = Uc(UC_ARCH_X86, UC_MODE_32) + + def hook(uc, address, size, userdata): + record.append(address) + + def hook_stop(uc, address, size, userdata): + if address == 0x1002: + uc.emu_stop() + uc.reg_write(UC_X86_REG_EIP, 0x1006) + + uc.mem_map(0, 0x2000) + uc.hook_add(UC_HOOK_CODE, hook) + uc.hook_add(UC_HOOK_CODE, hook_stop) + uc.emu_start(0x1000, 0x1008) + self.assertListEqual(record, [0x1000, 0x1002, 0x1006]) + + def test_multiprocess(self): + record = [] + + def hook(uc, address, size, userdata): + record.append(address) + + uc1 = Uc(UC_ARCH_X86, UC_MODE_32) + uc1.hook_add(UC_HOOK_CODE, hook) + uc1.mem_map(0, 0x2000) + + uc2 = Uc(UC_ARCH_X86, UC_MODE_32) + uc2.hook_add(UC_HOOK_CODE, hook) + uc2.mem_map(0, 0x2000) + + uc1.emu_start(0x1000, 0x1006) + self.assertListEqual(record, [0x1000, 0x1002, 0x1004]) + + uc2.emu_start(0x1000, 0x1006) + self.assertListEqual( + record, [0x1000, 0x1002, 0x1004, 0x1000, 0x1002, 0x1004] + ) + + uc1.reg_write(UC_X86_REG_EAX, 5) + context1 = uc1.context_save() + + uc2.reg_write(UC_X86_REG_EAX, 6) + context2 = uc2.context_save() + + self.assertEqual(uc1.reg_read(UC_X86_REG_EAX), 5) + self.assertEqual(uc2.reg_read(UC_X86_REG_EAX), 6) + + uc2.context_restore(context1) + uc1.context_restore(context2) + self.assertEqual(uc1.reg_read(UC_X86_REG_EAX), 6) + self.assertEqual(uc2.reg_read(UC_X86_REG_EAX), 5) + + def test_what_executes_when_switching_eip(self): + record = [] + + def hook(uc, address, size, userdata): + record.append(address) + if address == 0x1002: + uc.reg_write(UC_X86_REG_EIP, 0x1006) + + w = Zelos(None) + w.internal_engine.interrupt_handler.disable() + uc = w.internal_engine.emu + uc.mem_map(0, 0x2000) + # Push 1, Push 2, push 3... that way you can tell what + # instructions have actually executed + # by investigating the stack + uc.mem_write(0x1000, b"\x6A\x01\x6A\x02\x6A\x03\x6A\x04\x6A\x05") + w.internal_engine.emu.setSP(0x100) + + uc.hook_add(UC_HOOK_CODE, hook) + uc.emu_start(0x1000, 0x1008) + self.assertListEqual(record, [0x1000, 0x1002, 0x1006]) + self.assertEqual(0xF8, w.internal_engine.emu.getSP()) + + self.assertEqual(4, w.internal_engine.emu.getstack(0)) + self.assertEqual(1, w.internal_engine.emu.getstack(1)) + self.assertEqual(0, w.internal_engine.emu.getstack(2)) + + def test_many_prefixes(self): + # These two instructions need to fail, since they are too long. + + w = Zelos(None) + uc = w.internal_engine.emu + uc.mem_map(0, 0x2000) + uc.mem_write( + 0x1000, + b"\x3E\x3E\x3E\x3E\x3E\x3E\x3E\x3E\x3E\x3E\x3E\xA1\xF0\x10" + b"\x00\x00", + ) + try: + uc.emu_start(0x1000, 0x1010) + self.assertEqual(1, 0) + except Exception: + pass + # self.assertEqual(uc.getIP(), 0x1010) + uc.mem_write( + 0x1100, + b"\x3E\x3E\x3E\x3E\x3E\x3E\x3E\x3E\x3E\x3E\x3E\x3E\x3E" + b"\x3E\x3E\xA1\xF0\x10\x00\x00", + ) + try: + uc.emu_start(0x1100, 0x1114) + self.assertEqual(1, 0) + except Exception: + pass + # self.assertEqual(uc.getIP(), 0x1114) + + def test_x86_64_syscall(self): + print("Emulate x86_64 code with 'syscall' instruction") + ADDRESS = 0x1000000 + X86_CODE64_SYSCALL = b"\x0f\x05" # SYSCALL + # Initialize emulator in X86-64bit mode + mu = Uc(UC_ARCH_X86, UC_MODE_64) + + # map 2MB memory for this emulation + mu.mem_map(ADDRESS, 2 * 1024 * 1024) + + # write machine code to be emulated to memory + mu.mem_write(ADDRESS, X86_CODE64_SYSCALL) + + def hook_syscall(mu, user_data): + rax = mu.reg_read(UC_X86_REG_RAX) + if rax == 0x100: + mu.reg_write(UC_X86_REG_RAX, 0x200) + else: + print("ERROR: was not expecting rax=%d in syscall" % rax) + + # hook interrupts for syscall + mu.hook_add(UC_HOOK_INSN, hook_syscall, None, 1, 0, UC_X86_INS_SYSCALL) + + # syscall handler is expecting rax=0x100 + mu.reg_write(UC_X86_REG_RAX, 0x100) + + try: + # emulate machine code in infinite time + mu.emu_start(ADDRESS, ADDRESS + len(X86_CODE64_SYSCALL)) + except UcError as e: + print("ERROR: %s" % e) + + # now print out some registers + print(">>> Emulation done. Below is the CPU context") + + rax = mu.reg_read(UC_X86_REG_RAX) + print(">>> RAX = 0x%x" % rax) + + # This will cause a segfault + # def test_run_within_hook(self): + # record = [] + + # def hook(uc, address, size, userdata): + # record.append(address) + # if address == 0x1002: + # uc.emu_stop() + # uc.emu_start(0x1006, 0x1008) + + # w = Zelos(None) + # uc = w.internal_engine.emu + # uc.mem_map(0, 0x2000) + # # Push 1, Push 2, push 3... that way you can tell what + # # instructions have actually executed + # # by investigating the stack + # uc.mem_write( + # 0x1000, b'\x6A\x01\x6A\x02\x6A\x03\x6A\x04\x6A\" + # b"x05\x6A\x06') + # w.internal_engine.emu.setSP(0x100) + + # uc.hook_add(UC_HOOK_CODE, hook) + # uc.emu_start(0x1000, 0x100a) + # self.assertListEqual(record, [0x1000, 0x1002, 0x1006]) + # self.assertEqual(0xf8, w.internal_engine.emu.getSP()) + + # self.assertEqual(4, w.internal_engine.emu.getstack(0)) + # self.assertEqual(1, w.internal_engine.emu.getstack(1)) + # self.assertEqual(0, w.internal_engine.emu.getstack(2)) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tests/test_zelos_profile.py b/tests/test_zelos_profile.py new file mode 100644 index 0000000..84293f7 --- /dev/null +++ b/tests/test_zelos_profile.py @@ -0,0 +1,48 @@ +# Copyright (C) 2020 Zeropoint Dynamics + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# ====================================================================== + +import cProfile +import unittest + +from os import path + +from zelos import Zelos # noqa: F401 + + +DATA_DIR = path.join(path.dirname(path.abspath(__file__)), "data") + +""" +Run this to get a profile of zelos, in order to understand what +function calls are taking time. +""" + + +class ZelosTest(unittest.TestCase): + def test_profile_helloworld(self): + cProfile.runctx( + 'z = Zelos(path.join(DATA_DIR, "static_elf_helloworld"))', + globals(), + locals(), + ) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..8a5c193 --- /dev/null +++ b/tox.ini @@ -0,0 +1,86 @@ +[pytest] +strict = true +addopts = -ra +testpaths = tests examples +filterwarnings = + once::Warning + ignore:::pympler[.*] + + +[tox] +envlist = lint,py36,py37,py38,manifest,docs,pypi-description,coverage-report +isolated_build = True + + +[testenv] +# Prevent random setuptools/pip breakages like +# https://github.com/pypa/setuptools/issues/1042 from breaking our builds. +setenv = + VIRTUALENV_NO_DOWNLOAD=1 +extras = {env:TOX_AP_TEST_EXTRAS:tests} +commands = python -m pytest -n auto --rootdir={envsitepackagesdir}/zelos {posargs} + + +[testenv:py37] +# Python 3.6+ has a number of compile-time warnings on invalid string escapes. +# PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run. +install_command = pip install --no-compile {opts} {packages} +setenv = + PYTHONWARNINGS=d +extras = {env:TOX_AP_TEST_EXTRAS:tests} +commands = coverage run --parallel -m pytest {posargs} + + +[testenv:py38] +# Python 3.6+ has a number of compile-time warnings on invalid string escapes. +# PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run. +basepython = python3.8 +install_command = pip install --no-compile {opts} {packages} +setenv = + PYTHONWARNINGS=d +extras = {env:TOX_AP_TEST_EXTRAS:tests} +commands = coverage run --parallel -m pytest {posargs} + + +[testenv:coverage-report] +basepython = python3.7 +skip_install = true +deps = coverage +commands = + coverage combine + coverage report + + +[testenv:lint] +basepython = python3.6 +skip_install = true +deps = pre-commit +passenv = HOMEPATH # needed on Windows +commands = pre-commit run --all-files + + +[testenv:docs] +basepython = python3.6 +extras = docs +commands = + sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html + sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html + python -m doctest README.md + + +[testenv:manifest] +basepython = python3.7 +deps = check-manifest +skip_install = true +commands = check-manifest --ignore-bad-ideas src/zelos/ext/env/*.so,tests/*.so --ignore tests/data/* + + +[testenv:pypi-description] +basepython = python3.7 +skip_install = true +deps = + twine + pip >= 18.0.0 +commands = + pip wheel -w {envtmpdir}/build --no-deps . + twine check {envtmpdir}/build/*