破一桩疑案——硬件错误

阅读量    194152 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0、引言

这次分析的问题 ,与熊力书中遇到的一个案例特别类似,分析之余,发现还是有很多思路以及很多新奇的知识可以记录下来,与君分享。

涉及到以下知识点:

1、硬件断点的使用;
2、硬件cpuid指令与dmp中相关信息的查询;
3、dmp分析的一般思想方法;
4、虚拟内存的VAD树和页表;

 

1、背景

这次的dmp次数不多,目前根据后台聚类之后的结果,同类的只有一次。本来可以不用操心之,但谁让咸吃萝卜淡操心的我就喜欢干这个事呢。分析之后,确实发现了一个很奇怪的事情,安推论这种情况出现的概率是极其小的,基本不该发生,可偏偏就发生了,与我之前在熊力书中所看过的一个案例极其相似——硬件故障。敢下这个结论,也是极其小心,做了很多事情的。具体分析,且看下文。

 

2、分析过程

2.1 step1:看一下异常记录,如下,为了规避具体哪款软件触发的crash,这里隐藏掉相关信息,包括进程的各个模块名,代之以AppModule,AppExe这样的名字;

0:030> .exr -1
ExceptionAddress: 51212dae (VCRUNTIME140!memmove+0x0000004e)
   ExceptionCode: c0000005 (Access violation)
   ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 00000001
   Parameter[1]: 04c86000
Attempt to write to address 04c86000

简单看下,是个常规的访问异常,往0x04c86000这个内存地址中写入数据了;但奇怪的是触发问题的模块居然是VCRUNTIME140,这个模块可是OS的,或者说是IDE自带的库。这就奇怪了,且先看看这个内存地址是Free状态,还是只读状态或者什么其他诡异的情况:

0:030> !address 04c86000
Usage:                  Free
Base Address:           04c86000
End Address:            04ca0000
Region Size:            0001a000
Type:                   00000000
State:                  00010000    MEM_FREE
Protect:                00000001    PAGE_NOACCESS

由上可知,是Free状态,且保护属性是NOACCESS,这里多说一句,很多做Windows开发的人在这里可能会迷糊,State和Protect这两个有啥区别,微软为啥搞两个?其实对内核熟悉,或者多CPU的页机制熟悉的话,这个就很好理解了,前者是Windows操作系统为虚拟地址空间维护的一个树节点中,该虚拟内存的状态属性;后者是Windows内核为每个进程维护的页表中该虚拟内存对应的页表的属性;正常情况下这两个属性是完全一致的或者说是接近的;当然,前者是底层无关的,换一个架构比如PowerPC,Mips,Arm等等,也都是没问题,OK的;后者则是架构相关的,虽然其他的架构也有类似于x86的设计,但底层的实现逻辑肯定是有区别的;好了,暂且打住,回归正题;那既然这块内存不该写,为啥会往里边写?看看栈回溯吧,看看是否能够发现点蛛丝马迹;
2.2 step2:寻找程序执行的路径——栈回溯

0:030> .ecxr
eax=073d56c0 ebx=00000d90 ecx=ffeff6c4 edx=00000d90 esi=074d5ffc edi=04c86000
eip=51212dae esp=09edfbb8 ebp=09edfbdc iopl=0         nv up ei pl nz na po cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010203
VCRUNTIME140!memmove+0x4e:
51212dae f3a4            rep movs byte ptr es:[edi],byte ptr [esi]
0:030> kb
  *** Stack trace for last set context - .thread/.cxr resets it
