PS:以下题目均在BUUCTF
1、easyre(学会简单使用逆向工具)
题目

做法
先自行下载Exeinfo PE这个软件
下载压缩包,解压,把解压后的文件拖进Exeinfo PE进行分析

64位
把文件进IDA(64位)
找到main,双击
按F5反编译成c语言

flag就这样水灵灵地出现了,去提交就OK了
2、reverse1(学会找信息、代码分析)
题目

做法
下载压缩包,解压,把解压后的文件拖进Exeinfo PE进行分析

64位,无壳
扔进IDA(64位),找main函数
但是我们并没有发现main函数,那我们先按Shift+F12打开string窗口,一键找出所有的字符串,去寻找它的后门函数

我们从上往下看一遍,发现有些字符串带有flag,因为我们最后交的东西也是flag,所以我们重点关注一下
这里有三个带有flag的:
第2行的wrong flag
第4行的this is the right flag
第7行的input the flag
翻译一下就显而易见了
我们双击this is the right flag

然后直接按Ctrl+x

确定

然后按F5反编译成c语言

进入到主函数(一般是main,一般我们直接找到main,然后双击,再F5反汇编成c语言就可以了),分析代码
因为逆向需要猜像sub_1400111D1这种的实际表达意义是啥,因此我们从我们点进来的this is the right flag这行代码开始往上看(当然,也可以直接从最上面分析到最下面)
sub_1400111D1("this is the right flag!\n");
直观地可以看出sub_1400111D1表示的是print(经验之谈,一般这种有“”且是绿色的都是程序打印给你的东西)
if ( !strncmp(&Str1, Str2, v3) )
比较两个字符串的前 v3 个字符是否相同。如果相同,条件表达式的结果为真,程序会执行 if 语句块内的代码
到这里,我们知道了它的成立条件,我们直接去找str1和str2
往上继续看
sub_14001128F("%20s", &Str1);
我们不难猜出sub_14001128F表示的是类似于 scanf 的函数,表示用户的输入会存放到&str1
因此,我们的输入值要等于str2,就可以得到flag
继续往上看,找到有str2或者跟它有关联的东西
for ( j = 0; ; ++j )
{
v8 = j;
v2 = j_strlen(Str2);
if ( v8 > v2 )
break;
if ( Str2[j] == 111 )
Str2[j] = 48;
}
我们注意到这里有关于str2的东西,这是一个for循环
执行顺序总结
- 初始化
j = 0。 - 把
j的值赋给v8。 - 调用
j_strlen函数计算Str2的长度并存储在v2中。 - 检查
v8 > v2是否成立,若成立则跳出循环;若不成立则继续。 - 检查
Str2[j]是否等于 111,若相等则将其替换为 48。 j自增 1。- 重复步骤 2 – 6,直至满足循环终止条件。
了解大概后,补充一个小知识点——我们可以对111,48这些数字单击后按r将数字转换为字符,eg.
if ( Str2[j] == 'o' )
Str2[j] = '0';
分析完这段代码,再往上也没有关于str2的信息了,我们尝试双击一下str2看看会不会有啥惊喜

诶,我们看到一个{}括起来的字符串,有点像我们要提交的flag格式,然后想到上面要把o换成0
我们把它复制一下,把o换成0,然后再最前面加上flag
回到题目提交看看

Yes,成功解出!给自己的进步点个小赞吧!
3、reverse2(学会找信息、代码分析)
题目

做法
下载压缩包,解压,把解压后的文件拖进Exeinfo PE进行分析

64位,无壳
扔进IDA(64位),找到main,F5反编译

老规矩,从得出flag那里开始往上看
if ( !strcmp(&flag, &s2) )
result = puts("this is the right flag!");
如果&flag里的内容等于&s2里的内容,我们的flag就是对的
继续往上
printf("input the flag:", argv);
__isoc99_scanf("%20s", &s2);
不难发现,s2里的内容是用户输入的内容
for ( i = 0; i <= strlen(&flag); ++i )
{
if ( *(&flag + i) == 105 || *(&flag + i) == 114 )
*(&flag + i) = 49;
}
一个for循环,里面有关于&flag的内容
- 将循环变量
i赋值为 0 。 - 每次循环时,调用
strlen函数计算flag字符数组的长度(&flag传递的是数组地址) 。 - 检查
i是否小于等于计算得到的字符串长度,若成立则进入循环体,否则结束循环。 - 在循环体中,通过指针运算
*(&flag + i)访问flag数组中索引为i的字符,判断该字符的 ASCII 码值是否为 105(字符'i')或者 114(字符'r')。若满足条件,则将该字符替换为 ASCII 码值为 49 的字符(字符'1')。 - 循环体执行完毕后,将
i的值自增 1,回到步骤 3 继续判断循环条件,重复上述过程直至循环结束。
看到数字,我们先按r把它变成字符
if ( *(&flag + i) == 'i' || *(&flag + i) == 'r' )
*(&flag + i) = '1';
要把i/r转换成1
然后我们双击flag进去看看有没有东西

