唉,看雪发帖有一点不好,就是复制过来颜色都没了,本来自己写的注释是红色的,希望能够比较醒目,不过这样一来全变成黑的了。。。没办法,第一次发帖自然要认真一些,手动把所有原来的注释都改成红色的了,希望这样能够看得更加清楚一些。
我是菜鸟哈,各位有什么意见尽可和我说,欢迎拍砖吐槽!
这是《黑客免杀攻防》中关于逆向C++的唯一一篇总结,前面的基础知识由于去年已经学习过了钱林松老师的《C++反汇编与逆向分析技术》并做了笔记所以就只是看了一下,发现内容大同小异,不过看过一遍之后也温习了一下C++逆向的基础知识,对于下面的这个菱形继承的实验还是有帮助的。并且之前在学习钱老师的书的时候并没有给继承以及虚函数机制写过总结,这篇就当作是C++逆向系列的补充吧。
为了分析清楚C++的继承以及虚函数机制,这里使用了程序的Debug版本。直接使用IDA加载分析得到如下代码:
.text:0042D670 push ebp
.text:0042D671 mov ebp, esp
.text:0042D673 push 0FFFFFFFFh
………………………… …………………………..
;以上代码均为保存寄存器以及栈初始化等操作的代码,可不必理会
.text:0042D69C mov eax, dword_4950C8
.text:0042D6A1 xor eax, ebp
.text:0042D6A3 push eax
.text:0042D6A4 lea eax, [ebp+var_C]
.text:0042D6A7 mov large fs:0, eax
.text:0042D6AD lea ecx, [ebp+class1]
;取地址指令
.text:0042D6B0 call sub_42BA28
;结合上面的取地址指令可以知道这里应该是调用第一个类的构造函数,因此使用IDA的改名功能改成class1
.text:0042D6B5 mov [ebp+var_4], 0
.text:0042D6BC lea ecx, [ebp+class2]
.text:0042D6BF call sub_42B320
;同理,这是class2
.text:0042D6C4 mov byte ptr [ebp+var_4], 1
;这类指令将对象数目存至栈中,只有debug版本才有,无需理会
.text:0042D6C8 lea ecx, [ebp+class3]
.text:0042D6CB call sub_42BB5E ;这是class3
.text:0042D6D0 mov byte ptr [ebp+var_4], 2
.text:0042D6D4 lea ecx, [ebp+class4]
.text:0042D6DA call sub_42B64F ;这是class4
.text:0042D6DF mov byte ptr [ebp+var_4], 3
.text:0042D6E3 lea eax, [ebp+class1]
.text:0042D6E6 mov [ebp+pobj1], eax
;上面这几句代码是取出class1的地址然后赋值给另外一个变量,这个变量应该是一个class1类或是其基类的指针,改名为pobj1
.text:0042D6EC mov esi, esp
.text:0042D6EE push 0
;这是参数
.text:0042D6F0 mov eax, [ebp+pobj1]
.text:0042D6F6 mov edx, [eax]
.text:0042D6F8 mov ecx, [ebp+pobj1]
.text:0042D6FE mov eax, [edx]
;上面蓝色三条指令就是取出类的虚表中的函数的固定步骤,下一句便是调用取出的函数
;而红色的则是将this指针放到ecx中,更加确定了这个函数是类中的函数
.text:0042D700 call eax
.text:0042D702 cmp esi, esp
.text:0042D704 call sub_42BD70
;书上没有上面这两条语句,经过OD调试,发现这是VS2010在dubug版本中加入的栈检查函数,可不必理会
.text:0042D709 lea eax, [ebp+class2]
.text:0042D70C mov [ebp+pobj1], eax
.text:0042D712 mov esi, esp
.text:0042D714 push 1
.text:0042D716 mov eax, [ebp+pobj1]
.text:0042D71C mov edx, [eax]
.text:0042D71E mov ecx, [ebp+pobj1]
.text:0042D724 mov eax, [edx]
.text:0042D726 call eax
.text:0042D728 cmp esi, esp
.text:0042D72A call sub_42BD70
.text:0042D72F lea eax, [ebp+class3]
.text:0042D732 mov [ebp+pobj1], eax
.text:0042D738 mov esi, esp
.text:0042D73A push 2
.text:0042D73C mov eax, [ebp+pobj1]
.text:0042D742 mov edx, [eax]
.text:0042D744 mov ecx, [ebp+pobj1]
.text:0042D74A mov eax, [edx]
.text:0042D74C call eax
;直到这里都是同一类型的函数调用,说明class1、class2、class3虚表中至少有一个函数,并且都是使用的同一个指针,说明这三个对象要不就是同一个类,要不就是基类相同
.text:0042D74E cmp esi, esp
.text:0042D750 call sub_42BD70
.text:0042D755 push 0Ah
.text:0042D757 lea ecx, [ebp+class4]
.text:0042D75D call sub_42B087
;这里调用class4的函数,不过这里没有使用pobj1指针来调用,说明是通过对象调用的
.text:0042D762 push 1Ch
.text:0042D764 call sub_42C086
;这个函数有一个0x1C作为参数,但是不知道是干嘛用的,先继续看
.text:0042D769 add esp, 4
.text:0042D76C mov [ebp+var_1F0], eax
.text:0042D772 mov byte ptr [ebp+var_4], 4
.text:0042D776 cmp [ebp+var_1F0], 0
.text:0042D77D jz short loc_42D792
;将函数的返回值和0比较,若为0则调到函数结尾,这是需要注意的
.text:0042D77F mov ecx, [ebp+var_1F0]
.text:0042D785 call sub_42B320
;看到这里是不是感觉有点熟悉了?看到ecx?并且将前一个函数的返回值传入给ecx,有经验的同学应该知道这里就应该是构造函数了,再看上面class2的构造函数,发现是同一个,说明这正是新创建一个class2对象,但是和上面又有点不一样,按照理论上说ecx应该传入this指针,这里的ecx是前一个函数的返回值,这有什么联系呢?很显然这应该是在堆中分配的类,到这里前面的诸多疑惑都迎刃而解了
.text:0042D78A mov [ebp+var_204], eax
.text:0042D790 jmp short loc_42D79C
.text:0042D792 ; ---------------------------------------------------------------------------
.text:0042D792
.text:0042D792 loc_42D792: ; CODE XREF: sub_42D670+10Dj
.text:0042D792 mov [ebp+var_204], 0
.text:0042D79C
.text:0042D79C loc_42D79C: ; CODE XREF: sub_42D670+120j
.text:0042D79C mov eax, [ebp+var_204]
.text:0042D7A2 mov [ebp+var_1FC], eax
.text:0042D7A8 mov byte ptr [ebp+var_4], 3
.text:0042D7AC mov ecx, [ebp+var_1FC]
.text:0042D7B2 mov [ebp+pclass2], ecx
;最终将新建的对象地址赋值给栈中的一个值,这里将其命名为pclass2,下面是一样的过程,也是在堆中申请内存然后创建一个class3对象
.text:0042D7B8 push 1Ch
.text:0042D7BA call sub_42C086
.text:0042D7BF add esp, 4
.text:0042D7C2 mov [ebp+var_1D8], eax
.text:0042D7C8 mov byte ptr [ebp+var_4], 5
.text:0042D7CC cmp [ebp+var_1D8], 0
.text:0042D7D3 jz short loc_42D7E8
.text:0042D7D5 mov ecx, [ebp+var_1D8]
.text:0042D7DB call sub_42BB5E
.text:0042D7E0 mov [ebp+var_204], eax
.text:0042D7E6 jmp short loc_42D7F2
.text:0042D7E8 ; ---------------------------------------------------------------------------
.text:0042D7E8
.text:0042D7E8 loc_42D7E8: ; CODE XREF: sub_42D670+163j
.text:0042D7E8 mov [ebp+var_204], 0
.text:0042D7F2
.text:0042D7F2 loc_42D7F2: ; CODE XREF: sub_42D670+176j
.text:0042D7F2 mov eax, [ebp+var_204]
.text:0042D7F8 mov [ebp+var_1E4], eax
.text:0042D7FE mov byte ptr [ebp+var_4], 3
.text:0042D802 mov ecx, [ebp+var_1E4]
.text:0042D808 mov [ebp+pclass3], ecx
;将其命名为pclass3
.text:0042D80E mov eax, [ebp+pclass2]
.text:0042D814 mov edx, [eax]
.text:0042D816 mov esi, esp
.text:0042D818 mov ecx, [ebp+pclass2]
.text:0042D81E mov eax, [edx+4]
.text:0042D821 call eax
;上几条代码显而易见又是调用虚表中的函数,只不过多了一次取址,因为这是指针存储着堆中对象的地址,所以需要比平常多一次取址。同时可以看到这里调用的虚表中的函数和上面的不一样,说明class2至少有两个虚表函数
.text:0042D823 cmp esi, esp
.text:0042D825 call sub_42BD70
.text:0042D82A mov eax, [ebp+pclass3]
.text:0042D830 mov edx, [eax]
.text:0042D832 mov esi, esp
.text:0042D834 mov ecx, [ebp+pclass3]
.text:0042D83A mov eax, [edx+4]
.text:0042D83D call eax
;同理class3也至少有两个虚表函数
.text:0042D83F cmp esi, esp
.text:0042D841 call sub_42BD70
.text:0042D846 mov eax, [ebp+pclass2]
.text:0042D84C mov edx, [eax]
.text:0042D84E mov esi, esp
.text:0042D850 mov ecx, [ebp+pclass2]
.text:0042D856 mov eax, [edx+8]
.text:0042D859 call eax
;哈,发现这里还有一个class2虚表中的函数
.text:0042D85B cmp esi, esp
.text:0042D85D call sub_42BD70
.text:0042D862 mov eax, [ebp+pclass3]
.text:0042D868 mov edx, [eax]
.text:0042D86A mov esi, esp
.text:0042D86C mov ecx, [ebp+pclass3]
.text:0042D872 mov eax, [edx+8]
.text:0042D875 call eax
;不出意料,class3也有第三个虚表函数
.text:0042D877 cmp esi, esp
.text:0042D879 call sub_42BD70
.text:0042D87E lea ecx, [ebp+class4]
.text:0042D884 call sub_42C1E4
;调用class4中的一个函数
.text:0042D889 lea ecx, [ebp+class4]
.text:0042D88F call sub_42B82A
;调用class4中的另一个函数,至此发现class4至少有3个函数
.text:0042D894 mov eax, [ebp+pclass2]
.text:0042D89A mov [ebp+var_1C0], eax
.text:0042D8A0 mov ecx, [ebp+var_1C0]
.text:0042D8A6 mov [ebp+var_1CC], ecx
.text:0042D8AC cmp [ebp+var_1CC], 0
.text:0042D8B3 jz short loc_42D8CA
;以上的代码是判断pclass2中的地址,也就是堆中的class2是否为空,这一步到底是要干嘛呢?让我们接下去看
.text:0042D8B5 push 1
.text:0042D8B7 mov ecx, [ebp+var_1CC]
.text:0042D8BD call sub_42B122
;按照上面的格式,这个函数也应该是class2种的一个函数,但是调用这个函数之前却要判断this指针是否为空,这就让我们不禁联想到了这是一个析构函数
.text:0042D8C2 mov [ebp+var_204], eax
.text:0042D8C8 jmp short loc_42D8D4
.text:0042D8CA ; ---------------------------------------------------------------------------
.text:0042D8CA
.text:0042D8CA loc_42D8CA: ; CODE XREF: sub_42D670+243j
.text:0042D8CA mov [ebp+var_204], 0
.text:0042D8D4
.text:0042D8D4 loc_42D8D4: ; CODE XREF: sub_42D670+258j
.text:0042D8D4 mov eax, [ebp+pclass3]
.text:0042D8DA mov [ebp+var_1A8], eax
.text:0042D8E0 mov ecx, [ebp+var_1A8]
.text:0042D8E6 mov [ebp+var_1B4], ecx
.text:0042D8EC cmp [ebp+var_1B4], 0
.text:0042D8F3 jz short loc_42D90A
.text:0042D8F5 push 1
.text:0042D8F7 mov ecx, [ebp+var_1B4]
.text:0042D8FD call sub_42B122
;这里也是一样的,只不过对象换成了pclass3。之前堆中的对象需要new来申请,而这里则需要使用delete来释放,猜想这里就应该是类的析构函数
.text:0042D902 mov [ebp+var_204], eax
.text:0042D908 jmp short loc_42D914
.text:0042D90A ; ---------------------------------------------------------------------------
.text:0042D90A
.text:0042D90A loc_42D90A: ; CODE XREF: sub_42D670+283j
.text:0042D90A mov [ebp+var_204], 0
.text:0042D914
.text:0042D914 loc_42D914: ; CODE XREF: sub_42D670+298j
.text:0042D914 mov [ebp+var_19C], 0
.text:0042D91E mov byte ptr [ebp+var_4], 2
;接下来快到函数的末尾了,差不多应该是最开始栈中构造的函数析构的时候了,只不过析构函数的调用顺序和构造函数相反罢了
.text:0042D922 lea ecx, [ebp+class4]
.text:0042D928 call sub_42B546
.text:0042D92D mov byte ptr [ebp+var_4], 1
.text:0042D931 lea ecx, [ebp+class3]
.text:0042D934 call sub_42B875
.text:0042D939 mov byte ptr [ebp+var_4], 0
.text:0042D93D lea ecx, [ebp+class2]
.text:0042D940 call sub_42B92E
.text:0042D945 mov [ebp+var_4], 0FFFFFFFFh
.text:0042D94C lea ecx, [ebp+class1]
.text:0042D94F call sub_42B771
.text:0042D954 mov eax, [ebp+var_19C]
;下面便是恢复寄存器的值等一些还原现场的操作了
…………………….. …………………………………..
.text:0042D988 pop ebp
.text:0042D989 retn
.text:0042D989 sub_42D670 endp
从上面这样看下来,无非是多个类调用虚函数等的一些操作,没什么好说的,但是进入构造函数和析构函数之后谜底就会揭晓,下面来看比较典型的class2的构造函数和析构函数:
.text:0042DC00 sub_42DC00 proc near ; CODE XREF: sub_42B320j
.text:0042DC00
.text:0042DC00 var_CC = byte ptr -0CCh
.text:0042DC00 var_8 = dword ptr -8
;初始化的代码忽略,直接进入正题,这里ecx便是之前传进来的class2的this指针:
.text:0042DC20 mov [ebp+var_8], ecx
.text:0042DC23 mov ecx, [ebp+var_8]
.text:0042DC26 call sub_42B3E3
;这里有一个奇怪的地方,使用this指针调用另外一个函数?并且也很想一个构造函数,这让我们不禁联想起了,class2是继承于另外一个类的,并且通过比较函数地址,发现这个构造函数是另外一个类的构造函数,但是这个构造函数里面却又调用了class1的构造函数,说明class2是间接继承了class1的。这里就很明确的告诉我们class2是class1的子类。
;知道了这个后面的就很容易了,你可以跟进继续看class1的构造函数,也可以直接往下看
.text:0042DC2B mov eax, [ebp+var_8]
.text:0042DC2E mov dword ptr [eax], offset off_483CF8
;上面一条语句便是将class2的虚表地址放入class2对象中,而下面的便是初始化表达式以及用户自定义的初始化代码了
.text:0042DC34 mov eax, [ebp+var_8]
.text:0042DC37 mov dword ptr [eax+14h], 11111111h
.text:0042DC3E mov eax, 2222h
.text:0042DC43 mov ecx, [ebp+var_8]
.text:0042DC46 mov [ecx+18h], ax
.text:0042DC4A push offset aCboyConstructo ; "CBoy() Constructor...\r\n"
.text:0042DC4F call sub_42C144
;函数结尾代码省略
.text:0042DC6D retn
.text:0042DC6D sub_42DC00 endp
既然知道了class2的构造函数并且class2是继承于class1的,那class3也就很容易了,这里就不说了。再来看class2的析构函数:
.text:0042DD20 sub_42DD20 proc near ; CODE XREF: sub_42B92Ej
.text:0042DD20
.text:0042DD20 var_CC = byte ptr -0CCh
.text:0042DD20 var_8 = dword ptr -8
;函数开头代码省略
.text:0042DD40 mov [ebp+var_8], ecx
.text:0042DD43 mov eax, [ebp+var_8]
.text:0042DD46 mov dword ptr [eax], offset off_483CF8
;上面的代码又是将虚表指针复制到对象中,这步叫做虚表恢复,就是在调用虚构函数之前不管你虚表是对是错,先把你赋值为正确的值就可以了
.text:0042DD4C push offset aCboyDestructor ; "CBoy() Destructor...\r\n"
.text:0042DD51 call sub_42C144
;printf函数
.text:0042DD56 add esp, 4
.text:0042DD59 mov ecx, [ebp+var_8]
.text:0042DD5C call sub_42B54B
;前面的构造函数讲的很清楚了,这里应该是调用父类的析构函数了
;函数结尾代码省略
.text:0042DD74 sub_42DD20 endp
Class3的析构函数自然也是差不多的,说明class2和class3是在同一个类层次上的,按照上面的方法可以发现class4的是继承于class2和class3的,并且拥有两张虚表,如果用OD动态调试会更加清楚,这里就不详细说明了。
至此C++关于类、继承以及虚表机制的反汇编代码差不多都已经比较熟悉了,至于后面如何灵活运用,那又是另外一回事了。我自认为对于C++反汇编理解的还可以,但是遇上实践中的一些代码经常会无从下手,这应该和经验不误关联吧。
写完一看时间都过12点了,赶紧洗洗睡吧,但愿今晚做梦不要又梦到逆向!!!让我睡个好觉吧!
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)