As we all know, the Linux kernel has a monolithic architecture. That basically
means that every piece of code that is executed by the kernel has to be loaded
into kernel memory. To prevent having to rebuild the kernel every time new
hardware is added (to add drivers for it), Mr. Linus Torvalds and the gang
came up with the loadable module concept that we all came to love: the linux
kernel modules (lkm's for short). This article begins by pointing out yet more
interesting things that can be done using lkm's in the networking layer, and
finishes by trying to provide a solution to kernel backdooring.
----[ Socket Kernel Buffers
TCP/IP is a layered set of protocols. This means that the kernel needs to use
several routine functions to process the different packet layers in order to
fully "understand" the packet and connect it to a socket, etc. First, it
needs a routine to handle the link-layer header and, once processed there, the
packet is passed to the IP-layer handling routine(s), then to the transport-
layer routine(s) and so on. Well, the different protocols need a way
to communicate with each other as the packets are being processed. Under Linux
the answer to this are socket kernel buffers (or sk_buff's). These are used to
pass data between the different protocol layers (handling routines) and
the network device drivers.
The sk_buff{} structure (only the most important items are presented, see
linux/include/linux/skbuff.h for more):
next: pointer to the next sk_buff{}.
prev: pointer to the previous sk_buff{}.
dev: device we are currently using.
head: pointer to beginning of buffer which holds our packet.
data: pointer to the actual start of the protocol data. This may vary
depending of the protocol layer we are on.
tail: pointer to the end of protocol data, also varies depending of the
protocol layer using he sk_buff.
end: points to the end of the buffer holding our packet. Fixed value.
For further enlightenment, imagine this:
- host A sends a packet to host B
- host B receives the packet through the appropriate network device.
- the network device converts the received data into sk_buff data structures.
- those data structures are added to the backlog queue.
- the scheduler then determines which protocol layer to pass the received
packets to.
Thus, our next question arises... How does the scheduler determine which
protocol to pass the data to? Well, each protocol is registered in a
packet_type{} data structure which is held by either the ptype_all list or
the ptype_base hash table. The packet_type{} data structure holds information
on protocol type, network device, pointer to the protocol's receive data
processing routine and a pointer to the next packet_type{} structure. The
network handler matches the protocol types of the incoming packets (sk_buff's)
with the ones in one or more packet_type{} structures. The sk_buff is then
passed to the matching protocol's handling routine(s).
----[ The Hack
What we do is code our own kernel module that registers our packet_type{}
data structure to handle all incoming packets (sk_buff's) right after they
come out of the device driver. This is easier than it seems. We simply fill
in a packet_type{} structure and register it by using a kernel exported
function called dev_add_pack(). Our handler will then sit between the device
driver and the next (previously the first) routine handler. This means that
every sk_buff that arrives from the device driver has to pass first through our
packet handler.
----[ The Examples
We present you with three real-world examples, a protocol "mutation" layer,
a kernel-level packet bouncer, and a kernel-level packet sniffer.
----[ OTP (Obscure Transport Protocol)
The first one is really simple (and fun too), it works in a client-server
paradigm, meaning that you need to have two modules loaded, one on the client
and one on the server (duh). The client module catches every TCP packet with
the SYN flag on and swaps it with a FIN flag. The server module does exactly
the opposite, swaps the FIN for a SYN. I find this particularly fun since both
sides behave like a regular connection is undergoing, but if you watch it on
the wire it will seem totally absurd. This can also do the same for ports and
source address. Let's look at an example taken right from the wire.
Imagine the following scenario, we have host 'doubt' who wishes to make a
telnet connection to host 'hardbitten'. We load the module in both sides
telling it to swap port 23 for 80 and to swap a SYN for a FIN and vice-versa.
[lifeline@doubt ITP]$ telnet hardbitten
A regular connection (without the modules loaded) looks like this:
When, what is happening in fact, is that 'doubt' is (successfully) requesting a
telnet session to host 'hardbitten'. This is a nice way to evade IDSes and
many firewall policies. It is also very funny. :-)
Ah, There is a problem with this, when closing a TCP connection the FIN's are
replaced by SYN's because of the reasons stated above, there is, however, an
easy way to get around this, is to tell our lkm just to swap the flags when the
socket is in TCP_LISTEN, TCP_SYN_SENT or TCP_SYN_RECV states. I have not
implemented this partly to avoid misuse by "script kiddies", partly because of
laziness and partly because I'm just too busy. However, it is not hard to do
this, go ahead and try it, I trust you.
----[ A Kernel Traffic Bouncer
This packet relaying tool is mainly a proof of concept work at this point.
This one is particularly interesting when combined with the previous example.
We load our module on the host 'medusa' that then sits watching every packet
coming in. We want to target host 'hydra' but this one only accepts telnet
connections from the former. However, it's too risky to log into 'medusa'
right now, because root is logged. No problem, we send an ICMP_ECHO_REQUEST
packet that contains a magic cookie or password and 2 ip's and 2 ports like:
<sourceip:srcport, destip:destport>. We can however omit srcport without too
much trouble (as we did on the example shown below). Our module then accepts
this cookie and processes it. It now knows that any packet coming from
sourceip:srcport into medusa:destport is to be sent to destip:destport.
- any packet coming to host medusa from `sourceip:srcprt` with destination
port `dstport` is routed to `destip`, and vice-versa. The packets are
never processed by the rest of the stack on medusa.
Note that as I said above, in the coded example we removed `srcprt` from the
information sent to the bouncer. This means it will accept packets from any
source port. This can be dangerous: imagine that I have this bouncing rule
processed on host 'medusa':
<intruder, hydra:23>
Now try to telnet from 'medusa' to 'hydra'. You won't make it. Every packet
coming back from hydra is sent to 'intruder', so no response appears to the
user executing the telnet. Intruder will drop the packets obviously, since he
didn't start a connection. Using a source port on the rule minimizes this
risk, but there is still a possibility (not likely) that a user on medusa uses
the same source port we used on our bouncing rule. This should be possible to
avoid by reserving the source port on host medusa (see masquerading code in
the kernel).
As a side note, this technique can be used on almost all protocols, even those
without port abstraction (UDP/TCP). Even icmp bouncing should be possible
using cookies. This is a more low-level approach than ip masquerading, and
IMHO a much better one :)
Issues with the bouncer:
- Source port ambiguity. My suggestion to solving this is to accept the
rules without a source port, and then add that to the rule after a SYN packet
reaches the bouncer. The rule then only affects that connection. The
source port is then cleared by an RST or a timeout waiting for packets.
- No timeout setting on rules.
- The bouncer does not handle IP fragments.
Also, there's a bigger issue in hand. Notice in the source that I'm sending
the packets right through the device they came. This is a bad situation for
routers. This happens because I only have immediate access to the hardware
address of the originating packet's device. To implement routing to another
device, we must consult IP routing tables, find the device that is going to
send the packet, and the destination machine's MAC address (if it is an
ethernet device), that may only be available after an ARP request. It's tricky
stuff. This problem, depending on the network, can become troublesome.
Packets could be stuck on 2 hosts looping until they expire (via TTL), or, if
the network has traffic redundancy, they might escape safely.
----[ A Kernel Based Sniffer
Another proof of concept tool, the sniffer is a bit simpler in concept than
the bouncer. It just sits in its socket buffer handler above all other
protocol handlers and listens for, say, TCP packets, and then logs them to a
file. There are some tricks to it of course... We have to be able to
identify packets from different connections, and better yet, we have to
order out-of-sequence tcp packets, in order to get coherent results. This
is particularly nasty in case of telnet connections.
(a timeout feature is
missing too, and the capability
of sniffing more than one connection at a given moment (this one is tricky).
Ideally, the module should store all results in kernel memory and send them
back to us (if we say, send it a special packet). But this is a proof of
concept, and it is not a finished "script kiddies" product, so I leave you
smart readers to polish the code, learn it, and experiment with it :)
----[ A Solution For Kernel Harassing
So, having fun kicking kernel ass from left to right? Let's end the tragedy,
the linux kernel is your friend! :) Well, I've read Silvio's excellent article
about patching the kernel using /dev/kmem, so obviously compiling the kernel
without module support is not enough. I leave you with an idea. It should be
fairly simple to code. It's a module (yes, another one), that when loaded
prevents any other modules to load, and turns /dev/kmem into a read-only
device (kernel memory can only be accessed with ring 0 privilege). So
without any kernel routine made available to the outside, the kernel is the
only one that can touch it's own memory. Readers should know that this is not
something new. Securelevels are (somewhat) implemented in kernels 2.0.x and
do some cool stuff like not allowing writing directly to critical devices,
such as /dev/kmem, /dev/mem, and /dev/hd*. This was not implemented in 2.2.x,
so it would be nice to have a module like this. When an administrator is
through loading modules, and wants to leave the system just a bit more secure,
he loads the 'lock' module, and presto, no more kernel harassing. This must
be of course be accompanied by other measures. I believe a real secure system
should have this module installed and the kernel image file stored on a read
only media, such as a floppy disk drive, and no boot loader such as lilo.
You should also be worried about securing the CMOS data. You just want to
boot using the floppy. Securing the CMOS data can be tricky on a rooted
system as I noticed on a recent discussion on irc (liquidk, you intelligent
bastard), but this is out of the scope of this article. This idea could
also be implemented directly in the kernel without using modules. Mainly I
would like to see a real secure levels implementation on 2.2.x :)
---[ References
+ The Linux Kernel by David A. Rusling
+ TCP/IP Illustrated, Volume 1 by W. Richard Stevens (Addison Wesley)
+ Phrack Issue 52, article 18 (P52-18) by plaguez.
+ Windows 98 Unleashed by Stev...oh. no. wait, this can't be right... :-)
----[ Acknowledgements
Both the authors would like to thank to:
+ HPT (http://www.hackers-pt.org) for being a bunch of idiots (hehe).
+ pmsac@toxyn.org for support and coming up with the idea for the
kernel based sniffer.
+ LiquidK for coming up with the OTP concept and fucking up some of
our seemingly 'invincible' concepts :)
+ All of you leet hackers from Portugal, you know who you are.
The scene shall be one again!! :)
----[ The Code: OTP
<++> P55/Linux-lkm/OTP/otp.c !bf8d47e0
/*
* Obscure Transport Protocol
*
* Goal: Change TCP behavior to evade IDS and firewall policies.
*
* lifeline (c) 1999
* <arai@hackers-pt.org>
*
* gcc -O6 -c otp.c -I/usr/src/linux/include
* insmod otp.o dev=eth0 ip=123.123.123.123
*
* In ip= use only numerical dotted ip's!!
* Btw, this is the ip of the other machine that also has the module.
*
* Load this module in both machines putting in the ip= argument each other's
* machine numerical dotted ip.
*
* Oh, and don't even think about flaming me if this fucks up your machine,
* it works fine on mine with kernel 2.2.5.
* This tool stands on its own. I'm not responsible for any damage caused by it.
*
* You will probably want to make some arrangements with the #define's below.
*
*/
/* Define here if you want to swap ports also */
#define REALPORT 23 /* port you which to communicate */
#define FAKEPORT 80 /* port that appears on the wire */