QQ 2005贺岁版登录口令加密算法及其源代码
                      Binny(Binny@vip.163.com)

拿到QQ 2005贺岁版后,发现其加密原理并没有新的改变,经过跟踪和分析,编制出暴力破解本地QQ密码的程序。

需要说明的是,我的QQ标本其版本号为12.87.0.8059(按鼠标右键看QQ.exe的属性),如果你手头的版本与我的有差异,请理解我的意图后再动手做实验。

QQ密码在正确登陆后,会将加密的结果保存在用户目录的ewh.db文件中,加密采用公开的MD5算法,通过N次循环以及异或后求反,最终计算出加密的结果,与用户的ewh.db文件中的密文比较后,发出“输入密码与上次成功登录的密码不一致,$0A是否到服务器验证?”(这条信息在BasicCtrlDll.dll的资源中,$0AC的格式化中为回车)。根据这个提示,完成本地QQ密码的暴力破解。

QQ系统中,“QD”标志代表QQ Data,例如,我们可以在文件User.dbewh.db中找到这个以QD开头的数据结构。

一、        ewh.db原始数据

51 44 01 01 03 00 04 03 00 BD AF A8 04 00 00 00

00 2E 06 00 07 03 00 B9 AB B4 10 00 00 00 07 22

AA 96 56 19 A3 9E 82 19 B7 2B BD 2D 34 4A 04 03

00 A9 B5 B2 04 00 00 00 3C A8 93 06

其中,红色为AST循环次数,蓝色为EWH加密字符串,绿色为UIN QQ号(110340156=0x0693A83CIntel体系内存中排列顺序为:3CA89306)。

二、        ewh.db数据结构

HEX

偏移

DEC

偏移

数据

注释

变量

标志

0000

1

51 44

QDQQ Data 数据标志

Flag

0002

3

01 01

保留的数据结构

Reserve

0004

5

03 00

总数据段(Data Sections)的个数

Sections

0006

7

04

第一个数据段(简称1S,下同)的类型,可以从0x010x0F04代表本数据没经过加密处理。

Type1S

0007

8

03 00

1S标志的长度。

LenFlag1S

0009

10

BD AF A8

1S标志(例如ASTUINEWH等),是经过简单的异或并求反计算处理的,此处是AST,可能是Algorithm Shift Times Axxx Switch Time,管他的呢!

Flag1S

000C

13

04 00 00 00

1S数据的长度

LenData1S

0010

17

00 2E 06 00

= (404992)

1S数据,这里是进行MD5转换的次数。

这个数据是同计算机的性能有关的,性能越高的计算机,在QQ注册成功后产生的这个循环控制变量就越大。

Data1S

0014

21

07

2S数据的类型,07代表使用MD5进行加密

Type2S

0015

22

03 00

2S标志的长度

LenFlag2S

0017

24

B9 AB B4

2S标志,此处是EWH,代表本数据段是EWH密码数据,可能是Encrypt With Hash的缩写

Flag2S

001A

27

10 00 00 00

2S数据的长度

LenData2S

001E

31

07 22 AA 96

56 19 A3 9E

82 19 B7 2B

BD 2D 34 4A

2S数据,是经过MD5加密计算后产生的数据,当然还要经过异或并求反的计算处理,参考下面程序中的1000B858 行代码。

Data2S

002E

47

04

3S数据的类型

Type3S

002F

48

03 00

3S标志的长度

LenFlag3S

0031

50

A9 B5 B2

3S标志,此处是UIN,代表本数据段是QQ号码,可能是:User Identifier Number的缩写

Flag3S

0034

53

04 00 00 00

3S数据的长度

LenData3S

0038

57

3C A8 93 06

3S数据,3C A8 93 06 = 110340156

Data3S

三、        加密原理

下面VB伪代码的部分符号引自以上第二点《结构说明》中的变量标志,请注意理解:

Pwd = MD5(Pwd, Len(Pwd)) 

' Pwd为用户输入的密码,第一轮MD5后,Pwd成为16位字节长度的MD5串。

XorKey As Long = 0  'XorKey为用于解密的字节

For k = 1 To Data1S – 1  '因为前面已经做过一轮,所以此处要减一

 Pwd = MD5(Pwd, 16)

Next k

XorKey = XorKey And &HFFFF

XorKey = (LenData2S And &HFF) Xor (LenData2S \ 256)

XorKey = &HFF - XorKey '求反

For k = 1 To 16

 Pwd(k) = Pwd(k) Xor XorKey

Next k

If Pwd <> Data2S Then

  MsgBox "输入密码与上次成功登录的密码不一致," & vbcrlf & "是否到服务器验证?"

End If

 

通过以上的流程,我真的佩服QQ的设计者,如此巨大的循环量,加上循环次数的随机性,如果希望产生一个QQ MD5词典简直不可能。虽然理论上,可以产生一个MD5字典,但是,这个字典将有1.15E+77*16个字节之巨,因此,只好根据ewh.db文件提供的数据暴力破解了,不知是不是有更好的方法呢?

不过我的感觉是,循环次数加多了,应该会产生更多的MD5碰撞,不见得是个好事。

还有一种破解思路,也许更加直接,将在后面的文章中详细探讨。但是我只有在有时间做完实验后才有资格评述,不在本文章的讨论范围内。

四、        破解算法

重复进行数十万次MD5加密,会消耗计算机很多时间,如果使用传统的VBVC,对于一个密码的等待时间也是很可观的(例如使用VB代码,消耗的时间可能是汇编的400倍),因此,我使用汇编语言来编制低层加密解密算法,通过MASM32编译连接,最后用高级语言调用。通过提供算法动态库的方式,方便其他有兴趣的读者自己增加丰富的功能。例如增加多线程等,这也将在以后的探讨中实现。在此不做深入讨论。

附带的例子为VBVC调用汇编语言动态库的例子,VB代码简单实现了通过密码字典进行单线程破解的功能,读者可以丰富其内容。增加更多的功能。

五、        QQ数据结构分析

下面为动态库BasicCtrlDll.dll中反汇编的代码以及代码分析,主要用于分析EWH.DB中数据结构以及QQ数据解调算法的问题。另外,这个算法也可以将User.db中的数据提取出来。深入研究下去,做一个聊天记录查看器之类的软件也非难事。

 

1000B71D

mov eax,BasicCtr.100116AC

 

1000B722

call BasicCtr.1000FDB0

 

1000B727

sub esp,3C

 

1000B72A

mov eax,dword ptr ss:[ebp+8]

将数据的开始地址赋给EAX,实际数据为**DataEAX=*Data

1000B72D

push ebx

 

1000B72E

push esi

 

1000B72F

push edi

 

1000B730

mov esi,dword ptr ds:[eax]

需要转换的字符串,EAX指示一个结构,第一个成员为实际的数据指针

1000B732

mov dword ptr ss:[ebp-28],ecx

局部变量[ebp-28]保存全局的标志结构,ECX为全局参数地址,在调用本函数时跟入

1000B735

mov eax,dword ptr ds:[esi-8]

CString结构中长度的成员,表示总共多少个字节

1000B738

cmp eax,6

如果长度小于6,则为无效的数据

1000B73B

jb BasicCtr.1000B9C2

如果比6小则跳转退出,说明数据量不够解调的

1000B741

cmp byte ptr ds:[esi],51

是否为 QQ Data 的数据,QDQQ数据标志

1000B744

jnz BasicCtr.1000B9C2

 

