diff --git a/cmd/ipmi-pcap-decrypt/authenticator.go b/cmd/ipmi-pcap-decrypt/authenticator.go new file mode 100644 index 0000000..02035c5 --- /dev/null +++ b/cmd/ipmi-pcap-decrypt/authenticator.go @@ -0,0 +1,223 @@ +package main + +import ( + "crypto/hmac" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "encoding/binary" + "fmt" + "hash" + + "github.com/gebn/bmc/pkg/ipmi" +) + +// truncatedHash truncates a hash.Hash's output. This is used to implement +// algorithms like HMAC-SHA1-96 (the first 12 bytes of HMAC-SHA1), and +// HMAC-SHA256-128 (the first 16 bytes of HMAC-SHA256). +type truncatedHash struct { + hash.Hash + length int +} + +func (t truncatedHash) Sum(b []byte) []byte { + sum := t.Hash.Sum(b) + return sum[:len(b)+t.length] +} + +func (t truncatedHash) Size() int { + return t.length +} + +// authenticationAlgorithmParams produces Hash implementations to generate +// various values. It contains the configurable parameters of the session +// establishment authentication algorithm. An instance of this struct can be +// created from an authentication algorithm alone, and means we can treat all +// algorithms identically from the point it is obtained. +type authenticationAlgorithmParams struct { + + // hashGen is a function returning the underlying hash algorithm of the + // HMAC. + hashGen func() hash.Hash + + // icvLength is the length to truncate integrity check values to. This is + // only used to generate the HMAC for RAKP Message 4. A value of 0 means no + // truncation. + icvLength int +} + +// AuthCode returns a hash.Hash implementation for producing and verifying the +// AuthCodes in RAKP messages 2 and 3. All material required to generate these +// values is available in RAKP messages 1 and 2. +func (g *authenticationAlgorithmParams) AuthCode(kuid []byte) hash.Hash { + return hmac.New(g.hashGen, kuid) +} + +// SIK returns a hash.Hash implementation for producing the SIK from RAKP +// messages 1 and 2. +func (g *authenticationAlgorithmParams) SIK(kg []byte) hash.Hash { + return hmac.New(g.hashGen, kg) +} + +// K returns a hash.Hash implementation for creating additional key material, +// also referred to as K_N. +func (g *authenticationAlgorithmParams) K(sik []byte) hash.Hash { + return hmac.New(g.hashGen, sik) +} + +// ICV returns a hash.Hash implementation for validating the ICV field in RAKP +// Message 4. The inputs to this hash are contained in RAKP messages 1 and 2. +func (g *authenticationAlgorithmParams) ICV(sik []byte) hash.Hash { + if g.icvLength == 0 { + return g.K(sik) + } + return truncatedHash{ + Hash: g.K(sik), + length: g.icvLength, + } +} + +// algorithmAuthenticationParams builds an authenticator for the specified +// algorithm, using the provided key. This authenticator can then be used in the +// RAKP session establishment process. +func algorithmAuthenticationHashGenerator(a ipmi.AuthenticationAlgorithm) (*authenticationAlgorithmParams, error) { + switch a { + // TODO support ipmi.AuthenticationAlgorithmNone - this is difficult as we + // need to create a bunch of valid but completely useless structs... + case ipmi.AuthenticationAlgorithmHMACSHA1: + return &authenticationAlgorithmParams{ + hashGen: sha1.New, + icvLength: 12, + }, nil + case ipmi.AuthenticationAlgorithmHMACSHA256: + return &authenticationAlgorithmParams{ + hashGen: sha256.New, + icvLength: 16, + }, nil + case ipmi.AuthenticationAlgorithmHMACMD5: + return &authenticationAlgorithmParams{ + hashGen: md5.New, // ICV not truncated + }, nil + default: + return nil, fmt.Errorf("unknown authentication algorithm: %v", a) + } +} + +// authenticator is not as simple as hash([]byte) []byte - the input array must +// be constructed manually by serialising fields from various packets + +// executeHash is a convenience function to calculate the hash of a slice of +// bytes. It leaves the hash in a reset state. Note that this function cannot be +// called concurrently on a single underlying hash.Hash. +func executeHash(h hash.Hash, b []byte) []byte { + if h == nil { + return nil + } + h.Write(b) + sum := h.Sum(nil) + h.Reset() + return sum +} + +// AdditionalKeyMaterialGenerator is satisfied by types that can produce key +// material derived from the Session Integrity Key, as defined in section 13.32 +// of IPMI v2.0. This additional key material is referred to as K_N. In +// practice, only K_1 and K_2 are used, for packet authentication and +// confidentiality respectively +type AdditionalKeyMaterialGenerator interface { + + // K computes the value of K_N for a given value of N, using the negotiated + // authentication algorithm (used during session establishment) loaded with + // the SIK. N is only defined for values 1 through 255. This method is not + // used by the library itself, and is assumed to be only for + // informational/debugging purposes, so we make no attempt to memoise + // results. This function is not safe for concurrent use by multiple + // goroutines. + K(n int) []byte +} + +type additionalKeyMaterialGenerator struct { + hash hash.Hash +} + +func (g additionalKeyMaterialGenerator) K(n int) []byte { + // when the spec says HMAC block size, it means the size of the output tag, + // not the block size of the underlying algorithm (e.g. 20 rather than 64 + // for SHA-1). + constant := make([]byte, g.hash.Size()) + for i := 0; i < g.hash.Size(); i++ { + constant[i] = uint8(n) + } + return executeHash(g.hash, constant) +} + +func calculateSIK(h hash.Hash, rakpMessage1 *ipmi.RAKPMessage1, rakpMessage2 *ipmi.RAKPMessage2) []byte { + h.Write(rakpMessage1.RemoteConsoleRandom[:]) // R_M + h.Write(rakpMessage2.ManagedSystemRandom[:]) // R_C + role := uint8(rakpMessage1.MaxPrivilegeLevel) + if !rakpMessage1.PrivilegeLevelLookup { + role |= 1 << 4 + } + h.Write([]byte{role}) // Role_M (entire byte from original wire format) + h.Write([]byte{uint8(len(rakpMessage1.Username))}) // ULength_M + h.Write([]byte(rakpMessage1.Username)) // UName_M + sum := h.Sum(nil) + h.Reset() + return sum +} + +// calculateRAKPMessage2AuthCode computes the ICV that should be sent by the BMC +// in RAKP Message 2 based on the RAKP Message 1 sent by the remote console and +// the RAKP Message 2 sent by the BMC. +func calculateRAKPMessage2AuthCode(h hash.Hash, rakpMessage1 *ipmi.RAKPMessage1, rakpMessage2 *ipmi.RAKPMessage2) []byte { + buf := [4]byte{} + + // session IDs are in wire byte order, presumably for efficiency, but we'd + // rather decode and re-encode for the sake of code organisation + binary.LittleEndian.PutUint32(buf[:], rakpMessage2.RemoteConsoleSessionID) + h.Write(buf[:]) // SID_M + binary.LittleEndian.PutUint32(buf[:], rakpMessage1.ManagedSystemSessionID) + h.Write(buf[:]) // SID_C + + h.Write(rakpMessage1.RemoteConsoleRandom[:]) // R_M + h.Write(rakpMessage2.ManagedSystemRandom[:]) // R_C + h.Write(rakpMessage2.ManagedSystemGUID[:]) // GUID_C + role := uint8(rakpMessage1.MaxPrivilegeLevel) + if !rakpMessage1.PrivilegeLevelLookup { + role |= 1 << 4 + } + h.Write([]byte{role}) // Role_M (entire byte from original wire format) + h.Write([]byte{uint8(len(rakpMessage1.Username))}) // ULength_M + h.Write([]byte(rakpMessage1.Username)) // UName_M + sum := h.Sum(nil) + h.Reset() + return sum +} + +func calculateRAKPMessage3AuthCode(h hash.Hash, rakpMessage1 *ipmi.RAKPMessage1, rakpMessage2 *ipmi.RAKPMessage2) []byte { + h.Write(rakpMessage2.ManagedSystemRandom[:]) // R_C + buf := [4]byte{} + binary.LittleEndian.PutUint32(buf[:], rakpMessage2.RemoteConsoleSessionID) + h.Write(buf[:]) // SID_M + role := uint8(rakpMessage1.MaxPrivilegeLevel) + if !rakpMessage1.PrivilegeLevelLookup { + role |= 1 << 4 + } + h.Write([]byte{role}) // Role_M (entire byte from original wire format) + h.Write([]byte{uint8(len(rakpMessage1.Username))}) // ULength_M + h.Write([]byte(rakpMessage1.Username)) // UName_M + sum := h.Sum(nil) + h.Reset() + return sum +} + +func calculateRAKPMessage4ICV(h hash.Hash, rakpMessage1 *ipmi.RAKPMessage1, rakpMessage2 *ipmi.RAKPMessage2) []byte { + h.Write(rakpMessage1.RemoteConsoleRandom[:]) // R_M + buf := [4]byte{} + binary.LittleEndian.PutUint32(buf[:], rakpMessage1.ManagedSystemSessionID) + h.Write(buf[:]) // SID_C + h.Write(rakpMessage2.ManagedSystemGUID[:]) // GUID_C + sum := h.Sum(nil) + h.Reset() + return sum +} diff --git a/cmd/ipmi-pcap-decrypt/main.go b/cmd/ipmi-pcap-decrypt/main.go new file mode 100644 index 0000000..5961268 --- /dev/null +++ b/cmd/ipmi-pcap-decrypt/main.go @@ -0,0 +1,201 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "log" + "os" + + "github.com/gebn/bmc/pkg/ipmi" + "github.com/google/gopacket" + "github.com/google/gopacket/pcapgo" +) + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} + +// To capture a pcap file on linux: +// sudo tcpdump -w ipmidump.pcap -i any "port 623" +func run() error { + if len(os.Args) < 2 { + return errors.New("missing required argument: path to .pcap file to decrypt") + } + filename := os.Args[1] + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + password, ok := os.LookupEnv("IPMI_PASSWORD") + if !ok { + return errors.New("missing required IPMI_PASSWORD env variable") + } + + handle, err := pcapgo.NewReader(file) + if err != nil { + return err + } + ph := packetHandler{ + Password: password, + } + packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) + for packet := range packetSource.Packets() { + ph.packetCountTotal++ + + err := ph.handle(packet) + if err != nil { + fmt.Println(ph.packetCountTotal, "handle error:", err) + } + } + return nil +} + +type packetHandler struct { + ExpectedUsername string + Password string + OpenSessionRsp *ipmi.OpenSessionRsp + RAKPMessage1 *ipmi.RAKPMessage1 + RAKPMessage2 *ipmi.RAKPMessage2 + cipherLayer *ipmi.AES128CBC + decode gopacket.DecodingLayerFunc + + packetCountTotal int +} + +func (p *packetHandler) handle(packet gopacket.Packet) error { + + ignoredLayerTypes := []gopacket.LayerType{ + ipmi.LayerTypeOpenSessionReq, // not decodable yet + ipmi.LayerTypeRAKPMessage3, // not decodable yet + ipmi.LayerTypeRAKPMessage4, + } + for _, t := range ignoredLayerTypes { + layer := packet.Layer(t) + if layer == nil { + continue + } + fmt.Println(p.packetCountTotal, layer.LayerType()) + return nil + } + + if layer := packet.Layer(ipmi.LayerTypeOpenSessionRsp); layer != nil { + p.OpenSessionRsp = layer.(*ipmi.OpenSessionRsp) + p.RAKPMessage1 = nil + p.RAKPMessage2 = nil + + fmt.Println(p.packetCountTotal, "Open Session Response") + + switch authAlgo := p.OpenSessionRsp.AuthenticationPayload.Algorithm; authAlgo { + case ipmi.AuthenticationAlgorithmHMACSHA1: + default: + return fmt.Errorf("unsupported authentication algorithm: %s", authAlgo.String()) + } + + switch integAlgo := p.OpenSessionRsp.IntegrityPayload.Algorithm; integAlgo { + case ipmi.IntegrityAlgorithmHMACSHA196: + default: + return fmt.Errorf("unsupported integrity algorithm: %s", integAlgo.String()) + } + return nil + } + + if layer := packet.Layer(ipmi.LayerTypeRAKPMessage1); layer != nil { + fmt.Println(p.packetCountTotal, "RAKP Message 1") + if p.OpenSessionRsp == nil { + return errors.New("got RAKP Message 1 before the open session request") + } + p.RAKPMessage1 = layer.(*ipmi.RAKPMessage1) + if p.ExpectedUsername != "" && p.RAKPMessage1.Username != p.ExpectedUsername { + return fmt.Errorf("unexpected username; expected %q, got %q", p.ExpectedUsername, p.RAKPMessage1.Username) + } + return nil + } + + if layer := packet.Layer(ipmi.LayerTypeRAKPMessage2); layer != nil { + if p.RAKPMessage1 == nil { + return errors.New("got RAKP Message 2 before the RAKP Message 1") + } + p.RAKPMessage2 = layer.(*ipmi.RAKPMessage2) + + hashGenerator, err := algorithmAuthenticationHashGenerator(p.OpenSessionRsp.AuthenticationPayload.Algorithm) + if err != nil { + return err + } + + effectiveBMCKey := make([]byte, 16) + copy(effectiveBMCKey, []byte(p.Password)) + + sikHash := hashGenerator.SIK(effectiveBMCKey) + sik := calculateSIK(sikHash, p.RAKPMessage1, p.RAKPMessage2) + + k2Hash := hashGenerator.K(sik) + k2Hash.Write(bytes.Repeat([]byte{0x02}, 20)) + k2 := k2Hash.Sum(nil) + + key := [16]byte{} + copy(key[:], k2) + fmt.Printf("%d RAKP Message 2 Key[% x]\n", p.packetCountTotal, key) + + p.cipherLayer, err = ipmi.NewAES128CBC(key) + if err != nil { + return err + } + + // There is surely a way to include the key in the packet stack + // so that gopacket can decrypt and decode them. + // But I have no idea how... + + // keyMaterialGen := additionalKeyMaterialGenerator{ + // hash: hashGenerator.K(sik), + // } + + // cipherLayer, err := algorithmCipher( + // p.OpenSessionRsp.ConfidentialityPayload.Algorithm, keyMaterialGen) + // if err != nil { + // return err + // } + // p.cipherLayer = cipherLayer + + // dlc := gopacket.DecodingLayerContainer(gopacket.DecodingLayerArray(nil)) + // dlc = dlc.Put(cipherLayer) + // dlc = dlc.Put(&ipmi.Message{}) + // p.decode = dlc.LayersDecoder(ipmi.LayerTypeV2Session, gopacket.NilDecodeFeedback) + + return nil + } + + if p.cipherLayer == nil { + return errors.New("no cipherLayer set yet") + } + + // quick and dirty: get the encrypted payload and decypher it manually + // print the decoded payload + if layer := packet.Layer(ipmi.LayerTypeSessionSelector); layer != nil { + sess := layer.(*ipmi.SessionSelector) + + encrypted := sess.Payload[12:] + if len(encrypted) < 2*16 { + return errors.New("payload too short") + } + encrypted = encrypted[:2*16] + err := p.cipherLayer.DecodeFromBytes(encrypted, nil) + if err != nil { + return fmt.Errorf("Payload[% x]\n%w", encrypted, err) + } + + fmt.Printf("%d Decoded[% x]\n", p.packetCountTotal, p.cipherLayer.Payload) + + return nil + } + + if p.cipherLayer == nil { + return errors.New("no cipherLayer set yet") + } + + return nil +} diff --git a/go.sum b/go.sum index 4a8e55d..d6ed5be 100644 --- a/go.sum +++ b/go.sum @@ -270,6 +270,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=