avatar

Catalog
KCTF2022春季赛第三题

前言

这是一次失败的经历,从周五放出题开始,不到2个小时,就有人解出题目,上午刚刚写完wp的喜悦瞬间消散,开始着手准备解题。这一天没有进展。直至第二天傍晚,才开始有所眉目。所谓的异常处理,并不会对解题有太大影响,相反,题目算法本身的复杂度,成了影响的关键因素,从周六傍晚开始逆算法,直至第二天凌晨4点30,终于逆出了完整的算法,此时我已精疲力竭。然而,仅有算法还不足以解题,需要逆算法才可以,此时精力已经不足,在进行一些简单的尝试后,遂放弃了此题。

中午过后,公开了各个大佬的wp,发现自己原来把魔改后的AES逆了一遍,显得有些愚蠢、呆板,但是看着大佬们的解题思路,豁然开朗,这是一次绝佳的学习机会,所以,趁着现在对该题还有印象,又不想浪费自己的分析,这里会结合各个大佬的wp,简单分析一下程序的坑点和流程。并在最后,依次分析大佬们的解题思路和思维方式。

分析流程

异常处理

  • 问题描述:该题的第一个坎是异常处理,如图中所示,红色方框会向0地址写入数据,从而触发异常。本题中引发异常的位置还有若干个,触发异常后,再在异常处理函数中修正EIP的值。

  • 解决方案

    1. 理解异常发生后的程序控制流,patch程序让程序的控制流恢复正常,这里可以参考以下两篇“看雪CTF签到题SEH异常处理”“SEH的非常好的总结”关于SEH机制的文章
    2. 较为密集的下断点,从而推测出程序的执行流程。例如这里,可在图中橙色方框伪代码对应的汇编处下断点

Findcrypt

题目使用了魔改的MD5和AES,由于对AES的生疏,做题期间,我自始至终都没有察觉其存在。根据部分大佬的wp,通过IDA的插件Findcrypt可以快速定位加密算法的存在。这里附上安装说明官网链接。此外还需要yara环境支持。安装后可以在Edit -> Plugins -> Findcrypt进行使用。

如上图所示,经过简单分析,可以将原先的函数备注为MD5_EncryptAES_Encrypt,尽管这俩都经过了魔改

MD5_Encrypt

第一个需要看的是MD5_Encrypt,图中x32dbg中显示的duplicity.7B1109,就是伪代码中被我们标记为MD5_Encrypt的函数

参数分析

  • v8[9]:指向字符串”Enj0y_1t_4_fuuuN”
  • 0x10:长度单位
  • v8[2]:
    • 执行前指向一块初始化为0的栈空间
    • 执行后该栈空间更新为16字节的MD5密文

AES_Encrypt

这是一个经过魔改的AES_Encrypt加密,先分析参数:

参数分析

  • v8[2]:经过MD5加密后的密文
  • 0x10:经过MD5加密后的密文长度
  • v8[30]:输入的字符串,从橙色方框中的gets_s获取
  • v8[18]:经过AES加密后的输出字符串,之后通过memcmp将其与结果进行了一次比较
  • 32:输入字符串的长度,橙色方框中通过strlen进行了一次过滤

AES算法

以上为AES算法的流程图,这里不对该算法做详细介绍,但理解AES算法还是非常重要的,这里分享如下链接,简略版可参考1,详细版可以参考2,综合版可以参考3,C语言版可以参考4。在对AES了解的情况下,可以找到相似项目,手动导入结构体及符号,以助于伪代码的分析。

KeyExpansion

分析

  1. 首先是keyExpansion的代码,这部分会对MD5计算出的key进行扩展,跟进去发现,又是一个会触发的异常,这里需要patch try部分,patch后核心代码可见,这里可以参考sunfishi的分析文章,他patch了此部分,并导入了符号与结构体,看着会更清晰。
  2. 这里还有很重要的一点,keyExpansion进行扩展密钥时,进行了sbox中数据的交换,从而使得真正使用的S_box与原版AES的S_box有所不同,这里是题目魔改AES的一处地方,会影响后续做题。

