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

版权声明:本文为博主辛辛苦苦码字原创文章,转载记得表明出处哦~ https://blog.csdn.net/isinstance/article/details/79363150

Lab 10-1

问题

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

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

图片

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

图片

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

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

图片

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

图片

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

图片

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

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

图

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

图片

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

图片

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

图片

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

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

图片

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

图片

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

图

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

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

图片

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

图片

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

图片

后面的精确时间是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.dllsysmain.sdbsystest.sdb,最后当然还有那个sys驱动Mlwx486.sys


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

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

WinDbg里面运行命令

lm

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

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

图片

然后现在我们就可以确定一个名为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<>OFSF代表了运算结果的符号,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

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

书上是这样说:

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

这里的f7ab2486RootKit替换的那个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的值为ebp1esp的值为esp1,执行初始化之后的调用函数l里的ebp值为ebp2esp值为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,然后这个结构体的定义就是上面这个定义的

所以根据我们上面分析的,esiFileInformation这个东西,然后这个东西是函数返回的一个结构体,这个结构体现在确定是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的定义,我们可以知道,这个函数在比较Mlwxdir列出来的各个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之后就退出函数了,逻辑上来说就是,如果每次传入的FileNameMlwx不相等,函数直接就退出

然后我们继续往下

现在我们分析它是如何修改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的值

而我们根据MSDNNextEntryOffset的定义,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的值变成了00000180c0*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以参数KeServiceDescirptorTableNtQueryDircetoryFile做入参,然后用MmGetSystemRoutineAddress这个函数来查找这个两个地址的偏移量,接下来他把地址做了一个替换

本文完

猜你喜欢

转载自blog.csdn.net/isinstance/article/details/79363150
今日推荐