Windbg实战系列之升级半成品引发的血案

阅读量    91102 |   稿费 300

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

《Windbg实战系列》围绕着Windbg解决实际开发中遇到的各种各样的问题,其间会涉及到:

1)OS的异常分发机制:

a、从CPU到内核到用户态,一个异常是如何被分发的;

b、VEH、SEH、UEF、VCH等在异常分发中所扮演的角色;

c、关键的异常分发函数的逆向分析;

d、与异常分发相关的注册表配置项;

继续补充

2)Windbg的dmp分析:

a、dmp分析的各类命令与背后的实现原理;

b、各类dmp的分析实战;

c、dmp的文件格式解析;

d、常用的抓取dmp的方法;

e、dmp分析中栈回溯的各种方法;

继续补充

3)Windbg的动态调试排错:

a、动态调试及涉及到的断点命令与各类断点技巧;

b、gflags.exe与调试相关的配置;

c、堆对调试的支持——各种堆结构分析;

d、栈对调试的支持——GS选项;

e、通过调试解决各类问题:死锁、资源泄漏、内存泄漏、性能分析、镜像校验等等等;

继续补充

4)Windbg的内核学习:

a、在调试器中突破各种OS内核安全防护机制;

b、在调试器中分析OS的内核对象管理目录结构;

c、在调试器中分析OS的注册表结构实现注册表穿透;

d、在调试器中分析OS全局句柄表及进程局部句柄表实现提权;

e、在调试器中遍历全局ATOM表;

f、 在调试器中遍历全局HOOK链/局部HOOK链;

g、 在调试器中手动解析PDE/PTE页表,给0x00000000手动挂物理页;

继续补充

0、为了避免具体公司及相关项目信息的泄漏,本文中隐去了所有名称信息,代之以A、B之类的标识符;

 

1、背景

这两天client灰度了新版本,根据后台上报的crash来看,总体情况还算稳定,但列表中出现一类新的crash,之前没这么集中的出现过,这个新版本连着出现了多次,肯定是不正常的;下载一个下来分析了下,挺有代表性,记录下来;

 

2、分析过程:

2.1、step1:Windbg打开,.exr -1看了下异常记录里所呈现的信息,如下:

0:000> .exr -1
ExceptionAddress: 00822d00
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 00000008
   Parameter[1]: 00822d00
Attempt to execute non-executable address 00822d00

上边标红的两行,第一行的意思是说“访问异常”,基本就是访问了不该访问的内存,比如处于Free状态的内存;或者是该内存是只读的,却往里边写数据了;第二行的意思是说,代码执行了不可执行的部分,换句话说就是EIP跑飞了;当然这个第二行的提示文本是Windbg善解人意的自动推导出来辅助你分析的,可能对也可能不对;且看下边继续分析;

2.2、step2:看一下00822d00附近的代码:

0:000> u 00822d00
00822d00 0000            add     byte ptr [eax],al
00822d02 0000            add     byte ptr [eax],al
00822d04 0000            add     byte ptr [eax],al
00822d06 0000            add     byte ptr [eax],al
00822d08 0000            add     byte ptr [eax],al
00822d0a 0000            add     byte ptr [eax],al
00822d0c 70e1            jo      00822cef
00822d0e 8400            test    byte ptr [eax],al

确实不正常,从反汇编出来的代码看,这段数据不应该是可执行代码,EIP确实跑飞了;为了确保万无一失,再看下这块内存的属性,如下:

0:000> !address 00822d00
Usage:                  Image
Base Address:           007fb000
End Address:            0084b000
Region Size:            00050000 ( 320.000 kB)
State:                  00001000          MEM_COMMIT
Protect:                00000002          PAGE_READONLY
Type:                   01000000          MEM_IMAGE
Allocation Base:        00690000
Allocation Protect:     00000080          PAGE_EXECUTE_WRITECOPY
Image Path:             G:aaaabbbbccccdddd.dll
Module Name:            dddd
Loaded Image Name:      dddd.dll
Mapped Image Name:      c:usersaaaadesktoppdbdddd.dll