ChildEBP RetAddr  Args to Child
09edfbbc 5470cb95 04b84934 073d4930 00000d90 VCRUNTIME140!memmove+0x4e [d:\agent\_work\2\s\src\vctools\crt\vcruntime\src\string\i386\MEMCPY.ASM @ 194]
09edfbdc 54751fbd 073d4930 00000d90 073a4058 AppExe+0xcb95
09edfc00 54751ae3 073d4930 000048f4 5487fd84 AppExe+0x51fbd
09edfc38 55d925d5 073d4930 00000d90 d3e7c270 AppExe+0x51ae3
09edfc70 55d922ea d3e7c290 05fbc528 55d92220 AppModule+0x25d5
09edfc90 55d9222a 09edfcd4 5115d5ef 09b9fa44 AppModule+0x22ea
09edfc98 5115d5ef 09b9fa44 d3e7f4e2 00000000 AppModule+0x222a
09edfcd4 75c7344d 05fbc528 09edfd20 77e59802 ucrtbase+0x3d5ef
09edfce0 77e59802 05fbc528 7e0d54e4 00000000 kernel32+0x1344d
09edfd20 77e597d5 5115d5b0 05fbc528 00000000 ntdll+0x39802
09edfd38 00000000 5115d5b0 05fbc528 00000000 ntdll+0x397d5

void *memmove(void *dst, const void *src, size_t n)

很清晰,代码里在拷贝数据,从VCRUNTIME140!memmove这一帧可知,代码执行时,是想从0x073d4930拷贝数据到0x04b84934,拷贝的长度是0x00000d90;根据对汇编中,这种拷贝指令的了解,一般情况下,编译器在生成汇编指令时,都是用的ecx/rcx作为的重复计算的次数,CPU内部每次执行完带有循环结构的指令,如loop或者rep之类的前缀时,都会检测rcx/ecx是否为0;那我们暂且先按照这个假设来看下出问题的寄存器上下文;ecx居然是0xffeff6c4;这不正常,这大概率是异常了,即ecx不停的减1,减到0时没有停止而是继续减下去了;这不合理啊。好了,不猜测了,我们看下memmove的汇编代码;根据Windbg提供的信息可知,memmove这个函数的实现在 MEMCPY.ASM这个文件中,用EveryThing搜索的了下,结果如下:

可以选择看源码去,也可以选择直接用Windbg 反汇编dmp文件中的汇编代码,前者留给你们去看,我们这次就直接看dmp中的汇编代码;截取部分如下:

0:030> uf VCRUNTIME140!memmove
Flow analysis was incomplete, some code may be missing
VCRUNTIME140!memmove [d:\agent\_work\2\s\src\vctools\crt\vcruntime\src\string\i386\MEMCPY.ASM @ 139]:
  139 51212d60 57              push    edi
  151 51212d61 56              push    esi
  156 51212d62 8b742410        mov     esi,dword ptr [esp+10h]
  157 51212d66 8b4c2414        mov     ecx,dword ptr [esp+14h]
  158 51212d6a 8b7c240c        mov     edi,dword ptr [esp+0Ch]
  168 51212d6e 8bc1            mov     eax,ecx
  170 51212d70 8bd1            mov     edx,ecx
  171 51212d72 03c6            add     eax,esi
  173 51212d74 3bfe            cmp     edi,esi
  174 51212d76 7608            jbe     VCRUNTIME140!memmove+0x20 (51212d80)
VCRUNTIME140!memmove+0x18 [d:\agent\_work\2\s\src\vctools\crt\vcruntime\src\string\i386\MEMCPY.ASM @ 176]:
  176 51212d78 3bf8            cmp     edi,eax
  177 51212d7a 0f8294020000    jb      VCRUNTIME140!TrailingUpVec+0x50 (51213014)
VCRUNTIME140!memmove+0x20 [d:\agent\_work\2\s\src\vctools\crt\vcruntime\src\string\i386\MEMCPY.ASM @ 183]:
  183 51212d80 83f920          cmp     ecx,20h
  184 51212d83 0f82d2040000    jb      VCRUNTIME140!TrailingDownVec+0x1eb (5121325b)
