突破C++类的访问控制机制

众所周知,在C++中类成员能够具有三种访问权限,分别为public/protected/private
[ISO/IEC 14882:2014]A member of a class can be

  • private: that is, its name can be used only by members and friends of the class in which it is declared.
  • protected: that is, its name can be used only by members and friends of the class in which it is declared, by classes derived from that class, and by their friends (see 11.4).
  • public: that is, its name can be used anywhere without access restriction.

从标准意图上来看,是希望隐藏类的实现细节和底层数据,即为封装。但是我们也可以通过一些特殊的方式来突破访问权限的限制。

先来矫正一个观念:C++中类的访问控制所限制的是成员的访问权限,而不是可见权限

[ISO/IEC 14882:2014] It should be noted that it is access to members and base classes that is controlled, not their visibility.

一个类中的任何成员对于任何可以看到其类实现的代码都是可见的,问题是是否可以访问。即,当你直接访问一个类内成员的名字(标识符)时会被检查是否有访问该成员的权限,如果你标记为private且在类定义的外部或友元外部访问会提示编译错误。关于访问控制的可见性与可访问性的具体描述请看我的另一篇文章:访问控制机制的可见性与可访问性
所以我们可以通过不使用成员名字也就不会触发访问控制机制,不使用类成员名字却可以访问类成员的方式为——指向类成员的指针。关于C++中指向类成员的指针的具体内容可以看我之前的一篇文章:C++中指向类成员的指针并非指针

侵入式实现角度来看,我们可以通过类成员函数主动提供private成员访问的方法。
比较常用的方式为——返回private数据成员的引用或者返回指向类成员的指针,而我比较推崇返回指向成员的指针,因为如果返回引用后期可以访问到的只是某一个具体对象的成员,而函数指针则可以访问到任何一个该类对象的成员。

1
2
3
4
5
6
7
8
9
10
class A{
public:
A(int x=0):private_(x){}
void print(){ std::cout<<private_<<std::endl; }
auto retPrivateVal(){ return &A::private_; }
auto retPrivateFunc(){ return &A::privateFunc; }
private:
void privateFunc(){ std::cout<<"A::privateFunc()"<<std::endl; }
int private_;
};

因为这里获取到的是对象地址(数据成员/函数对象)则在外部均可以通过一个类对象或者类对象的指针访问,因为对指向类成员指针的访问实际上是通过this来计算该成员的偏移值,所以不会触发任何名字查找以及可访问性检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A example(456);
example.print();

auto memPtr=example.retPrivate();
example.*memPtr=123;
example.print();

auto memFuncPtr=example.retPrivateFunc();
(example.*memFuncPtr)();
/*
output:
456
123
A::privateFunc()
*/

可以看到,类A的private数据成员在外部被修改了,而且也可以在外部调用其private成员函数,也可以将其组合在STL的算符库中批量对类对象执行某种操作。
但是这是基于类的实现者主动提供访问权限这一前提的。

还有一些出于某些原因有时候我们并不希望自己手动修改类的代码(增加或删除类的接口),但是仍然有访问private成员的需求,那么可以用以下两种方式。
假定我们具有以下类:

1
2
3
4
5
6
7
8
9
class A{
public:
A(int x=0):private_(x){}
template<typename T>
void func(const T& x){/* something...*/}
void print(){cout<<private_<<endl;}
private:
int private_;
};

因为其具有一个模板成员函数,我们就可以从这个模板成员函数来突破类的访问权限。
方法就是,在外部添加一个该成员函数的特化版本,因为其是类成员的特化,它也具有访问类内所有成员的权限:

1
2
3
4
5
6
7
8
9
10
namespace {
struct ZZZ{
ZZZ(int i):ival(i){}
int ival;
};
}
template<>
void A::func(const ZZZ& z){
private_=z.ival;
}

这里,当我们使用ZZZ作为参数来调用A::func成员函数时,就能够访问到private成员,也就突破了C++类的访问限制。在里面直接操作private也好,传递出成员指针也好(传递给传入对象),在外部都可以访问到类A的private成员了。

1
2
3
4
5
6
7
8
9
10
11
A example(456);
example.print();

example.func(ZZZ(123));
example.print();

/*
output:
456
123
*/

如有类具有友元模板也同样可以实现上面的行为,而且这种方式是完全符合C++标准的。

下面说一种,依赖于编译器实现的突破访问权限的方法:
同样使用上面的类A作为突破的目标。
可以模仿出和突破目标相同布局的类,但将对应的private成员的访问标号改为public或者protected等更宽松的访问权限(实际上这个是依赖于实现的)。

