首页
社区
课程
招聘
[原创]kctf2023 第二题 CN星际基地
2023-9-4 23:40 8476

[原创]kctf2023 第二题 CN星际基地

2023-9-4 23:40
8476

过反调试

ida打开先看初始化部分,有一个反调试,直接patch掉,IsDebuggerPresent的if判断取个反:
图片描述

分析流程

直接调试main函数,主要逻辑如下(以下代码来自ida7.7):

读取输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
v3 = 0i64;
v132 = 0;
v4 = time64(0i64);
srand(v4);
v5 = sub_7FF6DD8E2F10(std::cout, &unk_7FF6DD8E66E8);
v6 = std::ostream::operator<<(v5, sub_7FF6DD8E30E0);
sub_7FF6DD8E2F10(v6, "Serial:\t\t");
v154 = 0i64;
v155 = 15i64;
LOBYTE(Src) = 0;
sub_7FF6DD8E2A90(&Src, &unk_7FF6DD8E6698, 0i64);
LOBYTE(v7) = 10;
v8 = std::ios::widen((char *)&std::cin + *(int *)(std::cin + 4i64), v7);
sub_7FF6DD8E32F0(std::cin, &Src, v8);

打印提示字符:"Serial:\t\t",然后std::cin读取用户输入,存在src中。

判断输入长度是否是156:

1
2
3
4
5
6
7
  if ( v10 != 156 )
  {
LABEL_16:
    v15 = sub_7FF6DD8E2F10(std::cout, "fail");
    std::ostream::operator<<(v15, sub_7FF6DD8E30E0);
    goto LABEL_233;
  }

分割字符串

图片描述

上述代码生成了一个string,根据调试结果得知v25(也就是string)是将输入等分为长度为39的四组,取每组的第i个元素(外层还有while)组成一个四字节的字符串。v26是将v25按照10进制转换为整数,留着后面用。

对于分割可以做如下理解:

1
2
3
4
5
6
7
8
9
# 输入的序列号
1234567812345678
# 分割后的序列号
# 我这里一行只有四个,题目中一行有39个值,因为输入总长度为156,分割为4组
1234
5678
1234
5678
# 此时每轮v25的值就是'1515'、'2626'...以此类推

然后if语句的三个判断条件主要是判断长度和上述分割出来的string中是否包含'2'(50的ascii)

  1. 如果包含'2'则继续
  2. 如果不包含'2'就将其按照2进制转换为整数,判断转换是否成功,并且转换之后的结果不能等于15,否则就退出,15的二进制是'1111',所以输入的字符串分组后不能有'1111'

判断四个二

图片描述

判断分割后的字符串中是否有2,有2的情况下判断有几个2,如果四个都是2就退出,所以输入的字符串分割后不能有'2222'

单调递增

图片描述

v26就是之前将分割后是string按照10进制转换为整数,v130初始值是0,但是每次比较完之后会将v26赋给v130,由于在while循环中,如果break,就会输出fail并退出。
所以这段代码的核心逻辑的意思是每一组分割后的字符串的十进制值都要比前一组的大,所以输入的串根据前面的分割逻辑分割之后的列向量需要是单调递增的
还有一个细节就是v130的初始值是0,v26要大于v130,所以我们的输入的不能包含'0000'

矩阵存储

图片描述

这段逻辑就是将输入的值分为四行,每行39个元素,按照矩阵的形式存入off_7FF6ED1798A8地址中。并且将2转换为-1。
从这里也可以看出我们的输入只能包含'0','1','2'三个元素