VCRUNTIME140!memmove+0x29 [d:\agent\_work\2\s\src\vctools\crt\vcruntime\src\string\i386\MEMCPY.ASM @ 185]:
  185 51212d89 81f980000000    cmp     ecx,80h
  186 51212d8f 7313            jae     VCRUNTIME140!memmove+0x44 (51212da4)
VCRUNTIME140!memmove+0x31 [d:\agent\_work\2\s\src\vctools\crt\vcruntime\src\string\i386\MEMCPY.ASM @ 187]:
  187 51212d91 0fba2514f0215101 bt      dword ptr [VCRUNTIME140!__isa_enabled (5121f014)],1
  188 51212d99 0f828e040000    jb      VCRUNTIME140!TrailingDownVec+0x1bd (5121322d)
VCRUNTIME140!memmove+0x3f [d:\agent\_work\2\s\src\vctools\crt\vcruntime\src\string\i386\MEMCPY.ASM @ 189]:
  189 51212d9f e9e3010000      jmp     VCRUNTIME140!memmove+0x227 (51212f87)
。。。省略

这个函数很长,大家在以后分析dmp或者做安全逆向分析恶意代码时也会遇到,没有源文件,那么我们肯定不会用Windbg或者其他工具来直接看,效率太低下了,用Notepad++来分析,会事半功倍;如下截图所示:

下边我们的任务显然就是来看看这个VCRUNTIME140!_favor的值是多少了;如下:

0:030> dd VCRUNTIME140!__favor
5121f2d0  ???????? ???????? ???????? ????????
5121f2e0  ???????? ???????? ???????? ????????
5121f2f0  ???????? ???????? ???????? ????????

很不幸,该dmp中没有保存这个数据,没办法,为了减小dmp文件的大小,必然要压缩掉一些数据,而有时候这些数据确实至关重要的。那怎么办?没关系,我们本地写个简单的小程序来测试下:

由图可知,这个变量的bit1位为1,为1意味着jae那条指令不跳,不跳意味着memmove的拷贝由rep这个指令前缀来重复操作完成的,但这里有个问题,我本地的这个__favor为2,是不是就意味着dmp用户那里的也为2呢?这个不好说,那怎么办?山穷水尽了吗?还没有,最快捷的办法就是百度下看看这个变量干嘛用的。可惜的是,百度出来一堆垃圾,或许Google一下能有一点点,但也别抱太大希望。好了,那就自求多福吧;

2.3 step3:寻找VCRUNTIME140!__favor的写入点——看看这个变量到底指代什么
如何确定这个变量到底在哪里初始化的呢?方法很多,比如用内存搜寻软件遍历IDE下的所有文件,或许能找到些,但作为专业的人应该干专业的事情,上调试器吧,下一个内存写断点,一切搞定。对于这个目的,VS可以做,但不专业,祭出Windbg吧,如下:

0:000> ba r4 VCRUNTIME140D!__favor
0:000> g
Breakpoint 0 hit
eax=00000000 ebx=029c67af ecx=00000004 edx=00000200 esi=00c0f0e4 edi=0f755720
eip=0f755947 esp=00c0f0bc ebp=00c0f104 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
VCRUNTIME140D!__isa_available_init+0x1f7:
0f755947 83c802          or      eax,2

有两点值得说明:1)此时我们应该用r标志,读写都可以断下开;2)此时我们下断点的模块是VCRUNTIME140D而不是VCRUNTIME140,应该当前处于调试模式;断下来了,多么神奇的操作,瞬间定位到感兴趣的地方;看样子是VCRUNTIME140D!__isa_available_init()这个函数里边初始化的;稍微往前看下这个指令的or eax,2在什么情况下会走到;

三次ub指令便找到了根源;原来是用的cpuid指令,传入的参数是7;当然根据善解人意的Windbg的提示可知,这个函数在cpu_disp.c这个文件中实现了,那就找下这个文件;

啥也没有,看来微软并不想把这个公开出来,没关系;我们先看下栈吧,看看什么逻辑走到的这里,然后再来深究这个cpuid+7到底干啥的,调用栈如下:

