恶意代码分析实战 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这个函数来查找这个两个地址的偏移量,接下来他把地址做了一个替换
本文完
相关文章推荐
- 恶意代码分析实战 Lab 10-1 习题笔记
- 恶意代码分析实战 Lab 7-2 习题笔记
- 恶意代码分析实战 Lab 5-1 习题笔记
- 恶意代码分析实战 Lab 6-3 习题笔记
- 恶意代码分析实战 Lab 3-1 习题笔记
- 恶意代码分析实战 Lab 3-2 习题笔记
- 恶意代码分析实战 Lab 4 习题笔记
- 恶意代码分析实战 Lab 9-3 习题笔记
- 恶意代码分析实战 Lab 8 习题笔记
- 恶意代码分析实战 Lab 6-1 习题笔记
- 恶意代码分析实战 Lab 1-3 习题笔记
- 恶意代码分析实战 Lab 7-3 习题笔记
- 恶意代码分析实战 Lab 1-2 习题笔记
- 恶意代码分析实战 Lab 6-2 习题笔记
- 恶意代码分析实战 Lab 3-4 习题笔记
- 恶意代码分析实战 Lab 7-1 习题笔记
- 恶意代码分析实战 Lab 3-3 习题笔记
- 恶意代码分析实战 Lab 9-2 习题笔记
- 恶意代码分析实战 Lab 9-1 习题笔记