1
2
3
4
5
6
7
8
9
// 唯一的区别就是private_在类B中为public
class B{
public:
B(int x=0):private_(x){}
template<typename T>
void func(const T& x){/* something...*/}
void print(){cout<<private_<<endl;}
int private_;
};

然后就可以用来搞事情了:

1
2
3
4
5
6
7
8
9
10
11
12
A example(456);
example.print();

(reinterpret_cast<B&>(example)).private_=123;

example.print();

/*
output:
456
123
*/

最核心的就是reinterpret_cast<B&>(example),强制让编译器把一个类A对象解释为类B的对象,然后访问类B中的public成员private_,但是实际上改动的是类A的private成员private_
但是这里也是依赖于编译器的实现,因为我们假定修改了访问标号的类B和类A的布局完全相同,但是这是不一定的(在G++中运行通过)。

[TC++PL4th]A compiler may reorder sections of a class with separate access specifiers.

也可以将类A和类B的对象放入一个union中,以A的方式存入,以B的方式读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
union U{
A aobj;
B bobj;
};
int main()
{
U uobj{456};
uobj.aobj.print();
uobj.bobj.private_=123;
uobj.aobj.print();
}
/*
output:
456
123
*/

还可以通过另一种奇淫巧技来访问类的私有成员,直接看代码如下(最新代码会放在gist上:hack-private-data-member.cpp):

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <iostream>
#include <cstdio>
using namespace std;
namespace Hacker
{
template<typename Tag, typename Tag::MemType M>
struct PrivateMemberStealer
{
// define friend funtion GetPrivate,return class member pointer
friend typename Tag::MemType GetPrivate(Tag) { return M; }
};
}

#define DECL_HACK_PRIVATE_DATA(ClassName,DataType,DataName) namespace Hacker{\
struct ClassName##_##DataName\
{\
typedef DataType ClassName::*MemType;\
friend MemType GetPrivate(ClassName##_##DataName);\
};\
template struct PrivateMemberStealer<ClassName##_##DataName, &ClassName::DataName>;\
}

#define DECL_HACK_PRIVATE_STATIC_DATA(ClassName,DataType,DataName) namespace Hacker{\
struct ClassName##_##DataName\
{\
typedef DataType* MemType;\
friend MemType GetPrivate(ClassName##_##DataName);\
};\
template struct PrivateMemberStealer<ClassName##_##DataName, &ClassName::DataName>;\
}

