nssctf_sign-ezc++

下载、查壳\、64位\、IDA打开,按f5查看伪代码

代码分析:

是一道 C++ 面向对象 + 虚函数 入门逆向题,核心逻辑是:创建一个 Man 类对象,通过虚函数表调用第二个虚函数,flag 的输出逻辑就藏在这个被调用的虚函数里。


二、代码逐段解析

1. 变量与类结构
复制代码
Man *v3;
Human *v4;
std::string name;
Human *m;
  • Human 是基类,Man 是继承自 Human 的派生类,两个类都包含虚函数。

  • name 是 C++ 标准字符串对象,用来存储构造对象时传入的名称参数。

    std::string name; 是 C++ 语言里的字符串对象定义语句

  • std:C++ 标准库的命名空间(standard),所有标准库组件都在这个命名空间下。

  • string:C++ 标准库提供的字符串类 ,是封装好的高级字符串类型,内部自动管理内存、支持拼接、比较、截取等各种操作,比 C 语言原生的 char 数组更易用。

  • name:变量名。

整句的意思就是:在栈上定义一个名为 name 的 C++ 标准字符串对象,初始状态是空字符串。

_main()GCC/MinGW 编译器自动插入的运行时初始化函数 ,并不是开发者手写的业务代码。

它是 C++ 程序启动时的标准前置操作,作用是初始化 C++ 运行时环境,执行所有全局静态对象的构造函数,确保全局变量、全局类对象在 main 函数业务逻辑执行前都完成初始化。

2. 构造字符串与对象
复制代码
std::string::string(&name, "NISACTF", &v7);
v3 = (Man *)operator new(0x18ui64);
Man::Man(v3, (std::string)&name, 4);
m = v3;

std::string::string(&name, "NISACTF", &v7);

这是 C++ 标准库内存分配器的构造语句 ,属于 std::string 字符串的配套底层实现代码,完全由编译器自动生成,不是开发者手写的业务逻辑。

逐部分拆解:

  • std::allocator<char>:C++ 标准库的内存分配器模板类 ,专门负责给 char 类型的元素申请、释放内存,是 std::string 底层用来管理字符串存储空间的工具。
  • ::allocator(&v7):这是分配器的构造函数,在栈上变量 v7 的位置,构造出一个 char 类型的分配器对象。

v3 = (Man *)operator new(0x18ui64);

这行是 C++ 对象创建的第一步:在堆上申请内存 ,对应开发者写的 new Man(...) 语句的底层拆分。

C++ 的 new 运算符在执行时会自动拆成两个步骤:① 申请足够大小的堆内存;② 在这块内存上调用类的构造函数完成初始化。IDA 反编译时会把这两个底层步骤分开显示,这行对应的就是第一步。

二、逐部分拆解

  1. operator new
    C++ 标准库提供的全局内存分配函数,功能和 C 语言的 malloc 类似,负责向系统申请指定字节数的堆内存,返回 void* 类型的通用指针。它只负责分配内存,不做对象初始化。
  2. 0x18ui64
    申请的内存大小,单位是字节:
    • 0x18 是十六进制,等于十进制的 24 字节 ,这是 Man 类对象在内存中占用的总大小(包含虚函数表指针、所有成员变量、内存对齐填充)。
    • ui64unsigned __int64 的缩写,表示这是 64 位无符号整数,对应 64 位程序的内存寻址。
  3. (Man *)
    强制类型转换。operator new 返回的是无类型的 void* 指针,强转成 Man* 类指针后,才能用来调用构造函数、访问类成员。

Man::Man(v3, (std::string)&name, 4);

这是 Man 类的构造函数调用 ,对应 C++ 中 new Man(...) 的第二步:在已经申请好的堆内存上,执行类的构造函数,完成对象的初始化工作。

上一行 operator new 只负责申请空白内存,这一行负责把这块空白内存填充成一个完整的 Man 类对象。


二、逐部分拆解

  1. Man::Man
    C++ 构造函数的命名格式:类名::构造函数名。构造函数是类的特殊成员函数,名字和类名完全相同,专门负责对象创建时的初始化工作。
  2. 第一个参数:v3
    这是 IDA 反编译的显式呈现,对应 C++ 的 this 指针
    所有类的成员函数(包括构造函数),底层都会把对象的地址当成第一个隐藏参数自动传入。这里的 v3 就是上一行 operator new 申请到的堆内存首地址,构造函数会在这个地址上完成对象初始化。
  3. 第二个参数:(std::string)&name
    构造函数的第一个业务参数,内容是之前创建好的字符串 "NISACTF"
    作用是初始化 Man 对象内部的字符串类型成员变量(比如对象的名字属性),(std::string) 是 IDA 加上的类型强转标识。
  4. 第三个参数:4
    构造函数的第二个业务参数,是一个整型数值。
    作用是初始化 Man 对象内部的整型成员变量(比如年龄、编号、等级这类属性)。
  • 先创建一个内容为 NISACTF 的字符串。
  • 申请堆内存,调用 Man 类的构造函数,传入两个参数:字符串 NISACTF 和数字 4,完成对象初始化。
  • 构造完成后,对象地址存入 m