有个疑似flag格式的字符串
复制下来,补全格式,然后把i/r转换成1提交看看

成功解出
4、内涵的软件(眼神好+小经验)
题目

做法
下载文件,拖进Exeinfo PE进行分析

32位,无壳
扔进IDA(32位),找到main,F5反编译

一眼看到第9行{}括起来的内容,有点像flag的格式,复制下来,括号前加flag,返回题目试试

哦吼,成功了
5、新年快乐(upx脱壳)
题目

做法
下载压缩包,解压,把解压后的文件拖进Exeinfo PE进行分析

32位,有upx壳,需脱壳(自行下载upx脱壳工具)
返回桌面搜索cmd,以管理员身份运行命令提示符

cd命令切换到你下载的upx脱壳工具(总的,不是某一个文件)存放地址
然后输入代码
upx -d 要脱壳的文件地址

脱壳成功
再把文件拖入Exeinfo PE

已经没有壳了
这时再扔进IDA(32位),找到main,F5反编译(注:这里有俩main,对比一下就很清楚地知道该分析哪个)


那么好,现在我们开始从关键点从上往下进行分析
if ( !strncmp((const char *)&v5, &v4, strlen(&v4)) )
result = puts("this is true flag!");
如果v5 的前 strlen(&v4) 个字符和 v4 相同,就会输出 "this is true flag!"
然后再往上看
scanf("%s", &v5);
v5是用户的输入内容储存地址
因此,我们需要找到v4的内容是什么
strcpy(&v4, "HappyNewYear!");
v5 = 0;
memset(&v6, 0, 0x1Eu);
继续往上看,关键内容就这些了
复制HappyNewYear!到v4…
诶,v4这不找到了嘛
结合上面的分析,我们把v4的内容打包一下就是这题的flag啦

6、xor(异或脚本编写)
题目

做法
下载压缩包,解压,把解压后的文件拖进Exeinfo PE进行分析
(注:当我们进行附件解压时会出现MACOX这一个文件夹,这个是属于mac端解压下来的垃圾文件,不必理会,你拖进Exeinfo PE也不会有什么信息给你)

64位,无壳
扔进IDA(64位),找到main,F5反编译

没有什么像flag这样的直接关键词,我们直接从上往下分析一下代码
上面都是一些无关痛痒的东西,简单会看懂就行,我们看到这里
for ( i = 1; i < 33; ++i )
v6[i] ^= v6[i - 1];
代码功能:
这段代码是一个 for 循环,其功能是对数组 v6 进行按位异或(XOR)操作。
具体步骤如下:
- 循环从索引
i = 1开始,到i < 33结束。也就是说,循环会对数组v6中索引从 1 到 32 的元素进行处理。 - 在每次循环中,使用按位异或运算符
^将当前元素v6[i]与前一个元素v6[i - 1]进行异或操作,然后将结果重新赋值给v6[i]。
然后继续往下
if ( !strncmp(v6, global, 0x21uLL) )
printf("Success", v3);
比较数组 v6 和 global 的前 0x21uLL(十六进制,转换为十进制是 33)个字符。
若这前 33 个字符完全相同,就会输出 "Success"。
结合全文进行分析,v3=global,我们的输入内容存放地址为v6
即我们输入内容和v3相等,就返回”成功”
先提取v3(global)的值,点v3点不进去,我们双击global

再双击红框圈起的地方

按Shift+E导出数据

