首页
社区
课程
招聘
[原创]KCTF2021 春季赛 第四题 英雄救美 WP
2021-5-16 21:53 9202

[原创]KCTF2021 春季赛 第四题 英雄救美 WP

2021-5-16 21:53
9202

这道题本人认为还是比较有意思的,一道数独题,不过也有一些考虑不周的地方,数独的解应该是唯一的,不过注册码到数独的输入数据的转换有些瑕疵,所以不唯一,虽然用的是注册码的MD5解码代码,只有正确的注册码才能显示成功,但能过数独的多个注册码都能进入到SHELLCODE中去,表现为程序崩溃.

 

本文重点还是分析关于多个注册码能过数独验证的的部分,分析见后面程序中的注释:

 

主程序:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int lenSn; // kr00_4
  int v4; // ecx
  __m128i *v5; // esi
  int v6; // edi
  void (*v8)(void); // [esp+Ch] [ebp-2CCh]
  int v9[22]; // [esp+10h] [ebp-2C8h] BYREF
  int iTab[128]; // [esp+68h] [ebp-270h] BYREF
  BYTE md5Sn[16]; // [esp+268h] [ebp-70h] BYREF
  char sn[92]; // [esp+278h] [ebp-60h] BYREF
 
  printf("\t\t\t看雪CTF大赛\r\n");
  printf("\t\t祝愿看雪CTF大赛越办越好\r\n");
  printf("Serial: ");                           // 输入SN
  scanf_s("%s", sn);                            // 取SN长度
  lenSn = strlen(sn);
  if ( lenSn <= 64 && sub_401240(lenSn, (int)sn, iTab) == 1 && sub_401000((int)iTab, lenSn - 9) == 1 )
  {                                             // SN长度<=64
                                                // sub_401240 对输入的SN变换,转为长度0x37的数值大小(1-9)数组,输入正确返回1
                                                // sub_401000 对输入的0x37个数据依次填入9*9的数独二维数组中为0的位置,并验证数独的正确性,成功返回1
    *(_OWORD *)md5Sn = 0i64;
    memset(v9, 0, sizeof(v9));
    v9[5] = 0;
    v9[4] = 0;
    v9[0] = 0x67452301;
    v9[1] = 0xEFCDAB89;
    v9[2] = 0x98BADCFE;
    v9[3] = 0x10325476;
    sub_4014E0((int)sn, (int)v9, lenSn);
    sub_4015B0((int)md5Sn, (int)v9);            // 计算SN的MD5
    sub_401ED0((int)iTab, v4, md5Sn);           // 用md5Sn初始化解密数据
    v8 = (void (*)(void))VirtualAlloc(0, 0x620u, 0x1000u, 0x40u);
    v5 = (__m128i *)v8;
    v6 = 98;
    do
    {
      *v5 = _mm_loadu_si128((__m128i *)((char *)v5 + &unk_4181A0 - (_UNKNOWN *)v8));
      ((void (__fastcall *)(int *, __m128i *))loc_4028B0)(iTab, v5++);// 对代码解码
      --v6;
    }
    while ( v6 );
    v8();                                       // 执行解密后的代码;
                                                // 由于SN转换数独数据的缺陷导致能过数独验证的SN本身就不是唯一,并且后面没有明确的正确性验证,因此解码数据不唯一,在非预期SN时程序会跑飞
  }
  return 0;
}

SN转数独数据:

1
2
3
4
5
由分析发现SN到数独填充数据的转换过程是按行读取,共9行,每行为一组,由效验字符结束,即当前行所需填充的数据个数+效验字符=9,并且SN到独数数据为SN字符在每组9个字符中的位置(1-9),不过分析中发现2个BUG:
 
1.本来是一行一组转换字符表(一组9个),不过查表过程中没有限制查表仅在本组9个字符中,而是在之后的所有组中,也就是说仅最后一组应该是预期(唯一),之前的8组查表数据可以为之后的所有组中的字符,这样SN到数独数据的转换就不是唯一,且SN越靠前的重复的可能越高,最高一个数字可能有9个不同的值.
 
