Hack 虚拟内存系列(二):Python 字节对象
字数 1198 2025-08-29 08:32:24

Python 字节对象与虚拟内存操作详解

1. 概述

本教程将深入探讨如何通过虚拟内存操作来修改Python 3中运行的字节对象。我们将学习Python字节对象在内存中的表示方式,以及如何定位和修改这些对象。

2. 前提知识

在开始之前,需要掌握以下基础知识:

  • C语言基础
  • Python基础
  • Linux文件系统和shell基础
  • /proc文件系统基础

3. 环境准备

测试环境:

  • Ubuntu 14.04 LTS
  • Linux内核版本:4.4.0-31-generic
  • gcc 4.8.4
  • Python 3.4.3

4. Python字节对象基础

4.1 字节对象与字符串的区别

在Python 3中,字节对象(bytes)和字符串(str)是不同的类型:

s = "Betty"  # <class 'str'>
s = b"Betty"  # <class 'bytes'>

4.2 Python字节对象的内存表示

Python中的所有东西都是对象,包括字节对象。字节对象在CPython中的内部表示如下(来自bytesobject.h):

typedef struct {
    PyObject_VAR_HEAD
    Py_hash_t ob_shash;
    char ob_sval[1];
    
    /* 不变量:
     * ob_sval包含'ob_size+1'个元素的空间
     * ob_sval[ob_size] == 0
     * ob_shash是字符串的哈希值,如果尚未计算则为-1
     */
} PyBytesObject;

其中:

  • PyObject_VAR_HEAD 包含引用计数和类型信息
  • ob_size 存储字符串长度
  • ob_sval 存储实际的字节数据,以null结尾

5. 定位字节对象的内存地址

5.1 使用id()函数

Python的id()函数返回对象的内存地址:

s = b"Holberton"
print(hex(id(s)))  # 输出对象地址

5.2 检查/proc/[pid]/maps

通过检查进程的maps文件,可以确定对象所在的虚拟内存区域:

cat /proc/[pid]/maps

6. 创建C扩展来检查字节对象

为了更深入地检查字节对象,我们可以创建一个C扩展:

6.1 C函数实现

#include "Python.h"

void print_python_bytes(PyObject *p) {
    PyBytesObject *s;
    unsigned int i;
    
    printf("[.] bytes object info\n");
    s = (PyBytesObject *)p;
    
    if (s && PyBytes_Check(s)) {
        printf(" address of the object: %p\n", (void *)s);
        printf(" size: %ld\n", s->ob_base.ob_size);
        printf(" trying string: %s\n", s->ob_sval);
        printf(" address of the data: %p\n", (void *)(s->ob_sval));
        printf(" bytes:");
        
        for (i = 0; i < s->ob_base.ob_size + 1; i++) {
            printf(" %02x", s->ob_sval[i] & 0xff);
        }
        printf("\n");
    } else {
        fprintf(stderr, " [ERROR] Invalid Bytes Object\n");
    }
}

6.2 编译动态库

gcc -Wall -Wextra -pedantic -Werror -std=c99 -shared -Wl,-soname,libPython.so -o libPython.so -fPIC -I/usr/include/python3.4 bytes.c

6.3 Python中调用

import ctypes
lib = ctypes.CDLL('./libPython.so')
lib.print_python_bytes.argtypes = [ctypes.py_object]

s = b"Holberton"
lib.print_python_bytes(s)

7. 修改内存中的字节对象

7.1 内存搜索与替换脚本

创建一个可以搜索和替换所有可读写内存区域的Python脚本:

#!/usr/bin/env python3
'''
Locates and replaces all occurrences of an ASCII string in the 
entire virtual memory of a process.
'''

import sys

def print_usage_and_exit():
    print('Usage: {} pid search write'.format(sys.argv[0]))
    exit(1)

# 检查参数
if len(sys.argv) != 4:
    print_usage_and_exit()

# 获取参数
pid = int(sys.argv[1])
if pid <= 0:
    print_usage_and_exit()

search_string = str(sys.argv[2])
if search_string == "":
    print_usage_and_exit()

write_string = str(sys.argv[3])
if search_string == "":
    print_usage_and_exit()

# 打开进程的maps和mem文件
maps_filename = "/proc/{}/maps".format(pid)
mem_filename = "/proc/{}/mem".format(pid)

try:
    maps_file = open(maps_filename, 'r')
except IOError as e:
    print("[ERROR] Can not open file {}:".format(maps_filename))
    print(" I/O error({}): {}".format(e.errno, e.strerror))
    exit(1)

for line in maps_file:
    # 解析maps文件行
    sline = line.split(' ')
    name = sline[-1][:-1]
    addr = sline[0]
    perm = sline[1]
    
    # 只检查可读写的内存区域
    if perm[0] != 'r' or perm[1] != 'w':
        continue
        
    # 获取内存区域起始和结束地址
    addr_start, addr_end = [int(x, 16) for x in addr.split("-")]
    
    try:
        mem_file = open(mem_filename, 'rb+')
        mem_file.seek(addr_start)
        region = mem_file.read(addr_end - addr_start)
        
        # 查找并替换字符串
        try:
            i = region.index(bytes(search_string, "ASCII"))
            while True:
                print("Found '{}' at {:x}".format(search_string, addr_start + i))
                mem_file.seek(addr_start + i)
                mem_file.write(bytes(write_string, "ASCII"))
                mem_file.flush()
                
                # 更新缓冲区继续搜索
                region = region[:i] + bytes(write_string, "ASCII") + region[i+len(search_string):]
                i = region.index(bytes(search_string, "ASCII"))
        except ValueError:
            pass
            
        mem_file.close()
    except IOError:
        continue

maps_file.close()

8. 操作步骤

  1. 运行目标Python脚本:
./main_bytes.py
  1. 获取进程ID:
ps aux | grep main_bytes.py | grep -v grep
  1. 运行内存修改脚本:
sudo ./rw_all.py [pid] Holberton "~ Betty ~"
  1. 在目标脚本中按Enter键查看修改结果

9. 关键发现

  1. Python字节对象在内存中的表示包含:

    • 对象头信息
    • 字符串长度(ob_size)
    • 实际数据(ob_sval),以null结尾
  2. Python对象不是存储在堆或栈中,而是在特定的内存区域

  3. 通过/proc文件系统可以访问和修改进程的虚拟内存

  4. id()函数返回的是Python对象的内存地址

10. 未解决的问题

  1. 为什么在堆内存区域也能找到字符串"Holberton"?
  2. Python如何在堆外分配内存?
  3. 如果Python没有使用堆,object.h中"对象是在堆上分配的结构"是什么意思?

这些问题将在后续教程中探讨。

11. 总结

本教程详细介绍了:

  • Python字节对象的内存结构
  • 如何通过/proc文件系统访问进程内存
  • 创建C扩展来检查Python对象
  • 编写脚本搜索和修改内存中的字符串

通过这些技术,我们能够深入理解Python内部实现,并实现对运行中Python程序的内存操作。

Python 字节对象与虚拟内存操作详解 1. 概述 本教程将深入探讨如何通过虚拟内存操作来修改Python 3中运行的字节对象。我们将学习Python字节对象在内存中的表示方式,以及如何定位和修改这些对象。 2. 前提知识 在开始之前,需要掌握以下基础知识: C语言基础 Python基础 Linux文件系统和shell基础 /proc文件系统基础 3. 环境准备 测试环境: Ubuntu 14.04 LTS Linux内核版本:4.4.0-31-generic gcc 4.8.4 Python 3.4.3 4. Python字节对象基础 4.1 字节对象与字符串的区别 在Python 3中,字节对象(bytes)和字符串(str)是不同的类型: 4.2 Python字节对象的内存表示 Python中的所有东西都是对象,包括字节对象。字节对象在CPython中的内部表示如下(来自bytesobject.h): 其中: PyObject_VAR_HEAD 包含引用计数和类型信息 ob_size 存储字符串长度 ob_sval 存储实际的字节数据,以null结尾 5. 定位字节对象的内存地址 5.1 使用id()函数 Python的 id() 函数返回对象的内存地址: 5.2 检查/proc/[ pid ]/maps 通过检查进程的maps文件,可以确定对象所在的虚拟内存区域: 6. 创建C扩展来检查字节对象 为了更深入地检查字节对象,我们可以创建一个C扩展: 6.1 C函数实现 6.2 编译动态库 6.3 Python中调用 7. 修改内存中的字节对象 7.1 内存搜索与替换脚本 创建一个可以搜索和替换所有可读写内存区域的Python脚本: 8. 操作步骤 运行目标Python脚本: 获取进程ID: 运行内存修改脚本: 在目标脚本中按Enter键查看修改结果 9. 关键发现 Python字节对象在内存中的表示包含: 对象头信息 字符串长度(ob_ size) 实际数据(ob_ sval),以null结尾 Python对象不是存储在堆或栈中,而是在特定的内存区域 通过/proc文件系统可以访问和修改进程的虚拟内存 id()函数返回的是Python对象的内存地址 10. 未解决的问题 为什么在堆内存区域也能找到字符串"Holberton"? Python如何在堆外分配内存? 如果Python没有使用堆,object.h中"对象是在堆上分配的结构"是什么意思? 这些问题将在后续教程中探讨。 11. 总结 本教程详细介绍了: Python字节对象的内存结构 如何通过/proc文件系统访问进程内存 创建C扩展来检查Python对象 编写脚本搜索和修改内存中的字符串 通过这些技术,我们能够深入理解Python内部实现,并实现对运行中Python程序的内存操作。