Scruby is released under the GPL v2. Have a look at the CHANGELOG.
Version 0.3 of Scruby (03/2008)
Scruby is now part of the Metasploit Framework (01/2008).
For Metasploit users, H D Moore has hacked on the source to make it fit better into the framework. Pick his version here: version 0.2.1-hdm of Scruby. UPDATE1: a newer version is available here: version 0.2.1-hdm-2 of Scruby (10/2007).
Version 0.2.2 of Scruby (02/2008)
Version 0.2.1 of Scruby (09/2007)
Version 0.2 of Scruby (09/2007)
Version 0.1 of Scruby (04/2007)
Scruby is a portable, customizable packet creation and sending/sniffing tool written in Ruby. It was tested on NetBSD, GNU/Linux and MacOS X, and should theoretically work on some other platforms such as FreeBSD, OpenBSD, and proprietary Unixes.
This is just of proof of concept of a minimal implementation of Scapy concepts in Ruby.
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
$ ./scruby.rb Welcome to Scruby, Copyright 2007 Sylvain Sarméjeanne If you're lost, just shout for "help". scruby> help This is Scruby, a portable, customizable packet creation, [...] See http://sylv1.tuxfamily.org/projects/scruby.html for more information. With Scruby, you can: [...] Available dissectors: ["Ether", "IP" [...] ] Available functions (type "lsc 'myfunction'" to have detailed information): ["send", "sendp", "ls", "lsc"]
scruby> lsc '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.
scruby> $conf iface (default interface): eth0 gateway_hwaddr (gateway Ethernet address): 00:00:00:00:00:00 promisc (promiscuous mode): trueTo 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
scruby> p=IP() <IP |>To display detailed information about your packet:
scruby> p.show ###[ IPv4 ]### version = 4 ihl = 5 tos = 0x0 len = 20 id = 0x0 flags = 0 () frag = 0 ttl = 64 proto = 6 (TCP) chksum = 0x0 src = 127.0.0.1 dst = 127.0.0.1To 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 <IP 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" <IP dst=www.google.com |><TCP |><Raw load="GET / HTTP 1.0\r\n\r\n" |> scruby> p.show ###[ IPv4 ]### version = 4 ihl = 5 tos = 0x0 len = 20 id = 0x0 flags = 0 () frag = 0 ttl = 64 proto = 6 (TCP) chksum = 0x0 src = 127.0.0.1 dst = www.google.com ###[ TCP ]### sport = 1024 dport = 80 seq = 0 ack = 0 dataofs = 5 reserved = 0 flags = 2 (SYN) 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()
As IP.proto is an Enum field, you could also have written (see below for more on this):
scruby> p=IP(:proto=>'ICMP')/ICMP()
scruby> ls 'IP' Field name Field type Default value version BitField 4 ihl BitField 5 tos XByteField 0x0 len ShortField 20 id XShortField 0x0 flags FlagsField 0 frag BitField 0 ttl ByteField 64 proto ByteEnumField 6 chksum XShortField 0x0 src IPField "127.0.0.1" dst IPField "127.0.0.1"
When you see an Enum field (e.g. IP.proto), that means some user-friendliness has been added to it. If you display the field value with full details, its value will be displayed in clear text as well:
scruby> IP().show version = 4 ihl = 5 tos = 0x0 len = 20 id = 0x0 flags = 0 () frag = 0 ttl = 64 proto = 6 (TCP) <-- here, instead of "proto = 6" only chksum = 0x0 src = 127.0.0.1 dst = 127.0.0.1
With an Enum field, you can also use the text version to specify the value:
scruby> IP(:proto=>'UDP') <IP proto=17 |>
If a field is of "FlagsField" type (e.g. IP and TCP flags), setting and displaying its value is user friendly as well:
scruby> p=TCP(:flags=>"SYN ACK") scruby> p.show ###[ TCP ]### sport = 1024 dport = 80 seq = 0 ack = 0 dataofs = 5 reserved = 0 flags = 18 (ACK SYN) <- here window = 8192 chksum = 0x0 urgptr = 0
RFC3514 way of life:
scruby> IP(:flags=>6).show ###[ IPv4 ]### version = 4 ihl = 5 tos = 0x0 len = 20 id = 0x0 flags = 6 (evil DF) <- here frag = 0 ttl = 64 proto = 6 (TCP) 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 sniffYou can also have an history:
scruby> p=IP() scruby> q=UDP() scruby> [up]q=UDP()[up]p=IP()
scruby> p=IP(:dst=>"www.google.com")/TCP()/"GET / HTTP 1.0\r\n\r\n"
scruby> s=p.to_net
scruby> puts "result=#{IP(s)}"
result=<IP 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)
<Ether |>
scruby> Ether('A'*6)
<Ether 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')
<IP ihl=1 |>
scruby> p.ttl
undefined method `ttl' for #<Scruby::Packet:0xb7cec0f4>
scruby> p.layers_list[0].ttl
64
scruby> s=(IP()/TCP()/"GET / HTTP/1.0\r\n\r\n").to_net scruby> p=Ether()/s <Ether |><Raw load="E\000\000:\000\000\000\000@\006|\274\177\000\000\001\177\000\000\001\004\000\000 P\000\000\000\000\000\000\000\000P\002 \000\256\336\000\000GET / HTTP/1.0\r\n\r\n" |> scruby> p.decode_payload_as(IP) scruby> p <Ether |><IP len=58 chksum=0x7cbc |><TCP chksum=0xaede |><Raw load="GET / HTTP/1.0\r\n\r\n" |>
Now that you have a fully dissected packet, you can ask for more information about its layers. The get_layer function gives a pointer to the first layer (including its own payload) corresponding to the given dissector:
scruby> q=p.get_layer(TCP) <TCP chksum=0xaede |><Raw load="GET / HTTP/1.0\r\n\r\n" |>
There are also the has_layer and the last_layer functions:
scruby> p.has_layer(TCP) true scruby> p.has_layer(UDP) false scruby> p.last_layer <Raw load="GET / HTTP/1.0\r\n\r\n" |>
Note that these *_layer functions returns a pointer, that is to say if you modify the returned object, the original is modified as well:
scruby> q.layers_list[0].sport=123 scruby> p <Ether |><IP len=58 chksum=0x7cbc |><TCP sport=123 chksum=0xaede |><Raw load="GET / HTTP/1.0\r\n\r\n" |>
scruby> p=Ether(:src=>"00:11:22:33:44:55")/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 <Ether dst=55:44:33:22:11:00 src=00:11:22:33:44:55 |> <IP 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" |>
scruby> sniff listening on eth0 1161032765.823136 <Ether dst=00:11:22:33:44:55 src=55:44:33:22:11:00 |> <IP len=59 id=0x48f proto=17 chksum=0xcd3 src=11.22.33.44.55 dst=212.27.54.252 |> <Raw load="5'JPawwwgooglefr" |> 1161032765.853398 <Ether dst=55:44:33:22:11:00 src=00:11:22:33:44:55 |> <IP len=103 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).
scruby> sniff(:offline=>"slammer.pcap") 1188850680.58843 <Ether dst=55:44:33:22:11:00 src=00:11:22:33:44:55 |><IP len=404 id=0xba2f ttl=37 proto=17 chksum=0xa275 |><UDP sport=1086 dport=1434 len=384 chksum=0xf958 |><Raw load="\004\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001 \001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001 \001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001 \001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\001\334\311\260B\353\016 \001\001\001\001\001\001\001p\256B\001p\256B\220\220\220\220\220\220\220\220h\334\311\260B\270\001\001 \001\0011\311\261\030P\342\3755\001\001\001\005P\211\345Qh.dllhel32hkernQhounthickChGetTf \271llQh32.dhws2_f\271etQhsockf\271toQhsend\276\030\020\256B\215E\324P\377\026P\215E\340P\215E\360P\377 \026P\276\020\020\256B\213\036\213\003=U\213\354Qt\005\276\034\020\256B\377\026\377\3201\311QQP\201\361 \003\001\004\233\201\361\001\001\001\001Q\215E\314P\213E\300P\377\026j\021j\002j\002\377\320P\215E\304P \213E\300P\377\026\211\306\t\333\201\363<a\331\377\213E\264\215\f@\215\024\210\301\342\004\001\302\301 \342\b)\302\215\004\220\001\330\211E\264j\020\215E\260P1\311Qf\201\361x\001Q\215E\003P\213E\254P\377\326 \353\312" |>
#! /usr/bin/env ruby require 'scruby' module Scruby sniff endThe 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
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.
Without any argument, sniff captures on the default interface:
example> sniff listening on eth0 1158608918.45960 <Ether dst=00:11:22:33:44:55 src=55:44:33:22:11:00 |> <IP 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 <Ether dst=55:44:33:22:11:00 src=00:11:22:33:44:55 |> <IP 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):
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.
To read from a PCAP file:
example> sniff(:offline=>"mycapture.pcap")
class ICMP(Packet):
name = "ICMP"
fields_desc = [ ByteEnumField('type',8, icmptypes),
ByteField('code',0),
XShortField('chksum', None),
XShortField('id',0),
XShortField('seq',0) ]
class ICMP<Layer
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.to_net() + 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:
When writing your own dissectors, note:
Moreover, some fields also have an Enum version, allowing to deal with text values. For the IP dissector, the proto field is defined as:
ByteEnumField('proto', IPPROTO_TCP, IPPROTO_ALL)
IPPROTO_TCP is the default value (6) and IPPROTO_ALL is a hash defined in const.rb:
IPPROTO_ALL = { IPPROTO_ICMP=>"ICMP",
IPPROTO_TCP=>"TCP",
IPPROTO_UDP=>"UDP" }
For a FlagsField, the "flags" argument must be an array of strings, each of those being a name for the associated bit. For instance, let's have a look at TCP.flags:
FlagsField('flags', 0x2, 8, TCPFLAGS)
TCPFLAGS is defined in const.rb:
TCPFLAGS = %w[FIN SYN RST PSH ACK URG ECN RES]
Namely, FIN is for bit 0, SYN for bit 1, etc.
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 (NB: this is only a historical example; XLEShortField is now "officially" implemented :):
class XLEShortField<Field def init @format = 'v' end endThat'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:
def to_human(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.
I wrote two small test (yes, there is not much to test in Scruby yet :):
$ 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
$ 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.528sNote: I expect even better performance with Ruby 2.0 (e.g. the same kind of performance I got with Scaperl).
$ 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
$ 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