文章目录
- crakeme练习
- crackme1
- crackme2
- crackme3
- 解题步骤
- 总结
- 关键代码查找方法
- 常见代码
- C++类对象逆向分析
- C++虚函数逆向分析
- 系统dll文件的指令
- kernel32.dll、user32.dll、ntdll.dll文件
- TEB、PEB
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 eaxmov edx, show2; //将show2函数的地址存放到edx中mov [eax], edx; //将show2函数地址替换虚函数表中的第一个虚函数(即show函数)}if (Addr) VirtualProtect(Addr, 0x4, OldProtect, &OldProtect); // 将读写权限修改回来pObj->show();return 0;
}
系统dll文件的指令
系统dll文件加载的地址一般都是0x7xxxxxxx。
kernel32.dll、user32.dll、ntdll.dll文件
- kernel32.dll:所有进程无论窗口还是控制台程序,都会引用kernel32.dll
- user32.dll:窗口程序专用,封装了所有跟窗口操作相关的API
- ntdll.dll:无论是kernel32.dll还是user32.dll文件都会去调用ntdll.dll
TEB、PEB
-
TEB:线程环境块,说白了就是一个结构体,该结构体保存了线程中的各种信息
-
PEB:进程环境块,存放进程相关信息
结论:
- FS:TEB
- FS:[0x30] == PEB // 其中保存了LDR的结构体
- FS:[0x3c] == LDR // 其中保存了InitalizationOrderModuleList指针的结构体
- FS:[0x3c+0x1c] == InitalizationOrderModuleList //初始化排序的dll
指令:
mov esi, FS:[0x30] // PEB地址
mov esi, [esi+0xc] // LDR地址
mov esi, [esi+0x1c] // InitalizationOrderModulelist地址InitalizationOrderModulelist结构体:
struct _LIST_ENTRY{_LIST_ENTRY *Flink; // 下一个结构体指针_LIST_ENTRY *Blink; // 上一个结构体指针 }InitalizationOrderModulelist保存的dll文件顺序:第一个是ntdll.dll,第二个是kernel32.dll或者kernelbase.dll
mov esi, [esi] // Flink,第二个dll文件的信息
经过分析发现该函数是用来判断输入的字符串是否是在’0’和’9’之间的字符
进入查看这个函数的具体内容,发现以下代码段:



























