diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/Module.manifest b/Ghidra/Debug/Debugger-agent-x64dbg/Module.manifest
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/Module.manifest
@@ -0,0 +1 @@
+
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/README.md b/Ghidra/Debug/Debugger-agent-x64dbg/README.md
new file mode 100644
index 0000000000..5497d72c08
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/README.md
@@ -0,0 +1 @@
+# Debugger-agent-x64dbg
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/build.gradle b/Ghidra/Debug/Debugger-agent-x64dbg/build.gradle
new file mode 100644
index 0000000000..8385fbd691
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/build.gradle
@@ -0,0 +1,33 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Not technically a Java project, but required to be a Help project
+apply from: "${rootProject.projectDir}/gradle/javaProject.gradle"
+apply from: "${rootProject.projectDir}/gradle/helpProject.gradle"
+apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle"
+apply from: "$rootProject.projectDir/gradle/nativeProject.gradle"
+apply from: "$rootProject.projectDir/gradle/hasPythonPackage.gradle"
+
+apply plugin: 'eclipse'
+eclipse.project.name = 'Debug Debugger-agent-x64dbg'
+
+dependencies {
+ // Only for Help :/
+ api project(':Debugger-rmi-trace')
+}
+
+tasks.assemblePyPackage {
+}
+
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/certification.manifest b/Ghidra/Debug/Debugger-agent-x64dbg/certification.manifest
new file mode 100644
index 0000000000..49f3984ab1
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/certification.manifest
@@ -0,0 +1,13 @@
+##VERSION: 2.0
+##MODULE IP: Apache License 2.0
+##MODULE IP: MIT
+Module.manifest||GHIDRA||||END|
+README.md||GHIDRA||||END|
+src/main/help/help/TOC_Source.xml||GHIDRA||||END|
+src/main/help/help/topics/x64dbg/x64dbg.html||GHIDRA||||END|
+src/main/py/LICENSE||GHIDRA||||END|
+src/main/py/MANIFEST.in||GHIDRA||||END|
+src/main/py/README.md||GHIDRA||||END|
+src/main/py/pyproject.toml||GHIDRA||||END|
+src/main/py/src/ghidraxdbg/py.typed||GHIDRA||||END|
+src/main/py/src/ghidraxdbg/schema.xml||GHIDRA||||END|
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/data/debugger-launchers/local-x64dbg-attach.bat b/Ghidra/Debug/Debugger-agent-x64dbg/data/debugger-launchers/local-x64dbg-attach.bat
new file mode 100644
index 0000000000..25c2531572
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/data/debugger-launchers/local-x64dbg-attach.bat
@@ -0,0 +1,34 @@
+:: ###
+:: IP: GHIDRA
+::
+:: Licensed under the Apache License, Version 2.0 (the "License");
+:: you may not use this file except in compliance with the License.
+:: You may obtain a copy of the License at
+::
+:: http://www.apache.org/licenses/LICENSE-2.0
+::
+:: Unless required by applicable law or agreed to in writing, software
+:: distributed under the License is distributed on an "AS IS" BASIS,
+:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+:: See the License for the specific language governing permissions and
+:: limitations under the License.
+:: ##
+::@title x64dbg attach
+::@desc
+::@desc
Attach with x64dbg (in a Python interpreter)
+::@desc
+::@desc This will attach to a running target on the local machine using x64dbg.dll.
+::@desc For setup instructions, press F1.
+::@desc
+::@desc
+::@menu-group x64dbg
+::@icon icon.debugger
+::@help x64dbg#attach
+::@depends Debugger-rmi-trace
+::@env OPT_PYTHON_EXE:file!="python" "Python command" "The path to the Python 3 interpreter. Omit the full path to resolve using the system PATH."
+::@env OPT_TARGET_PID:int=0 "Process id" "The target process id"
+::@env OPT_X64DBG_EXE:file="C:\\Software\\release\\x64\\x64dbg.exe" "Path to x64dbg.exe" "Path to x64dbg.exe (or equivalent)."
+
+@echo off
+
+"%OPT_PYTHON_EXE%" -i ..\support\local-x64dbg-attach.py
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/data/debugger-launchers/local-x64dbg.bat b/Ghidra/Debug/Debugger-agent-x64dbg/data/debugger-launchers/local-x64dbg.bat
new file mode 100644
index 0000000000..73d5c1e8fd
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/data/debugger-launchers/local-x64dbg.bat
@@ -0,0 +1,38 @@
+:: ###
+:: IP: GHIDRA
+::
+:: Licensed under the Apache License, Version 2.0 (the "License");
+:: you may not use this file except in compliance with the License.
+:: You may obtain a copy of the License at
+::
+:: http://www.apache.org/licenses/LICENSE-2.0
+::
+:: Unless required by applicable law or agreed to in writing, software
+:: distributed under the License is distributed on an "AS IS" BASIS,
+:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+:: See the License for the specific language governing permissions and
+:: limitations under the License.
+:: ##
+::@title x64dbg
+::@image-opt env:OPT_TARGET_IMG
+::@desc
+::@desc
Launch with x64dbg (in a Python interpreter)
+::@desc
+::@desc This will launch the target on the local machine using x64dbg.dll.
+::@desc For setup instructions, press F1.
+::@desc
+::@desc
+::@menu-group x64dbg
+::@icon icon.debugger
+::@help x64dbg#local
+::@depends Debugger-rmi-trace
+::@env OPT_PYTHON_EXE:file!="python" "Python command" "The path to the Python 3 interpreter. Omit the full path to resolve using the system PATH."
+:: Use env instead of args, because "all args except first" is terrible to implement in batch
+::@env OPT_TARGET_IMG:file="" "Image" "The target binary executable image"
+::@env OPT_TARGET_ARGS:str="" "Arguments" "Command-line arguments to pass to the target"
+::@env OPT_TARGET_DIR:str="" "Dir" "Initial directory"
+::@env OPT_X64DBG_EXE:file="C:\\Software\\release\\x64\\x64dbg.exe" "Path to x64dbg.exe" "Path to x64dbg.exe (or equivalent)."
+
+@echo off
+
+"%OPT_PYTHON_EXE%" -i ..\support\local-x64dbg.py
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/data/support/local-x64dbg-attach.py b/Ghidra/Debug/Debugger-agent-x64dbg/data/support/local-x64dbg-attach.py
new file mode 100644
index 0000000000..5885d3e8b6
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/data/support/local-x64dbg-attach.py
@@ -0,0 +1,62 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import os
+import sys
+
+
+def append_paths():
+ sys.path.append(
+ f"{os.getenv('MODULE_Debugger_rmi_trace_HOME')}/data/support")
+ from gmodutils import ghidra_module_pypath
+ sys.path.append(ghidra_module_pypath("Debugger-rmi-trace"))
+ sys.path.append(ghidra_module_pypath())
+
+
+def main():
+ append_paths()
+ # Delay these imports until sys.path is patched
+ from ghidraxdbg import commands as cmd
+ from ghidraxdbg.hooks import on_state_changed
+ from ghidraxdbg.util import dbg
+
+ # So that the user can re-enter by typing repl()
+ global repl
+ repl = cmd.repl
+
+ cmd.ghidra_trace_connect(os.getenv('GHIDRA_TRACE_RMI_ADDR'))
+ cmd.ghidra_trace_attach(os.getenv('OPT_TARGET_PID'), start_trace=False)
+
+ try:
+ dbg.wait()
+ except KeyboardInterrupt as ki:
+ dbg.interrupt()
+
+ cmd.ghidra_trace_start(os.getenv('OPT_TARGET_PID'))
+ cmd.ghidra_trace_sync_enable()
+
+ cmd.ghidra_trace_txstart()
+ cmd.ghidra_trace_put_all()
+
+ cmd.repl()
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except SystemExit as x:
+ if x.code != 0:
+ print(f"Exited with code {x.code}")
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/data/support/local-x64dbg.py b/Ghidra/Debug/Debugger-agent-x64dbg/data/support/local-x64dbg.py
new file mode 100644
index 0000000000..c93ecfc8c5
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/data/support/local-x64dbg.py
@@ -0,0 +1,69 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import os
+import sys
+
+
+def append_paths():
+ sys.path.append(
+ f"{os.getenv('MODULE_Debugger_rmi_trace_HOME')}/data/support")
+ from gmodutils import ghidra_module_pypath
+ sys.path.append(ghidra_module_pypath("Debugger-rmi-trace"))
+ sys.path.append(ghidra_module_pypath())
+
+
+def main():
+ append_paths()
+ # Delay these imports until sys.path is patched
+ from ghidraxdbg import commands as cmd
+ from ghidraxdbg.hooks import on_state_changed
+ from ghidraxdbg.util import dbg
+
+ # So that the user can re-enter by typing repl()
+ global repl
+ repl = cmd.repl
+
+ cmd.ghidra_trace_connect(os.getenv('GHIDRA_TRACE_RMI_ADDR'))
+ args = os.getenv('OPT_TARGET_ARGS')
+ initdir = os.getenv('OPT_TARGET_DIR')
+ target = os.getenv('OPT_TARGET_IMG')
+
+ cmd.ghidra_trace_create(target, args=args, initdir=initdir, start_trace=False)
+
+ try:
+ dbg.wait()
+ except KeyboardInterrupt as ki:
+ dbg.interrupt()
+
+ cmd.ghidra_trace_start(target)
+ cmd.ghidra_trace_sync_enable()
+
+ cmd.ghidra_trace_txstart()
+ if target is None or target == "":
+ cmd.ghidra_trace_put_available()
+ else:
+ cmd.ghidra_trace_put_all()
+
+ cmd.repl()
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except SystemExit as x:
+ if x.code != 0:
+ print(f"Exited with code {x.code}")
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/help/help/TOC_Source.xml b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/help/help/TOC_Source.xml
new file mode 100644
index 0000000000..72360c1bea
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/help/help/TOC_Source.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/help/help/topics/x64dbg/x64dbg.html b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/help/help/topics/x64dbg/x64dbg.html
new file mode 100644
index 0000000000..bcaa0eceff
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/help/help/topics/x64dbg/x64dbg.html
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+ Debugger Launchers: x64dbg Debugger
+
+
+
+
+
+
Debugger Launchers: x64dbg Debugger
+
+
Integration with x64dbg is achieved using the Python 3
+ API x64dbg-automate-pyclient and underlying plugin x64dbg-automate, kindly provided by Darius Houle
+ (see https://github.com/dariushoule/x64dbg-automate & x64dbg-automate-pyclient). The console
+ debugger launches a full x64dbg session by default, synchronized with the
+ Ghidra debugger UI.
+
+
Two launchers are included out of the box, one for a local process and one for a local pid:
+
+
Local
+
+
The plain "local-x64dbg" launches the current program as a user-mode process
+ on the local system. If there is no current program, the user may specify the Image option
+ explicitly or launch x64dbg without a target.
+
+
Setup
+
+
Make sure you have installed the executables for x64dbg-automate (typically the contents
+ of x64dbg/build[32|64]/Release) in the plugins directory for x64dbg (release/x[32|64]/plugins).
+
+
If you have access to PyPI, setting up your Python 3 environment is done using Pip. (Please
+ note the version specifier for Protobuf.)
python command: This is the command or path to the Python interpreter. It
+ must be version 3. Python 2 is not supported.
+
+
Image: This is the path to the target binary image (EXE file). Ghidra will try to
+ fill this in based on information gathered when the current program was imported. If the file
+ exists and is executable on the local machine, it will be filled in automatically. Otherwise,
+ it is up to you to locate it. NOTE: If you have patched the current program database,
+ these changes are not applied to the target. You can either 1) apply the same
+ patches to the target once it is running, or 2) export a patched copy of your image and
+ direct this launcher to run it.
+
+
Arguments: These are the command-line arguments to pass into the target process.
+
+
Dir: The initial directory for the target process.
+
+
Path to x64dbg.exe: where the x64dbg executable resides (or the x32dbg executable
+ for 32-bit programs).
+
+
+
Once running, you are presented with a command-line interface in Ghidra's Terminal. This CLI
+ accepts your usual x64dbg native commands. You can escape from this CLI and enter a Python 3 REPL
+ by entering ".exit". This is not an actual x64dbg command, but our implementation
+ understands this to mean exit the x64dbg REPL. From the Python 3 REPL, you can access the
+ underlying Python-based API x64dbg_automate. This is an uncommon need, but may be useful for
+ diagnostics and/or workarounds. To re-enter the x64dbg REPL, enter "repl()".
+ Alternatively, if you are trying to quit, but typed ".exit", just type
+ "quit()" to terminate the session.
+
+
+
Attach
+
+
This launcher allows the user to attach to a local running process. Options are the same as
+ those for the base x64dbg, except Process Id replaces Image.
+
+
Options
+
+
+
ProcessId: The pid of the process you wish to attach to.
+
+
+
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/LICENSE b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/LICENSE
new file mode 100644
index 0000000000..c026b6b79a
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/LICENSE
@@ -0,0 +1,11 @@
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/MANIFEST.in b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/MANIFEST.in
new file mode 100644
index 0000000000..150d74bdc4
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/MANIFEST.in
@@ -0,0 +1 @@
+graft src
\ No newline at end of file
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/README.md b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/README.md
new file mode 100644
index 0000000000..4b22ae4c35
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/README.md
@@ -0,0 +1,3 @@
+# Ghidra Trace RMI
+
+Package for connecting x64dbg to Ghidra via Trace RMI.
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/pyproject.toml b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/pyproject.toml
new file mode 100644
index 0000000000..51eb1f5b5a
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/pyproject.toml
@@ -0,0 +1,32 @@
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "ghidraxdbg"
+version = "12.0"
+authors = [
+ { name="Ghidra Development Team" },
+]
+description = "Ghidra's Plugin for x64dbg"
+readme = "README.md"
+requires-python = ">=3.9"
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: Apache Software License",
+ "Operating System :: OS Independent",
+]
+dependencies = [
+ "ghidratrace==12.0",
+ "x64dbg_automate>=0.5.0"
+]
+
+[project.urls]
+"Homepage" = "https://github.com/NationalSecurityAgency/ghidra"
+"Bug Tracker" = "https://github.com/NationalSecurityAgency/ghidra/issues"
+
+[tool.setuptools.package-data]
+ghidradbg = ["*.tlb", "py.typed"]
+
+[tool.setuptools]
+include-package-data = true
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/__init__.py b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/__init__.py
new file mode 100644
index 0000000000..979a8c65b7
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/__init__.py
@@ -0,0 +1,17 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from . import util, commands, methods, hooks
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/arch.py b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/arch.py
new file mode 100644
index 0000000000..3c15d89fc7
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/arch.py
@@ -0,0 +1,236 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from typing import Dict, List, Optional, Tuple
+
+from ghidratrace.client import Address, RegVal
+
+from . import util
+
+
+language_map: Dict[str, List[str]] = {
+ 'x86_32': ['x86:LE:32:default'],
+ 'x86_64': ['x86:LE:64:default']
+}
+
+data64_compiler_map: Dict[Optional[str], str] = {
+ None: 'pointer64',
+}
+
+x86_compiler_map: Dict[Optional[str], str] = {
+ 'windows': 'windows',
+ 'Cygwin': 'windows',
+ 'default': 'windows',
+}
+
+default_compiler_map: Dict[Optional[str], str] = {
+ 'windows': 'default',
+}
+
+windows_compiler_map: Dict[Optional[str], str] = {
+ 'windows': 'windows',
+}
+
+compiler_map : Dict[str, Dict[Optional[str], str]]= {
+ 'DATA:BE:64:default': data64_compiler_map,
+ 'DATA:LE:64:default': data64_compiler_map,
+ 'x86:LE:32:default': x86_compiler_map,
+ 'x86:LE:64:default': x86_compiler_map
+}
+
+
+def get_arch() -> str:
+ try:
+ type = str(util.dbg.get_actual_processor_type())
+ except Exception as e:
+ print(f"Error getting actual processor type: {e}")
+ return "Unknown"
+ if type == "32":
+ return "x86_32"
+ if type == "64":
+ return "x86_64"
+ if type == None:
+ return "x86_64"
+ return "Unknown"
+
+
+def get_endian() -> str:
+ parm = util.get_convenience_variable('endian')
+ if parm != 'auto':
+ return parm
+ return 'little'
+
+
+def get_osabi() -> str:
+ parm = util.get_convenience_variable('osabi')
+ if not parm in ['auto', 'default']:
+ return parm
+ try:
+ os = "Windows" #util.dbg.cmd("vertarget")
+ if "Windows" not in os:
+ return "default"
+ except Exception:
+ print("Error getting target OS/ABI")
+ pass
+ return "windows"
+
+
+def compute_ghidra_language() -> str:
+ # First, check if the parameter is set
+ lang = util.get_convenience_variable('ghidra-language')
+ if lang != 'auto':
+ return lang
+
+ # Get the list of possible languages for the arch. We'll need to sift
+ # through them by endian and probably prefer default/simpler variants. The
+ # heuristic for "simpler" will be 'default' then shortest variant id.
+ arch = get_arch()
+ endian = get_endian()
+ lebe = ':BE:' if endian == 'big' else ':LE:'
+ if not arch in language_map:
+ return 'DATA' + lebe + '64:default'
+ langs = language_map[arch]
+ matched_endian = sorted(
+ (l for l in langs if lebe in l),
+ key=lambda l: 0 if l.endswith(':default') else len(l)
+ )
+ if len(matched_endian) > 0:
+ return matched_endian[0]
+ # NOTE: I'm disinclined to fall back to a language match with wrong endian.
+ return 'DATA' + lebe + '64:default'
+
+
+def compute_ghidra_compiler(lang: str) -> str:
+ # First, check if the parameter is set
+ comp = util.get_convenience_variable('ghidra-compiler')
+ if comp != 'auto':
+ return comp
+
+ # Check if the selected lang has specific compiler recommendations
+ if not lang in compiler_map:
+ print(f"{lang} not found in compiler map")
+ return 'default'
+ comp_map = compiler_map[lang]
+ if comp_map == data64_compiler_map:
+ print(f"Using the DATA64 compiler map")
+ osabi = get_osabi()
+ if osabi in comp_map:
+ return comp_map[osabi]
+ if None in comp_map:
+ return comp_map[None]
+ print(f"{osabi} not found in compiler map")
+ return 'default'
+
+
+def compute_ghidra_lcsp() -> Tuple[str, str]:
+ lang = compute_ghidra_language()
+ comp = compute_ghidra_compiler(lang)
+ return lang, comp
+
+
+class DefaultMemoryMapper(object):
+
+ def __init__(self, defaultSpace: str) -> None:
+ self.defaultSpace = defaultSpace
+
+ def map(self, proc: int, offset: int) -> Tuple[str, Address]:
+ space = self.defaultSpace
+ return self.defaultSpace, Address(space, offset)
+
+ def map_back(self, proc: int, address: Address) -> int:
+ if address.space == self.defaultSpace:
+ return address.offset
+ raise ValueError(f"Address {address} is not in process {proc}")
+
+
+DEFAULT_MEMORY_MAPPER = DefaultMemoryMapper('ram')
+
+memory_mappers: Dict[str, DefaultMemoryMapper] = {}
+
+
+def compute_memory_mapper(lang: str) -> DefaultMemoryMapper:
+ if not lang in memory_mappers:
+ return DEFAULT_MEMORY_MAPPER
+ return memory_mappers[lang]
+
+
+class DefaultRegisterMapper(object):
+
+ def __init__(self, byte_order: str) -> None:
+ if not byte_order in ['big', 'little']:
+ raise ValueError("Invalid byte_order: {}".format(byte_order))
+ self.byte_order = byte_order
+
+ def map_name(self, proc: int, name: str):
+ return name
+
+ def map_value(self, proc: int, name: str, value: int):
+ try:
+ # TODO: this seems half-baked
+ av = value.to_bytes(8, "big")
+ except Exception:
+ raise ValueError("Cannot convert {}'s value: '{}', type: '{}'"
+ .format(name, value, type(value)))
+ return RegVal(self.map_name(proc, name), av)
+
+ def map_name_back(self, proc: int, name: str) -> str:
+ return name
+
+ def map_value_back(self, proc: int, name: str, value: bytes):
+ return RegVal(self.map_name_back(proc, name), value)
+
+
+class Intel_x86_64_RegisterMapper(DefaultRegisterMapper):
+
+ def __init__(self):
+ super().__init__('little')
+
+ def map_name(self, proc, name):
+ if name is None:
+ return 'UNKNOWN'
+ if name == 'efl':
+ return 'rflags'
+ if name.startswith('zmm'):
+ # Ghidra only goes up to ymm, right now
+ return 'ymm' + name[3:]
+ return super().map_name(proc, name)
+
+ def map_value(self, proc, name, value):
+ rv = super().map_value(proc, name, value)
+ if rv.name.startswith('ymm') and len(rv.value) > 32:
+ return RegVal(rv.name, rv.value[-32:])
+ return rv
+
+ def map_name_back(self, proc, name):
+ if name == 'rflags':
+ return 'eflags'
+
+
+DEFAULT_BE_REGISTER_MAPPER = DefaultRegisterMapper('big')
+DEFAULT_LE_REGISTER_MAPPER = DefaultRegisterMapper('little')
+
+register_mappers = {
+ 'x86:LE:632:default': DEFAULT_LE_REGISTER_MAPPER,
+ 'x86:LE:64:default': Intel_x86_64_RegisterMapper()
+}
+
+
+def compute_register_mapper(lang: str)-> DefaultRegisterMapper:
+ if not lang in register_mappers:
+ if ':BE:' in lang:
+ return DEFAULT_BE_REGISTER_MAPPER
+ if ':LE:' in lang:
+ return DEFAULT_LE_REGISTER_MAPPER
+ return register_mappers[lang]
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/commands.py b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/commands.py
new file mode 100644
index 0000000000..5d3cdca851
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/commands.py
@@ -0,0 +1,1343 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from concurrent.futures import Future
+from contextlib import contextmanager
+import inspect
+import os.path
+import re
+import socket
+import sys
+import time
+from typing import Any, Dict, Generator, Iterable, List, Optional, Sequence, Tuple, Union
+
+from ghidratrace import sch
+from ghidratrace.client import (Client, Address, AddressRange, Lifespan, RegVal,
+ Schedule, Trace, TraceObject, TraceObjectValue,
+ Transaction)
+from ghidratrace.display import print_tabular_values, wait
+
+from x64dbg_automate.models import BreakpointType, HardwareBreakpointType, MemoryBreakpointType
+
+from . import util, arch, methods, hooks
+
+STILL_ACTIVE = 259
+PAGE_SIZE = 4096
+
+SESSION_PATH = 'Sessions[0]' # Only ever one, it seems
+AVAILABLES_PATH = SESSION_PATH + '.Available'
+AVAILABLE_KEY_PATTERN = '[{pid}]'
+AVAILABLE_PATTERN = AVAILABLES_PATH + AVAILABLE_KEY_PATTERN
+PROCESSES_PATH = SESSION_PATH + '.Processes'
+PROCESS_KEY_PATTERN = '[{procnum}]'
+PROCESS_PATTERN = PROCESSES_PATH + PROCESS_KEY_PATTERN
+PROC_DEBUG_PATTERN = PROCESS_PATTERN + '.Debug'
+PROC_SBREAKS_PATTERN = PROC_DEBUG_PATTERN + '.Software Breakpoints'
+PROC_HBREAKS_PATTERN = PROC_DEBUG_PATTERN + '.Hardware Breakpoints'
+PROC_MBREAKS_PATTERN = PROC_DEBUG_PATTERN + '.Memory Breakpoints'
+PROC_BREAK_KEY_PATTERN = '[{breaknum}]'
+PROC_SBREAK_PATTERN = PROC_SBREAKS_PATTERN + PROC_BREAK_KEY_PATTERN
+PROC_HBREAK_PATTERN = PROC_HBREAKS_PATTERN + PROC_BREAK_KEY_PATTERN
+PROC_MBREAK_PATTERN = PROC_MBREAKS_PATTERN + PROC_BREAK_KEY_PATTERN
+PROC_EVENTS_PATTERN = PROC_DEBUG_PATTERN + '.Events'
+PROC_EVENT_KEY_PATTERN = '[{eventnum}]'
+PROC_EVENT_PATTERN = PROC_EVENTS_PATTERN + PROC_EVENT_KEY_PATTERN
+PROC_EXCS_PATTERN = PROC_DEBUG_PATTERN + '.Exceptions'
+PROC_EXC_KEY_PATTERN = '[{eventnum}]'
+PROC_EXC_PATTERN = PROC_EXCS_PATTERN + PROC_EXC_KEY_PATTERN
+ENV_PATTERN = PROCESS_PATTERN + '.Environment'
+THREADS_PATTERN = PROCESS_PATTERN + '.Threads'
+THREAD_KEY_PATTERN = '[{tnum}]'
+THREAD_PATTERN = THREADS_PATTERN + THREAD_KEY_PATTERN
+STACK_PATTERN = THREAD_PATTERN + '.Stack.Frames'
+FRAME_KEY_PATTERN = '[{level}]'
+FRAME_PATTERN = STACK_PATTERN + FRAME_KEY_PATTERN
+REGS_PATTERN = THREAD_PATTERN + '.Registers'
+USER_REGS_PATTERN = THREAD_PATTERN + '.Registers.User'
+MEMORY_PATTERN = PROCESS_PATTERN + '.Memory'
+REGION_KEY_PATTERN = '[{start:08x}]'
+REGION_PATTERN = MEMORY_PATTERN + REGION_KEY_PATTERN
+MODULES_PATTERN = PROCESS_PATTERN + '.Modules'
+MODULE_KEY_PATTERN = '[{modpath}]'
+MODULE_PATTERN = MODULES_PATTERN + MODULE_KEY_PATTERN
+SECTIONS_ADD_PATTERN = '.Sections'
+SECTION_KEY_PATTERN = '[{secname}]'
+SECTION_ADD_PATTERN = SECTIONS_ADD_PATTERN + SECTION_KEY_PATTERN
+GENERIC_KEY_PATTERN = '[{key}]'
+TTD_PATTERN = 'State.DebuggerVariables.{var}.TTD'
+
+
+# TODO: Symbols
+
+
+class ErrorWithCode(Exception):
+
+ def __init__(self, code: int) -> None:
+ self.code = code
+
+ def __str__(self) -> str:
+ return repr(self.code)
+
+
+class Extra(object):
+ def __init__(self) -> None:
+ self.memory_mapper: Optional[arch.DefaultMemoryMapper] = None
+ self.register_mapper: Optional[arch.DefaultRegisterMapper] = None
+
+ def require_mm(self) -> arch.DefaultMemoryMapper:
+ if self.memory_mapper is None:
+ raise RuntimeError("No memory mapper")
+ return self.memory_mapper
+
+ def require_rm(self) -> arch.DefaultRegisterMapper:
+ if self.register_mapper is None:
+ raise RuntimeError("No register mapper")
+ return self.register_mapper
+
+
+class State(object):
+
+ def __init__(self) -> None:
+ self.reset_client()
+
+ def require_client(self) -> Client:
+ if self.client is None:
+ raise RuntimeError("Not connected")
+ return self.client
+
+ def require_no_client(self) -> None:
+ if self.client != None:
+ raise RuntimeError("Already connected")
+
+ def reset_client(self) -> None:
+ self.client: Optional[Client] = None
+ self.reset_trace()
+
+ def require_trace(self) -> Trace[Extra]:
+ if self.trace is None:
+ raise RuntimeError("No trace active")
+ return self.trace
+
+ def require_no_trace(self) -> None:
+ if self.trace != None:
+ raise RuntimeError("Trace already started")
+
+ def reset_trace(self) -> None:
+ self.trace: Optional[Trace[Extra]] = None
+ util.set_convenience_variable('_ghidra_tracing', "false")
+ self.reset_tx()
+
+ def require_tx(self) -> Tuple[Trace, Transaction]:
+ trace = self.require_trace()
+ if self.tx is None:
+ raise RuntimeError("No transaction")
+ return trace, self.tx
+
+ def require_no_tx(self) -> None:
+ if self.tx != None:
+ raise RuntimeError("Transaction already started")
+
+ def reset_tx(self) -> None:
+ self.tx: Optional[Transaction] = None
+
+
+STATE = State()
+
+
+def ghidra_trace_connect(address: Optional[str] = None) -> None:
+ """Connect Python to Ghidra for tracing.
+
+ Address must be of the form 'host:port'
+ """
+
+ STATE.require_no_client()
+ if address is None:
+ raise RuntimeError(
+ "'ghidra_trace_connect': missing required argument 'address'")
+
+ parts = address.split(':')
+ if len(parts) != 2:
+ raise RuntimeError("address must be in the form 'host:port'")
+ host, port = parts
+ try:
+ c = socket.socket()
+ c.connect((host, int(port)))
+ # TODO: Can we get version info from the DLL?
+ STATE.client = Client(c, "x64dbg", methods.REGISTRY)
+ print(f"Connected to {STATE.client.description} at {address}")
+ except ValueError:
+ raise RuntimeError("port must be numeric")
+
+
+def ghidra_trace_listen(address: str = '0.0.0.0:0') -> None:
+ """Listen for Ghidra to connect for tracing.
+
+ Takes an optional address for the host and port on which to listen.
+ Either the form 'host:port' or just 'port'. If omitted, it will bind
+ to an ephemeral port on all interfaces. If only the port is given,
+ it will bind to that port on all interfaces. This command will block
+ until the connection is established.
+ """
+
+ STATE.require_no_client()
+ parts = address.split(':')
+ if len(parts) == 1:
+ host, port = '0.0.0.0', parts[0]
+ elif len(parts) == 2:
+ host, port = parts
+ else:
+ raise RuntimeError("address must be 'port' or 'host:port'")
+
+ try:
+ s = socket.socket()
+ s.bind((host, int(port)))
+ host, port = s.getsockname()
+ s.listen(1)
+ print("Listening at {}:{}...".format(host, port))
+ c, (chost, cport) = s.accept()
+ s.close()
+ print("Connection from {}:{}".format(chost, cport))
+ STATE.client = Client(c, "x64dbg", methods.REGISTRY)
+ except ValueError:
+ raise RuntimeError("port must be numeric")
+
+
+def ghidra_trace_disconnect() -> None:
+ """Disconnect Python from Ghidra for tracing."""
+
+ STATE.require_client().close()
+ STATE.reset_client()
+
+
+def compute_name(progname: Optional[str] = None) -> str:
+ if progname is None:
+ return 'x64dbg/noname'
+ return 'x64dbg/' + re.split(r'/|\\', progname)[-1]
+
+
+def start_trace(name: str) -> None:
+ language, compiler = arch.compute_ghidra_lcsp()
+ STATE.trace = STATE.require_client().create_trace(
+ name, language, compiler, extra=Extra())
+ STATE.trace.extra.memory_mapper = arch.compute_memory_mapper(language)
+ STATE.trace.extra.register_mapper = arch.compute_register_mapper(language)
+
+ frame = inspect.currentframe()
+ if frame is None:
+ raise AssertionError("cannot locate schema.xml")
+ parent = os.path.dirname(inspect.getfile(frame))
+ schema_fn = os.path.join(parent, 'schema.xml')
+ with open(schema_fn, 'r') as schema_file:
+ schema_xml = schema_file.read()
+ with STATE.trace.open_tx("Create Root Object"):
+ root = STATE.trace.create_root_object(schema_xml, 'X64DbgRoot')
+ root.set_value('_display', util.DBG_VERSION.full +
+ ' via x64dbg_automate')
+ STATE.trace.create_object(SESSION_PATH).insert()
+ util.set_convenience_variable('_ghidra_tracing', "true")
+
+
+def ghidra_trace_start(name: Optional[str] = None) -> None:
+ """Start a Trace in Ghidra."""
+
+ STATE.require_client()
+ name = compute_name(name)
+ STATE.require_no_trace()
+ start_trace(name)
+
+
+def ghidra_trace_stop() -> None:
+ """Stop the Trace in Ghidra."""
+
+ STATE.require_trace().close()
+ STATE.reset_trace()
+
+
+def ghidra_trace_restart(name: Optional[str] = None) -> None:
+ """Restart or start the Trace in Ghidra."""
+
+ STATE.require_client()
+ if STATE.trace != None:
+ STATE.trace.close()
+ STATE.reset_trace()
+ name = compute_name(name)
+ start_trace(name)
+
+
+def ghidra_trace_create(command: Optional[str] = None,
+ args: Optional[str] = '.',
+ initdir: Optional[str] = '.',
+ start_trace: bool = True,
+ wait: bool = False) -> None:
+ """Create a session."""
+
+ dbg = util.dbg.client
+ if command != None:
+ dbg.load_executable(command, cmdline=args, current_dir=initdir)
+ if wait:
+ try:
+ dbg.wait_until_debugging()
+ except KeyboardInterrupt as ki:
+ dbg.interrupt()
+ if start_trace:
+ ghidra_trace_start(command)
+
+
+def ghidra_trace_attach(pid: Optional[str] = None,
+ start_trace: bool = True) -> None:
+ """Create a session by attaching."""
+
+ dbg = util.dbg.client
+ if pid != None:
+ dbg.attach(int(pid, 0))
+ try:
+ dbg.wait_until_debugging()
+ except KeyboardInterrupt as ki:
+ dbg.interrupt()
+ if start_trace:
+ ghidra_trace_start(f"pid_{pid}")
+
+
+def ghidra_trace_connect_server(options: Union[str, bytes, None] = None) -> None:
+ """Connect to a process server session."""
+
+ dbg = util.dbg.client
+ if options != None:
+ if isinstance(options, str):
+ enc_options = options.encode()
+ #dbg._client.ConnectProcessServer(enc_options)
+
+
+def ghidra_trace_open(command: Optional[str] = None,
+ start_trace: bool = True) -> None:
+ """Create a session."""
+
+ dbg = util.dbg.client
+ if start_trace:
+ ghidra_trace_start(command)
+
+
+def ghidra_trace_kill() -> None:
+ """Kill a session."""
+
+ dbg = util.dbg.client
+ dbg.unload_executable()
+
+
+def ghidra_trace_info() -> None:
+ """Get info about the Ghidra connection."""
+
+ if STATE.client is None:
+ print("Not connected to Ghidra")
+ return
+ host, port = STATE.client.s.getpeername()
+ print(f"Connected to {STATE.client.description} at {host}:{port}")
+ if STATE.trace is None:
+ print("No trace")
+ return
+ print("Trace active")
+
+
+def ghidra_trace_info_lcsp() -> None:
+ """Get the selected Ghidra language-compiler-spec pair."""
+
+ language, compiler = arch.compute_ghidra_lcsp()
+ print("Selected Ghidra language: {}".format(language))
+ print("Selected Ghidra compiler: {}".format(compiler))
+
+
+def ghidra_trace_txstart(description: str = "tx") -> None:
+ """Start a transaction on the trace."""
+
+ STATE.require_no_tx()
+ STATE.tx = STATE.require_trace().start_tx(description, undoable=False)
+
+
+def ghidra_trace_txcommit() -> None:
+ """Commit the current transaction."""
+
+ STATE.require_tx()[1].commit()
+ STATE.reset_tx()
+
+
+def ghidra_trace_txabort() -> None:
+ """Abort the current transaction.
+
+ Use only in emergencies.
+ """
+
+ trace, tx = STATE.require_tx()
+ print("Aborting trace transaction!")
+ tx.abort()
+ STATE.reset_tx()
+
+
+@contextmanager
+def open_tracked_tx(description: str) -> Generator[Transaction, None, None]:
+ with STATE.require_trace().open_tx(description) as tx:
+ STATE.tx = tx
+ yield tx
+ STATE.reset_tx()
+
+
+def ghidra_trace_save() -> None:
+ """Save the current trace."""
+
+ STATE.require_trace().save()
+
+
+def ghidra_trace_new_snap(description: Optional[str] = None,
+ time: Optional[Schedule] = None) -> Dict[str, int]:
+ """Create a new snapshot.
+
+ Subsequent modifications to machine state will affect the new
+ snapshot.
+ """
+
+ description = str(description)
+ trace, tx = STATE.require_tx()
+ return {'snap': trace.snapshot(description, time=time)}
+
+
+def quantize_pages(start: int, end: int) -> Tuple[int, int]:
+ return (start // PAGE_SIZE * PAGE_SIZE, (end + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE)
+
+
+def put_bytes(start: int, end: int, pages: bool,
+ display_result: bool = False) -> Dict[str, int]:
+ # print("PUT BYTES")
+ # COLOSSAL HACK, but x32dbg will die if you access a 64-bit value
+ bitness = util.dbg.client.debugee_bitness()
+ if start > 1< int:
+ try:
+ result = util.parse_and_eval(address)
+ if isinstance(result, int):
+ return result
+ raise ValueError(f"Value '{address}' does not evaluate to an int")
+ except Exception:
+ raise RuntimeError(f"Cannot convert '{address}' to address")
+
+
+def eval_range(address: Union[str, int],
+ length: Union[str, int]) -> Tuple[int, int]:
+ start = eval_address(address)
+ try:
+ l = util.parse_and_eval(length)
+ except Exception as e:
+ raise RuntimeError(f"Cannot convert '{length}' to length")
+ if not isinstance(l, int):
+ raise ValueError(f"Value '{address}' does not evaluate to an int")
+ end = start + l
+ return start, end
+
+
+def putmem(address: Union[str, int], length: Union[str, int],
+ pages: bool = True, display_result: bool = True) -> Dict[str, int]:
+ start, end = eval_range(address, length)
+ return put_bytes(start, end, pages, display_result)
+
+
+def ghidra_trace_putmem(address: Union[str, int], length: Union[str, int],
+ pages: bool = True) -> Dict[str, int]:
+ """Record the given block of memory into the Ghidra trace."""
+
+ STATE.require_tx()
+ return putmem(address, length, pages, True)
+
+
+def putmem_state(address: Union[str, int], length: Union[str, int], state: str,
+ pages: bool = True) -> None:
+ trace = STATE.require_trace()
+ trace.validate_state(state)
+ start, end = eval_range(address, length)
+ if pages:
+ start, end = quantize_pages(start, end)
+ nproc = util.selected_process()
+ base, addr = trace.extra.require_mm().map(nproc, start)
+ if base != addr.space and state != 'unknown':
+ trace.create_overlay_space(base, addr.space)
+ trace.set_memory_state(addr.extend(end - start), state)
+
+
+def ghidra_trace_putmem_state(address: Union[str, int], length: Union[str, int],
+ state: str, pages: bool = True) -> None:
+ """Set the state of the given range of memory in the Ghidra trace."""
+
+ STATE.require_tx()
+ return putmem_state(address, length, state, pages)
+
+
+def ghidra_trace_delmem(address: Union[str, int],
+ length: Union[str, int]) -> None:
+ """Delete the given range of memory from the Ghidra trace.
+
+ Why would you do this? Keep in mind putmem quantizes to full pages
+ by default, usually to take advantage of spatial locality. This
+ command does not quantize. You must do that yourself, if necessary.
+ """
+
+ trace, tx = STATE.require_tx()
+ start, end = eval_range(address, length)
+ nproc = util.selected_process()
+ base, addr = trace.extra.require_mm().map(nproc, start)
+ # Do not create the space. We're deleting stuff.
+ trace.delete_bytes(addr.extend(end - start))
+
+
+def putreg() -> Dict[str, List[str]]:
+ trace = STATE.require_trace()
+ nproc = util.selected_process()
+ if nproc is None:
+ return {}
+ nthrd = util.selected_thread()
+ space = REGS_PATTERN.format(procnum=nproc, tnum=nthrd)
+ trace.create_overlay_space('register', space)
+ robj = trace.create_object(space)
+ robj.insert()
+ mapper = trace.extra.require_rm()
+ values = []
+ regs = util.dbg.client.get_regs()
+ ctxt = regs.context
+ for k in ctxt.model_fields.keys():
+ name = k
+ try:
+ value = getattr(ctxt, k)
+ except Exception:
+ value = 0
+ try:
+ if type(value) is int:
+ values.append(mapper.map_value(nproc, name, value))
+ robj.set_value(name, hex(value))
+ if type(value) is bytes:
+ value = int.from_bytes(value, "little")
+ values.append(mapper.map_value(nproc, name, value))
+ robj.set_value(name, hex(value))
+ except Exception:
+ pass
+ missing = trace.put_registers(space, values)
+ if isinstance(missing, Future):
+ return {'future': []}
+ return {'missing': missing}
+
+
+def ghidra_trace_putreg() -> None:
+ """Record the given register group for the current frame into the Ghidra
+ trace.
+
+ If no group is specified, 'all' is assumed.
+ """
+
+ STATE.require_tx()
+ putreg()
+
+
+def ghidra_trace_delreg(group='all') -> None:
+ """Delete the given register group for the curent frame from the Ghidra
+ trace.
+
+ Why would you do this? If no group is specified, 'all' is assumed.
+ """
+
+ trace, tx = STATE.require_tx()
+ nproc = util.selected_process()
+ nthrd = util.selected_thread()
+ space = REGS_PATTERN.format(procnum=nproc, tnum=nthrd)
+ mapper = trace.extra.require_rm()
+ names = []
+ regs = util.dbg.client.get_regs()
+ ctxt = regs.context
+ for i in ctxt.model_fields.keys():
+ names.append(mapper.map_name(nproc, i))
+ trace.delete_registers(space, names)
+
+
+def ghidra_trace_create_obj(path: str) -> None:
+ """Create an object in the Ghidra trace.
+
+ The new object is in a detached state, so it may not be immediately
+ recognized by the Debugger GUI. Use 'ghidra_trace_insert-obj' to
+ finish the object, after all its required attributes are set.
+ """
+
+ trace, tx = STATE.require_tx()
+ obj = trace.create_object(path)
+ obj.insert()
+ print(f"Created object: id={obj.id}, path='{obj.path}'")
+
+
+def ghidra_trace_insert_obj(path: str) -> None:
+ """Insert an object into the Ghidra trace."""
+
+ # NOTE: id parameter is probably not necessary, since this command is for
+ # humans.
+ trace, tx = STATE.require_tx()
+ span = trace.proxy_object_path(path).insert()
+ print(f"Inserted object: lifespan={span}")
+
+
+def ghidra_trace_remove_obj(path: str) -> None:
+ """Remove an object from the Ghidra trace.
+
+ This does not delete the object. It just removes it from the tree
+ for the current snap and onwards.
+ """
+
+ trace, tx = STATE.require_tx()
+ trace.proxy_object_path(path).remove()
+
+
+def to_bytes(value: Sequence) -> bytes:
+ return bytes(ord(value[i]) if type(value[i]) == str else int(value[i])
+ for i in range(0, len(value)))
+
+
+def to_string(value: Sequence, encoding: str) -> str:
+ b = to_bytes(value)
+ return str(b, encoding)
+
+
+def to_bool_list(value: Sequence) -> List[bool]:
+ return [bool(value[i]) for i in range(0, len(value))]
+
+
+def to_int_list(value: Sequence) -> List[int]:
+ return [ord(value[i]) if type(value[i]) == str else int(value[i])
+ for i in range(0, len(value))]
+
+
+def to_short_list(value: Sequence) -> List[int]:
+ return [ord(value[i]) if type(value[i]) == str else int(value[i])
+ for i in range(0, len(value))]
+
+
+def to_string_list(value: Sequence, encoding: str) -> List[str]:
+ return [to_string(value[i], encoding) for i in range(0, len(value))]
+
+
+def eval_value(value: Any, schema: Optional[sch.Schema] = None) -> Tuple[Union[
+ bool, int, float, bytes, Tuple[str, Address], List[bool], List[int],
+ List[str], str], Optional[sch.Schema]]:
+ if (schema == sch.BYTE or schema == sch.SHORT or
+ schema == sch.INT or schema == sch.LONG or schema == None):
+ value = util.parse_and_eval(value)
+ return value, schema
+ if schema == sch.CHAR:
+ value = util.parse_and_eval(ord(value))
+ return value, schema
+ if schema == sch.BOOL:
+ value = util.parse_and_eval(value)
+ return bool(value), schema
+ if schema == sch.ADDRESS:
+ value = util.parse_and_eval(value)
+ nproc = util.selected_process()
+ base, addr = STATE.require_trace().extra.require_mm().map(nproc, value)
+ return (base, addr), sch.ADDRESS
+ if schema == sch.BOOL_ARR:
+ return to_bool_list(value), schema
+ if schema == sch.BYTE_ARR:
+ return to_bytes(value), schema
+ if schema == sch.SHORT_ARR:
+ return to_short_list(value), schema
+ if schema == sch.INT_ARR:
+ return to_int_list(value), schema
+ if schema == sch.LONG_ARR:
+ return to_int_list(value), schema
+ if schema == sch.STRING_ARR:
+ return to_string_list(value, 'utf-8'), schema
+ if schema == sch.CHAR_ARR:
+ return to_string(value, 'utf-8'), schema
+ if schema == sch.STRING:
+ return to_string(value, 'utf-8'), schema
+
+ return value, schema
+
+
+def ghidra_trace_set_value(path: str, key: str, value: Any,
+ schema: Optional[str] = None) -> None:
+ """Set a value (attribute or element) in the Ghidra trace's object tree.
+
+ A void value implies removal.
+ NOTE: The type of an expression may be subject to the x64dbg's current
+ language, which current defaults to DEBUG_EXPR_CPLUSPLUS (vs DEBUG_EXPR_MASM).
+ For most non-primitive cases, we are punting to the Python API.
+ """
+ real_schema = None if schema is None else sch.Schema(schema)
+ trace, tx = STATE.require_tx()
+ if real_schema == sch.OBJECT:
+ val: Union[bool, int, float, bytes, Tuple[str, Address], List[bool],
+ List[int], List[str], str, TraceObject,
+ Address] = trace.proxy_object_path(value)
+ else:
+ val, real_schema = eval_value(value, real_schema)
+ if real_schema == sch.ADDRESS and isinstance(val, tuple):
+ base, addr = val
+ val = addr
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+ trace.proxy_object_path(path).set_value(key, val, real_schema)
+
+
+def ghidra_trace_retain_values(path: str, keys: str) -> None:
+ """Retain only those keys listed, settings all others to null.
+
+ Takes a list of keys to retain. The first argument may optionally be one of
+ the following:
+
+ --elements To set all other elements to null (default)
+ --attributes To set all other attributes to null
+ --both To set all other values (elements and attributes) to null
+
+ If, for some reason, one of the keys to retain would be mistaken for this
+ switch, then the switch is required. Only the first argument is taken as the
+ switch. All others are taken as keys.
+ """
+
+ key_list = keys.split(" ")
+
+ trace, tx = STATE.require_tx()
+ kinds = 'elements'
+ if key_list[0] == '--elements':
+ kinds = 'elements'
+ key_list = key_list[1:]
+ elif key_list[0] == '--attributes':
+ kinds = 'attributes'
+ key_list = key_list[1:]
+ elif key_list[0] == '--both':
+ kinds = 'both'
+ key_list = key_list[1:]
+ elif key_list[0].startswith('--'):
+ raise RuntimeError("Invalid argument: " + key_list[0])
+ trace.proxy_object_path(path).retain_values(key_list, kinds=kinds)
+
+
+def ghidra_trace_get_obj(path: str) -> None:
+ """Get an object descriptor by its canonical path.
+
+ This isn't the most informative, but it will at least confirm
+ whether an object exists and provide its id.
+ """
+
+ trace = STATE.require_trace()
+ object = trace.get_object(path)
+ print(f"{object.id}\t{object.path}")
+
+
+def ghidra_trace_get_values(pattern: str) -> None:
+ """List all values matching a given path pattern."""
+
+ trace = STATE.require_trace()
+ values = wait(trace.get_values(pattern))
+ print_tabular_values(values, print)
+
+
+def ghidra_trace_get_values_rng(address: Union[str, int],
+ length: Union[str, int]) -> None:
+ """List all values intersecting a given address range."""
+
+ trace = STATE.require_trace()
+ start, end = eval_range(address, length)
+ nproc = util.selected_process()
+ base, addr = trace.extra.require_mm().map(nproc, start)
+ # Do not create the space. We're querying. No tx.
+ values = wait(trace.get_values_intersecting(addr.extend(end - start)))
+ print_tabular_values(values, print)
+
+
+def activate(path: Optional[str] = None) -> None:
+ trace = STATE.require_trace()
+ if path is None:
+ nproc = util.selected_process()
+ if nproc is None:
+ path = PROCESSES_PATH
+ else:
+ nthrd = util.selected_thread()
+ if nthrd is None:
+ path = PROCESS_PATTERN.format(procnum=nproc)
+ else:
+ path = THREAD_PATTERN.format(procnum=nproc, tnum=nthrd)
+ #frame = util.selected_frame()
+ #if frame is None:
+ # path = THREAD_PATTERN.format(procnum=nproc, tnum=nthrd)
+ #else:
+ # path = FRAME_PATTERN.format(
+ # procnum=nproc, tnum=nthrd, level=frame)
+ trace.proxy_object_path(path).activate()
+
+
+def ghidra_trace_activate(path: Optional[str] = None) -> None:
+ """Activate an object in Ghidra's GUI.
+
+ This has no effect if the current trace is not current in Ghidra. If
+ path is omitted, this will activate the current frame.
+ """
+
+ activate(path)
+
+
+def ghidra_trace_disassemble(address: Union[str, int]) -> None:
+ """Disassemble starting at the given seed.
+
+ Disassembly proceeds linearly and terminates at the first branch or
+ unknown memory encountered.
+ """
+
+ trace, tx = STATE.require_tx()
+ start = eval_address(address)
+ nproc = util.selected_process()
+ base, addr = trace.extra.require_mm().map(nproc, start)
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+
+ length = trace.disassemble(addr)
+ print("Disassembled {} bytes".format(length))
+
+
+def compute_proc_state(nproc: Optional[int] = None) -> str:
+ try:
+ if util.dbg.client.is_running():
+ return 'RUNNING'
+ return 'STOPPED'
+ except Exception:
+ return 'TERMINATED'
+
+
+def put_processes(running: bool = False) -> None:
+ # NB: This speeds things up, but desirable?
+ if running:
+ return
+
+ trace = STATE.require_trace()
+
+ keys = []
+ # Set running=True to avoid process changes, even while stopped
+ for i, p in enumerate(util.process_list0(running=True)):
+ pid = p[0]
+ ipath = PROCESS_PATTERN.format(procnum=pid)
+ keys.append(PROCESS_KEY_PATTERN.format(procnum=pid))
+ procobj = trace.create_object(ipath)
+
+ istate = compute_proc_state(i)
+ procobj.set_value('State', istate)
+ procobj.set_value('PID', pid)
+ procobj.set_value('_display', f'{i} {pid}')
+ if len(p) > 1:
+ procobj.set_value('Name', str(p[1]))
+ #procobj.set_value('PEB', hex(int(p[2])))
+ procobj.insert()
+ trace.proxy_object_path(PROCESSES_PATH).retain_values(keys)
+
+
+def put_state(event_process: int) -> None:
+ ipath = PROCESS_PATTERN.format(procnum=event_process)
+ trace = STATE.require_trace()
+ procobj = trace.create_object(ipath)
+ state = compute_proc_state(event_process)
+ procobj.set_value('State', state)
+ procobj.insert()
+ tnum = util.selected_thread()
+ if tnum is not None:
+ ipath = THREAD_PATTERN.format(procnum=event_process, tnum=tnum)
+ threadobj = trace.create_object(ipath)
+ threadobj.set_value('State', state)
+ threadobj.insert()
+
+
+def ghidra_trace_put_processes() -> None:
+ """Put the list of processes into the trace's Processes list."""
+
+ trace, tx = STATE.require_tx()
+ with trace.client.batch() as b:
+ put_processes()
+
+
+def put_available() -> None:
+ trace = STATE.require_trace()
+ keys = []
+ for i, p in enumerate(util.process_list(running=True)):
+ pid = p[0]
+ ipath = AVAILABLE_PATTERN.format(pid=pid)
+ keys.append(AVAILABLE_KEY_PATTERN.format(pid=pid))
+ procobj = trace.create_object(ipath)
+ procobj.set_value('PID', pid)
+ procobj.set_value('_display', f'{i} {pid}')
+ if len(p) > 1:
+ name = str(p[1])
+ procobj.set_value('Name', name)
+ procobj.set_value('_display', f'{i} {pid} {name}')
+ procobj.insert()
+ trace.proxy_object_path(AVAILABLES_PATH).retain_values(keys)
+
+
+def ghidra_trace_put_available() -> None:
+ """Put the list of available processes into the trace's Available list."""
+
+ trace, tx = STATE.require_tx()
+ with trace.client.batch() as b:
+ put_available()
+
+
+def put_single_breakpoint(bp, bpath, nproc: int, ikeys: List[int]) -> None:
+ trace = STATE.require_trace()
+ mapper = trace.extra.require_mm()
+
+ address = bp.addr
+ brkobj = trace.create_object(bpath)
+
+ brkobj.set_value('_display', f'{hex(address)}')
+ if address is not None: # Implies execution break
+ base, addr = mapper.map(nproc, address)
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+ brkobj.set_value('Range', addr.extend(1))
+ brkobj.set_value('Active', bp.active)
+ brkobj.set_value('Enabled', bp.enabled)
+ brkobj.set_value('Slot', str(bp.slot))
+ brkobj.set_value('Type', str(bp.type))
+ brkobj.set_value('TypeEx', str(bp.typeEx))
+ if bp.fastResume is True:
+ brkobj.set_value('FastResume', bp.fastResume)
+ if bp.silent is True:
+ brkobj.set_value('Silent', bp.silent)
+ if bp.singleshoot is True:
+ brkobj.set_value('SingleShot', bp.singleshoot)
+ if bp.mod is not None:
+ brkobj.set_value('Module', bp.mod)
+ if bp.name is not None and bp.name != "":
+ brkobj.set_value('Name', bp.name)
+ brkobj.set_value('_display', f'{hex(address)} {bp.name}')
+ if bp.commandText is not None and bp.commandText != "":
+ brkobj.set_value('Command', bp.commandText)
+ if bp.breakCondition is not None and bp.breakCondition != "":
+ brkobj.set_value('Condition', bp.breakCondition)
+ if bp.logText is not None and bp.logText != "":
+ brkobj.set_value('LogText', bp.logText)
+ if bp.logCondition is not None and bp.logCondition != "":
+ brkobj.set_value('LogCondition', bp.logCondition)
+ if bp.hwSize is not None and bp.hwSize != 0:
+ brkobj.set_value('HW Size', bp.hwSize)
+ brkobj.set_value('Range', addr.extend(bp.hwSize))
+ brkobj.set_value('HitCount', bp.hitCount)
+ if bp.type == BreakpointType.BpNormal:
+ brkobj.set_value('Kinds', 'SW_EXECUTE')
+ if bp.type == BreakpointType.BpHardware:
+ prot = {0: 'READ', 1: 'WRITE', 2: 'HW_EXECUTE'}[bp.typeEx]
+ brkobj.set_value('Kinds', prot)
+ if bp.type == BreakpointType.BpMemory:
+ prot = {0: 'READ', 1: 'WRITE', 2: 'HW_EXECUTE', 3: 'ACCESS'}[bp.typeEx]
+ brkobj.set_value('Kinds', prot)
+ brkobj.insert()
+
+ k = PROC_BREAK_KEY_PATTERN.format(breaknum=address)
+ ikeys.append(k)
+
+
+def put_breakpoints(type: BreakpointType) -> None:
+ nproc = util.selected_process()
+
+ trace = STATE.require_trace()
+ target = util.get_target()
+ pattern = ''
+ prot = ''
+ if type == BreakpointType.BpNormal:
+ pattern = PROC_SBREAKS_PATTERN
+ if type == BreakpointType.BpHardware:
+ pattern = PROC_HBREAKS_PATTERN
+ if type == BreakpointType.BpMemory:
+ pattern = PROC_MBREAKS_PATTERN
+ ibpath = pattern.format(procnum=nproc)
+ ibobj = trace.create_object(ibpath)
+ keys: List[str] = []
+ ikeys: List[int] = []
+ for bp in util.dbg.client.get_breakpoints(type):
+ bpath = ibpath + PROC_BREAK_KEY_PATTERN.format(breaknum=bp.addr)
+ keys.append(bpath)
+ put_single_breakpoint(bp, bpath, nproc, ikeys)
+ ibobj.insert()
+ trace.proxy_object_path(pattern).retain_values(keys)
+ ibobj.retain_values(ikeys)
+
+
+def ghidra_trace_put_breakpoints() -> None:
+ """Put the current process's breakpoints into the trace."""
+
+ trace, tx = STATE.require_tx()
+ with trace.client.batch() as b:
+ put_breakpoints(BreakpointType.BpNormal)
+ put_breakpoints(BreakpointType.BpHardware)
+ put_breakpoints(BreakpointType.BpMemory)
+
+
+def put_environment() -> None:
+ trace = STATE.require_trace()
+ nproc = util.selected_process()
+ epath = ENV_PATTERN.format(procnum=nproc)
+ envobj = trace.create_object(epath)
+ envobj.set_value('Debugger', 'x64dbg')
+ envobj.set_value('Arch', arch.get_arch())
+ envobj.set_value('OS', arch.get_osabi())
+ envobj.set_value('Endian', arch.get_endian())
+ envobj.insert()
+
+
+def ghidra_trace_put_environment() -> None:
+ """Put some environment indicators into the Ghidra trace."""
+
+ trace, tx = STATE.require_tx()
+ with trace.client.batch() as b:
+ put_environment()
+
+
+def put_regions() -> None:
+ nproc = util.selected_process()
+ try:
+ regions = util.dbg.client.memmap()
+ except Exception:
+ regions = []
+ #if len(regions) == 0:
+ # regions = util.full_mem()
+
+ trace = STATE.require_trace()
+ mapper = trace.extra.require_mm()
+ keys = []
+ mod_keys = []
+ # r : MemMap
+ for r in regions:
+ rpath = REGION_PATTERN.format(procnum=nproc, start=r.base_address)
+ keys.append(REGION_KEY_PATTERN.format(start=r.base_address))
+ regobj = trace.create_object(rpath)
+ (start_base, start_addr) = map_address(r.base_address)
+ regobj.set_value('Range', start_addr.extend(r.region_size))
+ regobj.set_value('_readable', r.protect ==
+ None or r.protect & 0x66 != 0)
+ regobj.set_value('_writable', r.protect ==
+ None or r.protect & 0xCC != 0)
+ regobj.set_value('_executable', r.protect ==
+ None or r.protect & 0xF0 != 0)
+ regobj.set_value('AllocationBase', hex(r.allocation_base))
+ regobj.set_value('Protect', hex(r.protect))
+ regobj.set_value('Type', hex(r.type))
+ if hasattr(r, 'info') and r.info is not None:
+ regobj.set_value('_display', r.info)
+ base = util.dbg.eval('mod.base({})'.format(hex(r.base_address)))
+ if base is not None and isinstance(base, int) is False:
+ base = base[0]
+ if base == r.base_address:
+ name = r.info
+ hbase = hex(base)
+ mpath = MODULE_PATTERN.format(procnum=nproc, modpath=hbase)
+ modobj = trace.create_object(mpath)
+ mod_keys.append(MODULE_KEY_PATTERN.format(modpath=hbase))
+ base_base, base_addr = mapper.map(nproc, base)
+ if base_base != base_addr.space:
+ trace.create_overlay_space(base_base, base_addr.space)
+ modsize = util.dbg.eval('mod.size({})'.format(hbase))
+ if modsize is None or len(modsize) < 2:
+ size = 1
+ else:
+ size = modsize[0]
+ modobj.set_value('Range', base_addr.extend(size))
+ modobj.set_value('Name', name)
+ modentry = util.dbg.eval('mod.entry({})'.format(hbase))
+ if modentry is not None and isinstance(modentry, int):
+ modobj.set_value('Entry', modentry)
+ elif modentry is not None and len(modentry) > 0:
+ modobj.set_value('Entry', modentry[0])
+ modobj.insert()
+ regobj.insert()
+ if STATE.trace is None:
+ return
+ STATE.trace.proxy_object_path(
+ MEMORY_PATTERN.format(procnum=nproc)).retain_values(keys)
+ STATE.trace.proxy_object_path(MODULES_PATTERN.format(
+ procnum=nproc)).retain_values(mod_keys)
+
+
+def ghidra_trace_put_regions() -> None:
+ """Read the memory map, if applicable, and write to the trace's Regions."""
+
+ trace, tx = STATE.require_tx()
+ with trace.client.batch() as b:
+ put_regions()
+
+
+def put_modules() -> None:
+ put_regions()
+
+
+def ghidra_trace_put_modules() -> None:
+ """Gather object files, if applicable, and write to the trace's Modules."""
+
+ trace, tx = STATE.require_tx()
+ with trace.client.batch() as b:
+ put_modules()
+
+
+def compute_thread_display(i: int, pid: Optional[int], tid: int, t) -> str:
+ return f'{i} {pid}:{tid}'
+
+
+def put_threads(running: bool = False) -> None:
+ # NB: This speeds things up, but desirable?
+ if running:
+ return
+
+ pid = util.selected_process()
+ if pid is None:
+ return
+ trace = STATE.require_trace()
+
+ mapper = trace.extra.require_mm()
+ keys = []
+
+ for i, t in enumerate(util.thread_list(running=False)):
+ tid = int(t[0])
+ tpath = THREAD_PATTERN.format(procnum=pid, tnum=tid)
+ tobj = trace.create_object(tpath)
+ keys.append(THREAD_KEY_PATTERN.format(tnum=tid))
+
+ tobj.set_value('_short_display', f'{i} {pid}:{tid}')
+ tobj.set_value('_display', compute_thread_display(i, pid, tid, t))
+ tobj.set_value('TID', tid)
+ if tid in util.threads:
+ thread_data = util.threads[tid]
+ base, offset_start = mapper.map(pid, thread_data.lpStartAddress)
+ tobj.set_value('Start', offset_start)
+ base, offset_base = mapper.map(pid, thread_data.lpThreadLocalBase)
+ tobj.set_value('TLB', offset_base)
+ tobj.insert()
+ trace.proxy_object_path(THREADS_PATTERN.format(
+ procnum=pid)).retain_values(keys)
+
+
+def put_event_thread(nthrd: Optional[int] = None) -> None:
+ trace = STATE.require_trace()
+ nproc = util.selected_process()
+ # Assumption: Event thread is selected by x64dbg upon stopping
+ if nthrd is None:
+ nthrd = util.selected_thread()
+ if nthrd != None:
+ tpath = THREAD_PATTERN.format(procnum=nproc, tnum=nthrd)
+ tobj = trace.proxy_object_path(tpath)
+ else:
+ tobj = None
+ trace.proxy_object_path('').set_value('_event_thread', tobj)
+
+
+def ghidra_trace_put_threads() -> None:
+ """Put the current process's threads into the Ghidra trace."""
+
+ trace, tx = STATE.require_tx()
+ with trace.client.batch() as b:
+ put_threads()
+
+
+# TODO: if eventually exposed...
+# def put_frames() -> None:
+# nproc = util.selected_process()
+# if nproc < 0:
+# return
+# nthrd = util.selected_thread()
+# if nthrd is None:
+# return
+#
+# trace = STATE.require_trace()
+#
+# mapper = trace.extra.require_mm()
+# keys = []
+# # f : _DEBUG_STACK_FRAME
+# for f in util.dbg.client.backtrace_list():
+# fpath = FRAME_PATTERN.format(
+# procnum=nproc, tnum=nthrd, level=f.FrameNumber)
+# fobj = trace.create_object(fpath)
+# keys.append(FRAME_KEY_PATTERN.format(level=f.FrameNumber))
+# base, offset_inst = mapper.map(nproc, f.InstructionOffset)
+# if base != offset_inst.space:
+# trace.create_overlay_space(base, offset_inst.space)
+# fobj.set_value('Instruction Offset', offset_inst)
+# base, offset_stack = mapper.map(nproc, f.StackOffset)
+# if base != offset_stack.space:
+# trace.create_overlay_space(base, offset_stack.space)
+# base, offset_ret = mapper.map(nproc, f.ReturnOffset)
+# if base != offset_ret.space:
+# trace.create_overlay_space(base, offset_ret.space)
+# base, offset_frame = mapper.map(nproc, f.FrameOffset)
+# if base != offset_frame.space:
+# trace.create_overlay_space(base, offset_frame.space)
+# fobj.set_value('Stack Offset', offset_stack)
+# fobj.set_value('Return Offset', offset_ret)
+# fobj.set_value('Frame Offset', offset_frame)
+# fobj.set_value('_display', "#{} {}".format(
+# f.FrameNumber, offset_inst.offset))
+# fobj.insert()
+# trace.proxy_object_path(STACK_PATTERN.format(
+# procnum=nproc, tnum=nthrd)).retain_values(keys)
+
+
+# def ghidra_trace_put_frames() -> None:
+# """Put the current thread's frames into the Ghidra trace."""
+#
+# trace, tx = STATE.require_tx()
+# with trace.client.batch() as b:
+# put_frames()
+
+
+def map_address(address: int) -> Tuple[str, Address]:
+ nproc = util.selected_process()
+ trace = STATE.require_trace()
+ mapper = trace.extra.require_mm()
+ base, addr = mapper.map(nproc, address)
+ if base != addr.space:
+ trace.create_overlay_space(base, addr.space)
+ return base, addr
+
+
+def ghidra_trace_put_all() -> None:
+ """Put everything currently selected into the Ghidra trace."""
+
+ trace, tx = STATE.require_tx()
+ with trace.client.batch() as b:
+ #util.dbg.client.wait_cmd_ready()
+ try:
+ put_processes()
+ put_environment()
+ put_threads()
+ putreg()
+ put_regions()
+ putmem(util.get_pc(), 1)
+ putmem(util.get_sp(), 1)
+ put_breakpoints(BreakpointType.BpNormal)
+ put_breakpoints(BreakpointType.BpHardware)
+ put_breakpoints(BreakpointType.BpMemory)
+ put_available()
+ activate()
+ except Exception as e:
+ print(e)
+ pass
+
+
+def ghidra_trace_install_hooks() -> None:
+ """Install hooks to trace in Ghidra."""
+
+ hooks.install_hooks()
+
+
+def ghidra_trace_remove_hooks() -> None:
+ """Remove hooks to trace in Ghidra.
+
+ Using this directly is not recommended, unless it seems the hooks
+ are preventing x64dbg or other extensions from operating. Removing
+ hooks will break trace synchronization until they are replaced.
+ """
+
+ hooks.remove_hooks()
+
+
+def ghidra_trace_sync_enable() -> None:
+ """Synchronize the current process with the Ghidra trace.
+
+ This will automatically install hooks if necessary. The goal is to
+ record the current frame, thread, and process into the trace
+ immediately, and then to append the trace upon stopping and/or
+ selecting new frames. This action is effective only for the current
+ process. This command must be executed for each individual process
+ you'd like to synchronize. In older versions of x64dbg, certain
+ events cannot be hooked. In that case, you may need to execute
+ certain "trace put" commands manually, or go without.
+
+ This will have no effect unless or until you start a trace.
+ """
+
+ hooks.install_hooks()
+ hooks.enable_current_process()
+
+
+def ghidra_trace_sync_disable() -> None:
+ """Cease synchronizing the current process with the Ghidra trace.
+
+ This is the opposite of 'ghidra_trace_sync-disable', except it will
+ not automatically remove hooks.
+ """
+
+ hooks.disable_current_process()
+
+
+def get_prompt_text() -> str:
+ try:
+ return "dbg>" #util.dbg.get_prompt_text()
+ except util.DebuggeeRunningException:
+ return 'Running>'
+
+
+def exec_cmd(cmd: str) -> None:
+ dbg = util.dbg
+ dbg.cmd(cmd, quiet=False)
+ stat = dbg.exec_status() # type:ignore
+ if stat != 'BREAK':
+ dbg.wait() # type:ignore
+
+
+def repl() -> None:
+ print("")
+ print("This is the Windows Debugger REPL. To drop to Python, type .exit")
+ while True:
+ print(get_prompt_text(), end=' ')
+ try:
+ cmd = input().strip()
+ if cmd == '':
+ continue
+ elif cmd == '.exit':
+ break
+ exec_cmd(cmd)
+ except KeyboardInterrupt as e:
+ util.dbg.interrupt()
+ except BaseException as e:
+ pass # Error is printed by another mechanism
+ print("")
+ print("You have left the Windows Debugger REPL and are now at the Python "
+ "interpreter.")
+ print("To re-enter, type repl()")
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/hooks.py b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/hooks.py
new file mode 100644
index 0000000000..df8f69d9dc
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/hooks.py
@@ -0,0 +1,417 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from bisect import bisect_left, bisect_right
+from dataclasses import dataclass, field
+import functools
+import sys
+import threading
+import time
+import traceback
+from typing import Any, Callable, Collection, Dict, Optional, TypeVar, cast
+
+from ghidratrace.client import Schedule
+from x64dbg_automate.events import EventType
+from x64dbg_automate.models import BreakpointType
+
+from . import commands, util
+
+
+ALL_EVENTS = 0xFFFF
+
+
+@dataclass(frozen=False)
+class HookState:
+ installed = False
+ mem_catchpoint = None
+
+
+@dataclass(frozen=False)
+class ProcessState:
+ first = True
+ # For things we can detect changes to between stops
+ regions = False
+ modules = False
+ threads = False
+ breaks = True
+ watches = False
+ # For frames and threads that have already been synced since last stop
+ visited: set[Any] = field(default_factory=set)
+ waiting = False
+
+ def record(self, description: Optional[str] = None,
+ time: Optional[Schedule] = None) -> None:
+ first = self.first
+ self.first = False
+ trace = commands.STATE.require_trace()
+ if description is not None:
+ trace.snapshot(description, time=time)
+ if first:
+ commands.put_available()
+ commands.put_processes()
+ commands.put_environment()
+ commands.put_threads()
+ if self.threads:
+ commands.put_threads()
+ self.threads = False
+ thread = util.selected_thread()
+ if thread is not None:
+ if first or thread not in self.visited:
+ try:
+ commands.putreg()
+ commands.putmem('0x{:x}'.format(util.get_pc()),
+ "1", display_result=False)
+ commands.putmem('0x{:x}'.format(util.get_sp()-1),
+ "2", display_result=False)
+ commands.put_breakpoints(BreakpointType.BpNormal)
+ commands.put_breakpoints(BreakpointType.BpHardware)
+ commands.put_breakpoints(BreakpointType.BpMemory)
+ except Exception:
+ pass
+ #commands.put_frames()
+ self.visited.add(thread)
+ # TODO: hoping to support this at some point once the relevant APIs are exposed
+ # frame = util.selected_frame()
+ # hashable_frame = (thread, frame)
+ # if first or hashable_frame not in self.visited:
+ # self.visited.add(hashable_frame)
+ try:
+ if first or self.regions or self.modules:
+ commands.put_regions()
+ self.regions = False
+ self.modules = False
+ except:
+ pass
+
+ def record_continued(self) -> None:
+ try:
+ proc = util.selected_process()
+ commands.put_state(proc)
+ commands.put_breakpoints(BreakpointType.BpNormal)
+ commands.put_breakpoints(BreakpointType.BpHardware)
+ commands.put_breakpoints(BreakpointType.BpMemory)
+ except Exception:
+ pass
+
+ def record_exited(self, description: Optional[str] = None,
+ time: Optional[Schedule] = None) -> None:
+ # print("RECORD_EXITED")
+ trace = commands.STATE.require_trace()
+ if description is not None:
+ trace.snapshot(description, time=time)
+ proc = util.selected_process()
+ ipath = commands.PROCESS_PATTERN.format(procnum=proc)
+ procobj = trace.proxy_object_path(ipath)
+ #procobj.set_value('Exit Code', exit_code)
+ procobj.set_value('State', 'TERMINATED')
+
+
+@dataclass(frozen=False)
+class BrkState:
+ break_loc_counts: Dict[int, int] = field(default_factory=dict)
+
+ def update_brkloc_count(self, b, count: int) -> None:
+ self.break_loc_counts[b.GetID()] = count
+
+ def get_brkloc_count(self, b) -> int:
+ return self.break_loc_counts.get(b.GetID(), 0)
+
+ def del_brkloc_count(self, b) -> int:
+ if b not in self.break_loc_counts:
+ return 0 # TODO: Print a warning?
+ count = self.break_loc_counts[b.GetID()]
+ del self.break_loc_counts[b.GetID()]
+ return count
+
+
+HOOK_STATE = HookState()
+BRK_STATE = BrkState()
+PROC_STATE: Dict[int, ProcessState] = {}
+
+
+C = TypeVar('C', bound=Callable)
+
+
+def log_errors(func: C) -> C:
+ """Wrap a function in a try-except that prints and reraises the exception.
+
+ This is needed for exceptions that occur during event callbacks.
+ """
+ @functools.wraps(func)
+ def _func(*args, **kwargs) -> Any:
+ try:
+ return func(*args, **kwargs)
+ except:
+ traceback.print_exc()
+ raise
+ return cast(C, _func)
+
+
+@log_errors
+def on_state_changed(*args) -> None:
+ # print("ON_STATE_CHANGED")
+ ev_type = args[0].event_type
+ # print(ev_type)
+ proc = util.selected_process()
+ if proc not in PROC_STATE:
+ return
+ PROC_STATE[proc].waiting = False
+ trace = commands.STATE.require_trace()
+ with trace.client.batch():
+ with trace.open_tx("State changed proc {}".format(proc)):
+ commands.put_state(proc)
+ try:
+ if ev_type == EventType.EVENT_RESUME_DEBUG:
+ on_cont()
+ elif ev_type == EventType.EVENT_PAUSE_DEBUG:
+ on_stop()
+ except Exception:
+ pass
+
+
+@log_errors
+def on_breakpoint_hit(*args) -> None:
+ # print("ON_THREADS_CHANGED")
+ proc = util.selected_process()
+ if proc not in PROC_STATE:
+ return
+ data = args[0].event_data
+ PROC_STATE[proc].breaks = True
+
+
+
+@log_errors
+def on_new_process(*args) -> None:
+ # print("ON_NEW_PROCESS")
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ with trace.client.batch():
+ with trace.open_tx("New Process {}".format(util.selected_process())):
+ commands.put_processes()
+
+
+def on_process_selected() -> None:
+ # print("PROCESS_SELECTED")
+ proc = util.selected_process()
+ if proc not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ with trace.client.batch():
+ with trace.open_tx("Process {} selected".format(proc)):
+ PROC_STATE[proc].record()
+ commands.activate()
+
+
+@log_errors
+def on_process_deleted(*args) -> None:
+ # print("ON_PROCESS_DELETED")
+ exit_code = args[0]
+ proc = util.selected_process()
+ on_exited(proc)
+ if proc in PROC_STATE:
+ del PROC_STATE[proc]
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ with trace.client.batch():
+ with trace.open_tx("Process {} deleted".format(proc)):
+ commands.put_processes() # TODO: Could just delete the one....
+
+
+@log_errors
+def on_threads_changed(*args) -> None:
+ # print("ON_THREADS_CHANGED")
+ data = args[0].event_data
+ proc = util.selected_process()
+ if proc not in PROC_STATE:
+ return
+ util.threads[data.dwThreadId] = data
+ state = PROC_STATE[proc]
+ state.threads = True
+ state.waiting = False
+ trace = commands.STATE.require_trace()
+ with trace.client.batch():
+ with trace.open_tx("Threads changed proc {}".format(proc)):
+ #commands.put_threads()
+ commands.put_state(proc)
+
+
+
+def on_thread_selected(*args) -> None:
+ # print("THREAD_SELECTED: args={}".format(args))
+ # sys.stdout.flush()
+ nthrd = args[0][1]
+ nproc = util.selected_process()
+ if nproc not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ with trace.client.batch():
+ with trace.open_tx("Thread {}.{} selected".format(nproc, nthrd)):
+ commands.put_state(nproc)
+ state = PROC_STATE[nproc]
+ if state.waiting:
+ state.record_continued()
+ else:
+ state.record()
+ commands.activate()
+
+
+def on_register_changed(regnum) -> None:
+ # print("REGISTER_CHANGED")
+ proc = util.selected_process()
+ if proc not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ with trace.client.batch():
+ with trace.open_tx("Register {} changed".format(regnum)):
+ commands.putreg()
+ commands.activate()
+
+
+def on_memory_changed(space) -> None:
+ proc = util.selected_process()
+ if proc not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ # Not great, but invalidate the whole space
+ # UI will only re-fetch what it needs
+ # But, some observations will not be recovered
+ try:
+ with trace.client.batch():
+ with trace.open_tx("Memory changed"):
+ commands.putmem_state(0, 2**64, 'unknown')
+ except Exception:
+ pass
+
+
+def on_cont(*args) -> None:
+ # print("ON CONT")
+ proc = util.selected_process()
+ if proc not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ state = PROC_STATE[proc]
+ with trace.client.batch():
+ with trace.open_tx("Continued"):
+ state.record_continued()
+ return
+
+
+def on_stop(*args) -> None:
+ # print("ON STOP")
+ proc = util.selected_process()
+ if proc not in PROC_STATE:
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ state = PROC_STATE[proc]
+ state.visited.clear()
+ time = None
+ with trace.client.batch():
+ with trace.open_tx("Stopped"):
+ description = "Stopped"
+ state.record(description, time)
+ try:
+ commands.put_event_thread()
+ except:
+ pass
+ commands.activate()
+
+
+def on_exited(proc) -> None:
+ # print("ON EXITED")
+ if proc not in PROC_STATE:
+ # print("not in state")
+ return
+ trace = commands.STATE.trace
+ if trace is None:
+ return
+ state = PROC_STATE[proc]
+ state.visited.clear()
+ description = "Exited"
+ with trace.client.batch():
+ with trace.open_tx("Exited"):
+ state.record_exited(description)
+ commands.activate()
+
+
+@log_errors
+def on_modules_changed(*args) -> None:
+ # print("ON_MODULES_CHANGED")
+ #data = args[0].event_data
+ proc = util.selected_process()
+ if proc not in PROC_STATE:
+ return
+ state = PROC_STATE[proc]
+ state.modules = True
+ state.waiting = False
+ trace = commands.STATE.require_trace()
+ with trace.client.batch():
+ with trace.open_tx("Modules changed proc {}".format(proc)):
+ #commands.put_modules()
+ commands.put_state(proc)
+
+
+def install_hooks() -> None:
+ # print("Installing hooks")
+ if HOOK_STATE.installed:
+ return
+ HOOK_STATE.installed = True
+
+ dbg = util.dbg.client
+ dbg.watch_debug_event(EventType.EVENT_OUTPUT_DEBUG_STRING, lambda x: on_breakpoint_hit(x))
+ dbg.watch_debug_event(EventType.EVENT_BREAKPOINT, lambda x: on_state_changed(x))
+ dbg.watch_debug_event(EventType.EVENT_SYSTEMBREAKPOINT, lambda x: on_state_changed(x))
+ dbg.watch_debug_event(EventType.EVENT_EXCEPTION, lambda x: on_state_changed(x))
+ dbg.watch_debug_event(EventType.EVENT_CREATE_THREAD, lambda x: on_threads_changed(x))
+ dbg.watch_debug_event(EventType.EVENT_EXIT_THREAD, lambda x: on_threads_changed(x))
+ dbg.watch_debug_event(EventType.EVENT_LOAD_DLL, lambda x: on_modules_changed(x))
+ dbg.watch_debug_event(EventType.EVENT_UNLOAD_DLL, lambda x: on_modules_changed(x))
+ dbg.watch_debug_event(EventType.EVENT_STEPPED, lambda x: on_state_changed(x))
+ dbg.watch_debug_event(EventType.EVENT_PAUSE_DEBUG, lambda x: on_state_changed(x))
+ dbg.watch_debug_event(EventType.EVENT_RESUME_DEBUG, lambda x: on_state_changed(x))
+ #dbg.watch_debug_event(EventType.EVENT_DEBUG, lambda x: on_state_changed(x))
+
+
+def remove_hooks() -> None:
+ # print("Removing hooks")
+ if HOOK_STATE.installed:
+ HOOK_STATE.installed = False
+
+
+def enable_current_process() -> None:
+ # print("Enable current process")
+ proc = util.selected_process()
+ PROC_STATE[proc] = ProcessState()
+
+
+def disable_current_process() -> None:
+ proc = util.selected_process()
+ if proc in PROC_STATE:
+ # Silently ignore already disabled
+ del PROC_STATE[proc]
+
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/methods.py b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/methods.py
new file mode 100644
index 0000000000..d3fa95740c
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/methods.py
@@ -0,0 +1,680 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from concurrent.futures import Future, ThreadPoolExecutor
+from contextlib import redirect_stdout
+from io import StringIO
+import re
+import sys
+from typing import Annotated, Any, Dict, Optional
+
+from ghidratrace import sch
+from ghidratrace.client import (MethodRegistry, ParamDesc, Address,
+ AddressRange, Schedule, TraceObject)
+
+from x64dbg_automate.events import *
+from x64dbg_automate.models import BreakpointType, HardwareBreakpointType, MemoryBreakpointType
+from . import util, commands
+
+REGISTRY = MethodRegistry(ThreadPoolExecutor(
+ max_workers=1, thread_name_prefix='MethodRegistry'))
+
+
+def extre(base: re.Pattern, ext: str) -> re.Pattern:
+ return re.compile(base.pattern + ext)
+
+
+WATCHPOINT_PATTERN = re.compile('Watchpoints\\[(?P\\d*)\\]')
+BREAKPOINT_PATTERN = re.compile('Breakpoints\\[(?P\\d*)\\]')
+BREAK_LOC_PATTERN = extre(BREAKPOINT_PATTERN, '\\[(?P\\d*)\\]')
+SESSIONS_PATTERN = re.compile('Sessions')
+SESSION_PATTERN = extre(SESSIONS_PATTERN, '\\[(?P\\d*)\\]')
+AVAILABLE_PATTERN = extre(SESSION_PATTERN, '\\.Available\\[(?P\\d*)\\]')
+PROCESSES_PATTERN = extre(SESSION_PATTERN, '\\.Processes')
+PROCESS_PATTERN = extre(PROCESSES_PATTERN, '\\[(?P\\d*)\\]')
+PROC_DEBUG_PATTERN = extre(PROCESS_PATTERN, '.Debug')
+PROC_SBREAKS_PATTERN = extre(PROC_DEBUG_PATTERN, '\\.Software Breakpoints')
+PROC_HBREAKS_PATTERN = extre(PROC_DEBUG_PATTERN, '\\.Hardware Breakpoints')
+PROC_MBREAKS_PATTERN = extre(PROC_DEBUG_PATTERN, '\\.Memory Breakpoints')
+PROC_SBREAKBPT_PATTERN = extre(PROC_SBREAKS_PATTERN, '\\[(?P\\d*)\\]')
+PROC_HBREAKBPT_PATTERN = extre(PROC_HBREAKS_PATTERN, '\\[(?P\\d*)\\]')
+PROC_MBREAKBPT_PATTERN = extre(PROC_MBREAKS_PATTERN, '\\[(?P\\d*)\\]')
+ENV_PATTERN = extre(PROCESS_PATTERN, '\\.Environment')
+THREADS_PATTERN = extre(PROCESS_PATTERN, '\\.Threads')
+THREAD_PATTERN = extre(THREADS_PATTERN, '\\[(?P\\d*)\\]')
+STACK_PATTERN = extre(THREAD_PATTERN, '\\.Stack.Frames')
+#FRAME_PATTERN = extre(STACK_PATTERN, '\\[(?P\\d*)\\]')
+REGS_PATTERN0 = extre(THREAD_PATTERN, '\\.Registers')
+#REGS_PATTERN = extre(FRAME_PATTERN, '\\.Registers')
+MEMORY_PATTERN = extre(PROCESS_PATTERN, '\\.Memory')
+MODULES_PATTERN = extre(PROCESS_PATTERN, '\\.Modules')
+
+
+def find_availpid_by_pattern(pattern: re.Pattern, object: TraceObject,
+ err_msg: str) -> int:
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ pid = int(mat['pid'])
+ return pid
+
+
+def find_availpid_by_obj(object: TraceObject) -> int:
+ return find_availpid_by_pattern(AVAILABLE_PATTERN, object, "an Attachable")
+
+
+def find_proc_by_num(id: int) -> int:
+ return util.selected_process()
+
+
+def find_proc_by_pattern(object: TraceObject, pattern: re.Pattern,
+ err_msg: str) -> int:
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ procnum = int(mat['procnum'])
+ return find_proc_by_num(procnum)
+
+
+def find_proc_by_obj(object: TraceObject) -> int:
+ return find_proc_by_pattern(object, PROCESS_PATTERN, "an Process")
+
+
+def find_proc_by_env_obj(object: TraceObject) -> int:
+ return find_proc_by_pattern(object, ENV_PATTERN, "an Environment")
+
+
+def find_proc_by_threads_obj(object: TraceObject) -> int:
+ return find_proc_by_pattern(object, THREADS_PATTERN, "a ThreadContainer")
+
+
+def find_proc_by_mem_obj(object: TraceObject) -> int:
+ return find_proc_by_pattern(object, MEMORY_PATTERN, "a Memory")
+
+
+def find_proc_by_modules_obj(object: TraceObject) -> int:
+ return find_proc_by_pattern(object, MODULES_PATTERN, "a ModuleContainer")
+
+
+def find_thread_by_num(id: int) -> Optional[int]:
+ if id != util.selected_thread():
+ util.select_thread(id)
+ return util.selected_thread()
+
+
+def find_thread_by_pattern(pattern: re.Pattern, object: TraceObject,
+ err_msg: str) -> Optional[int]:
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ raise TypeError(f"{object} is not {err_msg}")
+ pnum = int(mat['procnum'])
+ tnum = int(mat['tnum'])
+ find_proc_by_num(pnum)
+ return find_thread_by_num(tnum)
+
+
+def find_thread_by_obj(object: TraceObject) -> Optional[int]:
+ return find_thread_by_pattern(THREAD_PATTERN, object, "a Thread")
+
+
+def find_thread_by_stack_obj(object: TraceObject) -> Optional[int]:
+ return find_thread_by_pattern(STACK_PATTERN, object, "a Stack")
+
+
+def find_thread_by_regs_obj(object: TraceObject) -> Optional[int]:
+ return find_thread_by_pattern(REGS_PATTERN0, object,
+ "a RegisterValueContainer")
+
+
+# TODO: if eventually exposed...
+# def find_frame_by_level(level: int) -> DbgEng._DEBUG_STACK_FRAME:
+# for f in util.dbg.client.backtrace_list():
+# if f.FrameNumber == level:
+# return f
+# # return dbg().backtrace_list()[level]
+#
+#
+# def find_frame_by_pattern(pattern: re.Pattern, object: TraceObject,
+# err_msg: str) -> DbgEng._DEBUG_STACK_FRAME:
+# mat = pattern.fullmatch(object.path)
+# if mat is None:
+# raise TypeError(f"{object} is not {err_msg}")
+# pnum = int(mat['procnum'])
+# tnum = int(mat['tnum'])
+# level = int(mat['level'])
+# find_proc_by_num(pnum)
+# find_thread_by_num(tnum)
+# return find_frame_by_level(level)
+#
+#
+# def find_frame_by_obj(object: TraceObject) -> DbgEng._DEBUG_STACK_FRAME:
+# return find_frame_by_pattern(FRAME_PATTERN, object, "a StackFrame")
+
+
+def find_bpt_by_pattern(pattern: re.Pattern, object: TraceObject,
+ err_msg: str) -> int:
+ mat = pattern.fullmatch(object.path)
+ if mat is None:
+ return -1 #raise TypeError(f"{object} is not {err_msg}")
+ breaknum = int(mat['breaknum'])
+ return breaknum
+
+
+def find_sbpt_by_obj(object: TraceObject) -> int:
+ return find_bpt_by_pattern(PROC_SBREAKBPT_PATTERN, object, "a BreakpointSpec")
+
+
+def find_hbpt_by_obj(object: TraceObject) -> int:
+ return find_bpt_by_pattern(PROC_HBREAKBPT_PATTERN, object, "a BreakpointSpec")
+
+
+def find_mbpt_by_obj(object: TraceObject) -> int:
+ return find_bpt_by_pattern(PROC_MBREAKBPT_PATTERN, object, "a BreakpointSpec")
+
+
+shared_globals: Dict[str, Any] = dict()
+
+
+class Session(TraceObject):
+ pass
+
+
+class AvailableContainer(TraceObject):
+ pass
+
+
+class BreakpointContainer(TraceObject):
+ pass
+
+
+class ProcessContainer(TraceObject):
+ pass
+
+
+class Environment(TraceObject):
+ pass
+
+
+class ThreadContainer(TraceObject):
+ pass
+
+
+class Stack(TraceObject):
+ pass
+
+
+class RegisterValueContainer(TraceObject):
+ pass
+
+
+class Memory(TraceObject):
+ pass
+
+
+class ModuleContainer(TraceObject):
+ pass
+
+
+class State(TraceObject):
+ pass
+
+
+class Process(TraceObject):
+ pass
+
+
+class Thread(TraceObject):
+ pass
+
+
+class StackFrame(TraceObject):
+ pass
+
+
+class Attachable(TraceObject):
+ pass
+
+
+class BreakpointSpec(TraceObject):
+ pass
+
+
+class EventContainer(TraceObject):
+ pass
+
+
+class ExceptionContainer(TraceObject):
+ pass
+
+
+class ContinueOption(TraceObject):
+ pass
+
+
+class ExecutionOption(TraceObject):
+ pass
+
+
+@REGISTRY.method()
+def execute(cmd: str, to_string: bool=False):
+ """Execute a Python3 command or script."""
+ # print("***{}***".format(cmd))
+ # sys.stderr.flush()
+ # sys.stdout.flush()
+ if to_string:
+ data = StringIO()
+ with redirect_stdout(data):
+ exec(cmd, shared_globals)
+ return data.getvalue()
+ else:
+ exec(cmd, shared_globals)
+
+
+@REGISTRY.method(action='evaluate', display='Evaluate')
+def evaluate(
+ session: Session,
+ expr: Annotated[str, ParamDesc(display='Expr')]) -> str:
+ """Evaluate a Python3 expression."""
+ return str(eval(expr, shared_globals))
+
+
+@REGISTRY.method(action='refresh', display='Refresh Available')
+def refresh_available(node: AvailableContainer) -> None:
+ """List processes on x64dbg's host system."""
+ with commands.open_tracked_tx('Refresh Available'):
+ commands.ghidra_trace_put_available()
+
+
+@REGISTRY.method(action='refresh', display='Refresh Breakpoints')
+def refresh_breakpoints(node: BreakpointContainer) -> None:
+ """Refresh the list of breakpoints (including locations for the current
+ process)."""
+ with commands.open_tracked_tx('Refresh Breakpoints'):
+ commands.ghidra_trace_put_breakpoints()
+
+
+@REGISTRY.method(action='refresh', display='Refresh Processes')
+def refresh_processes(node: ProcessContainer) -> None:
+ """Refresh the list of processes."""
+ with commands.open_tracked_tx('Refresh Processes'):
+ commands.ghidra_trace_put_processes()
+
+
+@REGISTRY.method(action='refresh', display='Refresh Environment')
+def refresh_environment(node: Environment) -> None:
+ """Refresh the environment descriptors (arch, os, endian)."""
+ with commands.open_tracked_tx('Refresh Environment'):
+ commands.ghidra_trace_put_environment()
+
+
+@REGISTRY.method(action='refresh', display='Refresh Threads')
+def refresh_threads(node: ThreadContainer) -> None:
+ """Refresh the list of threads in the process."""
+ with commands.open_tracked_tx('Refresh Threads'):
+ commands.ghidra_trace_put_threads()
+
+
+# TODO: if eventually exposed...
+# @REGISTRY.method(action='refresh', display='Refresh Stack')
+# def refresh_stack(node: Stack) -> None:
+# """Refresh the backtrace for the thread."""
+# tnum = find_thread_by_stack_obj(node)
+# util.reset_frames()
+# with commands.open_tracked_tx('Refresh Stack'):
+# commands.ghidra_trace_put_frames()
+# with commands.open_tracked_tx('Refresh Registers'):
+# commands.ghidra_trace_putreg()
+
+
+@REGISTRY.method(action='refresh', display='Refresh Registers')
+def refresh_registers(node: RegisterValueContainer) -> None:
+ """Refresh the register values for the selected frame."""
+ tnum = find_thread_by_regs_obj(node)
+ with commands.open_tracked_tx('Refresh Registers'):
+ commands.ghidra_trace_putreg()
+
+
+@REGISTRY.method(action='refresh', display='Refresh Memory')
+def refresh_mappings(node: Memory) -> None:
+ """Refresh the list of memory regions for the process."""
+ with commands.open_tracked_tx('Refresh Memory Regions'):
+ commands.ghidra_trace_put_regions()
+
+
+@REGISTRY.method(action='refresh', display='Refresh Modules')
+def refresh_modules(node: ModuleContainer) -> None:
+ """Refresh the modules and sections list for the process.
+
+ This will refresh the sections for all modules, not just the
+ selected one.
+ """
+ with commands.open_tracked_tx('Refresh Modules'):
+ commands.ghidra_trace_put_modules()
+
+
+@REGISTRY.method(action='activate')
+def activate_process(process: Process,
+ time: Optional[str]=None) -> None:
+ """Switch to the process."""
+ find_proc_by_obj(process)
+
+
+@REGISTRY.method(action='activate')
+def activate_thread(thread: Thread,
+ time: Optional[str]=None) -> None:
+ """Switch to the thread."""
+ find_thread_by_obj(thread)
+
+
+# TODO: if eventually exposed...
+# @REGISTRY.method(action='activate')
+# def activate_frame(frame: StackFrame,
+# time: Optional[str]=None) -> None:
+# """Select the frame."""
+# do_maybe_activate_time(time)
+# f = find_frame_by_obj(frame)
+# util.select_frame(f.FrameNumber)
+# with commands.open_tracked_tx('Refresh Stack'):
+# commands.ghidra_trace_put_frames()
+# with commands.open_tracked_tx('Refresh Registers'):
+# commands.ghidra_trace_putreg()
+
+
+@REGISTRY.method(action='delete')
+def remove_process(process: Process) -> None:
+ """Remove the process."""
+ dbg().detach()
+
+
+@REGISTRY.method(action='attach', display='Attach')
+def attach_obj(target: Attachable) -> None:
+ """Attach the process to the given target."""
+ pid = find_availpid_by_obj(target)
+ dbg().attach(pid)
+ #dbg().wait_until_debugging()
+ commands.ghidra_trace_stop()
+ commands.ghidra_trace_start(str(pid))
+ commands.ghidra_trace_sync_enable()
+ with commands.open_tracked_tx('Put all'):
+ commands.ghidra_trace_put_all()
+
+
+@REGISTRY.method(action='attach', display='Attach by pid')
+def attach_pid(session: Session,
+ pid: Annotated[int, ParamDesc(display='PID')]) -> None:
+ """Attach the process to the given target."""
+ commands.ghidra_trace_stop()
+ commands.ghidra_trace_start(str(pid))
+ commands.ghidra_trace_sync_enable()
+ with commands.open_tracked_tx('Put all'):
+ commands.ghidra_trace_put_all()
+
+
+@REGISTRY.method(action='detach', display='Detach')
+def detach(process: Process) -> None:
+ """Detach the process's target."""
+ dbg().detach()
+
+
+@REGISTRY.method(action='launch', display='Launch')
+def launch(
+ Session: Session,
+ file: Annotated[str, ParamDesc(display='Image')],
+ args: Annotated[str, ParamDesc(display='Arguments')]='',
+ initial_dir: Annotated[str, ParamDesc(
+ display='Initial Directory')]='',
+ wait: Annotated[bool, ParamDesc(
+ display='Wait',
+ description='Perform the initial WaitForEvents')]=False) -> None:
+ """Run a native process with the given command line."""
+ commands.ghidra_trace_stop()
+ commands.ghidra_trace_create(command=file, args=args, initdir=initial_dir, start_trace=True, wait=wait)
+ commands.ghidra_trace_sync_enable()
+ with commands.open_tracked_tx('Put all'):
+ commands.ghidra_trace_put_all()
+
+
+@REGISTRY.method()
+def kill(process: Process) -> None:
+ """Kill execution of the process."""
+ commands.ghidra_trace_kill()
+
+
+@REGISTRY.method(action='resume', display='Go')
+def go(process: Process) -> None:
+ """Continue execution of the process."""
+ dbg().go()
+ proc = util.selected_process()
+ trace = commands.STATE.require_trace()
+ with trace.client.batch():
+ with trace.open_tx("Go proc {}".format(proc)):
+ commands.put_state(proc)
+
+
+@REGISTRY.method()
+def interrupt(process: Process) -> None:
+ """Interrupt the execution of the debugged program."""
+ # SetInterrupt is reentrant, so bypass the thread checks
+ dbg().pause()
+
+
+@REGISTRY.method(action='Hide PEB')
+def hide_peb(process: Process) -> None:
+ """Interrupt the execution of the debugged program."""
+ # SetInterrupt is reentrant, so bypass the thread checks
+ dbg().hide_debugger_peb()
+
+
+@REGISTRY.method(action='step_into')
+def step_into(thread: Thread,
+ n: Annotated[int, ParamDesc(display='N')]=1) -> None:
+ """Step one instruction exactly."""
+ # find_thread_by_obj(thread)
+ find_thread_by_obj(thread)
+ dbg().stepi(n)
+
+
+@REGISTRY.method(action='step_over')
+def step_over(thread: Thread,
+ n: Annotated[int, ParamDesc(display='N')]=1) -> None:
+ """Step one instruction, but proceed through subroutine calls."""
+ # find_thread_by_obj(thread)
+ find_thread_by_obj(thread)
+ dbg().stepo(n)
+
+
+@REGISTRY.method(action='skip', display='Skip')
+def skip(thread: Thread,
+ n: Annotated[int, ParamDesc(display='N')]=1) -> None:
+ """Step one instruction, but proceed through subroutine calls."""
+ # find_thread_by_obj(thread)
+ find_thread_by_obj(thread)
+ dbg().skip(n)
+
+
+@REGISTRY.method(action='step_out')
+def step_out(thread: Thread) -> None:
+ """Execute until the current stack frame returns."""
+ find_thread_by_obj(thread)
+ dbg().ret()
+
+
+@REGISTRY.method(action='pause_thread', display='Pause')
+def pause_thread(thread: Thread) -> None:
+ """Execute until the current stack frame returns."""
+ tid = find_thread_by_obj(thread)
+ if tid is not None:
+ dbg().thread_pause(tid)
+
+
+@REGISTRY.method(action='resume_thread', display='Resume')
+def resume_thread(thread: Thread) -> None:
+ """Execute until the current stack frame returns."""
+ tid = find_thread_by_obj(thread)
+ if tid is not None:
+ dbg().thread_resume(tid)
+
+
+@REGISTRY.method(action='kill_thread', display='Kill')
+def kill_thread(thread: Thread) -> None:
+ """Execute until the current stack frame returns."""
+ tid = find_thread_by_obj(thread)
+ if tid is not None:
+ dbg().thread_terminate(tid)
+
+
+@REGISTRY.method(action='break_sw_execute')
+def break_address(process: Process, address: Address) -> None:
+ """Set a breakpoint."""
+ find_proc_by_obj(process)
+ dbg().set_breakpoint(address_or_symbol=address.offset)
+
+
+@REGISTRY.method(action='break_ext', display='Set Breakpoint')
+def break_expression(expression: str) -> None:
+ """Set a breakpoint."""
+ # TODO: Escape?
+ dbg().set_breakpoint(address_or_symbol=expression)
+
+
+@REGISTRY.method(action='break_hw_execute')
+def break_hw_address(process: Process, address: Address) -> None:
+ """Set a hardware-assisted breakpoint."""
+ find_proc_by_obj(process)
+ dbg().set_hardware_breakpoint(address_or_symbol=address.offset)
+
+
+@REGISTRY.method(action='break_ext', display='Set Hardware Breakpoint')
+def break_hw_expression(expression: str) -> None:
+ """Set a hardware-assisted breakpoint."""
+ dbg().set_hardware_breakpoint(address_or_symbol=expression)
+
+
+@REGISTRY.method(action='break_read')
+def break_read_address(process: Process, address: Address, size: int) -> None:
+ """Set a read breakpoint."""
+ find_proc_by_obj(process)
+ dbg().set_hardware_breakpoint(address_or_symbol=address.offset, bp_type=HardwareBreakpointType.r, size=size)
+
+
+@REGISTRY.method(action='break_ext', display='Set Read Breakpoint')
+def break_read_expression(expression: str) -> None:
+ """Set a read breakpoint."""
+ dbg().set_hardware_breakpoint(address_or_symbol=expression, bp_type=HardwareBreakpointType.r)
+
+
+@REGISTRY.method(action='break_write')
+def break_write_address(process: Process, address: Address, size: int) -> None:
+ """Set a write breakpoint."""
+ find_proc_by_obj(process)
+ dbg().set_hardware_breakpoint(address_or_symbol=address.offset, bp_type=HardwareBreakpointType.w, size=size)
+
+
+@REGISTRY.method(action='break_ext', display='Set Write Breakpoint')
+def break_write_expression(expression: str) -> None:
+ """Set a write breakpoint."""
+ dbg().set_hardware_breakpoint(address_or_symbol=expression, bp_type=HardwareBreakpointType.w)
+
+
+@REGISTRY.method(action='break_access')
+def break_access_address(process: Process, address: Address) -> None:
+ """Set an access breakpoint."""
+ find_proc_by_obj(process)
+ dbg().set_memory_breakpoint(address_or_symbol=address.offset, bp_type=MemoryBreakpointType.a)
+
+
+@REGISTRY.method(action='break_ext', display='Set Access Breakpoint')
+def break_access_expression(expression: str) -> None:
+ """Set an access breakpoint."""
+ dbg().set_memory_breakpoint(address_or_symbol=expression, bp_type=MemoryBreakpointType.a)
+
+
+@REGISTRY.method(action='toggle', display='Toggle Breakpoint')
+def toggle_breakpoint(breakpoint: BreakpointSpec, enabled: bool) -> None:
+ """Toggle a breakpoint."""
+ bpt = find_sbpt_by_obj(breakpoint)
+ if bpt >= 0:
+ dbg().toggle_breakpoint(address_name_symbol_or_none=bpt, on=enabled)
+ with commands.open_tracked_tx('Toggle Breakpoints'):
+ commands.put_breakpoints(BreakpointType.BpNormal)
+ return
+ bpt = find_hbpt_by_obj(breakpoint)
+ if bpt >= 0:
+ dbg().toggle_breakpoint(address_name_symbol_or_none=bpt, on=enabled)
+ with commands.open_tracked_tx('Toggle Breakpoints'):
+ commands.put_breakpoints(BreakpointType.BpHardware)
+ return
+ bpt = find_mbpt_by_obj(breakpoint)
+ if bpt >= 0:
+ dbg().toggle_breakpoint(address_name_symbol_or_none=bpt, on=enabled)
+ with commands.open_tracked_tx('Toggle Breakpoints'):
+ commands.put_breakpoints(BreakpointType.BpMemory)
+
+
+@REGISTRY.method(action='delete', display='Delete Breakpoint')
+def delete_breakpoint(breakpoint: BreakpointSpec) -> None:
+ """Delete a breakpoint."""
+ bpt = find_sbpt_by_obj(breakpoint)
+ if bpt >= 0:
+ dbg().clear_breakpoint(address_name_symbol_or_none=bpt)
+ with commands.open_tracked_tx('Delete Breakpoints'):
+ commands.put_breakpoints(BreakpointType.BpNormal)
+ return
+ bpt = find_hbpt_by_obj(breakpoint)
+ if bpt >= 0:
+ dbg().clear_hardware_breakpoint(address_symbol_or_none=bpt)
+ with commands.open_tracked_tx('Delete Breakpoints'):
+ commands.put_breakpoints(BreakpointType.BpHardware)
+ return
+ bpt = find_mbpt_by_obj(breakpoint)
+ if bpt >= 0:
+ dbg().clear_memory_breakpoint(address_symbol_or_none=bpt)
+ with commands.open_tracked_tx('Delete Breakpoints'):
+ commands.put_breakpoints(BreakpointType.BpMemory)
+
+
+@REGISTRY.method()
+def read_mem(process: Process, range: AddressRange) -> None:
+ """Read memory."""
+ # print("READ_MEM: process={}, range={}".format(process, range))
+ nproc = find_proc_by_obj(process)
+ offset_start = process.trace.extra.require_mm().map_back(
+ nproc, Address(range.space, range.min))
+ with commands.open_tracked_tx('Read Memory'):
+ result = commands.put_bytes(
+ offset_start, offset_start + range.length() - 1, pages=True,
+ display_result=False)
+ if result['count'] == 0:
+ commands.putmem_state(
+ offset_start, offset_start + range.length() - 1, 'error')
+
+
+@REGISTRY.method()
+def write_mem(process: Process, address: Address, data: bytes) -> None:
+ """Write memory."""
+ nproc = find_proc_by_obj(process)
+ offset = process.trace.extra.required_mm().map_back(nproc, address)
+ dbg().write_memory(offset, data)
+
+
+@REGISTRY.method(action='set_reg', display='Set Register')
+def write_reg(reg: RegisterValueContainer, name: str, value: int) -> None:
+ """Write a register."""
+ nproc = util.selected_process()
+ dbg().set_reg(name, value)
+
+
+def dbg():
+ return util.dbg.client
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/py.typed b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/py.typed
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/schema.xml b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/schema.xml
new file mode 100644
index 0000000000..a477772f39
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/schema.xml
@@ -0,0 +1,317 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/util.py b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/util.py
new file mode 100644
index 0000000000..f28b00c339
--- /dev/null
+++ b/Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/util.py
@@ -0,0 +1,292 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from ghidratrace.client import Schedule
+from collections import namedtuple
+from ctypes import POINTER, byref, c_ulong, c_ulonglong, create_string_buffer
+import functools
+import io
+import os
+import queue
+import psutil
+import re
+import sys
+import threading
+import traceback
+from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, TypeVar, Union, cast
+
+from x64dbg_automate import X64DbgClient
+from x64dbg_automate.events import CreateThreadEventData
+from x64dbg_automate.models import Context32, Context64, Instruction, MemPage, RegDump
+
+DbgVersion = namedtuple('DbgVersion', ['full', 'name', 'dotted', 'arch'])
+conv_map: Dict[str, str] = {}
+threads: Dict[int, CreateThreadEventData] = {}
+
+class DebuggeeRunningException(BaseException):
+ pass
+
+class GhidraDbg(object):
+
+ def __init__(self) -> None:
+ self._new_base()
+ client = self._client
+ client.start_session()
+
+ def _new_base(self) -> None:
+ executable = os.getenv('OPT_X64DBG_EXE')
+ if executable is None:
+ return
+ self._client = X64DbgClient(executable)
+
+ @property
+ def client(self) -> X64DbgClient:
+ return self._client
+
+ def cmd(self, cmdline: str, quiet: bool = True) -> str:
+ # Here, we let it print without capture if quiet is False
+ if quiet:
+ buffer = io.StringIO()
+ #self.client.callbacks.stdout = buffer
+ self.client.cmd_sync(cmdline)
+ return "completed"
+ else:
+ self.client.cmd_sync(cmdline)
+ return ""
+
+ def wait(self) -> None:
+ self._client.wait_until_stopped()
+
+ def interrupt(self) -> None:
+ self._client.pause()
+
+ def eval(self, input: str) -> Optional[list[int]]:
+ try:
+ return self._client.eval_sync(input)
+ except:
+ return None
+
+ def get_actual_processor_type(self) -> int:
+ return self.client.debugee_bitness()
+
+ @property
+ def pid(self) -> Optional[int]:
+ try:
+ return self.client.get_debugger_pid()
+ except:
+ # There is no process
+ return None
+
+
+dbg = GhidraDbg()
+
+
+def compute_dbg_ver() -> DbgVersion:
+ ver = dbg.client.get_debugger_version()
+ executable = os.getenv('OPT_X64DBG_EXE')
+ bitness = dbg.client.debugee_bitness()
+ return DbgVersion('Unknown', 'Unknown', ver, 'x{}'.format(bitness))
+
+
+DBG_VERSION = compute_dbg_ver()
+
+
+def get_target():
+ return 0 #dbg.get_current_system_id()
+
+
+def disassemble1(addr: int) -> Instruction | None:
+ return dbg.client.disassemble_at(addr)
+
+
+def get_inst(addr: int) -> Instruction | None:
+ return disassemble1(addr)
+
+
+def get_inst_sz(addr: int) -> int:
+ inst = disassemble1(addr)
+ if inst is None:
+ return 0
+ return int(inst.instr_size)
+
+
+def selected_process() -> int:
+ try:
+ pid = dbg.client.debugee_pid()
+ return pid
+ except:
+ # NB: we're intentionally returning 0 instead of None
+ return 0
+
+
+def selected_process_space() -> int:
+ try:
+ return selected_process()
+ except:
+ # NB: we're intentionally returning 0 instead of None
+ return 0
+
+
+def selected_thread() -> Optional[int]:
+ try:
+ ev = dbg.eval('tid()')
+ if ev is None:
+ return None
+ return ev[0]
+ except:
+ return None
+
+
+def selected_frame() -> Optional[int]:
+ try:
+ line = dbg.cmd('.frame').strip()
+ if not line:
+ return None
+ num_str = line.split(sep=None, maxsplit=1)[0]
+ return int(num_str, 16)
+ except OSError:
+ return None
+ except ValueError:
+ return None
+
+
+def select_thread(id: int) -> bool:
+ return dbg.client.switch_thread(id)
+
+
+def select_frame(id: int) -> str:
+ return dbg.cmd('.frame /c {}'.format(id))
+
+
+def reset_frames() -> str:
+ return dbg.cmd('.cxr')
+
+
+def parse_and_eval(expr: Union[str, int],
+ type: Optional[int] = None) -> Union[int, float, bytes]:
+ if isinstance(expr, int):
+ return expr
+ return int(expr, 16)
+
+
+def get_pc() -> int:
+ ctxt = dbg.client.get_regs().context
+ if hasattr(ctxt, 'rip'):
+ return ctxt.rip
+ else:
+ return ctxt.eip
+
+
+def get_sp() -> int:
+ ctxt = dbg.client.get_regs().context
+ if hasattr(ctxt, 'rsp'):
+ return ctxt.rsp
+ else:
+ return ctxt.esp
+
+
+def process_list0(running: bool = False) -> Union[
+ Iterable[Tuple[int, str, int]], Iterable[Tuple[int]]]:
+ """Get the list of all processes."""
+ nproc = selected_process()
+ proc = psutil.Process(nproc)
+ sysids = []
+ names = []
+
+ try:
+ sysids.append(nproc)
+ names.append(proc.name())
+ return zip(sysids, names)
+ except Exception:
+ return zip(sysids)
+
+
+def process_list(running: bool = False) -> Union[
+ Iterable[Tuple[int, str, int]], Iterable[Tuple[int]]]:
+ """Get the list of all processes."""
+ sysids = []
+ names = []
+
+ try:
+ for pid in psutil.pids():
+ sysids.append(pid)
+ proc = psutil.Process(pid)
+ names.append(proc.name())
+ return zip(sysids, names)
+ except Exception:
+ return zip(sysids)
+
+
+def thread_list(running: bool = False) -> Union[
+ Iterable[Tuple[int, int, str]], Iterable[Tuple[int]]]:
+ """Get the list of all threads."""
+ nproc = selected_process()
+ proc = psutil.Process(nproc)
+ sysids = []
+
+ try:
+ for t in proc.threads():
+ sysids.append(t.id)
+ return zip(sysids)
+ except Exception:
+ return zip(sysids)
+
+
+def full_mem() -> List[MemPage]:
+ return []
+
+
+def split_path(pathString: str) -> List[str]:
+ list = []
+ segs = pathString.split(".")
+ for s in segs:
+ if s.endswith("]"):
+ if "[" not in s:
+ print(f"Missing terminator: {s}")
+ index = s.index("[")
+ list.append(s[:index])
+ list.append(s[index:])
+ else:
+ list.append(s)
+ return list
+
+
+def get_kind(obj) -> Optional[int]:
+ """Get the kind."""
+ if obj is None:
+ return None
+ kind = obj.GetKind()
+ if kind is None:
+ return None
+ return obj.GetKind().value
+
+
+def terminate_session() -> None:
+ dbg.client.terminate_session()
+
+
+def get_convenience_variable(id: str) -> Any:
+ if id not in conv_map:
+ return "auto"
+ val = conv_map[id]
+ if val is None:
+ return "auto"
+ return val
+
+
+def set_convenience_variable(id: str, value: Any) -> None:
+ conv_map[id] = value
+
diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/x64dbg/rmi/AbstractX64dbgTraceRmiTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/x64dbg/rmi/AbstractX64dbgTraceRmiTest.java
new file mode 100644
index 0000000000..61f7e00923
--- /dev/null
+++ b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/x64dbg/rmi/AbstractX64dbgTraceRmiTest.java
@@ -0,0 +1,512 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package agent.x64dbg.rmi;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.assumeTrue;
+
+import java.io.*;
+import java.net.*;
+import java.nio.file.*;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.function.Function;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
+import ghidra.app.plugin.core.debug.service.tracermi.TraceRmiPlugin;
+import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
+import ghidra.app.services.TraceRmiService;
+import ghidra.debug.api.tracermi.*;
+import ghidra.framework.*;
+import ghidra.framework.main.ApplicationLevelOnlyPlugin;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.plugintool.Plugin;
+import ghidra.framework.plugintool.PluginsConfiguration;
+import ghidra.framework.plugintool.util.*;
+import ghidra.program.model.address.Address;
+import ghidra.program.model.address.AddressRangeImpl;
+import ghidra.pty.testutil.DummyProc;
+import ghidra.trace.model.Lifespan;
+import ghidra.trace.model.breakpoint.TraceBreakpointKind;
+import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
+import ghidra.trace.model.target.TraceObject;
+import ghidra.trace.model.target.TraceObjectValue;
+import ghidra.util.*;
+
+public abstract class AbstractX64dbgTraceRmiTest extends AbstractGhidraHeadedDebuggerTest {
+ /**
+ * Some features have to be disabled to avoid permissions issues in the test container. Namely,
+ * don't try to disable ASLR.
+ */
+ public static final String PREAMBLE = """
+ from ghidraxdbg.commands import *
+ from ghidratrace.client import Schedule
+ from x64dbg_automate.models import *
+ """;
+ // Connecting should be the first thing the script does, so use a tight timeout.
+ protected static final int CONNECT_TIMEOUT_MS = 3000;
+ protected static final int TIMEOUT_SECONDS = 300;
+ protected static final int QUIT_TIMEOUT_MS = 1000;
+
+ /** Some snapshot likely to exceed the latest */
+ protected static final long SNAP = 100;
+
+ protected static boolean didSetupPython = false;
+
+ public static final String NOTEPAD = "C:\\\\Windows\\\\notepad.exe";
+ public static final String INSTRUMENT_STATE = """
+ import sys
+ from ghidraxdbg import commands
+ from x64dbg_automate.events import *
+ print("Instrumenting")
+ def on_state_changed(*args):
+ print("State changed")
+ sys.stdout.flush()
+ proc = util.selected_process()
+ trace = commands.STATE.trace
+ with commands.STATE.client.batch():
+ with trace.open_tx("State changed proc {}".format(proc)):
+ commands.put_state(proc)
+ return
+
+ def install_hooks():
+ print("Installing")
+ util.dbg.client.watch_debug_event(EventType.EVENT_DEBUG, lambda x: on_state_changed(x))
+
+ install_hooks()
+ """;
+
+ protected TraceRmiService traceRmi;
+ private Path pythonPath;
+ private Path outFile;
+ private Path errFile;
+
+ @BeforeClass
+ public static void setupPython() throws Throwable {
+ if (didSetupPython) {
+ // Only do this once when running the full suite.
+ return;
+ }
+ if (SystemUtilities.isInTestingBatchMode()) {
+ // Don't run gradle in gradle. It already did this task.
+ return;
+ }
+ String gradle = DummyProc.which("gradle.bat");
+ new ProcessBuilder(gradle, "assemblePyPackage")
+ .directory(TestApplicationUtils.getInstallationDirectory())
+ .inheritIO()
+ .start()
+ .waitFor();
+ didSetupPython = true;
+ }
+
+ protected void setPythonPath(ProcessBuilder pb) throws IOException {
+ String sep =
+ OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS ? ";" : ":";
+ String rmiPyPkg = Application.getModuleSubDirectory("Debugger-rmi-trace",
+ "build/pypkg/src").getAbsolutePath();
+ String gdbPyPkg = Application.getModuleSubDirectory("Debugger-agent-x64dbg",
+ "build/pypkg/src").getAbsolutePath();
+ String add = rmiPyPkg + sep + gdbPyPkg;
+ pb.environment().compute("PYTHONPATH", (k, v) -> v == null ? add : (v + sep + add));
+ }
+
+ protected void setX64dbgPath(ProcessBuilder pb) throws IOException {
+ pb.environment().put("OPT_X64DBG_EXE", "C:\\Software\\snapshot_2025-08-19_19-40\\release\\x64\\x64dbg.exe");
+ }
+
+ @BeforeClass
+ public static void assertOS() {
+ assumeTrue("Not on Windows",
+ OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS);
+ }
+
+ @Before
+ public void setupTraceRmi() throws Throwable {
+ traceRmi = addPlugin(tool, TraceRmiPlugin.class);
+
+ try {
+ pythonPath = Paths.get(DummyProc.which("python3"));
+ }
+ catch (RuntimeException e) {
+ pythonPath = Paths.get(DummyProc.which("python"));
+ }
+
+ pythonPath = new File("/C:/Python313/python.exe").toPath();
+ assertTrue(pythonPath.toFile().exists());
+ outFile = Files.createTempFile("pydbgout", null);
+ errFile = Files.createTempFile("pydbgerr", null);
+ }
+
+ protected void addAllDebuggerPlugins() throws PluginException {
+ PluginsConfiguration plugConf = new PluginsConfiguration() {
+ @Override
+ protected boolean accepts(Class extends Plugin> pluginClass) {
+ return !ApplicationLevelOnlyPlugin.class.isAssignableFrom(pluginClass);
+ }
+ };
+
+ for (PluginDescription pd : plugConf
+ .getPluginDescriptions(PluginPackage.getPluginPackage("Debugger"))) {
+ addPlugin(tool, pd.getPluginClass());
+ }
+ }
+
+ protected static String addrToStringForPython(InetAddress address) {
+ if (address.isAnyLocalAddress()) {
+ return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost.
+ }
+ return address.getHostAddress();
+ }
+
+ protected static String sockToStringForPython(SocketAddress address) {
+ if (address instanceof InetSocketAddress tcp) {
+ return addrToStringForPython(tcp.getAddress()) + ":" + tcp.getPort();
+ }
+ throw new AssertionError("Unhandled address type " + address);
+ }
+
+ protected record PythonResult(boolean timedOut, int exitCode, String stdout, String stderr) {
+ protected String handle() {
+ if (stderr.contains("Error") || (0 != exitCode && 1 != exitCode && 143 != exitCode)) {
+ throw new PythonError(exitCode, stdout, stderr);
+ }
+ System.out.println("--stdout--");
+ System.out.println(stdout);
+ System.out.println("--stderr--");
+ System.out.println(stderr);
+ return stdout;
+ }
+ }
+
+ protected record ExecInPython(Process python, CompletableFuture future) {}
+
+ protected void pump(InputStream streamIn, OutputStream streamOut) {
+ Thread t = new Thread(() -> {
+ try (PrintStream printOut = new PrintStream(streamOut);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(streamIn))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ printOut.println(line);
+ printOut.flush();
+ }
+ }
+ catch (IOException e) {
+ Msg.info(this, "Terminating stdin pump, because " + e);
+ }
+ });
+ t.setDaemon(true);
+ t.start();
+ }
+
+ protected void pumpTee(InputStream streamIn, File fileOut, PrintStream streamOut) {
+ Thread t = new Thread(() -> {
+ try (PrintStream fileStream = new PrintStream(fileOut);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(streamIn))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ streamOut.println(line);
+ streamOut.flush();
+ fileStream.println(line);
+ fileStream.flush();
+ }
+ }
+ catch (IOException e) {
+ Msg.info(this, "Terminating tee: " + fileOut + ", because " + e);
+ }
+ });
+ t.setDaemon(true);
+ t.start();
+ }
+
+ @SuppressWarnings("resource") // Do not close stdin
+ protected ExecInPython execInPython(String script) throws IOException {
+ ProcessBuilder pb = new ProcessBuilder(pythonPath.toString(), "-i");
+ setPythonPath(pb);
+ setX64dbgPath(pb);
+
+ // If commands come from file, Python will quit after EOF.
+ Msg.info(this, "outFile: " + outFile);
+ Msg.info(this, "errFile: " + errFile);
+
+ //pb.inheritIO();
+ pb.redirectInput(ProcessBuilder.Redirect.PIPE);
+ if (SystemUtilities.isInTestingBatchMode()) {
+ pb.redirectOutput(outFile.toFile());
+ pb.redirectError(errFile.toFile());
+ }
+ else {
+ pb.redirectOutput(ProcessBuilder.Redirect.PIPE);
+ pb.redirectError(ProcessBuilder.Redirect.PIPE);
+ }
+ Process pyproc = pb.start();
+
+ if (!SystemUtilities.isInTestingBatchMode()) {
+ pumpTee(pyproc.getInputStream(), outFile.toFile(), System.out);
+ pumpTee(pyproc.getErrorStream(), errFile.toFile(), System.err);
+ }
+
+ OutputStream stdin = pyproc.getOutputStream();
+ stdin.write(script.getBytes());
+ stdin.flush();
+
+ if (!SystemUtilities.isInTestingBatchMode()) {
+ pump(System.in, stdin);
+ }
+
+ return new ExecInPython(pyproc, CompletableFuture.supplyAsync(() -> {
+ try {
+ if (!pyproc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+ Msg.error(this, "Timed out waiting for Python");
+ pyproc.destroyForcibly();
+ pyproc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ return new PythonResult(true, -1, Files.readString(outFile),
+ Files.readString(errFile));
+ }
+ Msg.info(this, "Python exited with code " + pyproc.exitValue());
+ return new PythonResult(false, pyproc.exitValue(), Files.readString(outFile),
+ Files.readString(errFile));
+ }
+ catch (Exception e) {
+ return ExceptionUtils.rethrow(e);
+ }
+ finally {
+ pyproc.destroyForcibly();
+ }
+ }));
+ }
+
+ public static class PythonError extends RuntimeException {
+ public final int exitCode;
+ public final String stdout;
+ public final String stderr;
+
+ public PythonError(int exitCode, String stdout, String stderr) {
+ super("""
+ exitCode=%d:
+ ----stdout----
+ %s
+ ----stderr----
+ %s
+ """.formatted(exitCode, stdout, stderr));
+ this.exitCode = exitCode;
+ this.stdout = stdout;
+ this.stderr = stderr;
+ }
+ }
+
+ protected String runThrowError(String script) throws Exception {
+ CompletableFuture result = execInPython(script).future;
+ return result.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle();
+ }
+
+ protected record PythonAndConnection(ExecInPython exec, TraceRmiConnection connection)
+ implements AutoCloseable {
+ protected RemoteMethod getMethod(String name) {
+ return Objects.requireNonNull(connection.getMethods().get(name));
+ }
+
+ public void execute(String cmd) {
+ RemoteMethod execute = getMethod("execute");
+ try {
+ execute.invoke(Map.of("cmd", cmd));
+ } catch (Exception e) {
+ Msg.warn(this, e.getMessage());
+ }
+ }
+
+ public RemoteAsyncResult executeAsync(String cmd) {
+ RemoteMethod execute = getMethod("execute");
+ return execute.invokeAsync(Map.of("cmd", cmd));
+ }
+
+ public String executeCapture(String cmd) {
+ RemoteMethod execute = getMethod("execute");
+ return (String) execute.invoke(Map.of("cmd", cmd, "to_string", true));
+ }
+
+ @Override
+ public void close() throws Exception {
+ Msg.info(this, "Cleaning up python");
+ exec.python().destroy();
+ try {
+ PythonResult r = exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ r.handle();
+ waitForPass(this, () -> assertTrue(connection.isClosed()),
+ TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+ finally {
+ exec.python.destroyForcibly();
+ }
+ }
+ }
+
+ protected PythonAndConnection startAndConnectPython(Function scriptSupplier)
+ throws Exception {
+ TraceRmiAcceptor acceptor = traceRmi.acceptOne(null);
+ ExecInPython exec =
+ execInPython(scriptSupplier.apply(sockToStringForPython(acceptor.getAddress())));
+ acceptor.setTimeout(CONNECT_TIMEOUT_MS);
+ try {
+ TraceRmiConnection connection = acceptor.accept();
+ return new PythonAndConnection(exec, connection);
+ }
+ catch (SocketTimeoutException e) {
+ exec.python.destroyForcibly();
+ exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle();
+ throw e;
+ }
+ }
+
+ protected PythonAndConnection startAndConnectPython() throws Exception {
+ return startAndConnectPython(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ """.formatted(PREAMBLE, addr));
+ }
+
+ @SuppressWarnings("resource")
+ protected String runThrowError(Function scriptSupplier)
+ throws Exception {
+ PythonAndConnection conn = startAndConnectPython(scriptSupplier);
+ PythonResult r = conn.exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ String stdout = r.handle();
+ waitForPass(this, () -> assertTrue(conn.connection.isClosed()),
+ TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ return stdout;
+ }
+
+ protected void waitStopped(String message) {
+ TraceObject proc =
+ Objects.requireNonNull(tb.objAny("Sessions[].Processes[]", Lifespan.at(0)));
+ waitForPass(() -> assertEquals(message, "STOPPED", tb.objValue(proc, 0, "_state")));
+ waitTxDone();
+ }
+
+ protected void waitRunning(String message) {
+ TraceObject proc =
+ Objects.requireNonNull(tb.objAny("Sessions[].Processes[]", Lifespan.at(0)));
+ waitForPass(() -> assertEquals(message, "RUNNING", tb.objValue(proc, 0, "_state")));
+ waitTxDone();
+ }
+
+ protected String extractOutSection(String out, String head) {
+ String[] split = out.split("\n");
+ String xout = "";
+ for (String s : split) {
+ if (!s.startsWith("(python)") && !s.equals("")) {
+ xout += s + "\n";
+ }
+ }
+ return xout.split(head)[1].split("---")[0].replace("(python)", "").trim();
+ }
+
+ record MemDump(long address, byte[] data) {}
+
+ protected MemDump parseHexDump(String dump) throws IOException {
+ // First, get the address. Assume contiguous, so only need top line.
+ List lines = List.of(dump.split("\n"));
+ List toksLine0 = List.of(lines.get(0).split("\\s+"));
+ String addrstr = toksLine0.get(0);
+ if (addrstr.contains(":")) {
+ addrstr = addrstr.substring(0, addrstr.indexOf(":"));
+ }
+ long address = Long.parseLong(addrstr, 16);
+
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ for (String l : lines) {
+ List parts = List.of(l.split(":"));
+ assertEquals(2, parts.size());
+ String hex = parts.get(1).substring(0, 48);
+ byte[] lineData = NumericUtilities.convertStringToBytes(hex);
+ assertNotNull("Converted to null: " + hex, parts.get(1));
+ buf.write(lineData);
+ }
+ return new MemDump(address, buf.toByteArray());
+ }
+
+ record RegDump() {}
+
+ protected RegDump parseRegDump(String dump) {
+ return new RegDump();
+ }
+
+ protected ManagedDomainObject openDomainObject(String path) throws Exception {
+ DomainFile df = env.getProject().getProjectData().getFile(path);
+ assertNotNull(df);
+ return new ManagedDomainObject(df, false, false, monitor);
+ }
+
+ protected ManagedDomainObject waitDomainObject(String path) throws Exception {
+ DomainFile df;
+ long start = System.currentTimeMillis();
+ while (true) {
+ df = env.getProject().getProjectData().getFile(path);
+ if (df != null) {
+ return new ManagedDomainObject(df, false, false, monitor);
+ }
+ Thread.sleep(1000);
+ if (System.currentTimeMillis() - start > 30000) {
+ throw new TimeoutException("30 seconds expired waiting for domain file");
+ }
+ }
+ }
+
+ protected void assertBreakLoc(TraceObjectValue locVal, Address addr, int len, String type) throws Exception {
+ TraceObject loc = locVal.getChild();
+ TraceObject spec = loc;
+ assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue());
+ assertEquals(type, spec.getValue(0, "Type").getValue());
+ }
+
+ protected void assertWatchLoc(TraceObjectValue locVal, Address addr, int len, String type) throws Exception {
+ TraceObject loc = locVal.getChild();
+ assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue());
+ assertEquals(type, loc.getValue(0, "TypeEx").getValue());
+ }
+
+ protected void waitTxDone() {
+ waitFor(() -> tb.trace.getCurrentTransactionInfo() == null);
+ }
+
+ public static void waitForPass(Runnable runnable, long timeoutMs, long retryDelayMs) {
+ long start = System.currentTimeMillis();
+ AssertionError lastError = null;
+ while (System.currentTimeMillis() - start < timeoutMs) {
+ try {
+ runnable.run();
+ return;
+ }
+ catch (AssertionError e) {
+ lastError = e;
+ }
+ try {
+ Thread.sleep(retryDelayMs);
+ }
+ catch (InterruptedException e) {
+ // Retry sooner, I guess.
+ }
+ }
+ if (lastError == null) {
+ throw new AssertionError("Timed out before first try?");
+ }
+ throw lastError;
+ }
+}
diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/x64dbg/rmi/X64dbgCommandsTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/x64dbg/rmi/X64dbgCommandsTest.java
new file mode 100644
index 0000000000..79ab6c9b62
--- /dev/null
+++ b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/x64dbg/rmi/X64dbgCommandsTest.java
@@ -0,0 +1,1187 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package agent.x64dbg.rmi;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.nio.ByteBuffer;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.junit.Test;
+
+import generic.Unique;
+import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
+import ghidra.debug.api.tracermi.TraceRmiAcceptor;
+import ghidra.debug.api.tracermi.TraceRmiConnection;
+import ghidra.framework.Application;
+import ghidra.framework.model.DomainFile;
+import ghidra.program.model.address.*;
+import ghidra.program.model.lang.RegisterValue;
+import ghidra.program.model.listing.CodeUnit;
+import ghidra.trace.database.ToyDBTraceBuilder;
+import ghidra.trace.model.*;
+import ghidra.trace.model.memory.*;
+import ghidra.trace.model.modules.TraceModule;
+import ghidra.trace.model.target.TraceObject;
+import ghidra.trace.model.target.TraceObjectValue;
+import ghidra.trace.model.target.path.KeyPath;
+import ghidra.trace.model.target.path.PathFilter;
+import ghidra.trace.model.thread.TraceThread;
+import ghidra.trace.model.time.TraceSnapshot;
+import ghidra.util.Msg;
+import ghidra.util.NumericUtilities;
+
+public class X64dbgCommandsTest extends AbstractX64dbgTraceRmiTest {
+
+ //@Test
+ public void testManual() throws Exception {
+ TraceRmiAcceptor acceptor = traceRmi.acceptOne(null);
+ Msg.info(this,
+ "Use: ghidra_trace_connect(" + sockToStringForPython(acceptor.getAddress()) + ")");
+ TraceRmiConnection connection = acceptor.accept();
+ Msg.info(this, "Connected: " + sockToStringForPython(connection.getRemoteAddress()));
+ connection.waitClosed();
+ Msg.info(this, "Closed");
+ }
+
+ @Test
+ public void testConnectErrorNoArg() throws Exception {
+ try {
+ runThrowError("""
+ from ghidraxdbg.commands import *
+ ghidra_trace_connect()
+ util.terminate_session()
+ quit()
+ """);
+ fail();
+ }
+ catch (PythonError e) {
+ assertThat(e.stderr, containsString("'ghidra_trace_connect'"));
+ assertThat(e.stderr, containsString("'address'"));
+ }
+ }
+
+ @Test
+ public void testConnect() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ }
+
+ @Test
+ public void testDisconnect() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_disconnect()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ }
+
+ @Test
+ public void testStartTraceDefaults() throws Exception {
+ // Default name and lcsp
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ assertEquals("x86:LE:64:default",
+ tb.trace.getBaseLanguage().getLanguageID().getIdAsString());
+ assertEquals("windows",
+ tb.trace.getBaseCompilerSpec().getCompilerSpecID().getIdAsString());
+ }
+ }
+
+ @Test
+ public void testStartTraceDefaultNoFile() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_start()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/noname")) {
+ assertThat(mdo.get(), instanceOf(Trace.class));
+ }
+ }
+
+ @Test
+ public void testStartTraceCustomize() throws Exception {
+ runThrowError(
+ addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', start_trace=False, wait=True)
+ util.set_convenience_variable('ghidra-language','Toy:BE:64:default')
+ util.set_convenience_variable('ghidra-compiler','default')
+ ghidra_trace_start('myToy')
+ util.terminate_session()
+ quit()
+ """
+ .formatted(PREAMBLE, addr));
+ DomainFile dfMyToy = env.getProject().getProjectData().getFile("/New Traces/x64dbg/myToy");
+ assertNotNull(dfMyToy);
+ try (ManagedDomainObject mdo = new ManagedDomainObject(dfMyToy, false, false, monitor)) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ assertEquals("Toy:BE:64:default",
+ tb.trace.getBaseLanguage().getLanguageID().getIdAsString());
+ assertEquals("default",
+ tb.trace.getBaseCompilerSpec().getCompilerSpecID().getIdAsString());
+ }
+ }
+
+ @Test
+ public void testStopTrace() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_stop()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ DomainFile dfNotepad =
+ env.getProject().getProjectData().getFile("/New Traces/x64dbg/notepad.exe");
+ assertNotNull(dfNotepad);
+ // TODO: Given the 'quit' command, I'm not sure this assertion is checking anything.
+ assertFalse(dfNotepad.isOpen());
+ }
+
+ @Test
+ public void testInfo() throws Exception {
+ AtomicReference refAddr = new AtomicReference<>();
+ String out = runThrowError(addr -> {
+ refAddr.set(addr);
+ return """
+ %s
+ print('---Import---')
+ ghidra_trace_info()
+ print('---BeforeConnect---')
+ ghidra_trace_connect('%s')
+ print('---Connect---')
+ ghidra_trace_info()
+ print('---Create---')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ print('---Start---')
+ ghidra_trace_info()
+ ghidra_trace_stop()
+ print('---Stop---')
+ ghidra_trace_info()
+ ghidra_trace_disconnect()
+ print('---Disconnect---')
+ ghidra_trace_info()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr);
+ });
+
+ assertEquals("""
+ Not connected to Ghidra""",
+ extractOutSection(out, "---Import---"));
+ assertEquals("""
+ Connected to %s %s at %s
+ No trace""".formatted(
+ Application.getName(), Application.getApplicationVersion(), refAddr.get()),
+ extractOutSection(out, "---Connect---").replaceAll("\r", ""));
+ assertEquals("""
+ Connected to %s %s at %s
+ Trace active""".formatted(
+ Application.getName(), Application.getApplicationVersion(), refAddr.get()),
+ extractOutSection(out, "---Start---").replaceAll("\r", ""));
+ assertEquals("""
+ Connected to %s %s at %s
+ No trace""".formatted(
+ Application.getName(), Application.getApplicationVersion(), refAddr.get()),
+ extractOutSection(out, "---Stop---").replaceAll("\r", ""));
+ assertEquals("""
+ Not connected to Ghidra""",
+ extractOutSection(out, "---Disconnect---"));
+ }
+
+ @Test
+ public void testLcsp() throws Exception {
+ String out = runThrowError(
+ """
+ %s
+ print('---Import---')
+ ghidra_trace_info_lcsp()
+ print('---Create---')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', start_trace=False, wait=True)
+ print('---File---')
+ ghidra_trace_info_lcsp()
+ util.set_convenience_variable('ghidra-language','Toy:BE:64:default')
+ print('---Language---')
+ ghidra_trace_info_lcsp()
+ util.set_convenience_variable('ghidra-compiler','posStack')
+ print('---Compiler---')
+ ghidra_trace_info_lcsp()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE));
+
+ assertEquals("""
+ Selected Ghidra language: x86:LE:64:default
+ Selected Ghidra compiler: windows""",
+ extractOutSection(out, "---File---").replaceAll("\r", ""));
+ assertEquals("""
+ Toy:BE:64:default not found in compiler map
+ Selected Ghidra language: Toy:BE:64:default
+ Selected Ghidra compiler: default""",
+ extractOutSection(out, "---Language---").replaceAll("\r", ""));
+ assertEquals("""
+ Selected Ghidra language: Toy:BE:64:default
+ Selected Ghidra compiler: posStack""",
+ extractOutSection(out, "---Compiler---").replaceAll("\r", ""));
+ }
+
+ //@Test TODO - revisit after rebasing on master
+ public void testSave() throws Exception {
+ traceManager.setSaveTracesByDefault(false);
+
+ // For sanity check, verify failing to save drops data
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Create snapshot')
+ ghidra_trace_new_snap('Scripted snapshot')
+ ghidra_trace_txcommit()
+ ghidra_trace_stop()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ assertEquals(0, tb.trace.getTimeManager().getAllSnapshots().size());
+ }
+
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Create snapshot')
+ ghidra_trace_new_snap('Scripted snapshot')
+ ghidra_trace_txcommit()
+ ghidra_trace_save()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ assertEquals(1, tb.trace.getTimeManager().getAllSnapshots().size());
+ }
+ }
+
+ @Test
+ public void testSnapshot() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Create snapshot')
+ ghidra_trace_new_snap('Scripted snapshot')
+ ghidra_trace_txcommit()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ TraceSnapshot snapshot = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots());
+ assertEquals(0, snapshot.getKey());
+ assertEquals("Scripted snapshot", snapshot.getDescription());
+ }
+ }
+
+ @Test
+ public void testPutmem() throws Exception {
+ String out = runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Create snapshot')
+ ghidra_trace_new_snap('Scripted snapshot')
+ pc = util.get_pc()
+ ghidra_trace_putmem(pc, 16)
+ ghidra_trace_txcommit()
+ print('---PC---')
+ print(pc)
+ print('---')
+ print('---Dump---')
+ print(util.dbg.client.read_memory(pc, 4).hex())
+ print('---')
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey();
+ traceManager.openTrace(tb.trace);
+ traceManager.activateTrace(tb.trace);
+
+ String pc = extractOutSection(out, "---PC---");
+ long address = Long.parseLong(pc);
+ String dump = extractOutSection(out, "---Dump---");
+ byte[] lineData = NumericUtilities.convertStringToBytes(dump);
+ ByteBuffer buf = ByteBuffer.allocate(lineData.length);
+ tb.trace.getMemoryManager().getBytes(snap, tb.addr(address), buf);
+
+ assertArrayEquals(lineData, buf.array());
+ }
+ }
+
+ @Test
+ public void testPutmemState() throws Exception {
+ String out = runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Create snapshot')
+ ghidra_trace_new_snap('Scripted snapshot')
+ pc = util.get_pc()
+ ghidra_trace_putmem_state(pc, 16, 'error', pages=False)
+ ghidra_trace_txcommit()
+ print('---Start---')
+ print(pc)
+ #util.dbg.client.read_memory(pc, 4)
+ print('---')
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey();
+
+ String eval = extractOutSection(out, "---Start---");
+ Address addr = tb.addr(Long.parseLong(eval));
+
+ Entry entry =
+ tb.trace.getMemoryManager().getMostRecentStateEntry(snap, addr);
+ assertEquals(Map.entry(new ImmutableTraceAddressSnapRange(
+ rng(addr, 16), Lifespan.at(0)), TraceMemoryState.ERROR), entry);
+ }
+ }
+
+ @Test
+ public void testDelmem() throws Exception {
+ String out = runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Create snapshot')
+ ghidra_trace_new_snap('Scripted snapshot')
+ pc = util.get_pc()
+ ghidra_trace_putmem(pc, 16)
+ ghidra_trace_delmem(pc, 8)
+ ghidra_trace_txcommit()
+ print('---PC---')
+ print(pc)
+ print('---Dump---')
+ print(util.dbg.client.read_memory(pc, 16).hex())
+ print('---')
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey();
+
+ String pc = extractOutSection(out, "---PC---");
+ long address = Long.parseLong(pc);
+ String dump = extractOutSection(out, "---Dump---");
+ byte[] lineData = NumericUtilities.convertStringToBytes(dump);
+ Arrays.fill(lineData, 0, 8, (byte) 0);
+ ByteBuffer buf = ByteBuffer.allocate(lineData.length);
+ tb.trace.getMemoryManager().getBytes(snap, tb.addr(address), buf);
+
+ assertArrayEquals(lineData, buf.array());
+ }
+ }
+
+ @Test
+ public void testPutreg() throws Exception {
+ String count = IntStream.iterate(0, i -> i < 32, i -> i + 1)
+ .mapToObj(Integer::toString)
+ .collect(Collectors.joining(",", "{", "}"));
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ util.dbg.cmd('rax=0xdeadbeef')
+ ghidra_trace_txstart('Create snapshot')
+ ghidra_trace_new_snap('Scripted snapshot')
+ ghidra_trace_putreg()
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr, count));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey();
+ List regVals = tb.trace.getObjectManager()
+ .getValuePaths(Lifespan.at(0),
+ PathFilter.parse("Sessions[].Processes[].Threads[].Registers"))
+ .map(p -> p.getLastEntry())
+ .toList();
+ TraceObjectValue tobj = regVals.get(0);
+ AddressSpace t1f0 = tb.trace.getBaseAddressFactory()
+ .getAddressSpace(tobj.getCanonicalPath().toString());
+ TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false);
+
+ RegisterValue rax = regs.getValue(snap, tb.reg("rax"));
+ assertEquals("deadbeef", rax.getUnsignedValue().toString(16));
+ }
+ }
+
+ @Test
+ public void testDelreg() throws Exception {
+ String count = IntStream.iterate(0, i -> i < 32, i -> i + 1)
+ .mapToObj(Integer::toString)
+ .collect(Collectors.joining(",", "{", "}"));
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ util.dbg.cmd('rax=0xdeadbeef')
+ ghidra_trace_txstart('Create snapshot')
+ ghidra_trace_new_snap('Scripted snapshot')
+ ghidra_trace_putreg()
+ ghidra_trace_delreg()
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr, count));
+ // The spaces will be left over, but the values should be zeroed
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey();
+ List regVals = tb.trace.getObjectManager()
+ .getValuePaths(Lifespan.at(0),
+ PathFilter.parse("Sessions[].Processes[].Threads[].Registers"))
+ .map(p -> p.getLastEntry())
+ .toList();
+ TraceObjectValue tobj = regVals.get(0);
+ AddressSpace t1f0 = tb.trace.getBaseAddressFactory()
+ .getAddressSpace(tobj.getCanonicalPath().toString());
+ TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false);
+
+ RegisterValue rax = regs.getValue(snap, tb.reg("rax"));
+ assertEquals("0", rax.getUnsignedValue().toString(16));
+ }
+ }
+
+ @Test
+ public void testCreateObj() throws Exception {
+ String out = runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_start()
+ ghidra_trace_txstart('Create Object')
+ print('---Id---')
+ ghidra_trace_create_obj('Test.Objects[1]')
+ print('---')
+ ghidra_trace_txcommit()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/noname")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ TraceObject object = tb.trace.getObjectManager()
+ .getObjectByCanonicalPath(KeyPath.parse("Test.Objects[1]"));
+ assertNotNull(object);
+ String created = extractOutSection(out, "---Id---");
+ long id = Long.parseLong(created.split("id=")[1].split(",")[0]);
+ assertEquals(object.getKey(), id);
+ }
+ }
+
+ @Test
+ public void testInsertObj() throws Exception {
+ String out = runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_start()
+ ghidra_trace_txstart('Create Object')
+ ghidra_trace_create_obj('Test.Objects[1]')
+ print('---Lifespan---')
+ ghidra_trace_insert_obj('Test.Objects[1]')
+ print('---')
+ ghidra_trace_txcommit()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/noname")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ TraceObject object = tb.trace.getObjectManager()
+ .getObjectByCanonicalPath(KeyPath.parse("Test.Objects[1]"));
+ assertNotNull(object);
+ Lifespan life = Unique.assertOne(object.getLife().spans());
+ assertEquals(Lifespan.nowOn(0), life);
+ assertEquals("Inserted object: lifespan=[0,+inf)",
+ extractOutSection(out, "---Lifespan---"));
+ }
+ }
+
+ @Test
+ public void testRemoveObj() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Create Object')
+ ghidra_trace_create_obj('Test.Objects[1]')
+ ghidra_trace_insert_obj('Test.Objects[1]')
+ ghidra_trace_new_snap(time=Schedule(1))
+ ghidra_trace_remove_obj('Test.Objects[1]')
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ TraceObject object = tb.trace.getObjectManager()
+ .getObjectByCanonicalPath(KeyPath.parse("Test.Objects[1]"));
+ assertNotNull(object);
+ Lifespan life = Unique.assertOne(object.getLife().spans());
+ assertEquals(Lifespan.at(0), life);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ protected T runTestSetValue(String extra, String x64dbgExpr, String gtype)
+ throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Create Object')
+ ghidra_trace_create_obj('Test.Objects[1]')
+ ghidra_trace_insert_obj('Test.Objects[1]')
+ %s
+ ghidra_trace_set_value('Test.Objects[1]', 'test', %s, '%s')
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr, extra, x64dbgExpr, gtype));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ TraceObject object = tb.trace.getObjectManager()
+ .getObjectByCanonicalPath(KeyPath.parse("Test.Objects[1]"));
+ assertNotNull(object);
+ TraceObjectValue value = object.getValue(0, "test");
+ return value == null ? null : (T) value.getValue();
+ }
+ }
+
+ @Test
+ public void testSetValueNull() throws Exception {
+ assertNull(runTestSetValue("", "None", "VOID"));
+ }
+
+ @Test
+ public void testSetValueBool() throws Exception {
+ assertEquals(Boolean.TRUE, runTestSetValue("", "True", "BOOL"));
+ }
+
+ @Test
+ public void testSetValueByte() throws Exception {
+ assertEquals(Byte.valueOf((byte) 1), runTestSetValue("", "'1'", "BYTE"));
+ }
+
+ @Test
+ public void testSetValueChar() throws Exception {
+ assertEquals(Character.valueOf('A'), runTestSetValue("", "'A'", "CHAR"));
+ }
+
+ @Test
+ public void testSetValueShort() throws Exception {
+ assertEquals(Short.valueOf((short) 1), runTestSetValue("", "'1'", "SHORT"));
+ }
+
+ @Test
+ public void testSetValueInt() throws Exception {
+ assertEquals(Integer.valueOf(1), runTestSetValue("", "'1'", "INT"));
+ }
+
+ @Test
+ public void testSetValueLong() throws Exception {
+ assertEquals(Long.valueOf(1), runTestSetValue("", "'1'", "LONG"));
+ }
+
+ @Test
+ public void testSetValueString() throws Exception {
+ assertEquals("HelloWorld!", runTestSetValue("", "\'HelloWorld!\'", "STRING"));
+ }
+
+ @Test //- how do we input long strings in python
+ public void testSetValueStringWide() throws Exception {
+ assertEquals("HelloWorld!", runTestSetValue("", "u\'HelloWorld!\'", "STRING"));
+ }
+
+ @Test
+ public void testSetValueBoolArr() throws Exception {
+ assertArrayEquals(new boolean[] { true, false },
+ runTestSetValue("", "[True,False]", "BOOL_ARR"));
+ }
+
+ @Test
+ public void testSetValueByteArrUsingString() throws Exception {
+ assertArrayEquals(new byte[] { 'H', 1, 'W' },
+ runTestSetValue("", "'H\\1W'", "BYTE_ARR"));
+ }
+
+ @Test
+ public void testSetValueByteArrUsingArray() throws Exception {
+ assertArrayEquals(new byte[] { 'H', 0, 'W' },
+ runTestSetValue("", "['H',0,'W']", "BYTE_ARR"));
+ }
+
+ @Test
+ public void testSetValueCharArrUsingString() throws Exception {
+ assertArrayEquals(new char[] { 'H', 1, 'W' },
+ runTestSetValue("", "'H\\1W'", "CHAR_ARR"));
+ }
+
+ @Test
+ public void testSetValueCharArrUsingArray() throws Exception {
+ assertArrayEquals(new char[] { 'H', 0, 'W' },
+ runTestSetValue("", "['H',0,'W']", "CHAR_ARR"));
+ }
+
+ @Test
+ public void testSetValueShortArrUsingString() throws Exception {
+ assertArrayEquals(new short[] { 'H', 1, 'W' },
+ runTestSetValue("", "'H\\1W'", "SHORT_ARR"));
+ }
+
+ @Test
+ public void testSetValueShortArrUsingArray() throws Exception {
+ assertArrayEquals(new short[] { 'H', 0, 'W' },
+ runTestSetValue("", "['H',0,'W']", "SHORT_ARR"));
+ }
+
+ @Test
+ public void testSetValueIntArrayUsingMixedArray() throws Exception {
+ // Because explicit array type is chosen, we get null terminator
+ assertArrayEquals(new int[] { 'H', 0, 'W' },
+ runTestSetValue("", "['H',0,'W']", "INT_ARR"));
+ }
+
+ @Test
+ public void testSetValueIntArrUsingArray() throws Exception {
+ assertArrayEquals(new int[] { 1, 2, 3, 4 },
+ runTestSetValue("", "[1,2,3,4]", "INT_ARR"));
+ }
+
+ @Test
+ public void testSetValueLongArr() throws Exception {
+ assertArrayEquals(new long[] { 1, 2, 3, 4 },
+ runTestSetValue("", "[1,2,3,4]", "LONG_ARR"));
+ }
+
+ @Test
+ public void testSetValueStringArr() throws Exception {
+ assertArrayEquals(new String[] { "1", "A", "dead", "beef" },
+ runTestSetValue("", "['1','A','dead','beef']", "STRING_ARR"));
+ }
+
+ @Test
+ public void testSetValueAddress() throws Exception {
+ Address address = runTestSetValue("", "'0xdeadbeef'", "ADDRESS");
+ // Don't have the address factory to create expected address
+ assertEquals(0xdeadbeefL, address.getOffset());
+ assertEquals("ram", address.getAddressSpace().getName());
+ }
+
+ @Test
+ public void testSetValueObject() throws Exception {
+ TraceObject object = runTestSetValue("", "'Test.Objects[1]'", "OBJECT");
+ assertEquals("Test.Objects[1]", object.getCanonicalPath().toString());
+ }
+
+ @Test
+ public void testRetainValues() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Create Object')
+ ghidra_trace_create_obj('Test.Objects[1]')
+ ghidra_trace_insert_obj('Test.Objects[1]')
+ ghidra_trace_set_value('Test.Objects[1]', '[1]', '"A"', 'STRING')
+ ghidra_trace_set_value('Test.Objects[1]', '[2]', '"B"', 'STRING')
+ ghidra_trace_set_value('Test.Objects[1]', '[3]', '"C"', 'STRING')
+ ghidra_trace_new_snap(time=Schedule(10))
+ ghidra_trace_retain_values('Test.Objects[1]', '[1] [3]')
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ TraceObject object = tb.trace.getObjectManager()
+ .getObjectByCanonicalPath(KeyPath.parse("Test.Objects[1]"));
+ assertNotNull(object);
+ assertEquals(Map.ofEntries(
+ Map.entry("[1]", Lifespan.nowOn(0)),
+ Map.entry("[2]", Lifespan.span(0, 9)),
+ Map.entry("[3]", Lifespan.nowOn(0))),
+ object.getValues(Lifespan.ALL)
+ .stream()
+ .collect(Collectors.toMap(v -> v.getEntryKey(), v -> v.getLifespan())));
+ }
+ }
+
+ @Test
+ public void testGetObj() throws Exception {
+ String out = runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_start()
+ ghidra_trace_txstart('Create Object')
+ print('---Id---')
+ ghidra_trace_create_obj('Test.Objects[1]')
+ print('---')
+ ghidra_trace_txcommit()
+ print('---GetObject---')
+ ghidra_trace_get_obj('Test.Objects[1]')
+ print('---')
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/noname")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ TraceObject object = tb.trace.getObjectManager()
+ .getObjectByCanonicalPath(KeyPath.parse("Test.Objects[1]"));
+ assertNotNull(object);
+ assertEquals("3\tTest.Objects[1]", extractOutSection(out, "---GetObject---"));
+ }
+ }
+
+ @Test
+ public void testGetValues() throws Exception {
+ String out = runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Create Object')
+ ghidra_trace_create_obj('Test.Objects[1]')
+ ghidra_trace_insert_obj('Test.Objects[1]')
+ ghidra_trace_set_value('Test.Objects[1]', 'vnull', None, 'VOID')
+ ghidra_trace_set_value('Test.Objects[1]', 'vbool', True, 'BOOL')
+ ghidra_trace_set_value('Test.Objects[1]', 'vbyte', '1', 'BYTE')
+ ghidra_trace_set_value('Test.Objects[1]', 'vchar', 'A', 'CHAR')
+ ghidra_trace_set_value('Test.Objects[1]', 'vshort', 2, 'SHORT')
+ ghidra_trace_set_value('Test.Objects[1]', 'vint', 3, 'INT')
+ ghidra_trace_set_value('Test.Objects[1]', 'vlong', 4, 'LONG')
+ ghidra_trace_set_value('Test.Objects[1]', 'vstring', 'Hello', 'STRING')
+ vboolarr = [True, False]
+ ghidra_trace_set_value('Test.Objects[1]', 'vboolarr', vboolarr, 'BOOL_ARR')
+ vbytearr = [1, 2, 3]
+ ghidra_trace_set_value('Test.Objects[1]', 'vbytearr', vbytearr, 'BYTE_ARR')
+ vchararr = 'Hello'
+ ghidra_trace_set_value('Test.Objects[1]', 'vchararr', vchararr, 'CHAR_ARR')
+ vshortarr = [1, 2, 3]
+ ghidra_trace_set_value('Test.Objects[1]', 'vshortarr', vshortarr, 'SHORT_ARR')
+ vintarr = [1, 2, 3]
+ ghidra_trace_set_value('Test.Objects[1]', 'vintarr', vintarr, 'INT_ARR')
+ vlongarr = [1, 2, 3]
+ ghidra_trace_set_value('Test.Objects[1]', 'vlongarr', vlongarr, 'LONG_ARR')
+ ghidra_trace_set_value('Test.Objects[1]', 'vaddr', '0xdeadbeef', 'ADDRESS')
+ ghidra_trace_set_value('Test.Objects[1]', 'vobj', 'Test.Objects[1]', 'OBJECT')
+ ghidra_trace_txcommit()
+ print('---GetValues---')
+ ghidra_trace_get_values('Test.Objects[1].')
+ print('---')
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ assertEquals("""
+ Parent Key Span Value Type
+ Test.Objects[1] vaddr [0,+inf) ram:deadbeef ADDRESS
+ Test.Objects[1] vbool [0,+inf) True BOOL
+ Test.Objects[1] vboolarr [0,+inf) [True, False] BOOL_ARR
+ Test.Objects[1] vbyte [0,+inf) 1 BYTE
+ Test.Objects[1] vbytearr [0,+inf) b'\\x01\\x02\\x03' BYTE_ARR
+ Test.Objects[1] vchar [0,+inf) 'A' CHAR
+ Test.Objects[1] vchararr [0,+inf) 'Hello' CHAR_ARR
+ Test.Objects[1] vint [0,+inf) 3 INT
+ Test.Objects[1] vintarr [0,+inf) [1, 2, 3] INT_ARR
+ Test.Objects[1] vlong [0,+inf) 4 LONG
+ Test.Objects[1] vlongarr [0,+inf) [1, 2, 3] LONG_ARR
+ Test.Objects[1] vobj [0,+inf) Test.Objects[1] OBJECT
+ Test.Objects[1] vshort [0,+inf) 2 SHORT
+ Test.Objects[1] vshortarr [0,+inf) [1, 2, 3] SHORT_ARR
+ Test.Objects[1] vstring [0,+inf) 'Hello' STRING""",
+ extractOutSection(out, "---GetValues---").replaceAll("\r", ""));
+ }
+ }
+
+ @Test
+ public void testGetValuesRng() throws Exception {
+ String out = runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Create Object')
+ ghidra_trace_create_obj('Test.Objects[1]')
+ ghidra_trace_insert_obj('Test.Objects[1]')
+ ghidra_trace_set_value('Test.Objects[1]', 'vaddr', '0xdeadbeef', 'ADDRESS')
+ ghidra_trace_txcommit()
+ print('---GetValues---')
+ ghidra_trace_get_values_rng(0xdeadbeef, 10)
+ print('---')
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ assertEquals("""
+ Parent Key Span Value Type
+ Test.Objects[1] vaddr [0,+inf) ram:deadbeef ADDRESS""",
+ extractOutSection(out, "---GetValues---").replaceAll("\r", ""));
+ }
+ }
+
+ @Test
+ public void testActivateObject() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ #set language c++
+ ghidra_trace_txstart('Create Object')
+ ghidra_trace_create_obj('Test.Objects[1]')
+ ghidra_trace_insert_obj('Test.Objects[1]')
+ ghidra_trace_txcommit()
+ ghidra_trace_activate('Test.Objects[1]')
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ assertSame(mdo.get(), traceManager.getCurrentTrace());
+ assertEquals("Test.Objects[1]",
+ traceManager.getCurrentObject().getCanonicalPath().toString());
+ }
+ }
+
+ @Test
+ public void testDisassemble() throws Exception {
+ String out = runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Tx')
+ pc = util.get_pc()
+ ghidra_trace_putmem(pc, 16)
+ print('---Disassemble---')
+ ghidra_trace_disassemble(pc)
+ print('---')
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ // Not concerned about specifics, so long as disassembly occurs
+ long total = 0;
+ for (CodeUnit cu : tb.trace.getCodeManager().definedUnits().get(0, true)) {
+ total += cu.getLength();
+ }
+ String extract = extractOutSection(out, "---Disassemble---");
+ String[] split = extract.split("\r\n");
+ assertEquals("Disassembled %d bytes".formatted(total),
+ split[0]);
+ }
+ }
+
+ @Test
+ public void testPutProcesses() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_start()
+ ghidra_trace_txstart('Tx')
+ ghidra_trace_put_processes()
+ ghidra_trace_txcommit()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/noname")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ // Would be nice to control / validate the specifics
+ Collection processes = tb.trace.getObjectManager()
+ .getValuePaths(Lifespan.at(0), PathFilter.parse("Processes[]"))
+ .map(p -> p.getDestination(null))
+ .toList();
+ assertEquals(0, processes.size());
+ }
+ }
+
+ @Test
+ public void testPutAvailable() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_start()
+ ghidra_trace_txstart('Tx')
+ ghidra_trace_put_available()
+ ghidra_trace_txcommit()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/noname")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ // Would be nice to control / validate the specifics
+ Collection available = tb.trace.getObjectManager()
+ .getValuePaths(Lifespan.at(0), PathFilter.parse("Sessions[].Available[]"))
+ .map(p -> p.getDestination(null))
+ .toList();
+ assertThat(available.size(), greaterThan(2));
+ }
+ }
+
+ @Test
+ public void testPutBreakpoints() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ pc = util.get_pc()
+ util.dbg.client.clear_breakpoint(None)
+ util.dbg.client.clear_hardware_breakpoint(None)
+ util.dbg.client.set_breakpoint(address_or_symbol=pc)
+ util.dbg.client.set_hardware_breakpoint(address_or_symbol=pc+4, bp_type=HardwareBreakpointType.x)
+ ghidra_trace_txstart('Tx')
+ ghidra_trace_put_breakpoints()
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ List procSBreakLocVals = tb.trace.getObjectManager()
+ .getValuePaths(Lifespan.at(0),
+ PathFilter.parse("Sessions[].Processes[].Debug.Software Breakpoints[]"))
+ .map(p -> p.getLastEntry())
+ .sorted(Comparator.comparing(TraceObjectValue::getEntryKey))
+ .toList();
+ List procHBreakLocVals = tb.trace.getObjectManager()
+ .getValuePaths(Lifespan.at(0),
+ PathFilter.parse("Sessions[].Processes[].Debug.Hardware Breakpoints[]"))
+ .map(p -> p.getLastEntry())
+ .sorted(Comparator.comparing(TraceObjectValue::getEntryKey))
+ .toList();
+ assertEquals(1, procSBreakLocVals.size());
+ assertEquals(1, procHBreakLocVals.size());
+ AddressRange rangeMain =
+ procSBreakLocVals.get(0).getChild().getValue(0, "_range").castValue();
+ Address bp1 = rangeMain.getMinAddress();
+
+ assertBreakLoc(procSBreakLocVals.get(0), bp1, 1, "1"); // 1==SW
+ assertBreakLoc(procHBreakLocVals.get(0), bp1.add(4), 1, "2"); // 2==HW
+ }
+ }
+
+ @Test
+ public void testPutBreakpoints2() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Tx')
+ pc = util.get_pc()
+ util.dbg.client.clear_hardware_breakpoint(None)
+ util.dbg.client.set_hardware_breakpoint(address_or_symbol=pc, bp_type=HardwareBreakpointType.x)
+ util.dbg.client.set_hardware_breakpoint(address_or_symbol=pc+4, bp_type=HardwareBreakpointType.r)
+ util.dbg.client.set_hardware_breakpoint(address_or_symbol=pc+8, bp_type=HardwareBreakpointType.w)
+ ghidra_trace_put_breakpoints()
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ List procBreakVals = tb.trace.getObjectManager()
+ .getValuePaths(Lifespan.at(0),
+ PathFilter.parse("Sessions[].Processes[].Debug.Hardware Breakpoints[]"))
+ .map(p -> p.getLastEntry())
+ .sorted(Comparator.comparing(TraceObjectValue::getEntryKey))
+ .toList();
+ assertEquals(3, procBreakVals.size());
+ AddressRange rangeMain0 =
+ procBreakVals.get(0).getChild().getValue(0, "_range").castValue();
+ Address main0 = rangeMain0.getMinAddress();
+ AddressRange rangeMain1 =
+ procBreakVals.get(1).getChild().getValue(0, "_range").castValue();
+ Address main1 = rangeMain1.getMinAddress();
+ AddressRange rangeMain2 =
+ procBreakVals.get(2).getChild().getValue(0, "_range").castValue();
+ Address main2 = rangeMain2.getMinAddress();
+
+ assertWatchLoc(procBreakVals.get(0), main0, (int) rangeMain0.getLength(), "2"); // 2==x
+ assertWatchLoc(procBreakVals.get(1), main1, (int) rangeMain1.getLength(), "0"); // 0==r
+ assertWatchLoc(procBreakVals.get(2), main2, (int) rangeMain2.getLength(), "1"); // 1==w
+ }
+ }
+
+ @Test
+ public void testPutEnvironment() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Tx')
+ ghidra_trace_put_environment()
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ // Assumes LLDB on Linux amd64
+ TraceObject env =
+ Objects.requireNonNull(
+ tb.objAny("Sessions[].Processes[].Environment", Lifespan.at(0)));
+ assertEquals("x64dbg", env.getValue(0, "_debugger").getValue());
+ assertEquals("x86_64", env.getValue(0, "_arch").getValue());
+ assertEquals("windows", env.getValue(0, "_os").getValue());
+ assertEquals("little", env.getValue(0, "_endian").getValue());
+ }
+ }
+
+ @Test
+ public void testPutRegions() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Tx')
+ ghidra_trace_put_regions()
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ // Would be nice to control / validate the specifics
+ Collection extends TraceMemoryRegion> all =
+ tb.trace.getMemoryManager().getAllRegions();
+ assertThat(all.size(), greaterThan(2));
+ }
+ }
+
+ @Test
+ public void testPutModules() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Tx')
+ ghidra_trace_put_modules()
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ // Would be nice to control / validate the specifics
+ Collection extends TraceModule> all = tb.trace.getModuleManager().getAllModules();
+ TraceModule modBash =
+ Unique.assertOne(all.stream().filter(m -> m.getName(SNAP).contains("notepad")));
+ assertNotEquals(tb.addr(0), Objects.requireNonNull(modBash.getBase(SNAP)));
+ }
+ }
+
+ @Test
+ public void testPutThreads() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ ghidra_trace_create('C:\\\\Windows\\\\notepad.exe', wait=True)
+ ghidra_trace_txstart('Tx')
+ ghidra_trace_put_threads()
+ ghidra_trace_txcommit()
+ ghidra_trace_kill()
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ try (ManagedDomainObject mdo = openDomainObject("/New Traces/x64dbg/notepad.exe")) {
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ // Would be nice to control / validate the specifics
+ Collection extends TraceThread> threads = tb.trace.getThreadManager().getAllThreads();
+ assertThat(threads.size(), greaterThan(2));
+ }
+ }
+
+ @Test
+ public void testMinimal() throws Exception {
+ runThrowError(addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ print('FINISHED')
+ util.terminate_session()
+ quit()
+ """.formatted(PREAMBLE, addr));
+ }
+
+ @Test
+ public void testMinimal2() throws Exception {
+ Function scriptSupplier = addr -> """
+ %s
+ ghidra_trace_connect('%s')
+ util.terminate_session()
+ """.formatted(PREAMBLE, addr);
+ try (PythonAndConnection conn = startAndConnectPython(scriptSupplier)) {
+ conn.execute("print('FINISHED')");
+ conn.close();
+ }
+ }
+}
diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/x64dbg/rmi/X64dbgHooksTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/x64dbg/rmi/X64dbgHooksTest.java
new file mode 100644
index 0000000000..0f0e046a51
--- /dev/null
+++ b/Ghidra/Test/DebuggerIntegrationTest/src/test.slow/java/agent/x64dbg/rmi/X64dbgHooksTest.java
@@ -0,0 +1,399 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package agent.x64dbg.rmi;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.util.*;
+
+import org.junit.Test;
+
+import ghidra.app.plugin.core.debug.utils.ManagedDomainObject;
+import ghidra.debug.api.tracermi.RemoteMethod;
+import ghidra.program.model.address.AddressSpace;
+import ghidra.trace.database.ToyDBTraceBuilder;
+import ghidra.trace.model.Lifespan;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.memory.TraceMemorySpace;
+import ghidra.trace.model.target.TraceObject;
+import ghidra.trace.model.target.path.*;
+import ghidra.trace.model.time.TraceSnapshot;
+
+public class X64dbgHooksTest extends AbstractX64dbgTraceRmiTest {
+ private static final long RUN_TIMEOUT_MS = 5000;
+ private static final long RETRY_MS = 500;
+
+ record PythonAndTrace(PythonAndConnection conn, ManagedDomainObject mdo)
+ implements AutoCloseable {
+ public void execute(String cmd) {
+ conn.execute(cmd);
+ }
+
+ public String executeCapture(String cmd) {
+ return conn.executeCapture(cmd);
+ }
+
+ @Override
+ public void close() throws Exception {
+ try {
+ conn.execute("util.terminate_session()");
+ conn.close();
+ } catch (Exception e) {
+ //IGNORE
+ }
+ try {
+ mdo.close();
+ } catch (Exception e) {
+ //IGNORE
+ }
+
+ }
+ }
+
+ @SuppressWarnings("resource")
+ protected PythonAndTrace startAndSyncPython(String exec) throws Exception {
+ PythonAndConnection conn = startAndConnectPython();
+ try {
+ ManagedDomainObject mdo;
+ conn.execute("from ghidraxdbg.commands import *");
+ conn.execute(
+ "util.set_convenience_variable('ghidra-language', 'x86:LE:64:default')");
+ if (exec != null) {
+ start(conn, exec);
+ mdo = waitDomainObject("/New Traces/x64dbg/" + exec.substring(exec.lastIndexOf("\\")+1));
+ }
+ else {
+ conn.execute("ghidra_trace_start()");
+ mdo = waitDomainObject("/New Traces/x64dbg/noname");
+ }
+ clearBreakpoints(conn);
+ tb = new ToyDBTraceBuilder((Trace) mdo.get());
+ return new PythonAndTrace(conn, mdo);
+ }
+ catch (Exception e) {
+ clearBreakpoints(conn);
+ conn.execute("util.terminate_session()");
+ conn.close();
+ throw e;
+ }
+ }
+
+ protected long lastSnap(PythonAndTrace conn) {
+ return conn.conn.connection().getLastSnapshot(tb.trace);
+ }
+
+ static final int INIT_NOTEPAD_THREAD_COUNT = 4; // This could be fragile
+
+ //@Test - doesn't generate more than the initial 4
+ public void testOnNewThread() throws Exception {
+ try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
+ conn.execute("from ghidraxdbg.commands import *");
+ txPut(conn, "processes");
+
+ waitForPass(() -> {
+ TraceObject proc = tb.objAny0("Sessions[].Processes[]");
+ assertNotNull(proc);
+ assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
+ }, RUN_TIMEOUT_MS, RETRY_MS);
+
+ txPut(conn, "threads");
+ waitForPass(() -> assertEquals(INIT_NOTEPAD_THREAD_COUNT,
+ tb.objValues(lastSnap(conn), "Sessions[].Processes[].Threads[]").size()),
+ RUN_TIMEOUT_MS, RETRY_MS);
+
+ // Via method, go is asynchronous
+ RemoteMethod go = conn.conn.getMethod("go");
+ TraceObject proc = tb.objAny0("Sessions[].Processes[]");
+ go.invoke(Map.of("process", proc)); // Initial breakpoint
+ go.invoke(Map.of("process", proc));
+
+ waitForPass(() -> assertThat(
+ tb.objValues(lastSnap(conn), "Sessions[].Processes[].Threads[]").size(),
+ greaterThan(INIT_NOTEPAD_THREAD_COUNT)),
+ RUN_TIMEOUT_MS, RETRY_MS);
+ }
+ }
+
+ @Test
+ public void testOnNewModule() throws Exception {
+ try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
+ conn.execute("from ghidraxdbg.commands import *");
+ txPut(conn, "processes");
+
+ TraceObject proc = tb.objAny0("Sessions[].Processes[]");
+ waitForPass(() -> {
+ assertNotNull(proc);
+ assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
+ }, RUN_TIMEOUT_MS, RETRY_MS);
+
+ txPut(conn, "modules");
+ waitForPass(() -> assertThat(
+ tb.objValues(lastSnap(conn), "Sessions[].Processes[].Modules[]").size(),
+ greaterThan(0)),
+ RUN_TIMEOUT_MS, RETRY_MS);
+
+ int size = tb.objValues(lastSnap(conn), "Sessions[].Processes[].Modules[]").size();
+ // Via method, go is asynchronous
+ RemoteMethod go = conn.conn.getMethod("go");
+ go.invoke(Map.of("process", proc)); // Initial breakpoint
+ go.invoke(Map.of("process", proc));
+
+ waitForPass(() -> assertThat(
+ tb.objValues(lastSnap(conn), "Sessions[].Processes[].Modules[]").size(),
+ greaterThan(size)),
+ RUN_TIMEOUT_MS, RETRY_MS);
+ }
+ }
+
+ @Test
+ public void testOnThreadSelected() throws Exception {
+ try (PythonAndTrace conn = startAndSyncPython(NOTEPAD)) {
+ txPut(conn, "processes");
+ conn.execute("util.dbg.client.stepi()"); // no initial event
+
+ waitForPass(() -> {
+ TraceObject proc = tb.objAny0("Sessions[0].Processes[]");
+ assertNotNull(proc);
+ assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state"));
+ }, RUN_TIMEOUT_MS, RETRY_MS);
+
+ txPut(conn, "threads");
+ waitForPass(() -> {
+ List