1 of 100

Open Source Development Study: Pwndbg

Dominik 'Disconnect3d' Czarnota

@ Pykonik Tech Talks #76

2025.04.24, Cracow, Poland

1

2 of 100

# whoami

2

justCattheFish CTF team captain

Staff Security Engineer @

Maintainer of Pwndbg/Pwndbg

3 of 100

Pwndbg: what & why?

3

4 of 100

Pwndbg: what & why?

Plugin for GDB & LLDB

  • TL;DR: make GDB suck less …

Adds many features missing from above debuggers useful for security research, reverse engineering, exploit development

4

5 of 100

Pwndbg �features

5

6 of 100

6

Can be also invoked with command

7 of 100

7

Shows color LEGEND first :)

8 of 100

8

Then it displays sections

9 of 100

9

Then it displays sections

REGISTERS

10 of 100

10

Then it displays sections

REGISTERS

DISASM

11 of 100

11

Then it displays sections

REGISTERS

DISASM

SOURCE CODE (if available, can also be decompiled from IDA Hex-Rays)

12 of 100

12

Then it displays sections

REGISTERS

DISASM

SOURCE CODE (if available, can also be decompiled from IDA Hex-Rays)

STACK memory contents

13 of 100

13

Then it displays sections

REGISTERS

DISASM

SOURCE CODE (if available, can also be decompiled from IDA Hex-Rays)

STACK memory contents

BACKTRACE

14 of 100

“telescope”

aka�“dereference everything pls”

int******

int*****

int****

int***

int**

int*

int

15 of 100

typedef struct Error {

char* msg;

int code;

int (*callback)(const char*);

} Error;

