最近遇到了一个C++的pwn题,所以在这里对C++的程序进行深入学习一下:
C++逆向分析学习
C++的类
C++与C最大的区别就是【面向对象】这个概念。我们知道在C++中,class是一个常用的概念。那么在二进制程序中,class呈现的是什么样子的呢,我们这里用一个程序进行分析说明。
| 12
 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
 
 | #include <iostream>#include <string>
 
 using namespace std;
 
 class Person
 {
 
 public:
 Person(){
 age = 0;
 name = "";
 };
 ~Person(){
 
 };
 virtual void setName(string name) = 0;
 virtual void setAge(int age)  = 0;
 protected:
 int age;
 string name;
 
 };
 
 class Student :public Person{
 
 public:
 Student(){
 age = 17;
 }
 void setName(string name){
 this->name = name;
 }
 string getName(){
 return this->name;
 }
 void setAge(int age){
 if(age>30 || age < 6){
 cout<<"not suitable for school!"<<endl;
 return;
 }
 this->age = age;
 }
 int getAge(){
 return this->age;
 }
 };
 
 int main(){
 Student *stu = new Student();
 stu->setName("Link");
 stu->setAge(18);
 cout<<stu->getName()<<" with age "<<stu->getAge()<<endl;
 return 0;
 }
 
 | 
首先我们可以看到,类Person为虚类,因为其没有实现任何一个函数,而是通过其子类Student实现的。
关于属性分布
其实class本身只是struct的一种衍生方式,所以使用了虚拟函数的class本身的结构是这样的:

| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | strcut class{func* funcptr    --------> +-----------------+
 |  func1 addresss |
 +-----------------+
 |  func2 addresss |
 +-----------------+
 ...
 type a;
 type b;
 ...
 };
 
 | 
而当我们使用new函数进行申请对象的时候,比如如下
| 1
 | Person *p = new Person();
 | 
实际上是发生了这样的事情:
| 12
 3
 
 | Person *p = (Person*)malloc(sizeof(Person));
 Person::Person(p);
 
 | 
而在Person里面,会发生如下的操作:
| 12
 
 | *p = func_ptr其他属性进行初始化。
 
 | 
关于虚表(Virtual table)
首先复习一下什么叫做虚函数:
一个简单的复习
虚函数出现的目的是为了实现【多态】,也就是所谓的【父类指针指向子类函数】的功能,例如如下的函数
| 12
 3
 
 | void GiveFood(Animal animal, Food food){animal.eat(food);
 }
 
 | 
这个eat函数,不同的动物表现的不一样:
- 狗在吃的时候发出"Bow, Bow"
- 猫吃饭的时候发出"Miao, Miao"
- 鹦鹉在吃饭的时候支持我们输入数据
但是如果每个动物我们都用不同的函数去判断的话,那么整个过程就太冗长了。为了支持这个功能,C++提供了多态。也就是说对于所有的动物,他们共有的能力就是【吃饭】,那么为了能够调用这个【吃饭】的过程,我们可以不去考虑功能的细节,直接使用这个函数:
| 12
 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
 
 | #include <iostream>
 #include <string>
 
 using namespace std;
 
 class Animal
 {
 
 public:
 Animal(){
 name = "";
 };
 ~Animal(){
 
 };
 void eating(string food){
 cout<<"the "<<food<<"test well...."<<endl;
 };
 protected:
 string name;
 
 };
 
 class Dog :public Animal{
 
 public:
 void eating(string food){
 cout<<"Bow, Bow!"<<endl;
 }
 };
 
 class Cat :public Animal{
 
 public:
 void eating(string food){
 cout<<"Miao~"<<endl;
 }
 };
 
 class Parrot :public Animal{
 
 public:
 void eating(string food){
 string talking;
 cin>>talking;
 cout<<"I say"<<talking<<endl;
 }
 };
 
 void GiveFood(Animal *a, string food){
 a->eating(food);
 }
 
 int main(){
 Dog *doggy = new Dog();
 Cat *catty = new Cat();
 Parrot *parrot = new Parrot();
 GiveFood(doggy, "meat");
 GiveFood(catty, "fish");
 GiveFood(parrot, "bug");
 return 0;
 }
 
 | 
运行结果为:
| 12
 3
 4
 5
 
 | learnC++$ ./learnCpp_Poly.binBow, Bow!
 Miao~
 Emm..
 I sayEmm..
 
 | 
但是,如果我们不使用virtual关键字说明的话,此时得到的结果就是完全不一样的内容:
| 12
 3
 4
 
 | learnC++$ ./learnCpp_Poly.binthe meattest well....
 the fishtest well....
 the bugtest well....
 
 | 
