12.1 识别类和类之间的关系(10)
指令“call dword ptr [eax+8]”揭示出虚表中至少有三个元素!接下来分析虚表第三项是什么内容。
- .rdata:0040C0D0 vTable_40C0D0 dd offset sub_4011C0 ; 虚表偏移0处,也就是虚表的第一项
- .rdata:0040C0D4 off_40C0D4 dd offset sub_401140 ; 虚表偏移4处,也就是虚表的第二项
- ; 虚表偏移8处,也就是虚表的第三项,现在可以确定GetCChinese是虚表的元素之一
- .rdata:0040C0D8 pfnGetCChinese dd offset GetCChinese
接着往下看:- .text:00401145 push eax ; 向printf传入GetCChinese的返回值,是个字符串首地址
- .text:00401146 push offset Format ; "%s::ShowSpeak()\r\n"
- .text:0040114B call _printf
- .text:00401150 add esp, 8 ; 调用printf显示字符串,并平衡参数
- .text:00401153 retn
- .text:00401153 sub_401140 endp
这个函数的作用是调用虚表第三项元素,得到字符串,并将字符串格式化输出。由于是按虚表调用的,因此会形成多态性。顺便把这个函数的名称修改为ShowShtring,对应的虚表内的函数指针off_40C0D4修改为pfnShowShtring,修改后虚表结构如下所示: - .rdata:0040C0D0 vTable_40C0D0 dd offset sub_4011C0
- .rdata:0040C0D4 pfnShowShtring dd offset ShowShtring
- .rdata:0040C0D8 pfnGetCChinese dd offset GetCChinese
我们回到main函数处,继续分析:- .text:00401103 test esi, esi
- .text:00401105 jz short loc_40110F ; 检查堆指针,不为0则往下执行
- .text:00401107 mov edx, [esi] ; edx得到虚表
- .text:00401109 push 1 ; 传入参数
- .text:0040110B mov ecx, esi ; 传递this指针
- .text:0040110D call dword ptr [edx] ; 调用虚表中的第一项
- .text:0040110F ; 从00401105处跳转到此,其上没有jmp,所以这里是个单分支结构
- .text:0040110F loc_40110F:
- .text:0040110F mov ecx, [esp+14h+var_C] ; 函数退出,恢复环境,还原SEH
- .text:00401113 pop esi
- .text:00401114 mov large fs:0, ecx
- .text:0040111B add esp, 10h
- .text:0040111E retn
- .text:0040111E _main endp
call dword ptr [edx]命令调用虚表的第一项。在详细分析虚表的第一项之前,我们体验一下IDA中的交叉参考功能,一次性定位所有的构造函数和析构函数,先定位到虚表vTable_40C0D0处,然后右击,如图12-4所示。
在右键菜单中选择“Chart of xrefs to”,得到所有直接引用这个地址的位置,如图12-5所示。
可以看到,除了main函数访问了虚表vTable_40C0D0之外,sub_4011E0也访问了虚表vTable_40C0D0。通过前面的分析可知,是因为main函数中内联的构造函数存在写入虚表的操作,从而导致vTable_40C0D0被访问到。由于存在虚表,就算类中没有定义析构函数,编译器也会产生默认的析构函数,因此,毫无疑问另一个访问虚表的函数sub_4011E0就是析构函数。交叉参考这个功能很好用,如果你发现了一个父类的构造函数,想知道这个父类有多少个派生类,也能利用这个功能快速定位。
|
| 图12-4 交叉参考 |
|
| 图12-5 IDA自动生成的交叉参考图示 |