您的位置:首页 > 编程语言

恶意代码分析实战 Lab 10-2 习题笔记

2018-02-24 16:50 609 查看

Lab 10-1

问题

1.这个程序创建文件了吗?它创建了什么文件?

解答: 我们依旧先从静态分析开始,这里我们在第一个导入
DLL
里面注意到的有趣的函数是这个
WriteFile
,说明这个代码会改变这个文件



然后我们看第二个导入
DLL
的函数有哪些



这里有几个我们已经见过好几次的函数,
OpenSCManagerA
是用来打开服务管理器的函数,
StartServiceA
是用来启动一个服务的函数,
CreateServiceA
是创建一个服务的,说明这个代码会在宿主计算机上创建一个服务来运行代码

书中还说了这两个函数
LoadResource
SizeOfResource
,说明这个代码对
Lab10-02.exe
的资源节做了一些操作,我们找找这两个函数



这两个函数是
KERNEL32.DLL
的导入函数,不注意看还是难发现的,然后我们知道了这个代码会操作自己的资源节,那我们就去检查一下这个程序的资源节



这里和书中的类似,发现了一个
FILE
,里面包含了一个
PE
头,正常程序的资源节长什么样,如下



这是
ResourceHacker
的资源节的样子,对比一下就知道区别在哪里了

接下来我们进行基础动态分析,对注册表做快照之后的结果



运行代码之后,增加了6个键,18个值,改变了1个值,增加的有以下



在服务这里增加了一个叫
486 WS Driver
的服务,然后下面就是对这个服务的集体细节进行配置



既然知道了这个代码已经改变了注册表,我们现在追着这个线索来搜索一下
procmon




这里我们发现了一个叫
services.exe
的代码,执行了
RegCreateKey
,而且路径也和我们
Regshot
的结果相同

然后我们缩小搜索范围,搜索这个名叫
services.exe
的程序做了哪些其他事,记住此时这个程序的
PID
656




设置筛选条件为
WriteFile
之后,就会发现这个文件一共写了三个文件,一个是
system.LOG
,一个是
system
,还有一个是
SysEvent.Evt
,然后我们试试查找
Lab10-02.exe
这个进程名字,恶意行为分析本身就很繁琐



这里我们可以看到
Lab10-02.exe
这个文件创建了一个文件在
C:\WINDOWS\system32\conime.exe
,我们继续缩小搜索的范围



这里文件不仅创建了
conime.exe
,还有
apphelp.dll
sysmain.sdb
systest.sdb
,最后当然还有那个
sys
驱动
Mlwx486.sys


然后我们搜搜这个
conime.exe
有没有做过其他操作



这里我们可以看到这个
conime.exe
的所有操作,包括这个进程的启动,这里我们注意到这个
parents pid
,放大一点



这个
512
就是
Lab10-02.exe
PID
,记住这个进程起来的时间是多少



后面的精确时间是
4708081
,我们就可以找到这个程序创建进程的操作



然后这个动态分析大概就只能分析出来这些东西了,书中说,如果你去找那个
Mlwx486.sys
,你是找不到的,但是我们可以找到这个
conime.exe
文件



既然找到了这个文件,那我们就顺便用
IDA
来打开看看



但是这个代码很独特,没有
main
函数,没办法分析,我们就跟着书上的做法

我们获取一下内核驱动的状态

sc query "486 WS Driver"




可以很明显的看出来这个是内核的驱动
(KERNEL_DRIVER)


作者是如何知道这个内核驱动的名字是
486 WS Driver
呢?

我们可以从注册表中可以找到这个字符串的位置,然后也可以从从
CreateServiceA
中看出
(书中说)
,不过我无法找到这个
CreateServiceA
操作





然后还发现这个
conime.exe
有很多注册表修改失败
(BUFFER OVERFLOW)
的操作,估计这就是为啥这个
conime.exe
没有从内核删除自己的原因吧

言归正传,这里我们就会发现这个
486 WS Driver
是个内核驱动,然后状态是还在运行
(RUNNING)


所以这个问题的答案就是创建了
conime.exe
,还有
apphelp.dll
sysmain.sdb
systest.sdb
,最后当然还有那个
sys
驱动
Mlwx486.sys


2.这个程序有内核组件吗?

解答: 现在我们就要连接内核调试器来操作了

WinDbg
里面运行命令

lm


然后仔细找就可以找到这个驱动,这里如果不事先告诉你这个驱动的名字叫
Mlwx486
还真是难找,不过如果你回想刚刚我们查看创建的文件里面,就有一个
Mlwx486.sys


