Adding SHA1 and SHA256 to this command and output. Also if a file is signed or unsigned

Viewed 196

I've been trying to add another command and output it to show SHA1 and SHA256. Also checking for file if they are signed using Get-AuthenticodeSignature but can't seem to figure it out.

Get-ChildItem C:\ -Recurse -File -Force -ea SilentlyContinue -ev errs | % { Get-FileHash $_.Fullname -Algorithm MD5 } | Select-Object Hash,Path,@{Name='Name';Expression={[System.IO.Path]::GetFileName($_.Path)}} | Out-File C:\$env:COMPUTERNAME.csv -Encoding UTF8 -Force

Example would be.

Column1 Column2 Column3 Column4       Column5
MD5     SHA1    SHA256  Sign/Unsigned Path
2 Answers

What you want to do will take A LOT OF TIME to execute, so I would advise to not try that on the complete C:\ drive, but take it step by step..

In order to speed up the 3 different file hashes you want for each file, I'd suggest to read the file in as byte array and compute the hashes on these bytes which will save reading the same file over and over again.

Something like this:

# create the 3 different hasher objects
$md5Hasher    = [System.Security.Cryptography.MD5]::Create()
$sha1Hasher   = [System.Security.Cryptography.SHA1]::Create()
$sha256Hasher = [System.Security.Cryptography.SHA256]::Create()

# Loop through the files in the root path
Get-ChildItem -Path 'C:\SomePath' -File -Recurse -Force -ErrorAction SilentlyContinue | 
ForEach-Object { 
    # read the content of the file as byte array only once
    $bytes  = [System.IO.File]::ReadAllBytes($_.FullName)
    # output an object with all properties you need
    [PsCustomObject]@{
        MD5    = ($md5Hasher.ComputeHash($bytes) | ForEach-Object { '{0:X2}' -f $_ }) -join ''
        SHA1   = ($sha1Hasher.ComputeHash($bytes) | ForEach-Object { '{0:X2}' -f $_ }) -join ''
        SHA256 = ($sha256Hasher.ComputeHash($bytes) | ForEach-Object { '{0:X2}' -f $_ }) -join ''
        Signed = (Get-AuthenticodeSignature -Content $bytes -SourcePathorExtension $_.FullName).Status
        Path   = $_.FullName
    }
} | Export-Csv -Path "D:\Test\$env:COMPUTERNAME.csv" -Encoding UTF8 -NoTypeInformation

# clean op the hasher objects when done
$md5Hasher.Dispose()
$sha1Hasher.Dispose()
$sha256Hasher.Dispose()

Theo's excellent answer is spot on - hashing big files is a slow operation and you may want to optimize read operations from disk. For smaller files this will be optimal, but it won't work for files larger than 2GB.

The problem is that [System.IO.File]::ReadAllBytes($_.FullName) needs to read the entire file into a byte array in memory, and arrays in .NET can only hold up to ~2.1 billion items (~2GB).

You can work around this limitation by passing a file stream to $hasher.ComputeHash() - the only caveat being that we need to "re-wind" the file stream before passing it to the next algorithm.

If only a small subset of the files are expected to have signatures, another (less significant) optimization is to selectively filter which extensions to invoke Get-AuthenticodeSignature for:

function Get-FileMultiHash
{
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.IO.FileInfo]$InputObject,

        [Parameter(Mandatory = $false)]
        [string[]]$SignatureExtension
    )

    begin {
        $hashers = @(
            ($md5Hasher = [System.Security.Cryptography.MD5]::Create())
            ($sha1Hasher = [System.Security.Cryptography.SHA1]::Create())
            ($sha256Hasher = [System.Security.Cryptography.SHA256]::Create())
        )

        # Prepend any extension filter with `.` if not already present
        $SignatureExtensionFilter = $SignatureExtensionFilter -replace '^(?!\.)','.'
    }

    process {
        try{
            $shouldCheckSignature = -not$PSBoundParameters.ContainsKey('SignatureExtensionFilter') -or $InputObject.Extension -in $SignatureExtensionFilter

            # Open file stream, compute md5
            $fs = $InputObject.OpenRead()
            $md5sum = $md5Hasher.ComputeHash($fs)

            # Rewind file stream, compute sha1
            [void]$fs.Seek(0, [System.IO.SeekOrigin]::Begin)
            $sha1sum = $sha1Hasher.ComputeHash($fs)

            # Rewind file stream, compute sha256
            [void]$fs.Seek(0, [System.IO.SeekOrigin]::Begin)
            $sha256sum = $sha256Hasher.ComputeHash($fs)
        }
        finally{
            # Dispose of file stream
            if($fs){$fs.Dispose()}
        }

        # Output checksums and signature status
        [pscustomobject]@{
            MD5    = [BitConverter]::ToString($md5sum).Replace('-', '')
            SHA1   = [BitConverter]::ToString($sha1sum).Replace('-', '')
            SHA256 = [BitConverter]::ToString($sha256sum).Replace('-', '')
            Signed = if($shouldCheckSignature){(Get-AuthenticodeSignature -LiteralPath $_.FullName).Status}else{'Skipped'}
            Path = $_.FullName
        }
    }

    end {
        # Dispose of hash algo instances
        $hashers |ForEach-Object Dispose
    }
}

Then use like:

Get-ChildItem -Path C:\path\to\root\folder -Recurse -File -Force -ea SilentlyContinue -ev errs |Get-FileMultiHash

... or with an extension filter:

Get-ChildItem C:\Windows\System32 -File |Get-FileMultiHash -SignatureExtensionFilter exe,dll
Related