这几天学习了一下古老的PE病毒原理,不过刚开始没多久,就打算一心一意复习考研,不再搞别的事情分心了。本来准备发不止一个学习笔记,其中包括感染技术,传播技术等等,不过都没时间了,只好先把这个发出来再说吧。
对于Ring3的PE病毒来说,我想最重要的莫过于搜索API地址了,因为所有的所有别的事情,包括感染、注入、获取机器信息、升级到Ring0、传播甚至破坏,没有哪一个不会用到API的,只要找到了API地址,那么任何一个普通程序能做的事,病毒都能做到,甚至包括WinFlash干的事。
kernel32.dll里有一个函数:GetProcAddress,只要给定模块handle和API的名字,就可以得到API的地址,很不错的搜索函数。当然,GetProcAddress也是一个API,所以我们自己的搜索代码不是没有用武之地的,不过简单的来说,我们只用搜索出GetProcAddress就可以了。
在搜索API的方法上,病毒和蠕虫有点点区别。我现在还没有听说过可以攻击各种各样完全不同漏洞的通用蠕虫,当然,每个漏洞一般对应一个特定的程序的特定版本;我也没听说过只感染一个特定程序的病毒。针对特定漏洞的蠕虫所处的是一个特定的环境,这个环境是可预知的,所以,蠕虫编写者可以直接调试漏洞程序,分析漏洞程序导入表中GetProcAddress地址的位置,然后直接hardcode。那么如果漏洞程序升级了,导入表变了呢?放心,也别指望新版本里漏洞还给你留着。
病毒所面对的就是比较极端的环境了,完全可变的通用的环境,唯一知道就是这是一个PE程序(这是感染部分选择感染对象的职责,如果你硬要感染word文档我也没话说)。还有呢?有!这个程序至少加载了kernel32模块这是无条件的!否则这个进程怎么被创建的呢?主线程怎么创建的呢?
“正如你所知道的,当我们在执行一个应用程序的时候,代码是从KERNEL32 "call"一部分代
码的(也就像KERNEL 调用我们的代码一样)。而且,如果你还记得的话,当一个call 调用之
后,返回的地址是在堆栈里(即,在由ESP 所指定的内存地址里的)的。”
被引用过无数次的一个常识
其实这个call就是CreateProcess发出的,这很好理解。那么,有了这个处于kernel32内部的地址,寻找kernel32的基址应该不成问题,kernel32肯定是按页加载和对齐的,kernel32的基址肯定是一个页首。我们可以一页一页向前搜索kernel32的MZ头。
GetK32.asm
;---------------------------
; input:
; ESI = near page addr
; EAX = limit checkpagenum
;
; output:
; EAX = ImageBase of kernel32
;
; used reg:
; EAX,ESI,EDI
;---------------------------
GetK32 proc
and eax,eax
jz _GetK32_Fail
_GetK32_Start:
cmp word ptr [esi],"ZM"
jnz _GetK32_Loop
mov edi,[esi+3Ch]
add edi,esi
cmp dword ptr [edi],"EP"
jz _GetK32_Done
_GetK32_Loop:
sub esi,10000h
dec eax
jnz _GetK32_Start
_GetK32_Fail:
mov esi,7c800000h
_GetK32_Done:
xchg eax,esi
ret
GetK32 endp
仅仅判断"MZ"还不够,有可能会是碰上的,所以我们还要再追踪下"PE",当然像上面那样直接找可能会访问到非法区域。当然,可以添加SEH来避免错误被显示出来。我们还需要一个搜索页数的上限,避免无限制搜索下去。其实这个搜索是很快的,因为这个被调用函数是CreateProcess,具体原因后面我会提到。
好,现在找到了kernel32的基址,我们把它存在kernel这个变量里。
kernel dd 0
mov esi,[esp]
and esi,0ffff0000h
mov eax,100
call GetK32
mov kernel,eax
下面我们就要搜索kernel32的导出表了,熟悉PE文件结构的人肯定不会陌生,在导出表的名字表里一个一个比较,就这么简单。不过我从来都不喜欢字符串比较函数,包括C标准库里面的字符串函数我都不喜欢,我总想要更直观的函数,因为我们只需要看看两个字符串是否相等,当然,比较下他们的CRC32就可以了,刚好是一个双字的比较,一条指令。
首先,我们需要一个求CRC32的代码,我在Billy Belceb 病毒编写教程---Win32 篇看到了一个
;----------------------------------------------
; input:
; ESI = Offset where code to calculate begins........volatile
; EDI = Size of that code............................volatile
;
; output:
; EAX = CRC32 of given code
;
; used reg
; EAX,EBX,ECX,EDX,ESI,EDI
;----------------------------------------------
CRC32 proc
cld
xor ecx,ecx
dec ecx
mov edx,ecx
NextByteCRC:
xor eax,eax
xor ebx,ebx
lodsb
xor al,cl
mov cl,ch
mov ch,dl
mov dl,dh
mov dh,8
NextBitCRC:
shr bx,1
rcr ax,1
jnc NoCRC
xor ax,08320h
xor bx,0EDB8h
NoCRC:
dec dh
jnz NextBitCRC
xor ecx,eax
xor edx,ebx
dec edi ;---------------- 1 byte less
jnz NextByteCRC
not edx
not ecx
mov eax,edx
rol eax,16
mov ax,cx
ret
CRC32 endp
我想,有了kernel32的基址,得到kernel32导出表的名字表和地址表还有序号表,不是问题吧。
;--------------------------------------
; input:
; kernel = ImageBase of kernel32.dll
;
; output:
; ESI = OrdinalTableVA----VA
; EBX = NameTableVA-------VA
; EDI = AddressTableVA----VA
;
; used reg:
; EAX,EBX,ESI,EDI
;--------------------------------------
GetAPI_Init proc
mov esi,3ch
add esi,kernel ;------------ Get PE header of KERNEL32
lodsd
add eax,kernel
mov esi,[eax+78h] ;------------ Get a RAV pointer to its Export Table
add esi,1ch ;------------ RAV pointer to address table
add esi,kernel ;------------ Turn RVA to VA
mov edi,[esi] ;------------ Pointer to the address table
add edi,kernel
mov ebx,[esi+4]
add ebx,kernel
mov eax,[esi+8]
add eax,kernel
mov esi,eax
ret
GetAPI_Init endp
上面这个部分只运行一次,我不喜欢读写内存,在我印象里,读写内存是很慢的事,多用寄存器才是我喜欢的事。
;--------------------------------------
; input:
; EDX = CRC32 of the API ASCIIz name........volatile
; EBP = Length of API name
; ESI = OrdinalTableVA----VA
; EBX = NameTableVA-------VA
; EDI = AddressTableVA----VA
; kernel = ImageBase of kernel32.dll
;
; output:
; EAX = API address
;
; used reg
; EAX,EBX,ECX,EDX,ESI,EDI,EBP
;--------------------------------------
GetAPI_ET_CRC32 proc
push edi
push esi
xor ecx,ecx
p1: mov edi,[ebx+ecx*4]
add edi,kernel
mov esi,edi
xor al,al
lp: scasb
jnz lp
sub edi,esi
inc ecx
cmp ebp,edi
jnz p1
push ecx
push edx
push ebx
call CRC32
pop ebx
pop edx
pop ecx
cmp edx,eax
jnz p1
dec ecx
pop esi
pop edi
xor edx,edx
mov dx,word ptr [ecx*2+esi]
mov eax,[edx*4+edi]
add eax,kernel
ret
GetAPI_ET_CRC32 endp
把你要找的API的CRC32值放在edx里,名字长度(包括结尾的0)放在ebp里,eax输出的就是它的地址,不错吧。而且这个过程不破坏用于存放3个表地址的3个寄存器EBX,ESI,EDI,所以一次Init以后可以反复调用,直到把你自己构造的“导入表”(我习惯这么叫,叫跳转表也不错)添满为止。
不过我只要一个函数的地址,就是GetProcAddress,15字节,CRC32:0FFC97C1Fh
如果要再加一个的话,那就是LoadLibraryA,13字节,CRC32:04134D1ADh
写个实际的程序试试
test.asm
.386
.model flat,stdcall
option casemap:none
.data
kernel dd 0
GETPROCCRC equ 0FFC97C1Fh
LORDDLLCRC equ 04134D1ADh
user db "user32.dll",0
mess db "MessageBoxA",0
userhd dd 0
getprochd dd 0
.code
GetAPI_Init proc
......
GetAPI_Init endp
CRC32 proc
......
CRC32 endp
GetAPI_ET_CRC32 proc
......
GetAPI_ET_CRC32 endp
GetK32 proc
......
GetK32 endp
start:
mov esi,[esp]
and esi,0ffff0000h
mov eax,100
call GetK32
mov kernel,eax
call GetAPI_Init
mov edx,LORDDLLCRC
mov ebp,13
call GetAPI_ET_CRC32
push offset user
call eax
mov userhd,eax
mov edx,GETPROCCRC
mov ebp,15
call GetAPI_ET_CRC32
mov getprochd,eax
push offset mess
mov eax,userhd
push eax
mov eax,getprochd
call eax
push 0
push offset mess
push offset mess
push 0
call eax
over: mov edx,040F57181h
mov ebp,12
call GetAPI_ET_CRC32
push 0
call eax
end start
用MASM32编译连接
ml /c /coff test.asm
link /SUBSYSTEM:WINDOWS test.obj
我没有导入任何头文件和库文件,不过这个程序可以跑,这也就验证了我们的代码的功能。
最后再提几点:
1,我这程序显式地用到什么函数?从字符串来看,只能看到"MessageBoxA"!这就是CRC32搜索法的好处,GetProcAddress,LoadLibraryA,还有一个:ExitProcess,CRC32:040F57181h,直接在代码里看不出来的。
2,这种方法可以用在蠕虫里么?严格的来说不可以,因为看看我们搜索kernel32基址的函数,我们利用kernel32内的CreateProcess函数call我们程序本身的代码并将返回到CreateProcess内部的地址留在[esp]的特点来搜索kernel32头,因为病毒代码总是在宿主程序运行前执行,也就是处在一个进程刚刚创建的状态。而蠕虫所利用的漏洞一般在很复杂的、不知道有多少层的子函数里,这个时候的esp早不知道指向哪里去了。
3,为什么我说从CreateProcess(准确来说CreateProcessA或CreateProcessW)搜索kernel32头几步就完成了呢?因为我在调试时发现,kernel32导出表里名字表内的函数名排列是按照字典序排列的,而CreateProcess开头是C,是不是很快呢?这个特点也可以让我们写出更高效的kernel32的API搜索法,比如二分排序等等……(学计算机的,就总免不了在算法上较劲),当然实用性不高,呵呵
这篇文章不敢妄称原创,虽然除了算CRC32的代码外其他代码都是我自己写的,但是思想是别人的,我只是一个学习者,所以我这叫学习笔记,而且是[分享]而不是[原创]。这些实践大大加深了我对PE文件格式的理解,看来动手才是提高的唯一办法。
每次来看雪都有一种很亲切的感觉,大家都很热情,人气很高,讨论的内容也很有技术含量,不像别的论坛,一旦问题深入一点,就卖关子故弄玄虚甚至开口要价,好像不这样就显不出自己是高手一样。我想管理者与主持者的对技术的热情和无私,对培养后继者,端正论坛风气有很大益处,只有这样看雪才会继续繁荣下去。
参考文献:《Billy Belceb 病毒编写教程---Win32 篇》
- 标 题: PE病毒学习笔记搜索API
- 作 者:kmyc
- 时 间:2007-10-01 10:13
- 链 接:http://bbs.pediy.com/showthread.php?t=52630