puck tools — test your egress policy against real C2 traffic →
#linux#gdb

Teaching GDB new commands

By now you have typed bt then print counter more times than you would like. GDB lets you name a sequence and call it once. It has two ways of doing that, its own scripting language and Python, and it is worth seeing the same job done both ways.

We are still using the counter program from the attaching lesson.

GDB’s own scripting

define bundles commands under a name:

define peek
  bt 1
  print counter
end

Now the whole check is one word:

(gdb) peek
#0  0x0000770df18c4b7a in __GI___clock_nanosleep (...) at clock_nanosleep.c:78
$1 = 5

bt 1 is a backtrace limited to the innermost frame, which is all you need when you already know what the program does and only want to know where it is.

Definitions take arguments. $arg0 is the first, $arg1 the second, and $argc is how many arrived:

define bump
  set var counter = counter + $arg0
  print counter
end
(gdb) bump 1000
$2 = 1005

It has flow control too, so a definition can make decisions:

define level
  if counter > 1000
    printf "counter is high: %d\n", counter
  else
    printf "counter is fine: %d\n", counter
  end
end
(gdb) level
counter is fine: 0
(gdb) set var counter = 2000
(gdb) level
counter is high: 2000

Put these in the .gdbinit next to the binary and they are there every time you open it. A definition you have to retype is not saving you anything.

The same thing in Python

GDB ships with a Python interpreter and an API onto its own internals. Anything after python runs in it:

(gdb) python print(gdb.parse_and_eval("counter"))
1005

gdb.parse_and_eval evaluates an expression in the target and hands back a value you can do arithmetic on. gdb.execute runs a GDB command, and with to_string=True gives you its output as text instead of printing it.

A command is a class. Put this in peek.py:

import gdb

class Peek(gdb.Command):
    """Show where we are and what the arguments were."""

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

    def invoke(self, arg, from_tty):
        frame = gdb.selected_frame()
        print(f"in {frame.name()} at {frame.find_sal().line}")
        for sym in frame.block():
            if sym.is_argument:
                print(f"  {sym.name} = {sym.value(frame)}")

Peek()

invoke is what runs when the command is typed. Creating the instance at the bottom is what registers it. Load it and use it:

(gdb) source peek.py
(gdb) break check
(gdb) run Evan
Breakpoint 1, check (name=0x7fffffffe0f7 "Evan") at hello.c:5
(gdb) peek
in check at 5
  name = 0x7fffffffe0f7 "Evan"

That is doing something define cannot. It walked the frame, found which symbols are arguments, and printed each one, without being told their names.

Functions you can use inside expressions

gdb.Function adds something callable from any GDB expression, with a $ in front:

class IsZero(gdb.Function):
    """True when the value is zero."""

    def __init__(self):
        super().__init__("iszero")

    def invoke(self, v):
        return int(v) == 0

IsZero()
(gdb) print $iszero(0)
$1 = 1
(gdb) print $iszero(5)
$2 = 0

Which means it works in a breakpoint condition: break check if $iszero(counter).

The governor, rewritten

The attaching lesson kept the counter in range with a conditional breakpoint and a command list. Here is the same rule as a Python breakpoint:

import gdb

class Governor(gdb.Breakpoint):
    def __init__(self):
        super().__init__("counter.c:10")
        self.silent = True
        self.hits = 0

    def stop(self):
        if int(gdb.parse_and_eval("counter")) >= 1010:
            gdb.execute("set var counter = 1000")
            self.hits += 1
            print(f"[governor] reset #{self.hits}")
        return False

Governor()

stop runs every time the breakpoint is hit, and its return value decides whether the process actually stops. Returning False means it never does, so this runs as a permanent rule rather than an interruption:

counter = 1008
counter = 1009
counter = 1010
[governor] reset #1
counter = 1001
counter = 1002

The same off-by-one as before. It resumes on 1001 because line 10 is counter++ and the breakpoint fires before that line runs.

Compare it with the commands version earlier in the course. The GDB script was shorter and needed nothing installed. The Python version can count its own hits, which is already past what define will do for you, and it could just as easily write to a file or make a decision no breakpoint condition could express.

Which to use

Use define for a sequence of commands you type a lot. It is quicker to write, it lives in .gdbinit, and it has no dependencies.

Reach for Python when you need to inspect something rather than just run commands: walking frames, reading structures, keeping state between hits, or deciding something a breakpoint condition cannot express.

Both live in the same place. .gdbinit takes define blocks directly, and Python files are loaded with source:

source ~/tools/mycommands.py

Up next, let’s configure gdb to be a reverse engineering and exploit development powerhouse.