文章目录
- crakeme练习
- crackme1
- crackme2
- crackme3
- 解题步骤
- 总结
- 关键代码查找方法
- 常见代码
- C++类对象逆向分析
- C++虚函数逆向分析
crakeme练习
crackme1
学到的知识点:
- main函数查找方法:运行到EntryPoint -> 第一个call(一般在第三行) -> 第二个call(一般在第四行) -> 向下翻,找push,add,mov的三个连续call位置,选中间的call -> 找一群int3的位置上面的call。此时就进入了主函数。
- VS防止栈内存溢出的安全机制代码(一般放在函数开头):
push ebp mov ebp,esp sub esp,1C mov eax,dword ptr ds:[448004] xor eax,ebp mov dword ptr ss:[ebp-4],eax
- [ebp-xxx]:一般表示局部变量;[ebp+xxx]:一般表示函数参数
第一个参数:[ebp+0x8]
第二个参数:[ebp+0xC]
…
- add esp, xxx:一般在call函数调用完之后,用于堆栈平衡,xxx一般为【参数所占字节数】,可以根据xxx的大小判断函数传入的参数的个数
注意:xxx要用16进制
crackme2
学到的知识点:
- 可以根据程序运行的情况(关键API)来下断点,然后返回到上层调用,从而找到关键代码。
例如:bp MessageBoxA;或者在od上按ctrl+G
crackme3
思路:
- 按照常用思路找到主函数
- 发现第一个关键函数:
经过分析发现该函数是用来判断输入的字符串是否是在’0’和’9’之间的字符- 经过第一个关键函数,如果输入了0-9之外的字符,那么eax就会被设置为0,从而直接跳转到失败。如果输入的都是0-9之间的字符串,那么eax就会被设置为1,继续执行下面的代码。
- 发现第二个关键函数:
进入查看这个函数的具体内容,发现以下代码段:
经过分析发现这段代码是将输入的字符当作数字依次与5异或,得出的结果为"16716724"。- 这样就明确了思路,需要输入一段数字,使得每个数字与5异或之后,要和"16716724"这段数字对应相等:
写出如下python脚本:xor_string = '16716724' xor_num = 5 int_list = [int(i) for i in xor_string] result_str = '' for num in int_list: result_str += str(num ^ xor_num) print(result_str)
- 得到
payload=43243271
解题步骤
- 运行程序,观察程序功能
- 明确目标
- 找到关键代码的位置:
- 根据字符串查找
- 根据程序运行时的特征,比如程序运行时弹出了一个MessageBoxA窗口,那么就可以在所有的MessageBoxA位置下断点:bp MessageBoxA。然后返回到上层调用,从而找到关键代码。
总结
关键代码查找方法
- main函数查找方法:运行到EntryPoint -> 第一个call(一般在第三行) -> 第二个call(一般在第四行) -> 向下翻,找push,add,mov的三个call位置,选中间的call -> 找一群int3的位置上面的call。此时就进入了主函数。
- 可以根据程序运行的情况(关键API)来下断点,然后返回到上层调用,从而找到关键代码。
例如:bp MessageBoxA;或者在od上按ctrl+G
常见代码
- VS防止栈内存溢出的安全机制代码(一般放在函数开头):
push ebp mov ebp,esp sub esp,1C mov eax,dword ptr ds:[448004] xor eax,ebp mov dword ptr ss:[ebp-4],eax
- [ebp-xxx]:一般表示局部变量;[ebp+xxx]:一般表示函数参数
第一个参数:[ebp+0x8]
第二个参数:[ebp+0xC]
…
- add esp, xxx:一般在call函数调用完之后,用于堆栈平衡,xxx一般为【参数所占字节数】,可以根据xxx的大小判断函数传入的参数的个数
注意:xxx要用16进制
C++类对象逆向分析
#include<stdio.h>
class Base {
public:
Base() {
printf("Base::Base()\n");
}
};
class Child : public Base {
public:
Child() {
printf("Child::Child()\n");
}
};
int main() {
Child child;
return 0;
}
- this指针的构造
- 首先定义child,然后将child对象的地址放到ecx中
- 然后进入Child类中,将ecx的内容放到this指针中
- 子类中调用父类构造函数的方法
子类会在自己的构造函数内部添加一段call父类构造函数的代码,添加该代码的位置为子类构造函数中的所有命令前
#include<stdio.h>
class Base {
public:
~Base() {
printf("Base::~Base()\n");
}
};
class Child : public Base {
public:
~Child() {
printf("Child::~Child()\n");
}
};
int main() {
Child child;
return 0;
}
- 子类中调用父类析构函数的方法
子类会在自己的析构函数内部添加一段call父类析构函数的代码,添加该代码的位置为子类析构函数中的所有命令后
- 当编译器认为构造函数是不必要(没有执行指令)的时候,它是不会创建构造函数的
#include<stdio.h>
class CObj {
private:
int a = 1;
int b = 2;
public:
void set(int n1, int n2) {
a = n1;
b = n2;
printf("set(int a, int b)\n");
}
};
int main() {
CObj obj;
obj.set(20, 30);
return 0;
}
- 类中成员变量及其赋值方法
- 首先将obj的地址通过ecx赋值给this指针([this]),然后在传给eax
- 然后把1赋值给[eax],即源代码中的
int a=1
- 再把2赋值给[eax+4],即源代码中的
int b=2
- 调用成员函数修改成员变量
- 将参数传入
- 执行成员函数,首先将[this](对象的地址)赋值给eax
- 再将参数n1赋值给ecx
- 然后将ecx放到[eax]中,即对象的内存中
b = n2
同理
总结:
- 构造函数调用过程:先调用子类构造函数,进入子类构造函数内部,先去调用父类的构造函数,之后再去执行自己的代码
- 析构函数调用过程,先调用子类析构函数,进入子类析构函数内部,先去执行自己的代码,之后再去调用父类的析构函数
- 函数成员变量的访问:都是通过this指针+偏移的形式去访问。调用成员函数的时候,编译器会默认传this指针,放到我们的ecx寄存器。
C++虚函数逆向分析
#include <stdio.h>
#include <Windows.h>
class CObj
{
public:
int a = 1;
virtual void show()
{
printf("虚函数show\n");
}
void fun1() {
printf("成员函数fun1\n");
}
};
void show2()
{
printf("外部函数show2\n");
}
int main() {
CObj obj;
CObj* pObj = &obj;
pObj->show();
return 0;
}
虚函数会在对象的首地址(低地址)存放一个虚函数表指针(虚函数表用于存放所有的虚函数地址),该指针存放的是虚函数表的首地址。
- 执行虚函数的过程
- 将[pObj]的值(即Obj对象的地址)存放到eax中
- 去除Obj对象的前四个字节放到edx中,即将虚函数表指针放到edx中
- 将[edx]的值(虚函数表的前四个字节,即第一个虚函数的地址)存放到eax中
- call eax,即调用虚函数
- 根据虚函数调用的原理,将外部函数show2来替换虚函数show,即要使得
pObj->show()
调用show2函数。修改代码如下:
#include <stdio.h>
#include <Windows.h>
class CObj
{
public:
int a = 1;
virtual void show()
{
printf("虚函数show\n");
}
void fun1() {
printf("成员函数fun1\n");
}
};
void show2()
{
printf("外部函数show2\n");
}
int main()
{
DWORD OldProtect = 0;
LPVOID Addr = 0;
CObj obj;
CObj* pObj = &obj;
// 获取虚函数表
__asm
{
mov eax, dword ptr[pObj]; //取对象首地址
mov eax, [eax]; //取虚函数表指针,放到eax中
mov Addr, eax; // Addr存放虚函数表指针
push eax; //保存eax,防止VirtualProtect改变eax
}
if (Addr) VirtualProtect(Addr, 0x4, PAGE_READWRITE, &OldProtect);// 修改[eax]的读写权限,使得[eax]的内容可以更改
__asm
{
pop eax
mov edx, show2; //将show2函数的地址存放到edx中
mov [eax], edx; //将show2函数地址替换虚函数表中的第一个虚函数(即show函数)
}
if (Addr) VirtualProtect(Addr, 0x4, OldProtect, &OldProtect); // 将读写权限修改回来
pObj->show();
return 0;
}