逆向工程核心原理学习——(1-12)
第一章
对应用程序文件或进程内存内容的更改被称为“打补丁”,破解与其含义类似,但后者的意图是非法和不道德的。
代码逆向准备:
目标:每次学习一定要去量化目标,不能学了,也不知道有没有学会,也不知道学了多少,也不知道效果怎么样,能不能实践。
激情:达成目标往往会刺激奖励系统去让我们不断地学习
坚持:逆向的学习显然是枯燥的,我们除了理论上的学习,我们更加要多动手实践,在不无聊的同时,还巩固了理论的知识。
2024,6,9 星期日:
今日目标:
内容:前12章
方法:理论知识进行理解和做笔记,并且有自己的理解
实验内容完成实验,并写出实验过程,总结实验技巧
效果:完全理解前12章内容的学习,加深基础巩固。
第2章 逆向分析hello world程序
调试 hello world.exe程序
调试目标
调试helloworld.exe可执行文件,在转换得到的汇编语言代码中查找main函数。这一过程,我们要了解基本的调试方法和汇编指令。
开始调试
使用od打开可执行文件:

入口点
调试器停止的地点即为helloworld.exe执行的起始地址,它是一段EP代码,其中最引人注意的是call与jmp两个命令

上面两行汇编代码含义非常明确,先调用call 40270c地址处的函数,再跳转至JMP40104f地址处。
接下来继续调试:
EP(EntryPoint 入口点)
EP是Windows可执行文件(exe、dll、sys等)的代码入口点,是执行应用程序时最先执行的代码的起始位置,它依赖于cpu。
跟踪40270c函数

可以看到这里调用的API函数名称并不是我们在源码中调用的,所以这个并不是main函数,我们一直执行到4027a1地址处进行返回。
跟踪40104f跳转语句

查找main函数
执行到401056地址处的call 402524函数调用指令时,执行f7进入402524

我们很难把402524函数称为main函数,因为它的代码中并未发现调用messageBox API的代码 返回出去,继续查找。

401144会看到一条call 401000指令,进入该函数

由此可见,该函数调用了MessageBox函数,所以就是main函数。
进一步熟悉调试器
大本营:每次运行时,程序都会返回到EP处,并从此处开始新的调试,使用起来相当不方便。经验丰富的逆向分析专家需要在调试代码时设置某个重要的点,是调试能快速转到设置点上。
设置大本营的四种方法:
1.goto命令
直接使用goto命令到达大本营
2.设置断点
设置断点后,调试运行到断点处将会暂停。
3注释:
按键盘上的分号;,可以在指定地址处添加注释,还可以通过查找命令找到它。

鼠标右键菜单中一次选择search for -user deined commnet,这样就能看到用户输入的所有注释。
4标签:
按冒号添加标签,和注释一样,标签也可以索引,ctrl+G直接输入标签就可以转到标签所在地址处了。
快速查找指定代码的四种方法
代码执行法:
我们需要查找的是main函数中调用MessageBox函数地代码。在调试器中调试helloworld.exe时,main()函数地messageBox函数在某个时刻就会被执行调用,弹出消息对话框,显示“hello world”这条消息。
以上就是代码执行法地基本原理,程序功能非常明确时,逐条执行指令来查找需要查找地位置。代码执行法仅适用于被调试代码量不大、且程序功能明确的情况。遇到大量代码的程序时,该方法就不再适用了。
字符串检索法
鼠标右键菜单——search for referenced test strings
od初次载入待调试程序时,都会先经历一个预分析过程。此过程中会查看进程内存,程序中引用的字符串和调用的API都会被摘录出来,整理到另外一个列表中,使用all referenced text strings命令会弹出一个窗口,其中列出了程序代码引用的字符串。

地址401007处有一条push 004092a0命令,该命令中引用的004092a0处即是字符串“hello world!”。双击字符串,光标定位到main函数中调用Message Box W函数的代码处。
在od的dump窗口中使用go to,可以进一步查看位于内存4092a0地址处的字符串。首先使用鼠标单击dump窗口,按ctrl+G打开并搜索。