int main() {

Error e = {"DivByZero", 0x1, puts};

Error* ptr = &e;

// break here!

16 of 100

typedef struct Error {

char* msg;

int code;

int (*callback)(const char*);

} Error;

int main() {

Error e = {"DivByZero", 0x1, puts};

Error* ptr = &e;

// break here!

17 of 100

typedef struct Error {

char* msg;

int code;

int (*callback)(const char*);

} Error;

int main() {

Error e = {"DivByZero", 0x1, puts};

Error* ptr = &e;

// break here!

&ptr

18 of 100

typedef struct Error {

char* msg;

int code;

int (*callback)(const char*);

} Error;

int main() {

Error e = {"DivByZero", 0x1, puts};

Error* ptr = &e;

// break here!

&ptr

&e

19 of 100

typedef struct Error {

char* msg;

int code;

int (*callback)(const char*);

} Error;

int main() {

Error e = {"DivByZero", 0x1, puts};

Error* ptr = &e;

// break here!

&ptr

&e

Error e

20 of 100

Non-linear disassembly

21 of 100

Predicting values

statically

and by emulating code!

22 of 100

Predicting values

22

23 of 100

Non-linear disassembly

23

24 of 100

Non-linear disassembly

24

25 of 100

Non-linear disassembly

25

26 of 100

Non-linear disassembly

26

JE = "Jump If Equal" — but more precisely "Jump if ZF (Zero Flag) is set"

27 of 100

Non-linear disassembly

27

JE = "Jump If Equal" — but more precisely "Jump if ZF (Zero Flag) is set"

28 of 100

Non-linear disassembly

28

JE = "Jump If Equal" — but more precisely "Jump if ZF (Zero Flag) is set"

If we set ZF flag (setflag ZF 1):

29 of 100

Displaying function arguments

30 of 100

31 of 100

Proper hexdump o/

31

32 of 100

Vmmap: better than info proc mappings

32

33 of 100

Configure everything: config

33

34 of 100

Configure everything: theme

34

35 of 100

& really many many more…

36 of 100

& really many many more…

  • Displaying cool process info like opened files/sockets (procinfo)
  • Better memory search than GDB
  • glibc heap inspection (heap, bins, arena commands etc)
  • tracking GOT calls
  • tracking heap allocations
  • displaying canary values
  • finding pointers to pointers (probeleak, leakfind)

36

37 of 100

Pwndbg: history

37

38 of 100

PEDA

Created in 2012 �by @longid

github.com/longld/peda

Single Python script

38

39 of 100

GEF

Created in 2015

by @hugsy

github.com/hugsy/gef

Single Python script

39

40 of 100

Pwndbg

Fork of GEF in 2015

Made by @ebeip90�(Zach Riggle)

Split into modules

+ Include batteries�(dependencies)

40

41 of 100

Pwndbg: history

  • 2012 - @longld releases PEDA (Python Exploit Development Assistant)
  • 2015 - @hugsy creates GEF (GDB Enhanced Features)
  • >2015 - @ebeip90 (Zach Riggle) creates Pwndbg as GEF fork
  • 2017 - my first contribution
  • 2017-2020 - me taking over the project?
    • Can't get exact date because … GitHub
  • At some point: bata24/gef (GEF fork with lots of features)

41

42 of 100

Random stats

  • >8.5k stars on GitHub // GEF: 7.4k, bata24/gef: ~450
  • 979 forks
  • 253 contributors
  • 324 Python files, 80k LoC (10.5k comment lines)

42

123 commits

43 of 100

Random stats

43

44 of 100

Random stats

44

45 of 100

Pwndbg: my first contribution

45

46 of 100

Pwndbg: my first contribution

46

47 of 100

Various aspects of maintaining an open source project

47

48 of 100

Getting users & contributors

48

49 of 100

Getting users & contributors

How to live?

49

50 of 100

Getting users & contributors

  • Good setup / docs / usability / UX
    • E.g. how to find commands in Pwndbg?
    • MoTD/tips
  • Organize workshops & coding sprints
    • once we had a blindfold person contribute to Pwndbg!
  • Take part in Google/Python Summer of Code

50

51 of 100

Good git workflow

* just keep it simple & stupid

51

52 of 100

Our git workflow

Initially: dev, beta, stable branches

52

(Imperfect ChatGPT generated image…)

53 of 100

Our git workflow

Initially: dev, beta, stable branches

Now: dev + releases

53

(Imperfect ChatGPT generated image…)

Release 2

Release 1

54 of 100

"Easy" setup / installation

54

55 of 100

How to install?

Pwndbg not a "normal" Python module:

  • You can't just "pip install pwndbg"
  • you need GDB compiled with Python
  • So we have/had a setup.sh script

55

56 of 100

How to install?

56

57 of 100

How to install?

Pwndbg not a "normal" Python module: you need GDB compiled with Python

  • So we have/had a setup.sh script

57

  • …at the end:

pip install -r requirements.txt

echo "source gdbinit.py" >> ~/.gdbinit

58 of 100

How to install?

Pwndbg not a "normal" Python module: you need GDB compiled with Python

  • So we have/had a setup.sh script

58

  • …at the end:

pip install -r requirements.txt

echo "source gdbinit.py" >> ~/.gdbinit

59 of 100

Nix to the rescue!

59

60 of 100

Nix to the rescue!

We build

  • Distro packages (DEB, RPM, Arch, Alpine)
  • Portable builds: {GDB, LLDB} x {different archs} x {Linux, macOS}

That include everything: GDB, Python, native deps, Python deps, glibc etc.

See https://github.com/pwndbg/pwndbg/tree/dev/nix

60

61 of 100

Nix to the rescue!

61

62 of 100

But macOS quarantine…

* so we have a homebrew package too

62

63 of 100

But macOS quarantine…

63

64 of 100

But macOS quarantine…

64

65 of 100

But macOS quarantine…

65

66 of 100

But macOS quarantine…

66

67 of 100

But macOS quarantine…

67

68 of 100

Testing

68

69 of 100

Testing

  • We have different type of tests: unit, gdb & qemu
  • We use PyTest

69

70 of 100

Unit tests

  • Mock stuff that depend on GDB/LLDB

70

71 of 100

Unit tests

  • Mock stuff that depend on GDB/LLDB

71

72 of 100

GDB tests

  • Each test runs its own GDB

72

73 of 100

GDB tests

  • Each test runs its own GDB

73

74 of 100

QEMU tests

  • TL;DR: Run QEMU system emulation e.g. of Linux kernel
  • with a "QEMU gdb stub" gdbserver
  • attach to it with GDB + execute commands

74

75 of 100

Pwndbg plugin: how

75

76 of 100

Pwndbg plugin: how

How?

  • GDB: re-using GDB REPL (Read Eval Print Loop), using its hooks

  • LLDB: re-implementing our own REPL + using LLDB API

76

77 of 100

Pwndbg and GDB

  • GDB is written in C++
  • It embeds a Python interpreter & exposes a "gdb" module

77

# gdb --quiet --nx

(gdb) !cat hello.py

print("Hello world")

(gdb) source hello.py

Hello world

(gdb) pi print(gdb)

<module 'gdb' from '/usr/share/gdb/python/gdb/__init__.py'>

(gdb) pi

>>> 1+1

2

>>>

78 of 100

Pwndbg and GDB

  • GDB is written in C++
  • It embeds a Python interpreter & exposes a "gdb" module

But the API is lacking so sometimes we have to parse strings…:

78

lang = gdb.execute("show language", to_string=True)

# lang = 'The current source language is "auto; currently c".'

79 of 100

79

80 of 100

80

81 of 100

81

82 of 100

82

83 of 100

83

84 of 100

84

85 of 100

85

86 of 100

Pwndbg & LLDB: How its made

  • LLDB is also written in C++ & exposes API to Python
  • … so we implement REPL on top of it

86

while True:

line = input(PROMPT)

if not exec_repl_command(line, sys.stdout.buffer, dbg, driver, relay):

87 of 100

Pwndbg & LLDB: How its made

  • LLDB is also written in C++ & exposes API to Python
  • … so we implement REPL on top of it

  • some "smart" parsing…

87

while True:

line = input(PROMPT)

if not exec_repl_command(line, sys.stdout.buffer, dbg, driver, relay):

88 of 100

Pwndbg & LLDB: How its made

  • LLDB is also written in C++ & exposes API to Python
  • … so we implement REPL on top of it

^ Some "smart" parsing

88

while True:

line = input(PROMPT)

if not exec_repl_command(line, sys.stdout.buffer, dbg, driver, relay):

89 of 100

Pwndbg & LLDB: How its made

  • LLDB is also written in C++ & exposes API to Python
  • … so we implement REPL on top of it

^ Some "smart" parsing

89

while True:

line = input(PROMPT)

if not exec_repl_command(line, sys.stdout.buffer, dbg, driver, relay):

90 of 100

Not cool contributions:�"can i get assigned" (???)

90

91 of 100

91

92 of 100

92

93 of 100

(Not cool contributions:�"vibe coding" (???)

93

94 of 100

94

95 of 100

95

96 of 100

Random stories:

ipdb import time …

96

97 of 100

ipdb import time …

$ python --version

Python 3.12.7

$ cat import-pdb.py

import pdb

$ cat import-ipdb.py

import ipdb

$ hyperfine "python import-pdb.py"

Benchmark 1: python import-pdb.py

Time (mean ± σ): 75.4 ms ± 3.8 ms [User: 49.0 ms, System: 17.1 ms]

Range (min … max): 69.6 ms … 90.4 ms 33 runs

$ hyperfine "python import-ipdb.py"

Benchmark 1: python import-ipdb.py

Time (mean ± σ): 422.4 ms ± 22.4 ms [User: 321.4 ms, System: 82.2 ms]

Range (min … max): 402.2 ms … 470.4 ms 10 runs

97

98 of 100

ipdb import time …

$ python --version

Python 3.12.7

$ cat import-pdb.py

import pdb

$ cat import-ipdb.py

import ipdb

$ hyperfine "python import-pdb.py"

Benchmark 1: python import-pdb.py

Time (mean ± σ): 75.4 ms ± 3.8 ms [User: 49.0 ms, System: 17.1 ms]

Range (min … max): 69.6 ms … 90.4 ms 33 runs

$ hyperfine "python import-ipdb.py"

Benchmark 1: python import-ipdb.py

Time (mean ± σ): 422.4 ms ± 22.4 ms [User: 321.4 ms, System: 82.2 ms]

Range (min … max): 402.2 ms … 470.4 ms 10 runs

98

99 of 100

ipdb import time …

$ python --version

Python 3.12.7

$ cat import-pdb.py

import pdb

$ cat import-ipdb.py

import ipdb

$ hyperfine "python import-pdb.py"

Benchmark 1: python import-pdb.py

Time (mean ± σ): 75.4 ms ± 3.8 ms [User: 49.0 ms, System: 17.1 ms]

Range (min … max): 69.6 ms … 90.4 ms 33 runs

$ hyperfine "python import-ipdb.py"

Benchmark 1: python import-ipdb.py

Time (mean ± σ): 422.4 ms ± 22.4 ms [User: 321.4 ms, System: 82.2 ms]

Range (min … max): 402.2 ms … 470.4 ms 10 runs

99

100 of 100

And that's it… Questions? :)

100