Skip to content

Commit 555970d

Browse files
authored
Merge pull request #45 from ioncodes/ida9-improvements
IDA 9 improvements
2 parents d128198 + 2ddb0ce commit 555970d

5 files changed

Lines changed: 156 additions & 74 deletions

File tree

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ IDACode is still in a very early state and bugs are to be expected. Please open
99
* **Modularity**: IDACode does not make extensive use of safe wrappers for thread synchronization, this allows you to import any module from any path at any given time. Instead IDACode synchronizes the script execution thread with IDAs main thread to avoid performance and unexpected issues.
1010
* **Syncing**: As IDACode uses `debugpy` for communication, it syncs the output window naturally with VS Code's output panel.
1111

12-
IDACode supports both Python 2 and Python 3!
12+
IDACode has been tested on Windows and macos with IDA 8.4/9.0 and Python 3.12 (older python versions have issues with debugging).
1313

1414
## Setup
1515
To set up the dependencies for the IDA plugin run:
@@ -22,12 +22,11 @@ python -m pip install --user debugpy tornado
2222

2323
Either clone this repository or download a release package from [here](https://github.com/ioncodes/idacode/releases). `ida.zip` reflects the contents of the `ida` folder in this repository. Copy all files into IDAs plugin directory.
2424

25-
The next step is to configure your settings to match your environment. Edit `idacode_utils/settings.py` accordingly:
25+
The next step is to configure your settings to match your environment (optional). Edit `idacode_utils/settings.py` accordingly:
2626

2727
* `HOST`: This is the host address. This is always `127.0.0.1` unless you want it to be accessible from a remote location. **Keep in mind that this plugin does not make use of authentication.**
2828
* `PORT`: This is the port you want IDA to listen to. This is used for websocket communication between IDA and VS Code.
2929
* `DEBUG_PORT`: This is the port you want to listen on for incoming debug sessions.
30-
* `PYTHON`: This is the absolute path to the Python distribution that your IDA setup uses.
3130
* `LOGGING`: Determines whether the debugger should log into files. This is especially useful when you are running into issues with IDACode. Please submit a new issue if you find anything. The files are always located in your temp directory (e.g. Windows: `%TEMP%`). The files are called `debugpy.*.log`.
3231

3332
You can now start the plugin by clicking on `IDACode` in the plugins menu.

ida/idacode_utils/hooks.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,15 @@
44
getcwd_original = os.getcwd
55

66
def getcwd_hook():
7-
global script_folder
8-
9-
cwd = getcwd_original()
10-
if cwd.lower() in script_folder.lower() and script_folder.lower() != cwd.lower():
11-
cwd = script_folder
12-
return cwd
7+
# NOTE: We return the script folder here, otherwise breakpoints fail in VSCode
8+
if script_folder:
9+
return script_folder
10+
return getcwd_original()
1311

1412
def set_script_folder(folder):
1513
global script_folder
1614

1715
script_folder = folder
1816

1917
def install():
20-
os.getcwd = getcwd_hook
18+
os.getcwd = getcwd_hook

ida/idacode_utils/plugin.py

Lines changed: 97 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,93 @@
1-
import socket, sys, os, threading, inspect, subprocess
1+
import sys, threading, subprocess, logging
2+
import idacode_utils.settings as settings
23
try:
34
import tornado, debugpy
45
except ImportError:
5-
print("[IDACode] Dependencies missing, run: python -m pip install --user debugpy tornado")
6+
print("[IDACode] Dependencies missing, run:\n \"{}\" -m pip install --user debugpy tornado".format(settings.PYTHON))
67
sys.exit()
78
import idaapi
8-
import idacode_utils.dbg as dbg
9-
import idacode_utils.hooks as hooks
10-
import idacode_utils.settings as settings
119
from idacode_utils.socket_handler import SocketHandler
1210

11+
# Source: https://github.com/OALabs/hexcopy-ida/blob/8b0b2a3021d7dc9010c01821b65a80c47d491b61/hexcopy.py#L30
12+
major, minor = map(int, idaapi.get_kernel_version().split("."))
13+
using_ida7api = (major > 6)
14+
using_pyqt5 = using_ida7api or (major == 6 and minor >= 9)
15+
16+
if using_pyqt5:
17+
import PyQt5.QtWidgets as QtWidgets
18+
else:
19+
import PySide.QtGui as QtGui
20+
QtWidgets = QtGui
21+
1322
# Fix for https://github.com/tornadoweb/tornado/issues/2608
1423
if sys.version_info >= (3, 4):
1524
import asyncio
1625

17-
VERSION = "0.3.0"
18-
initialized = False
19-
20-
def setup_patches():
21-
hooks.install()
22-
#sys.executable = settings.PYTHON
23-
24-
def create_socket_handler():
25-
if sys.version_info >= (3, 4):
26-
asyncio.set_event_loop(asyncio.new_event_loop())
27-
app = tornado.web.Application([
28-
(r"/ws", SocketHandler),
29-
])
30-
server = tornado.httpserver.HTTPServer(app)
31-
print("[IDACode] Listening on {address}:{port}".format(address=settings.HOST, port=settings.PORT))
32-
server.listen(address=settings.HOST, port=settings.PORT)
33-
34-
def start_server():
35-
# Fix for https://github.com/tornadoweb/tornado/issues/2608
36-
if sys.platform=='win32' and sys.version_info >= (3,8):
37-
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
38-
39-
setup_patches()
40-
create_socket_handler()
41-
tornado.ioloop.IOLoop.current().start()
26+
def join_gui_thread(thread: threading.Thread, timeout=None):
27+
iterations = 0
28+
iteration_timeout = 0.1
29+
while True:
30+
if not thread.is_alive():
31+
return True
32+
thread.join(iteration_timeout)
33+
QtWidgets.QApplication.processEvents()
34+
if timeout is not None and iteration_timeout * iterations >= timeout:
35+
return False
36+
iterations += 1
37+
38+
class Server:
39+
def __init__(self):
40+
self.started = False
41+
self.server: tornado.httpserver.HTTPServer = None
42+
self.thread: threading.Thread = None
43+
44+
def start(self):
45+
self.stop()
46+
self.thread = threading.Thread(target=self.server_thread)
47+
self.thread.start()
48+
self.started = True
49+
50+
def stop(self):
51+
if not self.started:
52+
return
53+
54+
if self.server is not None:
55+
self.io_loop.add_callback(self.server.stop)
56+
self.io_loop.add_callback(self.server.close_all_connections)
57+
self.io_loop.add_callback(self.io_loop.stop)
58+
59+
if not join_gui_thread(self.thread, 1.0):
60+
print("[IDACode] Waiting for server to stop...")
61+
if not join_gui_thread(self.thread, 5.0):
62+
print("[IDACode] deadlock while stopping server, please report an issue!\n")
63+
self.thread = None
64+
self.server = None
65+
print("[IDACode] Server stopped")
66+
67+
def server_thread(self):
68+
# Create a new event loop for the thread
69+
# https://github.com/tornadoweb/tornado/issues/2308#issuecomment-372582005
70+
loop = asyncio.new_event_loop()
71+
loop.set_debug(False)
72+
logging.getLogger("asyncio").setLevel(logging.CRITICAL) # Remove some debug spam
73+
asyncio.set_event_loop(loop)
74+
75+
# Before starting the event loop, instantiate a WebSocketClient and add a
76+
# callback to the event loop to start it. This way the first thing the
77+
# event loop does is to start the client.
78+
self.io_loop = tornado.ioloop.IOLoop.current()
79+
app = tornado.web.Application([
80+
(r"/ws", SocketHandler),
81+
])
82+
self.server = tornado.httpserver.HTTPServer(app)
83+
print("[IDACode] Listening on {address}:{port}".format(address=settings.HOST, port=settings.PORT))
84+
self.server.listen(address=settings.HOST, port=settings.PORT)
85+
86+
# Start the event loop.
87+
self.io_loop.start()
88+
89+
# Signal that the service is finished
90+
self.started = False
4291

4392
def get_python_versions():
4493
settings_version = subprocess.check_output([settings.PYTHON, "-c", "import sys; print(sys.version + sys.platform)"])
@@ -47,36 +96,27 @@ def get_python_versions():
4796
return (settings_version, ida_version)
4897

4998
class IDACode(idaapi.plugin_t):
50-
def __init__(self):
51-
self.flags = idaapi.PLUGIN_UNL
52-
self.comment = "IDACode"
53-
self.help = "IDACode"
54-
self.wanted_name = "IDACode"
55-
self.wanted_hotkey = ""
99+
flags = idaapi.PLUGIN_KEEP
100+
comment = "IDACode"
101+
help = "IDACode"
102+
wanted_name = "IDACode"
103+
wanted_hotkey = "Ctrl-Shift-I"
56104

57105
def init(self):
58-
global initialized
59-
if not initialized:
60-
initialized = True
61-
if os.path.isfile(settings.PYTHON):
62-
settings_version, ida_version = get_python_versions()
63-
if settings_version != ida_version:
64-
print("[IDACode] settings.PYTHON version mismatch, aborting load:")
65-
print("[IDACode] IDA interpreter: {}".format(ida_version))
66-
print("[IDACode] settings.PYTHON: {}".format(settings_version))
67-
return idaapi.PLUGIN_SKIP
68-
else:
69-
print("[IDACode] settings.PYTHON ({}) does not exist, aborting load".format(settings.PYTHON))
70-
print("[IDACode] To fix this issue, modify idacode_utils/settings.py to point to the python executable")
71-
return idaapi.PLUGIN_SKIP
72-
print("[IDACode] Plugin version {}".format(VERSION))
73-
print("[IDACode] Plugin loaded, use Edit -> Plugins -> IDACode to start the server")
74-
return idaapi.PLUGIN_OK
106+
settings_version, ida_version = get_python_versions()
107+
if settings_version != ida_version:
108+
print("[IDACode] settings.PYTHON version mismatch, aborting load:")
109+
print("[IDACode] IDA interpreter: {}".format(ida_version))
110+
print("[IDACode] settings.PYTHON: {}".format(settings_version))
111+
return idaapi.PLUGIN_SKIP
112+
113+
self.server = Server()
114+
print("[IDACode] Plugin version 0.4.0")
115+
print("[IDACode] Plugin loaded, use Edit -> Plugins -> IDACode to start the server")
116+
return idaapi.PLUGIN_KEEP
75117

76118
def run(self, args):
77-
thread = threading.Thread(target=start_server)
78-
thread.daemon = True
79-
thread.start()
119+
self.server.start()
80120

81121
def term(self):
82-
pass
122+
self.server.stop()

ida/idacode_utils/settings.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
HOST = "127.0.0.1"
22
PORT = 7065
33
DEBUG_PORT = 7066
4-
PYTHON = "C:\\Program Files\\IDA 7.5\\python38\\python.exe"
5-
LOGGING = False
4+
ALLOW_UNSAFE_ORIGIN = False
5+
LOGGING = True
6+
7+
# Heuristically detect the python executable path
8+
PYTHON = ""
9+
import sys
10+
import os
11+
import platform
12+
for path in sys.path:
13+
if platform.system() == "Windows":
14+
path = path.replace("/", "\\")
15+
16+
split = path.split(os.sep)
17+
if split[-1].endswith(".zip"):
18+
path = os.path.dirname(path)
19+
if platform.system() == "Windows":
20+
python_executable = os.path.join(path, "python.exe")
21+
else:
22+
python_executable = os.path.join(path, "..", "bin", "python3")
23+
python_executable = os.path.abspath(python_executable)
24+
25+
if os.path.exists(python_executable):
26+
print("[IDACode] Detected python executable: " + python_executable)
27+
PYTHON = python_executable
28+
break
29+
if len(PYTHON) == 0 or not os.path.exists(PYTHON):
30+
raise FileNotFoundError("[IDACode] Could not find python executable, report an issue!")

ida/idacode_utils/socket_handler.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,42 @@ def create_env():
1212
"__name__": "__main__"
1313
}
1414

