Saturday, April 21, 2007

use scruby sniffing and dissecting the network

http://sylvainsarmejeanne.free.fr/projects-scruby

Version 0.1 of Scruby. It is released under the GPL v2.
What is Scruby?
Scruby is a portable, customizable packet creation and sending/sniffing tool written in Ruby. It was tested on NetBSD and GNU/Linux, and should theoretically work on some other platforms such as FreeBSD, OpenBSD, Mac OS X and proprietary Unixes.

This is just of proof of concept of a minimal, portable, generic, efficient (see section on performance) implementation of Scapy concepts (see Philippe Biondi's page at http://www.secdev.org/projects/scapy/), with readable, commented code and a good documentation.

Thanks to the similarities between Python and Ruby, you can copy your favorite dissectors for Scapy and paste them in Scruby without any modification (see section "Adding a dissector or a field for a new protocol").

You can contact me at sylvain.sarmejeanne.ml [AT] gmail.com

Screenshots
See Scruby in action:
NetBSD: sniffing and dissecting the network
GNU/Linux: sending a packet at layer 2 to Google and receiving the response
User guide
Installation
First download the Scruby archive from the section above.
GNU/Linux, *BSD, etc
install libpcap from your favorite package manager or from the tcpdump homepage. Version 0.9.3 (or above) is mandatory for sending packets. Version 0.8 (or above) is mandatory for being able to stop a running sniff.
the "standard" PCAP wrapper for Ruby is a bit weird, that's why Scruby is based on PcapRub, a minimal PCAP wrapper. Uncompress the archive, ruby extconf.rb, make, make install (version 0.6 works well). A local copy can be found here.
enjoy ./scruby.rb :) (some functions require you to be root).
Windows
I just didn't have enough time to test it, but it should work... If you are looking for a Scapy clone that works under Windows, try Scaperl :)
Known limitations
Don't expect Scruby to be as exhaustive as Scapy, it is just a proof of concept. The most important limitations in the current version are:
it was only tested on Ethernet link type.
only a few field types are implemented (see the list of fields).
only a few protocols are implemented (see the list of dissectors).
there is no BitField (for IP, bit fields are grouped by bytes...).
"undefined" is not a correct value for a field. As a consequence, checksum fields cannot have an arbitrary value, they are computed each time.
warnings or error messages are written to STDOUT making it irrelevant for honeyd scripts.
in the global configuration, only one gateway MAC address can be recorded at a time.
the documentation is not in the RDoc format.
there is no support for IPv6.
Getting help
The first thing to do in Scruby may be to get some general help. The help command displays a message that introduces Scruby, gives a link to this page, basically explains what you can do with Scruby and lists the available dissectors:
$ ./scruby.rb
Welcome to Scruby (0.1) Copyright 2007 Sylvain SARMEJEANNE
If you're lost, just shout for "help".
scruby> help
This is Scruby, a portable, customizable packet creation, [...]

See http://sylvainsarmejeanne.free.fr/projects/scruby for more information.

With Scruby, you can:
[...]

Available dissectors:
["Ether", "IP" [...] ]