1000B74A

cmp byte ptr ds:[esi+1],44

 

1000B74E

jnz BasicCtr.1000B9C2

 

1000B754

mov di,word ptr ds:[esi+4]

对于EWH来说,为第4+1个字节,为0003

1000B758

add esi,4

指向数据段(Sections)的个数

1000B75B

inc esi

 

1000B75C

add eax,-6

EAX去掉6个字节,对于EWH来说,剩下36H字节

1000B75F

inc esi

 

1000B760

mov dword ptr ss:[ebp+8],eax

指向第一个数据段

1000B763

call BasicCtr.1000BD36

在内存(ECX+9C)处开辟一个(100H)字节的空间,空间地址返回到EAX

1000B768

and dword ptr ss:[ebp-20],0

局部变量[ebp-20]清零

1000B76C

movzx eax,di

转换到EAX,对于EWHdi=3。表示有3段数据

1000B76F

test eax,eax

 

1000B771

mov dword ptr ss:[ebp-48],eax

局部变量[ebp-48]保存数据的段数

1000B774

jle BasicCtr.1000B99B

 

1000B77A

cmp dword ptr ss:[ebp+8],7

如果整个长度小于7,则剩下的应该是QQ号了。第一次进入时=36H

1000B77E

jb BasicCtr.1000B9C2

如果剩余的数据长度小于7则退出

1000B784

mov al,byte ptr ds:[esi]

第一次进入时,ESI指向第7个数据即数据段的类型,例如 04

1000B786

mov cx,word ptr ds:[esi+1]

CX=后一个数据,及数据段标志的长度,例如 0003

1000B78A

inc esi

 

1000B78B

mov edx,dword ptr ss:[ebp+8]

剩余的数据长度,如36H

1000B78E

sub dword ptr ss:[ebp+8],3

去掉3个,例如成为33H=51

1000B792

mov dword ptr ss:[ebp-38],ecx

局部变量[ebp-38]保存数据段中标志长度

1000B795

movzx edi,cx

EDI为标志长度了

1000B798

inc esi

 

1000B799

mov byte ptr ss:[ebp-1C],al

局部变量[ebp-1C]保存本段的类型

1000B79C

lea ecx,dword ptr ds:[edi+4]

ECX为取得的标志长度再加上4,例如=7

1000B79F

inc esi

第一次时,ESI指向第一个数据段的标志字段了,例如指向BDAFA8,第9个数据开始

1000B7A0

cmp dword ptr ss:[ebp+8],ecx

如果剩余的数据比“标志长度+4还少,则没有数据,因此不进行处理

1000B7A3

jb BasicCtr.1000B9C2

 

1000B7A9

mov ecx,dword ptr ds:[edi+esi]

跳过数据段的标志部分,将数据段的长度赋值给ECX。例如[edi+esi]=4

1000B7AC

lea ebx,dword ptr ds:[edi+esi]

EBX保存新的指针,指向数据段中数据部分的长度部分,例如,[EBX]=4

1000B7AF

mov dword ptr ss:[ebp-3C],ebx

局部变量[ebp-3C]保存数据段中数据部分的长度指针

1000B7B2

lea ecx,dword ptr ds:[edi+ecx+7]

从标志(自身长3,加上4个长度位=7)开始,加上数据长度,为完整数据长度。例如E

1000B7B6

cmp ecx,edx

如果剩下的数据长度(如36H)不够,则退出

1000B7B8

mov dword ptr ss:[ebp-34],ecx

局部变量[ebp-34]保存本数据段的总长度

1000B7BB

ja BasicCtr.1000B9C2

如果小于则退出程序

1000B7C1

and dword ptr ss:[ebp-10],0

局部变量[ebp-10]清零

1000B7C5

cmp al,1

al保存本段的类型,有效的类型是47

1000B7C7

je short BasicCtr.1000B7E1

 

1000B7C9

cmp al,2

 

1000B7CB

je short BasicCtr.1000B7E1

 

1000B7CD

cmp al,3

 

1000B7CF

je short BasicCtr.1000B7E1

 

1000B7D1

cmp al,4

 

1000B7D3

je short BasicCtr.1000B7E1

 

1000B7D5

cmp al,5

 

1000B7D7

je short BasicCtr.1000B7E1

 

1000B7D9

cmp al,7

 

1000B7DB

je short BasicCtr.1000B7E1

 

1000B7DD

cmp al,6

 

1000B7DF

jnz short BasicCtr.1000B7FA

 

1000B7E1

push ecx

为本段的总长度,包括(段类型+标志长度+标志+数据长度+数据),例如,对于密码段=1AH

1000B7E2

call <MFC42.operator new>

 

1000B7E7

push dword ptr ss:[ebp-34]

n=[ebp-34],局部变量[ebp-34]保存本数据段的总长度

1000B7EA

lea ecx,dword ptr ds:[esi-3]

[esi-3]指向本段的开始(从段类型算起)

1000B7ED

mov dword ptr ss:[ebp-10],eax

局部变量[ebp-10]保存拷贝后的数据

1000B7F0

push ecx

src

1000B7F1

push eax

dest

1000B7F2

call <MSVCRT.memcpy>

memcpy,将本段的整段数据拷贝到新的地方,数据指针保存在局部变量[ebp-10]

1000B7F7

add esp,10

 

1000B7FA

mov eax,dword ptr ss:[ebp-38]

局部变量[ebp-38]保存数据段中标志长度

1000B7FD

xor ecx,ecx

 

1000B7FF

xor al,ah

将低位长度与高位异或,例如3 xor 0=3

1000B801

test edi,edi

EDI为标志位的长度

1000B803

jbe short BasicCtr.1000B817

如果本段没有段标志,则跳转

1000B805

mov dl,byte ptr ds:[ecx+esi]

开始循环,循环次数为标志的长度。ECX第一次时为0,将第一个数据加载到DL中。

1000B808

xor dl,al

AL为长度的高位和低位的异或,这里为3

1000B80A

not dl

DL=NOT ([数据] xor [数据段标志长度的高位 xor 数据段标志长度的低位])

1000B80C

mov byte ptr ds:[ecx+esi],dl

数据保存在原始的内存中相应的地方

1000B80F

inc ecx

 

1000B810

cmp ecx,edi

 

1000B812

jb short BasicCtr.1000B805

 

1000B814

mov ebx,dword ptr ss:[ebp-3C]

局部变量[ebp-3C]保存数据段中数据部分的长度指针

1000B817

push edi

EDI为标志位的长度

1000B818

push esi

ESI为指向解密以后的数据,例如AST

1000B819

lea ecx,dword ptr ss:[ebp-18]

局部变量[ebp-18]存放强制类型转换以后的数据指针的指针,例如AST

1000B81C

call <MFC42.CString::CString>

将解密后的数据变成CString类型。返回的类型放在EAX指示的地址中

1000B821

and dword ptr ss:[ebp-4],0

局部变量[ebp-4]清零

1000B825

push -4

 

1000B827

pop eax

EAX=-4=FFFFFFFC

1000B828

mov esi,ebx

ebx保存数据段中数据部分的长度指针

1000B82A

sub eax,edi

EAX=EAX-EDI=FFFFFFFC-3=FFFFFFF9

1000B82C

mov ebx,dword ptr ds:[esi]

ebx为数据段中数据部分的长度了

1000B82E

add dword ptr ss:[ebp+8],eax

第一次时,[EBP+8]=33。执行后=2C,相当于33H-7H=2CH

