notion链接:https://rainbow-isthmus-f65.notion.site/FLIRT-567adf90e1ef4eba9ec95050195fa34b
C++编写socket服务
C++面向对象
class与struct的区别
realloc函数探究
SWPU2019wp
题目地址:https://files.buuoj.cn/files/e9761f09f9aac733705d36db1305f015/attachment.exe

使用file查看程序,是32的PE可执行文件,拖进IDA
这是main函数的伪代码:
如果第一次反编译的伪代码很奇怪,可以试着用IDA跑一遍程序,再次反编译的代码会清晰很多
main函数的逻辑很清晰,先是将输入与一个字符串循环异或,然后将异或后的结果传入encrypt函数,encrypt函数返回后将加密后的数据与比较数据进行比较
所以接下来的重点就是分析encrypt函数
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
这是encrypt函数的伪代码,远不如main函数清晰,IDA对于函数参数的识别也不尽如人意,勉强可以看出text(即等待加密的数据)被传入了sub_82270这个函数,进入这个函数
1 | void __cdecl encrypt(int a1, int a2, int a3, unsigned int a4, unsigned int a5) |
sub_82270函数的伪代码,更是令人费解,甚至出现了或运算,这个运算一般是不可逆的!
大致看了一下也不像是什么常见的加密算法
1 | unsigned int __fastcall sub_82270(unsigned __int8 *a1, unsigned __int8 *a2) |
像这种看起来很复杂,又不是常见的加密算法的,我们其实只需要关注数据的变化,使用内存硬件断点就可以达到这个目的

先把程序跑起来,输入一串字符,最好有规律一点,这个题目会在输入后判断字符的长度是否为32,所以我们输入的长度要是32

在这个循环中,将输入与一个常量字符串“SWPU_2019_CTF”循环异或

异或后的数据保存在这里,第一个字符b是已经异或过的结果,在b的位置下一个硬件断点,按F2即可

勾选上Hardware,因为是数据,所以下面的复选框中我们勾选Read和Write即可
然后F9运行程序,断在了这里


查看text的内存发现这里的操作是将异或后的数据拷贝到text中,所以在text的内存位置我们也要下一个硬件断点,之前下的那个硬件断点删不删都行
再次F9运行程序,断在了这里

这个循环将[eax-4]中的数据与ecx进行异或,查看内存发现[eax-4]的数据就是text的数据

所以这个循环就是将text异或,然后存入[ebx+eax-4]的地址,在这里我们继续下一个硬件断点

然后F9运行,程序断在这里:

这里已经是最后比较数据的地方了,所以其实输出的数据只是经过了两次异或加密,那些复杂的算式可能只是为了产生异或的数据,有了这个猜测,我们可以在异或的地址处下断点,检查不同的输入是否会产生相同的异或数据
第一次异或的数据是一个常量字符串,我们不用关心,第二次异或的数据我们倒是不清楚,所以在这里下一个断点

然后运行两次,输入不同的数据


查看两次运行的内存发现是一样的,所以异或的数据与我们的输入无关
我们把异或的数据提取出来
[0x86, 0x0C, 0x3E, 0xCA, 0x98, 0xD7, 0xAE, 0x19, 0xE2, 0x77, 0x6B, 0xA6, 0x6A, 0xA1, 0x77, 0xB0, 0x69, 0x91, 0x37, 0x05, 0x7A, 0xF9, 0x7B, 0x30, 0x43, 0x5A, 0x4B, 0x10, 0x86, 0x7D, 0xD4, 0x28]
第一次异或的数据是常量字符串“SWPU_2019_CTF”
比较的数据是:
[0xB3, 0x37, 0x0F, 0xF8, 0xBC, 0xBC, 0xAE, 0x5D, 0xBA, 0x5A, 0x4D, 0x86, 0x44, 0x97, 0x62, 0xD3, 0x4F, 0xBA, 0x24, 0x16, 0x0B, 0x9F, 0x72, 0x1A, 0x65, 0x68, 0x6D, 0x26, 0xBA, 0x6B, 0xC8, 0x67]
解密脚本:
1 | cmp = [0xB3, 0x37, 0x0F, 0xF8, 0xBC, 0xBC, 0xAE, 0x5D, 0xBA, 0x5A, 0x4D, 0x86, 0x44, 0x97, 0x62, 0xD3, 0x4F, 0xBA, 0x24, 0x16, 0x0B, 0x9F, 0x72, 0x1A, 0x65, 0x68, 0x6D, 0x26, 0xBA, 0x6B, 0xC8, 0x67] |