反思

  • 当时做题时,这部分我并没有分析,当时并未意识到这是AES的算法,keyExpansion函数执行后,栈中会生成一部分数据,在之后的计算中会用到,在试过不同的输入后,发现这段栈中的数据是固定不变的,于是就拿来直接用了……

  • 关于sbox魔改这块,我后来也发现了,不过是在最后,在已经逆完所有算法的情况下,发现计算出的结果不对。经过一个个函数的单步调试,终于发现了sbox值交换的这个操作,这里感谢我可爱的女朋友凌晨陪我调试代码!

图中duplicity.7B10BE即伪代码中标识出的keyExpansion在执行完后,栈中就会有生成扩展密钥,具体如下:

python
1
2
3
4
5
6
7
8
9
10
11
v10 = [0x2F65B1FF, 0x31ED86D0, 0x9A285C0F, 0x4048059D,
0x7C0EEFF6, 0x4DE36926, 0xD7CB3529, 0x978330B4,
0x920A627E, 0xDFE90B58, 0x08223E71, 0x9FA10EC5,
0xA4A1C4A5, 0x7B48CFFD, 0x736AF18C, 0xECCBFF49,
0xB3B7FF6B, 0xC8FF3096, 0xBB95C11A, 0x575E3E53,
0xFB051230, 0x33FA22A6, 0x886FE3BC, 0xDF31DDEF,
0x1CC4CDAE, 0x2F3EEF08, 0xA7510CB4, 0x7860D15B,
0x8CFAF412, 0xA3C41B1A, 0x049517AE, 0x7CF5C6F5,
0xEA4E1202, 0x498A0918, 0x4D1F1EB6, 0x31EAD843,
0x762F08C5, 0x3FA501DD, 0x72BA1F6B, 0x4350C728,
0x13E93CDF, 0x2C4C3D02, 0x5EF62269, 0x1DA6E541]

Load/StoreStateArray

分析

  1. 从for循环可以看出,输入的32字节的字符串,会被分为2份进行操作,每次操作16字节,图中保存在变量v8中
  2. 这16字节,会按照字节顺序,从左到右从上到下每行4个的方式进行排列,形成一个4x4的矩阵
  3. loadStateArray会将矩阵斜对角线两侧的数据进行一个交换
  4. storeStateArray会再按照矩阵斜对角线将两侧的数据交换回来

还原

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 注意这里只是还原算法,并不是解算法,后面同理
def loadStateArray(buf, orig):

for i in range(4):
for j in range(4):
buf[4 * j + i] = ord(orig[4 * i + j])
return buf


def storeStateArray(buf):

res = []
for i in range(16):
res.append(0)
for i in range(4):
for j in range(4):
res[i * 4 + j] = buf[4 * j + i]
return res

解法

  1. 该算法是自旋的,只需再调用一遍即可解锁

AddRoundKey

分析

  1. 这里的参数v10,实际上就是经过keyExpansion扩展后的密钥,在函数开头,v10与v12指向了同一地址
  2. addRoundKey中,会将输入的数据与密钥按照字节异或,结果覆盖原先字符串字节处的值
  3. 在每轮异或运算过后,参数会指向扩展密钥的后16个字节的开始位置,这部分可以参考AES加密的密钥加法层

还原

python
1
2
3
4
5
6
7
8
9
10
11
12
13
def addRoundKey(buf, local):

res = []

for i in range(16):
v5.append(0)
for i in range(16):
res.append(0)
for i in range(4):
for j in range(4):
v5 = (local[j] >> (8 * (3 - i))) & 0xFF
res[4 * i + j] = (buf[4 * i + j]) ^ v5
return res

解法

  1. 异或运算是对称的,因此可以再异或回去

SubBytes

