0%

双机调试

双机调试

1.打开源代码文件a.cpp

2.!process 0 0 进程名

3.显示eprocess结构体地址,输命令 .process /i /r /p 地址

4.g

5..reload /user /f

6.在cpp下断点,g

双机调试应用实例:

ps(调试的时候记得看microsoft file有没有文件;virtualKD:将target64复制到虚拟机,然后启动程序)

首先虚拟机按f8关闭强制签名

运行AutoRun.exe,然后在Windbg打开源代码文件,再break(注意这里打开的exe与cpp一定要是同一个文件)

!process 0 0 进程名(显示进程名)

img

显示eprocess结构体地址,输命令 .process /i /r /p 地址,然后输入g继续运行,再.reload /user /f加载符号

在cpp中下断点,断在CreateService函数的位置,一步一步调试到要到的位置,这个位置的选定要结合IDA,首先IDA打开CreateService的dll,下图中圈出来的前三个是该函数的开头字节码。

img

下图是该函数的伪代码,调试发现函数进的是箭头指向的函数,所以直接动态调试进该函数,我们动态调试的目的是需要看该函数的第二个参数是什么。但是由于参数地址后面加了3090,所以在静态分析的时候不能准确判断。

img

img

在windbg中动态调试查看第二个参数压入栈的寄存器的值,在IDA中找到该地址的值,就可以得到我们要的函数字节码

img

对比发现是4800h开头的值

img

img

未处理的异常

用户模式情况下会崩溃该进程,内核模式下会崩溃系统,导致蓝屏(实质上是一种保护机制,如果允许代码继续执行可能会造成一些不可逆转的损坏)。

终止

进程会自动释放内存和资源,包括关闭句柄,没有资源泄露。而对于内核驱动来说,如果没有释放资源,这些资源不会被自动释放,只有在下一次系统启动时才能被释放。

函数返回值

要注意处理内核模式下的函数返回值,因为一些简单的API函数也有可能失败,失败造成的结果相比用户模式比较严重。

IRQL

中断请求级别,当一个用户模式代码正在执行时,总是为0;内核模式下大部分时间为0.

C++使用

C++作为一种语言几乎完全支持内核代码,C++的一个术语RAII(资源获取就是初始化),是C++常用的管理资源、避免内存泄漏的方法,保证在任何情况下,使用对象是先构造对象,然后析构对象。但是在内核钟没有C++运行,所以一些C++特性不能被使用:

  • new和delete不被支持并且不会被编译,这是因为它们的正常操作是从用户模式堆中分配,这和内核无关。但是内核API会有替换函数,但是模仿的函数更类似malloc和free,

  • 将不会调用具有非默认构造函数的全局变量,当然这些情况可以通过这些方式避免避免使用构造函数中的任何代码,而是创建一些要从驱动程序代码中显式调用的Init函数只将指针分配为全局变量,并动态地创建实际实例。编译器将生成正确的代码来调用构造函数。

  • C++异常处理关键字(try,catch,throw)不进行编译。这是因为C++异常处理机制需要它自己的运行时,异常处理只能使用结构化异常处理(SEH)来完成——这是一种处理异常的内核机制

  • 在内核中无法使用标准的C++库。

严格地说,驱动程序可以用纯C语编写,没有任何问题。如果您更喜欢使用该路由,请使用具有C扩展名的文件,而不是CPP,这将自动调用C编译器。

2.Debug与Release

​ Debug默认不适用优化但是更易于调试。Release利用编译器优化来尽可能快地生成代码,在内核中是checked\Free.从编译角度看,内核调试定义符号DBG,并将其设置为1(用户模式定_DEBUG),这实际上是KdPrint宏所做的:在debug构建中,它编译为调用DbgPrint,而在Realse中,它什么都不编译,所以vKdPrint调用在Realse中没有影响。

3.内核API

​ 内核驱动程序使用从内核组件导出的函数,大多数都是在内核模块本身(NtOskrnl.exe)中实现的,但有些功能可以由其他内核模块实现,比如HAL(hal.dll)。

​ 大多数函数都以实现该功能地组件前缀开头,比如Nt,Ex。

​ 如果内核驱动程序需要调用系统服务,则它不应该受到对用户模式调用者施加的相同的检查和约束。这就是Zw函数发挥作用的地方。调用Zw函数会将前面的调用者模式设置为KernelMode(0),然后调用本机函数。

img

​ 调用Zw函数将前一个调用者模式设置为KernelMode(0),然后调用本机函数。例如,调用ZwCreateFile将上一个调用方设置为kernelMode,然后调用NtCreateFile,从而使NtCreateFile绕过一些安全性和缓冲区检查,否则将执行这些检查。

内核驱动都推荐用Zw,可以避免不必要的检查。

img

4.函数和错误代码

​ 大多数内核API函数返回一个指示操作成功或失败的状态。这是一个类型NTSTATUS,一个有符号的32位整数。值STATUS_SUCCESS(0)表示成功,负值表示存在某种错误。

​ 大多数代码路径都不关心错误的确切性质,因此测试最重要的位就足够了。这可以通过NT_SUCCESS宏来完成。

img

