Gdb python: Difference between revisions

From miki
Jump to navigation Jump to search
Line 40: Line 40:
</source>
</source>


=== Define Custom python breakpoint ===
=== Quit program gracefully with custom python breakpoint ===
Here for instance a command <code>Quit</code> (note that gdb would accept abbreviation <code>Q</code>), that set a temporary breakpoint:
Here for instance a command <code>Quit</code> (note that gdb would accept abbreviation <code>Q</code>), that set a temporary breakpoint that definitely quit when reaching a custom <code>exit_function</code>, and jump to some <code>cleanup_function</code>:
<source lang="python">
<source lang="python">
JUMP_TO_FUNCTION="some_entry_function"
JUMP_TO_FUNCTION="cleanup_function"
QUIT_ON_FUNCTION="some_exit_function"
QUIT_ON_FUNCTION="exit_function"


class QuitBreakpoint(gdb.Breakpoint):
class QuitBreakpoint(gdb.Breakpoint):
Line 59: Line 59:
def invoke(self, arg, from_tty):
def invoke(self, arg, from_tty):
print("Setting bkp")
print("Setting bkp")
QuitBreakpoint(MY_FUNCTION, gdb.BP_BREAKPOINT, temporary=True)
QuitBreakpoint(QUIT_ON_FUNCTION, gdb.BP_BREAKPOINT, temporary=True)
gdb.execute("set $eip=%s" % JUMP_TO_FUNCTION)
gdb.execute("set $eip=%s" % JUMP_TO_FUNCTION)
gdb.execute("continue")
gdb.execute("continue")

Revision as of 23:01, 23 January 2021

This page collects information on Python integration in gdb.

Links

Example for Dumping memory, colorizing listing, pretty printer, tracing and profiling.

Tutorial

From Greg Law video.

  • Single line python: for instance python print("Hello World")
  • Multi-line python: simply enter python, then finish with end
python
    import os
    print ("My pid is %d" % os.getpid())
end
  • Integration with gdb:
  • python print(gdb.breakpoints()[0].location)
  • python gdb.Breakpoint('7'), to insert breakpoint at line 7 in current file.

How-to

Define custom gdb command

class MyCommand( gdb.Command ):
    """MyCommand"""

    def __init__(self):
        super(MyCommand, self).__init__( "mycommand", gdb.COMMAND_USER )

    def invoke(self, arg, from_tty):
        print("My great command is alive!")
        print "R0=0x%08X" %( gdb.parse_and_eval("$R0") )

MyCommand()

Quit program gracefully with custom python breakpoint

Here for instance a command Quit (note that gdb would accept abbreviation Q), that set a temporary breakpoint that definitely quit when reaching a custom exit_function, and jump to some cleanup_function:

JUMP_TO_FUNCTION="cleanup_function"
QUIT_ON_FUNCTION="exit_function"

class QuitBreakpoint(gdb.Breakpoint):
    def stop(self):
        print("Quitting...")
        gdb.execute("quit")

class Quit( gdb.Command ):
    """Quit"""

    def __init__(self):
        super(Quit, self).__init__("Quit", gdb.COMMAND_USER )

    def invoke(self, arg, from_tty):
        print("Setting bkp")
        QuitBreakpoint(QUIT_ON_FUNCTION, gdb.BP_BREAKPOINT, temporary=True)
        gdb.execute("set $eip=%s" % JUMP_TO_FUNCTION)
        gdb.execute("continue")

Quit()

Using Capstone to trace program

import re
import gdb

from capstone import *
from capstone.arm import *
import binascii

# Documentation : https://sourceware.org/gdb/onlinedocs/gdb/Python-API.html
# Server-side : gdbserver --multi CLIENT_ADDR:REMOTE_PORT
# Client-side : gdb-multiarch ./my_exec
# Load the script in GDB CLI : source /path/to/gdb_script_st.py

REMOTE_ADDR = "192.168.20.150"
REMOTE_PORT = "6666"
REMOTE_FILE_PATH = "/path/on/remote/to/my_exec"

PAGINATION = "off"

LOG = "off"
LOG_REDIRECT = "on"
LOG_OVERWRITE = "on"
LOG_FILE = "trace.log"

TRACE_START_ADDR = 0x112fc
TRACE_END_ADDR = 0

exited = False

def read_register(register_name):
    return int(gdb.selected_frame().read_register(register_name)) & 0xffffffff


