逆向工程核心原理学习——(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访问寄存器比访问内存要快很多。

 

posted @ 2024-06-09 21:46  robot__i  阅读(102)  评论(0)    收藏  举报