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 알 수 없는 사용자
,

Hello_8Ug.exe 디버깅하기!

일단

이렇게 프로그램이 실행되는지 확인해요.

그리고

우리의 친구... 올리디버거 (햄버거 먹곺,,,,ㅜ,ㅜ )를 실행해서

Hello_8UG.exe 를 실행시켜 줍니다. 

그러면 이렇게 거뭇거뭇..... 검은건 바탕이요 흰건 글씨요 하는게 뚜든 하고 등장해요.

 

그럼 이제 본격적으로 놀아봅시다 +_+

요로쿠룽 재생버튼을 눌러주면.

실행될때 나타났던 검은창이 (정확히는 두번 누르면)

잘 돌아가는지 확인했고, 이제 막 찾아봅시당.

 

F7을 천천히 누르면서 봤는데

 

저 네모안에서 몇번이 돌아간다. 왜일까용????

 

.....저기서 계속 돌고 안빠져 나옴...(심각)

그래서 앞서올린 DHUJ님의 포스팅을 살짝쿵 참고했습니당...ㅠㅠㅠ

 

저도 하긴했는데....ㅎㅎㅎㅎ HU 가 너무...까리하게 정리 잘해놔서 ...☞☜;;;

 

다음에는 더 열정적으로 디버깅을 해보리라+_+

 

Posted by sher1ock.
,

 Helloworld.exe  올리디버그로 어셈코드를 분석!

HUJ

응응귀엽당..ㅎ



Helloworld.c를 컴파일해서 올리디버그로 각각 어셈코드를 분석해보겠습니다~


이쁜 Visual Studio 열어서 Hello world! 가 출력되게 다음과 같이 작성해줍니다.

#include <stdio.h>

int main()

{

printf("Hello world!\n");

return 0;

}


F5를 눌려서 디버깅을 해줍니다.

만약, Ctrl+F5를 눌려서 디버깅않고 시작해주면 다음 그림처럼 뜨게 되겠쥬


그럼 빠져나와서 Helloworld 프로젝트 저장한곳을 찾아줍니다. 그다음 Debug파일에 들어가셔서 다음그림처럼 Helloworld.exe를 찾으면 됩니다. 

찾기어려우시면 

[시작 - 검색창에 Helloworld.exe (자신이 만든 프로젝트이름)] 치고 오른쪽 클릭하셔서 파일위치열기 누르시면 나옵니다.~


다음 올리디버그를 열어주시고, 저 파일을 찾아 open 해줍니다.

그럼 다음과 같이 뜨게될껍니다.

(이제 그림밑에 설명써야징)


64bit컴퓨터에 버전 110을 사용하시면 저 하얀창 뒤엔 요렇게..ㅜ

그래서 전 다시 220을 깔고 다시 환경설정했습니다.ㅜㅜ


이제 안나타나고 잘실행되네요>< 본격적으로 분석해보겠습니다.


일단 처음해보는 분석이니, F7을 누르면서 하나하나 살펴보고, int main에 해당하는 부분을 찾아봅니다.


이것저것 보는중..ㅋㅋㅋ


일단 윗 그림처럼 찾기 쉬운, ASCII 코드로 되어있을 "Hello world!" 라고 출력될 코드를 찾아봅시다. 컨트롤 에프 눌려서 찾고싶다만.. 안됨...ㅠㅠ


제가 찾아본 방법은 이렇습니다.

CPU 창에서 오른쪽 클릭! 문자열string 일테니, [Search for - All referenced Strings] 으로 들어가 봅시당


창이 뜸과 동시에, ASCII 코드로 표시된 "Hello world!" 발견!!! 저 빨간상자를 눌려주시면 해당 주소로 알아서 찾아가줍니다ㅎㅎㅎ


ㅎㅎㅎㅎ발견ㅎㅎㅎ

거기서 F2로 BreakPoint설정하고 F7을 누르니, 스택 창보시면 comment에도 가리키는걸 보실수있습니다.


이제 return 0;의 어셈코드를 보고싶었는데,, 스택창 comment에서 return 같은게 있네요

이건 더 배우고 와야될듯ㅜ


엉엉ㅃㅃ

Posted by 알 수 없는 사용자
,


 올리디버거  (Ollydbg) 설치 및 사용법 생각중 HUJ


<사용 환경버전>

64bit 컴퓨터


<올리디버거 란?>

디버깅 프로그램중 하나이며, 디스어셈블리와 디버그가 모두 가능한 툴이라 리버싱에 기본적으로 사용됩니다.

지금 사용할 버전 110은 32bit 컴퓨터에 안정되어있습니다~


<다운로드 & 설치>

http://www.ollydbg.de/ 에서 64bit 컴퓨터는 201버전을 다운받아도 되지만, 

