Wednesday, July 4, 2012

Pyrasite infestation

The best thing about EuroPython 2012 is the chance to learn about great Python hacks like pyrasite.

Pyrasite is a debugging tool which uses gdb to attach to a running Python process so it can inject code dynamically into it.

To give it a try, first install it inside a virtual environment:

$ virtualenv ~/venv
$ source ~/venv/bin/activate
$ pip install pyrasite

Then create a simple Python program, let's say a.py, and run it:

#!/usr/bin/env python

import time
import sys

def f():
    while True:
        print "kalhmera"
        time.sleep(1)

def main():
    f()

if __name__ == "__main__":
    sys.exit(main())

Use a command like ps -ef|grep a.py to get its PID, let's assume its 15210, then
in a new terminal run:

$ source ~/venv/bin/activate
$ pyrasite-shell 15210
Pyrasite Shell 2.0
Connected to 'python ./a.py'
Python 2.6.6 (r266:84292, Dec 26 2010, 22:31:48)
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(DistantInteractiveConsole)

>>> import __main__
>>> import traceback
>>> __main__.__file__
'./a.py'

The shell you're seeing is actually running code inside the context of the running process!
You can do all sorts of nice stuff, like import new modules, inspect stack frames, take a look at variables, e.g. globals(), a number of great things come to mind.

Let's get a list of all running threads and their stack frames:

>>> for thread, frame in sys._current_frames().items():
...     print('Thread 0x%x' % thread)
...     traceback.print_stack(frame)
...     print()
...
Thread 0x7ffdc79af700
  File "/usr/lib/python2.6/threading.py", line 504, in __bootstrap
    self.__bootstrap_inner()
  File "/usr/lib/python2.6/threading.py", line 532, in __bootstrap_inner
    self.run()
  File "<string>", line 167, in run
  File "/usr/lib/python2.6/code.py", line 243, in interact
    more = self.push(line)
  File "/usr/lib/python2.6/code.py", line 265, in push
    more = self.runsource(source, self.filename)
  File "/usr/lib/python2.6/code.py", line 87, in runsource
    self.runcode(code)
  File "/usr/lib/python2.6/code.py", line 103, in runcode
    exec code in self.locals
  File "<console>", line 3, in <module>
()
Thread 0x7ffdc9172700
  File "./a.py", line 16, in <module>
    sys.exit(main())
  File "./a.py", line 13, in main
    f()
  File "./a.py", line 10, in f
    time.sleep(1)
()

The first thread is the thread created by pyrasite to infest the running process and get it under our control.

Under the hood, pyrasite uses gdb to attach to the running CPython process and inject code to fire up this new thread. This thread receives user requests over a socket connection and executes them inside the process itself.

For more information and examples of code to inject take a look at the pyrasite documentation, or explore the code itself, at ~/venv/lib/python2.6/site-packages/pyrasite/reverse.py for all the intriguing details.