今天第二次看见python字节码的逆向题,然后发现了一个介绍Python逆向的文章,所以把文章里的内容简单整理记录一下。
文章参考:https://www.cnblogs.com/blili/p/11799398.html
Python运行原理:
一.什么是Python
Python 是一种解释型、面向对象、动态数据类型的高级程序设计语言。
二.解释性语言和编译型语言的区别
我们编程都是用的高级语言,计算机不能直接理解高级语言,只能理解和运行机器语言,所以必须要把高级语言翻译成机器语言,计算机才能运行高级语言所编写的程序。翻译的方式有两种,一个是编译,一个是解释。
用编译型语言写的程序执行之前,需要一个专门的编译过程,通过编译系统(不仅仅只是通过编译器,编译器只是编译系统的一部分)把高级语言翻译成机器语言,把源高级程序编译成为机器语言文件。
解释型语言没有严格编译汇编过程,由解释器将代码块按需要变运行边翻译给机器执行。因此解释型语言一度存在运行效率底,重复解释的问题。但是通过对解释器的优化!可以提高解释型语言的运行效率。Python就属于这一种编程语言。
三.Python运行原理概述
Python没有严格意义上的编译和汇编过程。一般可以认为编写好的python源文件,由python解释器翻译成以.pyc为结尾的字节码文件。pyc文件是二进制文件,可以由python虚拟机直接运行。
注:有的朋友可能会问,为什么我运行python,有时候生成pyc文件,有时候没有呢?Python在执行import语句时,将会到已设定的path中寻找对应的模块。并且把对应的模块编译成相应的PyCodeObject(python中的一个类)中间结果,然后创建pyc文件,并将中间结果写入该文件。然后,Python会import这个pyc文件,实际上也就是将pyc文件中的PyCodeObject重新复制到内存中。而被直接运行的python代码一般不会生成pyc文件。
加载模块时,如果同时存在.py和.pyc,Python会尝试使用.pyc,如果.pyc的编译时间早于.py的修改时间,则重新编译.py并更新.pyc。
四.综述
Python源码->python解释器->.pyc文件->python虚拟机运行
Python的pyc文件结构
Python代码的编译结果就是PyCodeObject对象。PyCodeObject对象可以由虚拟机加载后直接运行,而pyc文件就是PyCodeObject对象在硬盘上的保存形式。因此我们先分析PyCodeObject对象的结构,随后再涉及pyc文件的二进制结构。
一.PyCodeObject对象结构
二.pyc文件生成:
python中使用marshal.dump的方法将PyCodeObject对象转化为对应的二进制文件结构。每个字段在二进制文件中的结构如下图:
pyc文件结构主要包括两部分:pyc文件头部表示和PyCodeObject对象部分。上面对PyCodeObject对象的二进制部分已经有了了解,pyc文件头部比较简单,在python2中只占用4个字节包含两个字段magic和mtime,完整的pyc文件结构见下图:
python字节码反编译
经过编译的python文件可以提高程序的运行速度,一定程度上也对源代码起到了保护作用。然而如果我们只有编译过的python字节码文件,就给我们审查源码造成了一定的困难,这就引出了python字节码反编译的需求。
根据python的编译原理我们知道PyCodeObjectData是python源文件作为一个实例化的类,通过python内置库函数marshal.dumps生成的二进制数据段,因此通过marshal.loads(PyCodeObjectData) ,我们可以得到PyCodeObjectData反序列化的对象。
使用python内置模块dis可以对PyCodeObject进行反编译,从而获取到python二进制字节码代码段的“汇编形式”。这样可以便于对字节码进行阅读。
Python字节码解读
字节码结构如下:
源码行号 | 跳转注释符 | 指令在函数中的偏移 | 指令符号(助记符) | 指令参数 | 实际参数值
上图表示:
- 该字节码指令在源码中对应59行
- 此处是跳转的目的地址
- 82该字节指令的字节码偏移
- 操作指令对应的助记符为LOAD_GLOBAL
- 操作参数为6
- 操作参数对应的实际值为disassemble
常见字节码解读
1.常量
加载常量只有一行LOAD_CONST,对应源码第1行,字节码偏移地址0字节,常量数组中索引0,实际常量值‘123 ’
2.局部变量
加载局部变量a:LOAD_CONST加载常量1,调用STORE_NAME(参数a),并将变量a存储为1
同理加载局部变量b
3.全局变量
加载全局变量a,与加载局部变量不同的是通过STORE_GLOBAL在存储变量
4.数据类型list
先将所有的list元素加载,调用BUILD_LIST方法生成list于内存中,通过STORE_NAME将堆栈中的list存储于局部变量a中
5.数据类型dict
BUILD_MAP声明字典元素数量,通过两次LOAD_CONST后,调用STORE_MAP生成键值对存于堆栈,最终通过STORE_NAME将堆栈中长度为2的两个键值对最为字典数据类型存储在a中
6.数学运算
字节码中显示先对局部变量a、b赋值,通过LOAD_NAME加载局部变量,调用加法BINARY_ADD,生成结果存储与堆栈中,使用STORE_NAME将堆栈中的计算结果存储与局部变量c
加减乘除的运算字节码相似,不不再赘述,读者可以自行分析,如下图:
上图中为对a、b做加减乘除的字节码,因为没有存储计算结果,所以每次运算完没有使用STORE_NAME方法存储,解释器默认调用POP_TOP方法将计算结果从堆栈顶部弹出,以保证堆栈平衡。
7.for循环
上图显示一个FOR循环的过程。SETUP_LOOP表明循环开始,参数说明此循环知道字节码偏移28字节的指令结束(也就是28字节开始不是循环)。调用range方法生成generator存于堆栈。FOR_ITER调用堆栈,声明generator作用到字节码偏移位置27字节。从第16字节起到27为generator迭代作用域。其中为一个print函数。
8.if判断
以一个简单的IF判断为例,先加载需要比较的常量,调用COMPARE_OP指令对堆栈中两个常量进行比较,将结果存入堆栈。调用POP_JUMP_IF_FALSE指令,判断栈顶值来决定程序运行顺序实现判断功能。