std::string::~string(&name);

3. 核心:虚函数调用
复制代码
(*((void (__fastcall **)(Human *))m->_vptr_Human + 1))(m);

一、整体作用

这是 std::string 类的析构函数调用,和前面的字符串构造函数成对出现,负责字符串对象的销毁与资源释放,是 C++ 自动资源管理机制的标准体现,完全由编译器自动生成。


二、逐部分拆解

  1. ~string
    C++ 中析构函数的固定命名格式:~ 波浪号是析构函数的专属标识,函数名与类名一致。它的作用和构造函数完全相反:构造函数负责创建、初始化对象,析构函数负责清理、销毁对象。
  2. &name
    要销毁的目标字符串对象的地址,对应底层的 this 指针,告诉析构函数要清理哪一个字符串对象。

三、执行的核心操作

调用这个析构函数时,会自动释放 name 字符串内部申请的堆内存 ------ 也就是存放 "NISACTF" 实际内容的那块动态存储空间,把内存归还给系统,避免内存泄漏。

这是 C++ RAII(资源获取即初始化) 机制的典型体现:对象生命周期结束时,自动完成资源清理,不需要开发者手动调用 free

std::allocator::~allocator(&v7);

这是 std::allocator<char> 内存分配器的析构函数调用,和代码开头的分配器构造函数严格成对出现,负责销毁分配器对象本身,属于 C++ 标准库的底层生命周期管理代码,完全由编译器自动生成。


二、逐部分拆解

  1. ~allocator
    析构函数的标准命名格式:波浪号 ~ + 类名,作用与构造函数相反,用于在对象生命周期结束时执行清理工作。
  2. &v7
    待销毁的分配器对象地址,对应底层的 this 指针,指定要销毁哪一个分配器实例,也就是代码开头构造的 v7

三、实际执行的操作

这里需要区分两个概念:分配器对象本身分配器申请的内存

  • 这个析构函数只负责销毁 v7 这个分配器工具对象本身。默认的标准分配器是无状态的,它的析构函数几乎不做任何实际操作,没有内存释放动作。
  • 字符串实际内容占用的堆内存,已经在前面的 std::string::~string(&name) 中,通过分配器的 deallocate 接口释放完成了,不需要在分配器析构时再处理。

简单理解:分配器是一个 "申请内存的工具",这行只是把这个工具本身销毁,真正的字符串内存已经在之前被工具回收好了。

(*((void (__fastcall **)(Human *))m->_vptr_Human + 1))(m);

一、整体作用

这是整段代码的核心业务逻辑 ,也是这道 C++ 逆向题的核心考点:通过对象的虚函数表指针,调用虚表中的第二个虚函数。flag 的生成或输出逻辑,就藏在这个被调用的虚函数内部。


二、逐层拆解语法(从内到外)

我们从最内层的表达式开始,一步步拆解这行复杂的指针运算。

1. m->_vptr_Human:取出虚函数表指针
  • 含有虚函数的 C++ 对象,内存开头的 8 字节(64 位程序)是一个虚指针(vptr) ,它指向一张虚函数表(vtable)
  • 虚函数表本质是一个「函数指针数组」,数组里按顺序存放着该类所有虚函数的入口地址。
  • _vptr_Human 就是 Human 类对应的虚指针,这里 Man 继承自 Human,因此对象里沿用了这个虚指针,实际指向 Man 类自己的虚函数表。
2. (void (__fastcall **)(Human *)):强制类型转换

这是一段函数指针类型声明,作用是告诉编译器:这块内存要当成「函数指针数组的指针」来处理。

  • __fastcall:调用约定,64 位程序下默认使用,第一个参数通过 rcx 寄存器传递,这里正好对应 C++ 成员函数的 this 指针。
  • (Human *):函数的参数是一个 Human* 类型的指针,也就是对象的 this 指针。
  • **:二级指针,一级指向虚表的第一个函数指针,再解引用才是真正的函数入口地址。
3. + 1:偏移到第二个虚函数

虚函数表是按顺序排列的函数地址数组:

  • 不加偏移时,指向虚表第 0 项,也就是第一个虚函数(通常是析构函数)。
  • + 1 表示向后偏移 1 个函数指针的长度(64 位下是 8 字节),指向虚表第 1 项,也就是第二个虚函数
