# Copyright 2016 The Fuchsia Authors # # Use of this source code is governed by a MIT-style # license that can be found in the LICENSE file or at # https://opensource.org/licenses/MIT # GDB support for zircon kernel. # TODO(dje): gdb should let us use a better command class than COMMAND_DATA. # TODO(dje): Add arm64 support. from __future__ import absolute_import from __future__ import division from __future__ import print_function import sys import gdb import gdb.printing import re from gdb.unwinder import Unwinder if sys.version_info > (3,): long = int # The command prefix, passed in from gdbinit.py. _ZIRCON_COMMAND_PREFIX = "zircon" _KERNEL_EXCEPTION_UNWINDER_PARAMETER = "kernel-exception-unwinder" _THREAD_MAGIC = 0x74687264 _BOOT_MAGIC = 0x544f4f42 _KERNEL_BASE_ADDRESS = "KERNEL_BASE_ADDRESS" print("Loading zircon.elf-gdb.py ...") def _get_architecture(): # TODO(dje): gdb doesn't provide us with a simple way to do this. return gdb.execute("show architecture", to_string=True) def _is_x86_64(): """Return True if we're on an x86-64 platform.""" return re.search(r"x86-64", _get_architecture()) def _is_arm64(): """Return True if we're on an aarch64 platform.""" return re.search(r"aarch64", _get_architecture()) # The default is 2 seconds which is too low. # But don't override something the user set. # [If the user set it to the default, too bad. :-)] # TODO(dje): Alas this trips over upstream PR 20084. #_DEFAULT_GDB_REMOTETIMEOUT = 2 #if int(gdb.parameter("remotetimeout")) == _DEFAULT_GDB_REMOTETIMEOUT: # gdb.execute("set remotetimeout 10") class _ZirconPrefix(gdb.Command): """zircon command prefix""" def __init__(self): super(_ZirconPrefix, self).__init__("%s" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA, prefix=True) class _InfoZircon(gdb.Command): """info zircon command prefix""" def __init__(self): super(_InfoZircon, self).__init__("info %s" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA, prefix=True) class _SetZircon(gdb.Command): """set zircon command prefix""" def __init__(self): super(_SetZircon, self).__init__("set %s" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA, prefix=True) class _ShowZircon(gdb.Command): """show zircon command prefix""" def __init__(self): super(_ShowZircon, self).__init__("show %s" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA, prefix=True) class _ZirconMaxInfoThreads(gdb.Parameter): """Parameter to limit output of "info zircon threads" command. This parameter is an escape hatch to catch corrupted lists. We don't want "info zircon threads" to loop forever. The value is the maximum number of threads that will be printed. """ set_doc = "Set the maximum number of zircon threads to print." show_doc = "Show the maximum number of zircon threads to print." def __init__(self): super(_ZirconMaxInfoThreads, self).__init__( "%s max-info-threads" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA, gdb.PARAM_UINTEGER) self.value = 1000 def get_show_string(self, pvalue): return "Maximum number of threads to print is " + pvalue + "." def get_set_string(self): # Ugh. There doesn't seem to be a way to implement a gdb parameter in # Python that will be silent when the user changes the value. if self.value is None: value = "unlimited" else: value = self.value return "Maximum number of threads to print been set to %s." % (value) class _ZirconMaxInfoProcesses(gdb.Parameter): """Parameter to limit output of "info zircon processes" command. This parameter is an escape hatch to catch corrupted lists. We don't want "info zircon processes" to loop forever. The value is the maximum number of processes that will be printed. """ set_doc = "Set the maximum number of zircon processes to print." show_doc = "Show the maximum number of zircon processes to print." def __init__(self): super(_ZirconMaxInfoProcesses, self).__init__( "%s max-info-processes" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA, gdb.PARAM_UINTEGER) self.value = 1000 def get_show_string(self, pvalue): return "Maximum number of processes to print is " + pvalue + "." def get_set_string(self): # Ugh. There doesn't seem to be a way to implement a gdb parameter in # Python that will be silent when the user changes the value. if self.value is None: value = "unlimited" else: value = self.value return "Maximum number of processes to print been set to %s." % (value) class _ZirconMaxInfoHandles(gdb.Parameter): """Parameter to limit output of "info zircon handles" command. This parameter is an escape hatch to catch corrupted lists. We don't want "info zircon handles" to loop forever. The value is the maximum number of handles that will be printed. """ set_doc = "Set the maximum number of zircon handles to print." show_doc = "Show the maximum number of zircon handles to print." def __init__(self): super(_ZirconMaxInfoHandles, self).__init__( "%s max-info-handles" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA, gdb.PARAM_UINTEGER) self.value = 1000 def get_show_string(self, pvalue): return "Maximum number of handles to print is " + pvalue + "." def get_set_string(self): # Ugh. There doesn't seem to be a way to implement a gdb parameter in # Python that will be silent when the user changes the value. if self.value is None: value = "unlimited" else: value = self.value return "Maximum number of handles to print been set to %s." % (value) def containerof(node_ptr, type_name, member_name): """Python version of zircon's containerof macro.""" # TODO(dje): This could only be computed once. # For more popular types, compute all possible once. char_ptr = gdb.lookup_type("char").pointer() ptr = node_ptr.cast(char_ptr) type_object_ptr = gdb.lookup_type(type_name).pointer() offsetof = long(gdb.Value(0).cast(type_object_ptr)[member_name].address) return (ptr - offsetof).cast(type_object_ptr) def _build_zircon_pretty_printers(): pp = gdb.printing.RegexpCollectionPrettyPrinter("zircon") # Insert printer registration here. #pp.add_printer("foo", "^foo$", _ZirconFooPrinter) return pp def register_zircon_pretty_printers(obj): if obj is None: obj = gdb gdb.printing.register_pretty_printer(obj, _build_zircon_pretty_printers(), replace=True) def _get_thread_list(): """ Return a list of all thread_t threads. The result is constrained by "zircon max-info-threads". """ threads = [] head = gdb.parse_and_eval("&thread_list") t = head["next"] count = 0 max_threads = gdb.parameter("%s max-info-threads" % (_ZIRCON_COMMAND_PREFIX)) int_type = gdb.lookup_type("int") ptr_size = int_type.pointer().sizeof # Note: A "corrupted" list can happen for a short time while an # element is being added/removed. And, in non-stop mode, the list can # change while we're at it. This isn't a problem in all-stop mode, but # non-stop mode is generally preferable. We'll see how this works in # practice. while t and t != head: if max_threads is not None and count >= max_threads: break # Catch misaligned pointers. # Casting to int shouldn't be necessary, but the python API doesn't # support creating an int from a pointer. # We assume the object is aligned at least as great as a pointer. if (t.cast(int_type) & (ptr_size - 1)) != 0: break # TODO(dje): Do a range check? thread_ptr = containerof(t, "thread", "thread_list_node") if thread_ptr["magic"] != _THREAD_MAGIC: break # TODO(dje): Move this to a routine, more list printers will want this. threads.append(thread_ptr) t = t["next"] count += 1 return threads def _get_process_list(): """Return list of all processes. The result is constrained by "zircon max-info-processes". """ processes = [] head = gdb.parse_and_eval("&process_list") head = head["head_"] p = head count = 0 max_processes = gdb.parameter("%s max-info-processes" % (_ZIRCON_COMMAND_PREFIX)) int_type = gdb.lookup_type("int") ptr_size = int_type.pointer().sizeof # Note: A "corrupted" list can happen for a short time while an # element is being added/removed. And, in non-stop mode, the list can # change while we're at it. This isn't a problem in all-stop mode, but # non-stop mode is generally preferable. We'll see how this works in # practice. while p: if max_processes is not None and count >= max_processes: break # Catch misaligned pointers. # Casting to int shouldn't be necessary, but the python API doesn't # support creating an int from a pointer. # We assume the object is aligned at least as great as a pointer. if (p.cast(int_type) & (ptr_size - 1)) != 0: break # TODO(dje): Do a range check? # TODO(dje): Move this to a routine, more list printers will want this. processes.append(p) p = p["next_"] count += 1 if p == head: break return processes def _get_handle_list(process): """Return list of all handles of process. The result is constrained by "zircon max-info-handles". """ handles = [] head = process["handles_"] head = head["head_"] h = head count = 0 max_handles = gdb.parameter("%s max-info-handles" % (_ZIRCON_COMMAND_PREFIX)) int_type = gdb.lookup_type("int") ptr_size = int_type.pointer().sizeof # Note: A "corrupted" list can happen for a short time while an # element is being added/removed. And, in non-stop mode, the list can # change while we're at it. This isn't a problem in all-stop mode, but # non-stop mode is generally preferable. We'll see how this works in # practice. while h: if max_handles is not None and count >= max_handles: break # Catch misaligned pointers. # Casting to int shouldn't be necessary, but the python API doesn't # support creating an int from a pointer. # We assume the object is aligned at least as great as a pointer. if (h.cast(int_type) & (ptr_size - 1)) != 0: break # TODO(dje): Do a range check? # TODO(dje): Move this to a routine, more list printers will want this. handles.append(h) h = h["next_"] count += 1 if h == head: break return handles def _print_thread_summary(thread, number, tls_entry, user_thread_ptr_t): user_thread = thread["tls"][tls_entry] if user_thread: user_thread_ptr = user_thread.cast(user_thread_ptr_t) # TODO(dje): Why is str necessary here? Otherwise -> # "Cannot convert value to int." pid = str(user_thread_ptr["process_"]["id_"]) else: pid = "kern" name = str(thread["name"].lazy_string().value()).strip('"') print("%3d %5s %#16x %-32s %s" % ( number, pid, thread.address, name, thread["state"])) class _InfoZirconThreads(gdb.Command): """info zircon threads command This command prints a list of all zircon threads. TODO: Allow specifying which threads to print. """ def __init__(self): super(_InfoZirconThreads, self).__init__("info %s threads" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_USER) def invoke(self, arg, from_tty): # Do this first to make sure the previous value gets cleared out. # There's no way to unset a convenience var, so KISS. gdb.execute("set $zx_threads = (thread_t*[1]) { 0 }") tls_entry_lkuser = gdb.parse_and_eval("TLS_ENTRY_LKUSER") threads = _get_thread_list() num_threads = len(threads) # The array is origin-1-indexed. Have a null first entry to KISS. gdb.execute("set $zx_threads = (thread_t*[%d]) { 0 }" % (num_threads + 1)) # Populate the array first, before printing the summary, to make sure this # gets done even if there's an error during printing. num = 1 for thread_ptr in threads: gdb.execute("set $zx_threads[%d] = (thread_t*) %u" % (num, thread_ptr)) num += 1 # Translating gdb values to python often trips over these. Heads up. save_print_address = "yes" if gdb.parameter("print address") else "no" save_print_symbol = "yes" if gdb.parameter("print symbol") else "no" gdb.execute("set print address off") gdb.execute("set print symbol off") print("%3s %5s %-18s %-32s %s" % ( "Num", "Pid", "thread_t*", "Name", "State")) # Make sure we restore these when we're done. try: user_thread_ptr_t = gdb.lookup_type("UserThread").pointer() num = 1 for thread_ptr in threads: # TODO(dje): remove dereference _print_thread_summary(thread_ptr.dereference(), num, tls_entry_lkuser, user_thread_ptr_t) num += 1 finally: gdb.execute("set print address %s" % (save_print_address)) gdb.execute("set print symbol %s" % (save_print_symbol)) if num_threads: print("Note: Each thread is now available in $zx_threads[num].") else: print("") def _print_process_summary(process, number): state = str(process["state_"]) state = state.replace("ProcessDispatcher::", "") print("%3d %#16x %4u %s" % ( number, process.address, process["id_"], state)) class _InfoZirconProcesses(gdb.Command): """info zircon processes command This command prints a list of all zircon processes. TODO: Allow specifying which processes to print. """ def __init__(self): super(_InfoZirconProcesses, self).__init__("info %s processes" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_USER) def invoke(self, arg, from_tty): # Do this first to make sure the previous value gets cleared out. # There's no way to unset a convenience var, so KISS. gdb.execute("set $zx_processes = (ProcessDispatcher*[1]) { 0 }") tls_entry_lkuser = gdb.parse_and_eval("TLS_ENTRY_LKUSER") processes = _get_process_list() num_processes = len(processes) # The array is origin-1-indexed. Have a null first entry to KISS. gdb.execute("set $zx_processes = (ProcessDispatcher*[%d]) { 0 }" % (num_processes + 1)) # Populate the array first, before printing the summary, to make sure this # gets done even if there's an error during printing. num = 1 for process_ptr in processes: gdb.execute("set $zx_processes[%d] = (ProcessDispatcher*) %u" % (num, process_ptr)) num += 1 # Translating gdb values to python often trips over these. Heads up. save_print_address = "yes" if gdb.parameter("print address") else "no" save_print_symbol = "yes" if gdb.parameter("print symbol") else "no" gdb.execute("set print address off") gdb.execute("set print symbol off") print("%3s %-18s %4s %s" % ( "Num", "ProcessDispatcher*", "Pid", "State")) # Make sure we restore these when we're done. try: num = 1 for process_ptr in processes: _print_process_summary(process_ptr.dereference(), num) num += 1 finally: gdb.execute("set print address %s" % (save_print_address)) gdb.execute("set print symbol %s" % (save_print_symbol)) if num_processes: print("Note: Each process is now available in $zx_processes[num].") else: print("") def _print_handle_summary(handle, number): process_id = handle["process_id_"] rights = handle["rights_"] dispatcher = handle["dispatcher_"]["ptr_"] # TODO(dje): This is a hack to get the underlying type from the vtable. # The python API should support this directly. dispatcher_text = gdb.execute("output *(Dispatcher*) %s" % (dispatcher), to_string=True) dispatcher_split_text = dispatcher_text.split(" ", 1) if len(dispatcher_split_text) == 2: dispatcher_type = dispatcher_split_text[0].strip("()") else: dispatcher_type = "Dispatcher" dispatcher_text = "(%s*) %s" % (dispatcher_type, dispatcher) print(" %3d %-18s %4u %#8x %s" % ( number, handle.address, process_id, rights, dispatcher_text)) class _InfoZirconHandles(gdb.Command): """info zircon handles command This command prints a list of all zircon handles. TODO: Allow specifying which handles to print. """ def __init__(self): super(_InfoZirconHandles, self).__init__("info %s handles" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_USER) def invoke(self, arg, from_tty): processes = _get_process_list() # Translating gdb values to python often trips over these. Heads up. save_print_address = "yes" if gdb.parameter("print address") else "no" save_print_symbol = "yes" if gdb.parameter("print symbol") else "no" gdb.execute("set print address on") gdb.execute("set print symbol off") # Make sure we restore these when we're done. try: for p in processes: handles = _get_handle_list(p) num_handles = len(handles) print("Process %u" % (p["id_"])) print(" %3s %-18s %4s %8s %s" % ( "Num", "Handle*", "Pid", "Rights", "Dispatcher")) num = 1 for handle_ptr in handles: _print_handle_summary(handle_ptr.dereference(), num) num += 1 if not num_handles: print(" ") finally: gdb.execute("set print address %s" % (save_print_address)) gdb.execute("set print symbol %s" % (save_print_symbol)) class _ZirconKernelExceptionUnwinder(gdb.Parameter): """Parameter to enable zircon kernel exception unwinding. This parameter is an escape hatch in case there are problems with the unwinder. N.B. Perhaps this command should flush registers. It doesn't now to avoid side-effects, and the user is responsible for typing "flushregs" if s/he wants to reprint a recent backtrace. """ set_doc = "Set whether the zircon kernel exception unwinder is enabled." show_doc = "Show whether the zircon kernel exception unwinder is enabled." def __init__(self): super(_ZirconKernelExceptionUnwinder, self).__init__( "%s %s" % (_ZIRCON_COMMAND_PREFIX, _KERNEL_EXCEPTION_UNWINDER_PARAMETER), gdb.COMMAND_DATA, gdb.PARAM_BOOLEAN) self.value = True def get_show_string(self, pvalue): value = "enabled" if self.value else "disabled" return "The kernel exception unwinder is %s." % (value) def get_set_string(self): # Ugh. There doesn't seem to be a way to implement a gdb parameter in # Python that will be silent when the user changes the value. value = "enabled" if self.value else "disabled" return "The kernel exception unwinder is %s." % (value) class _Amd64KernelExceptionUnwinder(Unwinder): # See arch/x86/64/exceptions.S. AT_IFRAME_SETUP = "interrupt_common_iframe_set_up_for_debugger" INTERRUPT_COMMON = "interrupt_common" class FrameId(object): def __init__(self, sp, pc): self.sp = sp self.pc = pc @staticmethod def lookup_minsym(minsym_name): """Return the address of "minimal symbol" minsym_name.""" # TODO(dje): What's here now is a quick hack to get things going. # GDB's python API doesn't yet provide the ability to look up minsyms. try: output = gdb.execute("output %s" % (minsym_name), to_string=True) except (gdb.error): return None symbol_value = None if not output.startswith("No symbol"): symbol_match = re.search(r"0x[0-9a-f]+", output) if symbol_match is not None: symbol_value = long(symbol_match.group(0), 16) return symbol_value @staticmethod def is_user_space(iframe): """Return True if iframe is from user space.""" # See arch/x86/include/arch/x86/descriptor.h:SELECTOR_PL. return (iframe["cs"] & 3) != 0 def __init__(self): super(_Amd64KernelExceptionUnwinder, self).__init__("AMD64 kernel exception unwinder") # We assume uintptr_t is present in the debug info. # We *could* use unsigned long, but it's a bit of an obfuscation. self.uintptr_t = gdb.lookup_type("uintptr_t") # We assume "unsigned int" is 32 bits. self.uint32_t = gdb.lookup_type("unsigned int") self.iframe_ptr_t = gdb.lookup_type("x86_iframe_t").pointer() # We need to know when the pc is at the point where it has called # x86_exception_handler. self.at_iframe_setup = self.lookup_minsym(_Amd64KernelExceptionUnwinder.AT_IFRAME_SETUP) self.interrupt_common_begin_addr = self.lookup_minsym(_Amd64KernelExceptionUnwinder.INTERRUPT_COMMON) def is_in_icommon(self, pc): """Return True if pc is in the interrupt_common function.""" # First do the preferred test. # If the pc is here then we've called into x86_exception_handler. if self.at_iframe_setup is not None and pc == self.at_iframe_setup: return True # Fall back to this in case the special symbol doesn't exist. if self.interrupt_common_begin_addr is None: return False end_addr = self.interrupt_common_begin_addr + 64 return pc >= self.interrupt_common_begin_addr and pc < end_addr def __call__(self, pending_frame): try: # Punt if disabled. if not gdb.parameter("%s %s" % (_ZIRCON_COMMAND_PREFIX, _KERNEL_EXCEPTION_UNWINDER_PARAMETER)): return None # Note: We use rip,rsp here instead of pc,sp to work around bug 20128 #pc = pending_frame.read_register("pc").cast(self.uintptr_t) pc = pending_frame.read_register("rip").cast(self.uintptr_t) #print "icommon unwinder, pc = %#x" % (long(str(pc))) # work around bug 20126 if not self.is_in_icommon(pc): return None sp = pending_frame.read_register("rsp").cast(self.uintptr_t) #print "icommon unwinder, sp = %#x" % (long(str(sp))) iframe = sp.cast(self.iframe_ptr_t) # This is only for kernel faults, not user-space ones. if self.is_user_space(iframe): return None frame_id = self.FrameId(sp, pc) unwind_info = pending_frame.create_unwind_info(frame_id) unwind_info.add_saved_register("rip", iframe["ip"]) unwind_info.add_saved_register("rsp", iframe["user_sp"]) unwind_info.add_saved_register("rax", iframe["rax"]) unwind_info.add_saved_register("rbx", iframe["rbx"]) unwind_info.add_saved_register("rcx", iframe["rcx"]) unwind_info.add_saved_register("rdx", iframe["rdx"]) unwind_info.add_saved_register("rbp", iframe["rbp"]) unwind_info.add_saved_register("rsi", iframe["rsi"]) unwind_info.add_saved_register("r8", iframe["r8"]) unwind_info.add_saved_register("r9", iframe["r9"]) unwind_info.add_saved_register("r10", iframe["r10"]) unwind_info.add_saved_register("r11", iframe["r11"]) unwind_info.add_saved_register("r12", iframe["r12"]) unwind_info.add_saved_register("r13", iframe["r13"]) unwind_info.add_saved_register("r14", iframe["r14"]) unwind_info.add_saved_register("r15", iframe["r15"]) # Flags is recorded as 64 bits, but gdb needs to see 32. unwind_info.add_saved_register("eflags", iframe["flags"].cast(self.uint32_t)) #print "Unwind info:" #print unwind_info return unwind_info except (gdb.error, RuntimeError): return None _ZirconPrefix() _InfoZircon() _SetZircon() _ShowZircon() _ZirconMaxInfoThreads() _ZirconMaxInfoProcesses() _ZirconMaxInfoHandles() _InfoZirconThreads() _InfoZirconProcesses() _InfoZirconHandles() _ZirconKernelExceptionUnwinder() _ull = gdb.lookup_type("unsigned long long") _uint = gdb.lookup_type("unsigned int") def _cast(value, t, shift=64): return int(value.cast(t)) & ((1 << shift)-1) def _cast_ull(value): """ Cast a value to unsigned long long """ global _ull return _cast(value, _ull) def _cast_uint(value): """ Cast a value to unsigned int""" global _uint return _cast(value, _uint, 32) def _read_symbol_address(name): """ Read the address of a symbol """ addr = gdb.parse_and_eval("&"+name) try: if addr is not None: return _cast_ull(addr) except gdb.MemoryError: pass print("Can't find %s to lookup KASLR relocation" % name) return None def _read_pointer(addr): """ Read a pointer in an address """ value = gdb.parse_and_eval("*(unsigned long*)0x%x" % addr) try: if value is not None: return _cast_ull(value) except gdb.MemoryError: pass print("Can't read 0x%x to lookup KASLR ptr value" % addr) return None def _read_uint(addr): """ Read a uint """ value = gdb.parse_and_eval("*(unsigned int*)0x%x" % addr) try: if value is not None: return _cast_uint(value) except gdb.MemoryError: pass print("Can't read 0x%x to lookup KASLR uint value" % addr) return None def _offset_symbols_and_breakpoints(kernel_relocated_base=None): """ Using the KASLR relocation address, reload symbols and breakpoints """ print("Update symbols and breakpoints for KASLR") base_address = _read_symbol_address(_KERNEL_BASE_ADDRESS) if not base_address: return False load_start = _read_symbol_address("IMAGE_LOAD_START") if not load_start: return False if not kernel_relocated_base: kernel_relocated_base = _read_symbol_address("kernel_relocated_base") if not kernel_relocated_base: return False relocated = _read_pointer(kernel_relocated_base) if not relocated: print("Failed to fetch KASLR base address") return False # There is no api for symbol management. # Everything has to be done by custom commands sym = gdb.execute("info target", to_string=True) m = re.match("^Symbols from \"([^\"]+)\"", sym) if not m: print("Error: Cannot find the target symbol") return False sym_path = m.group(1) # Identify all section addresses x = gdb.execute("info target", to_string=True) m = re.findall("\s0x([a-f0-9]+) - 0x[a-f0-9]+ is (.*)", x) offset = base_address - relocated sections = dict([(name, int(addr, 16)) for addr,name in m]) if len(sections) == 0: print("Error: Failed to find sections in binary") return False if ".text" not in sections: print("Error: Failed to find .text section") return False # Do not prompt the user confirm_was_enabled = gdb.parameter("confirm") if confirm_was_enabled: gdb.execute("set confirm off", to_string=True) # Disable auto loading to prevent the script to be reloaded auto_loading_enabled = gdb.parameter("auto-load python-scripts") if auto_loading_enabled: gdb.execute("set auto-load python-scripts off", to_string=True) # Remove all symbols gdb.execute("symbol-file", to_string=True) # Update all addresses to the relocated address sections = dict([(name, addr - offset) for name,addr in sections.iteritems()]) text_addr = sections[".text"] del sections[".text"] args = ["-s %s 0x%x" % (name, addr) for name,addr in sections.iteritems()] # Reload the ELF with all sections set gdb.execute("add-symbol-file \"%s\" 0x%x -readnow %s" \ % (sym_path, text_addr, " ".join(args)), to_string=True) if auto_loading_enabled: gdb.execute("set auto-load python-scripts on", to_string=True) if confirm_was_enabled: gdb.execute("set confirm on", to_string=True) # Verify it works as expected code_start = _read_symbol_address("__code_start") if not kernel_relocated_base: return False expected = relocated + (load_start - base_address) if code_start != expected: print("Error: Incorrect relocation for __code_start 0x%x vs 0x%x" \ % (expected, code_start)) return False print("KASLR: Correctly reloaded kernel at 0x%x" % relocated) return True class KASLRBreakpoint(gdb.Breakpoint): """ Helper class to setup breakpoints to load symbols for KASLR. """ def __init__(self): self._is_valid = False def is_valid(self): if gdb.Breakpoint.is_valid(self) == False: return False return self._is_valid def stop(self): # A breakpoint cannot change anything so get callback on the following stop gdb.events.stop.connect(self._stop_callback_internal) return True # Callback through events so the state can be changed def _stop_callback_internal(self, event): gdb.events.stop.disconnect(self._stop_callback_internal) self.delete() # Call the top callback self._stop_callback(event) class KASLRBootWatchpoint(KASLRBreakpoint): """ Watchpoint to catch read access to KASLR relocated address The assumption is the address was written before it is read. It is an architecture independent way to start at the right moment if the relocated KASLR symbol name is the same. """ def __init__(self, pc): KASLRBreakpoint.__init__(self) base_address = _read_symbol_address(_KERNEL_BASE_ADDRESS) if not base_address: return kernel_relocated_base = _read_symbol_address("kernel_relocated_base") if not kernel_relocated_base: return self._relocated_base_offset = kernel_relocated_base if _is_x86_64(): # x86_64 uses the physical address self._relocated_base_offset -= base_address elif _is_arm64(): # Search from pc to find BOOT tag found = False for addr in range(pc, pc+0x10000000, 0x10000): data = _read_uint(addr) if data == _BOOT_MAGIC: self._relocated_base_offset -= base_address self._relocated_base_offset += addr found = True break if found == False: print("Error: Could not found BOOT_MAGIC") return self._is_valid = True gdb.Breakpoint.__init__(self, "*0x%x" % self._relocated_base_offset, gdb.BP_WATCHPOINT, gdb.WP_READ, internal=True) self.silence = True # Callback from KASLRBreakpoint when it is safe to change state def _stop_callback(self, event): # Load symbols using load_offset _offset_symbols_and_breakpoints(self._relocated_base_offset) class KASLRStartBreakpoint(KASLRBreakpoint): """ Breakpoint to catch _start The ZBI boot path result with memory being moved before _start on x86_64. In this case, break on _start before setting up the watchpoint. """ def __init__(self): KASLRBreakpoint.__init__(self) base_address = _read_symbol_address(_KERNEL_BASE_ADDRESS) if not base_address: return _start = _read_symbol_address("_start") if not _start: return # Get the physical address _start -= base_address self._is_valid = True gdb.Breakpoint.__init__(self, "*0x%x" % _start, internal=True) self.silence = True # Callback from KASLRBreakpoint when it is safe to change state def _stop_callback(self, event): pc = _cast_ull(gdb.parse_and_eval("$pc")) x = KASLRBootWatchpoint(pc) if not x.is_valid(): print("Error: Failed create KASLR boot watchpoint after _stat") return print("Watchpoint set on KASLR relocated base variable") gdb.execute("continue") def _align(addr, shift): b64 = (1 << 64) - 1 mask = (1 << shift) - 1 return addr & (~mask & b64) def _identify_offset_to_reload(pc): """ From $pc, search the base address to the page size Used when attaching to an existing debugging session """ print("Search KASLR base address based on $pc") if (pc >> 63) != 1: print("Error: Didn't break into kernel code") return False base_address = _read_symbol_address(_KERNEL_BASE_ADDRESS) if not base_address: return False end = _read_symbol_address("_end") if not end: return False kernel_relocated_base = _read_symbol_address("kernel_relocated_base") if not kernel_relocated_base: return False offset = kernel_relocated_base - base_address max_size = end - base_address # Search base+offset == base for each previous page until the max size found = False addr = pc while addr > (pc - max_size): addr = _align(addr - 1, 12) target = addr + offset value = _read_pointer(target) if value == None: break if addr == value: found = True break if found == False: print("Error: Failed to find the KASLR relocation from the $pc") return False return _offset_symbols_and_breakpoints(target) def _is_earlyboot_pc(pc): """ Early boot if the top 32-bit is zero """ return not (pc >> 32) def _KASLR_stop_event(event): """ Called on first stop after debugger started """ gdb.events.stop.disconnect(_KASLR_stop_event) pc = _cast_ull(gdb.parse_and_eval("$pc")) if not _is_earlyboot_pc(pc): # If not early boot, try to find the kernel base and adapt _identify_offset_to_reload(pc) return x = None # ARM64 map _start at random place in memory, directly use the watchpoint if _is_arm64(): x = KASLRBootWatchpoint(pc) else: x = KASLRStartBreakpoint() if not x.is_valid(): print("Error: Failed create KASLR boot first breakpoint/watchpoint") return gdb.execute("continue") def _install(): current_objfile = gdb.current_objfile() register_zircon_pretty_printers(current_objfile) if current_objfile is not None and _is_x86_64(): gdb.unwinder.register_unwinder(current_objfile, _Amd64KernelExceptionUnwinder(), True) if not _is_x86_64() and not _is_arm64(): print("Warning: Unsupported architecture, KASLR support will be experimental") gdb.events.stop.connect(_KASLR_stop_event) _install()