是否可以在不使用 co_code 的情况下获取 python 字节码?

逆向工程 Python
2021-06-11 02:27:00

我在stackoverflow上发布了一段时间(虽然太旧了,无法迁移)。

假设我在 python 解释器中并定义一个函数如下:

def h(a):
  return a

如果我想查看字节码(不是使用 dis 进行反汇编),我通常可以使用h.func_code.co_code. 有没有其他方法可以查看字节码?这个特定的应用程序打包了一个自定义的 python 解释器(可能使用 py2exe),它删除了对 co_code 的访问。我不能只看 pyc 文件,因为它们是加密的。

例如,在解释器中,如果我只是输入h而不进行函数调用,我会得到函数的地址。我可以使用该地址来获取字节码吗?还有其他方法吗?

PS我当时这样做的最初目标是使用pyREtic(调用co_code)进行反编译。因为它调用了 co_code,所以它无法工作。我想出了一种方法来做到这一点,我最终将其作为答案发布。想看看其他人做了什么或想出了什么。

2个回答

首先,只是一个关于“什么是co_code的小提醒

在 Python 中,语言的每个元素(函数、方法、类等)都被定义并存储在一个对象中。co_code是连接到用于表示函数或方法的类中的一个字段。让我们用 Python 2.7 练习一下。

$> python2.7
Python 2.7.3 (default, Mar  4 2013, 14:57:34) 
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> def foo():
...     print('Hello World!')
... 
>>> dir(foo.__code__)

['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', 
 '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', 
 '__new__',  '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', 
 '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 
 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 
 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> foo.__code__.co_code
'd\x01\x00GHd\x00\x00S'

因此,您可以看到该co_code字段包含我们之前定义的函数的编译字节码。事实上,它似乎co_code只是一个缓冲区,以懒惰的方式存储编译后的字节码。只有在第一次访问时才编译它。

假设这样,这co_code只是一个统一的帮助程序来访问可能以多种形式存储的字节码。一种形式是*.pyc存储整个文件的已编译 Python 字节码的文件。另一种形式只是函数/方法的即时编译。

然而,有一种方法可以直接访问函数/方法定义,从而访问字节码。关键是拦截 Python 进程gdb并对其进行分析。网络上有一些关于此的教程(请参阅此处此处此处此处)。但是,这是一个快速示例(您需要先安装python-gdb软件包):

$> python2.7-dbg
Python 2.7.3 (default, Mar  4 2013, 14:27:19) 
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> def foo():
...     print('Hello World!')
... 
[40809 refs]
>>> foo
<function foo at 0x1a5e1b0>
[40811 refs]
>>> foo.__code__.co_code
'd\x01\x00GHd\x00\x00S'
[40811 refs]
>>> 
[1]+  Stopped                 python2.7-dbg

然后,您需要获取 Python 进程的 PID 并附加gdb在其上。

$ gdb -p 5164
GNU gdb (GDB) 7.4.1-debian
...
Attaching to process 5164
Program received signal SIGTSTP, Stopped (user).
Reading symbols from /usr/bin/python2.7-dbg...done.
Reading symbols from /lib/x86_64-linux-gnu/libpthread.so.0...
Reading symbols from /usr/lib/debug/lib/x86_64-linux-gnu/libpthread-2.13.so...done.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".done.
...
(gdb) print *(PyFunctionObject*)0x1a5e1b0
$1 = {_ob_next = 0x187aca0, _ob_prev = 0x189dd08, ob_refcnt = 2, 
      ob_type = 0x87ce00, func_code = <code at remote 0x187aca0>, 
      func_globals = {'__builtins__': <module at remote 0x7f5ebcb5e470>,
      '__name__': '__main__', 'foo': <function at remote 0x1a5e1b0>, '__doc__': None, 
      '__package__': None}, func_defaults = 0x0, func_closure = 0x0, func_doc = None, 
      func_name = 'foo', func_dict = 0x0, func_weakreflist = 0x0, 
      func_module = '__main__'}
(gdb) print  (*(PyFunctionObject*)0x1a5e1b0)->func_name
$2 = 'foo'
(gdb) print (*(PyCodeObject*)0x187aca0)
$3 = {_ob_next = 0x18983a8, _ob_prev = 0x1a5e1b0, ob_refcnt = 1, ob_type = 0x872680, 
      co_argcount = 0, co_nlocals = 0, co_stacksize = 1, co_flags = 67,
      co_code = 'd\x01\x00GHd\x00\x00S', co_consts = (None, 'Hello World!'),
      co_names = (), co_varnames = (), co_freevars = (), co_cellvars = (),
      co_filename = '<stdin>', co_name = 'foo', co_firstlineno = 1,
      co_lnotab = '\x00\x01', co_zombieframe = 0x0, co_weakreflist = 0x0}
(gdb) print (*(PyCodeObject*)0x187aca0)->co_code
$4 = 'd\x01\x00GHd\x00\x00S'

所以,这里是直接访问字节码的方法,给定函数的地址。

为了完整起见,我在 Python 字节码(以及如何访问它)上找到的最好的文档是 Python 代码本身,尤其是inspect模块(2.73.2)。试着看看它,它很有启发性。

您可以使用的另一个帮助是为 Python 字节码提供反汇编程序的dis模块这是一个可以执行此反汇编程序的示例。

$> python2.7
Python 2.7.3 (default, Mar  4 2013, 14:57:34) 
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> def foo():
...     print("Hello World!")
... 
>>> import dis
>>> dis.dis(foo)
  2           0 LOAD_CONST               1 ('Hello World!')
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       
              5 LOAD_CONST               0 (None)
              8 RETURN_VALUE 

perror 的回答我认为是正确的方法。我想发布我最终为别人做这件事的方式,以防万一我在对 perror 的回答的评论中提到的问题是正确的。我现在没有所有笔记,如有必要会更新。

基本上我在 gdb 中运行程序,在 PyObject_New(或者可能是 PyObject_Init)中设置一个断点。我在函数末尾附近设置了断点,以便创建对象。从那里我能够查看内存中的对象以提取字节码。

为了将这些信息返回给 pyREtic,我将函数名称和字节码从 GDB 中转储到一个文件中,修改了 pyREtic 以便它不会调用 co_code 来获取字节码,而是从文件中提取它。

就像我说的,已经有一段时间了(stackoverflow 问题来自 2012 年 9 月)。我会回顾我的笔记并填写详细信息。