using radare2 to debug with pwntools

did you know you’re not forced to use gdb to debug the stuff you do with pwntools?

what!? you knew that? oh well i didn’t know, let me just write a memo here.

sometime ago i stumbled upon this post by Michael Bann. check it out. tldr: he made a script to connect r2 to the gdbserver instance of the debugging program and execute commands given in the exploit with pwntools through r2pipe.

basically save this python file somewhere in PATH, change the line that spawns the terminal (noted at the head of the file) and setup context.terminal accordingly in pwntools, then you’ll be able to do something like this:

(generate the template)

pwn template ./vuln > exp.py

insert the r2 commands to execute in this line, something like:

gdbscript = '''
#r2.cmd('aaa; db sym.pwnme; dc')
'''.format(**locals())

then launch the exploit with argument ‘GDB’ to start the debugging process and spawn a radare2 window.

there are a couple of thing i changed in the script though; here’s my version

#!/usr/bin/env python2

"""
This enables use of radare2 for pwntools
Steps to enable
  1. Download and save as "pwntools-gdb" somewhere in your PATH
  2. chmod u+x pwntools-gdb
  3. In your gdbscript, start lines with hashtag "#" that you want to get executed by radare2. For instance, to set a breakpoint automatically, you would use gdbscript="#r2.cmd('db sym.amin')"
  4. Update line 51 if you're not a screen user..
"""

import sys
import argparse
import re
import os
import subprocess

pythondir = '{homedir:s}/.pwntools-r2'.format(homedir = os.environ['HOME'])
r2_python_template = """#!/usr/bin/env python2

import os
import r2pipe
r2 = r2pipe.open()

{user_commands:s}
"""

def set_python2():
    if not os.path.exists(pythondir):
        os.mkdir(pythondir)
    if not os.path.islink(pythondir + '/python'):
        python2 = subprocess.check_output(['which', 'python2']).strip()
        os.symlink(python2, pythondir + '/python')

def pwntools_r2():
    parser = argparse.ArgumentParser()
    parser.add_argument("-x", nargs=1)
    parser.add_argument("-q", action="store_true")
    parser.add_argument("file", nargs=1)
    args = parser.parse_args()

    file_name = args.file[0]

    gdb_script = args.x[0]

    with open(gdb_script, "r") as f:
        gdb_script = f.read()

    ip, port = re.findall("target remote (.+):([0-9]+)", gdb_script)[0]

    # Find the user commands that start with "#"
    user_commands = '\n'.join([line[1:] for line in gdb_script.split("\n") if line.startswith("#")])

    script_file = args.x[0] + ".py"
    with open(script_file,"w") as f:
        f.write(r2_python_template.format(mod_name=os.path.basename(file_name), user_commands=user_commands))

    set_python2()
    os.system('''/usr/bin/xfce4-terminal -e 'sh -c "PATH={pythondir:s}:$PATH r2 -i {script_file:s} -d gdb://{ip:s}:{port:s}"' '''.format(pythondir=pythondir, script_file=script_file, ip=ip, port=port))

def main():
    pwntools_r2()

if __name__ == '__main__':
    main()

i’m not a screen user so obviously i set it up to spawn a xfce4 terminal window. also, i have the python symlink pointing to python3, as it is my primary interpreter, and r2 would give me errors when trying to execute the commands in the gdbscript variable; apparently r2 ignores the shebang (!#/usr/bin/env python2) on the head of the script_file. the only way i managed to get it working without changing the python symlink every time is create another symlink in a directory, this time to python2, and launch the r2 process putting this dir ahead of any other in PATH, so it would be picked before the default python symlink.

the other thing i changed, or in fact removed, is this load_modules() function as, in my case, it just made r2 load the same file twice.