0:000> k
ChildEBP RetAddr  
00c0f104 0f749938 VCRUNTIME140D!__isa_available_init+0x1f7 [d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\misc\i386\cpu_disp.c @ 108]
00c0f10c 0f7556b8 VCRUNTIME140D!__vcrt_initialize+0x8 [d:\agent\_work\3\s\src\vctools\crt\vcruntime\src\internal\initialization.cpp @ 59]
00c0f114 0f755648 VCRUNTIME140D!DllMainProcessAttach+0x8 [d:\agent\_work\3\s\src\vctools\crt\vcruntime\src\dll\vcruntime_dllmain.cpp @ 17]
00c0f128 0f75573f VCRUNTIME140D!DllMainDispatch+0x28 [d:\agent\_work\3\s\src\vctools\crt\vcruntime\src\dll\vcruntime_dllmain.cpp @ 49]
00c0f13c 7786a896 VCRUNTIME140D!__vcrt_DllMain+0x1f [d:\agent\_work\3\s\src\vctools\crt\vcruntime\src\dll\vcruntime_dllmain.cpp @ 88]
00c0f15c 778401a6 ntdll!LdrxCallInitRoutine+0x16
00c0f1a8 77838141 ntdll!LdrpCallInitRoutine+0x55
00c0f230 77837e03 ntdll!LdrpInitializeNode+0x110
00c0f254 77837de6 ntdll!LdrpInitializeGraphRecurse+0x7d
00c0f27c 778a2a2b ntdll!LdrpInitializeGraphRecurse+0x60
00c0f4d8 77863242 ntdll!LdrpInitializeProcess+0x1d0f
00c0f534 7786310c ntdll!_LdrpInitialize+0xe0
00c0f544 00000000 ntdll!LdrInitializeThunk+0x1c

很清楚了,在进程刚启动时,加载VCRUNTIME140D执行其入口函数时,执行了初始化的动作;那下边就搜索下cpuid+7这个指令的具体作用了,如下:

根据搜索到的信息来看,这个所谓的ERMS确实是Intel提供的优化新能的指令,CPUID+7就是来检测当前的CPU是否支持这个性能优化指令的;一切都顺理成章;那下边就要确定下从哪一代Intel的CPU开始,这个ERMS特性是支持的了,这个要查询Intel的白皮书了;

由图可知,从Ivy Bridge这个架构开始就支持ERMS特性了,OK,那下一步就是确认,当前dmp产生式,用户的机器型号了,如下:

0:030> !cpuid
CP  F/M/S  Manufacturer     MHz
0  6,10,9  GenuineIntel    3201
1  6,10,9  GenuineIntel    3201
2  6,10,9  GenuineIntel    3201
3  6,10,9  GenuineIntel    3201

好,下边就检查一下这个参数所对应的型号信息;当然,我这里直接用了上报上来的信息了,如下:

那基本确定了;触发crash的用户的电脑是支持ERMS特性的;

2.4 step4:再次分析——得出结论

综上分析可知,程序最终走的是下边这行代码,ecx中存放的是复制的字节数,那看下数据;

51212dae f3a4            rep movs byte ptr es:[edi],byte ptr [esi]