플러그인을 따로 설치 해야되므로, 첨부된 파일을 저장하여 풀어 사용하는것이 좋습니다.

(기본설정되어있음.)

odbg110.zip

각종 플러그인 다운로드 사이트 : TUTS4YOU (http://www.tuts4you.com)


<환경설정, 기본설정>

-->저 파일을 다운받았을시, 이 부분은 무시하셔도 좋지만, 설정안된 부분이 있을 수 있으니 확인바랍니다.


-UDD

-Plugin 파일이 생성되어있습니다. (없다면 파일을 생성해주세요)


UDD 파일에는 브레이크 포인트 지점, 백업파일 등 디버깅 설정 저장 공간

Plugin 파일에는 각종 플러그인 파일들 저장

쉽게말해, 정리해서 보기좋고 경로를 쉽게 설정하기 위한 기본설정인 것입니다.


혹시 win32.hlp 파일이 없다면, http://www.tuts4you.com/download.php?view.258

에서 다운로드 하시면 됩니다.

(없어도 무방하지만, 나중에 API 찾을때 유용합니다.)


Plugin 파일에 각종 .dll 파일을 옮겨줍니다.


이제 OLLYDBG 을 실행해봅시다.

64bit 컴퓨터로 실행할경우, 노란주의 표시가 뜨는데, 그냥 확인을 누르셔도 되고,

실행할때 권리자 권한으로 실행해주셔도 됩니다.


아까, UDD와 Plugin 파일을 생성했으니 프로그램에 설정을 해줘야됩니다.

[Options - Appearance - Directories] 으로 들어가서 UDD와 Plugin path 경로를 아까 UDD와 Plugin 파일로 경로를 바꿔주시면 됩니다.


다음, 디버거로 열었을 때 시작할 위치를 설정해줍니다.

 [Alt+O] or [Options - Debugging options - Events] 에서 [Entry point of main module] 을 체크해주시면 됩니다.


그다음, jump 하는부분에 화살표를 만들어주는 설정을 해줍니다. 저기 ollydbg 구성설정 파일을 열어줍니다.


밑 사진 처럼, 저곳에 다음을 입력해줍니다. 복붙 복붙ㅋㅋ

Show jump direction=1

Show jump path=1

Show jumpfrom path=1

Show path if jump is not taken=1


다음과 같이 jump부분에서 화살표가 생긴것을 보실 수 있습니다.


여기까지 환경설정은 끝났고,,


<사용방법>

[File - Open] 이나 F3, 아니면 open할 파일을 드래그해서 열어봅니다.

1. 주소 창 : 명령어가 실행될 주소를 가르키는 창

2. OP코드 창 : 기계어를 보여주는 창

3. 디스어셈블리 창 : 기계어를 어셈블리로 바꿔놓은 창

4. 레지스터 창 : 레지스터의 값을 표시해주는 창 

(각 주소의 기계어나 어셈블리에 따라 값이 바뀐다.)

5. 메모리 덤프 창 : Address 주소, Hex dump, 각 Hex에 따른 ASCII코드로 해석된 창을 보여줌

6. 스택 창 : 스택 주소, 스택 값, comment 순으로 표시된 창


<기본 어셈블리어 간단한 설명>

PUSH : 스택에 값을 넣는 명령어.

POP : 스택에 값을 빼는 명령어.

INC (increase), DEC(decrease) : 증가, 감소 명령어.

ADD, SUB, MUL, DIY : 사칙연산 명령어.

OR, AND, XOR : 논리연산 명령어.    

예) XOR EAX : 값을 0으로 초기화 시키는 명령어.

CMP, TEST : CMP는 빼서 비교한다 (값이 변경됨) TEST는 값을 변경하지 않고 비교함.  (둘다 값이 같을때 ZF는 1, 다를경우 0)

CALL : 함수 호출 명령어.

RETN : 함수 리턴 명령어. 모두 반환되는 것이 아니라, LEAYE 명령어로 함수에 사용된 스택 값을 보내준다.

JMP 는  http://www.unixwiz.net/techtips/x86-jumps.html/ 참조.


<단축키>

Run (F9) : 프로그램을 실행.

Break Pint (F2) : Break Point 설정/해제  (Run (F9)으로 프로그램을 실행할때 이 break point까지 실행된다.)

Step into (F7) : 한줄씩 실행되면서, CALL에서 프로그램 내부 함수 로 들어가게 됨.

Step over (F8) : 한줄씩 실행되면서, CALL에서 코드 함수로 들어가지않고 진행됨.

Execute till return (Ctrl+F9) : RETN 명령어가 나올때까지 실행함.

그 외의 단축키 : http://www.ollydbg.de/quickst.htm 참조


바이포스팅 끝><

Posted by 알 수 없는 사용자
,