2020.03.30-2020.04.05

关于上周博客炸了的问题

原因是两个_config.yml(可能还有其他文件吧)里所有缩进都不见了…不知道是为啥,就甩给 vscode 和格式化代码插件吧(…)
然后,原来 .yml 要用缩进表示层级啊…
嗯顺便换了个主题

64 位软件逆向技术

虚函数

c++的三大核心机制是封装、继承、多态,虚函数是多态的一种体现。在逆向过程中,虚函数是一种还原面向对象代码的重要手段

虚表

不同的类虚表不同,相同的类对象共享一个虚表
(以下讲的是用 c++写的程序)
在构造函数中,首先初始化虚表指针,然后初始化数据成员,最后返回 this 指针

c++语法规定,析构函数需要调用虚函数的无多态性,因此析构函数首先需要赋值虚表
构造函数和析构函数特征一致,可根据调用的先后顺序确定
虚表地址在全局数据区中

序列号(注册码)保护方式

序列号保护机制

验证用户名和序列号之间的映射关系(…也有可能没有关系)
检查方法:

  1. 将用户名等信息通过变换后得到注册码
    序列号=F(用户名)
    这个方法计算出的序列号以明文形式在内存中出现
    也可通过修改比较指令的方法通过检查
    再现了生成注册码的过程,不安全
  2. 通过注册码验证用户名
    生成注册码时:序列号=F(用户名),检查注册码时:用户名=F^(-1)(序列号)
    生成注册码的函数和注册码明文未出现在软件代码中
    破解可考虑:1.修改比较指令,2.通过 F^(-1)找出 F
  3. 通过对等函数检查
    F1(用户名)=F2(序列号)
    与 2 类似
  4. 同时将用户名和序列号作为自变量
    特定值=F(用户名,序列号)
    可能失去了用户名和序列号的一一对应关系

攻击序列号保护机制

找到序列号或修改判断序列号后的跳转指令
跟踪程序启动时(需要将注册码读出并判断)或输入注册码,对 api 设置断点
常用:

  1. 将输入的内容复制到缓冲区: GetWindowTextA(W)、GetDlgItemTextA(W)、GetDlgItemInt

  2. 判断后显示的对话框:MessageBoxA(W)、MessageBoxExA(W)、ShowWindow、MessageBoxIndirectA(W)、CreateDialogParamA(W)、CreateDialogIndirectParamA(w)、DialogBoxParamA(W)、DialogBoxIndirectParamA(W)

  3. 启动时读取注册码:
    RegQueryValueExA(W)(序列号放在注册表);
    GetPrivateProfileStringA(W)、GetPrivateProfileIntA(W)、GetProfileIntA(W)、GetProfileStringA(W)(序列号放在 INI 文件中);
    CreateFileA(W)、_lopen()(放在一般文件)

数据约束性

只用在明文比较注册码的保护方式中使用。大多数情况下,真正的注册码会在某个时刻出现在内存中,一般会在用户输入的 ±90h。
例如,用 od 按’Alt+M’打开内存窗口,'Ctrl+B’打开搜索框,搜索输入的序列号,可在附近查找到真序列号

利用消息断点

按下和释放鼠标时会发送 WM_LBUTTONDOWN 和 WM_LUBTTONUP 消息,用这个消息下断点可以找到按钮的事件代码

利用提示信息

当输入错时提示“序列号错误,再来一次”等,可以查找相应的字符串,定位到相关代码
如 od 中,右键“search for”->“all referenced text string”

字符串比较形式

  1. 寄存器直接比较
  2. 函数比较
    比较内容放在寄存器或栈中
    call 一个用于比较的函数,可能是 api 函数或自己写的
1
2
3
call ....
test eax,eax
jz .... ;zf=1->eax=0 跳转
  1. 串比较
1
2
3
lea edi [   ] ;edi指向字符串a
lea esi [ ] ;....
repz cmpsd ;比较

edi、esi:变址寄存器,存放存储单元在段内的偏移量。
rep:按 ecx 中指定次数或在 zf 不满足条件前重复。
如果 ds:si 和 es:di 所指向的两个字节相等,则继续比较。REP(重复)、REPE(相等时重复)、REPNE(不相等时重复)、REPZ(为零时重复)及 REPNZ(不为零时重复)

CMPSB 比较字节 CMPSW 比较字 CMPSD 比较双字 ,方向标志位决定 ESI 和 EDI 的增加或减少

警告窗口

常用的方法是修改程序的资源、静态分析、动态分析
显示窗口的常用函数有 MessageBoxA(W)、MessageBoxExA(W)、DialogBoxParamA(W)、ShowWindow、CreateWindowExA(W)等,对某些警告窗口无效时可以尝试利用消息设置断点拦截

