Reversing Microsoft Visual C++

<Classes, Methods and RTTI> 정리


HUJ



여기저기서 과제가 넘치네여 ㅎㅎㅎ....


오늘은 리버싱 c++의 클래스, 메소드 및 RTTI에 대해 정리 해보겠습니다.

이 설명들은 기본적인 c++, 어셈블리 언어에 익숙하다고 가정할때의 설명이다.



기본 클래스 레이아웃


대부분 MSVC 는, 

1. 기본 클래스가 재사용 될 수 있음으로부터, 가상 메소드과 클래스에서 더 적합한

    테이블이 있는 경우에만 가상함수 테이블(_vtable_ 또는 _ vftable_)에 추가한다.

2. 기본 클래스들

3. 클래스 멤버들

순서로 놓는다.



자세한 설명에 앞서 예시가 있다.


//첫번째로 등장하는 가상 함수 테이블은 가상 메소드의 주소로 구성되어 있다.

(오버로드 된 함수의 주소는 기본클래스의 함수의 주소로 교체한다.)

따라서, 예시밑에 예시의 레이아웃을 표시해놨다.//

class A

    {
      int a1;
    public:
      virtual int A_virt1();
      virtual int A_virt2();
      static void A_static1();
      void A_simple1();
    };

    class B
    {
      int b1;
      int b2;
    public:
      virtual int B_virt1();
      virtual int B_virt2();
    };

    class C: public A, public B
    {
      int c1;
    public:
      virtual int A_virt2();
      virtual int B_virt2();
    };

class A    

{                                //{vfptr}: 0

int a1;                        //4 public:                          //A의 vftable virtual int A_virt1();         //0 virtual int A_virt2();         //4 static void A_static1(); void A_simple1(); };                               //class A의 size : 8

class B {                             //{vfptr}: 0 int b1;                        //4 int b2;                        //8 public:                         //B의 vftable virtual int B_virt1();         //0 virtual int B_virt2();         //4 };                               //class B의 size : 12 class C: public A, public B     //기본 클래스 A: {vfptr}: 0, A1: 4, {                               // B: {vfptr}: 8, B1: 12, B2: 16, int c1;                       // c1: 20 ---> class C의 size : 24 public: virtual int A_virt2();        //A에 대한 C의 vftable: 4 virtual int B_virt2();        //B에 대한 C의 vftable: 4 };


여기서, C는 두개의 vftable를 가지고 있다. 

가상 함수들이 이미 두개의 클래스에 상속되어있기 때문에, 

C의 주소 ::A_virt2는, A에 대한 C의 vftable안에, A의 주소::A_virt2로 대체한다. 그리고,

C::B_virt2는, 다른 테이들안의 B::B_virt2로 대체한다.



규칙과 클래스 메소드 호출


기본적으로 MSVC의 모든 클래스 메소드는 _thiscall_ 규칙을 사용한다.

COM클래스를 구현하는 경우에는, _stdcall_ 규칙이 사용된다.


다얀한 클래스 메소드의 유형을 설명해보겠습니다.


1) Static 메소드

이 메소드는 class instance를 필요로하지 않기 때문에 일반적인 함수와 같은 방식으로 작동한다. _this_ 포인터는 전달되지 않는다. 때문에 안정적으로 단순 함수에서 static 메서드를 구별 할 수 없다.

ex

 

A::A_static1();

call     A::A_static1

 


2) Simple 메소드

이 메소드는 class instance를 필요로 하기 때문에 _this_포인터는 일반적으로 _thiscall_ 규칙을 사용하여 숨겨진 첫 번째 매개 변수로 전달된다. 기본 객체가 처음에 위치하지 않을땐, _this_ 포인터 함수를 호출하기 전에 기본 하위 객체의 실제 시작 부분을 가리키도록 작성해야 된다.

ex

 

    ;pC->A_simple1(1);

    ;esi = pC

    push    1

    mov ecx, esi

    call    A::A_simple1


    ;pC->B_simple1(2,3);

    ;esi = pC

    lea edi, [esi+8] ;adjust this

    push    3

    push    2

    mov ecx, edi

   call    B::B_simple1

 

--> 여기서 _this_ 포인터는 B의 메서드를 호출하기 전에 B의 하위 객체를 가리키도록 조정되어있다.


3) 가상 메소드

먼저 컴파일러가 가상메소드를 부르는 것은, _vftable_에서 함수 주소를 가져오는것을 필요로 한다. 그 다음, simple 메소드와 같은 주소를 부른다.

ex

    ;pC->A_virt2()

    ;esi = pC

    mov eax, [esi]  ;가상 테이블 포인터를 가져옴

    mov ecx, esi

    call [eax+4]  ;두번째 가상 메소드를 호출

    

    ;pC->B_virt1()

    ;edi = pC

    lea edi, [esi+8] ;이 포인터를 조절

    mov eax, [edi]   ;가상 테이블 포인터를 가져옴

    mov ecx, edi

    call [eax]       ;첫번째 가상 메소드를 호출

 

4) 생성자와 소멸자