15+
debugpy_host = ""
16+
debugpy_port = 0
17+
1518
def start_debug_server():
19+
# At most one instance of debugpy can ever be created per process.
20+
# Reference: https://github.com/microsoft/debugpy/issues/297
21+
global debugpy_host, debugpy_port
22+
if debugpy_port and debugpy_port:
23+
print("[IDACode] debugpy server is already listening on {}:{}".format(debugpy_host, debugpy_port))
24+
return
25+
26+
# Install hook for os.getcwd
27+
hooks.install()
28+
29+
# Start debugpy server
1630
if settings.LOGGING:
1731
tmp_path = tempfile.gettempdir()
1832
debugpy.log_to(tmp_path)
1933
print("[IDACode] Logging to {} with pattern debugpy.*.log".format(tmp_path))
20-
debugpy.configure({ "python": settings.PYTHON })
21-
debugpy.listen((settings.HOST, settings.DEBUG_PORT))
22-
print("[IDACode] IDACode debug server listening on {address}:{port}".format(address=settings.HOST, port=settings.DEBUG_PORT))
34+
debugpy.configure(python=settings.PYTHON)
35+
debugpy_host, debugpy_port = debugpy.listen((settings.HOST, settings.DEBUG_PORT))
36+
print("[IDACode] Started debugpy server on {}:{}".format(debugpy_host, debugpy_port))
2337

2438
class SocketHandler(tornado.websocket.WebSocketHandler):
39+
def check_origin(self, origin):
40+
# NOTE: This is called when connecting from a browser
41+
return settings.ALLOW_UNSAFE_ORIGIN
42+
2543
def open(self):
2644
print("[IDACode] Client connected")
2745

2846
def on_message(self, message):
29-
message = json.loads(message.decode("utf8"))
30-
47+
if not isinstance(message, str):
48+
message = message.decode("utf-8")
49+
message = json.loads(message)
50+
3151
if message["event"] == "set_workspace":
3252
path = message["path"]
3353
hooks.set_script_folder(path)

0 commit comments

Comments
 (0)