小结
当遇到加密函数很复杂而且不是常见的加密算法时,我们可以先使用硬件断点将输入的变化过程分析出来,这可以避免去分析一些与外部变量无关的代码,大大节省解题时间
C++逆向分析
识别C++程序
C++程序的反汇编代码会呈现一些特殊之处,通过这些特征我们可以识别一个二进制文件是否是由C++编写
频繁使用ecx寄存器(this指针)
在函数调用之前,ecx被赋值为this指针

如果一个函数在使用ecx之前没有初始化他,这个函数可能是一个类成员函数

调用约定
如果一个函数调用完成后,eax被赋值给ecx,紧接着调用另一个函数
前一个函数可能是在实例化一个类,后一个函数可能是该类的构造函数

在C++程序中,虚函数的调用往往是隐式的

STL代码和导入的DLL
在IDA的import窗口可以查看程序导入的DLL,根据名称判断是否是C++程序
STL函数的调用:

类实例布局
一般的类
在C++中实例化一个类:

这个类在内存中的布局是这样的:

为了对齐四字节的地址,最后一个变量的结尾会被填充一个三字节的垃圾数据
含有虚函数的类
如果一个类中包含有虚函数

这个类在内存中的布局会是这样

在布局的最前方会加入一个vfptr,这里保存虚函数表的地址,这个地址指向的内存记录了每个虚函数的地址

单继承的类
如果一个类单继承另一个类

这个类在内存中的布局会是这样

仅仅是将派生类的成员变量添加在父类的后面
多继承的类


这个类在内存中的布局:


与单继承大同小异,子类的成员变量添加在父类后面
另外,子类的虚函数会添加到第一个父类的虚函数表中
识别类
一道VM入门题
玩逆向半年了,还是菜的发慌,本来想RE和PWN双修,无奈发现精力跟不上,目前的想法是先把RE该学的学了,有空的话PWN的常规知识也要学一下,实话实说PWN有点难,学起来不如RE快
寒假在家闲,最近开始接触虚拟机逆向,从一道简单的虚拟机逆向开始
ida打开程序,在函数窗口搜索main,发现没有main函数,但是在invoke_main中发现了_main,跳过去发现是main函数里面有花指令,修一下就好

nop掉一个花指令,就可以反编译了

main函数伪代码:
这是我修改过后的伪代码,对一些变量的名称进行了修改,使伪代码更加易懂
最外层是一个死循环,死循环中嵌套了一个小的for循环,for循环的作用是读取opcode与option_index进行比较,如果比较相同的话就退出for循环
显然如果循环退出,那么接下来的option[i]调用的函数必定是opcode[vm_eip]对应的函数
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
所以题目的关键就在于opcode以及其对应的option,我们进入option一个一个分析
这里是我分析好的option,随便挑几个说一下

vm_xor_reg
1 | int vm_xor_reg() |
vm_eip不用解释了吧,这是程序的计数器,在调试的时候可以推测出来
那first和second又是什么呢?
先看下面,有一条指令是vm_eip += 3,由此可以推断出一条opcode的长度是三个字节
这些就是opcode,他们的组成是:option_index | first | second
- option_index:对应的option的编号
- first:前一个操作数
- second:后一个操作数

