2023SharkCTF-week1-Reverse-XOR详解
2023SharkCTF-week1-Reverse-XOR详解
2023.8.15 by Shen_Fan
经过第一周的学习,相信大家对逆向工程都有了一个初步的了解。考虑到很多人都是还没入学的新生,这篇wp将更多地聚焦于工具的使用与代码的阅读而非flag如何获取。
由于时间有限,只能针对有人提出的XOR进行详解,若有其他问题欢迎来群里私戳我。
XOR
0b0程序逻辑详解
原本是第二周的签到题,由于一些失误导致被放到了第一周(不然第一周都是不用写脚本的简单题)。现在,我将逐行为各位讲解ida反汇编得到的类c代码
首先,把XOR.exe拖到ida64.exe中,解析完成后按下F5查看类c代码。我们从第一行开始看起
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
一开始是main函数的定义,第一个字符串”int”代表着该函数的返回值为int,第二个字符串”__cdecl”定义如下:
__cdecl是C Declaration的缩写(declaration,声明),cdecl调用方式又称为C调用方式,是C语言缺省的调用方式。
他意味着:
(1)参数从右向左依次压入堆栈.
(2)由调用者恢复堆栈,称为手动清栈。
(3)函数名自动加前导下划线。
缺省意味着该函数的某些参数可以留空,所以我们在编写程序时可以直接以int main(void) {}定义main函数
第三个字符串main代表着该函数名为main,也是一般c/c++程序中的入口函数(实际上并不是最先执行的函数,在第二周的某些题中就可看到[狗头])。
接下来的括号里则是main函数的三个参数,对于main函数,这些参数来自命令行和运行环境,有兴趣可以自行搜索了解。
下一行的花括号代表从这里开始到一个’}’是main函数的定义部分,就是这个函数里有啥,干了啥。
1 | char Str[48]; // [rsp+20h] [rbp-60h] BYREF |
接下来是若干行变量的声明。ida所反汇编得到的代码通常会在最开始将所有变量都进行声明。例如第一行声明了一个名为”Str”的变量,他是char类型,长为48的数组。之后的注释[rsp+xxh]和[rbp+xxh]则是代表该变量在栈中的位置。对于Reverse方向的初学者,只需要知道rbp和rsp中分别保存了栈在内存中的起始位置和结束位置,而后面的注释表明了这个变量相对这两个地址的位置。栈通常用来保存局部变量,也就是只在这个函数中使用的变量。
1 | _main(argc, argv, envp); |
用于初始化某些东西的函数,是由编译器生成的函数。re的一个难点便在于分辨人为书写的函数与库函数和编译器生成的函数。
1 | qmemcpy(v6, "qkdzfvVQ\"", 9); |
qmemcpy和memcpy的作用相同,都是从第二个参数复制第三个参数个字符到第一个参数所代表的地址。在这里,就是将字符串qkdzfvVQ”(由于字符串内不能直接出现”,会与字符串的头和尾混淆,所以使用转义符\配上”来表面字符串中含有双引号)这个长度为9的字符串复制到v6,也就是数组的前9个元素。之后的一系列赋值为v6的第10到28个元素赋值。
顺带一提,对于数组,数组名代表这个数组的头的位置,数组名[i]代表取数组中第i+1个元素的值。数组中的第一个元素是数组名[0]
1 | memset(Str, 0, sizeof(Str)); |
memset是memory set的缩写,和他的名字一样,他能够从内存中的某个位置开始,将之后的n个字节设置为指定值。在这里,他将Str数组的全部元素都设置为0。然后,程序将v5设置为0。
1 | printf("input your flag:"); |
printf打印了之后的信息,scanf前面的format代表其读入一个长度最长为40的字符串。之后的参数Str(正如之前所说,数组名代表了数组头的地址)代表了要读入到哪。
1 | v10 = 1; |
为v10和v9赋值捏。
1 | if ( strlen(Str) == 28 ) |
if和它的字面意思相同,代表着”如果“,这里的strlen函数会读入一个字符串所在的地址,返回其长度。把这两个元素结合起来,这段代码的意思便是”如果Str字符串的长度为28则“。之后的’{‘代表着代码块的开始,直到和他对应的’}’出现前的内容都是条件成立后要执行的内容。
1 | for ( i = 0; i <= 27; ++i ) |
for循环要求三个以;隔开的参数,第一个为初始化,在这里是将变量i的值初始化为0;第二个为循环条件,在这里为当i<=27时继续循环;第三个为每一轮循环结束后要执行的代码,在这里的++表示自增,在这里等价于i=i+1。
所以在这个循环中,i的值由0一路递增至27,并在i=27的循环结束后自增到28终止循环
之后的’{‘代表着一个代码块的起始,这个代码块是每一轮循环都要执行的。(代码块嵌套捏)
首先,^=是一种简写,Str[i] ^= v10 + v9也可以写成 Str[i] = Str[i] ^ (v10 + v9)。^在c中代表着异或操作,这里便是将Str数组中的第i个元素与v10+v9的和进行异或并赋值回去。
之后的v9+=v10;v10=v9-v10便是对这两个变量所作的操作:先将v9加上v10的值并赋值给v9,再将v9-v10的值赋值给v10。两个变量在每次循环开始时的值如下表所示。
i | v9 | v10 |
---|---|---|
0 | 1 | 1 |
1 | 2 | 1 |
2 | 3 | 2 |
3 | 5 | 3 |
4 | 8 | 5 |
看呐,斐波那契数列[狗头]
1 | for ( j = 0; j <= 27; ++j ) |
之后的for循环中初始化了一个变量j,j的值也是从0一路递增至27,并在28时退出循环。底下的if比较了Str数组和v6数组的每一个元素是否不相等(!=符号代表不相等),若不相等,就输出flag错误。system(“pause”);时一个用于暂停程序的函数,防止命令行一闪而过直接退出看不清输出。之后的return 0则是一个函数返回其返回值。在这里,就算代表着main函数终止并返回0。从这段逻辑可以看出,Str作操作后应该和v6一模一样,不然程序就会提示你输错了。
之后若循环顺利完成并因为j的值而终止,程序便会输出你的flag正确,然后暂停退出一条龙。
最后的一个’}’终止最开始那个if下的代码块,
1 | else |
还记得之前那个对长度进行判断的if不,这是他的else。对于一个if-else语句当条件不成立时,程序便会跳到else部分执行。很合理(确信)。如果没有else那就直接忽略if下条件成立的代码继续往后执行。
1 | } |
main函数的定义结束标记。和之前main后的那个’{‘相匹配。
0b1所以要怎么写呢?
上面的程序逻辑总结起来就是一句话:读入一个字符串,若长度正确,在对字符串进行xor后判断其结果和v6是否相等。由于xor的对称性质,我们可以将v6进行同等的xor得到应该输入的字符串。因此可以考虑直接将代码复制出来,只把输入由Str改为v6。
脚本如下:
1 |
|
得到flag: sharkctf{x0r_1s_1nt3rest1ng}