1000B831

add esi,4

ESI指向数据段中的数据部分了

1000B834

cmp dword ptr ss:[ebp+8],ebx

[ebp+8]=2Cebx=4

1000B837

jb BasicCtr.1000B9A7

 

1000B83D

cmp byte ptr ss:[ebp-1C],7

局部变量[ebp-1C]保存本段的类型,4或者7

1000B841

je short BasicCtr.1000B849

 

1000B843

cmp byte ptr ss:[ebp-1C],6

如果类型不为6,则执行1000B862

1000B847

jnz short BasicCtr.1000B862

 

1000B849

mov al,bl

如果数据段的类型为7,则执行此语句。BL包含本段的长度

1000B84B

xor edi,edi

 

1000B84D

xor al,bh

al=(长度的低位 xor 长度的高位)

1000B84F

test ebx,ebx

 

1000B851

jbe short BasicCtr.1000B862

如果长度为0,则表示没有密码

1000B853

mov cl,byte ptr ds:[edi+esi]

 

1000B856

xor cl,al

 

1000B858

not cl

DL=NOT ([数据] xor [数据段标志长度的高位xor 数据段标志长度的低位])

1000B85A

mov byte ptr ds:[edi+esi],cl

 

1000B85D

inc edi

 

1000B85E

cmp edi,ebx

 

1000B860

jb short BasicCtr.1000B853

循环直到全部数据解调完毕

1000B862

push ebx

数据串的长度

1000B863

push esi

原始的需要变换的数据

1000B864

lea ecx,dword ptr ss:[ebp-14]

局部变量[ebp-14]存放强制CString类型转换以后的数据指针的指针,例如DB2E0600

1000B867

call <MFC42.CString::CString>

 

1000B86C

mov al,byte ptr ss:[ebp-1C]

局部变量[ebp-1C]保存本段的类型,4或者7

1000B86F

sub dword ptr ss:[ebp+8],ebx

去掉已经处理的数据,执行后[ebp+8]=28Hebx为数据段中数据部分的长度

1000B872

add esi,ebx

ESI指向下一个数据段的开始部分,ebx保存数据段中数据部分的长度指针

1000B874

xor edi,edi

 

1000B876

test al,al

测试本段的类型是否为0

1000B878

mov byte ptr ss:[ebp-4],1

局部布尔型变量[ebp-4]=1

1000B87C

jbe BasicCtr.1000BA25

如果本段的数据类型为0,则执行1000BA25后退出

1000B882

cmp al,7

 

1000B884

jbe BasicCtr.1000B940

如果小于等于7,则跳转到1000B940执行。对于EWH来说就是这样

1000B88A

cmp al,8

 

1000B88C

je BasicCtr.1000BA06

如果数据类型为8,则直接退出。

1000B892

cmp al,9

 

1000B894

je short BasicCtr.1000B8F3

 

1000B896

cmp al,0A

 

1000B898

jnz BasicCtr.1000BA25

 

1000B89E

mov ecx,dword ptr ss:[ebp-28]

当数据类型为A时,执行本程序代码

1000B8A1

lea eax,dword ptr ss:[ebp-2C]

 

1000B8A4

push eax

 

此处删除了若干个字,没有阴私,只是不想文章太长……

1000B93E

jmp short BasicCtr.1000B8EC

 

1000B940

push 1

当数据段的类型<=7时,直接从此处执行

1000B942

pop edi

 

1000B943

mov ebx,dword ptr ss:[ebp-28]

局部变量[ebp-28]保存全局的标志结构

1000B946

lea eax,dword ptr ss:[ebp-14]

局部变量[ebp-14]存放强制类型转换以后的数据指针的指针,例如DB2E0600

1000B949

push eax

EAX存放强制类型转换以后的数据指针

1000B94A

lea eax,dword ptr ss:[ebp-18]

局部变量[ebp-18]存放强制类型转换以后的数据指针的指针,例如AST

1000B94D

push dword ptr ss:[ebp-1C]

局部变量[ebp-1C]中的第一个字节保存本段的类型,4或者7

1000B950

mov ecx,ebx

 

1000B952

push eax

 

1000B953

call BasicCtr.1000B60C

call 1000B60C(CString,Flag,CString)

1000B958

test edi,edi

 

1000B95A

je short BasicCtr.1000B974

 

1000B95C

mov eax,dword ptr ss:[ebp-20]

局部变量[ebp-28]第一次为0,为一个计数器

1000B95F

mov ecx,dword ptr ds:[ebx+64]

存在于标志结构中,为一个全局地址

1000B962

mov edx,dword ptr ss:[ebp-10]

局部变量[ebp-10]保存拷贝后的数据,即没有经过处理的。例如040300BDAF……

1000B965

shl eax,2

EAX=EAX*2

1000B968

mov dword ptr ds:[ecx+eax],edx

将未做解调的原始数据放到全局结构中某个指针指示的内存中

1000B96B

mov ecx,dword ptr ds:[ebx+78]

存在于标志结构中,为一个全局地址

1000B96E

mov edx,dword ptr ss:[ebp-34]

局部变量[ebp-34]保存本数据段的总长度

1000B971

mov dword ptr ds:[ecx+eax],edx

将数据长度放到全局结构中某个指针指示的内存中

1000B974

and byte ptr ss:[ebp-4],0

局部布尔型变量[ebp-4]=0

1000B978

lea ecx,dword ptr ss:[ebp-14]

 

1000B97B

call <MFC42.CString::~CString>

清除数据段中的数据部分CString

1000B980

or dword ptr ss:[ebp-4],FFFFFFFF

局部布尔型变量[ebp-4]=-1,为True

1000B984

lea ecx,dword ptr ss:[ebp-18]

 

1000B987

call <MFC42.CString::~CString>

清除数据段中的标志部分CString,例如AST

1000B98C

inc dword ptr ss:[ebp-20]

局部变量[ebp-28]计数器加一

1000B98F

mov eax,dword ptr ss:[ebp-20]

 

1000B992

cmp eax,dword ptr ss:[ebp-48]

局部变量[ebp-48]保存数据的段数

1000B995

jl BasicCtr.1000B77A

循环解调每个数据段

1000B99B

mov eax,dword ptr ss:[ebp+8]

最后剩余的长度

1000B99E

neg eax

 

此处删除了几百个字,同上 ……

1000BA25

and byte ptr ss:[ebp-4],0

 

1000BA29

lea ecx,dword ptr ss:[ebp-14]

 

1000BA2C

call <MFC42.CString::~CString>

 

1000BA31

jmp short BasicCtr.1000B9B6

 

 

以上代码看不懂没关系,在压缩包中的VB代码 LoAnalysisQD (fbyt() As Byte) 函数概括了上述的思想,不过,对于47以外的数据类型,由于与本文内容无关,因此,不深入分析。

六、        QQ的另类破解

1QQ密码算法和身份验证中存在的问题

搞个几十万次加密其实并不能阻止密码的破解,我对现在收集的EWH.db进行统计,那个循环的AST都在40万以上(可以买房子了,不是么?),因此,只要做一个40万的QQ Hash词典,在此基础上进行破解,破解速度不就提高了几万倍么?

这个词典如何形成?问得好!事实上,国际上有合作破解的先例。例如,可以给我分配000000000~999999999的段进行计算,给你分配999999999~1999999999的段进行计算……,最后,将所有结果合成为一个词典(或者根本不用合成,把Hash值发给大家就可以了,总有人能中大奖:)。