入上边标红的属性所示,这块内存确实不可执行,也正如Windbg提示的那样。此外,我们还可以得到的信息是,这块内存确实是cgclient.dll模块中的,而且这块内存也是MEM_COMMIT的,换句话说它确实是挂了物理页的;那么EXCEPTION_RECORD中记录的Access violation基本就是往只读内存块写入数据了,这个也被PAGE_READONLY所佐证;

2.3、step3:下面就是将上下文恢复到出现问题[异常]时的那会了;执行.ecxr即可;

0:000> .ecxr
eax=0080e41c ebx=0058ea28 ecx=0059e0b4 edx=0554ce08 esi=07b600d0 edi=0000001d
eip=00822d00 esp=0027d690 ebp=0027d6e0 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00210202
00822d00 0000            add     byte ptr [eax],al          ds:002b:0080e41c=d0

简单分析下上边所给出的信息;触发异常的显然就是这句汇编代码了,触发的原因也自然是eax这块内存有问题,啥问题?不可写或者根本就是Free的呗;到底是哪一种情况呢?看一下这块内存的属性吧,如上图所示,这块内存确实是MEM_COMMIT的,就排除了Free的可能性,那自然就是只读的这种情况了,结合PAGE_READONLY便铁证如山了;好,到现在已有的结论整理如下:
EIP跑飞了,去执行了一块不可执行内存的数据且往一块只读内存里写数据;这边留一个疑问,既然这块内存不可执行,那为何触发的不是不可执行异常呢?

2.4、step4:下一个问题就是哪里导致的程序EIP跑飞了?

大家可以先停一下,思考一下这个问题;从汇编角度来说,肯定是执行了修改EIP寄存器的指令了;常规的修改EIP之类的方法主要有两类,如下:

1)jcc类;
2)call类;

当然除了这两类,还有很多其他的变通方法,如ret、ret n、iret、syscall、sysexit、int n、调用门、中断门、陷阱门、任务门、任务段等等方式。但考虑到常规的开发中都是用高级语言编写的,不太会涉及到底层的汇编语言,而C/CPP这些语言遇到最多的便是if-else switch-case这些分支语句,他们编译后对应的都是jcc类,虚函数调用编译后则对应的是call类;jcc导致的EIP跑飞的可能性不大,因为正常情况下,jcc跳转都是段跳转,基本都是在一个函数内跳转,同一个函数内的内存属性是一致的,所以jcc基本排除;但这不是100%的,笔者曾经遇到过一起案例,就是jcc导致的EIP跑飞,这种场景在安全对抗中常见;那剩下的就是call这一类了;可能大家会问,为什么非要区分个所以然出来呢?嗯,好问题。既然要找到是哪里触发的EIP跑飞,那自然是想要知道上一条执行在哪里执行的了;

2.5、step5:直捣黄龙,揪出bug;且看下边的执行命令;

0:000> dd 0027d690
0027d690  013a2220 c3167915 0939bd50 017941f8
0027d6a0  0939be60 092b510c 092b5100 09331218
0027d6b0  00000000 07b600d0 30303100 00003330
0027d6c0  00000008 0939bd50 00000000 0000000f
0027d6d0  c3167915 0027d75c 0165f6c2 ffffffff
0027d6e0  0027d768 01610c54 0939bd60 0939bd74
0027d6f0  0027d708 00000001 c316789d 0939bd50
0027d700  07b38548 092b2958 0052e9f0 00540000

