2020.03.30-2020.04.05
关于上周博客炸了的问题
原因是两个_config.yml(可能还有其他文件吧)里所有缩进都不见了…不知道是为啥,就甩给 vscode 和格式化代码插件吧(…)
然后,原来 .yml 要用缩进表示层级啊…
嗯顺便换了个主题
64 位软件逆向技术
虚函数
c++的三大核心机制是封装、继承、多态,虚函数是多态的一种体现。在逆向过程中,虚函数是一种还原面向对象代码的重要手段
虚表
不同的类虚表不同,相同的类对象共享一个虚表
(以下讲的是用 c++写的程序)
在构造函数中,首先初始化虚表指针,然后初始化数据成员,最后返回 this 指针
c++语法规定,析构函数需要调用虚函数的无多态性,因此析构函数首先需要赋值虚表
构造函数和析构函数特征一致,可根据调用的先后顺序确定
虚表地址在全局数据区中
序列号(注册码)保护方式
序列号保护机制
验证用户名和序列号之间的映射关系(…也有可能没有关系)
检查方法:
- 将用户名等信息通过变换后得到注册码
序列号=F(用户名)
这个方法计算出的序列号以明文形式在内存中出现
也可通过修改比较指令的方法通过检查
再现了生成注册码的过程,不安全 - 通过注册码验证用户名
生成注册码时:序列号=F(用户名),检查注册码时:用户名=F^(-1)(序列号)
生成注册码的函数和注册码明文未出现在软件代码中
破解可考虑:1.修改比较指令,2.通过 F^(-1)找出 F - 通过对等函数检查
F1(用户名)=F2(序列号)
与 2 类似 - 同时将用户名和序列号作为自变量
特定值=F(用户名,序列号)
可能失去了用户名和序列号的一一对应关系
攻击序列号保护机制
找到序列号或修改判断序列号后的跳转指令
跟踪程序启动时(需要将注册码读出并判断)或输入注册码,对 api 设置断点
常用:
-
将输入的内容复制到缓冲区: GetWindowTextA(W)、GetDlgItemTextA(W)、GetDlgItemInt
-
判断后显示的对话框:MessageBoxA(W)、MessageBoxExA(W)、ShowWindow、MessageBoxIndirectA(W)、CreateDialogParamA(W)、CreateDialogIndirectParamA(w)、DialogBoxParamA(W)、DialogBoxIndirectParamA(W)
-
启动时读取注册码:
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”
字符串比较形式
- 寄存器直接比较
- 函数比较
比较内容放在寄存器或栈中
call 一个用于比较的函数,可能是 api 函数或自己写的
1 | call .... |
- 串比较
1 | lea edi [ ] ;edi指向字符串a |
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)等,对某些警告窗口无效时可以尝试利用消息设置断点拦截
时间限制
计时器
对于限制每次运行时长的软件
- setTimer 函数
应用程序在初始化时调用这个 api 函数,申请计时器并设定时间间隔,同时获得一个处理计时器超时的回调函数。若超时,系统会向申请的窗口发送 WM_TIMER 或调用那个回调函数。当程序不需要计时器,调用 KillTimer()进行销毁 - 高精度多媒体计时器
调用 timeSetEvent() - 其它
timeGetTime()、GetTickCount(),返回的都是系统启动以来经历过的时间,函数的精度取决于系统的设置;也可以利用各高级语言开发库里的函数实现计时,如 c 语言里的 time()(返回 1970.01.01 0 时起至今的秒数)
精度太高会对系统性能造成影响,故一般不需要太高精度。
时间限制
试用期
在安装软件或主程序第一次运行时获得系统日期并记录。程序每次运行都要去的当前系统日期并与之前的记录比较
软件一般最少要保存两个时间值,一个是安装(运行)日期(最好存在多个地方),一个是软件最近一次运行的日期(防止用户修改机器日期)
用于获取时间的 api 函数有 GetSystemTime、GetLocalTime、GetFileTime,即使不直接使用这些函数,高级语言中封装的类也调用了这些函数。
还有一种方法是读取需要频繁修改的系统文件,利用 FileTimeToSystem()
面向对象(OOP)涉及到的几个名词
主要是因为加密与解密里涉及到了(如虚函数)但不懂是啥....
类(class)&对象
类是用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例
当我们定义一个 class 的时候,我们实际上就定义了一种数据类型。
1 | class Box |
构造函数:实现对象初始化
析构函数:释放对象占用的内存空间
类的作用:安全、继承
继承
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据一个类来定义另一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类、父类或超类,新建的类称为派生类或子类。
继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物。
如果一个实例的数据类型是某个子类,那么它的数据类型也可以看作是父类
1 | class Shape |
多态
多态按字面的意思就是多种形态。存在的必要条件:继承、重写(子类对父类的方法做一定修改)、父类引用指向子类的对象
当子类和父类都存在相同的方法时,子类覆盖了父类的方法
对于一个变量,我们只需要知道它是 Animal 类型,无需确切地知道它的子类型,就可以放心地调用 run()方法,而具体调用的 run()方法是作用在 Animal、Dog、Cat 还是 Tortoise 对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种 Animal 的子类时,只要确保 run()方法编写正确,不用管原来的代码是如何调用的。继承和多态
虚函数
C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法。
1 | class A |
带有纯虚函数的类称为抽象类,只能作为基类,且不能定义对象(抽象类这边还涉及到了 abstract 和 virtual,但先不管了…)
虚函数表
编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针,这种数组成为虚函数表。即,每个类使用一个虚函数表,每个类对象用一个虚表指针。
封装
把数据和函数捆绑在一起。
通过创建类来进行封装和数据隐藏(public、protected、private)。默认情况下,类中定义的项目都是私有的,再提供对外 public 的接口
1 | class Adder{ |