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
这个函数来查找这个两个地址的偏移量,接下来他把地址做了一个替换
本文完