题目复现2

1.BUU-[FlareOn6]Overlong

解题思路

老步骤,先查壳,发现是无壳32位,直接IDA打开。

发现函数特别少,还挺简洁的。

我们能分析出来,Text是我们要的flag,最终会使用弹窗输出出来,我们可以直接动调看看,发现是

这个弹窗上的像是没有输出完一样,我们就查看unk_3D2008的数据有多少。

发现这个数据其实有很多很多

起始地址是0x003D2008

结束地址是0x003D20B7

我们可以使用计算器来计算它的大小,得出结果是0xAF

转到汇编看一下,现在已经清楚是分配的内存空间太小,以至于在进行sub_3D1160函数加密的时候,只能实现0x1C个字节,我们应该把0x1C改成0xAF,这样就能输出完整了。

如何改呢,在汇编成面push 1Ch 处打上断点,然后进行调试。

我们可以直接将1C patch 成 AF

改完后,我们直接按F9让它运行到弹窗,完整的flag就出来了。

记录

本题是给数据分配的内存空间太小,导致flag不完整,可以通过patch来修改内存大小。

2.BUU-[羊城杯 2020]easyre

解题思路

无壳使用IDA打开,分析主函数

大致的逻辑已经清楚,知道了flag是经过三层加密得到了str2这个密文。我们应该逆向分析,从第三个加密函数开始看起。

这个是对v9按照字符分类加密的,最后得到密文是Str1,也就是Str2。我们进行逆行分析,对Str2分别对应字符分类解密就能得到v9。

第三个加密方式是凯撒加密 +3,将字母或数字向右平移3位。解密就是凯撒加密 -3就 ok了。

但是要注意的是:如果直接减可能会变成负数,比如 'A' - 3'>'(非法),所以我们加上模数再取余

解密:这里先让c 看作是密文的角标

大写字母:(ord(c) - ord('A') - 3 + 26) % 26 + ord('A')

小写字母:(ord(c) - ord('a') - 3 + 26) % 26 + ord('a')

数字:(ord(c) - ord('0') - 3 + 10) % 10 + ord('0')

然后分析第二个加密函数

这个函数中 a3 是 v9 , a1是 v10,a2 是 v10的长度。我们从上一个加密函数中能解出 v9。

这个加密的过程是:

将a1的第0位到第13位赋给a3的第13到26位

将a1的第13位到第26位赋给a3的第39到52位

将a1的第26位到第39位赋给a3的第0到13位

将a1的第39位到第52位赋给a3的第26到39位

那我们解密时要将 v9 每13位一组分成4组,然后按照这个来重新组合。那我们就能得到v10了。

a1[00:13] ← a3[13:26]
a1[13:26] ← a3[39:52]
a1[26:39] ← a3[00:13]
a1[39:52] ← a3[26:39]

接着看第一个加密函数

根据这些特征,可以看出来这个是个base64加密,关键是找到表,表点击alphabet就能看到,这个表就是base64加密标准表,直接解密就好。a3就是v10。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
##decode_three
def decode_three(encoded):
decoded = ""
for c in encoded:
if 'A' <= c <= 'Z':
decoded += chr((ord(c) - ord('A') - 3 + 26) % 26 + ord('A'))
elif 'a' <= c <= 'z':
decoded += chr((ord(c) - ord('a') - 3 + 26) % 26 + ord('a'))
elif '0' <= c <= '9':
decoded += chr((ord(c) - ord('0') - 3 + 10) % 10 + ord('0'))
else:
decoded += c
return decoded

cipher = "EmBmP5Pmn7QcPU4gLYKv5QcMmB3PWHcP5YkPq3=cT6QckkPckoRG"
plain = decode_three(cipher)
print("decode_three结果:",plain)

##decode_two
def decode_two(enc: str) -> str:
if len(enc) < 52:
raise ValueError("密文长度不足52字符")

part1 = enc[13:26] # 原 a1[0:13]
part2 = enc[39:52] # 原 a1[13:26]
part3 = enc[0:13] # 原 a1[26:39]
part4 = enc[26:39] # 原 a1[39:52]

return part1 + part2 + part3 + part4

# 示例
enc = "BjYjM2Mjk4NzMR1dIVHs2NzJjY0MTEzM2VhMn0=zQ3NzhhMzhlOD"
# 只取前52位进行解密
decrypted = decode_two(enc[:52])
print("decode_two结果:", decrypted)

##decode_one
import base64
flag = base64.b64decode(decrypted).decode('utf-8')
print("decode_one结果:",flag)

#decode_three结果: BjYjM2Mjk4NzMR1dIVHs2NzJjY0MTEzM2VhMn0=zQ3NzhhMzhlOD
#decode_two结果: R1dIVHs2NzJjYzQ3NzhhMzhlODBjYjM2Mjk4NzM0MTEzM2VhMn0=
#decode_one结果: GWHT{672cc4778a38e80cb362987341133ea2}

记录

考了凯撒加密,分组排列和base64三种加密方法。