分析

  1. subBytes对应AES的字节代换层,字节代换层的主要功能就是让输入的数据通过S_box表完成从一个字节到另一个字节的映射,这g个S_box表是通过某种方法计算出来的

  2. 本题中,在本地偏移0x40B000处,有一个默认的S_box;但是在真正进行运算时S_box中的值被替换了(在keyExpansion中被替换了,然而这部分代码通过异常处理隐藏了,需要手动patch)

    c
    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
    // 原版S_box
    0040B000 63 7C 77 7B F2 6B 6F C5 30 01 67 2B FE D7 AB 76
    0040B010 CA 82 C9 7D FA 59 47 F0 AD D4 A2 AF 9C A4 72 C0
    0040B020 B7 FD 93 26 36 3F F7 CC 34 A5 E5 F1 71 D8 31 15
    0040B030 04 C7 23 C3 18 96 05 9A 07 12 80 E2 EB 27 B2 75
    0040B040 09 83 2C 1A 1B 6E 5A A0 52 3B D6 B3 29 E3 2F 84
    0040B050 53 D1 00 ED 20 FC B1 5B 6A CB BE 39 4A 4C 58 CF
    0040B060 D0 EF AA FB 43 4D 33 85 45 F9 02 7F 50 3C 9F A8
    0040B070 51 A3 40 8F 92 9D 38 F5 BC B6 DA 21 10 FF F3 D2
    0040B080 CD 0C 13 EC 5F 97 44 17 C4 A7 7E 3D 64 5D 19 73
    0040B090 60 81 4F DC 22 2A 90 88 46 EE B8 14 DE 5E 0B DB
    0040B0A0 E0 32 3A 0A 49 06 24 5C C2 D3 AC 62 91 95 E4 79
    0040B0B0 E7 C8 37 6D 8D D5 4E A9 6C 56 F4 EA 65 7A AE 08
    0040B0C0 BA 78 25 2E 1C A6 B4 C6 E8 DD 74 1F 4B BD 8B 8A
    0040B0D0 70 3E B5 66 48 03 F6 0E 61 35 57 B9 86 C1 1D 9E
    0040B0E0 E1 F8 98 11 69 D9 8E 94 9B 1E 87 E9 CE 55 28 DF
    0040B0F0 8C A1 89 0D BF E6 42 68 41 99 2D 0F B0 54 BB 16

    // 实际使用的S_box
    0040B000 63 7C 77 7B F2 6B 6F C5 30 01 67 2B FE D7 AB 76
    0040B010 CA 82 C9 7D FA 59 47 F0 AD D4 A2 AF 9C A4 72 C0
    0040B020 B7 FD 93 26 36 3F F7 CC 34 A5 E5 F1 71 D8 31 15
    0040B030 04 C7 23 C3 18 96 05 9A 07 12 80 E2 EB 27 B2 75
    0040B040 09 83 2C 1A 1B 6E 5A A0 52 3B D6 B3 29 E3 2F 84
    0040B050 53 D1 00 ED 20 FC B1 5B 6A CB BE 39 4A 4C 58 CF
    0040B060 D0 EF AA FB 43 4D 33 85 45 F9 02 7F 50 3C 9F A8
    0040B070 51 0A 40 8F 92 9D 38 F5 BC B6 DA 21 10 FF F3 D2
    0040B080 CD 0C 13 EC 5F 97 44 17 C4 A7 7E 3D 64 5D 19 73
    0040B090 60 81 4F DC 22 2A 90 88 46 EE B8 14 DE 5E 0B DB
    0040B0A0 E0 32 3A A3 49 06 24 5C C2 D3 AC 62 91 95 E4 79
    0040B0B0 E7 C8 37 6D 8D D5 4E A9 6C 56 F4 EA 65 7A AE 08
    0040B0C0 BA 78 25 2E 1C A6 B4 C6 E8 DD 74 1F 4B BD 8B 8A
    0040B0D0 70 3E B5 66 48 03 F6 0E 61 35 57 B9 86 C1 1D 9E
    0040B0E0 E1 F8 98 11 69 D9 8E 94 9B 1E 87 E9 CE 55 28 DF
    0040B0F0 8C A1 89 0D BF E6 42 68 41 99 2D 0F B0 54 BB 16
  3. 两个S_box不同之处在于地址0x0040B071(”0xA3”)与0x0040B0A3处的值(”0x0A”)交换了

  4. 由于交换是在keyExpansion中进行的,所以执行到此处时S_box已确定,可以直接从内存中dump出来用

还原

python
1
2
3
4
5
6
7
8
9
10
def subBytes(buf):

res = []