생성자와 파괴자는 simple 메소드와 유사한 일을 하고, 첫번재 매개 변수로 암시적인 _this_포인터를 갖는다. 생성자는 아무런 반환 값이 없는 경우에도, EAX에 _this_ 포인터를 반환한다.


RTTI 구현


RTTI는 특별한 컴파일러의 생성 정보이다. 

dynamic_cast<> 와 typeid(), 그리고 c++예외와 같은 c++ 연산자를 지원하는데 사용된다. 그 특성때문에 RTTI는 오직, 다향성 클래스를 생성한다. 

MSVC 컴파일러는 "Complete Object Locator" 를 부른 구조체에 포인터를 넣는다.

구조는 클래스들을 여러개 가질 수 있어서, 컴파일러는 특정 vftable 포인터로부터 완전한 객체의 위치를 찾을 수 있다.


COL은 다음과 같다.


struct RTTICompleteObjectLocator
{
    DWORD signature; //항상 0
    DWORD offset;    //완전한 클래스에서 vtable의 오프셋
    DWORD cdOffset;  //생성자 변위 오프셋
    struct TypeDescriptor* pTypeDescriptor; //전체 클래스의 TypeDescriptor
    struct RTTIClassHierarchyDescriptor* pClassDescriptor; //상속 계층 구조를 설명
};

클래스의 상속 계층 구조에 대한 설명되어있다.

(이 클래스의 모든 COLS를 공유함)


 

struct RTTIClassHierarchyDescriptor { DWORD signature; //항상 0 DWORD attributes; //bit 0 set = 다중 상속, bit 1 set = 가상 상속 DWORD numBaseClasses; //pBaseClassArray 클래스의 수 struct RTTIBaseClassArray* pBaseClassArray; };

기본 클래스 배열은 _dynamic_cast_연산자가 실행하는 동안, 그 중 하나에 파생 클래스를걸 수 있다는 정보와 함께, 모든 base 클래스들로 표현된다.


각 항목은 다음과 같은 구조를 가지고 있다.


struct RTTIBaseClassDescriptor

{ struct TypeDescriptor* pTypeDescriptor; //클래스 타입 설명 DWORD numContainedBases; //기본 클래스 배열에 따르는 중첩 클래스의 수

struct PMD where; //pointer-to-member 변위 정보 DWORD attributes; //flags, 보통 0 };

struct PMD

{ int mdisp; //member 변위 int pdisp; //vbtable 변위 int vdisp; //vbtable 내부의 변위 };

위의 PMD구조는 기본 클래스가, 전체 클래스 내부에 배치되는 방법을 나타낸다. 

간단한 상속일 땐, 오브젝트의 선두의 오프셋 위치에 고정하고(그 값은 _mdisp_필드.),

virtual base 라면, 추가적인 오프셋은 vbtable에서 가져와야 한다.


기본 클래스에서 파생된 클래스로 부터, _this_포인터를 조정하기 위한 유사 코드는 다음과 같다.


//char* pThis; struct PMD pmd;
    pThis+=pmd.mdisp;
    if (pmd.pdisp!=-1)
    {
      char *vbtable = pThis+pmd.pdisp;
      pThis += *(int*)(vbtable+pmd.vdisp);
    }



정보 추출


1) RTTI

RTTI는, 리버싱에서 중요한 정보를 제공해주는 소스이다. 이는, 클래스 이름과 상속 계층 구조를 복구할 수 있고, 클래스 레이아웃의 경우에는 부품역활도 한다.

(RTTI scanner 스크립트는 대부분의 정보를 보여준다.)


2) Static과 Global Initializers

글로벌 및 Static객체는 메인이 시작되기 전에 초기화가 필요하다.

방법은, MSVC는 구현 초기화(funclets)를 생성하고 _cinit function에 의한 CRT가 시작되는 동안 처리된 테이블에 자신의 주소를 넣는다.(테이블은 일반적으로 데이터 섹션의 처음에 있다.)


일반적인 초기화는 다음과 같다.


    _init_gA1:
        mov     ecx, offset _gA1
        call    A::A()
        push    offset _term_gA1
        call    _atexit
        pop     ecx
        retn
    _term_gA1:
        mov     ecx, offset _gA1
        call    A::~A()
        retn

(글로벌/Static객체 주소, 생성자, 소멸자 확인가능)


3) Unwind Funclets

모든 자동 객체가 함수에다 작성할 때, VC++컴파일러는 자동으로 예외가 발생하는 경우에 해당 개체의 삭제를 보장하고, 예외 처리 구조를 생성한다.


전형적인 Unwind funclet는 스택에 객체가 삭제됨을 볼 수 있다.


    unwind_1tobase:  ; state 1 -> -1
        lea     ecx, [ebp+a1]
        jmp     A::~A()


함수나 동일한 스택 변수에, 바로 처음 액세스 내부의 반대상태를 발견함으로써, 생성자를 찾을 수 있다.


    lea     ecx, [ebp+a1]
    call    A::A()
    mov     [ebp+__$EHRec$.state], 1