这个词典称为Hash词典,大致上是这样的(密码经过400000次加密后得到):

 

索引(密码)

Hash值(加密后的密文)

1

2E62CD38389C3A885D7F6789FEEE8AA5

2

1F9C9B12E93A0FF2A9B7A714EF4D6CA4

3

8FE71CEF95D0F0DDAD716651E635D19C

……

……

999999999

A041F9E6DDA44C178F24DABC9B292785

 

有了这个Hash词典,我们完全没有必要重新忍受漫长的计算过程,直接查表就可以了,不是吗?用黑客的行话来说,就是跑字典了。

利用压缩包中提供的函数QQMD5(unsigned char *, long, long, QQSum *),就完全可以形成这个词典了。

别笑,这好象是大炮打蚊子,对于一个小小的QQ来说,何必劳驾全世界呢?既然这是我们闲得无聊下来干的事,我们就该自己来做。

最简单的Hash词典就是利用黑客词典(网上简直多如牛毛)再通过QQMD5函数来生成,如果你想到用数据库来管理这些Hash值,你一定是个非常敬业的黑客,不过数据库也是TB级的了。

Hash散列算法非常容易产生碰撞,如果你偶尔发现输入12345和输入78952一样可以登录QQ,你千万不要怪腾讯公司没水平,这是因为1234578952产生的Hash相同(别试,我随便说的两个数),这说明,一个Hash输出可能对应多个输入,这就是碰撞。听说山东大学的王教授就很擅长产生这种碰撞,强烈要求他来做我们的斑竹。

因为碰撞,我们的词典就会小一点,不过,这些理想和共产主义一样,离我们很远,即使教授的快速碰撞理论,对于QQ这样的重复加密来说,只是加快计算时间,并不能对整个加密体系构成威胁,所以,即使这个世界有那么多的恐怖份子,你我夏天还是穿个短裤就可以上街。

况且,现在很多的及时通讯系统都采用本地和服务器联合验证的方式,这非常有效,即使本地的密码被攻破了,但是,不能保证服务器的也被攻破。因为,通过暴力方式破解的登录口令也只是众多产生碰撞的口令之一,如果使用不同的加密方式,即使这个口令能进入本地系统,但是不能保证同一个口令在服务器端得到验证。所以,QQ从这个意义上说,依然是相对安全的(不过众多口令中,可能就只有一个特别象口令)。

如果我是腾讯的老总,说实话,在我垄断市场的情况下,我并不希望自己的口令体系多么坚不可摧,用户有了危机感,我才能从用户手中收取保护费吧:)所以,诸如此类的文章或者工具即使漫天飞也只是帮他做市场而已。

刚才提到密码算法的问题,谈到的所有问题都是本地的密码机制,对于网络之间的报文,采用的又是TEA的加密算法。这个是我后面的文章要分析的。

QQ的身份验证中,还存在另外一个漏洞,这个漏洞很早就被发现了,一直保留到现在,也许将来也不会改变。

我不说大家也都知道,将EWH.DB中的QQ号(第一点中的那个3C A8 93 06)替换成别人的QQ号,然后把这个东西放在别人的目录下,就可以看到那个倒霉蛋的聊天记录了。不过,好在腾讯的服务器不买这个帐:看就看吧,只要我这里安全就没事了,多看点多学点,下次聊天就更欢了。

从低层的逻辑也可以看得出来,只要用户的EWH.DB文件符合我们前面的算法,QQ就认为这个用户是合法的。因此,我们完全可以想象,如果我自己做一个EWH.DB文件,让那个40万次的东西变成0,岂不是可以让我的计算机从那个傻呼呼的循环中解脱出来吗?更懒的人可能想到,如果不用密码岂不是更爽?好,我们来实现这个愿望:

运行QQPwdFinality.exe,将登录口令中的“12345”删掉,将“循环次数”改为0,进行单步计算后,是不是得到了一个串?如果你的计算机没骗你的话,大家应该都得到同一个结果:3BF2633660EF5DEB066FE6770317AD91,好了,我们将这个结果替换掉EWH.DB中的那个Hash07 22 AA 96 56 19 A3 9E 82 19 B7 2B BD 2D 34 4A,最后,EWH.DB文件变成下面这个样子(记得将循环次数也改成0哦):

 

51 44 01 01 03 00 04 03 00 BD AF A8 04 00 00 00

00 00 00 00 07 03 00 B9 AB B4 10 00 00 00 3B F2

63 36 60 EF 5D EB 06 6F E6 77 03 17 AD 91 04 03

00 A9 B5 B2 04 00 00 00 3C A8 93 06

 

我知道在座的懒人很多(包括我自己),我特地在QQPwdFinality.exe文件中加入了一个“产生EWH.DB文件”选项,我知道你可能连这都懒得勾,所以默认情况下,我帮你勾上了。 你可以选择任何口令和循环次数来形成EWH.DB文件,文件会生成在当前的目录下。

好了,现在,你再也不用拿着你那个EWH.DB文件到处奔波了,你就用这个吧,毛爷爷不是说过吗?打击敌人的同时还要保护自己,你用自己的口令到处看别人的记录,查出来多不好,况且,一旦忘记删掉了,被受害者拿去,访高手找名人,那你就可能重新申请QQ号了。不过,如果按照这个原理将自己的QQ改造一下,你就比任何人都更快地进入QQ,抢先零点几秒找到PLMM。但是千万别干坏事哦,如果你将别人的AST改成FFFFFF7F的话,估计他要等一天才能和自己心爱的MM聊天了,你忍心下手吗?是不是有人想到发明一个病毒,专门修改这个号,让登录越来越慢啊呵呵。

其实,腾讯稍做努力便可以改掉这个问题,要么在每个QQ Data结构后面加一个Hash验证,要么将用户的QQ号作为密钥,对口令进行加密。最简单的就是用QQ号取代MD5算法中的那个基本字串(即便那几十万个循环中用了一次),这样,自己的EWH.DB就只能自己用了,这才是数字指纹的真正用途嘛。也许你会说,这么做知道了算法还是徒劳,但是,至少可以打破现在的局面——只要会用鼠标的都可以修改EWH.DB,显得这些研究破解的同志多么没有存在的价值啊。

以上谈的这些东西总的说起来都是一些雕虫小技,估计有一半的人看到中途就睡着了,坚持下来的请跟随我进入下一个章节,如果最后还有幸存者,我希望和你交个朋友:)

谈到这里,我想,这篇文章不太适合高手拜读,如果你是高手并且很无聊地走到这里,那么,你可以休息了,走的时候别忘记关灯关门哦。

 

下面,我们主要谈谈关于网络的话题。

 

2QQ网络登录时存在的问题

MD5的算法固然不错,但并不能取代传统的算法,那种可逆算法在任何时候都是必要的,特别在网络里。我们可以反证一下,如果你在注册QQ或者修改密码的时候,腾讯公司得到的是MD5 Hash,他怎么能知道你原来的密码是多少呢?他绝对不会和我们一样傻,通过暴力破解的方式得到吧。当然,腾讯公司也可以只要MD5,如果是这样,他就等于失去了一次对用户的隐私进行窥探的机会,这对于任何一个公司来说都是不可能的。

退一步说,即使有的公司宣布,为了用户利益,对用户的密码不感兴趣,但是,他还是要知道是谁采用了合法的方式登录,因此,最终都不能免俗,都必须从用户发来的网络包中提取信息来完成身份的校验,以便对上帝们发出邀请或对恶魔们进行拒绝,OK,如果魔鬼扮成上帝呢?

