Reversing/Hooking

[Hooking] 2. 기초지식 - 가상함수와 Vtable

가상함수에 대해 말하기 전 - Overriding

클래스에서 선언된 함수에 대해, 해당 클래스의 자식 클래스가 정의되었을 때 자식 클래스에서 상속받은 함수를 새롭게 정의하여 사용하는것을 오버라이딩이라 한다. 부모 클래스의 함수에서 선언된 리턴타입과 함수 인자 구성이 똑같아야 한다.

이렇게 정의된 클래스 타입으로 변수를 선언하여 사용하는 경우, 함수를 새로 정의한 자식 클래스를 타입으로 지정하여 함수를 호출했을 때는 새롭게 정의된 함수가 호출된다.

#include <stdio.h>

class Parent {
public :
    void show() {
        printf("this is parent\n");
    }
};

class Child : public Parent {
public:
    void show() {
        printf("this is child\n");
    }
};

class Child2 : public Child{
public :
    void show() {
        printf("this is child2 \n");
    }
};

int main() {
    Parent* p = new Parent;
    Child* c = new Child;
    Child2 * c2 = new Child2;

    p->show();
    c->show();
    c2->show();
}
this is parent
this is child
this is child2

선언한 Parent클래스를 상속하는 Child, Child를 상속하는 Child2가 선언되었고, 각각 show() 함수를 새롭게 정의하였으므로 오버라이딩이 이뤄져 실행 결과가 다르게 나타난다.

p = c;
p->show();    
p = c2;
p->show();
this is parent
this is parent

이후에 Parent 클래스 형식으로 선언된 포인터 변수에 Child 객체를 복사하더라도 오버라이딩된 함수가 실행되지는 않는다. 이미 p 변수에 Parent의 show()함수가 바인딩 되어있는 상태로 빌드가 끝나버렸기 때문에 바이너리가 실행될 때 바인딩 된 상태가 유지되어 원본 함수가 실행되는 것.

Hiding

#include <stdio.h>

class Parent {
    public : 
        void show() {
            printf("this is parent \n");
        }

        void show(int a) {
            printf("this is parent %d \n",a);
        }
};

class Child : public Parent {
    public :
        void show() {
            printf("this is Child");
        }

};

int main() {
    Child* c = new Child;
    Parent * p = new Parent;
    p->show();
    p->show(10);
    c->show();
    c->show(10);

}

Child 는 부모 Parent를 상속받고 Parent에서는 show() 함수를 오버로딩하여 두개를 선언했다 .

이 때 child에서 show()함수를 오버라이드 한 경우 Parent에서 오버로딩했던 show(int a)는 호출되지 못한다. 이런 경우를 Hiding 이라고 한다.


가상함수

첫번째 예제 코드에서 p에 c를 복사하였음에도 불구하고, 바인딩 된 결과 때문에 오버라이딩된 함수 대신 부모의 show() 함수가 그대로 호출되는 현상이 발생했다.

이러한 문제를 해결하기 위해 virtual 키워드를 붙이는데, 이느 하나의 객체가 여러 형태의 자료형을 가지는 다형성을 지킬 수 있다.

#include <stdio.h>

class Parent {
public:
    virtual void show() {
        printf("this is parent\n");
    }
};

class Child : public Parent {
public:
    virtual void show() {
        printf("this is child\n");
    }
};

class Child2 : public Child {
public:
    virtual void show() {
        printf("this is child2 \n");
    }
};

int main() {
    Parent* p = new Parent;
    Child* c = new Child;
    Child2* c2 = new Child2;

    p->show();
    c->show();
    c2->show();

    p = c;
    p->show();
    p = c2;
    p->show();
}
this is parent
this is child
this is child2
this is child
this is child2

virtual 키워드를 붙여서 show() 함수를 선언하면 위와 같은결과가 나온다. 의도한 대로 동작했다고 볼 수 있겠다.


Vtable

vTable(Virtual Table) 은 가상 함수의 주소 번지 목록을 가지는 포인터의 배열로, 해당 클래스에 소속된 가상 함수들이 저장된 주소를 갖고있는 표라고 보면 된다.

