CTF Show逆向4reserve wp--mingyue
(本题难度较大,分析起来比较复杂,针对该题本文对其中比较重要的部分做了较详细的概述,有问题的地方,请指正)
第一步 查壳。本题为64位
第二步 各部分函数分析
(一)
分析main函数。
函数签名
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
- int __cdecl __noreturn:这表明这是一个main函数,使用__cdecl调用约定,且带有__noreturn修饰符,这表示该函数永远不会返回。这个修饰符通常用于那些程序会在函数结束时终止进程的情况下,比如通过调用exit()或是进入一个无限循环。
- main(int argc, const char **argv, const char **envp):这是标准的main函数签名,其中:
- argc是命令行参数的数量。
- argv是指向参数字符串的指针数组。
- envp是指向环境变量字符串的指针数组。
变量定义
qword_140004618 = (__int64)malloc(0x10ui64);
qword_140004620 = qword_140004618;
- qword_140004618 和 qword_140004620 是两个全局变量,可能是64位整型的变量(从名字中的qword可以推测)。具体来看:
- qword_140004618 = (__int64)malloc(0x10ui64);:调用malloc分配了16字节(0x10ui64)的内存,并将其地址转换为__int64类型,存储在qword_140004618中。
- qword_140004620 = qword_140004618;:将qword_140004618的值(即刚分配的内存地址)复制到qword_140004620。
内存操作
*(_QWORD *)(qword_140004618 + 8) = 0i64;
- *(_QWORD *)(qword_140004618 + 8) = 0i64;:这是一个指针操作,具体做了以下事情:
- qword_140004618 + 8计算了刚刚分配内存块的第8个字节位置。
- _QWORD代表64位的整数类型,*(...)表示解引用。
- 这行代码把0存储在内存块的第8到第15字节位置。
也就是说,分配的16字节内存中,第8个字节到第15个字节的位置被设置为0。
字符串输出与输入
sub_140001020("请输入正确的数字:\n");
sub_140001080("%lld");
- sub_140001020("请输入正确的数字:\n");:调用函数sub_140001020,参数是一个字符串 "请输入正确的数字:\n",这个函数可能是用于输出提示信息的,比如让用户输入数字。
- sub_140001080("%lld");:调用另一个函数sub_140001080,参数为格式化字符串"%lld",这个函数可能是用于接收输入的。"%lld"是用于读取长长整型(long long)的格式说明符。
函数调用
((void (__fastcall *)())sub_1400010E0)();
- ((void (__fastcall *)())sub_1400010E0)();:这是一个类型转换和调用,分两部分:
- ((void (__fastcall *)())sub_1400010E0)将sub_1400010E0函数指针转换为一个使用__fastcall调用约定的函数指针,该函数不带参数且返回类型为void。
- ()紧接在这个类型转换后的指针上,表示立即调用这个函数。
小结一下:
- 分配了一段16字节的内存,将其地址保存到两个全局变量qword_140004618和qword_140004620中。
- 将分配内存的第8到第15字节位置设置为0。
- 输出提示信息要求用户输入一个数字。
- 可能通过格式化字符串读取用户输入。
- 最后调用了一个sub_1400010E0的函数指针
(二)
分析sub_140001020
函数签名:
int sub_140001020(char *Format, ...)
这个函数名为 sub_140001020,返回类型是 int,接受一个字符指针(char *Format)和可变参数(...)。函数中的 Format 通常用于格式化输出。
局部变量:
FILE *v2; // rbx
unsigned __int64 *v3; // rax
va_list va; // [rsp+58h] [rbp+10h] BYREF
- v2 是一个指向 FILE 类型的指针,保存在寄存器 rbx 中。
- v3 是一个指向 unsigned __int64 类型的指针,保存在寄存器 rax 中。
- va 是一个 va_list 类型的变量,用来处理可变参数列表。
可变参数处理:
va_start(va, Format);
- va_start 宏初始化 va_list 变量,准备访问可变参数列表。这里的 va 变量将会存储传递给函数的可变参数信息。
获取标准输出流:
v2 = _acrt_iob_func(1u);
- _acrt_iob_func 是一个微软C运行时库(CRT)中的内部函数,通常用于获取标准输入输出流。
- _acrt_iob_func(1u) 返回标准输出流 stdout,即 v2 指向标准输出的 FILE 对象。
调用另一个函数:
v3 = (unsigned __int64 *)sub_140001000();
- sub_140001000 是另一个函数,被调用后返回一个 unsigned __int64 * 类型的指针,并赋值给 v3。这个指针可能指向某种控制结构或特定数据。
执行格式化输出:
return _stdio_common_vfprintf(*v3, v2, Format, 0i64, va);
- _stdio_common_vfprintf 是一个低级别的格式化输出函数,它负责处理带有可变参数的格式化输出。
- 这个函数的参数分别为:
- *v3: 通过解引用 v3 得到的值,这个值可能代表某种标志或格式化选项。
- v2: 指向标准输出的 FILE 对象。
- Format: 字符串格式,用于定义如何格式化输出。
- 0i64: 通常是一个与选项标志相关的值,这里指定为0。
- va: 处理后的可变参数列表。
- 函数的返回值是 _stdio_common_vfprintf 的返回值,即该函数成功写入的字符数,或者是遇到错误时的负值。
(三)
分析sub_140001080
- 函数声明:
int sub_140001080(char *Format, ...)
- 这是一个函数定义,返回类型为int,接受一个格式化字符串Format作为第一个参数,以及一个可变参数列表。
- 局部变量声明:
FILE *v2; // rbx
_QWORD *v3; // rax
va_list va; // [rsp+58h] [rbp+10h] BYREF
- v2 是一个指向 FILE 结构的指针(通常用于表示文件流)。
- v3 是一个 _QWORD 类型的指针,通常 _QWORD 是一个 64 位整数类型,用于存储 8 字节的数据。
- va_list va 是一个用于存储可变参数列表的对象。
3.初始化可变参数列表:
va_start(va, Format);
- 使用 va_start 宏初始化 va_list 对象 va,并使其指向 Format 之后的第一个可变参数。
4.获取标准输入流:
v2 = _acrt_iob_func(0);
- _acrt_iob_func(0) 是一个获取标准输入流 stdin 的函数,返回一个指向 FILE 结构的指针,并赋值给 v2。
5.调用非标准库函数:
v3 = (_QWORD *)sub_140001010();
- sub_140001010 是一个自定义的函数,返回一个 _QWORD 类型的指针,并将其结果赋值给 v3。该函数的具体实现未知,但返回值的使用表明它可能与格式化输入处理相关。
6.调用标准库函数:
return _stdio_common_vfscanf(*v3 | 1i64, v2, Format, 0i64, va);
- _stdio_common_vfscanf 是一个底层标准输入函数,用于根据提供的 Format 格式从 v2(通常是 stdin)中读取输入,并将结果存储到与可变参数 va 对应的变量中。
- *v3 | 1i64 代表对返回的 _QWORD 指针进行位或操作,可能是设置某个标志位(具体用途取决于 sub_140001010 的实现)。
(四)
进入sub_1400010E0(重点)
1.变量初始化和赋值:
v2 = 0;
v3 = (__int64)a1;
v2被初始化为0,v3被赋值为a1的64位整型值。
2.条件判断和循环:
if ( a1 )
{
v4 = &v9;
do
{
++v4;
++v2;
a1 = &a4890572163qwe[-26 * (v3 / 26)];
v5 = a1[v3];
v3 /= 26i64;
a2 = v3;
*(v4 - 1) = v5;
}
while ( v3 );
}
在a1不为空的情况下执行。它初始化v4为v9的地址,然后进入一个do-while循环:
- ++v4;:v4指针递增,指向下一个元素。
- ++v2;:v2计数器递增。
- a1 = &a4890572163qwe[-26 * (v3 / 26)];:这里有一个数组a4890572163qwe,a1被重新赋值为该数组的一个元素地址,该元素的索引是通过v3除以26并乘以-26计算得到的。
- v5 = a1[v3];:v5被赋值为a1指针所指向的数组元素的值。
- v3 /= 26i64;:v3整除26。
- a2 = v3;:将v3的值赋给a2。
- *(v4 - 1) = v5;:将v5的值赋给v4指针前一个位置的元素。
循环继续执行,直到v3为0。
3.第二个循环:
v6 = v2;
while ( v6 )
{
v7 = *(&v8 + v6--);
sub_1400011E0(v7 ^ 7u, a2, v3);
}
这里,v6被赋值为v2,然后进入一个while循环:
- v7 = *(&v8 + v6--);:v7被赋值为v8偏移v6个位置的元素的值,然后v6递减。
- sub_1400011E0(v7 ^ 7u, a2, v3);:调用一个名为sub_1400011E0的函数,传入参数为v7与7进行异或操作的结果、a2和v3。
循环继续执行,直到v6为0。
4.函数调用:
sub_140001220(a1, a2, v3);
最后,调用一个名为sub_140001220的函数,传入参数为a1、a2和v3。
(五)
进入sub_140001220(重点)
- 变量初始化:
v0 = qword_140004620;
v1 = 0;
v2 = 0i64;
v0被初始化为全局变量qword_140004620的值,v1和v2分别被初始化为0。
2.无限循环:
while ( 1 )
{
这里是一个无限循环,它将一直执行直到遇到break语句。
3.循环体内的操作:
v3 = *(_BYTE *)v0;
v4 = v1 + 1;
v5 = *(_QWORD *)(v0 + 8);
- v3被赋值为v0指向的字节。
- v4被赋值为v1加1。
- v5被赋值为v0加8地址处的64位值。
4.条件判断和赋值:
if ( v3 != aV4pY59[v2 - 1] )
v4 = v1;
qword_140004620 = v5;
if ( !v5 )
break;
- 如果v3不等于数组aV4pY59中v2-1索引处的值,则v4被重新赋值为v1。
- 更新全局变量qword_140004620为v5的值。
- 如果v5为0,则退出循环。
5.更多的循环体内操作:
v6 = *(_BYTE *)v5;
v7 = v4 + 1;
v0 = *(_QWORD *)(v5 + 8);
if ( v6 != aV4pY59[v2] )
v7 = v4;
qword_140004620 = v0;
- v6被赋值为v5指向的字节。
- v7被赋值为v4加1。
- v0被赋值为v5加8地址处的64位值。
- 如果v6不等于数组aV4pY59中v2索引处的值,则v7被重新赋值为v4。
- 更新全局变量qword_140004620为v0的值。
6.更新循环变量:
if ( v0 )
{
v2 += 2i64;
v1 = v7;
if ( v2 < 14 )
continue;
}
- 如果v0不为0,则v2增加2,v1被赋值为v7。
- 如果v2小于14,则跳过后续代码,继续下一次循环。
7.无条件跳转:
goto LABEL_11;
如果v0为0或v2等于或大于14,则跳转到标签LABEL_11。
8.循环结束后的操作:
v7 = v4;
LABEL_11:
if ( v7 == 14 )
sub_1400012E0();
sub_1400012B0();
- 循环结束后,v7被赋值为v4。
- 如果v7等于14,则调用函数sub_1400012E0。
- 不论条件如何,都会调用函数sub_1400012B0。
以上两个数组如下
第三步 编写脚本
分析一下脚本:
1. 变量定义
a4890572163qwe = ")(*&^%489$!057@#><:2163qwe"
aV4pY59 = "/..v4p$!>Y59-"
2. equal 函数
def equal(a, str1):
for i in range(0, 26):
if str1[i] == a:
return i
return -1
- equal 函数的作用是:在 str1 的前 26 个字符中找到字符 a 的位置(索引)。如果找到了,就返回这个索引;如果没找到,则返回 -1。
- 注意,这里假设 str1 至少有 26 个字符,否则可能会引发越界错误。
3. 主体逻辑分析
flag = 0
newarray = []
for i in range(14):
flag *= 26
if i < len(aV4pY59):
index = equal(chr(ord(aV4pY59[i]) ^ 7), a4890572163qwe)
newarray.append(index)
flag += index
else:
break
- 初始化 flag 为 0 和 newarray 为空列表。
- 进入一个循环,循环次数为 14 次。
循环体内的操作:
- 每次循环开始时,flag 被乘以 26,这意味着 flag 正在逐步扩展为一个 26 进制数。
- 接下来,通过判断 i < len(aV4pY59),确保不会超出 aV4pY59 的长度。
- 通过 ord(aV4pY59[i]) ^ 7 对 aV4pY59 中第 i 个字符执行异或操作,得到一个新的字符,然后用 chr() 将其转化为对应的字符。
- 使用 equal 函数查找这个新字符在 a4890572163qwe 中的位置(索引),并将结果保存到 index 中。
- 将 index 添加到 newarray 中,同时将 index 累加到 flag 上。
- 如果 i 达到了 aV4pY59 的长度,则退出循环。
验证成功
flag{2484524302484524302}