沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

Python之Pickle反序列化

0x00 前言


之前学过有关Pickle的反序列化问题,但是好记心不如烂笔头,何况我还没有好记心…所以,过去这么久了,我都忘得差不多了,悲剧ε(┬┬﹏┬┬)3, 今天就及时的补上把。

本篇包含以下元素:

  • Python中Pickle的介绍
  • Pickle反序列化的简单了解

这里就不写实践了,我现在打算每次写了一个东西,下一篇专门写一个实践;过去的懒狗总是不太重视题目的实践,导致虽然学过,但遇到了还是无从下手,久而久之连理论也忘记了。学如逆水行舟,不进则退。大家做什么事情,还是要始终如一的,😋共勉

0x01 Pickle


简单介绍

Pickle是Python中用来序列化的一个模块,但是除此之外还有其他的模块,只是Pickle使用的比较普遍,且有些序列化模块在无法正常序列化或反序列化时会尝试去调用Pickle。

简单的使用

如果是保存为文件,我们可以使用dump()load分别来序列化和反序列化;如果是转换为字节流,我们可以使用dumpsloads

import pickle
import os

class Chenlvtang:
    name = ""
    id = 123
    def __init__(self, name, id):
        self.name = name
        self.id = id
    def printf(self):
        print("Hello")

if __name__ == "__main__":
    chen = Chenlvtang("chenlvtang", 8003119052)
    
    #file
    with open("seri_test", "wb") as file:
        pickle.dump(chen, file) #serialize
    with open("seri_test", "rb") as file:
        c = pickle.load(file) #unserialize
        c.printf()

    #bytes
    seri_test =  pickle.dumps(chen)
    print("String Serialize:  \n")
    print(seri_test)
    c = pickle.loads(seri_test)
    c.printf()

简单的原理

上面的程序输出一个经Pickle处理过后的字符串,如下:

b'\x80\x03c__main__\nChenlvtang\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\n\x00\x00\x00chenlvtangq\x04X\x02\x00\x00\x00idq\x05\x8a\x05\xcc\xe7\x05\xdd\x01ub.'

“我看不懂,但是我大受震惊”,隐约还是能看到”name”和”chenlvtang”这种的,有点类似于PHP的序列化,但是还有一些奇奇怪怪的东西。其实这些看不懂的东西,是Pickle的OPCode(Operation Code),即操作码,顾名思义,就是会完成一些操作。我们可以在pickle.py (位于Python的lib文件夹中) 中看到关于这些操作码的详解:

image-20210825154342323

可以看到,它还是有多版本的,新版本大概是添加了新的功能,所以他是向下兼容的,并且它的第一个版本(protocol 0)采用的都是可见字符,在之后的版本中才加入了\x8c这种不可见字符。在源码中我们还能发现Pickle的load(s)和dump(s)分别由_Unpickler_pickler类中的对应方法实现:

def _dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None):
    _Pickler(file, protocol, fix_imports=fix_imports,
             buffer_callback=buffer_callback).dump(obj)
    
def _load(file, *, fix_imports=True, encoding="ASCII", errors="strict",
          buffers=None):
    return _Unpickler(file, fix_imports=fix_imports, buffers=buffers,
                     encoding=encoding, errors=errors).load()

如果继续跟进上面两个类,还可以发现他们采用了两个重要的数据结构:栈和存储区(标签区),Pickle正是通过这个两个重要的结构与OPCode,从而实现了序列化和反序列化,我们将在后文更详细的介绍这两个结构。

现在为了更加深入的了解Python反序列化的原理,我们下面就先来了解并熟悉一下这些OPCode吧。

0x02 OPCode


啰嗦一下

上面说到了,OPCode是有多个协议版本的,但是它向下兼容且第一个版本采用的都是可见字符,所以为了便于我们分析,我们这里就基于第一个版本(V0)来学习。但是不同Python版本,所采用的默认OPCode版本不同,那么我们要怎么指定版本呢?其实很简单,只需要在序列化函数中指定就好了,如:dumps(chen, 0)即使指定了初始版本。

pickletools

改变版本后,我们得到了如下的字符串:

b'ccopy_reg\n_reconstructor\np0\n(c__main__\nChenlvtang\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nVname\np6\nVchenlvtang\np7\nsVid\np8\nL8003119052L\nsb.'