有两种方法可以假扮上帝(如果真的有God,我在这里用您老人家做比喻希望能得到宽恕,amen):第一种,绑架他,然后伪装成他;第二种,分析其DNA结构,最后人工合成一个(Sorry,这是最后一次拿您做比较,保证以后不再,amen)。

第一种方法其实是比较复杂的,通过截获TCPUDP包,然后对包重定向,截获控制权等等,既需要技巧又需要耐心,还需要一点运气,做之前别忘记向全能的上帝祷告一下,让他帮助你猜中下一个TCP的序列号,自己研究吧。

这篇杂文主要讲第二种方法,通过对认证过程的研究最后合成自己的发送包,或者对截获的网络包进行分析,找出包主的登录密码或者其他信息。

关于网络包的截取有三种方法:

1、捕捉WSock32的传输函数

2、使用专用的安全工具例如NetXray或者Sniffer

3、用开发包自己做,例如WinPcap开发套件

我一般使用Sniffer工具,有工具不用,拿武汉话说叫苕货。记得93年时写网卡驱动程序的时候了,那个时候主要研究怎样将网卡改装成嗅包器,来对校园网的流量进行计费,很遥远的事情了。现在真好工具真多!

下面,我们来看一看服务器与客户机之间的对话:

第一次,当故意输入错误的密码登陆时,UDP会话如下:

 

发给服务器:

02 0c 57 00 31 00 05 06 93 a8 3c 01 …… 00 …… 03

服务器返回:

02 00 00 00 31 00 05 01 00 03

发给服务器:

02 0c 57 00 62 06 08 06 93 a8 3c 00 03

服务器返回:

02 00 00 00 62 06 08 00 18 53 f0 dc a5 97 1e 82 fb be f7 83 0a fb bd 7d 5a 48 5e 70 df 48 05 b2 7c 03

发给服务器:

02 0c 57 00 18 06 08 06 93 a8 3c ……20 6e 03

服务器返回:

02 01 00 00 18 06 08 92 78 52 77 50 d8 01 78 02 ff 7f 34 2c 23 cf cc 99 ea 2d e4 4a e1 da ac 03

 

这些信息中肯定包含登录数据,于是,通过尝试不同的密码,最后输入一次正确的密码,比较这些数据(过程就不罗嗦了,一些基本的技巧例如通过弹出的错误对话框倒推到数据比较直到密码数据的合成;跟踪重要的网络动态库WSock32的调用等),最终将登录过程搞得清清楚楚明明白白真真切切。

 

BasicCtrlDll.dll的代码位置100025EA处,有指令call BasicCtr.1000EB21

在反汇编代码中,几个参数为:

call BasicCtr.1000EB21([EBP+C],[EBP+10], BasicCtr.100127B8,ESI, [EBP-20])

本函数参数的意义参考下面:

BasicCtr.1000EB21(明文,明文长度, 密钥, 密文, 密文长度)

 

因此,可以考虑以下方案来形成自己的加密或解密代码:

 

1、自己写代码;2、直接利用动态库的代码(把内部代码看成黑匣子)

 

自己写代码,必须跟踪并弄清楚整个算法,如果我们偷个懒直接利用这个函数呢?现在是讲究效率的年代,除非这个加解密算法写的很精彩,否则,没必要浪费时间来深入分析。

不过,在没有弄清楚整个加解密思想之前,我们还得下一番功夫,根据我的研究,腾讯的通讯数据包使用的是TEA加密算法,他在这个加密算法上玩了几个花样,企图迷惑我们这些高级一点的菜鸟,不过,这些措施好像不能阻挡菜鸟来研究它,因为菜鸟见过比这更复杂的东东,好了,不多聊(聊多了成灌水的了:)。这个花样就是通过WSock32库附带的函数WSOCK32.ntohl打乱加密的数据,加密后又使用WSOCK32.ntohl函数还原,这应该是一种变形的TEA算法吧。另一方面,为了让每次加密的结果截然不同,程序还使用了随机密钥来辅助加密数据(我发现腾讯很喜欢反复加密的方式,有个性!)。不过,菜鸟有菜办法,我们不深入研究好了吧,我们不想盗版,我们只想盗密码(准确地说是窃密码而不是盗密码,鲁迅先生说得真好,“窃”是有技术含量的,“盗”TMD就是强盗,会被人打死的)。

使用动态库(更广泛地说是PE文件)中的代码,有两种办法:改写导出函数,直接指向我们需要的函数位置(以上的1000EB21明显是内部函数,这样,可以将内部函数变成外部函数),然后使用声明(VB中)或引用(VC)的方式执行。这么做有些麻烦同时也需要技巧,就是要找到合适的导出函数才好使用。另一个方法就是将PE文件加载到内存中,然后改变执行指针,指向被执行地址,执行完毕后平衡堆栈,取得需要的数据后返回。我们就使用第二种方法。

很多加密算法的破解都可以使用这种方法,这叫用其人之矛,攻其人之盾;或者叫用其人只盾,轧其人之矛。我不知道是不是我的首创,我在很久以前就在使用这种方法,并且屡战屡胜(在前面我已经劝高手们主动离开了,这里我就自吹自擂一下:)。

关于这一点,我突然有种悚然的感觉,加载到内存中的代码就象一群僵尸,我们的任务是给其中一个注入灵魂,让他活起来。

有关的注意事项我就不多说了,那些基本知识自己想办法掌握吧。

我们依然使用通俗易懂的VB来完成这个艰巨而光荣的任务,通过在VB中调用汇编代码的方式来控制程序流程,达到我们的目的。

在进一步讲解之前,先统一思想。下面是我们的思路:

 

加载动态库 找到入口指针→ 计算我们代码的偏移地址并执行 清理堆栈并返回数据 结束

 

为了验证程序的正确性,我们随便跟踪一个数据,其中:

密钥:

100127B8  0A AA 9D 3A 1A 88 16 CE 61 56 04 FD D4 C5 F5 BC

输入明文:

0143E078  47 65 74 4C 6F 63 61 6C 49 50 20 32 31 38 2E 35  GetLocalIP 218.5

0143E088  36 2E 32 33 2E 32 31 39 0D 0A                 6.233.219..

如果函数执行成功,我们会将会返回加密后的数据:

输出密文:

013F0F98  A8 BF B4 27 C3 FE 0B AC 9D 1B 26 3A 07 39 0B 54

013F0FA8  3A B8 6A 56 EA B4 71 1D F5 04 FD 0D 45 39 B7 F0

013F0FB8  78 81 C8 6F 40 15 FE 5E

 

如果你的输出与上述数据不同,一定是你没听懂我的话啦,是不是要我Speak English你才有感觉啊?

哦,好象每次加密真的不一样啊!记得我说过吗,这是使用了随机数的结果,如果你每次加密都一样,说明你的计算机太低级了,连随机数都不会算。

上面的跟踪中,我们发现加密函数位于动态库BasicCtrlDll.dll内存偏移1000EB21的位置,因此,我们随便找一个输出函数(参看附件中的BasicCtrlDll导出表.txt),例如我们在函数输出表中找到“??0CVQQSock@@QAE@XZ”函数(找个名字短一点的),其入口地址为10008D9F,然后,用上述加密函数的入口地址1000EB21减去输出函数的入口地址10008D9F,再加上动态加载时该输出函数的入口地址(使用API GetProcAddress得到)便是我们加密函数的地址了。通过技术处理,我们的指针就定位到内部加密的函数处了,在堆栈中压入完整的参数后,我们就可以享用这个函数给我们带来的快感(这些叙述请参考附件中的代码)。

