Introduction
In this post, I will first give a detailed explanation on how virtual function works on Linux platform. Based on my given information, I will further explain how virtual function call can be utilised to hijack control flow.
Virtual Function
The virtual function is a key part in C++, which supports polymorphism. I will talk about the constructor function, virtual table and virtual function call respectively. The source code used in this post is given below.
// g++ test.cpp -o test #include<iostream> using namespace std; class Test { public : int count; virtual void Show() { cout<<"I am in Test Class"<<endl; } Test() { count = -1; } }; class Test1 : public Test { public: virtual void Show() { cout<<"I am in Test1 Class"<<endl; } virtual void T1Show() { cout<<"I am in Test1 own Class"<<endl; } Test1() {t1 = 1;} private: int t1; }; class Test2 : public Test { public: virtual void Show() { cout<<"I am in Test2 Class"<<endl; } virtual void T2Show() { cout<<"I am in Test2 own Class"<<endl; } Test2() {t2 = 2;} private: int t2; }; class Test3: public Test1, public Test2 { public: virtual void Show() { cout<<"I am in Test Derived Class"<<endl; } virtual void T1Show() { cout<<"I am in T1 Derived Class"<<endl; } virtual void T2Show() { cout<<"I am in T2 Derived Class"<<endl; } Test3() {t3 = 3;} private: int t3; }; int foo(Test3 *t) { t->Show(); return 0; } int bar(Test t) { t.Show(); return 0; } int main() { Test *Obj; Test BaseObj; // Base Class Object Test1 Obj1; Test2 Obj2; Test3 Obj3; Test3 *Obj4 = new Test3(); int x; Obj = &BaseObj; Obj->Show(); //In this case derived class show function called. cin>>x; if(x%2==1) { Obj = &Obj1; } else if(x%2==0) { Obj = &Obj2; } Obj->Show(); foo(Obj4); bar(BaseObj); return 0; }
Constructor Function
The constructor function is the first step to create an object that supports virtual function. The following assembly code lists the constructor functions in the target binary. As presented in the source code, I create a diamond structure in the inheritance relation for Class test3. So let’s dive into the assembly code and see how the constructor function works.
|0x00000dca 4881ec980000. sub rsp, 0x98 |0x00000dd1 488d45c0 lea rax, qword [rbp - local_40h] |0x00000dd5 4889c7 mov rdi, rax |0x00000dd8 e889010000 call fcn.00000f66 |0x00000ddd 488d45b0 lea rax, qword [rbp - local_50h] |0x00000de1 4889c7 mov rdi, rax |0x00000de4 e811020000 call sym.Test1::Test1 |0x00000de9 488d45a0 lea rax, qword [rbp - local_60h] |0x00000ded 4889c7 mov rdi, rax |0x00000df0 e8a9020000 call sym.Test2::Test2 |0x00000df5 488d8570ffff. lea rax, qword [rbp - local_90h] |0x00000dfc 4889c7 mov rdi, rax |0x00000dff e882030000 call sym.Test3::Test3 |0x00000e04 bf28000000 mov edi, 0x28 ; '(' |0x00000e09 e822feffff call sym.operatornew |0x00000e0e 4889c3 mov rbx, rax |0x00000e11 4889df mov rdi, rbx |0x00000e14 e86d030000 call sym.Test3::Test3
Constructor Function of Base Object
As the base class in the source code, the constructor function only needs to do two jobs: (1) set the virtual table pointer of the current object (0xf6e), and (2) initialise the member variable (0xf80).
|; var int local_8h @ rbp-0x8 |; CALL XREF from 0x00000dd8 (sym.main) |; CALL XREF from 0x0000100d (sym.Test1::Test1) |; CALL XREF from 0x000010b1 (sym.Test2::Test2) |0x00000f66 55 push rbp |0x00000f67 4889e5 mov rbp, rsp |0x00000f6a 48897df8 mov qword [rbp - local_8h], rdi |0x00000f6e 488d15bb0d20. lea rdx, qword 0x00201d30 ; 0x201d30 |0x00000f75 488b45f8 mov rax, qword [rbp - local_8h] |0x00000f79 488910 mov qword [rax], rdx |0x00000f7c 488b45f8 mov rax, qword [rbp - local_8h] |0x00000f80 c74008ffffff. mov dword [rax + 8], 0xffffffff ; |0x00000f87 90 nop |0x00000f88 5d pop rbp \0x00000f89 c3 ret
Constructor Function of Derived Object Test1
As a derived class of the base class, the constructor function of class Test1 clearly contains more steps than base class. In the first, it will call the constructor function of base class to initialise the its member variable (count) and put the vtable (virtual table) pointer on the top of object. Next, the vtable pointer will be overwritten with its own vtable pointer (0x101d) and its own member variable will be initialised (0x1024).
|; var int local_8h @ rbp-0x8 |; CALL XREF from 0x00000de4 (sym.main) |; CALL XREF from 0x00001199 (sym.Test3::Test3) |0x00000ffa 55 push rbp |0x00000ffb 4889e5 mov rbp, rsp |0x00000ffe 4883ec10 sub rsp, 0x10 |0x00001002 48897df8 mov qword [rbp - local_8h], rdi |0x00001006 488b45f8 mov rax, qword [rbp - local_8h] |0x0000100a 4889c7 mov rdi, rax |0x0000100d e854ffffff call fcn.00000f66 |0x00001012 488d15f70c20. lea rdx, qword 0x00201d10 ; 0x201d10 |0x00001019 488b45f8 mov rax, qword [rbp - local_8h] |0x0000101d 488910 mov qword [rax], rdx |0x00001020 488b45f8 mov rax, qword [rbp - local_8h] |0x00001024 c7400c010000. mov dword [rax + 0xc], 1 |0x0000102b 90 nop |0x0000102c c9 leave \0x0000102d c3 ret
The constructor function of class Test2 is almost the same. It will first call the constructor function of the base class and initialise its own member variable afterwards.
Constructor Function of Derived Object Test3
The constructor function of a diamond structured object is a little bit complicated. It will try to call the constructor function of Test1 and Test2 in the first and then overwrite the vtable pointer of the current object and initialise its own membere variable.
|; var int local_8h @ rbp-0x8 |; CALL XREF from 0x00000dff (sym.main) |; CALL XREF from 0x00000e14 (sym.main) |0x00001186 55 push rbp |0x00001187 4889e5 mov rbp, rsp |0x0000118a 4883ec10 sub rsp, 0x10 |0x0000118e 48897df8 mov qword [rbp - local_8h], rdi |0x00001192 488b45f8 mov rax, qword [rbp - local_8h] |0x00001196 4889c7 mov rdi, rax |0x00001199 e85cfeffff call sym.Test1::Test1 |0x0000119e 488b45f8 mov rax, qword [rbp - local_8h] |0x000011a2 4883c010 add rax, 0x10 |0x000011a6 4889c7 mov rdi, rax |0x000011a9 e8f0feffff call sym.Test2::Test2 |0x000011ae 488d15f30a20. lea rdx, qword 0x00201ca8 ; 0x201ca8 |0x000011b5 488b45f8 mov rax, qword [rbp - local_8h] |0x000011b9 488910 mov qword [rax], rdx |0x000011bc 488d150d0b20. lea rdx, qword 0x00201cd0 ; 0x201cd0 |0x000011c3 488b45f8 mov rax, qword [rbp - local_8h] |0x000011c7 48895010 mov qword [rax + 0x10], rdx |0x000011cb 488b45f8 mov rax, qword [rbp - local_8h] |0x000011cf c74020030000. mov dword [rax + 0x20], 3 |0x000011d6 90 nop |0x000011d7 c9 leave \0x000011d8 c3 ret
From the operation above, we can infer the memory layout of class Test3 as following:
++++++++++++++++++++++++++++ 0x8+ vft addr of test1 + ++++++++++++++++++++++++++++ 0x10+ member variable of test1 + ++++++++++++++++++++++++++++ 0x18+ vft addr of test2 + ++++++++++++++++++++++++++++ 0x20+ member variable of test2 + ++++++++++++++++++++++++++++ 0x28+ private variable + ++++++++++++++++++++++++++++
Virtual Function Table
After discussing the constructor function, let’s talk about the virtual table.
Virtual Table of Class Test
In the virtual table of base class it only contains a function pointer, which points to Test::show.
[0x00000c50]> pxq 0x10 @0x201d30 0x00201d30 0x0000000000000f2e 0x0000000000000000 ................ [0x00000c50]> pd 1 @0xf2e ;-- Test::Show: 0x00000f2e 55 push rbp
Virtual Table of Class Test1
The virtual table of the derived object is shown as below. The first function pointer points to the function derived from base class (Test1::Show). The second function pointer points to its own virtual function (Test1::T1Show).
[0x00000c50]> pxq 0x10 @0x201d10 0x00201d10 0x0000000000000f8a 0x0000000000000fc2 ................ [0x00000c50]> pd 1 @0xf8a ;-- Test1::Show: 0x00000f8a 55 push rbp [0x00000c50]> pd 1 @0xfc2 ;-- Test1::T1Show: 0x00000fc2 55 push rbp
Virtual Table of Class Test3
The virtual table of class Test3 is much more interesting. In the object of class Test3, there exists two virtual table pointers (0x201ca8 and 0x201cd0) as shown below.
[0x00000c50]> pxq 0x18 @0x201ca8 0x00201ca8 0x00000000000010d2 0x0000000000001110 ................ 0x00201cb8 0x0000000000001148 H....... [0x00000c50]> pxq 0x10 @0x201cd0 0x00201cd0 0x0000000000001109 0x000000000000117f ................ [0x00000c50]> pd 1 @0x10d2 ;-- Test3::Show: 0x000010d2 55 push rbp [0x00000c50]> pd 1 @0x1110 ;-- Test3::T1Show: 0x00001110 55 push rbp [0x00000c50]> pd 1 @0x1148 ;-- Test3::T2Show: 0x00001148 55 push rbp [0x00000c50]> pd 3 @0x1109 | ;-- non-virtualthunktoTest3::Show(): | 0x00001109 4883ef10 sub rdi, 0x10 `=< 0x0000110d ebc3 jmp sym.Test3::Show 0x0000110f 90 nop [0x00000c50]> pd 3 @0x117f | ;-- non-virtualthunktoTest3::T2Show(): | 0x0000117f 4883ef10 sub rdi, 0x10 `=< 0x00001183 ebc3 jmp sym.Test3::T2Show 0x00001185 90 nop
The first virtual table pointer is normal, which contains the function pointer of Test3::Show, Test3::T1Show and Test3::T2Show. The second virtual table pointer contains two function pointer that jumps to Test3:Show and Test3:T2Show. The existence of this vtable pointer is for type casting. When an object of class Test3 is cast to class Test2, i.e. plus 0x10 to the base address of the current object, the cast object can still work well with the virtual function of class Test2
Virtual Function Call
Based on the information given above, let’s now come to how a virtual function works at binary level.
/* source code level Obj = &BaseObj; Obj->Show(); */ 0x00000e1d 488d45c0 lea rax, qword [rbp - 0x40] 0x00000e21 488945e8 mov qword [rbp - 0x18], rax 0x00000e25 488b45e8 mov rax, qword [rbp - 0x18] 0x00000e29 488b00 mov rax, qword [rax] 0x00000e2c 488b00 mov rax, qword [rax] 0x00000e2f 488b55e8 mov rdx, qword [rbp - 0x18] 0x00000e33 4889d7 mov rdi, rdx 0x00000e36 ffd0 call rax
Therefore, the basic workflow of a virtual function call can be divided into 4 steps:
(1) Fetch vtable pointer (0xe29)
(2) Fetch function pointer (0xe2c)
(3) Set argument variable (0xe33)
(4) Call virtual function (0xe36)
Virtual Function Call Hijacking
In real exploitation, a common technique is to corrupt the vtable pointer located in an object first via some memory corruption errors (use-after-free or overflow). Then attacker needs to craft a fake virtual table in memory and then trigger the virtual function call of the victim object.