里大家也可以从其他驱动的名字看到我这个虚拟机是用
VritualBox
运行的,所以没必要非要用各种破解版的
VMware
VritualBox
也是很好用的



然后现在我们就可以确定一个名为
Mlwx486
的驱动被载入内存中

然后我们开始查看
SSDT
的修改项

dd dwo(KeServiceDescriptorTable) L100


在这里我们就可以看到这个跳转很多的内存地址
b9887486




然后书中下一步是要把虚拟机恢复成
Rootkit
安装之前的状态来查找这个位置上原来的函数是什么,我们在恢复之前可以看看这个地址
b9887486
上的是什么函数



这个是一个
Mlwx486
里面自带的函数,然后我们重启,并且恢复虚拟机成未运行病毒状态

然后我们运行

dd dwo(KeServiceDescriptorTable) L100




我们找到这个未被改变之前的值,为
80573111
这个值,我们下一步查查这个函数是什么



这个函数原来的位置是
nt!NtQueryDirectoryFile
,然后接下来我们运行这个病毒,开始继续分析这个病毒,现在我们已经运行了病毒,找到那个函数的位置

我们先设置一个断点在
f7a75486
这里

bp f7a75486


然后

g


这时候断点不会马上名字,因为没涉及到查询文件夹的操作,我们回到虚拟机里面,点开
我的电脑
,断点马上就命中了



然后我就开始单步调试,来查明这个函数到底会做什么操作,函数开头的四句都栈初始化的

mov  edi, edi
push ebp
mov  ebp, esp
push esi


下面继续

mov  esi, dword ptr [ebp+1Ch]
push edi
push dword ptr [ebp+30h]
push dword ptr [ebp+2Ch]
push dword ptr [ebp+28h]
push dword ptr [ebp+24h]
push dword ptr [ebp+20h]
push esi
push dword ptr [ebp+18h]
push dword ptr [ebp+14h]
push dword ptr [ebp+10h]
push dword ptr [ebp+0Ch]
push dword ptr [ebp+8]
call Miwx486+0x514(f7a75514)


到这里,函数开始调用函数
Miwx486+0x514
,为了搞清楚这个入参都是什么东西,我们一个一个的分析和调查这个入参,先计算地址的值,然后查看内存地址上的值是多少,比如[ebp=30h]我们可以查到它的值是
0
,然后可以得出下面的关系



这里要注意的是数据结构的存储和识别,比如上面这图的第二段数据为
0348f0e8
,地址是
b8197d64
(b8197d60 + 4)
,其实真实是数据在计算机上是这样的
(倒着存放的)


b8197d64 e8
b8197d65 f0
b8197d66 48
b8197d67 03


知道这点就好办了

mov  esi, dword ptr [ebp+1Ch]
push edi                 = b95a5d64
/* Note: ebp = b95a5d30 */
push dword ptr [ebp+30h] = [b95a5d60] = 0
push dword ptr [ebp+2Ch] = [b95a5d5c] = 80 e1 70
push dword ptr [ebp+28h] = [b95a5d58] = 1
push dword ptr [ebp+24h] = [b95a5d54] = 3
push dword ptr [ebp+20h] = [b95a5d50] = 268
push esi = 0070e198
push dword ptr [ebp+18h] = [b95a5d48] = 68 e1 70
push dword ptr [ebp+14h] = [b95a5d44] = 0
push dword ptr [ebp+10h] = [b95a5d40] = 0
push dword ptr [ebp+0Ch] = [b95a5d3c] = 0
push dword ptr [ebp+8]   = [b95a5d38] = 464
call Miwx486+0x514(f7ab7514)


然后我们根据这个
MSDN
的文档,列出
NtQueryDirectoryFile
的定义

NTSTATUS ZwQueryDirectoryFile(
_In_     HANDLE                 FileHandle = 464,
_In_opt_ HANDLE                 Event = 0,
_In_opt_ PIO_APC_ROUTINE        ApcRoutine = 0,
_In_opt_ PVOID                  ApcContext = 0,
_Out_    PIO_STATUS_BLOCK       IoStatusBlock = 68 e1 70,
_Out_    PVOID                  FileInformation = 0070e198,
_In_     ULONG                  Length = 268,
_In_     FILE_INFORMATION_CLASS FileInformationClass = 3,
_In_     BOOLEAN                ReturnSingleEntry = 1,
_In_opt_ PUNICODE_STRING        FileName = 80 e1 70,
_In_     BOOLEAN                RestartScan = 0
);


