【C++逆向】虚表(Virtual table)逆向 | 安卓so虚函数逆向

文章解释了C++中多态的概念,通过一个ISpeaker虚基类和Dog、Human两个子类的例子展示了多态性。当Dog和Human对象通过ISpeaker*指针调用speak()方法时,表现出不同的行为。编译器通过生成虚表(vtable)来实现多态,每个派生类都有自己的虚表实例。

什么是多态

定义一个虚基类ISpeaker

class ISpeaker{
protected:
	size_t b;
public:
	ISpeaker( size_t _v )
		: b(_v) 
	{}
	virtual void speak() = 0;
};

有两个子类,都实现了虚函数speak():

class Dog : public ISpeaker {
public:
	Dog()
		: ISpeaker(0)
	{}
	//
	virtual void speak() override {
		printf("woof! %llu\n", b);
	}
};

class Human : public ISpeaker {
private:
	size_t c;
public:
	Human()
		: ISpeaker(1)
		, c(2)
	{}

	virtual void speak() override {
		printf("hello! %llu\n", c);
	}
};

main方法中把Dog*和Human*类型的指针,退化(强转)成ISpeaker*指针,并调用speak()方法:

int main( int argc, char** _argv ) {
	Human* pHuman = new Human();
	Dog* pDog = new Dog();
	//
	ISpeaker* speaker1 = (ISpeaker*)pHuman;
	ISpeaker* speaker2 = (ISpeaker*)pDog;
	//
	speaker2->speak();
	speaker1->speak();
	//
	return 0;
}

输出:

woof! 0
hello! 2

同样类型的指针(都是ISpeaker*),其函数表现出不同的行为(输出不同),称为多态

编译器如何实现多态

以MSVC编译器为例
在这里插入图片描述
编译器处理虚类ISpeaker时,生成内存模型(结构体)__ispeaker,其成员有虚表指针SpeakerTable* vt

Dog继承ISpeaker,所以对应的内存模型__dog会包含__ispeaker中的成员;
Human继承ISpeaker,所以对应的内存模型__human会包含__ispeaker中的所有成员,此外还有自己的成员size_t c
在这里插入图片描述
而后,编译器为每个类生成唯一的虚函数表(虚表)对象
__dogSpeakerTable和__humanSpeakerTable是虚表对象

虚表实例中的函数指针指向对应的函数
在这里插入图片描述
所以Dog和Human,对应的 虚表对象 中的函数指针指向的函数不同。

这就是为什么speaker2->speak();speaker1->speak();的输出不同。

在内存中,__dog和__humen的布局如下:

struct __dog {
	const SpeakerTable* vt;
	size_t b;
};

struct __human {
	const SpeakerTable* vt;
	size_t b;
	size_t c;
};

因此,想访问b或者c成员时,是按虚表对象之后的偏移量来访问的。

调用__dog_speak和__human_speak时会传递对象__dog和__human作为参数:

int main( int _argc, char** _argv ) {
    __dog* dog = createDog();
	__human* human = createHuman();
	//
	__ispeaker* speaker1 = (__ispeaker*)dog;
	__ispeaker* speaker2 = (__ispeaker*)human;
	//
	speaker1->vt->speak(speaker1);
	speaker2->vt->speak(speaker2);
	return 0;
}

访问b或c对象时,基于偏移量:

void __dog_speak( void* _ptr ) {
	uint8_t* p = (uint8_t*)_ptr;
	p+=sizeof(SpeakerTable*);
	size_t b = *((size_t*)p);
	printf("woof! %llu\n", b);
}

void __human_speak( void* _ptr ) {
	uint8_t* p = (uint8_t*)_ptr;
	p+=sizeof(SpeakerTable*);
	p+=sizeof(size_t);
	size_t b = *((size_t*)p);
	printf("hello! %llu\n", b);
}

例如 uint8_t* p = (uint8_t*)_ptr;拿到__dog对象的起始偏移,
p+=sizeof(SpeakerTable*);是越过虚表对象,
size_t b = *((size_t*)p);就找到b对象了。

反编译so时虚函数调用长什么样

在这里插入图片描述
比如这里面的*(*a1+32)(a1,a2,a3)
a1是类对应的结构体__dog的起始地址,也就是__dogconst SpeakerTable* vt的地址
*a1是const SpeakerTable* vt的值,也就是虚表__dogSpeakerTable的地址
*a1+32是虚表中某个虚表项的地址
*(*a1+32)是某个虚表项的内容,也就是某个虚函数的地址


参考:
http://showlinkroom.me/2017/08/21/C-%E9%80%86%E5%90%91%E5%88%86%E6%9E%90/

https://www.bilibili.com/video/BV15g4y1a7F3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值