[1-1-2] C++ Vtable을 구현해보고 디버깅 해보자 [1]

2018. 5. 17. 02:110x01 공부 스케줄/개인과제

728x90

가상함수 ... vptr, vtable 

이놈들 잘해야 분석 실력이 증가한다. 

 

Vtable 하기전에 가상함수부터 살펴보자.

가상함수?

클래스 타입의 포인터로 멤버 함수를 호출할 경우 동작하는 함수이며

키워드는 virtual이다.

 

삽질 1. A클래스와 A를 상속한 B클래스를 선언한 뒤 Message함수를 만들어 

A클래스 타입의 포인터 pa를 인수로 하여 

A객체와 B객체를 정적  / 동적 바인딩을 해보기.

 

 

Example SourceCode

#include <iostream>

#include <stdlib.h>

 

using namespace std;

 

class A

{

public:

virtual void OutMessage() // virtual 키워드가 붙게 되면 동적 바인딩이 된다 

{

cout << "Base Class" << endl;

}

};

 

class B : public A  // 부모클래스의 함수만 virtual로 선언해줘도 B클래스는 Second Class가 된다

{

public:

void OutMessage()

{

cout << "Second Class" << endl;

}

void Message(A *pa) // A 클래스의 포인터객체 때문 인 것 같다.  pa : 인수 

{

pa->OutMessage();

}

};

 

int main(int argc, char *argv[])

{

A a;

B b;

// a,b는 대상체가 된다

b.Message(&a);

b.Message(&b);

 

return 0;

}

 

 

상속 오버라이딩은 무엇일까?

 

상속 오버라이딩(Inheritance Overriding)

존재하고 있던 함수의 기능을 없애고, 새로운 기능을 덮는 행위를 말한다.

 

부모 클래스와 자식 클래스가 있을 때 자식클래스가 오버라이딩을 하게 되면, 

부모 함수의 기능은 사라지고 자식클래스에 정의된 기능이 부모의 기능을 덮어쓰면서 진행되게 된다.

 

핵심!

부모 함수와 자식 함수의 멤버함수와 원형이 완전히 같아야 한다. 

오버라이딩 시 부모 클래스 함수가 모두 자식 클래스 함수로 덮어 씌워진다.

 

Example SourceCode

#include <iostream>

 

using namespace std;

 

class A {

public:

void over() { cout << "A 클래스의 over 함수 호출!" << endl; }

};

 

class B : public A {

public:

void over() { cout << "B 클래스의 over 함수 호출!" << endl; }

};

 

int main()

{

// <이전 코드와의 차이점>

// 이전 코드는 A클래스의 대상체도 선언 되어 있었다 

// 이번 코드는 B클래스의 대상체만 선언 되어 있다

B b; 

b.over(); // 결과는 B 클래스의 over 함수 호출! 

return 0;

}

 

해당 코드를 디버깅 하면서 공부를 진행했다.

심볼 제거를 하고 보려 했는데, C++에 아직 미숙한 관계로 심볼 제거를 하지 않고 분석했다.

 

우선 메인 부분이다. 메인에서 b.over를 호출하는 것을 볼 수 있다.

 

 

Step In 한다.

 

_Val에 접근해보니 .rdata 섹션이었고, B클래스를 선언해둔 영역이었다.

 

char_trait에 접근해보았다. 

MSDN 참조. : https://msdn.microsoft.com/ko-kr/library/3dsft0c7.aspx

template <class CharType>   

struct char_traits; . 

 

변수가 선언이 되어 있지 않아서 그런가 분석이 영 찜찜하다고 생각이 들었다. 

 

변수를 선언하고 새로 디버깅을 진행해본다.

 

SourceCode

#include <iostream>

#include <string>

 

using namespace std;

 

typedef struct Security {

string name;

string team;

int age;

}security;

 

class A {

public:

typedef struct Security

{

string team;

string name;

int age;

}security;

void set(security s)

{

cout << "당신의 팀은? ";

cin >> s.team;

 

cout << "당신의 이름은? ";

cin >> s.name;

 

cout << "당신의 나이는? ";

cin >> s.age;

 

print(s);

}

 

void print(security s)

{

cout << "당신의 팀은 " << s.team << endl;

cout << "당신의 이름은 " << s.name << endl;

cout << "당신의 나이는 " << s.age << endl;

}

 

void over() { cout << "A 클래스의 over 함수 호출!" << endl; }

};

 

class B : public A {

public:

void over() { A::over();  cout << "B 클래스의 over 함수 호출!" << endl; }

};

 

int main()

{

// <이전 코드와의 차이점>

// 이전 코드는 A클래스의 대상체도 선언 되어 있었다 

// 이번 코드는 B클래스의 대상체만 선언 되어 있다

A a;

B b;

Security s = { "Demon","c0nstant",26 }; 

A::Security t; // A클래스 안에 Security 구조체가 있다 

 

cout << "당신의 팀은 " << s.team << endl;

cout << "당신의 이름은 " << s.name << endl;

cout << "당신의 나이는 " << s.age << endl;

 

a.set(t);

 

b.over(); // 결과는 B 클래스의 over 함수 호출! 

return 0;

}

 

클래스 내의 구조체 디버깅을 어떻게 하는지 알아보자.

 

메인함수이다. 상당히 복잡하다. 커컥...

