文章目录
- abex'crackme-2
- 1. Visual Basic文件的特征
- 1.1. VB专用引擎
- 1.2. 本地代码与伪代码
- 1.3. 事件处理程序
- 1.4. 未文档化的结构体
- 2. 开始调试
- 2.1. 间接调用
- 2.2. RT_MainStruct结构体
- 2.3. ThunRTMain()函数
- 3. 分析crackme
- 3.1. 检索字符串
- 3.2. 查找字符串地址
- 3.3. 生成Serial的算法
- 3.4. 预测代码
- 3.5. 读取name字符串的代码
- 3.6. 加密循环
- 3.7. 加密方法
- 函数的调用约定
- 1. 函数调用约定
- 1.1. cdecl
- 1.2. stdcall
- 1.3. fastcall
- 视频讲座-Tut.ReverseMe1
- 1. 运行
- 2. 分析
- 2.1. 去除消息框
- 2.2. 打补丁去除消息框
- 2.2.1. 第一次尝试
- 2.2.2. 第二次尝试
- 2.2.3. 如何查看401C17 函数的参数个数
- 2.3. 查找注册码
思维导图

abex’crackme-2
运行这个程序
要求我们输入正确的账户与序列号
1. Visual Basic文件的特征
Visual Basic(VB)文件现在已经不太常见了,尤其是在现代开发环境中。微软早在 2008 年就停止了对 VB6(经典 Visual Basic)的官方支持,而 VB.NET 作为 .NET 生态的一部分,虽然仍然在维护,但在开发者社区中的使用率已经大幅下降
此crackme是由 Visual Basic编写而成的。所以我们可以先了解一下其特征
1.1. VB专用引擎
VB文件使用名为MSVBVM60.dll
(MicrosoftVisual BasicVirtual Machine6.0)的VB专用引擎(也称为TheThunder Runtime Engine)。
举个使用VB引擎的例子,显示消息框时,VB代码中要调用 MsgBox
函数。其实,VB编辑器真正调用的是 MSVBVM60.dll
里的 rtcMsgBox
函数,在该函数内部通过调用 user32.dll
里的 MessageBoxW
函数(Win32API)来工作(也可以在VB代码中直接调用 user32.dll
里的 MessageBoxW
。
1.2. 本地代码与伪代码
根据使用的编译选项的不同,VB文件可以编译为本地代码(Ncode)与伪代码(Pcode)。
本地代码一般使用易于调试器解析的IA-32指令
伪代码是一种解释器(Interpreter)语言,它使用由VB引擎实现虚拟机并可自解析的指令(字节码)。因此,若想准确解析VB的伪代码,就需要分析VB引擎并实现模拟器。
1.3. 事件处理程序
VB主要用来编写GUI程序,IDE用户界面本身也最适合于GUI编程。由于VB程序采用Windows操作系统的事件驱动方式工作,所以在main
或WinMain
中并不存在用户代码(希望调试的代码),用户代码存在于各个事件处理程序(eventhandler)之中。
就上述abex’crackme ,用户代码在点击Check按钮时触发的事件处理程序内。
1.4. 未文档化的结构体
VB中使用的各种信息(Dialog、Control、Form、Module、Function等)以结构体形式保存在文件内部。由于微软未正式公开这种结构体信息,所以调试VB文件会难一些。
2. 开始调试
首先在EP代码中找到调用VB引擎的主函数 ThunRTMain()
00401232 $- FF25 A0104000 jmp dword ptr ds:[0x4010A0] ; msvbvm60.ThunRTMain
00401238 > $ 68 141E4000 push 0x401E14
0040123D . E8 F0FFFFFF call 00401232 ; <jmp.&MSVBVM60.#100>
分析一下几条命令
00401238
是EP的地址、命令 push 0x401E14
把 RT_MainStruct
结构体的地址 401E14
压入栈。
然后 call 00401232
调用 401232
处的指令。 即会跳转到VB引擎的主函数 ThunRTMain()
(前面压入栈的401E14 的值作为 ThunRTMain()
的参数)
这就是VB文件的全部启动代码,非常简单
2.1. 间接调用
40123D
地址处的 CALL 401232
命令用于调用 ThunRTMain
函数,这里使用了较为特别的技法。不是直接转到 MSVBVM60.dll
里的 ThunRTMain
函数,而是通过中间 401232
地址处的 JMP
命令跳转。
这是VC++ 、VB编译器中常用的间接调用法
4010A0
地址是IAT(Import Address Table,导入地址表)区域,包含着MSVBVM60.ThunRTMain
函数的实际地址
2.2. RT_MainStruct结构体
要注意的是 ThunRTMain
函数的参数 RT_MainStruct
结构体。这里,RT_MainStruct
结构体存在于 401E14
地址处,如图所示。
微软未公开 RT_MainStruct
,但是有国外的逆向分析高手已经完成了对 RTMainStruct
结构体的分析,并公布在网络上。
RT_MainStruct
结构体的成员是其他结构体的地址。也就是说,VB引擎通过参数传递过来的 RT_MainStruct
结构体获取程序运行需要的所有信息。此处省略对RT_MainStruct结构体的详细说明。
2.3. ThunRTMain()函数
图中显示了 ThunRTMain
代码的开始部分,可以看到内存地址完全不同了。这是 MSVBVM60.dII
模块的地址区域。换言之,我们分析的不是程序代码,而是VB引擎代码(现在还不需要分析如此庞大的代码)
3. 分析crackme
当前我们很难直接去分析 RT_MainStruct
结构体。 需要找一个更简单的方法
我们可以通过程序提示的字符串入手。因为程序会提示错误后的信息
3.1. 检索字符串
查找->所有参考字符串
双击跳转
我们这里找到了消息提示的调用函数
根据逻辑判断。看到是会根据我们的 name
生成一个序列号。然后判断是否正确,然后回显信息
这里我们往上翻 去找一下那个判断语句,而判断语句一般就是一个跳转命令
通过上面 call dword ptr ds:[0x401058]
命令调用 __vbaVarTstEq
函数比较(TEST命令)返回值(AX)后,由403332
地址的条件转移指令(JE指令)决定执行“真”代码还是“假”代码。
TEST:逻辑比较(Logical Compare)
与bit-wiselogical“AND’一样(仅改变EFLAGS寄存器而不改变操作数的值)若2个操作数中一个为0,则AND运算结果被置为0->ZF=1。
JE:条件转移指令(Jumpif equal)
若ZF=1,则跳转。
3.2. 查找字符串地址
我们现在已经找到了比较函数 __vbaVarTstEq
的地址,那么上面的两个push 就是传入的函数参数
我们先调试到比较函数地址 403329
处
我们观察上面的两处指令
这里 SS:[EBP-44]
指的是栈内地址。它恰好就是函数声明中的局部对象的地址(局部对象存储再栈区)
下面信息显示了栈内地址为 19f288
我们可以通过 选中地址,然后在数据框中跟随地址 即可查看对应的内存地址
查看存储在栈中的内存地址
这里与C++的string类一样,VB 字符串使用可变长度的字符串类型。 如图,直接显示的不是字符串,而是16字节大小的数据(这就是VB中使用的字符串对象)
不同的值被这样统一起来,仅方框显示的值是不同的,看上去就像内存地址一样(可变长度字符串类型内部持有实际动态分配的字符串缓存地址)
我们右键选择ASCII数据类型 即可查看到我们实际输入的值与 进行对比的值
切回来选择这个
如图
EAX 19f298
是我们输入的值 123456 字符串所在的地址是 66145c
EDX 19f288
是实际的序列号 C795D8D6 字符串所在地址是 6615c4
我们可以查看一下
VB默认使用基于Unicode的可变长度字符串对象。可变长度字符串对象会根据需要在内部随时动态分配/释放内存。因此,每次运行时字符串的地址会有所不同。此外,调试时无法一眼看全实际字符串,这也是调试的困难之一。
下面我们运行程序 输入对应的名字与序列号即可弹出正确的信息
3.3. 生成Serial的算法
这里我们探讨一下生成序列号的算法。 我们只知道了 c1trus
对应的序列号是 C795D8D6
那我们想批量生成序列号,肯定需要知道其算法
查找函数开始部分
我们上面找到了一个条件转移的代码。 那个应该就是check后进行的操作。当我们点击check后,这个计算序列号的函数就会被调用执行。然后就会进行对比,并弹出消息框
所以我们应该往上一点点找函数开始的部分。
而且结合 [[07.栈帧]]中的知识。我们知道在执行函数之前会生成栈帧
通常命令是 PUSH EBP
然后 MOV EBP,ESP
那我们就着重看这个命令,也可以直接查找。
很容易就找到了生成栈帧的代码
可以发现上面有大量的 nop命令
VB文件的函数之间存在着NOP指令(图8-14的402ECC~402ECF地址区)。NOP:NoOperation,不执行任何动作的指令(只消耗CPU时钟)。
为了方便分析代码。我们先在此处下断点
3.4. 预测代码
如果你有经验,就可以预测出生成序列号的方法,若是win32API程序
则一般有一下特点
- 读取Name字符串(使用
GetWindowsText
、GetDlgItemText
等API) - 启动循环,对字符进行加密(
XOR
ADD
SUB
等)
VB引擎编写的文件也有类似的规律,如果正确的化,我们从断点处开始调试,查找到读取name
字符串的那部分后,紧接着就是加密循环
3.5. 读取name字符串的代码
开始调试后我们会遇到好几个 call
直到调试到第四个 call
上面的 lea edx,dword ptr ss:[ebp-0x88]
指令把函数的局部对象 ss:[ebp-0x88]
地址传递给了函数的参数。 我们可以看一下这个地址
因为要查找的是Name字符串,在VB中,字符串使用字符串对象(这与C语言使用char数组不同)我们很难认出实际的字符串,所以需要调整一下地址视图的模式
然后继续调试到 call
函数之后
这里我们可以可以看到 name
字符串的值了。 其地址就是 19F244
就是 EBP-88
3.6. 加密循环
继续调试就会遇到以下循环。一系列的循环语句
简单讲解上述循环的动作原理,就像在链表中使用next指针引用下一个元素一样,
__vbaVarForInit
、__vbaVarForNext
可以使逆向分析人员在字符串对象中逐个引I用字符。并且设置loopcount(EBX)使其按指定次数运转循环。
实测仅使用接收的Name字符串中的前4个字符。在代码内检查字符串的长度,若少于4个字符,就会弹出错误消息框。
3.7. 加密方法
这里太难整了。我先跳过了。等以后再来分析
函数的调用约定
1. 函数调用约定
CallingConvention译成中文是“函数调用约定”,它是对函数调用时如何传递参数的一种约定。
通过前面的学习已经知道,调用函数前要先把参数压人栈然后再传递给函数。栈就是定义在进程中的一段内存空间,向下(低地址方向)扩展,且其大小被记录在PE头中。也就是说,进程运行时确定栈内存的大小(与malloc/new动态分配内存不同)。
提问1.函数执行完成后,栈中的参数如何处理?
回答1.不用管。
由于只是临时使用存储在栈中的值,即使不再使用,清除工作也会浪费CPU资源。下一次再向栈存人其他值时,原有值会被自然覆盖掉,并且栈内存是固定的,所以既不能也没必要释放内存。
提问2.函数执行完毕后,ESP值如何变化?
回答2.ESP值要恢复到函数调用之前,这样可引用的栈大小才不会缩减。栈内存是固定的,ESP用来指示栈的当前位置,若ESP指向栈底,则无法再使用该栈。函数调用后如何处理ESP,这就是函数调用约定要解决的问题。主要的函数调用约定如下。
cdecl
stdcall
fastcall
应用程序的调试中,cdecl与stdcall的区别非常明显。不管采用哪种方式,通过栈来传递参数的基本概念都是一样的。
调用者——调用函数的一方。
被调用者——被调用的函数。
比如在main
函数中调用printf
函数时,调用者为main
,被调用者为printf
1.1. cdecl
cdecl是主要在C语言中使用的方式,调用者负责处理栈
示例代码
#include "stdio.h"
int add(int a ,int b)
{
return a+b;
}
int main(int argc , char* argv[])
{
return add(1,2);
}
编译后进行调试
从图中
401013~40101C
地址间的代码可以发现,add
函数的参数1、2
以逆序方式压人栈,
调用 add
函数(401000)后,使用 ADD ESP,8
命令整理栈。调用者 main
函数直接清理其压人栈的函数参数,这样的方式即是cdecl。
cdecl方式的好处在于,它可以像C语言的
printf
函数一样,向被调用函数传递长度可变的参数。这种长度可变的参数在其他调用约定中很难实现。
1.2. stdcall
stdcall方式常用于Win32API,该方式由被调用者清理栈。前面讲解过C语言默认的函数调用方式为cdecl。若想使用stdcall方式编译源码,只要使用 _stdcall
关键字即可
示例代码
#include "stdio.h"
int _stdcall add(int a ,int b)
{
return a+b;
}
int main(int argc , char* argv[])
{
return add(1,2);
}
生成exe然后进行调试
从图中的代码可以看到,在 main
函数中调用 add
函数后,省略了清理栈的代码(ADD ESP,8
)。栈的清理工作由 add
函数中最后(40100A)的 RETN 8
命令来执行。RETN 8
命令的含义为 RETN+POP 8
字节,即返回后使ESP增加到指定大小
像这样在被调用者 add
函数内部清理栈的方式即为stdcall方式。
stdcall方式的好处在于,被调用者函数内部存在着栈清理代码,与每次调用函数时都要用 ADD ESP,XXX
命令的cdecl方式相比,代码尺寸要小。
虽然Win32API是使用C语言编写的库,但它使用的是stdcall方式,而不是C语言默认的cdecl方式。这是为了获得更好的兼容性,使C语言之外的其他语言(Delphi(Pascal)、VisualBasic等)也能直接调用API。
1.3. fastcall
fastcall方式与stdcall方式基本类似,但该方式通常会使用寄存器(而非栈内存)去传递那些需要传递给函数的部分参数(前2个)。若某函数有4个参数,则前2个参数分别使用ECX、EDX寄存器传递。
顾名思义,fastcall方式的优势在于可以实现对函数的快速调用(从CPU的立场看,访问寄存器的速度要远比内存快得多)。单从函数调用本身来看,fastcall方式非常快,但是有时需要额外的系统开销来管理ECX、EDX寄存器。倘若调用函数前ECX与EDX中存有重要数据,那么使用它们前必须先备份。此外,如果函数本身很复杂,需要把ECX、EDX寄存器用作其他用途时,也需要将它们中的参数值存储到另外某个地方。
视频讲座-Tut.ReverseMe1
一位名叫Lena的人在 http://www.tuts4you.com/ 公示板上贴了40个crackme讲座,以帮助初学者学习代码逆向分析技术。这些讲座非常受欢迎,因为所有讲座都以Flash视频形式呈现出来,且让学习者感到非常亲切。各位可以链接到tuts4you网站观看这些视频讲座,这对学习代码逆向分析技术非常有帮助。
1. 运行
运行要破解的程序 给我们了两条信息
就是说叫我们 去除提示框 并找到正确的注册码
2. 分析
2.1. 去除消息框
打开后发现也是用VB编写的软件,因为我们在EP中发现了 ThunRTMain
而且我们知道VB中调用消息框的函数是 rtcMsgBox
然后我们在调用模块中去查找,可以发现有三次调用此函数
在每一处调用都设置一个断点
然后运行程序
发现程序在 402CFE
处断了下来,此时应该就是要调用函数进行显示消息了。那么上面多半就有要显示的消息。果不其然,上面确实可以发现将要显示的消息
然后我们继续运行,我们找一下程序中点击 nag?
按钮后的断点在哪里
发现还是断在了 402CFE
处。那么我们只要对这一处进行修复即可
2.2. 打补丁去除消息框
2.2.1. 第一次尝试
我们先修改 402CFE
处的call命令
原来的
修改后的
402CFE
地址处的ADD ESP,14
命令的含义是,按照传递给rtcMsgBox
参数的大小(14)清理栈。并用NOP填充其余2个字节,以保证代码不会乱(原来CALL命令的大小为5字节,ADD命令用3个字节,还余下2个字节)。
看上去没有什么问题,但结果却“发生错误”。原因在于没有正确处理 rtcMsgBox
函数的返回值(EAX寄存器)。
如图,在 402CFE
地址处调用 rtcMsgBox
函数后,402D0C
地址处将返回值(EAX)存储到特定变量(EBP-9C)。此处消息框的返回值应该是1(表示“确定”按钮)。若存储的为1之外的值,则表示程序终止。那么最好试试其他方法。
(1)可以修改402CFE地址处的指令,如下所示。
ADD ESP,14
(Instruction:83C414)
MOV EAX,1
(Instruction:B801000000)以上两行汇编代码产生的结果与调用
rtcMsgBox
函数后用户按“确定”按钮的结果相同(栈与返回值相同)。之所以没有这样做是因为指令长度不合适。源文件402CFE
地址处的命令长度为5字节,但上面2行汇编命令的长度为8个字节,因此会侵占到后面的代码。(2)x86(IA-32)系统中使用EAX寄存器传递函数的返回值。
2.2.2. 第二次尝试
在断点处向上看可以发现 402C17
处表示函数开始的栈帧
402CFE
的 rtcMsgBox
函数调用代码也是属于其他函数内部的代码。所以如果上层函数无法调用,或直接返回,最终将不会调用 rtcMsgBox
函数。像下面这样修改401C17
处的指令(使用AssembleSpace指令)。
原来的
修改后
要根据传递给函数的参数大小跳转栈 (RETN XX)
然后运行 发现就去除了消息框
2.2.3. 如何查看401C17 函数的参数个数
首先找到 402C17
处函数的返回地址,在 402C17
下个断点,并运行到这里
然后可以在栈空间看到返回的地址是 7401E5A9
goto过去看返回地址上一条命令。即执行此命令后就完成 402C17
函数。并返回地址到 7401E5A9
如图所示 返回地址 (7401E5A9)
。该代码区域是 MSVBVM50.dll
模块区域。执行 7401E5A7
地址处的 CALL EAX
指令后即返回 7401E5A9
地址处。再次运行调试器(Ctrl+F2),在 7401E5A7
地址处设置断点后运行程序(F9),可以得知EAX的值为402656
此时会进入一个函数内部,并跳转到 402656
地址处,
然后我们使用 ctrl+f9
发现最后会回到 402C17
地址处
所以调用 call eax
就是在调用 402c17
地址处的函数,那么我们只要确认 CALL EAX
命令(7401E5A7)调用前后的栈地址即可得知 402C17
函数参数的个数(因使用的是stdcall调用方式,所以栈由被调用者负责清理)
调用之前的地址 19fab8
调用之后的地址 19fab4
相差4
2.3. 查找注册码
首先先定位到注册码判断的命令位置处
字符串找关键字符
跳转过去,往上找判断语句
这里发现调用了 __vbaStrCmp
函数。 __vbaStrCmp
API是VB中用于比较字符串的函数。这里大概率就是与上面压入栈中的参数 I'mlena151
进行比较
我们输入进行验证
成功了
PS:笔记仅记录自己学习。里面有大量书中原文。如有侵权,请联系我删除。