package nb.barmie.modes.enumeration; import java.io.ObjectStreamConstants; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import nb.barmie.exceptions.BaRMIeInvalidPortException; import nb.barmie.exceptions.BaRMIeInvalidReplyDataPacketException; import nb.barmie.net.TCPEndpoint; /*********************************************************** * Parser for RMI ReplyData packets (Java serialisation). * * Extracts class names, string annotations, and the TCP * endpoint details of a remote object from the RMI * ReplyData packet. * * Written by Nicky Bloor (@NickstaDB). **********************************************************/ public class RMIReplyDataParser { /******************* * Properties ******************/ private boolean _recordClasses; //Indicates whether class names should be recorded during object parsing private LinkedList<HashMap<Byte, ArrayList<Character>>> _classDataDesc; //List of classDescFlags mapped to field type codes from an object's classDesc /******************* * Construct the reply data parser. ******************/ public RMIReplyDataParser() { this._recordClasses = true; this._classDataDesc = new LinkedList<HashMap<Byte, ArrayList<Character>>>(); } /******************* * Extract object details from a ReplyData that was captured through the * RMI registry proxy. * * @param objName The object name bound to the RMI registry for which data is being extracted. * @param packetBytes The ReplyData captured from the RMI registry which contains the remote object description. * @return An RMIObject describing the remote object. ******************/ public RMIObject extractObjectDetails(String objName, ArrayList<Byte> packetBytes) { LinkedList<Byte> dataStack; RMIObject obj; byte b; int i; //Create the RMIObject with the given object name obj = new RMIObject(objName); //Copy the given buffer into a stack for parsing dataStack = new LinkedList<Byte>(); dataStack.addAll(packetBytes); //Set the 'recordClasses' flag to true so that class descriptions are added to the object description this._recordClasses = true; //Start parsing the object data try { //Validate the RMI packet type byte if(dataStack.peek() != 0x51) { throw new BaRMIeInvalidReplyDataPacketException("The data buffer begins with 0x" + String.format("%02x", dataStack.peek()) + ", which is not a ReplyData packet (0x51 expected)."); } dataStack.pop(); //Validate the serialisation header if(dataStack.pop() != (byte)0xac || dataStack.pop() != (byte)0xed) { throw new BaRMIeInvalidReplyDataPacketException("The data buffer does not contain the serialisation magic number data."); } //Validate the serialisation stream version if(dataStack.pop() != 0x00 || dataStack.pop() != 0x05) { throw new BaRMIeInvalidReplyDataPacketException("The data buffer does not contain version 5 serialisation data."); } //Parse the serialisation stream elements to extract class names, annotations, and endpoint details while(dataStack.size() > 0) { //Get the type of the next stream element b = dataStack.pop(); //Process the element accordingly switch(b) { //Skip over top-level block data elements case ObjectStreamConstants.TC_BLOCKDATA: //Read the block length b = dataStack.pop(); //Skip over the block bytes for(i = 0; i < b; ++i) { dataStack.pop(); } break; //Process the returned RMI object case ObjectStreamConstants.TC_OBJECT: this.handleNewObjectElement(obj, dataStack); break; //Unknown top-level stream element type default: throw new BaRMIeInvalidReplyDataPacketException("Unknown serialisation stream element (0x" + String.format("%02x", b) + ")."); } } } catch(Exception e) { //Something went wrong, store the exception in the object element so it can be reviewed obj.setParsingException(e); } //Return the RMIObject return obj; } /******************* * Handle a new object element in the ReplyData stream. * * @param obj The RMIObject to populate with class names. * @param dataStack The remaining data in the ReplyData packet. ******************/ private void handleNewObjectElement(RMIObject obj, LinkedList<Byte> dataStack) throws BaRMIeInvalidReplyDataPacketException { LinkedList<HashMap<Byte, ArrayList<Character>>> classDataDesc; HashMap<Byte, ArrayList<Character>> classDataDescElement; ArrayList<Character> classDataDescFields; //Reset the field data this._classDataDesc.clear(); //Read the class desc this.handleClassDesc(obj, dataStack); //Set the 'recordClasses' flag to false so that no further classes are added to the object description this._recordClasses = false; //Create a fresh copy of the class data description to use in reading the object data classDataDesc = new LinkedList<HashMap<Byte, ArrayList<Character>>>(); for(HashMap<Byte, ArrayList<Character>> el: this._classDataDesc) { classDataDescElement = new HashMap<Byte, ArrayList<Character>>(); for(Byte key: el.keySet()) { classDataDescFields = new ArrayList<Character>(); for(Character typeCode: el.get(key)) { classDataDescFields.add(typeCode); } classDataDescElement.put(key, classDataDescFields); } classDataDesc.add(classDataDescElement); } //Read in the class data based on the classDataDesc this.handleClassData(obj, dataStack, classDataDesc); } /******************* * Handle a classDesc element. * * @param obj The RMIObject to populate with class names. * @param dataStack The remaining data in the ReplyData packet. ******************/ private void handleClassDesc(RMIObject obj, LinkedList<Byte> dataStack) throws BaRMIeInvalidReplyDataPacketException { String className; //Delegate depending on the type of classDesc switch(dataStack.pop()) { //ClassDesc case ObjectStreamConstants.TC_CLASSDESC: //Read the class name className = this.extractUtf8(dataStack); //Skip over the serialVersionUID this.extractLong(dataStack); //Handle the classDescInfo element, pass the class name in as there may be annotations for the class in there this.handleClassDescInfo(obj, dataStack, className); break; //ProxyClassDesc case ObjectStreamConstants.TC_PROXYCLASSDESC: //Handle the proxyClassDescInfo element this.handleProxyClassDescInfo(obj, dataStack); break; //Null - e.g. when the super class is null case ObjectStreamConstants.TC_NULL: break; //Unknown classDesc type default: throw new BaRMIeInvalidReplyDataPacketException("Unknown classDesc element type."); } } /******************* * Handle a classDescInfo element and add the class name and any string * annotations to the object description. * * @param obj The RMIObject to populate with class names. * @param dataStack The remaining data in the ReplyData packet. * @param className The class name that was read prior to reading this classDescInfo element. ******************/ private void handleClassDescInfo(RMIObject obj, LinkedList<Byte> dataStack, String className) throws BaRMIeInvalidReplyDataPacketException { ArrayList<String> stringAnnotations; byte classDescFlags; int i; //Read the class desc flags classDescFlags = dataStack.pop(); //Read the field data this.handleFields(obj, dataStack, classDescFlags); //Read the class annotations stringAnnotations = this.handleClassAnnotation(obj, dataStack); //Add the class to the object description along with any string annotations if(this._recordClasses) { obj.addClass(className); for(String annotation: stringAnnotations) { obj.addStringAnnotation(className, annotation); } } //Read the super class description this.handleClassDesc(obj, dataStack); } /******************* * Handle a proxyClassDescInfo element and add the interface names and * string annotations to the object description. * * @param obj The RMIObject to populate with class names. * @param dataStack The remaining data in the ReplyData packet. ******************/ private void handleProxyClassDescInfo(RMIObject obj, LinkedList<Byte> dataStack) throws BaRMIeInvalidReplyDataPacketException { String[] interfaceNames; ArrayList<String> stringAnnotations; int interfaceCount; int i; //Read the number of interfaces from the packet interfaceCount = this.extractInt(dataStack); //Read in the interface names interfaceNames = new String[interfaceCount]; for(i = 0; i < interfaceCount; ++i) { interfaceNames[i] = this.extractUtf8(dataStack); } //Handle class annotations and retrieve any string annotations to add to the object description stringAnnotations = this.handleClassAnnotation(obj, dataStack); //Add the interfaces to the object description if(this._recordClasses) { for(i = 0; i < interfaceCount; ++i) { //Add the interface name obj.addClass(interfaceNames[i]); //Attach any related string annotations to the first interface if(i == 0) { for(String annotation: stringAnnotations) { obj.addStringAnnotation(interfaceNames[i], annotation); } } } } //Read the super class description this.handleClassDesc(obj, dataStack); } /******************* * Handle a classAnnotation element and return any string annotation * elements in the classAnnotation. * * @param obj The RMIObject to populate with class names. * @param dataStack The remaining data in the ReplyData packet. * @return An ArrayList of strings representing any string annotations extracted from the stream. ******************/ private ArrayList<String> handleClassAnnotation(RMIObject obj, LinkedList<Byte> dataStack) throws BaRMIeInvalidReplyDataPacketException { ArrayList<String> stringAnnotations; byte b; //Create the arraylist stringAnnotations = new ArrayList<String>(); //Read elements from the stream until a TC_ENDBLOCKDATA element is read while((b = dataStack.pop()) != ObjectStreamConstants.TC_ENDBLOCKDATA) { //Handle the annotation switch(b) { //Read string annotations into an array list to return case ObjectStreamConstants.TC_STRING: stringAnnotations.add(this.extractUtf8(dataStack)); break; //Skip over reference annotations case ObjectStreamConstants.TC_REFERENCE: //Read past the reference handle this.extractInt(dataStack); break; //Ignore null annotations... case ObjectStreamConstants.TC_NULL: break; //Unknown annotation type default: throw new BaRMIeInvalidReplyDataPacketException("Unknown classAnnotation element type (0x" + String.format("%02x", b) + ")."); } } //Return the string annotations return stringAnnotations; } /******************* * Handle field descriptions. * * @param obj The RMIObject to populate with class names. * @param dataStack The remaining data in the ReplyData packet. ******************/ private void handleFields(RMIObject obj, LinkedList<Byte> dataStack, byte classDescFlags) throws BaRMIeInvalidReplyDataPacketException { ArrayList<Character> fieldTypeCodes; HashMap<Byte, ArrayList<Character>> classDataDesc; char typeCode; int fieldCount; int i; //Create an array list of field type codes fieldTypeCodes = new ArrayList<Character>(); //Read the number of fields fieldCount = this.extractShort(dataStack); //Read the field descriptions for(i = 0; i < fieldCount; ++i) { //Read the field type code typeCode = (char)dataStack.pop().byteValue(); //Add it to the list fieldTypeCodes.add(typeCode); //Read the field data switch(typeCode) { //Handle primitive types by reading the field name case 'B': case 'C': case 'D': case 'F': case 'I': case 'J': case 'S': case 'Z': //Skip over the field name this.extractUtf8(dataStack); break; //Handle object and array types by reading the field name and class name case '[': case 'L': //Skip over the field name this.extractUtf8(dataStack); //Skip over the class name this.handleStringElement(dataStack); break; //Invalid field type default: throw new BaRMIeInvalidReplyDataPacketException("Invalid field type code (0x" + String.format("%02x", (byte)typeCode) + ")."); } } //Add the field data to the class data description classDataDesc = new HashMap<Byte, ArrayList<Character>>(); classDataDesc.put(classDescFlags, fieldTypeCodes); this._classDataDesc.push(classDataDesc); } /******************* * Handle a string element. * * @param obj The RMIObject to populate with class names. ******************/ private void handleStringElement(LinkedList<Byte> dataStack) throws BaRMIeInvalidReplyDataPacketException { //Handle a string based on the type switch(dataStack.pop()) { //Standard string case ObjectStreamConstants.TC_STRING: this.extractUtf8(dataStack); break; //Long string case ObjectStreamConstants.TC_LONGSTRING: this.extractLongUtf8(dataStack); break; //References case ObjectStreamConstants.TC_REFERENCE: this.extractInt(dataStack); break; //Invalid string type default: throw new BaRMIeInvalidReplyDataPacketException("Invalid string element type."); } } /******************* * Handle class data including extraction of the TC_BLOCKDATA element that * contains the endpoint for the remote object. * * @param obj The RMIObject to populate with class names. * @param dataStack The remaining data in the ReplyData packet. * @param classDataDesc A data structure defining the structure of the class data in the stream. ******************/ private void handleClassData(RMIObject obj, LinkedList<Byte> dataStack, LinkedList<HashMap<Byte, ArrayList<Character>>> classDataDesc) throws BaRMIeInvalidReplyDataPacketException { HashMap<Byte, ArrayList<Character>> desc; ArrayList<Character> fieldTypes; byte classDescFlags; byte objType; //Loop over the class data description elements while(classDataDesc.size() > 0) { //Pop a class data description off the stack (the data of the most-super class is written out first) desc = classDataDesc.pop(); //Get the flags and field types classDescFlags = (byte)desc.keySet().toArray()[0]; fieldTypes = desc.get(classDescFlags); //Read the class data based on the flags if((classDescFlags & ObjectStreamConstants.SC_SERIALIZABLE) == ObjectStreamConstants.SC_SERIALIZABLE) { //Read/skip over the field values based on type for(Character typeCode: fieldTypes) { switch(typeCode) { //Pop eight-byte values off the stack (pop four bytes, then fall through to pop remaining four bytes) case 'J': //Long case 'D': //Double dataStack.pop(); dataStack.pop(); dataStack.pop(); dataStack.pop(); //Pop four-byte values off case 'I': //Integer case 'F': //Float dataStack.pop(); dataStack.pop(); //Pop two-byte values off case 'S': //Short dataStack.pop(); //Pop one-byte values off the data stack case 'B': //Byte case 'C': //Char case 'Z': //Boolean dataStack.pop(); break; //Handle objects case 'L': //Work out the object type objType = dataStack.pop(); switch(objType) { //Recurse back into handleNewObjectElement() to handle object elements case ObjectStreamConstants.TC_OBJECT: this.handleNewObjectElement(obj, dataStack); break; //Handle strings case ObjectStreamConstants.TC_STRING: this.extractUtf8(dataStack); break; //Handle references case ObjectStreamConstants.TC_REFERENCE: this.extractInt(dataStack); break; //Handle NULL objects case ObjectStreamConstants.TC_NULL: break; //Unknown object type default: throw new BaRMIeInvalidReplyDataPacketException("Unexpected byte when handling an object field value."); } break; //Handle arrays case '[': //Or not, for now... throw new BaRMIeInvalidReplyDataPacketException("Invalid field type code ([)."); //Invalid type code default: throw new BaRMIeInvalidReplyDataPacketException("Invalid field type code (" + typeCode + ")."); } } //Read object annotations if necessary and extract the object endpoint if identified if((classDescFlags & ObjectStreamConstants.SC_WRITE_METHOD) == ObjectStreamConstants.SC_WRITE_METHOD) { this.handleObjectAnnotation(obj, dataStack); } } else if((classDescFlags & ObjectStreamConstants.SC_EXTERNALIZABLE) == ObjectStreamConstants.SC_EXTERNALIZABLE) { if((classDescFlags & ObjectStreamConstants.SC_BLOCK_DATA) == ObjectStreamConstants.SC_BLOCK_DATA) { //Read object annotations... throw new BaRMIeInvalidReplyDataPacketException("Class data loading with SC_EXTERNALIZABLE and SC_BLOCK_DATA not implemented yet..."); } else { //Read external contents // - NB: this is class-specific and requires knowledge of the underlying class(es) to parse throw new BaRMIeInvalidReplyDataPacketException("Class data loading with SC_EXTERNALIZABLE and !SC_BLOCK_DATA is class-specific and not available..."); } } } } /******************* * Handle an objectAnnotation element, extracting the object endpoint * details if found. * * @param obj The RMIObject to populate with class names. * @param dataStack The remaining data in the ReplyData packet. ******************/ private void handleObjectAnnotation(RMIObject obj, LinkedList<Byte> dataStack) throws BaRMIeInvalidReplyDataPacketException { byte b; //Read elements from the stream until a TC_ENDBLOCKDATA element is read while((b = dataStack.pop()) != ObjectStreamConstants.TC_ENDBLOCKDATA) { //Handle the annotation switch(b) { //Look for object endpoint details in block data elements case ObjectStreamConstants.TC_BLOCKDATA: //Push the block type back on to the stack and extract endpoint details if found dataStack.push(ObjectStreamConstants.TC_BLOCKDATA); this.extractObjectEndpointFromBlockData(obj, dataStack); break; //Skip over object annotations case ObjectStreamConstants.TC_OBJECT: this.handleNewObjectElement(obj, dataStack); break; //Ignore null annotations... case ObjectStreamConstants.TC_NULL: break; //Unknown annotation type default: throw new BaRMIeInvalidReplyDataPacketException("Unknown classAnnotation element type (0x" + String.format("%02x", b) + ")."); } } } /******************* * Handle a block data element found within an object annotation. * * This block of data may contain the host and port where a remote object * can be accessed. * * @param obj The RMIObject to populate with class names. * @param dataStack The remaining data in the ReplyData packet. ******************/ private void extractObjectEndpointFromBlockData(RMIObject obj, LinkedList<Byte> dataStack) throws BaRMIeInvalidReplyDataPacketException { LinkedList<Byte> blockData; int blockSize; int i; //Read the block data from the stack blockData = new LinkedList<Byte>(); switch(dataStack.pop()) { //Handle TC_BLOCKDATA elements case ObjectStreamConstants.TC_BLOCKDATA: //Read the block size and contents blockSize = Byte.toUnsignedInt(dataStack.pop()); for(i = 0; i < blockSize; ++i) { blockData.add(dataStack.pop()); } break; //Unknown block data type default: throw new BaRMIeInvalidReplyDataPacketException("Invalid block data element type in class annotation."); } //Examine the block data for object endpoint details (note peeking at the block data, not the data stack) if(this.peekShort(blockData) == 10) { //The first two bytes are 0x00 0a, check for the string "UnicastRef" if(this.extractUtf8(blockData).equals("UnicastRef")) { //UnicastRef found in the block data, extract the object's host and port and add them to the object description (note extraction from block data, not data stack) try { obj.setObjectEndpoint(new TCPEndpoint(this.extractUtf8(blockData), this.extractInt(blockData))); } catch(BaRMIeInvalidPortException bipe) { throw new BaRMIeInvalidReplyDataPacketException("UnicastRef contained an invalid port number.", bipe); } } } else if(this.peekShort(blockData) == 11) { //The first two bytes are 0x00 0b, check for the string "UnicastRef2" if(this.extractUtf8(blockData).equals("UnicastRef2")) { //UnicastRef2 found in the block data, extract the object's host and port and add them to the object description try { //Skip over a byte blockData.pop(); //Extract the host name and port obj.setObjectEndpoint(new TCPEndpoint(this.extractUtf8(blockData), this.extractInt(blockData))); } catch(BaRMIeInvalidPortException bipe) { throw new BaRMIeInvalidReplyDataPacketException("UnicastRef contained an invalid port number.", bipe); } } } } /******************* * Read a short from the data stack. * * @param dataStack The remaining data from the RMI ReplyData packet. * @return The short extracted from the serialisation data. ******************/ private short extractShort(LinkedList<Byte> dataStack) { //Read two bytes from the stack and bit-shift/mask them into a short return (short)( ((dataStack.pop() << 8) & 0xff00) + ( dataStack.pop() & 0xff) ); } /******************* * Return a short from the data stack without popping the short off the * stack. * * @param dataStack The remaining data from the RMI ReplyData packet. * @return The short extracted from the serialisation data. ******************/ private short peekShort(LinkedList<Byte> dataStack) { //Peek at the next two bytes from the stack and bit-shift/mask them into a short return (short)( ((dataStack.get(0) << 8) & 0xff00) + ( dataStack.get(1) & 0xff) ); } /******************* * Read an int from the data stack. * * @param dataStack The remaining data from the RMI ReplyData packet. * @return The int extracted from the serialisation data. ******************/ private int extractInt(LinkedList<Byte> dataStack) { //Read four bytes from the stack and bit-shift/mask them into an int return (int)( ((dataStack.pop() << 24) & 0xff000000) + ((dataStack.pop() << 16) & 0xff0000) + ((dataStack.pop() << 8) & 0xff00) + ( dataStack.pop() & 0xff) ); } /******************* * Read a long from the data stack. * * @param dataStack The remaining data from the RMI ReplyData packet. * @return The long extracted from the serialisation data. ******************/ private long extractLong(LinkedList<Byte> dataStack) { //Read eight bytes from the stack and bit-shift/mask them into a long return (long)( ((dataStack.pop() << 56) & 0xff00000000000000L) + ((dataStack.pop() << 48) & 0xff000000000000L) + ((dataStack.pop() << 40) & 0xff0000000000L) + ((dataStack.pop() << 32) & 0xff00000000L) + ((dataStack.pop() << 24) & 0xff000000 ) + ((dataStack.pop() << 16) & 0xff0000 ) + ((dataStack.pop() << 8) & 0xff00 ) + ( dataStack.pop() & 0xff ) ); } /******************* * Read a UTF8 string from the data stack. * * @param dataStack The remaining data from the RMI ReplyData packet. * @return The string extracted from the serialisation data. ******************/ private String extractUtf8(LinkedList<Byte> dataStack) { StringBuilder builder; int stringLength; int i; //Read the string length from the stack stringLength = Short.toUnsignedInt(this.extractShort(dataStack)); //Read the string from the data stack builder = new StringBuilder(); for(i = 0; i < stringLength; ++i) { builder.append((char)dataStack.pop().byteValue()); } //Return the result return builder.toString(); } /******************* * Read a long UTF8 string from the data stack. * * @param dataStack The remaining data from the RMI ReplyData packet. * @return The string extracted from the serialisation data. ******************/ private String extractLongUtf8(LinkedList<Byte> dataStack) { StringBuilder builder; long stringLength; int i; //Read the string length from the stack stringLength = this.extractLong(dataStack); //Read the string from the data stack builder = new StringBuilder(); for(i = 0; i < stringLength; ++i) { builder.append((char)dataStack.pop().byteValue()); } //Return the result return builder.toString(); } }