​ 在某些情况下,NTSTATUS将从函数返回并最终到用户模式。在这些情况下,STATUS_xxx值将通过GetLastError函数转换为用户模式可用的某些ERROR_yyy值。请注意,这些都不是相同的数字。首先,用户模式下的错误代码具有正值,其次,该映射并不是一对一的。

5.字符串

​ _UNICODE_STRING,操作该结构通常用Rtl开头的函数。

img

6.动态分配内存

​ 驱动通常都需要动态分配内存,驱动一般提供两种内存池,

​ 分页池—如果需要,可以调出的内存池。

​ 非分页池—从不分页并保证保留在RAM中的内存池(慎用)

​ 某些函数中的tag参数允许用4字节值标记分配。通常,此值最多由4个ASCII字符组成,这些字符在逻辑上标识驱动程序或驱动程序的某些部分。这些标记可用于指示内存泄漏—如果在卸载驱动程序后仍保留任何带有驱动程序标记的分配。

7.链表

​ 内核在很多内部数据结构中使用循环双链表。系统上的所有进程都由EPROCESS结构管理,连接在一个循环的双链表中,双链表的头存储在内核变量PsActiveProcessHead中。

image-20220425160841942

​ CONTAINING_RECORD是一个宏,根据一个结构体变量成员的地址来获取该结构体变量的地址,成员变量的地址-成员变量和结构体首地址间的偏移量,就是结构体的首地址了。

1
CONTAINING_RECORD (entry, PDO_DEVICE_DATA, Link);

​ entry是一个PDO_DEVICE_DATA结构中Link成员的地址,表达式得到的是其所在结构体变量的地址。
​ LIST_ENTRY是一个链表节点成员,一些结构中会定义一个此类型的成员,以便把很多结构体变量连成一个链表。在自定义结构中,最好把链表节点成员定义在最前,这样该成员的地址就是结构体的地址,不需要这样转换。

8.驱动对象

​ Driver在启动时对Driver_Object初始化,MajorFunction指定驱动支持哪些操作比如读写创建等等,通常这些都需要IRP_MJ前缀以前的MajorFunction队列由内核初始化,一个驱动至少要有Creat和Close操作。

img

9.设备对象

​ 客户端与驱动程序对话的实际通信端点是设备对象,要与Driver_Object通信,必须创建一个DeviceObject与之连接,一个驱动对象可以有多个设备对象,以链表方式存在。

​ CreateFile函数第一个参数是文件名,其实应该是驱动对象名。CreateFile的file其实意味着文件对象。打开一个文件或者设备句柄会创建内核结构FILE_OBJECT的实例。更准确地说,CreateFile接受一个符号链接(一个知道如何指向另外一个内核对象得内核对象。类似文件的快捷方式)。

​ 大部分的符号链接都在这个Global??目录里,指向设备目录下的内部设备名称。用户模式调用者不能直接访问此目录中的名称。但是内核调用者可以使用IoGetDevice对象指针API访问它们。

​ 驱动程序使用IoCreateDevice函数创建设备对象。此函数分配并初始化设备对象结构,并将其指针返回给调用者。设备对象实例存储在DRIVER_OBJECT结构的DeviceObject成员中。如果创建了多个设备对象,它们将形成一个单独的链接列表,其中DEVICE_OBJECT的成员NextDevice指向下一个设备对象。注意,设备对象插入到列表的顶部,因此创建的第一个设备对象最后存储;其NextDevice指向NULL。

img

1.编写驱动程序

img

2.加载驱动

创建服务

sc create sample type= kernel binPath= c:\dev\sample\x64\debug\sample.sys

查看注册表创建成功

img

驱动签名

bcdedit /set testsigning on

(依旧不能解决,需要在bios禁用签名,但是其实在设置中重启就可以解决)

设置-更新和安全-恢复-立即重启-疑难解答-高级选项-启动设置

以上,其实最好的方式是用virtualkd工具,在进OS一开始就关闭强制签名(f8)

1.进程

​ 包括可执行代码和数据、主Token、私有句柄表、私有虚拟地址空间、线程。

img

2.虚拟内存

img

​ 页:虚拟内存单元,通常4kb,大页2MB(\x64\x86\ARM64)、4MB(ARM),大页面一般用TLB.(状态:free、committed、reserved)

​ 系统空间:是内核本身、硬件抽象层(HAL)和内核驱动程序加载后驻留的地方。因此,内核驱动程序会自动受到保护,不受直接用户模式的访问,这也意味着它们可能会对整个系统产生影响。例如,如果内核驱动程序泄漏内存,即使在驱动程序卸载之后,内存也不会被释放。用户模式的进程则不会。

3.线程

​ 一个线程包括:

•当前访问模式,用户或内核。

•执行上下文,包括处理器寄存器和执行状态。

•一个或两个堆栈,用于局部变量分配和调用管理。

•线程本地存储(TLS)阵列,它提供了一种使用统一访问语义存储线程私有数据的方法。

•基本优先级和当前(动态)优先级。

•处理器关联,指示允许线程在哪个处理器上运行。

​ 一个线程至少有一个堆栈驻留在系统(内核)空间(12kb(x86)、24kb(x64)),一个用户模式进程在进程用户空间地址范围有第二个堆栈(默认可以到1MB)。用户模式堆栈从提交的一小部分内存开始,身下的堆栈地址空间作为保留,不会以任何方式分配(以防线程的到吗需要更多的堆栈空间,从而能扩展堆栈)。实现这一点是通过保护页,提交部分的后面下一页(or more),线程如果需要更多的堆栈空间他将写入保护页,将保护页抛出由内存管理器处理的异常

