0x0 虚表的简单介绍

虚表(Virtual Table)是一种在面向对象编程中常见的概念,特别是在使用C++等支持多态性(Polymorphism)的编程语言中。它是一种数据结构,用于实现类的动态绑定(Dynamic Binding)和多态性。在C++中,虚表通常与虚函数(Virtual Function)一起使用。在具有继承关系的类中,当一个基类指针或引用指向一个派生类对象时,如果基类声明了虚函数,那么通过这个指针或引用调用虚函数时,将会根据实际对象的类型来确定调用的是哪个函数。这种确定发生在运行时,被称为动态绑定。虚表的作用就是为实现动态绑定提供支持。每个带有虚函数的类都有一个虚表,其中存储了指向各个虚函数的指针。当通过基类指针或引用调用虚函数时,实际上是通过虚表来确定应该调用哪个函数。在编译时,编译器会根据类的虚函数情况生成虚表,并在每个对象的内存布局中添加一个指向该虚表的指针。这个指针被称为虚表指针(vptr)。当调用虚函数时,实际上是通过对象的虚表指针找到对应的虚表,然后再根据函数在虚表中的位置来调用正确的函数。总的来说,虚表是一种实现多态性和动态绑定的机制,它使得在面向对象编程中能够以更灵活的方式使用继承和多态性。

0x1 虚表的正向逆向示例

虚表的一个简单示例源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A

{

public:

void func1() { cout << "A func1" << endl; }

virtual void vfunc1() { cout << "A vfunc1" << endl; }

private:

int m_a;

int m_b;

};

虚表在内存中的图示:

多继承中源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class A

{

public:

void func1() { cout << "A func1" << endl; }

virtual void vfunc1() { cout << "A vfunc1" << endl; }

private:

int m_a;

int m_b;

};

class B : public A

{

public:

void func1() { cout << "B func1" << endl; }

virtual void vfunc2() { cout << "B vfunc2" << endl; }

private:

int m_a;

};

内存图示:

https://zhuanlan.zhihu.com/p/353260837

在ida中逆向分析时里的虚表例子:

*(_QWORD )v21为读取虚表,+0x28LL为虚表偏移(i0x8)

0x2 虚表hook的实现

综上所示,我们想要实现虚表hook的方式就很明了了,内存加载后虚表的位置是不会改变的,只需要用mprotect把想要hook的虚表所在内存页的属性修改为rw,然后把hook函数的地址写入指定虚表地址,写入后还原页属性即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void VFuncHook(uintptr_t VFunc, uintptr_t hookAddr)

{

// 假设 PAGE_SIZE 是系统的页面大小

size_t page_size = sysconf(_SC_PAGESIZE);

// 计算 addr 所在页的起始地址

void *page_start = (void *)((uintptr_t)VFunc & ~(page_size - 1));

// 计算要修改的地址的偏移量

size_t offset = (uintptr_t)VFunc - (uintptr_t)page_start;

// 新的权限:不可读写可执行

int prot = PROT_READ | PROT_WRITE;

// 使用 mprotect 修改内存权限

if (mprotect(page_start, page_size, prot) == -1)

{

LOGD("[-] mprotect Erro");

exit(EXIT_FAILURE);

}

Write(VFunc, hookAddr);

if (mprotect(page_start, page_size, PROT_READ) == -1)

{

LOGD("[-] mprotect Erro");

exit(EXIT_FAILURE);

}

// LOGD("[+] Hook");

}
1
2
3
4
5
void hookFunc(){

LOGD("FuncHooked");

}
1
2
3
4
5
void *main_thread(){

VFuncHook(VFuncPtr,(uintptr_t)hookFunc);

}

0x3 小结

虚表hook相较于inlinehook来说没有修改代码段,无法通过对代码段crc校验来检测,一般检测的话需要分析类虚表中有哪些引用,通过so地址+偏移计算出虚表函数中的地址并通过算法生成一个校验值,然后需要检测的时候可以选择比对整个虚表的校验值或者校验某些函数地址是否被修改。还可以通过hook mprotect,判断当前修改的地址是否合法,回溯调用栈,看调用的地址是否有异常。

说起mprotect,还有一种可以通过mprotect实现基于页面异常的hook(其实就是软件断点)这种hook可以实现不会触发crc校验。虽然linux mprotect只能修改整个页面,但是发生异常的时候handler接收的siginfo里会有发生异常的地址信息,能准确的对某个地方进行hook。这个东西后面有空的时候可以作为学习记录一下在自己实现的过程中发现的一些小技巧和坑。