可能大家还是会觉得,“虽然比之前更加的易懂了,但如果不去源码里一个个对照操作码,还是看不懂,源码又不想看,看又看不懂,只能不看这样子才能维持的下去~”。确实,还好Python贴心的为我们这种懒狗准备了一个工具——“pickletools”。

seri_test =  pickle.dumps(chen, 0)
print("String Serialize:  \n")
print(seri_test)
pickletools.dis(seri_test) #使用dis即可反汇编

得到的结果如下图所示:

image-20210825162452781

上文提到了,pickle是通过栈和存储区两个重要的结构与OPCode的配合实现了序列化,在开始详细介绍每个操作码的具体作用前,我们有必要先了解这两种结构。

栈和存储区

栈(Stack): 栈分为当前栈和前序栈(metastack),当前栈专门用来处理栈顶数据,前序栈则是处理除栈顶外的数据。

存储区(memo):用来存储经过处理完后的变量,采用类似于数组的形式,用索引来取得值。

image-20210826145905953

操作码

在大概了解了这两种结构后,我们便来细细的探究V0版本之下一些常见的操作码具体含义:

  • .:用来表示反序列化的结束

  • I S V: 一般大写的字母都是表示某个数据类型,这里我只指出常见的三种,其实还有其他一些。 I 表示整数,S表示字符串,V表示Unicode字符,后接详细数据,换行符\n表示数据的结束, 然后数据会被压入栈中,例子如下(注意最后有个结束符):

    image-20210826152229960

  • (:向栈中压入一个特殊的Mark Object,作为后面创建元组、字典、列表的标记

  • t、d、l:分别为创建元组、字典、列表。创建时,先弹栈,直到遇到上面(压入栈中的Mark,此时创建结束,弹出Mark后,把创建好的元组(字典、列表)压入栈中,一张经典动图如下:

    实例如下:

    image-20210826154632109

    注意上面创建字典的时候,必须是key,value这样的属性依次出现。

  • ]、}、):分别往栈中压入空的列表、字典、元组。

    image-20210826155229570

  • s、u、a、e:这几个都是对创建的列表等(除元组,因为元组一旦创建,不允许修改)的操作。s为往空字典中添加由栈顶端两个元素构成的一对键值(如果键相同,会更新值),相当于 dict[key] = value; u为更新多个键值,要配合之前的Mark来使用,相当于pydict[keyi] = valuei for i in 1, 2, ..., na为往列表中更新一个元素,相当于list.append(value); e为向列表更新多个元素,和u一样也要搭配Mark来使用。实例如下:

    image-20210826163945476

  • c:通过调用find_class方法将一个全局object压入栈中,需要两个参数,第一个参数为模块名,第二参数为类名,同样的,是以换行符来界定参数是否结束,实例如下:

    image-20210826205155980

  • p:把栈顶元素放入memo,索引的类型是string

  • g: 把存入memo的变量取出,索引类型是string

  • R:弹出栈顶的元组(arg1,arg2…)为参数,再弹出新栈顶的object,组成object(arg1,arg2..)执行,然后把执行结果压回栈中,当使用了__reduce__方法(这个方法会告诉pickle反序列化时做什么,相当于PHP中的__wakeup, 要在类中使用)会生成R指令,实例如下:

    image-20210826212322486

  • b: 用栈顶的字典给栈顶下的Object(即第二个元素)的某个属性更新值,实例如下:

    image-20210905102643935

  • i、o: 创建实例,但是传参方式有点区别。i 是以其后的一个元素为模块名,第二个为类或方法名,然后将从mark开始的元素作为参数,实例如下:

    image-20210905094059493

    o则是以mark后的一个元素为Object(常用c引入),之后的所有为参数,实例如下:

    image-20210905101230645

懒狗的结尾

就先学到这里吧,一开始得到的那一坨,你现在应该看得懂了,我懒得重新分析了。😴 下一篇文章里我们将来学习反序列化漏洞的利用

0x03 参考文章


Python pickle 反序列化安全简述 - X5tar’s Blog

Python Pickle反序列化漏洞 - FreeBuf网络安全行业门户

从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势 - 知乎 (zhihu.com)

OpCodes · Pickle.jl (juliahub.com)