看雪.TSRC 2017CTF秋季赛第五题WP
说来惭愧,这题只能写写我的做法,好多我也没有分析出来,看得好心烦。
查找重点流程
看图标就知道MFC写的窗体程序。拖入IDA,入口都不想去翻,直接查看字符串,好多。稍微翻下能看到KanXueCrackMe2017
字样和一串类似base64的字串,追到sub_406FC3
,像是验证算法。
再往上追,到sub_4071FD
,这时我比较确信这是主流程,而下层那个应该就是算法,看伪代码:
signed int __thiscall sub_4071FD(_DWORD *this)
{
_DWORD *v1; // esi
signed int result; // eax
v1 = this;
CWnd::UpdateData(1);
result = sub_406FC3(v1);
switch ( result )
{
case 1:
return CWnd::MessageBoxW(v1, (LPCWSTR)&unk_44BE10, &Caption, 0);// 成功
case 0:
return CWnd::MessageBoxW(v1, (LPCWSTR)&unk_44BE18, &Caption, 0);// 失败
case 2:
result = CWnd::MessageBoxW(v1, &Text, &Caption, 0);// 很遗憾,离成功还差一小步@¥@
break;
}
return result;
}
主流程根据算法流程的返回不同弹出三个不同的弹窗。
算法流程
直接贴上算法流程的伪代码,菜鸡如我,只会f5。
signed int __thiscall sub_406FC3(_DWORD *this)
{
_DWORD *v1; // ebx
_WORD *v2; // eax
int v3; // esi
int i; // ecx
size_t v5; // kr00_4
signed int result; // eax
signed int j; // [esp+24h] [ebp-D9F8h]
int v9[48]; // [esp+28h] [ebp-D9F4h]
char v11; // [esp+D9A8h] [ebp-74h]
char v12; // [esp+D9D0h] [ebp-4Ch]
__int16 v13; // [esp+D9E2h] [ebp-3Ah]
char v14[28]; // [esp+D9E4h] [ebp-38h]
CPPEH_RECORD ms_exc; // [esp+DA04h] [ebp-18h]
v1 = this;
sub_407307(v9);
strcpy(&v12, "KanXueCrackMe2017");
v13 = 0;
sub_4084A3((int)v9, &v12);
v2 = (_WORD *)v1[38];
v3 = *((_DWORD *)v2 - 3);
for ( i = 0; i < v3; ++v2 )
{
if ( i < 0 || i > v3 )
ATL::AtlThrowImpl(0x80070057);
v14[i++] = *v2;
}
v14[i] = 0;
__asm { int 2Dh; Windows NT - debugging services: eax = type }
ms_exc.registration.TryLevel = 0;
v5 = strlen(v14);
for ( j = 0; j < 1000; ++j )
{
sub_4031E6((int)&v11, (int)v14, v5); //base64
memcpy_0(v14, &v11, v5);
}
if ( !strcmp("Vm0wd2QyUXlVWGw==", &v11) )
{
result = 1;
ms_exc.registration.TryLevel = -2;
}
else
{
ms_exc.registration.TryLevel = -2;
result = 0;
}
return result;
}
先不说具体算法,先看流程。
似乎最后strcmp("Vm0wd2QyUXlVWGw==", &v11)
这比较成功,就行了。
但再看中间的__asm { int 2Dh; Windows NT - debugging services: eax = type }
这段。显示的调试坑啊。为了静态方便看正确的流程,我patch下代码,至于动态,网上说加了插件的OD直接过,我是把int 2D
改成int3
,再设置pass int3
过的,不然过不了,哎。
signed int __thiscall sub_406FC3(_DWORD *this)
{
_DWORD *v1; // ebx
_WORD *v2; // eax
int v3; // esi
int i; // ecx
signed int v5; // eax
signed int result; // eax
int v7[48]; // [esp+28h] [ebp-D9F4h]
int v8[13872]; // [esp+E8h] [ebp-D934h]
char v9; // [esp+D9D0h] [ebp-4Ch]
__int16 v10; // [esp+D9E2h] [ebp-3Ah]
char v11[28]; // [esp+D9E4h] [ebp-38h]
CPPEH_RECORD ms_exc; // [esp+DA04h] [ebp-18h]
v1 = this;
sub_407307(v7);
strcpy(&v9, "KanXueCrackMe2017");
v10 = 0;
sub_4084A3((int)v7, &v9);
v2 = (_WORD *)v1[38];
v3 = *((_DWORD *)v2 - 3);
for ( i = 0; i < v3; ++v2 )
{
if ( i < 0 || i > v3 )
ATL::AtlThrowImpl(0x80070057);
v11[i++] = *v2;
}
v11[i] = 0;
ms_exc.registration.TryLevel = 1;
sub_4084A3((int)v7, v11);
v5 = 0;
do
{
if ( v7[v5] != v8[v5] )
{
result = 0;
goto LABEL_7;
}
++v5;
}
while ( v5 < 48 );
if ( sub_40680C(v11) == 1 && strlen(v11) == 12 )
{
result = 1;
LABEL_7:
ms_exc.registration.TryLevel = -2;
return result;
}
return 2;
}
还是先看流程。校验一共有两处:一是v7
与v8
的48字节比较,不正确就返回0
;二是v11
长度是否为12
及sub_40680C(v11)
是否返回1
,若不是则返回2,是则返回1,即校验成功。
那v7
、v8
及v11
又是什么呢。下面开始理算法。
算法分析OR猜测
通过动静结合分析,v7
、v8
为sub_407307
初始化的48字节数组,其16进制表示为:
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F
v8
后由sub_4084A3
函数两次(实际上内部是多次)修改。而v11
则是输入。
这里我们得到第一个比较明确的条件:
len(input) == 12
为了弄清楚v8
的最后值与输入的关系,得先弄清楚sub_407307
和sub_4084A3
这两个函数。
sub_407307
除了初始化了v7
,v8
,还通过v7
另外6个数组生成了6个48x48变换矩阵。这6组数组如下:
m0=[
0x06, 0x07, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x08, 0x09, 0x0A, 0x0B, 0x22, 0x23, 0x24, 0x0F,
0x0E, 0x11, 0x12, 0x13, 0x14, 0x15, 0x0C, 0x0D, 0x16, 0x17, 0x10, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
0x20, 0x21, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F]
m1=[
0x10, 0x11, 0x12, 0x03, 0x04, 0x05, 0x06, 0x07, 0x0E, 0x0F, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
0x2C, 0x2D, 0x2E, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
0x00, 0x01, 0x02, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x20, 0x21, 0x22, 0x2F]
m2=[
0x00, 0x01, 0x1A, 0x1B, 0x1C, 0x05, 0x06, 0x07, 0x08, 0x09, 0x02, 0x03, 0x04, 0x0D, 0x0E, 0x0F,
0x16, 0x17, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x18, 0x19, 0x2A, 0x2B, 0x2C, 0x1D, 0x1E, 0x1F,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x0A, 0x0B, 0x0C, 0x2D, 0x2E, 0x2F]
m3=[
0x00, 0x01, 0x02, 0x03, 0x24, 0x25, 0x26, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x04, 0x05, 0x06, 0x17, 0x1E, 0x1F, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
0x20, 0x21, 0x22, 0x23, 0x28, 0x29, 0x2A, 0x27, 0x14, 0x15, 0x16, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F]
m4=[
0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x0E, 0x0F, 0x28, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x2E, 0x2F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x00, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x06, 0x07,
0x26, 0x27, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x18, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x1E, 0x1F]
m5=[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x12, 0x13, 0x14, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x1C, 0x1D, 0x1E, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x26, 0x27, 0x20, 0x1F,
0x0A, 0x21, 0x22, 0x23, 0x24, 0x25, 0x08, 0x09, 0x2E, 0x2F, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D]
再看sub_4084A3
。
void __stdcall sub_4084A3(int a1, const char *a2)
{
signed int v2; // ebx
int v3; // edi
char v4; // al
char *v5; // [esp+0h] [ebp-4h]
signed int v6; // [esp+10h] [ebp+Ch]
v5 = (char *)malloc(3 * strlen(a2));
sub_40829C(a2, (int)v5);
v6 = strlen(v5);
v2 = 0;
if ( v6 > 0 )
{
v3 = v6;
do
{
v4 = v5[v2];
if ( v4 < 48 || v4 > 57 )
{
if ( v4 >= 65 && v4 <= 72 )
v3 = v4 - 55;
}
else
{
v3 = v4 - 48;
}
sub_4080DF(v3 / 3, v3 % 3 + 1, a1);
++v2;
}
while ( v2 < v6 );
}
free(v5);
}
粗跟下,sub_40829C
似乎是一个转换,再将转换结果用作sub_4080DF
的矩阵计算。
sub_4080DF
的变换是以v3/3
值为索引选择矩阵变换表,v3%3+1
则是转换次数,变换目标为前面说的v8
,变换实际上就是矩阵乘法。
那sub_40829C
的字符变换到底是一个什么过程呢。细跟了一次,没什么头绪,除了发现里面有点像是大数运算操作。
不断翻看代码。注意到矩阵运算前的v3
是由转换后的字符算术运算得到的,运算规则是:
若字符为数字 则v3 = v4-48
若字符为A-H 则v3 = v4-55
那转换后的字符范围是0-9 A-H
,v3范围是0-17
。而在sub_40829C
中也有类似的计算:
if ( v3 < 48 || v3 > 57 )
{
if ( v3 < 65 || v3 > 90 )
{
if ( v3 < 97 || v3 > 122 )
v4 = -1;
else
v4 = v3 - 61;
}
else
{
v4 = v3 - 55;
}
}
else
{
v4 = v3 - 48;
}
其中的v3
是取的输入字符,其范围是0-9 A-Z a-z
,v4
范围是0-61
。
似乎转换后的字符是18进制,其它的还是没有头绪。后经人提醒,加上验证,输入字符应该是62进制字串,转换过程就是62进制到18进制的转换,这就与上面的代码全对上了。这个可以从1个字节、2个字节输入慢慢测出来。
开始看到两个校验,且两个校验的输入是全输入,就想能不能通过一个校验就能得出答案。通常有两个校验的情况不外乎这么两种:一是两个校验均不能得到唯一解,只能相互补充条件得到唯一解;二是其中一个校验是冗余校验,且难于反解出答案。如果此题是第一种情况,可以尝试爆破,从校验一结果中找到唯一解;如果是另一种情况,那只能先看看校验一能不能解了,反正校验二看了更蒙。
刚才说了校验一处的v8
由sub_4084A3
分别以KanXueCrackMe2017
和输入改变了两次。改变过程就是多次的矩阵变换,实际上就是由参数控制实现两次互逆的变换。
看了很多矩阵的相关知识,没找到办法。后来仔细分析了变换矩阵,原来此中的绝技。以第一个变换矩阵来说,它是由第一个变换数组得到的:
m0=[
0x06, 0x07, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x08, 0x09, 0x0A, 0x0B, 0x22, 0x23, 0x24, 0x0F,
0x0E, 0x11, 0x12, 0x13, 0x14, 0x15, 0x0C, 0x0D, 0x16, 0x17, 0x10, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
0x20, 0x21, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F]
看前8个元素,每变换一次,它们都是循环后移2个位置,m0[0:5]
后移,m0[6:7]
循环移动到前面,这样,每4次变换就是一个还原周期,其它位置的元素同样如此。
如:
>>> b*m0
matrix([[ 6, 7, 0, 1, 2, 3, 4, 5, 8, 9, 10, 11, 34, 35, 36, 15, 14,
17, 18, 19, 20, 21, 12, 13, 22, 23, 16, 27, 28, 29, 30, 31, 32, 33,
24, 25, 26, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]])
>>> b*m0*m0
matrix([[ 4, 5, 6, 7, 0, 1, 2, 3, 8, 9, 10, 11, 24, 25, 26, 15, 36,
17, 18, 19, 20, 21, 34, 35, 12, 13, 14, 27, 28, 29, 30, 31, 32, 33,
22, 23, 16, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]])
>>> b*m0*m0*m0
matrix([[ 2, 3, 4, 5, 6, 7, 0, 1, 8, 9, 10, 11, 22, 23, 16, 15, 26,
17, 18, 19, 20, 21, 24, 25, 34, 35, 36, 27, 28, 29, 30, 31, 32, 33,
12, 13, 14, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]])
>>> b*m0*m0*m0*m0
matrix([[ 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]])
对其它组变换矩阵进行了测试,同样如此。
也就是说
m[i]*m[i]*m[i]*m[i] = I
那就好办了。
运行记录第一次的变换操作
00408516 COND: index = 00000004
0040851C COND: count = 00000003
00408516 COND: index = 00000004
0040851C COND: count = 00000002
00408516 COND: index = 00000003
0040851C COND: count = 00000002
00408516 COND: index = 00000005
0040851C COND: count = 00000003
00408516 COND: index = 00000004
0040851C COND: count = 00000003
00408516 COND: index = 00000001
0040851C COND: count = 00000002
00408516 COND: index = 00000001
0040851C COND: count = 00000003
00408516 COND: index = 00000000
0040851C COND: count = 00000001
00408516 COND: index = 00000004
0040851C COND: count = 00000001
00408516 COND: index = 00000002
0040851C COND: count = 00000002
00408516 COND: index = 00000001
0040851C COND: count = 00000002
00408516 COND: index = 00000000
0040851C COND: count = 00000002
00408516 COND: index = 00000005
0040851C COND: count = 00000002
00408516 COND: index = 00000005
0040851C COND: count = 00000003
00408516 COND: index = 00000001
0040851C COND: count = 00000002
00408516 COND: index = 00000001
0040851C COND: count = 00000002
00408516 COND: index = 00000000
0040851C COND: count = 00000002
00408516 COND: index = 00000004
0040851C COND: count = 00000003
00408516 COND: index = 00000000
0040851C COND: count = 00000002
00408516 COND: index = 00000000
0040851C COND: count = 00000002
00408516 COND: index = 00000003
0040851C COND: count = 00000003
00408516 COND: index = 00000005
0040851C COND: count = 00000003
00408516 COND: index = 00000002
0040851C COND: count = 00000003
00408516 COND: index = 00000001
0040851C COND: count = 00000002
整理下,列表中的前面数值表示变换矩阵序号,后面数值表示变换次数:
lop1 = [[4,3],[4,2],[3,2],[5,3],[4,3],[1,2],[1,3],[0,1],[4,1],
[2,2],[1,2],[0,2],[5,2],[5,3],[1,2],[1,2],[0,2],[4,3],
[0,2],[0,2],[3,3],[5,3],[2,3],[1,2]]
看着这个就能直接写出逆变换操作:
lop2 = [[1,2],[2,1],[5,1],[3,1],[0,2],[0,2],[4,1],[0,2],[1,2],
[1,2],[5,1],[5,2],[0,2],[1,2],[2,2],[4,3],[0,3],[1,1],
[1,2],[4,1],[5,1],[3,2],[4,2],[4,1]]
计算出来的输入位数太多。细看操作列表,其中有好一些是可以合并操作的,进行合并,得新的操作列表:
lop3 = [[1,2],[2,1],[5,1],[3,1],[4,1],[0,2],[5,3],[0,2],[1,2],
[2,2],[4,3],[0,3],[1,3],[4,1],[5,1],[3,2],[4,3]]
出来结果校验直接通过,出乎意料,唯一结果。
校验二的代码现在是实在不想看,开始似乎看到了ecc,也是大数,如果有时间再补充吧。
最后给出部分代码:
import string
t18 = '0123456789ABCDEFGH'
t62 = string.digits+string.uppercase+string.lowercase
lop1 = [[4,3],[4,2],[3,2],[5,3],[4,3],[1,2],[1,3],[0,1],[4,1],
[2,2],[1,2],[0,2],[5,2],[5,3],[1,2],[1,2],[0,2],[4,3],
[0,2],[0,2],[3,3],[5,3],[2,3],[1,2]]
lop2 = [[1,2],[2,1],[5,1],[3,1],[4,1],[0,2],[5,3],[0,2],[1,2],[2,2],[4,3],[0,3],[1,3],[4,1],[5,1],[3,2],[4,3]]
#l=[[1,2],[2,1],[5,1],[3,1],[0,2],[0,2],[4,1],[0,2],[1,2],[1,2],[5,1],[5,2],[0,2],[1,2],[2,2],[4,3],[0,3],[1,1],[1,2],[4,1],[5,1],[3,2],[4,2],[4,1]]
#1,2 2,1 5,1 3,1 4,1 0,2 5,3 0,2 1,2 2,2 4,3 0,3 1,3 4,1 5,1 3,2 4,3
def t18to62(snum):
sum=0
for i in snum:
sum = sum*18+t18.index(i)
s = ''
while sum:
s += t62[sum%62]
sum = sum/62
return s
def t62to18(snum):
sum=0
for i in snum:
sum = sum*62+t62.index(i)
s = ''
while sum:
s += t18[sum%18]
sum = sum/18
return s
if __name__ == '__main__':
sn1 = ''
for [i,j] in lop2:
sn1 += t18[i*3+j-1]
print t18to62(sn1[::-1])
[公告]名企招聘!