2.每行数据的效验结束符其实并没有起作用,因为最后从输入数据序列中取出数据填充到数独表中时并没有考虑按对应的行读取,而只是整体作为一个一维数组来读取,所以行效验结束符其实可以没有,但因为预期有9个行结束符,而在填充数独数据时从总长度中减去了9个,因此在没有行结束验证字符时要在SN后随机加入字符(需在字符转换表中存在)才能过数独验证
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
int __usercall sub_401240@<eax>(int a1@<edx>, int a2@<ecx>, int *a3)
{
  int v3; // ebx
  int v4; // esi
  unsigned int v5; // edi
  char v6; // al
  signed int v7; // ecx
  int v9; // ecx
  int v10; // [esp+0h] [ebp-64h]
  int v11; // [esp+4h] [ebp-60h]
  __int128 v13[5]; // [esp+Ch] [ebp-58h]
  char v14; // [esp+5Ch] [ebp-8h]
 
  v13[0] = (__int128)_mm_load_si128((const __m128i *)&xmmword_416280);// 字符转换表,共9组,每组9个,每组数据对应数独数据的9行,每行数字的转换在当前组中的9个数中查表得到序数就是数独的填充数据
  v3 = 0;
  v13[1] = (__int128)_mm_load_si128((const __m128i *)&xmmword_4162A0);
  v4 = 0;
  v11 = a1;
  v10 = a2;
  v14 = 'q';
  v13[2] = (__int128)_mm_load_si128((const __m128i *)&xmmword_416270);
  v13[3] = (__int128)_mm_load_si128((const __m128i *)&xmmword_416290);
  v13[4] = (__int128)_mm_load_si128((const __m128i *)&xmmword_416260);
  if ( a1 <= 0 )
    return 1;
  v5 = 0;
  while ( 1 )
  {
    v6 = *(_BYTE *)(v4 + a2);                   // 依次从SN中取字符
    if ( v6 > '0' && v6 <= '9' )                // 如果是数字表示本行结束,不过本行数字个数+结束数字要等于9
      break;
    v7 = v5;
    if ( v5 >= 0x51 )
      return 0;
    while ( v6 != *((_BYTE *)v13 + v7) )
    {
      if ( (unsigned int)++v7 >= 0x51 )         // 不在表中的输入字符无效,返回0,不过这儿查表的数据没有限制在本组9个内,而是在当前组及之后的组内查询,可能会多解
        return 0;
    }
    v9 = v7 % 9 + 1;                            // 序号为0-8,对应数独数据的1-9
    if ( v9 == -1 )
      return 0;
    *a3 = v9;                                   // 保存一个数据
    a2 = v10;
    ++v3;
    ++a3;                                       // 下一个
    a1 = v11;
LABEL_13:
    if ( ++v4 >= a1 )                           // 数据取完,返回成功
      return 1;
  }
  if ( v3 + v6 == '9' )                         // 行结束判断,本行数据个数+最后的数字要为9
  {
    v3 = 0;
    v5 += 9;                                    // 数字转换表指向下一组
    goto LABEL_13;
  }
  return -1;
}