循环运算

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
while ( 1 )
      {
        v65 = 0;
        v66 = 0;
        v67 = 0;
        v68 = *(_QWORD *)((char *)off_7FF6ED1798A8 + v64);
        v69 = 2;
        for ( j = 12i64; j < 168; j += 52i64 )
        {
          v71 = *(_DWORD *)(j + v68 - 12);
          if ( v71 == -1 )
          {
            v65 += v69 - 1 == v63;
          }
          else if ( v71 == 1 )
          {
            v66 += v69 - 1 == v63;
          }
          v72 = v71 + v67;
          v73 = *(_DWORD *)(j + v68 - 8);
          if ( v73 == -1 )
          {
            v65 += v69 == v63;
          }
          else if ( v73 == 1 )
          {
            v66 += v69 == v63;
          }
          v74 = v73 + v72;
          v75 = *(_DWORD *)(j + v68 - 4);
          if ( v75 == -1 )
          {
            v65 += v69 + 1 == v63;
          }
          else if ( v75 == 1 )
          {
            v66 += v69 + 1 == v63;
          }
          v76 = v75 + v74;
          v77 = *(_DWORD *)(j + v68);
          if ( v77 == -1 )
          {
            v65 += v69 + 2 == v63;
          }
          else if ( v77 == 1 )
          {
            v66 += v69 + 2 == v63;
          }
          v78 = v77 + v76;
          v79 = *(_DWORD *)(j + v68 + 4);
          if ( v79 == -1 )
          {
            v65 += v69 + 3 == v63;
          }
          else if ( v79 == 1 )
          {
            v66 += v69 + 3 == v63;
          }
          v80 = v79 + v78;
          v81 = *(_DWORD *)(j + v68 + 8);
          if ( v81 == -1 )
          {
            v65 += v69 + 4 == v63;
          }
          else if ( v81 == 1 )
          {
            v66 += v69 + 4 == v63;
          }
          v82 = v81 + v80;
          v83 = *(_DWORD *)(j + v68 + 12);
          if ( v83 == -1 )
          {
            v65 += v69 + 5 == v63;
          }
          else if ( v83 == 1 )
          {
            v66 += v69 + 5 == v63;
          }
          v84 = v83 + v82;
          v85 = *(_DWORD *)(j + v68 + 16);
          if ( v85 == -1 )
          {
            v65 += v69 + 6 == v63;
          }
          else if ( v85 == 1 )
          {
            v66 += v69 + 6 == v63;
          }
          v86 = v85 + v84;
          v87 = *(_DWORD *)(j + v68 + 20);
          if ( v87 == -1 )
          {
            v65 += v69 + 7 == v63;
          }
          else if ( v87 == 1 )
          {
            v66 += v69 + 7 == v63;
          }
          v88 = v87 + v86;
          v89 = *(_DWORD *)(j + v68 + 24);
          if ( v89 == -1 )
          {
            v65 += v69 + 8 == v63;
          }
          else if ( v89 == 1 )
          {
            v66 += v69 + 8 == v63;
          }
          v90 = v89 + v88;
          v91 = *(_DWORD *)(j + v68 + 28);
          if ( v91 == -1 )
          {
            v65 += v69 + 9 == v63;
          }
          else if ( v91 == 1 )
          {
            v66 += v69 + 9 == v63;
          }
          v92 = v91 + v90;
          v93 = *(_DWORD *)(j + v68 + 32);
          if ( v93 == -1 )
          {
            v65 += v69 + 10 == v63;
          }
          else if ( v93 == 1 )
          {
            v66 += v69 + 10 == v63;
          }
          v94 = v93 + v92;
          v95 = *(_DWORD *)(j + v68 + 36);
          if ( v95 == -1 )
          {
            v65 += v69 + 11 == v63;
          }
          else if ( v95 == 1 )
          {
            v66 += v69 + 11 == v63;
          }
          v67 = v95 + v94;
          v69 += 13;
        }
        if ( v65 == v66 )
        {
          **(_DWORD **)((char *)off_7FF6ED1798B8 + v64) = 0;
        }
        else
        {
          v96 = -1;
          if ( v65 < v66 )
            v96 = 1;
          **(_DWORD **)((char *)off_7FF6ED1798B8 + v64) = v96;
        }
        if ( v67 )
          goto LABEL_16;

之后会进入一个超大的循环进行判断,我们调试的时候追踪一下v67的值可以发现他就是我们输入的每一行值之和(当然是2被转换为-1之后的),如果v67不为0,就退出程序,所以可以得出我们输入的每一行当中'1'和'2'的个数要一样,这样当2转换为-1之后,他们之和才会为0。

md5判断

图片描述
最后对输入做了一次md5计算,并且判断md5的值。至此有了思路,根据上述对输入的控制条件,以及最终的md5值,可以爆破出输入的序列号。

总结

上述分析中对于输入需要满足的要求都已经粗体显示了,这里总结一下:

  1. 序列号矩阵的列向量不能包含:'0000','1111','2222'
  2. 序列号矩阵的列向量组成的整数单调递增
  3. 序列号矩阵只能由'0','1','2'三个元素组成
  4. 序列号矩阵的每个行向量中'1'和'2'的个数要一样多

再加上提示中的两个条件:

  1. 列向量组里不存在相反值,如[1,-1,1,0]与[-1,1,-1,0]不能同时存在,对应矩阵存储前的值就是[1,2,1,0]和[2,1,2,0]不能同时存在。
  2. 每个行向量里的 0 的数量 占 1/3,即每行有13个0。

爆破脚本

结合上述条件可以爆破一下,大概两分钟就能跑完。

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
import sys
import copy
import itertools
import hashlib
from itertools import product
 
# 计算md5值
def compute_md5(input_string):
    m = hashlib.md5()
    m.update(input_string.encode('utf-8'))
    return m.hexdigest()
 
# 生成所有由0,1,2组成的四位数,并且不能包含0000,1111,2222
# sorted保持单调递增
def generate_numbers(n):
    return sorted((''.join(p)) for p in product('012', repeat=n) if p != ('0','0','0','0') and p != ('1','1','1','1') and p != ('2','2','2','2'))
 
numbers = generate_numbers(4)
# print(len(numbers))
# print(numbers)
 
# 将所有可能的四位数按照如下a数组的形式打印出来,并且复制给a。
# 我们输入的序列号就在a当中产生,a总共有78列,我们取39列,然后拼接成一行即为序列号
# print a
for i in range(4):
    for number in numbers:
        print(number[i], end='')
    print()
 
a = [
    '000000000000000000000000001111111111111111111111111122222222222222222222222222',
    '000000001111111112222222220000000001111111122222222200000000011111111122222222',
    '001112220001112220001112220001112220001122200011122200011122200011122200011122',
    '120120120120120120120120120120120120120201201201201201201201201201201201201201',
]
 
# 将所有互为相反的列向量的下标以键值对的形式存入reverse字典中
# 该字典中的每对key:value都不能同时存在需要输入的序列号中
# get get the opposite combination
reverse = {}
for i in range(len(numbers)):
    x = (numbers[i]).replace('1', 'temp').replace('2', '1').replace('temp', '2')
    if (x) in numbers:
        reverse[i] = numbers.index((x))
print(len(reverse))
print(reverse)
 
 
# 分别计算第0行元素为0,1,2的所有下标
list_0 = [index for index, char in enumerate(a[0]) if char == '0']
list_1 = [index for index, char in enumerate(a[0]) if char == '1']
list_2 = [index for index, char in enumerate(a[0]) if char == '2']
 
def check(combo):
    r = ''
    for row in a:
        s = [row[i] for i in combo]
 
        # 1和2的个数相等
        # 由于有1/3的0所以0,1,2的个数都是13个
        if s.count('1') == s.count('2') == 13:
            r += ''.join(s)
        else:
            return False
 
    if len(r) == 0x9C:
        # print(r)
        if compute_md5(r) == 'aac82b7ad77ab00dcef90ac079c9490d':
            print(f'flag: {r}')
            return True
        else:
            return False
 
# 每个选中的下标数组中都不能同时包含reverse的键值对
def check_combo(combo):
    for i in combo:
        if i in reverse and reverse[i] in combo:
            return False
    return True
 
 
# 只需要爆破长度为39的下标数组,然后从a中选择这39个列,拼接成一行即为序列号
# 由于1,2的个数相等,0占了1/3,所以0,1,2的个数都是13个
# 由于单调递增,所以第一行一定是:000000000000011111111111112222222222222
# 所以爆破的时候从第一行可以分为0,1,2三组来爆破,每组取13个值即可
for combo0 in itertools.combinations(list_0, 13):
    combo0 = list(combo0)
    # 0相反元素还是0,所以需要检查自己是否包含相反元素
    if not check_combo(combo0):
        continue
 
    for combo1 in itertools.combinations(list_1, 13):
        combo1 = list(combo1)
 
        # 2相反元素是1
        # 所以需要从2开头的下标数组中,把和已选中的1开头的下标数组相反的下标移除
        y = copy.deepcopy(list_2)
        for i in combo1:
            if i in reverse:
                if reverse[i] in y:
                    y.remove(reverse[i])
 
        if len(y) < 13:
            continue
 
        for combo2 in itertools.combinations(y, 13):
            combo2 = list(combo2)
 
            if check(sorted(combo0 + combo1 + combo2)):
                sys.exit()
# flag: 000000000000011111111111112222222222222000011111111100001122222220000011222222011100011122200120200112220112202001122101201201201212001212020120121202010201

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 2
打赏
分享
最新回复 (3)
雪    币: 230
活跃值: (237)
能力值: ( LV3,RANK:21 )
在线值:
发帖
回帖
粉丝
hesl 2023-9-5 14:08
2
0
          v138 = 0x67452301;
          v139 = 0xEFCDAB89;
          v140 = 0x98BADCFE;
          v141 = 0x10325476;
          v107 = v149;
          if ( v151 >= 0x10 )
            v107 = (void **)v149[0];
          sub_1400036A0(v137, v107, v150, v97);
          v108 = sub_1400034C0(v137);

你好,请问是如何判断这个是求MD5的函数; 是v138至v141这4个特征数字吗?

雪    币: 2259
活跃值: (1075)
能力值: ( LV8,RANK:129 )
在线值:
发帖
回帖
粉丝
Zero*/ 2 2023-9-5 18:14
3
0
hesl &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...
对的,md5特征值是这个,还可以复制这段算法给ai,他会从逐行给你分析,结合md5的算法过程也能确认,再加上组后那段字符串长得也像md5的串所以就可以判断
雪    币: 20199
活跃值: (29735)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-9-6 09:31
4
1
感谢分享
游客
登录 | 注册 方可回帖
返回