새로운 () 연산자를 사용하여 구성한 오브젝트의 경우엔, Unwind funclet 는 생성가 실패한 경우, 할당된 메모리의 삭제해준다.


    unwind_0tobase: ; state 0 -> -1
        mov     eax, [ebp+pA1]
        push    eax
        call    operator delete(void *)
        pop     ecx
        retn


;A* pA1 = new A(); push

call operator new(uint) add esp, 4 mov [ebp+pA1], eax test eax, eax mov [ebp+__$EHRec$.state], 0; state 0: 메모리는 할당됬지만 개체가
                                                   아직 구축되지 않음
        jz      short @@new_failed
        mov     ecx, eax
        call    A::A()
        mov     esi, eax
        jmp     short @@constructed_ok
    @@new_failed:
        xor     esi, esi
    @@constructed_ok:
        mov     [esp+14h+__$EHRec$.state], -1
     ;state -1: 객체가 메모리 할당 성공or 실패
     ;두 경우모두 추가 메모리관리는 사용자에 의해 이루어짐.


또다른 유형의 unwind funclets는 생성자와 소멸자에 사용된다. 예외의 경우에는 클래스멤자가 삭제된다. 이 경우의 funclets는 _this_ 포인터(스택 변수에 보관된) 를 사용한다.


    unwind_2to1:
        mov     ecx, [ebp+_this] ; state 2 -> 1
        add     ecx, 4Ch
        jmp     B1::~B1


다음 funclet는 오프셋 4Ch에서, 타입B1의 클래스 멤버를 삭제한다.


one. C++ 개체나 _operator new_로 할당된 오브젝트의 포인터를 나타내는 Stack 변수

two. 소멸자

three. 생성자

four. new'ed 개체의 크기


4) 생성자/소멸자 Recursion(재귀)

생성자는, 기본 클래스의 생성자 호출, 복잡한 클래스 멤버의 생성자 호출, 클래스가 가상 함수를 가졌을 경우 vfptr를 초기화, 작성된 생성자 본문 실행 순으로 작업을 수행한다.

소멸자는 생성자의 역순으로 수행한다.


5) 배열 구축 파괴

MSVC 컴파일러는 객체의 배열을 삭제하고 구성하는 helper함수를 사용한다.

다음 코를 보자.


    A* pA = new A[n];
    
    delete [] pA;

이 코드는 다음의 의사 코드로 변역된다.


    array = new char(sizeof(A)*n+sizeof(int))
    if (array)
    {
      *(int*)array=n; //저장소 배열 크기
      'eh vector constructor iterator'(array+sizeof(int),sizeof(A),count,&A::A,&A::~A);
    }
    pA = array;
    
    'eh vector destructor iterator'(pA,sizeof(A),count,&A::~A);


vftable가 있는 경우, 배열을 삭제할때 'vector deleting destructor(소멸자)'가 대신 출력된다.


    ;pA->'vector deleting destructor'(3);
    mov ecx, pA
    push 3 ; flags: 0x2=deleting an array, 0x1=free the memory
    call A::'vector deleting destructor'


소멸자가 가상일땐, 사실상 호출된다.


mov ecx, pA push 3 mov eax, [ecx] ;vtable 포인터 가져옴 call [eax] ;deleting destructor 호출


따라서, 벡터 생성자/소멸자 로 부터, 객체의 배열주소, 생성자, 소멸자, 클래스 사이즈를 알 수 있다.


6) 소멸자 삭제

클래스가 가상 소멸자를 가졌을 때, 컴파일러는 helper 함수를 생성한다.


    virtual void * A::'scalar deleting destructor'(uint flags)
    {
      this->~A();
      if (flags&1) A::operator delete(this);
    };

이 함수의 주소는 소멸자의 주소대신에 vftable가 배치된다. 이 경우에, 클래스의 _operator delete_는, 다른 클래스가 가상 소멸자를 무시할때 호출된다.


가끔은 다음 코드와 같이, 컴파일러 또한, vector deleting 소멸자를 생성 할 수 있다.


virtual void * A::'vector deleting destructor'(uint flags) { if (flags&2) //destructing a vector { array = ((int*)this)-1; //배열의 크기는 이 포인터 전에, 저장된다. count = array[0]; 'eh vector destructor iterator'(this,sizeof(A),count,A::~A); if (flags&1) A::operator delete(array); } else { this->~A(); if (flags&1) A::operator delete(this); } };

이건 현실에서 오히려 드물고 매우 복잡하기에, 가상 bases와 클래스의 구현 세부사항를 생략되었다.





끝! 죽을꺼같음ㅎ





링크에서 알게 된걸 포스팅하려했는데, 첨부터 끝까지 다 새로운거라서 ㅋㅋㅋㅋ 읽으면서 알게 된거 쓰다보니 해석판이 되어버렸네요.. 포스팅하고 다시 읽어야겠음.. 머리에 하나도 안들어왕ㄻㅣㄹㄴㅇ


다음 링크는 이 포스팅을 쓸때 해석하고, 참조한 출처입니다. 더욱 자세한 설명으로 되어있습니다. 물론 영어로 ㅋㅋㅋ...ㅜ_ㅜ

http://www.openrce.org/articles/full_view/23






Posted by 알 수 없는 사용자
,