不过,我好讨厌那些随机数,他们的存在,使我无法研究加密的规律,因此,我们可以在程序中去掉这些随机数,用常数代替,便于我们进一步的研究(也许你会怀疑,用常数代替随机数,QQ能干么?我请你用脚后跟想想,常数包含在随机数中,属于随机数的一种特例,因此对程序的运行不会产生任何影响,让EAX=0吧,然后跳转到正常的指令。因为这样修改后代码最少并且最可靠,大家注意下面的代码,那个<&MSVCRT.rand>比较危险,程序在运行的过程中会修改这个数据的,如果你使用90H代替后面的机器码,程序会死翘翘的。因为这个数据是根据程序加载的情况而定的,因此使用跳转指令最安全。不过,如果你跟踪修改后的指令,你会看到“花指令”的效果)。

 

原代码:

1000EB42     8B3D AC230110    mov edi,dword ptr ds:[<&MSVCRT.rand>]

1000EB48     FFD7             call edi

修改后:

1000EB42     33C0             XOR EAX,EAX

XXXXXXX    EB 04            JMP 1000EB49

XXXXXXX    00               XXXX

XXXXXXX    00               XXXX

XXXXXXX    00               XXXX

1000EB48     00               XXXX

 

修改方法:用十六进制编辑器打开BasicCtrlDll.dll,寻找8B 3D AC 23 01 10 FF D7(参考上面源代码中的机器码),修改成33 c0 eb 04 00 00 00 00(参考上面修改后的机器码)。同样,寻找FF 15 AC 23 01 10(如果到QQ.exe中搜索,会搜索到很多,看来他们的随机函数也不少用,居心叵测啊),修改成33 c0 eb 02 00 00。还有,别嫌麻烦,继续搜索43 FF D7 88 44 35 F0,将里面的FF D7改成33 C0好了(为什么不直接搜索FF D7呢?你自己琢磨一下),这次我们再运行程序试一试。

看到效果了吧,经过我们的修改,每次加密的结果固定不变了,当改变明文时,密文变化很大(附件中有个QQEncryptTest.exe的程序,附源码,分别调用“常数加密”和“随机加密”,便可以看到这种效果。注:未修改的代码为BasicCtrlDll_Old.dll,修改后的代码为BasicCtrlDll_New.dll)。

好了,现在,我们作个小结,我们应该掌握了一种方法,能够窥探程序的隐私并且将代码原封不动地为我所用。这种技术将用于破解许多具有各种陷阱的算法,因为我们既不跟踪又不分析,我们用最小的代价取得了最大的胜利。

 

有了加密函数,必然有解密函数,寻找加密函数已经让我们的脑袋大了一个Size,再去找解密函数,我们的帽子就得换大的了(换帽子的时候注意颜色哦:)。根据经验,解密函数一定也会在调用参数中加入密钥,因此,我们只要在内存中搜索100127B8就行了,搜索的结果真的令人失望,除了那个加密的地方有这个参数外,其余的地方没找到。这也可以理解,没事把解密的函数放在动态库中干吗?这个动态库只要负责加密就够了(如果你找到了千万要告诉我,反正我懒得找了,虽然没有找到,但是附件中的代码也演示了上述思想——调用BasicCtrlDll.dll的内部私有函数并且得到返回值),这里我向腾讯公司强烈呼吁,下一个版本中请放入解密函数。

不过,我们的工作没有结束,相反才开始,前期的努力已经打下坚实的基础,我们把目光集中在QQ.exe上,因为我坚信,肯定存在解密函数,东边不亮西边亮,这个函数负责将腾讯服务器传来的确认数据解密。

在上述代码中,我们看到了一个数据:0A AA 9D 3A 1A 88 16 CE 61 56 04 FD D4 C5 F5 BC,没错,这个数据就是密钥,如果我们用UltraEdit-32打开动态库,我们可以很容易搜索到这个16进制数据,这表示该密钥是集成并且固定在执行文件内部的。同样,我们也可以在QQ.exe内部搜索到这个数据,这应该令我们高兴,因为这说明两个问题:第一,该密码是永恒的。第二,QQ.exe中也包含同样的函数,解密函数肯定也在其中。

好了,我们从现在开始将注意力集中在QQ.exe里,别开小差哦:)

怎么找呢?面对这个大餐,真的不知道怎么下爪子了吧?

其实,有个特征很明显,由于我们已经知道了是用TEA的加密方法,我们根据MD5破解获得的经验,只要搜索标准的加密特征就够了。

根据我的经验,想知道是否使用了MD5加密,可以搜索十六进制56b7c7e8,想知道是否是TEA加密,可以搜索b979379e或者其反码4786C861,我们用4786C861来搜索,发现BasicCtrlDll.dll中只有一个地方有,这更加证明其中不包含解密函数。搜索QQ.exe,发现竟然有5个地方存在,至于它们是龙井TEA还是普饵TEA,只有跟踪才知道了。

用跟踪调试工具或者静态反汇编工具打开QQ.exe,搜索4786C861,找到加密(或解密)的代码,可以在此下断点了。那么怎么才能知道我找到的是加密还是解密函数呢?

如果你熟悉TEA,加密的特征是“y += …”,解密的特征是“z -= …”,因此,观察汇编代码,就能看出找到的是什么了。例如,搜索特征字后,我们找到:

 

::00409D55::  2D 4786C861

SUB EAX,61C88647

TEA特征字,注意顺序相反

::00409D5A::  C1E6 04

SHL ESI,4

 

::00409D5D::  0375 F0

ADD ESI,[EBP-10]

 

::00409D60::  33D6

XOR EDX,ESI

 

::00409D62::  8D3418

LEA ESI,[EAX+EBX]

 

::00409D65::  33D6

XOR EDX,ESI

 

::00409D67::  03FA

ADD EDI,EDX

 

::00409D69::  8BD7

MOV EDX,EDI

 

::00409D6B::  8BF7

MOV ESI,EDI

 

::00409D6D::  C1EA 05

SHR EDX,5

 

::00409D70::  0355 FC

ADD EDX,[EBP-4]

 

::00409D73::  C1E6 04

SHL ESI,4

 

::00409D76::  0375 F8

ADD ESI,[EBP-8]

 

::00409D79::  33D6

XOR EDX,ESI

 

::00409D7B::  8D3438

LEA ESI,[EAX+EDI]

 

::00409D7E::  33D6

XOR EDX,ESI

 

::00409D80::  03DA

ADD EBX,EDX

相当于y +=,为加密

::00409D82::  49

DEC ECX

 

::00409D83::  75 C6

JNZ SHORT 00409D4B

 

 

因此,这个函数是加密函数,同样我们可以找到解密的函数,这样,我们终于把武器凑齐了。如果对上述函数下断点,我们可以跟踪到外层函数,如下:

 

00408DB3

8D45 E0

lea eax,dword ptr ss:[ebp-20]

 

00408DB6

50

push eax

密文长度

00408DB7

56

push esi

加密后的密文

00408DB8

68 C46E4D00

push QQ.004D6EC4

密钥

00408DBD

FF75 10

push dword ptr ss:[ebp+10]

明文长度

