前两天和鞠总一起成功解决了一个加密锁服务程序的内存访问错误。对于内存访问错误,我们以前一直认为难以定位而置之不理,其实要是好好跟踪分析一下,还是有可能解决的。在这里把解决过程描述一下,希望对大家以后处理类似问题有些帮助。
硬件环境
TEST(192.168.0.3), PIII 700, 512M, Morrowsoft USB Lock Installed
软件环境
Windows 2000 Server, SQL Server 2000, LinkWorks 2.4 SP2, LinkProject 2.1, MRLock Server
背景说明
鞠总十一期间对加密锁驱动程序 MrDriver.dll 进行了若干改进,解决了当被加密软件与加密锁服务分别以服务方式和普通方式启动时,无法读写对方共享内存的问题。为了兼容以前的软件,在加密锁服务程序 MrLocks.exe 中增加了版本判断,根据客户端连接使用的加密锁驱动版本来决定使用哪种方式通讯。
错误现象
更新 TEST 机上的加密锁服务和加密锁驱动后,正常运行了两三天,但突然出现了内存访问错误(如图所示),导致加密锁服务异常退出,所有加密软件都因为无法连接到加密锁而弹出了加密锁连接对话框。LinkWorks 和 LinkProjects 都停止了运行。
错误分析
以前我们遇到这种内存访问错误,是很难定位的,一般会通过写错误日志,弹出调试信息对话框等方式来确定错误位置,但这种方式只适用于可以较快重现的错误。这次遇到加密锁服务错误后,再想重现却一直不出了,说明这个错误只在某种特定条件下才会出现,而我们现在很难确定是什么操作导致了这个错误。
初步尝试
我们唯一知道的信息就是出错地址,从图上看到错误地址是“0x00405529”,于是萌生了用反汇编工具查找错误位置的想法。我以前经常用 Borland Turbo Debugger 破解一些没有源码的 Delphi 控件,去掉讨厌的试用对话框,所以这次首先尝试了 Turbo Debugger。使用 TD32 加载 mrlocks.exe 后,输入偏移地址“00405529”,定位到的汇编码是
:00405528 EF out dx,ax
:00405529 AD lodsd
:0040552A 267676 jbe es:MRLOCKS.004055A3
从这段代码及其上下文中,我们看不出任何端倪,不过用 ULTRA EDIT 32 查看 mrlocks.exe,只搜索到了一处“EFAD”的十六进制码,于是让鞠总尝试将代码一段段注释编译,看注释掉哪段代码后不再出现“EFAD”。但马上发现,其实只要用 ASPACK 压缩过的文件,就会出现一个“EFAD”。这说明我们直接打开 exe 文件后看到的汇编码是不对的,因为已经被 ASPACK 封装过了。要看到实际的执行代码,必须去查看 exe 文件运行起来后加载到内存里的代码才行。
再次尝试
从网上检索反汇编工具,下载了一个比较有名的 W32Dasm。先运行 mrlocks.exe,然后运行 W32Dasm,选择“附加到一个活动的进程”,附加到 mrlocks.exe 上去,W32Dasm 经过一通分析,就将 mrlocks.exe 的内存执行代码都搞出来了。执行“转到地址”命令,输入发生错误的偏移地址“00405529”,W32Dasm 就定位到如下代码段
:004054BE nop
:004054BF nop
:004054C0 sub esp, 0000001C
:004054C3 mov dword ptr [esp], ecx
:004054C7 push ebx
:004054C8 mov ecx, dword ptr [esp+2C]
:004054CC push ebp
:004054CD push esi
:004054CE cmp ecx, 00000100
:004054D4 push edi
:004054D5 jg 004055BD
:004054DB lea eax, dword ptr [ecx+07]
:004054DE mov esi, dword ptr [esp+34]
:004054E2 cdq
:004054E3 and edx, 00000007
:004054E6 mov edi, 0043643C
:004054EB add eax, edx
:004054ED mov ebp, eax
:004054EF mov eax, ecx
:004054F1 shr ecx, 02
:004054F4 repz
:004054F5 movsd
:004054F6 mov ecx, eax
:004054F8 and ecx, 00000003
:004054FB sar ebp, 03
:004054FE repz
:004054FF movsb
:00405500 shl ebp, 03 (源码在这里有个乘 8 的操作)
:00405503 call MSVCRT.rand (这个 API 调用为定位源码位置有很大帮助)
:00405509 mov cl, byte ptr [0042DCA1]
:0040550F mov ebx, dword ptr [esp+30]
:00405513 cmp cl, 04 (这里就是判断软件所使用驱动版本的地方)
:00405516 jnb 0040552D (如不低于 4,即使用的是最新驱动,应该跳离)
:00405518 mov edx, dword ptr [esp+10]
:0040551C and ebx, 0000FFFF
:00405522 mov ecx, dword ptr [edx+4*ebx+00000138]
* :00405529 mov dword ptr [ecx], eax (出错点,本机使用的是最新驱动,代码不应该走到这里的)
:0040552B jmp mrlocks.00405540
:0040552D mov edx, dword ptr [esp+10] (>=4)
:00405531 and ebx, 0000FFFF
:00405537 mov ecx, dword ptr [edx+4*ebx+000002C8]
:0040553E mov dword ptr [ecx], eax
:00405540 mov al, byte ptr [0042DCA1]
:00405545 mov ecx, 00000006
:0040554A mov esi, 0044538C
:0040554F lea edi, dword ptr [esp+14]
:00405553 cmp al, 04
:00405555 repz
:00405556 movsd
:00405557 jnb 00405568
:00405559 mov edx, dword ptr [edx+4*ebx+00000138]
:00405560 mov eax, dword ptr [edx]
:00405562 mov dword ptr [esp+1D], eax
:00405566 jmp mrlocks.00405575
:00405568 mov ecx, dword ptr [edx+4*ebx+000002C8]
:0040556F mov edx, dword ptr [ecx]
:00405571 mov dword ptr [esp+1D], edx
:00405575 lea eax, dword ptr [esp+14]
:00405579 push eax
:0040557A push ebp
:0040557B push 0043643C
:00405580 call mrlocks.004017B0
:00405585 mov al, byte ptr [0042DCA1]
:0040558A mov edx, dword ptr [esp+1C]
:0040558E add esp, 0000000C
:00405591 cmp al, 04
:00405593 mov ecx, ebp
:00405595 mov esi, 0043643C
:0040559A jnb 004055A5
:0040559C mov edi, dword ptr [edx+4*ebx+00000138]
:004055A3 jmp mrlocks.004055AC
:004055A5 mov edi, dword ptr [edx+4*ebx+000002C8]
:004055AC mov eax, ecx
:004055AE add edi, 00000004
:004055B1 shr ecx, 02
:004055B4 repz
:004055B5 movsd
:004055B6 mov ecx, eax
:004055B8 and ecx, 00000003
:004055BB repz
:004055BC movsb
:004055BD pop edi
:004055BE pop esi
:004055BF pop ebp
:004055C0 pop ebx
:004055C1 add esp, 0000001C
:004055C4 ret 000C
:004055C7 nop
:004055C8 nop
错误定位
在对上面这段代码进行分析后,终于能够比较快的定位源码所在了。出错点的代码是 mov dword ptr [ecx], eax,这是个内存操作,与错误的表现是相符的,但程序有那么多内存指针操作,还需要更多信息来判断这是哪个函数里的代码。出错点上面的 call MSVCRT.rand 帮了大忙,这是调用 MSVCRT.DLL 中的 rand 函数,在源码中搜索所有调用 rand 的地方,再对比每个搜索结果的上下文,很快就知道这段代码来自处理加密锁本地连接的函数。因为这个函数在 rand 之前有一个乘 8 的操作,与 shl ebp, 03 恰好对应,在 rand 后面有版本比较的代码,最新驱动版本号就是 4,恰好与 cmp cl, 04 匹配了。如果软件以本地连接方式与加密锁通讯,会先判断本地驱动的版本号是否低于 4,如果不低于 4,则 jnb 0040552D,否则就使用老的通讯协议,往下执行这段导致错误的代码
:00405518 mov edx, dword ptr [esp+10]
:0040551C and ebx, 0000FFFF
:00405522 mov ecx, dword ptr [edx+4*ebx+00000138]
* :00405529 mov dword ptr [ecx], eax (出错点,本机使用的是最新驱动,代码不应该走到这里的)
:0040552B jmp mrlocks.00405540
判断原因
这样我们就比较容易判断问题所在了。错误出在处理本地连接加密锁的代码中,安装在 TEST 上的软件只有 LINKPROJECT 是采用本地连接方式访问加密锁的,其他软件都是通过网络连接访问加密锁,那么应该是 LINKPROJECT 服务被激活时出的错。可是,TEST 那台机器上的 MrDriver.dll 已经是最新的了,所以 cmp cl, 04 这句版本判断的结果肯定不应小于 4 啊。实际情况是,使用最新驱动的本地连接却被服务当成了使用旧版驱动的本地连接来处理,从而导致内存访问错误。那么,肯定是那个保存版本号的变量当时确实是小于 4 了。为什么它会小于 4 了呢?于是检查保存版本号的变量,发现这个变量居然被声明成全局变量了。按理说,应该是每个加密锁连接中保存一个私有的版本变量,这样才能根据各个连接使用的驱动版本决定如何进行通讯。由于这个变量被声明为全局公用变量,当有用户从其他机器使用旧版驱动访问加密锁时,这个变量就被改写为小于 4 的数,之后本机 LINKPROJECT 服务被激活,使用本机新版驱动去与加密锁服务通讯,而 mrlocks.exe 根据这个全局变量判断,还以为它使用的是旧版驱动的,于是采用旧版协议进行通讯,从而导致内存访问错误。
解决问题
找到问题所在后,解决就很简单了,将这个版本号作为私有变量放到到处理加密锁连接的类里去就行了,这样每个连接与加密锁服务通讯时用的使用的都是自己的版本号,不会相互影响,也就不会出错了。
作者:孙宗林