这里我们注意这个第八个入参
FileInformationClass
的值为
3
,然后我们按
t
来进入这个函数中

t




这里我调了一下字体,一进来这个函数的第一个代码就是一个
jmp
跳转来跳转到其他地方

jmp dword ptr [Mlwx486+0x580 (f7ab7580)]


注意这里不是跳转到
f7ab7580
,而是跳转到保存在地址
f7ab7580
上那个地址
(注意这里是地址,所以不需要倒过来看,如果这里存的是个字符串,就要倒过来看了)




所以下一个代码就会跳转来到这里



然后这里
WinDbg
已经将这个函数标注为
nt!NtQueryDirectoryFile
,就是那个被替换的函数的本身

函数调用完这个
nt!NtQueryDirectoryFile
之后,因为这个函数是
Windows
官方的函数,我们分析他没什么意义,我们等待这个函数调用完返回,之后就会跳到这里



然后下一个



注意到这个
[ebp+24h]
,他其实就是
FileInformationClass
的值,从这里开始比较这个
FileInformationClass
的值

然后继续看,下一个代码是啥



此时
eax
的值是
0


下一步就是要跳转了

cmp dword ptr [ebp+24h], 3
mov dword ptr [ebp+30h], eax
jne Mlwx486+0x505 (f7ab7505)


jne
的跳转条件是
ZF
=
0
,现在我们查看一下
ZF




所以不会跳转,但是我们可以看看跳转之后这个代码会做什么

这里如果
FileInformationClass
的值不是
3
,就会来到这里执行,然后就返回了



如果
FileInformationClass
的值是
3
的话,继续往下的操作就是

test eax, eax
jl   Mlwx486+0x505 (f7ab7505)


指令
jl
的跳转条件是
SF!=OF
也可以写成
SF<>OF
SF
代表了运算结果的符号,
OF
代表了运行有没有溢出



这两个相等的,也不会跳转

cmp byte ptr [ebp+28h], 0
jne Mlwx486+0x505 (f7ab7505)


这里的
[ebp+28h]
代表的就是刚刚那个
MSDN
结构体的
ReturnSingleEntry
,这里开始比较这个值是否是
0


如果等于
0
,就跳转,然后我们这里就跳转了

mov eax, dword ptr [ebp+30h]


这里将
RestartScan
的值赋值给
eax


然后运行到这里
内核
就退出恶意驱动函数的调用了,因为比较
ReturnSIngleEntry
这里时候,我们实际值是
1
,代码的期待值是
0


总结一下这个函数,函数的全部代码如下

/* 栈初始化开始 */
mov edi, edi push ebp mov ebp, esp push esi
/* 栈初始化结束 */
mov esi, dword ptr [ebp+1Ch]
push edi
push dword ptr [ebp+30h] // RestartScan
push dword ptr [ebp+2Ch] // FileName
push dword ptr [ebp+28h] // ReturnSingleEntry
push dword ptr [ebp+24h] // FileInformationClass
push dword ptr [ebp+20h] // Length
push esi // FileInformation
push dword ptr [ebp+18h] // IoStatusBlock
push dword ptr [ebp+14h] // ApcContext
push dword ptr [ebp+10h] // PacRoutine
push dword ptr [ebp+0Ch] // Event
push dword ptr [ebp+8] // FileHandle
call Mlwx486+0x514 //-> jmp dword ptr [Mlwx486+0x580 (f7ab2580)] -> nt!NtQueryDirectoryFile
xor edi, edi
cmp dword ptr [ebp+24h], 3 // FileInformationClass = 3
mov dword ptr [ebp+30h], eax // RestartScan, 0
jne Mlwx486+0x505 // if [ebp+24h] != 3 -> jmp and ret 2Ch
test eax, eax // eax is NtQueryDirectoryFile return value(success return 0)
jl Mlwx486+0x505 // if eax < 0 -> jmp and ret 2Ch
cmp byte ptr [ebp+28h], 0 // ReturnSingleEntry = 1
jne Mlwx486+0x505 // if [ebp+28h] != 0 -> jmp and ret 2Ch
push ebx //-> p(f7ab2486)
push 8 // function Mlwx486+0x4ca here
push offset Mlwx486+0x51a //-> 'Mlwx'
lea eax, [esi+5Eh]
push eax
xor bl, bl
call dword ptr [Mlwx486+0x590] // standard windows nt function RtlCompareMemory
cmp eax, 8 // eax = 0
jne Mlwx486+0x4f4
|_ mov eax , dword ptr [esi] // [esi] = 0
test eax, eax
je Mlwx486+0x504 // if eax == 0 -> jmp and ret 2Ch
test bl, bl // bl always equal 0
jne Mlwx486+0x500 // if bl != 0 -> jmp back to 'push 8'
mov edi, esi
add esi, eax
jmp Mlwx486+0x4ca // jmp back to 'push 8'
pop ebx
mov eax, dword ptr [ebp+30h]
pop edi
pop esi
pop ebp
ret 2Ch
inc bl
test edi, edi
je Mlwx486+0x4f4
mov eax, dword ptr [esi]
test eax, eax
jne Mlwx486+0x4f2
and dword ptr [edi], eax
jmp Mlwx486+0x4f4
add dword ptr [edi], eax
mov eax, dword ptr [esi]
test eax, eax
je Mlwx486+0x504
test bl, bl
jne Mlwx486+0x500
mov edi, esi
add esi, eax
jmp Mlwx486+0x4ca
pop ebx
mov eax dword ptr [ebp+30h]
pop edi
pop esi
pop ebp
ret 2Ch