根据global的值,再结合上面的for循环,我们就可以编写一个异或脚本,反向推出这道题的flag
(输入一个值进行异或操作后,再对异或结果使用相同的密钥进行一次异或操作,得到的是最初输入的值,也就是我们原本输入的还没有进行过异或操作的值)
exp(一般我们写脚本都是用Python写的,可以自行下载PyCharm等进行编写)
list1 = [0x66, 0x0A, 0x6B, 0x0C, 0x77, 0x26, 0x4F, 0x2E, 0x40, 0x11,
0x78, 0x0D, 0x5A, 0x3B, 0x55, 0x11, 0x70, 0x19, 0x46, 0x1F,
0x76, 0x22, 0x4D, 0x23, 0x44, 0x0E, 0x67, 0x06, 0x68, 0x0F,
0x47, 0x32, 0x4F, 0x00]
flag = chr(list1[0]) #结果为f 因为异或不会处理数据中第一个的值,但它也是flag的一部分,保留
#chr的作用是将一个整数(代表 Unicode 码位)转换为对应的 Unicode 字符
#我们的flag通常不是数字,而是字符串形式,这样做的目的是构建出一个字符串形式的 flag,下同
# 使用 for 循环从第二个元素开始进行异或操作
for i in range(1, len(list1)):
flag += chr(list1[i] ^ list1[i - 1]) #从输入的第二个数据开始,将其与前一位异或
print(flag)

运行,得出结果,去掉最后的0就是我们这道题的答案啦
7、reverse3(Base加密)
题目

做法
下载压缩包,解压,把解压后的文件拖进Exeinfo PE进行分析

32位,无壳
扔进IDA(32位),找到main,F5反编译

只是因为在人群中多看了你一眼——第31行的right flag,关键词找到,我们就从这里开始向上分析
if ( !strncmp(Dest, Str2, v2) )
sub_41132F("rigth flag!\n");
如果Dest和Str2的前v2个字符相同,系统会打印一个right flag给我们,sub_41132F不难猜出是print
我们点进Dest和Str2看看有啥东西(Dest没啥东西,Str2点进去如下,得到Str2的值)

结合上面分析,得出Dest的值(即Str2的值)
往上继续分析
for ( j = 0; j < v8; ++j )
Dest[j] += j;
一个for循环,Dest[0] = Dest[0]+0,Dest[1] = Dest[1]+1 ……这般规律为结果生成下去,直到j=v8(该循环对Dest做了变化)
再往上就没啥了
但是有一个点不清楚它的作用

点进sub_4110BE看看

继续点进去

遍历一下
(注:看到第16行的 3 和第19行的 4 ,这里就是base64对字符的二进制编码做的处理(经验之谈),要不确定的话我们可以继续往下看,这是定义了v9和v10来存放base64对字符的二进制变化;
Base64加密的核心逻辑(分组、补零、填充)必须通过if判断和循环来实现。下面还有if判断和while循环还有for循环,至此,我们基本可以判断本题是Base64加密题)
exp
import base64 #导入模块
Des = "e3nifIH9b_C@n@dH" #定义目标字符串
flag = "" #初始化结果字符串
for i in range(len(Des)):
flag += chr(ord(Des[i]) - i) #Des[i]:取出字符串 Des 中索引为 i 的字符。
#ord(Des[i]):使用 ord() 函数获取()内字符的 ASCII 码值。
#ord(Des[i]) - i:将该 ASCII 码值减去其索引位置 i。
print(base64.b64decode(flag)) #进行 Base64 解码并输出结果
得出结果,’ ‘里的内容换成flag{}格式提交即可


8、helloword(安卓逆向工具简单利用)
题目

做法
下载,不要解压,直接拖入Exeinfo PE进行分析

文件后缀是apk,判断为安卓逆向题
拖进ApkIDE

先找主函数main函数,这题的flag直接出来了
(搜索内容不要习惯性空格之类,这样会找不出来)


9、不一样的flag(迷宫题)
题目

做法
下载压缩包,解压,把解压后的文件拖进Exeinfo PE进行分析

32位,无壳
扔进IDA(32位),找到main,F5反编译