이 Vtable은 클래스마다 생성되는데, 이때 오버라이딩 된 함수라면 각기 다른 Vtable에 담긴 함수들은 같은 함수라도 주소가 다르고, 오버라이딩 되지 않았다면 같은 주소를 가진다.

#include <stdio.h>

class Parent {
public:
    virtual void show1() {
        printf("this is parent1\n");
    }
    virtual void show2() {
        printf("this is parent2\n");
    }
    virtual void show3() {
        printf("this is parent3\n");
    }
};

class Child : public Parent {
public:
    virtual void show1() {
        printf("this is child1\n");
    }

    virtual void show3() {
        printf("this is child1\n");
    }
};

class Child2 : public Child {
public:
    virtual void show1() {
        printf("this is child2 \n");
    }
};

int main() {
    Parent* p = new Parent;
    Child* c = new Child;
    Child2* c2 = new Child2;

    void (Parent:: * fptr1) (void) = &Parent::show1;
    void (Parent:: * fptr2) (void) = &Parent::show2;
    void (Parent:: * fptr3) (void) = &Parent::show3;

    void (Child:: * fptrc1) (void) = &Child::show1;
    void (Child:: * fptrc2) (void) = &Child::show2;
    void (Child:: * fptrc3) (void) = &Child::show3;

    void (Child2:: * fptrcc1) (void) = &Child2::show1;
    void (Child2:: * fptrcc2) (void) = &Child2::show2;
    void (Child2:: * fptrcc3) (void) = &Child2::show3;

    printf("Parent::show1 : 0x%08lx \n", fptr1);
    printf("Parent::show2 : 0x%08lx \n", fptr2);
    printf("Parent::show3 : 0x%08lx \n", fptr3);
    printf("----------------------------------\n");
    printf("Child::show1 : 0x%08lx \n", fptrc1); //overriding
    printf("Child::show2 : 0x%08lx \n", fptrc2);
    printf("Child::show3 : 0x%08lx \n", fptrc3); //overriding
    printf("----------------------------------\n");
    printf("Child2::show1 : 0x%08lx \n", fptrcc1); //overriding
    printf("Child2::show2 : 0x%08lx \n", fptrcc2);
    printf("Child2::show3 : 0x%08lx \n", fptrcc3);
}
Parent::show1 : 0x00a112b2
Parent::show2 : 0x00a11483
Parent::show3 : 0x00a1147e
----------------------------------
Child::show1 : 0x00a114a6
Child::show2 : 0x00a11483
Child::show3 : 0x00a11492
----------------------------------
Child2::show1 : 0x00a114ab
Child2::show2 : 0x00a11483
Child2::show3 : 0x00a11492

상속관계를 가지는 클래스를 선언하고 몇몇 함수들만 오버라이딩 하여 가상함수의 주소가 어떤식으로 구성되는지를 비교해본다.

Child는 Parent를 상속받아 show1과 show3을 오버라이딩했으므로, 오버라이딩 하지 않은 show2의 함수 주소는 Parent의 show2 함수 주소와 동일하다. 같은 함수를 불러온다는 소리다.

Child2는 Child를 상속받아 show1을 오버라이딩 했으므로, 오버라이딩 하지 않은 show2와 show3의 실제 주소는 Child와 같고, show2의 함수 주소는 Child에서도 오버라이딩 되지 않았으므로 Parent의 show2 함수 주소와 동일하다.

그럼 이 함수들의 실제 주소가 올라가있는 Vtable은 어떻게 구성이 되고있을까.

#include <stdio.h>

class Parent {
public:
    virtual void show1() {
        printf("this is parent1\n");
    }
    virtual void show2() {
        printf("this is parent2\n");
    }
    virtual void show3() {
        printf("this is parent3\n");
    }
};

class Child : public Parent {
public:
    virtual void show1() {
        printf("this is child1\n");
    }

    virtual void show3() {
        printf("this is child3\n");
    }
};

int main() {
    Parent* p = new Parent;
    Child* c = new Child;
    Child* c2 = new Child;

    p->show1();
    p->show2();
    p->show3();

    c->show1();
    c->show3();

    c2->show1();
}

IDA에서 보이는 대로면, 기준이 될 포인터를 하나 두고 해당 포인터로부터 +4, +8 위치에 있는 값을 가져와 call 하고 있다. 그 안에 들어갈 포인터 값은 디버깅을 해봐야 할듯.