可以看到,这个时候调用的是Anima本身的eating函数,而不是相应的子类函数。
为什么会导致这样的情况
我们这次用一个比较段的程序来说明这个事情
| 12
 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
 
 | class Person{
 
 public:
 Person(){
 age = 0;
 name = "";
 };
 ~Person(){
 
 };
 void setName(string name){
 cout<<"this man call "<<name<<endl;
 };
 virtual void setAge(int age){
 cout<<"person age is "<<age<<endl;
 };
 protected:
 int age;
 string name;
 
 };
 
 class Student :public Person{
 
 public:
 Student(){
 age = 17;
 }
 void setName(string name){
 this->name = name;
 }
 string getName(){
 return this->name;
 }
 void setAge(int age){
 if(age>30 || age < 6){
 cout<<"not suitable for school!"<<endl;
 return;
 }
 this->age = age;
 }
 int getAge(){
 return this->age;
 }
 };
 
 | 
这个程序和我们最初的内容很像,但是区别在于,其中的setAge为虚函数,而setName不为虚函数。然后我们观察其二进制,可以发现一些有趣的现象:

C++为了能够支持重载,会将函数名字本身进行破坏后重组,所以这里函数的名字看起来怪怪的。这个数据段中放了在运行中的一些重要信息:
| 12
 3
 4
 5
 6
 7
 
 | .rodata:0000000000401220                 public _ZTV7Student ; weak.rodata:0000000000401220 ; `vtable for'Student
 .rodata:0000000000401220 _ZTV7Student    dq 0
 .rodata:0000000000401228                 dq offset _ZTI7Student  ; `typeinfo for'Student
 .rodata:0000000000401230 off_401230      dq offset _ZN7Student6setAgeEi
 .rodata:0000000000401230                                         ; DATA XREF: Student::Student(void)+1Co
 .rodata:0000000000401230                                         ; Student::setAge(int)
 
 | 
