【逆向】逆向练习及相关总结

文章目录

  • crakeme练习
    • crackme1
    • crackme2
    • crackme3
  • 解题步骤
  • 总结
    • 关键代码查找方法
    • 常见代码
    • C++类对象逆向分析
    • C++虚函数逆向分析
    • 系统dll文件的指令
    • kernel32.dll、user32.dll、ntdll.dll文件
    • TEB、PEB

crakeme练习

crackme1

学到的知识点:

  1. main函数查找方法:运行到EntryPoint -> 第一个call(一般在第三行) -> 第二个call(一般在第四行) -> 向下翻,找push,add,mov的三个连续call位置,选中间的call -> 找一群int3的位置上面的call。此时就进入了主函数。
  2. 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
  1. [ebp-xxx]:一般表示局部变量;[ebp+xxx]:一般表示函数参数

第一个参数:[ebp+0x8]
第二个参数:[ebp+0xC]

  1. add esp, xxx:一般在call函数调用完之后,用于堆栈平衡,xxx一般为【参数所占字节数】,可以根据xxx的大小判断函数传入的参数的个数

注意:xxx要用16进制

crackme2

学到的知识点:

  1. 可以根据程序运行的情况(关键API)来下断点,然后返回到上层调用,从而找到关键代码。

例如:bp MessageBoxA;或者在od上按ctrl+G

crackme3

思路:

  1. 按照常用思路找到主函数
  2. 发现第一个关键函数:
    在这里插入图片描述经过分析发现该函数是用来判断输入的字符串是否是在’0’和’9’之间的字符
  3. 经过第一个关键函数,如果输入了0-9之外的字符,那么eax就会被设置为0,从而直接跳转到失败。如果输入的都是0-9之间的字符串,那么eax就会被设置为1,继续执行下面的代码。
  4. 发现第二个关键函数:
    在这里插入图片描述 进入查看这个函数的具体内容,发现以下代码段:
    在这里插入图片描述
    经过分析发现这段代码是将输入的字符当作数字依次与5异或,得出的结果为"16716724"。
  5. 这样就明确了思路,需要输入一段数字,使得每个数字与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)
  1. 得到payload=43243271

解题步骤

  1. 运行程序,观察程序功能
  2. 明确目标
  3. 找到关键代码的位置:
    • 根据字符串查找
    • 根据程序运行时的特征,比如程序运行时弹出了一个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
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指针的构造
    在这里插入图片描述
    在这里插入图片描述
  1. 首先定义child,然后将child对象的地址放到ecx中
  2. 然后进入Child类中,将ecx的内容放到this指针中
  • 子类中调用父类构造函数的方法
    在这里插入图片描述

子类会在自己的构造函数内部添加一段call父类构造函数的代码,添加该代码的位置为子类构造函数中的所有命令前

#include
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
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;
}
  • 类中成员变量及其赋值方法
    在这里插入图片描述
  1. 首先将obj的地址通过ecx赋值给this指针([this]),然后在传给eax
  2. 然后把1赋值给[eax],即源代码中的int a=1
  3. 再把2赋值给[eax+4],即源代码中的int b=2
  • 调用成员函数修改成员变量
    在这里插入图片描述
    在这里插入图片描述
  1. 将参数传入
  2. 执行成员函数,首先将[this](对象的地址)赋值给eax
  3. 再将参数n1赋值给ecx
  4. 然后将ecx放到[eax]中,即对象的内存中
  5. b = n2同理

总结:

  • 构造函数调用过程:先调用子类构造函数,进入子类构造函数内部,先去调用父类的构造函数,之后再去执行自己的代码
  • 析构函数调用过程,先调用子类析构函数,进入子类析构函数内部,先去执行自己的代码,之后再去调用父类的析构函数
  • 函数成员变量的访问:都是通过this指针+偏移的形式去访问。调用成员函数的时候,编译器会默认传this指针,放到我们的ecx寄存器。

C++虚函数逆向分析

#include 
#include 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;
}

虚函数会在对象的首地址(低地址)存放一个虚函数表指针(虚函数表用于存放所有的虚函数地址),该指针存放的是虚函数表的首地址。

  • 执行虚函数的过程
    在这里插入图片描述
  1. 将[pObj]的值(即Obj对象的地址)存放到eax中
  2. 去除Obj对象的前四个字节放到edx中,即将虚函数表指针放到edx中
  3. 将[edx]的值(虚函数表的前四个字节,即第一个虚函数的地址)存放到eax中
  4. call eax,即调用虚函数
  • 根据虚函数调用的原理,将外部函数show2来替换虚函数show,即要使得pObj->show()调用show2函数。修改代码如下:
#include 
#include 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文件的信息


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部