Available functions (type "help '<function>'" to have detailed information):
["send", "sendp" [...] ]
Help on a specific function
The last portion of the general help message gives the list of available functions. To get specific help on one of these (e.g. the sniff function):
scruby> help 'sniff'
This function captures packets on an interface. [...]
In this kind of message, each available argument for the function is explained and its default value is given, as well as usage examples.
Configuration
Global settings are stored in $conf:
scruby> $conf
iface (default interface): eth0
gateway_hwaddr (gateway Ethernet address): 00:00:00:00:00:00
promisc (promiscuous mode): true
To override a setting:
scruby> $conf.iface = "eth1"
scruby> $conf
iface (default interface): eth1
gateway_hwaddr (gateway Ethernet address): 00:00:00:00:00:00
promisc (promiscuous mode): true
Creating and modifying packets
Creating a packet is easy using the Ruby syntax. To create a simple IP packet with default values:
scruby> p=IP()
<IPv4 |>
To display detailed information about your packet:
scruby> p.show
###[ IPv4 ]###
version_ihl = 0x45
tos = 0x0
len = 20
id = 0x0
flags_offset = 0
ttl = 64
proto = 6
chksum = 0x0
src = 127.0.0.1
dst = 127.0.0.1
To modify your packet:
scruby> p.ttl = 128
scruby> p.dst = "www.google.com"
This is the same as:
scruby> p=IP(:ttl=>128, :dst=>"www.google.com")
If you just want to display non-default parameters:
scruby> p
<IPv4 ttl=128 dst=www.google.com |>
To create a real packet with several layers, just use the "/" (division) operator to bind layers or raw data together:
scruby> p=IP(:dst=>"www.google.com")/TCP()/"GET / HTTP 1.0\r\n\r\n"
scruby> p
<IPv4 dst=www.google.com |><TCP |><Raw load="GET / HTTP 1.0\r\n\r\n" |>
scruby> p.show
###[ IPv4 ]###
version_ihl = 0x45
tos = 0x0
len = 20
id = 0x0
flags_offset = 0
ttl = 64
proto = 6
chksum = 0x0
src = 127.0.0.1
dst = www.google.com
###[ TCP ]###
sport = 1024
dport = 80
seq = 0
ack = 0
dataofs_reserved = 80
flags = 0x2
window = 8192
chksum = 0x0
urgptr = 0
###[ Raw ]###
load = "GET / HTTP 1.0\r\n\r\n"
Note that unlike Scapy, binding layer B over A will not modify layer A, for performance reason (Scapy's behaviour may become an option in a future release). That is to say, the following is a incorrect ICMP packet:
scruby> p=IP()/ICMP()
You have to write:
scruby> p=IP(:proto=>1)/ICMP()
Available protocols/dissectors
The following protocols/dissectors can be used to build packets:
Ether (Ethernet and Linux loopback header)
IP (IPv4)
ICMP
TCP
UDP
Raw (any data)
ClassicBSDLoopback (loopback header for NetBSD, FreeBSD and Mac OS X)
OpenBSDLoopback (loopback header for OpenBSD)
A simple command will display the different fields for a dissector and their default values. Let's take IP as an example:
scruby> IP().show
###[ IPv4 ]###
version_ihl = 0x45
tos = 0x0
len = 20
id = 0x0
flags_offset = 0
ttl = 64
proto = 6
chksum = 0x0
src = 127.0.0.1
dst = 127.0.0.1
If your terminal supports it, you can have TAB completion for functions and dissectors:
scruby> p=I[tab][tab]
ICMP IP
scruby> s[tab][tab]
sendp sniff str
You can also have an history:
scruby> p=IP()
scruby> q=UDP()
scruby> [up]q=UDP()[up]p=IP()
Building a packet from a string (dissecting a string)
To create a Scruby packet from a string, pass it as an argument to the corresponding dissector:
scruby> p=IP(:dst=>"www.google.com")/TCP()/"GET / HTTP 1.0\r\n\r\n"
scruby> s=str(p)
scruby> puts "result=#{IP(s)}"
result=<IPv4 len=58 chksum=0x635c dst=66.249.85.104 |><TCP chksum=0xa47e |>
<Raw load="GET / HTTP 1.0\r\n\r\n" |>
This mechanism is used to build Scruby packets from bytes sniffed on a interface.
If not enough bytes are passed to dissect the whole fields of the protocol, Scruby dissects as much as it can:

scruby> Ether('A'*5)
<Ethernet |>
scruby> Ether('A'*6)
<Ethernet dst=41:41:41:41:41:41 |>
Note that dissecting a string returns a Packet and not a Layer, that's why you won't be able to directly access its fields. Use the layers_list member instead (this behaviour may change in a future release):

scruby> p=IP('A')
<IPv4 version_ihl=0x41 |>
scruby> p.ttl
undefined method `ttl' for #<Scruby::Packet:0xb7cec0f4>
scruby> p.layers_list[0].ttl
64
Sending a packet at layer 3
Note that if Libdnet is not available (which is the case in the current version), sending at layer 3 is not possible. Sending at layer 2 (see below) works, just specify the correct values for Ethernet (source) and IP (source and destination).
Sending a packet at layer 2
Make sure the configuration is good. The function used here is sendp:
scruby> p=Ether(:src=>"00:50:70:34:88:b4")/IP(:src=>"10.0.0.1", :dst=>"www.google.com")/TCP()/
"GET / HTTP 1.0\r\n\r\n"
scruby> sendp(p)
Sent on eth0.
The Ethernet destination field is filled with $conf.gateway_hwaddr if nothing is specified. After sending:
scruby> p
<Ethernet dst=00:07:cb:0c:67:a6 src=00:50:70:34:88:b4 |>
<IPv4 len=58 chksum=0xc429 src=11.22.33.44.55 dst=www.google.com |><TCP chksum=0x54c |>
<Raw load="GET / HTTP 1.0\r\n\r\n" |>
Sniffing on an interface
By default, the sniff function listens on the interface defined in $conf.iface (see configuration section above):
scruby> sniff
listening on eth0
1161032765.823136 <Ethernet dst=00:07:cb:0c:67:a6 src=00:50:70:34:88:b4 |>
<IPv4 len=59 id=0x48f flags_offset=16384 proto=17 chksum=0xcd3 src=11.22.33.44.55 dst=212.27.54.252 |>
<Raw load="5'JPawwwgooglefr" |>

1161032765.853398 <Ethernet dst=00:50:70:34:88:b4 src=00:07:cb:0c:67:a6 |>
<IPv4 len=103 flags_offset=16384 ttl=59 proto=17 chksum=0x1636 src=212.27.54.252 dst=11.22.33.44.55 |>
<Raw load="5ShawwwgooglefrÀ
XwwwgooglecomÀ+XÑUc" |>

[...]
To stop sniffing, press ^C. sniff has some interesting options, see the reference guide below.
Note that non-printable characters are displayed with their octal (not hexadecimal) values (this is the default behaviour of the inspect function in Ruby, this may change in a future Scruby version).

Scripting Scruby
Writing scripts that use Scruby is very easy. This may be one of the shortest sniffers ever:
#! /usr/bin/env ruby
require 'scruby'
module Scruby
sniff
end
The first two lines import everything from Scruby (same as "from scapy import *" in Python). Then write your code as if you were using the interpreter.
What about this useless tiny IDS that detects incoming packets on TCP/445 (Microsoft's CIFS):


#! /usr/bin/env ruby
require 'scruby'
module Scruby

def Scruby.callback(pcap, packet)
# Getting the link type
linktype = pcap.datalink

# Ethernet
if linktype == Pcap::DLT_EN10MB
dissect = Ether(packet)
l4 = dissect.layers_list[2]

return if l4.nil?
return if not l4.instance_of?(TCP)
return if not l4.dport == 445

puts "just received a packet on TCP/445: #{dissect}"

# Unknown link type
else
puts "Unknown link type: #{linktype}"
end
end

sniff(:prn=>:callback)

end

Reference guide
The help here is the same as in Scruby (help '<function>').
sendp
This function sends a packet at layer 2 on the default interface ($conf.iface). If not specified, the Ethernet destination will be $conf.gateway_hwaddr.
If Libdnet is available, source Ethernet address and source IP address are automatically filled according to this interface.

example> p=Ether(:src=>"00:11:22:33:44:55")/IP(:src=>"1.2.3.4", :dst=>"www.google.com")/TCP()/
"GET / HTTP 1.0\r\n\r\n"
example> sendp(p)
Sent on eth0.

str
This function transforms a packet into a string ready to be sent on the wire (that is to say, it "packs" it).
example> p=IP(:src=>"1.2.3.4", :dst=>"www.google.com")/TCP()/"GET / HTTP 1.0\r\n\r\n"
example> str(p).inspect
"E\000\000:\000\000\000\000@\006\035\374\001\002\003\004\321U\207g\004\000\000P\000\000\000\000\000
\000\000\000P\002 \000_\036\000\000GET / HTTP 1.0\r\n\r\n"
sniff
This function captures packets on an interface. The default capture interface is stored in $conf.iface.
Without any argument, sniff captures on the default interface:

example> sniff
listening on eth0

1158608918.45960 <Ethernet dst=00:11:22:33:44:55 src=55:44:33:22:11:00 |>
<IPv4 len=84 flags_offset=16384 proto=1 chksum=0x7c0f src=1.2.3.4 dst=4.3.2.1 |>
<ICMP chksum=17905 id=16922 seq=1 |>

1158608918.124147 <Ethernet dst=55:44:33:22:11:00 src=00:11:22:33:44:55 |>
<IPv4 len=84 flags_offset=16384 ttl=244 proto=1 chksum=0xc80e src=4.3.2.1 dst=1.2.3.4 |>
<ICMP type=0 chksum=19953 id=16922 seq=1 |>
The following arguments are available (with the default values between brackets):
iface: the interface to listen on ($conf.iface)
prn: a function that will be called for each packet received (:sniff_simple)
filter: a PCAP filter (undef)
count: the number of packets to capture. An argument less than or equal to 0 will read "loop forever" (-1)
promisc: capture in promiscuous mode or not ($conf.promisc)
timeout: capture timeout in milliseconds (1, seems not to work?)
store: not implemented yet
offline: not implemented yet
The prn argument is the most interesting one, it allows you to customize the behaviour of the sniff function (see section on scripting for examples):
example> def Scruby.my_prn(pcap, packet) puts "GOT ONE: raw=|#{packet.inspect}|" end
example> sniff(:iface=>"eth1", :prn=>:my_prn, :filter=>"icmp", :count=>2)
listening on eth0
GOT ONE: raw=|"\000\a\313\fg\246\000Pp4\210\264\b\000E\000\000T\000\000@\000@\001\030KR\357
\313I\324\e0\n\b\000\e\360}!\000\001O2%F\363q\f\000\b\t\n\v\f\r\016\017\020\021\022\023\024
\025\026\027\030\031\032\e\034\035\036\037 !\"\#$%&'()*+,-./01234567"|
GOT ONE: raw=|"\000Pp4\210\264\000\a\313\fg\246\b\000E\000\000T\235*\000\000{\001\200 \324
\e0\nR\357\313I\000\000#\360}!\000\001O2%F\363q\f\000\b\t\n\v\f\r\016\017\020\021\022\023
\024\025\026\027\030\031\032\e\034\035\036\037 !\"\#$%&'()*+,-./01234567"|
Note that by default, packets captured are not stored in memory for performance reason. To stop sniffing, press ^C.
Developer guide
Global organization
Scruby consists of several modules. In the archive, you will find the following files:
scruby.rb: the main script, imports everything from the other modules and spawns an interpreter.
layer.rb: definition of a Layer (instantiation of a dissector).
packet.rb: definition of a Packet (an array of Layers).
dissectors.rb: implemented dissectors (Ether, IP, etc). A dissector is like a list a Fields.
field.rb: implemented fields (for integers, shorts, bytes, strings, etc) to be used for building protocol dissectors.
func.rb: end-user functions (sniff, sendp, etc).
conf.rb: global configuration.
const.rb: global constants.
help.rb: everything related to help.
Adding a dissector or a field for a new protocol
Scruby has been explicitly devised so that the syntax for dissectors is the same as in Scapy. Let's have a look at how ICMP is implemented in both cases:
ICMP in Scapy

class ICMP(Packet):
name = "ICMP"
fields_desc = [ ByteEnumField('type',8, icmptypes),
ByteField('code',0),
XShortField('chksum', None),
XShortField('id',0),
XShortField('seq',0) ]
ICMP in Scruby
class ICMP<Layer
def method_missing(method, *args) return Scruby.field(method, *args) end

attr_accessor :type, :code, :chksum, :id, :seq

def init
@protocol = 'ICMP'
@fields_desc = [ ByteField('type', ICMPTYPE_ECHO),
ByteField('code', 0),
XShortField('chksum', 0),
XShortField('id', 0),
XShortField('seq', 0) ]
end

Yes, you just have to copy and paste dissectors from Scapy to get them work in Scruby :)
Note: in this version, you have to give "attr_accessors" in plain text AND the corresponding fields. I had a piece of code that did the trick automatically, but it was reeeeeeally slow (test 1 took 20 seconds instead of 9).

And some specific functions follow (checkum computation, etc).
Let's say you want to implement MySuperProtocol (MSP). In dissectors.rb, create a new package, inherit from Layer, describe your protocol (name and fields) and write specific functions if needed. Finally, don't forget to add MSP to DISSECTOR_LIST at the end of dissector.rb.

class MSP<Layer
def method_missing(method, *args) return Scruby.field(method, *args) end

# Global definition (name and fields)
attr_accessor :foo, :bar, :chksum

def init
@protocol = 'MySuperProtocol'
@fields_desc = [ ByteField('foo', 1),
StrField('bar', 2),
XShortField('chksum', 0) ]
end

def pre_send(underlayer, payload)
self.chksum = 0
self.chksum = Layer.checksum(self.tonet() + payload)
end
end

pre_send is a special method you can implement. It will be called just before sending packets and can be used for any purpose, e.g. checksum computation. Have a look at dissectors.rb for real-life examples.

Now, use your new MSP dissector:

scruby> p=MSP(:foo=>"A", :bar=>"pouet")
<MySuperProtocol foo=A bar="pouet" |>
Here is the list of fields that can be used to build dissectors:

StrField(field name, default value): string
ByteField(field name, default value): byte
XByteField(field name, default value): byte with hexadecimal display
ShortField(field name, default value): short
XShortField(field name, default value): short with hexadecimal display
LongField(field name, default value): long
LELongField(field name, default value): long (little endian order)
HostOrderLongField(field name, default value): long (host order)
IntField(field name, default value): integer
IPField(field name, default value): IP address
MACField(field name, default value): Ethernet MAC address
When implementing a new protocol, you may need to implement a new field. Imagine you want to implement a field for little endian short integers, with hexa display:

class XLEShortField<Field
def init
@format = 'v'
end
end
That's all for the general definition of the format. Looking at field.rb, you will see that there are 3 functions in the Field package that you can implement in your own fields:
fromnet: converts from network to internal encoding (e.g. IP addresses displayed as a 4-dotted number)
tonet: converts from internal encoding to network (e.g. IP address in 4-dotted form to the real network number)
tohuman: converts from internal encoding to human display (e.g. display checksum as "0xDEADBEEF")
In our XLEShortField example, we have to implement tohuman to get hexa display:
def tohuman(value)
return sprintf('0x%x', value)
end
In real life, you will first want to write a field for LEShortField, and then write another field for XLEShortField inheriting from LEShortField (have a look at XByteField for an example of this).
At the end of dissector.rb, you will notice a special hash name "$layer_bounds". There are stored the links between layers and field values. For Ethernet, this reads "if Ether type is ETHERTYPE_IPv4 (0x800, see const.rb), then the upper layer is IP":

'Ether' => [
['type', ETHERTYPE_IPv4, IP]
],
When adding a new protocol, don't forget to add information to this hash so as to be able to dissect the new protocol.
Performance
It seems that Scruby performs quite well compared to Scapy. Performance is in fact one of the reasons why I began Scaperl, which was used as a basis for Scruby. Note that Scruby was not really profiled yet (there exist special tools in Ruby for that purpose).
I wrote two small test (yes, there is not much to test in Scruby yet :):

Time to dissect M different strings
For this test to be honest, I removed all the protocols in Scapy's layer_bounds that were not implemented in Scruby.
Scapy

$ cat test1.py
#! /usr/bin/env python

from scapy import *

s = "\x00\x07\xcb\x0c\x67\xa6\x00\x50\x70\x34\x88\xb4\x08\x00\x45\x00\x02\x62\x9d\x23\x40"
s += "\x00\x40\x06\x84\xe8\x52\xef\xcb\x49\x40\xe9\xb7\x68\xbf\xe9\x00\x50\xfc\xec\x17"
s += "\x4e\x50\x75\x81\x49\x50\x18\x7f\xff\xf7\x7d\x00\x00\x47\x45\x54\x20\x2f\x20\x48"
s += "\x54\x54\x50\x2f\x31\x2e\x31\x0d\x0a\x0d\x0a"

for i in range(10000):
p=Ether(s+str(i))

$ time ./test1.py
real 0m9.707s
user 0m9.533s
sys 0m0.128s

Scruby
$ cat test1.rb
#! /usr/bin/env ruby

require 'scruby'
module Scruby

s = "\x00\x07\xcb\x0c\x67\xa6\x00\x50\x70\x34\x88\xb4\x08\x00\x45\x00\x02\x62\x9d\x23\x40";
s += "\x00\x40\x06\x84\xe8\x52\xef\xcb\x49\x40\xe9\xb7\x68\xbf\xe9\x00\x50\xfc\xec\x17";
s += "\x4e\x50\x75\x81\x49\x50\x18\x7f\xff\xf7\x7d\x00\x00\x47\x45\x54\x20\x2f\x20\x48";
s += "\x54\x54\x50\x2f\x31\x2e\x31\x0d\x0a\x0d\x0a";

for i in (0..9999)
p=Ether(s+i.to_s)
end
end

$ time ./test1.rb
real 0m9.208s
user 0m8.633s
sys 0m0.528s

Note: I expect even better performance with Ruby 2.0 (e.g. the same kind of performance I got with Scaperl).
Time to create a packet will N different layers
Scapy
$ cat test2.py
#! /usr/bin/env python

from scapy import *;

p=IP()

for i in range(500):
p/=IP(len=i)

$ time ./test2.py
real 0m26.908s
user 0m26.226s
sys 0m0.264s

Scruby
$ cat test2.rb
#! /usr/bin/env ruby

require 'scruby'
module Scruby

p=IP()

for i in (0..499)
p/=IP(:len=>i)
end
end

$ time ./test2.rb
real 0m0.544s
user 0m0.504s
sys 0m0.020s

No comments :