4. (* ... )(m):解引用并调用函数
  • 最外层的 *:对偏移后的地址解引用,取出对应虚函数的入口地址。
  • (m):调用这个函数,传入参数 m。这个参数就是成员函数的 this 指针,对应 C++ 里调用对象方法时自动传入的对象地址
  1. 虚指针与虚函数表 :C++ 中含有虚函数的类,每个对象的开头都会有一个虚指针(vptr) ,指向一张虚函数表(vtable)。虚函数表里按顺序存放着各个虚函数的入口地址。
  2. m->_vptr_Human:取出对象里的虚指针,指向虚表的第一个函数。
  3. + 1:向后偏移 1 个函数指针,取到虚表里的第二个虚函数
  4. 最后调用这个函数,参数 m 就是 C++ 的 this 指针。

为什么是第二个虚函数? 虚函数表按声明顺序排列虚函数地址,+1 即跳过第一个、指向第二个。若 Human 基类先声明 virtual void func1() 再声明 virtual void func2(),则 +1 取到的正是 func2。这也是本题 flag 输出逻辑所在函数。
this 指针的关系 :C++ 成员函数调用时,编译器会把对象地址作为隐藏参数传入。这里显式传入 m,等价于调用 m->func2(),只是 IDA 把虚调用还原成了最底层的指针运算形式。
实战提示 :在 IDA 中双击 m->_vptr_Human 可跳转到虚表,按 Ctrl+E 查看虚表项对应的函数地址,直接定位到 flag 输出函数,是解这类题的通用套路。#### 4. 对象析构与释放

复制代码
if ( m )
{
    Human::~Human(m);
    operator delete(v4);
}

调用完虚函数后,执行析构函数、释放堆内存,属于标准的 C++ 对象销毁流程,和 flag 逻辑无关。

双击 m->_vptr_Human

可以看到虚函数表里存放着两个函数指针,第一个是析构函数,第二个就是我们要找的 give_flag 函数。核心是 Human 类,双击 Human

进入 Human 类的结构视图后,可以看到它的虚函数表指针 off_4863D0,点击它

虚表里列出了两个函数:第一个是析构函数,第二个是 _ZN5Human9give_flagEv,也就是 Human::give_flag()。点击 _ZN5Human9give_flagEv

F5 查看伪码,可以看到 give_flag 函数的逻辑:它把一段密文逐字符与 0x0A 进行异或,得到真正的 flag。

点击 flag 变量,

得到异或后的密文,写解密脚本即可还原 flag。

f5查看伪码,说明每个字符与0x0A进行异或

点击flag

得到异或后的flag,写解密脚本

python 复制代码
cipher=[0x44, 0x59, 0x59, 0x49, 0x5E, 0x4C, 0x71, 0x7E, 0x62, 0x63,
  0x79, 0x55, 0x63, 0x79, 0x55, 0x44, 0x43, 0x59, 0x4B, 0x55,
  0x78, 0x6F, 0x55, 0x79, 0x63, 0x6D, 0x64, 0x77]
flag=""
for i in range(len(cipher)):
    temp=chr(cipher[i]^0x0A)
    flag+=temp

print(flag)

脚本逻辑很简单:give_flag 函数里对每个字符做了 ^ 0x0A 的异或运算,异或运算具有可逆性,所以只要对密文再异或一次 0x0A,就能还原出原始 flag。运行脚本即可得到最终答案。

相关推荐
小小代码狗4 小时前
Windows 权限提升技术详解
windows·网络安全
网安蟹佬霸5 小时前
Android安全攻防实战:从APK逆向到Frida动态Hook全流程详解(附脚本)
android·前端·安全·web安全·逆向·csrf·网安
YakProject6 小时前
如何让 AI Agent 跑得更快???
人工智能·测试工具·网络安全·agent·yakit·yaklang
网安蟹佬霸1 天前
蓝队防御实战:SIEM与SOAR构建安全运营中心(万字详解+实战配置)
android·安全·web安全·网络安全·网安
网安蟹佬霸1 天前
Zero Trust零信任架构实战:从架构设计到落地部署
安全·网络安全·ci/cd·架构·自动化·网安
拾光Ծ1 天前
【Linux网络】HTTPS如何保证数据安全?从对称加密、非对称加密到 CA 证书认证
计算机网络·网络安全·https·tls·加密算法
佳&弥1 天前
数据库提权
服务器·数据库·安全·web安全·网络安全
大方子1 天前
【PolarCTF】苦海
网络安全·polarctf
whcyhhh1 天前
CTF‑MISC 隐写术完整学习笔记|图片隐写全题型 + 工具 + 实战例题
python·网络安全·ctf·misc·信息隐藏