__int64 __fastcall main()
{
  std::basic_ostream<char,std::char_traits<char> > *v0; // rax
  std::basic_ostream<char,std::char_traits<char> > *v1; // rax
  std::basic_ostream<char,std::char_traits<char> > *v2; // rax
  std::basic_ostream<char,std::char_traits<char> > *v3; // rax
  std::basic_ostream<char,std::char_traits<char> > *v4; // rax
  __int64 v5; // rax
  A::Security *v6; // rax
  char v8; // [rsp+20h] [rbp-128h]
  char v9; // [rsp+21h] [rbp-127h]
  unsigned int v10; // [rsp+24h] [rbp-124h]
  A::Security *v11; // [rsp+28h] [rbp-120h]
  A::Security *v12; // [rsp+30h] [rbp-118h]
  __int64 v13; // [rsp+38h] [rbp-110h]
  char v14; // [rsp+40h] [rbp-108h]
  std::basic_string<char,std::char_traits<char>,std::allocator<char> > v15; // [rsp+90h] [rbp-B8h]
  std::basic_string<char,std::char_traits<char>,std::allocator<char> > _Str; // [rsp+B0h] [rbp-98h]
  unsigned int v17; // [rsp+D0h] [rbp-78h]
  A::Security __that; // [rsp+E0h] [rbp-68h]


  v13 = -2i64;
  std::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string<char,std::char_traits<char>,std::allocator<char>>(
    &v15,
    "Demon");
  std::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string<char,std::char_traits<char>,std::allocator<char>>(
    &_Str,
    "c0nstant");
  v17 = 26;
  A::Security::Security(&__that);
  v0 = std::operator<<<std::char_traits<char>>(
         *(std::basic_ostream<char,std::char_traits<char> > **)std::cout.gap0,
         &byte_1400054F8);
  v1 = std::operator<<<char,std::char_traits<char>,std::allocator<char>>(v0, &_Str);
  std::basic_ostream<char,std::char_traits<char>>::operator<<(v1, std::endl<char,std::char_traits<char>>);
  v2 = std::operator<<<std::char_traits<char>>(
         *(std::basic_ostream<char,std::char_traits<char> > **)std::cout.gap0,
         &byte_140005508);
  v3 = std::operator<<<char,std::char_traits<char>,std::allocator<char>>(v2, &v15);
  std::basic_ostream<char,std::char_traits<char>>::operator<<(v3, std::endl<char,std::char_traits<char>>);
  v4 = std::operator<<<std::char_traits<char>>(
         *(std::basic_ostream<char,std::char_traits<char> > **)std::cout.gap0,
         &byte_140005518);
  v5 = std::basic_ostream<char,std::char_traits<char>>::operator<<(v4, v17);
  std::basic_ostream<char,std::char_traits<char>>::operator<<(v5, std::endl<char,std::char_traits<char>>);
  v11 = (A::Security *)&v14;
  A::Security::Security((A::Security *)&v14, &__that);
  v12 = v6;
  A::set((A *)&v8, v6);
  B::over((B *)&v9);
  v10 = 0;
  Security::~Security(&__that);
  Security::~Security((A::Security *)&v15);
  return v10;
} 

 

 

소스코드를 모르는 상태에서 분석가가 본다고 가정을 하면, A::Security는 A클래스의 Security구조체라는 것을 알 수 있다. 

 

나는 :: 기호에 대해 잘 몰라서 공부를 하였다.

C++에서 :: 란?

Scope Operator (범위 지정 연산자)

 

메인함수와 Class는 Scope가 다르다.

메인함수는 A클래스의 Security 구조체 존재를 현재는 모르는 상태다.

이때, :: 연산자를 통해 A::Security 하게 되면, 메인함수는 A클래스에 접근을 할 수 있고, 

A클래스 안에 Security 구조체가 있다는 것을 알 수 있다. 

위의 예제에도 적어보았지만, 클래스와 global역시 Scope가 다르기 때문에 똑같은 구조체 명을 선언할 수가 있게 된다. 

하지만, 구조체 멤버변수는 동일하게 하면 오류가 발생하게 되었다.

 

메인함수이다.

 

A클래스 내부 구조체 말고, global에 선언되어 있는 구조체는 아래와 같이 메모리에 할당되게 된다.

 

 

 

   A:::Security에 접근해보았다.

 

 

아직 값을 입력 받지 않았기 때문에 단순히 할당만 하는 것을 볼 수 있다.

 

출력문 (std::cout) 부터 메모리에 넣게 된다. 현재 global에 선언 되어 있는 구조체를 나타내므로 cin은 없다.

착각을 하면 안된다.

 

 

 

이제 A::Set에 접근하게 된다.

매개변수로는 A클래스의 구조체를 담아두었다.

 

여기에서 cout -> cin 순서대로 보여진다.

하지만 정확한 input 행위는 밑의 함수 char_trait에서 진행되게 되는 것을 기억하자.

cout와 cin이 노출되어 있는 곳은 mov instruction으로 되어 있기 때문이다. 

 

만일, 32비트 기반의 바이너리 였다면, 인자를 push하여 스택에 넣으므로써 ESP가 변화하지만,

64비트 기반의 바이너리는 스택에 push하지 않고 인자를 레지스터로 처리하기 때문에 RSP가 변화 하지 않는다.

 

 

 

해당 메모리 덤프에 값들이 들어감을 확인하였다.

 

 

void __fastcall A::Security::Security(A::Security *this, A::Security *__that) 여기에서 한번 더 할당을 한다.

 

 

 

이번 디버깅의 목적은 구조체 내부 변수들이 어떻게 할당 되는가였다. 

string의 경우는 int와 다르게 할당하는 크기가 큰 것 같다. 

왜냐하면 각 string의 포인터끼리의 오프셋 거리는 32이고, 

string 하나와 int의 오프셋 거리는 16이었기 때문이다.

 

오늘은 여기까지 !