간단하게 아래와 같이 c++을 작성하고 분석해보았다.
#include "iostream" using namespace std; class A{ public: int myval; void set(int val) { myval = val; } virtual void print() { cout << "[A] myval : " << myval << endl; } }; class B : public A { public: void print() { cout << "[B] myval : " << myval << endl; } }; int main() { //A *ptr1 = new A(); //A *ptr1 = new B(); //B *ptr1 = new B(); ptr1->set(123); ptr1->print(); return 0; }
C++에서 가상함수는 다형성을 위해서 존재한다. 아래와 같이 A클래스의 포인터에 A객체가 들어 갈수도 있고 A객체를 상속하는 B객체가 들어갈 수 도 있다. 일반적으로 A객체 포인터가 print메소드를 호출한다면 A의 print메소드가 호출된다.
이 때 오버라이딩된 B객체에 대한 메소드가 호출될 수 있도록 A의 print 메소드에 virtual 선언을 해주면 된다.
A *ptr1 = new A(); ---> A::print()
A *ptr1 = new B(); ---> B::print() //virtual이 선언이 없다면 A::print() 었을것이다.
B *ptr1 = new B(); ---> B::print()
소스에서 주석처리된 A *ptr1 = new A(), A *ptr1 = new B(), B *ptr1 = new B() 의 3가지 경우에 대해 IDA로 볼 때 어떻게 다른지 살펴보자.
1. 먼저 A *ptr1 = new A();인 경우를 IDA로 열어 보면 Main부분은 아래와 같다.
int __cdecl sub_411500() { int v1; // [sp+Ch] [bp-DCh]@1 void *v2; // [sp+14h] [bp-D4h]@1 int v3; // [sp+E0h] [bp-8h]@4 memset(&v1, -858993460, 0xDCu); v2 = operator new(8u); if ( v2 ) v1 = sub_41113B(v2); else v1 = 0; v3 = v1; sub_41100F(123); (**(void (__thiscall ***)(_DWORD))v3)(v3); sub_4111B8(); return sub_4111B8(); }
[*] operator new에서 8byte만큼 할당하는데 myval의 4byte와 virtual선언된 함수가 있기 때문에 만들어진 vtable 4byte가 할당 된 것이다.
ex) vtable이 생성된다면 하상 아래와 같이 this포인터의 첫 4byte에 생성된다.
-----------------
| int myval |
----------------- <-- this
-----------------
| int myval |
-----------------
| &vtable |
----------------- <-- this
sub_41113B <-- A클래스의 생성자
sub_41100F <-- A클래스의 set함수
(**(void (__thiscall ***)(_DWORD))v3)(v3); <-- A클래스가 vtable을 참조하여 호출하는 print() 함수
- 객체의 data는 고유하지만 멤버 함수는 별도로 위치하여 객체들간에 서로 공유한다.
- virtual선언이 아닌 set함수는 바로 호출되고, virtual로 선언된 printf 메소드는 vtable 참조로 호출됨.
이제 생성자 부분을 보자
int __thiscall sub_411610(void *this) { void *v2; // [sp+D0h] [bp-8h]@1 v2 = this; *(_DWORD *)this = &off_417834; return (int)v2; }
this포인터의 첫 DWORD에 vtable의 주소 &off_417834를 넣어서 구성하는 것을 확인할 수 있다.
[ 여기서 &부분을 잠시 설명하면]
해당 클래스에 대한 구조체는 아래와 같이 만들어주면 분석할 준비가 완성된다.
00000000 A struc ; (sizeof=0x8)
00000000 vtable dd ? ; offset // 'Y' 커맨드로 vtable*형으로 만들어줌
00000004 myval dd ?
00000008 A ends
00000008
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 vtable struc ; (sizeof=0x4)
00000000 print dd ?
00000004 vtable ends
2. 그럼 이제 나머지 두 경우에 대해서 알아보자.
- A *ptr1 = new B();
- B *ptr1 = new B();
위의 경우에는 A클래스 객체가 아니라 A클래스를 상속하는 B클래스 객체를 할당한 상황이다. 이 때는 B클래스의 생성자에서A클래스의 생성자도 호출한다는 것만 달라진다.
IDA로 생성자 부분만 살펴 보겠다.
[ B 생성자 ]
int __thiscall sub_411620(void *this) { char v2; // [sp+Ch] [bp-CCh]@1 void *v3; // [sp+D0h] [bp-8h]@1 memset(&v2, -858993460, 0xCCu); v3 = this; sub_411145(this); //A의 생성자를 호출함 *(_DWORD *)v3 = &off_417834; return sub_4111C2(); }
[ A 생성자 ]
int __thiscall sub_411720(void *this) { void *v2; // [sp+D0h] [bp-8h]@1 v2 = this; *(_DWORD *)this = &off_417850; return (int)v2; }
A 생성자에서 먼저 vtable에 off_417850을 넣어준다음 다시 B생성자에서 off_417834를 넣어서 덮어버린다. 따라서 여기서는 B의 vtable이 들어가며 A의 vtable은 무시해도 된다.
A의 vtable과 B의 vtable을 보면 아래와 같다.
-----------------
| A::print() |
----------------- <--- A의 vtable - off_417850 (new A();한 경우 사용됨)
-----------------
| B::print() |
----------------- <--- B의 vtable - off_417834 (new B();한 경우 사용됨)
[*] vtable에는 해당 객체의 가장 마지막에 오버라이딩된 가상 함수들이 들어있다.
위와 같이 C++ 가상함수가 있을 때 vtable이 구성되는 것을 확인하였다. 구성 방식이 위와 같으므로 IDA로 볼 때 struct로 잘 naming하여 좀 더 편하게 분석할 수 있도록 해야겠다.
'Reversing > Reversing' 카테고리의 다른 글
code injection에서 shellcode to exe (0) | 2014.01.15 |
---|---|
64bit환경의 인자값 전달 방식 (0) | 2013.12.15 |
IDA - 변수에 따른 데이터 영역 정리 (0) | 2013.12.14 |
무료 .NET Decompiler (1) | 2013.11.04 |
ollydbg로 dll파일 디버깅 (0) | 2013.02.28 |