0:000> !address 013a2220
Usage:                  Image
Base Address:           01371000
End Address:            0168a000
Region Size:            00319000 (   3.098 MB)
State:                  00001000          MEM_COMMIT
Protect:                00000020          PAGE_EXECUTE_READ
Type:                   01000000          MEM_IMAGE
Allocation Base:        01370000
Allocation Protect:     00000080          PAGE_EXECUTE_WRITECOPY
Image Path:             G:aaaabbbbcccc..kkkkmmmm.exe
Module Name:            mmmm
Loaded Image Name:      mmmm.exe

0:000> ub 013a2220
013a21f5 ff1590b66801    call    dword ptr [0168b690]
013a21fb c70694447901    mov     dword ptr [esi],01794494
013a2201 c7460898467901  mov     dword ptr [esi+8],01794698
013a2208 c745fcffffffff  mov     dword ptr [ebp-4],0FFFFFFFFh
013a220f 8b8bec010000    mov     ecx,dword ptr [ebx+1ECh]
013a2215 89b3d8010000    mov     dword ptr [ebx+1D8h],esi
013a221b 8b01            mov     eax,dword ptr [ecx]
013a221d ff5054          call    dword ptr [eax+54h]

找出来了,罪魁祸首就是上边标黄色的这行;这行为什么会有问题呢?简单分析下,上边比较关键的几行汇编指令截取如下:

013a221b 8b01            mov     eax,dword ptr [ecx]
013a221d ff5054          call    dword ptr [eax+54h]

对CPP底层了解的同学应该知道,CPP的方法调用约定默认是 _thiscall,该调用约定的传参方式是第一个参数通过ECX寄存器传递,其他参数通过栈传递,那么对于CPP来说,通常第一个参数都是编译器自动安排的对象地址,那么ECX就是指向该对象地址了;带有虚函数的类,其对象的第一个元素便是虚表指针,指向该类所提供的虚函数;那么013a221b处汇编指令就是取出虚表指针了;我们可以通过查看下eax的数据来看下上边这个结论是否正确;如下:

由step3知道eax=0080e41