def process_register(register_name, access, occurences, readen_registers, written_registers, operand_string):
    if register_name not in occurences:
        occurences[register_name] = (match.span() for match in re.finditer(register_name, operand_string))
    start, end = next(occurences[register_name])
    if access & CS_AC_WRITE:
        written_registers.append(register_name)
    else:
        register_value = read_register(register_name)
        readen_registers.append((register_name, register_value, start + occurences["offset"], end + occurences["offset"]))
        occurences["offset"] += len(f"0x{register_value:x}") - len(register_name)


def concrete_operand_string(operand_string, readen_registers):
    for register_name, register_value, start, end in readen_registers:
        operand_string = operand_string[:start] + f"0x{register_value:x}" + operand_string[end:]
    return operand_string


def exit_handler(event):

    global exited

    exited = True
    if hasattr(event, "exit_code"):
        print(f"[!] Program has exited returning {event.exit_code}")
    else:
        print(f"[!] Program has exited (couldn't read exit code)")


class TraceRun (gdb.Command):

    def __init__(self):
        super(TraceRun, self).__init__("trace-run", gdb.COMMAND_RUNNING)


    def invoke(self, arg, from_tty):

        global exited

        exited = False
        inferior = gdb.selected_inferior()
        pc = TRACE_START_ADDR
        breakpoint = gdb.Breakpoint(f"*0x{pc:x}", gdb.BP_BREAKPOINT, temporary=True)
        cs_engine = Cs(CS_ARCH_ARM, CS_MODE_ARM)
        cs_engine.detail = True

        gdb.execute(f"r {arg}")

        if not exited:

            gdb.execute(f"set logging {LOG}")

            while(pc != TRACE_END_ADDR):

                readen_registers = []
                written_registers = []
                occurences = {"offset": 0}

                memory_view = inferior.read_memory(pc, 0x4)
                instruction = next(cs_engine.disasm(memory_view.tobytes(), pc))

                for operand in instruction.operands:
                    if operand.type == ARM_OP_REG:
                        process_register(instruction.reg_name(operand.value.reg), operand.access, occurences, readen_registers, written_registers, instruction.op_str)
                    if operand.type == ARM_OP_MEM:
                        if operand.value.mem.base:
                            process_register(instruction.reg_name(operand.value.mem.base), operand.access, occurences, readen_registers, written_registers, instruction.op_str)
                        if operand.value.mem.index:
                            process_register(instruction.reg_name(operand.value.mem.index), operand.access, occurences, readen_registers, written_registers, instruction.op_str)

                gdb.execute("ni")
                print(f"0x{instruction.address:x}: {instruction.mnemonic} {instruction.op_str}")
                concrete_op_str = concrete_operand_string(instruction.op_str, readen_registers)
                if instruction.op_str != concrete_op_str:
                    print(f"0x{instruction.address:x}: {instruction.mnemonic} {concrete_op_str}")
                for register_name in written_registers:
                    register_value = read_register(register_name)
                    print(f"0x{instruction.address:x}: {register_name} = 0x{register_value:x}")
                pc = read_register("pc")

            gdb.execute("set logging off")
            gdb.execute("c")


def main():
    gdb.execute(f"target extended-remote {REMOTE_ADDR}:{REMOTE_PORT}")
    gdb.execute(f"set remote exec-file {REMOTE_FILE_PATH}")
    gdb.execute(f"set pagination {PAGINATION}")
    gdb.execute(f"set logging file {LOG_FILE}")
    gdb.execute(f"set logging redirect {LOG_REDIRECT}")
    gdb.execute(f"set logging overwrite {LOG_OVERWRITE}")
    gdb.execute(f"set logging off")
    gdb.execute(f"set confirm off")
    gdb.events.exited.connect(exit_handler)


if __name__ == "__main__":
    # Commands registration
    TraceRun()
    # Main
    main()

Disable ptrace detection in a program

Some programs calls ptrace to detect whether they are debugged or traced (with strace).

We can set breakpoints to disable this detection:

  • On first call, ptrace must return 0 to indicate success.
  • On second call, ptrace must return -1 to indicate a process was attached already (the program itself did it in first call above).

We use Python variables to store this state, and reset on program exit:

import gdb

PTRACE_ADDR = 0x000153d0

unptrace_breakpoint = None
unptrace_breakpoint_value = 0


def exit_handler(event):

    global unptrace_breakpoint_value

    exited = True
    if hasattr(event, "exit_code"):
        print(f"[!] Program has exited returning {event.exit_code}")
    else:
        print(f"[!] Program has exited (couldn't read exit code)")
    unptrace_breakpoint_value=0