img

伪装成白名单的方法

程序本身不在白名单,此时需要伪装白名单的方式来伪装成白名单的调用,使用方法就是伪装进程PEB

PEB结构:进程环境信息块,该结构中存放了进程信息,每个进程都有自己的PEB信息。通过修改目标进程的PEB结构中的路径信息和命令行信息为想要伪装的对象一致,就可以将目标进程伪装成想要伪装的目标。实现原理:

  1. 通过NtQueryInformationProcess函数获取指定进程PEB地址。因为该进程与我们的进程可能不在一个进程空间内,所以需要调用WIN32API函数ReadProcessMemoryWriteProcessMemory函数来读写目标进程内存。
  2. 根据PEB中的ProcessParameters来获取并修改指定进程的RTL_USER_PROCESS_PARAMETERS信息,这个结构体中保存了PEB的路径信息、命令行信息,修改之后,即可实现进程伪装。

编程遇见的堆栈问题

内存申请中的堆和栈

堆:堆是在程序运行时,而不是在程序编译时,申请的某个大小的内存空间。即动态分配的内存,对其访问和对一般内存的访问没有区别。堆是应用程序在运行的时候请求操作系统分配给自己的内存,一般是申请/给予的过程,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收

栈:由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈

作用:暂时存储数据的地方,用来在函数调用的时候存储断点
栈在程序的运行中有着举足轻重的作用。最重要的是栈保存了一个函数调用时所需要的维护信息,这常常称之为堆栈帧或者活动记录。堆栈帧一般包含如下几方面的信息:
1.函数的返回地址和参数
2.临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量

内存中的栈区处于相对较高的地址。如果以地址的增长方向为上的话,栈地址是向下增长的,栈中分配局部变量空间。

而堆区是向上增长的用于分配程序员申请的内存空间。

另外还有静态区是分配静态变量,全局变量空间的;只读区是分配常量和程序代码空间的;以及其他一些分区。

img

一个C/C++程序占用的内存如下:

1、栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事
3、全局区、静态区(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 -程序结束后由系统释放
4、文字常量区:常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区:存放函数体的二进制代码。

申请内存后的响应

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

申请大小的限制

栈:在Windows中,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 **
堆:堆是
向高地址扩展的数据结构,是不连续**的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

申请效率的比较

由系统自动分配,速度较快。但程序员是无法控制的。
是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,既不是在堆,也不是在栈,是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

堆和栈中的存储内容

栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。**注意静态变量是不入栈的。 **
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

Using IRP and local alignment method to detect distributed malware

  • 分布式恶意软件注入:新型的规避技术,通过将恶意软件分成块,然后将块注入到良性进程中,每个块相互通信,可以依次实现出完整的恶意行为(2016年提出,一种新型的代码注入

  • 方法:用I/O请求包序列特征结合生物信息学中的局部对齐算法

    规避技术:环境监察,模拟攻击,代码重用,代码注入

    解决:基于特征(API序列、恶意行为、整体学习)的动态检测

  • 仿真(模拟)器:为了使得恶意软件分成的块的顺序执行,当运行其的进程或者代码块关闭的是偶,由该模拟器运行的数据会被传输到其他模拟器,然后恶意程序又会生成新的模拟器(确保在系统中模拟器的数量

  • 拆分方法

    1.BBS(基本块分割)

    根据IDA中识别的基本块分割恶意代码,是以控制流指令结束的完整代码块

​ 2.BAST(低于AV签名阈值)

​ 其分块大小小于第一种大于第三种,大小可以根据配置选择

​ 3.偏执型分块

​ 每个基本块仅仅包含一条指令

  • 局部对齐算法

    原理是基于最长公共序列和动态编程,其并不是要在两个序列之间执行完整的序列对其,而是在序列上的高度相似度

在这篇文章中,字母代替特定的驱动与IRP请求,类似于生物学上地蛋白序列,采用的是属于某个驱动器地IRPs,这样在执行特征向量化地时候,IRP序列地鉴别可以变得更高

​ 本文将一个完整的系统IRP序列设置为一个可疑序列,并将恶意软件IRP序列作为一个样本序列。将可疑序列与样本序列进行了比较。如果样本序列按可疑序列顺序一致,且匹配字符超过样本序列本身的80%,则标记为“检测到”

  • 局限

    正常下载器的IRP序列在模式方面与恶意软件下载器的IRP序列非常相似。在实验中,无法有效区分下载软件与正常下载软件的区别。下载器的行为也类似于一些后门恶意软件和木马恶意软件,这在识别过程中很容易产生假阳性。行为类似的恶意软件,或一些行为类似于恶意软件的良性软件,可能会导致误报,这也是基于IRP序列的检测方法的一个缺点。此外,使用本地对齐来检测分布式恶意软件也没有弹性。当恶意软件的行为与原始观察结果不匹配或出现未知恶意软件时,无法有效检测到。

    通过实验验证,本文提出的IRP序列特征可以使恶意软件的检测准确率达到93.4%。同时,利用沙盒检测被真实恶意软件修改的分布式恶意软件,证实了该方法可以有效地检测大多数类型的分布式恶意软件,使分布式恶意软件的检测准确率达到93%。检测性能优于最近的类似研究。由于本文提出的检测方法对具有类似行为的恶意软件有较高的误报率,并且需要提高检测精度,我们将尝试在今后的工作中加以解决。

KimSuky组织样本分析

MD5:ae986dd436082fb9a7fec397c8b6e717

SHA1:31a0168eb814b0d0753f88f6a766c04512b6ef03

SHA256:3110f00c1c48bbba24931042657a21c55e9a07d2ef315c2eae0a422234623194

1. 行为分析

image-20210428092813810

图标伪装成ESET杀毒软件,样本名字为eset_update.exe,伪装成升级程序。

打开火绒剑进行进程过滤 (过滤->进程过滤->添加->开启监控),双击样本后程序运行如下

image-20210428093420835

可以看到火绒剑在这里高亮了蓝色,这个是把C:\Users\Administrator\AppData\Roaming\目录下的程序加到开机启动项,前面的几步就是把释放的程序放到这个目录下

image-20210428093654954

有一个网络连接,应该是要和远程的一个网站进行交互

image-20210428094217330

2.PE结构和IDA分析

#### 2.1PEiD与Keygener

首先用PEiD查壳,显示32位程序无壳。

image-20210428094601103

image-20210428100058824

在Keygener中发现有Base64和CRC32怀疑程序内有一定的加密措施,后续直接拖到IDA分析

2.2 IDA分析

2.2.1导入表分析

可以看到关于文件的操作以及网络连接,印证了火绒剑行为分析

image-20210428101855133

image-20210428101618564

2.2.2 WinMain函数分析

image-20210428102245344

winmain函数中可以看到首先是创建了一个互斥对象,应该是为了创建相关进程或者检测相关进程是否存在,getlasterror判断指定的本地组是否存在

image-20210428102523874

2.2.3 sub_4011E0函数分析

进入函数后有一堆加密过的字符串和一堆GetProcAddress函数调用,猜测该函数为加载dll和相关字串解密获取相关api函数地址

image-20210428103256337

用OD动态调试查看加密的DLL名字,打开OD发现基址是00480000,所以在4811E0处f2下断点,f9直接运行到函数处调试,可以看到导入的3个dll(WINNET.dll,urlmon.dll,kernel32.dll)image-20210429095022290

前两个dll是获取了与网络相关的函数,kernel32获取了CreateToolhelp32SnapshotWinExec

image-20210429095622308

2.2.4 sub_403600函数分析

image-20210429150725499

image-20210429151023968

image-20210429152301294

先在OD里面一步一步看看这个函数里面的各个函数要做什么,参数是什么,注释在下图,这个函数功能主要是将自身复制到特定目录,然后操作注册表实现自启动,最后弹窗欺骗用户ESET升级成功,电脑已被保护。

image-20210429161950264

2.2.5 401580函数分析

​ 主要功能为获取本机网卡信息和执行获取磁盘序列号,以及随机数的生成,通过GetAdaptersInfo获取网卡配置和ip信息,后又通过GetVolumeInformationA来获取磁盘序列号,最后通过GetTickCount来生成随机数

image-20210429165557251

2.2.6 401770函数分析

该函数主要为获取系统版本信息的获取和操作系统的位数,通过GetVersion函数获取相关系统版本信息,然后通过GetNativeSystemInfo,格式化一下,然后做数学运算应该是一些加密操作。

image-20210429170323853

2.2.7 402790函数分析

和前面一样,先OD动态调试查看解密后的值等

image-20210429170936573

image-20210429171202113

image-20210429172418964

函数功能为注册表信息修改,然后将存储在qword_41A620处的恶意url写入注册表(此处先猜测该域名为C2服务器域名

image-20210429172627346

2.2.8 CreateThread分析

进入线程函数中查看线程函数完成的功能,首先是sub_402F30,前半部分构造http请求包

image-20210429193038167

用OD调试发现后半部分是网络请求设置(头,方式,协议)

image-20210429194705684

后续就是从C2服务器接受数据,然后Base64解码传输的数据,在进行多字节到宽字节的转换,把传输过来的数据经过一系列的运算来到sub_401AA0函数,

image-20210429195608233

但是因为C2服务器现在无法连接了,所以动态调试没法接受传输过来的数据,下面分析sub_401AA0函数,由于无法动态调试,所以无法知道加密的字符串是什么,查找网上的博客知道是rundll32.exe.image-20210429203028478

下面对C2服务器收到的数据通过”|“分割,依靠动物名字下达指令(核心功能远程控制)

image-20210429203440338

比如tiger为cmd操作,mongkey为创建进程执行下载程序。

image-20210429203656736

3.总结

  • 样本通过sub_4011E0调用解密函数解密一部分关键api,加载了WININET.dll、urlmon.dll、
    kennel32.dll,并通过GetProcAddress函数获取相应函数地址,函数如下InternetOpenA、InternetCloseHandle、InternetConnectA、HttpOpenRequestAHttpSend、RequestAURLDownloadToFileA、DeleteUrlCacheEntryA、WinExec、CreateToolhelp32Snapshot、InternetReadFile、InternetQueryDataAvailable。

  • 通过sub_403600函数实现文件自拷贝,自启动设置,程序弹窗设置。

  • 通过sub_401580获取主机网卡和ip信息。

  • 通过sub_401770获取系统版本信息的获取和操作系统的位数。

  • 通过sub_402790修改注册表写入恶意url。

  • 通过sub_931AA0实现相关的远程注入。

  • 该样本代码中加入了一系列动物名称,例如tiger、wolf等,属于KimSuky家族系列。

个人体会

这次样本分析发现该样本其实用到的函数在项目中经常见到,所以分析起来比较顺手,知道各个API在用的时候企图达到什么样的目的。并且在这一次分析用到了OD,并且学会了OD动态调试查看dll调用情况以及加密的参数值。

简单的间谍木马分析

0.前言

此篇笔记记录一下分析实战样本11_1的样本,技术已经过时,只是为了了解一下间谍木马的特征和行为。

1.静态分析

  • 由下图可知程序没有加壳是一个控制台程序。

image-20210416161119304

  • 查看导入表,可以看到对资源节操作的API,还有一些注册表相关的API

image-20210416162024328

image-20210416162311061

  • 使用ResHack查看资源数据, 可以发现一段未经加密的PE数据, 后续会分析这段PE

image-20210416163141671

  • IDA查看字符串,可以看到GinaDll,Windows的开机密码认证模块一般是由Gina DLL完成的。

    image-20210419092154616

    2.火绒剑行为检测

    查看行为,发现样本在同目录下释放了一个dll,经过对比发现,释放的dll就是资源中的PE数据(为什么),然后对注册表写入dll的路径,并且最后摧毁了自己的进程。

    image-20210419103650288

    image-20210419094915222

    3.IDA分析

    3.1 main分析

    ​ 观察main函数代码, 可以发现程序首先通过GetModuleFileNameA获取自身程序的全路径, 在调用401080后, 然后通过_strrchr截取\之前的内容, 与\\msgina32.dll进行字符串拼接 然后将拼接后的字符串作为参数传入堆栈调用401000

    image-20210419095821170

    ​ 接下来分析401080与401000

    3.2 401080分析

    ​ 可以明显看到资源节数据的操作,然后通过 VirtualAlloc 申请一段内存空间用于存放资源中的PE数据, 在将资源中的数据取出后, 通过一系列文件操作将PE数据保存到xx\msgina32.dll文件中, xx的路径就是main函数中通过GetModuleFileNameA取出的当前程序的运行路径。这里有个函数为401299, 这个函数就是类似printf的打印输出函数,这里我们将这个函数重命名, 防止后面的分析中再次出现 , 可以节省时间。

    image-20210420110324830

    ​ 到这里401080这个函数功能已经确定了, 取资源节数据并写入文件 , 文件名为msgina32.dll, 路径为程序同路径。

    3.3 401000分析

    ​ 该函数的参数是释放出的dll文件的全路径,可以看到一系列注册表操作, 函数功能是将dll全路径写入注册表路径为SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon, 名为GinaDLL的注册表项。

    image-20210421092749755

    image-20210421092844415

    3.4 exe总结

    ​ 整个exe的功能就是为了持久化安装dll文件:将自身资源中的PE数据写入与自身路径相同, 文件名为msgina32.dll的dll文件;将dll文件写入注册表;程序结束。

    ​ 所以说重要功能应该在dll里面,exe文件只是为了dll文件服务。

    4.IDA分析dll

    4.1 dllmain分析

    ​ 将dll文件拖入IDA,查看dllmain函数,首先是DisableThreadLibraryCalls禁用指定的DLL的DLL_THREAD_ATTACHDLL_THREAD_DETACH通知,这样可以减小某些程序的工作集大小。

    ​ 通过GetSystemDirectoryW获取系统目录, 与\MSGina拼接后获取正常的GinaDll , 然后通过LoadLibraryW加载获取dll句柄, 存入全局变量hLibModule, 然后dllmain函数执行结束

    image-20210421094451437

    根据前面exe的注册表操作可以知道,这是为了替换正常的GinalDll,

    4.2 dll导出函数

    查看dll导出函数,依次查看导出函数代码, 发现绝大多数导出函数都调用了10001000函数, 这个函数很明显用于导出函数的转发。

    image-20210421095103186

    DllRegisterDllUnregister也是很简单的函数, 对于注册表值的安装/卸载操作,通过查询百度相关文档信息, 我们知道WlxLoggedOutSAS是在windows注销时会调用的函数,接下来看看它想在注销时做什么。

    4.3 WlxLoggedOutSAS分析

    image-20210421100642564

    观察代码, 样本首先调用10001000进行正常功能的转发, 然后调用edi, 通过回溯寄存器(如何?), 我们知道了edi是正常的WlxLoggedOutSAS, 然后样本执行了10001570函数, 观察参数发现一个有趣的字符串UN %s DM %s PW %s OLD %s, 这种携带占位符的字符串常用于输出, 替换, 拼接等, 接下来我们就需要看看10001570处的函数

    4.4 10001570分析

    首先将传入的参数通过_vsnwprintf进行格式化, 然后打开文件msutil32.sys, 接着格式化时间, 然后调用fwprintf将数据写入流中, 通过寄存器回溯, 我们可以找到esi来源就是wfopen的返回值, 也就是代表文件msutil32.sys

    然后调用FormatMessageW转化一些数据, 观察参数大致能猜出来是系统调用时传递的数据. 最后关闭文件,结束程序。

    image-20210421100908023

    4.5 总结

    ​ 根据分析, 整个dll的作用就是劫持WlxLoggedOutSAS函数, 获取到函数参数后加入一些例如时间戳信息后写入文件msutil32.sys, 这也说明这个sys文件并不像它表现出的是个驱动文件, 而是一个简单的日志文件。

    ​ 由于GinaDll在新系统中已经不再使用, 因此想要复现需要XP或者更旧的系统, 这样在点击注销后, 样本会记录用户的账户密码, 也就是个最简单的间谍木马

样本名称:彩虹猫

MD5:19DBEC50735B5F2A72D4199C4E184960

SHA1:6FED7732F7CB6F59743795B2AB154A3676F4C822

实验平台:Windows10

分析工具:PEiD v0.95、StudyPE x64 v1.11、IDA x32 v7.0、VMWare WorkStation v16.2


一、运行观察以及信息收集

​ 双击病毒样本后接连弹出两个警告弹窗(告知用户危险)。点击确定后,桌面会慢慢出现一些诡异的现象,比如:

  • 自动弹出多个浏览器搜索窗口

  • 鼠标晃动异常

  • 窗口颜色怪异

  • 反复出现系统提示音

  • 出现六个病毒进程

​ 尝试关闭任意一个,或者手动关闭计算机,都会遭至大量弹窗随后蓝屏,接着Windows无法正常启动,只会循环播放一只彩虹猫附带欢快的背景音乐,无论重启多少次都是如此。运行观察结束,用基本的可执行文件信息查看工具分析。

1.PEiD

image1

​ 监测到该样本可能使用了ASProtect v1.32进行加壳或加密

2.StudyPE+

​ 查看样本导入表,可以看到很多对样本分析有用的函数,我们将函数与之前观察到的样本运行行为相关联。

image2

  • 自动弹出多个浏览器搜索窗口———————-ShellExecute

  • 鼠标异常晃动————————————–SetCursorPos、GetCursorPos

  • 桌面出现奇怪图标———————————DrawIcon

  • 窗口颜色怪异————————————–BitBlt、StretchBlt

  • 反复出现系统提示音——————————-PlaySoundA

  • 出现6个病毒进程——————————–ShellExecute

其他函数:

​ OpenProcessToken、AdjustTokenPrivilege、LookUpPrivilegeValue 给进程提权

​ GetMessage、TranslateMessage、DispatchMessage 建立消息循环

​ CreateToolhelp32Snapshot、Process32First、Process32Next 遍历进程

​ SetWindowHookEx、UnhookWindowHookEx、CallNextHookEx 给窗口下钩子

​ LoadLibrary、GetProcAddress 加载库并导入函数

二、入口部分

​ 使用IDA载入样本,定位到入口函数Start并按下F5获得伪代码,但是这些伪代码只是IDA根据现有信息猜测出来的,并不能保证100%准确,有的时候甚至会对分析工作产生误导,一会就能看到。

image-20210412162508459

​ 在start函数的起始部分调用GetCommandLineW、CommandLineToArgvW获取进程参数,之后整个Start函数就根据进程参数分成了几大块,整理后其结构大致可分为如下

4

​ 我们第一次双击样本的时候是不带参数的,所以先看无参数也就是else部分

三、无参数部分

​ 此处先是用MessagexBox依次弹出两个警告信息提示框,正是我们运行病毒时所看到的警告。若两个弹窗都被用户确认,使用GetModuleFileNameW获取当前进程的路径并保存,当然所保存的空间由LocalAlloc提前申请。接着用do while执行5次循环,每次循环都调用一次ShellExecuteW,参数正是刚才得到的样本进程路径,以及字符串**”/watchdog”,即以“/watchdog”**为参数,生成5个病毒exe进程。

image-20210412163127104

​ 在最后调用了ShellExecuteExW“/main”为参数生成了一个MEMZ.exe进程,一般带Ex后缀是加强版函数,表示在普通版函数基础上进行了功能的扩展,这里特意用了一个功能更强的函数一定有特别的目的。该函数接收一个SHELLEXECUTEINFOA结构体,结构体中设定了进程路径和执行参数,关键之处在于紧接着调用了SetPriorityClass,当中对hProcess成员复制为0x80,查看MSDN得知该值意味着的进程将拥有最高的响应优先级,最后调用ExitProcess使当前进程结束。

image-20210412163340505

image-20210412164320031

​ 现在知道在观察阶段看到的6个exe进程是怎么来的了:最开始双击生成了一个原始进程,原始进程生成了5个watchdog进程和1个/main进程,然后原始进程迅速结束了自己,等打开任务管理器查看时,还存在6个exe进程。

四、“watchdog”部分

1.主体部分

image-20210412164339025

​ 首先是CreateThread创建一个线程,IDA给自动命名为sub_40114A。以watchdog为参数运行的MEMZ.exe进程是有5个的,也就是说这里看到的创建一个线程是“某个进程“的行为,5个进程一共应该创建了5个线程。进入sub_40114A看看这个线程干了些什么事。

2.子函数sub_40114A

​ 前三个函数用来获取当前进程路径(这种多个函数配合起来做事的情况有很多,常常是最后一个函数完成关键功能,前面几个都是为它提供必要的参数),比如LocalAlloc申请空间用来存路径字符串,GetCurrentProcess获取当前进程句柄,这都是第三个函数要用到的。

​ 接着一个大的while循环把后面的代码都包了进去,该while循环的条件永远为真,说明是个死循环。在循环内部使用CreateToolhelp32Snapshot给进程拍快照,再用Process32FirstWProcess32NextW进行遍历,我们在观察阶段查看导入表时也发现了这三个函数,这里就用上了。

​ 每当遍历到一个进程时都要获取它的路径,并和之前获取的路径对比,如果相同就让计数器加1,很明显这就是在看当前有多少个exe的进程。当遍历完所有进程后,就统计出了exe的进程个数。然后由于Sleep函数的存在,死循环每隔一段时间就能统计出当前exe进程的个数并存放在v4变量中,然后先判断v4和v7的大小,再让v7保存v4的值。

​ 如此就形成了一个监测机制,v7永远只保存v4最大的值,一旦v4的值小于了v7就会被if语句监测到,并进入sub_401021子函数。这正好和我们观察阶段的发现一致。只要exe进程数量减少,系统就会蓝屏重启,这就是它的监测原理。反过来我们可以推测sub_401021就是完成系统蓝屏重启功能的。

image-20210412165606391

3.子函数sub_401021

​ 在这个子函数的开头是一个do while循环,循环次数为20次,用CreateThread创建线程,这没什么特别的,像往常一样我们双击StartAddress准备去看这个线程做了什么事,但是双击过后却没有任何反应,这其实是IDA的代码识别出现错误。(后续补充

​ 在这里为了不影响我们的分析主线,直接键盘按G输入地址4010FE就可以跳到StartAddress的代码。该处的MessageBoxA用于弹出消息,有26条消息保存在lpText所指向的地址中。使用sub_401A55获取随机数保存在v3,v3对0x1A取余以实现在26条消息中随机选取一条并显示。

​ 而SetWindowsHookExUnhookWindowsHookEx用于给窗口下钩子,干了什么事要到回调函数fn里去看。code==3表明目标窗口即将被创建,此时lParam表示该窗口的基本信息(坐标、大小等),修改这些信息可以在窗口真正创建之前生效,下方正是在随机修改窗口的位置。

11

12

sub_401021的后半部分是两种关机的方式:先主动诱发蓝屏关机,如果不成功则主动退出Windows。

​ 前一种方式使用LoadLibrary加载ntdll库,再连续两次调用GetProcAddress获取两个函数地址,这两个函数是未公开的Windows API,只能用这种方式来隐式调用。随后依次调用这两个函数,主动引发蓝屏。很多恶意程序在隐式调用时会对函数名字符串先加密,要用的时候再解密,或者干脆不使用函数名而用函数编号,显然我们分析的样本没有做这种“隐蔽处理”。

​ 后一种方式先用一系列函数给当前进程提权,然后调用ExitWindowsEx主动关闭Windows

image-20210412165900085

​ 总结一下,我们确认了之前的猜想,sub_401021确实是一个强制关机的函数,先创建线程用于弹出大量位置和内容都随机的窗口,再使用蓝屏或退出Windows的方式强制关闭计算机.

4.注册并创建窗口

​ 我们依次分析了函数开头->sub_40114A->sub_401021,是时候回到watchdog的主体部分。这张图之前出现过。在创建sub_40114A后,调用RegisterClassExA注册了一个名为“hax”的用户自定义窗口类型,并用CreateWindowExA将其创建。此处有个小问题,传递给RegisterClassExA函数的结构体变量pExecInfo定义为了SHELLEXECUTEINFOW类型,而不是该函数所需要的WNDCLASSEXA类型,这是IDA的识别出现了错误。

13

5.回调函数sub_401000

​ 创建的窗口有一个回调函数sub_401000。经查询MSDN,常量值16和22分别对应窗口消息WM_CLOSEWM_ENDSESSION,那么整体含义就很明了了:该窗口回调函数会对窗口消息进行过滤,若消息为WM_CLOSE或者WM_ENDSESSION,则调用sub_401021强制关机(这个函数前面刚分析过)。而WM_CLOSEWM_ENDSESSION消息是在系统关机时,由操作系统发送给各个窗口。

​ 若是其他消息,则并不做任何处理,丢给系统默认处理函数DefWindowProcW

​ 如此一来,sub_401021这个强制关机函数在两处被调用。第一个是监测watchdog进程数量,如有减少就调用。第二个是监测用户是否主动关机,如有也调用。这和我们观察阶段看到的完全一致。

image-20210412170948933

6.消息循环

​ 紧接着的代码GetMessage、TranslateMessage、DispatchMessage被包进一个大的while循环,这是常见的操作,叫做“消息循环”。由于在前面创建了窗口,并且还对发送给窗口的消息进行了过滤,意味着我们必须自己写消息循环完成收取消息和派发消息的工作,否则创建的窗口是收不到消息的。Windows并不会自动帮我们完成消息循环。

五、“/main”部分

1.主体

​ 由于开头部分代码和MBR有极强的联系,索性把它划到MBR章节了,到时候再一块说。这里有一个do while 循环,/main部分的分析就从这儿开始。

15

​ 此处的do while循环以v8为计数器,内部用CreateThread创建线程,循环跑10次(v8 < 0xA),一共创建10个线程。这不由地让人好奇,创建这么多线程是要做什么?先别慌跳进去看,它还附带一个参数v9,并被赋予初始值off_405130,每次循环自增2。以off开头表明是某个静态地址,我们双击过去看看。

off_405130地址所指向的是一块数据区,并且这里的数据还有非常强的规律性。每组都由1个dd类型的数据加4个db类型的数据组成,一共有10组。

image-20210412171849504

db: data byte 1字节数据

dw: data word 2字节数据

dd: data dword 4字节数据

dq: data qword 8字节数据

​ 再回过头来看看前面的do while,v9被赋值为off_405130并作为参数传入。Sleep函数里也用到了v9,它将v9看作了数组首地址,并每次取数组的第1项(数组项数从0开始)。每次循环v9都会自增2,由于v9的数据类型是DWORD 每次自增2就是自增两个DWORD的大小,即8个字节。

​ v9的值指向的都是不同的静态地址,如果挨个双击跳过去看,会发现它们其实都是函数地址。而v9[1]是Sleep函数的参数,所指向的肯定是时间量(毫秒)。v9是4个字节的DWORD * 类型,那它指向的当然就是4字节的DWORD类型,所以这些时间量都是4字节的DWORD类型。我们在IDA中右键单击地址就可以选择其解析长度,我们用4字节来解析它。

17

​ 看到这样的结构,很容易让人想到病毒作者写代码时应该用了结构体数组,结构体的构成如下:

1
2
3
4
5
struct tagFuncWithDelay
{
DWORD pFunction;
DWORD dwTimeDelay;
}

​ 10个这样的结构体在一个数组中,现在我们明白了,创建了10个进程,并且每次创建都会有延迟时间,这个延迟时间和进程的函数地址是放在一起的,一共有10组,都在一个大数组off_405130里。

2.子函数sub_401A2B

​ 这个子函数的代码很短:用大数组中的函数地址依次创建线程。由于是函数指针,要用它调用函数当然得声明其参数、返回值、调用方式之类的。

18

​ 这10个函数做的事很简单,我以sub_40156D为例。它调用GetCursorPos获取鼠标指针坐标,然后用各种方式修改这个坐标的值,比如这里调用sub_401A55生成随机数并返回,用这些随机数与鼠标指针坐标做运算,最后调用SetCursorPos来设置鼠标指针坐标,用户就会发现鼠标指针随机晃动了。所有这10个函数都干的类似的事,这里不再依次分析。

19

​ 这里还有一个小细节,10个函数每个的返回值部分都有不同,有的是返回一个随机值,有的是返回一个固定值。返回值被保存在v1,每次循环都会在if语句中判断!v1–是否为真,也即v1自减1后是否为0,控制v1的值就能决定循环的次数,再配合后面的Sleep函数就能决定这10个函数的激活时间。这10个函数中,有的需要一个固定的激活时间,有的需要一个随机的激活时间,只需要控制返回值即v1就可以了。

20

六、MBR部分

1.主体

MBR全称主引导记录(Master Boot Record),整个硬盘最开头的512字节就是它。计算机启动后会先运行MBR里的代码进行各种状态的检查和初始化的工作,然后再把控制权转交给操作系统(简单地讲就是一个JMP指令跳到操作系统的起始代码),Windows就加载启动了。

​ 这个病毒干了一件很可恶的事,它直接把整个MBR覆盖掉了,变成了它自己的代码,那么它想干什么都行了,只要它不主动交出代码执行流程,Windows绝没有启动的机会。

MBR里既有代码也有数据,开头的0-446字节是代码,紧接着就是数据,数据部分记录着硬盘的分区信息,结尾以固定的0x55 0xAA作为结束符(PE文件结构)。

21

​ 对MBR有个初步了解后,回头来看代码。现在先回到/main未分析的部分,我们前面提到过,它和MBR有紧密关联。

​ 首先使用CreateFileA以文件形式打开了主硬盘,也即PhysicalDrive0,这样方便后续的覆盖操作。紧接着用LocalAlloc分配一段以0为初始值的内存空间,并拷贝两段神秘数据到分配的内存空间。这两段数据中第一段byte_402118大小304字节,第二段byte_402248大小1952字节,拷贝时中间跳过了206字节。最后将内存空间的数据覆盖到主硬盘(PhysicalDrive0)的开头部位,原始MBR遭到破坏。

​ 第一段正好就覆盖了MBR的代码区域,所以几乎可以肯定这304字节是代码。第二段数据已经不属于MBR的范围,而且高达1952字节,很可能是用于显示动画的图像数据和音频数据。

image22

​ 一旦将MBR覆盖,侵入也即宣告成功,病毒作者赶紧写了一堆话想要好好炫耀一番,这段话被写入note.txt文件中,并用Windows自带的记事本打开。

image23

遗留问题:IDA伪代码的问题以及MBR调试的问题(MBR运行在16位环境直接和硬件打交道,此阶段Windows根本没加载,OllyDbg或者WinDebug这种常用的调试器当然用不了。这时该用–Bochs(发音同“box”))

参考链接:看雪论坛彩虹猫样本分析