API检索法(1):在调用代码中设置断点。
鼠标右键菜单——search for ——ALLintermodular calls
Windows编程中,若想向显示器显示内容,则需要使用win32API向OS请求显示输出。换言之,应用程序向显示器画面输出内容时,需要在程序内部调用win32API。认真观察一个程序的功能之后,我们能够大致推测出它在运行时调用的win32API,若能进一步查找到调用的win32API,则会为程序调试带来极大便利。
API检索法(2):在API代码中设置断点
鼠标右键菜单-Search for——Name in all calls
od并不能为所有可执行文件都列出API函数调用列表。使用压缩器/保护工具对可执行文件进行压缩或保护之后,文件结构就会改变,此时od就无法列出API 调用列表了
这种情况下,dll代码库被加载到进程内存后,我们可以直接向dll代码库添加断点。api是操作系统对用户应用程序提供的一系列函数。
使用打补丁方式修改helloworld字符串
打补丁
代码逆向分析中,打补丁是不可或缺的重要主题。利用“打补丁”技术不仅可以修改已有程序中的bug,还可以向程序中添加新功能。“打补丁”的对象可以是文件、内存、还可以是程序的代码、数据等。本实例中,我们将使用“打补丁”技术把helloworld程序消息窗口显示的helloworld改为其它字符串。
修改字符串的两种方法
(1)直接修改字符串缓冲区
(2)在其他内存区域生成新字符串并传递给消息函数
1.直接修改字符串缓冲区:


可以看到已经被修改。
保存更改到可执行文件

2.在其他内存区域新建字符串并传递给消息函数

第3章 小端序标记法
计算机领域中,字节序是多字节数据在内存中存储或网络传输时各字节的存储顺序,主要分为两大类,一类是小端序,另一类是大端序。
字节序:
BYTE b = 0x12;
WORD w = 0x1234;
DWORD dw = 0x12345678;
char str[]= "abcde"
大端序:[12],[12][34],[12][34][56][78],[61][62][63][64][65][100]
小端序:[12],[34][12],[78][56][34][12],[61][62][63][64][65][100]
数据类型为字节型(BYTE)时,其长度为1个字节,保存这样的数据,无论大端序还是小端序,字节顺序都是一样的。但是数据长度为2个字节以上时,采用不同字节序保存它们时存储顺粗不一样。采用大端序存储数据时,内存地址低位存储数据的高位,内存地址高位存储数据的低位,这是一种最直观的字节存储顺序;采用小端序存储数据时,地址高位存储数据的高位,地址低位存储数据的低位,这是一种逆序存储方式,保存的字节顺序被倒转,它是最符合人类思维的字节序。
再次强调一下,数据为单一字节时,无论采用大端序还是小端序保存,字节存储顺序都一样。
字符串”abcde“被保存在一个字符数组str中,字符数组在内存中是连续的,此时向字符数组存放数据,无论采用大端序还是小端序,存储顺序都相同。
在od中查看小端序:
查看main函数

查看变量的数据区内容

第4章 IA-32寄存器基本讲解
什么是cpu寄存器
寄存器是cpu内部用来存放数据的一些小型存储区域,它们与我们常说的RAM略有不同,CPU访问RAM中的数据要经过较长的物理路径,所以花费的时间要长一些;而寄存器集成在cpu内部拥有非常高的读写速度。
IA-32寄存器
IA-32是intel推出的32位元架构,属于复杂的指令集架构,它提供了非常丰富的功能,并且支持多种寄存器
基本程序运行寄存器

EAX:(针对操作数和结果数据)累加器
EBX:(DS段中的数据指针)基址寄存器
ECX:(字符串和循环操作的)计数器
EDX:(I/O指针)数据寄存器
EBP:(SS段中栈内数据指针)扩展基址指针寄存器
ESI:(字符串操作源指针)源变址寄存器
EDI:(字符串操作目标指针)目的变址寄存器
ESP:(SS段中栈指针)栈指针寄存器
CS:代码段寄存器
SS:栈段寄存器
DS:数据段寄存器
ES:附加段寄存器
FS:数据段寄存器
GS:数据段寄存器
程序调试中会经常用到fs寄存器,它用于计算SEH、TEB、PEB等地址。
第5章 栈
栈在进程中的作用如下:
(1)暂时保存函数内的局部变量
(2)调用函数时传递参数
(3)保存函数返回后的地址
第6章 分析abex’crakeme#1
运行这个程序