所以first[vm_eip]就相当于取前一个操作数,second同理
reg是一个数组,我们在这里可以把它看成是寄存器数组
变量都解释清楚了,那么这个函数的行为就显而易见了:
以first和second为下标取两个寄存器的值异或,并把结果放到第一个寄存器中
vm_set_zflag
1 | void vm_set_zflag() |
其他的都解释过了,这个vm_zflag是怎么来的呢?
我们查看vm_zflag的交叉引用,发现他出现在另一个函数中,在这个函数中,vm_zflag被当成了跳转的条件,所以推测这个变量是zflag,用两个数相减来得到zflag的值也合乎情理
vm_jz
1 | int vm_jz() |
vm_push_reg
1 | void vm_push_reg() |
stack_base也是根据调试推断出来的,有了这个名称,这个函数很容易就可以理解为push_reg
分析这些option的时候,前面会很难懂,但是当你理解了一些变量代表的含义并赋予他们恰当的名字,后面的分析就可以变得很快
一些变量的初始化:
1 | void *sub_4F14F0() |
现在,我们要做的就是将opcode翻译成这些option,编写一个翻译脚本
1 | opcode = [0x01, 0x03, 0x03, 0x05, 0x00, 0x00, 0x11, 0x00, 0x00, 0x01, 0x01, 0x11, 0x0C, 0x00, 0x01, 0x0D, 0x0A, 0x00, 0x01, 0x03, 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0x02, 0x00, 0x01, 0x00, 0x11, 0x0C, 0x00, 0x02, 0x0D, 0x2B, 0x00, 0x14, 0x00, 0x02, 0x01, 0x01, 0x61, 0x0C, 0x00, 0x01, 0x10, 0x1A, 0x00, 0x01, 0x01, 0x7A, 0x0C, 0x00, 0x01, 0x0F, 0x1A, 0x00, 0x01, 0x01, 0x47, 0x0A, 0x00, 0x01, 0x01, 0x01, 0x01, 0x06, 0x00, 0x01, 0x0B, 0x24, 0x00, 0x01, 0x01, 0x41, 0x0C, 0x00, 0x01, 0x10, 0x24, 0x00, 0x01, 0x01, 0x5A, 0x0C, 0x00, 0x01, 0x0F, 0x24, 0x00, 0x01, 0x01, 0x4B, 0x0A, 0x00, 0x01, 0x01, 0x01, 0x01, 0x07, 0x00, 0x01, 0x01, 0x01, 0x10, 0x09, 0x00, 0x01, 0x03, 0x01, 0x00, 0x03, 0x00, 0x00, 0x01, 0x01, 0x01, 0x06, 0x02, 0x01, 0x0B, 0x0B, 0x00, 0x02, 0x07, 0x00, 0x02, 0x0D, 0x00, 0x02, 0x00, 0x00, 0x02, 0x05, 0x00, 0x02, 0x01, 0x00, 0x02, 0x0C, 0x00, 0x02, 0x01, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x0D, 0x00, 0x02, 0x05, 0x00, 0x02, 0x0F, 0x00, 0x02, 0x00, 0x00, 0x02, 0x09, 0x00, 0x02, 0x05, 0x00, 0x02, 0x0F, 0x00, 0x02, 0x03, 0x00, 0x02, 0x00, 0x00, 0x02, 0x02, 0x00, 0x02, 0x05, 0x00, 0x02, 0x03, 0x00, 0x02, 0x03, 0x00, 0x02, 0x01, 0x00, 0x02, 0x07, 0x00, 0x02, 0x07, 0x00, 0x02, 0x0B, 0x00, 0x02, 0x02, 0x00, 0x02, 0x01, 0x00, 0x02, 0x02, 0x00, 0x02, 0x07, 0x00, 0x02, 0x02, 0x00, 0x02, 0x0C, 0x00, 0x02, 0x02, 0x00, 0x02, 0x02, 0x00, 0x01, 0x02, 0x01, 0x13, 0x01, 0x02, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x01, 0x0E, 0x5B, 0x00, 0x01, 0x01, 0x22, 0x0C, 0x02, 0x01, 0x0D, 0x59, 0x00, 0x01, 0x01, 0x01, 0x06, 0x02, 0x01, 0x0B, 0x4E, 0x00, 0x01, 0x03, 0x00, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0x03, 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00] |
以下是翻译脚本的输出,我分析后添加了一些注释
1 | 01: mov reg[3], 3 |
根据上面代码的逻辑写出模拟的python代码
1 | compare_data = [2, 2, 12, 2, 7, 2, 1, 2, 11, 7, 7, 1, 3, 3, 5, 2, 0, 3, 15, 5, 9, 0, 15, 5, 13, 0, 0, 1, 12, 1, 5, 0, 13, 7] |
然后再写出解密脚本:
1 | compare_data = [2, 2, 12, 2, 7, 2, 1, 2, 11, 7, 7, 1, 3, 3, 5, 2, 0, 3, 15, 5, 9, 0, 15, 5, 13, 0, 0, 1, 12, 1, 5, 0, 13, 7] |
1 | yiqiu@LAPTOP-I2IQS5DP ~/C/ezvm> python3 sim.py |
总结
做完这道题,我对VM逆向有个大概的了解,VM的出题思路大致就是:编写一个指令集,设计一串opcode,实现一个将opcode和指令集联系起来的解释器
所以我们的做题思路就是:逆向指令集,搞清楚每个操作对应的行为,提取opcode,写出一个小的反汇编器,将opcode反汇编成能看懂的文本格式,逆向这个文本格式的代码,写出解题脚本
参考
参考了SYJ学长的文章,附件也是在原文中下载
CSAPP第七章
7.1、编译器驱动程序
编译器驱动程序可以理解为一个软件包,像linux中常用的gcc,它可以自动完成程序的预处理、编译、汇编、链接
先看两个示例程序:
main.c
1 | #include<stdio.h> |
sum.c
1 | int sum(int *a, int n) |
在linux环境下使用命令
1 | $ gcc -Og -o prog main.c sum.c |
运行prog,程序返回3
1 | yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> ./prog |
prog的生成过程如图所示:

在这里gcc就是一个编译器驱动程序,它自动调用了cpp、cc1、as、ld等程序
7.2、静态链接
由ld完成,链接器主要做两件事:
- 符号解析
- 重定位
7.3、目标文件
目标文件有三种类型:
- 可重定位目标文件,一般以.o结尾
- 可执行文件,一般以,out结尾
- 共享目标文件,一般以.so结尾
可重定位和共享目标文件由编译器和汇编器生成,可执行目标文件由链接器生成
7.4、可重定位目标文件

一个典型的ELF可重定位目标文件格式如图所示
- ELF头:包含可重定位目标文件的大概信息
- 节头部表:包含可重定位目标文件的各个节的信息
- .text段:代码段
- .rodata段:只读数据段
- .data段:数据段,存放初始化的全局变量和静态变量
- .bss段:数据段,存放未初始化和初始化为零的全局变量和静态变量
- .symtab段:符号表,保存全局变量的符号以及函数名称
- .rel.text段:代码段的重定位信息
- .rel.data段:数据段的重定位信息
- .debug段:调试信息
- .line段:源代码和机器代码的行号映射关系
- .strtab段:字符串表
7.5、符号和符号表
在链接器的上下文中有三种不同的符号:
- 由本模块定义并能由其他模块引用的全局符号
- 由其他模块定义并能被本模块引用的全局符号
- 只被本模块定义和引用的局部符号
static前缀代表该变量是私有的,如果在多个函数中定义同名的static变量,编译器会为其生成不同的符号

- name:表示在符号表中的下标
- type:函数或者数据
- binding:本地或者全局
- value:相对于本节区头的偏移
符号表中的每个条目的组成如图所示,使用 readelf -s 可以查看我们之前生成的prog程序的符号表
1 | yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7 [1]> readelf -s prog |
COMMON和.bss的区别:

7.6、符号解析
符号解析是链接器的工作,编译器为程序生成符号,链接器通过解析这些符号来确定程序中的引用
对于局部符号,编译器直接生成与原来一样的符号,但是对于全局符号,这个过程就比较复杂,因为在全局符号中可能存在一样的符号
C++中的函数重载就涉及这个问题,两个函数有一样的函数名,但确实是不同的函数,C++的编译器使用了一种叫做符号修饰的方法

7.6.1、链接器如何解析多重定义的全局符号
编译器输出的符号分为强符号和弱符号
函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号
当一个程序中存在多重定义的全局符号时,链接器遵循以下原则:

7.6.2、与静态库链接
静态库相当于一个模块的集合,使用静态链接是,链接器会将程序中引用的模块从静态库中链接出来,而程序中没有引用的模块,链接器则不做处理
addvec.c
1 | int addcnt = 0; |
multvec.c
1 | int multcnt = 0; |
使用ar将这两个模块合并到一个静态库中:
1 | yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> gcc -c addvec.c multvec.c |
使用nm查看libvector.a中的符号:
1 | yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> nm libvector.a |
vector.h
1 | void addvec(int *x, int *y, int *z, int n); |
在main函数中引用addvec函数,最后静态链接起来
main2.c
1 | #include<stdio.h> |
1 | yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> gcc -c main2.c |
链接过程中,gcc只会将addvec模块链接进程序,而不会理会multvec,这大大减小了程序占用的磁盘空间和运行时的内存
prog2的生成过程:

7.6.2、链接器如何使用静态库来解析引用
需要注意的是:使用编译器驱动程序生成可执行程序时,命令行中文件的顺序是有要求的,符号的引用必须在定义之前
假设main.c引用了libx.a存档中的符号,那么在命令行中,main.c和libx.c的顺序必须是main.c在前,否则会产生报错
7.7、重定位
重定位分为两步:
- 重定位节和符号定义
- 重定位节中的符号引用
7.7.1、重定位条目
重定位条目是由汇编器生成的,指导链接器完成符号引用的重定位,.text的重定位条目位于.rel.text

重定位条目中的每个元素如图所示
下图是elf文件中最基本的两种重定位类型,第一种可以理解为偏移地址重定位,第二种可以理解为绝对地址重定位

7.7.2、重定位符号引用
重定位算法如图所示:
遍历节区和节区中的重定位条目,然后计算重定位符号引用的地址,然后根据重定位类型进入不同的if语句块

7.8、可执行目标文件

可执行目标文件被映射到内存中时,相同权限的节区会被映射到一个段,上图的文件在内存中的段布局:
一个只读段,一个可读可写段

7.9、加载可执行目标文件
可执行文件加载到内存后,会在内存中生成一个映像,这个映像中不仅有可执行文件的副本,还有诸如堆、栈等运行时所需结构
以及共享库和内核的代码,加载的过程由加载器完成,它将可执行文件的内容复制到内存中并将控制权转交给程序

7.10、动态链接共享库
还记得我们之前使用过的示例程序吗?
这一次我们让其使用共享库
1 | yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> gcc -shared -fpic -o libcvector.so addvec.c multvec.c |
程序的构建和运行过程如图所示:

相比于静态链接,动态链接更节省空间和内存,动态链接库一直保存在内存中,当有程序应用共享库中的模块时,它可以在自己的虚拟空间创建共享库的副本,程序结束后,这个副本也随之消失,就是说内存中只需要保留一份共享库代码就i可以供所有程序使用
如图所示,在形成可执行目标文件时,并不会有libc.so和libvector.so的代码被复制到可执行文件中,链接器只是重定位main2.o中的符号引用
在程序运行时,动态链接器将libc.so和libvector.so的代码复制到本进程的虚拟空间,然后重定位可执行文件中的符号引用,最后将控制权转交给程序

7.11、从应用程序中加载和链接共享库
C和C++提供了几个函数,可以在程序运行过程中加载和链接共享库,并在使用完之后卸载




示例程序:
prog2r.c
1 | #include<stdio.h> |
1 | yiqiu@LAPTOP-I2IQS5DP ~/C/csapp [1]> gcc -rdynamic -o prog2r.out prog2r.c -ldl |
java调用本地C/C++函数的实现方法:

7.12、位置无关代码
位置无关代码的作用是允许多个进程使用一个共享库代码的副本而不造成内存浪费,在gcc编译时使用-fpic选项指定生成位置无关代码
1、PIC数据引用
在共享库中,数据和代码的之间偏移是固定的,共享库中的代码被动态连接到内存中时,动态链接器还会根据一个全局偏移量表(GOT)来重定位代码中的数据引用

2、PIC函数调用