0:000> dps 0080e41c
0080e41c  006cd8d0 dddd!nn::hhhh::uuuu::`scalar deleting destructor'
0080e420  006cda10 dddd!nn::hhhh::uuuu::loadAAAA
0080e424  006cda20 dddd!nn::hhhh::uuuu::setBBBB 
0080e428  006cda40 dddd!nn::hhhh::uuuu::getBBBB 
0080e42c  006cda50 dddd!nn::hhhh::uuuu::setCCCC
0080e430  006cda70 dddd!nn::hhhh::uuuu::getCCCC
0080e434  006cda90 dddd!nn::hhhh::uuuu::setDDDD
0080e438  006cdab0 dddd!nn::hhhh::uuuu::getDDDD
0080e43c  006cdaf0 dddd!nn::hhhh::uuuu::setEEEE
0080e440  006cdb10 dddd!nn::hhhh::uuuu::getEEEE
0080e444  006cdb30 dddd!nn::hhhh::uuuu::setFFFF
0080e448  006cdb50 dddd!nn::hhhh::uuuu::getFFFF 
0080e44c  006cdb70 dddd!nn::hhhh::uuuu::getMMMM
0080e450  006cdbb0 dddd!nn::hhhh::uuuu::getMMMM
0080e454  006cdbc0 dddd!nn::hhhh::uuuu::updateJJJJ
0080e458  006cdc10 dddd!nn::hhhh::uuuu::getTTTT
0080e45c  006cdc30 dddd!nn::hhhh::uuuu::getRRRR
0080e460  006cdc70 dddd!nn::hhhh::uuuu::getEEEE
0080e464  006cdc80 dddd!nn::hhhh::uuuu::getWWWW
0080e468  006cdd10 dddd!nn::hhhh::uuuu::getQQQQ
0080e46c  006cddb0 dddd!nn::hhhh::uuuu::applyYYYY 
0080e470  00822d00 dddd!nn::hhhh::uuuu::`RTTI Complete Object Locator'
0080e474  006cd6e0 dddd!nn::hhhh::uuuu::`scalar deleting destructor'
0080e478  007c2b61 dddd!purecall
0080e47c  007c2b61 dddd!purecall

基本判断无误,好了接下来013a221d处的汇编代码就很好理解了,便是调用虚函数,该虚函数的索引号如下:

0:000> ?54/4
Evaluate expression: 21 = 00000015

细心数一下就会发现,21号虚函数对应的恰恰是上边标蓝的这行,再看下这个所谓的方法的地址是00822d00,这个地址与前边EXCEPTION_RECORD中记录的是一致的,相互佐证;

2.6、step6:为什么会call到这个地方来?编译器问题?玄学?

我去看了下git上该分支下这个类的虚函数定义,发现本次的更新包中,该类新增了两个虚函数方法,索引号21便是其中新增的一个虚函数,但这里却实实在在的告诉我——该类就是没有你新增的这个接口,赤裸裸的挑衅;啥原因呢?看下版本号吧,如下:

0:000> lmvm mmmm
Browse full module list
start    end        module name
01370000 0187d000   mmmm
    Loaded symbol image file: mmmm.exe
    Mapped memory image file: c:usersaaaadesktoppdbmmmm.exe
    Image path: G:aaaabbbbcccc..kkkkmmmm.exe
    Image name: mmmm.exe
    Browse all global symbols  functions  data
    Timestamp:        Mon Jun  1 11:08:39 2020 (5ED47137)
    CheckSum:         0050FCE2
    ImageSize:        0050D000
    File version:     10.30.2000.1085
    Product version:  10.30.2000.1085
    File flags:       0 (Mask 3F)
    File OS:          40004 NT Win32
    File type:        1.0 App
    File date:        00000000.00000000
    Translations:     0804.04b0
    Information from resource tables:
        CompanyName:      CCCC
        ProductName:      BBBB
        InternalName:     mmmm.exe
        OriginalFilename: mmmm.exe
        ProductVersion:   10.30.2000.1085
        FileVersion:      10.30.2000.1085
        FileDescription:  CCCC
        LegalCopyright:   Copyright (C) 2018

0:000> lmvm dddd
Browse full module list
start    end        module name
00690000 0086b000   dddd
    Loaded symbol image file: dddd.dll
    Mapped memory image file: c:usersaaaadesktoppdbdddd.dll
    Image path: G:aaaabbbbccccdddd.dll
    Image name: dddd.dll
    Browse all global symbols  functions  data
    Timestamp:        Wed May 20 16:58:22 2020 (5EC4F12E)
    CheckSum:         001E8204
    ImageSize:        001DB000
    Translations:     0000.04b0 0000.04e4 0409.04b0 0409.04e4
    Information from resource tables:

嗯,很明显,下边这个dll没有文件版本号这个信息,那该怎么办?退而求其次,比对下其他信息;

mmmm.exe的Timestamp为 Mon Jun  1 11:08:39 2020 (5ED47137) -------->2020.06.01 11:08:39
dddd.dll的Timestamp为 Wed May 20 16:58:22 2020 (5EC4F12E) -------->2020.05.20 16:58:22

很显然了,升级的时候,mmmm.exe升级成功了而dddd.dll升级失败了,下次用于再次启动mmmm.exe时,调用打了一个旧的dddd.dll中没有的方法,然后EIP就跑飞了,再然后程序就Crash了;

 

3、这便是整个分析过程了;

 

4、小彩蛋——探寻命令背后的实现原理

Q: .exr与.ecxr他们在干什么?不用这两个命令是否能够进行dmp的分析?
A: 当然可以,并且很多情况下,这两个命令压根没啥用,下篇将专门讲解.exr与.ecxr背后的实现原理,看看如何手工分析数据以替换这两个命令的工作;这里简单提下:

.exr           ---->    _EXCEPTION_REOCRD;
.ecxr/.excr    ---->    _CONTEXT
分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多