00408DC0

FF75 0C

push dword ptr ss:[ebp+C]

明文

00408DC3

E8 52130000

call QQ.0040A11A  

TEA数据加密函数

 

这个函数非常完美,因为其参数没有一个多余的,我们就用前面介绍的知识来调用这个函数吧,可以看到,这个函数的调用形式为:

 

call QQ.0040A11A(明文,明文长度,,密钥,,加密后的密文,密文长度)

 

QQExe也具备导出函数(参见QQ导出表.txt),很多时候,根据这些函数名称再配合人类的想象,基本可以断定函数的功能并且在我们的程序中利用这个功能。我们如法炮制,具体实现过程参考上面的内容。

值得注意的是,我们使用本方法调用动态库的时候,程序的执行基本没什么问题,而调用QQ.exe这类应用程序的时候可能会死掉,这主要是因为在加载动态库(使用函数LoadLibrary)时,动态库的初始代码已经将必要的变量包括引用的库进行了初始化(例如,BasicCtrlDll.dll中的加密函数在执行的时候,要访问WSock32.DLL,但是,我们执行的时候好象没这种感觉,该库已经加载了),因此,当执行“mov edi,dword ptr ds:[<&MSVCRT.rand>]”汇编代码(即调用随机函数Rand)时,内存已经加载了MSVCRT模块并且在数据段ds:[<&MSVCRT.rand>]处进行了填充。然而加载QQ.exe时,并没有这么做,因此,执行到同样的语句时,由于该块内存没有初始化造成代码无法访问,出现错误,解决的办法是,将所有对此处.rand的调用修改成固定的汇编语句(参考上面的说明)。

不过,这也不是办法,如果还有其他一些必须执行的代码怎么办呢?关键的问题是希望QQ.exe能够自己进行初始化,该调用的调用,该填充的填充,我们为了研究他这么辛苦,他总该自己做点什么吧。

思路就是,直接执行QQ.exe,一旦启动后,他就会主动地去做这些事情了。我们等他做得差不多的时候,再将那些感兴趣的代码拿过来为我所用。

 

以上,我们找到了加密函数的位置,下面的任务就是寻找解密函数了,不过没必要到处找,一般对于腾讯这种大公司,源代码的书写和管理是很讲究的,因此,在程序的编制中,往往同样的功能函数会放在一起,编译以后的代码肯定也在一起。根据这种猜测,我们只用看看这个函数的左麟右李是不是有相似的代码(一般加密和解密是对称的,用我们的肉眼就能分辨),最明显的证据是在调用时一定有五个参数。根据跳转表和调用表(建议使用C32Asm.exe工具),我们发现位于加密函数0040A11A下方的0040A2EA处的代码段有重大嫌疑(这点和我们的编程思路有点一样哦,一般是加密在上,解密在下)。

 

004943CC

8D45 0C

LEA EAX,[EBP+C]

004943CF

50

PUSH EAX

004943D0

56

PUSH ESI

004943D1

FF75 10

PUSH DWORD PTR [EBP+10]

004943D4

FF75 0C

PUSH DWORD PTR [EBP+C]

004943D7

FF75 08

PUSH DWORD PTR [EBP+8]

004943DA

E8 0B5FF7FF

CALL 0040A2EA

 

我们可以写成这种调用形式:

CALL 0040A2EA(密文,密文长度,密钥,明文,明文长度)

 

由于QQ的通讯包是用密码加密的,并且,根据上面的分析,我们知道在加密的过程中,密码是随机的,因此,即使你捕捉到了所有的通讯包,但是,你会发现,这些包在毫无规律地变化着。要研究QQ的通讯协议,首先我们必须将加密后的通讯包还原出来。

现在,大家已经知道如何来解密通讯包,但是,我们不太幸运,因为动态库BasicCtrlDll.dll中没有解密函数,而我们发现该函数存在于QQ.exe文件中。

这里有个问题,使用LoadLibrary加载PE文件时,该函数实际上首先执行动态库的公用入口函数DllMain,并且,所加载的动态库是位于本进程的4G空间内的。而EXE中,入口函数为WinMain,因此,在使用LoadLibrary加载QQ.EXE时,QQ.EXE涉及到的很多模块都没有加载或者说初始化。当使用上述方法执行的时候,如果要执行外部的代码,会出现内存访问错误。因此,想利用我们找到的解密函数0040A2EA,我们会出现一些困难。

前面我谈到,可以通过先执行QQ.EXE,让其完全初始化后,我们就可以利用里面的解密函数了。但是,新的问题产生了。

Windows系统中,一个进程将独立占用4G的空间,当QQ.EXE运行后,它就是一个独立的进程了。实际上,我们的解密主程序是无法直接访问QQ.EXE的进程的(关于这些知识请参考Windows相关的理论)。总之我们的程序和QQ.EXE程序是生活在完全两个世界中,就像以色列的隔离墙,将这两个世界完全隔绝。我们使用普通的方法是无法对另一个世界的代码(或数据)进行访问的。

连门都不让我们进,那怎样利用QQ.EXE里的解密函数呢?

我想起了《渡江侦察记》,想起了《敌营十八年》,也想起了病毒技术,我们可以在运行了QQ.EXE后,再让它感染一个病毒(说病毒可能吓着你了吧?准确地说不是病毒,因为它既不复制自己也不感染其他文件,并且,它不做任何坏事,虽然它也没做什么好事。称之为间谍代码可能更酷一点),由于这个代码是运行在QQ.EXE的世界里,因此,它可以访问QQ.EXE里所有的资源。这个代码很善良,它做的唯一事情是接收我们的命令和参数,然后在QQ.EXE的空间里寻找解密函数,通过该函数计算结果,最后将结果送出来。

既然前面我们使用VB来完成这项工作,我会继续这么做。本来,完成这项工作用不着动用很多技巧,直接注入代码(也需要一点技巧)即可。用VC做,一点问题没有,但是,用VB的时候,我遇到一些困难,因为我们查看QQ.exe所加载的模块中,没有MSVBVM60模块,这样,注入的VB代码可能不会正常执行,所以,我不得不使用VC做的动态库来完成注入工作(用VC做库的时候,由于不是远程加载,所以不要使用任何需要访问外部数据的代码,例如使用API等。请参考QQSpyDll代码)。

最终经过权衡,我还是废弃了动态库的方式,改用VB中嵌入汇编的方式。这么做使用纯VB便能完成所有的工作。不过,使用动态库对于代码的维护方面是比较有利的,并且,如果用C来做,代码也简洁有效得多。

注入间谍代码以后,我们怎么能够得到返回值呢?Windows的这种保护机制,给我们制造了很多麻烦,但是,既然选择了这条路,死也要走过去。

我们可以使用共享内存(内存映射文件)、管道、消息或文件、注册表、反向钩子等等一切手段,然而,对于这么一个小问题来说,这些东西都显得很笨重,那么,最后我怎么得到加密或解密后的返回值呢?你可以参考我的程序,我使用了一种非常另类的做法(如果你在世界上任何一个地方看到这个代码,作者一定是Binny:)。

如果你认为这些东西过于复杂,你可以选择回去,也可以选择跳过,继续我后面的讨论。虽然复杂,但是我认为这种方法具有非常普遍的意义,我们必须针对不同的敌人采取不同的战术。我将附件中的代码进行了增强,用于实现这个功能。原理可以参照代码的解释,我不再赘述。不过,需要说明的是,这段代码是在瘟2000上运行的,其他的操作系统是否能正常运行很难说,你不会还用98吧?另外,必须再次说明,我的QQ版本是12.87.0.8059,如果你的版本与我的有区别,请自己寻找加密和解密函数在QQ中的位置,找到后,请填入QQEncryptTest.exe程序的地址文本框中。