数独数据填充及验证:

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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
int __fastcall sub_401000(int a1, int a2)
{
  int v2; // esi
  int *v3; // edi
  int v4; // eax
  int v5; // eax
  int v6; // eax
  int v7; // eax
  int v8; // eax
  int v9; // eax
  int v10; // eax
  int v11; // eax
  int v12; // eax
  int (*v13)[9]; // eax
  int v14; // edi
  int v15; // esi
  int *v16; // ebx
  int v17; // ecx
  int v18; // edx
  int v19; // eax
  int *v20; // eax
  int v21; // edi
  int *v22; // ebx
  int *v23; // ecx
  int v24; // eax
  int v25; // esi
  BYTE *v26; // esi
  int v27; // edx
  int v28; // ebx
  int v29; // edi
  int v30; // ecx
  char *v31; // ebx
  int *v32; // edx
  int v33; // eax
  int v34; // ecx
  int v35; // ecx
  int v36; // eax
  int v38; // [esp+Ch] [ebp-Ch]
  int *v39; // [esp+10h] [ebp-8h]
  int v40; // [esp+10h] [ebp-8h]
  int (*v41)[9]; // [esp+14h] [ebp-4h]
  int *v42; // [esp+14h] [ebp-4h]
  int v43; // [esp+14h] [ebp-4h]
 
  v2 = 0;
  v3 = &dword_4187C0[0][1];
  do                                            // 遍历9*9的格子,一行9个数,为0就需要重输入序列中取一个填入
                                                // 不过所有输入在一个队列中,只要个数不小于所需求的个数就行,所以之前转换SN到数独数据时按行读(需要行结束符)并无意义
  {
    if ( !*(v3 - 1) )
    {
      v4 = *(_DWORD *)(a1 + 4 * v2++);
      *(v3 - 1) = v4;
    }
    if ( !*v3 )
    {
      v5 = *(_DWORD *)(a1 + 4 * v2++);
      *v3 = v5;
    }
    if ( !v3[1] )
    {
      v6 = *(_DWORD *)(a1 + 4 * v2++);
      v3[1] = v6;
    }
    if ( !v3[2] )
    {
      v7 = *(_DWORD *)(a1 + 4 * v2++);
      v3[2] = v7;
    }
    if ( !v3[3] )
    {
      v8 = *(_DWORD *)(a1 + 4 * v2++);
      v3[3] = v8;
    }
    if ( !v3[4] )
    {
      v9 = *(_DWORD *)(a1 + 4 * v2++);
      v3[4] = v9;
    }
    if ( !v3[5] )
    {
      v10 = *(_DWORD *)(a1 + 4 * v2++);
      v3[5] = v10;
    }
    if ( !v3[6] )
    {
      v11 = *(_DWORD *)(a1 + 4 * v2++);
      v3[6] = v11;
    }
    if ( !v3[7] )
    {
      v12 = *(_DWORD *)(a1 + 4 * v2++);
      v3[7] = v12;
    }
    if ( v2 >= a2 )
      break;
    v3 += 9;
  }
  while ( (int)v3 < (int)&unk_418908 );         // 9
  v13 = dword_4187C0;
  v14 = 0;
  v41 = dword_4187C0;
  while ( 2 )                                   // 检查每一行,不能有重复的数字
  {
    v15 = 1;
    v16 = (int *)v13;
    do
    {
      v17 = v15;
      if ( v15 < 9 )
      {
        v18 = *v16;
        while ( v18 )                           // 不能为0,也就是没有填的空
        {
          v19 = dword_4187C0[v14][v17];
          if ( !v19 || v18 == v19 )             // 比较本行中当前列之后的数不能有0,不能重复
            break;                              // 验证失败,则返回0
          if ( ++v17 >= 9 )
            goto LABEL_30;
        }
        return 0;
      }
LABEL_30:
      ++v15;
      ++v16;                                    // 下一列
    }
    while ( v15 < 10 );                         // 9
    ++v14;
    v13 = v41 + 1;
    v41 = v13;                                  // 下一行
    if ( (int)v13 < (int)&unk_418904 )          // 9
      continue;
    break;
  }
  v20 = dword_4187C0[1];
  v39 = dword_4187C0[1];
  while ( 2 )                                   // 检查每一列,不能有重复的数字
  {
    v21 = 1;
    v42 = v20;
    v22 = v20 - 9;
    v23 = v20;
    do
    {
      v24 = v21;
      if ( v21 < 9 )
      {
        v25 = *v22;
        while ( v25 && *v23 && v25 != *v23 )    // 本列中当前行之后的数字不能同前面有重复
        {
          ++v24;
          v23 += 9;
          if ( v24 >= 9 )
          {
            v23 = v42;
            goto LABEL_41;
          }
        }
        return 0;                               // 验证失败,则返回0
      }
LABEL_41:
      ++v21;
      v23 += 9;                                 // 指向下一行
      v22 += 9;
      v42 = v23;
    }
    while ( v21 < 10 );
    v20 = v39 + 1;                              // 指向下一列
    v39 = v20;
    if ( (int)v20 < (int)dword_4187C0[2] )
      continue;
    break;
  }
  v26 = (BYTE *)malloc(0xAu);                   // 分配计数数组[0-9],对小方格中的数字出现次数计数,0不能出现,1-9个数字必须且只能出现一次,也就是计数数组最后为[0,1,1,1,1,1,1,1,1,1]
  if ( v26 )
  {
    v27 = 0;
    v28 = 0;
    v43 = 0;
    v38 = 0;
LABEL_45:
    v29 = 0;
LABEL_46:
    *(_QWORD *)v26 = 0i64;
    *((_WORD *)v26 + 4) = 0;
    if ( !__OFSUB__(v27, v27 + 3) )
    {
      v30 = 3;                                  // 小方格共3
      v31 = (char *)dword_4187C0 + 4 * v28 + 4 * v29;// 指向3*3小方格数据开始位置
      v40 = 3;
      do
      {
        if ( !__OFSUB__(v29, v29 + 3) )
        {
          v32 = (int *)v31;
          v33 = 3;                              // 小方格一行3个数
          do
          {
            v34 = *v32++;                       // 数字做索引,对计数数组计数加一
            ++v26[v34];                         // 计数加1
            --v33;
          }
          while ( v33 );
          v30 = v40;
        }
        v31 += 36;
        v40 = --v30;
      }
      while ( v30 );
      v27 = v43;
      v28 = v38;
    }
    if ( !*v26 )                                // 第一个计数要为0, 也就是小方格中0不能出现
    {
      v35 = 0;
      v36 = 1;
      while ( (char)v26[v36] <= 1 )             // 计数不能大于1,也就是说小方格中不能有重复的数字
      {
        ++v36;
        ++v35;
        if ( v36 >= 10 )                        // 本小方格中所有1-9的数字都出现且仅出现1次,验证通过
        {
          if ( v35 != 9 )
            break;
          v29 += 3;
          if ( v29 < 9 )
            goto LABEL_46;
          v28 += 27;                            // 指向下一个小方格
          v27 += 3;
          v43 = v27;
          v38 = v28;
          if ( v28 < 81 )
            goto LABEL_45;
          free(v26);                            // 93*3的小方格都没有重复的,数独验证通过,返回1
          return 1;
        }
      }
    }
    free(v26);
  }
  return 0;                                     // 验证失败,返回0
}

