"""

A script that graphs all the exception handlers in a given process

It will be easy to see what thread uses what handler and what handlers are commonly used between threads

Copyright (c) 1990-2025 Hex-Rays
ALL RIGHTS RESERVED.
"""
from __future__ import print_function

import ida_kernwin
import ida_graph
import ida_idd
import ida_dbg
import ida_funcs

import idautils

# -----------------------------------------------------------------------
# Since Windbg debug module does not support get_thread_sreg_base()
# we will call the debugger engine "dg" command and parse its output
def WindbgGetRegBase(tid):
    ok, s = ida_dbg.send_dbg_command("dg %x" % idautils.cpu.fs)
    if not ok:
        return 0
    m = re.compile("[0-9a-f]{4} ([0-9a-f]{8})")
    t = m.match(s.split('\n')[-2])
    if not t:
        return 0
    return int(t.group(1), 16)

# -----------------------------------------------------------------------
def GetFsBase(tid):
    ida_dbg.select_thread(tid)
    base = ida_idd.dbg_get_thread_sreg_base(tid, idautils.cpu.fs)
    if base != 0:
      return base
    return WindbgGetRegBase(tid)

# -----------------------------------------------------------------------
# Walks the SEH chain and returns a list of handlers
def GetExceptionChain(tid):
    fs_base = GetFsBase(tid)
    print("FS_BASE for %s: %s (cpu.fs=%s)" % (repr(tid), repr(fs_base), repr(idautils.cpu.fs)))
    exc_rr = ida_bytes.get_wide_dword(fs_base)
    result = []
    while exc_rr != 0xffffffff:
        prev    = get_wide_dword(exc_rr)
        handler = get_wide_dword(exc_rr + 4)
        exc_rr  = prev
        result.append(handler)
    return result

# -----------------------------------------------------------------------
class SEHGraph(ida_graph.GraphViewer):
    def __init__(self, title, result):
        ida_graph.GraphViewer.__init__(self, title)
        self.result = result
        self.names  = {} # ea -> name

    def OnRefresh(self):
        self.Clear()
        addr_id = {}

        for (tid, chain) in self.result.items():
            # Each node data will contain a tuple of the form: (Boolean->Is_thread, Int->Value, String->Label)
            # For threads the is_thread will be true and the value will hold the thread id
            # For exception handlers, is_thread=False and Value=Handler address

            # Add the thread node
            id_parent = self.AddNode( (True, tid, "Thread %X" % tid) )

            # Add each handler
            for handler in chain:
              # Check if a function is created at the handler's address
              f = ida_funcs.get_func(handler)
              if not f:
                  # create function
                  ida_funcs.add_func(handler)

              # Node label is function name or address
              s = ida_funcs.get_func_name(handler)
              if not s:
                  s = "%x" % handler

              # cache name
              self.names[handler] = s

              # Get the node id given the handler address
              # We use an addr -> id dictionary so that similar addresses get similar node id
              if handler not in addr_id:
                  id = self.AddNode( (False, handler, s) )
                  addr_id[handler] = id # add this ID
              else:
                  id = addr_id[handler]

              # Link handlers to each other
              self.AddEdge(id_parent, id)
              id_parent = id

        return True

    def OnGetText(self, node_id):
        is_thread, value, label = self[node_id]
        if is_thread:
            return (label, 0xff00f0)
        return label

    def OnDblClick(self, node_id):
        is_thread, value, label = self[node_id]
        if is_thread:
            ida_dbg.select_thread(value)
            self.Show()
            s = "SEH chain for " + hex(value)
            t = "-" * len(s)
            print(t)
            print(s)
            print(t)
            for handler in self.result[value]:
                print("%x: %s" % (handler, self.names[handler]))
            print(t)
        else:
            ida_kernwin.jumpto(value)
        return True


# -----------------------------------------------------------------------
def main():
    if not ida_idd.dbg_can_query():
        print("The debugger must be active and suspended before using this script!")
        return

    # Save current thread id
    tid = ida_dbg.get_current_thread()

    # Iterate through all function instructions and take only call instructions
    result = {}
    for tid in idautils.Threads():
        result[tid] = GetExceptionChain(tid)

    # Restore previously selected thread
    ida_dbg.select_thread(tid)

    # Build the graph
    g = SEHGraph("SEH graph", result)
    g.Show()

main()
