├── doc └── jupyter_example.png ├── ghidra_jython_kernel ├── __init__.py ├── __main__.py ├── kernel.py └── repl.py ├── tests └── repl_test.py ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md └── setup.py /doc/jupyter_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllsafeCyberSecurity/ghidra-jython-kernel/HEAD/doc/jupyter_example.png -------------------------------------------------------------------------------- /ghidra_jython_kernel/__init__.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import get_distribution 2 | 3 | __version__ = get_distribution('ghidra_jython_kernel').version 4 | 5 | from .kernel import * 6 | from .repl import * 7 | -------------------------------------------------------------------------------- /ghidra_jython_kernel/__main__.py: -------------------------------------------------------------------------------- 1 | from ghidra_jython_kernel import GhidraJythonKernel 2 | 3 | if __name__ == '__main__': 4 | from ipykernel.kernelapp import IPKernelApp 5 | IPKernelApp.launch_instance(kernel_class=GhidraJythonKernel) -------------------------------------------------------------------------------- /tests/repl_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from ghidra_jython_kernel.repl import GhidraJythonRepl 3 | 4 | TEST_CASES = { 5 | # test case 1 6 | '1 + 1': '2', 7 | 8 | # test case 3 9 | 'print("tesuya")': 'tesuya', 10 | 11 | # test case 3 (nested) 12 | ''' 13 | for i in range(10): 14 | if i % 2 == 0: 15 | print(i, 'foo') 16 | else: 17 | print(i, 'bar') 18 | ''': "(0, 'foo')\n(1, 'bar')\n(2, 'foo')\n(3, 'bar')\n(4, 'foo')\n(5, 'bar')\n(6, 'foo')\n(7, 'bar')\n(8, 'foo')\n(9, 'bar')", 19 | 20 | # test case 4 (define function) 21 | ''' 22 | def echo(what): 23 | print(what) 24 | 25 | echo("tesuya") 26 | ''' : 'tesuya' 27 | 28 | } 29 | 30 | 31 | class TestGhidraJythonRepl(unittest.TestCase): 32 | def setUp(self): 33 | self.jython = GhidraJythonRepl() 34 | 35 | def test_repl(self): 36 | for test, expect in TEST_CASES.items(): 37 | ret = self.jython.repl(test) 38 | self.assertEqual(ret, expect) 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ghidra-jython-kernel test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: install kernel and test REPL 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions/setup-python@v1 11 | with: 12 | python-version: '3.x' 13 | architecture: 'x64' 14 | 15 | - name: install python module 16 | run: | 17 | python -m pip install --upgrade pip 18 | python setup.py install 19 | pip install -e . 20 | pip install pytest 21 | 22 | - if: success() # if install was success, process TEST 23 | name: java setup 24 | uses: actions/setup-java@v1 # use Java 11 25 | with: 26 | java-version: '11.0.x' 27 | java-package: jdk 28 | architecture: x64 29 | 30 | - name: install ghidra 31 | uses: er28-0652/setup-ghidra@master 32 | with: 33 | version: '9.1.1' 34 | 35 | - if: success() 36 | name: run test 37 | run: 38 | pytest -v tests 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Allsafe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ghidra_jython_kernel/kernel.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import subprocess 4 | from pathlib import Path 5 | from pexpect import spawn 6 | from ipykernel.kernelbase import Kernel 7 | 8 | from .__init__ import __version__ 9 | from ghidra_jython_kernel.repl import GhidraJythonRepl 10 | 11 | 12 | class GhidraJythonKernel(Kernel): 13 | implementation = 'Ghidra\'s Jython Kernel' 14 | implementation_version = __version__ 15 | language = 'jython' 16 | language_version = '2.7.0' 17 | language_info = { 18 | 'mimetype': 'text/x-python', 19 | 'name': 'jython', 20 | 'file_extension': '.py', 21 | 'codemirror_mode':{ 22 | 'version': 2, 23 | 'name': 'ipython' 24 | }, 25 | 'nbconvert_exporter': 'python', 26 | 'pygments_lexer':'ipython2', 27 | } 28 | banner = "GhidraJython Kernel" 29 | 30 | def __init__(self, **kwargs): 31 | Kernel.__init__(self, **kwargs) 32 | 33 | self.jython = None 34 | self._init_jython() 35 | 36 | def _init_jython(self): 37 | ''' Initialize Ghidra's Jython interpreter. ''' 38 | 39 | sig = signal.signal(signal.SIGINT, signal.SIG_DFL) 40 | try: 41 | self.jython = GhidraJythonRepl() 42 | finally: 43 | signal.signal(signal.SIGINT, sig) 44 | 45 | def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): 46 | if not silent: 47 | stream_content = {'name': 'stdout', 'text': self.jython.repl(code)} 48 | self.send_response(self.iopub_socket, 'stream', stream_content) 49 | 50 | return { 51 | 'status': 'ok', 52 | 'execution_count': self.execution_count, 53 | 'payload': [], 54 | 'user_expressions': {} 55 | } 56 | 57 | def do_shutdown(self, restart): 58 | self.jython.kill() 59 | return { 60 | 'status':'ok', 61 | 'restart': restart 62 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/AllsafeCyberSecurity/ghidra-jython-kernel/workflows/ghidra-jython-kernel%20test/badge.svg)](https://github.com/AllsafeCyberSecurity/ghidra-jython-kernel/actions) 2 | 3 | # Ghidra Jython Kernel 4 | 5 | Jupyter kernel for Ghidra's Jython. 6 | 7 | 8 | 9 | ## Install 10 | ```bash 11 | $ pip install ghidra-jython-kernel 12 | 13 | # make sure GHIDRA_INSTALL_DIR is defined 14 | # don't forget to add this line in your shell config (i.e. bashrc, zshrc) 15 | $ export GHIDRA_INSTALL_DIR=/path/to/your/ghidra_installation_folder 16 | ``` 17 | 18 | ## Usage 19 | 20 | Run your Jupyter(`jupyter notebook`), and select `GhidraJython` named kernel. 21 | 22 | 23 | Note that, unlike GhidraPython plugin's interpreter, in the context the current running Jython interpreter, you have to import program by yourself. This means, pre-initialized variables in GhidraScript, like `currentProgram` or `state`, aren't available unless you import manually. You can import programs as following. 24 | 25 | ```python 26 | from ghidra.app.util.importer import MessageLog, AutoImporter 27 | from ghidra.program.flatapi import FlatProgramAPI 28 | from ghidra.util.task import TaskMonitor 29 | from ghidra.util import Msg 30 | from java.io import File 31 | from java.util.function import Consumer 32 | 33 | class Cons(Consumer): 34 | def __init__(self, fn): 35 | self.accept = fn 36 | 37 | def import_program(filepath, enable_analysis=True): 38 | program = AutoImporter.importByUsingBestGuess( 39 | File(filepath), 40 | None, 41 | Cons(lambda x: Msg.error(None, err)), 42 | MessageLog(), 43 | TaskMonitor.DUMMY 44 | ) 45 | 46 | flatapi = FlatProgramAPI(program) 47 | 48 | if enable_analysis: 49 | flatapi.start() 50 | flatapi.analyze(flatapi.currentProgram) 51 | 52 | return flatapi 53 | 54 | ghidra_app = import_program('/path/to/your/program') 55 | 56 | # now you can access to `currentProgram` 57 | ghidra_app.currentProgram 58 | ``` 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import json 3 | import os 4 | import re 5 | import shutil 6 | import sys 7 | from pathlib import Path 8 | from distutils.command.install import install 9 | from tempfile import mkdtemp, TemporaryDirectory 10 | 11 | try: 12 | from setuptools import setup, find_packages 13 | except ImportError: 14 | from distutils.core import setup, find_packages 15 | 16 | MYPACKAGE_ROOT = 'ghidra_jython_kernel' 17 | 18 | # kernelspec info 19 | KERNELSPEC_JSON = { 20 | 'argv': [ 21 | sys.executable, 22 | '-m', 'ghidra_jython_kernel', 23 | '-f', '{connection_file}' 24 | ], 25 | 'display_name': 'GhidraJython', 26 | 'language': 'python', 27 | 'name': 'ghidra_jython_kernel' 28 | } 29 | 30 | def get_home_dir(): 31 | homedir = os.path.expanduser('~') 32 | homedir = os.path.realpath(homedir) 33 | return homedir 34 | 35 | def jupyter_config_dir(): 36 | env = os.environ 37 | home_dir = get_home_dir() 38 | 39 | if env.get('JUPYTER_NO_CONFIG'): 40 | return Path(mkdtemp(prefix='jupyter-clean-cfg' + '-')) 41 | 42 | if env.get('JUPYTER_CONFIG_DIR'): 43 | return Path(env['JUPYTER_CONFIG_DIR']) 44 | 45 | return Path(home_dir, '.jupyter') 46 | 47 | def get_jupyter_data_dir(): 48 | env = os.environ 49 | 50 | if env.get('JUPYTER_DATA_DIR'): 51 | return Path(env['JUPYTER_DATA_DIR']) 52 | 53 | home = get_home_dir() 54 | 55 | if sys.platform == 'darwin': 56 | return Path(home, 'Library', 'Jupyter') 57 | elif os.name == 'nt': 58 | appdata = env.get('APPDATA', None) 59 | if appdata: 60 | return Path(appdata, 'jupyter') 61 | else: 62 | return Path(jupyter_config_dir(), 'data') 63 | else: 64 | # Linux, non-OS X Unix, AIX, etc. 65 | xdg = env.get("XDG_DATA_HOME", None) 66 | if not xdg: 67 | xdg = Path(home, '.local', 'share') 68 | return Path(xdg, 'jupyter') 69 | 70 | def install_kernelspec(source_dir, kernel_name): 71 | source_dir = source_dir.rstrip('/\\') 72 | if not kernel_name: 73 | kernel_name = os.path.basename(source_dir) 74 | kernel_name = kernel_name.lower() 75 | 76 | destination = Path(get_jupyter_data_dir() / 'kernels', kernel_name) 77 | 78 | if destination.is_dir(): 79 | shutil.rmtree(str(destination)) 80 | 81 | shutil.copytree(source_dir, str(destination)) 82 | 83 | 84 | class install_with_kernelspec(install): 85 | def run(self): 86 | install.run(self) 87 | with TemporaryDirectory() as td: 88 | with Path(td, 'kernel.json').open('w') as f: 89 | json.dump(KERNELSPEC_JSON, f, sort_keys=True) 90 | 91 | kernel_name = KERNELSPEC_JSON['name'] 92 | install_kernelspec(td, kernel_name) 93 | 94 | 95 | def main(): 96 | setup( 97 | name='ghidra-jython-kernel', 98 | version='0.0.4', 99 | description='Jupyter kernel for Ghidra\'s Jython Interpreter', 100 | author='er28-0652', 101 | author_email='33626923+er28-0652@users.noreply.github.com', 102 | license='MIT', 103 | cmdclass={'install': install_with_kernelspec}, 104 | install_requires=[ 105 | 'IPython', 106 | 'ipykernel', 107 | 'jupyter_client', 108 | 'pexpect' 109 | ], 110 | packages=find_packages(), 111 | test_suite='tests' 112 | ) 113 | 114 | if __name__ == '__main__': 115 | main() 116 | -------------------------------------------------------------------------------- /ghidra_jython_kernel/repl.py: -------------------------------------------------------------------------------- 1 | import signal 2 | import subprocess 3 | import os 4 | from pathlib import Path 5 | from pexpect import spawn 6 | 7 | 8 | def execute(cmd): 9 | ''' run any command and get stdout result as utf-8 string. ''' 10 | 11 | # execute command 12 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 13 | stdout, stderr = p.communicate() 14 | 15 | # check status code is ok 16 | # if it's not, will raise RuntimeError exception 17 | if p.returncode != 0: 18 | raise RuntimeError('"{0}" run fails, err={1}'.format( 19 | cmd, stderr.decode('utf-8', errors='replace'))) 20 | 21 | # return stdout utf-8 string 22 | return stdout.decode('utf-8').replace('\r\n', '').replace('\n', '') 23 | 24 | 25 | class GhidraJythonRepl: 26 | def __init__(self, ghidra_home=None): 27 | 28 | # those paths come from "$GHIDRA_INSTALL_DIR/support/launch.sh" 29 | # User must define "GHIDRA_INSTALL_DIR" for Ghidra's installation directory 30 | # i.e. GHIDRA_INSTALL_DIR=/path/to/ghidra_9.1_PUBLIC 31 | self.INSTALL_DIR = Path(ghidra_home or os.environ['GHIDRA_INSTALL_DIR']) 32 | 33 | self._java_home = None 34 | self._java_vmargs = None 35 | 36 | # build pythonRun commandline 37 | run_cmd = '{java_home}/bin/java {java_vmargs} -showversion -cp "{utility_jar}" \ 38 | ghidra.GhidraLauncher "ghidra.python.PythonRun"'.format( 39 | java_home=self.java_home, 40 | java_vmargs=self.java_vmargs, 41 | utility_jar=self.INSTALL_DIR / 'Ghidra/Framework/Utility/lib/Utility.jar' 42 | ) 43 | 44 | # spawn Ghidra's Jython Interpreter (ghidra.python.PythonRun) 45 | # this is exactly same as running "pythonRun" script 46 | self.child = spawn(run_cmd, echo=False, encoding='utf-8') 47 | 48 | self.prompt1 = r'>>> ' 49 | self.prompt2 = r'... ' 50 | 51 | # wait for first prompt 52 | self.child.expect(self.prompt1) 53 | 54 | @property 55 | def java_home(self): 56 | if self._java_home is None: 57 | self._java_home = execute('java -cp "{0}" LaunchSupport "{1}" -jdk_home -save'.format( 58 | self.INSTALL_DIR / 'support/LaunchSupport.jar', self.INSTALL_DIR)) 59 | return self._java_home 60 | 61 | @property 62 | def java_vmargs(self): 63 | if self._java_vmargs is None: 64 | self._java_vmargs = execute('java -cp "{0}" LaunchSupport "{1}" -vmargs'.format( 65 | self.INSTALL_DIR / 'support/LaunchSupport.jar', self.INSTALL_DIR)) 66 | return self._java_vmargs 67 | 68 | def read_output(self): 69 | ''' Read current output. ''' 70 | 71 | result = '' 72 | 73 | # read output, expect echo content 74 | if self.child.before.splitlines()[1:]: 75 | out = self.child.before.splitlines()[1:] 76 | result += '\n'.join([line for line in out if line]) 77 | 78 | return result 79 | 80 | def _repl(self, code): 81 | self.child.sendline(code) 82 | 83 | # idk why tho, Ghidra's jython interpreter should wait twice 84 | self.child.expect_exact([self.prompt1, self.prompt2]) 85 | self.child.expect_exact([self.prompt1, self.prompt2]) 86 | 87 | return self.read_output() 88 | 89 | def repl(self, code): 90 | ''' Ghidra's Jython Interpreter REPL function. ''' 91 | 92 | code_lines = code.splitlines() 93 | 94 | # if code has new line, should send ENTER('') at last 95 | if '\n' in code: 96 | code_lines.append('') 97 | 98 | result = '' 99 | 100 | # REPL each line of code 101 | for c in code_lines: 102 | result += self._repl(c) 103 | 104 | return result 105 | 106 | def kill(self): 107 | self.child.kill(signal.SIGKILL) --------------------------------------------------------------------------------