TotalCommanderFtpPasswordRecovery.psm1
|
################################################################################ # # Total Commander FTP password decoder (wcx_ftp.ini stored form). # Offline port of the reverse-engineered algorithm. Checksum validation # present in Total Commander is not implemented here — invalid or tampered # ciphertext may still yield a string result. # # Version : PowerShell SDK v1.0.0 # PowerShell : Windows PowerShell 5.1 / PowerShell 7+ # Author : Bartosz Wójcik (support@pelock.com) # Homepage : https://www.pelock.com # ################################################################################ Set-StrictMode -Version Latest class TotalCommanderPasswordDecoder { hidden [uint32] $_randomSeed = 0 static [byte[]] HexStringToByteArray([string]$Str) { $lowered = $Str.ToLowerInvariant() $len = $lowered.Length if ($len -eq 0 -or ($len -band 1) -ne 0) { return $null } $out = New-Object byte[] ($len / 2) for ($i = 0; $i -lt $len; $i += 2) { $hi = [TotalCommanderPasswordDecoder]::HexNibble([int][char]$lowered[$i]) $lo = [TotalCommanderPasswordDecoder]::HexNibble([int][char]$lowered[$i + 1]) if ($hi -lt 0 -or $lo -lt 0) { return $null } $out[$i / 2] = [byte]((($hi -shl 4) -bor $lo) -band 0xFF) } return $out } static [int] HexNibble([int]$Code) { if ($Code -ge 0x30 -and $Code -le 0x39) { return $Code - 0x30 } if ($Code -ge 0x61 -and $Code -le 0x66) { return $Code - 0x61 + 10 } return -1 } [void] SeedPrng([uint32]$Seed) { $this._randomSeed = $Seed } hidden [uint32] NextRandMax([uint32]$NMax) { $modulus = [bigint]::Pow(2, 32) $nextSeed = (([bigint]$this._randomSeed * [bigint]0x8088405 + [bigint]1) % $modulus) $this._randomSeed = [uint32]$nextSeed $shifted = (([bigint]$this._randomSeed * [bigint]$NMax) -shr 32) return [uint32]$shifted } static [byte] Rol8([int]$Value, [int]$Counter) { $b = $Value -band 0xFF $c = $Counter -band 7 if ($c -eq 0) { return [byte]$b } return [byte]((($b -shl $c) -bor ($b -shr (8 - $c))) -band 0xFF) } [byte[]] DecryptPassword([string]$PasswordHex) { $normalized = [regex]::Replace($PasswordHex, '\s+', '') $bytes = [TotalCommanderPasswordDecoder]::HexStringToByteArray($normalized) if ($null -eq $bytes) { return $null } $passwordLength = $bytes.Length if ($passwordLength -le 4) { return $null } $passwordLength -= 4 $buf = New-Object byte[] $bytes.Length [Buffer]::BlockCopy($bytes, 0, $buf, 0, $bytes.Length) $this.SeedPrng(849521) for ($i = 0; $i -lt $passwordLength; $i++) { $buf[$i] = [TotalCommanderPasswordDecoder]::Rol8([int]$buf[$i], [int]$this.NextRandMax(8)) } $this.SeedPrng(12345) for ($i = 0; $i -lt 256; $i++) { $x = [int]$this.NextRandMax([uint32]$passwordLength) $y = [int]$this.NextRandMax([uint32]$passwordLength) $t = $buf[$x] $buf[$x] = $buf[$y] $buf[$y] = $t } $this.SeedPrng(42340) for ($i = 0; $i -lt $passwordLength; $i++) { $buf[$i] = [byte](($buf[$i] -bxor [byte]$this.NextRandMax(256)) -band 0xFF) } $this.SeedPrng(54321) for ($i = 0; $i -lt $passwordLength; $i++) { $buf[$i] = [byte]((([int]$buf[$i] - [int]$this.NextRandMax(256)) -band 0xFF)) } $plain = New-Object byte[] $passwordLength [Buffer]::BlockCopy($buf, 0, $plain, 0, $passwordLength) return $plain } } function New-TotalCommanderPasswordDecoder { <# .SYNOPSIS Creates an offline Total Commander FTP password decoder. .DESCRIPTION Factory for TotalCommanderPasswordDecoder. Decrypts hex passwords stored in wcx_ftp.ini. No HTTP calls are made. .EXAMPLE $decoder = New-TotalCommanderPasswordDecoder $bytes = $decoder.DecryptPassword('00112233445566778899aabbccddeeff') #> [CmdletBinding()] [OutputType([TotalCommanderPasswordDecoder])] param() return [TotalCommanderPasswordDecoder]::new() } Export-ModuleMember -Function @('New-TotalCommanderPasswordDecoder') |