├── .gitattributes ├── .github └── workflows │ └── sphinx.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── Makefile ├── api │ └── smali │ │ ├── base.rst │ │ ├── bridge.rst │ │ ├── bridge_api.rst │ │ ├── index.rst │ │ ├── language.rst │ │ ├── reader.rst │ │ ├── shell.rst │ │ ├── visitor.rst │ │ └── writer.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── development.rst ├── index.rst ├── installation.rst ├── make.bat └── requirements.txt ├── example.smali ├── examples ├── example.py ├── example.ssf ├── rewrite.py ├── rewrite.smali ├── shell_example.py ├── tests │ ├── add-int-lit.ssf │ └── add-int.ssf └── vm_example.py ├── pyproject.toml ├── requirements.txt └── smali ├── __init__.py ├── base.py ├── bridge ├── __init__.py ├── errors.py ├── executor.py ├── frame.py ├── lang.py ├── objects.py └── vm.py ├── opcode.py ├── reader.py ├── shell ├── __init__.py ├── __main__.py ├── cli.py └── model.py ├── visitor.py └── writer.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/sphinx.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Sphinx Documentation 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | 7 | 8 | jobs: 9 | build-deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Setup Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: '3.11' 20 | 21 | - name: Install dependencies 22 | run: pip install -r docs/requirements.txt && pip install -r requirements.txt 23 | 24 | - name: Build 25 | # Create .nojekyll file to disable Jekyll processing 26 | run: | 27 | cd docs 28 | make html 29 | touch build/html/.nojekyll 30 | 31 | - name: Deploy to GitHub Pages 32 | uses: peaceiris/actions-gh-pages@v3 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | publish_dir: docs/build/html -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PySmali 2 | 3 | [![python](https://img.shields.io/badge/python-3.9+-blue.svg?logo=python&labelColor=lightgrey)](https://www.python.org/downloads/) 4 | ![Status](https://img.shields.io:/static/v1?label=Status&message=Pre-Release&color=teal) 5 | ![Platform](https://img.shields.io:/static/v1?label=Platforms&message=Linux|Windows&color=lightgrey) 6 | [![Build and Deploy Sphinx Documentation](https://github.com/MatrixEditor/pysmali/actions/workflows/sphinx.yml/badge.svg)](https://github.com/MatrixEditor/pysmali/actions/workflows/sphinx.yml) 7 | [![PyPI](https://img.shields.io/pypi/v/pysmali)](https://pypi.org/project/pysmali/) 8 | 9 | 10 | The main functionalities of this repository cover creating and parsing Smali files with Python3 as well as interpret Smali source code files. There is also an interactive interpreter provided that acts as a Python-CLI. 11 | 12 | ### Contributors 13 | 14 | [![TheZ3ro](https://img.shields.io:/static/v1?label=Fixer&message=TheZ3ro)](https://github.com/TheZ3ro) 15 | [![serenees](https://img.shields.io:/static/v1?label=Patcher&message=serenees)](https://github.com/serenees) 16 | [![metalcorpe](https://img.shields.io:/static/v1?label=Patcher&message=metalcorpe)](https://github.com/metalcorpe) 17 | 18 | 19 | ## Installation 20 | 21 | By now, the only way to install the python module in this repository is by cloning it and running the following command: 22 | 23 | ```bash 24 | $ cd ./pysmali && pip install . 25 | # Or with pip 26 | $ pip install pysmali 27 | ``` 28 | 29 | ## Usage 30 | 31 | For a more detailed explanation of the Smali Visitor-API use the [Github-Pages Docs](https://matrixeditor.github.io/pysmali/). 32 | 33 | > **Info**: Make sure you are using ``pysmali>=0.2.0`` as it introduces a user-friendly type system to mitigate possible issues from parsing type descriptors. 34 | 35 | ### ISmali (Interactive Smali Interpreter) 36 | 37 | As of version `0.1.2` the interactive interpreter can be used to execute Smali code directly: 38 | 39 | ```bash 40 | $ ismali example.ssf 41 | # or start interactive mode 42 | $ ismali 43 | >>> vars 44 | {'p0': } 45 | ``` 46 | 47 | Some notes: 48 | 49 | * ``p0``: This register always stores the root-instance where defined fields and methods will be stored. 50 | * ``vars``: This command can be used to print all registers together with their values 51 | * `L;`: The name of the root-context class 52 | 53 | The API [documentation](https://matrixeditor.github.io/pysmali/) provides some usage examples and usage hints. 54 | 55 | ### Parsing Smali-Files 56 | 57 | The simplest way to parse code is to use a `SmaliReader` together with a visitor: 58 | 59 | ```python 60 | from smali import SmaliReader, ClassVisitor 61 | 62 | code = """ 63 | .class public final Lcom/example/Hello; 64 | .super Ljava/lang/Object; 65 | # One line comment 66 | .source "SourceFile" # EOL comment 67 | """ 68 | 69 | reader = SmaliReader() 70 | reader.visit(code, ClassVisitor()) 71 | ``` 72 | 73 | There are a few options to have in mind when parsing with a `SmaliReader`: 74 | 75 | * `comments`: To explicitly parse comments, set this variable to True (in constructor or directly) 76 | * `snippet`: To parse simple code snippets without a .class definition, use the 'snippet' variable (or within the constructor). Use this property only if you don't have a '.class' definition at the start of the source code 77 | * `validate`: Validates the parsed code 78 | * `errors`: With values `"strict"` or `"ignore"` this attribute will cause the reader to raise or ignore exceptions 79 | 80 | Actually, the code above does nothing as the `ClassVisitor` class does not handle any notification by the reader. For instance, to print out the class name of a parsed code, the following implementation could be used: 81 | 82 | ```python 83 | from smali import SmaliReader, ClassVisitor, SVMType 84 | 85 | class NamePrinterVisitor(ClassVisitor): 86 | def visit_class(self, name: str, access_flags: int) -> None: 87 | # The provided name is the type descriptor, so we have to 88 | # convert it: 89 | cls_type = SVMType(name) 90 | print('Class:', cls_type.pretty_name) # prints: com.example.Hello 91 | 92 | reader = SmaliReader() 93 | reader.visit(".class public final Lcom/example/Hello;", NamePrinterVisitor()) 94 | ``` 95 | 96 | > [!TIP] 97 | > There is an example Smali file in this repository. If you want to print out **all** 98 | > defined classes, you have to implement another method (based on the example above): 99 | > ```python 100 | > class NamePrinterVisitor(ClassVisitor): 101 | > # ... method from above does not change 102 | > def visit_inner_class(self, name: str, access_flags: int) -> ClassVisitor: 103 | > cls_type = SVMType(name) # same as above 104 | > print("Inner Class:", cls_type.pretty_name) 105 | > return self 106 | > ``` 107 | 108 | ### Writing Smali-Files 109 | 110 | Writing is as simple as parsing files. To write the exact same document the has been parsed, the `SmaliWriter` class can be used as the visitor: 111 | 112 | ```python 113 | from smali import SmaliReader, SmaliWriter 114 | 115 | reader = SmaliReader() 116 | writer = SmaliWriter() 117 | 118 | reader.visit(".class public final Lcom/example/Hello;", writer) 119 | # The source code can be retrieved via a property 120 | text = writer.code 121 | ``` 122 | 123 | To create own Smali files, the pre-defined `SmaliWriter` can be used again: 124 | 125 | ```python 126 | from smali import SmaliWriter, AccessType 127 | 128 | writer = SmaliWriter() 129 | # create the class definition 130 | writer.visit_class("Lcom/example/Hello;", AccessType.PUBLIC + AccessType.FINAL) 131 | writer.visit_super("Ljava/lang/Object;") 132 | 133 | # create a field 134 | field_writer = writer.visit_field("foo", AccessType.PRIVATE, "Ljava/lang/String") 135 | 136 | # create the finished source code, BUT don't forget visit_end() 137 | writer.visit_end() 138 | text = writer.code 139 | ``` 140 | 141 | ### Importing classes and execute methods 142 | 143 | As of version `0.1.2` you can import Smali files and execute defined methods: 144 | 145 | ```python 146 | from smali.bridge import SmaliVM, SmaliObject 147 | 148 | vm = SmaliVM() 149 | # Import class definition 150 | with open('example.smali', 'r', encoding='utf-8') as fp: 151 | smali_class = vm.classloader.load_class(fp, init=False) 152 | # Call method 153 | smali_class.clinit() 154 | 155 | # Create a new instance of the imported class 156 | instance = SmaliObject(smali_class) 157 | # Call the object's constructor 158 | instance.init() 159 | 160 | # Execute the method 'toString' 161 | toString = instance.smali_class.method("toString") 162 | # The instance must be always the first element (on 163 | # static methods this argument must be None) 164 | value = toString(instance) 165 | print(value) 166 | ``` 167 | 168 | 169 | 170 | ## License 171 | 172 | Distributed under the GNU GPLv3. See `LICENSE` for more information. 173 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api/smali/base.rst: -------------------------------------------------------------------------------- 1 | .. _smali_base_api: 2 | 3 | **************** 4 | Smali Components 5 | **************** 6 | 7 | .. automodule:: smali.base 8 | 9 | .. _Token-class: 10 | 11 | Smali token 12 | =========== 13 | 14 | .. autoclass:: smali.base.Token 15 | :members: 16 | 17 | 18 | Smali access modifiers 19 | ====================== 20 | 21 | .. autoclass:: smali.base.AccessType 22 | :members: 23 | 24 | 25 | Smali type descriptors 26 | ====================== 27 | 28 | .. autoclass:: smali.base.SVMType 29 | :members: 30 | 31 | .. autoclass:: smali.base.Signature 32 | :members: 33 | 34 | Smali type 35 | ========== 36 | 37 | .. autofunction:: smali.smali_value 38 | 39 | Utility components 40 | ================== 41 | 42 | .. autoclass:: smali.base.Line 43 | :members: -------------------------------------------------------------------------------- /docs/api/smali/bridge.rst: -------------------------------------------------------------------------------- 1 | .. _smali_bridge: 2 | 3 | ******** 4 | Smali VM 5 | ******** 6 | 7 | .. automodule:: smali.bridge.vm 8 | 9 | As of version ``0.1.1`` it is possible to import Smali source code files into 10 | the provided :class:`SmaliVM`. All classes are stored globally, so there can't be 11 | two classes with the same type descriptor. To create a simple Smali emulator, 12 | just create a new :class:`SmaliVM` instance: 13 | 14 | .. code:: python 15 | 16 | from smali.bridge import SmaliVM 17 | 18 | vm = SmaliVM() 19 | 20 | 21 | .. hint:: 22 | 23 | If you want to create a custom import process, just create sub-class of 24 | :class:`ClassLoader` and provide an instance of it in the constructor of the 25 | :class:`SmaliVM`. 26 | 27 | Next, classes are used as prototypes to create new objects of their type. The 28 | class creation and object initialization is rather simple: 29 | 30 | .. code-block:: python 31 | :linenos: 32 | 33 | from smali.bridge import SmaliClass, SmaliObject, SmaliVM 34 | 35 | # Let's assume, the class' source code is stored here 36 | source = ... 37 | 38 | vm = SmaliVM() 39 | # Load and define the class (don't call the method) 40 | my_class = vm.classloader.load_class(source, init=False) 41 | 42 | # To manually initialize the class, call 43 | my_class.clinit() 44 | 45 | # Instances can be created by providing the class object 46 | instance = SmaliObject(my_class) 47 | 48 | # Initialization must be done separately: 49 | instance.init(...) 50 | 51 | # Methods are stored in our class object (not in the 52 | # actual instance) 53 | toString = my_class.method("toString") 54 | 55 | # The first argument of instance method must be the 56 | # object itself (on static methods, just use None) 57 | value = toString(instance) 58 | 59 | # The returned value behaves like a string 60 | print("Returned value:", value) 61 | 62 | 63 | .. autoclass:: smali.bridge.vm.SmaliVM 64 | :members: 65 | 66 | .. autoclass:: smali.bridge.vm.ClassLoader 67 | :members: -------------------------------------------------------------------------------- /docs/api/smali/bridge_api.rst: -------------------------------------------------------------------------------- 1 | .. _smali_bridge_api: 2 | 3 | **************** 4 | Smali Bridge API 5 | **************** 6 | 7 | Execution Frame 8 | =============== 9 | 10 | .. automodule:: smali.bridge.frame 11 | 12 | .. autoclass:: smali.bridge.frame.Frame 13 | :members: 14 | 15 | Language members 16 | ================ 17 | 18 | .. automodule:: smali.bridge.lang 19 | 20 | .. autoclass:: smali.bridge.lang.SmaliMember 21 | :members: 22 | 23 | .. autoclass:: smali.bridge.lang.SmaliAnnotation 24 | :members: 25 | 26 | .. autoclass:: smali.bridge.lang.SmaliField 27 | :members: 28 | 29 | .. autoclass:: smali.bridge.lang.SmaliMethod 30 | :members: 31 | 32 | .. autoclass:: smali.bridge.lang._MethodBroker 33 | :members: 34 | 35 | .. autoclass:: smali.bridge.lang.SmaliClass 36 | :members: 37 | 38 | .. autoclass:: smali.bridge.lang.SmaliObject 39 | :members: 40 | 41 | Common error classes 42 | ==================== 43 | 44 | The following classes are defined in the ``smali.bridge.errors`` module: 45 | 46 | .. autoclass:: smali.bridge.errors.NoSuchClassError 47 | 48 | .. autoclass:: smali.bridge.errors.NoSuchMethodError 49 | 50 | .. autoclass:: smali.bridge.errors.NoSuchFieldError 51 | 52 | .. autoclass:: smali.bridge.errors.NoSuchOpcodeError 53 | 54 | .. autoclass:: smali.bridge.errors.InvalidOpcodeError 55 | 56 | .. autoclass:: smali.bridge.errors.NoSuchRegisterError 57 | 58 | .. autoclass:: smali.bridge.errors.ExecutionError 59 | :members: 60 | -------------------------------------------------------------------------------- /docs/api/smali/index.rst: -------------------------------------------------------------------------------- 1 | .. _smali_api: 2 | 3 | ********* 4 | Smali API 5 | ********* 6 | 7 | .. automodule:: smali 8 | 9 | .. contents:: Contents 10 | 11 | Overview 12 | ======== 13 | 14 | The overall structur of a decompiled Smali class is rather simple. In fact 15 | a decompiled source class contains: 16 | 17 | * A section describing the class' modifiers (such as ``public`` or ``private``), its name, super class and the implemented interfaces. 18 | 19 | * One section per annotation at class level. Each section describes the name, modifiers (such as ``runtime`` or ``system``), and the type. Optionally, there are sub-annotations included as values in each section. 20 | 21 | * One section per field declared in this class. Each section describes the name, modifiers (such as ``public`` or ``static``), and the type. Optionally, there are annotations placed within the section. 22 | 23 | * One section per method, constructor (````) and static initializer block (``clinit``) describing their name, parameter types and return type. Each section may store instruction information. 24 | 25 | * Sections for line-based comments (lines that start with a ``#``). 26 | 27 | As Smali source code files does not contain any ``package`` or ``import`` statements 28 | like they are present Java classes, all names must be fully qualified. The structure 29 | of these names are described in the Java ASM documentationin section 2.1.2 [1]_. 30 | 31 | .. _section-1.1.1: 32 | 33 | Type descriptors 34 | ---------------- 35 | 36 | Type descriptors used in Smali source code are similar to those used in compiled 37 | Java classes. The following list was taken from ASM API [1]_: 38 | 39 | .. list-table:: Type descriptors of some Smali types 40 | :header-rows: 1 41 | :widths: 10, 15, 10 42 | 43 | * - Smali type 44 | - Type descriptor 45 | - Example value 46 | * - ``void`` 47 | - V 48 | - -- 49 | * - ``boolean`` 50 | - Z 51 | - ``true`` or ``false`` 52 | * - ``char`` 53 | - C 54 | - ``'a'`` 55 | * - ``byte`` 56 | - B 57 | - ``1t`` 58 | * - ``short`` 59 | - S 60 | - ``2s`` 61 | * - ``int`` 62 | - I 63 | - ``0x1`` 64 | * - ``float`` 65 | - F 66 | - ``3.0f`` 67 | * - ``long`` 68 | - J 69 | - ``5l`` 70 | * - ``double`` 71 | - D 72 | - ``2.0`` 73 | * - ``Object`` 74 | - Ljava/lang/Object; 75 | - -- 76 | * - ``boolean[]`` 77 | - [Z 78 | - -- 79 | 80 | The descriptors of primitive types can be represented with single characters. Class 81 | type descriptors always start with a ``L`` and end with a semicolon. In addition to 82 | that, array type descriptors will start with opened square brackets according to the 83 | number of dimensions. For instance, a two dimensional array would get two opened square 84 | brackets in its type descriptor. 85 | 86 | This API contains a class called :class:`SVMType` that can be used to retrieve type descriptors 87 | as well as class names: 88 | 89 | .. code-block:: python 90 | :linenos: 91 | 92 | from smali import SVMType, Signature 93 | 94 | # simple type instance 95 | t = SVMType("Lcom/example/Class;") 96 | t.simple_name # Class 97 | t.pretty_name # com.example.Class 98 | t.dvm_name # com/example/Class 99 | t.full_name # Lcom/example/Class; 100 | t.svm_type # SVMType.TYPES.CLASS 101 | 102 | # create new type instance for method signature 103 | m = SVMType("getName([BLjava/lang/String;)Ljava/lang/String;") 104 | m.svm_type # SVMType.TYPES.METHOD 105 | # retrieve signature instance 106 | s = m.signature or Signature(m) 107 | s.return_type # SVMType("Ljava/lang/String;") 108 | s.parameter_types # [SVMType("[B"), SVMType("Ljava/lang/String;")] 109 | s.name # getName 110 | s.declaring_class # would return the class before '->' (only if defined) 111 | 112 | # array types 113 | array = SVMType("[B") 114 | array.svm_type # SVMType.TYPES.ARRAY 115 | array.array_type # SVMType("B") 116 | array.dim # 1 (one dimension) 117 | 118 | As an input can be used anything that represents the class as type descriptor, original 119 | class name or internal name (array types are supported as well). 120 | 121 | Method descriptors 122 | ------------------ 123 | 124 | Unlike method descriptors in compiled Java classes, Smali's method descriptors contain the 125 | method's name. The general structure, described in detail in the ASM API [1]_ documentation, 126 | is the same. To get contents of a method descriptor the :class:`SVMType` class, introduced before, 127 | can be used again: 128 | 129 | .. code-block:: python 130 | :linenos: 131 | 132 | from smali import SVMType 133 | 134 | method = SVMType("getName([BLjava/lang/String;)Ljava/lang/String;") 135 | # get the method's signature 136 | signature = method.signature 137 | # get parameter type descriptors 138 | params: list[SVMType] = signature.parameter_types 139 | # get return type descriptor 140 | return_type = signature.return_type 141 | 142 | # the class type can be retrieved if defined 143 | cls: SVMType = signature.declaring_class 144 | 145 | .. caution:: 146 | 147 | The initial method descriptor must be valid as it can cause undefined behaviour 148 | if custom strings are used. 149 | 150 | 151 | Interfaces and components 152 | ========================= 153 | 154 | The Smali Visitor-API for generating and transforming Smali-Source files 155 | (no bytecode data) is based on the :class:`ClassVisitor` class, similar to the 156 | ASM API [1]_ in Java. Each method in 157 | this class is called whenever the corresponding code structure has been 158 | parsed. There are two ways how to visit a code structure: 159 | 160 | 1. Simple visit: 161 | All necessary information are given within the method parameters 162 | 163 | 2. Extendend visit: 164 | To deep further into the source code, another visitor instance is 165 | needed (for fields, methods, sub-annotations or annotations and 166 | even inner classes) 167 | 168 | The same rules are applied to all other visitor classes. The base class of 169 | all visitors must be :class:`VisitorBase` as it contains common methods all sub 170 | classes need: 171 | 172 | .. code-block:: python 173 | 174 | class VisitorBase: 175 | def __init__(self, delegate) -> None: ... 176 | def visit_comment(self, text: str) -> None: ... 177 | def visit_eol_comment(self, text: str) -> None: ... 178 | def visit_end(self) -> None: ... 179 | 180 | All visitor classes come with a delegate that can be used together with the 181 | initial visitor. For instance, we can use our own visitor class together with 182 | the provided :class:`SmaliWriter` that automatically writes the source code. 183 | 184 | .. note:: 185 | The delegate must be an instance of the same class, so :class:`FieldVisitor` 186 | objects can't be applied to :class:`MethodVisitor` objects as a delegate. 187 | 188 | The provided Smali API provides three core components: 189 | 190 | * The :class:`SmaliReader` class is an implementation of a line-based parser that can handle *.smali* files. It can use both utf-8 strings or bytes as an input. It calls the corresponding *visitXXX* methods on the :class:`ClassVisitor`. 191 | 192 | * The :class:`SmaliWriter` is a subclass of :class:`ClassVisitor` that tries to build a Smali file based on the visited statements. It comes together with an ``AnnotationWriter``, ``FieldWriter`` and ``MethodWriter``. It produces an output utf-8 string that can be encoded into bytes. 193 | 194 | * The ``XXXVisitor`` classes delegate method calls to internal delegate candicates that must be set with initialisation. 195 | 196 | The next sections provide basic usage examples on how to generate or transform Smali class 197 | files with these components. 198 | 199 | 200 | Parsing classes 201 | --------------- 202 | 203 | The only required component to parse an existing Smali source file is the :class:`SmaliReader` 204 | component. To illustrate an example usage, assume we want to print out the parsed 205 | class name, super class and implementing interfaces: 206 | 207 | .. code-block:: python 208 | :linenos: 209 | 210 | from smali import ClassVisitor 211 | 212 | class SmaliClassPrinter(ClassVisitor): 213 | def visit_class(self, name: str, access_flags: int) -> None: 214 | # The provided name is the type descriptor - if we want the 215 | # Java class name, use a SVMType() call: 216 | # cls_name = SVMType(name).simple_name 217 | print(f'.class {name}') 218 | 219 | def visit_super(self, super_class: str) -> None: 220 | print(f".super {super_class}") 221 | 222 | def visit_implements(self, interface: str) -> None: 223 | print(f".implements {interface}") 224 | 225 | The second step is to use our previous defined visitor class with a :class:`SmaliReader` 226 | component: 227 | 228 | .. code-block:: python 229 | :linenos: 230 | :emphasize-lines: 5 231 | 232 | # Let's assume the source code is stored here 233 | source = ... 234 | 235 | printer = SmaliClassPrinter() 236 | reader = SmaliReader(comments=False) 237 | reader.visit(source, printer) 238 | 239 | The fifth line creates a :class:`SmaliReader` that ignores all comments in the source 240 | file to parse. The *visit* method is called at the end to parse the source code 241 | file. 242 | 243 | Generating classes 244 | ------------------ 245 | 246 | The only required component to generate a new Smali source file is the :class:`SmaliWriter` 247 | component. For instance, consider the following class: 248 | 249 | .. code-block:: smali 250 | :linenos: 251 | 252 | .class public abstract Lcom/example/Car; 253 | .super Ljava/lang/Object; 254 | 255 | .implements Ljava/lang/Runnable; 256 | 257 | .field private id:I 258 | 259 | .method public abstract run()I 260 | .end method 261 | 262 | It can be generated within seven method calls to a :class:`SmaliWriter`: 263 | 264 | .. code-block:: python 265 | :linenos: 266 | :emphasize-lines: 3,16,20 267 | 268 | from smali import SmaliWriter, AccessType 269 | 270 | writer = SmaliWriter() 271 | # Create the .class statement 272 | writer.visit_class("Lcom/example/Car;", AccessType.PUBLIC + AccessType.ABSTRACT) 273 | # Declare the super class 274 | writer.visit_super("Ljava/lang/Object;") 275 | # Visit the interface implementation 276 | writer.visit_implements("Ljava/lang/Runnable") 277 | 278 | # Create the field id 279 | writer.visit_field("id", AccessType.PRIVATE, "I") 280 | 281 | # Create the method 282 | m_writer = writer.visit_method("run", AccessType.PUBLIC + AccessType.ABSTRACT, [], "V") 283 | m_writer.visit_end() 284 | 285 | # finish class creation 286 | writer.visit_end() 287 | source_code = writer.code 288 | 289 | At line 3 a :class:`SmaliWriter` is created that will actually build the source code 290 | string. 291 | 292 | The call to ``visit_class`` defines the class header (see line 1 of smali source 293 | code). The first argument represents the class' type descriptor and the second its 294 | modifiers. To specify additional modifiers, use the :class:`AccessType` class. It provides 295 | two ways how to retrieve the actual modifiers: 296 | 297 | * Either by referencing the enum (like ``AccessType.PUBLIC``) 298 | * or by providing a list of keywords that should be translated into modifier flags: 299 | 300 | .. code-block:: python 301 | 302 | modifiers = AccessType.get_flags(["public", "final"]) 303 | 304 | The calls to ``visit_super`` defines the super class of our previously defined 305 | class and to ``visit_implements`` specifies which interfaces are implemented by our 306 | class. All arguments must be type descriptors to generate accurate Smali code (see 307 | section :ref:`section-1.1.1` for more information on the type class) 308 | 309 | 310 | 311 | .. Footnotes 312 | 313 | .. [1] ASM API documentation `here `_ -------------------------------------------------------------------------------- /docs/api/smali/language.rst: -------------------------------------------------------------------------------- 1 | .. _smali_language: 2 | 3 | ************** 4 | Smali Language 5 | ************** 6 | 7 | Smali is an assembly-like language used to write code for Android 8 | apps. It is designed to be human-readable as well as easy to understand, 9 | while still being low-level enough to allow for fine-grained control 10 | over the app's behavior. 11 | 12 | .. contents:: Contents 13 | 14 | Smali files come with the extension ``.smali`` and each of them 15 | correspond to a single Java class and has the same name as the class, 16 | but with slashes (``/``) replacing dots (``.``) in the package name. 17 | For example, the Java class ``com.example.MyClass`` would be represented 18 | by the Smali file ``com/example/MyClass.smali``. These names are referred 19 | to *internal names*. 20 | 21 | The structure of a Smali source code can be broken down into the following 22 | sections: 23 | 24 | Class Header 25 | ------------ 26 | 27 | The header of a Smali file contains several pieces of information that are 28 | used by the Smali assembler to build the final ``.dex`` file for an Android 29 | app. It starts with the ``.class`` directive, which specifies the name of the 30 | class being defined in the file. Here's an example: 31 | 32 | .. code-block:: smali 33 | 34 | .class public Lcom/example/MyClass; 35 | 36 | Here, we are defining a class called ``MyClass`` in the package ``com.example`` 37 | with ``public`` as its access modifier. The class name is preceded by the letter 38 | ``L`` and is followed by a semicolon, which is the standard syntax for type 39 | descriptors in Smali. For more information about type descriptors, see chapter 40 | :ref:`section-1.1.1` of the Smali API. 41 | 42 | After the ``.class`` directive, the header can include several optional directives 43 | to provide additional information about the class. For example, you can use the 44 | ``.super`` directive to specify the parent class of the current: 45 | 46 | .. code-block:: smali 47 | 48 | .super Ljava/lang/Object; 49 | 50 | This directive specifies that the our class inherits contents and functionalities 51 | from ``java.lang.Object``. 52 | 53 | Other optional directives that can appear in the header include: 54 | 55 | * ``.implements``: 56 | This directive is used to specify that the current class implements one or 57 | more interfaces. For instance, the following code specifies that our class 58 | implements ``java.io.Serializable`` and ``android.os.Parcelable``: 59 | 60 | .. code-block:: smali 61 | 62 | .implements Ljava/io/Serializable; 63 | .implements Landroid/os/Parcelable; 64 | 65 | * ``.source``: 66 | This directive is used to specify the name of the original source that Smali code 67 | was generated from. This information can be useful for debugging purposes. Note 68 | that for lambda-classes the source definition would point to ``lambda``: 69 | 70 | .. code-block:: smali 71 | 72 | .source "MyClass.java" 73 | 74 | * ``.debug``: 75 | This directive is used to enable debug information for the current class. When 76 | it is present, the Smali assembler will include additional metadata in the 77 | final ``.dex`` file that can be used by debuggers: 78 | 79 | .. code-block:: smali 80 | 81 | .debug 0 82 | 83 | .. note:: 84 | 85 | The visitor API provided by this repository uses different method call for each of 86 | these directives to provide more flexibility while parsing. Therefore, if you want 87 | to get the header information on a class, you have to implement the following 88 | methods: 89 | 90 | - ``visit_class``, and optionally 91 | - ``visit_super``, 92 | - ``visit_source``, 93 | - ``visit_implements`` and 94 | - ``visit_debug`` 95 | 96 | 97 | In summary, the header of a Smali source file contains important metadata about the 98 | class being defined, as well as optional directives that provide additional information. 99 | 100 | Class Annotations 101 | ----------------- 102 | 103 | In Smali, class annotations are used to provide metadata information about a class. They 104 | are defined using the ``.annotation`` directive followed by their descriptor and elements. 105 | Here is an example of how class annotations are defined in Smali: 106 | 107 | .. code-block:: smali 108 | :linenos: 109 | 110 | .class public Lcom/example/MyClass; 111 | .super Ljava/lang/Object; 112 | 113 | .annotation runtime Ljava/lang/Deprecated; 114 | .end annotation 115 | 116 | .annotation system Ldalvik/annotation/EnclosingClass; 117 | value = Lcom/example/OuterClass; 118 | .end annotation 119 | 120 | .annotation system Ldalvik/annotation/InnerClass; 121 | accessFlags = 0x0008 122 | name = "MyClass" 123 | outer = Lcom/example/OuterClass; 124 | .end annotation 125 | 126 | # class definition and methods go here 127 | 128 | In this example, we have defined three different class annotations: 129 | 130 | * ``@Deprecated``: 131 | 132 | This runtime annotation indicates that the class is deprecated and should no 133 | longer be used. It is defined using the ``@java/lang/Deprecated`` annotation 134 | descriptor. 135 | 136 | * ``@EnclosingClass``: 137 | 138 | This system annotation specifies the enclosing class of the current. It is 139 | defined using the ``@dalvik/annotation/EnclosingClass`` descriptor. In this 140 | case, it specifies that the enclosing class of ``MyClass`` is ``OuterClass``. 141 | 142 | * ``@InnerClass``: 143 | 144 | This system annotation specifies that the class is an inner class. It is 145 | defined using the ``@dalvik/annotation/InnerClass`` descriptor. In this case, 146 | it specifies that the access flags for the class are ``0x0008`` (which means 147 | it is static), the name of the class is ``MyClass``, and the outer class is 148 | ``OuterClass``. 149 | 150 | There is also a special annotation used to mark classes to contain generic types, 151 | named `@Signature`. When used on a class, the annotation specifies the generic 152 | signature of the class, including its type parameters. 153 | 154 | Here is an example of how the signature annotation can be used on a class in Smali: 155 | 156 | .. code-block:: 157 | :linenos: 158 | 159 | .class public interface abstract Lcom/example/MyClass; 160 | .super Ljava/lang/Object; 161 | 162 | # add the Signature annotation to the class 163 | .annotation system Ldalvik/annotation/Signature; 164 | value = { 165 | "", 168 | "Ljava/lang/Object;" 169 | } 170 | .end annotation 171 | 172 | In this example, the ``MyClass`` class is defined with a type parameter `T` using 173 | the signature annotation. Converted into Java code, the class would look like the 174 | folowing: 175 | 176 | .. code-block:: java 177 | :linenos: 178 | 179 | public interface MyClass { 180 | // ... 181 | } 182 | 183 | .. note:: 184 | 185 | All type descriptors that are defined after geenric type descriptors define the 186 | super classes of the declared class. In this case, there is only one super class 187 | (`Ljava/lang/Object;`). 188 | 189 | Fields 190 | ------ 191 | 192 | The ``.field`` directive in Smali is used to define a field within a class. The general 193 | syntax for the field directive is as follows: 194 | 195 | .. code-block:: bnf 196 | 197 | .field : [ = ] 198 | 199 | Here is a breakdown of the different components of the field directive: 200 | 201 | * ``access_flags``: 202 | 203 | Access flags specifiy the access modifiers for the field being defined. It can be 204 | one of the following values: 205 | 206 | .. list-table:: Access flags of the ``.field`` directive 207 | :header-rows: 1 208 | :widths: 10, 15 209 | 210 | * - Name 211 | - Description 212 | * - ``public`` 213 | - The field can be accessed from anywhere within the application. 214 | * - ``protected`` 215 | - The field can be accessed within the same package or by a subclass. 216 | * - ``private`` 217 | - The field can only be accessed within the same class or by a non-static 218 | subclass. 219 | * - ``static`` 220 | - The field belongs to the class, but not to any instance of the class. 221 | * - ``final`` 222 | - The field cannot be modified once it has been initialized. 223 | * - ``synthetic`` 224 | - The field is generated by the compiler and not present in the original source code. 225 | 226 | You can use a combination of these flags to specify the desired access level for 227 | the field. They can be retrieved by using the ``AccessType`` class: 228 | 229 | .. code-block:: python 230 | :linenos: 231 | 232 | from smali import AccessType 233 | 234 | # Retrieve integer flags for access modifiers 235 | flags: int = AccessType.get_flags(["public", "static", "final"]) 236 | # Convert the flags back into human readable names 237 | flag_names: list[str] = AccessType.get_names(flags) 238 | 239 | # or reference them directly 240 | flags: int = AccessType.PUBLIC + AccessType.STATIC + AccessType.FINAL 241 | 242 | * ````: 243 | 244 | This section specifies the name of the field. It should start with a letter and can contain 245 | letters, digits, underscores, and dollar signs. It must not start with a number. 246 | 247 | * ````: 248 | 249 | The type descriptor is a string that represents the type of the field. The structure of type 250 | descriptors is described in the :ref:`section-1.1.1` chapter of the Smali API. 251 | 252 | * ````: 253 | 254 | The value definition is used when the field should be assigned directly with a value. 255 | 256 | Let's take a quick look at a small example of the ``.field`` directive: 257 | 258 | .. code-block:: smali 259 | 260 | .field private myField:I 261 | 262 | Here, we are defining a private integer field named *myField*. The ``I`` type descriptor 263 | indicates that this field has an integer as its value. This field can only be accessed 264 | within the same class or any non-static sub-class. 265 | 266 | 267 | Methods 268 | ------- 269 | 270 | In Smali, a method definition consists of several components, including access modifiers, 271 | return type, parameter types, and implementation code. The code block contains the actual 272 | assembly-like code that is executed when the method is called. It can contain registers, 273 | instructions, labels, and exception handlers. 274 | 275 | .. code-block:: bnf 276 | 277 | .method () 278 | 279 | .end method 280 | 281 | Here is a breakdown of the different components of the method directive: 282 | 283 | * ````: 284 | 285 | Access flags specifiy the access modifiers for the method. It can have the 286 | same values as defined before in field definitions. 287 | 288 | * ````: 289 | 290 | Stores the actual method name used in references. There are two special method 291 | names that are pre-defined: ```` for constructor methods and ``clinit`` 292 | for static initializer blocks. 293 | 294 | * ````: 295 | 296 | These are the type descriptors of the parameters of the method. 297 | 298 | * ````: 299 | 300 | Defines the return type for this method (type descriptor). 301 | 302 | * ````: 303 | 304 | This is the actual code that performs the functionality of the method. It may 305 | contain the following sections: 306 | 307 | - Registers: 308 | 309 | Registers are used to store temporary data and intermediate results during the 310 | execution of the method. They are specified using the letter ``v`` followed 311 | by a number (e.g., ``v0``, ``v1``, ``v2``, etc.). 312 | 313 | - Instructions: 314 | 315 | Instructions are used to perform operations on registers or objects. Each 316 | instruction is represented by a mnemonic (e.g., ``move``, ``add``, ``sub``, etc.) 317 | followed by its operands. Operands can be registers, constants, or labels. 318 | 319 | - Labels: 320 | 321 | Labels are used to mark specific locations in the code block. They are 322 | specified using a colon (``:``) followed by its name (e.g., ``:label1``, 323 | ``:label2``, etc.). 324 | 325 | - Exception handlers: 326 | 327 | Exception handlers are used to handle exceptions that may occur during the 328 | execution of the method. They are specified using the ``.catch`` directive, 329 | followed by the type of the exception that is being caught and the label of the handler. 330 | 331 | The following example method written in Smali can be used to print ``"Hello World"`` 332 | to the console: 333 | 334 | .. code-block:: smali 335 | :linenos: 336 | 337 | .method public static main([Ljava/lang/String;)V 338 | .registers 2 339 | 340 | sget-object v0, Ljava/lang/System;->out:Ljava/lang/PrintStream; 341 | 342 | const-string v1, "Hello World" 343 | 344 | invoke-virtual {v0, v1}, Ljava/lang/PrintStream;->println(Ljava/lang/String;)V 345 | 346 | return-void 347 | 348 | .end method 349 | 350 | explanation: 351 | 352 | - Line 1: 353 | 354 | In the first line we defined a method with name ``main`` that returns nothing 355 | and takes a string array (``String[]``) as an argument. 356 | 357 | - Line 2: 358 | 359 | ``.registers 2`` declares that the method will use 2 registers, so we can use 360 | ``v0`` and ``v1`` within the method. 361 | 362 | - Line 4: 363 | 364 | The instruction ``sget-object`` loads the ``PrintStream`` object named *out* 365 | into the first register (``v0``). 366 | 367 | - Line 6: 368 | 369 | The instruction ``const-string`` defines a string variable that will be loaded 370 | into the register ``v1``. 371 | 372 | - Line 8: 373 | 374 | With ``invoke-virtual`` we are calling the method *println* of the Java class 375 | ``PrintStream`` to print our string in register ``v1`` to the console. 376 | 377 | - Line 10: 378 | 379 | ``return-void`` exits the method execution. 380 | 381 | 382 | Annotations 383 | ----------- 384 | 385 | The ``.annotation`` directive in Smali is used to define an annotation. The structure of this 386 | directive is as follows: 387 | 388 | .. code-block:: bnf 389 | 390 | .[sub]annotation 391 | [ ] 392 | .end [sub]annotation 393 | 394 | The annotation directive starts with the ``.annotation`` or ``.subannotation`` keyword followed 395 | by the visibility and the type descriptor for the annotation. There are three possible values 396 | for the annotation's visibility: 397 | 398 | - ``runtime``: The annotation is visible at runtime and can be queried using reflection. 399 | - ``system``: The annotation is a system annotation and is not visible to the user. 400 | - ``build``: Another system annotation that indicates special treating of the annotated value. 401 | 402 | After the visibility and annotation type descriptor, the properties are defined. These are 403 | key-value pairs, where the key is the property's name and the value is the property's value. The 404 | properties are defined using the syntax ``key = value``. It is possible to define multiple 405 | properties on separate lines within one annotation directive. 406 | 407 | Finally, the annotation directive is closed using the ``.end annotation`` keyword. Here is an 408 | example of an annotation directive in Smali: 409 | 410 | .. code-block:: smali 411 | :linenos: 412 | 413 | .annotation runtime Lcom/example/MyAnnotation; 414 | name = "John Doe" 415 | age = .subannotation runtime Lcom/example/Age; 416 | value = 30 417 | .end subannotation 418 | .end annotation 419 | 420 | The sample defines an annotation named ``MyAnnotation`` with two properties: *name* and *age*. 421 | The name property is a simple string and has a value of ``"John Doe"``, and the age property 422 | is a sub-annotation with a value of 30. The ``runtime`` keyword specifies that the annotation 423 | is visible at runtime of the application. -------------------------------------------------------------------------------- /docs/api/smali/reader.rst: -------------------------------------------------------------------------------- 1 | .. _smali_reader_api: 2 | 3 | ******************* 4 | Smali Reader API 5 | ******************* 6 | 7 | .. automodule:: smali.reader 8 | 9 | The parsing model is rather simple and will be described in the 10 | following chapter. 11 | 12 | .. note:: 13 | Please note that no optimization is done by default, so methods or fields 14 | that won't be visited by a :class:`SmaliWriter` won't be covered and won't be 15 | visible in the final file. 16 | 17 | To copy non-visited structures, just add the reader variable to the 18 | :class:`SmaliWriter` when creating a new one: 19 | 20 | .. code-block:: python 21 | :linenos: 22 | 23 | reader = SmaliReader(...) 24 | writer = SmaliWriter(reader) 25 | 26 | reader.visit(source_code, writer) 27 | 28 | .. hint:: 29 | You can add your own *copy_handler* to the reader instance if you want 30 | to use your own callback to copy raw lines. 31 | 32 | 33 | Parsing model 34 | ============= 35 | 36 | Parsing is done by inspecting each line as a possible input. Although, some 37 | statements consume more than one line, only one line per statement is used. 38 | 39 | Speaking of statements, they can be devided into groups: 40 | 41 | - Token: 42 | 43 | Statements begin with a leading ``.`` and can open a new statement block. 44 | They are specified in the :class:`Token` class described in :ref:`Token-class` 45 | 46 | - Invocation blocks: 47 | 48 | Block statements used within method declarations start with a ``:`` and 49 | just specify the block's id. 50 | 51 | - Annotation values: 52 | 53 | Annotation values don't have a leading identifier and will only be parsed 54 | within ``.annotation`` or ``.subannotation`` statements. 55 | 56 | - Method instructions: 57 | 58 | Same goes with method instructions - they will be handled only if a method 59 | context is present. 60 | 61 | .. autoclass:: smali.reader.SupportsCopy 62 | :members: 63 | 64 | .. autoclass:: smali.reader.SmaliReader 65 | :members: 66 | 67 | 68 | -------------------------------------------------------------------------------- /docs/api/smali/shell.rst: -------------------------------------------------------------------------------- 1 | .. _smali_shell: 2 | 3 | ******************** 4 | Smali Shell (ISmali) 5 | ******************** 6 | 7 | Introducing a new file format that combines the simplicity of shell scripts with the power of Smali-Code 8 | in version :ref:`release-0.1.1` of this project. This new format, which will be called Smali-Script is 9 | defined by the file suffix ``".ssf"``. It is designed to allow users to create and run Smali-Code directly 10 | from a single file, using Smali's syntax. 11 | 12 | To use a Smali-Script file, users simply need to create a new file with the ``.ssf`` suffix and write their 13 | Smali-Code. They can then run it with ``ismali``. This makes it an ideal format for executing small code 14 | snippets that can be used within deobfuscation. 15 | 16 | .. code-block:: bash 17 | 18 | $ ismali 19 | ISmali $VERSION on $PLATFORM 20 | >>> # put Smali instructions here 21 | 22 | Before starting to execute Smali instructions, it is worth to know that there are some 23 | global variables that can be queried: 24 | 25 | * ``vars``: This command will print out all registers with their values 26 | * ``label``: Prints the name of the current label 27 | * ``fields``: Prints all fields that have been defined in the root context 28 | * ``import``: Loads a class defined in a ``*.smali`` file. The class can be used afterwards 29 | 30 | .. note:: 31 | 32 | By now, only primitive types and ``String`` objects can be instantiated. Instances of other 33 | types can be created after importing the class. All instructions will be executed directly and 34 | fields will be stored in the root class named ``L;``. 35 | 36 | 37 | Define Smali-Fields 38 | =================== 39 | 40 | Fields can be defined rather easily. By now, only one-liner are accepted: 41 | 42 | .. code-block:: bash 43 | :linenos: 44 | 45 | >>> .field public static abc:Ljava/lang/String; = "Hello, World!" 46 | >>> # Retrieve the value via sget on static fields 47 | >>> sget v0, L;->abc:Ljava/lang/String; 48 | >>> vars 49 | {'p0': , 'v0': 'Hello, World!'} 50 | 51 | In the first line of the code above we defined a new static field with a pre-defined value 52 | ``"Hello, World!"``. Next, the value of our field is stored in the register ``v0`` (actually, 53 | the name can be customized). Because we moved abc's value into a register, we can see that 54 | value by typing ``vars``. 55 | 56 | Execute Smali-Methods 57 | ===================== 58 | 59 | The next example illustrates how to simply invoke a method within the interpreter. 60 | 61 | .. warning:: 62 | 63 | As of version ``0.2.0`` it is not possible two define methods in the root-context. This feature 64 | is proposed to be released in the next version. 65 | 66 | .. code-block:: bash 67 | :linenos: 68 | 69 | >>> invoke-virtual {p0}, Ljava/lang/Object;->toString()Ljava/lang/String; 70 | >>> move-result-object v1 71 | >>> vars 72 | {'p0': , 73 | 'v1': ';>'} 74 | 75 | In this example we called ``Object.toString()`` within the root-context. As we can see, the first 76 | register stores the actual instance and the second (``v0``) a string representation of it. 77 | 78 | 79 | Shell Components 80 | ================ 81 | 82 | .. automodule:: smali.shell 83 | :members: 84 | 85 | .. autoclass:: smali.shell.ISmaliShell 86 | :members: 87 | 88 | -------------------------------------------------------------------------------- /docs/api/smali/visitor.rst: -------------------------------------------------------------------------------- 1 | .. _smali_visitor_api: 2 | 3 | ***************** 4 | Smali Visitor API 5 | ***************** 6 | 7 | .. automodule:: smali.visitor 8 | 9 | 10 | .. autoclass:: smali.visitor.VisitorBase 11 | :members: 12 | 13 | .. autoclass:: smali.visitor.ClassVisitor 14 | :members: 15 | 16 | .. autoclass:: smali.visitor.AnnotationVisitor 17 | :members: 18 | 19 | .. autoclass:: smali.visitor.FieldVisitor 20 | :members: 21 | 22 | .. autoclass:: smali.visitor.MethodVisitor 23 | :members: 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/api/smali/writer.rst: -------------------------------------------------------------------------------- 1 | .. _smali_writer_api: 2 | 3 | **************** 4 | Smali Writer API 5 | **************** 6 | 7 | .. automodule:: smali.writer 8 | 9 | 10 | Code cache 11 | ========== 12 | 13 | .. autoclass:: smali.writer._ContainsCodeCache 14 | :members: get_cache 15 | 16 | .. autoclass:: smali.writer._CodeCache 17 | :members: 18 | 19 | 20 | Writer implementation 21 | ===================== 22 | 23 | .. autoclass:: smali.writer.SmaliWriter 24 | :members: 25 | 26 | .. autoclass:: smali.writer._SmaliClassWriter 27 | :members: code, reset 28 | 29 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | ********* 4 | Changelog 5 | ********* 6 | 7 | .. _release-0.2.8: 8 | 9 | 0.2.8 10 | ----- 11 | 12 | Some small bug fixes in :class:`SmaliReader`: 13 | 14 | * Fixed parsing of :code:`.local` lines 15 | * EOL comments are now captured correctly using a new regex 16 | 17 | .. _release-0.2.7: 18 | 19 | 0.2.7 20 | ----- 21 | 22 | Some small bug fixes and behavior changes in :class:`_SmaliClassWriter` 23 | 24 | * Fixed a bug where the default classloader would be used everytime in a :class:`SmaliVM` instance 25 | * Fixed a bug in :code:`SmaliClass.fields()` where a call to :code:`.items()` was missing 26 | * :class:`_SmaliClassWriter`, :class:`_SmaliMethodWriter`, :class:`_SmaliFieldWriter` and :class:`_SmaliAnnotationWriter` now return the result of the delegate visitor instead of a new one if the delegate is an instance of the target writer class. 27 | 28 | .. _release-0.2.6: 29 | 30 | 0.2.6 31 | ----- 32 | 33 | Some bug fixes by `WaferJay `_. 34 | 35 | * Fixed a bug in end of line comments not parsed properly in strings with `#` inside 36 | * Improved `.catchall` directive 37 | * Other fixes, see `Commit details `_ 38 | 39 | 40 | .. _release-0.2.5: 41 | 42 | 0.2.5 43 | ----- 44 | 45 | Some fixes around :class:`_SmaliMethodWriter` by `WaferJay `_. 46 | 47 | * Added missing `.end` directives for packed-switch, sparse-switch and array-data 48 | 49 | .. _release-0.2.4: 50 | 51 | 0.2.4 52 | ----- 53 | 54 | * Improved `pretty_name` and `dvm_name` of :class:`SVMType` 55 | 56 | .. _release-0.2.3: 57 | 58 | 0.2.3 59 | ----- 60 | 61 | * Fixed issues #1 and `#2 `_ 62 | * Added an appropriate error message upon invalid statements. 63 | 64 | .. _release-0.2.2: 65 | 66 | 0.2.2 67 | ----- 68 | 69 | A small patch created by `TheZ3ro `_ fixing the following issues: 70 | 71 | * Fixed ``sparse_switch`` executor that was not executed due to a typo 72 | * String regex now support handling of unicode strings (e.g. ``const-string v0, "\u06e4\u06eb``"), which initially would result in an error 73 | 74 | In addition, this patch introduces the following new features: 75 | 76 | * Hex values regex now support negative and positive numbers (e.g. ``const v1, -0x1``) 77 | * Added ``move_object`` executor 78 | * Added ``java.lang.String.hashCode()`` implementation as a direct-call 79 | * Refactored direct-call implementations for Object and Class 80 | 81 | .. _release-0.2.1: 82 | 83 | 0.2.1 84 | ----- 85 | 86 | Kudos to `serenees `_ for fixing these issues: 87 | 88 | * Fixed an issue where ``Line.split_line`` would return incorrect results if the input string started with the provided separator. 89 | * Added *".subannotation"* token verification 90 | * Added support to handle unnamed *".param"* declarations 91 | * Changed exception descriptor of *".catchall"* to ``Ljava/lang/Exception;`` for all cases 92 | 93 | 94 | .. _release-0.2.0: 95 | 96 | 0.2.0 97 | ===== 98 | 99 | This minor update includes a new smali type system and removes the ``SmaliValueProxy``. The following changes were made: 100 | 101 | * Removed the class ``SmaliValueProxy`` completely, the method ``smali_value`` now returns one of: int, float, str, SVMType, bool 102 | * New classes :class:`SVMType` and :class:`Signature` to represent smali types: 103 | 104 | .. code-block:: python 105 | :caption: Some usage examples 106 | :linenos: 107 | 108 | from smali import SVMType, Signature 109 | 110 | # simple type instance 111 | t = SVMType("Lcom/example/Class;") 112 | t.simple_name # Class 113 | t.pretty_name # com.example.Class 114 | t.dvm_name # com/example/Class 115 | t.full_name # Lcom/example/Class; 116 | t.svm_type # SVMType.TYPES.CLASS 117 | 118 | # create new type instance for method signature 119 | m = SVMType("getName([BLjava/lang/String;)Ljava/lang/String;") 120 | m.svm_type # SVMType.TYPES.METHOD 121 | # retrieve signature instance 122 | s = m.signature or Signature(m) 123 | s.return_type # SVMType("Ljava/lang/String;") 124 | s.parameter_types # [SVMType("[B"), SVMType("Ljava/lang/String;")] 125 | s.name # getName 126 | s.declaring_class # would return the class before '->' (only if defined) 127 | 128 | # array types 129 | array = SVMType("[B") 130 | array.svm_type # SVMType.TYPES.ARRAY 131 | array.array_type # SVMType("B") 132 | array.dim # 1 (one dimension) 133 | 134 | .. _release-0.1.3: 135 | 136 | 0.1.3 137 | ===== 138 | 139 | * Fixed an issue in :class:`SmaliReader` that causes it to run into infinite loops (kudos to `metalcorpe `_) 140 | * Moved to Github-Pages instead of ReadTheDocs 141 | * Added the field ``parent`` to an execution :class:`Frame` to enable backtracking of call stacks 142 | * Some issues around :class:`Type` and :class:`SmaliValueProxy` fixed 143 | 144 | .. _release-0.1.2: 145 | 146 | 0.1.2 147 | ===== 148 | 149 | * :class:`SmaliVM` is now able to use customized executors. 150 | 151 | .. note:: 152 | The default class loader won't throw any exception upon unknown by default. You 153 | can change this behaviour by setting the ``use_strict`` attribute to ``True``: 154 | 155 | .. code-block:: python 156 | 157 | vm = SmaliVM(use_strict=True) 158 | 159 | * Code was formatted properly 160 | * Documentation update 161 | 162 | 163 | .. _release-0.1.1: 164 | 165 | 0.1.1 166 | ===== 167 | 168 | * ISmali (interactive Smali shell) pre-release 169 | * Implementation of almost half of all Smali-opcodes 170 | * Fixed an error of :class:`SmaliValueProxy` that caused exceptions on operations with an object of the same class 171 | * Multiple bug fixes in the process of class definition (import) 172 | 173 | .. _release-1.0.0: 174 | 175 | 0.0.1 176 | ===== 177 | 178 | * Start keeping changelog :) 179 | * Documentation on Smali language 180 | * Smali parser implementation (line-based) 181 | * Small Smali-VM 182 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | import sys 9 | import os 10 | 11 | project = 'pysmali' 12 | copyright = '2023-2024, MatrixEditor' 13 | author = 'MatrixEditor' 14 | 15 | # The short X.Y version. 16 | version = '0.2' 17 | # The full version, including alpha/beta/rc tags. 18 | release = '0.2.8' 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | sys.path.insert(0, os.path.abspath('..')) 24 | 25 | # -- General configuration --------------------------------------------------- 26 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 27 | extensions = [ 28 | 'sphinx.ext.autodoc', 29 | 'sphinx.ext.doctest', 30 | 'sphinx.ext.todo', 31 | ] 32 | 33 | templates_path = ['_templates'] 34 | exclude_patterns = [] 35 | 36 | # The master toctree document. 37 | master_doc = 'index' 38 | 39 | # The suffix of source filenames. 40 | source_suffix = '.rst' 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # List of patterns, relative to source directory, that match files and 46 | # directories to ignore when looking for source files. 47 | exclude_patterns = ['_build', 'README.rst'] 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 51 | # To build locally, run "pip install sphinx-rtd-theme" before 52 | import sphinx_rtd_theme 53 | 54 | html_theme = 'sphinx_rtd_theme' 55 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 56 | 57 | html_static_path = ['_static'] 58 | html_theme_options = { 59 | 'logo_only': False, 60 | 'display_version': True, 61 | 'prev_next_buttons_location': 'bottom', 62 | 'style_external_links': False, 63 | # Toc options 64 | 'collapse_navigation': False, 65 | 'titles_only': False 66 | } 67 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are what make the open source community such an amazing place to learn, 8 | inspire, and create. Any contributions you make are greatly appreciated. 9 | 10 | If you have a suggestion that would make this better, please fork the repo and create 11 | a pull request. You can also simply open an issue with the tag "enhancement". 12 | 13 | Set up a local environment 14 | -------------------------- 15 | 16 | 1. Clone or fork the repository 17 | 18 | .. code-block:: console 19 | 20 | $ git clone https://github.com/MatrixEditor/pysmali.git 21 | 22 | 2. Install the required dependencies: 23 | 24 | .. code-block:: console 25 | 26 | $ pip install -r ./pysmali/requirements.txt 27 | 28 | 29 | Create a new feature request 30 | ---------------------------- 31 | 32 | * Fork the Project 33 | * Create your Feature Branch (``git checkout -b feature/AmazingFeature``) 34 | * Commit your Changes (``git commit -m 'Add some AmazingFeature'``) 35 | * Push to the Branch (``git push origin feature/AmazingFeature``) 36 | * Open a Pull Request 37 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | .. _development: 2 | 3 | =========== 4 | Development 5 | =========== 6 | 7 | Developers working on ``pysmali`` should consider the following guidelines for developing 8 | and releases. 9 | 10 | .. _supported-dependencies: 11 | 12 | Supported dependencies 13 | ---------------------- 14 | 15 | The package supports the following dependencies: 16 | 17 | .. list-table:: Supported dependencies 18 | :header-rows: 1 19 | :widths: 10, 10 20 | 21 | * - Dependency 22 | - Versions 23 | * - Python 24 | - at least Python 3 25 | 26 | Roadmap 27 | ------- 28 | 29 | Several releases are planned in the development roadmap. Backward 30 | incompatible changes, deprecations, and major features are noted for each of 31 | these releases. 32 | 33 | Releases published follow `semantic versioning`_, and so it is recommended that 34 | dependencies on ``pysmali`` are pinned to a specific version below the next major 35 | version. 36 | 37 | .. _semantic versioning: http://semver.org/ 38 | 39 | 40 | 0.3.0 41 | ~~~~~ 42 | 43 | :Planned release date: Unknown 44 | 45 | This release should integrate JPype to support more functionalities within the 46 | SmaliVM. -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | PySmali's documentation 2 | ======================= 3 | 4 | Welcome to the documentation for `pysmali`, a Python3 package designed for parsing, 5 | transforming and generating Smali source code files, as well as interpreting source 6 | code files. This documentation is intended to provide an overview of the package's 7 | features, installation instructions, and usage examples to help you get started with 8 | using the package in your Python projects. 9 | 10 | Using this library 11 | ------------------ 12 | 13 | :doc:`installation` 14 | How to install the python package locally or in a virtal environment. 15 | 16 | :doc:`Using the Smali API ` 17 | Introduction into Smali and the provided Smali API. 18 | 19 | :doc:`Using the Smali-Emulator ` 20 | Introduction into the provided Smali-Interpreter. 21 | 22 | :ref:`supported-dependencies` 23 | Supported project dependencies. 24 | 25 | 26 | Examples 27 | -------- 28 | 29 | .. code-block:: python 30 | :caption: Reading Smali code 31 | :linenos: 32 | 33 | from smali import SmaliReader, SmaliWriter 34 | 35 | reader = SmaliReader(comments=False) 36 | writer = SmaliWriter(reader) 37 | 38 | with open('example.smali', 'r', encoding='utf-8') as fp: 39 | source = fp.read() 40 | 41 | # The writer can be any instance of a class visitor 42 | reader.visit(source, writer) 43 | print(writer.code) 44 | # or 45 | print(str(writer)) 46 | 47 | .. code-block:: python 48 | :linenos: 49 | :caption: Modifying Smali code 50 | 51 | from smali import SmaliWriter, AccessType 52 | 53 | writer = SmaliWriter() 54 | # Create the .class statement 55 | writer.visit_class("Lcom/example/Car;", AccessType.PUBLIC + AccessType.ABSTRACT) 56 | # Declare the super class 57 | writer.visit_super("Ljava/lang/Object;") 58 | # Visit the interface implementation 59 | writer.visit_implements("Ljava/lang/Runnable") 60 | 61 | # Create the field id 62 | writer.visit_field("id", AccessType.PRIVATE, "I") 63 | 64 | # Create the method 65 | m_writer = writer.visit_method("run", AccessType.PUBLIC + AccessType.ABSTRACT, [], "V") 66 | m_writer.visit_end() 67 | 68 | # finish class creation 69 | writer.visit_end() 70 | source_code = writer.code 71 | 72 | 73 | .. code-block:: python 74 | :linenos: 75 | :caption: Execute Smali code 76 | 77 | from smali.bridge import SmaliClass, SmaliObject, SmaliVM 78 | 79 | # Let's assume, the class' source code is stored here 80 | source = ... 81 | 82 | vm = SmaliVM() 83 | # Load and define the class (don't call the method) 84 | my_class: SmaliClass = vm.classloader.load_class(source, init=False) 85 | 86 | # To manually initialize the class, call 87 | my_class.clinit() 88 | 89 | # Instances can be created by providing the class object 90 | instance: SmaliObject = SmaliObject(my_class) 91 | 92 | # Initialization must be done separately: 93 | instance.init(...) 94 | 95 | # Methods are stored in our class object (not in the 96 | # actual instance) 97 | toString: SmaliMethod = my_class.method("toString") 98 | 99 | # The first argument of instance method must be the 100 | # object itself (on static methods, just use None) 101 | value = toString(instance) 102 | 103 | # The returned value behaves like a string 104 | print("Returned value:", value) 105 | 106 | 107 | To execute Smali files from the cli, just use the ``ismali`` command that comes with 108 | installation of this module: 109 | 110 | .. code-block:: bash 111 | 112 | $ ismali 113 | >>> vars 114 | {'p0': } 115 | >>> const-string v1, "Hello World" 116 | >>> vars 117 | {'p0': , 'v1': 'Hello World'} 118 | 119 | 120 | Development 121 | ----------- 122 | 123 | :doc:`contributing` 124 | How to contribute changes to the project. 125 | 126 | :doc:`Development guidelines ` 127 | Guidelines the theme developers use for developing and testing changes. 128 | 129 | :doc:`changelog` 130 | The package development changelog. 131 | 132 | .. toctree:: 133 | :maxdepth: 3 134 | :caption: General Documentation 135 | :hidden: 136 | 137 | installation 138 | api/smali/language 139 | development 140 | contributing 141 | changelog 142 | 143 | .. Hidden TOCs 144 | 145 | .. toctree:: 146 | :maxdepth: 3 147 | :caption: Smali API Documentation 148 | :hidden: 149 | :numbered: 150 | 151 | api/smali/index 152 | api/smali/reader 153 | api/smali/writer 154 | api/smali/visitor 155 | api/smali/base 156 | 157 | 158 | .. toctree:: 159 | :maxdepth: 3 160 | :caption: Smali Bridge 161 | :hidden: 162 | :numbered: 163 | 164 | api/smali/bridge 165 | api/smali/bridge_api 166 | api/smali/shell 167 | 168 | 169 | 170 | Indices and tables 171 | ================== 172 | 173 | * :ref:`genindex` 174 | * :ref:`modindex` 175 | * :ref:`search` 176 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | 8 | How to install the package 9 | -------------------------- 10 | 11 | Install the whole ``pysmali`` package (or add it to your ``requirements.txt`` file): 12 | 13 | .. code:: console 14 | 15 | $ pip install pysmali 16 | 17 | 18 | .. hint:: 19 | 20 | In most cases you want to create a virtual environment as it provides extra security features 21 | when working with packages. To create a simple virtual environment, run: 22 | 23 | .. code-block:: console 24 | :caption: Linux 25 | 26 | $ python3 -m venv ./venv && source ./venv/bin/activate 27 | 28 | .. code-block:: console 29 | :caption: Windows 30 | 31 | $ py -m venv ./venv && ./venv/Scripts/activate.bat 32 | 33 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | requests 4 | urllib3 -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | from smali import SmaliReader, SmaliWriter 2 | 3 | reader = SmaliReader(comments=False) 4 | writer = SmaliWriter(reader) 5 | 6 | with open('example.smali', 'r', encoding='utf-8') as fp: 7 | source = fp.read() 8 | 9 | reader.visit(source, writer) 10 | print(writer.code) 11 | -------------------------------------------------------------------------------- /examples/example.ssf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ismali 2 | # This file is an example of a Smali-Script that muliplies two values 3 | # and stores the result in an extra register. 4 | 5 | # Recursive imports won't be done if 'shell.check_imports' is enabled 6 | import examples/example.ssf 7 | 8 | const/16 a, 50 9 | const/16 b, 5 10 | const/16 c, 0 11 | 12 | mul-int c,a,b 13 | 14 | # use the vars-command to print the result as JSON string 15 | vars -------------------------------------------------------------------------------- /examples/rewrite.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from smali import SmaliReader, SmaliWriter, MethodWriter 3 | 4 | 5 | class CustomInstructionsWriter(MethodWriter): 6 | def __init__(self, delegate=None, indent=0) -> None: 7 | super().__init__(delegate, indent) 8 | 9 | # if you know the structure, it will be easy to install 10 | # the right hook 11 | def visit_registers(self, registers: int) -> None: 12 | super().visit_registers(registers) 13 | # add custom instruction 14 | self.visit_locals(1000) 15 | 16 | 17 | class CustomWriter(SmaliWriter): 18 | def visit_method( 19 | self, name: str, access_flags: int, parameters: list, return_type: str 20 | ) -> Optional[MethodWriter]: 21 | if name == "main": 22 | return CustomInstructionsWriter() 23 | 24 | 25 | 26 | reader = SmaliReader(comments=False) 27 | writer = SmaliWriter(reader, delegate=CustomWriter()) 28 | 29 | with open("rewrite.smali", "r", encoding="utf-8") as fp: 30 | source = fp.read() 31 | 32 | reader.visit(source, writer) 33 | print(writer.code) -------------------------------------------------------------------------------- /examples/rewrite.smali: -------------------------------------------------------------------------------- 1 | .class public final Lcom/example/Example; 2 | .super Ljava/lang/Object; 3 | .source "Example.java" 4 | 5 | .method public static main([Ljava/lang/String;)V 6 | .registers 1 7 | 8 | return-void 9 | .end method -------------------------------------------------------------------------------- /examples/shell_example.py: -------------------------------------------------------------------------------- 1 | from smali.shell import ISmaliShell, start_cli 2 | 3 | # Just start the shell in your code or 4 | # call it from the CLI with 'python3 -m smali.shell' 5 | shell = ISmaliShell() 6 | 7 | # disable import checks so that recursive imports are 8 | # possible (NOT recommended and disabled by default) 9 | shell.check_import = False 10 | shell.cmdloop() 11 | 12 | # Or just call start CLI 13 | start_cli() 14 | -------------------------------------------------------------------------------- /examples/tests/add-int-lit.ssf: -------------------------------------------------------------------------------- 1 | # Expected output: {'a': 5, 'c': 10, 'p0': } 2 | const/16 a, 5 3 | 4 | add-int/lit16 c,a,5 5 | vars -------------------------------------------------------------------------------- /examples/tests/add-int.ssf: -------------------------------------------------------------------------------- 1 | # result: {'a': 50, 'c': 55, 'b': 5, 'p0': ...} 2 | const/16 a, 50 3 | const/16 b, 5 4 | const/16 c, 0 5 | 6 | add-int c,a,b 7 | vars -------------------------------------------------------------------------------- /examples/vm_example.py: -------------------------------------------------------------------------------- 1 | from smali.bridge import SmaliVM, SmaliObject 2 | 3 | vm = SmaliVM() 4 | with open('example.smali', 'r', encoding='utf-8') as fp: 5 | smali_class = vm.classloader.load_class(fp, init=False) 6 | 7 | smali_class.clinit() 8 | 9 | instance = SmaliObject(smali_class) 10 | instance.init() 11 | 12 | toString = instance.smali_class.method("toString") 13 | value = toString(instance) 14 | 15 | print(value) 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pysmali" 3 | version = "0.2.8" 4 | description="Smali Visitor-API and Smali emulator" 5 | authors = [ 6 | { name="MatrixEditor", email="not@supported.com" }, 7 | ] 8 | readme = "README.md" 9 | classifiers = [ 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Intended Audience :: Science/Research', 12 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 13 | 'Programming Language :: Python :: 3.9', 14 | 'Programming Language :: Python :: 3.10', 15 | ] 16 | 17 | [project.scripts] 18 | ismali = "smali.shell:start_cli" 19 | 20 | [project.urls] 21 | "Homepage" = "https://github.com/MatrixEditor/pysmali" 22 | "API-Docs" = "https://matrixeditor.github.io/pysmali/" 23 | 24 | [tool.setuptools.packages.find] 25 | where = ["."] 26 | include = ["smali*"] 27 | 28 | [build-system] 29 | requires = ["setuptools"] 30 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixEditor/pysmali/bd00d66c097c16d97d4c2c5007c992d1ff1ee4a5/requirements.txt -------------------------------------------------------------------------------- /smali/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | __doc__ = """ 17 | Implementation of a line-based Smali source code parser using a visitor API. 18 | """ 19 | 20 | from smali.visitor import * 21 | from smali.base import * 22 | from smali import opcode 23 | from smali.reader import * 24 | from smali.writer import * 25 | 26 | SmaliValue = smali_value 27 | 28 | VERSION = '0.2.8' 29 | -------------------------------------------------------------------------------- /smali/base.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | from typing import Any 18 | 19 | __doc__ = """ 20 | Basic component classes when working with the Smali language. 21 | """ 22 | 23 | import re 24 | 25 | from enum import Enum, IntFlag 26 | 27 | __all__ = [ 28 | "AccessType", 29 | "Token", 30 | "Line", 31 | "smali_value", 32 | "is_type_descriptor", 33 | "Signature", 34 | "SVMType", 35 | ] 36 | 37 | 38 | class AccessType(IntFlag): 39 | """Contains all access modifiers for classes, fields, methods and annotations. 40 | 41 | There is also a possibility to use values of this class with an ``in`` 42 | statement: 43 | 44 | >>> flags = AccessType.PUBLIC + AccessType.FINAL 45 | >>> flags in AccessType.PUBLIC 46 | True 47 | >>> flags in AccessType.PRIVATE 48 | False 49 | """ 50 | 51 | # !! IMPORTANT: Although, all access types from here (https://source.android.com/docs/core/runtime/dex-format#access-flags) 52 | # are covered, their values differ as there are multiple access flags with the same value. 53 | # REVISIT: Maybe convert this class into a dictionary 54 | PUBLIC = 0x1 55 | PRIVATE = 0x2 56 | PROTECTED = 0x4 57 | STATIC = 0x8 58 | FINAL = 0x10 59 | SYNCHRONIZED = 0x20 60 | VOLATILE = 0x40 61 | BRIDGE = 0x80 62 | TRANSIENT = 0x100 63 | VARARGS = 0x200 64 | NATIVE = 0x400 65 | INTERFACE = 0x800 66 | ABSTRACT = 0x1000 67 | STRICTFP = 0x2000 68 | SYNTHETIC = 0x4000 69 | ANNOTATION = 0x8000 70 | ENUM = 0x10000 71 | CONSTRUCTOR = 0x20000 72 | DECLARED_SYNCHRONIZED = 0x40000 73 | SYSTEM = 0x80000 74 | RUNTIME = 0x100000 75 | BUILD = 0x200000 76 | 77 | @staticmethod 78 | def get_flags(values: list) -> int: 79 | """Converts the given readable access modifiers into an integer. 80 | 81 | :param values: the keyword list 82 | :type values: list 83 | :return: an integer storing all modifiers 84 | :rtype: int 85 | """ 86 | result = 0 87 | for element in values: 88 | if not element: 89 | continue 90 | 91 | element = str(element).lower() 92 | for val in AccessType: 93 | if val.name.lower().replace("_", "-") == element: 94 | result |= val.value 95 | return result 96 | 97 | @staticmethod 98 | def get_names(flags: int) -> list: 99 | """Converts the given access modifiers to a human readable representation. 100 | 101 | :param flags: the access modifiers 102 | :type flags: int 103 | :return: a list of keywords 104 | :rtype: list 105 | """ 106 | result = [] 107 | for val in AccessType: 108 | if flags in val: 109 | result.append(val.name.lower().replace("_", "-")) 110 | return result 111 | 112 | @staticmethod 113 | def find(value: str) -> bool: 114 | """Returns whether the given keyword is a valid modifier. 115 | 116 | :param value: the value to check 117 | :type value: str 118 | :return: True, if the given value represents an access modifier 119 | :rtype: bool 120 | """ 121 | for val in AccessType: 122 | name = val.name.lower().replace("_", "-") 123 | if name == value: 124 | return True 125 | return False 126 | 127 | def __contains__(self, other: int) -> bool: 128 | if isinstance(other, self.__class__): 129 | return super().__contains__(other) 130 | if isinstance(other, int): 131 | return self.value & other != 0 132 | raise TypeError(f"Unsupported type: {type(other)}") 133 | 134 | 135 | class Token(Enum): 136 | """Defines all common token in a Smali file. 137 | 138 | There are some special methods implemented to use constants of this 139 | class in the following situations: 140 | 141 | >>> "annotation" == Token.ANNOTATION 142 | True 143 | >>> "local" != Token.LOCALS 144 | True 145 | >>> len(Token.ENUM) 146 | 4 147 | >>> str(Token.ARRAYDATA) 148 | 'array-data' 149 | """ 150 | 151 | ANNOTATION = "annotation" 152 | ARRAYDATA = "array-data" 153 | CATCH = "catch" 154 | CATCHALL = "catchall" 155 | CLASS = "class" 156 | END = "end" 157 | ENUM = "enum" 158 | FIELD = "field" 159 | IMPLEMENTS = "implements" 160 | LINE = "line" 161 | LOCAL = "local" 162 | LOCALS = "locals" 163 | METHOD = "method" 164 | PACKEDSWITCH = "packed-switch" 165 | PARAM = "param" 166 | PROLOGUE = "prologue" 167 | REGISTERS = "registers" 168 | RESTART = "restart" 169 | SOURCE = "source" 170 | SPARSESWITCH = "sparse-switch" 171 | SUBANNOTATION = "subannotation" 172 | SUPER = "super" 173 | DEBUG = "debug" 174 | 175 | def __eq__(self, other: object) -> bool: 176 | if isinstance(other, self.__class__): 177 | return super().__eq__(other) 178 | return self.value == other 179 | 180 | def __ne__(self, other: object) -> bool: 181 | if isinstance(other, self.__class__): 182 | return super().__ne__(other) 183 | return self.value != other 184 | 185 | def __len__(self) -> int: 186 | return len(str(self.value)) 187 | 188 | def __str__(self) -> str: 189 | return self.value 190 | 191 | 192 | class Line: 193 | """Simple peekable Iterator implementation.""" 194 | 195 | RE_EOL_COMMENT = re.compile(r"\"(?:\\.|[^\"\\])*\"|(?P#.*)") 196 | """Pattern for EOL (end of line) comments""" 197 | 198 | _default = object() 199 | """The default object which is returned to indicate the 200 | end of the current line has been reached.""" 201 | 202 | raw: str 203 | """The raw line as it was passed through the constructor.""" 204 | 205 | cleaned: str 206 | """The cleaned line without any leading and trailing whitespace""" 207 | 208 | eol_comment: str 209 | """The removed trailing EOL comment (if present)""" 210 | 211 | def __init__(self, line: str | None) -> None: 212 | if isinstance(line, (bytearray, bytes)): 213 | line = line.decode() 214 | 215 | self._it = None 216 | self._head = self._default 217 | self._elements = [] 218 | self.reset(line) 219 | 220 | def _get_next(self) -> str | Any: 221 | try: 222 | return next(self._it) 223 | except StopIteration: 224 | return self._default 225 | 226 | def __next__(self) -> str | Any: 227 | value = self._head 228 | if value == self._default: 229 | raise StopIteration() 230 | 231 | self._head = self._get_next() 232 | return value 233 | 234 | def reset(self, line: str | None = None) -> None: 235 | """Resets this line and/or initialized it with the new value. 236 | 237 | :param line: the next line, defaults to None 238 | :type line: str, optional 239 | """ 240 | if not line: 241 | self._head = self._default 242 | self._it = None 243 | return 244 | 245 | self.eol_comment = None 246 | self.raw = line.rstrip() 247 | self.cleaned = self.raw.lstrip() 248 | eol_match = Line.RE_EOL_COMMENT.search(self.cleaned + "\n") 249 | if eol_match is not None: 250 | comment = eol_match.group("comment") 251 | if comment is not None: 252 | # Remove the EOL comment and save it in a variable. Note 253 | # that the visitor will be notified when StopIteration is 254 | # raised. 255 | self.eol_comment = comment 256 | self.cleaned = self.cleaned[: -len(comment)] 257 | 258 | self._elements = Line.split_line(self.cleaned) 259 | self._it = iter(self._elements) 260 | self._head = self._get_next() 261 | 262 | def peek(self, default: str | Any = _default) -> str | Any: 263 | """Returns the current element if this line. 264 | 265 | This method won't move forwards. 266 | 267 | :param default: the default value to return, defaults to _default 268 | :type default: str, optional 269 | :raises StopIteration: if the end of this line has been reached 270 | :return: the current value 271 | :rtype: str 272 | """ 273 | if self._head == self._default: 274 | if default != self._default: 275 | return default 276 | raise StopIteration() 277 | return self._head 278 | 279 | def last(self) -> str: 280 | """Returns the last element of this line without modifying the iterator. 281 | 282 | :return: the last element 283 | :rtype: str 284 | """ 285 | return self._elements[-1] 286 | 287 | def has_eol(self) -> bool: 288 | """Returns whether this line contains an EOL comment 289 | 290 | :return: True, if the line contains an EOL comment 291 | :rtype: bool 292 | """ 293 | return self.eol_comment is not None 294 | 295 | def __bool__(self) -> bool: 296 | return self._head != self._default 297 | 298 | def has_next(self) -> bool: 299 | """Returns whether there as a following element. 300 | 301 | :return: True if next() can be called safely 302 | :rtype: bool 303 | """ 304 | return self.__bool__() 305 | 306 | def __len__(self) -> int: 307 | return len(self.cleaned) 308 | 309 | @staticmethod 310 | def split_line(cleaned: str, sep: str = " ") -> list: 311 | """Splits the line by the given delimiter and ignores values in strings 312 | 313 | :param cleaned: the input string 314 | :type cleaned: str 315 | :param sep: the delimiter, defaults to ' ' 316 | :type sep: str, optional 317 | :return: the splitted values 318 | :rtype: list 319 | """ 320 | end = cleaned.find(sep) 321 | start = 0 322 | in_literal = False 323 | elements = [] 324 | 325 | while end != -1: 326 | if end - 1 > 0 and cleaned[end - 1] == '"' and in_literal: 327 | in_literal = False 328 | elements.append(cleaned[start:end]) 329 | start = end + 1 330 | 331 | elif cleaned[start] == '"': 332 | in_literal = True 333 | 334 | elif not in_literal: 335 | elements.append(cleaned[start:end]) 336 | start = end + 1 337 | 338 | end = cleaned.find(sep, end + 1) 339 | elements.append(cleaned[start:]) 340 | return elements 341 | 342 | 343 | class Signature: 344 | """Internal class to encapsulate method signatures.""" 345 | 346 | CLINIT = "" 347 | """Static block initializer""" 348 | 349 | INIT = "" 350 | """Constructor method""" 351 | 352 | def __init__(self, __signature: str | SVMType) -> None: 353 | self.__signature = str(__signature) 354 | self.__params = None 355 | self.__return_type = None 356 | self.__name = None 357 | 358 | @property 359 | def sig(self) -> str: 360 | """Returns the fully qualified signature string. 361 | 362 | >>> s = Signature("(II)V") 363 | >>> s.sig 364 | '(II)V' 365 | 366 | :return: the input string 367 | :rtype: str 368 | """ 369 | return self.__signature 370 | 371 | @property 372 | def name(self) -> str: 373 | """Returns the method name 374 | 375 | >>> s = Signature("Lcom/example/Class;->getLength(Ljava/lang/String;)I") 376 | >>> s.name 377 | 'getLength' 378 | 379 | :return: the absolute method name 380 | :rtype: str 381 | """ 382 | if self.__name: 383 | return self.__name 384 | 385 | idx = self.__signature.find("(") 386 | if idx == -1: 387 | raise TypeError( 388 | f"Invalid method signature: could not find name ({self.__signature})" 389 | ) 390 | 391 | name = self.__signature[:idx] 392 | if "->" in name: 393 | name = name[name.find("->") + 2 :] 394 | # Handle bracket names if not or 395 | if name in (Signature.INIT, Signature.CLINIT): 396 | return name 397 | 398 | return name.rstrip(">").lstrip("<") 399 | 400 | @property 401 | def declaring_class(self) -> SVMType | None: 402 | """Returns the :class:`SVMType` of the method's declaring class. 403 | 404 | >>> s1 = Signature("Lcom/example/Class;->(II)V") 405 | >>> str(s1.declaring_class) 406 | 'Lcom/example/Class;' 407 | >>> s2 = Signature("(II)V") 408 | >>> str(s2.declaring_class) 409 | 'None' 410 | 411 | :return: the method's declaring class 412 | :rtype: SVMType | None 413 | """ 414 | if "->" not in self.sig: 415 | return None # no class defined 416 | 417 | return SVMType(self.sig.split("->")[0]) 418 | 419 | @property 420 | def parameter_types(self) -> list[SVMType]: 421 | """Returns the method parameter types. 422 | 423 | >>> s = Signature("(II)V") 424 | >>> params: list[SVMType] = s.parameter_types 425 | >>> [str(x) for x in params] 426 | ["I", "I"] 427 | 428 | :return: the method parameters 429 | :rtype: list 430 | """ 431 | if self.__params: 432 | return self.__params 433 | 434 | start = self.__signature.find("(") 435 | end = self.__signature.find(")") # we don't want the closing brace 436 | if start == -1 or end == -1: 437 | raise TypeError("Invalid method signature") 438 | 439 | params = self.__signature[start + 1 : end] 440 | if not params: 441 | return [] 442 | 443 | param_list = [] 444 | current_param = "" 445 | is_type_def = False 446 | for char in params: 447 | current_param = f"{current_param}{char}" 448 | 449 | if char == "L": 450 | is_type_def = True 451 | continue 452 | 453 | if char == ";": 454 | # This check has to be made before validating whether 455 | # the current param is a custom type, otherwise the 456 | # whole string would be returned. See issue #3 457 | is_type_def = False 458 | 459 | if char == "[" or is_type_def: 460 | continue 461 | 462 | param_list.append(SVMType(current_param)) 463 | current_param = "" 464 | 465 | if current_param: 466 | param_list.append(SVMType(current_param)) 467 | 468 | self.__params = param_list 469 | return self.__params 470 | 471 | @property # lazy 472 | def return_type(self) -> SVMType: 473 | """Retrieves the method's return type 474 | 475 | >>> s = Signature("(II)V") 476 | >>> str(s.return_type) # returns a SVMType instance 477 | 'V' 478 | 479 | :raises TypeError: if there is no valid return type 480 | :return: the return type's descriptor 481 | :rtype: str 482 | """ 483 | if not self.__return_type: 484 | end = self.__signature.find(")") 485 | if end == -1: 486 | raise TypeError("Invalid method signature") 487 | self.__return_type = SVMType(self.__signature[end + 1 :]) 488 | 489 | return self.__return_type 490 | 491 | @property 492 | def descriptor(self) -> str: 493 | """Returns the method descriptor of this signature 494 | 495 | >>> s = Signature("(II)V") 496 | >>> s.descriptor 497 | '(II)V' 498 | 499 | :raises ValueError: if there is no descriptor 500 | :return: the method's descriptor string 501 | :rtype: str 502 | """ 503 | idx = self.sig.find("(") 504 | if idx == -1: 505 | raise ValueError("Invalid method signature - expected '('") 506 | 507 | return self.sig[idx:] 508 | 509 | def __str__(self) -> str: 510 | return self.sig 511 | 512 | def __repr__(self) -> str: 513 | return f'Signature("{self.__signature}")' 514 | 515 | 516 | class SVMType: 517 | class TYPES(Enum): 518 | """Represents the classification of a type descriptor.""" 519 | 520 | ARRAY = 1 521 | PRIMITIVE = 2 522 | CLASS = 3 523 | METHOD = 4 524 | UNKNOWN = 5 525 | 526 | def __init__(self, __type: str) -> None: 527 | self.__class = SVMType.TYPES.UNKNOWN 528 | self.__dim = __type.count("[") 529 | self.__type = self._clean(str(__type)) 530 | self.__array_type = None 531 | if self.dim > 0 and not self.is_signature(): 532 | self.__class = SVMType.TYPES.ARRAY 533 | self.__array_type = SVMType(self.__type.replace("[", "")) 534 | 535 | def _clean(self, __type: str) -> str: 536 | # 1. check if we have a primitive type: 537 | if re.match(r"\[*[ZCBSIFVJD]$", __type): 538 | self.__class = SVMType.TYPES.PRIMITIVE 539 | return __type 540 | 541 | # 2. perform normalization 542 | value = __type.replace(".", "/") 543 | if "(" in value: 544 | # TODO: how to handle malformed method signatures 545 | self.__class = SVMType.TYPES.METHOD 546 | return value 547 | 548 | # 3. Perform class normalization 549 | idx = value.rfind("[") + 1 550 | if value[idx] != "L": 551 | if self.dim > 0: 552 | value = f"{value[:idx]}L{value[idx]}" 553 | else: 554 | value = f"L{value}" 555 | 556 | if not value.endswith(";"): 557 | value = f"{value};" 558 | 559 | self.__class = SVMType.TYPES.CLASS 560 | return value 561 | 562 | def __str__(self) -> str: 563 | return self.__type 564 | 565 | def __repr__(self) -> str: 566 | return f'SVMType("{self.__type}")' 567 | 568 | def is_signature(self) -> bool: 569 | """Returns whether this type instance is a method signature. 570 | 571 | >>> t = SVMType("(II)V") 572 | >>> t.is_signature() 573 | True 574 | 575 | :return: True, if this instance represents a method signature 576 | :rtype: bool 577 | """ 578 | return self.__class == SVMType.TYPES.METHOD 579 | 580 | @property 581 | def signature(self) -> Signature | None: 582 | """Creates a :class:`Signature` object from this type instance. 583 | 584 | :return: the signature object or nothing if this type does not represent a 585 | method signature. 586 | :rtype: Signature | None 587 | """ 588 | return None if self.svm_type != SVMType.TYPES.METHOD else Signature(self.__type) 589 | 590 | @property 591 | def dim(self) -> int: 592 | """Returns the amount of array dimensions. 593 | 594 | :return: the amount of array dimensions. 595 | :rtype: int 596 | """ 597 | return self.__dim 598 | 599 | @property 600 | def svm_type(self) -> SVMType.TYPES: 601 | """Returns the descriptor classification (ARRAY, PRIMITIVE, METHOD or CLASS) 602 | 603 | :return: the classification of this type instance 604 | :rtype: SVMType.TYPES 605 | """ 606 | return self.__class 607 | 608 | @property 609 | def array_type(self) -> SVMType | None: 610 | """Returns the underlying array type (if any) 611 | 612 | >>> array = SVMType("[[B") 613 | 614 | :return: the underlying array type instance 615 | :rtype: SVMType 616 | """ 617 | return self.__array_type 618 | 619 | @property 620 | def pretty_name(self) -> str: 621 | """Returns a prettified version of the class name. 622 | 623 | >>> array = SVMType("[[Lcom/example/Class;") 624 | >>> array.pretty_name 625 | 'com.example.Class[][]' 626 | 627 | :return: full name without ``L`` and ``;``; ``/`` is replaced by a dot. 628 | :rtype: str 629 | """ 630 | array_type = self.array_type 631 | if not array_type: 632 | value = str(self) 633 | else: 634 | value = str(array_type) 635 | return re.sub(r"\/|(->)", ".", value.removeprefix("L").removesuffix(";")) + ( 636 | "[]" * self.dim 637 | ) 638 | 639 | @property 640 | def dvm_name(self) -> str: 641 | """Returns the DVM representation of this type descriptor. 642 | 643 | >>> cls = SVMType("Lcom/example/Class;") 644 | >>> cls.dvm_name 645 | 'com/example/Class' 646 | 647 | Arrays won't be covered by this method, thus the returned value 648 | returns the full class name only: 649 | 650 | >>> cls = SVMType("[[Lcom/example/Class;") 651 | >>> cls.dvm_name 652 | 'com/example/Class' 653 | 654 | :return: the full name without ``L`` and ``;``. 655 | :rtype: str 656 | """ 657 | array_type = self.array_type 658 | if not array_type: 659 | value = str(self) 660 | else: 661 | value = str(array_type) 662 | return value.removeprefix("L").removesuffix(";") 663 | 664 | @property 665 | def full_name(self) -> str: 666 | """Returns the full name of this type descriptor (input value) 667 | 668 | >>> cls = SVMType("com.example.Class") 669 | >>> cls.full_name 670 | 'Lcom/example/Class;' 671 | 672 | :return: the full name 673 | :rtype: str 674 | """ 675 | return str(self) 676 | 677 | @property 678 | def simple_name(self) -> str: 679 | """Returns only the class name (not applicable to method signatures) 680 | 681 | :return: the class' name 682 | :rtype: str 683 | """ 684 | return self.pretty_name.split(".")[-1] 685 | 686 | 687 | def smali_value(value: str) -> int | float | str | SVMType | bool: 688 | """Parses the given string and returns its Smali value representation. 689 | 690 | :param value: the value as a string 691 | :type value: str 692 | :raises ValueError: if it has no valid Smali type 693 | :return: the Smali value representation 694 | :rtype: int | float | str | SVMType | bool 695 | """ 696 | actual_value = None 697 | for i, entry in enumerate(TYPE_MAP): 698 | matcher, wrapper = entry 699 | if matcher.match(value): 700 | if i <= 3: # hex value possible 701 | hex_val = RE_HEX_VALUE.match(value) is not None 702 | if not hex_val: 703 | actual_value = wrapper(value) 704 | else: 705 | actual_value = wrapper(value, base=16) 706 | else: 707 | actual_value = wrapper(value) 708 | break 709 | 710 | # Handling of null values is not implemented yet 711 | if actual_value is None: 712 | raise ValueError(f"Could not find any matching primitive type for {value}") 713 | 714 | return actual_value 715 | 716 | 717 | RE_INT_VALUE = re.compile(r"[\-\+]?(0x)?[\dabcdefABCDEF]+$") 718 | """Pattern for ``int`` values.""" 719 | 720 | RE_BYTE_VALUE = re.compile(r"[\-\+]?(0x)?[\dabcdefABCDEF]+t$") 721 | """Pattern for ``byte`` values.""" 722 | 723 | RE_SHORT_VALUE = re.compile(r"[\-\+]?(0x)?[\dabcdefABCDEF]+s$") 724 | """Pattern for ``short`` values.""" 725 | 726 | RE_FLOAT_VALUE = re.compile(r"[\-\+]?\d+\.\d+f$") 727 | """Pattern for ``float`` values.""" 728 | 729 | RE_DOUBLE_VALUE = re.compile(r"[\-\+]?\d+\.\d+") 730 | """Pattern for ``double`` values.""" 731 | 732 | RE_LONG_VALUE = re.compile(r"[\-\+]?(0x)?[\dabcdefABCDEF]+l$") 733 | """Pattern for ``long`` values.""" 734 | 735 | RE_CHAR_VALUE = re.compile(r"^'.*'$") 736 | """Pattern for ``char`` values.""" 737 | 738 | RE_STRING_VALUE = re.compile(r'^".*"$') 739 | """Pattern for ``String`` values.""" 740 | 741 | RE_TYPE_VALUE = re.compile(r"\[*((L\S*;$)|([ZCBSIFVJD])$)") # NOQA 742 | """Pattern for type descriptors.""" 743 | 744 | RE_BOOL_VALUE = re.compile(r"true|false") 745 | """Pattern for ``boolean`` values.""" 746 | 747 | RE_HEX_VALUE = re.compile(r"[\-\+]?0x[\dabcdefABCDEF]+") 748 | """Pattern for integer values.""" 749 | 750 | TYPE_MAP: list = [ 751 | (RE_SHORT_VALUE, lambda x, **kw: int(x[:-1], **kw)), 752 | (RE_LONG_VALUE, lambda x, **kw: int(x[:-1], **kw)), 753 | (RE_BYTE_VALUE, lambda x, **kw: int(x[:-1], **kw)), 754 | (RE_INT_VALUE, int), 755 | (RE_BOOL_VALUE, lambda x: str(x).lower() == "true"), 756 | (RE_FLOAT_VALUE, lambda x: float(x[:-1])), 757 | (RE_DOUBLE_VALUE, lambda x: float(x[:-1])), 758 | (RE_CHAR_VALUE, lambda x: str(x[1:-1])), 759 | ( 760 | RE_STRING_VALUE, 761 | lambda x: str(x[1:-1]).encode().decode("unicode_escape"), 762 | ), # support unicode 763 | (RE_TYPE_VALUE, SVMType), 764 | ] 765 | """Defines custom handlers for actual value defintions 766 | 767 | :meta private: 768 | """ 769 | 770 | 771 | def is_type_descriptor(value: str) -> bool: 772 | """Returns whether the given value is a valid type descriptor. 773 | 774 | :param value: the value to check 775 | :type value: str 776 | :return: True, if the value is a valid type descriptor 777 | :rtype: bool 778 | """ 779 | return RE_TYPE_VALUE.match(value) is not None 780 | -------------------------------------------------------------------------------- /smali/bridge/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from smali.bridge.errors import * 18 | from smali.bridge.frame import Frame 19 | from smali.bridge.lang import * 20 | from smali.bridge import objects, executor 21 | from smali.bridge.vm import * 22 | -------------------------------------------------------------------------------- /smali/bridge/errors.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | __doc__ = """ 17 | Common exceptions of the Smali-Python-Bridge 18 | """ 19 | 20 | __all__ = [ 21 | "NoSuchClassError", 22 | "NoSuchMethodError", 23 | "NoSuchFieldError", 24 | "NoSuchRegisterError", 25 | "NoSuchOpcodeError", 26 | "InvalidOpcodeError", 27 | "ExecutionError", 28 | ] 29 | 30 | 31 | class NoSuchClassError(Exception): 32 | """The class is not defined or wasn't found""" 33 | 34 | 35 | class NoSuchMethodError(Exception): 36 | """The method is not defined""" 37 | 38 | 39 | class NoSuchFieldError(Exception): 40 | """The requested field is not defined""" 41 | 42 | 43 | class NoSuchRegisterError(Exception): 44 | """Unknown register was requested to be read""" 45 | 46 | 47 | class NoSuchOpcodeError(Exception): 48 | """The opcode does not exists or no implementation is present""" 49 | 50 | 51 | class InvalidOpcodeError(Exception): 52 | """The opcode is invalid""" 53 | 54 | 55 | class ExecutionError(Exception): 56 | """Wrapper class for runtime exceptions.""" 57 | 58 | name: str 59 | """The exception class name""" 60 | 61 | def __init__(self, name: str, *args: object) -> None: 62 | super().__init__(*args) 63 | self.name = name 64 | 65 | def __repr__(self) -> str: 66 | return f"<{self.name} at {id(self)}" 67 | 68 | def __str__(self) -> str: 69 | return super().__str__().replace("ExecutionError", self.name) 70 | -------------------------------------------------------------------------------- /smali/bridge/executor.py: -------------------------------------------------------------------------------- 1 | # along with this program. If not, see . 2 | import struct 3 | 4 | from smali import SmaliValue, SVMType 5 | from smali.opcode import * # noqa 6 | from smali.bridge.frame import Frame 7 | from smali.bridge.errors import ExecutionError 8 | from smali.bridge.lang import SmaliObject 9 | from smali.bridge.objects import implementations 10 | 11 | cache = {} 12 | """Sepcial dict tat stores all opcodes with their executors""" 13 | 14 | 15 | class Executor: 16 | opcode: str 17 | """The linked opcode""" 18 | 19 | action = None 20 | """Optional action if this class is not subclassed.""" 21 | 22 | frame: Frame 23 | """The current execution frame""" 24 | 25 | args: tuple 26 | """THe current execution arguments""" 27 | 28 | kwargs: dict 29 | """The current execution""" 30 | 31 | def __init__(self, action, name=None, map_to: list = None) -> None: 32 | self.opcode = name 33 | self.action = action 34 | self.frame = None 35 | if self.action: 36 | self.opcode = str(action.__name__).replace("__", "/").replace("_", "-") 37 | 38 | if not self.opcode: 39 | raise ValueError("Opcode name must be non-null!") 40 | cache[self.opcode] = self 41 | if map_to: 42 | for op_name in map_to: 43 | cache[op_name] = self 44 | 45 | def __call__(self, frame: Frame): 46 | self.frame = frame 47 | self.args = self.args or () 48 | if self.action: 49 | self.action(self, *self.args) 50 | 51 | def __repr__(self) -> str: 52 | return f"<{self.opcode} at {id(self):#x}>" 53 | 54 | def __str__(self) -> str: 55 | return self.__repr__() 56 | 57 | def cast(self, value, classes): 58 | if not isinstance(value, classes): 59 | raise ExecutionError( 60 | "ClassCastError", f"Could not cast '{type(value)}' to {classes}" 61 | ) 62 | 63 | return value 64 | 65 | 66 | def opcode_executor(map_to: list = None): 67 | def wrapper(func): 68 | return Executor(func, map_to=map_to if map_to else []) 69 | 70 | return wrapper 71 | 72 | 73 | def get_executor(name: str) -> Executor: 74 | if name not in cache: 75 | raise KeyError(f"Could not find executor for opcode: {name}") 76 | 77 | return cache[name] 78 | 79 | 80 | @opcode_executor() 81 | def nop(self, *Args): 82 | pass 83 | 84 | 85 | ################################################################################ 86 | # RETURN 87 | ################################################################################ 88 | 89 | 90 | @opcode_executor(map_to=[RETURN_VOID_BARRIER, RETURN_VOID_NO_BARRIER]) 91 | def return_void(self: Executor): 92 | self.frame.return_value = None 93 | self.frame.finished = True 94 | 95 | 96 | @opcode_executor(map_to=[RETURN, RETURN_WIDE]) 97 | def return_object(self: Executor, register: str): 98 | self.frame.return_value = self.frame[register] 99 | self.frame.finished = True 100 | 101 | 102 | ################################################################################ 103 | # GOTO 104 | ################################################################################ 105 | 106 | 107 | @opcode_executor(map_to=[GOTO_16, GOTO_32]) 108 | def goto(self: Executor, label: str): 109 | if label not in self.frame.labels: 110 | raise ExecutionError("NoSuchLabelError", label) 111 | 112 | self.frame.label = label 113 | self.frame.pos = self.frame.labels[label] 114 | 115 | 116 | ################################################################################ 117 | # INVOKE 118 | ################################################################################ 119 | 120 | 121 | @opcode_executor(map_to=[INVOKE_DIRECT, INVOKE_STATIC, INVOKE_VIRTUAL]) 122 | def invoke(self: Executor, inv_type, args, owner, method): 123 | # TODO: add direct calls to int or str objects 124 | if inv_type in ("direct", "virtual", "static"): 125 | vm_class = None 126 | if owner in implementations: 127 | impl = implementations[owner] 128 | 129 | # If class methods should be invoked, just use the 130 | # previously moved class object 131 | vm_class = self.frame[args[0]] 132 | if method not in impl: 133 | raise ExecutionError( 134 | "NoSuchMethodError", f"method '{method}' not defined for {owner}!" 135 | ) 136 | 137 | self.frame.method_return = impl[method](vm_class) 138 | return 139 | 140 | values = [self.frame[register] for register in args] 141 | instance = None 142 | if inv_type != "static": 143 | instance = values[0] 144 | super_cls = instance.smali_class.super_cls 145 | if super_cls == owner: 146 | vm_class = self.frame.vm.get_class(super_cls) 147 | # Remove the first argument 148 | values = values[1:] 149 | 150 | if not vm_class: 151 | vm_class = self.frame.vm.get_class(owner) 152 | 153 | target = vm_class.method(method) 154 | self.frame.method_return = self.frame.vm.call( 155 | target, instance, *values, vm__frame=self.frame 156 | ) 157 | 158 | 159 | @opcode_executor() 160 | def throw(self: Executor, register: str): 161 | self.frame.error = ExecutionError("RuntimeError", self.frame[register]) 162 | 163 | 164 | ################################################################################ 165 | # INT-2-OBJECT, LONG-2-OBJECT 166 | ################################################################################ 167 | 168 | 169 | @opcode_executor() 170 | def int_to_long(self: Executor, dest: str, src: str): 171 | self.frame[dest] = self.frame[src] & 0xFFFFFFFFFFFFFFFF 172 | 173 | 174 | @opcode_executor(map_to=[LONG_TO_INT]) 175 | def int_to_int(self: Executor, dest: str, src: str): 176 | self.frame[dest] = self.frame[src] & 0xFFFFFFFF 177 | 178 | 179 | @opcode_executor(map_to=[INT_TO_SHORT]) 180 | def int_to_char(self: Executor, dest: str, src: str): 181 | self.frame[dest] = self.frame[src] & 0xFFFF 182 | 183 | 184 | @opcode_executor() 185 | def int_to_byte(self: Executor, dest: str, src: str): 186 | (self.frame[dest],) = struct.unpack(">i", self.frame[src]) 187 | 188 | 189 | @opcode_executor(map_to=[INT_TO_DOUBLE]) 190 | def int_to_float(self: Executor, dest: str, src: str): 191 | self.frame[dest] = float(self.frame[src]) 192 | 193 | 194 | ################################################################################ 195 | # GET, PUT 196 | ################################################################################ 197 | 198 | 199 | @opcode_executor( 200 | map_to=[ 201 | SPUT, 202 | SPUT_BOOLEAN, 203 | SPUT_SHORT, 204 | SPUT_CHAR, 205 | SPUT_BYTE, 206 | SPUT_OBJECT_VOLATILE, 207 | SPUT_WIDE, 208 | SPUT_WIDE_VOLATILE, 209 | ] 210 | ) 211 | def sput_object(self: Executor, register: str, dest: str): 212 | # The destination string contains the owner class name, field name and 213 | # field type. 214 | value = self.frame[register] 215 | 216 | owner, name_type = dest.split("->") 217 | name, _ = name_type.split(":") 218 | 219 | cls = self.frame.vm.get_class(owner) 220 | field = cls.field(name) 221 | field.value = value 222 | 223 | 224 | @opcode_executor( 225 | map_to=[ 226 | SGET, 227 | SGET_BOOLEAN, 228 | SGET_BYTE, 229 | SGET_OBJECT_VOLATILE, 230 | SGET_VOLATILE, 231 | SGET_WIDE, 232 | SGET_WIDE_VOLATILE, 233 | SGET_CHAR, 234 | ] 235 | ) 236 | def sget_object(self: Executor, register: str, dest: str): 237 | # The destination string contains the owner class name, field name and 238 | # field type. 239 | owner, name_type = dest.split("->") 240 | name, _ = name_type.split(":") 241 | 242 | cls = self.frame.vm.get_class(owner) 243 | field = cls.field(name) 244 | self.frame[register] = field.value 245 | 246 | 247 | @opcode_executor( 248 | map_to=[ 249 | IGET_BOOLEAN, 250 | IGET_BYTE, 251 | IGET_CHAR, 252 | IGET_SHORT, 253 | IGET_VOLATILE, 254 | IGET_WIDE, 255 | IGET, 256 | IGET_OBJECT_VOLATILE, 257 | ] 258 | ) 259 | def iget_object(self: Executor, dest: str, src: str, info: str): 260 | smali_object = self.cast(self.frame[src], SmaliObject) 261 | _, field = info.split("->") 262 | field_name, _ = field.split(":") 263 | 264 | self.frame[dest] = smali_object[field_name] 265 | 266 | 267 | @opcode_executor( 268 | map_to=[ 269 | IPUT, 270 | IPUT_BOOLEAN, 271 | IPUT_BYTE, 272 | IPUT_CHAR, 273 | IPUT_SHORT, 274 | IPUT_OBJECT_VOLATILE, 275 | IPUT_VOLATILE, 276 | IPUT_WIDE, 277 | ] 278 | ) 279 | def iput_object(self: Executor, src: str, obj: str, info: str): 280 | smali_object = self.cast(self.frame[obj], SmaliObject) 281 | _, field = info.split("->") 282 | field_name, _ = field.split(":") 283 | 284 | smali_object[field_name] = self.frame[src] 285 | 286 | 287 | ################################################################################ 288 | # CONST 289 | ################################################################################ 290 | 291 | 292 | @opcode_executor( 293 | map_to=[ 294 | CONST_STRING, 295 | CONST_STRING_JUMBO, 296 | CONST_16, 297 | CONST_4, 298 | CONST_WIDE, 299 | CONST_WIDE_HIGH16, 300 | CONST_WIDE_32, 301 | CONST_STRING_JUMBO, 302 | ] 303 | ) 304 | def const(self: Executor, register: str, value: str): 305 | self.frame[register] = SmaliValue(value) 306 | 307 | 308 | @opcode_executor(map_to=[CONST_CLASS]) 309 | def const_class(self: Executor, register: str, name: str): 310 | self.frame[register] = self.frame.vm.get_class(name) 311 | 312 | 313 | ################################################################################ 314 | # MOVE 315 | ################################################################################ 316 | 317 | 318 | @opcode_executor(map_to=[MOVE_RESULT_OBJECT]) 319 | def move_result(self: Executor, register: str): 320 | self.frame[register] = self.frame.method_return 321 | 322 | 323 | @opcode_executor() 324 | def move(self: Executor, dest: str, src: str): 325 | self.frame[dest] = self.frame[src] 326 | 327 | 328 | @opcode_executor( 329 | map_to=[ 330 | MOVE_OBJECT, 331 | MOVE_OBJECT_16, 332 | MOVE_OBJECT_FROM16 333 | ] 334 | ) 335 | def move_object(self: Executor, dest: str, src: str): 336 | self.frame[dest] = self.frame[src] 337 | 338 | 339 | @opcode_executor() 340 | def move_exception(self: Executor, dest: str): 341 | self.frame[dest] = self.frame.error 342 | 343 | 344 | ################################################################################ 345 | # NEW-INSTANCE 346 | ################################################################################ 347 | 348 | 349 | @opcode_executor() 350 | def new_instance(self: Executor, register: str, descriptor: str): 351 | # Filter out primitive values 352 | if descriptor in "ISBJ": 353 | self.frame[register] = 0 354 | elif descriptor in "FD": 355 | self.frame[register] = 0.0 356 | elif descriptor in ("Ljava/lang/String;", "C", "Ljava/lang/Character;"): 357 | self.frame[register] = "" 358 | elif descriptor in ( 359 | "Ljava/lang/Integer;", 360 | "Ljava/lang/Byte;", 361 | "Ljava/lang/Long", 362 | "Ljava/lang/Short;", 363 | ): 364 | self.frame[register] = 0 365 | elif descriptor in ("Ljava/lang/Boolean", "Z"): 366 | self.frame[register] = False 367 | elif descriptor in ("Ljava/util/ArrayList;", "Ljava/util/LinkedList;"): 368 | self.frame[register] = [] 369 | else: 370 | smali_class = self.frame.vm.get_class(descriptor) 371 | instance = SmaliObject(smali_class) 372 | instance.init() 373 | 374 | self.frame[register] = instance 375 | 376 | 377 | @opcode_executor() 378 | def new_array(self: Executor, dest: str, count_register: str, descriptor: str): 379 | cls_type = SVMType(descriptor) 380 | values = [None] * self.frame[count_register] 381 | if cls_type.svm_type == SVMType.TYPES.PRIMITIVE: 382 | if cls_type.simple_name in "BSIJ": # number 383 | values = [0] * self.frame[count_register] 384 | 385 | elif cls_type.simple_name in "FD": # floating point 386 | values = [0.0] * self.frame[count_register] 387 | 388 | self.frame[dest] = values 389 | 390 | 391 | @opcode_executor(map_to=[INSTANCE_OF]) 392 | def check_cast(self: Executor, dest: str, descriptor: str): 393 | src_class = self.frame[dest] 394 | if not isinstance(src_class, SmaliObject): 395 | return 396 | 397 | src_class = src_class.smali_class 398 | dest_class = self.frame.vm.get_class(descriptor) 399 | 400 | if not src_class.is_assignable(dest_class): 401 | raise ExecutionError( 402 | "ClassCastError", f"Could not cast {dest_class} to {src_class}" 403 | ) 404 | 405 | 406 | ################################################################################ 407 | # SWITCH 408 | ################################################################################ 409 | 410 | 411 | @opcode_executor() 412 | def packed_switch(self: Executor, register: str, data: str): 413 | switch_value, cases = self.frame.switch_data[data] 414 | value = self.frame[register] 415 | 416 | # We must convert the switch_value into a SmaliValue object 417 | # as it is of type string and we need an integer 418 | idx = value - SmaliValue(switch_value) 419 | 420 | if idx < 0 or idx >= len(cases): 421 | # Default branch does nothing 422 | return 423 | 424 | # Call GOTO-instruction directly 425 | goto.action(self, cases[idx]) 426 | 427 | 428 | @opcode_executor() 429 | def sparse_switch(self: Executor, register: str, label_name: str): 430 | branches = self.frame.switch_data[label_name] 431 | value = self.frame[register] 432 | 433 | # The iteration is needed as the branch values are strings 434 | for key, label in branches.items(): 435 | if SmaliValue(key) == value: 436 | goto.action(self, label) 437 | break 438 | 439 | 440 | ################################################################################ 441 | # IF 442 | ################################################################################ 443 | 444 | 445 | @opcode_executor() 446 | def if_le(self: Executor, left: str, right: str, label: str): 447 | if self.frame[left] <= self.frame[right]: 448 | goto.action(self, label) 449 | 450 | 451 | @opcode_executor() 452 | def if_ge(self: Executor, left: str, right: str, label: str): 453 | if self.frame[left] >= self.frame[right]: 454 | goto.action(self, label) 455 | 456 | 457 | @opcode_executor() 458 | def if_gez(self: Executor, left: str, label: str): 459 | if self.frame[left] >= 0: 460 | goto.action(self, label) 461 | 462 | 463 | @opcode_executor() 464 | def if_ltz(self: Executor, left: str, label: str): 465 | if self.frame[left] < 0: 466 | goto.action(self, label) 467 | 468 | 469 | @opcode_executor() 470 | def if_gt(self: Executor, left: str, right: str, label: str): 471 | if self.frame[left] > self.frame[right]: 472 | goto.action(self, label) 473 | 474 | 475 | @opcode_executor() 476 | def if_lt(self: Executor, left: str, right: str, label: str): 477 | if self.frame[left] < self.frame[right]: 478 | goto.action(self, label) 479 | 480 | 481 | @opcode_executor() 482 | def if_gtz(self: Executor, left: str, label: str): 483 | if self.frame[left] > 0: 484 | goto.action(self, label) 485 | 486 | 487 | @opcode_executor() 488 | def if_ne(self: Executor, left: str, right: str, label: str): 489 | if self.frame[left] != self.frame[right]: 490 | goto.action(self, label) 491 | 492 | 493 | @opcode_executor() 494 | def if_lez(self: Executor, left: str, label: str): 495 | if self.frame[left] <= 0: 496 | goto.action(self, label) 497 | 498 | 499 | @opcode_executor() 500 | def if_nez(self: Executor, left: str, label: str): 501 | if self.frame[left] != 0: 502 | goto.action(self, label) 503 | 504 | 505 | @opcode_executor() 506 | def if_eqz(self: Executor, left: str, label: str): 507 | if self.frame[left] == 0: 508 | goto.action(self, label) 509 | 510 | 511 | ################################################################################ 512 | # ARRAY 513 | ################################################################################ 514 | 515 | 516 | @opcode_executor() 517 | def array_length(self: Executor, dest: str, array: str): 518 | self.frame[dest] = len(self.frame[array]) 519 | 520 | 521 | @opcode_executor() 522 | def fill_array_data(self: Executor, dest: str, label: str): 523 | self.frame[dest] = self.frame.array_data[label] 524 | 525 | 526 | @opcode_executor( 527 | map_to=[AGET_BOOLEAN, AGET_BYTE, AGET_CHAR, AGET_OBJECT, AGET_SHORT, AGET_WIDE] 528 | ) 529 | def aget(self: Executor, dest: str, array: str, index: str): 530 | idx_value = self.frame[index] 531 | array_data = self.frame[array] 532 | if idx_value < 0 or idx_value >= len(array): 533 | raise ExecutionError( 534 | "IndexOutOfBoundsError", 535 | f"Index {idx_value} is out of bounds for length {len(array_data)}", 536 | ) 537 | 538 | self.frame[dest] = array_data[idx_value] 539 | 540 | 541 | @opcode_executor( 542 | map_to=[APUT_BOOLEAN, APUT_BYTE, APUT_CHAR, APUT_OBJECT, APUT_SHORT, APUT_WIDE] 543 | ) 544 | def aput(self: Executor, src: str, array: str, index: str): 545 | idx_value = self.frame[index] 546 | array_data = self.frame[array] 547 | if idx_value < 0 or idx_value > len(array): 548 | raise ExecutionError( 549 | "IndexOutOfBoundsError", 550 | f"Index {idx_value} is out of bounds for length {len(array_data)}", 551 | ) 552 | 553 | if len(array) == idx_value: 554 | array_data.append(self.frame[src]) 555 | else: 556 | array_data[idx_value] = self.frame[src] 557 | 558 | 559 | ################################################################################ 560 | # Int operations (2addr) 561 | ################################################################################ 562 | 563 | 564 | @opcode_executor(map_to=[NEG_DOUBLE, NEG_FLOAT, NEG_LONG]) 565 | def neg_int(self: Executor, dest: str, src: str): 566 | self.frame[dest] = -self.frame[src] 567 | 568 | 569 | @opcode_executor(map_to=[NOT_LONG]) 570 | def not_int(self: Executor, dest: str, src: str): 571 | self.frame[dest] = ~self.frame[src] 572 | 573 | 574 | @opcode_executor(map_to=[OR_LONG_2ADDR]) 575 | def or_int__2addr(self: Executor, dest: str, src: str): 576 | self.frame[dest] |= self.frame[src] 577 | 578 | 579 | @opcode_executor(map_to=[AND_LONG_2ADDR]) 580 | def and_int__2addr(self: Executor, dest: str, src: str): 581 | self.frame[dest] &= self.frame[src] 582 | 583 | 584 | @opcode_executor(map_to=[SUB_DOUBLE_2ADDR, SUB_FLOAT_2ADDR, SUB_LONG_2ADDR]) 585 | def sub_int__2addr(self: Executor, dest: str, src: str): 586 | self.frame[dest] -= self.frame[src] 587 | 588 | 589 | @opcode_executor(map_to=[XOR_LONG_2ADDR]) 590 | def xor_int__2addr(self: Executor, dest: str, src: str): 591 | self.frame[dest] ^= self.frame[src] 592 | 593 | 594 | @opcode_executor(map_to=[ADD_DOUBLE_2ADDR, ADD_FLOAT_2ADDR, ADD_LONG_2ADDR]) 595 | def add_int__2addr(self: Executor, dest: str, src: str): 596 | self.frame[dest] += self.frame[src] 597 | 598 | 599 | @opcode_executor(map_to=[DIV_LONG_2ADDR]) 600 | def div_int__2addr(self: Executor, dest: str, src: str): 601 | self.frame[dest] //= self.frame[src] 602 | 603 | 604 | @opcode_executor(map_to=[DIV_DOUBLE_2ADDR]) 605 | def div_float__2addr(self: Executor, dest: str, src: str): 606 | self.frame[dest] /= self.frame[src] 607 | 608 | 609 | @opcode_executor(map_to=[SHL_LONG_2ADDR]) 610 | def shl_int__2addr(self: Executor, dest: str, src: str): 611 | self.frame[dest] <<= self.frame[src] 612 | 613 | 614 | @opcode_executor(map_to=[SHR_LONG_2ADDR, USHR_INT_2ADDR, USHR_LONG_2ADDR]) 615 | def shr_int__2addr(self: Executor, dest: str, src: str): 616 | self.frame[dest] >>= self.frame[src] 617 | 618 | 619 | @opcode_executor(map_to=[REM_DOUBLE_2ADDR, REM_FLOAT_2ADDR, REM_LONG_2ADDR]) 620 | def rem_int__2addr(self: Executor, dest: str, src: str): 621 | self.frame[dest] %= self.frame[src] 622 | 623 | 624 | @opcode_executor(map_to=[MUL_DOUBLE_2ADDR, MUL_FLOAT_2ADDR, MUL_LONG_2ADDR]) 625 | def mul_int__2addr(self: Executor, dest: str, src: str): 626 | self.frame[dest] *= self.frame[src] 627 | 628 | 629 | ################################################################################ 630 | # Int operations (lit8 and lit16) 631 | # div, add, sub, rem, and, or, xor, shl, shr, rsub(lit8) 632 | ################################################################################ 633 | 634 | 635 | @opcode_executor() 636 | def div_int__lit8(self: Executor, dest: str, left: str, right: str): 637 | self.frame[dest] = self.frame[left] // (SmaliValue(right) & 0xFF) 638 | 639 | 640 | @opcode_executor() 641 | def div_int__lit16(self: Executor, dest: str, left: str, right: str): 642 | self.frame[dest] = self.frame[left] // (SmaliValue(right) & 0xFFFF) 643 | 644 | 645 | @opcode_executor() 646 | def add_int__lit8(self: Executor, dest: str, left: str, right: str): 647 | self.frame[dest] = self.frame[left] + (SmaliValue(right) & 0xFF) 648 | 649 | 650 | @opcode_executor() 651 | def add_int__lit16(self: Executor, dest: str, left: str, right: str): 652 | self.frame[dest] = self.frame[left] + (SmaliValue(right) & 0xFFFF) 653 | 654 | 655 | @opcode_executor() 656 | def sub_int__lit8(self: Executor, dest: str, left: str, right: str): 657 | self.frame[dest] = self.frame[left] - (SmaliValue(right) & 0xFF) 658 | 659 | 660 | @opcode_executor() 661 | def sub_int__lit16(self: Executor, dest: str, left: str, right: str): 662 | self.frame[dest] = self.frame[left] - (SmaliValue(right) & 0xFFFF) 663 | 664 | 665 | @opcode_executor() 666 | def mul_int__lit8(self: Executor, dest: str, left: str, right: str): 667 | self.frame[dest] = self.frame[left] * (SmaliValue(right) & 0xFF) 668 | 669 | 670 | @opcode_executor() 671 | def mul_int__lit16(self: Executor, dest: str, left: str, right: str): 672 | self.frame[dest] = self.frame[left] * (SmaliValue(right) & 0xFFFF) 673 | 674 | 675 | @opcode_executor() 676 | def rem_int__lit8(self: Executor, dest: str, left: str, right: str): 677 | self.frame[dest] = self.frame[left] % (SmaliValue(right) & 0xFF) 678 | 679 | 680 | @opcode_executor() 681 | def rem_int__lit16(self: Executor, dest: str, left: str, right: str): 682 | self.frame[dest] = self.frame[left] % (SmaliValue(right) & 0xFFFF) 683 | 684 | 685 | @opcode_executor() 686 | def and_int__lit8(self: Executor, dest: str, left: str, right: str): 687 | self.frame[dest] = self.frame[left] & (SmaliValue(right) & 0xFF) 688 | 689 | 690 | @opcode_executor() 691 | def and_int__lit16(self: Executor, dest: str, left: str, right: str): 692 | self.frame[dest] = self.frame[left] & (SmaliValue(right) & 0xFFFF) 693 | 694 | 695 | @opcode_executor() 696 | def or_int__lit8(self: Executor, dest: str, left: str, right: str): 697 | self.frame[dest] = self.frame[left] | (SmaliValue(right) & 0xFF) 698 | 699 | 700 | @opcode_executor() 701 | def or_int__lit16(self: Executor, dest: str, left: str, right: str): 702 | self.frame[dest] = self.frame[left] | (SmaliValue(right) & 0xFFFF) 703 | 704 | 705 | @opcode_executor() 706 | def xor_int__lit8(self: Executor, dest: str, left: str, right: str): 707 | self.frame[dest] = self.frame[left] ^ (SmaliValue(right) & 0xFF) 708 | 709 | 710 | @opcode_executor() 711 | def xor_int__lit16(self: Executor, dest: str, left: str, right: str): 712 | self.frame[dest] = self.frame[left] ^ (SmaliValue(right) & 0xFFFF) 713 | 714 | 715 | @opcode_executor() 716 | def shl_int__lit8(self: Executor, dest: str, left: str, right: str): 717 | self.frame[dest] = self.frame[left] << (SmaliValue(right) & 0xFF) 718 | 719 | 720 | @opcode_executor() 721 | def shl_int__lit16(self: Executor, dest: str, left: str, right: str): 722 | self.frame[dest] = self.frame[left] << (SmaliValue(right) & 0xFFFF) 723 | 724 | 725 | @opcode_executor() 726 | def shr_int__lit8(self: Executor, dest: str, left: str, right: str): 727 | self.frame[dest] = self.frame[left] >> (SmaliValue(right) & 0xFF) 728 | 729 | 730 | @opcode_executor() 731 | def shr_int__lit16(self: Executor, dest: str, left: str, right: str): 732 | self.frame[dest] = self.frame[left] >> (SmaliValue(right) & 0xFFFF) 733 | 734 | 735 | @opcode_executor() 736 | def rsub_int__lit8(self: Executor, dest: str, left: str, right: str): 737 | self.frame[dest] = (SmaliValue(right) & 0xFF) - self.frame[left] 738 | 739 | 740 | ################################################################################ 741 | # Int operations 742 | ################################################################################ 743 | 744 | 745 | @opcode_executor() 746 | def div_int(self: Executor, dest: str, left: str, right: str): 747 | self.frame[dest] = self.frame[left] // self.frame[right] 748 | 749 | 750 | @opcode_executor(map_to=[DIV_DOUBLE]) 751 | def div_float(self: Executor, dest: str, left: str, right: str): 752 | self.frame[dest] = self.frame[left] / self.frame[right] 753 | 754 | 755 | @opcode_executor(map_to=[ADD_DOUBLE, ADD_FLOAT, ADD_LONG]) 756 | def add_int(self: Executor, dest: str, left: str, right: str): 757 | self.frame[dest] = self.frame[left] + self.frame[right] 758 | 759 | 760 | @opcode_executor(map_to=[SUB_DOUBLE, SUB_FLOAT, SUB_LONG]) 761 | def sub_int(self: Executor, dest: str, left: str, right: str): 762 | self.frame[dest] = self.frame[left] - self.frame[right] 763 | 764 | 765 | @opcode_executor(map_to=[REM_DOUBLE, REM_FLOAT, REM_LONG]) 766 | def rem_int(self: Executor, dest: str, left: str, right: str): 767 | self.frame[dest] = self.frame[left] % self.frame[right] 768 | 769 | 770 | @opcode_executor(map_to=[MUL_DOUBLE, MUL_FLOAT, MUL_LONG]) 771 | def mul_int(self: Executor, dest: str, left: str, right: str): 772 | self.frame[dest] = self.frame[left] * self.frame[right] 773 | 774 | 775 | @opcode_executor(map_to=[OR_LONG]) 776 | def or_int(self: Executor, dest: str, left: str, right: str): 777 | self.frame[dest] = self.frame[left] | self.frame[right] 778 | 779 | 780 | @opcode_executor(map_to=[AND_LONG]) 781 | def and_int(self: Executor, dest: str, left: str, right: str): 782 | self.frame[dest] = self.frame[left] & self.frame[right] 783 | 784 | 785 | @opcode_executor(map_to=[XOR_LONG]) 786 | def xor_int(self: Executor, dest: str, left: str, right: str): 787 | self.frame[dest] = self.frame[left] ^ self.frame[right] 788 | 789 | 790 | @opcode_executor(map_to=[SHL_LONG]) 791 | def shl_int(self: Executor, dest: str, left: str, right: str): 792 | self.frame[dest] = self.frame[left] << self.frame[right] 793 | 794 | 795 | @opcode_executor(map_to=[SHR_LONG, USHR_LONG, USHR_INT]) 796 | def shr_int(self: Executor, dest: str, left: str, right: str): 797 | self.frame[dest] = self.frame[left] >> self.frame[right] 798 | -------------------------------------------------------------------------------- /smali/bridge/frame.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | __doc__ = """ 17 | Execution frame objects are used within method calls the Smali 18 | emulator. They store the current opcode position, label and values 19 | of all registers in the current context. 20 | 21 | On top of that, ``Frame`` objects store method return values (sub- 22 | calls) as well as the final return value. If any errors occur, they 23 | will be stored in a separate variable. 24 | """ 25 | 26 | from smali.bridge.errors import ExecutionError, NoSuchRegisterError 27 | 28 | 29 | class Frame: 30 | """Class to represent execution frames. 31 | 32 | There are different special functions implemented to simplify 33 | the process of getting and setting register values: 34 | 35 | >>> value = frame["v0"] 36 | 0 37 | >>> "v0" in frame 38 | True 39 | >>> frame["v0"] = 1 40 | >>> for register_name in frame: 41 | ... value = frame[register_name] 42 | """ 43 | 44 | labels: dict = {} 45 | """A mapping with labels to their execution position.""" 46 | 47 | return_value: object = None 48 | """Stores a return value of the current method.""" 49 | 50 | method_return: object = None 51 | """Stores the latest method return value""" 52 | 53 | opcodes: list = [] 54 | """Stores all parsed opcodes with their arguments""" 55 | 56 | finished: bool = False 57 | """Stores whether te current frame has been executed.""" 58 | 59 | pos: int = 0 60 | """The current position in opcode list""" 61 | 62 | label: str 63 | """The current label context""" 64 | 65 | error: ExecutionError 66 | """Defines the current execution error""" 67 | 68 | registers: dict 69 | """Stores values for the current registers.""" 70 | 71 | catch: dict 72 | """Stores all try-catch handlers. 73 | 74 | Note that teh stored handlers will be tuples with 75 | the exception type and the handler block""" 76 | 77 | array_data: dict 78 | """Stores all array-data objecs mapped to their label.""" 79 | 80 | vm = None 81 | """VM reference""" 82 | 83 | switch_data: dict 84 | """Stores packed and sparse-switch data""" 85 | 86 | parent: "Frame" = None 87 | """Parent execution context (mainly used for backtracking)""" 88 | 89 | def __init__(self): 90 | self.registers = {} 91 | self.opcodes = [] 92 | self.labels = {} 93 | self.catch = {} 94 | self.array_data = {} 95 | self.switch_data = {} 96 | self.reset() 97 | 98 | def reset(self) -> None: 99 | """Resets this frame and removes execution information.""" 100 | self.label = None 101 | self.error = None 102 | self.finished = False 103 | self.pos = 0 104 | self.return_value = None 105 | self.parent = None 106 | self.registers.clear() 107 | 108 | def __getitem__(self, key: str): 109 | if key not in self.registers: 110 | raise NoSuchRegisterError(f"Register with name {key} not found!") 111 | 112 | return self.registers[key] 113 | 114 | def __setitem__(self, key: str, value): 115 | self.registers[key] = value 116 | 117 | def __contains__(self, key: str) -> bool: 118 | return key in self.registers 119 | 120 | def __iter__(self): 121 | return iter(self.registers) 122 | 123 | def __len__(self) -> int: 124 | return len(self.opcodes) 125 | -------------------------------------------------------------------------------- /smali/bridge/objects.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | """ 17 | Standard Java wrapper classes that will be registered to a VM 18 | once it was created. 19 | """ 20 | 21 | Object = { 22 | "toString()Ljava/lang/String;": str, 23 | "()V": lambda x: x, 24 | "hashCode()I": id, 25 | "getClass()Ljava/lang/Class;": lambda x: x.smali_class 26 | } 27 | 28 | Class = { 29 | "getSimpleName()Ljava/lang/String;": lambda x: x.name, 30 | "getName()Ljava/lang/String;": lambda x: x.simple_name 31 | } 32 | 33 | def java_lang_String_hashCode(text): 34 | sign = 1 << 31 35 | hashCode = sum(ord(t)*31**i for i, t in enumerate(reversed(text))) 36 | return (hashCode & sign-1) - (hashCode & sign) 37 | 38 | String = { 39 | "hashCode()I": java_lang_String_hashCode, 40 | } 41 | 42 | implementations = { 43 | "Ljava/lang/Class;": Class, 44 | "Ljava/lang/Object;": Object, 45 | "Ljava/lang/String;": String, 46 | } -------------------------------------------------------------------------------- /smali/bridge/vm.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from __future__ import annotations 17 | from typing import Optional 18 | 19 | __doc__ = """ 20 | Implementation of a simple Smali emulator named *SmaliVM*. It supports 21 | execution of small code snippets as well as the execution of whole 22 | class files. 23 | 24 | Debugging can be done by providing a :class:`DebugHandler` and enabling the 25 | debug-option wihtin the VM object. It is also possible to use a custom 26 | ``ClassLoader`` to load or define new classes. 27 | """ 28 | 29 | from io import IOBase 30 | from abc import ABCMeta, abstractmethod 31 | 32 | from smali import SmaliValue, opcode 33 | from smali.base import AccessType, SVMType 34 | from smali.reader import SmaliReader 35 | from smali.visitor import ClassVisitor, MethodVisitor, FieldVisitor, AnnotationVisitor 36 | from smali.bridge.lang import ( 37 | SmaliClass, 38 | SmaliMethod, 39 | SmaliField, 40 | SmaliAnnotation, 41 | RE_REGISTER, 42 | ) 43 | from smali.bridge.frame import Frame 44 | from smali.bridge.errors import ( 45 | ExecutionError, 46 | NoSuchMethodError, 47 | InvalidOpcodeError, 48 | NoSuchClassError, 49 | ) 50 | from smali.bridge import executor 51 | 52 | __all__ = [ 53 | "ClassLoader", 54 | "DebugHandler", 55 | "SmaliVM", 56 | "SmaliVMAnnotationReader", 57 | "SmaliVMFieldReader", 58 | "SmaliVMMethodReader", 59 | "SmaliVMClassReader", 60 | "SmaliClassLoader", 61 | ] 62 | 63 | 64 | class ClassLoader(metaclass=ABCMeta): 65 | """Abstract base class for SmaliClassLoader""" 66 | 67 | @abstractmethod 68 | def define_class(self, source: bytes | str | IOBase) -> SmaliClass: 69 | """Defines a new SmaliClass by parsing the given source file. 70 | 71 | :param source: the source code 72 | :type source: bytes | str 73 | :return: the parsed class definition 74 | :rtype: SmaliClass 75 | """ 76 | 77 | @abstractmethod 78 | def load_class(self, source: str | bytes | IOBase, init=True, lookup_missing=False) -> SmaliClass: 79 | """Parses the given source code and initializes the given class if enabled. 80 | 81 | :param source: the source code 82 | :type source: str | bytes | IOBase 83 | :param init: whether ```` should be executed, defaults to True 84 | :type init: bool, optional 85 | :param lookup_missing: whether missing classes should be searched before parsing 86 | can continue, defaults to False 87 | :type lookup_missing: bool, optional 88 | :raises NoSuchClassError: if the given class is not defined and ``lookup_missing`` is true 89 | :raises InvalidOpcodeError: if the parsed opcode is invalid 90 | :return: the parsed class definition 91 | :rtype: SmaliClass 92 | """ 93 | 94 | 95 | class DebugHandler: 96 | """Basic adapter class used for debugging purposes""" 97 | 98 | def precall( 99 | self, vm: "SmaliVM", method: SmaliMethod, opc: executor.Executor 100 | ) -> None: 101 | """Called before an opcode executor is processed""" 102 | 103 | def postcall( 104 | self, vm: "SmaliVM", method: SmaliMethod, opc: executor.Executor 105 | ) -> None: 106 | """Called after the opcode has been executed""" 107 | 108 | 109 | class SmaliVM: 110 | """Basic implementation of a Smali emulator in Python.""" 111 | 112 | classloader: ClassLoader 113 | """The class loader used to define classes.""" 114 | 115 | debug_handler: Optional[DebugHandler] 116 | """The debug handler to use.""" 117 | 118 | executors: dict[str, executor.Executor] 119 | """External executors used to operate on a single opcode.""" 120 | 121 | use_strict: bool = False 122 | """Tells the VM to throw exceptions on unkown opcodes.""" 123 | 124 | __classes: dict[str, SmaliClass] = {} 125 | """All classes are stored in a dict 126 | 127 | :meta public: 128 | """ 129 | 130 | __frames: dict[int, Frame] = {} 131 | """Stores all execution frames mapped to their method object 132 | 133 | :meta public: 134 | """ 135 | 136 | # This map is used to determine which parameters are applied to the right 137 | # values. 138 | __type_map: dict = { 139 | int: ( 140 | "B", 141 | "S", 142 | "I", 143 | "J", 144 | "Ljava/lang/Byte;", 145 | "Ljava/lang/Short;", 146 | "Ljava/lang/Integer;", 147 | "Ljava/lang/Long;", 148 | ), 149 | float: ("F", "D", "Ljava/lang/Double;", "Ljava/lang/Float;"), 150 | str: ("Ljava/lang/String;", "C", "Ljava/lang/Character;"), 151 | bool: ("Z", "Ljava/lang/Boolean;"), 152 | } 153 | 154 | def __init__( 155 | self, 156 | class_loader: Optional[ClassLoader] = None, 157 | executors: Optional[dict[str, executor.Executor]] = None, 158 | use_strict: bool = False, 159 | ) -> None: 160 | self.classloader = class_loader or _SmaliClassLoader(self) 161 | self.executors = executors or executor.cache 162 | self.use_strict = use_strict 163 | self.debug_handler = None 164 | 165 | def new_class(self, __class: SmaliClass): 166 | """Defines a new class that can be accessed globally. 167 | 168 | :param cls: the class to be defined 169 | :type cls: SmaliClass 170 | """ 171 | if not __class: 172 | raise ValueError("SmaliClass object is null!") 173 | 174 | self.__classes[__class.signature] = __class 175 | 176 | def new_frame(self, method: SmaliMethod, frame: Frame): 177 | """Creates a new method frame that will be mapped to the method's signature- 178 | 179 | :param method: the target method 180 | :type method: SmaliMethod 181 | :param frame: the execution frame 182 | :type frame: Frame 183 | """ 184 | mhash = hash(method) 185 | if mhash not in self.__frames: 186 | self.__frames[mhash] = frame 187 | frame.vm = self 188 | 189 | 190 | def get_class(self, name: str) -> SmaliClass: 191 | """Searches for a class with the given name. 192 | 193 | :param name: the class name 194 | :type name: str 195 | :raises NoSuchClassError: if no class with the given name is defined 196 | :return: the defined Smali class 197 | :rtype: SmaliClass 198 | """ 199 | if name not in self.__classes: 200 | raise NoSuchClassError(f'Class "{name}" not defined!') 201 | 202 | return self.__classes[name] 203 | 204 | def call(self, method: SmaliMethod, instance, *args, **kwargs) -> object: 205 | """Executes the given method in the given object instance. 206 | 207 | Before the method will be executed, there is an input parameter 208 | check to validate all passed arguments. The required registers 209 | will be filled automatically. 210 | 211 | Debugging is done via the :class:`DebugHandler` that must be set globally 212 | in this object. 213 | 214 | :param method: the method to execute 215 | :type method: :class:`SmaliMethod` 216 | :param instance: the smali object 217 | :type instance: :class:`SmaliObject` 218 | :raises NoSuchMethodError: if no frame is registered to the given method 219 | :raises ExecutionError: if an error occurs while execution 220 | :return: the return value of the executed method 221 | :rtype: object 222 | """ 223 | mhash = hash(method) 224 | if mhash not in self.__frames: 225 | raise NoSuchMethodError(f"Method not registered! {method}") 226 | 227 | frame: Frame = self.__frames[mhash] 228 | frame.reset() 229 | for i in range(method.locals): 230 | frame.registers[f"v{i}"] = None 231 | 232 | if method.modifiers not in AccessType.STATIC: 233 | if not instance: 234 | raise ExecutionError( 235 | "NullPtrError", 236 | f"Expected instance of '{instance.smali_class.name}'", 237 | ) 238 | frame.registers["p0"] = instance 239 | 240 | prev_frame = kwargs.pop("vm__frame", None) 241 | if prev_frame: 242 | frame.parent = prev_frame 243 | 244 | # validate method and set parameter values 245 | self._validate_call(method, frame, args, kwargs) 246 | while not frame.finished: 247 | opcode_exec, args = frame.opcodes[frame.pos] 248 | opcode_exec.args = args 249 | if self.debug_handler: 250 | self.debug_handler.precall(self, method, opcode_exec) 251 | 252 | opcode_exec(frame) 253 | opcode_exec.args = None 254 | frame.pos += 1 255 | if self.debug_handler: 256 | self.debug_handler.postcall(self, method, opcode_exec) 257 | 258 | if frame.error: 259 | if isinstance(frame.error, ExecutionError): 260 | raise frame.error 261 | 262 | raise ExecutionError(frame.error) 263 | 264 | value = frame.return_value 265 | frame.reset() 266 | return value 267 | 268 | def _validate_call(self, method: SmaliMethod, frame: Frame, args: tuple, kwargs: dict): 269 | parameters = method.parameters 270 | 271 | registers = {} 272 | for key, value in kwargs.items(): 273 | if RE_REGISTER.match(key): 274 | registers[key] = value 275 | 276 | start = 1 if method.modifiers not in AccessType.STATIC else 0 277 | for i, value in enumerate(args, start=start): 278 | registers[f"p{i}"] = value 279 | 280 | if len(parameters) != len(registers): 281 | raise ValueError( 282 | "Invalid argument count! - expected %s, got %d" 283 | % (len(parameters), len(registers)) 284 | ) 285 | 286 | for param, register in zip(parameters, registers): 287 | param_type: SVMType = param 288 | # Lookup primitive types 289 | for primitive, ptypes in self.__type_map.items(): 290 | if param_type.full_name in ptypes: 291 | if not isinstance(registers[register], primitive): 292 | raise TypeError( 293 | "Invalid type for parameter, expected %s - got %s" 294 | % (param, type(registers[register])) 295 | ) 296 | 297 | if param_type.full_name not in self.__classes: 298 | raise NoSuchClassError(f'Class "{param_type}" not defined!') 299 | 300 | frame.registers.update(registers) 301 | 302 | 303 | #################################################################################################### 304 | # INTERNAL 305 | #################################################################################################### 306 | 307 | 308 | class _SourceAnnotationVisitor(AnnotationVisitor): 309 | annotation: SmaliAnnotation 310 | """The final annotation""" 311 | 312 | def __init__(self, annotation: SmaliAnnotation) -> None: 313 | super().__init__(None) 314 | self.annotation = annotation 315 | 316 | def visit_value(self, name: str, value) -> None: 317 | self.annotation[name] = SmaliValue(value) 318 | 319 | def visit_array(self, name: str, values: list) -> None: 320 | self.annotation[name] = [SmaliValue(x) for x in values] 321 | 322 | def visit_enum(self, name: str, owner: str, const: str, value_type: str) -> None: 323 | # TODO: handle enum values 324 | return super().visit_enum(name, owner, const, value_type) 325 | 326 | def visit_subannotation( 327 | self, name: str, access_flags: int, signature: str 328 | ) -> "AnnotationVisitor": 329 | sub = SmaliAnnotation(self.annotation, signature, access_flags) 330 | self.annotation[name] = sub 331 | return SmaliVMAnnotationReader(sub) 332 | 333 | 334 | class _SourceFieldVisitor(FieldVisitor): 335 | field: SmaliField 336 | 337 | def __init__(self, field: SmaliField) -> None: 338 | super().__init__() 339 | self.field = field 340 | 341 | def visit_annotation(self, access_flags: int, signature: str) -> AnnotationVisitor: 342 | annotation = SmaliAnnotation(self.field, signature, access_flags) 343 | self.field.annotations.append(annotation) 344 | return SmaliVMAnnotationReader(annotation) 345 | 346 | 347 | class _SourceMethodVisitor(MethodVisitor): 348 | method: SmaliMethod 349 | frame: Frame 350 | 351 | def __init__(self, method: SmaliMethod) -> None: 352 | super().__init__() 353 | self.method = method 354 | self.frame = Frame() 355 | self.pos = 0 356 | self._last_label = None 357 | 358 | def visit_annotation(self, access_flags: int, signature: str) -> AnnotationVisitor: 359 | annotation = SmaliAnnotation(self.method, signature, access_flags) 360 | self.method.annotations.append(annotation) 361 | return SmaliVMAnnotationReader(annotation) 362 | 363 | def visit_block(self, name: str) -> None: 364 | self.frame.labels[name] = self.pos 365 | self.pos += 1 366 | self._last_label = name 367 | 368 | def visit_locals(self, local_count: int) -> None: 369 | self.method.locals = local_count 370 | 371 | def visit_registers(self, registers: int) -> None: 372 | self.method.locals = registers - len(self.method.parameters) 373 | 374 | def visit_catch(self, exc_name: str, blocks: tuple) -> None: 375 | start, _, handler = blocks 376 | self.frame.catch[start] = (exc_name, handler) 377 | 378 | def visit_catchall(self, exc_name: str, blocks: tuple) -> None: 379 | self.visit_catch(exc_name, blocks) 380 | 381 | def visit_goto(self, block_name: str) -> None: 382 | self.frame.opcodes.append((executor.goto, block_name)) 383 | self.pos += 1 384 | 385 | def visit_array_data(self, length: str, value_list: list) -> None: 386 | self.frame.array_data[self._last_label] = value_list 387 | 388 | def visit_invoke(self, inv_type: str, args: list, owner: str, method: str) -> None: 389 | self.frame.opcodes.append((executor.invoke, (inv_type, args, owner, method))) 390 | self.pos += 1 391 | 392 | def visit_return(self, ret_type: str, args: list) -> None: 393 | if ret_type: 394 | self.visit_instruction(f"return-{ret_type}", args) 395 | else: 396 | self.visit_instruction("return", args) 397 | 398 | def visit_instruction(self, ins_name: str, args: list) -> None: 399 | cache: dict = self.frame.vm.executors 400 | for _, value in opcode.__dict__.items(): 401 | # If the value of an attribute is equal to the given instruction 402 | # name, 403 | if value == ins_name: 404 | # Check if the instruction name is not in the list of executors 405 | # in the current frame's virtual machine 406 | if ins_name not in cache and not self.frame.vm.use_strict: 407 | if "*" in cache: 408 | func = cache["*"] 409 | else: 410 | # If not, add a tuple of the "nop" opcode function and the 411 | # instruction arguments to the frame's opcodes list. 412 | func = cache["nop"] if "nop" in cache else executor.nop 413 | self.frame.opcodes.append((func, args)) 414 | else: 415 | # If yes, add a tuple of the executor function for the instruction 416 | # name and the instruction arguments to the frame's opcodes list. 417 | self.frame.opcodes.append((self.frame.vm.executors[ins_name], args)) 418 | self.pos += 1 419 | return 420 | 421 | raise InvalidOpcodeError(f"Invalid OpCode: {ins_name}") 422 | 423 | def visit_packed_switch(self, value: str, blocks: list) -> None: 424 | self.frame.switch_data[self._last_label] = (value, blocks) 425 | 426 | def visit_sparse_switch(self, branches: dict) -> None: 427 | self.frame.switch_data[self._last_label] = branches 428 | 429 | 430 | class _SourceClassVisitor(ClassVisitor): 431 | smali_class: SmaliClass 432 | """The result class object""" 433 | 434 | vm: SmaliVM 435 | """The vm used wihtin method visitor objects""" 436 | 437 | def __init__(self, vm: SmaliVM, smali_class: SmaliClass = None) -> None: 438 | super().__init__() 439 | self.vm = vm 440 | self.smali_class = smali_class 441 | 442 | def visit_annotation(self, access_flags: int, signature: str) -> AnnotationVisitor: 443 | annotation = SmaliAnnotation(self.smali_class, signature, access_flags) 444 | self.smali_class.annotations.append(annotation) 445 | return SmaliVMAnnotationReader(annotation) 446 | 447 | def visit_class(self, name: str, access_flags: int) -> None: 448 | self.smali_class = SmaliClass(None, name, access_flags) 449 | 450 | def visit_inner_class(self, name: str, access_flags: int) -> "ClassVisitor": 451 | inner = SmaliClass(self.smali_class, name, access_flags) 452 | self.smali_class[name] = inner 453 | return SmaliVMClassReader(self.vm, inner) 454 | 455 | def visit_field( 456 | self, name: str, access_flags: int, field_type: str, value=None 457 | ) -> FieldVisitor: 458 | field = SmaliField( 459 | field_type, 460 | self.smali_class, 461 | f"{name}:{field_type}", 462 | access_flags, 463 | name, 464 | value=SmaliValue(value) if value else None, 465 | ) 466 | self.smali_class[name] = field 467 | return SmaliVMFieldReader(field) 468 | 469 | def visit_method( 470 | self, name: str, access_flags: int, parameters: list, return_type: str 471 | ) -> MethodVisitor: 472 | signature = f"{name}({''.join(parameters)}){return_type}" 473 | method = SmaliMethod(self.vm, self.smali_class, signature, access_flags) 474 | visitor = SmaliVMMethodReader(method) 475 | self.vm.new_frame(method, visitor.frame) 476 | self.smali_class[name] = method 477 | return visitor 478 | 479 | def visit_implements(self, interface: str) -> None: 480 | self.smali_class.interfaces.append(SVMType(interface)) 481 | 482 | def visit_super(self, super_class: str) -> None: 483 | self.smali_class.super_cls = SVMType(super_class) 484 | 485 | 486 | class _SmaliClassLoader(ClassLoader): 487 | vm: SmaliVM 488 | """The vm storing all defined classes.""" 489 | 490 | def __init__(self, vm: SmaliVM) -> None: 491 | self.vm = vm 492 | 493 | def define_class(self, source: bytes) -> SmaliClass: 494 | reader = SmaliReader(validate=True, comments=False) 495 | 496 | visitor = SmaliVMClassReader(self.vm) 497 | reader.visit(source, visitor) 498 | smali_class = visitor.smali_class 499 | if not smali_class: 500 | raise ValueError("Could not parse class!") 501 | 502 | self.vm.new_class(smali_class) 503 | return smali_class 504 | 505 | def load_class(self, source: str, init=True, lookup_missing=False) -> SmaliClass: 506 | smali_class = self.define_class(source) 507 | if init: 508 | smali_class.clinit() 509 | return smali_class 510 | 511 | 512 | #################################################################################################### 513 | # EXTERNAL 514 | #################################################################################################### 515 | SmaliVMAnnotationReader = _SourceAnnotationVisitor 516 | SmaliVMFieldReader = _SourceFieldVisitor 517 | SmaliVMMethodReader = _SourceMethodVisitor 518 | SmaliVMClassReader = _SourceClassVisitor 519 | SmaliClassLoader = _SmaliClassLoader 520 | -------------------------------------------------------------------------------- /smali/opcode.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | NOP = "nop" 19 | MOVE = "move" 20 | MOVE_FROM16 = "move/from16" 21 | MOVE_16 = "move/16" 22 | MOVE_WIDE = "move-wide" 23 | MOVE_WIDE_FROM16 = "move-wide/from16" 24 | MOVE_WIDE_16 = "move-wide/16" 25 | MOVE_OBJECT = "move-object" 26 | MOVE_OBJECT_FROM16 = "move-object/from16" 27 | MOVE_OBJECT_16 = "move-object/16" 28 | MOVE_RESULT = "move-result" 29 | MOVE_RESULT_WIDE = "move-result-wide" 30 | MOVE_RESULT_OBJECT = "move-result-object" 31 | MOVE_EXCEPTION = "move-exception" 32 | RETURN_VOID = "return-void" 33 | RETURN = "return" 34 | RETURN_WIDE = "return-wide" 35 | RETURN_OBJECT = "return-object" 36 | CONST_4 = "const/4" 37 | CONST_16 = "const/16" 38 | CONST = "const" 39 | CONST_HIGH16 = "const/high16" 40 | CONST_WIDE_16 = "const-wide/16" 41 | CONST_WIDE_32 = "const-wide/32" 42 | CONST_WIDE = "const-wide" 43 | CONST_WIDE_HIGH16 = "const-wide/high16" 44 | CONST_STRING = "const-string" 45 | CONST_STRING_JUMBO = "const-string/jumbo" 46 | CONST_CLASS = "const-class" 47 | MONITOR_ENTER = "monitor-enter" 48 | MONITOR_EXIT = "monitor-exit" 49 | CHECK_CAST = "check-cast" 50 | INSTANCE_OF = "instance-of" 51 | ARRAY_LENGTH = "array-length" 52 | NEW_INSTANCE = "new-instance" 53 | NEW_ARRAY = "new-array" 54 | FILLED_NEW_ARRAY = "filled-new-array" 55 | FILLED_NEW_ARRAY_RANGE = "filled-new-array/range" 56 | FILL_ARRAY_DATA = "fill-array-data" 57 | THROW = "throw" 58 | GOTO = "goto" 59 | GOTO_16 = "goto/16" 60 | GOTO_32 = "goto/32" 61 | PACKED_SWITCH = "packed-switch" 62 | SPARSE_SWITCH = "sparse-switch" 63 | CMPL_FLOAT = "cmpl-float" 64 | CMPG_FLOAT = "cmpg-float" 65 | CMPL_DOUBLE = "cmpl-double" 66 | CMPG_DOUBLE = "cmpg-double" 67 | CMP_LONG = "cmp-long" 68 | IF_EQ = "if-eq" 69 | IF_NE = "if-ne" 70 | IF_LT = "if-lt" 71 | IF_GE = "if-ge" 72 | IF_GT = "if-gt" 73 | IF_LE = "if-le" 74 | IF_EQZ = "if-eqz" 75 | IF_NEZ = "if-nez" 76 | IF_LTZ = "if-ltz" 77 | IF_GEZ = "if-gez" 78 | IF_GTZ = "if-gtz" 79 | IF_LEZ = "if-lez" 80 | AGET = "aget" 81 | AGET_WIDE = "aget-wide" 82 | AGET_OBJECT = "aget-object" 83 | AGET_BOOLEAN = "aget-boolean" 84 | AGET_BYTE = "aget-byte" 85 | AGET_CHAR = "aget-char" 86 | AGET_SHORT = "aget-short" 87 | APUT = "aput" 88 | APUT_WIDE = "aput-wide" 89 | APUT_OBJECT = "aput-object" 90 | APUT_BOOLEAN = "aput-boolean" 91 | APUT_BYTE = "aput-byte" 92 | APUT_CHAR = "aput-char" 93 | APUT_SHORT = "aput-short" 94 | IGET = "iget" 95 | IGET_WIDE = "iget-wide" 96 | IGET_OBJECT = "iget-object" 97 | IGET_BOOLEAN = "iget-boolean" 98 | IGET_BYTE = "iget-byte" 99 | IGET_CHAR = "iget-char" 100 | IGET_SHORT = "iget-short" 101 | IPUT = "iput" 102 | IPUT_WIDE = "iput-wide" 103 | IPUT_OBJECT = "iput-object" 104 | IPUT_BOOLEAN = "iput-boolean" 105 | IPUT_BYTE = "iput-byte" 106 | IPUT_CHAR = "iput-char" 107 | IPUT_SHORT = "iput-short" 108 | SGET = "sget" 109 | SGET_WIDE = "sget-wide" 110 | SGET_OBJECT = "sget-object" 111 | SGET_BOOLEAN = "sget-boolean" 112 | SGET_BYTE = "sget-byte" 113 | SGET_CHAR = "sget-char" 114 | SGET_SHORT = "sget-short" 115 | SPUT = "sput" 116 | SPUT_WIDE = "sput-wide" 117 | SPUT_OBJECT = "sput-object" 118 | SPUT_BOOLEAN = "sput-boolean" 119 | SPUT_BYTE = "sput-byte" 120 | SPUT_CHAR = "sput-char" 121 | SPUT_SHORT = "sput-short" 122 | INVOKE_VIRTUAL = "invoke-virtual" 123 | INVOKE_SUPER = "invoke-super" 124 | INVOKE_DIRECT = "invoke-direct" 125 | INVOKE_STATIC = "invoke-static" 126 | INVOKE_INTERFACE = "invoke-interface" 127 | INVOKE_VIRTUAL_RANGE = "invoke-virtual/range" 128 | INVOKE_SUPER_RANGE = "invoke-super/range" 129 | INVOKE_DIRECT_RANGE = "invoke-direct/range" 130 | INVOKE_STATIC_RANGE = "invoke-static/range" 131 | INVOKE_INTERFACE_RANGE = "invoke-interface/range" 132 | NEG_INT = "neg-int" 133 | NOT_INT = "not-int" 134 | NEG_LONG = "neg-long" 135 | NOT_LONG = "not-long" 136 | NEG_FLOAT = "neg-float" 137 | NEG_DOUBLE = "neg-double" 138 | INT_TO_LONG = "int-to-long" 139 | INT_TO_FLOAT = "int-to-float" 140 | INT_TO_DOUBLE = "int-to-double" 141 | LONG_TO_INT = "long-to-int" 142 | LONG_TO_FLOAT = "long-to-float" 143 | LONG_TO_DOUBLE = "long-to-double" 144 | FLOAT_TO_INT = "float-to-int" 145 | FLOAT_TO_LONG = "float-to-long" 146 | FLOAT_TO_DOUBLE = "float-to-double" 147 | DOUBLE_TO_INT = "double-to-int" 148 | DOUBLE_TO_LONG = "double-to-long" 149 | DOUBLE_TO_FLOAT = "double-to-float" 150 | INT_TO_BYTE = "int-to-byte" 151 | INT_TO_CHAR = "int-to-char" 152 | INT_TO_SHORT = "int-to-short" 153 | ADD_INT = "add-int" 154 | SUB_INT = "sub-int" 155 | MUL_INT = "mul-int" 156 | DIV_INT = "div-int" 157 | REM_INT = "rem-int" 158 | AND_INT = "and-int" 159 | OR_INT = "or-int" 160 | XOR_INT = "xor-int" 161 | SHL_INT = "shl-int" 162 | SHR_INT = "shr-int" 163 | USHR_INT = "ushr-int" 164 | ADD_LONG = "add-long" 165 | SUB_LONG = "sub-long" 166 | MUL_LONG = "mul-long" 167 | DIV_LONG = "div-long" 168 | REM_LONG = "rem-long" 169 | AND_LONG = "and-long" 170 | OR_LONG = "or-long" 171 | XOR_LONG = "xor-long" 172 | SHL_LONG = "shl-long" 173 | SHR_LONG = "shr-long" 174 | USHR_LONG = "ushr-long" 175 | ADD_FLOAT = "add-float" 176 | SUB_FLOAT = "sub-float" 177 | MUL_FLOAT = "mul-float" 178 | DIV_FLOAT = "div-float" 179 | REM_FLOAT = "rem-float" 180 | ADD_DOUBLE = "add-double" 181 | SUB_DOUBLE = "sub-double" 182 | MUL_DOUBLE = "mul-double" 183 | DIV_DOUBLE = "div-double" 184 | REM_DOUBLE = "rem-double" 185 | ADD_INT_2ADDR = "add-int/2addr" 186 | SUB_INT_2ADDR = "sub-int/2addr" 187 | MUL_INT_2ADDR = "mul-int/2addr" 188 | DIV_INT_2ADDR = "div-int/2addr" 189 | REM_INT_2ADDR = "rem-int/2addr" 190 | AND_INT_2ADDR = "and-int/2addr" 191 | OR_INT_2ADDR = "or-int/2addr" 192 | XOR_INT_2ADDR = "xor-int/2addr" 193 | SHL_INT_2ADDR = "shl-int/2addr" 194 | SHR_INT_2ADDR = "shr-int/2addr" 195 | USHR_INT_2ADDR = "ushr-int/2addr" 196 | ADD_LONG_2ADDR = "add-long/2addr" 197 | SUB_LONG_2ADDR = "sub-long/2addr" 198 | MUL_LONG_2ADDR = "mul-long/2addr" 199 | DIV_LONG_2ADDR = "div-long/2addr" 200 | REM_LONG_2ADDR = "rem-long/2addr" 201 | AND_LONG_2ADDR = "and-long/2addr" 202 | OR_LONG_2ADDR = "or-long/2addr" 203 | XOR_LONG_2ADDR = "xor-long/2addr" 204 | SHL_LONG_2ADDR = "shl-long/2addr" 205 | SHR_LONG_2ADDR = "shr-long/2addr" 206 | USHR_LONG_2ADDR = "ushr-long/2addr" 207 | ADD_FLOAT_2ADDR = "add-float/2addr" 208 | SUB_FLOAT_2ADDR = "sub-float/2addr" 209 | MUL_FLOAT_2ADDR = "mul-float/2addr" 210 | DIV_FLOAT_2ADDR = "div-float/2addr" 211 | REM_FLOAT_2ADDR = "rem-float/2addr" 212 | ADD_DOUBLE_2ADDR = "add-double/2addr" 213 | SUB_DOUBLE_2ADDR = "sub-double/2addr" 214 | MUL_DOUBLE_2ADDR = "mul-double/2addr" 215 | DIV_DOUBLE_2ADDR = "div-double/2addr" 216 | REM_DOUBLE_2ADDR = "rem-double/2addr" 217 | ADD_INT_LIT16 = "add-int/lit16" 218 | RSUB_INT = "rsub-int" 219 | MUL_INT_LIT16 = "mul-int/lit16" 220 | DIV_INT_LIT16 = "div-int/lit16" 221 | REM_INT_LIT16 = "rem-int/lit16" 222 | AND_INT_LIT16 = "and-int/lit16" 223 | OR_INT_LIT16 = "or-int/lit16" 224 | XOR_INT_LIT16 = "xor-int/lit16" 225 | ADD_INT_LIT8 = "add-int/lit8" 226 | RSUB_INT_LIT8 = "rsub-int/lit8" 227 | MUL_INT_LIT8 = "mul-int/lit8" 228 | DIV_INT_LIT8 = "div-int/lit8" 229 | REM_INT_LIT8 = "rem-int/lit8" 230 | AND_INT_LIT8 = "and-int/lit8" 231 | OR_INT_LIT8 = "or-int/lit8" 232 | XOR_INT_LIT8 = "xor-int/lit8" 233 | SHL_INT_LIT8 = "shl-int/lit8" 234 | SHR_INT_LIT8 = "shr-int/lit8" 235 | USHR_INT_LIT8 = "ushr-int/lit8" 236 | 237 | IGET_VOLATILE = "iget-volatile" 238 | IPUT_VOLATILE = "iput-volatile" 239 | SGET_VOLATILE = "sget-volatile" 240 | SPUT_VOLATILE = "sput-volatile" 241 | IGET_OBJECT_VOLATILE = "iget-object-volatile" 242 | IGET_WIDE_VOLATILE = "iget-wide-volatile" 243 | IPUT_WIDE_VOLATILE = "iput-wide-volatile" 244 | SGET_WIDE_VOLATILE = "sget-wide-volatile" 245 | SPUT_WIDE_VOLATILE = "sput-wide-volatile" 246 | 247 | THROW_VERIFICATION_ERROR = "throw-verification-error" 248 | EXECUTE_INLINE = "execute-inline" 249 | EXECUTE_INLINE_RANGE = "execute-inline/range" 250 | INVOKE_DIRECT_EMPTY = "invoke-direct-empty" 251 | INVOKE_OBJECT_INIT_RANGE = "invoke-object-init/range" 252 | RETURN_VOID_BARRIER = "return-void-barrier" 253 | RETURN_VOID_NO_BARRIER = "return-void-no-barrier" 254 | IGET_QUICK = "iget-quick" 255 | IGET_WIDE_QUICK = "iget-wide-quick" 256 | IGET_OBJECT_QUICK = "iget-object-quick" 257 | IPUT_QUICK = "iput-quick" 258 | IPUT_WIDE_QUICK = "iput-wide-quick" 259 | IPUT_OBJECT_QUICK = "iput-object-quick" 260 | IPUT_BOOLEAN_QUICK = "iput-boolean-quick" 261 | IPUT_BYTE_QUICK = "iput-byte-quick" 262 | IPUT_CHAR_QUICK = "iput-char-quick" 263 | IPUT_SHORT_QUICK = "iput-short-quick" 264 | IGET_BOOLEAN_QUICK = "iget-boolean-quick" 265 | IGET_BYTE_QUICK = "iget-byte-quick" 266 | IGET_CHAR_QUICK = "iget-char-quick" 267 | IGET_SHORT_QUICK = "iget-short-quick" 268 | 269 | INVOKE_VIRTUAL_QUICK = "invoke-virtual-quick" 270 | INVOKE_VIRTUAL_QUICK_RANGE = "invoke-virtual-quick/range" 271 | INVOKE_SUPER_QUICK = "invoke-super-quick" 272 | INVOKE_SUPER_QUICK_RANGE = "invoke-super-quick/range" 273 | 274 | IPUT_OBJECT_VOLATILE = "iput-object-volatile" 275 | SGET_OBJECT_VOLATILE = "sget-object-volatile" 276 | SPUT_OBJECT_VOLATILE = "sput-object-volatile" 277 | 278 | PACKED_SWITCH_PAYLOAD = "packed-switch-payload" 279 | SPARSE_SWITCH_PAYLOAD = "sparse-switch-payload" 280 | ARRAY_PAYLOAD = "array-payload" 281 | 282 | INVOKE_POLYMORPHIC = "invoke-polymorphic" 283 | INVOKE_POLYMORPHIC_RANGE = "invoke-polymorphic/range" 284 | 285 | INVOKE_CUSTOM = "invoke-custom" 286 | INVOKE_CUSTOM_RANGE = "invoke-custom/range" 287 | 288 | CONST_METHOD_HANDLE = "const-method-handle" 289 | CONST_METHOD_TYPE = "const-method-type" 290 | -------------------------------------------------------------------------------- /smali/shell/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | __doc__ = """ 17 | Implementation of a "shell"-like interpreter that can be run interactively 18 | or can execute smali files. 19 | 20 | The options the ``smali.shell`` module accept are the following: 21 | 22 | file [file ...] 23 | The Smali-Script files (``.ssf``) to be interpreted. Note that files 24 | as input won't result in interactive mode. Use the ``-i`` flag to run 25 | the interactive mode afterwards. 26 | 27 | -i / --interactive 28 | Runs the interactive mode after executing/importing input files. 29 | 30 | """ 31 | 32 | from smali.shell.model import ISmaliShell, SMALI_SCRIPT_SUFFIX 33 | from smali.shell.cli import start_cli 34 | 35 | -------------------------------------------------------------------------------- /smali/shell/__main__.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | __doc__ = "Main module of smali.shell" 17 | 18 | from smali.shell.cli import start_cli 19 | 20 | if __name__ == '__main__': 21 | start_cli() 22 | -------------------------------------------------------------------------------- /smali/shell/cli.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | import sys 17 | 18 | from argparse import ArgumentParser 19 | 20 | from smali import VERSION 21 | from smali.shell import ISmaliShell 22 | 23 | 24 | def start_cli(): 25 | """Starts the Smali interpreter.""" 26 | 27 | parser = ArgumentParser( 28 | description=( 29 | "ISmali (Interactive Smali interpreter) can execute Smali-Code snippets" 30 | "on the fly as well as import *.smali files and use them later on." 31 | ) 32 | ) 33 | parser.add_argument( 34 | "file", 35 | nargs="*", 36 | help=( 37 | "The Smali-Script files ('.ssf') to be interpreted. Note that" 38 | " files as input won't result in interactive mode. Use the '-i'" 39 | " flag to run the interactive mode afterwards." 40 | ), 41 | ) 42 | parser.add_argument( 43 | "-i", 44 | "--interactive", 45 | action="store_true", 46 | help=("Runs the interactive mode after executing/ importing input files."), 47 | ) 48 | parser.add_argument( 49 | "--version", action="store_true", help="Stores the current interpreter version" 50 | ) 51 | 52 | args = parser.parse_args().__dict__ 53 | if args["version"]: 54 | print(VERSION) 55 | sys.exit(0) 56 | 57 | shell = ISmaliShell() 58 | files = args["file"] 59 | 60 | if len(files) > 0: 61 | for path in args["file"]: 62 | shell.do_import(path) 63 | 64 | if len(files) == 0 or args["interactive"]: 65 | shell.cmdloop( 66 | ( 67 | f"ISmali {VERSION} on {sys.platform}\nType" 68 | " 'help' or 'copyright' for more information." 69 | ) 70 | ) 71 | -------------------------------------------------------------------------------- /smali/shell/model.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | import os 17 | import sys 18 | import traceback 19 | import pprint 20 | 21 | from cmd import Cmd 22 | 23 | from smali import ( 24 | AccessType, 25 | opcode, 26 | SmaliValue, 27 | MethodVisitor, 28 | ClassVisitor, 29 | FieldVisitor, 30 | SmaliReader, 31 | ) 32 | from smali.bridge import ( 33 | Frame, 34 | executor, 35 | SmaliObject, 36 | SmaliClass, 37 | SmaliField, 38 | SmaliMethod, 39 | ) 40 | from smali.bridge.vm import ( 41 | SmaliVMClassReader, 42 | SmaliVMMethodReader, 43 | SmaliVMFieldReader, 44 | SmaliVM, 45 | ) 46 | 47 | SMALI_SCRIPT_SUFFIX = "ssf" 48 | """File suffix for Smali-Script files""" 49 | 50 | 51 | class DefaultVisitor(MethodVisitor, ClassVisitor): 52 | """Visitor implementation that handles both class and method definitions""" 53 | 54 | frame: Frame 55 | """The root method context""" 56 | 57 | cls: SmaliVMClassReader 58 | """The root class""" 59 | 60 | shell: "ISmaliShell" 61 | """The shell reference""" 62 | 63 | last_label: str 64 | """Stores the last label that has been typed""" 65 | 66 | importing: bool = False 67 | 68 | def __init__(self, shell: "ISmaliShell") -> None: 69 | super().__init__(None) 70 | self.shell = shell 71 | self.frame = Frame() 72 | self.last_label = None 73 | self.pos = 0 74 | 75 | self.frame.vm = shell.emulator 76 | self.reset_var("p0", shell.root) 77 | 78 | def visit_restart(self, register: str) -> None: 79 | if register != "p0": 80 | self.reset_var(register) 81 | else: 82 | self.reset_var(register, self.shell.root) 83 | 84 | def reset_var(self, register: str, val=None): 85 | """Applies a new value to the given register name""" 86 | self.frame.registers[register] = val 87 | 88 | def visit_block(self, name: str) -> None: 89 | self.frame.labels[name] = self.pos 90 | self.pos += 1 91 | self.last_label = name 92 | self.frame.label = name 93 | 94 | def visit_registers(self, registers: int) -> None: 95 | for i in range(registers): 96 | self.reset_var(f"v{i}") 97 | 98 | def visit_locals(self, local_count: int) -> None: 99 | self.visit_registers(local_count) 100 | 101 | def visit_catch(self, exc_name: str, blocks: tuple) -> None: 102 | start, _, handler = blocks 103 | self.frame.catch[start] = (exc_name, handler) 104 | 105 | def visit_catchall(self, exc_name: str, blocks: tuple) -> None: 106 | self.visit_catch(exc_name, blocks) 107 | 108 | def visit_instruction(self, ins_name: str, args: list) -> None: 109 | for _, value in opcode.__dict__.items(): 110 | if value == ins_name: 111 | exc = executor.get_executor(ins_name) 112 | exc.args = args 113 | exc(self.frame) 114 | self.pos += 1 115 | 116 | if "return" in value: 117 | print(self.frame.return_value) 118 | return 119 | 120 | if self.importing: 121 | self.shell.onecmd(f"{ins_name} {' '.join(args)}") 122 | else: 123 | raise SyntaxError(f"Invalid OpCode: {ins_name}") 124 | 125 | def visit_field( 126 | self, name: str, access_flags: int, field_type: str, value=None 127 | ) -> FieldVisitor: 128 | field = SmaliField( 129 | field_type, 130 | self.shell.root.smali_class, 131 | f"{name}:{field_type}", 132 | access_flags, 133 | name, 134 | value=SmaliValue(value) if value else None, 135 | ) 136 | self.shell.root.smali_class[name] = field 137 | if access_flags not in AccessType.STATIC and value is not None: 138 | self.shell.root[name] = value 139 | 140 | return SmaliVMFieldReader(field) 141 | 142 | def visit_return(self, ret_type: str, args: list) -> None: 143 | if ret_type: 144 | self.visit_instruction(f"return-{ret_type}", args) 145 | else: 146 | self.visit_instruction("return", args) 147 | 148 | def visit_invoke(self, inv_type: str, args: list, owner: str, method: str) -> None: 149 | if inv_type: 150 | self.visit_instruction( 151 | f"invoke-{inv_type}", [inv_type, args, owner, method] 152 | ) 153 | else: 154 | self.visit_instruction("invoke", [inv_type, args, owner, method]) 155 | 156 | def visit_method( 157 | self, name: str, access_flags: int, parameters: list, return_type: str 158 | ) -> MethodVisitor: 159 | signature = f"{name}({''.join(parameters)}){return_type}" 160 | smali_class = self.shell.root.smali_class 161 | 162 | method = SmaliMethod(self.shell.emulator, smali_class, signature, access_flags) 163 | visitor = SmaliVMMethodReader(method) 164 | self.shell.emulator.new_frame(method, visitor.frame) 165 | smali_class[name] = method 166 | return visitor 167 | 168 | def visit_inner_class(self, name: str, access_flags: int) -> "ClassVisitor": 169 | if self.importing: 170 | smali_class = self.shell.root.smali_class 171 | inner = SmaliClass(smali_class, name, access_flags) 172 | 173 | smali_class[name] = inner 174 | return SmaliVMClassReader(self.shell.emulator, inner) 175 | 176 | 177 | class ISmaliShell(Cmd): 178 | """Implementation of an interactive Smali-Interpreter.""" 179 | 180 | DEFAULT_PROMPT = ">>> " 181 | """The default prompt""" 182 | 183 | INLINE_PROMPT = "... " 184 | """Prompt used for field and method definitions""" 185 | 186 | visitor: DefaultVisitor 187 | """The default visitor instance""" 188 | 189 | reader: SmaliReader 190 | """The reader used to parse code snippets.""" 191 | 192 | emulator: SmaliVM 193 | """The actual VM reference that will execute each method""" 194 | 195 | root: SmaliObject 196 | """The base context used to define fields and methods.""" 197 | 198 | prompt: str = ">>> " 199 | """The prompt used by ``cmd.Cmd``""" 200 | 201 | check_import: bool = True 202 | """Used to indicate whether this shell should verify each import""" 203 | 204 | __imported_files: list = [] 205 | """Internal file list""" 206 | 207 | def __init__(self) -> None: 208 | super().__init__() 209 | self.stack = [] 210 | self.reader = SmaliReader(comments=False, snippet=True) 211 | self.emulator = SmaliVM() 212 | 213 | cls = SmaliClass(None, "L;", AccessType.PUBLIC + AccessType.FINAL) 214 | self.emulator.new_class(cls) 215 | self.root = SmaliObject(cls) 216 | self.visitor = DefaultVisitor(self) 217 | 218 | def do_import(self, path: str) -> None: 219 | """usage: import 220 | 221 | This command will try to import the given smali file or 222 | Smali-Script file (.ssf). Note that files with wrong suffixes 223 | will be ignored. 224 | """ 225 | if not os.path.exists(path): 226 | print(f'! Could not find file at "{path}"') 227 | return 228 | 229 | if path in self.__imported_files and self.check_import: 230 | return 231 | 232 | if not path.endswith((".smali", f".{SMALI_SCRIPT_SUFFIX}")): 233 | print(f"! Unknown file format (unknown suffix) at '{path}'") 234 | return 235 | 236 | cls = None 237 | with open(path, "r", encoding="utf-8") as source: 238 | self.__imported_files.append(path) 239 | if path.endswith(f".{SMALI_SCRIPT_SUFFIX}"): 240 | self.visitor.importing = True 241 | self.reader.visit(source, self.visitor) 242 | self.visitor.importing = False 243 | else: 244 | cls = self.emulator.classloader.load_class(source, init=False) 245 | 246 | try: 247 | if cls is not None: 248 | cls.clinit() 249 | except Exception: # :noqa 250 | print(traceback.format_exc()) 251 | 252 | def do_vars(self, _): 253 | """usage: vars 254 | 255 | Prints all variables with the help of ``pprint``. 256 | """ 257 | pprint.pprint(self.visitor.frame.registers) 258 | 259 | def do_fields(self, _): 260 | """usage: fields 261 | 262 | Prints all fields that have been defined in the root context. 263 | """ 264 | pprint.pprint(list(self.root.smali_class.fields())) 265 | 266 | def do_label(self, _): 267 | """:usage label 268 | 269 | Prints the name of the active label. 270 | """ 271 | print(self.visitor.frame.label) 272 | 273 | def do_del(self, register): 274 | """usage: del 275 | 276 | Deletes the variable at the specified register. The root 277 | context at 'p0' can't be deleted. 278 | """ 279 | if register == "p0": 280 | print("! Attempted to delete root-context - skipping...\n") 281 | return 282 | 283 | if register in self.visitor.frame.registers: 284 | val = self.visitor.frame.registers.pop(register) 285 | del val 286 | 287 | def precmd(self, line: str): 288 | if len(line) == 0: 289 | self.change_prompt(ISmaliShell.DEFAULT_PROMPT) 290 | return "EOF" 291 | 292 | return line 293 | 294 | def default(self, line: str) -> None: 295 | """Handles the instruction or register name 296 | 297 | :param line: the input line 298 | :type line: str 299 | """ 300 | if line == "EOF": 301 | return 302 | 303 | if isinstance(self.visitor, DefaultVisitor): 304 | if line in self.visitor.frame.registers: 305 | print(self.visitor.frame.registers[line]) 306 | return 307 | 308 | try: 309 | self.reader.visit(line, self.visitor) 310 | except Exception: 311 | print(traceback.format_exc()) 312 | 313 | def do_exit(self, _): 314 | """Exits the interpreter""" 315 | sys.exit(0) 316 | 317 | def do_copyright(self, _): 318 | """Prints copyright information""" 319 | print("Copyright (C) 2023-2024 MatrixEditor") 320 | 321 | def change_prompt(self, new_prompt: str): 322 | """Changes the prompt (for later usage)""" 323 | self.prompt = new_prompt 324 | -------------------------------------------------------------------------------- /smali/visitor.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | from typing import List, Optional, Generic, TypeVar 17 | 18 | _VT = TypeVar("_VT", bound="VisitorBase") 19 | 20 | 21 | class VisitorBase(Generic[_VT]): 22 | """Base class for Smali-Class visitor classes. 23 | 24 | :param delegate: A delegate visitor, defaults to None 25 | :type delegate: BaseVisitor subclass, optional 26 | """ 27 | 28 | def __init__(self, delegate: Optional[_VT] = None) -> None: 29 | self.delegate = delegate 30 | # does not apply to muliple inheritance 31 | if delegate and not isinstance(delegate, self.__class__.__base__ or type): 32 | raise TypeError( 33 | f"Invalid Visitor type - expected subclass of {self.__class__}" 34 | ) 35 | 36 | def visit_comment(self, text: str) -> None: 37 | """Visits a comment string. 38 | 39 | Important: if you want to visit inline comments (EOL comments) 40 | use `#visit_eol_comment()` instead. 41 | 42 | :param text: the comment's text without the leading '#' 43 | :type text: str 44 | """ 45 | if self.delegate: 46 | self.delegate.visit_comment(text) 47 | 48 | def visit_eol_comment(self, text: str) -> None: 49 | """Visits an inlined comment (EOL comment) 50 | 51 | :param text: the text without the leading '#' 52 | :type text: str 53 | """ 54 | if self.delegate: 55 | self.delegate.visit_eol_comment(text) 56 | 57 | def visit_end(self) -> None: 58 | """Called at then end of an annotation.""" 59 | if self.delegate: 60 | self.delegate.visit_end() 61 | 62 | 63 | class AnnotationVisitor(VisitorBase["AnnotationVisitor"]): 64 | """Base class for annotation visitors.""" 65 | 66 | def visit_value(self, name: str, value: str) -> None: 67 | """Visits a simple annotation value. 68 | 69 | :param name: the value's name 70 | :type name: str 71 | :param value: the value 72 | :type value: str 73 | """ 74 | if self.delegate: 75 | self.delegate.visit_value(name, value) 76 | 77 | def visit_array(self, name: str, values: list) -> None: 78 | """Visits an array of values. 79 | 80 | :param name: the value name 81 | :type name: str 82 | :param values: the array's values 83 | :type values: list 84 | """ 85 | if self.delegate: 86 | self.delegate.visit_array(name, values) 87 | 88 | def visit_subannotation( 89 | self, name: str, access_flags: int, signature: str 90 | ) -> Optional["AnnotationVisitor"]: 91 | """Prepares to visit an internal annotation. 92 | 93 | :param name: the annotation value name 94 | :type name: str 95 | :param access_flags: the annotations access flags (zero on most cases) 96 | :type access_flags: int 97 | :param signature: the class signature 98 | :type signature: str 99 | """ 100 | if self.delegate: 101 | return self.delegate.visit_subannotation(name, access_flags, signature) 102 | 103 | def visit_enum(self, name: str, owner: str, const: str, value_type: str) -> None: 104 | """Visits an enum value 105 | 106 | :param owner: the declaring class 107 | :type owner: str 108 | :param name: the annotation value name 109 | :type name: str 110 | :param const: the enum constant name 111 | :type const: str 112 | :param value_type: the value type 113 | :type value_type: str 114 | """ 115 | if self.delegate: 116 | self.delegate.visit_enum(name, owner, const, value_type) 117 | 118 | 119 | class MethodVisitor(VisitorBase["MethodVisitor"]): 120 | """Base class for method visitors.""" 121 | 122 | def visit_catch(self, exc_name: str, blocks: tuple) -> None: 123 | """Called on a ``.catch`` statement. 124 | 125 | The blocks contain the two enclosing goto blocks and the returning 126 | definition: 127 | 128 | .. code-block:: bnf 129 | 130 | .catch { .. } 131 | 132 | :param exc_name: the exception descriptor 133 | :type exc_name: str 134 | :param blocks: the goto-blocks definition 135 | :type blocks: tuple 136 | """ 137 | if self.delegate: 138 | self.delegate.visit_catch(exc_name, blocks) 139 | 140 | def visit_catchall(self, exc_name: str, blocks: tuple) -> None: 141 | """Called on a ``.catchall`` statement. 142 | 143 | The blocks contain the two enclosing goto blocks and the returning 144 | definition: 145 | 146 | .. code-block:: bnf 147 | 148 | .catchall { .. } 149 | 150 | :param exc_name: the exception descriptor 151 | :type exc_name: str 152 | :param blocks: the goto-blocks definition 153 | :type blocks: tuple 154 | """ 155 | if self.delegate: 156 | self.delegate.visit_catchall(exc_name, blocks) 157 | 158 | def visit_param(self, register: str, name: str) -> None: 159 | """Called on a ``.param`` statement 160 | 161 | :param register: the register 162 | :type register: str 163 | :param name: the parameter's name 164 | :type name: str 165 | """ 166 | if self.delegate: 167 | return self.delegate.visit_param(register, name) 168 | 169 | def visit_annotation( 170 | self, access_flags: int, signature: str 171 | ) -> Optional[AnnotationVisitor]: 172 | """Prepares to visit an annotation. 173 | 174 | :param access_flags: the annotations access flags (zero on most cases) 175 | :type access_flags: int 176 | :param signature: the class signature 177 | :type signature: str 178 | """ 179 | if self.delegate: 180 | return self.delegate.visit_annotation(access_flags, signature) 181 | 182 | def visit_locals(self, local_count: int) -> None: 183 | """Called on a ``.locals`` statement. 184 | 185 | The execution context of this method should be the same as of 186 | *visit_registers*. 187 | 188 | :param locals: the amount of local variables 189 | :type locals: int 190 | """ 191 | if self.delegate: 192 | self.delegate.visit_locals(local_count) 193 | 194 | def visit_registers(self, registers: int) -> None: 195 | """Called on a '.registers' statement. 196 | 197 | The execution context of this method should be the same as of 198 | 'visit_locals'. 199 | 200 | :param registers: the amount of local variables 201 | :type registers: int 202 | """ 203 | if self.delegate: 204 | self.delegate.visit_registers(registers) 205 | 206 | def visit_line(self, number: int) -> None: 207 | """Called when a line definition is parsed. 208 | 209 | :param name: the line number 210 | :type name: str 211 | """ 212 | if self.delegate: 213 | self.delegate.visit_line(number) 214 | 215 | def visit_block(self, name: str) -> None: 216 | """Called when a goto-block definition is parsed. 217 | 218 | :param name: the block's name 219 | :type name: str 220 | """ 221 | if self.delegate: 222 | self.delegate.visit_block(name) 223 | 224 | def visit_invoke(self, inv_type: str, args: list, owner: str, method: str) -> None: 225 | """Handles an 'invoke-' statement. 226 | 227 | This method is called whenever an 'invoke-' statement hias been 228 | parsed. That includes 'invoke-virtual' as well as 'invoke-direct'. 229 | 230 | The provided metho string contains the method signature which can 231 | be passed into the Type constructor. 232 | 233 | :param inv_type: the invocation type (direct, virtual, ...) 234 | :type inv_type: str 235 | :param args: the argument list 236 | :type args: list 237 | :param owner: the owner class of the referenced method 238 | :type owner: str 239 | :param method: the method to call 240 | :type method: str 241 | """ 242 | if self.delegate: 243 | self.delegate.visit_invoke(inv_type, args, owner, method) 244 | 245 | def visit_return(self, ret_type: str, args: List[str]) -> None: 246 | """Handles 'return-' statements. 247 | 248 | :param ret_type: the return type, e.g. "object" or "void", ... 249 | :type ret_type: str 250 | :param args: the argument list 251 | :type args: list 252 | """ 253 | if self.delegate: 254 | self.delegate.visit_return(ret_type, args) 255 | 256 | def visit_instruction(self, ins_name: str, args: List[str]) -> None: 257 | """Visits common instructions with one or two parameters. 258 | 259 | :param ins_name: the instruction name 260 | :type ins_name: str 261 | :param args: the argument list 262 | :type args: list 263 | """ 264 | if self.delegate: 265 | self.delegate.visit_instruction(ins_name, args) 266 | 267 | def visit_goto(self, block_name: str) -> None: 268 | """Visits 'goto' statements. 269 | 270 | :param block_name: the destination block name 271 | :type block_name: str 272 | """ 273 | if self.delegate: 274 | self.delegate.visit_goto(block_name) 275 | 276 | def visit_packed_switch(self, value: str, blocks: List[str]) -> None: 277 | """Handles the packed-switch statement. 278 | 279 | :param value: the value which will be "switched" 280 | :type value: str 281 | :param blocks: the block ids 282 | :type blocks: list[str] 283 | """ 284 | if self.delegate: 285 | self.delegate.visit_packed_switch(value, blocks) 286 | 287 | def visit_array_data(self, length: str, value_list: List[int]) -> None: 288 | """Called on an '.array-data' statement. 289 | 290 | :param length: the array's length 291 | :type length: str 292 | :param value_list: the array's values 293 | :type value_list: list 294 | """ 295 | if self.delegate: 296 | self.delegate.visit_array_data(length, value_list) 297 | 298 | def visit_local( 299 | self, register: str, name: str, descriptor: str, full_descriptor: str 300 | ) -> None: 301 | """Handles debug information packed into .local statements. 302 | 303 | :param register: the variable register 304 | :type register: str 305 | :param name: the variable name 306 | :type name: str 307 | :param descriptor: the type descriptor 308 | :type descriptor: str 309 | :param full_descriptor: the java descriptor 310 | :type full_descriptor: str 311 | """ 312 | if self.delegate: 313 | self.delegate.visit_local(register, name, descriptor, full_descriptor) 314 | 315 | def visit_sparse_switch(self, branches: dict) -> None: 316 | """Visits a .sparse-switch statement. 317 | 318 | The branches takes the original case value as their key 319 | and the block_id as their value. 320 | 321 | :param branches: the switch branches 322 | :type branches: dict 323 | """ 324 | if self.delegate: 325 | self.delegate.visit_sparse_switch(branches) 326 | 327 | def visit_prologue(self) -> None: 328 | """Visits a .prologue statement. 329 | 330 | Note that this call comes without any arguments. 331 | """ 332 | if self.delegate: 333 | self.delegate.visit_prologue() 334 | 335 | def visit_restart(self, register: str) -> None: 336 | """Visits a .restart statement. 337 | 338 | :param register: the register 339 | :type register: str 340 | """ 341 | if self.delegate: 342 | self.delegate.visit_restart(register) 343 | 344 | 345 | class FieldVisitor(VisitorBase["FieldVisitor"]): 346 | """Base class for field visitors.""" 347 | 348 | def visit_annotation( 349 | self, access_flags: int, signature: str 350 | ) -> Optional[AnnotationVisitor]: 351 | """Prepares to visit an annotation. 352 | 353 | :param access_flags: the annotations access flags (zero on most cases) 354 | :type access_flags: int 355 | :param signature: the class signature 356 | :type signature: str 357 | """ 358 | if self.delegate: 359 | return self.delegate.visit_annotation(access_flags, signature) 360 | 361 | 362 | class ClassVisitor(VisitorBase["ClassVisitor"]): 363 | """Base class for Smali class visitors.""" 364 | 365 | def visit_class(self, name: str, access_flags: int) -> None: 366 | """Called when the class definition has been parsed. 367 | 368 | :param name: the class name (type descriptor, e.g. "Lcom/example/A;") 369 | :type name: str 370 | :param access_flags: different access flags (PUBLIC, FINAL, ...) 371 | :type access_flags: int 372 | """ 373 | if self.delegate: 374 | self.delegate.visit_class(name, access_flags) 375 | 376 | def visit_super(self, super_class: str) -> None: 377 | """Called when a .super statement has been parsed. 378 | 379 | :param super_class: the super class name as type descriptor 380 | :type super_class: str 381 | """ 382 | if self.delegate: 383 | self.delegate.visit_super(super_class) 384 | 385 | def visit_implements(self, interface: str) -> None: 386 | """Colled upon an implements directive. 387 | 388 | :param interface: the class name (internal name) 389 | :type interface: str 390 | """ 391 | if self.delegate: 392 | self.delegate.visit_implements(interface) 393 | 394 | def visit_field( 395 | self, name: str, access_flags: int, field_type: str, value: Optional[str]=None 396 | ) -> Optional[FieldVisitor]: 397 | """Called when a global field definition has been parsed. 398 | 399 | :param name: the field's name 400 | :type name: str 401 | :param access_flags: the access flags like PUBLIC, FINAL, ... 402 | :type access_flags: str 403 | :param field_type: the field's type (can be primitive) 404 | :type field_type: str 405 | :param value: the field's value 406 | :type value: _type_ 407 | """ 408 | if self.delegate: 409 | return self.delegate.visit_field(name, access_flags, field_type, value) 410 | 411 | def visit_method( 412 | self, name: str, access_flags: int, parameters: List[str], return_type: str 413 | ) -> Optional[MethodVisitor]: 414 | """Called when a method definition has been parsed. 415 | 416 | :param name: the method's name 417 | :type name: str 418 | :param access_flags: the access flags (PUBLIC, PRIVATE, ...) 419 | :type access_flags: int 420 | :param parameters: the parameter list (internal names) 421 | :type parameters: list 422 | :param return_type: the return type (internal name) 423 | :type return_type: str 424 | :return: a MethodVisitor that handles method parsing events 425 | :rtype: MethodVisitor 426 | """ 427 | if self.delegate: 428 | return self.delegate.visit_method( 429 | name, access_flags, parameters, return_type 430 | ) 431 | 432 | def visit_inner_class( 433 | self, name: str, access_flags: int 434 | ) -> Optional["ClassVisitor"]: 435 | """Called when the class definition has been parsed. 436 | 437 | :param name: the class name (type descriptor, e.g. "Lcom/example/A;") 438 | :type name: str 439 | :param access_flags: different access flags (PUBLIC, FINAL, ...) 440 | :type access_flags: int 441 | """ 442 | if self.delegate: 443 | return self.delegate.visit_inner_class(name, access_flags) 444 | 445 | def visit_annotation( 446 | self, access_flags: int, signature: str 447 | ) -> Optional[AnnotationVisitor]: 448 | """Prepares to visit an annotation. 449 | 450 | :param access_flags: the annotations access flags (zero on most cases) 451 | :type access_flags: int 452 | :param signature: the class signature 453 | :type signature: str 454 | """ 455 | if self.delegate: 456 | return self.delegate.visit_annotation(access_flags, signature) 457 | 458 | def visit_source(self, source: str) -> None: 459 | """Visits the source type of the smali file. 460 | 461 | :param source: the source type 462 | :type source: str 463 | """ 464 | if self.delegate: 465 | self.delegate.visit_source(source) 466 | 467 | def visit_debug(self, enabled: int) -> None: 468 | """Visits a ``.debug`` directive. 469 | 470 | :param enabled: whether debugging symbols are enabled. 471 | :type enabled: int 472 | """ 473 | if self.delegate: 474 | self.delegate.visit_debug(enabled) 475 | -------------------------------------------------------------------------------- /smali/writer.py: -------------------------------------------------------------------------------- 1 | # This file is part of pysmali's Smali API 2 | # Copyright (C) 2023-2024 MatrixEditor 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | __doc__ = """ 17 | Contains standard implementations for Smali writers that are able 18 | to procude classes, method, fields and annotations. 19 | """ 20 | 21 | import abc 22 | from typing import List, Optional 23 | from smali.visitor import ClassVisitor, MethodVisitor, FieldVisitor, AnnotationVisitor 24 | from smali.base import AccessType, Token 25 | from smali.reader import SupportsCopy, SmaliReader 26 | from smali import opcode 27 | 28 | __all__ = ["SmaliWriter", "FieldWriter", "MethodWriter", "AnnotationWriter"] 29 | 30 | 31 | class _ContainsCodeCache(SupportsCopy): 32 | """Interface to make sure the code cache is returned via a method.""" 33 | 34 | @abc.abstractmethod 35 | def get_cache(self) -> "_CodeCache": 36 | """Returns the current code cache. 37 | 38 | :return: the code cache 39 | :rtype: _CodeCache 40 | """ 41 | 42 | def copy(self, line: str, context: type = ClassVisitor) -> None: 43 | if isinstance(self, context): 44 | self.get_cache().add(line) 45 | 46 | else: 47 | last_writer = self.get_cache().peek() 48 | if not last_writer: 49 | self.get_cache().add(line) 50 | 51 | elif isinstance(last_writer, SupportsCopy): 52 | last_writer.copy(line, context) 53 | 54 | elif isinstance(last_writer, context): 55 | last_writer.get_cache().add(line) 56 | 57 | 58 | class _CodeCache: 59 | """Simple container class for a code cache. 60 | 61 | :param indent: the indentation level of this code, defaults to 0 62 | :type indent: int, optional 63 | """ 64 | 65 | default_indent = " " 66 | 67 | def __init__(self, indent=0) -> None: 68 | self.__indent = indent 69 | self.__code = [] 70 | self.__cache = [] 71 | self.__comment_cache = [] 72 | 73 | @property 74 | def indent(self) -> int: 75 | """Returns the indentation level 76 | 77 | :return: the indent amount 78 | :rtype: int 79 | """ 80 | return self.__indent 81 | 82 | @indent.setter 83 | def indent(self, value: int) -> None: 84 | self.__indent = value 85 | 86 | def add( 87 | self, line: str, start: str = "", end: str = "", custom_indent: int = -1 88 | ) -> None: 89 | """Appends the given line of code at the end of this cache. 90 | 91 | :param line: the line to add 92 | :type line: str 93 | :param start: additional prefix, defaults to "" 94 | :type start: str, optional 95 | :param end: additional suffix, defaults to "" 96 | :type end: str, optional 97 | """ 98 | indent = self.default_indent * ( 99 | self.indent if custom_indent == -1 else custom_indent 100 | ) 101 | self.__code.append(start + indent + line + end) 102 | 103 | def add_to_cache(self, cache: "_ContainsCodeCache") -> None: 104 | """Appends the given code cache to the end of this cache. 105 | 106 | :param cache: the cache to append 107 | :type cache: _CodeCache 108 | """ 109 | if cache: 110 | self.__cache.append(cache) 111 | 112 | def add_comment(self, comment: str) -> None: 113 | """Adds the given comment to the last line 114 | 115 | :param comment: the comment to add 116 | :type comment: str 117 | """ 118 | line = self.__code[-1] 119 | new_line = line[:-1] if line[-1] == "\n" else str(line) 120 | new_line += f" # {comment}" 121 | if line[-1] == "\n": 122 | new_line += "\n" 123 | self.__code[-1] = new_line 124 | 125 | def pop_comments(self) -> list: 126 | """Clears the comment cache and returns all elements. 127 | 128 | :return: the comments that have been stored. 129 | :rtype: list 130 | """ 131 | values = self.__comment_cache.copy() 132 | self.__comment_cache.clear() 133 | return values 134 | 135 | def apply_code_cache(self, clear_caches=False) -> None: 136 | """Applies stored caches to this one. 137 | 138 | The stored caches will be cleared and removed afterwards. 139 | 140 | :param clear_caches: whether sub-caches should be applied, defaults to False 141 | :type clear_caches: bool, optional 142 | """ 143 | for element in self.__cache: 144 | cache = element.get_cache() 145 | end = ( 146 | "\n" 147 | if isinstance(element, (_SmaliFieldWriter, _SmaliMethodWriter)) 148 | else "" 149 | ) 150 | if cache: 151 | self.add(cache.get_code(clear_cache=clear_caches), end=end) 152 | cache.clear() 153 | self.__cache.clear() 154 | 155 | def get_code(self, clear_cache=False) -> str: 156 | """Returns the Smali-Code 157 | 158 | :param clear_cache: whether cached objects should be applied first, defaults to False 159 | :type clear_cache: bool, optional 160 | :return: the source code as utf-8 string 161 | :rtype: str 162 | """ 163 | if clear_cache: 164 | self.apply_code_cache(True) 165 | return "\n".join(self.__code) 166 | 167 | def clear(self) -> None: 168 | """Clears this cache.""" 169 | self.__cache.clear() 170 | self.__comment_cache.clear() 171 | self.__code.clear() 172 | 173 | def peek(self) -> _ContainsCodeCache | None: 174 | """Returns the last element of this cache 175 | 176 | :return: the last element that contains itself a ``_CodeCache`` 177 | :rtype: _ContainsCodeCache 178 | """ 179 | if len(self.__cache) > 0: 180 | return self.__cache[-1] 181 | 182 | 183 | ########################################################################################## 184 | # ANNOTATION IMPLEMENTATION 185 | ########################################################################################## 186 | class _SmaliAnnotationWriter(AnnotationVisitor, _ContainsCodeCache): 187 | cache: _CodeCache 188 | 189 | def __init__( 190 | self, 191 | delegate: Optional[AnnotationVisitor] = None, 192 | indent=0, 193 | name=Token.ANNOTATION, 194 | ) -> None: 195 | super().__init__(delegate) 196 | self.cache = _CodeCache(indent) 197 | self._name = name 198 | 199 | def get_cache(self) -> "_CodeCache": 200 | return self.cache 201 | 202 | def visit_value(self, name: str, value) -> None: 203 | super().visit_value(name, value) 204 | indent = self.cache.indent + 1 205 | self.cache.add(f"{name} = {value}", custom_indent=indent) 206 | 207 | def visit_enum(self, name: str, owner: str, const: str, value_type: str) -> None: 208 | super().visit_enum(name, owner, const, value_type) 209 | indent = self.cache.indent + 1 210 | self.cache.add( 211 | f"{name} = .{Token.ENUM} {owner}->{const}:{value_type}", 212 | custom_indent=indent, 213 | ) 214 | 215 | def visit_subannotation( 216 | self, name: str, access_flags: int, signature: str 217 | ) -> "AnnotationVisitor": 218 | delegate = super().visit_subannotation(name, access_flags, signature) 219 | desc = f"{name} = .{Token.SUBANNOTATION} {signature}" 220 | if isinstance(delegate, _SmaliAnnotationWriter): 221 | a_visitor = delegate 222 | a_visitor.cache.indent = self.cache.indent + 1 223 | else: 224 | a_visitor = _SmaliAnnotationWriter( 225 | delegate, self.cache.indent + 1, Token.SUBANNOTATION 226 | ) 227 | 228 | a_visitor.cache.add(desc) 229 | self.cache.add_to_cache(a_visitor) 230 | return a_visitor 231 | 232 | def visit_array(self, name: str, values: list) -> None: 233 | indent_value = self.cache.default_indent 234 | indent = self.cache.indent 235 | 236 | if len(values) == 0: 237 | self.cache.add("%s = {}" % name, custom_indent=indent + 1) # noqa 238 | else: 239 | sep_value = ",\n" + indent_value * (indent + 2) 240 | self.cache.add( 241 | "%s = {\n%s%s\n%s}" 242 | % ( 243 | name, 244 | indent_value * (indent + 2), 245 | sep_value.join(values), 246 | indent_value * (indent + 1), 247 | ), 248 | custom_indent=indent + 1, 249 | ) 250 | return super().visit_array(name, values) 251 | 252 | def visit_comment(self, text: str) -> None: 253 | super().visit_comment(text) 254 | self.cache.add(f"# {text}") 255 | 256 | def visit_eol_comment(self, text: str) -> None: 257 | super().visit_eol_comment(text) 258 | self.cache.add_comment(text) 259 | 260 | def visit_end(self) -> None: 261 | super().visit_end() 262 | self.cache.apply_code_cache(clear_caches=True) 263 | self.cache.add(f".{Token.END} {self._name}\n") 264 | 265 | 266 | ########################################################################################## 267 | # FIELD IMPLEMENTATION 268 | ########################################################################################## 269 | class _SmaliFieldWriter(FieldVisitor, _ContainsCodeCache): 270 | cache: _CodeCache 271 | 272 | def __init__(self, delegate: Optional[FieldVisitor] = None, indent=0) -> None: 273 | super().__init__(delegate) 274 | self.cache = _CodeCache(indent) 275 | 276 | def visit_annotation(self, access_flags: int, signature: str) -> AnnotationVisitor: 277 | delegate = super().visit_annotation(access_flags, signature) 278 | desc = f".{Token.ANNOTATION} {' '.join(AccessType.get_names(access_flags))} {signature}" 279 | if isinstance(delegate, _SmaliAnnotationWriter): 280 | a_visitor = delegate 281 | a_visitor.cache.indent = self.cache.indent + 1 282 | else: 283 | a_visitor = _SmaliAnnotationWriter(delegate, self.cache.indent + 1) 284 | 285 | a_visitor.cache.add(desc) 286 | self.cache.add_to_cache(a_visitor) 287 | return a_visitor 288 | 289 | def get_cache(self) -> "_CodeCache": 290 | return self.cache 291 | 292 | def visit_comment(self, text: str) -> None: 293 | super().visit_comment(text) 294 | self.cache.add(f"# {text}") 295 | 296 | def visit_eol_comment(self, text: str) -> None: 297 | super().visit_eol_comment(text) 298 | self.cache.add_comment(text) 299 | 300 | def visit_end(self) -> None: 301 | super().visit_end() 302 | self.cache.apply_code_cache(True) 303 | self.cache.add(f".{Token.END} {Token.FIELD}") 304 | 305 | 306 | ########################################################################################## 307 | # METHOD IMPLEMENTATION 308 | ########################################################################################## 309 | class _SmaliMethodWriter(MethodVisitor, _ContainsCodeCache): 310 | cache: _CodeCache 311 | 312 | def __init__(self, delegate: Optional[MethodVisitor] = None, indent=0) -> None: 313 | super().__init__(delegate) 314 | self.cache = _CodeCache(indent) 315 | 316 | def visit_annotation(self, access_flags: int, signature: str) -> AnnotationVisitor: 317 | delegate = super().visit_annotation(access_flags, signature) 318 | desc = f".{Token.ANNOTATION} {' '.join(AccessType.get_names(access_flags))} {signature}" 319 | if isinstance(delegate, _SmaliAnnotationWriter): 320 | a_visitor = delegate 321 | a_visitor.cache.indent = self.cache.indent + 1 322 | else: 323 | a_visitor = _SmaliAnnotationWriter(delegate, self.cache.indent + 1) 324 | 325 | a_visitor.cache.add(desc) 326 | self.cache.add_to_cache(a_visitor) 327 | return a_visitor 328 | 329 | def visit_block(self, name: str) -> None: 330 | self.cache.apply_code_cache(True) 331 | super().visit_block(name) 332 | self.cache.add(f":{name}", custom_indent=self.cache.indent + 1) 333 | 334 | def visit_line(self, number: int) -> None: 335 | self.cache.apply_code_cache(True) 336 | super().visit_line(number) 337 | self.cache.add(f".{Token.LINE} {number}", custom_indent=self.cache.indent + 1) 338 | 339 | def visit_goto(self, block_name: str) -> None: 340 | self.cache.apply_code_cache(True) 341 | super().visit_goto(block_name) 342 | self.cache.add( 343 | f"{opcode.GOTO} :{block_name}", custom_indent=self.cache.indent + 1 344 | ) 345 | 346 | def visit_instruction(self, ins_name: str, args: list) -> None: 347 | self.cache.apply_code_cache(True) 348 | super().visit_instruction(ins_name, args) 349 | self.cache.add( 350 | f"{ins_name} {', '.join(args)}", 351 | custom_indent=self.cache.indent + 1, 352 | end="\n", 353 | ) 354 | 355 | def visit_param(self, register: str, name: str) -> None: 356 | self.cache.apply_code_cache(True) 357 | super().visit_param(register, name) 358 | self.cache.add( 359 | f'.{Token.PARAM} {register} "{name}"', custom_indent=self.cache.indent + 1 360 | ) 361 | 362 | def visit_comment(self, text: str) -> None: 363 | super().visit_comment(text) 364 | self.cache.add(f"# {text}", custom_indent=self.cache.indent + 1) 365 | 366 | def visit_restart(self, register: str) -> None: 367 | self.cache.apply_code_cache(True) 368 | super().visit_restart(register) 369 | self.cache.add( 370 | f".{Token.RESTART} {register}", custom_indent=self.cache.indent + 1 371 | ) 372 | 373 | def visit_locals(self, local_count: int) -> None: 374 | self.cache.apply_code_cache(True) 375 | super().visit_locals(local_count) 376 | self.cache.add( 377 | f".{Token.LOCALS} {local_count}", custom_indent=self.cache.indent + 1 378 | ) 379 | 380 | def visit_local( 381 | self, register: str, name: str, descriptor: str, full_descriptor: str 382 | ) -> None: 383 | self.cache.apply_code_cache(True) 384 | super().visit_local(register, name, descriptor, full_descriptor) 385 | self.cache.add( 386 | f'.{Token.LOCAL} {register}, "{name}":{descriptor}, "{full_descriptor}"', 387 | custom_indent=self.cache.indent + 1, 388 | ) 389 | 390 | def visit_prologue(self) -> None: 391 | self.cache.apply_code_cache(True) 392 | super().visit_prologue() 393 | self.cache.add(f".{Token.PROLOGUE}", custom_indent=self.cache.indent + 1) 394 | 395 | def visit_catch(self, exc_name: str, blocks: tuple) -> None: 396 | self.cache.apply_code_cache(True) 397 | super().visit_catch(exc_name, blocks) 398 | start, end, catch = blocks 399 | self.cache.add( 400 | ".%s %s { :%s .. :%s } :%s" % (Token.CATCH, exc_name, start, end, catch), 401 | custom_indent=self.cache.indent + 1, 402 | ) 403 | 404 | def visit_catchall(self, exc_name: str, blocks: tuple) -> None: 405 | self.cache.apply_code_cache(True) 406 | super().visit_catchall(exc_name, blocks) 407 | start, end, catch = blocks 408 | self.cache.add( 409 | ".%s { :%s .. :%s } :%s" % (Token.CATCHALL, start, end, catch), 410 | custom_indent=self.cache.indent + 1, 411 | end="\n", 412 | ) 413 | 414 | def visit_registers(self, registers: int) -> None: 415 | self.cache.apply_code_cache(True) 416 | super().visit_registers(registers) 417 | self.cache.add( 418 | f".{Token.REGISTERS} {registers}", 419 | custom_indent=self.cache.indent + 1, 420 | end="\n", 421 | ) 422 | 423 | def visit_return(self, ret_type: str, args: list) -> None: 424 | self.cache.apply_code_cache(True) 425 | super().visit_return(ret_type, args) 426 | if ret_type: 427 | ret_type = f"-{ret_type}" 428 | 429 | self.cache.add( 430 | f"return{ret_type} {' '.join(args)}", custom_indent=self.cache.indent + 1 431 | ) 432 | 433 | def visit_invoke(self, inv_type: str, args: list, owner: str, method: str) -> None: 434 | self.cache.apply_code_cache(True) 435 | super().visit_invoke(inv_type, args, owner, method) 436 | self.cache.add( 437 | "invoke-%s { %s }, %s->%s" % (inv_type, ", ".join(args), owner, method), 438 | custom_indent=self.cache.indent + 1, 439 | end="\n", 440 | ) 441 | 442 | def visit_array_data(self, length: str, value_list: List[int]) -> None: 443 | self.cache.apply_code_cache(True) 444 | super().visit_array_data(length, value_list) 445 | indent_value = self.cache.default_indent * (self.cache.indent + 2) 446 | sep_value = "\n" + indent_value 447 | value_list = map(hex, value_list) 448 | self.cache.add( 449 | f".{Token.ARRAYDATA} {length}\n{indent_value}{sep_value.join(value_list)}\n.{Token.END} {Token.ARRAYDATA}", 450 | end="\n", 451 | ) 452 | 453 | def visit_packed_switch(self, value: str, blocks: list) -> None: 454 | self.cache.apply_code_cache(True) 455 | super().visit_packed_switch(value, blocks) 456 | indent_value = self.cache.default_indent * (self.cache.indent + 2) 457 | sep_value = "\n" + indent_value + ":" 458 | self.cache.add( 459 | f".{Token.PACKEDSWITCH} {value}\n{indent_value}:{sep_value.join(blocks)}\n.{Token.END} {Token.PACKEDSWITCH}", 460 | end="\n", 461 | ) 462 | 463 | def visit_sparse_switch(self, branches: dict) -> None: 464 | self.cache.apply_code_cache(True) 465 | super().visit_sparse_switch(branches) 466 | indent_value = self.cache.default_indent * (self.cache.indent + 2) 467 | values = [f"{x} -> :{y}" for x, y in branches.items()] 468 | sep_value = "\n" + indent_value 469 | self.cache.add( 470 | f".{Token.SPARSESWITCH}\n{indent_value}{sep_value.join(values)}\n.{Token.END} {Token.SPARSESWITCH}" 471 | ) 472 | 473 | def visit_eol_comment(self, text: str) -> None: 474 | super().visit_eol_comment(text) 475 | self.cache.add_comment(text) 476 | 477 | def visit_end(self) -> None: 478 | self.cache.apply_code_cache(True) 479 | super().visit_end() 480 | self.cache.add(f".{Token.END} {Token.METHOD}") 481 | 482 | def get_cache(self) -> "_CodeCache": 483 | return self.cache 484 | 485 | 486 | ########################################################################################## 487 | # CLASS IMPLEMENTATION 488 | ########################################################################################## 489 | class _SmaliClassWriter(ClassVisitor, _ContainsCodeCache): 490 | """Public standard implementation of a Smali Source-Code writer.""" 491 | 492 | cache: _CodeCache 493 | """The code cache to use.""" 494 | 495 | def __init__( 496 | self, 497 | reader: Optional[SmaliReader] = None, 498 | indent=0, 499 | delegate: Optional[ClassVisitor] = None, 500 | ) -> None: 501 | super().__init__(delegate) 502 | self.cache = _CodeCache(indent) 503 | if reader: 504 | reader.copy_handler = self 505 | self._reader = reader 506 | 507 | def __str__(self) -> str: 508 | return self.code 509 | 510 | @property 511 | def code(self) -> str: 512 | """Returns the source code as an utf-8 string 513 | 514 | :return: the complete source code 515 | :rtype: str 516 | """ 517 | return self.cache.get_code(clear_cache=True) 518 | 519 | def reset(self) -> None: 520 | """Resets the writer. 521 | 522 | Use this method before calling #visit() to ensure the internal 523 | buffer is cleared. 524 | """ 525 | self.cache.clear() 526 | 527 | def get_cache(self) -> "_CodeCache": 528 | return self.cache 529 | 530 | ###################################################################################### 531 | # INTERNAL 532 | ###################################################################################### 533 | def visit_class(self, name: str, access_flags: int) -> None: 534 | super().visit_class(name, access_flags) 535 | flags = " ".join(AccessType.get_names(access_flags)) 536 | self.cache.add(f".{Token.CLASS} {flags} {name}") 537 | 538 | def visit_super(self, super_class: str) -> None: 539 | super().visit_super(super_class) 540 | self.cache.add(f".{Token.SUPER} {super_class}\n") 541 | 542 | def visit_implements(self, interface: str) -> None: 543 | super().visit_implements(interface) 544 | self.cache.add(f".{Token.IMPLEMENTS} {interface}") 545 | 546 | def visit_source(self, source: str) -> None: 547 | super().visit_source(source) 548 | self.cache.add(f'.{Token.SOURCE} "{source}"\n') 549 | 550 | def visit_field( 551 | self, name: str, access_flags: int, field_type: str, value=None 552 | ) -> Optional[FieldVisitor]: 553 | flags = " ".join(AccessType.get_names(access_flags)) 554 | desc = f".{Token.FIELD} {flags} {name}:{field_type}" 555 | if value: 556 | # String values come with their '"' characters 557 | desc = f"{desc} = {value}" 558 | 559 | delegate = super().visit_field(name, access_flags, field_type, value) 560 | if isinstance(delegate, _SmaliFieldWriter): 561 | f_visitor = delegate 562 | else: 563 | f_visitor = _SmaliFieldWriter(delegate, self.cache.indent) 564 | f_visitor.cache.add(desc) 565 | self.cache.add_to_cache(f_visitor) 566 | return f_visitor 567 | 568 | def visit_annotation( 569 | self, access_flags: int, signature: str 570 | ) -> Optional[AnnotationVisitor]: 571 | delegate = super().visit_annotation(access_flags, signature) 572 | flags = " ".join(AccessType.get_names(access_flags)) 573 | desc = f".{Token.ANNOTATION} {flags} {signature}" 574 | if isinstance(delegate, _SmaliAnnotationWriter): 575 | a_visitor = delegate 576 | a_visitor.cache.indent = self.cache.indent 577 | else: 578 | a_visitor = _SmaliAnnotationWriter(delegate, self.cache.indent) 579 | 580 | a_visitor.cache.add(desc) 581 | self.cache.add_to_cache(a_visitor) 582 | return a_visitor 583 | 584 | def visit_inner_class(self, name: str, access_flags: int) -> Optional[ClassVisitor]: 585 | delegate = super().visit_inner_class(name, access_flags) 586 | flags = " ".join(AccessType.get_names(access_flags)) 587 | desc = f".{Token.CLASS} {flags} {name}" 588 | 589 | if isinstance(delegate, _SmaliClassWriter): 590 | c_visitor = delegate 591 | else: 592 | c_visitor = _SmaliClassWriter(self._reader, delegate=delegate) 593 | c_visitor.cache.add(desc) 594 | self.cache.add_to_cache(c_visitor) 595 | return c_visitor 596 | 597 | def visit_method( 598 | self, name: str, access_flags: int, parameters: list, return_type: str 599 | ) -> Optional[MethodVisitor]: 600 | delegate = super().visit_method(name, access_flags, parameters, return_type) 601 | flags = " ".join(AccessType.get_names(access_flags)) 602 | params = "".join(parameters) 603 | desc = f".{Token.METHOD} {flags} {name}({params}){return_type}" 604 | if isinstance(delegate, _SmaliMethodWriter): 605 | m_visitor = delegate 606 | m_visitor.cache.indent = self.cache.indent 607 | else: 608 | m_visitor = _SmaliMethodWriter(delegate, self.cache.indent) 609 | 610 | m_visitor.cache.add(desc) 611 | self.cache.add_to_cache(m_visitor) 612 | return m_visitor 613 | 614 | def visit_comment(self, text: str) -> None: 615 | self.cache.apply_code_cache(True) 616 | super().visit_comment(text) 617 | self.cache.add(f"# {text}") 618 | 619 | def visit_eol_comment(self, text: str) -> None: 620 | super().visit_eol_comment(text) 621 | self.cache.add_comment(text) 622 | 623 | def visit_debug(self, enabled: int) -> None: 624 | super().visit_debug(enabled) 625 | self.cache.add(f".{Token.DEBUG} {enabled}") 626 | 627 | def visit_end(self) -> None: 628 | self.cache.apply_code_cache(True) 629 | super().visit_end() 630 | 631 | def copy(self, line: str, context: type = ClassVisitor) -> None: 632 | if context == ClassVisitor: 633 | self.cache.add(line) 634 | 635 | else: 636 | last_writer = self.cache.peek() 637 | if not last_writer: 638 | self.cache.add(line) 639 | 640 | elif isinstance(last_writer, SupportsCopy): 641 | last_writer.copy(line, context) 642 | 643 | elif isinstance(last_writer, (context, _ContainsCodeCache)): 644 | last_writer.get_cache().add(line) 645 | 646 | else: 647 | print("Line excluded:", line, " =", context) 648 | 649 | 650 | ########################################################################################## 651 | # EXPORTS 652 | ########################################################################################## 653 | SmaliWriter = _SmaliClassWriter 654 | FieldWriter = _SmaliFieldWriter 655 | MethodWriter = _SmaliMethodWriter 656 | AnnotationWriter = _SmaliAnnotationWriter 657 | --------------------------------------------------------------------------------