#define DECL_HACK_PRIVATE_STATIC_FUNCTION(ClassName,MemberName,ReturnType,...) \
namespace Hacker{\
struct ClassName##_##MemberName \
{\
typedef ReturnType (*MemType)(__VA_ARGS__);\
friend MemType GetPrivate(ClassName##_##MemberName);\
};\
template struct PrivateMemberStealer<ClassName##_##MemberName, &ClassName::MemberName>;\
}

#define DECL_HACK_PRIVATE_NOCONST_FUNCTION(ClassName,MemberName,ReturnType,...) \
namespace Hacker{\
struct ClassName##_##MemberName \
{\
typedef ReturnType (ClassName::*MemType)(__VA_ARGS__);\
friend MemType GetPrivate(ClassName##_##MemberName);\
};\
template struct PrivateMemberStealer<ClassName##_##MemberName, &ClassName::MemberName>;\
}
#define DECL_HACK_PRIVATE_CONST_FUNCTION(ClassName,MemberName,ReturnType,...) \
namespace Hacker{\
struct ClassName##_##MemberName \
{\
typedef ReturnType (ClassName::*MemType)(__VA_ARGS__)const;\
friend MemType GetPrivate(ClassName##_##MemberName);\
};\
template struct PrivateMemberStealer<ClassName##_##MemberName, &ClassName::MemberName>;\
}

#define GET_VAR_PRIVATE_DATA_MEMBER(ClassInstancePointer,ClassName,DataName) ClassInstancePointer->*GetPrivate(::Hacker::ClassName##_##DataName())
#define GET_REF_PRIVATE_DATA_MEMBER(RefName,ClassInstancePointer,ClassName,DataName) auto& RefName = ClassInstancePointer->*GetPrivate(::Hacker::ClassName##_##DataName())
#define GET_PRIVATE_STATIC_DATA_MEMBER_PTR(PtrName,ClassName,DataName) auto PtrName = GetPrivate(::Hacker::ClassName##_##DataName())
// using ADL found to ::Hacker::Getprivate
#define GET_PRIVATE_MEMBER_FUNCTION(ClassName,MemberName) GetPrivate(::Hacker::ClassName##_##MemberName())
#define CALL_MEMBER_FUNCTION(ClassPointer,MemberFuncPointer,...) (ClassPointer->*MemberFuncPointer)(__VA_ARGS__)

class A{
public:
A(int ivalp=0):ival{ivalp}{}

static void printStaticIval2()
{
printf("%d\n",A::static_ival2);
}
private:
int func(int ival)const
{
printf("A::func(int)\t%d\n",ival);
printf("ival is %d\n",ival);
return 0;
}
int ival;

static int static_ival2;

};

// private member data
DECL_HACK_PRIVATE_DATA(A,int,ival)
// private member function
DECL_HACK_PRIVATE_CONST_FUNCTION(A, func, int, int)
// private static member
DECL_HACK_PRIVATE_STATIC_DATA(A,int,static_ival2)
// private static member function
DECL_HACK_PRIVATE_STATIC_FUNCTION(A,printStaticIval2,void);

int A::static_ival2 = 666;

int main()
{
A aobj(789);
// get private non-static data member
GET_REF_PRIVATE_DATA_MEMBER(ref_ival, &aobj, A, ival);
ref_ival=456;
std::cout<<GET_VAR_PRIVATE_DATA_MEMBER(&aobj, A, ival)<<endl;

// call private non-static member function
auto A_func=GET_PRIVATE_MEMBER_FUNCTION(A, func);
std::cout<<CALL_MEMBER_FUNCTION(&aobj, A_func, 123)<<std::endl;

// get private static data member
GET_PRIVATE_STATIC_DATA_MEMBER_PTR(A_static_ival2, A, static_ival2);
std::cout<<*A_static_ival2<<std::endl;
*A_static_ival2 = 123456;

// cll private static function member
auto A_printStaticIval2_func_ptr = GET_PRIVATE_MEMBER_FUNCTION(A,printStaticIval2);
A_printStaticIval2_func_ptr();
}

上面的代码实现是通过模板来获取成员函数的函数指针,展开之后是这样的:

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
class A{
void func(){
printf("A::func\n");
}
};

template<typename T,typename T::MemType M>
struct PrivateMemberHacker{
friend typename T::MemType GetPrivate(T){return M;};
};

struct A_func
{
typedef void (A::*MemType)(void);
friend MemType GetPrivate(A_func);
};
template struct PrivateMemberHacker<A_func,&A::func>;


int main()
{
void (A::*MemType)(void)=GetPrivate(A_func());
A aobj;
(&aobj->*MemType)();
return 0;
}

使用模板的原因就是要在编译时绕过访问权限检查。
因为我们要获取的是A::func的地址,在运行时&A::func具有访问权限检查,是无法通过编译的,所以需要模板来进行提前执行。
所以新建一个类的定义,将要获取的成员的类型作为一个typedef(MemType),从而将我们不能访问的成员类型A::func,变成可以访问的成员类型A_func::MemType,最重要的是将模板的实参(&A::func)传递给它(T::MemType M),来突破访问类的控制权限。

并且,多态也是支持的:

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
class A{
public:
A()=default;

protected:
virtual void virtualfunc()
{
printf("A::virtualfunc\n");
printf("%d %d\n",ival,ival2);
}

int ival = 123;
int ival2 = 456;
};

class B:public A
{
protected:
virtual void virtualfunc()override
{
printf("B::virtualfunc\n");
printf("%d\n",ival3);
}
int ival3 = 789;
};

// private member function
DECL_HACK_PRIVATE_NOCONST_FUNCTION(A, virtualfunc,void)

int main()
{
B bobj;

auto A_virtualfunc=GET_PRIVATE_MEMBER_FUNCTION(A, virtualfunc);
CALL_MEMBER_FUNCTION(&bobj, A_virtualfunc);
}

结语:可以从第一种突破类访问控制的方式中看到成员函数模板和类访问控制之间的影响,可以通过成员模板来绕过访问控制的机制,而且最重要的是它是可移植的,并且可以进行代码升级的(只要成员名字没有变化),但是不建议任何突破成员访问限制的行为。

全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:突破C++类的访问控制机制
文章作者:查利鹏
发布时间:2017年05月12日 11时58分
更新时间:2021年05月24日 17时28分
本文字数:本文一共有2.4k字
原始链接:https://imzlp.com/posts/12080/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!