ida find
shitf+f12

双击上图“.rdata:0000000000404019 0000000F C you are right!”跳转到

上图ctrl+x或右键,跳转到引用,如下图

伪代码分析
f5转伪代码,同时右键把str2等转字符串。
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 Str2; // [rsp+20h] [rbp-60h]
int v5; // [rsp+28h] [rbp-58h]
__int16 v6; // [rsp+2Ch] [rbp-54h]
char v7; // [rsp+2Eh] [rbp-52h]
char Str1[1000]; // [rsp+410h] [rbp+390h]
int v9; // [rsp+7F8h] [rbp+778h]
int i; // [rsp+7FCh] [rbp+77Ch]
_main();
Str2 = '3r_ys43{';
v5 = 'sr3v';
v6 = '}3';
v7 = 0;
printf("please put your flag:");
scanf("%s", Str1);
for ( i = 0; i <= 665; ++i )
{
if ( Str1[i] == 101 )
Str1[i] = 51;
}
for ( i = 0; i <= 665; ++i )
{
if ( Str1[i] == 97 )
Str1[i] = 52;
}
v9 = strcmp(Str1, (const char *)&Str2);
if ( v9 )
printf("you are wrong,see again!");
else
printf("you are right!");
system("pause");
return 0;
}
我们来逐行分析这段IDA伪代码,并构造POC获取flag。
1. 代码分析:
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 Str2; // [rsp+20h] [rbp-60h]
int v5; // [rsp+28h] [rbp-58h]
__int16 v6; // [rsp+2Ch] [rbp-54h]
char v7; // [rsp+2Eh] [rbp-52h]
char Str1[1000]; // [rsp+410h] [rbp+390h]
int v9; // [rsp+7F8h] [rbp+778h]
int i; // [rsp+7FCh] [rbp+77Ch]
_main();
// 注意:这里是小端存储,实际字符串是反向的.x86架构是小端序,所以程序中的v5(int)和v6(short)在内存中是小端存储。
Str2 = '3r_ys43{';
v5 = 'sr3v';
v6 = '}3';
v7 = 0; //字符串结束标识
printf("please put your flag:");
scanf("%s", Str1);
for ( i = 0; i <= 665; ++i )
{
if ( Str1[i] == 101 ) // 101是字符'e'的ASCII
Str1[i] = 51; // 51是字符'3'的ASCII
}
for ( i = 0; i <= 665; ++i )
{
if ( Str1[i] == 97 ) // 97是字符'a'的ASCII
Str1[i] = 52; // 52是字符'4'的ASCII
}
v9 = strcmp(Str1, (const char *)&Str2);//str2是字符串,到'\0'才结束。
if ( v9 )
printf("you are wrong,see again!");
else
printf("you are right!");
system("pause");
return 0;
}
2. 关键逻辑:
- 程序首先初始化了一个字符串
Str2(实际上是一个字节数组,包括Str2、v5、v6、v7,它们共同组成一个完整的字符串)。 - 然后读取用户输入的字符串到
Str1。 - 对
Str1进行两次遍历:- 第一次:将所有字符'e'替换为'3'。
- 第二次:将所有字符'a'替换为'4'。
- 最后将处理后的
Str1与Str2(即目标字符串)比较,如果相同则输出正确。
因此,flag是经过替换后的字符串(即目标字符串Str2),但我们需要知道原始输入(即替换前的字符串)才是真正的flag。
3. 目标字符串Str2的还原:
注意代码中:
所以我们需要将这些值转换为字节序列(小端)并拼接起来。
-
Str2是__int64(8字节),值为'3r_ys43{'(注意:这是一个多字符常量,实际值是小端存储)。在C语言中,多字符常量(如
'3r_ys43{')的实际数值取决于字节顺序(小端)。但这里我们更关心的是内存中的字节序列:实际上,
Str2在内存中存储的字符串是"{34sy_r3"(因为小端,所以反向)。但注意:
Str2本身是8字节,但字符串可能不止8字节(因为后面还有v5和v6)。
实际上,整个目标字符串是&Str2开始的连续内存,包括:
Str2(8字节)v5(4字节)v6(2字节)v7(1字节,为0,即结束符)
所以我们需要将Str2、v5、v6的值转换为小端字节序列,然后拼接。
"{34sy_r3" + "v3rs" + "3}" + "\0"
即:"{34sy_r3v3rs3}"
4. 还原原始flag:
由于程序将输入中的'e'替换为'3'、'a'替换为'4',然后与目标字符串"{34sy_r3v3rs3}"比较。
所以,原始输入(即flag)应该是对目标字符串进行反向替换(即把'3'换回'e',把'4'换回'a')。
原始输入:flag{easy_reverse}
1. 为什么Str2、v5、v6、v7共同构成一个完整的字符串?
在C语言中,局部变量在栈内存中的布局是连续的(除非有对齐要求)。代码中:
__int64 Str2; // 8字节,地址假设为addr int v5; // 4字节,地址为addr+8 __int16 v6; // 2字节,地址为addr+12 char v7; // 1字节,地址为addr+14
这些变量在内存中是相邻的(没有填充,因为类型大小匹配偏移:8+4+2+1=15,没有对齐问题)。
程序使用:
v9 = strcmp(Str1, (const char *)&Str2);
这里&Str2是Str2的起始地址。strcmp会从该地址开始逐字节比较,直到遇到空字节(\0)为止。因此,它会读取:
Str2的8字节、- 然后
v5的4字节(因为紧挨着)、 - 然后
v6的2字节、 - 然后
v7的1字节(正好是0,即字符串结束符)。
所以,这些变量共同构成了一个以&Str2为起始、以v7(0)结尾的连续字符串。
为什么需要拼接?
因为strcmp比较的是从&Str2开始的内存,直到遇到\0。所以我们必须将这些变量的小端字节序列拼接起来,才能得到完整的字符串。
2. 为什么需要将值转换为小端字节序列并拼接?
计算机存储多字节数据(如int、short)时有字节序问题:
- 小端(Little-endian):低字节存储在低地址,高字节存储在高地址。
- 大端(Big-endian):高字节存储在低地址,低字节存储在高地址。
x86架构是小端序,所以程序中的v5(int)和v6(short)在内存中是小端存储。
总结:
- 变量在内存中连续布局,
&Str2指向的字符串包括Str2、v5、v6、v7。 - 由于小端存储,必须将数值转换为其字节序列(低字节在前)才能得到正确的字符串。
flag
flag{easy_reverse}
