diff --git a/README.md b/README.md index 8cf3770..e30743f 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,9 @@ import urllib2; exec(urllib2.urlopen('https://raw.githubusercontent.com/fidgetin To enable the integrated server, you can choose "Integrated Server" after right-clicking the IDArling widget located in the status bar. -The integrated server requires PyQt5, which is integrated into IDA. If you're +The integrated server requires PySide6, which is integrated into IDA. If you're using an external Python installation, we recommand using Python 3, which offers -a pre-built package that can be installed with a simple `pip install PyQt5`. +a pre-built package that can be installed with a simple `pip install PySide6`. ## Connection to server and usage diff --git a/idarling/core/core.py b/idarling/core/core.py index c48a4f1..1bcff3d 100644 --- a/idarling/core/core.py +++ b/idarling/core/core.py @@ -21,7 +21,7 @@ import ida_netnode import ida_typeinf -from PyQt5.QtCore import QCoreApplication, QFileInfo # noqa: I202 +from PySide6.QtCore import QCoreApplication, QFileInfo # noqa: I202 from .hooks import HexRaysHooks, IDBHooks, IDPHooks, UIHooks from ..module import Module @@ -121,7 +121,7 @@ def tick(self, tick): self.save_netnode() def update_local_types_map(self): - for i in range(1, ida_typeinf.get_ordinal_qty(ida_typeinf.get_idati())): + for i in range(1, ida_typeinf.get_ordinal_count(ida_typeinf.get_idati())): t = ImportLocalType(i) self.local_type_map[i] = t diff --git a/idarling/core/events.py b/idarling/core/events.py index 8c1a096..06b4f5c 100644 --- a/idarling/core/events.py +++ b/idarling/core/events.py @@ -15,7 +15,6 @@ import sys import ida_bytes -import ida_enum import ida_funcs import ida_hexrays import ida_idaapi @@ -28,7 +27,6 @@ import ida_range import ida_segment import ida_segregs -import ida_struct import ida_typeinf import ida_ua import ida_idc @@ -115,7 +113,7 @@ def __init__(self, ea, flags, size, sname): self.sname = sname def __call__(self): - ida_bytes.create_data(self.ea, ida_bytes.calc_dflags(self.flags, True), self.size, ida_struct.get_struc_id(self.sname) if self.sname else ida_netnode.BADNODE) + ida_bytes.create_data(self.ea, ida_bytes.calc_dflags(self.flags, True), self.size, idc.get_struc_id(self.sname) if self.sname else ida_netnode.BADNODE) class RenamedEvent(Event): @@ -133,8 +131,13 @@ def __call__(self): self.ea, self.new_name, flags | ida_name.SN_NOWARN ) ida_kernwin.request_refresh(ida_kernwin.IWID_DISASMS) - ida_kernwin.request_refresh(ida_kernwin.IWID_STRUCTS) ida_kernwin.request_refresh(ida_kernwin.IWID_STKVIEW) + + if hasattr(ida_kernwin, "IWID_STRUCTS"): + ida_kernwin.request_refresh(ida_kernwin.IWID_STRUCTS) + else: + ida_kernwin.refresh_idaview_anyway() + HexRaysEvent.refresh_pseudocode_view(self.ea) @@ -295,7 +298,7 @@ def __call__(self): py_type = py_type[1:] if len(py_type) >= 2: if self.name: - r = ida_struct.get_member_by_fullname(self.name) + r = idc.get_member_by_fullname(self.name) if r: self.ea = r[0].id ida_typeinf.apply_type( @@ -404,14 +407,14 @@ def __call__(self): if self.op == "offset": ida_offset.op_plain_offset(self.ea, self.n, 0) if self.op == "enum": - id = ida_enum.get_enum(self.extra["ename"]) + id = idc.get_enum(self.extra["ename"]) ida_bytes.op_enum(self.ea, self.n, id, self.extra["serial"]) if self.op == "struct": path_len = len(self.extra["spath"]) path = ida_pro.tid_array(path_len) for i in range(path_len): sname = self.extra["spath"][i] - path[i] = ida_struct.get_struc_id(sname) + path[i] = idc.get_struc_id(sname) insn = ida_ua.insn_t() ida_ua.decode_insn(insn, self.ea) ida_bytes.op_stroff( @@ -433,7 +436,7 @@ def __init__(self, enum, name): self.name = name def __call__(self): - ida_enum.add_enum(self.enum, self.name, 0) + idc.add_enum(self.enum, self.name, 0) class EnumDeletedEvent(Event): @@ -444,7 +447,7 @@ def __init__(self, ename): self.ename = ename def __call__(self): - ida_enum.del_enum(ida_enum.get_enum(self.ename)) + idc.del_enum(idc.get_enum(self.ename)) class EnumRenamedEvent(Event): @@ -458,11 +461,11 @@ def __init__(self, oldname, newname, is_enum): def __call__(self): if self.is_enum: - enum = ida_enum.get_enum(self.oldname) - ida_enum.set_enum_name(enum, self.newname) + enum = idc.get_enum(self.oldname) + idc.set_enum_name(enum, self.newname) else: - emem = ida_enum.get_enum_member_by_name(self.oldname) - ida_enum.set_enum_member_name(emem, self.newname) + emem = idc.get_enum_member_by_name(self.oldname) + idc.set_enum_member_name(emem, self.newname) class EnumBfChangedEvent(Event): @@ -474,8 +477,8 @@ def __init__(self, ename, bf_flag): self.bf_flag = bf_flag def __call__(self): - enum = ida_enum.get_enum(self.ename) - ida_enum.set_enum_bf(enum, self.bf_flag) + enum = idc.get_enum(self.ename) + idc.set_enum_bf(enum, self.bf_flag) class EnumCmtChangedEvent(Event): @@ -488,9 +491,9 @@ def __init__(self, emname, cmt, repeatable_cmt): self.repeatable_cmt = repeatable_cmt def __call__(self): - emem = ida_enum.get_enum_member_by_name(self.emname) + emem = idc.get_enum_member_by_name(self.emname) cmt = self.cmt if self.cmt else "" - ida_enum.set_enum_cmt(emem, cmt, self.repeatable_cmt) + idc.set_enum_cmt(emem, cmt, self.repeatable_cmt) class EnumMemberCreatedEvent(Event): @@ -504,8 +507,8 @@ def __init__(self, ename, name, value, bmask): self.bmask = bmask def __call__(self): - enum = ida_enum.get_enum(self.ename) - ida_enum.add_enum_member( + enum = idc.get_enum(self.ename) + idc.add_enum_member( enum, self.name, self.value, self.bmask ) @@ -521,8 +524,8 @@ def __init__(self, ename, value, serial, bmask): self.bmask = bmask def __call__(self): - enum = ida_enum.get_enum(self.ename) - ida_enum.del_enum_member(enum, self.value, self.serial, self.bmask) + enum = idc.get_enum(self.ename) + idc.del_enum_member(enum, self.value, self.serial, self.bmask) class StrucCreatedEvent(Event): @@ -535,7 +538,7 @@ def __init__(self, struc, name, is_union): self.is_union = is_union def __call__(self): - ida_struct.add_struc( + idc.add_struc( ida_idaapi.BADADDR, self.name, self.is_union ) @@ -548,8 +551,8 @@ def __init__(self, sname): self.sname = sname def __call__(self): - struc = ida_struct.get_struc_id(self.sname) - ida_struct.del_struc(ida_struct.get_struc(struc)) + struc = idc.get_struc_id(self.sname) + idc.del_struc(idc.get_struc(struc)) class StrucRenamedEvent(Event): @@ -561,8 +564,8 @@ def __init__(self, oldname, newname): self.newname = newname def __call__(self): - struc = ida_struct.get_struc_id(self.oldname) - ida_struct.set_struc_name(struc, self.newname) + struc = idc.get_struc_id(self.oldname) + idc.set_struc_name(struc, self.newname) class StrucCmtChangedEvent(Event): @@ -576,16 +579,16 @@ def __init__(self, sname, smname, cmt, repeatable_cmt): self.repeatable_cmt = repeatable_cmt def __call__(self): - struc = ida_struct.get_struc_id(self.sname) - sptr = ida_struct.get_struc(struc) + struc = idc.get_struc_id(self.sname) + sptr = idc.get_struc(struc) cmt = self.cmt if self.cmt else "" if self.smname: - mptr = ida_struct.get_member_by_name( + mptr = idc.get_member_by_name( sptr, self.smname ) - ida_struct.set_member_cmt(mptr, cmt, self.repeatable_cmt) + idc.set_member_cmt(mptr, cmt, self.repeatable_cmt) else: - ida_struct.set_struc_cmt(sptr.id, cmt, self.repeatable_cmt) + idc.set_struc_cmt(sptr.id, cmt, self.repeatable_cmt) class StrucMemberEvent(Event): @@ -594,14 +597,14 @@ class StrucMemberEvent(Event): """ @staticmethod def _get_sptr(struct_name): - struc_id = ida_struct.get_struc_id(struct_name) - return ida_struct.get_struc(struc_id) + struc_id = idc.get_struc_id(struct_name) + return idc.get_struc(struc_id) @staticmethod def _get_member_type(type_flag, extra): mt = ida_nalt.opinfo_t() if ida_bytes.is_struct(type_flag): - mt.tid = ida_struct.get_struc_id(extra['struc_name']) + mt.tid = idc.get_struc_id(extra['struc_name']) if type_flag & ida_bytes.off_flag(): mt.ri = ida_nalt.refinfo_t() mt.ri.init( @@ -635,7 +638,7 @@ def __init__(self, sname, fieldname, offset, flag, nbytes, extra): def __call__(self): sptr = self._get_sptr(self.sname) mt = self._get_member_type(self.flag, self.extra) - ida_struct.add_struc_member( + idc.add_struc_member( sptr, self.fieldname, self.offset, @@ -659,7 +662,7 @@ def __init__(self, sname, soff, eoff, flag, extra): def __call__(self): sptr = self._get_sptr(self.sname) mt = self._get_member_type(self.flag, self.extra) - ida_struct.set_member_type( + idc.set_member_type( sptr, self.soff, self.flag, mt, self.eoff - self.soff ) @@ -674,7 +677,7 @@ def __init__(self, sname, offset): def __call__(self): sptr = self._get_sptr(self.sname) - ida_struct.del_struc_member(sptr, self.offset) + idc.del_struc_member(sptr, self.offset) class StrucMemberRenamedEvent(StrucMemberEvent): @@ -688,7 +691,7 @@ def __init__(self, sname, offset, newname): def __call__(self): sptr = self._get_sptr(self.sname) - ida_struct.set_member_name( + idc.set_member_name( sptr, self.offset, self.newname ) @@ -703,9 +706,9 @@ def __init__(self, sname, offset, delta): self.delta = delta def __call__(self): - struc = ida_struct.get_struc_id(self.sname) - sptr = ida_struct.get_struc(struc) - ida_struct.expand_struc(sptr, self.offset, self.delta) + struc = idc.get_struc_id(self.sname) + sptr = idc.get_struc(struc) + idc.expand_struc(sptr, self.offset, self.delta) class SegmAddedEvent(Event): diff --git a/idarling/core/hooks.py b/idarling/core/hooks.py index 409b12d..e440f33 100644 --- a/idarling/core/hooks.py +++ b/idarling/core/hooks.py @@ -15,17 +15,16 @@ import ida_auto import ida_bytes -import ida_enum import ida_funcs import ida_hexrays import ida_idaapi import ida_idp import ida_kernwin import ida_nalt +import idc import ida_netnode import ida_pro import ida_segment -import ida_struct import ida_typeinf fDebug = False @@ -77,9 +76,9 @@ def auto_empty(self): def local_types_changed(self): changed_types = [] # self._plugin.logger.trace(self._plugin.core.local_type_map) - for i in range(1, ida_typeinf.get_ordinal_qty(ida_typeinf.get_idati())): + for i in range(1, ida_typeinf.get_ordinal_count(ida_typeinf.get_idati())): t = ImportLocalType(i) - if t and t.name and ida_struct.get_struc_id(t.name) == ida_idaapi.BADADDR and ida_enum.get_enum(t.name) == ida_idaapi.BADADDR: + if t and t.name and idc.get_struc_id(t.name) == ida_idaapi.BADADDR and idc.get_enum(t.name) == ida_idaapi.BADADDR: if i in self._plugin.core.local_type_map: t_old = self._plugin.core.local_type_map[i] if t_old and not t.isEqual(t_old): @@ -170,8 +169,8 @@ def local_types_changed(self): def ti_changed(self, ea, type, fname): self._plugin.logger.debug("ti_changed(ea = 0x%X, type = %s, fname = %s)" % (ea, type, fname)) name = "" - if ida_struct.is_member_id(ea): - name = ida_struct.get_struc_name(ea) + if idc.is_member_id(ea): + name = idc.get_struc_name(ea) type = ida_typeinf.idc_get_type_raw(ea) self._send_packet(evt.TiChangedEvent(ea, (ParseTypeString(type[0]) if type else [], type[1] if type else None), name)) return 0 @@ -184,7 +183,7 @@ def op_type_changed(self, ea, n): self._plugin.logger.debug("op_type_changed(ea = %x, n = %d)" % (ea,n)) def gather_enum_info(ea, n): id = ida_bytes.get_enum_id(ea, n)[0] - serial = ida_enum.get_enum_idx(id) + serial = idc.get_enum_idx(id) return id, serial extra = {} @@ -209,7 +208,7 @@ def is_flag(type): elif is_flag(ida_bytes.enum_flag()): op = "enum" id, serial = gather_enum_info(ea, n) - ename = ida_enum.get_enum_name(id) + ename = idc.get_enum_name(id) extra["ename"] = Event.decode(ename) extra["serial"] = serial elif flags & ida_bytes.stroff_flag(): @@ -221,7 +220,7 @@ def is_flag(type): ) spath = [] for i in range(path_len): - sname = ida_struct.get_struc_name(path[i]) + sname = idc.get_struc_name(path[i]) spath.append(Event.decode(sname)) extra["delta"] = delta.value() extra["spath"] = spath @@ -236,41 +235,41 @@ def is_flag(type): return 0 def enum_created(self, enum): - name = ida_enum.get_enum_name(enum) + name = idc.get_enum_name(enum) self._send_packet(evt.EnumCreatedEvent(enum, name)) return 0 # XXX - use enum_deleted(self, id) instead? def deleting_enum(self, id): - self._send_packet(evt.EnumDeletedEvent(ida_enum.get_enum_name(id))) + self._send_packet(evt.EnumDeletedEvent(idc.get_enum_name(id))) return 0 # XXX - use enum_renamed(self, id) instead? def renaming_enum(self, id, is_enum, newname): if is_enum: - oldname = ida_enum.get_enum_name(id) + oldname = idc.get_enum_name(id) else: - oldname = ida_enum.get_enum_member_name(id) + oldname = idc.get_enum_member_name(id) self._send_packet(evt.EnumRenamedEvent(oldname, newname, is_enum)) return 0 def enum_bf_changed(self, id): - bf_flag = 1 if ida_enum.is_bf(id) else 0 - ename = ida_enum.get_enum_name(id) + bf_flag = 1 if idc.is_bf(id) else 0 + ename = idc.get_enum_name(id) self._send_packet(evt.EnumBfChangedEvent(ename, bf_flag)) return 0 def enum_cmt_changed(self, tid, repeatable_cmt): - cmt = ida_enum.get_enum_cmt(tid, repeatable_cmt) - emname = ida_enum.get_enum_name(tid) + cmt = idc.get_enum_cmt(tid, repeatable_cmt) + emname = idc.get_enum_name(tid) self._send_packet(evt.EnumCmtChangedEvent(emname, cmt, repeatable_cmt)) return 0 def enum_member_created(self, id, cid): - ename = ida_enum.get_enum_name(id) - name = ida_enum.get_enum_member_name(cid) - value = ida_enum.get_enum_member_value(cid) - bmask = ida_enum.get_enum_member_bmask(cid) + ename = idc.get_enum_name(id) + name = idc.get_enum_member_name(cid) + value = idc.get_enum_member_value(cid) + bmask = idc.get_enum_member_bmask(cid) self._send_packet( evt.EnumMemberCreatedEvent(ename, name, value, bmask) ) @@ -278,24 +277,24 @@ def enum_member_created(self, id, cid): # XXX - use enum_member_deleted(self, id, cid) instead? def deleting_enum_member(self, id, cid): - ename = ida_enum.get_enum_name(id) - value = ida_enum.get_enum_member_value(cid) - serial = ida_enum.get_enum_member_serial(cid) - bmask = ida_enum.get_enum_member_bmask(cid) + ename = idc.get_enum_name(id) + value = idc.get_enum_member_value(cid) + serial = idc.get_enum_member_serial(cid) + bmask = idc.get_enum_member_bmask(cid) self._send_packet( evt.EnumMemberDeletedEvent(ename, value, serial, bmask) ) return 0 def struc_created(self, tid): - name = ida_struct.get_struc_name(tid) - is_union = ida_struct.is_union(tid) + name = idc.get_struc_name(tid) + is_union = idc.is_union(tid) self._send_packet(evt.StrucCreatedEvent(tid, name, is_union)) return 0 # XXX - use struc_deleted(self, struc_id) instead? def deleting_struc(self, sptr): - sname = ida_struct.get_struc_name(sptr.id) + sname = idc.get_struc_name(sptr.id) self._send_packet(evt.StrucDeletedEvent(sname)) return 0 @@ -310,19 +309,19 @@ def renaming_struc(self, id, oldname, newname): # XXX - use struc_expanded(self, sptr) instead def expanding_struc(self, sptr, offset, delta): - sname = ida_struct.get_struc_name(sptr.id) + sname = idc.get_struc_name(sptr.id) self._send_packet(evt.ExpandingStrucEvent(sname, offset, delta)) return 0 def struc_member_created(self, sptr, mptr): extra = {} - sname = ida_struct.get_struc_name(sptr.id) - fieldname = ida_struct.get_member_name(mptr.id) + sname = idc.get_struc_name(sptr.id) + fieldname = idc.get_member_name(mptr.id) offset = 0 if mptr.unimem() else mptr.soff flag = mptr.flag nbytes = mptr.eoff if mptr.unimem() else mptr.eoff - mptr.soff mt = ida_nalt.opinfo_t() - is_not_data = ida_struct.retrieve_member_info(mt, mptr) + is_not_data = idc.retrieve_member_info(mt, mptr) if is_not_data: if flag & ida_bytes.off_flag(): extra["target"] = mt.ri.target @@ -343,7 +342,7 @@ def struc_member_created(self, sptr, mptr): ) ) elif flag & ida_bytes.stru_flag(): - extra["struc_name"] = ida_struct.get_struc_name(mt.tid) + extra["struc_name"] = idc.get_struc_name(mt.tid) if flag & ida_bytes.strlit_flag(): extra["strtype"] = mt.strtype self._send_packet( @@ -360,13 +359,13 @@ def struc_member_created(self, sptr, mptr): return 0 def struc_member_deleted(self, sptr, off1, off2): - sname = ida_struct.get_struc_name(sptr.id) + sname = idc.get_struc_name(sptr.id) self._send_packet(evt.StrucMemberDeletedEvent(sname, off2)) return 0 # XXX - use struc_member_renamed(self, sptr, mptr) instead? def renaming_struc_member(self, sptr, mptr, newname): - sname = ida_struct.get_struc_name(sptr.id) + sname = idc.get_struc_name(sptr.id) offset = mptr.soff self._send_packet(evt.StrucMemberRenamedEvent(sname, offset, newname)) return 0 @@ -374,10 +373,10 @@ def renaming_struc_member(self, sptr, mptr, newname): def struc_member_changed(self, sptr, mptr): extra = {} - sname = ida_struct.get_struc_name(sptr.id) + sname = idc.get_struc_name(sptr.id) flag = mptr.flag mt = ida_nalt.opinfo_t() - is_not_data = ida_struct.retrieve_member_info(mt, mptr) + is_not_data = idc.retrieve_member_info(mt, mptr) if is_not_data: if flag & ida_bytes.off_flag(): extra["target"] = mt.ri.target @@ -398,7 +397,7 @@ def struc_member_changed(self, sptr, mptr): ) ) elif flag & ida_bytes.stru_flag(): - extra["struc_name"] = ida_struct.get_struc_name(mt.tid) + extra["struc_name"] = idc.get_struc_name(mt.tid) if flag & ida_bytes.strlit_flag(): extra["strtype"] = mt.strtype self._send_packet( @@ -415,13 +414,13 @@ def struc_member_changed(self, sptr, mptr): return 0 def struc_cmt_changed(self, id, repeatable_cmt): - fullname = ida_struct.get_struc_name(id) + fullname = idc.get_struc_name(id) if "." in fullname: sname, smname = fullname.split(".", 1) else: sname = fullname smname = "" - cmt = ida_struct.get_struc_cmt(id, repeatable_cmt) + cmt = idc.get_struc_cmt(id, repeatable_cmt) self._send_packet( evt.StrucCmtChangedEvent(sname, smname, cmt, repeatable_cmt) ) @@ -529,12 +528,14 @@ def sgr_changed(self, start_ea, end_ea, regnum, value, old_value, tag): def make_data(self, ea, flags, tid, size): self._plugin.logger.debug("make_data(ea = %x, flags = %x, tid = %x, size = %x)" % (ea, flags, tid, size)) # Note: MakeDataEvent.sname == '' is convention for BADNODE - self._send_packet(evt.MakeDataEvent(ea, flags, size, ida_struct.get_struc_name(tid) if tid != ida_netnode.BADNODE else '')) + self._send_packet(evt.MakeDataEvent(ea, flags, size, idc.get_struc_name(tid) if tid != ida_netnode.BADNODE else '')) return 0 def renamed(self, ea, new_name, local_name): self._plugin.logger.debug("renamed(ea = %x, new_name = %s, local_name = %d)" % (ea, new_name, local_name)) - if ida_struct.is_member_id(ea) or ida_struct.get_struc(ea) or ida_enum.get_enum_name(ea): + # `idc.get_struc` was removed in newer IDA Python APIs; avoid calling it. + # We only need to detect member-id or enum rename events here. + if idc.is_member_id(ea) or idc.get_enum_name(ea): # Drop hook to avoid duplicate since already handled by the following hooks: # - renaming_struc_member() -> sends 'StrucMemberRenamedEvent' # - renaming_struc() -> sends 'StrucRenamedEvent' diff --git a/idarling/interface/actions.py b/idarling/interface/actions.py index 9dbb914..3e900ab 100644 --- a/idarling/interface/actions.py +++ b/idarling/interface/actions.py @@ -22,9 +22,9 @@ import ida_kernwin import ida_loader -from PyQt5.QtCore import QCoreApplication, QFileInfo, Qt # noqa: I202 -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QMessageBox, QProgressDialog +from PySide6.QtCore import QCoreApplication, QFileInfo, Qt # noqa: I202 +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import QMessageBox, QProgressDialog from .dialogs import OpenDialog, SaveDialog from ..shared.commands import DownloadFile, UpdateFile @@ -207,7 +207,7 @@ def _file_downloaded(self, project, snapshot, progress, reply): # Get the absolute path of the file app_path = QCoreApplication.applicationFilePath() app_name = QFileInfo(app_path).fileName() - file_ext = "i64" if "64" in app_name else "idb" + file_ext = "i64" file_name = "%s_%s_%s.%s" % (project.name, snapshot.binary, snapshot.name, file_ext) file_path = os.path.join(self._plugin.config["files_dir"], file_name) @@ -251,7 +251,7 @@ def _file_downloaded(self, project, snapshot, progress, reply): # Create a temporary copy of the new database because we cannot use # the snapshot functionality to restore the currently opened database - file_ext = ".i64" if "64" in app_name else ".idb" + file_ext = ".i64" tmp_file, tmp_path = tempfile.mkstemp(suffix=file_ext) shutil.copyfile(file_path, tmp_path) diff --git a/idarling/interface/dialogs.py b/idarling/interface/dialogs.py index 2d61912..7611dfa 100644 --- a/idarling/interface/dialogs.py +++ b/idarling/interface/dialogs.py @@ -20,9 +20,9 @@ import ida_nalt import idc -from PyQt5.QtCore import QRegExp, Qt, QDir # noqa: I202 -from PyQt5.QtGui import QIcon, QRegExpValidator -from PyQt5.QtWidgets import ( +from PySide6.QtCore import QRegularExpression, Qt, QDir # noqa: I202 +from PySide6.QtGui import QIcon, QRegularExpressionValidator +from PySide6.QtWidgets import ( QCheckBox, QColorDialog, QComboBox, @@ -44,6 +44,14 @@ QWidget, QSizePolicy, QFileDialog, ) +# QHeaderView enum compatibility between PyQt5 and PySide6 +try: + _HV_Stretch = QHeaderView.ResizeMode.Stretch + _HV_ResizeToContents = QHeaderView.ResizeMode.ResizeToContents +except AttributeError: + _HV_Stretch = QHeaderView.Stretch + _HV_ResizeToContents = QHeaderView.ResizeToContents + from ..shared.commands import ( CreateProject, CreateBinary, @@ -164,7 +172,7 @@ def __init__(self, plugin): self._snapshots_table.setHorizontalHeaderLabels(labels) horizontal_header = self._snapshots_table.horizontalHeader() horizontal_header.setSectionsClickable(False) - horizontal_header.setSectionResizeMode(0, horizontal_header.Stretch) + horizontal_header.setSectionResizeMode(0, _HV_Stretch) self._snapshots_table.verticalHeader().setVisible(False) self._snapshots_table.setSelectionBehavior(QTableWidget.SelectRows) self._snapshots_table.setSelectionMode(QTableWidget.SingleSelection) @@ -685,7 +693,7 @@ def __init__(self, plugin): self._nameLabel = QLabel("Project Name") layout.addWidget(self._nameLabel) self._nameEdit = QLineEdit() - self._nameEdit.setValidator(QRegExpValidator(QRegExp("[a-zA-Z0-9-]+"))) + self._nameEdit.setValidator(QRegularExpressionValidator(QRegularExpression("[a-zA-Z0-9-]+"))) layout.addWidget(self._nameEdit) buttons = QWidget(self) @@ -897,9 +905,9 @@ def state_changed(state): "Auto")) horizontal_header = self._servers_table.horizontalHeader() horizontal_header.setSectionsClickable(False) - horizontal_header.setSectionResizeMode(0, QHeaderView.Stretch) - horizontal_header.setSectionResizeMode(1, QHeaderView.ResizeToContents) - horizontal_header.setSectionResizeMode(2, QHeaderView.ResizeToContents) + horizontal_header.setSectionResizeMode(0, _HV_Stretch) + horizontal_header.setSectionResizeMode(1, _HV_ResizeToContents) + horizontal_header.setSectionResizeMode(2, _HV_ResizeToContents) self._servers_table.verticalHeader().setVisible(False) self._servers_table.setSelectionBehavior(QTableWidget.SelectRows) self._servers_table.setSelectionMode(QTableWidget.SingleSelection) diff --git a/idarling/interface/filter.py b/idarling/interface/filter.py index 9ae9c9f..c0efaeb 100644 --- a/idarling/interface/filter.py +++ b/idarling/interface/filter.py @@ -13,11 +13,10 @@ import ida_funcs import ida_kernwin -from PyQt5.QtCore import QEvent, QObject, Qt # noqa: I202 -from PyQt5.QtGui import QContextMenuEvent, QIcon, QImage, QPixmap, QShowEvent -from PyQt5.QtWidgets import ( - QAction, - qApp, +from PySide6.QtCore import QEvent, QObject, Qt # noqa: I202 +from PySide6.QtGui import QAction, QContextMenuEvent, QIcon, QImage, QPixmap, QShowEvent +from PySide6.QtWidgets import ( + QApplication, QDialog, QGroupBox, QLabel, @@ -26,6 +25,7 @@ QWidget, ) + from .widget import StatusWidget from ..shared.commands import InviteToLocation @@ -43,11 +43,11 @@ def __init__(self, plugin, parent=None): def install(self): self._plugin.logger.debug("Installing the event filter") - qApp.instance().installEventFilter(self) + QApplication.instance().installEventFilter(self) def uninstall(self): self._plugin.logger.debug("Uninstalling the event filter") - qApp.instance().removeEventFilter(self) + QApplication.instance().removeEventFilter(self) def _replace_icon(self, label): pixmap = QPixmap(self._plugin.plugin_resource("idarling.png")) diff --git a/idarling/interface/interface.py b/idarling/interface/interface.py index eab53da..bb460a7 100644 --- a/idarling/interface/interface.py +++ b/idarling/interface/interface.py @@ -12,8 +12,8 @@ # along with this program. If not, see . import time -from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import qApp, QMainWindow +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QApplication, QMainWindow from .actions import OpenAction, SaveAction from .filter import EventFilter @@ -37,7 +37,8 @@ def __init__(self, plugin): # Find the QMainWindow instance self._plugin.logger.debug("Searching for the main window") - for widget in qApp.topLevelWidgets(): + app = QApplication.instance() + for widget in app.topLevelWidgets(): if isinstance(widget, QMainWindow): self._window = widget break diff --git a/idarling/interface/invites.py b/idarling/interface/invites.py index f67ee0c..19d1a89 100644 --- a/idarling/interface/invites.py +++ b/idarling/interface/invites.py @@ -10,16 +10,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from PyQt5.QtCore import ( - pyqtProperty, +from PySide6.QtCore import ( + Property, QPoint, QPropertyAnimation, QRect, Qt, QTimer, ) -from PyQt5.QtGui import QBrush, QColor, QPainter -from PyQt5.QtWidgets import QHBoxLayout, QLabel, QWidget +from PySide6.QtGui import QBrush, QColor, QPainter +from PySide6.QtWidgets import QHBoxLayout, QLabel, QWidget class Invite(QWidget): @@ -186,7 +186,7 @@ def hide_animation(self): self._animation.setEndValue(0.0) self._animation.start() - @pyqtProperty(float) + @Property(float) def popup_opacity(self): return self._popup_opacity diff --git a/idarling/interface/painter.py b/idarling/interface/painter.py index 799d3a2..369fdad 100644 --- a/idarling/interface/painter.py +++ b/idarling/interface/painter.py @@ -15,15 +15,15 @@ import ida_funcs import ida_kernwin -from PyQt5.QtCore import ( # noqa: I202 +from PySide6.QtCore import ( # noqa: I202 QAbstractItemModel, QModelIndex, QObject, Qt, ) -from PyQt5.QtGui import QColor -from PyQt5.QtWidgets import QStyledItemDelegate, QWidget -import sip +from PySide6.QtGui import QColor +from PySide6.QtWidgets import QStyledItemDelegate, QWidget +from shiboken6 import wrapInstance from .widget import StatusWidget @@ -81,7 +81,7 @@ def __init__(self, plugin): self._ida_nav_colorizer = None self._nbytes = 0 - # XXX - unused since moved to Python3 in IDA due to errors in PyQt5 + # XXX - unused since moved to Python3 in IDA due to errors in PySide6 # "OverflowError: Python int too large to convert to C long" # See https://www.hex-rays.com/products/ida/support/idapython_docs/ida_kernwin-module.html#set_nav_colorizer def nav_colorizer(self, ea, nbytes): @@ -138,7 +138,7 @@ def get_bg_color(self, ea): return None def widget_visible(self, twidget): - widget = sip.wrapinstance(long(twidget), QWidget) + widget = wrapInstance(int(twidget), QWidget) if widget.windowTitle() != "Functions window": return table = widget.layout().itemAt(0).widget() diff --git a/idarling/interface/widget.py b/idarling/interface/widget.py index 64552db..be66be2 100644 --- a/idarling/interface/widget.py +++ b/idarling/interface/widget.py @@ -14,9 +14,9 @@ from functools import partial, lru_cache import time -from PyQt5.QtCore import QPoint, QRect, QSize, Qt, QTimer -from PyQt5.QtGui import QIcon, QImage, QPainter, QPixmap, QRegion -from PyQt5.QtWidgets import QAction, QActionGroup, QLabel, QMenu, QWidget +from PySide6.QtCore import QPoint, QRect, QSize, Qt, QTimer +from PySide6.QtGui import QAction, QActionGroup, QIcon, QImage, QPainter, QPixmap, QRegion +from PySide6.QtWidgets import QLabel, QMenu, QWidget from .dialogs import SettingsDialog diff --git a/idarling/network/client.py b/idarling/network/client.py index d3bfd9e..e5c1dc7 100644 --- a/idarling/network/client.py +++ b/idarling/network/client.py @@ -13,7 +13,7 @@ import ida_auto import ida_kernwin -from PyQt5.QtGui import QImage, QPixmap # noqa: I202 +from PySide6.QtGui import QImage, QPixmap # noqa: I202 from ..interface.widget import StatusWidget from ..shared.commands import ( @@ -102,8 +102,8 @@ def send_packet(self, packet): packet.tick = self._plugin.core.tick return ClientSocket.send_packet(self, packet) - def disconnect(self, err=None): - ret = ClientSocket.disconnect(self, err) + def terminate(self, err=None): + ret = ClientSocket.close_connection(self, err) self._plugin.network._client = None self._plugin.network._server = None @@ -188,12 +188,12 @@ def _handle_download_file(self, query): def _handle_delete_project(self, packet): # TODO: Handle situation then user snapshot in deleted project - self.disconnect() + self.terminate() def _handle_delete_binary(self, packet): # TODO: Handle situation then user snapshot in deleted binary - self.disconnect() + self.terminate() def _handle_delete_snapshot(self, packet): # TODO: Handle situation then user snapshot deleted - self.disconnect() \ No newline at end of file + self.terminate() \ No newline at end of file diff --git a/idarling/network/network.py b/idarling/network/network.py index 3044a6a..e6359f2 100644 --- a/idarling/network/network.py +++ b/idarling/network/network.py @@ -117,7 +117,7 @@ def connect(self, server): raise OSError(err, os.strerror(err), '') except OSError as e: self._plugin._logger.exception(e) - self._client.disconnect() + self._client.terminate() def disconnect(self): """Disconnect from the current server.""" @@ -126,7 +126,7 @@ def disconnect(self): return self._plugin.logger.info("Disconnecting...") - self._client.disconnect() + self._client.terminate() def send_packet(self, packet): """Send a packet to the server.""" diff --git a/idarling/plugin.py b/idarling/plugin.py index 7104ef5..dd65f05 100644 --- a/idarling/plugin.py +++ b/idarling/plugin.py @@ -28,7 +28,7 @@ -class Plugin(ida_idaapi.plugin_t): +class IdarlingPlugin(ida_idaapi.plugin_t): """ This is the main class of the plugin. It subclasses plugin_t as required by IDA. It holds the modules of plugin, which themselves provides the @@ -51,7 +51,7 @@ class Plugin(ida_idaapi.plugin_t): @staticmethod def description(): """Return the description displayed in the console.""" - return "{} v{}".format(Plugin.PLUGIN_NAME, Plugin.PLUGIN_VERSION) + return "{} v{}".format(IdarlingPlugin.PLUGIN_NAME, IdarlingPlugin.PLUGIN_VERSION) @staticmethod def plugin_resource(filename): @@ -87,7 +87,7 @@ def default_config(): """ r, g, b = colorsys.hls_to_rgb(random.random(), 0.5, 1.0) color = int(b * 255) << 16 | int(g * 255) << 8 | int(r * 255) - file_path = Plugin.user_resource("files", "") + file_path = IdarlingPlugin.user_resource("files", "") return { "level": logging.INFO, "servers": [], @@ -107,7 +107,7 @@ def __init__(self): # Then setup the default logger log_path = self.user_resource("logs", "idarling.%s.log" % os.getpid()) level = self.config["level"] - self._logger = start_logging(log_path, "IDArling.Plugin", level) + self._logger = start_logging(log_path, "IDArling.IdarlingPlugin", level) self._core = Core(self) self._interface = Interface(self) diff --git a/idarling/server.py b/idarling/server.py index 11b36d8..bcda617 100644 --- a/idarling/server.py +++ b/idarling/server.py @@ -16,7 +16,7 @@ import sys import traceback -from PyQt5.QtCore import QCoreApplication, QTimer +from PySide6.QtCore import QCoreApplication, QTimer from .shared.server import Server from .shared.utils import start_logging @@ -25,7 +25,7 @@ class DedicatedServer(Server): """ This is the dedicated/standalone server. It can be invoked from the command line. It - requires only PyQt5 and should be invoked from Python 3. The dedicated + requires only PySide6 and should be invoked from Python 3. The dedicated server should be used when the integrated doesn't fulfil the user's needs. """ diff --git a/idarling/shared/discovery.py b/idarling/shared/discovery.py index 0c7ff57..151f553 100644 --- a/idarling/shared/discovery.py +++ b/idarling/shared/discovery.py @@ -13,8 +13,9 @@ import platform import socket import time +import errno -from PyQt5.QtCore import QObject, QSocketNotifier, QTimer +from PySide6.QtCore import QObject, QSocketNotifier, QTimer DISCOVERY_REQUEST = "IDARLING_DISCOVERY_REQUEST" @@ -92,10 +93,15 @@ def _send_request(self): ) request = request[sent:] except socket.error as e: - self._logger.warning("Couldn't send discovery request: {}".format(e)) + # Some systems may not be able to broadcast on all interfaces + # ("No route to host" / network unreachable). Don't spam + # warnings for those expected transient errors. + err_no = getattr(e, "errno", None) + if err_no in (errno.EHOSTUNREACH, errno.ENETUNREACH): + self._logger.debug("Couldn't send discovery request (network unreachable): {}".format(e)) + else: + self._logger.warning("Couldn't send discovery request: {}".format(e)) # Force return, otherwise the while loop will halt IDA - # This is a temporary fix, and it's gonna yield the above - # warning every every n seconds.. return def _notify_read(self): diff --git a/idarling/shared/forms.py b/idarling/shared/forms.py index 6685a34..856eda8 100644 --- a/idarling/shared/forms.py +++ b/idarling/shared/forms.py @@ -11,10 +11,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . import idaapi -from PyQt5.QtWidgets import * -from PyQt5.QtGui import * -from PyQt5.QtCore import * -from PyQt5 import QtGui, QtCore, QtWidgets +from PySide6.QtWidgets import * +from PySide6.QtGui import * +from PySide6.QtCore import * +from PySide6 import QtGui, QtCore, QtWidgets from difflib import * diff --git a/idarling/shared/server.py b/idarling/shared/server.py index 843eeef..34c3ebe 100644 --- a/idarling/shared/server.py +++ b/idarling/shared/server.py @@ -125,12 +125,12 @@ def process(self, msg, kwargs): self._logger = CustomAdapter(self._logger, {}) - def disconnect(self, err=None, notify=True): + def close_client(self, err=None, notify=True): # Notify other users that we disconnected self.parent().reject(self) if self._project and self._binary and self._snapshot and notify: self.parent().forward_users(self, LeaveSession(self.name, False)) - ClientSocket.disconnect(self, err) + ClientSocket.close_connection(self, err) def recv_packet(self, packet): if isinstance(packet, Command): @@ -162,7 +162,7 @@ def recv_packet(self, packet): if packet.tick and interval and packet.tick % interval == 0: def file_downloaded(reply): - file_name = "%s_%s_%s.idb" % (self._project, self._binary, self._snapshot) + file_name = "%s_%s_%s.i64" % (self._project, self._binary, self._snapshot) file_path = self.parent().server_file(file_name) # Write the file to disk @@ -206,8 +206,8 @@ def _handle_rename_binary(self, query): # for queries snapshots = self.parent().storage.select_snapshots(query.project, query.new_name) for snapshot in snapshots: - old_file_name = "%s_%s_%s.idb" % (query.project, query.old_name, snapshot.name) - new_file_name = "%s_%s_%s.idb" % (query.project, query.new_name, snapshot.name) + old_file_name = "%s_%s_%s.i64" % (query.project, query.old_name, snapshot.name) + new_file_name = "%s_%s_%s.i64" % (query.project, query.new_name, snapshot.name) old_file_path = self.parent().server_file(old_file_name) new_file_path = self.parent().server_file(new_file_name) # If a rename happens before a file is uploaded, the @@ -241,7 +241,7 @@ def _handle_list_snapshots(self, query): snapshots = self.parent().storage.select_snapshots(query.project, query.binary) for snapshot in snapshots: snapshot_info = snapshot.project, snapshot.binary, snapshot.name - file_name = "%s_%s_%s.idb" % (snapshot_info) + file_name = "%s_%s_%s.i64" % (snapshot_info) file_path = self.parent().server_file(file_name) if os.path.isfile(file_path): snapshot.tick = self.parent().storage.last_tick(*snapshot_info) @@ -265,7 +265,7 @@ def _handle_upload_file(self, query): snapshot = self.parent().storage.select_snapshot( query.project, query.binary, query.snapshot ) - file_name = "%s_%s_%s.idb" % (query.project, snapshot.binary, snapshot.name) + file_name = "%s_%s_%s.i64" % (query.project, snapshot.binary, snapshot.name) file_path = self.parent().server_file(file_name) # Write the file received to disk @@ -279,7 +279,7 @@ def _handle_download_file(self, query): snapshot = self.parent().storage.select_snapshot( query.project, query.binary, query.snapshot ) - file_name = "%s_%s_%s.idb" % (query.project, snapshot.binary, snapshot.name) + file_name = "%s_%s_%s.i64" % (query.project, snapshot.binary, snapshot.name) file_path = self.parent().server_file(file_name) # Read file from disk and sent it @@ -365,7 +365,7 @@ def _delete_binary_files(self, project, binary): self._delete_snapshot_files(project, binary, db.name) def _delete_snapshot_files(self, project, binary, snapshot): - file_name = "%s_%s_%s.idb" % (project, binary, snapshot) + file_name = "%s_%s_%s.i64" % (project, binary, snapshot) file_path = self.parent().server_file(file_name) try: os.remove(file_path) @@ -587,7 +587,7 @@ def start(self, host, port=0, ssl_=None): sock.settimeout(0) # No timeout sock.setblocking(0) # No blocking sock.listen(5) - self.connect(sock) + self.set_socket(sock) # Start discovering clients host, port = sock.getsockname() @@ -600,8 +600,8 @@ def stop(self): self._discovery.stop() # Disconnect all clients for client in list(self._clients): - client.disconnect(notify=False) - self.disconnect() + client.close_client(notify=False) + self.close_listener() try: self.db_update_lock.release() except RuntimeError: diff --git a/idarling/shared/sockets.py b/idarling/shared/sockets.py index d83c4c4..19572e7 100644 --- a/idarling/shared/sockets.py +++ b/idarling/shared/sockets.py @@ -18,7 +18,7 @@ import ssl import sys -from PyQt5.QtCore import QCoreApplication, QEvent, QObject, QSocketNotifier +from PySide6.QtCore import QCoreApplication, QEvent, QObject, QSocketNotifier from .packets import Container, Packet, PacketDeferred, Query, Reply from ..shared.commands import ( @@ -86,7 +86,7 @@ def wrap_socket(self, sock): self._socket = sock - def disconnect(self, err=None): + def close_connection(self, err=None): """Terminates the current connection.""" if not self._socket: return @@ -151,7 +151,7 @@ def _check_socket(self): ret = self._socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) if ret != 0: if ret != errno.EINPROGRESS and ret != errno.EWOULDBLOCK: - self.disconnect(socket.error(ret, os.strerror(ret))) + self.close_connection(socket.error(ret, os.strerror(ret))) return False else: self._connected = True @@ -167,7 +167,7 @@ def _notify_read(self): try: data = self._socket.recv(ClientSocket.MAX_DATA_SIZE) if not data: - self.disconnect() + self.close_connection() return except socket.error as e: if ( @@ -175,7 +175,7 @@ def _notify_read(self): and not isinstance(e, ssl.SSLWantReadError) and not isinstance(e, ssl.SSLWantWriteError) ): - self.disconnect(e) + self.close_connection(e) return # No more data available self._read_buffer.extend(data) @@ -191,10 +191,22 @@ def _notify_read(self): # Try to parse the line (= packet) try: - dct = json.loads(line.decode("utf-8")) - self._read_packet = Packet.parse_packet( - dct, self._server - ) + try: + decoded = line.decode("utf-8") + except UnicodeDecodeError as e: + # Received binary data (for example an SSL/TLS + # handshake) on a plaintext socket. Close the + # connection to avoid trying to parse arbitrary + # binary data as JSON. + self._logger.warning( + "Invalid (non-UTF8) packet received: %s", line + ) + self._logger.debug("Closing connection") + self.close_connection() + return + + dct = json.loads(decoded) + self._read_packet = Packet.parse_packet(dct, self._server) except Exception as e: msg = "Invalid packet received: %s" % line self._logger.warning(msg) @@ -267,7 +279,7 @@ def _notify_write(self): and not isinstance(e, ssl.SSLWantReadError) and not isinstance(e, ssl.SSLWantWriteError) ): - self.disconnect(e) + self.close_connection(e) return # Can't write anything # Trigger the upback @@ -356,7 +368,7 @@ def connected(self): """Is the underlying socket connected?""" return self._connected - def connect(self, sock): + def set_socket(self, sock): """Sets the underlying socket to utilize.""" self._accept_notifier = QSocketNotifier( sock.fileno(), QSocketNotifier.Read, self @@ -367,7 +379,7 @@ def connect(self, sock): self._socket = sock self._connected = True - def disconnect(self, err=None): + def close_listener(self, err=None): """Terminates the current connection.""" if not self._socket: return @@ -390,7 +402,7 @@ def _notify_accept(self): except socket.error as e: if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK): break - self.disconnect(e) + self.close_listener(e) break self._accept(sock) diff --git a/idarling_plugin.py b/idarling_plugin.py index f847e75..fa48794 100644 --- a/idarling_plugin.py +++ b/idarling_plugin.py @@ -10,9 +10,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from idarling.plugin import Plugin +from idarling.plugin import IdarlingPlugin def PLUGIN_ENTRY(): # noqa: N802 """Mandatory entry point for IDAPython plugins.""" - return Plugin() + return IdarlingPlugin() diff --git a/setup.py b/setup.py index fb4bb2d..43a6ea5 100755 --- a/setup.py +++ b/setup.py @@ -8,10 +8,10 @@ description="Collaborative Reverse Engineering plugin for IDA Pro", url="https://github.com/IDArlingTeam/IDArling", packages=find_packages(), - install_requires=["PyQt5; python_version >= '3.0'"], + install_requires=["PySide6; python_version >= '3.9'"], include_package_data=True, entry_points={ - "idapython_plugins": ["idarling=idarling.plugin:Plugin"], + "idapython_plugins": ["idarling=idarling.plugin:IdarlingPlugin"], "console_scripts": ["idarling_server=idarling.server:main"], }, )