vtable
2025년 10월 10일
ISO 표준 #
Itanium ABI #
MSVC ABI #
테스트 환경
ubuntu 22.04 WSL
1class Base {
2public:
3 Base(){}
4 ~Base(){}
5 int b0 = 10;
6 int b1 = 20;
7 int* b2 = &b0;
8 int* b3 = &b1;
9
10 void print() { std::cout << "test" << std::endl; }
11
12};
13
14class Derive : public Base {
15public:
16 int d0 = 11;
17 int d1 = 21;
18 int* d2 = &d0;
19 int* d3 = &d1;
20
21 void print2() { std::cout << "test" << std::endl; }
22};
23
24class Base2 {
25public:
26 Base2(){}
27 ~Base2(){}
28 int b0 = 30;
29 int b1 = 40;
30 int* b2 = &b0;
31 int* b3 = &b1;
32
33 virtual void print() { std::cout << "test" << std::endl; }
34
35};
36
37class Derive2 : public Base2 {
38public:
39 int d0 = 31;
40 int d1 = 41;
41 int* d2 = &d0;
42 int* d3 = &d1;
43
44 void print() override { std::cout << "test" << std::endl; }
45};
1pwndbg> x/8gx *(void**)($rbp-0x20)
20x55555556b300: 0x0000555555557d38 0x000000280000001e
30x55555556b310: 0x000055555556b308 0x000055555556b30c
40x55555556b320: 0x000000290000001f 0x000055555556b320
50x55555556b330: 0x000055555556b324 0x000000000000ecd1
6pwndbg> x/8gx *(void**)($rbp-0x28)
70x55555556aeb0: 0x000000140000000a 0x000055555556aeb0
80x55555556aec0: 0x000055555556aeb4 0x000000150000000b
90x55555556aed0: 0x000055555556aec8 0x000055555556aecc
100x55555556aee0: 0x0000000000000000 0x0000000000000411
virtual 함수가 있어야만 vtable 포인터가 생성된다.
1+0x00: int b0
2+0x04: int b1
3+0x08: int* b2 (&b0)
4+0x10: int* b3 (&b1)
5+0x18: int d0
6+0x1C: int d1
7+0x20: int* d2 (&d0)
8+0x28: int* d3 (&d1)
9
10
11+0x00: void* vptr (vtable 엔트리들을 가리킴)
12+0x08: int b0
13+0x0C: int b1
14+0x10: int* b2 (&b0)
15+0x18: int* b3 (&b1)
16+0x20: int d0
17+0x24: int d1
18+0x28: int* d2 (&d0)
19+0x30: int* d3 (&d1)
Derive2 클래스 vtable의 offset-to-top 부터 읽어보면, 이런 형식으로 되어있다.
vtable[0]은 override된 print함수이다.
1pwndbg> x/4gx (*((void**)*(void**)($rbp-0x20)) - 0x10)
20x555555557d28 <_ZTV7Derive2>: 0x0000000000000000 0x0000555555557d58
30x555555557d38 <_ZTV7Derive2+16>: 0x00005555555553f8 0x0000000000000000
4
5pwndbg> x/10i **(void***)*(void**)($rbp-0x20)
6 0x5555555553f8 <_ZN7Derive25printEv>: endbr64
7 0x5555555553fc <_ZN7Derive25printEv+4>: push rbp
8 0x5555555553fd <_ZN7Derive25printEv+5>: mov rbp,rsp
9 0x555555555400 <_ZN7Derive25printEv+8>: sub rsp,0x10
10
11pwndbg> x/6gx *(void**)(*((void**)*(void**)($rbp-0x20)) - 0x8)
120x555555557d58 <_ZTI7Derive2>: 0x00007ffff7fa3c30 0x0000555555556010
130x555555557d68 <_ZTI7Derive2+16>: 0x0000555555557d70 0x00007ffff7fa2fa0
140x555555557d78 <_ZTI5Base2+8>: 0x0000555555556019 0x0000000000000001
RTTI 객체는 7d58이고, 구조는 아래와 같다.
1[0] vptr to vtable of __si_class_type_info
2[1] const char* name
3[2] const __class_type_info* base_type
그래서 실제로 읽어보면 [1]6010은 Derive 문자열이고, [2]7d70은 Base 클래스의 RTTI라서 [2][1]은 Base 문자열인 것을 확인할 수 있다.
1pwndbg> x/1s 0x555555556010
20x555555556010 <_ZTS7Derive2>: "7Derive2"
3
4pwndbg> x/2gx 0x0000555555557d70
50x555555557d70 <_ZTI5Base2>: 0x00007ffff7fa2fa0 0x0000555555556019
6
7pwndbg> x/1s 0x0000555555556019
80x555555556019 <_ZTS5Base2>: "5Base2"