// Package to work with VHD images // See https://technet.microsoft.com/en-us/virtualization/bb676673.aspx package vhd import ( "bytes" "encoding/binary" "encoding/hex" "fmt" "math" "os" "strconv" "time" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" ) const VHD_COOKIE = "636f6e6563746978" // conectix const VHD_DYN_COOKIE = "6378737061727365" // cxsparse const VHD_CREATOR_APP = "676f2d766864" // go-vhd const VHD_CREATOR_HOST_OS = "5769326B" // Win2k const VHD_BLOCK_SIZE = 2 * 1024 * 1024 // 2MB const VHD_HEADER_SIZE = 512 const SECTOR_SIZE = 512 const FOURK_SECTOR_SIZE = 4096 const VHD_EXTRA_HEADER_SIZE = 1024 // A VDH file type VHD struct { Footer VHDHeader ExtraHeader VHDExtraHeader } // VHD Header type VHDHeader struct { Cookie [8]byte Features [4]byte FileFormatVersion [4]byte DataOffset [8]byte Timestamp [4]byte CreatorApplication [4]byte CreatorVersion [4]byte CreatorHostOS [4]byte OriginalSize [8]byte CurrentSize [8]byte DiskGeometry [4]byte DiskType [4]byte Checksum [4]byte UniqueId [16]byte SavedState [1]byte Reserved [427]byte } // VHD extra header, for dynamic and differential disks type VHDExtraHeader struct { Cookie [8]byte DataOffset [8]byte TableOffset [8]byte HeaderVersion [4]byte MaxTableEntries [4]byte BlockSize [4]byte Checksum [4]byte ParentUUID [16]byte ParentTimestamp [4]byte Reserved [4]byte ParentUnicodeName [512]byte ParentLocatorEntry1 [24]byte ParentLocatorEntry2 [24]byte ParentLocatorEntry3 [24]byte ParentLocatorEntry4 [24]byte ParentLocatorEntry5 [24]byte ParentLocatorEntry6 [24]byte ParentLocatorEntry7 [24]byte ParentLocatorEntry8 [24]byte Reserved2 [256]byte } // Options for the CreateSparseVHD function type VHDOptions struct { UUID string Timestamp int64 } /* * VHDExtraHeader methods */ func (header *VHDExtraHeader) CookieString() string { return string(header.Cookie[:]) } // Calculate and add the VHD dynamic/differential header checksum func (h *VHDExtraHeader) addChecksum() { buffer := new(bytes.Buffer) binary.Write(buffer, binary.BigEndian, h) checksum := 0 bb := buffer.Bytes() for counter := 0; counter < VHD_EXTRA_HEADER_SIZE; counter++ { checksum += int(bb[counter]) } binary.BigEndian.PutUint32(h.Checksum[:], uint32(^checksum)) } /* * VHDHeader methods */ func (h *VHDHeader) DiskTypeStr() (dt string) { switch h.DiskType[3] { case 0x00: dt = "None" case 0x01: dt = "Deprecated" case 0x02: dt = "Fixed" case 0x03: dt = "Dynamic" case 0x04: dt = "Differential" case 0x05: dt = "Reserved" case 0x06: dt = "Reserved" default: panic("Invalid disk type detected!") } return } // Return the timestamp of the header func (h *VHDHeader) TimestampTime() time.Time { tstamp := binary.BigEndian.Uint32(h.Timestamp[:]) return time.Unix(int64(946684800+tstamp), 0) } // Calculate and add the VHD header checksum func (h *VHDHeader) addChecksum() { buffer := new(bytes.Buffer) binary.Write(buffer, binary.BigEndian, h) checksum := 0 bb := buffer.Bytes() for counter := 0; counter < VHD_HEADER_SIZE; counter++ { checksum += int(bb[counter]) } binary.BigEndian.PutUint32(h.Checksum[:], uint32(^checksum)) } func CreateFixedHeader(size uint64, options *VHDOptions) VHDHeader { header := VHDHeader{} hexToField(VHD_COOKIE, header.Cookie[:]) hexToField("00000002", header.Features[:]) hexToField("00010000", header.FileFormatVersion[:]) hexToField("ffffffffffffffff", header.DataOffset[:]) // LOL Y2038 if options.Timestamp != 0 { binary.BigEndian.PutUint32(header.Timestamp[:], uint32(options.Timestamp)) } else { t := uint32(time.Now().Unix() - 946684800) binary.BigEndian.PutUint32(header.Timestamp[:], t) } hexToField(VHD_CREATOR_APP, header.CreatorApplication[:]) hexToField(VHD_CREATOR_HOST_OS, header.CreatorHostOS[:]) binary.BigEndian.PutUint64(header.OriginalSize[:], size) binary.BigEndian.PutUint64(header.CurrentSize[:], size) // total sectors = disk size / 512b sector size totalSectors := math.Floor(float64(size / 512)) // [C, H, S] geometry := calculateCHS(uint64(totalSectors)) binary.BigEndian.PutUint16(header.DiskGeometry[:2], uint16(geometry[0])) header.DiskGeometry[2] = uint8(geometry[1]) header.DiskGeometry[3] = uint8(geometry[2]) hexToField("00000002", header.DiskType[:]) // Fixed 0x00000002 hexToField("00000000", header.Checksum[:]) if options.UUID != "" { copy(header.UniqueId[:], uuidToBytes(options.UUID)) } else { copy(header.UniqueId[:], uuidgenBytes()) } header.addChecksum() return header } func RawToFixed(f *os.File, options *VHDOptions) { info, err := f.Stat() check(err) size := uint64(info.Size()) header := CreateFixedHeader(size, options) binary.Write(f, binary.BigEndian, header) } func VHDCreateSparse(size uint64, name string, options VHDOptions) VHD { header := VHDHeader{} hexToField(VHD_COOKIE, header.Cookie[:]) hexToField("00000002", header.Features[:]) hexToField("00010000", header.FileFormatVersion[:]) hexToField("0000000000000200", header.DataOffset[:]) // LOL Y2038 if options.Timestamp != 0 { binary.BigEndian.PutUint32(header.Timestamp[:], uint32(options.Timestamp)) } else { t := uint32(time.Now().Unix() - 946684800) binary.BigEndian.PutUint32(header.Timestamp[:], t) } hexToField(VHD_CREATOR_APP, header.CreatorApplication[:]) hexToField(VHD_CREATOR_HOST_OS, header.CreatorHostOS[:]) binary.BigEndian.PutUint64(header.OriginalSize[:], size) binary.BigEndian.PutUint64(header.CurrentSize[:], size) // total sectors = disk size / 512b sector size totalSectors := math.Floor(float64(size / 512)) // [C, H, S] geometry := calculateCHS(uint64(totalSectors)) binary.BigEndian.PutUint16(header.DiskGeometry[:2], uint16(geometry[0])) header.DiskGeometry[2] = uint8(geometry[1]) header.DiskGeometry[3] = uint8(geometry[2]) hexToField("00000003", header.DiskType[:]) // Sparse 0x00000003 hexToField("00000000", header.Checksum[:]) if options.UUID != "" { copy(header.UniqueId[:], uuidToBytes(options.UUID)) } else { copy(header.UniqueId[:], uuidgenBytes()) } header.addChecksum() // Fill the sparse header header2 := VHDExtraHeader{} hexToField(VHD_DYN_COOKIE, header2.Cookie[:]) hexToField("ffffffffffffffff", header2.DataOffset[:]) // header size + sparse header size binary.BigEndian.PutUint64(header2.TableOffset[:], uint64(VHD_EXTRA_HEADER_SIZE+VHD_HEADER_SIZE)) hexToField("00010000", header2.HeaderVersion[:]) maxTableSize := uint32(size / (VHD_BLOCK_SIZE)) binary.BigEndian.PutUint32(header2.MaxTableEntries[:], maxTableSize) binary.BigEndian.PutUint32(header2.BlockSize[:], VHD_BLOCK_SIZE) binary.BigEndian.PutUint32(header2.ParentTimestamp[:], uint32(0)) header2.addChecksum() f, err := os.Create(name) check(err) defer f.Close() binary.Write(f, binary.BigEndian, header) binary.Write(f, binary.BigEndian, header2) /* Write BAT entries The BAT is always extended to a sector (4K) boundary 1536 = 512 + 1024 (the VHD Header + VHD Sparse header size) */ for count := uint32(0); count < (FOURK_SECTOR_SIZE - 1536); count += 1 { f.Write([]byte{0xff}) } /* Windows creates 8K VHDs by default */ for i := 0; i < (FOURK_SECTOR_SIZE - VHD_HEADER_SIZE); i += 1 { f.Write([]byte{0x0}) } binary.Write(f, binary.BigEndian, header) return VHD{ Footer: header, ExtraHeader: header2, } } /* * VHD */ func FromFile(f *os.File) (vhd VHD) { vhd = VHD{} vhd.Footer = readVHDFooter(f) vhd.ExtraHeader = readVHDExtraHeader(f) return vhd } func (vhd *VHD) PrintInfo() { fmt.Println("\nVHD footer") fmt.Println("==========") vhd.PrintFooter() if vhd.Footer.DiskType[3] == 0x3 || vhd.Footer.DiskType[3] == 0x04 { fmt.Println("\nVHD sparse/differential header") fmt.Println("===============================") vhd.PrintExtraHeader() } } func (vhd *VHD) PrintExtraHeader() { header := vhd.ExtraHeader fmtField("Cookie", fmt.Sprintf("%s (%s)", hexs(header.Cookie[:]), header.CookieString())) fmtField("Data offset", hexs(header.DataOffset[:])) fmtField("Table offset", hexs(header.TableOffset[:])) fmtField("Header version", hexs(header.HeaderVersion[:])) fmtField("Max table entries", hexs(header.MaxTableEntries[:])) fmtField("Block size", hexs(header.BlockSize[:])) fmtField("Checksum", hexs(header.Checksum[:])) fmtField("Parent UUID", uuid(header.ParentUUID[:])) // Seconds since January 1, 1970 12:00:00 AM in UTC/GMT. // 946684800 = January 1, 2000 12:00:00 AM in UTC/GMT. tstamp := binary.BigEndian.Uint32(header.ParentTimestamp[:]) t := time.Unix(int64(946684800+tstamp), 0) fmtField("Parent timestamp", fmt.Sprintf("%s", t)) fmtField("Reserved", hexs(header.Reserved[:])) parentNameBytes, _, err := transform.Bytes( unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder(), header.ParentUnicodeName[:], ) if err != nil { panic(err) } parentName := string(parentNameBytes) fmtField("Parent Name", parentName) // Parent locator entries ignored since it's a dynamic disk sum := 0 for _, b := range header.Reserved2 { sum += int(b) } fmtField("Reserved2", strconv.Itoa(sum)) } func (vhd *VHD) PrintFooter() { header := vhd.Footer //fmtField("Cookie", string(header.Cookie[:])) fmtField("Cookie", fmt.Sprintf("%s (%s)", hexs(header.Cookie[:]), string(header.Cookie[:]))) fmtField("Features", hexs(header.Features[:])) fmtField("File format version", hexs(header.FileFormatVersion[:])) dataOffset := binary.BigEndian.Uint64(header.DataOffset[:]) fmtField("Data offset", fmt.Sprintf("%s (%d bytes)", hexs(header.DataOffset[:]), dataOffset)) //// Seconds since January 1, 1970 12:00:00 AM in UTC/GMT. //// 946684800 = January 1, 2000 12:00:00 AM in UTC/GMT. t := time.Unix(int64(946684800+binary.BigEndian.Uint32(header.Timestamp[:])), 0) fmtField("Timestamp", fmt.Sprintf("%s", t)) fmtField("Creator application", string(header.CreatorApplication[:])) fmtField("Creator version", hexs(header.CreatorVersion[:])) fmtField("Creator OS", string(header.CreatorHostOS[:])) originalSize := binary.BigEndian.Uint64(header.OriginalSize[:]) fmtField("Original size", fmt.Sprintf("%s ( %d bytes )", hexs(header.OriginalSize[:]), originalSize)) currentSize := binary.BigEndian.Uint64(header.OriginalSize[:]) fmtField("Current size", fmt.Sprintf("%s ( %d bytes )", hexs(header.CurrentSize[:]), currentSize)) cilinders := int64(binary.BigEndian.Uint16(header.DiskGeometry[:2])) heads := int64(header.DiskGeometry[2]) sectors := int64(header.DiskGeometry[3]) dsize := cilinders * heads * sectors * 512 fmtField("Disk geometry", fmt.Sprintf("%s (c: %d, h: %d, s: %d) (%d bytes)", hexs(header.DiskGeometry[:]), cilinders, heads, sectors, dsize)) fmtField("Disk type", fmt.Sprintf("%s (%s)", hexs(header.DiskType[:]), header.DiskTypeStr())) fmtField("Checksum", hexs(header.Checksum[:])) fmtField("UUID", uuid(header.UniqueId[:])) fmtField("Saved state", fmt.Sprintf("%d", header.SavedState[0])) } /* Utility functions */ func calculateCHS(ts uint64) []uint { var sectorsPerTrack, heads, cylinderTimesHeads, cylinders float64 totalSectors := float64(ts) ret := make([]uint, 3) if totalSectors > 65535*16*255 { totalSectors = 65535 * 16 * 255 } if totalSectors >= 65535*16*63 { sectorsPerTrack = 255 heads = 16 cylinderTimesHeads = math.Floor(totalSectors / sectorsPerTrack) } else { sectorsPerTrack = 17 cylinderTimesHeads = math.Floor(totalSectors / sectorsPerTrack) heads = math.Floor((cylinderTimesHeads + 1023) / 1024) if heads < 4 { heads = 4 } if (cylinderTimesHeads >= (heads * 1024)) || heads > 16 { sectorsPerTrack = 31 heads = 16 cylinderTimesHeads = math.Floor(totalSectors / sectorsPerTrack) } if cylinderTimesHeads >= (heads * 1024) { sectorsPerTrack = 63 heads = 16 cylinderTimesHeads = math.Floor(totalSectors / sectorsPerTrack) } } cylinders = cylinderTimesHeads / heads // This will floor the values ret[0] = uint(cylinders) ret[1] = uint(heads) ret[2] = uint(sectorsPerTrack) return ret } func hexToField(hexs string, field []byte) { h, err := hex.DecodeString(hexs) check(err) copy(field, h) } // Return the number of blocks in the disk, diskSize in bytes func getMaxTableEntries(diskSize uint64) uint64 { return diskSize * (2 * 1024 * 1024) // block size is 2M } func readVHDExtraHeader(f *os.File) (header VHDExtraHeader) { buff := make([]byte, 1024) _, err := f.ReadAt(buff, 512) check(err) binary.Read(bytes.NewBuffer(buff[:]), binary.BigEndian, &header) return header } func readVHDFooter(f *os.File) (header VHDHeader) { info, err := f.Stat() check(err) buff := make([]byte, 512) _, err = f.ReadAt(buff, info.Size()-512) check(err) binary.Read(bytes.NewBuffer(buff[:]), binary.BigEndian, &header) return header } func readVHDHeader(f *os.File) (header VHDHeader) { buff := make([]byte, 512) _, err := f.ReadAt(buff, 0) check(err) binary.Read(bytes.NewBuffer(buff[:]), binary.BigEndian, &header) return header }