书上说,我们刚刚跳转结束那里吗可以设置一个条件断点,当
returnSingleEntry
0
时候,才会中断,然后我们看看这个断点怎么设置

书上是这样说:

bp f7ab2486 ".if dwo(esp+0x24)==0 {} .else {gc}"


这里的
f7ab2486
RootKit
替换的那个
SSDT
地址,每次运行都不会相同

然后这里我们用
dir
命令去查看
C:\WINDOWS\system32\
,书中介绍了为什么不能用资源管理器的原因

不过这里我一直搞不明白,这里为什么是
esp+0x24


这里为了在
ReturnSingleEntry
=
0
时候中断,而
ReturnSingleEntry
的值应该是
[ebp+0x28]
,所以我们这里一般会觉得这个条件中断的语句应该这样写

bp f7ab2486 ".if dwo(ebp+0x28)==0 {} .else {gc}"


但是,书上什么写的
[esp+24h]
,为什么是
esp+24h
,这里我们着重分析一下

首先我们必须要明白,函数在被调用之后,第一步要做的操作就是保存调用着的堆栈信息,就是所谓的函数初始化堆栈,初始化的过程如下(代码截取于上面恶意驱动)

push ebp
mov  ebp, esp
push esi


如果我们画成栈图的话就是如下

(1). 函数执行到
call
语句时候的栈分布,原函数的栈分布

---------  <--- ESP(低地址) <- 地址值为esp1
|    3    |    /|\
|---------|     |
|    2    |     | <- 数据增长方向(地址递减)
|---------|     |
|    1    |     |
---------  <--- EBP(高地址) <- 地址值为ebp1


(2). 函数进入
call
,之后,开始执行初始化指令
(就是上面那三条)
,初始化完之后的栈分布

---------  <--- ESP(依旧指向栈顶)
|   esi   |
|---------| <--- EBP(EBP移动到原来ESP-4的位置) // 因为之前执行了push ebp的操作
|   ebp1  |
|---------| <--- 未执行push ebp操作之前ESP指向的位置,执行完push之后esp往上一格
|    3    |
|---------|
|    2    |
|---------|
|    1    |
---------


这就是函数调用之前的栈初始化过程,明白这点后面就好解释了

由于我们的断点是设置在外面的大循环,在中断的时候,并未执行栈初始化的过程,现在我们设准备调用函数的
旧函数
里面的
ebp
的值为
ebp1
esp
的值为
esp1
,执行
初始化
之后的调用函数l里的
ebp
值为
ebp2
esp
值为
esp2


由此我们可得如下关系

ebp2 = esp1 - 0x4
esp2 = esp1 - 0x8


已知我们在被调用函数里面的
ReturnSingleEntry
的值为
[ebp+28h]


也就是
ebp2+28h
=
esp1-4h+28h
=
esp1+24h


因为我们断点是在函数调用之前会被命中的,所以我们这里的断点要设置为
esp+24h
=
0


讲了这么多,为什么是
esp
不是
ebp
就解释到这里

然后我们输入上面这个语句来设置
条件中断
,然后我们用
dir
命令来列出
C:\WINDOWS\system32
这个文件夹下面的所有文件和文件夹

dir C:\WINDOWS\system32


然后就会发现,我们的条件断点被命中了,因为这里我们已经把这个恶意驱动的所有代码都列在了上面,所以这里我们就只列出必要的代码来进行分析