没啥关键词,Shift+F12也找不到什么有用的点
从上往下分析吧
puts("1 up");
puts("2 down");
puts("3 left");
printf("4 right\n:");
选1234都有对应的上下左右选择,是迷宫题吗
scanf("%d", &v5);
if ( v5 == 2 )
{
++*(_DWORD *)&v3[25];
}
else if ( v5 > 2 )
{
if ( v5 == 3 )
{
--v4;
}
else
{
if ( v5 != 4 )
LABEL_13:
exit(1);
++v4;
}
}
else
{
if ( v5 != 1 )
goto LABEL_13;
--*(_DWORD *)&v3[25];
}
把我们输入的值放进v5,根据我们输入的数字不同,v3或v4会有不同变化,亦或是异常退出
if ( *(_DWORD *)&v3[4 * i + 25] >= 5u )
exit(1);
}
if ( v7[5 * *(_DWORD *)&v3[25] - 41 + v4] == 49 )
exit(1);
if ( v7[5 * *(_DWORD *)&v3[25] - 41 + v4] == 35 )
{
puts("\nok, the order you enter is the flag!");
我们输入的值让v3或v4变化,满足不同公式,系统会异常退出亦或是返回’好的,你输入的顺序就是标志‘
至此,就再无别的信息了
我们尝试双击运行解压后的文件

分别输入1234进行测试

尝试多次后,发现规律——每次弹出来的四个选项中,只有一个是正确答案,且这个正确答案不是固定的,要自己不断慢慢试出来
结合IDA的代码分析,我们要去算那个公式的话,有很多种不同组合方式,而且更繁杂
我们选择直接打开文件一个个测,测出一个正确的就记下来,直到返回\nok, the order you enter is the flag!
最后成果——222441144222,包上flag{}去提交吧!

10、SimpleRev(大小端序,算法分析)
题目

做法
下载文件,拖进Exeinfo PE

64位,无壳
扔进IDA(64位),找到main,F5反编译

有数字,我们按r转换为字符看看

若v4=d/D,我们就可以进入Decry函数
若v4=q/Q,我们就退出
输入除去这几个的其他字母,结果均是break
putchar函数呢,则是将用户输入的字符回显到屏幕,没啥作用
我们点进去Decry函数看看

找到真面目了
if ( !strcmp(text, str2) )
puts("Congratulation!\n");
看到这里的congratulation,结合上面的Please input your flag,我们就知道该从这里往上分析了
congratulation的条件是:让text等于str2
然后我们自上而下进行分析(以下只节选重要部分进行分析)
一、变量初始化与内存布局
v11 = __readfsqword(0x28u);
*(_QWORD *)src = 0x534C43444ELL;
v9[0] = 0x776F646168LL;
- 栈保护机制
v11 = __readfsqword(0x28u):读取线程局部存储(TLS)中的金丝雀值(canary),用于检测栈溢出攻击。
- 字符串初始化(小端序存储)
src被初始化为0x534C43444ELL(十进制 357761762382):- 按字节拆分:
0x53('S') 0x4C('L') 0x43('C') 0x44('D') 0x4E('N') - 小端序存储后内存布局为:
4E 44 43 4C 53→ 字符串为"NDCLS"
- 按字节拆分:
v9[0]被初始化为0x776F646168LL:- 按字节拆分:
0x77('w') 0x6F('o') 0x64('d') 0x61('a') 0x68('h') - 小端序存储后内存布局为:
68 61 64 6F 77→ 字符串为"hadow"
- 按字节拆分:
二、密钥与目标字符串生成
text = (char *)join(key3, v9);
strcpy(key, key1);
strcat(key, src);
-
生成目标字符串
textjoin(key3, v9)将key3和"hadow"连接,结果存储在text中。 key1和key3都是一样,双击进去即可看到其值

-
生成加密密钥
keykey初始化为key1,再追加"NDCLS"。
-
密钥处理
- 将
key中的所有大写字母转换为小写:for (i = 0; i < v5; ++i) { if (key[v3 % v5] > 64 && key[v3 % v5] <= 90) key[i] = key[v3 % v5] + 32; // 大写转小写 ++v3; }
- 将
-
条件判断:
key[v3 % v5] > 64 && key[v3 % v5] <= 90
检查字符是否为大写字母(ASCII 范围 65-90) -
转换操作:
key[i] = key[v3 % v5] + 32
将大写字母转换为小写(ASCII 中,小写字母比大写字母大 32)
当 v3 从 0 递增到 9 时,v3 % v5 的结果为 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
因此,v3 % v5 会循环遍历 key 的每个字符(0 到 v5-1)
key[i]是数组,索引从0开始,因此,上面结果为0的key[0]即是key的第一个元素
%:百分号,表示求余运算,如5%4=1
注:
可能会有觉得上面的再扩展的话(比如v3=10,v5=10,v3%v5时结果为key[0]与v3=0,v5=10时也是等于key[0]这般重复的情况)会有俩个key[0],key[1]等 但是这种情况是不存在的,这种情况已经跳出了v5的范围,想法也是错误的 因为程序规定为< v5,这个想法已经不属于这个范畴了 v3不可能取值到10,若其取值到10,则与< v5冲突了 若v3取值到10,则说明v5有11个元素,但是v5只有10个元素
正确的:(假设v5为14)(与上面作出区分,便于观察) (一般规律,下次碰到一样的就不用继续算了)
| 迭代 | i |
v3 |
v3 % 14 |
key[v3 % 14] |
操作 |
|---|---|---|---|---|---|
| 1 | 0 | 0 | 0 | ‘a’ (97) | 不转换 |
| 2 | 1 | 1 | 1 | ‘b’ (98) | 不转换 |
| … | … | … | … | … | … |
| 14 | 13 | 13 | 13 | ‘n’ (110) | 不转换 |
上面说的有俩个key[0],key[1]这般重复的情况还有一种为v5被误设为较小值 这种情况的话一般会提前结束,若还是此题目的逻辑的话(< v5)则程序只会处理key的前v5个字符,而不会处理v5及其后字符
因此,无论何种情况,均不会有俩个key[0],key[1]这般重复的情况
补充: 在 C 语言中,字符本质上是以ASCII 码(或其他字符编码)的形式存储的。当你对字符进行数学运算时,C 语言会自动将字符转换为对应的整数值(即 ASCII 码值)
char c = 'A';
int num = c + 32; // 'A'的ASCII码(65) + 32 = 97
printf("%c\n", num); // 输出: 'a' (ASCII码97对应的字符)
三、用户输入处理与加密过程
printf("Please input your flag:");
while (1) {
v1 = getchar();
if (v1 == 10) // 换行符
break;
if (v1 == 32) // 空格
{
++v2;
} //在此之下就是else开始加密,因此为过滤掉这俩
// 加密逻辑
if ( v1 <= 96 || v1 > 122 ) //非小写字母ascll码范畴
{
if ( v1 > 64 && v1 <= 90 ) //如果是大写字母
{
str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97;
//任何整数除以 26 的余数必定在 0-25 之间
//最后+97是把ascll码范围回到小写字母的ascll码范围(即(0~25)+97对应)
++v3;
}
}
else //小写字母
{
str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97; //同上处理方式
++v3;
}
if ( !(v3 % v5) ) //如果v3是v5的整数倍(即处理完一轮密钥)
//本来 % 是取余运算,算出结果是余数,这里前面加了!即为没有余数,则只能是整数
putchar(32); //输出一个空格
++v2;
- 输入过滤
- 忽略换行符(
\n)和空格。
- 忽略换行符(
-
加密算法
-
对非小写字母进行处理:
str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97;
-
- 密钥循环使用
- 密钥
key循环使用(通过v3 % v5实现) - 每处理完一轮密钥(即
v3是密钥长度的整数倍),输出一个空格。
- 密钥
四、验证与结果
if (!strcmp(text, str2))
puts("Congratulation!\n");
else
puts("Try again!\n");
- 将用户输入加密后的结果(
str2)与目标字符串(text)比较。 - 若匹配,输出
"Congratulation!",否则输出"Try again!"。
五、解密方法
要逆向推导出原始 flag,需根据加密公式编写解密函数:
key = "adsfkndcls"
text = "killshadow"
v3 = 0
for i in range(10): #10代表我们要解密的字符数
for j in range(128):
# 跳过非字母字符
if j < ord('A') or (j > ord('Z') and j < ord('a')) or j > ord('z'):
continue
# 这里的ord是让字符变成ascll码,这里说的是范围规定在英文大小写字母范围内
# 核心解密公式
if (j - 39 - ord(key[v3 % 10]) + 97) % 26 + 97 == ord(text[i]):
print(chr(j), end='')
v3 += 1
break
得出flag,返回题目提交

补充
1、大小端序
eg.0x123456 0x12通常是高字节,然后0x56是低字节(一般都是这么个顺序,前高后低)
大端序:高字节存放在低地址,低字节存放在高地址 小端序:高字节存放在高地址,低字节存放在低地址
二者正好相反
读取顺序一般是从低地址开始读起
eg.0x123456 大端序:从0x12开始向右读 小端序:从0x56开始向左读
2、如何判断题目给的到底是大端序还是小端序
(1)查看方法
大小端序可以使用软件查看,比如:Detect It Easy
可以看到字节序(小LE)/(大BE)
(2)代码中隐含的端序:
当代码使用整数直接初始化字符串时(如*(QWORD*)src = ...),默认依赖于编译环境的端序(如 x86 为小端序)。
(3)显式判断端序:
int isLittleEndian() {
int num = 1;
return (*(char*)&num == 1); // 1为小端序,0为大端序
}
(4)常见应用场景:
小端序:x86/AMD64 架构(Windows/Linux)、ARM 架构(移动端)。 大端序:网络协议(如 IP 地址)、Java 字节码、部分嵌入式系统(如 PowerPC)。 混合场景:文件格式(如 BMP 为小端序,PNG 为大端序)。





