关于逆向工程
代码逆向工程,是逆向工程在软件领域的应用。使用RCE、RE、逆向工程等简称。
分析逆向工程的方法有静态分析法和动态分析法。
静态分析法:是在不执行代码文件情形下,对代码静态分析的方法。不执行代码,而是观察代码外部特征,获取文件类型、大小、PE头信息、导入导出API、内部字符串、是否运行时解压缩、注册信息、调试信息、数字证书等信息。使用反汇编工具查看内部代码、分析代码结构也属于静态分析。
动态分析法:程序文件执行过程中对代码进行动态分析。通过调试获得代码流,获得内存状态。通过动态分析法可以观察文件、注册表、网络等同时分析软件行为。常使用debugger分析程序的内部结构和动作原理。
逆向分析Hello World程序
HelloWorld代码在VS2010中写的。解决方案配置选择release,然后生成exe可执行文件。代码如下:
使用OD打开helloworld可执行文件。
左上为代码区域,左下为数据窗口区域,右上为寄存器窗口区域,右下为栈窗口区域。
入口点
调试器载入程序后,停止的地方即为HelloWorld.exe的起始地址。是一段EP(EntryPoint)代码,其中,Address为进程虚拟内存地址,Instruction为指令,Disassembled code为反汇编代码,comment为注释。EP是windows可执行文件的代码入口点,是执行文件最先执行代码的位置。
OllyDbg基本指令:
1,Restart(ctrl + F2):重新开始调试(终止正在调试的进程再次运行)
2,step into(F7):执行一句OP code,如果遇到调用指令则进入函数代码内部
3,Step over(F8):执行一句OP code,如果遇到调用指令仅执行函数本身,不跟随进入
4,Excute till Return(Ctrl + F9):一直在函数代码内部运行,直到遇到RETURN命令跳出函数。
5,Go to(Ctrl + G)移动到指定位置,用来查看内存或代码,运行时不可用
6,Execute till Cursor(F4)执行到光标位置,即直接转到调试地址
7,F2 设置或取消断点(BP)
8,F9 运行(如果设置断点则执行到断点处)
9,Preview CALL/JUMP address:enter,如果光标处有CALL、JMP等命令,则跟踪并显示相关地址(运行时不可用,简单查看函数时候有用)
通过步进调试,找到了main()函数,显示出来两个字符串参数。
#### 快速查找指定代码
1,逐条执行命令来查找需要查找的位置。仅适用于被调试的代码量不大、程序功能明确的情况。
2,字符串检索法:鼠标右键菜单 — Search for — All referenced text strings.VC++中,static字符串会被默认保存为Unicode码形式,static字符串是指在程序内部被硬编码的字符串。
在windows中,代码和数据所在区域是分开的。
3,API检索法:调用代码中设置断点
Windows编程中,如果向显示器显示内容,需要使用Win32 API向OS请求显示输出,一定会调用Win32 API。如果弹出消息窗口,会调用user32.MessageBoxW() API。如果能推测出代码使用的API,使用(search for - all intermodular calls)会很好用。
4,API检索法:在API代码中设置断点
鼠标右键 — search for — Name in all calls
OD不能为所有可执行文件列出API函数调用列表,使用压缩器、保护器工具对可执行文件进行压缩或保护后,文件结构就会改变。此时OD无法列API调用列表。此时可以用直接向DLL代码库添加断点。DLL代码库存在C:\Windows\system32文件夹中。
方法:首先使用OD菜单栏选择View-Memory菜单,然后找到加载库(例子中为User32),使用OD的Name in all modules命令列出被加载的DLL提供所有API,找出MessageBoxW后,光标会定位到MessageBoxW。双击打开代码,按F2设置断点。然后按F9继续执行。即可。
使用打补丁方式修改HelloWorld字符串
1,直接修改字符串缓冲区(buffer)(一定要注意新字符串长度不能长于原来字符串,否则可能破坏数据,Unicode字符串必须以NULL结束,在HEX项目中添加)这种方法用起来十分简单,但是缺点是新字符串长度有限制,新字符串长度不应比原字符串长。
修改之后,在Dump窗口中,选择更改后的hello reversing字符串,点击鼠标右键选择copy to executable file菜单,在弹出的Hex窗口中单击鼠标右键,选择save file菜单,在save file as中输入名称后保存为可执行文件,然后运行该文件发现已经改变。
2,在其它内存区域新建字符串并传递给消息函数
在向MessageBoxW()函数传递字符串参数时,传递的是字符串所在区域首地址。如果改变字符串地址,消息框就会显示变更后的字符串。在内存某个区域新建一个长字符串,并把新字符串首地址传递给MessageBoxW()函数,可以认为传递的是完全不同的字符串地址。首先找个空的位置,设置成新的字符串,然后在原函数PUSH的地址那输入Assemble指令,输入PUSH 新的字符串地址的首地址 指令,然后按F9运行程序即可修改成功。
Assembly基础指令
CALL xxxx 调用xxxx地址处的函数
JMP xxxx 跳转到xxxx地址
PUSH xxxx 保存xxxx到栈
RETN 跳转到栈中保持的地址
PE文件
是Portable Executable的简称。是Windows可执行文件的格式,主要包括对文件规格的描述。
小端序标记法
字节序
是多字节数据在计算机内存中存放的字节顺序。采用大端序存储数据时,内存地址低位存储数据的高位,内存地址高位存储数据低位,是最直观的字节存储顺序。采用小端序存储数据时,地址高位存储数据高位,地址低位存储数据低位,是逆序存储方式。当数据为单一字节时,无论采取大端序还是小端序字节存储顺序都一样。只有数据长度在2个字节以上时选择大端序或者小端序会导致数据存储顺序不同。字符数组在内存中是连续的,此时无论采用大端序还是小端序存储顺序都相同。
WORD w:0x1234 大端序 [12][34],小端序[34][12]
DWORD dw:0x12345678 大端序[12][34][56][78],小端序[78][56][34][12]
char[] str:大端序[61][62][63][64][65][00],小端序[61][62][63][64][65][00]
对大型UnIx服务器,常采用大端序保存多字节数据,而对Intel X86 CPU则采用小端序。小端序使用逆序方式存储数据,进行算数运算和扩展、缩小数据效率非常高。
IA-32寄存器(Intel Architecture Register)
寄存器是CPU内部用来存放数据的小型存储区域,寄存器集成在CPU内部,有非常高的读写速度。因为大部分汇编指令用于操作寄存器或者检查其中的数据,必须掌握寄存器的相关内容才能真正明白这些汇编指令的意义。
寄存器分类
通用寄存器(General Purpose Registers)(32位,8个)
是通用型寄存器,用于传送和暂存数据,也可参与算数逻辑运算并保存运算结果。IA-32中通用寄存器主要用来存储常量和地址等。各个寄存器的名称如下:
EAX(针对操作数和结果数据)累加器:一般用在函数返回值中,所有Win32 API函数会先把返回值保存到EAX再返回。
EBX(DS段中的数据指针)基址寄存器
ECX(字符串和循环操作的)计数器:循环指令LOOP中,ECX用来循环计数,每执行一次操作ECX会减一。
EDX(I/O指针)数据寄存器
EBP:扩展基址指针寄存器:表示栈区域基地址,函数被调用时候保存ESP值。函数返回时把值返回ESP,保证栈不会崩溃(称为栈帧技术)
ESI:字符串操作源指针(源变址寄存器)
EDI:字符串操作目标指针(目的变址寄存器)
ESI和EDI主要用于内存复制
ESP:(SS段中栈指针)栈指针寄存器:指示栈区域的栈顶地址,某些指令可以直接用来操作ESP如PUSH、POP、CALL、RET等。ESP不能用于其它用途。
段寄存器(segment registers)(16位,6个)
段:是一种内存保护技术,将内存划分为多个区段,并为每个区段赋予起始地址、范围、访问权限等,以保护内存。段内存记录在Segment Descriptor Table 段描述符表中,段寄存器持有这些SDT的索引。段寄存器的名称如下:
1,CS Code Segment:代码段寄存器
2,SS Stack Segment:栈段寄存器
3,DS Data Segment:数据段寄存器
4,ES:Extra data Segment:附加数据段寄存器1
5,FS:Data Segment:数据段寄存器
6,GS:Data Segment:数据段寄存器
CS用来存放应用程序代码所在段的段基址,SS寄存器用来存放栈段的段基址,DS寄存器用于存放数据段的段基址,ES、FS、GS用来存放程序使用的附加数据段的段基址。
程序状态和控制寄存器(Program Status and Control Registers)(32位,1个)
EFLAGS:Flag Register 标志寄存器
大小为4个字节,每一位都有意义,数值为0或者1,有些位由系统直接设定,有些根据程序命令的执行结果设置。
ZF:ZeroFlag,若运算结果为0则值为1,否则值为0
OF:Overflow Flag,有符号整数溢出时,OF值被设为1,MSB(Most Significant Bit 最高有效位)改变时,其值也被设为1
CF:Carry Flag,无符号整数溢出时,其值被设为1
指令指针寄存器(Instruction Pointer)(32位,1个)
保存在CPU要执行的指令地址,大小为32位。EIP的数值不能直接被修改,只能通过其它指令间接修改。
栈
通常被用来存储局部变量、传递函数参数、保存函数返回地址等。调试程序时需要不断查看栈内存。按照FILO原则存储数据,后进先出。栈顶指针ESP初始状态指向栈底端,执行PUSH命令将数据压入栈,栈顶指针会上移到栈顶端。执行POP命令时候,栈顶指针会下移。栈是由高地址向低地址扩展的数据结构,从下向上扩展。栈是逆向扩展的。
向栈压入数据时,栈顶指针减小,向低地址移动;从栈中弹出数据,栈顶指针增加,向高地址移动。
栈顶指针在初始状态下指向栈底
栈内存在进程中作用
1,暂时保存函数内的1局部变量
2,调用函数时传递参数
3,保存函数返回后的地址
分析abex’ crackme
绝大多数crackme小程序都是猜序列号(serial key)
PUSH ---- 入栈指令
CALL ---- 调用指定位置的函数
INC ---- 值加1
DEC ---- 值减1
JMP ---- 跳转到指定位置
CMP ---- 比较给定的两个操作数,和SUB命令类似,但操作数的值不会改变,仅改变EFLAGS寄存器(如果2个操作数值一致则SUB结果为0,ZF变为1)
JE ---- 条件跳转指令(Jump if equal)若ZF为1则跳转
破解思路:将汇编指令 JE SHORT 0040103D换成 JMP 0040103D,由条件跳转变成无条件跳转。然后使用Copy to executable命令将修改后的代码保存为文件。
函数调用时压栈
一般调用函数,传参时候用压栈方法把函数需要的参数逆序压入栈。用PUSH指令。函数调用时候用正序,函数入栈时用逆序。
栈帧
使用EBP寄存器访问栈内局部变量、参数、函数返回地址等的手段。ESP寄存器承担着栈顶指针的作用,而EBP寄存器则负责行使栈帧指针的作用。程序运行时ESP的值随时改变,因此在调用函数时,先把基准点(函数起始位置)的ESP值保存到EBP,并维持在函数内部。这样无论ESP值如何变化,以EBP值为基准能够安全访问到相关函数的局部变量、参数、返回值等。其中EBP寄存器作为栈帧指针的作用。