现在我们注意到以下这些代码,存在一个字符串

push offset Mlwa486+0x51a


这里压栈的这个值,我们可以查到这个值是
Mlwx




然后这段函数是这样的

push ebx                       // ebx is Mlwx486 function start address
push 8
push offset Mlwx486+0x51a      // Mlwx
lea  eax, [esi+5Eh]
push eax
xor  bl, bl
call dword ptr [Mlwa486+0x590] // RtlCompareMemory


然后引用
MSDN
的定义

SIZE_T RtlCompareMemory(
_In_ const VOID   *Source1,
_In_ const VOID   *Source2,
_In_       SIZE_T Length
);


由图中可知道,
eax
要和
Mlwx
这个字符串进行比较,然后这个比较的最大长度为
8
,这里我在刚刚的调用
Mlwx486+0x514
时,就分别标注过各个参数在
MSDN
中的意义和名称,其中
esi
的值被标注为
FileInformation
,而且这个值是为
3


这里我们就可以确定这个
FileInformation
的具体意义就是
FileBothDirectoryInformation
,然后这个值返回的是一个
FILE_BOTH_DIR_INFORMATION
的结构

关于这里如何知道值为3的意义就是FileBothDirectoryInformation,这个我也不是很清楚,因为MSDN里面并没有很明确的标注了这个值是多少,不过你可以通过bing FileBothDirectoryInformation就可以发现好多代码里面写的都等于3,这个问题已经反馈给了MSDN的维护组,希望能很快得到他们的回复,然后我们再说

(2018/3/5) MSDN的维护者给我发回了反馈,全文的MSDN原文链接如下MSDN原文,然后这是Github上的回复,总结来说就是文档的维护者现在暂时无法提供这个值的文档查询方式,但是他给了我们一个方法在
WinDbg
里面查询的方法,这里后面他还说会将这个作为新的功能性在未来加入,原文如下

Hello isinstance

Thank you for the feedback. Unfortunately, at this time we are unable to provide enum values.
You can view the values in the debugger by using the dt command or view them in the header.

Apologize for the inconvenience. We'll definitely consider this as a feature request.

Please let us know if you have any other comments!

Page Writer


这里的意思就是我们可以用
dt
命令来查询他的值,我们试试

kd> dt FileBothDirectoryInformation
*************************************************************************
***                                                                   ***
***                                                                   ***
***    Either you specified an unqualified symbol, or your debugger   ***
***    doesn't have full symbol information.  Unqualified symbol      ***
***    resolution is turned off by default. Please either specify a   ***
***    fully qualified symbol module!symbolname, or enable resolution ***
***    of unqualified symbols by typing ".symopt- 100". Note that     ***
***    enabling unqualified symbol resolution with network symbol     ***
***    server shares in the symbol path may cause the debugger to     ***
***    appear to hang for long periods of time when an incorrect      ***
***    symbol name is typed or the network symbol server is down.     ***
***                                                                   ***
***    For some commands to work properly, your symbol path           ***
***    must point to .pdb files that have full type information.      ***
***                                                                   ***
***    Certain .pdb files (such as the public OS symbols) do not      ***
***    contain the required information.  Contact the group that      ***
***    provided you with these symbols if you need this command to    ***
***    work.                                                          ***
***                                                                   ***
***    Type referenced: FileBothDirectoryInformation                  ***
***                                                                   ***
*************************************************************************
Symbol FileBothDirectoryInformation not found.


还是依旧无法查找到这个结构体,看来我们只能在
header
里面找寻这个变量了,但是不知道
Windows
这样的闭源操作系统会不会开放他的
header
出来,所以这里是根据书上和各种道听途说的搜索知道了这个对应的是
FileBothDirectoryInformation
,但是如果下次变成了2的话就不知道怎么对应。。。

我们可以观察一下这个结构的定义

typedef struct _FILE_BOTH_DIR_INFORMATION {
ULONG         NextEntryOffset;
ULONG         FileIndex;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG         FileAttributes;
ULONG         FileNameLength;
ULONG         EaSize;
CCHAR         ShortNameLength;
WCHAR         ShortName[12];
WCHAR         FileName[1];
} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION;


反正就是不论如何,现在我们确定了这个结构体是
FILE_BOTH_DIR_INFORMATION
,然后这个结构体的定义就是上面这个定义的

所以根据我们上面分析的,
esi
FileInformation
这个东西,然后这个东西是函数返回的一个结构体,这个结构体现在确定是
FILE_BOTH_DIR_INFORMATION


所以在代码