PIC函数调用通过GOT表、PLT表和延迟绑定机制来实现
函数没有被调用前,GOT表中的地址并不是函数的真正地址,而是对应PIL表中的第二条代码,函数第一次被调用时,经过一系列操作,会将函数的真实地址写入GOT表,以后调用时就可知直接跳转到函数的地址了
第一次调用addvec
- call指令调用函数
- 跳转到PLT表
- 跳转到GOT表指定的位置,即PLT表中的下一条指令
- pushq压入addvec函数的序号
- 跳转到PLT[0]
- 调用动态加载器
- 执行函数并将函数的地址写入GOT表
第二次调用addvec
- call指令调用函数
- 跳转到PLT表
- 跳转到GOT表指定的位置,即函数位置
7.13、库打桩机制
按书中的描述,打桩就是hook,将库函数更换为你自己编写的函数
7.13.1、编译时打桩
示例代码:
int.c
1 | #include<stdio.h> |
malloc.h
1 | #define malloc(size) mymalloc(size) |
mymalloc.c
1 | #ifdef COMPILETIME |
1 | yiqiu@LAPTOP-I2IQS5DP ~/C/csapp [1]> gcc -DCOMPILETIME -c mymalloc.c |
关键代码在malloc.h中,将malloc函数替换为mymalloc函数,将free函数替换为myfree函数
编译后,调用malloc函数就相当于调用mymalloc函数,free函数同理
7.13.2、链接时打桩
使用–wrap f将f替换为__wrap_f,而__real_f会被替换为f
修改mymalloc.c代码:
1 | #ifdef LINKTIME |
1 | yiqiu@LAPTOP-I2IQS5DP ~/C/csapp> gcc -DLINKTIME -c mymalloc.c |
7.13.3、运行时打桩
编译时打桩需要源代码,链接时打桩需要目标文件,但是运行时打桩只需要可执行文件
LD_PRELOAD环境变量可以设置可执行程序优先加载的共享库
修改mymalloc.c的代码:
1 | #ifdef RUNTIME |
1 | yiqiu@LAPTOP-I2IQS5DP ~/C/csapp [SIGSEGV]> gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl |
7.14、处理目标文件的工具

ida逆向分析main之前的函数
我们常说程序的入口点是main函数
但是main函数真的是程序开始的地方吗?
答案是:不是,start函数才是,这篇blog旨在分析main函数进行之前,程序都做了哪些事
将示例程序拖进ida,查看start函数的伪代码
1 | void __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)(void)) |
可以看到:start函数内部调用了libc_start_main函数,这个函数有7个参数
我们只关心其中两个:
第一个参数是main函数的函数指针,有了这个参数,libc_start_main才能调用main函数
第四个参数是init函数的指针,init函数就是我们此次重点关注的对象
1 | void __fastcall init(unsigned int a1, __int64 a2, __int64 a3) |
init函数中首先调用了init_proc函数,这个函数里面又调用了gmon_start函数,这个函数用来生成profile文件,在逆向中我们不需要关注它
通常,在init_proc函数中还会调用全局构造函数_do_global_ctors_aux
这个函数的源代码如下:
他会循环调用所有全局构造函数
1 | __do_global_ctors_aux (void) |
1 | __int64 (**init_proc())(void) |
在init函数中,再往下就是一个循环,首先得到v4的值,然后如果i不等于v4,就依次调用funcs_1E69这个函数指针数组中的函数,这个数组里保存的是用户自定义的函数,这个例题中有两个自定义函数

第一个函数啥也没做,只是返回一个0
第二个函数的伪代码如下:
作用是检查程序是否处于调试环境,如果不是,就将”w0wy0ugot1t”这个字符串粘贴到aHappyhg4me位置
而这个位置保存的是main函数中一个加密算法的密钥
1 | unsigned __int64 sub_55DFEAA3DC27() |
在逆向过程中,这里存放的自定义通常是用来反调试的,建议每次都看看里面有没有什么东西
做完这些事,init函数返回到libc_start_main函数,接着就是由libc_start_main函数调用main函数,在逆向中,主要要注意的就是main函数之前的init函数
里面可能存放着一些关键代码