0:030> db 073d4930 ld90
073d4930  b6 f9 2c bf af 27 b0 b7-56 2f 7f d8 7c e6 1f f6  ..,..'..V/..|...
073d4940  7b 33 47 6e be 17 04 f3-fb 12 90 75 9a 2a d7 0e  {3Gn.......u.*..
073d4950  8f 85 63 b8 09 a8 31 f8-00 63 ed 72 84 91 e0 2f  ..c...1..c.r.../
073d4960  53 c5 96 ee 14 6d ae 3c-c5 fc f5 84 59 31 6e ea  S....m.<....Y1n.
073d4970  06 8d 50 61 a0 74 06 6d-39 1c 64 4d e4 ce 24 d8  ..Pa.t.m9.dM..$.
073d4980  48 54 e8 be b4 05 3f b6-c4 a0 28 36 8b ef c0 a6  HT....?...(6....
073d4990  5f 1e 3e 1d 38 75 85 3f-77 97 97 e5 c8 ba 73 27  _.>.8u.?w.....s'
073d49a0  c7 a5 40 30 06 59 89 8f-02 e6 26 48 39 4a 51 c4  ..@0.Y....&H9JQ.
073d49b0  76 51 1b e5 5e b6 7e 7d-31 1b 62 df 3d 82 ed a0  vQ..^.~}1.b.=...
073d49c0  f6 3d 2c 13 07 0a d8 8b-75 73 56 7e 7f 7c a9 1b  .=,.....usV~.|..

0:030> db 04b84934 ld90
04b84934  b6 f9 2c bf af 27 b0 b7-56 2f 7f d8 7c e6 1f f6  ..,..'..V/..|...
04b84944  7b 33 47 6e be 17 04 f3-fb 12 90 75 9a 2a d7 0e  {3Gn.......u.*..
04b84954  8f 85 63 b8 09 a8 31 f8-00 63 ed 72 84 91 e0 2f  ..c...1..c.r.../
04b84964  53 c5 96 ee 14 6d ae 3c-c5 fc f5 84 59 31 6e ea  S....m.<....Y1n.
04b84974  06 8d 50 61 a0 74 06 6d-39 1c 64 4d e4 ce 24 d8  ..Pa.t.m9.dM..$.
04b84984  48 54 e8 be b4 05 3f b6-c4 a0 28 36 8b ef c0 a6  HT....?...(6....
04b84994  5f 1e 3e 1d 38 75 85 3f-77 97 97 e5 c8 ba 73 27  _.>.8u.?w.....s'
04b849a4  c7 a5 40 30 06 59 89 8f-02 e6 26 48 39 4a 51 c4  ..@0.Y....&H9JQ.
04b849b4  76 51 1b e5 5e b6 7e 7d-31 1b 62 df 3d 82 ed a0  vQ..^.~}1.b.=...
04b849c4  f6 3d 2c 13 07 0a d8 8b-75 73 56 7e 7f 7c a9 1b  .=,.....usV~.|..

数据都是一样的,说明拷贝的没什么问题,再来看看拷贝的长度:

0:030> r
Last set context:
eax=073d56c0 ebx=00000d90 ecx=ffeff6c4 edx=00000d90 esi=074d5ffc edi=04c86000
eip=51212dae esp=09edfbb8 ebp=09edfbdc iopl=0         nv up ei pl nz na po cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010203
VCRUNTIME140!memmove+0x4e:
51212dae f3a4            rep movs byte ptr es:[edi],byte ptr [esi]
0:030> ?esi-073d4930
Evaluate expression: 1054412 = 001016cc
0:030> ?04c86000-04b84934
Evaluate expression: 1054412 = 001016cc
0:030> ?00000d90+100000000-ffeff6c4
Evaluate expression: 1054412 = 001016cc

数据都是对的上的,基本确定是rep重复执行过程中,ecx为0了,但CPU却没有停止这个循环过程,而是继续执行,导致ecx变为了-1即0xFFFFFFFF,然后就一发不可收拾了;但不能100%保证这是个硬件问题,很可能是受到外部什么东西的干扰了,或者其他未知的情况,至此这桩议案总算有点眉目了;

 

3、总结

从一次dmp的分析中,我们先后讲解了分析问题的思路,在缺少关键数据的情况下如何在本地复现问题,又是如何利用调试器帮助我们快速的定位到关键数据点的;而后又紧接着介绍了如何根据仅有的数据一步一步搜索到更为有用的信息,最终基本确定问题的起因,希望通过此文,大家能够在面对此类问题时,有一个解决问题的思路;

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多