lea eax, [esi+5Eh]


此处,我们可以找到这个
esi+5Eh
这个地方为
WCHAR FileName[1]
这个地方,分析过程如下

首先我们确定各个数据类型所占的字节数,因为我们这个运行的虚拟机是
32
位的,所以可以得出如下结论

ULONG         = 8 byte
LARGE_INTEGER = 8 byte
CCHAR         = 1 byte
WCHAR         = 2 byte


这里我们要记住这个定理

原则1:数据成员对齐规则:结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。

原则2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)

原则3:收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。

重点是第一点和第三点,结构会存在对齐的特性,具体原理就不解释了。

然后我们看,根据结构体定义,可以得出下面这样地址递增列表:

typedef struct _FILE_BOTH_DIR_INFORMATION {               START - END
ULONG         NextEntryOffset; 8 byte(32 bit) addr = esi+0x00 - esi+0x07
ULONG         FileIndex;       8 byte(32 bit)        esi+0x08 - esi+0x0f
LARGE_INTEGER CreationTime;    8 byte(64 bit)        esi+0x10 - esi+0x17
LARGE_INTEGER LastAccessTime;  8 byte(64 bit)            0x18 - 0x1f
LARGE_INTEGER LastWriteTime;   8 byte(64 bit)            0x20 - 0x27
LARGE_INTEGER ChangeTime;      8 byte(64 bit)            0x28 - 0x2f
LARGE_INTEGER EndOfFile;       8 byte(64 bit)            0x30 - 0x37
LARGE_INTEGER AllocationSize;  8 byte(64 bit)            0x38 - 0x3f
ULONG         FileAttributes;  8 byte(64 bit)            0x40 - 0x47
ULONG         FileNameLength;  8 byte(64 bit)            0x48 - 0x4f
ULONG         EaSize;          8 byte(64 bit)            0x50 - 0x57
CCHAR         ShortNameLength; 1 byte(8  bit)  (4 byte)  0x58 - 0x5b
WCHAR         ShortName[12];   2 byte(16 bit)  2*12=0x18 0x5c - 0x5d
WCHAR         FileName[1];     2 byte(16 bit)            0x5e - 0x5f
} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION;


然后我们再把
5Eh
比较一下,就可以得出这个
esi+5eh
就是
FileName
这个结构元素的起始地址,然后我们继续往下

push 8                         // length
push offset Mlwx486+0x51a      // Mlwx
lea  eax, [esi+5Eh]
push eax                       // filename
xor bl, bl
call dword ptr [Mlwx486+0x590] // RtlCompareMemory


到这时,就可以确定,这个函数的入参的具体值是什么

SIZE_T RtlCompareMemory(
_In_ const VOID   *Source1,
_In_ const VOID   *Source2,
_In_       SIZE_T Length
);


再根据
MSDN
关于
RtlCompareMemory
的定义,我们可以知道,这个函数在比较
Mlwx
dir
列出来的各个
filename
,这个函数的意义在于,比较的是第一个入参和第二个入参,如果想的就返回了
Length
的值,如果不同就返回相同的字节数。

RtlCompareMemory returns the number of bytes in the two blocks that match. If all bytes match up to the specified Length value, the Length value is returned.

我们现在就去看看这个地址上的
FileName
具体是什么值了,使用
db
来查看

db esi+5eh




这里可以看出,
FileName
参数是在
C:\WINDOWS\system32
下的各个文件的名字,这里我们抓到的是
aaaamon.dll
,结果可能会不同,但是不影响我们的继续分析

这里的操作是将
C:\WINDOWS\system32
下的各个文件名和
Mlwx
比较,比较完之后会执行下面的操作

cmp  eax, 8
jne  Mlwx486+0x4f4


这里比较返回值和
8
的大小,一般来说,这个返回值是不会等于
8
的,除非你遇到
Mlwx486.sys


如果返回值不等于
8
之后,程序就会跳到
Mlwx486+0x4f4
这个地方,这个地方的代码如下:

mov  eax, dword ptr [esi]
test eax, eax
je   Mlwx486+0x500 // if eax == 0 -> jump and return 2Ch
test bl, bl        // bl always equal 0
jne  Mlwx486+0x500 // if bl != 0 -> jump back to push '8'
mov  edi, esi
add  esi, eax
jmp  Mlwx486+0x4ca // jump back to 'push 8'
pop  ebx
pop  esi
pop  ebp
ret  2Ch


上面的代码一般正常情况下,就会返回
2Ch
之后就退出函数了,逻辑上来说就是,如果每次传入的
FileName
Mlwx
不相等,函数直接就退出