这段内容其实放的是Student类本身的【虚表】,也就是这个类中的虚类的基本信息
| 12
 3
 4
 5
 
 | +---------------------------------+|00401228   `typeinfo for Student | Student类本身的属性
 +---------------------------------+
 |00401230    Student6setAgeEi     | Student类的函数:setAge
 +---------------------------------+
 
 | 
| 12
 3
 4
 5
 
 | .rodata:0000000000401270                 public _ZTI7Student ; weak.rodata:0000000000401270 ; `typeinfo for'Student
 .rodata:0000000000401270 _ZTI7Student    dq offset unk_602270    ; DATA XREF: .rodata:0000000000401228o
 .rodata:0000000000401278                 dq offset _ZTS7Student  ; "7Student"
 .rodata:0000000000401280                 dq offset _ZTI6Person   ; `typeinfo for'Person
 
 | 
这一段则是介绍了Student这个类的基本属性。上面的typeinfo for Student所指向的地址就是这个里。这里记录了Student这个类的名字(_ZTS开头的表示的是字符串)以及其父类内容(ZTI表示的是对应的info)
观察了这么多,我们能够发现两个事情:
- Person和Student的setAge都被放在这个表格中,分别叫做_ZN6Person6setAgeEi和 _ZN7Student6setAgeEi
- 无论是Person还是Student的setName函数都不见了。
这个现象蛮有趣的,我们观察一下main函数的基本内容:
| 12
 3
 4
 
 | Student::setName(v10, &v9);std::string::~string((std::string *)&v9);
 std::allocator<char>::~allocator(&v8);
 (**(void (__fastcall ***)(Student *, signed __int64))v10)(v10, 18LL);
 
 | 
对比源码后不难发现,最后的这个函数指针就是函数setAge。也就是说,当调用setAge的时候,程序的逻辑其实上是:
| 1
 | Stu对象调用setAge --> 找到其虚函数表 --> 第一个内容为setAge --> 调用对象的第一个函数表中内容。
 | 
而在调用setName的时候,由于此时没有使用virtual关键字,导致此时setName被认为是父类和子类两个完全不同的函数,因此直接采用与普通的函数相同的处理方法,直接在.text中查找对应的函数。
使用VTable的理由存在
为了实现【多态】,那么势必是要在【运行中才能确定对应调用的函数】。比如我们之前喂动物的例子,不运行起来,GiveFood永远不知道此时的Animal本身对应的对象是哪一个对象,因此就需要再运行的时候再对其进行绑定,为了实现这个运行时绑定的效果,使用虚表就变得方便了很多。
虚类的特征 – VTable中的函数
之前我们虽然提到过虚类,但是没有给出过虚类对象在内存中存在的某个特征。在虚类中,本来要实现的函数的位置上会有一个叫做purecall的函数进行填充,结构如下:
| 12
 3
 4
 
 | .rdata:00007FF6F848A500 ??_7Character@@6B@ dq offset _purecall  ; DATA XREF: sub_7FF6F8482A50:loc_7FF6F8482A91↑o.rdata:00007FF6F848A500                                         ; sub_7FF6F8482B60+A↑o ...
 .rdata:00007FF6F848A508 dq offset _purecall
 .rdata:00007FF6F848A510 dq offset _purecall
 
 | 
这里就相当于说,Character对象有三个要实现的虚函数,这里先用purecall进行提前的填充。
How To Pwn
知道了上述的说法,我们就有了第一种在C++下可以进行pwn的手段。
首先,我们知道,用new申请的对象,首先要进行【获得当前vptr的过程】
| 12
 3
 4
 5
 
 |             +------------+obj_ptr --> |    vptr    | -------+
 +------------+        |      +---------+
 +----->| func1   |
 +---------+
 
 | 
也就是说,如果我们此时调用func1的时候,实际的调用过程是:
mov rax, QWORD PTR [rbp-0x18] ; 取出obj_ptr
mov rax, QWORD PTR [rax]      ; 取出vptr的地址
mov rax, QWORD PTR [rax]      ; 取出vptr中第一项,也就是func1的地址
call rax                      ; 跳转到func1上
此时obj_ptr指向的堆是这样的
| 12
 3
 4
 5
 
 | +---------------+| vtable address|----+      vtable
 +---------------+    |      +------------+
 |    attrib     |    +----->|    func1   |
 +---------------+           +------------+
 
 | 
那么,如果我们能够伪造一个vptr,然后将堆上的vptr addr改成我们指定的一个伪造的vptr,那么会变成:
| 12
 3
 4
 5
 6
 
 | +---------------+| fake address  |----+
 +---------------+    |          fake table
 |    attrib     |    |          +-----------+            +-------------+
 +---------------+    +--------->|&shellcode |----------->|  shellcode  |
 +-----------+            +-------------+
 
 | 
这样我们就能够实现pwn了!
without NX
由于一般的堆都是在读入数据的时候申请的,因此此时我们直接将fake address填入堆地址,堆的地址可以通过各种方法泄露出来。这样的话我们同时能够得到多个可以利用的堆
with NX
这种时候就只能考虑到使用一般的堆溢出攻击了。。。。
关于传参的细节
在windows系统下,使用vs进行代码编译之后,发现了一个特征:
在x86环境下,传参使用压栈的方式,越往右的参数越先压栈
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | func(pn....p3,p2,p1)
 +-------+   stack frame
 |  p1   |
 +-------+       |
 |  p2   |       |
 +-------+       |
 |  p3   |       |
 +-------+       |
 ...          |
 |  pn   |       V
 +-------+
 stack point
 
 | 
在x64环境下,传参的一部分在寄存器中进行传递,网上大部分查到的顺序是rdi,rsi,rcx,rdx,r8,r9,但是实际上,发现在windows下,使用vs编译后,传参的顺序变成了rcx,rdx,r8,r9:

关于常见的string类型
C++中的string类型使用的倒是很平常,但是其反汇编一般不好认。。。这里记录一下对string的第一感觉:
string类型再执行的时候,结构体本身如下:
| 12
 3
 4
 5
 6
 
 | 00000000 String          struc ; (sizeof=0x10, mappedto_54)00000000 ptr             dd ?
 00000004 size_           dd ?
 00000008 all_size        dd ?
 0000000C is_large        dd ?
 00000010 String          ends
 
 | 
简单介绍一下:
- ptr: 指向string分配空间的指针
- size_: 当前string中字符总大小
- all_size: 当前分配的字节是否达到了某个大小(待修正)
- is_large: 当前使用的字节空间超过了某个限度,此时需要重新分配空间
运行过程
ptr指向字符串的指针,然后这个指向的位置为一开始申请的大小,如果我们不断的insert新的数据的时候,系统会将当前ptr指向的堆块的前四个字节变成指针,然后这四个指针指向的位置为新申请的空间。如:
此时空间结构如下:
| 12
 3
 
 | ptr ------> +------------+|    123     |
 +------------+
 
 | 
如果此时我们插入了过多的字符串的话,其中的is_large参数就会被修改为1,此时如果还要发生insert等过程的话,ptr所指向的位置的前4个字节就会被置为下一处分配的地址,并且原先的数据也会移动过去
| 12
 3
 4
 5
 
 | ptr -------> +----------------+         +------------------+| ptr  |         |         |   123            |
 +----------------+         +------------------+
 |                       |
 +-----------------------+
 
 | 
总觉得这里有可以发生pwn的位置。。下次试试把