-
-
[原创]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); / / 9 个 3 * 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编辑
,原因:
赞赏
他的文章
看原图