记录一次来自BUUCTF平台reverse系列

进入平台点击该题目,首先下载题目中给的zip压缩包并打开,然后解压

之后拖到IDA中寻找main_0函数,发现有类似的flag值

但是这次可不是明面上获得的答案,不信你可以提交以下试试?

这个时候需要按f5来查看程序内容

我们来做个分析

函数基础信息:

int __fastcall main_0(int argc, const char **argv, const char **envp)  /**函数签名**/

__fastcall:调用约定,前两个参数(argc、argv)会通过寄存器(RCX、RDX)传递,第三个参数(envp)通过 R8 传递(x64 架构);这是程序的主逻辑函数,负责 flag 验证


分析1.局部变量定义

char *v3;        // rdi           /***字符指针,关联rdi寄存器(x64函数调用的第一个参数寄存器)
__int64 i;       // rcx                             /***64位整数,关联rcx寄存器(循环计数器)
size_t v5;       // rax                        /***无符号整数,关联rax寄存器(存储字符串长度)
char v7;       // [rsp+0h] [rbp-20h] BYREF       /***栈上的字符变量,作为内存初始化的起始地址
int j;          // [rsp+24h] [rbp+4h]                     /***32位整数,Str2替换循环的计数器
char Str1[224]; // [rsp+48h] [rbp+28h] BYREF          /***存储用户输入的flag(224字节缓冲区)
__int64 v10;    // [rsp+128h] [rbp+108h]                /***临时变量,存储j的值(无实际作用)

开头先定义了一系列局部变量,这些变量会分配在栈上(RBP 为栈基址)

  • 关键:BYREF 表示 “按引用”,是逆向工具(如 IDA)的标注,实际就是栈上的普通变量;
  • 栈偏移(如[rsp+0h] [rbp-20h]):表示变量在栈上的位置,无需深究,知道是栈内存即可。

分析 2:第一个循环(栈内存初始化,无实际业务意义)

这是编译器自动生成的 “栈清零” 代码,和 flag 验证无关,逐行拆解:

v3 = &v7;                  // 步骤2.1:初始化指针v3,指向栈变量v7的地址(内存初始化的起始位置)
for ( i = 82; i; --i )     // 步骤2.2:循环初始化
{
  *(_DWORD *)v3 = -858993460;   // 步骤2.3:给v3指向的内存赋值
  v3 += 4;                      // 步骤2.4:v3指针向后移动4字节(因为赋值的是4字节DWORD)
}
  • 循环次数:i = 82 → 循环 82 次;
  • 赋值数值:-858993460 的十六进制是 0xCCCCCCCC,这是 MSVC 编译器对未初始化栈变量的默认填充值(表示 “未使用的栈内存”);
  • 执行效果:从 v7 的地址开始,连续填充 82×4=328 字节的 0xCCCCCCCC,仅为了内存安全,和 flag 无关。

分析3:第二个循环(核心部分,Str2 的字符替换)

这是决定 flag 的关键逻辑,逐行拆解执行流程:

for ( j = 0; ; ++j )            // 步骤3.1:初始化计数器j=0,无终止条件(靠内部break退出)
{
  v10 = j;                      // 步骤3.2:临时存储j的值(逆向残留代码,无实际作用)
  if ( j > j_strlen(Str2) )     // 步骤3.3:判断是否遍历完Str2
    break;                      // 满足则退出循环
  if ( Str2[j] == 111 )         // 步骤3.4:判断Str2第j个字符是否是'o'
    Str2[j] = 48;               // 步骤3.5:若是,替换为数字'0'
}
  • j_strlen(Str2):自定义的字符串长度函数(和标准strlen功能完全一致),返回Str2的字符数(不含结束符\0);
  • 循环终止条件:j > j_strlen(Str2) → 比如Str2长度是 10,j 从 0 到 10 时触发 break(j=10 时Str2[10]是结束符\0,不会触发替换);
  • ASCII 码对应                                             

                1.111 → 字符 'o'(小写字母 o);   

                2.48 → 字符 '0'(数字零);

  • 核心效果:遍历预设字符串Str2的每一个字符,把所有的'o'替换成'0'(比如Str2"abc_o123o",替换后变成"abc_01230")。
  • 循环次数:i = 82 → 循环 82 次;
  • 赋值数值:-858993460 的十六进制是 0xCCCCCCCC,这是 MSVC 编译器对未初始化栈变量的默认填充值(表示 “未使用的栈内存”);
  • 执行效果:从 v7 的地址开始,连续填充 82×4=328 字节的 0xCCCCCCCC,仅为了内存安全,和 flag 无关。

分析4:提示输入 + 读取用户输入

调用两个自定义函数,实现 “打印提示 + 读取输入”:

sub_1400111D1("input the flag:");  // 步骤4.1:打印提示语
sub_14001128F("%20s", Str1);       // 步骤4.2:读取用户输入到Str1
  • sub_1400111D1:对应标准库的printf/puts,作用是打印字符串;
  • sub_14001128F:对应标准库的scanf,作用是读取用户输入;
  • 格式符%20s:限制输入最多 20 个字符(超出会截断),输入存储到Str1缓冲区。

分析5:对比输入与替换后的 Str2,输出结果

这是验证 flag 的核心判断逻辑:

v5 = j_strlen(Str2);  // 步骤5.1:获取替换后Str2的长度(记为v5)
if ( !strncmp(Str1, Str2, v5) )  // 步骤5.2:对比两个字符串
  sub_1400111D1("this is the right flag!\n");  // 匹配则输出正确
else
  sub_1400111D1("wrong flag\n");  // 不匹配则输出错误

关键细节:

  • strncmp(Str1, Str2, v5)
  1. 功能:对比Str1Str2的前v5个字符;
  2. 返回值:完全一致返回 0,不一致返回非 0;
  3. !strncmp(...):即判断返回值是否为 0(是否完全一致);
  • 验证规则:用户输入的 Str1,必须和替换后的 Str2 的前 v5 个字符完全一致,才是正确 flag。

分析6:函数返回
return 0;  // 程序正常退出,返回值0

答案结果为flag{hell0_w0rld}


小知识:点击数字之后按R键可以转换后的字符

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