Parent 클래스의 함수들이 호출되는 시점이다.

메모리에 함수 주소들이 들어있는 것 확인됨.

c1으로 선언된 Child 클래스의 함수들이 호출되는 시점이다

두번째에 위치한 show2함수의 주소는 Parent함수와 동일한것이 확인된다.

그 다음, 마지막으로 c2로 선언된 Child 클래스의 함수인 show1이 호출되는 시점인데, c1으로 선언되었을때와 동일한 주소인 0x918b84값이 edx 레지스터에 담긴게 확인된다.

그러므로 vtable에서 참조하는 함수 포인터도 동일할 수 밖에.

즉, 같은 클래스로 선언되어있으면 같은 vtable을 참조한다고 보면 될듯.

[ 그림 하나 그려서 이해하기 ]

가상함수의 시작주소는 vtable을 통해 가져온다는 느낌인데, 그럼 이 vtable 의 시작주소는 어떻게 찾아오는걸까.


Vtable 주소 찾기

디컴파일 코드 내용대로면 클래스 Parent, Child 각각에 대해 선언자처럼 생긴 함수가 호출되고 그 반환값을 가져와 포인터로 활용하는 모양새다. 어셈블리로 까보자.

저런 함수들이 호출되는 시점은 클래스가 선언되는 시점으로 파악된다.

특정 함수가 함수가 call 되면 함수 테이블을 참조하여 위에서 말한 함수로 점프한다.

그렇게 점프한 parent 클래스의 vtable을 가져오는 부분이다. 함수 실행시에 ECX레지스터를 통해 인자로 전달된 임의의 주소가 스택에 저장되고, 스택에 저장된 주소를 포인터로 하여 vftable 값을 복사하는게 보인다.

1204390 메모리 영역에 00EF8b34가 복사되었다. 이후 스택에 들어있는 1204390 주소를 그대로 EAX 레지스터에 복사하여 리턴되고, EAX값은 스택에 저장되어 포인터로 쓰인다.

이번엔 Child 클래스의 vtable을 가져오는 부분이다. Parent의 상속이기 때문에 사소한 부분은 차이가 있을 수 있는데, 핵심인 vftable을 복사하는 루틴은 코드영역에 어셈블리로 고정된 주소값인 00EF8B84 외에는 동일하게 보인다.

즉, vftable의 시작주소를 가져오는 루틴에서 vtable의 시작주소는 코드영역에 고정되어 박혀있다는 뜻이고, 이는 하나의 바이너리에서 거의 큰 차이가 없는 고정 루틴이라고 봐도 무방할듯 싶다. 주소값 말곤 달라질만한 부분이 보이지 않는 간단한 루틴이다.

 

이렇게 찾아온 00EF8B34는 vtable의 시작주소로써 기능해 클래스 구성 함수들의 주소를 가지게 된다. 즉, 해당 코드 위치만 찾을 수 있으면 어셈블리 영역에 박혀있는 vtable 시작주소를 찾아올 수 있다는 의미가 된다.

정적 상태에서 해당 코드 위치를 보면 34 8b 41 00 이 박혀있고, 이는 다른 헥스뷰어에서도 동일하다. 바이너리 실행 후에 변경되는 가변주소라고 생각하면 될듯.


정리하면, 바이너리에서 클래스별로 vtable 의 주소를 가져오는 시점은 클래스가 선언되는 시점으로 파악된다. 클래스가 선언되는 시점에 vtable 주소를 가져와 가지고 있다가, 클래스 멤버 가상함수를 호출할 때에 활용하는것으로 정리할 수 있겠다.

즉, 특정 가상함수의 가상주소를 찾고싶으면 해당 함수가 속하는 클래스를 찾고, 그 클래스가 선언되는 시점으로 가서 vtable 시작주소를 구해올 수 있다는 의미.

이후 D3D 후킹 진행시에도 이와 같은 방법으로 vtable의 시작주소를 가져오는 루틴을 찾고, 해당 루틴으로부터 vtable의 시작주소를 구해오는 식으로 후킹을 진행할 예정이다.


참고 문서

https://cosyp.tistory.com/228 - Vtable과 Overriding