Gdb python

From miki
Revision as of 08:25, 21 November 2019 by Mip (talk | contribs) (→‎Python script)
Jump to navigation Jump to search

This page collects information on Python integration in gdb.

How-to

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()