开始调试:

分析代码
00401000 >/$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00401002 |. 68 00204000 PUSH abexcm1-.00402000 ; |Title = "abex' 1st crackme"
00401007 |. 68 12204000 PUSH abexcm1-.00402012 ; |Text = "Make me think your HD is a CD-Rom."
0040100C |. 6A 00 PUSH 0 ; |hOwner = NULL
0040100E |. E8 4E000000 CALL <JMP.&USER32.MessageBoxA> ; \MessageBoxA
00401013 |. 68 94204000 PUSH abexcm1-.00402094 ; /RootPathName = "c:\"
00401018 |. E8 38000000 CALL <JMP.&KERNEL32.GetDriveTypeA> ; \GetDriveTypeA
0040101D |. 46 INC ESI
0040101E |. 48 DEC EAX
0040101F |. EB 00 JMP SHORT abexcm1-.00401021
00401021 |> 46 INC ESI
00401022 |. 46 INC ESI
00401023 |. 48 DEC EAX
00401024 |. 3BC6 CMP EAX,ESI
00401026 |. 74 15 JE SHORT abexcm1-.0040103D
00401028 |. 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
0040102A |. 68 35204000 PUSH abexcm1-.00402035 ; |Title = "Error"
0040102F |. 68 3B204000 PUSH abexcm1-.0040203B ; |Text = "Nah... This is not a CD-ROM Drive!"
00401034 |. 6A 00 PUSH 0 ; |hOwner = NULL
00401036 |. E8 26000000 CALL <JMP.&USER32.MessageBoxA> ; \MessageBoxA
0040103B |. EB 13 JMP SHORT abexcm1-.00401050
0040103D |> 6A 00 PUSH 0 ; |/Style = MB_OK|MB_APPLMODAL
0040103F |. 68 5E204000 PUSH abexcm1-.0040205E ; ||Title = "YEAH!"
00401044 |. 68 64204000 PUSH abexcm1-.00402064 ; ||Text = "Ok, I really think that your HD is a CD-ROM! :p"
00401049 |. 6A 00 PUSH 0 ; ||hOwner = NULL
0040104B |. E8 11000000 CALL <JMP.&USER32.MessageBoxA> ; |\MessageBoxA
00401050 \> E8 06000000 CALL <JMP.&KERNEL32.ExitProcess> ; \ExitProcess
00401055 $-FF25 50304000 JMP DWORD PTR DS:[<&KERNEL32.GetDriveTyp>; KERNEL32.GetDriveTypeA
0040105B .-FF25 54304000 JMP DWORD PTR DS:[<&KERNEL32.ExitProcess>; KERNEL32.ExitProcess
00401061 $-FF25 5C304000 JMP DWORD PTR DS:[<&USER32.MessageBoxA>] ; USER32.MessageBoxA
00401067 00 DB 00
首先会将第一个Message Box A函数的参数放入栈中,然后调用这个API接口,等到点击ok之后,再去调用GetDriverTypeA函数去获取驱动的类型,返回值保存在eax中,为3,此时esi为2,比较eax是否为2,相等则跳转,不相等则error。
破解
我们可以直接让跳转指令变为无条件跳转并且保存下来。

将参数压入栈
在调用MessageBoxA函数之前使用了4个push命令,把函数需要的参数逆序压入栈。因为栈的结构为FILO。
第7章 栈帧
栈帧,简而言之就是利用ebp寄存器访问栈内局部变量、参数、函数返回地址等的手段,程序运行过程中,esp的值随时会发生变化,访问栈中函数的局部变量、参数时,若以esp值为基准编写程序会十分困难,并且也很难使cpu引用到准确的地址。所以,在调用某函数时,先要把esp放入ebp中。这样无论esp如何变化,ebp的值为基准就能够安全访问到相关函数的局部变量、参数、返回地址,这就是ebp寄存器作为栈帧指针的作用。

调试示例stackframe.exe

00401000 /$ 55 PUSH EBP
00401001 |. 8BEC MOV EBP,ESP
00401003 |. 83EC 08 SUB ESP,8
00401006 |. 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8]
00401009 |. 8945 F8 MOV DWORD PTR SS:[EBP-8],EAX
0040100C |. 8B4D 0C MOV ECX,DWORD PTR SS:[EBP+C]
0040100F |. 894D FC MOV DWORD PTR SS:[EBP-4],ECX
00401012 |. 8B45 F8 MOV EAX,DWORD PTR SS:[EBP-8]
00401015 |. 0345 FC ADD EAX,DWORD PTR SS:[EBP-4]
00401018 |. 8BE5 MOV ESP,EBP
0040101A |. 5D POP EBP
0040101B \. C3 RETN
0040101C CC INT3
这段代码类似于一段add函数
int add(int a,int b)
{
int x = a;
int y = b;
x = x + y;
return x;
}
0040101F CC INT3
00401020 /$ 55 PUSH EBP
00401021 |. 8BEC MOV EBP,ESP
00401023 |. 83EC 08 SUB ESP,8
00401026 |. C745 FC 010000>MOV DWORD PTR SS:[EBP-4],1
0040102D |. C745 F8 020000>MOV DWORD PTR SS:[EBP-8],2
00401034 |. 8B45 F8 MOV EAX,DWORD PTR SS:[EBP-8]
00401037 |. 50 PUSH EAX ; /Arg2
00401038 |. 8B4D FC MOV ECX,DWORD PTR SS:[EBP-4] ; |
0040103B |. 51 PUSH ECX ; |Arg1
0040103C |. E8 BFFFFFFF CALL StackFra.00401000 ; \StackFra.00401000
00401041 |. 83C4 08 ADD ESP,8
00401044 |. 50 PUSH EAX
00401045 |. 68 84B34000 PUSH StackFra.0040B384 ; ASCII "%d
"
0040104A |. E8 18000000 CALL StackFra.00401067
0040104F |. 83C4 08 ADD ESP,8
00401052 |. 33C0 XOR EAX,EAX
00401054 |. 8BE5 MOV ESP,EBP
00401056 |. 5D POP EBP
00401057 \. C3 RETN
00401058 $ 3B0D 04C04000 CMP ECX,DWORD PTR DS:[40C004]
0040105E . 75 02 JNZ SHORT StackFra.00401062
00401060 . F3: PREFIX REP: ; Superfluous prefix
00401061 . C3 RETN
int main()
{
int arg1 = 1;
int arg2 = 2;
int result = add(1,2);
prinf("%d\n",result);
return 0;
}
设置od选项
disasm选项
打开od的Debugging option对话框
第8章 abex'crackme #2

Visual Basic文件的特性
VB专用引擎:
VB文件使用名为MSVBVM60.dll的VB专用引擎,举个例子,显示消息框时,VB代码中要调用msgBox函数。其实,VB编辑器真正调用的是MSVBVM60.dll里的rtcMsgBox函数,在该函数内部通过user32.dll里的Message Box W函数来工作。
本地代码和伪代码:
根据使用编译选项的不同,VB文件可以编译为本地代码与伪代码,本地代码一般使用易于调试器解析的IA-32指令,而伪代码是一种解释器语言,它使用由VB引擎实现虚拟机并可自解析的指令因此若想准确解析VB的伪代码,就需要分析VB引擎并实现模拟器。
事件处理程序:
VB主要用来编写GUI程序,IDE用户界面本身也最适合于GUI编程。由于VB程序采用windows操作系统的事件驱动方式工作,所以在main或winmain中并不存在用户代码,用户代码存在于各个事件处理程序之中。
未文档化的结构体:
VB中使用的各种信息以结构体形式保存在文件内部。
开始调试:

执行程序之后,在EP代码中首先要做的是调用VB引擎的主函数(ThunRTMain())
00401232 $-FF25 A0104000 JMP [<&MSVBVM60.#100>] ; MSVBVM60.ThunRTMain
00401238 > $ 68 141E4000 PUSH abexcm2-.00401E14
0040123D . E8 F0FFFFFF CALL <JMP.&MSVBVM60.#100>
EP的地址为401238,此处的push 401e14 命令用来把RT_MainStrcut结构体的地址(401e14)压入栈。然后40123d地址处的call00401232命令调用401232地址处的JMP_DWORD PTR DS:[4010a0]指令,该指令会跳转到VB引擎的主函数ThunRTMain。
以上是VB文件的全部启动代码,。虽然非常简单,但是有3个方面需要留意。
间接调用:
很明显这里使用了一些小技巧,不是直接跳转到ThunRTMain函数的,而是通过中间代码JMP命令进行跳转。
RT_MainStruct结构体

总而言之:RT_MainStrcut结构体的成员是其他结构体的地址。也就是说,VB引擎通过参数传递过来的RT_MainStrcut结构体获取程序运行需要的所有信息。
分析crackme
检索字符串:

双击字符串转到相应地址处

可以看到这里显示信息的标题是wrong serial!,内容是Nope,this serial is wrong!.
从编程的观点来看,使用某种算法生成序列号,通过比较用户输入的序列号与字符串,代码分为TRUE,和FALSE两部分,换句话说,上述代码附近存在字符串比较带代码,这也就是我们该进行补丁的地方。

调用__vbaVarTstEq函数,比较(test命令)返回值(AX),JE指令进行跳转。
查找字符串地址
__vbaVarTstEq已知这个函数是字符串比较函数,其上方的2个push指令为比较函数的参数。

eax和edx寄存器中存放着字符串的地址。
这里的LEA EDX ,[EBP-44]原为
LEA EDX,SS:[EBP-44]
ss为栈段,EBP是基址指针寄存器,
执行

可以看出这里应该是对的ebp-44

查看栈地址内容:

可以看到这里这个serial是我们输入的,那么这个D2C5D1C9就是正确的序列号了。

可以看到已经check成功了。
但是如果把输入的name换了就又不可以了,所以猜测这个serial和name的值有关,应该有一个算法去根据name产生serial的值。
生成serial的算法:
将刚才的比较函数代码区往上移动,会发现类似的代码。

很明显这里是一个函数的开始,因为在构建新的栈帧。
所以这很可能是check之后的函数,也就是有可能这里会进行相应serial的生成。
在这个开始处下断点,然后进行调试,遇到以下代码时查看:

此时edx寄存器被赋值,查看这个地址的内容。
啥都没有,然后push edx,调用了函数,也就是edx作为参数传入,此时我们再去看edx刚才地址处。如下图:

可以看到name已经出现了,也就是说,这个函数是用来取name值得函数。
加密循环:
继续调试会遇到一系列循环:

这里的算法相对比较简单,就是先将name的每一个字符取出转换为ascii码的数值,在对这个数值加64,最后在将这个数值转化为字符串。
第9章 PROCESS EXPLORER——最优秀的进程管理工具

第10章 函数调用约定
calling convention,它是对函数调用时如何传递参数的一种约定。函数执行完成之后,栈中的参数如何处理?
不用管
函数执行完毕后,esp的值如何变化
恢复为原来的值,esp原来的值存放在ebp中,ebp原来的值存放在栈中。
函数调用约定有那些,
cdecl
stdcall
fastcall
cdecl主要是在c语言中使用,调用者负责处理栈。
#include<stdio.h>
int add(int a,int b)
{
return(a+b);
}
int main( int argc,char *argv[])
{
return add(a,2);
}

可以看到参数是通过栈传递的,好处在于,参数的长度不受限制。
stdcall
#include<stdio.h>
int _stdcall add(int a,int b)
{
return(a+b);
}
int main(int argc,char *argv[])
{
return add(1,2);
}

先这样,在被调用者add函数内部清理栈的方式即为stdcall方式。好处在于,代码尺寸小。
fastcall
fast,快就快在使用寄存器传递参数,cpu访问寄存器比访问内存要快很多。

浙公网安备 33010602011771号