由以上分析可知:
1.本程序实为数独填充;
2.数独填充数据为SN中每个字符在每行数据(对应每组9个字符中的序号决定1-9);
3.每行数据由校验字符结束(本行所需数字个数+校验数字=9);
4.虽然能过数独验证的SN有多个,不过要能正确显示成功的SN还是要为预期,因为是用SN的MD5值对程序解码,只有预期SN解码出的代码才能正常运行,也就是说SN一定要有行结束验证符,且第N行所需的数字要在第N组字符转换表中.

 

在网上借用了个暴力解数独的程序,改了下,加上字符转换得KEYGEN程序:

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
#原始数独数据,0就是还没填的
grid0 = [
    [0, 4, 0, 7, 0, 0, 0, 0, 0],
    [9, 2, 0, 0, 0, 0, 6, 0, 7],
    [8, 3, 0, 0, 0, 5, 4, 0, 0],
    [0, 1, 0, 0, 0, 3, 0, 0, 0],
    [0, 0, 0, 2, 0, 1, 0, 0, 0],
    [0, 0, 0, 5, 0, 0, 0, 4, 0],
    [0, 0, 4, 9, 0, 0, 0, 7, 1],
    [3, 0, 5, 0, 0, 0, 0, 9, 4],
    [0, 0, 0, 0, 0, 8, 0, 6, 0],
]
 
def findNextCellToFill(grid, i, j):
    for x in range(i,9):
        for y in range(j,9):
            if grid[x][y] == 0:
                return x,y
    for x in range(0,9):
        for y in range(0,9):
            if grid[x][y] == 0:
                return x,y
    return -1,-1
 
def isValid(grid, i, j, e):
    rowOk = all([e != grid[i][x] for x in range(9)])
    if rowOk:
        columnOk = all([e != grid[x][j] for x in range(9)])
        if columnOk:
            secTopX, secTopY = 3 *(i//3), 3 *(j//3)             for x in range(secTopX, secTopX+3):
                for y in range(secTopY, secTopY+3):
                    if grid[x][y] == e:
                        return False
            return True
    return False
 
def solveSudoku(grid, i=0, j=0):
    i,j = findNextCellToFill(grid, i, j)
    if i == -1:
        return True
    for e in range(1,10):
        if isValid(grid,i,j,e):
            grid[i][j] = e
            if solveSudoku(grid, i, j):
                return True
            # Undo the current cell for backtracking
            grid[i][j] = 0
    return False
 
grid = []
for y in range(9):
    grid.append(grid0[y][:])
solveSudoku(grid)
#得到填好后的数据:
'''
grid = [
    [5, 4, 6, 7, 1, 9, 2, 3, 8],
    [9, 2, 1, 8, 3, 4, 6, 5, 7],
    [8, 3, 7, 6, 2, 5, 4, 1, 9],
    [7, 1, 8, 4, 6, 3, 9, 2, 5],
    [4, 5, 3, 2, 9, 1, 7, 8, 6],
    [6, 9, 2, 5, 8, 7, 1, 4, 3],
    [2, 8, 4, 9, 5, 6, 3, 7, 1],
    [3, 6, 5, 1, 7, 2, 8, 9, 4],
    [1, 7, 9, 3, 4, 8, 5, 6, 2],
]
'''
#9组,每组9个共 9*9=81个,字符转换表
sTab = "$BPV:ubfYp}]DtN>aT^MGmJQ#*Hr`O'wjic0!hdy{oZz-@n+?&%s_/g<e[W)XUxRFSLRA;.l=CEkvK-(q"
 
sn = ''
for y in range(9):
    ct = 0
    for x in range(9):
        if grid0[y][x] == 0:      #需填的
            d = grid[y][x]        #所填的
            sn += sTab[y*9+d-1]   #在第y组的9个中查找第d-1个
            ct += 1
    sn += chr(0x39 - ct % 9)      #计算行结束校验字
print(sn)

得到SN:
:u$YBPf2pa]Dt4#QM^H4ic'j0`w2y{d-Zzo2%/n_s@+2<UW)e4AR;F.4=-qEkvC2


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2021-5-16 21:57 被AloneWolf编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (1)
游客
登录 | 注册 方可回帖
返回