Configuring a tmux layout for pwndbg
I’ve been wanting to have a more pleasant GDB experience than just plain pwndbg and having all the information shown in the same place. The pwndbg features page contains some info on how to split the context sections using tmux, and a small example, as well as using splitmind to create a layout.
Creating a layout seems easy enough, and since the splitmind
project hasn’t been updated in 5 years, I decided to create pwnmux
to: a) have a quick way to setup GDB to my liking, and b) understand how the layout creation work, so that it can easily be changed when needed.
In this post we’ll go through the creation of a gdbinit
that splits a tmux window into multiple panes, where each pane contains a different context section from pwndbg
.
If you just want the use the layout, here’s the TLDR of how the layout looks:
And the github repository that you can clone and use: https://github.com/joaogodinho/pwnmux
Requirements
I built the configuration on the following versions:
Creating A Custom Configuration
The gdbinit
File
Before we move into the actual layout configuration, let’s start by understanding the .gdbinit
file, how it will be used in this scenario, and how it can also be used for other useful tasks.
As defined in man gdbinit
, the gdbinit
file can “contain GDB commands to automatically execute during GDB startup.”
Allowing us to customize and automate our debugging session.
The gdbinit
file is loaded in the following order:
/etc/gdb/gdbinit
/etc/gdb/gdbinit.d/*
~/.config/gdb/gdbinit
~/.gdbinit
./.gdbinit
We’re just interested in using the ~/.gdbinit
, but it’s worth noting that using ./.gdbinit
is super useful for setting custom commands when debugging a specific executable.
To load the `./.gdinit` file, the `~/.gdbinit` file must have `set auto-load safe-path .`. This has safety concerns.
One other interesting feature of gdbinit
(and GDB in general) is the availability of Python scripts.
This is available directly from inside GDB, but can also be used from scripts, or more specifically for our case, from .gdbinit
.
Furthermore, GDB also provides a Python module that can be used to call GDB commands from the Python script.
Below we show an example .gdbinit
file that uses Python:
# We're running GDB commands, so we need to tell GDB to run the following from python
python
import gdb
print('Hello from python')
# Same as running `start` from GDB
gdb.execute('start')
# Tell GDB we're done sending Python
end
An alternative way of loading this Python script would be move the Python code into its own file and then add the following in .gdbinit
:
source <path to script>.py
For pwnmux
we’ll use the loading via the source
command.
Creating the layout
With a better understanding on how we’ll build pwnmux
, let’s move into the tmux
layout we want.
As shown in the Pwnmux layout picture above, we want to have 6 panes with different context sections.
Using tmux split-window
we can create the panes, but we have to take into account from where the window will be split, its size, how to identify it, and what command to run inside the new pane:
- For the position, we can control it using the
-b
and-t
flag. With-b
we can create the window to the left or above (default is right and below), and with the-t
flag we can specify the target pane from where the split will happen (e.g.: top-left, bottom-right). - For the size we’ll use the
-l
followed by a percentage for the new pane. - To identify it we’ll use
-P
to return information about the created pane, and-F "#{pane_id}:#{pane_tty}"
to specify the output format as the pane ID and TTY. - As for the command to be run, this will use the
-d
flag with thecat -d
command for all panes, with the exception of theipython
pane that will haveipython
as the command.
Based on this, we can create the following:
import os
panes = {
# Split horizontal to make the main window at the bottom
'disasm': os.popen('tmux split-window -vb -P -F "#{pane_id}:#{pane_tty}" -l 75% -d "cat -"').read().strip().split(":"),
# Split horizontal to make the disasm + regs on the top, stack + stacktrace on bottom
'stack': os.popen('tmux split-window -v -P -F "#{pane_id}:#{pane_tty}" -l 40% -t {top} -d "cat -"').read().strip().split(":"),
# Split vertical next to the stack for the backtrace
'backtrace': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t -1 -l 30% -d "cat -"').read().strip().split(":"),
# Split vertical next to the disassemble for the registers
'regs': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t {top} -l 30% -d "cat -"').read().strip().split(":"),
'ipython': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t {bottom} -l 30% -d "ipython"').read().strip().split(":"),
}
We’re using a dict to be able to easily reference all the created panes.
The .read().strip().spit(":")
allows us to get a tuple where the first position is the pane ID and the second is the pane TTY.
Having created the panes, we must now tell pwndbg
what to place in each.
To do this we can simply iterate the panes
dict and call contextoutput
from pwndbg.commands.context
.
from pwndbg.commands.context import contextoutput
# Tell pwndbg which panes are to be used for what
for section, p in panes.items():
contextoutput(section, p[1], True, 'top', False)
With the exception of the ipython
dict key, all other keys are valid sections in pwndbg
.
The third argument specifies that the output should be cleared, the fourth argument specifies the position of the banner, and the fifth argument specifies the width of the banner.
By setting it to False
, it will just print the name of the section.
If we place all of the above in a Python script and source
it from the .gdbinit
we’ll get the layout we want, but there are some things to take care of still.
First we want to also have a section for the legend (tells us what type of data each color means) and a section for the expressions. We add those with:
# Also add the sections legend and expressions to already existing panes
contextoutput("legend", panes['stack'][1], True)
contextoutput("expressions", panes['regs'][1], True, 'top', False)
By calling contextoutput
with panes that are already being used, we can append sections to them.
Second we want to add some customization to GDB (increase the number of lines in some sections).
For this we have two options: use the gdb
module to run GDB commands (as previously described), or use pwndbg.config
object to change the configuration.
We’ll use the pwndbg.config
object to make the script debugger agnostic (thanks for the hint @disconnect3d):
# To see more options to customize run `theme` and `config` in gdb
# Increase the amount of lines shown in disasm and stack
pwndbg.config.context_disasm_lines.value = 25
pwndbg.config.context_stack_lines.value = "18"
# Give backtrace a little more color
pwndbg.config.backtrace_prefix_color.value = "red,bold"
pwndbg.config.backtrace_address_color.value = "gray"
pwndbg.config.backtrace_symbol_color.value = "red"
pwndbg.config.backtrace_frame_label_color.value = "green"
In this case we modify the number of lines to 25 and 18 for the disasm and stack to fit our screen, and change the backtrace colors.
Lastly we want to close the panes when we exit GDB, which can be done with atexit
:
import atexit
# Remove the panes when gdb is exited
atexit.register(lambda: [os.popen(F"tmux kill-pane -t {p[0]}").read() for p in panes.values()])
We iterate the values of the panes
dict and use the first tuple entry (the pane ID) as the argument to the target pane to kill.
When we exit GDB the lambdas are called and the panes we created are killed.
And that’s it… The final script should like like this:
import os
import atexit
import pwndbg
from pwndbg.commands.context import contextoutput
panes = {
# Split horizontal to make the main window at the bottom
'disasm': os.popen('tmux split-window -vb -P -F "#{pane_id}:#{pane_tty}" -l 75% -d "cat -"').read().strip().split(":"),
# Split horizontal to make the disasm + regs on the top, stack + stacktrace on bottom
'stack': os.popen('tmux split-window -v -P -F "#{pane_id}:#{pane_tty}" -l 40% -t {top} -d "cat -"').read().strip().split(":"),
# Split vertical next to the stack for the backtrace
'backtrace': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t -1 -l 30% -d "cat -"').read().strip().split(":"),
# Split vertical next to the disassemble for the registers
'regs': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t {top} -l 30% -d "cat -"').read().strip().split(":"),
'ipython': os.popen('tmux split-window -h -P -F "#{pane_id}:#{pane_tty}" -t {bottom} -l 30% -d "ipython"').read().strip().split(":"),
}
# Tell pwndbg which panes are to be used for what
for section, p in panes.items():
contextoutput(section, p[1], True, 'top', False)
# Also add the sections legend and expressions to already existing panes
contextoutput("legend", panes['stack'][1], True)
contextoutput("expressions", panes['regs'][1], True, 'top', False)
# To see more options to customize run `theme` and `config` in gdb
# Increase the amount of lines shown in disasm and stack
pwndbg.config.context_disasm_lines.value = 25
pwndbg.config.context_stack_lines.value = "18"
# Give backtrace a little more color
pwndbg.config.backtrace_prefix_color.value = "red,bold"
pwndbg.config.backtrace_address_color.value = "gray"
pwndbg.config.backtrace_symbol_color.value = "red"
pwndbg.config.backtrace_frame_label_color.value = "green"
# Remove the panes when gdb is exited
atexit.register(lambda: [os.popen(F"tmux kill-pane -t {p[0]}").read() for p in panes.values()])
This can be easily customizable for specific needs (e.g.: screen sizes, colors), and can also be easily extended.