From 4a1a43bbe973622b1fd277e5cd131484987e7345 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Sun, 19 May 2013 16:28:46 -0400 Subject: [PATCH] Almost working IPMI 2.0 support..... disable encryption to facilitate/narrow debug... currently integrity algorithm is apparantly failing to pass on *outgoing* packets, though RAKP4 incoming did pass --- ipmibase.py | 257 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 240 insertions(+), 17 deletions(-) diff --git a/ipmibase.py b/ipmibase.py index e92de3ad..7c2ead8c 100644 --- a/ipmibase.py +++ b/ipmibase.py @@ -1,17 +1,34 @@ #!/usr/bin/env python # This represents the low layer message framing portion of IPMI +import os import select -import Crypto +from Crypto.Hash import HMAC, SHA +from Crypto.Cipher import AES import socket +import atexit from collections import deque from time import time from hashlib import md5 from struct import pack, unpack -from ipmi_constants import payload_types, ipmi_completion_codes, command_completion_codes +from ipmi_constants import payload_types, ipmi_completion_codes, command_completion_codes, payload_types, rmcp_codes from random import random initialtimeout = 0.5 #minimum timeout for first packet to retry in any given session. This will be randomized to stagger out retries in case of congestion +def _aespad(data): # ipmi demands a certain pad scheme, per table 13-20 AES-CBC encrypted payload fields + newdata=list(data) + currlen=len(data)+1 #need to count the pad length field as well + neededpad=currlen%16 + if neededpad: #if it happens to be zero, hurray, but otherwise invert the sense of the padding + neededpad = 16-neededpad + padval=1 + while padval <= neededpad: + newdata.append(padval) + padval+=1 + newdata.append(neededpad) + return newdata + + ''' In order to simplify things, in a number of places there is a callback facility and optional arguments to pass in. An OO oriented caller may find the additional argument needless. Allow them to ignore it by skipping the argument if None @@ -41,8 +58,16 @@ class IPMISession: bmc_handlers={} waiting_sessions={} peeraddr_to_nodes={} + ''' + Upon exit of python, make sure we play nice with BMCs by assuring closed sessions for all that we tracked + ''' + @classmethod + def _cleanup(cls): + for session in cls.bmc_handlers.itervalues(): + session.logout() @classmethod def _createsocket(cls): + atexit.register(cls._cleanup) cls.socket = socket.socket(socket.AF_INET6,socket.SOCK_DGRAM) #INET6 can do IPv4 if you are nice to it try: #we will try to fixup our receive buffer size if we are smaller than allowed. maxmf = open("/proc/sys/net/core/rmem_max") @@ -71,10 +96,14 @@ class IPMISession: if 'error' in response: raise Exception(response['error']) - def __init__(self,bmc,userid,password,port=623,onlogon=None,onlogonargs=None): + def __init__(self,bmc,userid,password,port=623,kg=None,onlogon=None,onlogonargs=None): self.bmc=bmc self.userid=userid self.password=password + if kg is not None: + self.kg=kg + else: + self.kg=password self.port=port self.onlogonargs=onlogonargs if (onlogon is None): @@ -90,9 +119,16 @@ class IPMISession: while not self.logged: IPMISession.wait_for_rsp() def _initsession(self): + self.localsid=2017673555 #this number can be whatever we want. I picked 'xCAT' minus 1 so that a hexdump of packet would show xCAT + self.privlevel=4 #for the moment, assume admin access TODO: make flexible + self.confalgo=0 + self.aeskey=None + self.integrityalgo=0 + self.k1=None + self.rmcptag=1 self.ipmicallback=None self.ipmicallbackargs=None - self.sessioncontext=0 + self.sessioncontext=None self.sequencenumber=0 self.sessionid=0 self.authtype=0 @@ -152,9 +188,9 @@ class IPMISession: def _send_ipmi_net_payload(self,netfn,command,data): ipmipayload=self._make_ipmi_payload(netfn,command,data) payload_type = payload_types['ipmi'] - if hasattr(self,"integrity_algorithm"): + if self.integrityalgo: payload_type |= 0b01000000 - if hasattr(self,"confidentiality_algorithm"): + if self.confalgo: payload_type |= 0b10000000 self._pack_payload(payload=ipmipayload,payload_type=payload_type) def _pack_payload(self,payload=None,payload_type=None): @@ -169,10 +205,12 @@ class IPMISession: message.append(self.authtype) if (self.ipmiversion == 2.0): message.append(payload_type) - if (payload_type == 2): + if (baretype == 2): raise Exception("TODO: OEM Payloads") - elif (payload_type == 1): + elif (baretype == 1): raise Exception("TODO: SOL Payload") + elif baretype not in payload_types.values(): + raise Exception("Unrecognized payload type %d"%baretype) message += unpack("!4B",pack(">8); + iv=os.urandom(16) + message += list(unpack("16B",iv)) + payloadtocrypt=_aespad(payload) + crypter = AES.new(self.aeskey,AES.MODE_CBC,iv) + crypted = crypter.encrypt(pack("%dB"%len(payloadtocrypt),*payloadtocrypt)) + crypted = list(unpack("%dB"%len(crypted),crypted)) + message += crypted + else: #no confidetiality algorithm + message.append(psize&0xff) + message.append(psize>>8); + message += list(payload) + if self.integrityalgo: #see table 13-8, RMCP+ packet format TODO: SHA256 which is now allowed + integdata = message[4:] + neededpad=(len(integdata)-2)%4 + if neededpad: + needpad = 4-neededpad + message += [0xff]*neededpad + message.append(neededpad) + message.append(7) #reserved, 7 is the required value for the specification followed + authcode = HMAC.new(self.k1,pack("%dB"%len(integdata),*integdata),SHA).digest()[:12] #SHA1-96 per RFC2404 truncates to 96 bits + message += unpack("12B",authcode) self.netpacket = pack("!%dB"%len(message),*message) self._xmit_packet() @@ -256,7 +322,6 @@ class IPMISession: data=response['data'] self.sessionid=unpack(">2 == self.expectednetfn and payload[5] == self.expectedcmd): @@ -423,6 +646,6 @@ class IPMISession: if __name__ == "__main__": - ipmis = IPMISession(bmc="10.240.181.1",userid="USERID",password="Passw0rd") + import sys + ipmis = IPMISession(bmc=sys.argv[1],userid=sys.argv[2],password=os.environ['IPMIPASS']) print ipmis.raw_command(command=2,data=[1],netfn=0) - ipmis.logout()