3.BUU-[WUSTCTF2020]level4

解题思路

这个题是ELF文件,我们先用IDA打开看看

这段程序是二叉树 遍历(Traversal),给了三种遍历方式,type1,type2和 type3。我们可以从程序里得到type1和type2的函数内容,但不知道type3进行了什么遍历。我们先看看这些函数都是什么用处。

init() 函数的作用是初始化一个数据结构,这里是树结构,并从字符串 "I{_}Af2700ih_secTS2Et_wr" 中提取字符并写入结构数组 x。它设置大量全局变量指向特定结构体地址,构建“二叉树”的指针结构。

看看type1,这是中序遍历,中序:左根右。

看看type2,这是后序遍历,后序:左右根

我们在虚拟机上运行可以知道结果

但是发现这两种遍历得到的都不是flag,那就是type3遍历,应该是前序遍历。我们只要已知一个二叉树的中序遍历(In-order)和后序遍历(Post-order)序列,就可以唯一确定这棵二叉树的结构,从而也就能推出其前序遍历(Pre-order)结果。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

char post[] = "20f0Th{2tsIS_icArE}e7__w"; //后序遍历结果
char mid[] = "2f0t02T{hcsiI_SwA__r7Ee}"; //中序遍历结果

void pre(int root,int start,int end)
{
if(start > end)
return ;
int i = start;
while(i < end && mid[i] != post[root])
i++; //定位根在中序的位置
printf("%c",mid[i]);
pre(root - 1-(end - i),start,i - 1); //递归处理左子树
pre(root-1,i + 1,end); //递归处理右子树
}

int main()
{
pre(24,0,24);//24为flag的位数
return 0;
}
//wctf2020{This_IS_A_7reE}

记录

考了二叉树的三种遍历方式,前序,中序和后序。前序:根左右,中序:左根右,后序:左右根

而且给定前序遍历+中序遍历或者给定了中序遍历+后序遍历,二叉树的构造方法都是唯一的

4.[GUET-CTF2019]number_game

解题思路

拿到文件后先查壳,这个没有壳,ELF64位,直接放入IDA进行静态分析。

进入主函数对其进行分析

这个 main 函数的核心流程是输入一段字符串,对其进行合法性检查、二叉树构建与遍历处理,并通过一系列检查逻辑,决定是否输出 flag。我们先按照顺序来分析,先看校验是否为合法输入的函数。

这个函数是遍历a1的值,确保a1的值在0~4范围内,否则输出Wrong!