时间限制

计时器

对于限制每次运行时长的软件

  1. setTimer 函数
    应用程序在初始化时调用这个 api 函数,申请计时器并设定时间间隔,同时获得一个处理计时器超时的回调函数。若超时,系统会向申请的窗口发送 WM_TIMER 或调用那个回调函数。当程序不需要计时器,调用 KillTimer()进行销毁
  2. 高精度多媒体计时器
    调用 timeSetEvent()
  3. 其它
    timeGetTime()、GetTickCount(),返回的都是系统启动以来经历过的时间,函数的精度取决于系统的设置;也可以利用各高级语言开发库里的函数实现计时,如 c 语言里的 time()(返回 1970.01.01 0 时起至今的秒数)

精度太高会对系统性能造成影响,故一般不需要太高精度。

时间限制

试用期
在安装软件或主程序第一次运行时获得系统日期并记录。程序每次运行都要去的当前系统日期并与之前的记录比较
软件一般最少要保存两个时间值,一个是安装(运行)日期(最好存在多个地方),一个是软件最近一次运行的日期(防止用户修改机器日期)
用于获取时间的 api 函数有 GetSystemTime、GetLocalTime、GetFileTime,即使不直接使用这些函数,高级语言中封装的类也调用了这些函数。
还有一种方法是读取需要频繁修改的系统文件,利用 FileTimeToSystem()

面向对象(OOP)涉及到的几个名词

主要是因为加密与解密里涉及到了(如虚函数)但不懂是啥....

类(class)&对象

类是用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例
当我们定义一个 class 的时候,我们实际上就定义了一种数据类型。

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
class Box
{
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume()
{
return length * breadth * height;
}
int compare(Box box)
{
return this->Volume() > box.Volume();
// 每一个对象都能通过this 指针访问自己的地址
}
private: //类成员的属性,还可为private 或 protected
double length; // 盒子的长度
double breadth; // 盒子的宽度
double height; // 盒子的高度
};
Box Box1; // 声明对象 Box1,类型为 Box
Box Box2; // 声明对象 Box2,类型为 Box

构造函数:实现对象初始化
析构函数:释放对象占用的内存空间
类的作用:安全、继承

继承

面向对象程序设计中最重要的一个概念是继承。继承允许我们依据一个类来定义另一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类、父类或超类,新建的类称为派生类或子类。
继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物。
如果一个实例的数据类型是某个子类,那么它的数据类型也可以看作是父类

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
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
class PaintCost
{
public:
int getCost(int area)
{
return area * 70;
}
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
//多继承,即一个子类可以有多个父类,它继承了多个父类的特性
class Rectangle: public Shape, public PaintCost
{
public:
int getwhatever()
{
return (width * height *height);
}
};

多态

多态按字面的意思就是多种形态。存在的必要条件:继承、重写(子类对父类的方法做一定修改)、父类引用指向子类的对象
当子类和父类都存在相同的方法时,子类覆盖了父类的方法

对于一个变量,我们只需要知道它是 Animal 类型,无需确切地知道它的子类型,就可以放心地调用 run()方法,而具体调用的 run()方法是作用在 Animal、Dog、Cat 还是 Tortoise 对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种 Animal 的子类时,只要确保 run()方法编写正确,不用管原来的代码是如何调用的。继承和多态

虚函数

C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法。

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
class A
{
public:
virtual void foo()
//这里的foo()也可以啥都不输出,即virtual void foo();
//如果写为virtual void foo()=0 则为一个纯虚函数,仅提供一个接口,在继承时必须实现
{
cout<<"A::foo() is called"<<endl;
}
};
class B:public A
{
public:
virtual void foo()
{
cout<<"B::foo() is called"<<endl;
}
};
int main(void)
{
A *a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
return 0;
}

带有纯虚函数的类称为抽象类,只能作为基类,且不能定义对象(抽象类这边还涉及到了 abstract 和 virtual,但先不管了…)

虚函数表

编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针,这种数组成为虚函数表。即,每个类使用一个虚函数表,每个类对象用一个虚表指针。

封装

把数据和函数捆绑在一起。
通过创建类来进行封装和数据隐藏(public、protected、private)。默认情况下,类中定义的项目都是私有的,再提供对外 public 的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Adder{
public:
// 构造函数
Adder(int i = 0)
{
total = i;
}
// 对外的接口
void addNum(int number)
{
total += number;
}
// 对外的接口
int getTotal()
{
return total;
};
private:
// 对外隐藏的数据
int total;
};

python 爬虫学习

https://brubbish.github.io/710f8e5f.html#RE库的match对象