然后我们继续往下

现在我们分析它是如何修改
NtQueryDirectoryFile
的返回值然后隐藏
Mlwx486.sys
文件的,我们可以查一下
NtQueryDirectoryFile
的文档,然后就可以知道,
NtQueryDirectoryFile
的返回值
FILE_BOTH_DIR_INFORMATION
结构是由一系列
FILE_BOTH_DIR_INFORMATION
结构串联而成的,如下图

---------------------------
| FILE_BOTH_DIR_INFORMATION | ---
---------------------------     |
--- | FILE_BOTH_DIR_INFORMATION | <--
|     ---------------------------
--> | FILE_BOTH_DIR_INFORMATION |
---------------------------


通常来说,第一个结构体是指向第二个结构体的,然后第二个结构体指向第三个结构体,这样依次下去

知道这些我们下面就可以来分析接下来的代码了,如果我们
RtlCompareMemory
返回值是
8
的话,就会执行以下这些代码

inc  bl                   // bl now is equal 0 by [xor bl, bl]
test edi, edi
je   Mlwx486+0x4f4        // if edi == 0, jump here -------------------
mov  eax, dword ptr [esi] // esi -> FileInformation structure          |
test eax, eax                                                          |
jne  Mlwx486+0x4f2        // if eax !=0, jump here -----------------   |
and  dword ptr [edi], eax //                                        |  |
jmp  Mlwx486+0x4f4        // jump here -----------------------------|->|
add  dword ptr [edi], eax // <--------------------------------------   |
mov  eax, dword ptr [esi] // <-----------------------------------------
test eax, eax
je   Mlwx486+0x504        // return 2Ch
test bl, bl
jne  Mlwx486+0x500        // if bl != 0, jump here ------
mov  edi, esi                                            |
add  esi, eax             // <---------------------------
// esi now point to the next FILE_BOTH_DIR_INFORMATION structure
jmp  Mlwx486+0x4ca        // jump back to push '8'
pop  ebx
mov  eax dword ptr [ebp+30h]
pop  edi
pop  esi
pop  ebp
ret  2Ch


这个函数的大致操作就是如上所示,注意那出现了两次的那个指令你就明白它把指针往后移动了以为,抹除了
Mlwx486.sys
文件的
FILE_BOTH_DIR_INFORMATION
结构,之后就达到了隐藏文件的目的

mov  eax, dword ptr [esi]


如果你还是有点不理解他是怎么操作的,可以看如下的解释

加强版解释,执行第一次

mov  eax, dword ptr [esi]


的时候,
eax
成为了指向
Mlwx486.sys
的信息结构
FILE_BOTH_DIR_INFORMATION
的指针,这个结构体就是上面我推算内存地址时候那个结构体,现在
eax
指向了它

我们假设这个值不是空,然后就会跳到这里执行

add  dword ptr [edi], eax


假设在执行这句之前,
edi
=
0015fbe0
,而
eax
的值是
00000078
,一个
add
操作之后,就会对存储在
0015fbe0
地址上的数据加上
00000078
(其实这个
0015fbe0
存储的数据就是
eax
的值)

而根据我们推导的
FILE_BOTH_DIR_INFORMATION
结构的内存地址分布,可以看出
esi
这个值其实就是
FILE_BOTH_DIR_INFORMATION
结构内元素
NextEntryOffset
的起始地址,也就是修改
[esi]
这个值其实修改的是结构体
FILE_BOTH_DIR_INFORMATION
内的元素
NextEntryOffset
的值

而我们根据
MSDN
NextEntryOffset
的定义,
NextEntryOffset
指向的是下一个
FILE_BOTH_DIR_INFORMATION
的地址,也就是将指向第二个结构体的指针往后偏移了好几个结构体

NextEntryOffset

Byte offset of the next FILE_BOTH_DIR_INFORMATION entry, if multiple entries are present in a buffer. This member is zero if no other entries follow this one.

这个病毒对隐藏自生这里处理的比较充满,他是直接将
NextEntryOffset
的值乘以2(相同值相加等于这个值乘以2)