class PtraceBreakpoint (gdb.Breakpoint):
    def stop(self):
        global unptrace_breakpoint_value

        print(f"set $r0={unptrace_breakpoint_value}")
        gdb.execute(f"set $r0={unptrace_breakpoint_value}")
        gdb.execute("set $pc=$lr")
        print(f"unptrace: returned {unptrace_breakpoint_value}!")
        unptrace_breakpoint_value=0xffffffff
        return False

class UnPtrace (gdb.Command):

    def __init__(self):
        global unptrace_breakpoint
        global unptrace_breakpoint_value

        super(UnPtrace, self).__init__("unptrace", gdb.COMMAND_USER)
        unptrace_breakpoint = None
        unptrace_breakpoint_value = 0

    def invoke(self, arg, from_tty):
        global unptrace_breakpoint

        if unptrace_breakpoint == None:
            unptrace_breakpoint = PtraceBreakpoint(f"*0x{PTRACE_ADDR:x}", gdb.BP_BREAKPOINT)
            unptrace_breakpoint_value = 0

def main():
    gdb.events.exited.connect(exit_handler)


if __name__ == "__main__":
    # Commands registration
    UnPtrace()
    # Main
    main()

Trace and inspect some functions calls

Here we set up some breakpoint hook to inspect calls to read(2) and write(2):

import gdb

import binascii

READ_ADDR = 0x000152f0
WRITE_ADDR = 0x00015300

exited = False

def exit_handler(event):

    global exited

    exited = True
    if hasattr(event, "exit_code"):
        print(f"[!] Program has exited returning {event.exit_code}")
    else:
        print(f"[!] Program has exited (couldn't read exit code)")

read_r0 = None
read_r1 = None
read_r2 = None

class ReadBackBreakpoint (gdb.Breakpoint):
    def stop(self):
        global read_r0, read_r1, read_r2

        if read_r0 != None:
            lr = read_register("pc")        # PC is LR when we created the bkp
            r0 = read_r0
            r1 = read_r1
            r2 = read_r2
            r_bytes = gdb.selected_inferior().read_memory(r1,r2).tobytes()
            print(f"0x{lr-4:08x}: read_({r0},0x{r1:08x},{r2:3}) <-- {binascii.hexlify(r_bytes).decode()}")

        (read_r0,read_r1,read_r2) = (None,None,None)
        return False

class ReadBreakpoint (gdb.Breakpoint):
    def stop(self):
        global read_r0, read_r1, read_r2

        r0 = read_register("r0")
        r1 = read_register("r1")
        r2 = read_register("r2")
        lr = read_register("lr")
        # print(f"0x{lr-4:08x}: read_({r0},0x{r1:08x},{r2:3})")

        # Set up temporary bkp to collect read bytes
        (read_r0,read_r1,read_r2) = (r0,r1,r2)
        breakpoint = ReadBackBreakpoint(f"*0x{lr:x}", gdb.BP_BREAKPOINT, temporary=True)

        return False

class WriteBreakpoint (gdb.Breakpoint):
    def stop(self):
        lr = read_register("lr")
        r0 = read_register("r0")
        r1 = read_register("r1")
        r2 = read_register("r2")
        w_bytes = gdb.selected_inferior().read_memory(r1, r2).tobytes()

        print(f"0x{lr-4:08x}: write({r0},0x{r1:08x},{r2:3}) ==> {binascii.hexlify(w_bytes).decode()}")
        print()
        return False

class HookRW (gdb.Command):

    def __init__(self):
        super(HookRW, self).__init__("hookrw", gdb.COMMAND_USER)
        self.read_bkp = None
        self.write_bkp = None

    def invoke(self, arg, from_tty):
        if self.read_bkp == None:
            self.read_bkp = ReadBreakpoint(f"*0x{READ_ADDR:x}", gdb.BP_BREAKPOINT)
        if self.write_bkp == None:
            self.write_bkp = WriteBreakpoint(f"*0x{WRITE_ADDR:x}", gdb.BP_BREAKPOINT)

def main():
    gdb.execute(f"set pagination {PAGINATION}")
    gdb.execute(f"set logging file {LOG_FILE}")
    gdb.execute(f"set logging redirect {LOG_REDIRECT}")
    gdb.execute(f"set logging overwrite {LOG_OVERWRITE}")
    gdb.execute(f"set logging off")
    gdb.execute(f"set confirm off")
    gdb.events.exited.connect(exit_handler)

if __name__ == "__main__":
    # Commands registration
    HookRW()
    # Main
    main()

Define stop hook from Python

def stop_handler (event):
    gdb.execute('dd')
    gdb.execute('bt')

gdb.events.stop.connect (stop_handler)

Quit gdb from Python

Simply call quit():

python quit()