然后分析构造二叉树的函数sub_400758(&v5, 0, 10):将输入字符串转为一棵二叉树,返回一个指向根节点的指针(即 _QWORD *

这个函数返回一个 _QWORD *,即一个指向二叉树节点的指针,每个节点占用 24 字节。v5:用于暂存当前数组中的字符。v6:将作为当前新建的树节点指针。

第一步:将地址 (a1 + a2) 处的字节读取出来赋值给 v5。 意思是从数组中取出当前下标 a2 的字符。

第二步:判断是否构造当前节点,如果当前字符是 ' '(ASCII 32)或 '\n'(ASCII 10),就不创建节点(这些字符被视为空节点);

或者当前索引越界(a2 >= a3),返回空指针。所以此函数跳过空格、换行、或越界位置的内容。

第三步:分配内存,为当前节点分配 24 字节内存。

第四步:设置当前节点的值。*(_BYTE *)v6 = v5;

第五步:递归构造左子树,左子节点的索引是 2 * a2 + 1,符合完全二叉树(堆)数组索引规律

第六步:递归构造右子树,右子节点的索引是 2 * (a2 + 1),即 2 * a2 + 2

第七步:返回当前节点指针。

然后分析中序遍历函数sub_400807(v4, &v7):对树进行中序遍历,把结果保存在某个缓冲区中(v7 开头的位置)

a1:指向一个结构体或对象的指针,代表二叉树形结构中的一个节点a2:一个缓冲区的地址,用来写入某种输出数据。

如果当前节点不为空,就递归地遍历其左子树、处理当前节点、再遍历右子树。这实现了中序遍历。其节点结构为:

*(_QWORD *)(a1 + 8) = 左子树指针

*(_QWORD *)(a1 + 16) = 右子树指针

*(_BYTE *)a1 = 当前节点值

然后分析一下 sub_400881(&v7)函数

它的作用是把输入字符串 a1 中的前 10 个字节依次复制到某些特定的全局变量地址(全局缓冲区)中。也就是把中序遍历后的结果放在了缓冲区里面,点进去看看却发现这些都是 '#' ,我们需要知道这些具体的数据。

我们先继续往下进行分析,现在要分析sub_400917()函数,这是最后的校验逻辑

很明显的这是从unk_601060缓冲区开始读取25个字节,把它看作5x5的矩阵来分析,逻辑是对于每一行和每一列都要求元素不相同,简单来说,这就是一个数独游戏,我们可以根据这个校验逻辑来算出中序遍历的结果是什么。这里我选择手动计算。

我们现在就知道了这个中序遍历后的结果是0421421430,那么我们现在要求的flag是v5,也就是输入构成二叉树的数据,我们只需要得出这个二叉树中序遍历访问的节点顺序,然后一一映射就能得出flag。

这里可以通过写脚本得出中序遍历的节点顺序,也可以动调。动调的方法参考

https://blog.csdn.net/qq_45892237/article/details/115664816

节点顺序是[7, 3, 8, 1, 9, 4, 0, 5, 2, 6],一一映射举个例子,比如第一个节点是7,那么节点为7储存的就是中序遍历后的第一个数据为’0’,我们要将这个0移动到下标为7的位置。也就是以这个节点顺序为新的索引重新排列得到原来的数据。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def inorder_indices(pos, n):
"""根据给定的特殊规则,对二叉树索引做中序遍历,返回节点访问顺序列表"""
if pos >= n:
return []
return inorder_indices(2 * pos + 1, n) + [pos] + inorder_indices(2 * (pos + 1), n)

# 原始输入字符串(长度为10)
original = list("0421421430")

# 二叉树节点数
n = len(original)

# 计算中序遍历索引顺序
mapping = inorder_indices(0, n)

print("中序遍历得到的节点访问顺序:", mapping)

# 将原来第i位字符放到mapping[i]位置
flag = [''] * n
for i, pos in enumerate(mapping):
flag[pos] = original[i]

print("最终的 flag:", ''.join(flag))
## flag :1134240024

记录

考了二叉树的遍历和数独

5.BUU-[ACTF新生赛2020]SoulLike

解题思路

先查壳,这个无壳,直接IDA进行分析主函数

这个大致逻辑是将flag从v9[6]的位置开始输入,将 “actf{“ 写入到v9[0]~v9[4]中,\0写在v9[5]中。然后进入前缀是否相等的判断中。

v5先设为1,进入判断,如果v9[i]!=v9[i+6],那么v5=0,跳转到LABEL_6,当循环结束时,会执行!v5这个条件跳转,是满足的,会跳转到LABEL_16,这就说明了本质上是要求它们是相等的。

如果v9[i]==v9[i+6],也就是字符串是这个形式actf{actf{xxxxx……,v5还是等于1,!v5是0,不会跳转到LABEL_16,而是继续往下执行到LABEL_6。

进入到LABEL_6的校验逻辑中,v8是v9[11]~v9[22],也就是除去了flag的前缀头,是flag真正内容的部分。我们要分析sub_83A函数,但是发现了这个函数太大,IDA无法反编译,该怎么办呐???

解决办法:

我们找到IDA目录,打开 IDA 安装目录下的 IDA/cfg/hexrays.cfg 文件,可能路径会有不同,直接搜索就可以

找到配置项:

1
MAX_FUNCSIZE=64

修改为更大的值:

1
MAX_FUNCSIZE=1024

然后重启IDA,经过很长一段时间就能成功反编译了

发现sub_83A函数竟然有3000多行的异或加密,密密麻麻是我的自尊~

拖到最后知道这个校验逻辑,加密后的flag内容与v3相等,那么可以考虑的是爆破,这里直接用的大佬的exp

https://blog.csdn.net/2201_76139143/article/details/132511141

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <string.h>
int v3[12]={126,50,37,88,89,107,53,110,0,19,30,56};
int sub_83A(char *a1,int i)
{
*a1 ^= 0x2Bu;
a1[1] ^= 0x6Cu;
a1[2] ^= 0x7Eu;
a1[3] ^= 0x56u;
a1[4] ^= 0x39u;
a1[5] ^= 3u;
a1[6] ^= 0x2Du;
a1[7] ^= 0x28u;
a1[8] ^= 8u;
++a1[9];
a1[10] ^= 0x2Fu;
a1[11] ^= 0xAu;
++*a1;
//这里直接将那3000多行代码复制粘贴就行
//这个不完整,下面省略了

if(a1[i] == v3[i])
return 1;
else
return 0;

}
int main()
{
int i,j;
char flag[13] = ""; //保存当前尝试的字符串
char tmp[13] = ""; //保存已经确定的正确字符
for(i = 0; i < 12 ; i++)
{
for(j = 33; j <= 126 ; j++) //尝试常见的可打印字符
{
strcpy(flag,tmp); //先复制已经确定的正确字符到flag
flag[i] = j; //修改第i个字符为尝试字符j
if(sub_83A(flag,i)) //传入flag和i,判断是否正确
{
tmp[i] = j; //如果通过,保存字符j到tmp中
break; //跳出循环,尝试下一个
}
}
}
printf("%s\n",tmp);
return 0;
}
//b0Nf|Re_LiT!

记录

知道一个函数过大IDA无法反编译时可以修改配置文件hexrays.cfg中MAX_FUNCSIZE=64,把64换成更大的数,然后就能反编译出来了。这道题可以爆破处理。