typedef struct _FILE_BOTH_DIR_INFORMATION {               START - END
ULONG         NextEntryOffset; 8 byte(32 bit) addr = esi+0x00 - esi+0x07
ULONG         FileIndex;       8 byte(32 bit)        esi+0x08 - esi+0x0f
LARGE_INTEGER CreationTime;    8 byte(64 bit)        esi+0x10 - esi+0x17
LARGE_INTEGER LastAccessTime;  8 byte(64 bit)            0x18 - 0x1f
LARGE_INTEGER LastWriteTime;   8 byte(64 bit)            0x20 - 0x27
LARGE_INTEGER ChangeTime;      8 byte(64 bit)            0x28 - 0x2f
LARGE_INTEGER EndOfFile;       8 byte(64 bit)            0x30 - 0x37
LARGE_INTEGER AllocationSize;  8 byte(64 bit)            0x38 - 0x3f
ULONG         FileAttributes;  8 byte(64 bit)            0x40 - 0x47
ULONG         FileNameLength;  8 byte(64 bit)            0x48 - 0x4f
ULONG         EaSize;          8 byte(64 bit)            0x50 - 0x57
CCHAR         ShortNameLength; 1 byte(8  bit)  (4 byte)  0x58 - 0x5b
WCHAR         ShortName[12];   2 byte(16 bit)  2*12=0x18 0x5c - 0x5d
WCHAR         FileName[1];     2 byte(16 bit)            0x5e - 0x5f
} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION;


这个操作就是如下所示这样,假设第一个结构体的偏移量为
00000060
,因为结构体长度为
0x60


---------------------------
00000000 | FILE_BOTH_DIR_INFORMATION | - | NextEntryOffset = 00000000 |
---------------------------
00000060 | FILE_BOTH_DIR_INFORMATION | - | NextEntryOffset = 00000060 |
---------------------------
000000c0 | FILE_BOTH_DIR_INFORMATION | - | NextEntryOffset = 000000c0 |
---------------------------
00000120 | FILE_BOTH_DIR_INFORMATION | - | NextEntryOffset = 00000120 |
---------------------------


我们假设恶意驱动在
NtQueryDirectoryFile
返回的第三个结构体里面发现了文件名字和
Mlwx
匹配,之后通过赋值语句

mov  eax, dword ptr [esi]


获得了
Mlwx486.sys
这个文件的
FILE_BOTH_DIR_INFORMATION
结构里面的
NextEntryOffset
的值,然后将这个这个偏移值乘以
2


注:

这里的
NextEntryOffset
不是地址,是地址的偏移值(
offset
),也就是基地址加上偏移值等于真实地址的那个偏移值

真实地址 = 基地址 + 偏移地址

然后继续刚刚那个假设,程序在第三个结构体发现
Mlwx
之后,通过

add  dword ptr [edi], eax


偏移值
NextEntryOffset
的值变成了
00000180
c0
*
2


然后计算机通过上面那个公式计算真实地址

本来第三个结构体的地址是
000000c0
,但是经过这么一个通过改变
offset
之后,计算机计算之后,得出的地址就变成
00000180
(根据上面那个计算公式)

计算机通过计算之后,认为第三个结构体存在
00000180
这个地址上,就去
00000180
上取数据,从而跳过了第三个结构体,所以这个通过改变
offset
在不改变数据结构的前提之下,达到了隐藏文件的目的,也只会有天才才会想的出来了

然后分析基本就到这里

第二问的答案就是这个程序拥有一个内核模块,存储在程序的资源节上,执行的时候释放
sys
文件,然后这个
sys
文件就会加载到内核中执行

3.这个程序做了些什么?

解答:通过上面的分析,可以得出,这是用来隐藏文件的
RootKit
,它使用
SSDT
来挂钩覆盖
NtQueryDirectoryFile
函数,通过自定义一些操作,来隐藏文件

我们可以把被隐藏的
sys
文件导出来看看,书中给我们提供了三种方法来导出这个被隐藏的文件:

1. 禁用驱动的服务
2. 从安装的资源节提取出这个文件
3. 访问文件的目录,用cp命令将文件重命名后显示


我们这里先试试第一种,也是推荐的方法,这里需要重启

我们先用
cmd
来查询这个服务在运行了没有



然后我们输入命令

sc stop "486 WS Driver"




服务无法被控制,那么没办法,再试试第二个



点这个然后保存到桌面上,用
IDA
来打开就行了

我们试试第三种方法



成功了,然后我们打开看看,这就是这个文件打开的样子



我们进入
DriverEntry
这个例程



这里就不详细分析这个代码了,书上说是
RtlInitUnicodeString
以参数
KeServiceDescirptorTable
NtQueryDircetoryFile
做入参,然后用
MmGetSystemRoutineAddress
这个函数来查找这个两个地址的偏移量,接下来他把地址做了一个替换

本文完
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  代码分析 病毒