Friday, May 5, 2017

BACnet - Bro - Spicy Part 2

So in BACnet - Bro - Spicy Part 1 I explained that I've been working with the BACnet protocol developing a CTF challenge.  I'm using Bro with a Spicy parser and a logging script to detect when a specific BACnet command is send in a packet. When triggered, the sending IP Address is recorded in the log file. That file is then used to restrict access to a webpage that contains the flag. Probably not the best plan, but it got me working on something and thinking through a problem.


The Plan:

  1. Set up some virtual machines in my VMware environment to experiment. 
    1. Install the BACnet-stack on two: server and client. (see BACnet - Bro - Spicy Part 1)
    2. Install Docker and the rsmmr-hilti docker image on the bacnet server machine.
  2. Write a Bro script that uses the events created by the BACnet parser to log the source IP address from packets with the correct BACnet command.
  3. Install the php-apache docker image on the bacnet server machine and run it with the same shared volume as the rsmmr-hilti container where the log file is saved.
  4. Write a PHP webpage/script that shows the flag only to the IPs in the log file and a different page to all other IPs.

1.2) Install Docker and the rsmmr-hilti docker image on a third machine.

Docker was very easy and I was up and running very quickly. The rsmmr-hilti image already has Bro and Spicy installed and working. It also has BACnet parser Spicy scripts included and some test bro scripts to get an idea of how they work.


  1. I started with a fresh install of Ubuntu 16.04 LTS and OpenSSH server installed and really just followed the steps at the Docker Community Edition for Ubuntu site. 
    • sudo apt-get -y install apt-transport-https ca-certificates curl
    • curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
    • sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
    • sudo apt-get update
    • sudo apt-get -y install docker-ce
    • sudo docker run hello-world
  2. Next I installed the rsmmr-hilti image from https://hub.docker.com/r/rsmmr/hilti/ 
      • mkdir ~/hilti-docker
      • cd hilti-docker
      • sudo docker pull rsmmr/hilti
    1. The hilti site has instructions to run the docker image and test bro and spicy.
      • Start the container in interactive mode with a pseudo-TTY
        • sudo docker run -i -t "rsmmr/hilti"  
      • Verify the BRO_PLUGIN_PATH is /opt/hilti/build/bro/
        • env
      • Use the spicy-driver tool to test executing a spicy parser script:
        • spicy-driver hello-world.spicy
      • Run bro with the verbose plugin check option. This returns info about what the provides.
        • bro -NN Bro::Hilti
      • Tests running the script with bro and the spicy plugin 
        • bro hello-world.spicy
      • Runs bro at the command line reading in the trace pcap file, using the .evt file that defines events and the the .bro file that describes the response to those events. In this case, it writes to standard out information about detected SSH banners.
        • bro -r ssh-single-conn.trace ./ssh-banner.bro ssh.evt
    2. From here, I started looking at what was available in the BACnet scripts.  I verified the bacnet.evt plugin was installed and working.
      • bro --NN bacnet.evt
      • grep for bacnet and you should see:
    Bro::Hilti - Dynamically compiled HILTI/Spicy functionality (*.spicy, *.evt, *.hlt) (dynamic, version 0.1)
    [Analyzer] spicy_BACnet (ANALYZER_SPICY_BACNET, enabled)
    [Event] bacnet_message
    [Event] bacnet_bvlc_result
    1. The bacnet .bro test script files are located in:
      • /opt/hilti/bro/tests/spicy/bacnet
    2. The bacnet trace files are located in:
      •  /opt/hilti/bro/Traces/bacnet
    3. The docker image comes with some pcap files capturing specific bacnet traffic and bro scripts to detect bacnet commands.  For example, the who-has.pcap captures who-has traffic and the apdu_who.bro script will detect it.  This is the command to test a .bro script is that uses a definition in the bacnet.evt plugin:
      • bro -r /opt/hilti/bro/tests/Traces/bacnet/who-has.pcap bacnet.evt /opt/hilti/bro/tests/spicy/bacnet/apdu_who.bro


    2) Write a Bro script that uses the events created using the BACnet parser to log the source IP address from packets with the correct BACnet command.

    Spicy (formerly BinPAC++) is a language used to write a protocol parser for Bro. Spicy files have the extension .spicy and they define how to parse a packet.  Bro uses .evt files to determine which parser is used to parse each kind of traffic and which events to generate based on that parsed traffic.  In my case, the bacnet.spicy and bacnet.evt files already existed within the rsmmr/hilti docker image, so I only needed to modify them to create an event based on the traffic I was watching for; specifically the write-property command packet.

    bacnet.spicy
    In the bacnet.spicy file, I needed to keep track of the segments. All segments of a segmented request must have the same invokeID. The invokeID should be incremented for each new BACnet-Confirmed-Request-PDU. So I modified the /opt/hilti/bro/spicy/bacnet.spicy file as follows:

    [...]
    type PDU_confirmedRequest = unit(head: bytes, len: uint16) {
      var len: uint16 = 4;
      var segmentation_status: bool = False;

      [...]

      switch ( self.confirmed_service_choice ) {
        BACnetConfirmedServiceChoice::confirmedEventNotification -> confirmedEventNotification: ConfirmedEventNotification_Request( len - self.len );
        BACnetConfirmedServiceChoice::readProperty -> readProperty: ReadProperty_Request( len - self.len, self.invokeID );
        BACnetConfirmedServiceChoice::writeProperty -> writeProperty: WriteProperty_Request( len - self.len, self.invokeID );

      [...]

      switch ( self.confirmed_service_choice ) {
        BACnetConfirmedServiceChoice::confirmedEventNotification -> confirmedEventNotification: ConfirmedEventNotification_Request( len - self.len );
        BACnetConfirmedServiceChoice::readProperty -> readProperty: ReadProperty_Request( len - self.len, self.invokeID );
        BACnetConfirmedServiceChoice::writeProperty -> writeProperty: WriteProperty_Request( len - self.len, self.invokeID );

    [...]

    type WriteProperty_Request = unit(len: uint16, invokeID: uint8) {
      # context-specific tags 0 - 4

      first_tag: BACnetTag;

    [...]

    I also needed to work around an issue with the property value I was looking for being part of an array, but the parser not fully working yet with this array. What I changed worked for my very scoped purpose, but really isn't the solution to the array issue overall.  However, I again modified the /opt/hilti/bro/spicy/bacnet.spicy file as follows:

    [...]

    type WriteProperty_Request = unit(len: uint16, invokeID: uint8) {
      # context-specific tags 0 - 4
      first_tag: BACnetTag;
      objectIdentifier: BACnetContextMessage(BACnetType::BACnetObjectIdentifier, 0, self.first_tag);
      propertyIdentifier_tag: BACnetTag; # tag 1
      propertyIdentifier: bytes &length=self.propertyIdentifier_tag.lvt &convert=BACnetPropertyIdentifier($$.to_uint(Spicy::ByteOrder::Network));
      propertyArrayIndex: BACnetContextMessage(BACnetType::UnsignedInteger, 2, self.propertyIdentifier_tag);
      propertyValue_tag: BACnetTag if ( self.propertyArrayIndex.tag.tag != 3 ); # check if previous tag was optional. This tag = 3
      #propertyValue: BACnetArray_Partial(3);
      propertyValue: BACnetMessage;

    [...]


    bacnet.evt
    I then needed to modify the bacnet.evt file to generate an event for the write property I was watching for. I had to add some logic to create different events for write property packets that were writing analog (realval) values and those writing digital (enumerated) values. Then I used those to generate and event when the property being written was an enumerated value of type 4 (Binary Output), instance 3, property identifier 85 (present_value). This was the packet I would be looking for. 

    I added the following lines to the end of  /opt/hilti/bro/spicy/bacnet.evt:

    [...]

    on BACnet::WriteProperty_Request if (self.propertyValue.tag.tpe == BACnetType::Real)
      -> event bacnet_writeproperty_request($conn, cast<uint64>(invokeID), self.objectIdentifier.oid.tpe, cast<uint64>(self.objectIdentifier.oid.data.instanceNumber), self.propertyIdentifier, self.propertyValue.realval);

    on BACnet::WriteProperty_Request if ((self.objectIdentifier.oid.tpe == 4) && (self.propertyValue.tag.tpe == BACnetType::Enumerated) && (self.objectIdentifier.oid.data.instanceNumber == 3) && (self.propertyIdentifier == 85))
      -> event bacnetctf_writeproperty_request($conn, cast<uint64>(invokeID), self.objectIdentifier.oid.tpe, cast<uint64>(self.objectIdentifier.oid.data.instanceNumber), self.propertyIdentifier, self.propertyValue.unsigned);


    bacnetctf.bro
    A Bro script uses events that are generated to trigger some actions. I wrote a bro script that would pull the IP address from the selected packet that triggered the event my bacnet.evt file additions created. It wrote that IP to a file which would be used later on. Here is the script called bacnetctf.bro:

    #Module to parse bacnet ctf writes to binary output 3 on device 422

    #load processes the __load__.bro scripts in the directories loaded 
    #which basically includes libraries
    @load base/protocols/conn

    #create namespace 
    module bacnetCTF;

    export {
    #Create an ID for our new stream. 
    redef enum Log::ID += { LOG };

    #Define the record type that will contain the data to log.
    type Info: record {
    packet_src: addr &log;
    };
    }

    event bro_init()  &priority=5
    {
    #Create the stream. this adds a default filter automatically
    Log::create_stream(bacnetCTF::LOG, [$columns=Info, $path="bacnetCTF"]);
    }

    #add a new field to the connection record so that data is accessible in variety of event handlers
    redef record connection += {
    bacnetctf: Info &optional;
    };

    event bacnetctf_writeproperty_request(c: connection, invokeID: count, tpe: BACnet::BACnetObjectType, instance: count, property: BACnet::BACnetPropertyIdentifier, propval: string)
       {

       #Don't count packets from ssh box (simulate coming from different subnet to force packet crafting)
     local src_ip: addr;

     if (c$id$orig_h == 192.168.141.111)
     {
           src_ip = 192.168.1.1;
     } else
     {
           src_ip = c$id$orig_h;
     }

      #Log format
      local rec: bacnetCTF::Info = [$packet_src=src_ip];

      c$bacnetctf = rec;

      Log::write(bacnetCTF::LOG, rec);

    }



    Part 3 will pick up from here with building the docker image with these new files.




    No comments:

    Post a Comment