程序逆向:Visual Basic程序逆向分析

一、Visual Basic简介:

1.百度百科简介:

Visual Basic(简称VB)是Microsoft公司开发的一种通用的基于对象的程序设计语言,为结构化的、模块化的、面向对象的、包含协助开发环境的事件驱动为机制的可视化程序设计语言。是一种可用于微软自家产品开发的语言。

2.个人总结:

tips:目前VB语言碰到的都是crackme里的一些验证算法中利用VB做一个图形界面,下面记载的知识点也基本是针对VB在Crack中的应用。

  • VB函数的调用遵循 stdcall 原则,并且函数之间会有NOP分隔,而且VB程序不是直接调Windows API函数,而是调用VB库里的函数。可以看到下图导入的函数都来自于MSVBVM50.dll
  •  VB文件使用名为MSVBVM60.dll的VB专用引擎(Microsoft Visual Basic Virtual Machine 6.0)
  • 根据使用的编译选项不同VB文件可以编译为本地代码(N code)与伪代码(P code),前者适用于调试器解析的IA-32指令;后者是一种解释器语言,它使用由VB引擎实现虚拟机并可自解析的指令(字节码)。因此,若想准确解析VB的伪代码就需要分析VB引擎并实现模拟器
    • 这里面的N表示的是自然编译(Native):
      • 自然编译的VB程序,直接生成x86汇编代码,可以直接使用OD、IDA进行分析
    • 而P表示的是伪编译:
      • 运行时依赖解释器将伪代码翻译为汇编代码再执行
  • 该语言适用于编写GUI图形界面,VB程序采用windows操作系统的事件驱动方式工作,所以在main()或Winmain()中并不存在用户代码(希望调试的代码),用户代码存在于各个事件处理程序之中。

二、vb程序的EP特征:

vb程序的EP一开始就是一条push命令,这里实际上压入的是一个指针,指向的是VBHeader结构体,然后就是call指令调用MSVBVM60.dll中的ThunRTMain。这两条指令的目的是调用ThunRTMain函数初始化各种变量。

VBHeader结构体:(VBHeader)

typedef struct
{
    char Signature[4];           //00H 四个字节的签名符号,和PEHEADER里的那个signature是类似性质的东西,VB文件都是"VB5!"
    WORD RtBuild;                   //04H 运行时创立的变量(类似编译的时间)
    BYTE LangDLL[14];         //06H 语言DLL文件的名字(如果是0x2A的话就代表是空或者是默认的)
    BYTE BakLangDLL[14];        //14H 备份DLL语言文件的名字(如果是0x7F的话就代表是空或者是默认的,改变这个值堆EXE文件的运行没有作用)
    WORD RtDLLVer;                  //22H 运行时DLL文件的版本
    DWORD LangID;                 //24H 语言的ID
    DWORD BakLangID;                //28H 备份语言的ID(只有当语言ID存在时它才存在)
    DWORD pSubMain;               //2CH RVA(实际研究下来是VA) sub main过程的地址指针(3.)(如果时00000000则代表这个EXE时从FORM窗体文件开始运行的)
    DWORD pProjInfo;              //30H VA 工程信息的地址指针,指向一个ProjectInfo_t结构(2.)
    DWORD fMDLIntObjs;         //34H ?详细见"MDL 内部组建的标志表"
    DWORD fMDLIntObjs2;          //36H ?详细见"MDL 内部组建的标志表"
    DWORD ThreadFlags;       //38H 线程的标志
    //* 标记的定义(ThreadFlags数值的含义)
    //+-------+----------------+--------------------------------------------------------+
    //| 值    | 名字           | 描述                                                
    //+-------+----------------+--------------------------------------------------------+
    //|  0x01 | ApartmentModel | 特别化的多线程使用一个分开的模型                        
    //|  0x02 | RequireLicense | 特别化需要进行认证(只对OCX)                             
    //|  0x04 | Unattended     | 特别化的没有GUI图形界面的元素需要初始化                       
    //|  0x08 | SingleThreaded | 特别化的静态区时单线程的                                                 
    //|  0x10 | Retained       | 特别化的将文件保存在内存中(只对Unattended)                    
    //+-------+----------------+--------------------------------------------------------+
    //ex: 如果是0x15就表示是一个既有多线程,内存常驻,并且没有GUI元素要初始化
    DWORD ThreadCount;                //3CH 线程个数
    WORD FrmCount;                    //41H 窗体个数
    WORD pExternalComponentCount;    //44H VA 外部引用个数例如WINSOCK组件的引用
    DWORD ThunkCount;                  //48H ?大概是内存对齐相关的东西
    DWORD GUITable;                  //4CH VA GUI元素表的地址指针(指向一个GUITable_t结构)
    DWORD pExternalComponentTable;        //50H VA 外部引用表的地址指针
//    DWORD pProjDep;                        // VA 工程的描述的地址指针(这个其实没有)
    DWORD pComRegData;                //54H VA COM注册数据的地址指针
    DWORD oProjExename;                //58H Offset 指向工程EXE名字的字符串
    DWORD oProjTitle;                        //5CH Offset 指向工程标题的字符串
    DWORD oHelpFile;                        //60H Offset 指向帮助文件的字符串
    DWORD oProjName;                        //64H Offset 指向工程名的字符串
}VBHeader_t;

 可以在数据窗口跟踪push的结构体:

结构体的前面4个字节为魔数字段“VB5!”,对于该结构体需要重点关注的成员是pSubMain 它才是VB程序的真正入口。 

tips:关于这个结构体还有一个技巧叫做C4法,这个技巧将在下面的第二个示例中进行详细介绍。

示例一:

前期信息探查:

1.EXEinfo:

32位,无壳,VB编写的程序

2.运行一下:

随便输入一点东西:

主要关注一下出错后弹出的信息。

动态调试:

1.定位关键跳转

首先查找所有参考文本字串

如上图找到之前运行时得到的报错信息,双击跟过去看看:

注意这个红色箭头,它告诉我们它是从哪里跳转过来的,跟着它往上看:

这里就是校验跳转了,接下来就是往上看它的校验算法是怎么一回事。

2.分析校验算法:

直接往上划,找到构建栈帧的地方,然后略过那些初始化的函数,定位第一个对我们分析有用的函数调用:

栈帧构建处:

第一个操作:返回输入字符串长度

这里说一说定位到这个函数的方式:

在构建栈帧处下断点,结合动调一步一步F8,这里我输入的用户名为“hahaha”

从构建栈帧处往下看会出现一堆__vbaObjSet函数,它的作用是将对象变量指向内存中的对象,这里直接忽略即可。

一直调试到下图时,发现__vbaLenBstr函数将我输入的name作为参数调用,这个函数的作用是返回字符串串长

思路其实很简单就是观察call调用的函数有啥作用,然后根据自己的输入,再配合栈窗口的变化来定位的。下面继续跟进指令,__vbaLenBstr函数讲返回值存在了eax中:

分析一下__vbaLenBstr函数这一坨的操作:(402415~402427)

  • 40241B处,eax存着返回的串长将其存入edi中
  • 40241D处,将name的值存入ecx
  • 402420处,看到着个我觉得有点奇怪,imul指令是两个指令数的相乘,但是为啥这里有三个参数,这里我选择动调看结果就知道了
    当做完这个指令后edi的值为8EDE2也就是6*0x17CFB。
  • 402427处,这里是判断OF寄存器的也就是判断是否溢出。所以我们输入的用户名不能过长

下一part是取出字符串的第一个值然后加上edi的值,然后把这个值转成十进制,存起来:

__rtcAnsiValueBstr之前遇到过就是获取该字符的ASCII码的十六进制值并保存在 eax中(eax 直接存储着这个码),然后放入edx中,与edi的值相加。这里同样也验证了一次是否有溢出,然后将操作过后的值压栈保存

0x8EDE2+0x=8EE4A

然后下面的函数__vbaStrI4:将十六进制转换成十进制

十六进制数8EE4A转换十进制后为:585290,然后将它作为参数调用__vbaStrMove进行字符串拷贝

最后一个部分在402523处:

这个函数好理解,就是把“AKA-”与ECX寄存器的值(就是之前得出的585290)相连接,最后进行strcmp比较:

这个图就是比较函数内部,查看压栈的两个参数一个是经过算法运算后的序列号,一个是我们自己输入的序列号。这里没有必要管strcmp是怎么比较的直接Ctrl+F9。在之后的代码就是作比较然后根据strcmp的值确定跳转输出序列号成功与否。

注册机编写:

1.确认整个算法逻辑:
  • 接受用户名字符串;
  • 获取用户名的长度;
  • 长度乘以0x17CFB,如果溢出的话跳转至处理溢出的函数
  • 相乘的结果加上用户名字符串的第一个字符的ASCII码值
  • 将结果转换成十进制
  • 将“AKA-”与计算的结果连接便是最后的序列号了
2.注册机:
#include"stdio.h"
#include"stdlib.h"
#include"string.h"
int main()
{
        char str1[10] = "";
        int len,seriali;

        puts("Please Input Your Name:");
        scanf("%s", str1);
        len = strlen(str1);
        len = 0x17CFB * len;
        seriali = len + str1[0];

        printf("AKA-%d",seriali);

        return 0;
}

搞一下:

示例二:

前期信息探查:

1.EXEinfo:

32位,无壳,VB编写的程序

2.运行一下:

双击程序会发现弹出一个Neg窗口,过了很久才弹出主窗口:

随便输入一点东西:

主要关注一下出错后弹出的信息。

前置知识:

tips:这里列一下分析这个程序会用到的一些知识点,但有些知识点不会在这里写得太详细,可以移步到百度或者我博客里关于一些知识点的整理。

1.Timer:

在 Windows 应用程序中常常要用到时间控制的功能,如在程序界面上显示当前时间,或者每隔多长时间触发一个事件,等等。而 Visual Basic 中的 Timer(时间)控制器就是专门解决这方面问题的控件。Timer 控件可以使用 Name 属性与 Enabled 属性,但最重要的是 Interval 即时间间隔属性。Interval 属性决定了时钟事件之间的间隔,以毫秒为单位,取值范围为 0 ~ 65535 ,因此其最大时间间隔不能超过 65 秒,即一分钟多一点的时间。如果把 Interval 属性设置为 1000 ,则表示每秒钟触发一个 Timer 事件。 其语法格式:Timer.Interval = X ,其中,X 代表具体的时间间隔。

一个TImer对象需指定定义回叫方法的 TimerCallback 委托、传递到该回叫的可选状态对象、首次调用该回叫前的延迟时间以及两次回叫调用之间的时间间隔。

timer = new Timer(
	callback: new TimerCallback(TimerTask),
	state: timerState,
	dueTime: 1000,
	period: 2000);
2.汇编中关于浮点寄存器:

Notion – The all-in-one workspace for your notes, tasks, wikis, and databases.A new tool that blends your everyday work apps into one. It's the all-in-one workspace for you and your teamicon-default.png?t=N7T8https://reveone.notion.site/470788e8c8634c4a99183a4fb374fdd0?pvs=4

去除Neg弹窗:

方法一:直接搜索Timer

首先可以直接在数据框中搜索字符串“Timer”:

OD打开程序,在右击数据窗口选择查找→二进制字串→输入Timer:

找到Timer后可以看到后面有有一串数值“0x1B58”(注意小端存储),这个数值就是调用Timer后的延迟时间,转换成10进制为7000,而Timer的单位为毫秒也就是说这个Neg窗口的停留时间为7秒,既然它存在了数据区里面那我们可以直接修改停留时间(修改为1毫秒):

同样还是要注意一个小端存储的问题,这里要输的值为“10 00”,这里在补充一个知识点就是当我们修改好一个程序后,如何保存为新的程序:

在指令窗口右击→选择复制到可执行文件→点击所有修改

然后会弹出一个选项卡:

选择复制,之后弹出一个文件窗格:

在窗口内右击然后选择保存文件就会弹出文件另存为窗口了。

保存为新的程序后双击运行就会发现Neg窗口一闪而过后就直接跳转到序列号窗口。