用绝对偏移的方式可以得到主进程的位置(我没有指NT),如果希望得到其他模块的偏移,还需要多费周折。幸好我们所有的东西在QQ.exe中全部包含。

到现在为止,你是否感觉附件中的程序越来越大文章越来越长呢?是否感觉你的瞌睡越来越多理解越来越难呢?我们说到这里还没有切入正题。好了,我们现在开始研究QQ的通讯协议吧。聊了这么多,我们还在原始社会的石器时代,我们费了好大的劲做了很多石头工具,不过将来,你会利用这些工具完成人类的进化历程。

目前,我还没有在其他的地方看到这种方法,我不知道叫什么好,姑且称之为“代码借用”吧,因为我们做的所有事情,都是为使用这些已经编译好的代码,让他完成我们的功能。我想,以后如果你遇到了算法非常复杂的东西,你应该想起现在学到的一切。

开始运行你的程序QQEncryptTest.exe,你会看到,无论是选择“常数加密”、“随机加密”还是选择“EXE加密”,解密后,都可以得到唯一的结果。这说明,我们非常成功地破解了QQ的加密算法,该算法可以对包括汉字进行加解密。

QQEncryptTest.exe程序还提供了一个可以解密QQLog文件QQLog的功能让你小试牛刀,你可以将这个成果查看经过加密的QQ事件。看来,QQ真的很罗嗦,什么乱七八糟的都写到QQLog中,害得那个文件越来越大,也不知道是不是有循环覆盖的功能?不过,如果你解密所有的QQ事件,你会发现里面包含很多有趣的东西,至少你可以发现是谁每次偷偷地用你的QQ。我没有考虑代码的执行效率,如果你想做一个专业查看QQ日志的软件,是要重新规划的。

好了,回过头来看一看我们抓到的这些网络数据,它们除了戴的帽子差不多外,其余真的很不相象,现在我们开始使用上述的工具来对这些奇怪的数据进行分析吧。

02 0c 57 00 22 04 52 06 93 a8 3c ed eb 9e f1 67

a6 31 25 3b 13 6d 2a 45 f1 23 …… a0 03

这些东西代表什么呢?我们来做一个研究吧,现在你自己决定,是直接翻到最后一页看答案还是随着我慢慢揭开这个秘密?

为了知道这些数据的意义,我们就需要采集一些原始数据。仅仅有这些数据是不够的,最好我们能够知道QQ拿这些数据来干什么就好。因此,我们一方面采集网络中的数据,一方面来监视QQ,这样才能彻底了解其中的意义。

谈到监视,就需要使用HOOK技术了。HOOK包含的技术很多,并且分类也复杂,我们将要采用的技术和HOOK API有点类似,但是比HOOK API复杂点。因为没有简单的跳转表供我们修改,我们要修改的是QQ中的私有函数。你一定也猜到了,就是加密和解密函数了。我们的目的是让QQ加密前先通知我们看看它加密什么,QQ解密后再让我们看看是它解密了什么。不知我说清楚没?

原理很简单,在QQ的任何部分调用加密(或解密)函数前,先执行我们的程序,让我们把明文(或密文)保存下来后再执行那个函数,执行完毕后,再回到我们这里报到,将结果交给我们后然后再继续做其他的事情。QQ真的有这么乖么?其实,就像对女人一样,如果你理解她,她就会变乖的啦:P

说来惭愧,我们不是用真情来打动QQ,我们是用劫持和胁迫的手段达到这个目的。我们在函数前加一个强行跳转指令跳转到我们的程序,向我们报到后再回到它的世界,打佯后回到我们这里交税,最后我们才还给它自由。如果你想理解整个过程请参考我的程序代码LoInsertHook

代码里,为了偷懒我还使用了一个技巧,我在前期跟踪动态库BasicCtrlDll.dll的过程中,发现QQ利用这个动态库中的加密函数对部分QQ事件进行记录,并且输出到QQLog文件中,因此,我调用了该库中保存文件的私有函数(主要是不想设计日志文件,如果你不怕麻烦的话,你可以自己做一个更加专业的日志文件,但是,一定要记住你记录的是一个远程的进程,你必须考虑怎么加载的问题),在我的版本中这个函数位于10002577处,有如下的调用形式:

call BasicCtr.10002577(文件名称,数据,数据长度)

可惜这是一个加密函数,如果我有更多的时间,我会找一个更加完美的函数,因为我发现QQ还在记录QQAvatar_Log.txt文件。不过,如果我真的有很多时间的话,我就自己做一个LOG文件了。一定要记住我们的目的是研究QQ包的秘密,因此我们完全有理由偷这个懒,我不想把这个工具做得太完美,就随便利用一下QQ自己的保存文件的功能吧,反正我们有解密的函数。

现在,你可能已经体会到,原来VB也有这么强大的功能啊?不过你也许同时体会到了,VB的强大是建立在其他辅助语言的基础上的。几种语言亲密无间的配合,才能让我们将VB玩弄在股掌之间,才能完成这个复杂的工程。我之所以使用VB,是因为它很容易生成界面,让我们把注意力更加集中到破解QQ通讯包的秘密上。

我分别使用两个文件来记录QQ的加解密事件过程,缺省的情况下,这两个文件名称分别是QQHook_En.binQQHook_De.bin,文件位于工具QQEncryptTest下,这样,当我们想要监视QQ的通讯时,只需要单击QQEncryptTest .exe工具中的“加密HOOK”或“解密HOOK”键就可以了,然后,你可以在我们定义的LOG文件中得到QQ所有的加密和解密的过程调用的详细信息,用这些信息和Sniff到的数据包进行同步比较,就能非常清楚地观察到QQ的整个登录过程或者其他过程了。

谈到这里,我突然感觉到我们已经走得太远了太深了,无论是技术的底线还是道德的底线。我决定不再公开后面的这些源码以及HOOK功能,除非QQ彻底改版,否则真的可能对社会产生一点冲击。由于我没有收取你的任何费用,所以你不允许对我有任何意见。最后我想声明一下,我纯粹出于兴趣来做这些研究,并且我个人对腾讯公司没有任何敌意。如果你将这种手段用于破坏,你就违背了我的原则。如果你用于商业目的,你应该先告诉我一下,我不会收取任何费用,我只想知道我的这些东西没有用于非法目的。

这篇文章利用了QQ这个牺牲品给大家提供一个破解思路——怎样用其人之道制其人之身,怎样以毒攻毒,目的达到了,多的话我就不说了。我不是高手,我希望这个世界也不要有太多的高手。

最后我想说明的一点就是,我不会解答你提出的任何关于QQ的问题包括提供破解服务,这些问题我只同我最亲密的朋友交流。

 

 

Binny 2005-4-4于厦门

全文完 ……

附件下载压缩包说明:

ASM

目录包含qqmd5.dll的汇编源码

VB

目录包含QQ暴力破解,QQ TEA加密解密测试,Access 破解算法,

VBMD5密码算法

VC

为调用QQMD5.dll的示例以及QQ注入代码QQSpyDll.dll

App

包含上述测试和和应用程序,由于本包有所有的源代码,所以功能恕不说明。