C++
的虚表和动态派发的特性紧密关联。一个函数可能接受一个某类A
的对象,然后调用其某个虚方法。具体会调用哪个函数无法在编译的时候确定,从反汇编上看,函数的地址也是动态获取的,有时候会给我们的逆向带来分析的难度。
举个例子,这里有一个call_vir
函数:
int call_vir(base1 *b) {
return b->b1();
}
反汇编后会变成这个样子:
__int64 __fastcall call_vir(__int64 (__fastcall ***a1)(_QWORD))
{
return (**a1)(a1);
}
C++ ABI
关于虚表的具体内存排布与ABI
规范有关。ABI
制定了一套二进制程序兼容性的规范。主要规定的有传参的方式,C++
函数命名的Mangle
策略等,虚表结构也包含在内。
在linux平台上,gcc
编译器一般使用的是Itanium C++ ABI,arm架构也是指定的这套ABI。关于虚表的部分可以在这里找到Itanium C++ ABI。
Virtual Table
虚表在只读的数据段上,编译器自动生成的构造函数会设置对象的虚表。在Itanium ABI下,虚表位于对象内存的第一个位置。
虚表具体内容如下1:
- 第一个参数是指向
this
指针的偏移,如果采用了多继承,那么后面的虚表会设置该值为一个负数的偏移。 - 第二个参数是
RTTI
的类型相关信息,可以利用该信息自动解析出类名。 - 后面是一些函数指针。
Pure Virtual
C++
的纯虚函数是用来定义一个抽象接口。
class base2 {
virtual int b2() = 0;
int data;
};
设置为0表示该函数没有实现,如果子类没有重载该方法就无法通过编译。 实际上,编译器会将其函数指针替换成一个___cxa_pure_virtual
函数。
040E0 __ZTV5base2 DCQ 0 ; DATA XREF: base2::base2(void)+C↑o
__const:00000001000040E0 ; offset to this
__const:00000001000040E8 DCQ __ZTI5base2 ; `typeinfo for'base2
__const:00000001000040F0 DCQ ___cxa_pure_virtual
Multiple Inheritance / Secondary Virtual Table
我们都知道,C++
的public
继承表示的是is-a
的关系。那么,一个有两个父类A
,B
的对象既可以被当作A,也可以被当作B来使用。
具体在编译器的实现上,也是如此。对象的存储结构是A和B紧接着,附有两个虚表,都位在前八位。这样,如果有某个接受B
类的函数,类型转换就把this
的指针加上16字节,就完成了转换。
这样也就带来一个副作用,也即下面的等式会不成立:
Derived obj;
assert((void *)dynamic_cast<Derived *>(&obj) == (void *)dynamic_cast<Base *>(&obj));
下图是一个多继承的例子。(来自2)
--- title: Class Hierarchy --- classDiagram class base1 { +int data +b1() int } class base2 { +int data +b2() int } class der2 { +int data +b2() int } class derived { +d() int +b2() int } base1 <|-- derived base2 <|-- der2 der2 <|-- derived
derived
的构造
void __fastcall derived::derived(derived *this)
{
base1::base1(this: this);
der2::der2(this: (derived *)((char *)this + 16));
*(_QWORD *)this = &off_100004010;
*((_QWORD *)this + 2) = &off_100004038;
}
虚表如下:
__const:0000000100004000 ; `vtable for'derived
__const:0000000100004000 __ZTV7derived DCQ 0 ; DATA XREF: derived::derived(void)+2C↑o
__const:0000000100004000 ; offset to this
__const:0000000100004008 DCQ __ZTI7derived ; `typeinfo for'derived
__const:0000000100004010 off_100004010 DCQ __ZN5base12b1Ev ; base1::b1(void)
__const:0000000100004018 DCQ __ZN7derived1dEv ; derived::d(void)
__const:0000000100004020 DCQ __ZN7derived2b2Ev ; derived::b2(void)
__const:0000000100004028 DCQ -16 ; offset to this
__const:0000000100004030 DCQ __ZTI7derived ; `typeinfo for'derived
__const:0000000100004038 off_100004038 DCQ __ZThn16_N7derived2b2Ev ; `non-virtual thunk to'derived::b2(void)
Thunk
一个可能的问题是,子类重载了父类的方法,但是此时由于偏移的存在,this
指向的并非是整个对象。C++
对此采取了Thunk
的机制,该函数唯一的作用就是对传入的指针加上offset
的偏移(为负数),实现转换,再调用实际的函数。
Proximity
这部分参考3
在一个编译模块内部,其数据排布的位置是临近的。这意味着某些我们所关心的数据,虚表,或者类型信息可能都在某个数据段很靠近的位置上。
Analysis Tools
对一个使用了C++
虚函数的二进制程序,我们主要关注是否可以:
- 首先,找到虚表的位置(一些强大的分析工具可以自动查找,不过也有一定使用条件)
- 其次,怎样快速构建出虚表上函数的类型
- 对使用了改虚表的对象,设置对象类型,辅助分析(可以手动修改)
IDA Pro
IDA提供了对虚函数这一C++
特性的支持。
首先,IDA
可以自动找到虚表。其实现是依赖于RTTI
的,因为当指定了编译参数-fno-rtti
后,IDA
就无法识别出任何虚表。
(RTTI
确实会包含类型名称等信息)
可以在IDA
中以受限的C++
语法生成类型声明。在IDA
的类型窗口,可以写入一个class
的声明。例如,输出下面的代码:
class base1 { virtual int b1(); int data; };
class base2 { virtual int b2(); int data; };
class der2 : public base2 { virtual int b2(); int data; };
class derived : public base1, public der2 { virtual int d(); };
对于像这样的简单的继承关系(单继承),IDA
可以自动生成虚表类型和对象类型:
__int64 __fastcall call_vir(__int64 (__fastcall ***a1)(_QWORD))
{
return (**a1)(a1);
}
在我们将参数类型修正为base2
后,ida
的结果如下:
int call_vir(base2 *b) {
return b->b2();
}
对于更复杂的例如有多继承的类,可以手动创建类型(满足下列条件)
- VFT pointer must have the "__vftable" name
- VFT type must follow the "CLASSNAME_vtbl" pattern
Angr
vtable
的查找采取了下面的策略:
如果被代码块引用,而且本身是指向函数的指针,就认为是
vtable
的开头。
实例代码:
p = angr.Project(os.path.join(test_location, "x86_64", "cpp_classes"), auto_load_libs=False)
vtables_sizes = {0x403CB0: 24, 0x403CD8: 16, 0x403CF8: 16, 0x403D18: 16}
vtable_analysis = p.analyses.VtableFinder()
vtables = vtable_analysis.vtables_list
assert len(vtables) == 4
for vtable in vtables:
assert vtable.vaddr in [0x403CB0, 0x403CD8, 0x403CF8, 0x403D18]
assert vtables_sizes[vtable.vaddr] == vtable.size
Binary Ninja
- Devirtualizing C++ with Binary Ninja | Trail of Bits Blog
- C++ Types - Binary Ninja User Documentation
struct base1 __packed {
struct vtable_for_base1* vtable;
int data;
};
struct base2 __packed {
struct vtable_for_base2* vtable;
int data;
};
struct __base(base1, 0) __base(base2, 0x10) der2 __packed {
struct vtable_for_der2* vtable;
int data;
};
struct __data_var_refs vtable_for_base1
{
void (* b1)(struct base1* thiz);
};
struct __data_var_refs vtable_for_base2
{
void (* b2)(struct base2* thiz);
};