-->

C++ 리버싱을 위한 vtable 분석

C++ 리버싱은 C 리버싱과는 많이 다르기 때문에 항상 난항을 겪는다. C와는 다른 대표적인 C++ 특징 중 하나가 가상 함수 테이블인 vtable이 있는데 이 vtable 중 하나의 함수를 후킹하는 경우도 있기 때문에 덤프분석을 하거나 할때는 vtable도 잘 살펴봐야 한다.

 

따라서 가상함수 호출 시 vtable을 어떻게 참조하는지 어떤 특징이 있는지 등과 덤프에서 vtable 영역을 찾으려면 어떻게 해야하는지를 여기에 정리해보려 한다.

 

 

가상 함수 테이블 (vtable) 이란

가상 함수 테이블(vtable)이란 C++에서 virtual 메소드를 사용해 함수를 만들 때 생기는 테이블로 각 클래스 별 가상함수만 빼내어 그 주소를 가르키는 함수포인터를 저장해놓은 일종의 함수 포인터 배열이라고 할 수 있다.

 

C의 경우 call sub_XXXXXXXX 의 형태로 함수를 호출하기 때문에 '이 주소에서 이 주소의 함수를 call 하는구나' 정도는 알 수가 있는데 vtable의 가상함수를 호출하게 되면 직접적으로 함수주소값이 나오지 않기 때문에 덤프만 가지고 분석하거나 하게될 경우 함수 호출조차 제대로 살펴보지 못하고 미궁으로 빠질 수 있다.....

 

일단, 분석에 사용할 바이너리로 사용하기 위해 클래스 내부에서 가상함수를 선언하고 main문에서 객체 선언 및 가상함수 호출을 수행하는 간단한 코드를 컴파일했다.

 

#include <iostream>

using namespace std;

class Parent
{
public:
	virtual void func1() { cout << "Parent::func1()" << endl; }
	virtual void func2() { cout << "Parent::func2()" << endl; }
	virtual void func3() { cout << "Parent::func3()" << endl; }
};

class Child : public Parent
{
public:
	virtual void func4() { cout << "Parent::func4()" << endl; }
};

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

	p->func1(); // Parent의 func1 함수 호출
	p->func2(); // Parent의 func2 함수 호출
	p->func3(); // Parent의 func3 함수 호출
	c->func1(); // Parent의 func1 함수 호출
	c->func4(); // Child의 func4 함수 호출

	system("pause");
	return 0;
}

 

 

Class 객체 선언과 vtable 저장

객체를 생성하면, 객체 생성 시 사용한 포인터 변수들은 전부 해당 클래스의 vtable을 가리키게 된다. 객체 생성을 위한 call 후의 반환값이 eax가 각각 esi, edi에 저장되고 있으므로 이게 포인터변수이고 각 포인터변수의 값에 vtable offset이 저장되는 것을 볼 수 있다.

 

  • p ---> Parent::vftable
  • c ---> Child::vftable

 

그리고 각 객체마다 vtable이 전부 따로 생성이 되는데 아래 어셈 코드에서도 Parent 객체 생성 후 Parent::vftable이 [esi]에 저장되고, Child 객체 생성 후 Child::vftable이 [edi]에 또 저장하고있다. 보통 분석할 때, 심볼이 있는 경우보다는 없는 경우가 더 많았기 때문에 심볼이 없는 경우에 대한 캡쳐도 같이 올려두었다.

 

클래스 객체 생성 (심볼 있는 경우)
클래스 객체 생성 (심볼 없는 경우) 

 

 

rdata에 저장되어있는 Parent, Child 객체의 vtable을 확인해보면, Child의 경우 Parent를 상속받았기 때문에 Parent의 가상함수인 func1, func2, func3까지 같이 저장되어 있다. 상속받는 함수의 경우 따로 vtable에 있지 않고 상위 vtable을 참조하는 형태일 줄 알았는데 아니었다. 

 

  • p ---> Parent::vftable (Parent::func1)
  •             Parent::func2
  •             Parent::func3
  • c ---> Child::vftable (Parent::func1)
  •             Parent::func2
  •             Parent::func3
  •             Child::func4

 

그리고 vtable에 저장되는 함수는 선언 순서대로 저장되어 있는 것을 알 수 있다. 따라서 C++ 데이터 영역에서 다음과 같이 함수 포인터배열이 있다면 vtable 영역으로 의심해볼 수 있을 것이다.

 

각 클래스의 vtable (심볼 있는 경우)
각 클래스의 vtable (심볼 없는 경우)

 

 

vtable에서의 가상함수 호출

vtable offset이 각 객체 포인터변수([p], [c])에 저장되었기 때문에 가상함수 호출 시 해당 값을 사용해서 호출할 것이라고 예상할 수 있다.

 

 

직접 덤프를 확인해보면서 보면 더 가시적으로 확인하기 쉽다. vtable의 시작지점에서부터 func1 호출은 +0x0, func2 호출은 0x4, func3 호출은 +0x8이 된다. 

 

 

 

한가지 특이한 점은, C++에서 this 포인터를 ecx 레지스터로 사용하고 클래스 멤버변수에 접근할 때 사용한다는 것은 얼핏 듣긴했는데 가상함수 호출 후에 계속 ecx 레지스터에 vtable offset 값이 저장된다는 것이다. 이와 관련해선 좀더 공부 후 추가하려 한다. 

 

 

덤프에서 vtable 영역 찾기

그렇다면! 덤프에서 vtable 영역을 알아내려면 어떻게 해야 할까. 물론 심볼이 있는 경우는 *vftable*로 서치를 해주면 된기 때문에 매우 간단하다. 하지만 심볼이 없는 경우라면? 생각할게 많아진다..ㅋㅋㅋㅋ vtable 영역을 못찾아서 삽질한 경우가 있기 때문에, 이 경우에 대해선 다음 포스팅에서 자세히 다뤄보도록 한다.

 

 

댓글

Designed by JB FACTORY