Skip to content

Commit

Permalink
Merge pull request #3 from NtesEyes/develop
Browse files Browse the repository at this point in the history
Update Readme, error format and inject method
  • Loading branch information
valensc authored Feb 1, 2019
2 parents c5f3b47 + 54a7cca commit 58de773
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 39 deletions.
41 changes: 24 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,45 @@

[![PyPI version](https://badge.fury.io/py/pylane.svg)](https://badge.fury.io/py/pylane)

A python vm injector with debug tools, based on gdb and ptrace.

## Features
[简体中文](README.zh-cn.md)

* Attach a running python process with its pid, directly access and change anything in python vm, or run a user defined python script.
* Provide a python remote shell.
* Use IPython as an interactive interface, support some IPython features such as ? % magic functions.
* Provide remote auto completion.
* Provide debug toolkit, which can get class/instances by name, get object's source code, etc.
* Defined an executor in program, and use the shell as a command interface.
* Support Linux and BSD
Pylane is a python vm injector with debug tools, based on gdb and ptrace.
Pylane uses gdb to trace python process, inject and run some code in its python vm.

## Usage

install:
![pylane_show](misc/pylane_show.gif)

use inject command to inject a python script in an process:

```
pip install pylane
pylane inject <PID> <YOUR_PYTHON_FILE>
```

use:
use shell command to inject an interactive shell:

```
pylane inject <PID> <YOUR_PYTHON_FILE>
pylane shell <PID>
```

to run your code in a process
Pylane shell features:

or
* use IPython as its interactive interface, support magic functions like ? and %
* support remote automatic completion
* provide debug toolkit functions, such as:
* lookup class or instance by name
* get source code of an object
* print all threads' stack and locals

## Install

```
pylane shell <PID>
pip install pylane
```

to get an interactive shell
pylane should be installed in virtualenv the target process uses or in os python lib.

## Compatibility

Support Linux and BSD
46 changes: 46 additions & 0 deletions README.zh-cn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# pylane

[![PyPI version](https://badge.fury.io/py/pylane.svg)](https://badge.fury.io/py/pylane)

[English](README.md)

Pylane 是一个基于gdb的python进程注入和调试工具,
通过gdb trace python进程,然后在该进程的python vm中动态地注入一段python代码,
从而对一个运行中的python进程执行一段任意的逻辑。

## 用法

![pylane_show](misc/pylane_show.gif)

使用inject命令把一个python脚本注入到目标进程:

```
pylane inject <PID> <YOUR_PYTHON_FILE>
```

使用shell命令对目标进程注入一个交互式的shell:

```
pylane shell <PID>
```

Pylane shell特性:

* 使用IPython作为交互接口,支持 ? % 等魔术方法
* 支持完整的远程自动补全
* 提供常用工具函数,例如:
* 按名字搜索类或实例
* 获取对象源代码
* 打印所有线程栈和局部变量

## 安装

```
pip install pylane
```

如果使用virtualenv,需要pylane安装在被attach进程所用的env或系统库.

## 兼容性

兼容 Linux 和 BSD
Binary file added misc/pylane_show.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions pylane/core/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except PylaneException as e:
print(e.args)
for arg in e.args:
print(arg)
# TODO
except Exception as e:
print('Internal error occured, contact author if u need help.')
print(e.args)
for arg in e.args:
print(arg)
print(traceback.format_exc())
exit(1)
return wrapper
62 changes: 43 additions & 19 deletions pylane/core/injector.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-

import os
import sys
import signal
import time
import tempfile
import platform
Expand Down Expand Up @@ -66,14 +68,15 @@ def env_detect(self):
)
# check ptrace
ptrace_scope = '/proc/sys/kernel/yama/ptrace_scope'
ptrace_req_msg = ('ptrace is disabled, enable it by:'
'echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope . '
'arg --privileged may be also needed for docker '
'exec/run command to override ptrace_scope.')
if os.path.exists(ptrace_scope):
with open(ptrace_scope, 'r') as f:
value = int(f.read().strip())
if value == 1:
raise RequirementsInvalid(
'ptrace is disabled, enable it by: ' +
'echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope'
)
raise RequirementsInvalid(ptrace_req_msg)
else:
getsebool = '/usr/sbin/getsebool'
if os.path.exists(getsebool):
Expand All @@ -82,10 +85,7 @@ def env_detect(self):
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
if 'deny_ptrace --> on' in out:
raise RequirementsInvalid(
'ptrace is disabled, enable it by: ' +
'sudo setsebool -P deny_ptrace=off'
)
raise RequirementsInvalid(ptrace_req_msg)

def cleanup(self):
""""""
Expand All @@ -112,7 +112,7 @@ def ensure_code_file(self, code, file_path):
self.code_file = self.temp_file = temp_file_path
else:
raise RequirementsInvalid(
'Neither code or code file_path specified.'
'Neither code nor code file_path specified.'
)
st = os.stat(self.code_file)
os.chmod(self.code_file,
Expand Down Expand Up @@ -153,12 +153,31 @@ def generate_gdb_codes(self):
# deprecated, cause its hard to pass python code in shell args
# exec code is passed by shell command line, so add \\\" and \\\\n
# run_code = 'exec(\\\"%s\\\")' % (self.code.replace('\n', '\\\\n'))
run_code = ' '.join([
'code_file = open(\\\"%s\\\");' % self.code_file,
'raw_code = code_file.read();',
'code_file.close();',
'exec(raw_code)'
])
if sys.version_info.major == 2:
run_code = ' '.join([
'__code_file = open(\\\"%s\\\");' % self.code_file,
'__raw_code = __code_file.read();',
'__code_file.close();',
# python 2 donot support exec as Thread's target param
'exec(__raw_code);',
'del __code_file;',
'del __raw_code;'
])
else:
run_code = ' '.join([
'__code_file = open(\\\"%s\\\");' % self.code_file,
'__raw_code = __code_file.read();',
'__code_file.close();',
# run code async and stop injection early to keep target process safe
'from threading import Thread as __Thread;'
'__thread = __Thread(target=exec, args=(__raw_code,));'
'__thread.daemon = True;'
'__thread.start();'
'del __code_file;'
'del __raw_code;'
'del __Thread;'
'del __thread;'
])
# TODO injected code may change path as well
cleanup_code = ' '.join([
'%s.path = %s.path[:-%s];' % (
Expand All @@ -172,14 +191,20 @@ def generate_gdb_codes(self):
'call PyRun_SimpleString("%s")' % prepare_code,
'call PyRun_SimpleString("%s")' % run_code,
'call PyRun_SimpleString("%s")' % cleanup_code,
# make sure previous codes are safe.
# gdb exit without GIL release is a disaster for target process.
'call PyGILState_Release($1)',
]

def timeout_exit(self, process):
print("timeout in %s secs, exit." % self.timeout)
os.kill(process.pid, signal.SIGTERM)

def inject(self):
"""Run inject"""
codes = self.generate_gdb_codes()
process = self.run(codes)
timer = Timer(self.timeout, lambda p: p.kill(), [process])
timer = Timer(self.timeout, self.timeout_exit, (process,))
out = b''
err = b''
try:
Expand All @@ -189,9 +214,8 @@ def inject(self):
timer.cancel()
self.cleanup()
if self.verbose:
print('stdout:')
print(out)
print('stderr:')
print('stdout:', out)
print('stderr:', err)
print(err)
if b'Operation not permitted' in err:
print('Cannot attach a process without perm.')
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from os import path


VERSION = '0.0.5'
VERSION = '0.0.6'

this_directory = path.abspath(path.dirname(__file__))
with open(path.join(this_directory, 'README.md')) as f:
Expand Down

0 comments on commit 58de773

Please sign in to comment.