Skip to content

Allow calling GDB Python API #1617

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mephi42 opened this issue Jun 29, 2020 · 9 comments · Fixed by #1695
Closed

Allow calling GDB Python API #1617

mephi42 opened this issue Jun 29, 2020 · 9 comments · Fixed by #1695
Labels

Comments

@mephi42
Copy link
Contributor

mephi42 commented Jun 29, 2020

It would be great if pwntools could allow using GDB's Python API like this:

io = gdb.debug(['./pwnme'])
...
class Bp(io.gdb.Breakpoint):
    def stop(self):
        print(io.gdb.inferiors()[0].read_memory(addr, 8))
        io.gdb.execute('c')

This would make it possible to write assertions against program state, e.g. compare leaked variables with their actual values, check that heap massaging produces desired layout, or ensure that sending a certain input at a certain time triggers a certain function with certain arguments, etc.

A possible implementation might be injecting RPyC server into gdb via gdbscript and using gdb.post_event to talk to the main loop. RPyC claims to support callbacks so triggering breakpoint processing on pwntools side should be possible.

If this looks like a valuable feature, I could at least implement a PoC. Please let me know what you think!

@Arusekk
Copy link
Member

Arusekk commented Jun 29, 2020

Yes, this looks like a useful feature, and a PoC would be a great starting point. I mainly thought about (ab)using gdbscript parameter as a means of that, since the exploitation flow, and the debugged victim flow are kind of connected, but logically separate (you can e.g. break a program while the exploitation chain is halted waiting for output, or, in gdb, skip through multiple layers of irrelevant stuff that the exploit does). Most interesting debugged actions normally happen after a victim receives some payload, which would be difficult to catch from an exploit's perspective. What use do you find the most illustrative?

@mephi42
Copy link
Contributor Author

mephi42 commented Jun 29, 2020

What prompted me to think about this was solving CTF heap challenges, where I have to constantly go back and forth between exploit, gdbscript trace output and gdb itself, making sure that modifications to exploit produce desired results and don't break anything.

What's especially frustrating is realizing at some point that you need to modify the very beginning of the exploitation chain, and then figure out what other parts need to be adjusted. To keep track of all that I normally write comments like "here heap should be in XYZ state" and, when needed, compare them with what I see in gdb, but having the ability to automatically check all that would be invaluable.

The simplest use case therefore would be:

io.sendline('command')
io.recvuntil('prompt')
bins1 = parse_bins(io.gdb.execute('heap bins'))  # GEF integration for structured output might be cool too
assert bins1.fast[0x20][0] & 0xfff == 0xa20

Catching victim processing in simple cases might look like this:

class MallocBp(io.gdb.Breakpoint):
    def __init__(self):
        super().__init__(self, "malloc")
        self.count = 0

    def stop(self):
        self.count += 1
        if self.count == 5:
            io.gdb.execute('finish')
            assert int(io.gdb.parse_and_eval('$rax')) == free_hook_addr

io.recvuntil('prompt')
bp = MallocBp()
io.sendline('command')
io.recvuntil('prompt')
assert bp.count == 20
bp.delete()

@zachriggle
Copy link
Member

zachriggle commented Jun 30, 2020 via email

@zachriggle
Copy link
Member

Probably the easiest way to do this would be to inject an XMLRPCServer stub into GDB and communicate with it that way, but it would be a large undertaking and I'm not sure how well GDB handles multiple Python threads while the inferior is running.

@mephi42
Copy link
Contributor Author

mephi42 commented Jul 11, 2020

I made a small working prototype with RPyC. The advantage of RPyC is that everything is transparent - pwntools users would interact with GDB API directly without us having to wrap or register each individual call. Unfortunately, RPyC developers said that inheriting from remote classes is a hard problem, therefore there would have to be some special magic for Breakpoint class.

Regarding threading, yes, the server code has to be liberally sprinkled with gdb.post_event(). I'm experimenting with making RPyC network code run in a separate thread while dispatching remote calls to GDB thread.

@mephi42
Copy link
Contributor Author

mephi42 commented Sep 30, 2020

I finally got some time to look into this again and made some progress - now the minimal example (which includes breakpoints) is fully working. The next step is to try using more GDB scripting features.

@mephi42
Copy link
Contributor Author

mephi42 commented Oct 1, 2020

Attaching and dumping registers/memory seems to work fine. I've extended the example and added some doctests - at the moment they hang because gdb_faketerminal.py gets in the way with its auto-continue.

@mephi42
Copy link
Contributor Author

mephi42 commented Oct 2, 2020

I didn't manage to implement a perfect solution to this, so I settled for the following compromise: I set a new GDB_FAKETERMINAL=0 environment variable in attach() before calling misc.run_in_new_terminal(cmd) if GDB Python API is enabled, and remove it afterwards. gdb_faketerminal.py doesn't do auto-continue when it's set. Let's see how it goes in Travis.

An alternative was to set this variable inside the doctest, but there doesn't seem to be a way to hide it from the resulting documentation, which is ugly.

@mephi42
Copy link
Contributor Author

mephi42 commented Oct 4, 2020

Not sure what's causing test failures (FileNotFoundError when connecting to GDB), since locally everything is fine both in the host OS and in Docker. I've added an extra commit that enables output from terminal emulator when running doctests. Let's see if it sheds some light on the issue.

Edit: The bridge dies with ModuleNotFoundError: No module named 'rpyc'. RPyC is installed into /opt/hostedtoolcache/Python/3.8.5/x64/lib/python3.8/site-packages/rpyc - most likely gdb sees only system modules. I guess the right solution for the CI is to install RPyC globally as well.

This raises an interesting question - what to do if pwntools is used from a virtualenv? It might be tempting to try to play with GDB's PYTHONPATH, but in the worst case GDB might be linked to an incompatible libpython. So it's better to simply require that both pwntools' and GDB's pythons have RPyC.

mephi42 added a commit to mephi42/pwntools that referenced this issue Oct 5, 2020
@Arusekk Arusekk linked a pull request Oct 27, 2020 that will close this issue
Arusekk added a commit that referenced this issue Nov 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants