[长城杯 2024] novel1
NSSCTF 里面有两道长城杯的附件,可以去复现一下:

先对程序基本运行一下:

main

1 | int __fastcall __noreturn main(int argc, const char **argv, const char **envp) |
prologue():

1 | char *prologue(void) |
1 | std::unordered_map<unsigned int,unsigned long>::clear(&bloodstains); |
- 作用:清空一个名为
bloodstains的 C++ 无序映射表(哈希表)。这个表的键 Key 是无符号整数unsigned int,值 Value 是无符号长整数unsigned long。 - 代码特征:这是典型的反编译器生成的语法。在正常的 C++ 源码中,程序员只会写 bloodstains.clear();。反编译器将其还原成了底层的函数调用形式。
1 | std::operator<<<std::char_traits<char>>(&std::cout, "Author: "); |
简化一下就是std::cout << "Author: ";
std::cout是 C++ 中的标准输出流对象(通常指向终端屏幕)。- 前面的 & 取地址符表示将
std::cout对象的内存地址作为第一个参数传进函数。把接下来的内容写入到这个输出流里
chapter
chapter 函数,包含了菜单和 index 的输入

1 | __int64 chapter(void) |
以最简洁的 C++代码来看就是一段连续输出和部分输入
1 | unsigned int chapter() { |
part1

1 | _QWORD *part1(void) |
1 |
|
part1 函数相当于 add,检查 blood 的个数不可以超过 0x1F,然后输入 blood_index,进行一系列检测,
通过 find 操作防止重复。
part2

1 | unsigned __int64 part2(void) |
1 | unsigned long part2() { |
哈希桶 (Bucket) 的概念
在前面我们提到过,std::unordered_map 的底层是哈希表。
当你把一个 Key 存进去时,C++ 会计算它的哈希值,然后把它放进对应的桶(Bucket)里。
- 如果两个不同的 Key 算出来的哈希值一样,或者对桶的数量取模后一样,它们就会被放进同一个桶里。这在计算机科学中叫哈希碰撞 。
- C++ 的处理方式是:把同一个桶里的元素用一个链表串起来。
代码中的 std::unordered_map::bucket 就是用来找某个 Key 在哪个桶里;bucket_size 就是看这个桶里串了多少个元素。
致命的 std::copy 操作
1 | std::copy<...>(v8, (char *)v7 + 1, buf); |
这对应的就是 std::copy(begin, end, buf),把指定桶里的所有键值对,全部复制到 buf 这个栈变量里。
我们继续堆 part2 分析:这个 part2 函数在最后调用了 cout << buf ,相当于 show。由于 memset 和 copy 操作都是针对于 v3 的,因此我们猜测 v3 有漏洞
epilogue

1 | void __noreturn epilogue(void) |
RACHE
除此之外,还存在着一个 RACHE 函数,里面有很多 pop 可以当做 gadgets 进行利用:

1 | .text:00000000004025B6 ; =============== S U B R O U T I N E ======================================= |
接下来我们去动调,看一看栈上的情况:
EXP:
1 | from pwn import * |
更新: 2026-05-06 21:59:06
原文: https://www.yuque.com/idcm/wnemg9/ibi2xs1gslnzchqu