其实这里我有尝试过把时间改为0秒,但是运行后发现程序会一直停留在Neg窗口,我对Timer不是很熟悉,只能是猜测可能当数值为0时,就等于一直停留吧。

方法二:4C法

这个方法首先要明白的一个点就是啥是4C法,这里的4C指的其实就是对于VBHeader结构体中偏移为4C位置处的成员GUITable,这个成员也是一个指针其作用是指向Form GuI描述表。通俗的来说这个指针指向的就是窗体参数数据块,在这里面我们看到的窗口其出现的顺序,并且可以利用这个特征来根据按照我们的个人意愿来调整各窗口出现的顺序。

我们可以分析一下GUITable结构体的主要成员的定义:

Signature DWORD //00H.必须是50000000
FomID TGUID //04,可能是以GUID方式命名的formID
Index BYTE //24H 窗体的序号
Flag1 BYTE //28H 第一个窗体的启动标志,可能是90 也可能是10
AGUIDescriptionTable DWORD //48H指针指向以“FFCC…“开始的FormGUI表
Flag3 Dword //4CH.意义不明

具体操作如下:

把程序丢入IDA分析,根据之前笔记的分析得知,EP的第一条指令push的是一个指针,指向的是VBHeader结构体。跟随到数据窗口:

找到GUITable 成员:

继续跳到0x406868处:

可以看到这里根据窗口分为了两个块每个块的大小就是一个GUITable的结构体的大小(0x50)

箭头所指的地方就是窗口出现的顺序,这里我修改了两个窗口的出场顺序,将序列号窗口放到了前面,而当执行完序列号窗口后就会直接退出程序,也就是Neg消息窗口根本就不会出现了。

将修改后的程序进行保存,我将修改后的程序命名为AfKayAs.banNeg,运行一下会发现已经不会弹出Neg窗口了。

动调分析算法:

1.定位关键跳转:

还是跟之前一样的老套路,采取的方式就是查找参考字符串:

双击跟踪字符串“You Get Wrong”根据跳转到错误字符串箭头定位到关键判断代码(当然也可以直接跟踪正确字符串,都可以)

2.分析校验算法:

在确认关键的判断点后下个断点,然后直接翻到构建栈帧的地方下断点,F9直接运行,用户名输入“hahaha”和序列号为“123456”。这些操作就不一一截图了,这些东西包括如何定位一些关于序列号生成算法的主要指令在之前的crack分析中都已经详细的走过了(主要是本人懒,可以看一下之前crackme的第二题学习笔记,那里面有讲)。这里直接贴关于序列号生成算法的主要流程:

首先是获取用户名长度

后面三条指令一个是将name长度放入edi,然后是将name存入ecx,第三条是将name的长度乘上0x15B38在存入edi

大概就是edi=name的长度*0x15B38+第一个字符的ASCII码

然后将算出来的值转换成十进制,我算出来的值为533432:

将算出来的值转换成浮点型之后加上2.0然后以浮点的形式表示:

之后还是将之前那个数放入ST0并乘上3,再减去2,等于1600300

最后将1600300减去负的15得出最后的序列号“”:

验证一下:

注册机编写:

1.确认整个算法逻辑:

这个算法的验证过程虽然有用到浮点数但作用仅在于影响我们分析,基本的逻辑和上一个crack差不都,都是取name的第一个字符,验证过程如下:

  • 取出name的长度将其乘上一个固定的值(0x15B38)
  • 再加上name的第一个字符的ASCII码值,再加上2
  • 之后再乘3减2
  • 最后再加上15,得出来的值为序列号
2.注册机:
#include"stdio.h"
#include"stdlib.h"
#include"string.h"
int main()
{
	char str1[10] = "";
	int len,seriali;

	puts("Please Input Your Name:");
	scanf("%s", str1);
	len = strlen(str1);
	len = 0x15B38 * len;
	seriali = (len + str1[0] + 2) * 3 - 2 + 15;

	printf("%d",seriali);

	return 0;
}

搞一搞:

猜你喜欢

转载自blog.csdn.net/weixin_46175201/article/details/133340945
今日推荐