for i in range(16):
res.append(0)
for i in range(4):
for j in range(4):
res[4 * i + j] = byte_40B000[buf[4 * i + j]]
return res

解法

  1. 拥有密钥(MD5生成的16字节密钥)的情况下解AES,通常需要逆S_box盒,这里S_box修改了,逆S_box盒也要相应修改
  2. 直接在S_box找到对应的值的下标(0

ShiftRows

这一块代码不能F5的,会显示错误sp-analysis failed,文末也推荐了几篇解决不能F5的文章,所以我给它patch了(我觉得这算是作者的一个提示,如果允许F5,然后还是隐藏在异常处理里的话,那我可能就不会去patch了)。这里我将原先的int 0x8B指令改掉了,不让它触发异常,直接跳转到异常处理函数里面,这样就可以保存栈平衡,就能够F5直接看到程序代码逻辑了。

分析

  1. shiftRows对应AES的行位移操作,这里作者进行了魔改,将4x4矩阵的每行依次循环右移0/1/2/3个字节
  2. 在原版的AES操作中,是依次循环左移0/1/2/3个字节

还原

python
1
2
3
4
5
6
7
8
9
def shiftRows(buf):

res = []
for i in range(16):
res.append(0)
for i in range(4):
res[4*i:5*i] = buf[4*i+4-i:4*i+4]
res[5*i:4*i+4] = buf[4*i:4*i+4-i]
return res

解法

  1. 解法很简单,依次循环左移即可

MixColumns

分析

  1. 列混淆,对应列混淆子层,是AES算法中最为复杂的部分
  2. 需要将输入的4x4矩阵左乘一个给定的4x4矩阵。而它们之间的加法、乘法都在扩展域GF(2^8)中进行

还原

python
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
50
51
52
53
54
55
def mixColumns(buf):

v9 = []
res = []
for i in range(16):
res.append(0)
for i in range(44):
v9.append(0xCC)
v9[0] = 2;
v9[1] = 3;
v9[2] = 1;
v9[3] = 1;
v9[4] = 1;
v9[5] = 2;
v9[6] = 3;
v9[7] = 1;
v9[8] = 1;
v9[9] = 1;
v9[10] = 2;
v9[11] = 3;
v9[12] = 3;
v9[13] = 1;
v9[14] = 1;
v9[15] = 2;


for i in range(4):
for j in range(4):
v9[4 * i + 24 + j] = buf[4 * i + j]

for k in range(4):
for m in range(4):
tmp = sub_401118(v9[4 * k], v9[m + 24])
v1 = tmp
tmp = sub_401118(v9[4 * k + 1], v9[m + 28])
v2 = tmp ^ v1
tmp = sub_401118(v9[4 * k + 2], v9[m + 32])
v3 = tmp ^ v2
tmp = sub_401118(v9[4 * k + 3], v9[m + 36])
v4 = tmp ^ v3
res[4 * k + m] = v4
return res


def sub_401118(a1, a2):
v5 = 0
for i in range(8):
if a1 & 1 != 0:
v5 = (a2 ^ v5) & 0xFF
v3 = a2 & 0x80
a2 = (a2 * 2) & 0xFF
if v3 != 0:
a2 = (a2 ^ 0x1B) & 0xFF
a1 = (a1 >> 1) & 0xFF
return v5

解法

  1. wx_孤城发现调用mixColumns自旋3下即可解密

  2. 通过逆列混合矩阵进行解密,这部分可以参考堂前燕的文章

    c
    1
    2
    3
    4
    5
    6
    static const int deColM[4][4] = { 
    0xe, 0xb, 0xd, 0x9,
    0x9, 0xe, 0xb, 0xd,
    0xd, 0x9, 0xe, 0xb,
    0xb, 0xd, 0x9, 0xe
    };

失败的还原

虽然很失败,但还是靠着一点点单步,还原了完整的加密算法,不甘心就这样浪费心血了,所以博客里附上我还原的完整代码

python
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
import itertools
import random

v10 = [0x2F65B1FF, 0x31ED86D0, 0x9A285C0F, 0x4048059D,
0x7C0EEFF6, 0x4DE36926, 0xD7CB3529, 0x978330B4,
0x920A627E, 0xDFE90B58, 0x08223E71, 0x9FA10EC5,
0xA4A1C4A5, 0x7B48CFFD, 0x736AF18C, 0xECCBFF49,
0xB3B7FF6B, 0xC8FF3096, 0xBB95C11A, 0x575E3E53,
0xFB051230, 0x33FA22A6, 0x886FE3BC, 0xDF31DDEF,
0x1CC4CDAE, 0x2F3EEF08, 0xA7510CB4, 0x7860D15B,
0x8CFAF412, 0xA3C41B1A, 0x049517AE, 0x7CF5C6F5,
0xEA4E1202, 0x498A0918, 0x4D1F1EB6, 0x31EAD843,
0x762F08C5, 0x3FA501DD, 0x72BA1F6B, 0x4350C728,
0x13E93CDF, 0x2C4C3D02, 0x5EF62269, 0x1DA6E541]


byte_40B000 = [
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0x0A, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0xA3, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16]


def sub_402070(s):

v8 = []
for i in range(16):
v8.append(0)
v8 = sub_401145(v8, s)
#for i in range(4):
# print(hex(v8[4 * i]), hex(v8[4 * i + 1]), hex(v8[4 * i + 2]), hex(v8[4 * i + 3]))
#print()

v8 = sub_401186(v8, v10[0:4])

for j in range(9):

# Get Value From Byte_40B000
v8 = sub_401172(v8)

# ROR
v8 = sub_40122B(v8)

v8 = sub_40100F(v8)

# Xor Local
v8 = sub_401186(v8, v10[4+j*4:8+j*4])

v8 = sub_401172(v8)

v8 = sub_40122B(v8)

len_v10 = len(v10)
v8 = sub_401186(v8, v10[len_v10-4:len_v10])
for i in range(4):
print(hex(v8[4 * i]), hex(v8[4 * i + 1]), hex(v8[4 * i + 2]), hex(v8[4 * i + 3]))
print()

v8 = sub_40125D(v8)

return v8


def sub_40125D(buf):

res = []
for i in range(16):
res.append(0)
for i in range(4):
for j in range(4):
res[i * 4 + j] = buf[4 * j + i]
return res


def sub_401145(buf, orig):

k = 0
#print('orig', 'now', 'value')
for i in range(4):
for j in range(4):
buf[4 * j + i] = ord(orig[4 * i + j])
k = k + 1

#print(4 * i + j, 4 * j + i, hex(ord(orig[4 * i + j])))

return buf


def sub_401186(buf, local):

v5 = []
res = []

#print()
#print('s', 'orig', 'now')
for i in range(16):
v5.append(0)
for i in range(16):
res.append(0)
for i in range(4):
for j in range(4):
v5 = (local[j] >> (8 * (3 - i))) & 0xFF
res[4 * i + j] = (buf[4 * i + j]) ^ v5
#print(4 * i + j, hex(local[j]), hex(local[j] >> (8 * (3 - i))), hex(v5), hex(res[4 * i + j]))
return res


def sub_401172(buf):

res = []
#print()
#print('s', 'orig', 'now')
for i in range(16):
res.append(0)
for i in range(4):
for j in range(4):
res[4 * i + j] = byte_40B000[buf[4 * i + j]]
#print(4 * i + j, hex(buf[4 * i + j]), hex(res[4 * i + j]))
return res


# ROR
def sub_40122B(buf):

res = []
for i in range(16):
res.append(0)
for i in range(4):
res[4*i:5*i] = buf[4*i+4-i:4*i+4]
res[5*i:4*i+4] = buf[4*i:4*i+4-i]
#print()
#print('after ror:')
#for i in range(4):
# print(hex(res[4 * i]), hex(res[4 * i + 1]), hex(res[4 * i + 2]), hex(res[4 * i + 3]))
return res


def sub_40100F(buf):

v9 = []
res = []
for i in range(16):
res.append(0)
for i in range(44):
v9.append(0xCC)
v9[0] = 2;
v9[1] = 3;
v9[2] = 1;
v9[3] = 1;
v9[4] = 1;
v9[5] = 2;
v9[6] = 3;
v9[7] = 1;
v9[8] = 1;
v9[9] = 1;
v9[10] = 2;
v9[11] = 3;
v9[12] = 3;
v9[13] = 1;
v9[14] = 1;
v9[15] = 2;

for i in range(4):
for j in range(4):
v9[4 * i + 24 + j] = buf[4 * i + j]
#for x in v9:
# print(hex(x))
#print()

for k in range(4):
for m in range(4):
tmp = sub_401118(v9[4 * k], v9[m + 24])
v1 = tmp
tmp = sub_401118(v9[4 * k + 1], v9[m + 28])
v2 = tmp ^ v1
tmp = sub_401118(v9[4 * k + 2], v9[m + 32])
v3 = tmp ^ v2
tmp = sub_401118(v9[4 * k + 3], v9[m + 36])
v4 = tmp ^ v3
res[4 * k + m] = v4
return res


def sub_401118(a1, a2):
v5 = 0
for i in range(8):
if a1 & 1 != 0:
v5 = (a2 ^ v5) & 0xFF
v3 = a2 & 0x80
a2 = (a2 * 2) & 0xFF
if v3 != 0:
a2 = (a2 ^ 0x1B) & 0xFF
a1 = (a1 >> 1) & 0xFF
return v5


if __name__ == '__main__':

s1 = '0123456789abcdefghijklmnopqrstuv'
s2 = 'xyzdefghijklmnopqrstuv0123456789'
sub_402070(s2[0:16])
#sub_402070(s2[16:32])

大佬思路

  • dead.ash
    1. 这位大佬的分析思路比较值得参考
    2. 首先单步一遍,理清大概的思路,把大部分的坑都踩一踩
    3. 梳理单步的逻辑,猜测校验流程
    4. 多次输入测试字符串,观察反馈结果
    5. F5开始分析程序
    6. 依次分析算法,并分析出对应的逆算法
  • ThTsOd
    1. 多处下断,梳理主要逻辑
    2. 找到关注点如长度判断与最后用于比较的memcmp
    3. IDA中使用Findcrypt进行算法侦测
    4. 分析与原本算法不同之处,即魔改的地方
    5. 针对魔改处,修改原本算法的解密算法
  • mb_mgodlfyn
    1. F5,梳理main函数逻辑
    2. 分析导致异常的暗装,进行对应的绕过(mb_mgodlfyn对绕过说明的更细)和patch(这部分sunfishi贴出了patch后的代码,几处关键的patch都贴出了)
    3. 与标准的AES做对比进行分析
    4. 参考标准的AES解密算法进行修改并解密

还有部分大佬的就不贴出来了,整体的分析逻辑大差不差,其中wx_孤城的做题技巧值得学习,他通过自旋的方式,成功解密了比较复杂的加密步骤;而sunfishi则细心的patch了隐藏在异常处理中的核心逻辑,并且根据开源的AES项目导入了符号和结构体,看的也更加清晰;以上都是值得学习的思路和技巧。

参考链接

  1. 看雪:Findcrypt安装说明
  2. Github:Findcrypt官网
  3. 简书:看雪CTF签到题SEH异常处理
  4. CSDN:SEH的非常好的总结
  5. 看雪:trackL分析文章
  6. 看雪:mb_mgodlfyn分析文章
  7. Github:lmshao AES
  8. 看雪:AES加密算法
  9. 看雪:密码学基础:AES加密算法
  10. CSDN:AES加密算法的详细介绍与实现
  11. CDSN:AES算法描述及C语言实现
  12. 看雪:sunfishi分析文章
  13. 看雪:wx_孤城
  14. 看雪:IDA为什么产生 sp-analysis failed
  15. 看雪:IDA sp-analysis failed 不能F5的 解决方案之(一)
  16. 看雪:IDA sp-analysis failed 不能F5的 解决方案之(二)
  17. 看雪:用DUMP的方式解决IDA F5失败
  18. 看雪:堂前燕分析文章
Author: cataLoc
Link: http://cata1oc.github.io/2022/05/15/KCTF2022%E6%98%A5%E5%AD%A3%E8%B5%9B%E7%AC%AC%E4%B8%89%E9%A2%98/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